Skip to content
Merged
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
2 changes: 1 addition & 1 deletion consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ import (
)

const (
defaultSpanLength = 6400 // Default span length i.e. number of bor blocks in a span
defaultSpanLength = params.DefaultSpanLength
zerothSpanEnd = 255 // End block of 0th span
checkpointInterval = 1024 // Number of blocks after which to save the vote snapshot to the database
inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory
Expand Down
31 changes: 22 additions & 9 deletions core/blockchain.go
Original file line number Diff line number Diff line change
Expand Up @@ -574,11 +574,6 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine,
}
}
}
// The first thing the node will do is reconstruct the verification data for
// the head block (ethash cache or clique voting snapshot). Might as well do
// it in advance.
// BOR - commented out intentionally
// bc.engine.VerifyHeader(bc, bc.CurrentHeader())

// Check the current state of the block hashes and make sure that we do not have any of the bad blocks in our chain
for hash := range BadHashes {
Expand Down Expand Up @@ -4232,6 +4227,8 @@ func (bc *BlockChain) startHeaderVerificationLoop() {

// verifyPendingHeaders checks headers after the latest finalized block
// and rewinds the chain if invalid headers are found.
// Verification is capped to params.DefaultSpanLength + 1 blocks from the current head.
// This covers atleast 2 spans for max reorg protection without unbounded memory growth.
func (bc *BlockChain) verifyPendingHeaders() {
Comment thread
kamuikatsurgi marked this conversation as resolved.
// Get the latest finalized block
hasMilestone, milestoneNumber, _ := bc.checker.GetWhitelistedMilestone()
Expand All @@ -4251,9 +4248,19 @@ func (bc *BlockChain) verifyPendingHeaders() {
return // Rio is not enabled yet
}

// Collect headers from finalized block + 1 to current head
var headers []*types.Header
for i := milestoneNumber + 1; i <= currentHead.Number.Uint64(); i++ {
// Cap verification window to span_duration + 1 blocks from the head.
// This covers 2 spans reorg depth, preventing unbounded memory growth and
// header reads when the gap between milestone and head is large.
startBlock := milestoneNumber + 1
headNumber := currentHead.Number.Uint64()

if headNumber > params.DefaultSpanLength && headNumber-params.DefaultSpanLength > startBlock {
startBlock = headNumber - params.DefaultSpanLength
}

// Collect headers from startBlock to current head
headers := make([]*types.Header, 0, headNumber-startBlock+1)
for i := startBlock; i <= headNumber; i++ {
header := bc.GetHeaderByNumber(i)
if header == nil {
log.Debug("Missing header during verification", "number", i)
Expand Down Expand Up @@ -4285,10 +4292,16 @@ func (bc *BlockChain) verifyPendingHeaders() {
"number", header.Number.Uint64(), "hash", header.Hash(), "err", err)
// Rewind to the last valid block
if lastValidNumber < currentHead.Number.Uint64() {
dropCount := int64(currentHead.Number.Uint64() - lastValidNumber)

log.Warn("Rewinding chain due to invalid header",
"from", currentHead.Number.Uint64(), "to", lastValidNumber)
"from", currentHead.Number.Uint64(), "to", lastValidNumber, "drop", dropCount)

if err := bc.SetHead(lastValidNumber); err != nil {
log.Error("Failed to rewind chain", "err", err)
} else {
blockReorgMeter.Mark(1)
blockReorgDropMeter.Mark(dropCount)
}
}
return
Expand Down
165 changes: 165 additions & 0 deletions core/blockchain_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5138,6 +5138,171 @@ func TestHeaderVerificationWithNilChecker(t *testing.T) {
}
}

// headerCountingEngine wraps ethash and records how many headers VerifyHeaders receives.
type headerCountingEngine struct {
*ethash.Ethash
headersVerified atomic.Int64
}

func (m *headerCountingEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) {
m.headersVerified.Store(int64(len(headers)))
return m.Ethash.VerifyHeaders(chain, headers)
}

// TestVerifyPendingHeadersCapBoundary tests that verifyPendingHeaders caps the
// verification window to DefaultSpanLength + 1 headers from the current head.
func TestVerifyPendingHeadersCapBoundary(t *testing.T) {
t.Run("GapSmallerThanCap", func(t *testing.T) {
engine := &headerCountingEngine{Ethash: ethash.NewFaker()}

config := *params.TestChainConfig
config.Bor = &params.BorConfig{
RioBlock: big.NewInt(0),
}
genesis := &Genesis{
Config: &config,
BaseFee: big.NewInt(params.InitialBaseFee),
}

mockValidator := &mockChainValidator{
hasMilestone: true,
milestoneNumber: 3,
milestoneHash: common.HexToHash("0x123"),
}

_, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 20, nil)

cfg := DefaultConfig()
cfg.Checker = mockValidator
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg)
if err != nil {
t.Fatalf("failed to create blockchain: %v", err)
}
defer chain.Stop()

if _, err := chain.InsertChain(blocks, false); err != nil {
t.Fatalf("failed to insert chain: %v", err)
}

chain.verifyPendingHeaders()

// Gap is 20 - 3 = 17, which is less than DefaultSpanLength (6400).
// All 17 headers (blocks 4-20) should be verified.
if got := engine.headersVerified.Load(); got != 17 {
t.Errorf("expected 17 headers verified (no cap), got %d", got)
}
})

t.Run("GapLargerThanCap", func(t *testing.T) {
engine := &headerCountingEngine{Ethash: ethash.NewFaker()}
chainLength := int(params.DefaultSpanLength) + 100

config := *params.TestChainConfig
config.Bor = &params.BorConfig{
RioBlock: big.NewInt(0),
}
genesis := &Genesis{
Config: &config,
BaseFee: big.NewInt(params.InitialBaseFee),
}

mockValidator := &mockChainValidator{
hasMilestone: true,
milestoneNumber: 0,
milestoneHash: common.HexToHash("0x123"),
}

_, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, chainLength, nil)

cfg := DefaultConfig()
cfg.Checker = mockValidator
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg)
if err != nil {
t.Fatalf("failed to create blockchain: %v", err)
}
defer chain.Stop()

if _, err := chain.InsertChain(blocks, false); err != nil {
t.Fatalf("failed to insert chain: %v", err)
}

chain.verifyPendingHeaders()

// Gap is 6500 - 0 = 6500, which exceeds DefaultSpanLength (6400).
// Only DefaultSpanLength + 1 = 6401 headers should be verified.
expected := int64(params.DefaultSpanLength + 1)
if got := engine.headersVerified.Load(); got != expected {
t.Errorf("expected %d headers verified (capped), got %d", expected, got)
}
})
}

