Skip to content

feat: embed commit-sealing timings in extra data - #2277

Draft
minhd-vu wants to merge 2 commits into
feature/timenano-placeholder-forkfrom
feature/seal-timings-extradata
Draft

feat: embed commit-sealing timings in extra data#2277
minhd-vu wants to merge 2 commits into
feature/timenano-placeholder-forkfrom
feature/seal-timings-extradata

Conversation

@minhd-vu

Copy link
Copy Markdown
Contributor

Summary

Embed the producer's commit-sealing timings -- the same elapsed and finalize values logged by "Commit new sealing work" -- into the block's BlockExtraData, mirroring the existing TimeNano feature so the values can be read directly from the chain instead of scraped from logs.

  • Add SealElapsedNano and SealFinalizeNano optional fields to BlockExtraData, plus GetSealTimings getter and SetSealTimings setter.
  • Capture and embed the timings in miner commit() after FinalizeAndAssemble, gated on the Placeholder fork, rebuilding the block before sealing. The existing log line is left unchanged.
  • Presence-validate both fields in verifyHeader (errMissingSealTimings); values are per-producer and non-deterministic so only presence is checked, as with TimeNano and the Giugliano fields.

Executed tests

<what was actually run beyond CI's standard unit / integration / e2e gates: kurtosis scenarios, chaos runs, manual checks against Amoy / mainnet RPCs, devnet upgrades, etc. Include output or pointers to where the run lives.>

Rollout notes

<consensus-affecting? requires coordinated upgrade? backwards-compatible? operator-facing change?>

…ra data

Embed the producer's commit-sealing timings -- the same elapsed and finalize
values logged by "Commit new sealing work" -- into the block's BlockExtraData,
mirroring the existing TimeNano feature so the values can be read directly from
the chain instead of scraped from logs.

- Add SealElapsedNano and SealFinalizeNano optional fields to BlockExtraData,
  plus GetSealTimings getter and SetSealTimings setter.
- Capture and embed the timings in miner commit() after FinalizeAndAssemble,
  gated on the Placeholder fork, rebuilding the block before sealing. The
  existing log line is left unchanged.
- Presence-validate both fields in verifyHeader (errMissingSealTimings); values
  are per-producer and non-deterministic so only presence is checked, as with
  TimeNano and the Giugliano fields.
@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

Failed conditions
8.4% Duplication on New Code (required ≤ 3%)

See analysis details on SonarQube Cloud

@codecov

codecov Bot commented Jun 17, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 60.71429% with 22 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.23%. Comparing base (567cfb7) to head (6ab41c8).

Files with missing lines Patch % Lines
core/types/block.go 60.46% 14 Missing and 3 partials ⚠️
miner/worker.go 0.00% 4 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (60.71%) is below the target coverage (90.00%). You can increase the patch coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@                          Coverage Diff                          @@
##           feature/timenano-placeholder-fork    #2277      +/-   ##
=====================================================================
- Coverage                              55.23%   55.23%   -0.01%     
=====================================================================
  Files                                    912      912              
  Lines                                 165883   165936      +53     
=====================================================================
+ Hits                                   91631    91659      +28     
- Misses                                 68790    68809      +19     
- Partials                                5462     5468       +6     
Files with missing lines Coverage Δ
consensus/bor/bor.go 86.64% <100.00%> (+0.04%) ⬆️
miner/worker.go 84.03% <0.00%> (-0.26%) ⬇️
core/types/block.go 47.80% <60.46%> (+1.31%) ⬆️

... and 22 files with indirect coverage changes

Files with missing lines Coverage Δ
consensus/bor/bor.go 86.64% <100.00%> (+0.04%) ⬆️
miner/worker.go 84.03% <0.00%> (-0.26%) ⬇️
core/types/block.go 47.80% <60.46%> (+1.31%) ⬆️

... and 22 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@pratikspatil024

Copy link
Copy Markdown
Member

codegenie review

@github-actions

github-actions Bot commented Sep 4, 2026

Copy link
Copy Markdown

🧞 Codegenie Review

