Skip to content

feat: add TimeNano field for nanosecond block timestamps - #2253

Draft
minhd-vu wants to merge 6 commits into
developfrom
feature/timenano-placeholder-fork
Draft

feat: add TimeNano field for nanosecond block timestamps#2253
minhd-vu wants to merge 6 commits into
developfrom
feature/timenano-placeholder-fork

Conversation

@minhd-vu

@minhd-vu minhd-vu commented Jun 3, 2026

Copy link
Copy Markdown
Contributor

Summary

Add nanosecond-precision block timestamp capture via new TimeNano field in BlockExtraData. This enables tracking precise block propagation timing beyond the second-precision header.Time field.

Changes:

  • Add TimeNano field to BlockExtraData (RLP optional)
  • Add GetTimeNano() helper on Header
  • Add Placeholder hardfork to gate TimeNano validation
  • Validate TimeNano presence in verifyHeader for Placeholder+ blocks
  • Restructure Prepare to calculate timestamps before BlockExtraData encoding

The Placeholder hardfork name is temporary and will be renamed.

Executed tests

Rollout notes

minhd-vu added 3 commits June 3, 2026 10:38
…stamps

Add nanosecond-precision block timestamp capture via new TimeNano field
in BlockExtraData. This enables tracking precise block propagation timing
beyond the second-precision header.Time field.

Changes:
- Add TimeNano field to BlockExtraData (RLP optional)
- Add GetTimeNano() helper on Header
- Add Placeholder hardfork to gate TimeNano validation
- Validate TimeNano presence in verifyHeader for Placeholder+ blocks
- Restructure Prepare to calculate timestamps before BlockExtraData encoding

The Placeholder hardfork name is temporary and will be renamed.
@codecov

codecov Bot commented Jun 3, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 85.71429% with 8 lines in your changes missing coverage. Please review.
✅ Project coverage is 55.23%. Comparing base (545b9d1) to head (567cfb7).
⚠️ Report is 1 commits behind head on develop.

