diff --git a/ethclient/ethclient.go b/ethclient/ethclient.go index 7172add4e6..47e326fb7b 100644 --- a/ethclient/ethclient.go +++ b/ethclient/ethclient.go @@ -73,6 +73,7 @@ type Client interface { BlockByHash(context.Context, common.Hash) (*types.Block, error) BlockByNumber(context.Context, *big.Int) (*types.Block, error) BlockNumber(context.Context) (uint64, error) + BlockReceipts(context.Context, rpc.BlockNumberOrHash) ([]*types.Receipt, error) HeaderByHash(context.Context, common.Hash) (*types.Header, error) HeaderByNumber(context.Context, *big.Int) (*types.Header, error) TransactionByHash(context.Context, common.Hash) (tx *types.Transaction, isPending bool, err error) @@ -175,6 +176,16 @@ func (ec *client) BlockNumber(ctx context.Context) (uint64, error) { return uint64(result), err } +// BlockReceipts returns the receipts of a given block number or hash. +func (ec *client) BlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]*types.Receipt, error) { + var r []*types.Receipt + err := ec.c.CallContext(ctx, &r, "eth_getBlockReceipts", blockNrOrHash.String()) + if err == nil && r == nil { + return nil, interfaces.NotFound + } + return r, err +} + type rpcBlock struct { Hash common.Hash `json:"hash"` Transactions []rpcTransaction `json:"transactions"` diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index f044fa89e3..c8f857a03f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -873,6 +873,34 @@ func (s *BlockChainAPI) GetStorageAt(ctx context.Context, address common.Address return res[:], state.Error() } +// GetBlockReceipts returns the block receipts for the given block hash or number or tag. +func (s *BlockChainAPI) GetBlockReceipts(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) ([]map[string]interface{}, error) { + block, err := s.b.BlockByNumberOrHash(ctx, blockNrOrHash) + if block == nil || err != nil { + // When the block doesn't exist, the RPC method should return JSON null + // as per specification. + return nil, nil + } + receipts, err := s.b.GetReceipts(ctx, block.Hash()) + if err != nil { + return nil, err + } + txs := block.Transactions() + if len(txs) != len(receipts) { + return nil, fmt.Errorf("receipts length mismatch: %d vs %d", len(txs), len(receipts)) + } + + // Derive the sender. + signer := types.MakeSigner(s.b.ChainConfig(), block.Number(), block.Time()) + + result := make([]map[string]interface{}, len(receipts)) + for i, receipt := range receipts { + result[i] = marshalReceipt(receipt, block.Hash(), block.NumberU64(), signer, txs[i], i) + } + + return result, nil +} + // OverrideAccount indicates the overriding fields of account during the execution // of a message call. // Note, state and stateDiff can't be specified at the same time. If state is @@ -1769,13 +1797,18 @@ func (s *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common. // Derive the sender. signer := types.MakeSigner(s.b.ChainConfig(), header.Number, header.Time) + return marshalReceipt(receipt, blockHash, blockNumber, signer, tx, int(index)), nil +} + +// marshalReceipt marshals a transaction receipt into a JSON object. +func marshalReceipt(receipt *types.Receipt, blockHash common.Hash, blockNumber uint64, signer types.Signer, tx *types.Transaction, txIndex int) map[string]interface{} { from, _ := types.Sender(signer, tx) fields := map[string]interface{}{ "blockHash": blockHash, "blockNumber": hexutil.Uint64(blockNumber), - "transactionHash": hash, - "transactionIndex": hexutil.Uint64(index), + "transactionHash": tx.Hash(), + "transactionIndex": hexutil.Uint64(txIndex), "from": from, "to": tx.To(), "gasUsed": hexutil.Uint64(receipt.GasUsed), @@ -1800,7 +1833,7 @@ func (s *TransactionAPI) GetTransactionReceipt(ctx context.Context, hash common. if receipt.ContractAddress != (common.Address{}) { fields["contractAddress"] = receipt.ContractAddress } - return fields, nil + return fields } // sign is a helper function that signs a transaction with the private key of the given address. diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index 7b041fa07b..095608c554 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -32,6 +32,7 @@ import ( "crypto/ecdsa" "encoding/json" "errors" + "fmt" "hash" "math/big" "reflect" @@ -39,6 +40,8 @@ import ( "testing" "time" + "github.com/stretchr/testify/require" + "github.com/ava-labs/coreth/accounts" "github.com/ava-labs/coreth/consensus" "github.com/ava-labs/coreth/consensus/dummy" @@ -243,7 +246,7 @@ func (b testBackend) HeaderByNumber(ctx context.Context, number rpc.BlockNumber) return b.chain.GetHeaderByNumber(uint64(number)), nil } func (b testBackend) HeaderByHash(ctx context.Context, hash common.Hash) (*types.Header, error) { - panic("implement me") + return b.chain.GetHeaderByHash(hash), nil } func (b testBackend) HeaderByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Header, error) { panic("implement me") @@ -258,13 +261,16 @@ func (b testBackend) BlockByNumber(ctx context.Context, number rpc.BlockNumber) return b.chain.GetBlockByNumber(uint64(number)), nil } func (b testBackend) BlockByHash(ctx context.Context, hash common.Hash) (*types.Block, error) { - panic("implement me") + return b.chain.GetBlockByHash(hash), nil } func (b testBackend) BlockByNumberOrHash(ctx context.Context, blockNrOrHash rpc.BlockNumberOrHash) (*types.Block, error) { if blockNr, ok := blockNrOrHash.Number(); ok { return b.BlockByNumber(ctx, blockNr) } - panic("implement me") + if blockHash, ok := blockNrOrHash.Hash(); ok { + return b.BlockByHash(ctx, blockHash) + } + panic("unknown type rpc.BlockNumberOrHash") } func (b testBackend) GetBody(ctx context.Context, hash common.Hash, number rpc.BlockNumber) (*types.Body, error) { return b.chain.GetBlock(hash, uint64(number.Int64())).Body(), nil @@ -291,7 +297,12 @@ func (b testBackend) StateAndHeaderByNumberOrHash(ctx context.Context, blockNrOr } func (b testBackend) PendingBlockAndReceipts() (*types.Block, types.Receipts) { panic("implement me") } func (b testBackend) GetReceipts(ctx context.Context, hash common.Hash) (types.Receipts, error) { - panic("implement me") + header, err := b.HeaderByHash(ctx, hash) + if header == nil || err != nil { + return nil, err + } + receipts := rawdb.ReadReceipts(b.db, hash, header.Number.Uint64(), header.Time, b.chain.Config()) + return receipts, nil } func (b testBackend) GetTd(ctx context.Context, hash common.Hash) *big.Int { panic("implement me") } func (b testBackend) GetEVM(ctx context.Context, msg *core.Message, state *state.StateDB, header *types.Header, vmConfig *vm.Config, blockContext *vm.BlockContext) (*vm.EVM, func() error) { @@ -742,3 +753,174 @@ func TestRPCMarshalBlock(t *testing.T) { } } } + +func setupReceiptBackend(t *testing.T, genBlocks int) (*testBackend, []common.Hash) { + // Initialize test accounts + var ( + acc1Key, _ = crypto.HexToECDSA("8a1f9a8f95be41cd7ccb6168179afb4504aefe388d1e14474d32c45c72ce7b7a") + acc2Key, _ = crypto.HexToECDSA("49a7b37aa6f6645917e7b807e9d1c00d4fa71f18343b0d4122a4d2df64dd6fee") + acc1Addr = crypto.PubkeyToAddress(acc1Key.PublicKey) + acc2Addr = crypto.PubkeyToAddress(acc2Key.PublicKey) + contract = common.HexToAddress("0000000000000000000000000000000000031ec7") + genesis = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + acc1Addr: {Balance: big.NewInt(params.Ether)}, + acc2Addr: {Balance: big.NewInt(params.Ether)}, + // // SPDX-License-Identifier: GPL-3.0 + // pragma solidity >=0.7.0 <0.9.0; + // + // contract Token { + // event Transfer(address indexed from, address indexed to, uint256 value); + // function transfer(address to, uint256 value) public returns (bool) { + // emit Transfer(msg.sender, to, value); + // return true; + // } + // } + contract: {Balance: big.NewInt(params.Ether), Code: common.FromHex("0x608060405234801561001057600080fd5b506004361061002b5760003560e01c8063a9059cbb14610030575b600080fd5b61004a6004803603810190610045919061016a565b610060565b60405161005791906101c5565b60405180910390f35b60008273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516100bf91906101ef565b60405180910390a36001905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610101826100d6565b9050919050565b610111816100f6565b811461011c57600080fd5b50565b60008135905061012e81610108565b92915050565b6000819050919050565b61014781610134565b811461015257600080fd5b50565b6000813590506101648161013e565b92915050565b60008060408385031215610181576101806100d1565b5b600061018f8582860161011f565b92505060206101a085828601610155565b9150509250929050565b60008115159050919050565b6101bf816101aa565b82525050565b60006020820190506101da60008301846101b6565b92915050565b6101e981610134565b82525050565b600060208201905061020460008301846101e0565b9291505056fea2646970667358221220b469033f4b77b9565ee84e0a2f04d496b18160d26034d54f9487e57788fd36d564736f6c63430008120033")}, + }, + } + signer = types.LatestSignerForChainID(params.TestChainConfig.ChainID) + txHashes = make([]common.Hash, genBlocks) + ) + backend := newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + var ( + tx *types.Transaction + err error + ) + switch i { + case 0: + // transfer 1000wei + tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &acc2Addr, Value: big.NewInt(1000), Gas: params.TxGas, GasPrice: b.BaseFee(), Data: nil}), types.HomesteadSigner{}, acc1Key) + case 1: + // create contract + tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: nil, Gas: 53100, GasPrice: b.BaseFee(), Data: common.FromHex("0x60806040")}), signer, acc1Key) + case 2: + // with logs + // transfer(address to, uint256 value) + data := fmt.Sprintf("0xa9059cbb%s%s", common.HexToHash(common.BigToAddress(big.NewInt(int64(i + 1))).Hex()).String()[2:], common.BytesToHash([]byte{byte(i + 11)}).String()[2:]) + tx, err = types.SignTx(types.NewTx(&types.LegacyTx{Nonce: uint64(i), To: &contract, Gas: 60000, GasPrice: b.BaseFee(), Data: common.FromHex(data)}), signer, acc1Key) + case 3: + // dynamic fee with logs + // transfer(address to, uint256 value) + data := fmt.Sprintf("0xa9059cbb%s%s", common.HexToHash(common.BigToAddress(big.NewInt(int64(i + 1))).Hex()).String()[2:], common.BytesToHash([]byte{byte(i + 11)}).String()[2:]) + fee := big.NewInt(500) + fee.Add(fee, b.BaseFee()) + tx, err = types.SignTx(types.NewTx(&types.DynamicFeeTx{Nonce: uint64(i), To: &contract, Gas: 60000, Value: big.NewInt(1), GasTipCap: big.NewInt(500), GasFeeCap: fee, Data: common.FromHex(data)}), signer, acc1Key) + case 4: + // access list with contract create + accessList := types.AccessList{{ + Address: contract, + StorageKeys: []common.Hash{{0}}, + }} + tx, err = types.SignTx(types.NewTx(&types.AccessListTx{Nonce: uint64(i), To: nil, Gas: 58100, GasPrice: b.BaseFee(), Data: common.FromHex("0x60806040"), AccessList: accessList}), signer, acc1Key) + } + if err != nil { + t.Errorf("failed to sign tx: %v", err) + } + if tx != nil { + b.AddTx(tx) + txHashes[i] = tx.Hash() + } + }) + return backend, txHashes +} + +func TestRPCGetBlockReceipts(t *testing.T) { + t.Parallel() + + var ( + genBlocks = 5 + backend, _ = setupReceiptBackend(t, genBlocks) + api = NewBlockChainAPI(backend) + ) + blockHashes := make([]common.Hash, genBlocks+1) + ctx := context.Background() + for i := 0; i <= genBlocks; i++ { + header, err := backend.HeaderByNumber(ctx, rpc.BlockNumber(i)) + if err != nil { + t.Errorf("failed to get block: %d err: %v", i, err) + } + blockHashes[i] = header.Hash() + } + + var testSuite = []struct { + test rpc.BlockNumberOrHash + want string + }{ + // 0. block without any txs(hash) + { + test: rpc.BlockNumberOrHashWithHash(blockHashes[0], false), + want: `[]`, + }, + // 1. block without any txs(number) + { + test: rpc.BlockNumberOrHashWithNumber(0), + want: `[]`, + }, + // 2. earliest tag + { + test: rpc.BlockNumberOrHashWithNumber(rpc.EarliestBlockNumber), + want: `[]`, + }, + // 3. latest tag + { + test: rpc.BlockNumberOrHashWithNumber(rpc.LatestBlockNumber), + want: `[{"blockHash":"0xea3014b7aaa2b451f024d393c6a258876660dfe670d244a827ac3a73fb29676b","blockNumber":"0x5","contractAddress":"0xfdaa97661a584d977b4d3abb5370766ff5b86a18","cumulativeGasUsed":"0xe01c","effectiveGasPrice":"0x2ecde015a8","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0xe01c","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":null,"transactionHash":"0xa9616380994fd7502d0350ee57882bb6e95d6678fa6c4782f179c9f5f3529c48","transactionIndex":"0x0","type":"0x1"}]`, + }, + // 4. block with legacy transfer tx(hash) + { + test: rpc.BlockNumberOrHashWithHash(blockHashes[1], false), + want: `[{"blockHash":"0x1db4db39bd2505db96090946cfabd2d1d16fb02fd40af3cf353ee7c24886d38d","blockNumber":"0x1","contractAddress":null,"cumulativeGasUsed":"0x5208","effectiveGasPrice":"0x34630b8a00","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0x5208","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":"0x0d3ab14bbad3d99f4203bd7a11acb94882050e7e","transactionHash":"0x09220a8629fd020cbb341ab146e6acb4dc4811ab5fdf021bec3d3219c5a29ab3","transactionIndex":"0x0","type":"0x0"}]`, + }, + // 5. block with contract create tx(number) + { + test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(2)), + want: `[{"blockHash":"0x7e7958ad3c28186f4422d83426582145f66c6bcf1c6bb97d5350a5e56503de91","blockNumber":"0x2","contractAddress":"0xae9bea628c4ce503dcfd7e305cab4e29e7476592","cumulativeGasUsed":"0xcf50","effectiveGasPrice":"0x32ee841b80","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0xcf50","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":null,"transactionHash":"0x517f3174bd4501d55f0f93589ef0102152ab808f51bf595f2779461f04871a32","transactionIndex":"0x0","type":"0x0"}]`, + }, + // 6. block with legacy contract call tx(hash) + { + test: rpc.BlockNumberOrHashWithHash(blockHashes[3], false), + want: `[{"blockHash":"0x931e848eb68753a8332bee071a17c34870edfceb2a4f7edc019db79ee74cc924","blockNumber":"0x3","contractAddress":null,"cumulativeGasUsed":"0x5e28","effectiveGasPrice":"0x318455c568","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0x5e28","logs":[{"address":"0x0000000000000000000000000000000000031ec7","topics":["0xddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef","0x000000000000000000000000703c4b2bd70c169f5717101caee543299fc946c7","0x0000000000000000000000000000000000000000000000000000000000000003"],"data":"0x000000000000000000000000000000000000000000000000000000000000000d","blockNumber":"0x3","transactionHash":"0x0e9c460065fee166157eaadf702a01fb6ac1ce27b651e32850a8b09f71f93937","transactionIndex":"0x0","blockHash":"0x931e848eb68753a8332bee071a17c34870edfceb2a4f7edc019db79ee74cc924","logIndex":"0x0","removed":false}],"logsBloom":"0x00000000000000000000008000000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000800000000000000008000000000000000000000000000000000020000000080000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000800000000000000000400000000002000000000000800000000000000000000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000","status":"0x1","to":"0x0000000000000000000000000000000000031ec7","transactionHash":"0x0e9c460065fee166157eaadf702a01fb6ac1ce27b651e32850a8b09f71f93937","transactionIndex":"0x0","type":"0x0"}]`, + }, + // 7. block with dynamic fee tx(number) + { + test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(4)), + want: `[{"blockHash":"0x682de8741b70a2fd77d9b1bb553cae3c994ebe5b6fb61daf11d559e130ad1db8","blockNumber":"0x4","contractAddress":null,"cumulativeGasUsed":"0x538d","effectiveGasPrice":"0x302436f3a8","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0x538d","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x0","to":"0x0000000000000000000000000000000000031ec7","transactionHash":"0xcdd1122456f8ea113309e2ba5ecc8f389bbdc2e6bcced8eb103c6fdef201bf1a","transactionIndex":"0x0","type":"0x2"}]`, + }, + // 8. block is empty + { + test: rpc.BlockNumberOrHashWithHash(common.Hash{}, false), + want: `null`, + }, + // 9. block is not found + { + test: rpc.BlockNumberOrHashWithHash(common.HexToHash("deadbeef"), false), + want: `null`, + }, + // 10. block is not found + { + test: rpc.BlockNumberOrHashWithNumber(rpc.BlockNumber(genBlocks + 1)), + want: `null`, + }, + } + + for i, tt := range testSuite { + var ( + result interface{} + err error + ) + result, err = api.GetBlockReceipts(context.Background(), tt.test) + if err != nil { + t.Errorf("test %d: want no error, have %v", i, err) + continue + } + data, err := json.Marshal(result) + if err != nil { + t.Errorf("test %d: json marshal error", i) + continue + } + want, have := tt.want, string(data) + require.JSONEqf(t, want, have, "test %d: json not match, want: %s, have: %s", i, want, have) + } +} diff --git a/rpc/types.go b/rpc/types.go index cf87132648..6bc1a769d6 100644 --- a/rpc/types.go +++ b/rpc/types.go @@ -31,7 +31,6 @@ import ( "encoding/json" "fmt" "math" - "strconv" "strings" "github.com/ethereum/go-ethereum/common" @@ -238,7 +237,7 @@ func (bnh *BlockNumberOrHash) Number() (BlockNumber, bool) { func (bnh *BlockNumberOrHash) String() string { if bnh.BlockNumber != nil { - return strconv.Itoa(int(*bnh.BlockNumber)) + return bnh.BlockNumber.String() } if bnh.BlockHash != nil { return bnh.BlockHash.String() diff --git a/rpc/types_test.go b/rpc/types_test.go index 09041cdee7..a255c1e9f7 100644 --- a/rpc/types_test.go +++ b/rpc/types_test.go @@ -163,3 +163,24 @@ func TestBlockNumberOrHash_WithNumber_MarshalAndUnmarshal(t *testing.T) { }) } } + +func TestBlockNumberOrHash_StringAndUnmarshal(t *testing.T) { + tests := []BlockNumberOrHash{ + BlockNumberOrHashWithNumber(math.MaxInt64), + BlockNumberOrHashWithNumber(PendingBlockNumber), + BlockNumberOrHashWithNumber(LatestBlockNumber), + BlockNumberOrHashWithNumber(EarliestBlockNumber), + BlockNumberOrHashWithNumber(32), + BlockNumberOrHashWithHash(common.Hash{0xaa}, false), + } + for _, want := range tests { + marshalled, _ := json.Marshal(want.String()) + var have BlockNumberOrHash + if err := json.Unmarshal(marshalled, &have); err != nil { + t.Fatalf("cannot unmarshal (%v): %v", string(marshalled), err) + } + if !reflect.DeepEqual(want, have) { + t.Fatalf("wrong result: have %v, want %v", have, want) + } + } +}