Reviewed all 9 hunks (4 deep, 5 normal); no skipped or failed hunks. One critical correctness defect: the Hampi-gated block rebuild in miner/worker.go commit() reconstructs the block from env.txs, which does not include the bor state-sync transaction appended inside finalizeAndAssemble, producing a block whose TxHash and ReceiptHash are mutually inconsistent on sprint-start blocks. One contract-consistency question: the new post-Hampi errMissingSealTimings presence rule is enforced for every header, but the fields are written on only one of the miner's block-assembly paths — please confirm the other paths never seal bor blocks post-Hampi. Two low-severity test-coverage gaps: the finalizeNano == nil disjunct of the new consensus guard and the pre-Austin branch / nil-optional-field boundary of SetSealTimings are unpinned.

Coverage

Reviewed 9/9 hunks.
Coverage levels: deep 4, normal 5, light 0, skip 0.

⚠️ Findings

🔴 Critical: Block rebuild after SetSealTimings drops the bor state-sync transaction on sprint-start blocks

File: miner/worker.go:3171
Confidence: high

The Hampi-gated rebuild reconstructs the block from env.txs, but env.txs does not contain the bor state-sync transaction that finalizeAndAssemble appends to its own local body. The receipts, however, do include the state-sync receipt because they are returned and assigned to env.receipts.

if w.chainConfig.Bor != nil && w.chainConfig.Bor.IsHampi(env.header.Number) {
	if err := env.header.SetSealTimings(w.chainConfig, uint64(time.Since(start).Nanoseconds()), uint64(finalizeDuration.Nanoseconds())); err != nil {
		return err
	}
	block = types.NewBlock(env.header, &types.Body{Transactions: env.txs}, env.receipts, trie.NewStackTrie(nil))
}

The transaction list handed to FinalizeAndAssemble is a fresh literal, so the callee's append is never visible to the caller:

block, env.receipts, commitTime, err = w.engine.FinalizeAndAssemble(w.chain, env.header, env.state, &types.Body{
	Transactions: env.txs,
}, env.receipts)

Inside consensus/bor/bor.go finalizeAndAssemble, the state-sync transaction is appended to the local body and its receipt is folded into the returned receipts:

if len(stateSyncData) > 0 && c.config != nil && c.config.IsMadhugiri(big.NewInt(int64(headerNumber))) {
	stateSyncTx := types.NewTx(&types.StateSyncTx{StateSyncData: stateSyncData})
	body.Transactions = append(body.Transactions, stateSyncTx)
	receipts = insertStateSyncTransactionAndCalculateReceipt(stateSyncTx, header, body, state, receipts)
}
block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))

params/config.go contains configs where HampiBlock and MadhugiriBlock are both active (e.g. both big.NewInt(0)), so the Hampi-gated rebuild runs on Madhugiri sprint-start blocks.

Impact: On a sprint-start block with pending state-sync data, the sealed block omits the StateSyncTx from its body while its ReceiptHash is derived from receipts that include the state-sync receipt. The TxHash/ReceiptHash pair is unreproducible by any verifier and the state-sync transaction is lost from the canonical body, so every such block is rejected by peers and the producer misses its slots. This is not covered by the PR intent, which describes the rebuild only as refreshing Extra ("the block built above carries the pre-timing Extra and must be rebuilt before it is handed off for sealing").

Suggested fix: Rebuild from the body actually returned by FinalizeAndAssemble rather than from env.txs:

block = types.NewBlock(env.header, block.Body(), env.receipts, trie.NewStackTrie(nil))

Alternatively, have FinalizeAndAssemble return the final transaction list and reassign env.txs, or set the timings on the header before assembly / use a header-only re-hash path so no rebuild is needed.

Suggested test: With a chain config enabling both Hampi and Madhugiri, produce a sprint-start block with pending state-sync events and assert the committed block's body contains the StateSyncTx, that len(block.Transactions()) matches the receipts count, and that bor verification of the sealed block succeeds.

⚪ Low: TestSetSealTimings covers only the post-Austin branch with all optional fields populated

File: core/types/block_test.go:890
Confidence: medium

TestSetSealTimings fixes a single configuration — Austin active, all three earlier optional fields non-nil — leaving two live boundaries of SetSealTimings unverified:

