Skip to content
Merged
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
25 changes: 13 additions & 12 deletions consensus/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -419,10 +419,11 @@
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 {
Expand All @@ -431,17 +432,17 @@
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)

Check warning on line 445 in consensus/bor/bor.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Complete the task associated to this TODO comment.

See more on https://sonarcloud.io/project/issues?id=0xPolygon_bor&issues=AZzSz8AWk6QWoM0HXLX-&open=AZzSz8AWk6QWoM0HXLX-&pullRequest=2099
// 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
Expand Down Expand Up @@ -533,7 +534,7 @@
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
Expand Down Expand Up @@ -1151,8 +1152,8 @@
}
}

// 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{}) {
Expand Down Expand Up @@ -1431,8 +1432,8 @@
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
}
Expand Down
235 changes: 229 additions & 6 deletions consensus/bor/bor_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -855,7 +855,7 @@
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
Expand Down Expand Up @@ -1762,6 +1762,86 @@
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) {

Check warning on line 1771 in consensus/bor/bor_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "TestVerifyHeader_Giugliano_Boundary" to match the regular expression ^(_|[a-zA-Z0-9]+)$

See more on https://sonarcloud.io/project/issues?id=0xPolygon_bor&issues=AZzSz725k6QWoM0HXLX7&open=AZzSz725k6QWoM0HXLX7&pullRequest=2099
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 := &params.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 := &params.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 := &params.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}}}
Expand Down Expand Up @@ -4346,11 +4426,11 @@

// 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 := &params.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
Expand Down Expand Up @@ -4384,7 +4464,7 @@
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
Expand Down Expand Up @@ -4430,6 +4510,149 @@
})
}

// TestPrepare_WaitGate_GiuglianoOnly verifies that the wait-in-Prepare
// mechanism activates only when IsGiugliano is true.
func TestPrepare_WaitGate_GiuglianoOnly(t *testing.T) {

Check warning on line 4515 in consensus/bor/bor_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "TestPrepare_WaitGate_GiuglianoOnly" to match the regular expression ^(_|[a-zA-Z0-9]+)$

See more on https://sonarcloud.io/project/issues?id=0xPolygon_bor&issues=AZzSz725k6QWoM0HXLX8&open=AZzSz725k6QWoM0HXLX8&pullRequest=2099
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 := &params.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 := &params.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) {

Check warning on line 4586 in consensus/bor/bor_test.go

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Rename function "TestSeal_PrimaryProducerDelay_GiuglianoBoundary" to match the regular expression ^(_|[a-zA-Z0-9]+)$

See more on https://sonarcloud.io/project/issues?id=0xPolygon_bor&issues=AZzSz725k6QWoM0HXLX9&open=AZzSz725k6QWoM0HXLX9&pullRequest=2099
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 := &params.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 := &params.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}}}
Expand Down
4 changes: 2 additions & 2 deletions miner/worker.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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)
Expand Down
Loading
Loading