Skip to content
Draft
Show file tree
Hide file tree
Changes from all 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
12 changes: 12 additions & 0 deletions consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,10 @@ var (
// the nanosecond-precision timestamp in its extra data.
errMissingTimeNano = errors.New("missing time nano in extra data")

// errMissingSealTimings is returned if a post-Placeholder block is missing
// the producer's commit-sealing timings (elapsed/finalize) in its extra data.
errMissingSealTimings = errors.New("missing seal timings in extra data")

// errInvalidMixDigest is returned if a block's mix digest is non-zero.
errInvalidMixDigest = errors.New("non-zero mix digest")

Expand Down Expand Up @@ -514,6 +518,14 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head
if timeNano == nil {
return errMissingTimeNano
}

// Verify that the producer's commit-sealing timings are present. As with
// TimeNano and the Giugliano fields, we only check presence, not the values,
// since they are per-producer and not consensus-deterministic.
elapsedNano, finalizeNano := header.GetSealTimings(c.chainConfig)
if elapsedNano == nil || finalizeNano == nil {
return errMissingSealTimings
}
}

// Ensure that the mix digest is zero as we don't have fork protection currently
Expand Down
47 changes: 47 additions & 0 deletions consensus/bor/bor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5862,6 +5862,53 @@ func TestVerifyHeader_PlaceholderTimeNanoPresent(t *testing.T) {
}
}

