diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 9a9c4a2cab..ecdff7b24c 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -419,10 +419,11 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head number := header.Number.Uint64() now := uint64(time.Now().Unix()) - if c.config.IsRio(header.Number) { - // Rio HF introduced flexible blocktime (can be set larger than consensus without approval). - // Using strict CalcProducerDelay would reject valid blocks, so we just ensure announcement - // time comes after parent time to allow for flexible blocktime. + if c.config.IsGiugliano(header.Number) { + // Rio introduced flexible blocktime (can be set larger than consensus without approval). + // Using strict CalcProducerDelay for early block announcement (introduced back in Giugliano) + // would reject valid blocks, so we just ensure announcement time comes after parent time to + // allow for flexible blocktime. var parent *types.Header if len(parents) > 0 { @@ -431,17 +432,17 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head parent = chain.GetHeader(header.ParentHash, number-1) } if parent == nil || now < parent.Time { - log.Error("Block announced too early post rio", "number", number, "headerTime", header.Time, "now", now) + log.Error("Block announced too early post giugliano", "number", number, "headerTime", header.Time, "now", now) return consensus.ErrFutureBlock } // Upper-bound check: a block whose timestamp is more than maxAllowedFutureBlockTimeSeconds // ahead of the local clock is rejected. if header.Time > now+maxAllowedFutureBlockTimeSeconds { - log.Error("Block timestamp too far in future post rio", "number", number, "headerTime", header.Time, "now", now) + log.Error("Block timestamp too far in future post giugliano", "number", number, "headerTime", header.Time, "now", now) return consensus.ErrFutureBlock } } else if c.config.IsBhilai(header.Number) { - // Allow early blocks if Bhilai HF is enabled + // TODO: Once Amoy and Mainnet supports Giugliano HF, we are safe to remove this check (since it only works for block future blocks) // Don't waste time checking blocks from the future but allow a buffer of block time for // early block announcements. Note that this is a loose check and would allow early blocks // from non-primary producer. Such blocks will be rejected later when we know the succession @@ -533,7 +534,7 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head cacheTTL := veblopBlockTimeout nowTime := time.Now() headerTime := time.Unix(int64(header.Time), 0) - if headerTime.After(nowTime) { + if headerTime.After(nowTime) && c.config.IsGiugliano(header.Number) { // Add the time from now until header time as extra to the base timeout extraTime := headerTime.Sub(nowTime) cacheTTL = veblopBlockTimeout + extraTime @@ -1151,8 +1152,8 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w } } - // Wait before start the block production if needed (previsously this wait was on Seal) - if c.config.IsBhilai(header.Number) && waitOnPrepare { + // Wait before start the block production if needed (previously this wait was on Seal) + if c.config.IsGiugliano(header.Number) && waitOnPrepare { var successionNumber int // if signer is not empty (RPC nodes have empty signer) if currentSigner.signer != (common.Address{}) { @@ -1431,8 +1432,8 @@ func (c *Bor) Seal(chain consensus.ChainHeaderReader, block *types.Block, witnes var delay time.Duration // Sweet, the protocol permits us to sign the block, wait for our time - if c.config.IsBhilai(header.Number) && successionNumber == 0 { - delay = 0 // delay was moved to Prepare for bhilai and later + if c.config.IsGiugliano(header.Number) && successionNumber == 0 { + delay = 0 // delay was moved to Prepare for giugliano and later } else { delay = time.Until(header.GetActualTime()) // Wait until we reach header time } diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 6caf7bf280..4db081af2c 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -855,7 +855,7 @@ func TestCustomBlockTimeBackwardCompatibility(t *testing.T) { Period: map[string]uint64{"0": 2}, ProducerDelay: map[string]uint64{"0": 3}, BackupMultiplier: map[string]uint64{"0": 2}, - RioBlock: big.NewInt(0), + RioBlock: big.NewInt(0), // blockTime=0 always takes the else-branch regardless of hardfork } chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, uint64(time.Now().Unix())) b.blockTime = 0 @@ -1762,6 +1762,86 @@ func TestVerifyHeader_RequestsHash(t *testing.T) { require.ErrorIs(t, err, consensus.ErrUnexpectedRequests) } +// TestVerifyHeader_Giugliano_Boundary verifies that the flexible blocktime +// timestamp validation in verifyHeader activates exactly at GiuglianoBlock. +// +// Before GiuglianoBlock the old code-path is used (header.Time > now fails), +// at and after GiuglianoBlock the new path is used (parent-time check + +// upper-bound check instead of a strict now comparison). +func TestVerifyHeader_Giugliano_Boundary(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + const giuglianoBlock = 100 + + now := uint64(time.Now().Unix()) + + t.Run("before GiuglianoBlock – future timestamp is rejected", func(t *testing.T) { + // GiuglianoBlock is far in the future, so the legacy path is taken. + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(1_000_000), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, now) + + h := &types.Header{ + Number: big.NewInt(giuglianoBlock - 1), + Time: now + 3600, // 1 hour in the future – must be rejected + Extra: make([]byte, 32+65), + } + err := b.VerifyHeader(chain.HeaderChain(), h) + require.ErrorIs(t, err, consensus.ErrFutureBlock, "pre-Giugliano: future timestamp should be rejected") + }) + + t.Run("at GiuglianoBlock – timestamp within upper bound is accepted", func(t *testing.T) { + // GiuglianoBlock active from genesis so every block uses the new path. + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, now) + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + // Timestamp slightly in the future but within maxAllowedFutureBlockTimeSeconds. + h := &types.Header{ + Number: big.NewInt(giuglianoBlock), + ParentHash: genesis.Hash(), + Time: now + maxAllowedFutureBlockTimeSeconds - 1, + Extra: make([]byte, 32+65), + } + // verifyHeader will proceed past the timestamp check; subsequent checks + // (mixDigest, difficulty, etc.) may still fail, but ErrFutureBlock must not. + err := b.VerifyHeader(chain.HeaderChain(), h) + require.NotErrorIs(t, err, consensus.ErrFutureBlock, "post-Giugliano: timestamp within bound should not return ErrFutureBlock") + }) + + t.Run("at GiuglianoBlock – timestamp beyond upper bound is rejected", func(t *testing.T) { + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(0), + } + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, now) + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + h := &types.Header{ + Number: big.NewInt(giuglianoBlock), + ParentHash: genesis.Hash(), + Time: now + maxAllowedFutureBlockTimeSeconds + 10, // beyond upper bound + Extra: make([]byte, 32+65), + } + err := b.VerifyHeader(chain.HeaderChain(), h) + require.ErrorIs(t, err, consensus.ErrFutureBlock, "post-Giugliano: timestamp beyond upper bound must be rejected") + }) +} + func TestVerifyCascadingFields_Genesis(t *testing.T) { t.Parallel() sp := &fakeSpanner{vals: []*valset.Validator{{Address: common.HexToAddress("0x1"), VotingPower: 1}}} @@ -4346,11 +4426,11 @@ func TestBorPrepare_WaitOnPrepareFlag(t *testing.T) { // Test 2: Prepare with waitOnPrepare=true should wait for the proper block time t.Run("with_wait", func(t *testing.T) { - // Create a config with Bhilai fork enabled to activate wait logic + // Create a config with Giugliano enabled to activate wait-in-Prepare logic borCfgWithBhilai := ¶ms.BorConfig{ - Sprint: map[string]uint64{"0": 64}, - Period: map[string]uint64{"0": 2}, - BhilaiBlock: big.NewInt(0), // Enable Bhilai fork from block 0 + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(0), // Enable Giugliano from block 0 } // Set genesis time 3 seconds in the future to ensure enough wait time @@ -4384,7 +4464,7 @@ func TestBorPrepare_WaitOnPrepareFlag(t *testing.T) { t.Fatalf("Prepare with waitOnPrepare=true failed: %v", err) } - // With Bhilai enabled, DevFakeAuthor=true (making this node the primary producer), + // With Giugliano enabled, DevFakeAuthor=true (making this node the primary producer), // and waitOnPrepare=true, should wait until parent (genesis) time has passed // Allow 100ms tolerance for timing precision and scheduling overhead minWait := expectedDelay - 100*time.Millisecond @@ -4430,6 +4510,149 @@ func TestBorPrepare_WaitOnPrepareFlag(t *testing.T) { }) } +// TestPrepare_WaitGate_GiuglianoOnly verifies that the wait-in-Prepare +// mechanism activates only when IsGiugliano is true. +func TestPrepare_WaitGate_GiuglianoOnly(t *testing.T) { + t.Parallel() + + addr := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr, VotingPower: 1}}} + + t.Run("before Giugliano – waitOnPrepare=true returns quickly", func(t *testing.T) { + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + // GiuglianoBlock not set → IsGiugliano always false + } + // Set genesis time slightly in the future so there would be a non-trivial delay + // if the wait were active. + genesisTime := uint64(time.Now().Add(2 * time.Second).Unix()) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr, genesisTime) + defer chain.Stop() + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + header := &types.Header{Number: big.NewInt(1), ParentHash: genesis.Hash()} + + start := time.Now() + err := b.Prepare(chain, header, true) + elapsed := time.Since(start) + + require.NoError(t, err) + // Without Giugliano the wait block is skipped; should return in < 200 ms + require.Less(t, elapsed, 200*time.Millisecond, + "Prepare should not wait when Giugliano is not active") + }) + + t.Run("at Giugliano – waitOnPrepare=true waits for primary producer", func(t *testing.T) { + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(0), + } + // Genesis 3 s in the future → there will be a measurable wait. + genesisTime := uint64(time.Now().Add(3 * time.Second).Unix()) + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr, genesisTime) + defer chain.Stop() + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + // Measure expected delay right before calling Prepare, same pattern as TestBorPrepare_WaitOnPrepareFlag. + expectedDelay := time.Until(time.Unix(int64(genesis.Time), 0)) + if expectedDelay < 100*time.Millisecond { + t.Skip("genesis time already passed due to slow setup") + } + + header := &types.Header{Number: big.NewInt(1), ParentHash: genesis.Hash()} + + start := time.Now() + err := b.Prepare(chain, header, true) + elapsed := time.Since(start) + + require.NoError(t, err) + minWait := expectedDelay - 200*time.Millisecond + if minWait < 0 { + minWait = 0 + } + require.Greater(t, elapsed, minWait, + "Prepare should wait for primary producer when Giugliano is active") + }) +} + +// TestSeal_PrimaryProducerDelay_GiuglianoBoundary verifies that delay=0 in Seal +// for the primary producer (succession==0) is gated on IsGiugliano. +func TestSeal_PrimaryProducerDelay_GiuglianoBoundary(t *testing.T) { + t.Parallel() + + addr := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr, VotingPower: 1}}} + now := uint64(time.Now().Unix()) + + makeHeader := func(borCfg *params.BorConfig) (*types.Header, *Bor, *core.BlockChain) { + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr, now) + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + h := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + Extra: make([]byte, 32+65), + UncleHash: uncleHash, + Difficulty: big.NewInt(1), + GasLimit: 8_000_000, + } + // Set header.Time so GetActualTime() returns something in the past + h.Time = now - 1 + return h, b, chain + } + + t.Run("before Giugliano – primary producer has non-zero delay", func(t *testing.T) { + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + // GiuglianoBlock not set + } + h, b, chain := makeHeader(borCfg) + defer chain.Stop() + + snap, err := b.snapshot(chain.HeaderChain(), h, nil, false) + require.NoError(t, err) + + successionNumber, err := snap.GetSignerSuccessionNumber(addr) + require.NoError(t, err) + require.Equal(t, 0, successionNumber, "DevFakeAuthor should be primary producer") + + // Before Giugliano the delay=0 branch should NOT be taken. + // The else branch sets delay = time.Until(header.GetActualTime()). + // Since header.Time is in the past, delay ≤ 0 — but the point is the branch + // selected is the else, not the delay=0 one. + isNewHF := b.config.IsGiugliano(h.Number) + require.False(t, isNewHF, "IsGiugliano should be false before GiuglianoBlock") + }) + + t.Run("at Giugliano – primary producer gets delay=0", func(t *testing.T) { + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + GiuglianoBlock: big.NewInt(0), + } + h, b, chain := makeHeader(borCfg) + defer chain.Stop() + + snap, err := b.snapshot(chain.HeaderChain(), h, nil, false) + require.NoError(t, err) + + successionNumber, err := snap.GetSignerSuccessionNumber(addr) + require.NoError(t, err) + require.Equal(t, 0, successionNumber, "DevFakeAuthor should be primary producer") + + isNewHF := b.config.IsGiugliano(h.Number) + require.True(t, isNewHF, "IsGiugliano should be true at GiuglianoBlock=0") + // The Seal function would take the delay=0 branch for this signer/header combination. + }) +} + func newBorForMilestoneFetcherTest(t *testing.T) *Bor { t.Helper() sp := &fakeSpanner{vals: []*valset.Validator{{Address: common.HexToAddress("0x1"), VotingPower: 1}}} diff --git a/miner/worker.go b/miner/worker.go index a5a0688681..fa8354871e 100644 --- a/miner/worker.go +++ b/miner/worker.go @@ -1871,7 +1871,8 @@ func (w *worker) commitWork(interrupt *atomic.Int32, noempty bool, timestamp int } var interruptPrefetch atomic.Bool - if w.config.EnablePrefetch { + newBlockNumber := new(big.Int).Add(parent.Number, common.Big1) + if w.config.EnablePrefetch && w.chainConfig.Bor != nil && w.chainConfig.Bor.IsGiugliano(newBlockNumber) { go func() { defer func() { if r := recover(); r != nil { @@ -1985,7 +1986,6 @@ func (w *worker) prefetchFromPool(parent *types.Header, throwaway *state.StateDB w.mu.RUnlock() if err != nil { - log.Warn("Prefetch failed to create header", "err", err) return } signer := types.MakeSigner(w.chainConfig, header.Number, header.Time) diff --git a/miner/worker_test.go b/miner/worker_test.go index 4e05ada410..8da774b8e4 100644 --- a/miner/worker_test.go +++ b/miner/worker_test.go @@ -351,13 +351,25 @@ func newTestWorker(t TensingObject, config *Config, chainConfig *params.ChainCon return w, backend, w.close } +// borUnittestChainConfigWithGiugliano returns a shallow copy of BorUnittestChainConfig +// with GiuglianoBlock activated at block 0. Required for tests that exercise +// Giugliano-gated features such as prefetchFromPool. +func borUnittestChainConfigWithGiugliano() *params.ChainConfig { + cfg := *params.BorUnittestChainConfig + borCfg := *cfg.Bor + borCfg.GiuglianoBlock = big.NewInt(0) + cfg.Bor = &borCfg + + return &cfg +} + // setupBorWorkerWithPrefetch sets up a worker with Bor consensus engine and prefetch enabled. // Returns worker, backend, consensus engine, and mock controller for cleanup. // nolint:thelper func setupBorWorkerWithPrefetch(t *testing.T, gasPercent uint64, recommit time.Duration) (*worker, *testWorkerBackend, consensus.Engine, *gomock.Controller) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller ) @@ -2067,7 +2079,7 @@ func TestPrefetchRaceWithSetExtra(t *testing.T) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller ) @@ -2151,7 +2163,7 @@ func TestPrefetchGoroutineLifecycle(t *testing.T) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller ) @@ -2321,7 +2333,7 @@ func TestStateDBLifecycle_WithoutWait(t *testing.T) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller ) @@ -2696,7 +2708,7 @@ func BenchmarkBlockProductionLatency(b *testing.B) { b.Run("WithPrefetch", func(b *testing.B) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller ) @@ -2772,7 +2784,7 @@ func BenchmarkPrefetchMemoryOverhead(b *testing.B) { b.Run("WithPrefetch", func(b *testing.B) { var ( engine consensus.Engine - chainConfig = params.BorUnittestChainConfig + chainConfig = borUnittestChainConfigWithGiugliano() db = rawdb.NewMemoryDatabase() ctrl *gomock.Controller )