From bd4e9c28f15d0748aa0724a82cbe8101d0ef036a Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 12 Mar 2026 11:13:22 -0300 Subject: [PATCH 1/3] giugliano HF --- polygon/bor/bor.go | 61 ++++++++-- polygon/bor/bor_internal_test.go | 191 +++++++++++++++++++++++++++++++ 2 files changed, 240 insertions(+), 12 deletions(-) diff --git a/polygon/bor/bor.go b/polygon/bor/bor.go index 87268a12a4b..4b62cb71a7e 100644 --- a/polygon/bor/bor.go +++ b/polygon/bor/bor.go @@ -64,6 +64,8 @@ import ( const inmemorySignatures = 4096 // Number of recent block signatures to keep in memory +const maxAllowedFutureBlockTimeSeconds = uint64(30) + // Bor protocol constants. var ( // Default number of blocks after which to checkpoint and reset the pending votes @@ -227,7 +229,21 @@ func ValidateHeaderTime( config *borcfg.BorConfig, signaturesCache *lru.ARCCache[common.Hash, common.Address], ) error { - if config.IsBhilai(header.Number.Uint64()) { + if config.IsGiugliano(header.Number.Uint64()) { + // Parent time check (announcement must come at or after parent time) + if parent != nil && uint64(now.Unix()) < parent.Time { + return fmt.Errorf("%w: announcement before parent time: parent=%s, now=%s", + consensus.ErrFutureBlock, + time.Unix(int64(parent.Time), 0), now) + } + // 30-second future cap + if header.Time > uint64(now.Unix())+maxAllowedFutureBlockTimeSeconds { + return fmt.Errorf("%w: expected: %s(%s), got: %s", + consensus.ErrFutureBlock, + time.Unix(now.Unix()+int64(maxAllowedFutureBlockTimeSeconds), 0), now, + time.Unix(int64(header.Time), 0)) + } + } else if config.IsBhilai(header.Number.Uint64()) { // 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 @@ -256,8 +272,9 @@ func ValidateHeaderTime( return err } - // Post Bhilai HF, reject blocks from non-primary producers if they're earlier than the expected time - if config.IsBhilai(header.Number.Uint64()) && succession != 0 { + // Post Bhilai HF (pre-Giugliano), reject blocks from non-primary producers if earlier than expected + // Giugliano lifted this restriction — all producers may announce early within the 30s cap. + if config.IsBhilai(header.Number.Uint64()) && !config.IsGiugliano(header.Number.Uint64()) && succession != 0 { if header.Time > uint64(now.Unix()) { return fmt.Errorf("%w: expected: %s(%s), got: %s", consensus.ErrFutureBlock, time.Unix(now.Unix(), 0), now, time.Unix(int64(header.Time), 0)) } @@ -461,19 +478,39 @@ func (c *Bor) verifyHeader(chain consensus.ChainHeaderReader, header *types.Head now := time.Now().Unix() // Don't waste time checking blocks from the future - // Allow early blocks if Bhilai HF is enabled - if c.config.IsBhilai(number) { - // 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 - // number of the signer in the current sprint. + // Allow early blocks with increasing buffers per HF + if c.config.IsGiugliano(number) { + // Giugliano: blocks may be announced early; cap at 30s future and require + // announcement time to be at or after parent block time. + var parent *types.Header + if len(parents) > 0 { + parent = parents[len(parents)-1] + } else { + parent = chain.GetHeader(header.ParentHash, number-1) + } + if parent != nil && uint64(now) < parent.Time { + return fmt.Errorf("%w: announcement before parent time: parent=%s, now=%s", + consensus.ErrFutureBlock, + time.Unix(int64(parent.Time), 0), + time.Unix(now, 0)) + } + if header.Time > uint64(now)+maxAllowedFutureBlockTimeSeconds { + return fmt.Errorf("%w: expected: %s, got: %s", + consensus.ErrFutureBlock, + time.Unix(now+int64(maxAllowedFutureBlockTimeSeconds), 0), + time.Unix(int64(header.Time), 0)) + } + } else if c.config.IsBhilai(number) { + // Bhilai: allow one block period buffer for early announcements (primary producer only; + // non-primary producers are rejected later after succession is known). if header.Time > uint64(now)+c.config.CalculatePeriod(number) { - return fmt.Errorf("%w: expected: %s, got: %s", consensus.ErrFutureBlock, time.Unix(now, 0), time.Unix(int64(header.Time), 0)) + return fmt.Errorf("%w: expected: %s, got: %s", consensus.ErrFutureBlock, + time.Unix(now, 0), time.Unix(int64(header.Time), 0)) } } else { - // Don't waste time checking blocks from the future if header.Time > uint64(now) { - return fmt.Errorf("%w: expected: %s, got: %s", consensus.ErrFutureBlock, time.Unix(now, 0), time.Unix(int64(header.Time), 0)) + return fmt.Errorf("%w: expected: %s, got: %s", consensus.ErrFutureBlock, + time.Unix(now, 0), time.Unix(int64(header.Time), 0)) } } diff --git a/polygon/bor/bor_internal_test.go b/polygon/bor/bor_internal_test.go index 3a49c9c4380..3eef64b6471 100644 --- a/polygon/bor/bor_internal_test.go +++ b/polygon/bor/bor_internal_test.go @@ -22,13 +22,17 @@ import ( "testing" "time" + lru "github.com/hashicorp/golang-lru/arc/v2" "github.com/holiman/uint256" + "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" "go.uber.org/mock/gomock" "github.com/erigontech/erigon-lib/common" + "github.com/erigontech/erigon-lib/crypto" "github.com/erigontech/erigon/execution/consensus" "github.com/erigontech/erigon/execution/types" + "github.com/erigontech/erigon/polygon/bor/borcfg" "github.com/erigontech/erigon/polygon/bor/statefull" polychain "github.com/erigontech/erigon/polygon/chain" "github.com/erigontech/erigon/polygon/heimdall" @@ -110,3 +114,190 @@ func TestCommitStatesIndore(t *testing.T) { require.NoError(t, err) require.Equal(t, 1, called) } + +// fixedSuccession implements ValidateHeaderTimeSignerSuccessionNumber for tests. +type fixedSuccession struct{ n int } + +func (f fixedSuccession) GetSignerSuccessionNumber(common.Address, uint64, *borcfg.BorConfig) (int, error) { + return f.n, nil +} + +// signTestHeader creates a header with a valid bor signature in Extra. +func signTestHeader(t *testing.T, header *types.Header, config *borcfg.BorConfig) common.Address { + t.Helper() + key, err := crypto.GenerateKey() + require.NoError(t, err) + sealHash := SealHash(header, config) + sig, err := crypto.Sign(sealHash[:], key) + require.NoError(t, err) + // extra must be at least ExtraSealLength (65) bytes; put sig at the end + if len(header.Extra) < types.ExtraSealLength { + header.Extra = make([]byte, types.ExtraSealLength) + } + copy(header.Extra[len(header.Extra)-types.ExtraSealLength:], sig) + return crypto.PubkeyToAddress(key.PublicKey) +} + +// newTestBorConfig returns a minimal BorConfig active from block 0 with +// period=2, using the provided HF block numbers (nil = fork disabled). +func newTestBorConfig(bhilaiBlock, giuglianoBlock *big.Int) *borcfg.BorConfig { + return &borcfg.BorConfig{ + Period: map[string]uint64{"0": 2}, + ProducerDelay: map[string]uint64{"0": 1}, + Sprint: map[string]uint64{"0": 16}, + BackupMultiplier: map[string]uint64{"0": 2}, + BhilaiBlock: bhilaiBlock, + GiuglianoBlock: giuglianoBlock, + } +} + +func newTestSigCache(t *testing.T) *lru.ARCCache[common.Hash, common.Address] { + t.Helper() + cache, err := lru.NewARC[common.Hash, common.Address](16) + require.NoError(t, err) + return cache +} + +// TestValidateHeaderTime_PreBhilai verifies the strict "no future blocks" rule +// that applies before the Bhilai hard fork. +func TestValidateHeaderTime_PreBhilai(t *testing.T) { + config := newTestBorConfig(nil, nil) // no HFs enabled + now := time.Now() + + tests := []struct { + name string + headerTs uint64 + wantErr bool + }{ + {"at now", uint64(now.Unix()), false}, + {"1s past", uint64(now.Unix()) - 1, false}, + {"1s future", uint64(now.Unix()) + 1, true}, + {"30s future", uint64(now.Unix()) + 30, true}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + header := &types.Header{ + Number: big.NewInt(1), + Time: tc.headerTs, + } + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + if tc.wantErr { + assert.ErrorIs(t, err, consensus.ErrFutureBlock, "expected ErrFutureBlock") + } else { + assert.NoError(t, err) + } + }) + } +} + +// TestValidateHeaderTime_Bhilai verifies that a one-period early-announcement +// buffer is allowed and that non-primary producers are rejected when their +// block is early (succession check). +func TestValidateHeaderTime_Bhilai(t *testing.T) { + config := newTestBorConfig(big.NewInt(0), nil) // Bhilai from block 0, no Giugliano + period := config.CalculatePeriod(1) // 2s + now := time.Now() + nowTs := uint64(now.Unix()) + + t.Run("within period buffer", func(t *testing.T) { + header := &types.Header{Number: big.NewInt(1), Time: nowTs + period} + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + assert.NoError(t, err) + }) + + t.Run("exceeds period buffer", func(t *testing.T) { + header := &types.Header{Number: big.NewInt(1), Time: nowTs + period + 1} + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + assert.ErrorIs(t, err, consensus.ErrFutureBlock) + }) + + // Succession check: Bhilai rejects early blocks from non-primary producers. + // We need a signed header and a parent so that Ecrecover runs. + t.Run("non-primary producer early block rejected", func(t *testing.T) { + cache := newTestSigCache(t) + header := &types.Header{ + Number: big.NewInt(1), + Time: nowTs + 1, // header is 1s in the future + Difficulty: big.NewInt(1), + Extra: make([]byte, types.ExtraSealLength), + } + addr := signTestHeader(t, header, config) + // inject the signer directly into the cache to bypass Ecrecover's hash dependency + cache.Add(header.Hash(), addr) + parent := &types.Header{Number: big.NewInt(0), Time: nowTs - 2} + err := ValidateHeaderTime(header, now, parent, fixedSuccession{1}, config, cache) + assert.ErrorIs(t, err, consensus.ErrFutureBlock) + }) + + // Succession check: Bhilai does not fire ErrFutureBlock when header.Time == now, + // even for non-primary producers. A different error (BlockTooSoonError) may still + // fire, but that is a separate check unrelated to the future-block gate. + t.Run("non-primary producer at now: no ErrFutureBlock", func(t *testing.T) { + cache := newTestSigCache(t) + header := &types.Header{ + Number: big.NewInt(1), + Time: nowTs, + Difficulty: big.NewInt(1), + Extra: make([]byte, types.ExtraSealLength), + } + addr := signTestHeader(t, header, config) + cache.Add(header.Hash(), addr) + parent := &types.Header{Number: big.NewInt(0), Time: nowTs - 2} + err := ValidateHeaderTime(header, now, parent, fixedSuccession{1}, config, cache) + assert.NotErrorIs(t, err, consensus.ErrFutureBlock) + }) +} + +// TestValidateHeaderTime_Giugliano verifies: +// - 30-second future cap replaces the per-period buffer +// - announcement before parent block time is rejected +// - non-primary producers are no longer rejected for early blocks +func TestValidateHeaderTime_Giugliano(t *testing.T) { + config := newTestBorConfig(big.NewInt(0), big.NewInt(0)) // both HFs from block 0 + now := time.Now() + nowTs := uint64(now.Unix()) + + t.Run("29s future accepted", func(t *testing.T) { + header := &types.Header{Number: big.NewInt(1), Time: nowTs + 29} + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + assert.NoError(t, err) + }) + + t.Run("exactly 30s future accepted", func(t *testing.T) { + header := &types.Header{Number: big.NewInt(1), Time: nowTs + 30} + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + assert.NoError(t, err) + }) + + t.Run("31s future rejected", func(t *testing.T) { + header := &types.Header{Number: big.NewInt(1), Time: nowTs + 31} + err := ValidateHeaderTime(header, now, nil, fixedSuccession{0}, config, newTestSigCache(t)) + assert.ErrorIs(t, err, consensus.ErrFutureBlock) + }) + + t.Run("announcement before parent time rejected", func(t *testing.T) { + // parent.Time is 1s after now: announcement arrives before parent was produced + parent := &types.Header{Number: big.NewInt(0), Time: nowTs + 1} + header := &types.Header{Number: big.NewInt(1), Time: nowTs + 5} + err := ValidateHeaderTime(header, now, parent, fixedSuccession{0}, config, newTestSigCache(t)) + assert.ErrorIs(t, err, consensus.ErrFutureBlock) + }) + + // Giugliano lifts the Bhilai restriction: non-primary producers may announce + // early within the 30s cap. + t.Run("non-primary producer early block accepted", func(t *testing.T) { + cache := newTestSigCache(t) + header := &types.Header{ + Number: big.NewInt(1), + Time: nowTs + 10, // early but within 30s cap + Difficulty: big.NewInt(1), + Extra: make([]byte, types.ExtraSealLength), + } + addr := signTestHeader(t, header, config) + cache.Add(header.Hash(), addr) + parent := &types.Header{Number: big.NewInt(0), Time: nowTs - 2} + err := ValidateHeaderTime(header, now, parent, fixedSuccession{1}, config, cache) + assert.NoError(t, err) + }) +} From 4bd357f9164af9aee8a65f4061bad743115bb7bb Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 12 Mar 2026 16:10:27 -0300 Subject: [PATCH 2/3] upgrade lint to fix CI --- .golangci.yml | 13 +++++++++++++ erigon-lib/.golangci.yml | 7 +++++++ erigon-lib/tools/golangci_lint.sh | 4 +++- polygon/bor/bor_internal_test.go | 16 ++++++++-------- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/.golangci.yml b/.golangci.yml index f23d21cfc70..125ec98e0c5 100644 --- a/.golangci.yml +++ b/.golangci.yml @@ -79,6 +79,10 @@ linters: - preferStringWriter - commentedOutCode - preferFprint + - dupOption # new in golangci-lint v2.11.3; + - deprecatedComment # new in golangci-lint v2.11.3; + - zeroByteRepeat # new in golangci-lint v2.11.3; + - appendCombine # new in golangci-lint v2.11.3; enabled-tags: - performance - diagnostic @@ -118,11 +122,13 @@ linters: - gocritic - gosec - perfsprint + - prealloc - unused path: test\.go - linters: - gocritic - gosec + - prealloc - unused path: hack\.go - linters: @@ -138,6 +144,13 @@ linters: - gocritic - gosec path: p2p/dnsdisc + - linters: + - govet + text: "impossible condition|tautological condition" + path: _test\.go + - linters: + - prealloc + text: "Consider preallocating" paths: - third_party$ - builtin$ diff --git a/erigon-lib/.golangci.yml b/erigon-lib/.golangci.yml index 03ab2ad7b4f..da493a6760a 100644 --- a/erigon-lib/.golangci.yml +++ b/erigon-lib/.golangci.yml @@ -80,6 +80,10 @@ linters: - builtinShadowDecl - uncheckedInlineErr - preferStringWriter + - dupOption # new in golangci-lint v2.11.3; + - deprecatedComment # new in golangci-lint v2.11.3; + - zeroByteRepeat # new in golangci-lint v2.11.3; + - appendCombine # new in golangci-lint v2.11.3; enabled-tags: - performance - diagnostic @@ -145,6 +149,9 @@ linters: - gocritic - gosec path: metrics/sample\.go + - linters: + - prealloc + text: "Consider preallocating" paths: - third_party$ - builtin$ diff --git a/erigon-lib/tools/golangci_lint.sh b/erigon-lib/tools/golangci_lint.sh index c1db248d6ac..e88f7f685c9 100755 --- a/erigon-lib/tools/golangci_lint.sh +++ b/erigon-lib/tools/golangci_lint.sh @@ -2,7 +2,7 @@ scriptDir=$(dirname "${BASH_SOURCE[0]}") scriptName=$(basename "${BASH_SOURCE[0]}") -version="v2.4.0" +version="v2.11.3" if [[ "$1" == "--install-deps" ]] then @@ -10,6 +10,8 @@ then exit fi +export PATH="$PATH:$(go env GOPATH)/bin" + if ! which golangci-lint > /dev/null then echo "golangci-lint tool is not found, install it with:" diff --git a/polygon/bor/bor_internal_test.go b/polygon/bor/bor_internal_test.go index 3eef64b6471..eb09428f07d 100644 --- a/polygon/bor/bor_internal_test.go +++ b/polygon/bor/bor_internal_test.go @@ -142,12 +142,12 @@ func signTestHeader(t *testing.T, header *types.Header, config *borcfg.BorConfig // period=2, using the provided HF block numbers (nil = fork disabled). func newTestBorConfig(bhilaiBlock, giuglianoBlock *big.Int) *borcfg.BorConfig { return &borcfg.BorConfig{ - Period: map[string]uint64{"0": 2}, - ProducerDelay: map[string]uint64{"0": 1}, - Sprint: map[string]uint64{"0": 16}, + Period: map[string]uint64{"0": 2}, + ProducerDelay: map[string]uint64{"0": 1}, + Sprint: map[string]uint64{"0": 16}, BackupMultiplier: map[string]uint64{"0": 2}, - BhilaiBlock: bhilaiBlock, - GiuglianoBlock: giuglianoBlock, + BhilaiBlock: bhilaiBlock, + GiuglianoBlock: giuglianoBlock, } } @@ -165,9 +165,9 @@ func TestValidateHeaderTime_PreBhilai(t *testing.T) { now := time.Now() tests := []struct { - name string - headerTs uint64 - wantErr bool + name string + headerTs uint64 + wantErr bool }{ {"at now", uint64(now.Unix()), false}, {"1s past", uint64(now.Unix()) - 1, false}, From 209a344b4ccc64735998c9d29f8c331c2b042475 Mon Sep 17 00:00:00 2001 From: Lucca Martins Date: Thu, 12 Mar 2026 16:22:15 -0300 Subject: [PATCH 3/3] bumps lint on ci --- .github/workflows/lint.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml index 887e11c3fe1..d1e6329a396 100644 --- a/.github/workflows/lint.yml +++ b/.github/workflows/lint.yml @@ -31,6 +31,6 @@ jobs: - name: Install golangci-lint uses: golangci/golangci-lint-action@v8 with: - version: 'v2.4.0' + version: 'v2.11.3' - run: make lint