Files with missing lines Patch % Lines
core/types/block.go 66.66% 3 Missing and 2 partials ⚠️
consensus/bor/bor.go 92.50% 2 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (85.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            @@
##           develop    #2253   +/-   ##
========================================
  Coverage    55.23%   55.23%           
========================================
  Files          912      912           
  Lines       165870   165883   +13     
========================================
+ Hits         91616    91631   +15     
+ Misses       68803    68790   -13     
- Partials      5451     5462   +11     
Files with missing lines Coverage Δ
tests/bor/helper.go 72.69% <100.00%> (ø)
consensus/bor/bor.go 86.60% <92.50%> (+0.24%) ⬆️
core/types/block.go 46.48% <66.66%> (+2.18%) ⬆️

... and 27 files with indirect coverage changes

Files with missing lines Coverage Δ
tests/bor/helper.go 72.69% <100.00%> (ø)
consensus/bor/bor.go 86.60% <92.50%> (+0.24%) ⬆️
core/types/block.go 46.48% <66.66%> (+2.18%) ⬆️

... and 27 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.

@sonarqubecloud

Copy link
Copy Markdown

Quality Gate Failed Quality Gate failed

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

See analysis details on SonarQube Cloud

@minhd-vu

Copy link
Copy Markdown
Contributor Author

This should be reviewed alongside: #2277

@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 15 hunks (11 deep, 2 normal, 2 light); no hunks skipped or failed. Three verified findings, all tied to the new TimeNano extra-data field.

The main one is a producer/consumer mismatch in core/types/block.go: BlockExtraData gained a fifth RLP-optional field, but the internal mirror struct blockExtraDataRawTxDeps used by the fast header-decode path still declares four, so pre-Austin headers carrying TimeNano fail to decode and GetValidatorBytes/GetBaseFeeParams return nil. This only fires on configs where Hampi is active at heights where Austin is not, so the author should either add the field or confirm the fork-ordering invariant.

The other two are lower severity: the new verifyHeader Hampi check decodes header.Extra a second time per header (and on the pre-Austin branch expands TxDependency), and the added tests do not exercise sub-second TimeNano precision through Prepare.

Not covered by verified findings, worth confirming during merge: whether a test asserts errMissingTimeNano for a post-Hampi header lacking TimeNano, and whether hampiChainConfig adds Hampi-specific ChainConfig gating or relies solely on BorConfig.HampiBlock.

Coverage

Reviewed 15/15 hunks.
Coverage levels: deep 11, normal 2, light 2, skip 0.

⚠️ Findings

🔵 Medium: blockExtraDataRawTxDeps mirror struct missing TimeNano; pre-Austin fast decode rejects TimeNano-bearing headers

File: core/types/block.go:188
Confidence: medium

BlockExtraData gained a fifth RLP field (TimeNano), but the internal mirror struct blockExtraDataRawTxDeps — which the fast header-decode path uses for the pre-Austin branch — still declares only four fields. Its own comment asserts wire-identity with BlockExtraData, and that invariant no longer holds.

Impact: On any chain config where Hampi is active at a height where Austin is not, Prepare emits a pre-Austin BlockExtraData RLP list with 5 elements. Header.decodeExtraFieldsFast takes the non-Austin branch, RLP decoding fails with rlp: input list has too many elements, and the error is only logged at Debug level before returning ok=false. GetValidatorBytes and GetBaseFeeParams then return nil, so header verification cannot obtain the validator set or base-fee params for blocks the node itself produced.

Encoder now emits the field on the pre-Austin shape:

return rlp.EncodeToBytes(&BlockExtraData{
	ValidatorBytes: validatorBytes,
	GasTarget: gasTarget,
	BaseFeeChangeDenominator: baseFeeChangeDenom,
	TimeNano: timeNano, // new 5th element in the pre-Austin shape
})

The mirror struct in core/types/block.go (lines 164-170) was not updated:

// blockExtraDataRawTxDeps mirrors BlockExtraData but keeps TxDependency as an
// rlp.RawValue ... The wire format is identical to BlockExtraData.
type blockExtraDataRawTxDeps struct {
	ValidatorBytes []byte
	TxDependency rlp.RawValue
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
}

And the decode path swallows the failure (core/types/block.go, ~line 557):

var blockExtraData blockExtraDataRawTxDeps
if err := rlp.DecodeBytes(raw, &blockExtraData); err != nil {
	log.Debug("error while decoding block extra data", "err", err)
	return nil, nil, nil, false
}

The TimeNano emission in consensus/bor/bor.go (~line 1147) is gated on Hampi, independently of the Austin gate inside EncodeBlockExtraData:

var timeNano *uint64
if c.config.IsHampi(header.Number) {
	value := uint64(header.GetActualTime().UnixNano())
	timeNano = &value
}
...
blockExtraDataBytes, err := types.EncodeBlockExtraData(c.chainConfig, header.Number, nil, gasTarget, baseFeeChangeDenom, timeNano)

This is a contract change to the pre-Austin wire shape; please confirm the intended fork ordering. On configs where Austin precedes or coincides with Hampi the post-Austin branch is taken and nothing breaks, but dev/test configs enabling Hampi at block 0 hit total loss of validator bytes and base-fee params.

Suggested fix: add the trailing optional field to the mirror struct (zero-risk, since it is rlp:"optional" and last):

type blockExtraDataRawTxDeps struct {
	ValidatorBytes []byte
	TxDependency rlp.RawValue
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
	TimeNano *uint64 `rlp:"optional"`
}

Suggested test: in core/types/block_test.go, build a pre-Austin chain config with Hampi active, encode a BlockExtraData with TxDependency, GasTarget, BaseFeeChangeDenominator and TimeNano all set via EncodeBlockExtraData, place the bytes into Header.Extra between vanity and seal, and assert decodeExtraFieldsFast returns ok=true with the expected validator bytes and base-fee params. This fails today with rlp: input list has too many elements.

⚪ Low: Hampi TimeNano check re-decodes header Extra on the hot verification path

File: consensus/bor/bor.go:498
Confidence: medium

The new Hampi check in verifyHeader RLP-decodes header.Extra a second time per header:

if c.config.IsHampi(header.Number) && header.GetTimeNano(c.chainConfig) == nil {
	return errMissingTimeNano
}

GetTimeNano calls DecodeBlockExtraData, and a few lines later verifyHeader already decodes the same bytes:

validatorBytes, gasTarget, bfcd := header.GetValidatorBytesAndBaseFeeParams(c.chainConfig)

Impact: verifyHeader runs for every imported and gossiped header, so this adds a duplicate decode on a consensus hot path. Worse, DecodeBlockExtraData's pre-Austin branch expands TxDependency into [][]uint64 — exactly the cost the existing fast path was written to avoid:

// blockExtraDataRawTxDeps mirrors BlockExtraData but keeps TxDependency as an
// rlp.RawValue, delaying its decode. Header verification only needs
// ValidatorBytes and the base-fee params, so this avoids expanding a
// potentially large TxDependency into [][]uint64.

Hampi is unscheduled on mainnet/amoy (HampiBlock: nil) but is 0 in at least one config, so chains where Hampi activates before Austin pay the full TxDependency expansion per verified header.

Suggested fix: reuse a single decode — e.g. extend GetValidatorBytesAndBaseFeeParams or the fast decoder to also return TimeNano — instead of calling GetTimeNano separately in verifyHeader.

Suggested test: benchmark verifyHeader with a Hampi-enabled, pre-Austin config and a header carrying a large TxDependency payload to quantify the decode cost.

⚪ Low: No test proves Prepare emits sub-second TimeNano precision

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

TestPrepare_HampiTimeNano only asserts self-consistency with GetActualTime on the non-Rio path, where the value is always a whole second:

// TimeNano should match header.GetActualTime().UnixNano()
// In the non-Rio path, ActualTime is not set so GetActualTime falls back to Time
expectedTimeNano := uint64(h.GetActualTime().UnixNano())
require.Equal(t, expectedTimeNano, *timeNano,
	"TimeNano should equal header.GetActualTime().UnixNano()")

Prepare derives the value from GetActualTime:

var timeNano *uint64
if c.config.IsHampi(header.Number) {
	value := uint64(header.GetActualTime().UnixNano())
	timeNano = &value
}

On the non-Rio branch Prepare sets only header.Time (whole seconds), and GetActualTime falls back to time.Unix(int64(h.Time), 0), so the produced TimeNano always has a zero nanosecond remainder. TestTimeNano_PreservesNanoseconds covers only RLP encode/decode of a hand-built value, not Prepare.

Impact: the PR's stated goal — nanosecond-precision capture through the block production path — has no test that would fail if Prepare produced second-granular timestamps.

Suggested fix: add a Prepare test on the Rio/blockTime path (where header.ActualTime carries sub-second components) asserting the nanosecond remainder is preserved.

Suggested test: configure a Bor with c.blockTime set and IsRio true, run Prepare, and assert uint64(h.ActualTime.UnixNano()) == *h.GetTimeNano(cfg) with a non-zero *timeNano % 1_000_000_000.

Stats

  • 🤖 Model: anthropic claude-opus-5 high
  • 🧞 Codegenie: v0.5.6 (a662388fde)
  • Elapsed time: 3m 51s
  • Git: 0xPolygon/bor from develop to feature/timenano-placeholder-fork (567cfb7906)
  • Posting: 3 inline
  • Review completeness: complete.
  • Usage: model calls 54, tokens 1346219, cost $5.0075.
  • Effective caps: tokens 8000000.
  • Local context pressure: 2 tool-budget rejections, 13 degraded tool results, 5 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 15 hunks (11 deep, 2 normal, 2 light); no hunks skipped or failed. Three verified findings, all tied to the new TimeNano extra-data field.

The main one is a producer/consumer mismatch in core/types/block.go: BlockExtraData gained a fifth RLP-optional field, but the internal mirror struct blockExtraDataRawTxDeps used by the fast header-decode path still declares four, so pre-Austin headers carrying TimeNano fail to decode and GetValidatorBytes/GetBaseFeeParams return nil. This only fires on configs where Hampi is active at heights where Austin is not, so the author should either add the field or confirm the fork-ordering invariant.

The other two are lower severity: the new verifyHeader Hampi check decodes header.Extra a second time per header (and on the pre-Austin branch expands TxDependency), and the added tests do not exercise sub-second TimeNano precision through Prepare.

Not covered by verified findings, worth confirming during merge: whether a test asserts errMissingTimeNano for a post-Hampi header lacking TimeNano, and whether hampiChainConfig adds Hampi-specific ChainConfig gating or relies solely on BorConfig.HampiBlock.

Reviewed 15/15 hunks.
Coverage levels: deep 11, normal 2, light 2, skip 0.

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

Comment thread core/types/block.go
ValidatorBytes: validatorBytes,
GasTarget: gasTarget,
BaseFeeChangeDenominator: baseFeeChangeDenom,
TimeNano: timeNano,

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

BlockExtraData gained a fifth RLP field (TimeNano), but the internal mirror struct blockExtraDataRawTxDeps — which the fast header-decode path uses for the pre-Austin branch — still declares only four fields. Its own comment asserts wire-identity with BlockExtraData, and that invariant no longer holds.

Impact: On any chain config where Hampi is active at a height where Austin is not, Prepare emits a pre-Austin BlockExtraData RLP list with 5 elements. Header.decodeExtraFieldsFast takes the non-Austin branch, RLP decoding fails with rlp: input list has too many elements, and the error is only logged at Debug level before returning ok=false. GetValidatorBytes and GetBaseFeeParams then return nil, so header verification cannot obtain the validator set or base-fee params for blocks the node itself produced.

Encoder now emits the field on the pre-Austin shape:

return rlp.EncodeToBytes(&BlockExtraData{
	ValidatorBytes: validatorBytes,
	GasTarget: gasTarget,
	BaseFeeChangeDenominator: baseFeeChangeDenom,
	TimeNano: timeNano, // new 5th element in the pre-Austin shape
})

The mirror struct in core/types/block.go (lines 164-170) was not updated:

// blockExtraDataRawTxDeps mirrors BlockExtraData but keeps TxDependency as an
// rlp.RawValue ... The wire format is identical to BlockExtraData.
type blockExtraDataRawTxDeps struct {
	ValidatorBytes []byte
	TxDependency rlp.RawValue
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
}

And the decode path swallows the failure (core/types/block.go, ~line 557):

var blockExtraData blockExtraDataRawTxDeps
if err := rlp.DecodeBytes(raw, &blockExtraData); err != nil {
	log.Debug("error while decoding block extra data", "err", err)
	return nil, nil, nil, false
}

The TimeNano emission in consensus/bor/bor.go (~line 1147) is gated on Hampi, independently of the Austin gate inside EncodeBlockExtraData:

var timeNano *uint64
if c.config.IsHampi(header.Number) {
	value := uint64(header.GetActualTime().UnixNano())
	timeNano = &value
}
...
blockExtraDataBytes, err := types.EncodeBlockExtraData(c.chainConfig, header.Number, nil, gasTarget, baseFeeChangeDenom, timeNano)

This is a contract change to the pre-Austin wire shape; please confirm the intended fork ordering. On configs where Austin precedes or coincides with Hampi the post-Austin branch is taken and nothing breaks, but dev/test configs enabling Hampi at block 0 hit total loss of validator bytes and base-fee params.

Suggested fix: add the trailing optional field to the mirror struct (zero-risk, since it is rlp:"optional" and last):

type blockExtraDataRawTxDeps struct {
	ValidatorBytes []byte
	TxDependency rlp.RawValue
	GasTarget *uint64 `rlp:"optional"`
	BaseFeeChangeDenominator *uint64 `rlp:"optional"`
	TimeNano *uint64 `rlp:"optional"`
}

Suggested test: in core/types/block_test.go, build a pre-Austin chain config with Hampi active, encode a BlockExtraData with TxDependency, GasTarget, BaseFeeChangeDenominator and TimeNano all set via EncodeBlockExtraData, place the bytes into Header.Extra between vanity and seal, and assert decodeExtraFieldsFast returns ok=true with the expected validator bytes and base-fee params. This fails today with rlp: input list has too many elements.

Comment thread consensus/bor/bor.go
return consensus.ErrFutureBlock
}
}
if c.config.IsHampi(header.Number) && header.GetTimeNano(c.chainConfig) == 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 new Hampi check in verifyHeader RLP-decodes header.Extra a second time per header:

