Skip to content

Commit

Permalink
jsonrpc: Implement debug_getBadBlocks (#11888)
Browse files Browse the repository at this point in the history
#8171

- reference of rpc api query params/return value is taken from geth
[here](https://github.com/ethereum/go-ethereum/blob/b4d99e39176f89bb0ffbb32635e9cf17b5fd7367/eth/api_debug.go#L107)
- difference is that geth returns ALL bad blocks, this PR returns 100.
  • Loading branch information
sudeepdino008 authored Dec 4, 2024
1 parent 5d99935 commit 53b973d
Show file tree
Hide file tree
Showing 9 changed files with 381 additions and 2 deletions.
35 changes: 35 additions & 0 deletions core/rawdb/accessors_chain.go
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ package rawdb

import (
"bytes"
"container/heap"
"context"
"encoding/binary"
"fmt"
Expand All @@ -41,6 +42,7 @@ import (
"github.com/erigontech/erigon-lib/kv/rawdbv3"

"github.com/erigontech/erigon-lib/rlp"
"github.com/erigontech/erigon/core/rawdb/utils"
"github.com/erigontech/erigon/core/types"
"github.com/erigontech/erigon/ethdb/cbor"
)
Expand Down Expand Up @@ -79,6 +81,10 @@ func TruncateCanonicalHash(tx kv.RwTx, blockFrom uint64, markChainAsBad bool) er
if err := tx.Put(kv.BadHeaderNumber, blockHash, blockNumBytes); err != nil {
return err
}

if bheapCache != nil {
heap.Push(bheapCache, &utils.BlockId{Number: binary.BigEndian.Uint64(blockNumBytes), Hash: common.BytesToHash(blockHash)})
}
}
return tx.Delete(kv.HeaderCanonical, blockNumBytes)
}); err != nil {
Expand All @@ -87,6 +93,35 @@ func TruncateCanonicalHash(tx kv.RwTx, blockFrom uint64, markChainAsBad bool) er
return nil
}

/* latest bad blocks start */
var bheapCache utils.ExtendedHeap

func GetLatestBadBlocks(tx kv.Tx) ([]*types.Block, error) {
if bheapCache == nil {
ResetBadBlockCache(tx, 100)
}

blockIds := bheapCache.SortedValues()
blocks := make([]*types.Block, len(blockIds))
for i, blockId := range blockIds {
blocks[i] = ReadBlock(tx, blockId.Hash, blockId.Number)
}

return blocks, nil
}

// mainly for testing purposes
func ResetBadBlockCache(tx kv.Tx, limit int) error {
bheapCache = utils.NewBlockMaxHeap(limit)
// load the heap
return tx.ForEach(kv.BadHeaderNumber, nil, func(blockHash, blockNumBytes []byte) error {
heap.Push(bheapCache, &utils.BlockId{Number: binary.BigEndian.Uint64(blockNumBytes), Hash: common.BytesToHash(blockHash)})
return nil
})
}

/* latest bad blocks end */

