From b85c86022e130a76eeb588a5b36e97149b342188 Mon Sep 17 00:00:00 2001 From: Adrian Sutton Date: Tue, 17 Oct 2023 22:34:01 +1000 Subject: [PATCH] api/bind: add CallOpts.BlockHash to allow calling contracts at a specific block hash (#28084) * api/bind: Add CallOpts.BlockHash to allow calling contracts at a specific block hash. * ethclient: Add BalanceAtHash, NonceAtHash and StorageAtHash functions --- accounts/abi/bind/backend.go | 15 +++++ accounts/abi/bind/backends/simulated.go | 39 ++++++++++++ accounts/abi/bind/backends/simulated_test.go | 56 ++++++++++++++++- accounts/abi/bind/base.go | 18 ++++++ accounts/abi/bind/base_test.go | 65 ++++++++++++++++++++ ethclient/ethclient.go | 28 +++++++++ ethclient/ethclient_test.go | 33 ++++++++++ 7 files changed, 253 insertions(+), 1 deletion(-) diff --git a/accounts/abi/bind/backend.go b/accounts/abi/bind/backend.go index 34530a645..d13b91964 100644 --- a/accounts/abi/bind/backend.go +++ b/accounts/abi/bind/backend.go @@ -36,6 +36,10 @@ var ( // on a backend that doesn't implement PendingContractCaller. ErrNoPendingState = errors.New("backend does not support pending state") + // ErrNoBlockHashState is raised when attempting to perform a block hash action + // on a backend that doesn't implement BlockHashContractCaller. + ErrNoBlockHashState = errors.New("backend does not support block hash state") + // ErrNoCodeAfterDeploy is returned by WaitDeployed if contract creation leaves // an empty contract behind. ErrNoCodeAfterDeploy = errors.New("no contract code after deployment") @@ -64,6 +68,17 @@ type PendingContractCaller interface { PendingCallContract(ctx context.Context, call ethereum.CallMsg) ([]byte, error) } +// BlockHashContractCaller defines methods to perform contract calls on a specific block hash. +// Call will try to discover this interface when access to a block by hash is requested. +// If the backend does not support the block hash state, Call returns ErrNoBlockHashState. +type BlockHashContractCaller interface { + // CodeAtHash returns the code of the given account in the state at the specified block hash. + CodeAtHash(ctx context.Context, contract common.Address, blockHash common.Hash) ([]byte, error) + + // CallContractAtHash executes an Ethereum contract all against the state at the specified block hash. + CallContractAtHash(ctx context.Context, call ethereum.CallMsg, blockHash common.Hash) ([]byte, error) +} + // ContractTransactor defines the methods needed to allow operating with a contract // on a write only basis. Besides the transacting method, the remainder are helpers // used when the user does not provide some needed values, but rather leaves it up diff --git a/accounts/abi/bind/backends/simulated.go b/accounts/abi/bind/backends/simulated.go index 6892668cb..a26ee12e0 100644 --- a/accounts/abi/bind/backends/simulated.go +++ b/accounts/abi/bind/backends/simulated.go @@ -50,6 +50,7 @@ var _ bind.ContractBackend = (*SimulatedBackend)(nil) var ( errBlockNumberUnsupported = errors.New("simulatedBackend cannot access blocks other than the latest block") + errBlockHashUnsupported = errors.New("simulatedBackend cannot access blocks by hash other than the latest block") errBlockDoesNotExist = errors.New("block does not exist in blockchain") errTransactionDoesNotExist = errors.New("transaction does not exist") ) @@ -202,6 +203,24 @@ func (b *SimulatedBackend) CodeAt(ctx context.Context, contract common.Address, return stateDB.GetCode(contract), nil } +// CodeAtHash returns the code associated with a certain account in the blockchain. +func (b *SimulatedBackend) CodeAtHash(ctx context.Context, contract common.Address, blockHash common.Hash) ([]byte, error) { + b.mu.Lock() + defer b.mu.Unlock() + + header, err := b.headerByHash(blockHash) + if err != nil { + return nil, err + } + + stateDB, err := b.blockchain.StateAt(header.Root) + if err != nil { + return nil, err + } + + return stateDB.GetCode(contract), nil +} + // BalanceAt returns the wei balance of a certain account in the blockchain. func (b *SimulatedBackend) BalanceAt(ctx context.Context, contract common.Address, blockNumber *big.Int) (*big.Int, error) { b.mu.Lock() @@ -320,7 +339,11 @@ func (b *SimulatedBackend) blockByNumber(ctx context.Context, number *big.Int) ( func (b *SimulatedBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { b.mu.Lock() defer b.mu.Unlock() + return b.headerByHash(hash) +} +// headerByHash retrieves a header from the database by hash without Lock. +func (b *SimulatedBackend) headerByHash(hash common.Hash) (*types.Header, error) { if hash == b.pendingBlock.Hash() { return b.pendingBlock.Header(), nil } @@ -436,6 +459,22 @@ func (b *SimulatedBackend) CallContract(ctx context.Context, call ethereum.CallM if blockNumber != nil && blockNumber.Cmp(b.blockchain.CurrentBlock().Number) != 0 { return nil, errBlockNumberUnsupported } + return b.callContractAtHead(ctx, call) +} + +// CallContractAtHash executes a contract call on a specific block hash. +func (b *SimulatedBackend) CallContractAtHash(ctx context.Context, call ethereum.CallMsg, blockHash common.Hash) ([]byte, error) { + b.mu.Lock() + defer b.mu.Unlock() + + if blockHash != b.blockchain.CurrentBlock().Hash() { + return nil, errBlockHashUnsupported + } + return b.callContractAtHead(ctx, call) +} + +// callContractAtHead executes a contract call against the latest block state. +func (b *SimulatedBackend) callContractAtHead(ctx context.Context, call ethereum.CallMsg) ([]byte, error) { stateDB, err := b.blockchain.State() if err != nil { return nil, err diff --git a/accounts/abi/bind/backends/simulated_test.go b/accounts/abi/bind/backends/simulated_test.go index 11900a6cf..a41d16841 100644 --- a/accounts/abi/bind/backends/simulated_test.go +++ b/accounts/abi/bind/backends/simulated_test.go @@ -996,6 +996,43 @@ func TestCodeAt(t *testing.T) { } } +func TestCodeAtHash(t *testing.T) { + testAddr := crypto.PubkeyToAddress(testKey.PublicKey) + sim := simTestBackend(testAddr) + defer sim.Close() + bgCtx := context.Background() + code, err := sim.CodeAtHash(bgCtx, testAddr, sim.Blockchain().CurrentHeader().Hash()) + if err != nil { + t.Errorf("could not get code at test addr: %v", err) + } + if len(code) != 0 { + t.Errorf("got code for account that does not have contract code") + } + + parsed, err := abi.JSON(strings.NewReader(abiJSON)) + if err != nil { + t.Errorf("could not get code at test addr: %v", err) + } + auth, _ := bind.NewKeyedTransactorWithChainID(testKey, big.NewInt(1337)) + contractAddr, tx, contract, err := bind.DeployContract(auth, parsed, common.FromHex(abiBin), sim) + if err != nil { + t.Errorf("could not deploy contract: %v tx: %v contract: %v", err, tx, contract) + } + + blockHash := sim.Commit() + code, err = sim.CodeAtHash(bgCtx, contractAddr, blockHash) + if err != nil { + t.Errorf("could not get code at test addr: %v", err) + } + if len(code) == 0 { + t.Errorf("did not get code for account that has contract code") + } + // ensure code received equals code deployed + if !bytes.Equal(code, common.FromHex(deployedCode)) { + t.Errorf("code received did not match expected deployed code:\n expected %v\n actual %v", common.FromHex(deployedCode), code) + } +} + // When receive("X") is called with sender 0x00... and value 1, it produces this tx receipt: // // receipt{status=1 cgas=23949 bloom=00000000004000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000000000000000000000040200000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000 logs=[log: b6818c8064f645cd82d99b59a1a267d6d61117ef [75fd880d39c1daf53b6547ab6cb59451fc6452d27caa90e5b6649dd8293b9eed] 000000000000000000000000376c47978271565f56deb45495afa69e59c16ab200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000060000000000000000000000000000000000000000000000000000000000000000158 9ae378b6d4409eada347a5dc0c180f186cb62dc68fcc0f043425eb917335aa28 0 95d429d309bb9d753954195fe2d69bd140b4ae731b9b5b605c34323de162cf00 0]} @@ -1038,7 +1075,7 @@ func TestPendingAndCallContract(t *testing.T) { t.Errorf("response from calling contract was expected to be 'hello world' instead received %v", string(res)) } - sim.Commit() + blockHash := sim.Commit() // make sure you can call the contract res, err = sim.CallContract(bgCtx, ethereum.CallMsg{ @@ -1056,6 +1093,23 @@ func TestPendingAndCallContract(t *testing.T) { if !bytes.Equal(res, expectedReturn) || !strings.Contains(string(res), "hello world") { t.Errorf("response from calling contract was expected to be 'hello world' instead received %v", string(res)) } + + // make sure you can call the contract by hash + res, err = sim.CallContractAtHash(bgCtx, ethereum.CallMsg{ + From: testAddr, + To: &addr, + Data: input, + }, blockHash) + if err != nil { + t.Errorf("could not call receive method on contract: %v", err) + } + if len(res) == 0 { + t.Errorf("result of contract call was empty: %v", res) + } + + if !bytes.Equal(res, expectedReturn) || !strings.Contains(string(res), "hello world") { + t.Errorf("response from calling contract was expected to be 'hello world' instead received %v", string(res)) + } } // This test is based on the following contract: diff --git a/accounts/abi/bind/base.go b/accounts/abi/bind/base.go index b03f431f7..6da15f147 100644 --- a/accounts/abi/bind/base.go +++ b/accounts/abi/bind/base.go @@ -48,6 +48,7 @@ type CallOpts struct { Pending bool // Whether to operate on the pending state or the last known one From common.Address // Optional the sender address, otherwise the first account is used BlockNumber *big.Int // Optional the block number on which the call should be performed + BlockHash common.Hash // Optional the block hash on which the call should be performed Context context.Context // Network context to support cancellation and timeouts (nil = no timeout) } @@ -189,6 +190,23 @@ func (c *BoundContract) Call(opts *CallOpts, results *[]interface{}, method stri return ErrNoCode } } + } else if opts.BlockHash != (common.Hash{}) { + bh, ok := c.caller.(BlockHashContractCaller) + if !ok { + return ErrNoBlockHashState + } + output, err = bh.CallContractAtHash(ctx, msg, opts.BlockHash) + if err != nil { + return err + } + if len(output) == 0 { + // Make sure we have a contract to operate on, and bail out otherwise. + if code, err = bh.CodeAtHash(ctx, c.address, opts.BlockHash); err != nil { + return err + } else if len(code) == 0 { + return ErrNoCode + } + } } else { output, err = c.caller.CallContract(ctx, msg, opts.BlockNumber) if err != nil { diff --git a/accounts/abi/bind/base_test.go b/accounts/abi/bind/base_test.go index ca0128148..44552ab12 100644 --- a/accounts/abi/bind/base_test.go +++ b/accounts/abi/bind/base_test.go @@ -114,6 +114,26 @@ func (mc *mockPendingCaller) PendingCallContract(ctx context.Context, call ether return mc.pendingCallContractBytes, mc.pendingCallContractErr } +type mockBlockHashCaller struct { + *mockCaller + codeAtHashBytes []byte + codeAtHashErr error + codeAtHashCalled bool + callContractAtHashCalled bool + callContractAtHashBytes []byte + callContractAtHashErr error +} + +func (mc *mockBlockHashCaller) CodeAtHash(ctx context.Context, contract common.Address, hash common.Hash) ([]byte, error) { + mc.codeAtHashCalled = true + return mc.codeAtHashBytes, mc.codeAtHashErr +} + +func (mc *mockBlockHashCaller) CallContractAtHash(ctx context.Context, call ethereum.CallMsg, hash common.Hash) ([]byte, error) { + mc.callContractAtHashCalled = true + return mc.callContractAtHashBytes, mc.callContractAtHashErr +} + func TestPassingBlockNumber(t *testing.T) { mc := &mockPendingCaller{ mockCaller: &mockCaller{ @@ -400,6 +420,15 @@ func TestCall(t *testing.T) { Pending: true, }, method: method, + }, { + name: "ok hash", + mc: &mockBlockHashCaller{ + codeAtHashBytes: []byte{0}, + }, + opts: &bind.CallOpts{ + BlockHash: common.Hash{0xaa}, + }, + method: method, }, { name: "pack error, no method", mc: new(mockCaller), @@ -413,6 +442,14 @@ func TestCall(t *testing.T) { }, method: method, wantErrExact: bind.ErrNoPendingState, + }, { + name: "interface error, blockHash but not a BlockHashContractCaller", + mc: new(mockCaller), + opts: &bind.CallOpts{ + BlockHash: common.Hash{0xaa}, + }, + method: method, + wantErrExact: bind.ErrNoBlockHashState, }, { name: "pending call canceled", mc: &mockPendingCaller{ @@ -460,6 +497,34 @@ func TestCall(t *testing.T) { mc: new(mockCaller), method: method, wantErrExact: bind.ErrNoCode, + }, { + name: "call contract at hash error", + mc: &mockBlockHashCaller{ + callContractAtHashErr: context.DeadlineExceeded, + }, + opts: &bind.CallOpts{ + BlockHash: common.Hash{0xaa}, + }, + method: method, + wantErrExact: context.DeadlineExceeded, + }, { + name: "code at error", + mc: &mockBlockHashCaller{ + codeAtHashErr: errors.New(""), + }, + opts: &bind.CallOpts{ + BlockHash: common.Hash{0xaa}, + }, + method: method, + wantErr: true, + }, { + name: "no code at hash", + mc: new(mockBlockHashCaller), + opts: &bind.CallOpts{ + BlockHash: common.Hash{0xaa}, + }, + method: method, + wantErrExact: bind.ErrNoCode, }, { name: "unpack error missing arg", mc: &mockCaller{ diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index af373b993..83c89a2fd 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -370,6 +370,13 @@ func (ec *Client) BalanceAt(ctx context.Context, account common.Address, blockNu return (*big.Int)(&result), err } +// BalanceAtHash returns the wei balance of the given account. +func (ec *Client) BalanceAtHash(ctx context.Context, account common.Address, blockHash common.Hash) (*big.Int, error) { + var result hexutil.Big + err := ec.c.CallContext(ctx, &result, "eth_getBalance", account, rpc.BlockNumberOrHashWithHash(blockHash, false)) + return (*big.Int)(&result), err +} + // StorageAt returns the value of key in the contract storage of the given account. // The block number can be nil, in which case the value is taken from the latest known block. func (ec *Client) StorageAt(ctx context.Context, account common.Address, key common.Hash, blockNumber *big.Int) ([]byte, error) { @@ -378,6 +385,13 @@ func (ec *Client) StorageAt(ctx context.Context, account common.Address, key com return result, err } +// StorageAtHash returns the value of key in the contract storage of the given account. +func (ec *Client) StorageAtHash(ctx context.Context, account common.Address, key common.Hash, blockHash common.Hash) ([]byte, error) { + var result hexutil.Bytes + err := ec.c.CallContext(ctx, &result, "eth_getStorageAt", account, key, rpc.BlockNumberOrHashWithHash(blockHash, false)) + return result, err +} + // CodeAt returns the contract code of the given account. // The block number can be nil, in which case the code is taken from the latest known block. func (ec *Client) CodeAt(ctx context.Context, account common.Address, blockNumber *big.Int) ([]byte, error) { @@ -386,6 +400,13 @@ func (ec *Client) CodeAt(ctx context.Context, account common.Address, blockNumbe return result, err } +// CodeAtHash returns the contract code of the given account. +func (ec *Client) CodeAtHash(ctx context.Context, account common.Address, blockHash common.Hash) ([]byte, error) { + var result hexutil.Bytes + err := ec.c.CallContext(ctx, &result, "eth_getCode", account, rpc.BlockNumberOrHashWithHash(blockHash, false)) + return result, err +} + // NonceAt returns the account nonce of the given account. // The block number can be nil, in which case the nonce is taken from the latest known block. func (ec *Client) NonceAt(ctx context.Context, account common.Address, blockNumber *big.Int) (uint64, error) { @@ -394,6 +415,13 @@ func (ec *Client) NonceAt(ctx context.Context, account common.Address, blockNumb return uint64(result), err } +// NonceAtHash returns the account nonce of the given account. +func (ec *Client) NonceAtHash(ctx context.Context, account common.Address, blockHash common.Hash) (uint64, error) { + var result hexutil.Uint64 + err := ec.c.CallContext(ctx, &result, "eth_getTransactionCount", account, rpc.BlockNumberOrHashWithHash(blockHash, false)) + return uint64(result), err +} + // Filters // FilterLogs executes a filter query. diff --git a/ethclient/ethclient_test.go b/ethclient/ethclient_test.go index beaa4240a..4aa25759f 100644 --- a/ethclient/ethclient_test.go +++ b/ethclient/ethclient_test.go @@ -584,6 +584,11 @@ func testCallContract(t *testing.T, client *rpc.Client) { func testAtFunctions(t *testing.T, client *rpc.Client) { ec := NewClient(client) + block, err := ec.HeaderByNumber(context.Background(), big.NewInt(1)) + if err != nil { + t.Fatalf("BlockByNumber error: %v", err) + } + // send a transaction for some interesting pending status sendTransaction(ec) time.Sleep(100 * time.Millisecond) @@ -601,6 +606,13 @@ func testAtFunctions(t *testing.T, client *rpc.Client) { if err != nil { t.Fatalf("unexpected error: %v", err) } + hashBalance, err := ec.BalanceAtHash(context.Background(), testAddr, block.Hash()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if balance.Cmp(hashBalance) == 0 { + t.Fatalf("unexpected balance at hash: %v %v", balance, hashBalance) + } penBalance, err := ec.PendingBalanceAt(context.Background(), testAddr) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -613,6 +625,13 @@ func testAtFunctions(t *testing.T, client *rpc.Client) { if err != nil { t.Fatalf("unexpected error: %v", err) } + hashNonce, err := ec.NonceAtHash(context.Background(), testAddr, block.Hash()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if hashNonce == nonce { + t.Fatalf("unexpected nonce at hash: %v %v", nonce, hashNonce) + } penNonce, err := ec.PendingNonceAt(context.Background(), testAddr) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -625,6 +644,13 @@ func testAtFunctions(t *testing.T, client *rpc.Client) { if err != nil { t.Fatalf("unexpected error: %v", err) } + hashStorage, err := ec.StorageAtHash(context.Background(), testAddr, common.Hash{}, block.Hash()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(storage, hashStorage) { + t.Fatalf("unexpected storage at hash: %v %v", storage, hashStorage) + } penStorage, err := ec.PendingStorageAt(context.Background(), testAddr, common.Hash{}) if err != nil { t.Fatalf("unexpected error: %v", err) @@ -637,6 +663,13 @@ func testAtFunctions(t *testing.T, client *rpc.Client) { if err != nil { t.Fatalf("unexpected error: %v", err) } + hashCode, err := ec.CodeAtHash(context.Background(), common.Address{}, block.Hash()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !bytes.Equal(code, hashCode) { + t.Fatalf("unexpected code at hash: %v %v", code, hashCode) + } penCode, err := ec.PendingCodeAt(context.Background(), testAddr) if err != nil { t.Fatalf("unexpected error: %v", err)