chainConfig := &params.ChainConfig{
	ChainID: big.NewInt(137),
	CancunBlock: cancunBlock,
	Bor: &params.BorConfig{
 AustinBlock: big.NewInt(100),
 HampiBlock: big.NewInt(200),
	},
}
encoded, err := EncodeBlockExtraData(chainConfig, big.NewInt(200), nil, &gasTarget, &bfcd, &timeNano)
header := &Header{Number: big.NewInt(200), Extra: extra}
if err := header.SetSealTimings(chainConfig, elapsedNano, finalizeNano); err != nil {
  1. Pre-Austin branch is never executed. With AustinBlock: 100 and header number 200, only the BlockExtraDataPostAustin branch runs. The else branch decodes into BlockExtraData, where TxDependency sits ahead of the optional run and must survive the in-place Extra rewrite:
} else {
	var blockExtraData BlockExtraData
	if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil {
 return fmt.Errorf("decode block extra data: %w", err)
	}
	blockExtraData.SealElapsedNano = &elapsedNano
	blockExtraData.SealFinalizeNano = &finalizeNano
	blockExtraDataBytes, err = rlp.EncodeToBytes(&blockExtraData)
}

The sole production caller (miner/worker.go:3168) is IsHampi-gated but does not itself guarantee Austin, so this branch ships untested.

  1. The nil-vs-zero boundary is unexercised. rlp:"optional" only trims a trailing run of zero values:
type BlockExtraDataPostAustin struct {
	ValidatorBytes []byte
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
	TimeNano *uint64 `rlp:"optional"`
	SealElapsedNano *uint64 `rlp:"optional"`
	SealFinalizeNano *uint64 `rlp:"optional"`
}

Setting the two new trailing fields when an earlier optional pointer is nil forces that earlier field onto the wire as an empty value, which decodes back as a non-nil zero pointer — GetTimeNano/GasTarget go from nil to 0. The fixture always passes non-nil values, so this is never observed.

Impact: SetSealTimings rewrites chain-visible header Extra in place, and consensus/bor/bor.go:503 presence-validates TimeNano and both seal timings post-Hampi. A future edit to the untested branch or to struct field order would not be caught.

Suggested fix: Add subtests TestSetSealTimings/pre_austin_preserves_tx_dependency (pre-Austin config with non-empty TxDependency, asserting TxDependency, ValidatorBytes, vanity and seal all survive) and TestSetSealTimings/nil_time_nano_promotion (extra encoded with timeNano == nil, asserting the resulting GetTimeNano/DecodeBlockExtraData semantics explicitly — either still nil, or documented as 0).

⚪ Low: New Hampi seal-timings tests never exercise the finalize-nil disjunct (elapsed set, finalize absent)

File: consensus/bor/bor_test.go:6121
Confidence: medium

The new consensus guard is a two-term disjunction, but no test makes the second term the deciding condition:

elapsedNano, finalizeNano := header.GetSealTimings(c.chainConfig)
if elapsedNano == nil || finalizeNano == nil {
	return errMissingSealTimings
}

TestVerifyHeader_HampiTimeNanoPresent sets both fields; TestVerifyHeader_HampiMissingSealTimings sets neither, so elapsedNano == nil short-circuits first:

extra := buildBlockExtraBytes(&types.BlockExtraData{
	GasTarget: &gasTarget,
	BaseFeeChangeDenominator: &bfcd,
	TimeNano: &timeNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))
chain := newRawDBChain(s.db, s.cfg, h, nil, nil)
require.ErrorIs(t, s.b.verifyHeader(chain, h, nil), errMissingSealTimings)

Both fields are RLP optional-tail entries:

SealElapsedNano *uint64 `rlp:"optional"`
SealFinalizeNano *uint64 `rlp:"optional"`

so a peer-suppliable header that encodes SealElapsedNano and omits the trailing SealFinalizeNano is valid RLP and decodes to elapsed != nil, finalize == nil. GetSealTimings forwards the decoded pointers verbatim without normalization. Tree-wide searches for SealElapsedNano/SealFinalizeNano/errMissingSealTimings found only the both-set and neither-set cases (consensus/bor/bor_test.go:6086-6103, :6106-6122, core/types/block_test.go:901-904).

Impact: Dropping the finalizeNano == nil term (or flipping || to &&) would still pass the entire suite while loosening a fork-gated header validation rule — a partially populated header would be accepted by a patched node and rejected by others. The shipped guard is correct today; this is a test-coverage gap on a chain-split-class check.