func TestVerifyHeader_PlaceholderMissingSealTimings(t *testing.T) {
t.Parallel()
s := newPlaceholderVerifySetup(t, true)

// TimeNano present but seal timings omitted.
gasTarget := uint64(15_000_000)
bfcd := uint64(64)
timeNano := uint64(1700000000_000_000_000)
extra := buildBlockExtraBytes(&types.BlockExtraData{
GasTarget: &gasTarget,
BaseFeeChangeDenominator: &bfcd,
TimeNano: &timeNano,
// SealElapsedNano / SealFinalizeNano intentionally omitted
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))

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

func TestVerifyHeader_PlaceholderSealTimingsPresent(t *testing.T) {
t.Parallel()
s := newPlaceholderVerifySetup(t, true)

gasTarget := uint64(15_000_000)
bfcd := uint64(64)
timeNano := uint64(1700000000_000_000_000)
elapsedNano := uint64(2_648_000)
finalizeNano := uint64(1_500_000)
extra := buildBlockExtraBytes(&types.BlockExtraData{
GasTarget: &gasTarget,
BaseFeeChangeDenominator: &bfcd,
TimeNano: &timeNano,
SealElapsedNano: &elapsedNano,
SealFinalizeNano: &finalizeNano,
})
h := s.makeSignedChild(t, extra, big.NewInt(params.InitialBaseFee))

chain := newRawDBChain(s.db, s.cfg, h, nil, nil)
err := s.b.verifyHeader(chain, h, nil)
// Should not fail for missing seal timings (other unrelated checks may still error).
if err != nil {
require.NotErrorIs(t, err, errMissingSealTimings)
}
}

// placeholderVerifySetup holds shared state for verifyHeader Placeholder tests.
type placeholderVerifySetup struct {
b *Bor
Expand Down
59 changes: 59 additions & 0 deletions core/types/block.go
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,15 @@ type BlockExtraData struct {
BaseFeeChangeDenominator *uint64 `rlp:"optional"`
// TimeNano is the nanosecond-precision Unix timestamp when the block was prepared (post-Chicago)
TimeNano *uint64 `rlp:"optional"`
// SealElapsedNano is the producer's total commit-sealing time in nanoseconds
// (transaction processing + FinalizeAndAssemble). It mirrors the "elapsed" field
// of the "Commit new sealing work" log. The value is per-producer and not
// consensus-deterministic, so only its presence is validated (post-Placeholder).
SealElapsedNano *uint64 `rlp:"optional"`
// SealFinalizeNano is the FinalizeAndAssemble duration in nanoseconds. It mirrors
// the "finalize" field of the "Commit new sealing work" log. Like SealElapsedNano,
// only its presence is validated (post-Placeholder).
SealFinalizeNano *uint64 `rlp:"optional"`
}

// field type overrides for gencodec
Expand Down Expand Up @@ -587,6 +596,56 @@ func (h *Header) GetTimeNano(chainConfig *params.ChainConfig) *uint64 {
return blockExtraData.TimeNano
}

// GetSealTimings extracts the producer's commit-sealing timings (elapsed and
// finalize durations, in nanoseconds) from the header's Extra field. Either
// value is nil for pre-Cancun blocks, on decode error, or if the producer did
// not set it. Seal timings are presence-validated starting from the Placeholder
// fork. If you need multiple fields from BlockExtraData, prefer DecodeBlockExtraData
// to avoid redundant RLP decodes.
func (h *Header) GetSealTimings(chainConfig *params.ChainConfig) (elapsedNano *uint64, finalizeNano *uint64) {
blockExtraData := h.DecodeBlockExtraData(chainConfig)
if blockExtraData == nil {
return nil, nil
}

return blockExtraData.SealElapsedNano, blockExtraData.SealFinalizeNano
}

// SetSealTimings writes the producer's commit-sealing timings into the header's
// already-encoded BlockExtraData. The header's Extra must be in the post-Cancun
// [vanity | RLP(BlockExtraData) | seal] layout; it is decoded, the timing fields
// are set, and Extra is re-encoded in place. This is a producer-only path — the
// values are not consensus-validated, only their presence.
func (h *Header) SetSealTimings(elapsedNano, finalizeNano uint64) error {
if len(h.Extra) < ExtraVanityLength+ExtraSealLength {
return fmt.Errorf("invalid extra data length %d, cannot set seal timings", len(h.Extra))
}

vanity := h.Extra[:ExtraVanityLength]
seal := h.Extra[len(h.Extra)-ExtraSealLength:]

var blockExtraData BlockExtraData
if err := rlp.DecodeBytes(h.Extra[ExtraVanityLength:len(h.Extra)-ExtraSealLength], &blockExtraData); err != nil {
return fmt.Errorf("failed to decode block extra data: %w", err)
}

blockExtraData.SealElapsedNano = &elapsedNano
blockExtraData.SealFinalizeNano = &finalizeNano

blockExtraDataBytes, err := rlp.EncodeToBytes(&blockExtraData)
if err != nil {
return fmt.Errorf("failed to encode block extra data: %w", err)
}

extra := make([]byte, 0, ExtraVanityLength+len(blockExtraDataBytes)+ExtraSealLength)
extra = append(extra, vanity...)
extra = append(extra, blockExtraDataBytes...)
extra = append(extra, seal...)
h.Extra = extra

return nil
}

func (b *Block) BaseFee() *big.Int {
if b.header.BaseFee == nil {
return nil
Expand Down
70 changes: 70 additions & 0 deletions core/types/block_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -863,6 +863,76 @@ func TestGetBaseFeeParams(t *testing.T) {
}
}

func TestSetSealTimings(t *testing.T) {
t.Parallel()

cancunBlock := big.NewInt(100)
chainConfig := &params.ChainConfig{
ChainID: big.NewInt(137),
CancunBlock: cancunBlock,
}

// 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 := rlp.EncodeToBytes(&BlockExtraData{
GasTarget: &gasTarget,
BaseFeeChangeDenominator: &bfcd,
TimeNano: &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}

// Use values with non-zero nanosecond components to prove full precision.
elapsedNano := uint64(2_648_123) // 2.648123ms
finalizeNano := uint64(1_500_456)
if err := header.SetSealTimings(elapsedNano, finalizeNano); err != nil {
t.Fatalf("SetSealTimings failed: %v", err)
}

gotElapsed, gotFinalize := header.GetSealTimings(chainConfig)
if gotElapsed == nil || *gotElapsed != elapsedNano {
t.Errorf("SealElapsedNano mismatch: got %v, want %d", gotElapsed, elapsedNano)
}
if gotFinalize == nil || *gotFinalize != finalizeNano {
t.Errorf("SealFinalizeNano mismatch: got %v, want %d", gotFinalize, finalizeNano)
}

// Vanity and seal must be untouched by the rewrite.
if !bytes.Equal(header.Extra[:ExtraVanityLength], vanity) {
t.Errorf("vanity bytes modified by SetSealTimings")
}
if !bytes.Equal(header.Extra[len(header.Extra)-ExtraSealLength:], seal) {
t.Errorf("seal bytes modified by SetSealTimings")
}

// Pre-existing optional fields must be preserved.
bed := header.DecodeBlockExtraData(chainConfig)
if bed == nil {
t.Fatalf("DecodeBlockExtraData returned nil after SetSealTimings")
}
if bed.TimeNano == nil || *bed.TimeNano != timeNano {
t.Errorf("TimeNano not preserved: got %v, want %d", bed.TimeNano, timeNano)
}
if bed.GasTarget == nil || *bed.GasTarget != gasTarget {
t.Errorf("GasTarget not preserved: got %v, want %d", bed.GasTarget, gasTarget)
}

// Short extra data must error rather than panic.
short := &Header{Number: big.NewInt(200), Extra: []byte{0x01, 0x02}}
if err := short.SetSealTimings(1, 2); err == nil {
t.Errorf("expected error for short extra data, got nil")
}
}

func TestDecodeBlockExtraData(t *testing.T) {
t.Parallel()

Expand Down
14 changes: 14 additions & 0 deletions miner/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -2922,6 +2922,20 @@ func (w *worker) commit(env *environment, interval func(), update bool, start ti
return err
}

// Post-Placeholder: embed the commit-sealing timings into the block's extra
// data so they can be read directly from the chain (verifyHeader enforces
// their presence). The timings are only known after FinalizeAndAssemble, so
// the block built above carries the pre-timing Extra and must be rebuilt
// before it is handed off for sealing. The embedded elapsed is captured here
// rather than reusing the logged value below so the existing log line is left
// untouched; the two differ only by negligible sub-microsecond timing.
if w.chainConfig.Bor != nil && w.chainConfig.Bor.IsPlaceholder(env.header.Number) {
if err := env.header.SetSealTimings(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))
}

select {
case w.taskCh <- &task{receipts: env.receipts, state: env.state, block: block, createdAt: time.Now(), productionElapsed: time.Since(firstNonZeroTime(productionStartFrom(genParams), start)), intermediateRootTime: commitTime}:
fees := totalFees(block, env.receipts)
Expand Down
Loading