services/scribe/backend/backend.go
package backend
import (
"context"
"fmt"
"math/big"
"github.com/benbjohnson/immutable"
"github.com/ethereum/go-ethereum"
"github.com/ethereum/go-ethereum/common"
"github.com/ethereum/go-ethereum/core/types"
"github.com/lmittmann/w3/module/eth"
"github.com/lmittmann/w3/w3types"
"github.com/synapsecns/sanguine/core/metrics"
"github.com/synapsecns/sanguine/ethergo/client"
"github.com/synapsecns/sanguine/ethergo/util"
"golang.org/x/exp/constraints"
)
// ScribeBackend is the set of functions that the scribe needs from a client.
type ScribeBackend interface {
// ChainID gets the chain id from the rpc server.
ChainID(ctx context.Context) (*big.Int, error)
// BlockNumber gets the latest block number.
BlockNumber(ctx context.Context) (uint64, error)
// HeaderByNumber returns the block header with the given block number.
HeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error)
// BatchWithContext batches multiple
BatchWithContext(ctx context.Context, calls ...w3types.Caller) error
}
// DialBackend returns a scribe backend.
func DialBackend(ctx context.Context, url string, handler metrics.Handler) (ScribeBackend, error) {
//nolint:wrapcheck
return client.DialBackend(ctx, url, handler)
}
// GetLogsInRange gets all logs in a range with a single batch request
// in successful cases an immutable list is returned, otherwise an error is returned.
func GetLogsInRange(ctx context.Context, backend ScribeBackend, contractAddresses []common.Address, expectedChainID uint64, chunks []*util.Chunk, topics [][]common.Hash) (*immutable.List[*[]types.Log], error) {
calls := make([]w3types.Caller, len(chunks)+2)
results := make([][]types.Log, len(chunks))
chainID := new(uint64)
calls[0] = eth.ChainID().Returns(chainID)
maxHeight := new(big.Int)
calls[1] = eth.BlockNumber().Returns(maxHeight)
for i := 0; i < len(chunks); i++ {
startBlock := chunks[i].StartBlock
endBlock := chunks[i].EndBlock
// handle desc iterator
if startBlock.Uint64() > endBlock.Uint64() {
startBlock = chunks[i].EndBlock
endBlock = chunks[i].StartBlock
}
filter := ethereum.FilterQuery{
FromBlock: startBlock,
ToBlock: endBlock,
Addresses: contractAddresses,
Topics: topics,
}
calls[i+2] = eth.Logs(filter).Returns(&results[i])
}
// for logging purposes
startHeight := chunks[0].StartBlock.Uint64()
endHeight := chunks[len(chunks)-1].EndBlock.Uint64()
if err := backend.BatchWithContext(ctx, calls...); err != nil {
return nil, fmt.Errorf("could not fetch logs in range %d to %d: %w", startHeight, endHeight, err)
}
if expectedChainID != *chainID {
return nil, fmt.Errorf("could not fetch logs in range %d to %d: Incorrect RPC used for expected chainID: %d, RPC used chainID %d instead", startHeight, endHeight, expectedChainID, chainID)
}
if endHeight > maxHeight.Uint64() {
return nil, fmt.Errorf("could not fetch logs in range %d to %d: Block not available past max height: %d", startHeight, endHeight, maxHeight.Uint64())
}
// use an immutable list for additional safety to the caller, don't allocate until batch returns successfully
res := immutable.NewListBuilder[*[]types.Log]()
for _, result := range results {
logChunk := result
if len(result) > 0 {
res.Append(&logChunk)
}
}
return res.List(), nil
}
// BlockHashesInRange gets all block hashes in a range with a single batch request
// in successful cases an immutable map is returned of [height->hash], otherwise an error is returned.
func BlockHashesInRange(ctx context.Context, backend ScribeBackend, startHeight uint64, endHeight uint64) (*immutable.Map[uint64, string], error) {
// performance impact will be negligible here because of external constraints on blocksize
blocks := MakeRange(startHeight, endHeight)
bulkSize := len(blocks)
calls := make([]w3types.Caller, bulkSize)
results := make([]types.Header, bulkSize)
for i, blockNumber := range blocks {
calls[i] = eth.HeaderByNumber(new(big.Int).SetUint64(blockNumber)).Returns(&results[i])
}
if err := backend.BatchWithContext(ctx, calls...); err != nil {
return nil, fmt.Errorf("could not fetch blocks in range %d to %d: %w", startHeight, endHeight, err)
}
// use an immutable map for additional safety to the caller, don't allocate until batch returns successfully
res := immutable.NewMapBuilder[uint64, string](nil)
for _, result := range results {
res.Set(result.Number.Uint64(), result.Hash().String())
}
return res.Map(), nil
}
// MakeRange returns a range of integers from min to max inclusive.
func MakeRange[T constraints.Integer](min, max T) []T {
a := make([]T, max-min+1)
for i := range a {
a[i] = min + T(i)
}
return a
}