Suggested fix: Add a third case (ideally a table-driven subtest sharing newHampiVerifySetup) that builds extra with GasTarget, BaseFeeChangeDenominator, TimeNano and SealElapsedNano only, omitting SealFinalizeNano:

extra := buildBlockExtraBytes(&types.BlockExtraData{
	GasTarget: &gasTarget,
	BaseFeeChangeDenominator: &bfcd,
	TimeNano: &timeNano,
	SealElapsedNano: &elapsedNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))
require.ErrorIs(t, s.b.verifyHeader(newRawDBChain(s.db, s.cfg, h, nil, nil), h, nil), errMissingSealTimings)

⚪ Low: Hampi seal-timings presence rule is enforced globally but written on only one block-assembly path

File: consensus/bor/bor.go:503
Confidence: low

Post-Hampi, verifyHeader rejects any header whose decoded BlockExtraData lacks either seal-timing field:

if c.config.IsHampi(header.Number) {
	if header.GetTimeNano(c.chainConfig) == nil {
 return errMissingTimeNano
	}
	elapsedNano, finalizeNano := header.GetSealTimings(c.chainConfig)
	if elapsedNano == nil || finalizeNano == nil {
 return errMissingSealTimings
	}
}

This rule applies to every header, but the fields are written on only one assembly path. SetSealTimings has exactly one non-test caller, worker.commit() at miner/worker.go:3168. The second assembly site, generateWork() at miner/worker.go:2387 (reached from worker.go:1190 via getWorkCh and from miner/payload_building.go:227/:262), assembles a header with no seal-timings write:

var block *types.Block
block, work.receipts, _, err = w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, &body, work.receipts)
if err != nil {
	return &newPayloadResult{err: err}
}

miner/pipeline.go similarly constructs blocks at lines 192, 439 and 793 with no mention of SetSealTimings.

Impact: The header contract changes so that writer and validator coverage must match. If a block produced through generateWork/getSealingBlock or the pipelined commit path is ever sealed and broadcast on a Hampi-enabled bor network, every verifying peer rejects it with errMissingSealTimings. Whether those paths can seal a bor block post-Hampi was not verified; please confirm the intended producer paths for this contract.