if c.config.IsHampi(header.Number) && header.GetTimeNano(c.chainConfig) == nil {
	return errMissingTimeNano
}

GetTimeNano calls DecodeBlockExtraData, and a few lines later verifyHeader already decodes the same bytes:

validatorBytes, gasTarget, bfcd := header.GetValidatorBytesAndBaseFeeParams(c.chainConfig)

Impact: verifyHeader runs for every imported and gossiped header, so this adds a duplicate decode on a consensus hot path. Worse, DecodeBlockExtraData's pre-Austin branch expands TxDependency into [][]uint64 — exactly the cost the existing fast path was written to avoid:

// blockExtraDataRawTxDeps mirrors BlockExtraData but keeps TxDependency as an
// rlp.RawValue, delaying its decode. Header verification only needs
// ValidatorBytes and the base-fee params, so this avoids expanding a
// potentially large TxDependency into [][]uint64.

Hampi is unscheduled on mainnet/amoy (HampiBlock: nil) but is 0 in at least one config, so chains where Hampi activates before Austin pay the full TxDependency expansion per verified header.

Suggested fix: reuse a single decode — e.g. extend GetValidatorBytesAndBaseFeeParams or the fast decoder to also return TimeNano — instead of calling GetTimeNano separately in verifyHeader.

Suggested test: benchmark verifyHeader with a Hampi-enabled, pre-Austin config and a header carrying a large TxDependency payload to quantify the decode cost.