// TestVerifyPendingHeadersReorgMetrics tests that reorg metrics are recorded
// when verifyPendingHeaders rewinds the chain due to an invalid header.
func TestVerifyPendingHeadersReorgMetrics(t *testing.T) {
failingHeaders := map[uint64]bool{6: true}
engine := &mockFailingEngine{
Ethash: ethash.NewFaker(),
shouldFailHeader: failingHeaders,
allowInitialInsertion: true,
}

config := *params.TestChainConfig
config.Bor = &params.BorConfig{
RioBlock: big.NewInt(0),
}
genesis := &Genesis{
Config: &config,
BaseFee: big.NewInt(params.InitialBaseFee),
}

mockValidator := &mockChainValidator{
hasMilestone: true,
milestoneNumber: 3,
milestoneHash: common.HexToHash("0x123"),
}

_, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil)

cfg := DefaultConfig()
cfg.Checker = mockValidator
chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg)
if err != nil {
t.Fatalf("failed to create blockchain: %v", err)
}
defer chain.Stop()

if _, err := chain.InsertChain(blocks, false); err != nil {
t.Fatalf("failed to insert chain: %v", err)
}

engine.markInsertionComplete()

// Snapshot metrics before
reorgCountBefore := blockReorgMeter.Snapshot().Count()
reorgDropBefore := blockReorgDropMeter.Snapshot().Count()

chain.verifyPendingHeaders()

// Chain should have rewound to block 5
newHead := chain.CurrentBlock().Number.Uint64()
if newHead != 5 {
t.Errorf("expected head to rewind to 5, got %d", newHead)
}

// Reorg execute meter should have incremented by 1
reorgCountAfter := blockReorgMeter.Snapshot().Count()
if reorgCountAfter-reorgCountBefore != 1 {
t.Errorf("expected blockReorgMeter to increment by 1, got %d", reorgCountAfter-reorgCountBefore)
}

// Reorg drop meter should have incremented by 3 (dropped blocks 6, 7, 8)
reorgDropAfter := blockReorgDropMeter.Snapshot().Count()
if reorgDropAfter-reorgDropBefore != 3 {
t.Errorf("expected blockReorgDropMeter to increment by 3, got %d", reorgDropAfter-reorgDropBefore)
}
}

// TestEIP7702 deploys two delegation designations and calls them. It writes one
// value to storage which is verified after.
func TestEIP7702(t *testing.T) {
Expand Down
7 changes: 6 additions & 1 deletion params/config.go
Original file line number Diff line number Diff line change
Expand Up @@ -901,6 +901,11 @@ type BlockRangeOverrideValidatorSet struct {
Validators []common.Address `json:"validators"`
}

// DefaultSpanLength is the number of bor blocks in a span. This must match
// heimdall-v2's bor module Params.span_duration to ensure reorg protection
// boundaries stay consistent between the execution and consensus layers.
const DefaultSpanLength = 6400

// BorConfig is the consensus engine configs for Matic bor based sealing.
type BorConfig struct {
Period map[string]uint64 `json:"period"` // Number of seconds between blocks to enforce
Expand Down Expand Up @@ -1142,7 +1147,7 @@ func (c *ChainConfig) Description() string {
banner += fmt.Sprintf(" - Lisovo: #%-8v\n", c.Bor.LisovoBlock)
}
if c.Bor.LisovoProBlock != nil {
banner += fmt.Sprintf(" - Lisovo Pro: #%-8v\n", c.Bor.LisovoProBlock)
banner += fmt.Sprintf(" - Lisovo Pro: #%-8v\n", c.Bor.LisovoProBlock)
}
return banner
}
Expand Down
Loading