func IsCanonicalHash(db kv.Getter, hash common.Hash, number uint64) (bool, error) {
canonicalHash, err := ReadCanonicalHash(db, number)
if err != nil {
Expand Down
67 changes: 67 additions & 0 deletions core/rawdb/accessors_chain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import (
"github.com/stretchr/testify/require"
"golang.org/x/crypto/sha3"

"github.com/erigontech/erigon-lib/common"
libcommon "github.com/erigontech/erigon-lib/common"
"github.com/erigontech/erigon-lib/common/u256"
"github.com/erigontech/erigon-lib/crypto"
Expand Down Expand Up @@ -758,6 +759,72 @@ func TestShanghaiBodyForStorageNoWithdrawals(t *testing.T) {
require.Equal(uint32(2), body.TxCount)
}

func TestBadBlocks(t *testing.T) {
t.Parallel()
m := mock.Mock(t)
tx, err := m.DB.BeginRw(m.Ctx)
require.NoError(t, err)
defer tx.Rollback()

require := require.New(t)
var testKey, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291")
testAddr := crypto.PubkeyToAddress(testKey.PublicKey)

mustSign := func(tx types.Transaction, s types.Signer) types.Transaction {
r, err := types.SignTx(tx, s, testKey)
require.NoError(err)
return r
}

putBlock := func(number uint64) common.Hash {
// prepare db so it works with our test
signer1 := types.MakeSigner(params.MainnetChainConfig, number, number-1)
body := &types.Body{
Transactions: []types.Transaction{
mustSign(types.NewTransaction(number, testAddr, u256.Num1, 1, u256.Num1, nil), *signer1),
mustSign(types.NewTransaction(number+1, testAddr, u256.Num1, 2, u256.Num1, nil), *signer1),
},
Uncles: []*types.Header{{Extra: []byte("test header")}},
}

header := &types.Header{Number: big.NewInt(int64(number))}
require.NoError(rawdb.WriteCanonicalHash(tx, header.Hash(), number))
require.NoError(rawdb.WriteHeader(tx, header))
require.NoError(rawdb.WriteBody(tx, header.Hash(), number, body))

return header.Hash()
}
rawdb.ResetBadBlockCache(tx, 4)

// put some blocks
for i := 1; i <= 6; i++ {
putBlock(uint64(i))
}
hash1 := putBlock(7)
hash2 := putBlock(8)
hash3 := putBlock(9)
hash4 := putBlock(10)

// mark some blocks as bad
require.NoError(rawdb.TruncateCanonicalHash(tx, 7, true))
badBlks, err := rawdb.GetLatestBadBlocks(tx)
require.NoError(err)
require.Len(badBlks, 4)

require.Equal(badBlks[0].Hash(), hash4)
require.Equal(badBlks[1].Hash(), hash3)
require.Equal(badBlks[2].Hash(), hash2)
require.Equal(badBlks[3].Hash(), hash1)

// testing the "limit"
rawdb.ResetBadBlockCache(tx, 2)
badBlks, err = rawdb.GetLatestBadBlocks(tx)
require.NoError(err)
require.Len(badBlks, 2)
require.Equal(badBlks[0].Hash(), hash4)
require.Equal(badBlks[1].Hash(), hash3)
}

func checkReceiptsRLP(have, want types.Receipts) error {
if len(have) != len(want) {
return fmt.Errorf("receipts sizes mismatch: have %d, want %d", len(have), len(want))
Expand Down
84 changes: 84 additions & 0 deletions core/rawdb/utils/block_max_heap.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package utils

import (
"container/heap"

"github.com/erigontech/erigon-lib/common"
)

// blockMaxHeap stores a (limited-length) priority queue of blocks.
// the queue will maintain a fixed length of blocks, discarding the lowest block num when limit is exceeded.
// Effectively the heap stores the maximum `limit` blocks (by block_num) among all blocks pushed.
// -1 means unlimited length
type blockMaxHeap struct {
heap []*BlockId
limit int
}

type BlockId struct {
Number uint64
Hash common.Hash
}

type ExtendedHeap interface {
heap.Interface
SortedValues() []*BlockId
}

func NewBlockMaxHeap(limit int) ExtendedHeap {
return &blockMaxHeap{limit: limit}
}

func (h *blockMaxHeap) Len() int {
return len(h.heap)
}

func (h *blockMaxHeap) Less(i, j int) bool {
return h.heap[i].Number < h.heap[j].Number
}

func (h *blockMaxHeap) Swap(i, j int) {
h.heap[i], h.heap[j] = h.heap[j], h.heap[i]
}

func (h *blockMaxHeap) Push(x any) {
if h.limit == 0 {
return
}
len := len(h.heap)
if h.limit != -1 && len >= h.limit && len > 0 {
if h.heap[0].Number < x.(*BlockId).Number {
_ = heap.Pop(h)
} else {
// discard
return
}
}
h.heap = append(h.heap, x.(*BlockId))
}

func (h *blockMaxHeap) Pop() any {
n := len(h.heap)
x := h.heap[n-1]
h.heap = h.heap[0 : n-1]
return x
}

func (h *blockMaxHeap) copy() *blockMaxHeap {
newHeap := NewBlockMaxHeap(h.limit)
for _, b := range h.heap {
heap.Push(newHeap, b)
}
return newHeap.(*blockMaxHeap)
}

func (h *blockMaxHeap) SortedValues() []*BlockId {
// copy
copyHeap := h.copy()
res := make([]*BlockId, len(copyHeap.heap))
for copyHeap.Len() > 0 {
res[copyHeap.Len()-1] = heap.Pop(copyHeap).(*BlockId)
}

return res
}
82 changes: 82 additions & 0 deletions core/rawdb/utils/block_max_heap_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
package utils_test

import (
"container/heap"
"testing"

"github.com/erigontech/erigon-lib/common"
"github.com/erigontech/erigon/core/rawdb/utils"
)

func TestPush(t *testing.T) {
h := utils.NewBlockMaxHeap(-1)
heap.Push(h, newB(1, []byte{}))
if h.Len() != 1 {
t.Fatal("expected 1")
}

heap.Push(h, newB(2, []byte{}))
if h.Len() != 2 {
t.Fatal("expected 2")
}
}

func TestPop(t *testing.T) {
h := utils.NewBlockMaxHeap(-1)
heap.Push(h, newB(4, []byte{0x01}))
heap.Push(h, newB(99, []byte{0x02}))
heap.Push(h, newB(3, []byte{0x03}))

x := heap.Pop(h).(*utils.BlockId)
if x.Number != 3 || lastByte(x) != 0x03 {
t.Fatal("unexpected")
}

x = heap.Pop(h).(*utils.BlockId)
if x.Number != 4 || lastByte(x) != 0x01 {
t.Fatal("unexpected")
}

x = h.Pop().(*utils.BlockId)
if x.Number != 99 || lastByte(x) != 0x02 {
t.Fatal("unexpected")
}
}

func TestPopWithLimits(t *testing.T) {
h := utils.NewBlockMaxHeap(2)
heap.Push(h, newB(4, []byte{0x01}))
heap.Push(h, newB(2, []byte{0x04}))
heap.Push(h, newB(99, []byte{0x02}))
heap.Push(h, newB(3, []byte{0x03}))

if h.Len() != 2 {
t.Fatal("expected 2 got ", h.Len())
}
x := heap.Pop(h).(*utils.BlockId)
if x.Number != 4 || lastByte(x) != 0x01 {
t.Fatal("unexpected")
}

x = heap.Pop(h).(*utils.BlockId)
if x.Number != 99 || lastByte(x) != 0x02 {
t.Fatal("unexpected")
}
}

func TestPopWithEmptyHeap(t *testing.T) {
h := utils.NewBlockMaxHeap(0)
heap.Push(h, newB(4, []byte{0x01}))
if h.Len() != 0 {
t.Fatal("expected 0")
}
}

func newB(id uint64, hash []byte) *utils.BlockId {
return &utils.BlockId{Number: id, Hash: common.BytesToHash(hash)}
}

func lastByte(b *utils.BlockId) byte {
by := b.Hash.Bytes()
return by[len(by)-1]
}
4 changes: 2 additions & 2 deletions erigon-lib/kv/tables.go
Original file line number Diff line number Diff line change
Expand Up @@ -71,8 +71,8 @@ const (
DatabaseInfo = "DbInfo"

// Naming:
// NeaderNumber - Ethereum-specific block number. All nodes have same BlockNum.
// NeaderID - auto-increment ID. Depends on order in which node see headers.
// HeaderNumber - Ethereum-specific block number. All nodes have same BlockNum.
// HeaderID - auto-increment ID. Depends on order in which node see headers.
// Invariant: for all headers in snapshots Number == ID. It means no reason to store Num/ID for this headers in DB.
// Same about: TxNum/TxID, BlockNum/BlockID
HeaderNumber = "HeaderNumber" // header_hash -> header_num_u64
Expand Down
4 changes: 4 additions & 0 deletions turbo/jsonrpc/corner_cases_support_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,4 +74,8 @@ func TestNotFoundMustReturnNil(t *testing.T) {
j, err := api.GetBlockTransactionCountByNumber(ctx, 10_000)
require.Nil(j)
require.Nil(err)

k, err := api.GetBadBlocks(ctx)
require.Nil(k)
require.Nil(err)
}
1 change: 1 addition & 0 deletions turbo/jsonrpc/eth_api.go
Original file line number Diff line number Diff line change
Expand Up @@ -61,6 +61,7 @@ type EthAPI interface {
// Block related (proposed file: ./eth_blocks.go)
GetBlockByNumber(ctx context.Context, number rpc.BlockNumber, fullTx bool) (map[string]interface{}, error)
GetBlockByHash(ctx context.Context, hash rpc.BlockNumberOrHash, fullTx bool) (map[string]interface{}, error)
GetBadBlocks(ctx context.Context) ([]map[string]interface{}, error)
GetBlockTransactionCountByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*hexutil.Uint, error)
GetBlockTransactionCountByHash(ctx context.Context, blockHash common.Hash) (*hexutil.Uint, error)

Expand Down
37 changes: 37 additions & 0 deletions turbo/jsonrpc/eth_block.go
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ import (
"github.com/erigontech/erigon-lib/kv/rawdbv3"
"github.com/erigontech/erigon-lib/log/v3"

"github.com/erigontech/erigon-lib/rlp"
"github.com/erigontech/erigon/cl/clparams"
"github.com/erigontech/erigon/core"
"github.com/erigontech/erigon/core/rawdb"
Expand Down Expand Up @@ -333,6 +334,42 @@ func (api *APIImpl) GetBlockByHash(ctx context.Context, numberOrHash rpc.BlockNu
return response, err
}

func (api *APIImpl) GetBadBlocks(ctx context.Context) ([]map[string]interface{}, error) {
tx, err := api.db.BeginRo(ctx)
if err != nil {
return nil, err
}
defer tx.Rollback()

blocks, err := rawdb.GetLatestBadBlocks(tx)
if err != nil || len(blocks) == 0 {
return nil, err
}

results := make([]map[string]interface{}, 0, len(blocks))
for _, block := range blocks {
var blockRlp string
if rlpBytes, err := rlp.EncodeToBytes(block); err != nil {
blockRlp = err.Error() // hack
} else {
blockRlp = fmt.Sprintf("%#x", rlpBytes)
}

blockJson, err := ethapi.RPCMarshalBlock(block, true, true, nil)
if err != nil {
log.Error("Failed to marshal block", "err", err)
blockJson = map[string]interface{}{}
}
results = append(results, map[string]interface{}{
"hash": block.Hash(),
"block": blockRlp,
"rlp": blockJson,
})
}

return results, nil
}

// GetBlockTransactionCountByNumber implements eth_getBlockTransactionCountByNumber. Returns the number of transactions in a block given the block's block number.
func (api *APIImpl) GetBlockTransactionCountByNumber(ctx context.Context, blockNr rpc.BlockNumber) (*hexutil.Uint, error) {
tx, err := api.db.BeginRo(ctx)
Expand Down
Loading

0 comments on commit 53b973d

Please sign in to comment.