Suggested fix: Write the seal timings on a path shared by all bor block assembly (e.g. inside bor's FinalizeAndAssemble, alongside where TimeNano is set), or replicate the IsHampi-gated SetSealTimings plus block rebuild in generateWork and the pipelined commit path. If those paths are payload/dev-only and never seal bor blocks, document that explicitly next to the new check.

Suggested test: Drive the getSealingBlock/generateWork path with a Hampi-enabled bor chain config and assert bor.verifyHeader does not return errMissingSealTimings for the resulting header; add the same assertion for the pipelined commit path.

Stats

  • 🤖 Model: anthropic claude-opus-5 high
  • 🧞 Codegenie: v0.5.6 (a662388fde)
  • Elapsed time: 4m 23s
  • Git: 0xPolygon/bor from feature/timenano-placeholder-fork to feature/seal-timings-extradata (6ab41c87c1)
  • Posting: 3 inline · 1 demoted to review body
  • Review completeness: complete.
  • Usage: model calls 56, tokens 1207413, cost $5.4281.
  • Effective caps: tokens 8000000.
  • Local context pressure: 3 tool-budget rejections, 12 degraded tool results, 2 degraded hunks.

View Workflow Job

@github-actions github-actions Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🧞 Codegenie Review

Reviewed all 9 hunks (4 deep, 5 normal); no skipped or failed hunks. One critical correctness defect: the Hampi-gated block rebuild in miner/worker.go commit() reconstructs the block from env.txs, which does not include the bor state-sync transaction appended inside finalizeAndAssemble, producing a block whose TxHash and ReceiptHash are mutually inconsistent on sprint-start blocks. One contract-consistency question: the new post-Hampi errMissingSealTimings presence rule is enforced for every header, but the fields are written on only one of the miner's block-assembly paths — please confirm the other paths never seal bor blocks post-Hampi. Two low-severity test-coverage gaps: the finalizeNano == nil disjunct of the new consensus guard and the pre-Austin branch / nil-optional-field boundary of SetSealTimings are unpinned.

Reviewed 9/9 hunks.
Coverage levels: deep 4, normal 5, light 0, skip 0.
Inline findings included in the review body:

  • ⚪ Low: Hampi seal-timings presence rule is enforced globally but written on only one block-assembly path (consensus/bor/bor.go:503)
    Post-Hampi, verifyHeader rejects any header whose decoded BlockExtraData lacks either seal-timing field:

    if c.config.IsHampi(header.Number) {
    	if header.GetTimeNano(c.chainConfig) == nil {
     return errMissingTimeNano
    	}
    	elapsedNano, finalizeNano := header.GetSealTimings(c.chainConfig)
    	if elapsedNano == nil || finalizeNano == nil {
     return errMissingSealTimings
    	}
    }

    This rule applies to every header, but the fields are written on only one assembly path. SetSealTimings has exactly one non-test caller, worker.commit() at miner/worker.go:3168. The second assembly site, generateWork() at miner/worker.go:2387 (reached from worker.go:1190 via getWorkCh and from miner/payload_building.go:227/:262), assembles a header with no seal-timings write:

    var block *types.Block
    block, work.receipts, _, err = w.engine.FinalizeAndAssemble(w.chain, work.header, work.state, &body, work.receipts)
    if err != nil {
    	return &newPayloadResult{err: err}
    }

    miner/pipeline.go similarly constructs blocks at lines 192, 439 and 793 with no mention of SetSealTimings.

    Impact: The header contract changes so that writer and validator coverage must match. If a block produced through generateWork/getSealingBlock or the pipelined commit path is ever sealed and broadcast on a Hampi-enabled bor network, every verifying peer rejects it with errMissingSealTimings. Whether those paths can seal a bor block post-Hampi was not verified; please confirm the intended producer paths for this contract.

    Suggested fix: Write the seal timings on a path shared by all bor block assembly (e.g. inside bor's FinalizeAndAssemble, alongside where TimeNano is set), or replicate the IsHampi-gated SetSealTimings plus block rebuild in generateWork and the pipelined commit path. If those paths are payload/dev-only and never seal bor blocks, document that explicitly next to the new check.

    Suggested test: Drive the getSealingBlock/generateWork path with a Hampi-enabled bor chain config and assert bor.verifyHeader does not return errMissingSealTimings for the resulting header; add the same assertion for the pipelined commit path.

— codegenie v0.5.6 (a662388fde) · View Workflow Job

Comment thread miner/worker.go
Comment on lines +3167 to +3171
if w.chainConfig.Bor != nil && w.chainConfig.Bor.IsHampi(env.header.Number) {
if err := env.header.SetSealTimings(w.chainConfig, uint64(time.Since(start).Nanoseconds()), uint64(finalizeDuration.Nanoseconds())); err != nil {
return err
}
block = types.NewBlock(env.header, &types.Body{Transactions: env.txs}, env.receipts, trie.NewStackTrie(nil))

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The Hampi-gated rebuild reconstructs the block from env.txs, but env.txs does not contain the bor state-sync transaction that finalizeAndAssemble appends to its own local body. The receipts, however, do include the state-sync receipt because they are returned and assigned to env.receipts.

if w.chainConfig.Bor != nil && w.chainConfig.Bor.IsHampi(env.header.Number) {
	if err := env.header.SetSealTimings(w.chainConfig, uint64(time.Since(start).Nanoseconds()), uint64(finalizeDuration.Nanoseconds())); err != nil {
		return err
	}
	block = types.NewBlock(env.header, &types.Body{Transactions: env.txs}, env.receipts, trie.NewStackTrie(nil))
}

The transaction list handed to FinalizeAndAssemble is a fresh literal, so the callee's append is never visible to the caller:

block, env.receipts, commitTime, err = w.engine.FinalizeAndAssemble(w.chain, env.header, env.state, &types.Body{
	Transactions: env.txs,
}, env.receipts)

Inside consensus/bor/bor.go finalizeAndAssemble, the state-sync transaction is appended to the local body and its receipt is folded into the returned receipts:

if len(stateSyncData) > 0 && c.config != nil && c.config.IsMadhugiri(big.NewInt(int64(headerNumber))) {
	stateSyncTx := types.NewTx(&types.StateSyncTx{StateSyncData: stateSyncData})
	body.Transactions = append(body.Transactions, stateSyncTx)
	receipts = insertStateSyncTransactionAndCalculateReceipt(stateSyncTx, header, body, state, receipts)
}
block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil))

params/config.go contains configs where HampiBlock and MadhugiriBlock are both active (e.g. both big.NewInt(0)), so the Hampi-gated rebuild runs on Madhugiri sprint-start blocks.

Impact: On a sprint-start block with pending state-sync data, the sealed block omits the StateSyncTx from its body while its ReceiptHash is derived from receipts that include the state-sync receipt. The TxHash/ReceiptHash pair is unreproducible by any verifier and the state-sync transaction is lost from the canonical body, so every such block is rejected by peers and the producer misses its slots. This is not covered by the PR intent, which describes the rebuild only as refreshing Extra ("the block built above carries the pre-timing Extra and must be rebuilt before it is handed off for sealing").

Suggested fix: Rebuild from the body actually returned by FinalizeAndAssemble rather than from env.txs:

block = types.NewBlock(env.header, block.Body(), env.receipts, trie.NewStackTrie(nil))

Alternatively, have FinalizeAndAssemble return the final transaction list and reassign env.txs, or set the timings on the header before assembly / use a header-only re-hash path so no rebuild is needed.

Suggested test: With a chain config enabling both Hampi and Madhugiri, produce a sprint-start block with pending state-sync events and assert the committed block's body contains the StateSyncTx, that len(block.Transactions()) matches the receipts count, and that bor verification of the sealed block succeeds.

Comment thread core/types/block_test.go
Comment on lines +868 to +890
chainConfig := &params.ChainConfig{
ChainID: big.NewInt(137),
CancunBlock: cancunBlock,
Bor: &params.BorConfig{
AustinBlock: big.NewInt(100),
HampiBlock: big.NewInt(200),
},
}

// Distinctive vanity and seal bytes to confirm they survive the rewrite.
vanity := bytes.Repeat([]byte{0xab}, ExtraVanityLength)
seal := bytes.Repeat([]byte{0xcd}, ExtraSealLength)

gasTarget := uint64(15000000)
bfcd := uint64(64)
timeNano := uint64(1700000000_000_000_000) + 123456789
encoded, err := EncodeBlockExtraData(chainConfig, big.NewInt(200), nil, &gasTarget, &bfcd, &timeNano)
if err != nil {
t.Fatalf("failed to encode BlockExtraData: %v", err)
}

extra := append(append(append([]byte{}, vanity...), encoded...), seal...)
header := &Header{Number: big.NewInt(200), Extra: extra}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TestSetSealTimings fixes a single configuration — Austin active, all three earlier optional fields non-nil — leaving two live boundaries of SetSealTimings unverified:

chainConfig := &params.ChainConfig{
	ChainID: big.NewInt(137),
	CancunBlock: cancunBlock,
	Bor: &params.BorConfig{
 AustinBlock: big.NewInt(100),
 HampiBlock: big.NewInt(200),
	},
}
encoded, err := EncodeBlockExtraData(chainConfig, big.NewInt(200), nil, &gasTarget, &bfcd, &timeNano)
header := &Header{Number: big.NewInt(200), Extra: extra}
if err := header.SetSealTimings(chainConfig, elapsedNano, finalizeNano); err != nil {
  1. Pre-Austin branch is never executed. With AustinBlock: 100 and header number 200, only the BlockExtraDataPostAustin branch runs. The else branch decodes into BlockExtraData, where TxDependency sits ahead of the optional run and must survive the in-place Extra rewrite:
} else {
	var blockExtraData BlockExtraData
	if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil {
 return fmt.Errorf("decode block extra data: %w", err)
	}
	blockExtraData.SealElapsedNano = &elapsedNano
	blockExtraData.SealFinalizeNano = &finalizeNano
	blockExtraDataBytes, err = rlp.EncodeToBytes(&blockExtraData)
}

The sole production caller (miner/worker.go:3168) is IsHampi-gated but does not itself guarantee Austin, so this branch ships untested.

  1. The nil-vs-zero boundary is unexercised. rlp:"optional" only trims a trailing run of zero values:
type BlockExtraDataPostAustin struct {
	ValidatorBytes []byte
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
	TimeNano *uint64 `rlp:"optional"`
	SealElapsedNano *uint64 `rlp:"optional"`
	SealFinalizeNano *uint64 `rlp:"optional"`
}

Setting the two new trailing fields when an earlier optional pointer is nil forces that earlier field onto the wire as an empty value, which decodes back as a non-nil zero pointer — GetTimeNano/GasTarget go from nil to 0. The fixture always passes non-nil values, so this is never observed.

Impact: SetSealTimings rewrites chain-visible header Extra in place, and consensus/bor/bor.go:503 presence-validates TimeNano and both seal timings post-Hampi. A future edit to the untested branch or to struct field order would not be caught.

Suggested fix: Add subtests TestSetSealTimings/pre_austin_preserves_tx_dependency (pre-Austin config with non-empty TxDependency, asserting TxDependency, ValidatorBytes, vanity and seal all survive) and TestSetSealTimings/nil_time_nano_promotion (extra encoded with timeNano == nil, asserting the resulting GetTimeNano/DecodeBlockExtraData semantics explicitly — either still nil, or documented as 0).

Comment thread consensus/bor/bor_test.go
Comment on lines +6106 to +6121
func TestVerifyHeader_HampiMissingSealTimings(t *testing.T) {
t.Parallel()
s := newHampiVerifySetup(t, true)

gasTarget := uint64(15_000_000)
bfcd := uint64(64)
timeNano := uint64(1_700_000_000_000_000_000)
extra := buildBlockExtraBytes(&types.BlockExtraData{
GasTarget: &gasTarget,
BaseFeeChangeDenominator: &bfcd,
TimeNano: &timeNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))

chain := newRawDBChain(s.db, s.cfg, h, nil, nil)
require.ErrorIs(t, s.b.verifyHeader(chain, h, nil), errMissingSealTimings)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

The new consensus guard is a two-term disjunction, but no test makes the second term the deciding condition:

elapsedNano, finalizeNano := header.GetSealTimings(c.chainConfig)
if elapsedNano == nil || finalizeNano == nil {
	return errMissingSealTimings
}

TestVerifyHeader_HampiTimeNanoPresent sets both fields; TestVerifyHeader_HampiMissingSealTimings sets neither, so elapsedNano == nil short-circuits first:

extra := buildBlockExtraBytes(&types.BlockExtraData{
	GasTarget: &gasTarget,
	BaseFeeChangeDenominator: &bfcd,
	TimeNano: &timeNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))
chain := newRawDBChain(s.db, s.cfg, h, nil, nil)
require.ErrorIs(t, s.b.verifyHeader(chain, h, nil), errMissingSealTimings)

Both fields are RLP optional-tail entries:

SealElapsedNano *uint64 `rlp:"optional"`
SealFinalizeNano *uint64 `rlp:"optional"`

so a peer-suppliable header that encodes SealElapsedNano and omits the trailing SealFinalizeNano is valid RLP and decodes to elapsed != nil, finalize == nil. GetSealTimings forwards the decoded pointers verbatim without normalization. Tree-wide searches for SealElapsedNano/SealFinalizeNano/errMissingSealTimings found only the both-set and neither-set cases (consensus/bor/bor_test.go:6086-6103, :6106-6122, core/types/block_test.go:901-904).

Impact: Dropping the finalizeNano == nil term (or flipping || to &&) would still pass the entire suite while loosening a fork-gated header validation rule — a partially populated header would be accepted by a patched node and rejected by others. The shipped guard is correct today; this is a test-coverage gap on a chain-split-class check.

Suggested fix: Add a third case (ideally a table-driven subtest sharing newHampiVerifySetup) that builds extra with GasTarget, BaseFeeChangeDenominator, TimeNano and SealElapsedNano only, omitting SealFinalizeNano:

extra := buildBlockExtraBytes(&types.BlockExtraData{
	GasTarget: &gasTarget,
	BaseFeeChangeDenominator: &bfcd,
	TimeNano: &timeNano,
	SealElapsedNano: &elapsedNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))
require.ErrorIs(t, s.b.verifyHeader(newRawDBChain(s.db, s.cfg, h, nil, nil), h, nil), errMissingSealTimings)

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants