Skip to content

Commit

Permalink
Add RPC method eth_getBlockReceipts (#467)
Browse files Browse the repository at this point in the history
* feat: add api rpc eth_getBlockReceipts
* update unit test
  • Loading branch information
c98tristan authored Nov 7, 2024
1 parent 396b851 commit 455bf36
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 48 deletions.
10 changes: 10 additions & 0 deletions ethclient/ethclient.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,16 @@ func (ec *Client) BlockByNumber(ctx context.Context, number *big.Int) (*types.Bl
return ec.getBlock(ctx, "eth_getBlockByNumber", toBlockNumArg(number), true)
}

// BlockReceipts returns the receipts of a given block number or hash
func (ec *Client) BlockReceipts(ctx context.Context, blockNr rpc.BlockNumber) ([]*types.Receipt, error) {
var r []*types.Receipt
err := ec.c.CallContext(ctx, &r, "eth_getBlockReceipts", blockNr)
if err == nil && r == nil {
return nil, ethereum.NotFound
}
return r, err
}

type rpcBlock struct {
Hash common.Hash `json:"hash"`
Transactions []rpcTransaction `json:"transactions"`
Expand Down
54 changes: 45 additions & 9 deletions internal/ethapi/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -424,7 +424,8 @@ func (s *PrivateAccountAPI) SignTransaction(ctx context.Context, args SendTxArgs
// safely used to calculate a signature from.
//
// The hash is calulcated as
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
//
// keccak256("\x19Ethereum Signed Message:\n"${message length}${message}).
//
// This gives context to the signed message and prevents signing of transactions.
func signHash(data []byte) []byte {
Expand Down Expand Up @@ -628,6 +629,34 @@ func (s *PublicBlockChainAPI) GetStorageAt(ctx context.Context, address common.A
return res[:], state.Error()
}

// GetBlockReceipts returns the block receipts for the given block hash or number or tag.
func (api *PublicBlockChainAPI) GetBlockReceipts(ctx context.Context, blockNr rpc.BlockNumber) ([]map[string]interface{}, error) {
block, err := api.b.BlockByNumber(ctx, blockNr)
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 := api.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(api.b.ChainConfig(), block.Number())

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
}

func (s *PublicBlockChainAPI) GetBlockSignersByHash(ctx context.Context, blockHash common.Hash) ([]common.Address, error) {
block, err := s.b.GetBlock(ctx, blockHash)
if err != nil || block == nil {
Expand Down Expand Up @@ -1310,8 +1339,8 @@ func (s *PublicBlockChainAPI) findNearestSignedBlock(ctx context.Context, b *typ
}

/*
findFinalityOfBlock return finality of a block
Use blocksHashCache for to keep track - refer core/blockchain.go for more detail
findFinalityOfBlock return finality of a block
Use blocksHashCache for to keep track - refer core/blockchain.go for more detail
*/
func (s *PublicBlockChainAPI) findFinalityOfBlock(ctx context.Context, b *types.Block, masternodes []common.Address) (uint, error) {
engine, _ := s.b.GetEngine().(*posv.Posv)
Expand Down Expand Up @@ -1376,7 +1405,7 @@ func (s *PublicBlockChainAPI) findFinalityOfBlock(ctx context.Context, b *types.
}

/*
Extract signers from block
Extract signers from block
*/
func (s *PublicBlockChainAPI) getSigners(ctx context.Context, block *types.Block, engine *posv.Posv) ([]common.Address, error) {
var err error
Expand Down Expand Up @@ -1644,13 +1673,18 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(ctx context.Context, ha
if tx.Protected() {
signer = types.NewEIP155Signer(tx.ChainId())
}
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),
Expand All @@ -1673,7 +1707,7 @@ func (s *PublicTransactionPoolAPI) GetTransactionReceipt(ctx context.Context, ha
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.
Expand Down Expand Up @@ -2970,7 +3004,8 @@ func GetSignersFromBlocks(b Backend, blockNumber uint64, blockHash common.Hash,
// GetStakerROI Estimate ROI for stakers using the last epoc reward
// then multiple by epoch per year, if the address is not masternode of last epoch - return 0
// Formular:
// ROI = average_latest_epoch_reward_for_voters*number_of_epoch_per_year/latest_total_cap*100
//
// ROI = average_latest_epoch_reward_for_voters*number_of_epoch_per_year/latest_total_cap*100
func (s *PublicBlockChainAPI) GetStakerROI() float64 {
blockNumber := s.b.CurrentBlock().Number().Uint64()
lastCheckpointNumber := blockNumber - (blockNumber % s.b.ChainConfig().Posv.Epoch) - s.b.ChainConfig().Posv.Epoch // calculate for 2 epochs ago
Expand All @@ -2996,7 +3031,8 @@ func (s *PublicBlockChainAPI) GetStakerROI() float64 {
// GetStakerROIMasternode Estimate ROI for stakers of a specific masternode using the last epoc reward
// then multiple by epoch per year, if the address is not masternode of last epoch - return 0
// Formular:
// ROI = latest_epoch_reward_for_voters*number_of_epoch_per_year/latest_total_cap*100
//
// ROI = latest_epoch_reward_for_voters*number_of_epoch_per_year/latest_total_cap*100
func (s *PublicBlockChainAPI) GetStakerROIMasternode(masternode common.Address) float64 {
votersReward := s.b.GetVotersRewards(masternode)
if votersReward == nil {
Expand Down
198 changes: 160 additions & 38 deletions internal/ethapi/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,13 @@ package ethapi
import (
"context"
"crypto/ecdsa"
"encoding/json"
"errors"
"fmt"
"github.com/stretchr/testify/require"
"github.com/tomochain/tomochain/accounts"
"github.com/tomochain/tomochain/common"
"github.com/tomochain/tomochain/common/hexutil"
"github.com/tomochain/tomochain/common/math"
"github.com/tomochain/tomochain/consensus"
"github.com/tomochain/tomochain/consensus/ethash"
"github.com/tomochain/tomochain/core"
Expand All @@ -26,6 +27,8 @@ import (
"github.com/tomochain/tomochain/tomox/tradingstate"
"github.com/tomochain/tomochain/tomoxlending"
"math/big"
"os"
"path/filepath"
"slices"
"testing"
"time"
Expand Down Expand Up @@ -114,8 +117,7 @@ func (t testBackend) GetBlock(ctx context.Context, blockHash common.Hash) (*type
}

func (t testBackend) GetReceipts(ctx context.Context, blockHash common.Hash) (types.Receipts, error) {
//TODO implement me
panic("implement me")
return core.GetBlockReceipts(t.db, blockHash, core.GetBlockNumber(t.db, blockHash)), nil
}

func (t testBackend) GetTd(blockHash common.Hash) *big.Int {
Expand All @@ -124,7 +126,6 @@ func (t testBackend) GetTd(blockHash common.Hash) *big.Int {
}

func (b testBackend) GetEVM(ctx context.Context, msg core.Message, state *state.StateDB, tomoxState *tradingstate.TradingStateDB, header *types.Header, vmCfg vm.Config) (*vm.EVM, func() error, error) {
state.SetBalance(msg.From(), math.MaxBig256)
vmError := func() error { return nil }

context := core.NewEVMContext(msg, header, b.chain, nil)
Expand Down Expand Up @@ -202,8 +203,7 @@ func (t testBackend) SendLendingTx(ctx context.Context, signedTx *types.LendingT
}

func (t testBackend) ChainConfig() *params.ChainConfig {
//TODO implement me
panic("implement me")
return t.chain.Config()
}

func (t testBackend) CurrentBlock() *types.Block {
Expand Down Expand Up @@ -313,14 +313,9 @@ func TestEstimateGas(t *testing.T) {
accounts[1].addr: {Balance: big.NewInt(params.Ether)},
},
}
genBlocks = 10
signer = types.HomesteadSigner{}
randomAccounts = newAccounts(2)
genBlocks = 10
signer = types.HomesteadSigner{}
)
fmt.Printf("accounts[0]: %v\n", accounts[0].addr)
fmt.Printf("accounts[1]: %v\n", accounts[1].addr)
fmt.Printf("randomAccounts[0]: %v\n", randomAccounts[0].addr)
fmt.Printf("randomAccounts[1]: %v\n", randomAccounts[1].addr)
api := NewPublicBlockChainAPI(newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) {
// Transfer from account[0] to account[1]
// value: 1000 wei
Expand Down Expand Up @@ -349,37 +344,16 @@ func TestEstimateGas(t *testing.T) {
expectErr: nil,
want: 21000,
},
// simple transfer with insufficient funds on latest block
// empty create
{
blockNumber: rpc.LatestBlockNumber,
call: CallArgs{
From: randomAccounts[0].addr,
To: &accounts[1].addr,
Value: (hexutil.Big)(*big.NewInt(1000)),
},
expectErr: core.ErrInsufficientFunds,
want: 21000,
call: CallArgs{},
expectErr: nil,
want: 53000,
},
// empty create
//{
// blockNumber: rpc.LatestBlockNumber,
// call: CallArgs{},
// expectErr: nil,
// want: 53000,
//},
//{
// blockNumber: rpc.LatestBlockNumber,
// call: CallArgs{
// From: randomAccounts[0].addr,
// To: &randomAccounts[1].addr,
// Value: (hexutil.Big)(*big.NewInt(1000)),
// },
// expectErr: core.ErrInsufficientFunds,
//},
}
for i, tc := range testSuite {
result, err := api.EstimateGas(context.Background(), tc.call, &tc.blockNumber)
fmt.Println(result)
if tc.expectErr != nil {
if err == nil {
t.Errorf("test %d: want error %v, have nothing", i, tc.expectErr)
Expand All @@ -399,3 +373,151 @@ func TestEstimateGas(t *testing.T) {
}
}
}

func TestRPCGetBlockReceipts(t *testing.T) {
t.Parallel()

var (
genBlocks = 3
backend, _ = setupReceiptBackend(t, genBlocks)
api = NewPublicBlockChainAPI(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.BlockNumber
want string
}{
// 1. block without any txs(number)
{
test: rpc.BlockNumber(0),
want: `[]`,
},
// 2. earliest tag
{
test: rpc.EarliestBlockNumber,
want: `[]`,
},
// 3. latest tag
{
test: rpc.LatestBlockNumber,
want: `[{"blockHash":"0x7b30611be396a2b3135482fb49975fa1641b9703da2bb9e8ddef4dd5ab0c36e8", "blockNumber":"0x3", "contractAddress":null, "cumulativeGasUsed":"0xea60", "from":"0x703c4b2bd70c169f5717101caee543299fc946c7", "gasUsed":"0xea60", "logs":[], "logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", "status":"0x0", "to":"0x0000000000000000000000000000000000031ec7", "transactionHash":"0x0fa8c0c52f331c690c832c11c9cdc6c9e635bc5b055729230b1eb2b35c53419f", "transactionIndex":"0x0"}]`,
},
// 5. block with contract create tx(number)
{
test: rpc.BlockNumber(2),
want: `[{"blockHash":"0xa56b19f6ed7acd69a6b17ab17388cca59de28fe8c49ae62be68752476386b39d","blockNumber":"0x2","contractAddress":null,"cumulativeGasUsed":"0x5318","from":"0x703c4b2bd70c169f5717101caee543299fc946c7","gasUsed":"0x5318","logs":[],"logsBloom":"0x00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000","status":"0x1","to":"0x0000000000000000000000000000000000000000","transactionHash":"0x537c16d5b0f04d33a2a40bc879f892c2a8e5866a3a7db99eeb78165b003d3d55","transactionIndex":"0x0"}]`,
},
// 10. block is not found
{
test: 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)
}
}

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.HomesteadSigner{}
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)
tx, err = types.SignTx(types.NewTransaction(uint64(i), acc2Addr, big.NewInt(1000), params.TxGas, nil, nil), signer, 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)
tx, err = types.SignTx(types.NewTransaction(uint64(i), common.Address{}, nil, 53100, nil, 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)
tx, err = types.SignTx(types.NewTransaction(uint64(i), contract, nil, 60000, nil, common.FromHex(data)), 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 testRPCResponseWithFile(t *testing.T, testid int, result interface{}, rpc string, file string) {
data, err := json.MarshalIndent(result, "", " ")
if err != nil {
t.Errorf("test %d: json marshal error", testid)
return
}
outputFile := filepath.Join("testdata", fmt.Sprintf("%s-%s.json", rpc, file))
fmt.Println("outputFile: ", outputFile)
if os.Getenv("WRITE_TEST_FILES") != "" {
os.WriteFile(outputFile, data, 0644)
}
want, err := os.ReadFile(outputFile)
if err != nil {
t.Fatalf("error reading expected test file: %s output: %v", outputFile, err)
}
require.JSONEqf(t, string(want), string(data), "test %d: json not match, want: %s, have: %s", testid, string(want), string(data))
}
Loading

0 comments on commit 455bf36

Please sign in to comment.