hermez-node/eth/ethereum.go
eth/ethereum.goRaw
1package eth
2
3import (
4 "context"
5 "fmt"
6 "math/big"
7 "time"
8
9 "github.com/ethereum/go-ethereum/accounts"
10 "github.com/ethereum/go-ethereum/accounts/abi/bind"
11 ethKeystore "github.com/ethereum/go-ethereum/accounts/keystore"
12 ethCommon "github.com/ethereum/go-ethereum/common"
13 "github.com/ethereum/go-ethereum/core/types"
14 "github.com/ethereum/go-ethereum/ethclient"
15 "github.com/hermeznetwork/hermez-node/common"
16 "github.com/hermeznetwork/hermez-node/log"
17)
18
19// EthereumInterface is the interface to Ethereum
20type EthereumInterface interface {
21 EthCurrentBlock() (int64, error)
22 // EthHeaderByNumber(context.Context, *big.Int) (*types.Header, error)
23 EthBlockByNumber(context.Context, int64) (*common.Block, error)
24 EthAddress() (*ethCommon.Address, error)
25 EthTransactionReceipt(context.Context, ethCommon.Hash) (*types.Receipt, error)
26}
27
28var (
29 // ErrAccountNil is used when the calls can not be made because the account is nil
30 ErrAccountNil = fmt.Errorf("Authorized calls can't be made when the account is nil")
31 // ErrReceiptStatusFailed is used when receiving a failed transaction
32 ErrReceiptStatusFailed = fmt.Errorf("receipt status is failed")
33 // ErrReceiptNotReceived is used when unable to retrieve a transaction
34 ErrReceiptNotReceived = fmt.Errorf("receipt not available")
35 // ErrBlockHashMismatchEvent is used when there's a block hash mismatch
36 // beetween different events of the same block
37 ErrBlockHashMismatchEvent = fmt.Errorf("block hash mismatch in event log")
38)
39
40const (
41 errStrDeploy = "deployment of %s failed: %w"
42 errStrWaitReceipt = "wait receipt of %s deploy failed: %w"
43
44 // default values
45 defaultCallGasLimit = 300000
46 defaultDeployGasLimit = 1000000
47 defaultGasPriceDiv = 100
48 defaultReceiptTimeout = 60
49 defaultIntervalReceiptLoop = 200
50)
51
52// EthereumConfig defines the configuration parameters of the EthereumClient
53type EthereumConfig struct {
54 CallGasLimit uint64
55 DeployGasLimit uint64
56 GasPriceDiv uint64
57 ReceiptTimeout time.Duration // in seconds
58 IntervalReceiptLoop time.Duration // in milliseconds
59}
60
61// EthereumClient is an ethereum client to call Smart Contract methods and check blockchain information.
62type EthereumClient struct {
63 client *ethclient.Client
64 account *accounts.Account
65 ks *ethKeystore.KeyStore
66 ReceiptTimeout time.Duration
67 config *EthereumConfig
68}
69
70// NewEthereumClient creates a EthereumClient instance. The account is not mandatory (it can
71// be nil). If the account is nil, CallAuth will fail with ErrAccountNil.
72func NewEthereumClient(client *ethclient.Client, account *accounts.Account, ks *ethKeystore.KeyStore, config *EthereumConfig) *EthereumClient {
73 if config == nil {
74 config = &EthereumConfig{
75 CallGasLimit: defaultCallGasLimit,
76 DeployGasLimit: defaultDeployGasLimit,
77 GasPriceDiv: defaultGasPriceDiv,
78 ReceiptTimeout: defaultReceiptTimeout,
79 IntervalReceiptLoop: defaultIntervalReceiptLoop,
80 }
81 }
82 return &EthereumClient{client: client, account: account, ks: ks, ReceiptTimeout: config.ReceiptTimeout * time.Second, config: config}
83}
84
85// BalanceAt retieves information about the default account
86func (c *EthereumClient) BalanceAt(addr ethCommon.Address) (*big.Int, error) {
87 return c.client.BalanceAt(context.TODO(), addr, nil)
88}
89
90// Account returns the underlying ethereum account
91func (c *EthereumClient) Account() *accounts.Account {
92 return c.account
93}
94
95// EthAddress returns the ethereum address of the account loaded into the EthereumClient
96func (c *EthereumClient) EthAddress() (*ethCommon.Address, error) {
97 if c.account == nil {
98 return nil, ErrAccountNil
99 }
100 return &c.account.Address, nil
101}
102
103// CallAuth performs a Smart Contract method call that requires authorization.
104// This call requires a valid account with Ether that can be spend during the
105// call.
106func (c *EthereumClient) CallAuth(gasLimit uint64,
107 fn func(*ethclient.Client, *bind.TransactOpts) (*types.Transaction, error)) (*types.Transaction, error) {
108 if c.account == nil {
109 return nil, ErrAccountNil
110 }
111
112 gasPrice, err := c.client.SuggestGasPrice(context.Background())
113 if err != nil {
114 return nil, err
115 }
116 inc := new(big.Int).Set(gasPrice)
117 inc.Div(inc, new(big.Int).SetUint64(c.config.GasPriceDiv))
118 gasPrice.Add(gasPrice, inc)
119 log.Debugw("Transaction metadata", "gasPrice", gasPrice)
120
121 auth, err := bind.NewKeyStoreTransactor(c.ks, *c.account)
122 if err != nil {
123 return nil, err
124 }
125 auth.Value = big.NewInt(0) // in wei
126 if gasLimit == 0 {
127 auth.GasLimit = c.config.CallGasLimit // in units
128 } else {
129 auth.GasLimit = gasLimit // in units
130 }
131 auth.GasPrice = gasPrice
132
133 tx, err := fn(c.client, auth)
134 if tx != nil {
135 log.Debugw("Transaction", "tx", tx.Hash().Hex(), "nonce", tx.Nonce())
136 }
137 return tx, err
138}
139
140// ContractData contains the contract data
141type ContractData struct {
142 Address ethCommon.Address
143 Tx *types.Transaction
144 Receipt *types.Receipt
145}
146
147// Deploy a smart contract. `name` is used to log deployment information. fn
148// is a wrapper to the deploy function generated by abigen. In case of error,
149// the returned `ContractData` may have some parameters filled depending on the
150// kind of error that occurred.
151func (c *EthereumClient) Deploy(name string,
152 fn func(c *ethclient.Client, auth *bind.TransactOpts) (ethCommon.Address, *types.Transaction, interface{}, error)) (ContractData, error) {
153 var contractData ContractData
154 log.Infow("Deploying", "contract", name)
155 tx, err := c.CallAuth(
156 c.config.DeployGasLimit,
157 func(client *ethclient.Client, auth *bind.TransactOpts) (*types.Transaction, error) {
158 addr, tx, _, err := fn(client, auth)
159 if err != nil {
160 return nil, err
161 }
162 contractData.Address = addr
163 return tx, nil
164 },
165 )
166 if err != nil {
167 return contractData, fmt.Errorf(errStrDeploy, name, err)
168 }
169 log.Infow("Waiting receipt", "tx", tx.Hash().Hex(), "contract", name)
170 contractData.Tx = tx
171 receipt, err := c.WaitReceipt(tx)
172 if err != nil {
173 return contractData, fmt.Errorf(errStrWaitReceipt, name, err)
174 }
175 contractData.Receipt = receipt
176 return contractData, nil
177}
178
179// Call performs a read only Smart Contract method call.
180func (c *EthereumClient) Call(fn func(*ethclient.Client) error) error {
181 return fn(c.client)
182}
183
184// WaitReceipt will block until a transaction is confirmed. Internally it
185// polls the state every 200 milliseconds.
186func (c *EthereumClient) WaitReceipt(tx *types.Transaction) (*types.Receipt, error) {
187 return c.waitReceipt(context.TODO(), tx, c.ReceiptTimeout)
188}
189
190// GetReceipt will check if a transaction is confirmed and return
191// immediately, waiting at most 1 second and returning error if the transaction
192// is still pending.
193func (c *EthereumClient) GetReceipt(tx *types.Transaction) (*types.Receipt, error) {
194 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
195 defer cancel()
196 return c.waitReceipt(ctx, tx, 0)
197}
198
199// EthTransactionReceipt returns the transaction receipt of the given txHash
200func (c *EthereumClient) EthTransactionReceipt(ctx context.Context, txHash ethCommon.Hash) (*types.Receipt, error) {
201 return c.client.TransactionReceipt(ctx, txHash)
202}
203
204func (c *EthereumClient) waitReceipt(ctx context.Context, tx *types.Transaction, timeout time.Duration) (*types.Receipt, error) {
205 var err error
206 var receipt *types.Receipt
207
208 txHash := tx.Hash()
209 log.Debugw("Waiting for receipt", "tx", txHash.Hex())
210
211 start := time.Now()
212 for {
213 receipt, err = c.client.TransactionReceipt(ctx, txHash)
214 if receipt != nil || time.Since(start) >= timeout {
215 break
216 }
217 time.Sleep(c.config.IntervalReceiptLoop * time.Millisecond)
218 }
219
220 if receipt != nil && receipt.Status == types.ReceiptStatusFailed {
221 log.Errorw("Failed transaction", "tx", txHash.Hex())
222 return receipt, ErrReceiptStatusFailed
223 }
224
225 if receipt == nil {
226 log.Debugw("Pendingtransaction / Wait receipt timeout", "tx", txHash.Hex(), "lasterr", err)
227 return receipt, ErrReceiptNotReceived
228 }
229 log.Debugw("Successful transaction", "tx", txHash.Hex())
230
231 return receipt, err
232}
233
234// EthCurrentBlock returns the current block number in the blockchain
235func (c *EthereumClient) EthCurrentBlock() (int64, error) {
236 ctx, cancel := context.WithTimeout(context.TODO(), 1*time.Second)
237 defer cancel()
238 header, err := c.client.HeaderByNumber(ctx, nil)
239 if err != nil {
240 return 0, err
241 }
242 return header.Number.Int64(), nil
243}
244
245// EthHeaderByNumber internally calls ethclient.Client HeaderByNumber
246// func (c *EthereumClient) EthHeaderByNumber(ctx context.Context, number *big.Int) (*types.Header, error) {
247// return c.client.HeaderByNumber(ctx, number)
248// }
249
250// EthBlockByNumber internally calls ethclient.Client BlockByNumber and returns *common.Block
251func (c *EthereumClient) EthBlockByNumber(ctx context.Context, number int64) (*common.Block, error) {
252 blockNum := big.NewInt(number)
253 if number == 0 {
254 blockNum = nil
255 }
256 block, err := c.client.BlockByNumber(ctx, blockNum)
257 if err != nil {
258 return nil, err
259 }
260 b := &common.Block{
261 EthBlockNum: block.Number().Int64(),
262 Timestamp: time.Unix(int64(block.Time()), 0),
263 Hash: block.Hash(),
264 }
265 return b, nil
266}