client.go 4.87 KB
Newer Older
1 2 3 4
package node

import (
	"context"
5
	"errors"
6
	"fmt"
7 8 9
	"math/big"
	"time"

10
	"github.com/ethereum/go-ethereum/common"
11 12
	"github.com/ethereum/go-ethereum/common/hexutil"
	"github.com/ethereum/go-ethereum/core/types"
13
	"github.com/ethereum/go-ethereum/ethclient"
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28
	"github.com/ethereum/go-ethereum/rpc"
)

const (
	// defaultDialTimeout is default duration the processor will wait on
	// startup to make a connection to the backend
	defaultDialTimeout = 5 * time.Second

	// defaultRequestTimeout is the default duration the processor will
	// wait for a request to be fulfilled
	defaultRequestTimeout = 10 * time.Second
)

type EthClient interface {
	FinalizedBlockHeight() (*big.Int, error)
29

30
	BlockHeadersByRange(*big.Int, *big.Int) ([]*types.Header, error)
31
	BlockHeaderByHash(common.Hash) (*types.Header, error)
32

33 34
	StorageHash(common.Address, *big.Int) (common.Hash, error)

35 36 37 38 39 40 41
	RawRpcClient() *rpc.Client
}

type client struct {
	rpcClient *rpc.Client
}

42
func DialEthClient(rpcUrl string) (EthClient, error) {
43 44 45 46 47 48 49 50 51 52 53 54
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultDialTimeout)
	defer cancel()

	rpcClient, err := rpc.DialContext(ctxwt, rpcUrl)
	if err != nil {
		return nil, err
	}

	client := &client{rpcClient: rpcClient}
	return client, nil
}

55 56 57 58
func NewEthClient(rpcClient *rpc.Client) EthClient {
	return &client{rpcClient}
}

59 60 61 62 63 64 65 66 67
func (c *client) RawRpcClient() *rpc.Client {
	return c.rpcClient
}

// FinalizedBlockHeight retrieves the latest block height in a finalized state
func (c *client) FinalizedBlockHeight() (*big.Int, error) {
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84
	// Local devnet is having issues with the "finalized" block tag. Switch to "latest"
	// to iterate faster locally but this needs to be updated
	header := new(types.Header)
	err := c.rpcClient.CallContext(ctxwt, header, "eth_getBlockByNumber", "latest", false)
	if err != nil {
		return nil, err
	}

	return header.Number, nil
}

// BlockHeaderByHash retrieves the block header attributed to the supplied hash
func (c *client) BlockHeaderByHash(hash common.Hash) (*types.Header, error) {
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

	header, err := ethclient.NewClient(c.rpcClient).HeaderByHash(ctxwt, hash)
85 86 87 88
	if err != nil {
		return nil, err
	}

89 90 91 92 93 94
	// sanity check on the data returned
	if header.Hash() != hash {
		return nil, errors.New("header mismatch")
	}

	return header, nil
95 96 97 98 99 100
}

// BlockHeadersByRange will retrieve block headers within the specified range -- includsive. No restrictions
// are placed on the range such as blocks in the "latest", "safe" or "finalized" states. If the specified
// range is too large, `endHeight > latest`, the resulting list is truncated to the available headers
func (c *client) BlockHeadersByRange(startHeight, endHeight *big.Int) ([]*types.Header, error) {
101
	count := new(big.Int).Sub(endHeight, startHeight).Uint64() + 1
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133
	batchElems := make([]rpc.BatchElem, count)
	for i := uint64(0); i < count; i++ {
		height := new(big.Int).Add(startHeight, new(big.Int).SetUint64(i))
		batchElems[i] = rpc.BatchElem{
			Method: "eth_getBlockByNumber",
			Args:   []interface{}{toBlockNumArg(height), false},
			Result: new(types.Header),
			Error:  nil,
		}
	}

	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()
	err := c.rpcClient.BatchCallContext(ctxwt, batchElems)
	if err != nil {
		return nil, err
	}

	// Parse the headers.
	//  - Ensure integrity that they build on top of each other
	//  - Truncate out headers that do not exist (endHeight > "latest")
	size := 0
	headers := make([]*types.Header, count)
	for i, batchElem := range batchElems {
		if batchElem.Error != nil {
			return nil, batchElem.Error
		} else if batchElem.Result == nil {
			break
		}

		header := batchElem.Result.(*types.Header)
		if i > 0 && header.ParentHash != headers[i-1].Hash() {
134
			// Warn here that we got a bad (malicious?) response
135 136 137 138 139 140 141 142 143 144 145
			break
		}

		headers[i] = header
		size = size + 1
	}
	headers = headers[:size]

	return headers, nil
}

146 147 148 149 150 151 152 153 154 155 156 157 158 159
// StorageHash returns the sha3 of the storage root for the specified account
func (c *client) StorageHash(address common.Address, blockNumber *big.Int) (common.Hash, error) {
	ctxwt, cancel := context.WithTimeout(context.Background(), defaultRequestTimeout)
	defer cancel()

	proof := struct{ StorageHash common.Hash }{}
	err := c.rpcClient.CallContext(ctxwt, &proof, "eth_getProof", address, nil, toBlockNumArg(blockNumber))
	if err != nil {
		return common.Hash{}, err
	}

	return proof.StorageHash, nil
}

160 161 162
func toBlockNumArg(number *big.Int) string {
	if number == nil {
		return "latest"
163 164
	} else if number.Sign() >= 0 {
		return hexutil.EncodeBig(number)
165 166
	}

167 168 169 170
	// It's negative.
	if number.IsInt64() {
		tag, _ := rpc.BlockNumber(number.Int64()).MarshalText()
		return string(tag)
171 172
	}

173 174
	// It's negative and large, which is invalid.
	return fmt.Sprintf("<invalid %d>", number)
175
}