Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Implementation of the debug_getAccessibleState and debug_dbGet methods #398

Open
wants to merge 5 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 17 additions & 0 deletions blockchain/storagev2/leveldb/leveldb.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,23 @@ func (l *levelDB) Get(t uint8, k []byte) ([]byte, bool, error) {
return data, true, nil
}

// Get retrieves the key-value pair in leveldb storage
novosandara marked this conversation as resolved.
Show resolved Hide resolved
func (l *levelDB) Has(t uint8, k []byte) (bool, error) {
mc := tableMapper[t]
k = append(k, mc...)

ok, err := l.db.Has(k, nil)
if err != nil {
if err.Error() == "leveldb: not found" {
return false, nil
}

return false, err
}

return ok, nil
}

// Close closes the leveldb storage instance
func (l *levelDB) Close() error {
return l.db.Close()
Expand Down
20 changes: 20 additions & 0 deletions blockchain/storagev2/mdbx/mdbx.go
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,26 @@ func (db *MdbxDB) Get(t uint8, k []byte) ([]byte, bool, error) {
return data, true, nil
}

func (db *MdbxDB) Has(t uint8, k []byte) (bool, error) {
novosandara marked this conversation as resolved.
Show resolved Hide resolved
tx, err := db.env.BeginTxn(nil, mdbx.Readonly)
defer tx.Abort()

if err != nil {
return false, err
}

_, err = tx.Get(db.dbi[t], k)
if err != nil {
if err.Error() == "key not found" {
return false, nil
}

return false, err
}

return true, nil
}

// Close closes the mdbx storage instance
func (db *MdbxDB) Close() error {
db.env.Close()
Expand Down
9 changes: 9 additions & 0 deletions blockchain/storagev2/memory/memory.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,15 @@ func (m *memoryDB) Get(t uint8, k []byte) ([]byte, bool, error) {
return v, true, nil
}

func (m *memoryDB) Has(t uint8, k []byte) (bool, error) {
novosandara marked this conversation as resolved.
Show resolved Hide resolved
_, ok := m.db[t].kv[hex.EncodeToHex(k)]
if !ok {
return false, nil
}

return true, nil
}

func (m *memoryDB) Close() error {
return nil
}
Expand Down
1 change: 1 addition & 0 deletions blockchain/storagev2/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (
type Database interface {
Close() error
Get(t uint8, k []byte) ([]byte, bool, error)
Has(t uint8, k []byte) (bool, error)
novosandara marked this conversation as resolved.
Show resolved Hide resolved
NewBatch() Batch
}

Expand Down
87 changes: 87 additions & 0 deletions jsonrpc/debug_endpoint.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import (
"context"
"errors"
"fmt"
"math"
"os"
"path/filepath"
"runtime"
Expand Down Expand Up @@ -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(codeHas types.Hash) ([]byte, error)
novosandara marked this conversation as resolved.
Show resolved Hide resolved

// 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)

Expand Down Expand Up @@ -723,6 +731,85 @@ 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) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

return uint64 instead of interface

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) {
Copy link

@oliverbundalo oliverbundalo Oct 3, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Return []byte instead of interface.

There is a question from which database data needs to be returned, state or blockchain? IMO we should check both, but in that case we need to extend blockchain API with Get method. Although it's not complicated I am not sure if it's worth it. For now we can keep it this way, returning value only from state DB.

return d.throttling.AttemptRequest(
context.Background(),
func() (interface{}, error) {
hash := types.StringToHash(key)

return d.store.Get(hash)
},
)
}

func (d *Debug) traceBlock(
block *types.Block,
config *TraceConfig,
Expand Down
134 changes: 134 additions & 0 deletions jsonrpc/debug_endpoint_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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(types.Hash) ([]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)
Expand Down Expand Up @@ -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(codeHas types.Hash) ([]byte, error) {
novosandara marked this conversation as resolved.
Show resolved Hide resolved
return s.getFn(codeHas)
}

func (s *debugEndpointMockStore) GetIteratorDumpTree(block *types.Block, opts *state.DumpInfo) (*state.IteratorDump, error) {
return s.getIteratorDumpTreeFn(block, opts)
}
Expand Down Expand Up @@ -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()

Expand Down
28 changes: 28 additions & 0 deletions server/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -707,6 +707,24 @@ func (j *jsonRPCHub) GetStorage(stateRoot types.Hash, addr types.Address, slot t
return res.Bytes(), nil
}

func (j *jsonRPCHub) Get(rootHash types.Hash) ([]byte, error) {
novosandara marked this conversation as resolved.
Show resolved Hide resolved
snap, err := j.state.NewSnapshotAt(rootHash)
if err != nil {
return nil, fmt.Errorf("failed to create snapshot at root hash: %w", err)
}

data, ok, err := snap.Get(rootHash)
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", rootHash.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 {
Expand All @@ -721,6 +739,16 @@ 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 {
snap, err := j.state.NewSnapshotAt(rootHash)
novosandara marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
return false
}

return snap.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())
Expand Down
8 changes: 8 additions & 0 deletions state/immutable-trie/snapshot.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,14 @@ func (s *Snapshot) GetCode(hash types.Hash) ([]byte, bool) {
return s.state.GetCode(hash)
}

func (s *Snapshot) Has(hash types.Hash) bool {
novosandara marked this conversation as resolved.
Show resolved Hide resolved
return s.state.Has(hash)
}

func (s *Snapshot) Get(hash types.Hash) ([]byte, bool, error) {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need for this method.

return s.state.storage.Get(hash.Bytes())
}

func (s *Snapshot) GetRootHash() types.Hash {
tt := s.trie.Txn(s.state.storage)

Expand Down
Loading