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
2 changes: 1 addition & 1 deletion .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
13 changes: 13 additions & 0 deletions .golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -118,11 +122,13 @@ linters:
- gocritic
- gosec
- perfsprint
- prealloc
- unused
path: test\.go
- linters:
- gocritic
- gosec
- prealloc
- unused
path: hack\.go
- linters:
Expand All @@ -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$
Expand Down
7 changes: 7 additions & 0 deletions erigon-lib/.golangci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Comment thread
lucca30 marked this conversation as resolved.
enabled-tags:
- performance
- diagnostic
Expand Down Expand Up @@ -145,6 +149,9 @@ linters:
- gocritic
- gosec
path: metrics/sample\.go
- linters:
- prealloc
text: "Consider preallocating"
paths:
- third_party$
- builtin$
Expand Down
4 changes: 3 additions & 1 deletion erigon-lib/tools/golangci_lint.sh
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,16 @@

scriptDir=$(dirname "${BASH_SOURCE[0]}")
scriptName=$(basename "${BASH_SOURCE[0]}")
version="v2.4.0"
version="v2.11.3"

if [[ "$1" == "--install-deps" ]]
then
curl -sSfL https://raw.githubusercontent.com/golangci/golangci-lint/master/install.sh | sh -s -- -b "$(go env GOPATH)/bin" "$version"
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:"
Expand Down
61 changes: 49 additions & 12 deletions polygon/bor/bor.go
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,8 @@ import (

const inmemorySignatures = 4096 // Number of recent block signatures to keep in memory

const maxAllowedFutureBlockTimeSeconds = uint64(30)
Comment thread
cffls marked this conversation as resolved.

// Bor protocol constants.
var (
// Default number of blocks after which to checkpoint and reset the pending votes
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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))
}
Expand Down Expand Up @@ -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))
}
}

Expand Down
191 changes: 191 additions & 0 deletions polygon/bor/bor_internal_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down Expand Up @@ -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)
})
}
Loading