Comment thread consensus/bor/bor_test.go

// TimeNano should match header.GetActualTime().UnixNano()
// In the non-Rio path, ActualTime is not set so GetActualTime falls back to Time
expectedTimeNano := uint64(h.GetActualTime().UnixNano())

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

TestPrepare_HampiTimeNano only asserts self-consistency with GetActualTime on the non-Rio path, where the value is always a whole second:

// TimeNano should match header.GetActualTime().UnixNano()
// In the non-Rio path, ActualTime is not set so GetActualTime falls back to Time
expectedTimeNano := uint64(h.GetActualTime().UnixNano())
require.Equal(t, expectedTimeNano, *timeNano,
	"TimeNano should equal header.GetActualTime().UnixNano()")

Prepare derives the value from GetActualTime:

var timeNano *uint64
if c.config.IsHampi(header.Number) {
	value := uint64(header.GetActualTime().UnixNano())
	timeNano = &value
}

On the non-Rio branch Prepare sets only header.Time (whole seconds), and GetActualTime falls back to time.Unix(int64(h.Time), 0), so the produced TimeNano always has a zero nanosecond remainder. TestTimeNano_PreservesNanoseconds covers only RLP encode/decode of a hand-built value, not Prepare.

Impact: the PR's stated goal — nanosecond-precision capture through the block production path — has no test that would fail if Prepare produced second-granular timestamps.

Suggested fix: add a Prepare test on the Rio/blockTime path (where header.ActualTime carries sub-second components) asserting the nanosecond remainder is preserved.

Suggested test: configure a Bor with c.blockTime set and IsRio true, run Prepare, and assert uint64(h.ActualTime.UnixNano()) == *h.GetTimeNano(cfg) with a non-zero *timeNano % 1_000_000_000.

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