From d46f91865d484d205c996e2879c5a547ec603f1c Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Mon, 15 Dec 2025 22:16:15 -0300 Subject: [PATCH 1/3] remove recommit and track parent actual time --- consensus/bor/bor.go | 23 ++++++++++++++++++----- miner/worker.go | 22 ++++++++++++---------- 2 files changed, 30 insertions(+), 15 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index e51314aff1..f4776f1935 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -252,7 +252,7 @@ type Bor struct { // The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. blockTime time.Duration - lastMinedBlockTime time.Time + parentActualTimeCache *lru.Cache quit chan struct{} closeOnce sync.Once @@ -326,6 +326,8 @@ func New( }, }) + c.parentActualTimeCache, _ = lru.New(100) + // make sure we can decode all the GenesisAlloc in the BorConfig. for key, genesisAlloc := range c.config.BlockAlloc { if _, err := decodeGenesisAlloc(genesisAlloc); err != nil { @@ -1017,12 +1019,19 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header) e if c.blockTime > 0 && c.config.IsRio(header.Number) { // Only enable custom block time for Rio and later - parentActualTime := c.lastMinedBlockTime - if parentActualTime.IsZero() || parentActualTime.Before(time.Unix(int64(parent.Time), 0)) { - parentActualTime = time.Unix(int64(parent.Time), 0) + + parentChainTime := time.Unix(int64(parent.Time), 0) + // Default to parent chain timestamp + parentActualTime := parentChainTime + // If we have the parent's ActualTime locally (by parent hash), prefer it + if c.parentActualTimeCache != nil { + if v, ok := c.parentActualTimeCache.Get(header.ParentHash); ok { + if at, ok := v.(time.Time); ok && at.After(parentChainTime) { + parentActualTime = at + } + } } actualNewBlockTime := parentActualTime.Add(c.blockTime) - c.lastMinedBlockTime = actualNewBlockTime header.Time = uint64(actualNewBlockTime.Unix()) header.ActualTime = actualNewBlockTime } else { @@ -1238,6 +1247,10 @@ func (c *Bor) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *typ // Assemble block block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) + if c.parentActualTimeCache != nil && !block.Header().ActualTime.IsZero() { + c.parentActualTimeCache.Add(block.Hash(), block.Header().ActualTime) + } + // return the final block for sealing return block, receipts, nil } diff --git a/miner/worker.go b/miner/worker.go index 5bc989c75f..af36a9b401 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -562,16 +562,18 @@ func (w *worker) newWorkLoop(recommit time.Duration) { veblopTimer.Reset(veblopTimeout) case <-timer.C: - // If sealing is running resubmit a new work cycle periodically to pull in - // higher priced transactions. Disable this overhead for pending blocks. - if w.IsRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { - // Short circuit if no new transaction arrives. - if w.newTxs.Load() == 0 { - timer.Reset(recommit) - continue - } - commit(true, commitInterruptResubmit) - } + // Recommit disabled due to the current low block period (no need to capture more txs on the block already built) + continue + // // If sealing is running resubmit a new work cycle periodically to pull in + // // higher priced transactions. Disable this overhead for pending blocks. + // if w.IsRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { + // // Short circuit if no new transaction arrives. + // if w.newTxs.Load() == 0 { + // timer.Reset(recommit) + // continue + // } + // commit(true, commitInterruptResubmit) + // } case interval := <-w.resubmitIntervalCh: // Adjust resubmit interval explicitly by user. From 7e665da22ec8719b5845ab978f59a0d36c34a2c2 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Tue, 16 Dec 2025 09:26:38 -0300 Subject: [PATCH 2/3] applying pr comments --- consensus/bor/bor.go | 23 +++++++----- consensus/bor/bor_test.go | 77 ++++++++++++++++++++++++++++++++------- miner/worker.go | 10 ----- 3 files changed, 77 insertions(+), 33 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index f4776f1935..d63581f754 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -252,6 +252,7 @@ type Bor struct { // The block time defined by the miner. Needs to be larger or equal to the consensus block time. If not set (default = 0), the miner will use the consensus block time. blockTime time.Duration + // Cache to store the actual times of the parent blocks parentActualTimeCache *lru.Cache quit chan struct{} @@ -326,7 +327,7 @@ func New( }, }) - c.parentActualTimeCache, _ = lru.New(100) + c.parentActualTimeCache, _ = lru.New(10) // make sure we can decode all the GenesisAlloc in the BorConfig. for key, genesisAlloc := range c.config.BlockAlloc { @@ -1020,26 +1021,30 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header) e if c.blockTime > 0 && c.config.IsRio(header.Number) { // Only enable custom block time for Rio and later - parentChainTime := time.Unix(int64(parent.Time), 0) - // Default to parent chain timestamp - parentActualTime := parentChainTime + parentBlockTime := time.Unix(int64(parent.Time), 0) + // Default to parent block timestamp + parentActualBlockTime := parentBlockTime // If we have the parent's ActualTime locally (by parent hash), prefer it if c.parentActualTimeCache != nil { if v, ok := c.parentActualTimeCache.Get(header.ParentHash); ok { - if at, ok := v.(time.Time); ok && at.After(parentChainTime) { - parentActualTime = at + if at, ok := v.(time.Time); ok && at.After(parentBlockTime) { + parentActualBlockTime = at } } } - actualNewBlockTime := parentActualTime.Add(c.blockTime) + actualNewBlockTime := parentActualBlockTime.Add(c.blockTime) header.Time = uint64(actualNewBlockTime.Unix()) header.ActualTime = actualNewBlockTime } else { header.Time = parent.Time + CalcProducerDelay(number, succession, c.config) } - if header.Time < uint64(time.Now().Unix()) { - header.Time = uint64(time.Now().Unix()) + now := time.Now() + if header.Time < uint64(now.Unix()) { + header.Time = uint64(now.Unix()) + if c.blockTime > 0 && c.config.IsRio(header.Number) { + header.ActualTime = now + } } return nil diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index c2ceedda6e..eec1e70b00 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -50,7 +50,7 @@ func (s *fakeSpanner) CommitSpan(ctx context.Context, _ borTypes.Span, _ []stake } // newChainAndBorForTest centralizes common Bor + HeaderChain initialization for tests -func newChainAndBorForTest(t *testing.T, sp Spanner, borCfg *params.BorConfig, devFake bool, signerAddr common.Address) (*core.BlockChain, *Bor) { +func newChainAndBorForTest(t *testing.T, sp Spanner, borCfg *params.BorConfig, devFake bool, signerAddr common.Address, genesisTime uint64) (*core.BlockChain, *Bor) { cfg := ¶ms.ChainConfig{ChainID: big.NewInt(1), Bor: borCfg} b := &Bor{chainConfig: cfg, config: cfg.Bor, DevFakeAuthor: devFake} @@ -76,8 +76,9 @@ func newChainAndBorForTest(t *testing.T, sp Spanner, borCfg *params.BorConfig, d if devFake && signerAddr != (common.Address{}) { b.authorizedSigner.Store(&signer{signer: signerAddr}) } + b.parentActualTimeCache, _ = lru.New(10) - genspec := &core.Genesis{Config: cfg} + genspec := &core.Genesis{Config: cfg, Timestamp: genesisTime} db := rawdb.NewMemoryDatabase() _ = genspec.MustCommit(db, triedb.NewDatabase(db, triedb.HashDefaults)) chain, err := core.NewBlockChain(rawdb.NewMemoryDatabase(), genspec, b, core.DefaultConfig()) @@ -392,7 +393,7 @@ func TestPerformSpanCheck(t *testing.T) { t.Run(c.name, func(t *testing.T) { sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr2, VotingPower: 1}}} borCfg := ¶ms.BorConfig{Sprint: map[string]uint64{"0": 64}, Period: map[string]uint64{"0": 2}} - chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}) + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) var parents []*types.Header var parentHash common.Hash @@ -469,7 +470,7 @@ func TestGetVeBlopSnapshot(t *testing.T) { t.Run(c.name, func(t *testing.T) { sp := &fakeSpanner{vals: c.spVals} borCfg := ¶ms.BorConfig{Sprint: map[string]uint64{"0": 64}, Period: map[string]uint64{"0": 2}, RioBlock: big.NewInt(0)} - chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}) + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) h := &types.Header{Number: big.NewInt(int64(c.targetNum))} snap, err := b.getVeBlopSnapshot(chain.HeaderChain(), h, nil, c.checkNewSpan) require.NoError(t, err) @@ -516,7 +517,7 @@ func TestSnapshot(t *testing.T) { sp := &fakeSpanner{vals: c.spVals} // Configure RioBlock far in the future so IsRio(header.Number) == false borCfg := ¶ms.BorConfig{Sprint: map[string]uint64{"0": 64}, Period: map[string]uint64{"0": 2}, RioBlock: big.NewInt(1_000_000)} - chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}) + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) gen := chain.HeaderChain().GetHeaderByNumber(0) require.NotNil(t, gen) target := &types.Header{Number: big.NewInt(1), ParentHash: gen.Hash()} @@ -590,7 +591,7 @@ func TestCustomBlockTimeValidation(t *testing.T) { Period: map[string]uint64{"0": tc.consensusPeriod}, RioBlock: big.NewInt(0), // Enable Rio from genesis } - chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = tc.blockTime // Get genesis block as parent @@ -626,7 +627,7 @@ func TestCustomBlockTimeCalculation(t *testing.T) { Period: map[string]uint64{"0": 2}, RioBlock: big.NewInt(0), } - chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = 5 * time.Second genesis := chain.HeaderChain().GetHeaderByNumber(0) @@ -652,7 +653,7 @@ func TestCustomBlockTimeCalculation(t *testing.T) { Period: map[string]uint64{"0": 2}, RioBlock: big.NewInt(0), } - chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = 3 * time.Second genesis := chain.HeaderChain().GetHeaderByNumber(0) @@ -678,22 +679,23 @@ func TestCustomBlockTimeCalculation(t *testing.T) { Period: map[string]uint64{"0": 2}, RioBlock: big.NewInt(0), } - chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = 4 * time.Second genesis := chain.HeaderChain().GetHeaderByNumber(0) require.NotNil(t, genesis) baseTime := genesis.Time + parentHash := genesis.Hash() if baseTime > 10 { - b.lastMinedBlockTime = time.Unix(int64(baseTime-10), 0) + b.parentActualTimeCache.Add(parentHash, time.Unix(int64(baseTime-10), 0)) } else { - b.lastMinedBlockTime = time.Unix(0, 0) + b.parentActualTimeCache.Add(parentHash, time.Unix(0, 0)) } header := &types.Header{ Number: big.NewInt(1), - ParentHash: genesis.Hash(), + ParentHash: parentHash, } err := b.Prepare(chain.HeaderChain(), header) @@ -718,7 +720,7 @@ func TestCustomBlockTimeBackwardCompatibility(t *testing.T) { BackupMultiplier: map[string]uint64{"0": 2}, RioBlock: big.NewInt(0), } - chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = 0 genesis := chain.HeaderChain().GetHeaderByNumber(0) @@ -736,6 +738,53 @@ func TestCustomBlockTimeBackwardCompatibility(t *testing.T) { }) } +func TestCustomBlockTimeClampsToNowAlsoUpdatesActualTime(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + // Force parent time far in the past so that after adding blockTime, header.Time is still < now + // and the "clamp to now" block triggers. + pastParentTime := time.Now().Add(-10 * time.Minute).Unix() + + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + RioBlock: big.NewInt(0), // Rio enabled from genesis + } + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(pastParentTime)) + + // Enable custom block time (must be >= Period to avoid validation error) + b.blockTime = 5 * time.Second + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + + before := time.Now() + err := b.Prepare(chain.HeaderChain(), header) + after := time.Now() + + require.NoError(t, err) + + // Validate the clamp happened: header.Time should be "now-ish", not the past-derived time. + require.GreaterOrEqual(t, int64(header.Time), before.Unix(), "header.Time should be clamped up to now") + require.LessOrEqual(t, int64(header.Time), after.Unix()+1, "header.Time should be close to now") + + // Critical regression assertion: + // When custom blockTime is enabled for Rio, clamping header.Time to now must also set ActualTime = now. + require.False(t, header.ActualTime.IsZero(), "ActualTime should be set when blockTime > 0 and Rio is enabled") + require.GreaterOrEqual(t, header.ActualTime.Unix(), before.Unix(), "ActualTime should be updated to now when clamping occurs") + require.LessOrEqual(t, header.ActualTime.Unix(), after.Unix()+1, "ActualTime should be close to now when clamping occurs") + + // Optional: since clamping sets both from the same `now`, they should match on Unix seconds. + require.Equal(t, int64(header.Time), header.ActualTime.Unix(), "header.Time and ActualTime should align after clamping") +} + func TestVerifySealRejectsOversizedDifficulty(t *testing.T) { t.Parallel() @@ -757,7 +806,7 @@ func TestVerifySealRejectsOversizedDifficulty(t *testing.T) { } // devFake=false, we need real signatures for the sake of this test - chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}) + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) parent := chain.HeaderChain().GetHeaderByNumber(0) require.NotNil(t, parent) diff --git a/miner/worker.go b/miner/worker.go index af36a9b401..df727a4665 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -564,16 +564,6 @@ func (w *worker) newWorkLoop(recommit time.Duration) { case <-timer.C: // Recommit disabled due to the current low block period (no need to capture more txs on the block already built) continue - // // If sealing is running resubmit a new work cycle periodically to pull in - // // higher priced transactions. Disable this overhead for pending blocks. - // if w.IsRunning() && (w.chainConfig.Clique == nil || w.chainConfig.Clique.Period > 0) { - // // Short circuit if no new transaction arrives. - // if w.newTxs.Load() == 0 { - // timer.Reset(recommit) - // continue - // } - // commit(true, commitInterruptResubmit) - // } case interval := <-w.resubmitIntervalCh: // Adjust resubmit interval explicitly by user. From 5df4bf56275195afa5988278820454fea5c67740 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Tue, 16 Dec 2025 10:29:15 -0300 Subject: [PATCH 3/3] fix actual time cache store place --- consensus/bor/bor.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index d63581f754..7f4e4829b1 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1252,10 +1252,6 @@ func (c *Bor) FinalizeAndAssemble(chain consensus.ChainHeaderReader, header *typ // Assemble block block := types.NewBlock(header, body, receipts, trie.NewStackTrie(nil)) - if c.parentActualTimeCache != nil && !block.Header().ActualTime.IsZero() { - c.parentActualTimeCache.Add(block.Hash(), block.Header().ActualTime) - } - // return the final block for sealing return block, receipts, nil } @@ -1327,6 +1323,10 @@ func (c *Bor) Seal(chain consensus.ChainHeaderReader, block *types.Block, witnes return err } + if c.parentActualTimeCache != nil && !header.ActualTime.IsZero() { + c.parentActualTimeCache.Add(header.Hash(), header.ActualTime) + } + // Wait until sealing is terminated or delay timeout. log.Info("Waiting for slot to sign and propagate", "number", number, "hash", header.Hash(), "delay-in-sec", uint(delay), "delay", common.PrettyDuration(delay))