diff --git a/jsonrpc/debug_endpoint.go b/jsonrpc/debug_endpoint.go index 1219e5739b..ae3ad73143 100644 --- a/jsonrpc/debug_endpoint.go +++ b/jsonrpc/debug_endpoint.go @@ -4,6 +4,7 @@ import ( "context" "errors" "fmt" + "math" "os" "path/filepath" "runtime" @@ -55,6 +56,13 @@ type debugBlockchainStore interface { // GetPendingTx returns the transaction by hash in the TxPool (pending txn) [Thread-safe] GetPendingTx(txHash types.Hash) (*types.Transaction, bool) + // Has returns true if the DB does contains the given key. + Has(hashRoot types.Hash) bool + + // Get gets the value for the given key. It returns ErrNotFound if the + // DB does not contains the key. + Get(key string) ([]byte, error) + // GetIteratorDumpTree returns a set of accounts based on the given criteria and depends on the starting element. GetIteratorDumpTree(block *types.Block, opts *state.DumpInfo) (*state.IteratorDump, error) @@ -723,6 +731,83 @@ func (d *Debug) DumpBlock(blockNumber BlockNumber) (interface{}, error) { ) } +// GetAccessibleState returns the first number where the node has accessible +// state on disk. Note this being the post-state of that block and the pre-state +// of the next block. +// The (from, to) parameters are the sequence of blocks to search, which can go +// either forwards or backwards +func (d *Debug) GetAccessibleState(from, to BlockNumber) (interface{}, error) { + return d.throttling.AttemptRequest( + context.Background(), + func() (interface{}, error) { + getBlockNumber := func(num BlockNumber) (int64, error) { + n, err := GetNumericBlockNumber(num, d.store) + if err != nil { + return 0, fmt.Errorf("failed to get block number: %w", err) + } + + if n > math.MaxInt64 { + return 0, fmt.Errorf("block number %d overflows int64", n) + } + + return int64(n), nil + } + + // Get start and end block numbers + start, err := getBlockNumber(from) + if err != nil { + return 0, err + } + + end, err := getBlockNumber(to) + if err != nil { + return 0, err + } + + if start == end { + return 0, fmt.Errorf("'from' and 'to' block numbers must be different") + } + + delta := int64(1) + if start > end { + delta = -1 + } + + for i := start; i != end; i += delta { + if i < 0 { + return 0, fmt.Errorf("block number overflow: %d", i) + } + + blockNum := uint64(i) + h, ok := d.store.GetHeaderByNumber(blockNum) + + if !ok { + return 0, fmt.Errorf("missing header for block number %d", i) + } + + if d.store.Has(h.StateRoot) { + return blockNum, nil + } + } + + // No state found + return 0, errors.New("no accessible state found between the given block numbers") + }, + ) +} + +// DbGet returns the raw value of a key stored in the database. +// +//nolint:stylecheck +func (d *Debug) DbGet(key string) (interface{}, error) { + return d.throttling.AttemptRequest( + context.Background(), + func() (interface{}, error) { + return d.store.Get(key) + }, + ) +} + func (d *Debug) traceBlock( block *types.Block, config *TraceConfig, diff --git a/jsonrpc/debug_endpoint_test.go b/jsonrpc/debug_endpoint_test.go index 639c881a29..2d8a172c6e 100644 --- a/jsonrpc/debug_endpoint_test.go +++ b/jsonrpc/debug_endpoint_test.go @@ -23,6 +23,8 @@ type debugEndpointMockStore struct { getReceiptsByHashFn func(types.Hash) ([]*types.Receipt, error) readTxLookupFn func(types.Hash) (uint64, bool) getPendingTxFn func(types.Hash) (*types.Transaction, bool) + hasFn func(types.Hash) bool + getFn func(string) ([]byte, error) getIteratorDumpTreeFn func(*types.Block, *state.DumpInfo) (*state.IteratorDump, error) dumpTreeFn func(*types.Block, *state.DumpInfo) (*state.Dump, error) getBlockByHashFn func(types.Hash, bool) (*types.Block, bool) @@ -54,6 +56,14 @@ func (s *debugEndpointMockStore) GetPendingTx(txHash types.Hash) (*types.Transac return s.getPendingTxFn(txHash) } +func (s *debugEndpointMockStore) Has(hash types.Hash) bool { + return s.hasFn(hash) +} + +func (s *debugEndpointMockStore) Get(key string) ([]byte, error) { + return s.getFn(key) +} + func (s *debugEndpointMockStore) GetIteratorDumpTree(block *types.Block, opts *state.DumpInfo) (*state.IteratorDump, error) { return s.getIteratorDumpTreeFn(block, opts) } @@ -1466,6 +1476,130 @@ func TestDumpBlock(t *testing.T) { } } +func TestGetAccessibleState(t *testing.T) { + t.Parallel() + + tests := []struct { + name string + start, end BlockNumber + store *debugEndpointMockStore + result interface{} + returnErr string + err bool + }{ + { + name: "GetNumericBlockNumberNotValid", + start: BlockNumber(-5), + end: BlockNumber(-5), + store: &debugEndpointMockStore{}, + returnErr: "failed to get block number", + result: 0, + err: true, + }, + { + name: "BlockNotDifferent", + start: *LatestBlockNumberOrHash.BlockNumber, + end: *LatestBlockNumberOrHash.BlockNumber, + + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestBlock.Header + }, + }, + + returnErr: "'from' and 'to' block numbers must be different", + result: 0, + err: true, + }, + { + name: "HeaderNotFound", + start: *LatestBlockNumberOrHash.BlockNumber, + end: BlockNumber(testHeader10.Number), + + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestBlock.Header + }, + + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + return nil, false + }, + }, + + returnErr: "missing header for block number", + result: 0, + err: true, + }, + { + name: "resultNotFound", + start: *LatestBlockNumberOrHash.BlockNumber, + end: BlockNumber(testHeader10.Number), + + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestBlock.Header + }, + + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + return testLatestBlock.Header, true + }, + + hasFn: func(hash types.Hash) bool { + return false + }, + }, + + returnErr: "no accessible state found between the given block numbers", + result: 0, + err: true, + }, + + { + name: "resultsValid", + start: *LatestBlockNumberOrHash.BlockNumber, + end: BlockNumber(testHeader10.Number), + + store: &debugEndpointMockStore{ + headerFn: func() *types.Header { + return testLatestBlock.Header + }, + + getHeaderByNumberFn: func(num uint64) (*types.Header, bool) { + return testLatestBlock.Header, true + }, + + hasFn: func(hash types.Hash) bool { + return true + }, + }, + + returnErr: "", + result: testLatestBlock.Header.Number, + err: false, + }, + } + + for _, test := range tests { + test := test + + t.Run(test.name, func(t *testing.T) { + t.Parallel() + + endpoint := NewDebug(test.store, 100000) + + res, err := endpoint.GetAccessibleState(test.start, test.end) + + require.Equal(t, test.result, res) + + if test.err { + require.ErrorContains(t, err, test.returnErr) + } else { + require.NoError(t, err) + } + }) + } +} + func Test_newTracer(t *testing.T) { t.Parallel() diff --git a/server/server.go b/server/server.go index 601d8c14cf..dfd9ba759c 100644 --- a/server/server.go +++ b/server/server.go @@ -707,6 +707,21 @@ func (j *jsonRPCHub) GetStorage(stateRoot types.Hash, addr types.Address, slot t return res.Bytes(), nil } +func (j *jsonRPCHub) Get(key string) ([]byte, error) { + hash := types.StringToHash(key) + + data, ok, err := j.state.Get(hash) + if err != nil { + return nil, fmt.Errorf("failed to get data for root hash: %w", err) + } + + if !ok { + return nil, fmt.Errorf("data not found for root hash: %s", hash.String()) + } + + return data, nil +} + func (j *jsonRPCHub) GetCode(root types.Hash, addr types.Address) ([]byte, error) { account, err := getAccountImpl(j.state, root, addr) if err != nil { @@ -721,6 +736,11 @@ func (j *jsonRPCHub) GetCode(root types.Hash, addr types.Address) ([]byte, error return code, nil } +// Has returns true if the DB does contains the given key. +func (j *jsonRPCHub) Has(rootHash types.Hash) bool { + return j.state.Has(rootHash) +} + // DumpTree retrieves accounts based on the specified criteria for the given block. func (j *jsonRPCHub) DumpTree(block *types.Block, opts *state.DumpInfo) (*state.Dump, error) { parentHeader, ok := j.GetHeaderByHash(block.ParentHash()) diff --git a/state/immutable-trie/snapshot.go b/state/immutable-trie/snapshot.go index aabecb2c51..2ce605f632 100644 --- a/state/immutable-trie/snapshot.go +++ b/state/immutable-trie/snapshot.go @@ -74,6 +74,10 @@ func (s *Snapshot) GetCode(hash types.Hash) ([]byte, bool) { return s.state.GetCode(hash) } +func (s *Snapshot) Get(hash types.Hash) ([]byte, bool, error) { + return s.state.storage.Get(hash.Bytes()) +} + func (s *Snapshot) GetRootHash() types.Hash { tt := s.trie.Txn(s.state.storage) diff --git a/state/immutable-trie/state.go b/state/immutable-trie/state.go index 95a9e9905d..c7d812848a 100644 --- a/state/immutable-trie/state.go +++ b/state/immutable-trie/state.go @@ -54,6 +54,27 @@ func (s *State) GetCode(hash types.Hash) ([]byte, bool) { return s.storage.GetCode(hash) } +func (s *State) Has(hash types.Hash) bool { + if hash == types.EmptyCodeHash { + return false + } + + ok, err := s.storage.Has(hash.Bytes()) + if err != nil { + return false + } + + return ok +} + +func (s *State) Get(hash types.Hash) ([]byte, bool, error) { + if hash == types.EmptyCodeHash { + return nil, false, nil + } + + return s.storage.Get(hash.Bytes()) +} + // newTrieAt returns trie with root and if necessary locks state on a trie level func (s *State) newTrieAt(root types.Hash) (*Trie, error) { if root == types.EmptyRootHash { diff --git a/state/immutable-trie/storage.go b/state/immutable-trie/storage.go index 7c4fa44e5e..9c7934ec66 100644 --- a/state/immutable-trie/storage.go +++ b/state/immutable-trie/storage.go @@ -33,6 +33,7 @@ type Batch interface { type Storage interface { Put(k, v []byte) error Get(k []byte) ([]byte, bool, error) + Has(k []byte) (bool, error) Batch() Batch SetCode(hash types.Hash, code []byte) error GetCode(hash types.Hash) ([]byte, bool) @@ -93,6 +94,19 @@ func (kv *KVStorage) Get(k []byte) ([]byte, bool, error) { return data, true, nil } +func (kv *KVStorage) Has(k []byte) (bool, error) { + ok, err := kv.db.Has(k, nil) + if err != nil { + if err.Error() == levelDBNotFoundMsg { + return false, nil + } + + return false, err + } + + return ok, nil +} + func (kv *KVStorage) Close() error { return kv.db.Close() } @@ -145,6 +159,15 @@ func (m *memStorage) Get(p []byte) ([]byte, bool, error) { return v, true, nil } +func (m *memStorage) Has(p []byte) (bool, error) { + m.l.Lock() + defer m.l.Unlock() + + _, ok := m.db[hex.EncodeToHex(p)] + + return ok, nil +} + func (m *memStorage) SetCode(hash types.Hash, code []byte) error { return m.Put(append(codePrefix, hash.Bytes()...), code) } diff --git a/state/state.go b/state/state.go index 00653f284d..91036f4fe6 100644 --- a/state/state.go +++ b/state/state.go @@ -15,6 +15,8 @@ type State interface { NewSnapshotAt(types.Hash) (Snapshot, error) NewSnapshot() Snapshot GetCode(hash types.Hash) ([]byte, bool) + Get(hash types.Hash) ([]byte, bool, error) + Has(hash types.Hash) bool } type Snapshot interface { diff --git a/state/txn.go b/state/txn.go index 31960fb2bc..6f43e75211 100644 --- a/state/txn.go +++ b/state/txn.go @@ -30,6 +30,7 @@ type readSnapshot interface { GetStorage(addr types.Address, root types.Hash, key types.Hash) types.Hash GetAccount(addr types.Address) (*Account, error) GetCode(hash types.Hash) ([]byte, bool) + Get(hash types.Hash) ([]byte, bool, error) GetRootHash() types.Hash } diff --git a/state/txn_test.go b/state/txn_test.go index 50cef75ebd..c8a2b1a9ea 100644 --- a/state/txn_test.go +++ b/state/txn_test.go @@ -47,6 +47,14 @@ func (m *mockSnapshot) GetCode(hash types.Hash) ([]byte, bool) { return nil, false } +func (m *mockSnapshot) Has(hash types.Hash) bool { + return false +} + +func (m *mockSnapshot) Get(hash types.Hash) ([]byte, bool, error) { + return nil, false, nil +} + func (m *mockSnapshot) GetRootHash() types.Hash { return emptyStateHash }