From 380ee0eb3ce327011f170cddf0e54adf03a77a3d Mon Sep 17 00:00:00 2001 From: Khang Nguyen Date: Tue, 5 Nov 2024 14:50:02 +0700 Subject: [PATCH] Implement eip-1186, eth_getProof --- common/bytes.go | 9 +++ core/state/statedb.go | 35 +++++++++ eth/api_backend.go | 11 +-- internal/ethapi/api.go | 66 +++++++++++++++++ internal/ethapi/api_test.go | 138 ++++++++++++++++++++++++++++++++++-- 5 files changed, 251 insertions(+), 8 deletions(-) diff --git a/common/bytes.go b/common/bytes.go index ba00e8a4b2..b82f41f16c 100644 --- a/common/bytes.go +++ b/common/bytes.go @@ -28,6 +28,15 @@ func ToHex(b []byte) string { return "0x" + hex } +// ToHexArray creates a array of hex-string based on []byte +func ToHexArray(b [][]byte) []string { + r := make([]string, len(b)) + for i := range b { + r[i] = ToHex(b[i]) + } + return r +} + func FromHex(s string) []byte { if len(s) > 1 { if s[0:2] == "0x" || s[0:2] == "0X" { diff --git a/core/state/statedb.go b/core/state/statedb.go index 7a3357b3e8..2bc243f4c4 100644 --- a/core/state/statedb.go +++ b/core/state/statedb.go @@ -18,6 +18,7 @@ package state import ( + "errors" "fmt" "math/big" "sort" @@ -36,6 +37,17 @@ type revision struct { journalIndex int } +type proofList [][]byte + +func (n *proofList) Put(key []byte, value []byte) error { + *n = append(*n, value) + return nil +} + +func (n *proofList) Delete(key []byte) error { + panic("not supported") +} + var ( // emptyState is the known hash of an empty state trie entry. emptyState = crypto.Keccak256Hash(nil) @@ -92,6 +104,29 @@ func (self *StateDB) SubRefund(gas uint64) { self.refund -= gas } +// GetProof returns the Merkle proof for a given account. +func (s *StateDB) GetProof(addr common.Address) ([][]byte, error) { + return s.GetProofByHash(crypto.Keccak256Hash(addr.Bytes())) +} + +// GetProofByHash returns the Merkle proof for a given account. +func (s *StateDB) GetProofByHash(addrHash common.Hash) ([][]byte, error) { + var proof proofList + err := s.trie.Prove(addrHash[:], 0, &proof) + return proof, err +} + +// GetStorageProof returns the Merkle proof for given storage slot. +func (s *StateDB) GetStorageProof(a common.Address, key common.Hash) ([][]byte, error) { + var proof proofList + trie := s.StorageTrie(a) + if trie == nil { + return proof, errors.New("storage trie for requested address does not exist") + } + err := trie.Prove(crypto.Keccak256(key.Bytes()), 0, &proof) + return proof, err +} + func (self *StateDB) GetCommittedState(addr common.Address, hash common.Hash) common.Hash { stateObject := self.getStateObject(addr) if stateObject != nil { diff --git a/eth/api_backend.go b/eth/api_backend.go index 7fd7aac3b1..4615ff2eed 100644 --- a/eth/api_backend.go +++ b/eth/api_backend.go @@ -98,17 +98,20 @@ func (b *EthApiBackend) BlockByNumber(ctx context.Context, blockNr rpc.BlockNumb return b.eth.blockchain.GetBlockByNumber(uint64(blockNr)), nil } -func (b *EthApiBackend) StateAndHeaderByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*state.StateDB, *types.Header, error) { +func (b *EthApiBackend) StateAndHeaderByNumber(ctx context.Context, number rpc.BlockNumber) (*state.StateDB, *types.Header, error) { // Pending state is only known by the miner - if blockNr == rpc.PendingBlockNumber { + if number == rpc.PendingBlockNumber { block, state := b.eth.miner.Pending() return state, block.Header(), nil } // Otherwise resolve the block number and return its state - header, err := b.HeaderByNumber(ctx, blockNr) - if header == nil || err != nil { + header, err := b.HeaderByNumber(ctx, number) + if err != nil { return nil, nil, err } + if header == nil { + return nil, nil, errors.New("header not found") + } stateDb, err := b.eth.BlockChain().StateAt(header.Root) return stateDb, header, err } diff --git a/internal/ethapi/api.go b/internal/ethapi/api.go index e7b7576f58..7cf531b71f 100644 --- a/internal/ethapi/api.go +++ b/internal/ethapi/api.go @@ -526,6 +526,72 @@ func (s *PublicBlockChainAPI) GetBalance(ctx context.Context, address common.Add return b, state.Error() } +// Result structs for GetProof +type AccountResult struct { + Address common.Address `json:"address"` + AccountProof []string `json:"accountProof"` + Balance *hexutil.Big `json:"balance"` + CodeHash common.Hash `json:"codeHash"` + Nonce hexutil.Uint64 `json:"nonce"` + StorageHash common.Hash `json:"storageHash"` + StorageProof []StorageResult `json:"storageProof"` +} +type StorageResult struct { + Key string `json:"key"` + Value *hexutil.Big `json:"value"` + Proof []string `json:"proof"` +} + +// GetProof returns the Merkle-proof for a given account and optionally some storage keys. +func (s *PublicBlockChainAPI) GetProof(ctx context.Context, address common.Address, storageKeys []string, blockNr rpc.BlockNumber) (*AccountResult, error) { + state, _, err := s.b.StateAndHeaderByNumber(ctx, blockNr) + if state == nil || err != nil { + return nil, err + } + + storageTrie := state.StorageTrie(address) + storageHash := types.EmptyRootHash + codeHash := state.GetCodeHash(address) + storageProof := make([]StorageResult, len(storageKeys)) + + // if we have a storageTrie, (which means the account exists), we can update the storagehash + if storageTrie != nil { + storageHash = storageTrie.Hash() + } else { + // no storageTrie means the account does not exist, so the codeHash is the hash of an empty bytearray. + codeHash = crypto.Keccak256Hash(nil) + } + + // create the proof for the storageKeys + for i, key := range storageKeys { + if storageTrie != nil { + proof, storageError := state.GetStorageProof(address, common.HexToHash(key)) + if storageError != nil { + return nil, storageError + } + storageProof[i] = StorageResult{key, (*hexutil.Big)(state.GetState(address, common.HexToHash(key)).Big()), common.ToHexArray(proof)} + } else { + storageProof[i] = StorageResult{key, &hexutil.Big{}, []string{}} + } + } + + // create the accountProof + accountProof, proofErr := state.GetProof(address) + if proofErr != nil { + return nil, proofErr + } + + return &AccountResult{ + Address: address, + AccountProof: common.ToHexArray(accountProof), + Balance: (*hexutil.Big)(state.GetBalance(address)), + CodeHash: codeHash, + Nonce: hexutil.Uint64(state.GetNonce(address)), + StorageHash: storageHash, + StorageProof: storageProof, + }, state.Error() +} + // GetBlockByNumber returns the requested block. When blockNr is -1 the chain head is returned. When fullTx is true all // transactions in the block are returned in full detail, otherwise only the transaction hash is returned. func (s *PublicBlockChainAPI) GetBlockByNumber(ctx context.Context, blockNr rpc.BlockNumber, fullTx bool) (map[string]interface{}, error) { diff --git a/internal/ethapi/api_test.go b/internal/ethapi/api_test.go index ab9149cc38..92ecdb8445 100644 --- a/internal/ethapi/api_test.go +++ b/internal/ethapi/api_test.go @@ -5,6 +5,12 @@ import ( "crypto/ecdsa" "errors" "fmt" + "math/big" + "slices" + "strings" + "testing" + "time" + "github.com/tomochain/tomochain/accounts" "github.com/tomochain/tomochain/common" "github.com/tomochain/tomochain/common/hexutil" @@ -25,10 +31,6 @@ import ( "github.com/tomochain/tomochain/tomox" "github.com/tomochain/tomochain/tomox/tradingstate" "github.com/tomochain/tomochain/tomoxlending" - "math/big" - "slices" - "testing" - "time" ) type testBackend struct { @@ -399,3 +401,131 @@ func TestEstimateGas(t *testing.T) { } } } + +func TestPublicBlockChainAPI_GetProof(t *testing.T) { + t.Parallel() + var ( + accounts = newAccounts(2) + genesis = &core.Genesis{ + Config: params.TestChainConfig, + Alloc: core.GenesisAlloc{ + accounts[0].addr: {Balance: big.NewInt(params.Ether)}, + accounts[1].addr: {Balance: big.NewInt(params.Ether)}, + }, + } + genBlocks = 10 + signer = types.HomesteadSigner{} + ) + + backend := newTestBackend(t, genBlocks, genesis, func(i int, b *core.BlockGen) { + tx, err := types.SignTx(types.NewTransaction(uint64(i), accounts[1].addr, big.NewInt(1000), params.TxGas, nil, nil), signer, accounts[0].key) + if err != nil { + t.Fatal(err) + } + b.AddTx(tx) + }) + + api := NewPublicBlockChainAPI(backend) + + testCases := []struct { + name string + address common.Address + storageKeys []string + blockNr rpc.BlockNumber + wantErr bool + errMsg string + expected *AccountResult + }{ + { + name: "Valid account proof latest block", + address: accounts[0].addr, + storageKeys: []string{}, + blockNr: rpc.LatestBlockNumber, + wantErr: false, + }, + { + name: "Valid account with storage proof", + address: accounts[0].addr, + storageKeys: []string{"0x0000000000000000000000000000000000000000000000000000000000000000"}, + blockNr: rpc.LatestBlockNumber, + wantErr: false, + }, + { + name: "Non-existent account", + address: common.HexToAddress("0x1234567890123456789012345678901234567890"), + storageKeys: []string{}, + blockNr: rpc.LatestBlockNumber, + wantErr: false, + }, + { + name: "Invalid block number", + address: accounts[0].addr, + storageKeys: []string{}, + blockNr: rpc.BlockNumber(-5), // Using -5 to ensure it's invalid + wantErr: false, + expected: nil, + }, + // { + // name: "Pending block", + // address: accounts[0].addr, + // storageKeys: []string{}, + // blockNr: rpc.PendingBlockNumber, + // wantErr: true, + // errMsg: "proof not supported for pending block", + // }, + } + + for _, tc := range testCases { + t.Run(tc.name, func(t *testing.T) { + result, err := api.GetProof(context.Background(), tc.address, tc.storageKeys, tc.blockNr) + + if tc.wantErr { + if err == nil { + t.Errorf("expected error containing '%s' but got none", tc.errMsg) + return + } + if !strings.Contains(err.Error(), tc.errMsg) { + t.Errorf("expected error containing '%s', got '%v'", tc.errMsg, err) + } + return + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + return + } + + // Verify result fields + if result == nil && tc.expected == nil { + return + } + + if result.Address != tc.address { + t.Errorf("address mismatch: got %v, want %v", result.Address, tc.address) + } + + if len(result.AccountProof) == 0 { + t.Error("account proof should not be empty") + } + + if result.Balance == nil { + t.Error("balance should not be nil") + } + + if result.CodeHash == (common.Hash{}) { + t.Error("codehash should not be empty") + } + + if result.StorageHash == (common.Hash{}) { + t.Error("storagehash should not be empty") + } + + if len(tc.storageKeys) > 0 { + if len(result.StorageProof) != len(tc.storageKeys) { + t.Errorf("storage proof length mismatch: got %d, want %d", + len(result.StorageProof), len(tc.storageKeys)) + } + } + }) + } +}