diff --git a/builder/files/genesis-amoy.json b/builder/files/genesis-amoy.json index 99f4743f7e..9e58eaa75c 100644 --- a/builder/files/genesis-amoy.json +++ b/builder/files/genesis-amoy.json @@ -27,6 +27,7 @@ "madhugiriProBlock": 29287400, "dandeliBlock": 31890000, "lisovoBlock": 33634700, + "lisovoProBlock": 34062000, "skipValidatorByteCheck": [26160367, 26161087, 26171567, 26173743, 26175647], "stateSyncConfirmationDelay": { "0": 128 diff --git a/builder/files/genesis-mainnet-v1.json b/builder/files/genesis-mainnet-v1.json index db03197a09..f2e18ed80b 100644 --- a/builder/files/genesis-mainnet-v1.json +++ b/builder/files/genesis-mainnet-v1.json @@ -26,6 +26,8 @@ "madhugiriBlock": 80084800, "madhugiriProBlock": 80084800, "dandeliBlock": 81424000, + "lisovoBlock": 83756500, + "lisovoProBlock": 83756500, "stateSyncConfirmationDelay": { "44934656": 128 }, @@ -112,7 +114,8 @@ }, "burntContract": { "23850000": "0x70bca57f4579f58670ab2d18ef16e02c17553c38", - "50523000": "0x7A8ed27F4C30512326878652d20fC85727401854" + "50523000": "0x7A8ed27F4C30512326878652d20fC85727401854", + "83756500": "0x3ef57def668054dd750bd260526105c4eeef104f" } } }, @@ -161,4 +164,4 @@ "number": "0x0", "gasUsed": "0x0", "parentHash": "0x0000000000000000000000000000000000000000000000000000000000000000" -} \ No newline at end of file +} diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 4d7b609f64..aa85d31f8e 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -52,7 +52,7 @@ import ( ) const ( - defaultSpanLength = 6400 // Default span length i.e. number of bor blocks in a span + defaultSpanLength = params.DefaultSpanLength zerothSpanEnd = 255 // End block of 0th span checkpointInterval = 1024 // Number of blocks after which to save the vote snapshot to the database inmemorySnapshots = 128 // Number of recent vote snapshots to keep in memory @@ -265,6 +265,10 @@ type Bor struct { quit chan struct{} closeOnce sync.Once + + // ctx is cancelled when Close() is called, allowing in-flight operations to abort promptly. + ctx context.Context + ctxCancel context.CancelFunc } type signer struct { @@ -309,6 +313,8 @@ func New( // Create a new span store spanStore := NewSpanStore(heimdallClient, spanner, chainConfig.ChainID.String()) + ctx, ctxCancel := context.WithCancel(context.Background()) + c := &Bor{ chainConfig: chainConfig, config: borConfig, @@ -325,6 +331,8 @@ func New( DevFakeAuthor: devFakeAuthor, blockTime: blockTime, quit: make(chan struct{}), + ctx: ctx, + ctxCancel: ctxCancel, } c.authorizedSigner.Store(&signer{ @@ -543,10 +551,15 @@ func (c *Bor) verifyCascadingFields(chain consensus.ChainHeaderReader, header *t parent = chain.GetHeader(header.ParentHash, number-1) } - if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash { + if parent == nil || parent.Hash() != header.ParentHash { return consensus.ErrUnknownAncestor } + // Verify block number continuity + if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 { + return consensus.ErrInvalidNumber + } + // Verify that the gasUsed is <= gasLimit if header.GasUsed > header.GasLimit { return fmt.Errorf("invalid gasUsed: have %d, gasLimit %d", header.GasUsed, header.GasLimit) @@ -583,7 +596,7 @@ func (c *Bor) verifyCascadingFields(chain consensus.ChainHeaderReader, header *t // validation stateless, we use the span from heimdall (via span store) instead of // span from validator set genesis contract as both are supposed to be equivalent. if number > zerothSpanEnd && IsSprintStart(number+1, c.config.CalculateSprint(number)) { - span, err := c.spanStore.spanByBlockNumber(context.Background(), number+1) + span, err := c.spanStore.spanByBlockNumber(c.ctx, number+1) if err != nil { return err } @@ -693,7 +706,7 @@ func (c *Bor) snapshot(chain consensus.ChainHeaderReader, targetHeader *types.He hash := checkpoint.Hash() // get validators from span - span, err := c.spanStore.spanByBlockNumber(context.Background(), number+1) + span, err := c.spanStore.spanByBlockNumber(c.ctx, number+1) if err != nil { return nil, err } @@ -772,7 +785,7 @@ func (c *Bor) getVeBlopSnapshot(chain consensus.ChainHeaderReader, targetHeader } } - span, err := c.spanStore.spanByBlockNumber(context.Background(), number) + span, err := c.spanStore.spanByBlockNumber(c.ctx, number) if err != nil { return nil, err } @@ -1080,7 +1093,7 @@ func (c *Bor) Prepare(chain consensus.ChainHeaderReader, header *types.Header, w } now := time.Now() - if header.Time < uint64(now.Unix()) { + if now.After(header.GetActualTime()) { additionalBlockTime := time.Duration(c.config.CalculatePeriod(number)) * time.Second if c.blockTime > 0 && c.config.IsRio(header.Number) { additionalBlockTime = c.blockTime @@ -1469,6 +1482,7 @@ func (c *Bor) APIs(chain consensus.ChainHeaderReader) []rpc.API { // Close implements consensus.Engine. func (c *Bor) Close() error { c.closeOnce.Do(func() { + c.ctxCancel() close(c.quit) if c.HeimdallClient != nil { c.HeimdallClient.Close() @@ -1490,7 +1504,9 @@ func (c *Bor) runMilestoneFetcher() { select { case <-ticker.C: if c.HeimdallClient != nil { - milestone, err := c.HeimdallClient.FetchMilestone(context.Background()) + ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second) + milestone, err := c.HeimdallClient.FetchMilestone(ctx) + cancel() if err != nil { log.Warn("Error while fetching milestone", "error", err) continue @@ -1692,9 +1708,9 @@ func (c *Bor) CommitStates( var eventRecords []*clerk.EventRecordWithTime // Wait for heimdall to be synced before fetching state sync events - c.spanStore.waitUntilHeimdallIsSynced(context.Background()) + c.spanStore.waitUntilHeimdallIsSynced(c.ctx) - eventRecords, err = c.HeimdallClient.StateSyncEvents(context.Background(), from, to.Unix()) + eventRecords, err = c.HeimdallClient.StateSyncEvents(c.ctx, from, to.Unix()) if err != nil { log.Error("Error occurred when fetching state sync events", "fromID", from, "to", to.Unix(), "err", err) diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 58c19a6689..f4b85bcb90 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -14,7 +14,7 @@ import ( "github.com/stretchr/testify/require" lru "github.com/hashicorp/golang-lru" - ttlcache "github.com/jellydator/ttlcache/v3" + "github.com/jellydator/ttlcache/v3" "github.com/ethereum/go-ethereum/accounts" "github.com/ethereum/go-ethereum/common" @@ -38,6 +38,8 @@ import ( borTypes "github.com/0xPolygon/heimdall-v2/x/bor/types" stakeTypes "github.com/0xPolygon/heimdall-v2/x/stake/types" ctypes "github.com/cometbft/cometbft/rpc/core/types" + "github.com/ethereum/go-ethereum/tests/bor/mocks" + "go.uber.org/mock/gomock" ) // fakeSpanner implements Spanner for tests @@ -174,7 +176,8 @@ func newAllForksChainConfig(borCfg *params.BorConfig) *params.ChainConfig { func newChainAndBorForTestWithConfig(t *testing.T, sp Spanner, cfg *params.ChainConfig, devFake bool, signerAddr common.Address, genesisTime uint64, genOpts ...func(*core.Genesis)) (*core.BlockChain, *Bor) { t.Helper() - b := &Bor{chainConfig: cfg, config: cfg.Bor, DevFakeAuthor: devFake} + ctx, ctxCancel := context.WithCancel(context.Background()) + b := &Bor{chainConfig: cfg, config: cfg.Bor, DevFakeAuthor: devFake, ctx: ctx, ctxCancel: ctxCancel} b.db = rawdb.NewMemoryDatabase() b.recents = ttlcache.New( ttlcache.WithTTL[common.Hash, *Snapshot](veblopBlockTimeout), @@ -4373,3 +4376,320 @@ func TestBorPrepare_WaitOnPrepareFlag(t *testing.T) { t.Logf("Both waitOnPrepare modes produce compatible headers for block %d", header1.Number.Uint64()) }) } + +func newBorForMilestoneFetcherTest(t *testing.T) *Bor { + t.Helper() + sp := &fakeSpanner{vals: []*valset.Validator{{Address: common.HexToAddress("0x1"), VotingPower: 1}}} + borCfg := ¶ms.BorConfig{Sprint: map[string]uint64{"0": 64}, Period: map[string]uint64{"0": 2}} + _, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, uint64(time.Now().Unix())) + b.quit = make(chan struct{}) + return b +} + +func TestRunMilestoneFetcher_ContextHasDeadline(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + client := mocks.NewMockIHeimdallClient(ctrl) + + gotDeadline := make(chan bool, 1) + + client.EXPECT().FetchMilestone(gomock.Any()).DoAndReturn(func(ctx context.Context) (*milestone.Milestone, error) { + _, ok := ctx.Deadline() + gotDeadline <- ok + return nil, errors.New("not available") + }).AnyTimes() + + b := newBorForMilestoneFetcherTest(t) + b.HeimdallClient = client + + go b.runMilestoneFetcher() + defer close(b.quit) + + select { + case hasDeadline := <-gotDeadline: + require.True(t, hasDeadline, "context passed to FetchMilestone must have a deadline") + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for FetchMilestone to be called") + } +} + +func TestRunMilestoneFetcher_StoresMilestoneBlock(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + client := mocks.NewMockIHeimdallClient(ctrl) + + client.EXPECT().FetchMilestone(gomock.Any()).Return(&milestone.Milestone{EndBlock: 12345}, nil).AnyTimes() + + b := newBorForMilestoneFetcherTest(t) + b.HeimdallClient = client + + go b.runMilestoneFetcher() + defer close(b.quit) + + require.Eventually(t, func() bool { + return b.latestMilestoneBlock.Load() == 12345 + }, 5*time.Second, 50*time.Millisecond) +} + +func TestRunMilestoneFetcher_ErrorDoesNotUpdateBlock(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + client := mocks.NewMockIHeimdallClient(ctrl) + + called := make(chan struct{}, 1) + + client.EXPECT().FetchMilestone(gomock.Any()).DoAndReturn(func(ctx context.Context) (*milestone.Milestone, error) { + select { + case called <- struct{}{}: + default: + } + return nil, errors.New("heimdall unreachable") + }).AnyTimes() + + b := newBorForMilestoneFetcherTest(t) + b.HeimdallClient = client + + go b.runMilestoneFetcher() + defer close(b.quit) + + select { + case <-called: + case <-time.After(5 * time.Second): + t.Fatal("timed out waiting for FetchMilestone to be called") + } + + // Give an extra tick to ensure no late update + time.Sleep(100 * time.Millisecond) + require.Equal(t, uint64(0), b.latestMilestoneBlock.Load()) +} + +func TestRunMilestoneFetcher_BlockingCallRespectsTimeout(t *testing.T) { + t.Parallel() + + ctrl := gomock.NewController(t) + client := mocks.NewMockIHeimdallClient(ctrl) + + callReturned := make(chan struct{}, 1) + + client.EXPECT().FetchMilestone(gomock.Any()).DoAndReturn(func(ctx context.Context) (*milestone.Milestone, error) { + // Simulate unreachable Heimdall: block until context is cancelled + <-ctx.Done() + select { + case callReturned <- struct{}{}: + default: + } + return nil, ctx.Err() + }).AnyTimes() + + b := newBorForMilestoneFetcherTest(t) + b.HeimdallClient = client + + go b.runMilestoneFetcher() + defer close(b.quit) + + // The context timeout is 30s; the blocked call should eventually return. + // We use a generous test timeout but this verifies the call doesn't block forever. + select { + case <-callReturned: + // Success: the blocking FetchMilestone returned because the context timed out + case <-time.After(35 * time.Second): + t.Fatal("FetchMilestone blocked beyond the context timeout; goroutine would leak without the fix") + } +} + +// TestSubSecondLateBlockTriggersTimeAdjustment verifies that when a block's target +// time has already passed (even by sub-second), the late-block adjustment triggers +// and pushes header.Time into the future to give the miner real build time. +// +// Without the fix, the integer comparison `header.Time < now.Unix()` misses the case +// where header.Time == now.Unix() but the sub-second target has already passed. This +// causes the interrupt timer to expire immediately and Pending() to return an empty +// map, producing a block with 0 transactions. +func TestSubSecondLateBlockTriggersTimeAdjustment(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + + // waitForMidSecond spins until we're between 300ms-700ms into the current + // second. This ensures the 200ms sub-second offset used by the tests won't + // cross a second boundary, which would make the old integer comparison + // trigger regardless of the fix. + waitForMidSecond := func() { + for { + ms := time.Now().Nanosecond() / 1_000_000 + if ms >= 300 && ms <= 700 { + return + } + time.Sleep(10 * time.Millisecond) + } + } + + t.Run("default path without custom blockTime", func(t *testing.T) { + t.Parallel() + + // Consensus period = 1s, no custom blockTime. + // This is the path where ActualTime is never set (stays zero) + // and GetActualTime() falls back to time.Unix(header.Time, 0). + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 1}, + } + + waitForMidSecond() + now := time.Now() + + // Set genesis time so that header.Time = genesis.Time + period = now.Unix() + // (the block target is the start of the current second, already in the past). + genesisTime := uint64(now.Unix()) - 1 + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, genesisTime) + + 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, false) + require.NoError(t, err) + + expectedMin := uint64(before.Add(1 * time.Second).Unix()) + require.GreaterOrEqual(t, header.Time, expectedMin, + "header.Time should be at least now + period to provide build time") + }) + + t.Run("custom blockTime with Rio", func(t *testing.T) { + t.Parallel() + + blockTimeDuration := 2 * time.Second + 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), + } + + waitForMidSecond() + now := time.Now() + + // Set parent's cached ActualTime so that: + // actualNewBlockTime = parentActualTime + blockTime = now - 200ms + // This is sub-second in the past, but truncated header.Time equals now.Unix(). + parentActualTime := now.Add(-blockTimeDuration).Add(-200 * time.Millisecond) + genesisTime := uint64(parentActualTime.Unix()) + + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, genesisTime) + b.blockTime = blockTimeDuration + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, genesis) + + // Cache the parent ActualTime with sub-second precision + b.parentActualTimeCache.Add(genesis.Hash(), parentActualTime) + + header := &types.Header{ + Number: big.NewInt(1), + ParentHash: genesis.Hash(), + } + + before := time.Now() + err := b.Prepare(chain.HeaderChain(), header, false) + require.NoError(t, err) + + require.False(t, header.ActualTime.IsZero(), + "ActualTime should be set for Rio with custom blockTime") + require.True(t, header.ActualTime.After(before), + "ActualTime should be in the future after adjustment, got %v which is before %v", + header.ActualTime, before) + + expectedMin := before.Add(blockTimeDuration) + require.GreaterOrEqual(t, header.ActualTime.Unix(), expectedMin.Unix(), + "ActualTime should be at least now + blockTime to provide build time") + }) +} + +func TestVerifyHeaderRejectsInvalidBlockNumber(t *testing.T) { + t.Parallel() + + privKey, err := crypto.GenerateKey() + require.NoError(t, err) + + signerAddr := crypto.PubkeyToAddress(privKey.PublicKey) + + sp := &fakeSpanner{ + vals: []*valset.Validator{ + {Address: signerAddr, VotingPower: 1}, + }, + } + + borCfg := ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 64}, + Period: map[string]uint64{"0": 2}, + } + + // Use a fixed past timestamp to avoid "block in the future" errors + chain, b := newChainAndBorForTest(t, sp, borCfg, false, common.Address{}, 1600000000) + + parent := chain.HeaderChain().GetHeaderByNumber(0) + require.NotNil(t, parent) + + // Block number that skips ahead (non-contiguous) + header := &types.Header{ + ParentHash: parent.Hash(), + Number: big.NewInt(10), // Should be 1 + Time: parent.Time + 1000, + Difficulty: big.NewInt(2), + Extra: make([]byte, 32+65), + UncleHash: types.EmptyUncleHash, + GasLimit: parent.GasLimit, + BaseFee: parent.BaseFee, + } + + sigHash := SealHash(header, borCfg) + sig, err := crypto.Sign(sigHash.Bytes(), privKey) + require.NoError(t, err) + copy(header.Extra[len(header.Extra)-65:], sig) + + err = b.VerifyHeader(chain.HeaderChain(), header) + if err == nil { + t.Fatal("expected VerifyHeader to reject non-contiguous block number") + } + if !errors.Is(err, consensus.ErrInvalidNumber) { + t.Fatalf("expected ErrInvalidNumber, got %v", err) + } + + // Test overflow case: parent + 1 + 2^64 (would pass with uint64 truncation) + overflow := new(big.Int).Lsh(big.NewInt(1), 64) + overflow.Add(overflow, parent.Number) + overflow.Add(overflow, big.NewInt(1)) + + header2 := &types.Header{ + ParentHash: parent.Hash(), + Number: overflow, + Time: parent.Time + 1000, + Difficulty: big.NewInt(2), + Extra: make([]byte, 32+65), + UncleHash: types.EmptyUncleHash, + GasLimit: parent.GasLimit, + BaseFee: parent.BaseFee, + } + + sigHash2 := SealHash(header2, borCfg) + sig2, err := crypto.Sign(sigHash2.Bytes(), privKey) + require.NoError(t, err) + copy(header2.Extra[len(header2.Extra)-65:], sig2) + + err = b.VerifyHeader(chain.HeaderChain(), header2) + if err == nil { + t.Fatal("expected VerifyHeader to reject overflow block number") + } + if !errors.Is(err, consensus.ErrInvalidNumber) { + t.Fatalf("expected ErrInvalidNumber for overflow, got %v", err) + } +} diff --git a/consensus/bor/heimdallgrpc/state_sync.go b/consensus/bor/heimdallgrpc/state_sync.go index f4b38d3d41..fa4098ddd3 100644 --- a/consensus/bor/heimdallgrpc/state_sync.go +++ b/consensus/bor/heimdallgrpc/state_sync.go @@ -14,17 +14,21 @@ import ( "github.com/0xPolygon/heimdall-v2/x/clerk/types" ) +const ( + stateSyncTotalTimeout = 1 * time.Minute +) + func (h *HeimdallGRPCClient) StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) { log.Info("Fetching state sync events", "fromID", fromID, "to", to) var err error - ctxWithTimeout, cancel := context.WithTimeout(ctx, defaultTimeout) + globalCtx, cancel := context.WithTimeout(ctx, stateSyncTotalTimeout) defer cancel() // Start the timer and set the request type on the context. start := time.Now() - ctx = heimdall.WithRequestType(ctxWithTimeout, heimdall.StateSyncRequest) + ctx = heimdall.WithRequestType(globalCtx, heimdall.StateSyncRequest) // Defer the metrics call. defer func() { @@ -33,36 +37,47 @@ func (h *HeimdallGRPCClient) StateSyncEvents(ctx context.Context, fromID uint64, eventRecords := make([]*clerk.EventRecordWithTime, 0) - pagination := query.PageRequest{ - Limit: stateFetchLimit, - } + for { + pagination := query.PageRequest{ + Limit: stateFetchLimit, + } - req := &types.RecordListWithTimeRequest{ - FromId: fromID, - ToTime: time.Unix(to, 0), - Pagination: pagination, - } + req := &types.RecordListWithTimeRequest{ + FromId: fromID, + ToTime: time.Unix(to, 0), + Pagination: pagination, + } - res, err := h.clerkQueryClient.GetRecordListWithTime(ctx, req) - if err != nil { - return nil, err - } + var res *types.RecordListWithTimeResponse + pageCtx, pageCancel := context.WithTimeout(ctx, defaultTimeout) + res, err = h.clerkQueryClient.GetRecordListWithTime(pageCtx, req) + pageCancel() + if err != nil { + return nil, err + } - events := res.GetEventRecords() - - for _, event := range events { - eventRecord := &clerk.EventRecordWithTime{ - EventRecord: clerk.EventRecord{ - ID: event.Id, - Contract: common.HexToAddress(event.Contract), - Data: event.Data, - TxHash: common.HexToHash(event.TxHash), - LogIndex: event.LogIndex, - ChainID: event.BorChainId, - }, - Time: event.RecordTime, + events := res.GetEventRecords() + + for _, event := range events { + eventRecord := &clerk.EventRecordWithTime{ + EventRecord: clerk.EventRecord{ + ID: event.Id, + Contract: common.HexToAddress(event.Contract), + Data: event.Data, + TxHash: common.HexToHash(event.TxHash), + LogIndex: event.LogIndex, + ChainID: event.BorChainId, + }, + Time: event.RecordTime, + } + eventRecords = append(eventRecords, eventRecord) } - eventRecords = append(eventRecords, eventRecord) + + if len(events) < stateFetchLimit { + break + } + + fromID += uint64(stateFetchLimit) } log.Info("Fetched state sync events", "fromID", fromID, "to", to) diff --git a/consensus/clique/clique.go b/consensus/clique/clique.go index 1a437a5e4e..6201a00d76 100644 --- a/consensus/clique/clique.go +++ b/consensus/clique/clique.go @@ -353,10 +353,15 @@ func (c *Clique) verifyCascadingFields(chain consensus.ChainHeaderReader, header parent = chain.GetHeader(header.ParentHash, number-1) } - if parent == nil || parent.Number.Uint64() != number-1 || parent.Hash() != header.ParentHash { + if parent == nil || parent.Hash() != header.ParentHash { return consensus.ErrUnknownAncestor } + // Verify block number continuity + if diff := new(big.Int).Sub(header.Number, parent.Number); diff.Cmp(big.NewInt(1)) != 0 { + return consensus.ErrInvalidNumber + } + if parent.Time+c.config.Period > header.Time { return errInvalidTimestamp } diff --git a/consensus/clique/clique_test.go b/consensus/clique/clique_test.go index 157a46c130..65d826c1aa 100644 --- a/consensus/clique/clique_test.go +++ b/consensus/clique/clique_test.go @@ -17,10 +17,12 @@ package clique import ( + "errors" "math/big" "testing" "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/core" "github.com/ethereum/go-ethereum/core/rawdb" "github.com/ethereum/go-ethereum/core/types" @@ -128,3 +130,77 @@ func TestSealHash(t *testing.T) { t.Errorf("have %x, want %x", have, want) } } + +func TestVerifyHeaderRejectsInvalidBlockNumber(t *testing.T) { + var ( + db = rawdb.NewMemoryDatabase() + key, _ = crypto.HexToECDSA("b71c71a67e1177ad4e901695e1b4b9ee17ae16c6668d313eac2f96dbcda3f291") + addr = crypto.PubkeyToAddress(key.PublicKey) + engine = New(params.AllCliqueProtocolChanges.Clique, db) + ) + + genspec := &core.Genesis{ + Config: params.AllCliqueProtocolChanges, + ExtraData: make([]byte, extraVanity+common.AddressLength+extraSeal), + Alloc: map[common.Address]types.Account{ + addr: {Balance: big.NewInt(10000000000000000)}, + }, + BaseFee: big.NewInt(params.InitialBaseFee), + } + copy(genspec.ExtraData[extraVanity:], addr[:]) + + chain, _ := core.NewBlockChain(rawdb.NewMemoryDatabase(), genspec, engine, nil) + defer chain.Stop() + + parent := chain.CurrentBlock() + + // Block number that skips ahead (non-contiguous) + header := &types.Header{ + ParentHash: parent.Hash(), + Number: big.NewInt(10), // Should be 1 + Time: parent.Time + 1, + Difficulty: diffInTurn, + Extra: make([]byte, extraVanity+extraSeal), + UncleHash: types.EmptyUncleHash, + GasLimit: parent.GasLimit, + BaseFee: parent.BaseFee, + } + + sig, _ := crypto.Sign(SealHash(header).Bytes(), key) + copy(header.Extra[len(header.Extra)-extraSeal:], sig) + + err := engine.VerifyHeader(chain, header) + if err == nil { + t.Fatal("expected VerifyHeader to reject non-contiguous block number") + } + if !errors.Is(err, consensus.ErrInvalidNumber) { + t.Fatalf("expected ErrInvalidNumber, got %v", err) + } + + // Test overflow case: parent + 1 + 2^64 (would pass with uint64 truncation) + overflow := new(big.Int).Lsh(big.NewInt(1), 64) + overflow.Add(overflow, parent.Number) + overflow.Add(overflow, big.NewInt(1)) + + header2 := &types.Header{ + ParentHash: parent.Hash(), + Number: overflow, + Time: parent.Time + 1, + Difficulty: diffInTurn, + Extra: make([]byte, extraVanity+extraSeal), + UncleHash: types.EmptyUncleHash, + GasLimit: parent.GasLimit, + BaseFee: parent.BaseFee, + } + + sig2, _ := crypto.Sign(SealHash(header2).Bytes(), key) + copy(header2.Extra[len(header2.Extra)-extraSeal:], sig2) + + err = engine.VerifyHeader(chain, header2) + if err == nil { + t.Fatal("expected VerifyHeader to reject overflow block number") + } + if !errors.Is(err, consensus.ErrInvalidNumber) { + t.Fatalf("expected ErrInvalidNumber for overflow, got %v", err) + } +} diff --git a/consensus/misc/eip1559/eip1559.go b/consensus/misc/eip1559/eip1559.go index c5b88d3329..7137ded60f 100644 --- a/consensus/misc/eip1559/eip1559.go +++ b/consensus/misc/eip1559/eip1559.go @@ -53,6 +53,11 @@ func VerifyEIP1559Header(config *params.ChainConfig, parent, header *types.Heade if header.BaseFee == nil { return errors.New("header is missing baseFee") } + // Verify the parent header is not malformed + if config.IsLondon(parent.Number) && parent.BaseFee == nil { + return errors.New("parent header is missing baseFee") + } + // Verify the baseFee is correct based on the parent header. // Post-Lisovo: Validate that base fee changes are within allowed boundaries if config.Bor != nil && config.Bor.IsLisovo(header.Number) { @@ -105,7 +110,7 @@ func verifyBaseFeeWithinBoundaries(parent, header *types.Header) error { // CalcBaseFee calculates the basefee of the header. func CalcBaseFee(config *params.ChainConfig, parent *types.Header) *big.Int { // If the current block is the first EIP-1559 block, return the InitialBaseFee. - if !config.IsLondon(parent.Number) { + if !config.IsLondon(parent.Number) || parent.BaseFee == nil { return new(big.Int).SetUint64(params.InitialBaseFee) } diff --git a/consensus/misc/eip1559/eip1559_test.go b/consensus/misc/eip1559/eip1559_test.go index 1305c8df14..d675cbfefb 100644 --- a/consensus/misc/eip1559/eip1559_test.go +++ b/consensus/misc/eip1559/eip1559_test.go @@ -235,6 +235,177 @@ func TestCalcBaseFeeBhilai(t *testing.T) { } } +// TestCalcBaseFeeNilParent tests that CalcBaseFee doesn't panic when +// the parent's BaseFee is nil. +func TestCalcBaseFeeNilParent(t *testing.T) { + t.Parallel() + + testConfig := config() + + t.Run("nil baseFee for post-London parent returns InitialBaseFee", func(t *testing.T) { + // Create a post-London parent header with nil BaseFee + parent := &types.Header{ + Number: big.NewInt(6), // Post-London because LondonBlock is 5 in test config + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, + } + + // CalcBaseFee should not panic but return InitialBaseFee + result := CalcBaseFee(testConfig, parent) + expected := big.NewInt(params.InitialBaseFee) + + require.NotNil(t, result, "CalcBaseFee should not return nil") + require.Equal(t, expected, result, + "CalcBaseFee should return InitialBaseFee when the parent's BaseFee is nil for post-London block") + }) + + t.Run("pre-London parent with nil baseFee returns InitialBaseFee", func(t *testing.T) { + // Pre-London blocks should have nil BaseFee anyway + parent := &types.Header{ + Number: big.NewInt(4), // Pre-London because LondonBlock is 5 in test config + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, + } + + result := CalcBaseFee(testConfig, parent) + expected := big.NewInt(params.InitialBaseFee) + + require.NotNil(t, result, "CalcBaseFee should not return nil") + require.Equal(t, expected, result, + "CalcBaseFee should return InitialBaseFee for first EIP-1559 block") + }) +} + +// TestVerifyEIP1559HeaderNilParentBaseFee tests that VerifyEIP1559Header rejects post-London parents with nil BaseFee. +func TestVerifyEIP1559HeaderNilParentBaseFee(t *testing.T) { + t.Parallel() + + testConfig := config() + + t.Run("post-London parent with nil BaseFee is rejected", func(t *testing.T) { + // Malicious parent: post-London block with nil BaseFee + parent := &types.Header{ + Number: big.NewInt(6), // Post-London (LondonBlock is 5) + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, + } + + // Child header with valid BaseFee + child := &types.Header{ + Number: big.NewInt(7), + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + // VerifyEIP1559Header must reject due to nil parent's BaseFee + err := VerifyEIP1559Header(testConfig, parent, child) + require.Error(t, err, "VerifyEIP1559Header must reject nil parent's BaseFee") + require.Contains(t, err.Error(), "parent header is missing baseFee", + "Error message should indicate parent BaseFee is missing") + }) + + t.Run("pre-London parent with nil BaseFee is accepted", func(t *testing.T) { + parent := &types.Header{ + Number: big.NewInt(4), // Pre-London (LondonBlock is 5) + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, // Expected for pre-London blocks + } + + child := &types.Header{ + Number: big.NewInt(5), // LondonBlock + GasLimit: 40000000, // parent.GasLimit * elasticityMultiplier = 20M * 2 + GasUsed: 20000000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + err := VerifyEIP1559Header(testConfig, parent, child) + require.NoError(t, err, "First London block with InitialBaseFee should be accepted") + }) + + t.Run("post-London parent with valid BaseFee is accepted", func(t *testing.T) { + // Valid parent + parent := &types.Header{ + Number: big.NewInt(6), // Post-London (LondonBlock is 5) + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + // Valid child + expectedBaseFee := CalcBaseFee(testConfig, parent) + child := &types.Header{ + Number: big.NewInt(7), + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: expectedBaseFee, + } + + err := VerifyEIP1559Header(testConfig, parent, child) + require.NoError(t, err, "Valid parent and child should be accepted") + }) +} + +// TestBatchVerification tests that if a peer sends header batch [A, B] where A has nil BaseFee and future +// timestamp, and B is a child of A, it should not panic but return an error. +func TestBatchVerification(t *testing.T) { + t.Parallel() + + testConfig := config() + + t.Run("batch A->B does not panic", func(t *testing.T) { + // Header A: post-London, nil BaseFee, forwarded to child verification in batch + headerA := &types.Header{ + Number: big.NewInt(6), // Post-London (LondonBlock is 5) + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, + } + + // Header B: child of A, trying to exploit the nil BaseFee + headerB := &types.Header{ + Number: big.NewInt(7), + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + // verify B with A as parent doesn't panic but returns the expected error + var err error + require.NotPanics(t, func() { + err = VerifyEIP1559Header(testConfig, headerA, headerB) + }, "VerifyEIP1559Header must not panic when parent.BaseFee is nil") + + require.Error(t, err, "VerifyEIP1559Header must reject child when parent.BaseFee is nil") + require.Contains(t, err.Error(), "parent header is missing baseFee", + "Error must indicate parent BaseFee issue") + }) + + t.Run("CalcBaseFee called directly does not panic", func(t *testing.T) { + // Header with nil BaseFee + header := &types.Header{ + Number: big.NewInt(6), // Post-London (LondonBlock is 5) + GasLimit: 20000000, + GasUsed: 10000000, + BaseFee: nil, + } + + // CalcBaseFee doesn't panic + var result *big.Int + require.NotPanics(t, func() { + result = CalcBaseFee(testConfig, header) + }, "CalcBaseFee must not panic when parent.BaseFee is nil") + + require.NotNil(t, result, "CalcBaseFee should return non-nil result") + require.Equal(t, big.NewInt(params.InitialBaseFee), result, + "CalcBaseFee should return InitialBaseFee as fallback") + }) +} + func TestCalcParentGasTarget(t *testing.T) { t.Parallel() diff --git a/consensus/misc/eip4844/eip4844.go b/consensus/misc/eip4844/eip4844.go index d4e30c10a6..edb83501da 100644 --- a/consensus/misc/eip4844/eip4844.go +++ b/consensus/misc/eip4844/eip4844.go @@ -123,6 +123,9 @@ func VerifyEIP4844Header(config *params.ChainConfig, parent, header *types.Heade func CalcExcessBlobGas(config *params.ChainConfig, parent *types.Header, headTimestamp uint64) uint64 { isOsaka := config.IsOsaka(config.LondonBlock) bcfg := latestBlobConfig(config, headTimestamp) + if bcfg == nil { + return 0 + } return calcExcessBlobGas(isOsaka, bcfg, parent) } diff --git a/core/blockchain.go b/core/blockchain.go index 8b092d27c3..af4becaf84 100644 --- a/core/blockchain.go +++ b/core/blockchain.go @@ -240,7 +240,12 @@ type BlockChainConfig struct { // This defines the cutoff block for history expiry. // Blocks before this number may be unavailable in the chain database. HistoryPruningCutoff uint64 - Stateless bool // Whether the node is in stateless mode + + // Whether the node is in stateless mode or not. + Stateless bool + + // MilestoneFetcher returns the latest milestone end block from Heimdall. + MilestoneFetcher func(ctx context.Context) (uint64, error) } // DefaultConfig returns the default config. @@ -403,14 +408,14 @@ type BlockChain struct { stateSizer *state.SizeTracker // State size tracking // Bor related changes - borReceiptsCache *lru.Cache[common.Hash, *types.Receipt] // Cache for the most recent bor receipt receipts per block - stateSyncMu sync.RWMutex // Mutex to protect the stateSyncData access - borReceiptsRLPCache *lru.Cache[common.Hash, rlp.RawValue] // Cache for the most recent bor receipt RLPs per block - stateSyncData []*types.StateSyncData // State sync data - stateSyncFeed event.Feed // State sync feed - chain2HeadFeed event.Feed // Reorg/NewHead/Fork data feed - chainSideFeed event.Feed // Side chain data feed (removed from geth but needed in bor) - checker ethereum.ChainValidator + borReceiptsCache *lru.Cache[common.Hash, *types.Receipt] // Cache for the most recent bor receipt receipts per block + stateSyncMu sync.RWMutex // Mutex to protect the stateSyncData access + borReceiptsRLPCache *lru.Cache[common.Hash, rlp.RawValue] // Cache for the most recent bor receipt RLPs per block + stateSyncData []*types.StateSyncData // State sync data + stateSyncFeed event.Feed // State sync feed + chain2HeadFeed event.Feed // Reorg/NewHead/Fork data feed + chainSideFeed event.Feed // Side chain data feed (removed from geth but needed in bor) + milestoneFetcher func(ctx context.Context) (uint64, error) // Function to fetch the latest milestone end block from Heimdall. } // NewBlockChain returns a fully initialised block chain using information @@ -465,7 +470,7 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, borReceiptsCache: lru.NewCache[common.Hash, *types.Receipt](receiptsCacheLimit), borReceiptsRLPCache: lru.NewCache[common.Hash, rlp.RawValue](receiptsCacheLimit), logger: cfg.VmConfig.Tracer, - checker: cfg.Checker, + milestoneFetcher: cfg.MilestoneFetcher, } bc.hc, err = NewHeaderChain(db, chainConfig, engine, bc.insertStopped) @@ -587,11 +592,6 @@ func NewBlockChain(db ethdb.Database, genesis *Genesis, engine consensus.Engine, } } } - // The first thing the node will do is reconstruct the verification data for - // the head block (ethash cache or clique voting snapshot). Might as well do - // it in advance. - // BOR - commented out intentionally - // bc.engine.VerifyHeader(bc, bc.CurrentHeader()) // Check the current state of the block hashes and make sure that we do not have any of the bad blocks in our chain for hash := range BadHashes { @@ -4228,9 +4228,9 @@ func (bc *BlockChain) ProcessBlockWithWitnesses(block *types.Block, witness *sta // verifies headers after the latest finalized block and rewinds the chain if // invalid headers are detected. func (bc *BlockChain) startHeaderVerificationLoop() { - if bc.checker == nil { - log.Warn("chain validator service is not set, skipping header verification loop") - return // No checker available + if bc.milestoneFetcher == nil { + log.Warn("milestone fetcher is not set, skipping header verification loop") + return } bc.wg.Add(1) @@ -4239,7 +4239,7 @@ func (bc *BlockChain) startHeaderVerificationLoop() { ticker := time.NewTicker(2 * time.Second) defer ticker.Stop() - log.Info("Started header verification loop") + log.Info("Starting header verification loop") for { select { @@ -4253,51 +4253,57 @@ func (bc *BlockChain) startHeaderVerificationLoop() { }() } -// verifyPendingHeaders checks headers after the latest finalized block -// and rewinds the chain if invalid headers are found. +// verifyPendingHeaders fetches the latest milestone from Heimdall and verifies +// all headers between that milestone's end block and the current chain head. If an invalid +// header is found, the chain is rewound to the last valid block. func (bc *BlockChain) verifyPendingHeaders() { - // Get the latest finalized block - hasMilestone, milestoneNumber, _ := bc.checker.GetWhitelistedMilestone() - if !hasMilestone { - return // No finalized block yet - } - currentHead := bc.CurrentBlock() - if currentHead.Number.Uint64() <= milestoneNumber { - return // Nothing to verify - } chainConfig := bc.Config() - - // We don't need to verify headers before Rio if chainConfig.Bor == nil || !chainConfig.Bor.IsRio(currentHead.Number) { - return // Rio is not enabled yet + return } - // Collect headers from finalized block + 1 to current head - var headers []*types.Header - for i := milestoneNumber + 1; i <= currentHead.Number.Uint64(); i++ { + ctx, cancel := context.WithTimeout(context.Background(), time.Second) + defer cancel() + + milestoneEndBlock, err := bc.milestoneFetcher(ctx) + if err != nil { + log.Error("Failed to fetch milestone end block from Heimdall for header verification", "err", err) + return + } + + headNumber := currentHead.Number.Uint64() + if milestoneEndBlock >= headNumber { + return // Still syncing or synced to the milestone end block, nothing to verify. + } + + startBlock := milestoneEndBlock + 1 + + // Collect headers from startBlock to current head. + headers := make([]*types.Header, 0, headNumber-startBlock+1) + for i := startBlock; i <= headNumber; i++ { header := bc.GetHeaderByNumber(i) if header == nil { log.Debug("Missing header during verification", "number", i) - return // Missing header, skip verification + return } headers = append(headers, header) } if len(headers) == 0 { + log.Debug("No headers to verify") return } - log.Debug("Verifying pending headers", "from", headers[0].Number.Uint64(), - "to", headers[len(headers)-1].Number.Uint64(), "count", len(headers)) + log.Debug("Verifying pending headers", + "from", headers[0].Number.Uint64(), "to", headers[len(headers)-1].Number.Uint64(), "count", len(headers)) - // Verify headers abort, results := bc.engine.VerifyHeaders(bc, headers) defer close(abort) - // Check results and find the last valid header - lastValidNumber := milestoneNumber + // Check results and find the last valid header. + lastValidNumber := milestoneEndBlock for _, header := range headers { select { case <-bc.quit: @@ -4306,12 +4312,18 @@ func (bc *BlockChain) verifyPendingHeaders() { if err != nil { log.Warn("Invalid header detected during background verification", "number", header.Number.Uint64(), "hash", header.Hash(), "err", err) - // Rewind to the last valid block - if lastValidNumber < currentHead.Number.Uint64() { - log.Warn("Rewinding chain due to invalid header", - "from", currentHead.Number.Uint64(), "to", lastValidNumber) + + if lastValidNumber < headNumber { + dropCount := int64(headNumber - lastValidNumber) + + log.Warn("Rewinding chain due to an invalid header", + "from", headNumber, "to", lastValidNumber, "drop", dropCount) + if err := bc.SetHead(lastValidNumber); err != nil { - log.Error("Failed to rewind chain", "err", err) + log.Error("Failed to rewind chain to the last valid header", "err", err) + } else { + blockReorgMeter.Mark(1) + blockReorgDropMeter.Mark(dropCount) } } return diff --git a/core/blockchain_test.go b/core/blockchain_test.go index 34f5538774..7d4b7f708e 100644 --- a/core/blockchain_test.go +++ b/core/blockchain_test.go @@ -4737,50 +4737,6 @@ func TestPragueRequests(t *testing.T) { } } -// mockChainValidator is a mock implementation of ethereum.ChainValidator for testing -type mockChainValidator struct { - hasMilestone bool - milestoneNumber uint64 - milestoneHash common.Hash -} - -func (m *mockChainValidator) IsValidPeer(fetchHeadersByNumber func(number uint64, amount int, skip int, reverse bool) ([]*types.Header, []common.Hash, error)) (bool, error) { - return true, nil -} - -func (m *mockChainValidator) IsValidChain(currentHeader *types.Header, chain []*types.Header) (bool, error) { - return true, nil -} - -func (m *mockChainValidator) GetWhitelistedCheckpoint() (bool, uint64, common.Hash) { - return false, 0, common.Hash{} -} - -func (m *mockChainValidator) GetWhitelistedMilestone() (bool, uint64, common.Hash) { - return m.hasMilestone, m.milestoneNumber, m.milestoneHash -} - -func (m *mockChainValidator) ProcessCheckpoint(endBlockNum uint64, endBlockHash common.Hash) {} - -func (m *mockChainValidator) ProcessMilestone(endBlockNum uint64, endBlockHash common.Hash) {} - -func (m *mockChainValidator) ProcessFutureMilestone(num uint64, hash common.Hash) {} - -func (m *mockChainValidator) PurgeWhitelistedCheckpoint() {} - -func (m *mockChainValidator) PurgeWhitelistedMilestone() {} - -func (m *mockChainValidator) LockMutex(endBlockNum uint64) bool { return true } - -func (m *mockChainValidator) UnlockMutex(doLock bool, milestoneId string, endBlockNum uint64, endBlockHash common.Hash) { -} - -func (m *mockChainValidator) UnlockSprint(endBlockNum uint64) {} - -func (m *mockChainValidator) RemoveMilestoneID(milestoneId string) {} - -func (m *mockChainValidator) GetMilestoneIDsList() []string { return nil } - // mockEngine that can fail header verification for specific block numbers type mockFailingEngine struct { *ethash.Ethash @@ -4829,24 +4785,23 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { // Test case 1: Valid chain - no rewinds should happen t.Run("ValidChain", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator that has a finalized block at height 3 - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 8, nil) - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4891,19 +4846,14 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator that has a finalized block at height 3 - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil) - // Create blockchain with mock validator and failing engine + // Create blockchain with milestone fetcher and failing engine cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4934,25 +4884,26 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { } }) - // Test case 3: No finalized block - verification should not run + // Test case 3: Fetcher returns error - verification should not run t.Run("NoFinalizedBlock", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create a mock validator with no finalized block - mockValidator := &mockChainValidator{ - hasMilestone: false, - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 0, fmt.Errorf("no milestone available") + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -4976,27 +4927,26 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { } }) - // Test case 4: Current head at finalized block - no verification needed + // Test case 4: Milestone at head - no verification needed t.Run("HeadAtFinalizedBlock", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create a mock validator where finalized block equals current head - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 5, // Same as head - milestoneHash: blocks[4].Hash(), - } - - // Create blockchain with mock validator cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 5, nil // milestone at head + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5023,23 +4973,23 @@ func testHeaderVerificationLoop(t *testing.T, scheme string) { // Test case 5: Verify proper shutdown when blockchain stops t.Run("ProperShutdown", func(t *testing.T) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 2, - milestoneHash: common.HexToHash("0x123"), - } - // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 5, nil) - // Create blockchain cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 2, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5064,24 +5014,24 @@ func TestVerifyPendingHeaders(t *testing.T) { func testVerifyPendingHeaders(t *testing.T, scheme string) { engine := ethash.NewFaker() + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } genesis := &Genesis{ - Config: params.TestChainConfig, + Config: &config, BaseFee: big.NewInt(params.InitialBaseFee), } // Generate blocks _, blocks, _ := GenerateChainWithGenesis(genesis, engine, 8, nil) - // Test with mock validator - mockValidator := &mockChainValidator{ - hasMilestone: true, - milestoneNumber: 3, - milestoneHash: common.HexToHash("0x123"), - } - - // Create blockchain + // Create blockchain with milestone fetcher cfg := DefaultConfig() - cfg.Checker = mockValidator + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil + } chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5105,15 +5055,16 @@ func testVerifyPendingHeaders(t *testing.T, scheme string) { } } -// TestHeaderVerificationWithNilChecker tests that verification is skipped when checker is nil -func TestHeaderVerificationWithNilChecker(t *testing.T) { +// TestHeaderVerificationWithNilFetcher tests that the verification loop is skipped +// when MilestoneFetcher is nil. +func TestHeaderVerificationWithNilFetcher(t *testing.T) { engine := ethash.NewFaker() genesis := &Genesis{ Config: params.TestChainConfig, BaseFee: big.NewInt(params.InitialBaseFee), } - // Create blockchain with nil checker + // Create blockchain without MilestoneFetcher chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, DefaultConfig()) if err != nil { t.Fatalf("failed to create blockchain: %v", err) @@ -5128,13 +5079,202 @@ func TestHeaderVerificationWithNilChecker(t *testing.T) { initialHead := chain.CurrentBlock().Number.Uint64() - // Wait a bit - the verification loop should not run since checker is nil + // Wait a bit - the verification loop should not run since milestoneFetcher is nil time.Sleep(2 * time.Second) // Head should not have changed newHead := chain.CurrentBlock().Number.Uint64() if newHead != initialHead { - t.Errorf("Head should not have changed when checker is nil, got %d, want %d", newHead, initialHead) + t.Errorf("Head should not have changed when milestoneFetcher is nil, got %d, want %d", newHead, initialHead) + } +} + +// headerCountingEngine wraps ethash and records how many headers VerifyHeaders receives. +type headerCountingEngine struct { + *ethash.Ethash + headersVerified atomic.Int64 +} + +func (m *headerCountingEngine) VerifyHeaders(chain consensus.ChainHeaderReader, headers []*types.Header) (chan<- struct{}, <-chan error) { + m.headersVerified.Store(int64(len(headers))) + return m.Ethash.VerifyHeaders(chain, headers) +} + +// TestVerifyPendingHeadersMilestoneFetcher tests that verifyPendingHeaders +// verifies only headers between the Heimdall milestone and the chain head. +func TestVerifyPendingHeadersMilestoneFetcher(t *testing.T) { + t.Run("VerifiesFromMilestoneToHead", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 20, nil) + + cfg := DefaultConfig() + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + chain.verifyPendingHeaders() + + // Should verify blocks 4-20 = 17 headers + if got := engine.headersVerified.Load(); got != 17 { + t.Errorf("expected 17 headers verified, got %d", got) + } + }) + + t.Run("SkipsWhenMilestoneAheadOfHead", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 10, nil) + + cfg := DefaultConfig() + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 100, nil // milestone ahead of head (still syncing) + } + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + engine.headersVerified.Store(0) // reset counter after initial insertion + + chain.verifyPendingHeaders() + + // Should not verify anything since milestone > head + if got := engine.headersVerified.Load(); got != 0 { + t.Errorf("expected 0 headers verified when milestone ahead of head, got %d", got) + } + }) + + t.Run("SkipsOnFetcherError", func(t *testing.T) { + engine := &headerCountingEngine{Ethash: ethash.NewFaker()} + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 10, nil) + + cfg := DefaultConfig() + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 0, fmt.Errorf("heimdall unavailable") + } + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + engine.headersVerified.Store(0) // reset counter after initial insertion + + chain.verifyPendingHeaders() + + // Should not verify anything when fetcher returns error + if got := engine.headersVerified.Load(); got != 0 { + t.Errorf("expected 0 headers verified on fetcher error, got %d", got) + } + }) +} + +// TestVerifyPendingHeadersReorgMetrics tests that reorg metrics are recorded +// when verifyPendingHeaders rewinds the chain due to an invalid header. +func TestVerifyPendingHeadersReorgMetrics(t *testing.T) { + failingHeaders := map[uint64]bool{6: true} + engine := &mockFailingEngine{ + Ethash: ethash.NewFaker(), + shouldFailHeader: failingHeaders, + allowInitialInsertion: true, + } + + config := *params.TestChainConfig + config.Bor = ¶ms.BorConfig{ + RioBlock: big.NewInt(0), + } + genesis := &Genesis{ + Config: &config, + BaseFee: big.NewInt(params.InitialBaseFee), + } + + _, blocks, _ := GenerateChainWithGenesis(genesis, engine.Ethash, 8, nil) + + cfg := DefaultConfig() + cfg.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + return 3, nil // milestone at block 3 + } + chain, err := NewBlockChain(rawdb.NewMemoryDatabase(), genesis, engine, cfg) + if err != nil { + t.Fatalf("failed to create blockchain: %v", err) + } + defer chain.Stop() + + if _, err := chain.InsertChain(blocks, false); err != nil { + t.Fatalf("failed to insert chain: %v", err) + } + + engine.markInsertionComplete() + + // Snapshot metrics before + reorgCountBefore := blockReorgMeter.Snapshot().Count() + reorgDropBefore := blockReorgDropMeter.Snapshot().Count() + + chain.verifyPendingHeaders() + + // Chain should have rewound to block 5 + newHead := chain.CurrentBlock().Number.Uint64() + if newHead != 5 { + t.Errorf("expected head to rewind to 5, got %d", newHead) + } + + // Reorg execute meter should have incremented by 1 + reorgCountAfter := blockReorgMeter.Snapshot().Count() + if reorgCountAfter-reorgCountBefore != 1 { + t.Errorf("expected blockReorgMeter to increment by 1, got %d", reorgCountAfter-reorgCountBefore) + } + + // Reorg drop meter should have incremented by 3 (dropped blocks 6, 7, 8) + reorgDropAfter := blockReorgDropMeter.Snapshot().Count() + if reorgDropAfter-reorgDropBefore != 3 { + t.Errorf("expected blockReorgDropMeter to increment by 3, got %d", reorgDropAfter-reorgDropBefore) } } diff --git a/core/vm/contracts.go b/core/vm/contracts.go index 18336ad55d..6f061fa784 100644 --- a/core/vm/contracts.go +++ b/core/vm/contracts.go @@ -134,7 +134,6 @@ var PrecompiledContractsPrague = PrecompiledContracts{ common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{}, common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{}, common.BytesToAddress([]byte{0x09}): &blake2F{}, - common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{}, common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{}, common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{}, @@ -191,7 +190,6 @@ var PrecompiledContractsMadhugiri = PrecompiledContracts{ common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{}, common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{}, common.BytesToAddress([]byte{0x09}): &blake2F{}, - common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{}, common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{}, common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{}, @@ -213,7 +211,6 @@ var PrecompiledContractsMadhugiriPro = PrecompiledContracts{ common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{}, common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{}, common.BytesToAddress([]byte{0x09}): &blake2F{}, - common.BytesToAddress([]byte{0x0a}): &kzgPointEvaluation{}, common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{}, common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{}, common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{}, @@ -247,7 +244,30 @@ var PrecompiledContractsLisovo = PrecompiledContracts{ common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{eip7951: true}, } +// PrecompiledContractsLisovoPro contains the set of pre-compiled Ethereum +// contracts used in the LisovoPro release (bor HF). +var PrecompiledContractsLisovoPro = PrecompiledContracts{ + common.BytesToAddress([]byte{0x01}): &ecrecover{}, + common.BytesToAddress([]byte{0x02}): &sha256hash{}, + common.BytesToAddress([]byte{0x03}): &ripemd160hash{}, + common.BytesToAddress([]byte{0x04}): &dataCopy{}, + common.BytesToAddress([]byte{0x05}): &bigModExp{eip2565: true, eip7823: true, eip7883: true}, + common.BytesToAddress([]byte{0x06}): &bn256AddIstanbul{}, + common.BytesToAddress([]byte{0x07}): &bn256ScalarMulIstanbul{}, + common.BytesToAddress([]byte{0x08}): &bn256PairingIstanbul{}, + common.BytesToAddress([]byte{0x09}): &blake2F{}, + common.BytesToAddress([]byte{0x0b}): &bls12381G1Add{}, + common.BytesToAddress([]byte{0x0c}): &bls12381G1MultiExp{}, + common.BytesToAddress([]byte{0x0d}): &bls12381G2Add{}, + common.BytesToAddress([]byte{0x0e}): &bls12381G2MultiExp{}, + common.BytesToAddress([]byte{0x0f}): &bls12381Pairing{}, + common.BytesToAddress([]byte{0x10}): &bls12381MapG1{}, + common.BytesToAddress([]byte{0x11}): &bls12381MapG2{}, + common.BytesToAddress([]byte{0x01, 0x00}): &p256Verify{eip7951: true}, +} + var ( + PrecompiledAddressesLisovoPro []common.Address PrecompiledAddressesLisovo []common.Address PrecompiledAddressesMadhugiriPro []common.Address PrecompiledAddressesMadhugiri []common.Address @@ -294,10 +314,15 @@ func init() { for k := range PrecompiledContractsLisovo { PrecompiledAddressesLisovo = append(PrecompiledAddressesLisovo, k) } + for k := range PrecompiledContractsLisovoPro { + PrecompiledAddressesLisovoPro = append(PrecompiledAddressesLisovoPro, k) + } } func activePrecompiledContracts(rules params.Rules) PrecompiledContracts { switch { + case rules.IsLisovoPro: + return PrecompiledContractsLisovoPro case rules.IsLisovo: return PrecompiledContractsLisovo case rules.IsMadhugiriPro: @@ -331,6 +356,8 @@ func ActivePrecompiledContracts(rules params.Rules) PrecompiledContracts { // ActivePrecompiles returns the precompile addresses enabled with the current configuration. func ActivePrecompiles(rules params.Rules) []common.Address { switch { + case rules.IsLisovoPro: + return PrecompiledAddressesLisovoPro case rules.IsLisovo: return PrecompiledAddressesLisovo case rules.IsMadhugiriPro: diff --git a/core/vm/contracts_test.go b/core/vm/contracts_test.go index acf9cd217b..d07fd314e2 100644 --- a/core/vm/contracts_test.go +++ b/core/vm/contracts_test.go @@ -535,6 +535,7 @@ func TestReinforceMultiClientPreCompilesTest(t *testing.T) { "IsMadhugiri", "IsMadhugiriPro", "IsLisovo", + "IsLisovoPro", } if len(actual) != len(expected) { @@ -586,3 +587,39 @@ func TestLisovoCLZOpcode(t *testing.T) { t.Errorf("CLZ gas: got %d, want %d", postLisovo[CLZ].constantGas, GasFastStep) } } + +// TestKZGPointEvaluationPrecompileRemoval verifies that the kzgPointEvaluation precompile +// is present before LisovoPro and removed starting with LisovoPro. +func TestKZGPointEvaluationPrecompileRemoval(t *testing.T) { + t.Parallel() + + kzgPointEvaluationAddr := common.BytesToAddress([]byte{0x0a}) + + // Test Lisovo: should have kzgPointEvaluation + lisovoRules := params.Rules{ + IsLisovo: true, + IsMadhugiriPro: true, + IsMadhugiri: true, + } + lisovoPrecompiles := ActivePrecompiledContracts(lisovoRules) + if _, exists := lisovoPrecompiles[kzgPointEvaluationAddr]; !exists { + t.Error("kzgPointEvaluation (0x0a) should exist in Lisovo precompiles") + } + + // Verify it's the correct type + if _, ok := lisovoPrecompiles[kzgPointEvaluationAddr].(*kzgPointEvaluation); !ok { + t.Error("precompile at 0x0a should be kzgPointEvaluation type in Lisovo") + } + + // Test LisovoPro: should not have kzgPointEvaluation + lisovoProRules := params.Rules{ + IsLisovoPro: true, + IsLisovo: true, + IsMadhugiriPro: true, + IsMadhugiri: true, + } + lisovoProPrecompiles := ActivePrecompiledContracts(lisovoProRules) + if _, exists := lisovoProPrecompiles[kzgPointEvaluationAddr]; exists { + t.Error("kzgPointEvaluation (0x0a) should not exist in LisovoPro precompiles") + } +} diff --git a/core/vm/evm.go b/core/vm/evm.go index 961a4d1d35..0e373a24a5 100644 --- a/core/vm/evm.go +++ b/core/vm/evm.go @@ -150,6 +150,8 @@ func NewEVM(blockCtx BlockContext, statedb StateDB, chainConfig *params.ChainCon evm.precompiles = activePrecompiledContracts(evm.chainRules) switch { + case evm.chainRules.IsLisovoPro: + evm.table = &lisovoProInstructionSet case evm.chainRules.IsLisovo: evm.table = &lisovoInstructionSet case evm.chainRules.IsOsaka: diff --git a/core/vm/jump_table.go b/core/vm/jump_table.go index de89b1224a..283f8cf397 100644 --- a/core/vm/jump_table.go +++ b/core/vm/jump_table.go @@ -64,6 +64,7 @@ var ( pragueInstructionSet = newPragueInstructionSet() osakaInstructionSet = newOsakaInstructionSet() lisovoInstructionSet = newLisovoInstructionSet() + lisovoProInstructionSet = newLisovoProInstructionSet() ) // JumpTable contains the EVM opcodes supported at a given fork. @@ -94,6 +95,12 @@ func newLisovoInstructionSet() JumpTable { return validate(instructionSet) } +func newLisovoProInstructionSet() JumpTable { + instructionSet := newPragueInstructionSet() + enable7939(&instructionSet) // EIP-7939 (CLZ opcode) + return validate(instructionSet) +} + func newOsakaInstructionSet() JumpTable { instructionSet := newPragueInstructionSet() enable7939(&instructionSet) // EIP-7939 (CLZ opcode) diff --git a/core/vm/jump_table_export.go b/core/vm/jump_table_export.go index f8c918e0ab..4a325d47e3 100644 --- a/core/vm/jump_table_export.go +++ b/core/vm/jump_table_export.go @@ -28,6 +28,8 @@ func LookupInstructionSet(rules params.Rules) (JumpTable, error) { switch { // Note: geth only returns an error for the verkle-fork. // Return nil for other forks. + case rules.IsLisovoPro: + return newLisovoProInstructionSet(), nil case rules.IsLisovo: return newLisovoInstructionSet(), nil case rules.IsMadhugiriPro: diff --git a/core/vm/jump_table_test.go b/core/vm/jump_table_test.go index c54b3a38e2..e71dd90c0c 100644 --- a/core/vm/jump_table_test.go +++ b/core/vm/jump_table_test.go @@ -17,6 +17,7 @@ package vm import ( + "reflect" "testing" "github.com/stretchr/testify/require" @@ -35,3 +36,60 @@ func TestJumpTableCopy(t *testing.T) { require.Equal(t, uint64(100), deepCopy[SLOAD].constantGas) require.Equal(t, uint64(0), tbl[SLOAD].constantGas) } + +// TestLisovoProMatchesLisovo verifies that lisovoPro instruction set is identical to lisovo +func TestLisovoProMatchesLisovo(t *testing.T) { + t.Parallel() + + lisovo := newLisovoInstructionSet() + lisovoPro := newLisovoProInstructionSet() + + // Compare all 256 operations in the jump table + for i := 0; i < 256; i++ { + opLisovo := lisovo[i] + opLisovoPro := lisovoPro[i] + + // Both should be non-nil + require.NotNil(t, opLisovo, "lisovo operation at index %d is nil", i) + require.NotNil(t, opLisovoPro, "lisovoPro operation at index %d is nil", i) + + // Compare all fields + require.Equal(t, opLisovo.constantGas, opLisovoPro.constantGas, + "constantGas mismatch at opcode %#x", i) + require.Equal(t, opLisovo.minStack, opLisovoPro.minStack, + "minStack mismatch at opcode %#x", i) + require.Equal(t, opLisovo.maxStack, opLisovoPro.maxStack, + "maxStack mismatch at opcode %#x", i) + require.Equal(t, opLisovo.undefined, opLisovoPro.undefined, + "undefined mismatch at opcode %#x", i) + + // Compare function pointers using reflection + require.Equal(t, reflect.ValueOf(opLisovo.execute).Pointer(), + reflect.ValueOf(opLisovoPro.execute).Pointer(), + "execute function mismatch at opcode %#x", i) + + // Compare dynamicGas (can be nil) + if opLisovo.dynamicGas == nil && opLisovoPro.dynamicGas == nil { + // Both nil, ok + } else if opLisovo.dynamicGas == nil || opLisovoPro.dynamicGas == nil { + t.Errorf("dynamicGas nil mismatch at opcode %#x: lisovo=%v, lisovoPro=%v", + i, opLisovo.dynamicGas == nil, opLisovoPro.dynamicGas == nil) + } else { + require.Equal(t, reflect.ValueOf(opLisovo.dynamicGas).Pointer(), + reflect.ValueOf(opLisovoPro.dynamicGas).Pointer(), + "dynamicGas function mismatch at opcode %#x", i) + } + + // Compare memorySize (can be nil) + if opLisovo.memorySize == nil && opLisovoPro.memorySize == nil { + // Both nil, ok + } else if opLisovo.memorySize == nil || opLisovoPro.memorySize == nil { + t.Errorf("memorySize nil mismatch at opcode %#x: lisovo=%v, lisovoPro=%v", + i, opLisovo.memorySize == nil, opLisovoPro.memorySize == nil) + } else { + require.Equal(t, reflect.ValueOf(opLisovo.memorySize).Pointer(), + reflect.ValueOf(opLisovoPro.memorySize).Pointer(), + "memorySize function mismatch at opcode %#x", i) + } + } +} diff --git a/crypto/crypto.go b/crypto/crypto.go index 504c25f185..0fab914d75 100644 --- a/crypto/crypto.go +++ b/crypto/crypto.go @@ -145,6 +145,10 @@ func UnmarshalPubkey(pub []byte) (*ecdsa.PublicKey, error) { if x == nil { return nil, errInvalidPubkey } + // Check coordinates are < P, to protect against potential misses in Unmarshal implementations. + if x.Cmp(S256().Params().P) >= 0 || y.Cmp(S256().Params().P) >= 0 { + return nil, errInvalidPubkey + } if !S256().IsOnCurve(x, y) { return nil, errInvalidPubkey } diff --git a/crypto/secp256k1/curve.go b/crypto/secp256k1/curve.go index 0054958a3c..1219c7321c 100644 --- a/crypto/secp256k1/curve.go +++ b/crypto/secp256k1/curve.go @@ -73,6 +73,11 @@ func (bitCurve *BitCurve) Params() *elliptic.CurveParams { // IsOnCurve returns true if the given (x,y) lies on the BitCurve. func (bitCurve *BitCurve) IsOnCurve(x, y *big.Int) bool { + // Reject non-canonical encodings to ensure coordinates are < P + if x.Cmp(bitCurve.P) >= 0 || y.Cmp(bitCurve.P) >= 0 { + return false + } + // y² = x³ + b y2 := new(big.Int).Mul(y, y) //y² y2.Mod(y2, bitCurve.P) //y²%P @@ -267,6 +272,11 @@ func (bitCurve *BitCurve) Unmarshal(data []byte) (x, y *big.Int) { x = new(big.Int).SetBytes(data[1 : 1+byteLen]) y = new(big.Int).SetBytes(data[1+byteLen:]) + // Reject non-canonical encodings to ensure coordinates are < P + if x.Cmp(bitCurve.P) >= 0 || y.Cmp(bitCurve.P) >= 0 { + return nil, nil + } + return } diff --git a/crypto/secp256k1/ext.h b/crypto/secp256k1/ext.h index 1c485e2603..470feb1ec2 100644 --- a/crypto/secp256k1/ext.h +++ b/crypto/secp256k1/ext.h @@ -109,8 +109,13 @@ int secp256k1_ext_scalar_mul(const secp256k1_context* ctx, unsigned char *point, ARG_CHECK(scalar != NULL); (void)ctx; - secp256k1_fe_set_b32_limit(&feX, point); - secp256k1_fe_set_b32_limit(&feY, point+32); + // Reject non-canonical field elements by ensuring coordinates are < P + if (!secp256k1_fe_set_b32_limit(&feX, point)) { + return 0; + } + if (!secp256k1_fe_set_b32_limit(&feY, point+32)) { + return 0; + } secp256k1_ge_set_xy(&ge, &feX, &feY); secp256k1_scalar_set_b32(&s, scalar, &overflow); if (overflow || secp256k1_scalar_is_zero(&s)) { diff --git a/crypto/signature_nocgo.go b/crypto/signature_nocgo.go index d76127c258..ae6352e356 100644 --- a/crypto/signature_nocgo.go +++ b/crypto/signature_nocgo.go @@ -164,6 +164,13 @@ type btCurve struct { *secp256k1.KoblitzCurve } +func (curve btCurve) IsOnCurve(x, y *big.Int) bool { + if x.Cmp(secp256k1.Params().P) >= 0 || y.Cmp(secp256k1.Params().P) >= 0 { + return false + } + return curve.KoblitzCurve.IsOnCurve(x, y) +} + // Marshal converts a point given as (x, y) into a byte slice. func (curve btCurve) Marshal(x, y *big.Int) []byte { byteLen := (curve.Params().BitSize + 7) / 8 @@ -189,5 +196,11 @@ func (curve btCurve) Unmarshal(data []byte) (x, y *big.Int) { } x = new(big.Int).SetBytes(data[1 : 1+byteLen]) y = new(big.Int).SetBytes(data[1+byteLen:]) + + // Reject non-canonical encodings to ensure coordinates are < P + if x.Cmp(curve.Params().P) >= 0 || y.Cmp(curve.Params().P) >= 0 { + return nil, nil + } + return } diff --git a/crypto/signature_test.go b/crypto/signature_test.go index bbef79d8d8..54bb9f6690 100644 --- a/crypto/signature_test.go +++ b/crypto/signature_test.go @@ -19,6 +19,7 @@ package crypto import ( "bytes" "crypto/ecdsa" + "math/big" "reflect" "testing" @@ -174,3 +175,116 @@ func BenchmarkDecompressPubkey(b *testing.B) { } } } + +// TestNonCanonicalCoordinates verifies that public keys with coordinates >= P +// are rejected during unmarshalling +func TestNonCanonicalCoordinates(t *testing.T) { + P := S256().Params().P + + // Test case 1: X = P + t.Run("X_equals_P", func(t *testing.T) { + pubkey := make([]byte, 65) + pubkey[0] = 4 // uncompressed + P.FillBytes(pubkey[1:33]) + // Y = 1 + pubkey[64] = 1 + + // Should be rejected by UnmarshalPubkey + _, err := UnmarshalPubkey(pubkey) + if err == nil { + t.Fatal("expected error for X >= P, got nil") + } + }) + + // Test case 2: X = P + 1 + t.Run("X_greater_than_P", func(t *testing.T) { + xPlus1 := new(big.Int).Add(P, big.NewInt(1)) + pubkey := make([]byte, 65) + pubkey[0] = 4 + xPlus1.FillBytes(pubkey[1:33]) + pubkey[64] = 1 // Y = 1 + + _, err := UnmarshalPubkey(pubkey) + if err == nil { + t.Fatal("expected error for X > P, got nil") + } + }) + + // Test case 3: Y = P + t.Run("Y_equals_P", func(t *testing.T) { + pubkey := make([]byte, 65) + pubkey[0] = 4 + pubkey[32] = 1 // X = 1 + P.FillBytes(pubkey[33:65]) + + _, err := UnmarshalPubkey(pubkey) + if err == nil { + t.Fatal("expected error for Y >= P, got nil") + } + }) + + // Test case 4: Valid coordinates + t.Run("valid_coordinates", func(t *testing.T) { + params := S256().Params() + pubkey := make([]byte, 65) + pubkey[0] = 4 + params.Gx.FillBytes(pubkey[1:33]) + params.Gy.FillBytes(pubkey[33:65]) + + pub, err := UnmarshalPubkey(pubkey) + if err != nil { + t.Fatalf("valid coordinates rejected: %v", err) + } + if pub.X.Cmp(params.Gx) != 0 || pub.Y.Cmp(params.Gy) != 0 { + t.Fatal("unmarshalled coordinates don't match") + } + }) + + // Test case 5: Verify CompressPubkey doesn't panic on valid keys + t.Run("compress_valid_key", func(t *testing.T) { + key, err := GenerateKey() + if err != nil { + t.Fatalf("key generation failed: %v", err) + } + + compressed := CompressPubkey(&key.PublicKey) + if len(compressed) != 33 { + t.Fatalf("wrong compressed length: %d", len(compressed)) + } + }) +} + +// TestIsOnCurveNonCanonical verifies that IsOnCurve rejects non-canonical +// coordinates, preventing misuse by callers who might bypass Unmarshal +func TestIsOnCurveNonCanonical(t *testing.T) { + curve := S256() + P := curve.Params().P + + // Test case 1: X = P should be rejected + t.Run("X_equals_P", func(t *testing.T) { + x := new(big.Int).Set(P) + y := big.NewInt(1) + + if curve.IsOnCurve(x, y) { + t.Fatal("IsOnCurve accepted X = P (non-canonical)") + } + }) + + // Test case 2: Y = P should be rejected + t.Run("Y_equals_P", func(t *testing.T) { + x := big.NewInt(1) + y := new(big.Int).Set(P) + + if curve.IsOnCurve(x, y) { + t.Fatal("IsOnCurve accepted Y = P (non-canonical)") + } + }) + + // Test case 3: Valid generator point should still work + t.Run("valid_generator", func(t *testing.T) { + params := curve.Params() + if !curve.IsOnCurve(params.Gx, params.Gy) { + t.Fatal("IsOnCurve rejected valid generator point") + } + }) +} diff --git a/eth/backend.go b/eth/backend.go index 1752f3fe04..5f1a0b8914 100644 --- a/eth/backend.go +++ b/eth/backend.go @@ -325,6 +325,17 @@ func New(stack *node.Node, config *ethconfig.Config) (*Ethereum, error) { options.Overrides = &overrides options.Checker = checker + // Wire MilestoneFetcher so verifyPendingHeaders queries Heimdall directly. + if borEngine, ok := eth.engine.(*bor.Bor); ok && borEngine.HeimdallClient != nil { + options.MilestoneFetcher = func(ctx context.Context) (uint64, error) { + m, err := borEngine.HeimdallClient.FetchMilestone(ctx) + if err != nil { + return 0, err + } + return m.EndBlock, nil + } + } + // check if Parallel EVM is enabled // if enabled, use parallel state processor if config.ParallelEVM.Enable { diff --git a/eth/tracers/data.csv b/eth/tracers/data.csv index 01e51ce952..ea81ea7ab5 100644 --- a/eth/tracers/data.csv +++ b/eth/tracers/data.csv @@ -1,41 +1,41 @@ TransactionIndex, Incarnation, VersionTxIdx, VersionInc, Path, Operation -0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 0 , 0, -1 , -1, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write -0 , 0, -1 , -1, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 1 , 0, 0 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write +2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 2 , 0, 1 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write +2 , 0, 1 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read -3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write -3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 3 , 0, 2 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write -4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read -4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write +3 , 0, 2 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Read 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Read +4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Read +4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Read +4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000001, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000001, Write 4 , 0, 3 , 0, 000000000000000000000000000000000000dead00000000000000000000000000000000000000000000000000000000000000000103, Write -4 , 0, 3 , 0, 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000103, Write diff --git a/internal/cli/server/chains/amoy.go b/internal/cli/server/chains/amoy.go index 3524abcefa..7db336f37f 100644 --- a/internal/cli/server/chains/amoy.go +++ b/internal/cli/server/chains/amoy.go @@ -40,6 +40,7 @@ var amoyTestnet = &Chain{ MadhugiriProBlock: big.NewInt(29287400), DandeliBlock: big.NewInt(31890000), LisovoBlock: big.NewInt(33634700), + LisovoProBlock: big.NewInt(34062000), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, diff --git a/internal/cli/server/chains/mainnet.go b/internal/cli/server/chains/mainnet.go index dd624c2260..de4955598c 100644 --- a/internal/cli/server/chains/mainnet.go +++ b/internal/cli/server/chains/mainnet.go @@ -40,6 +40,8 @@ var mainnetBor = &Chain{ MadhugiriBlock: big.NewInt(80084800), MadhugiriProBlock: big.NewInt(80084800), DandeliBlock: big.NewInt(81424000), + LisovoBlock: big.NewInt(83756500), + LisovoProBlock: big.NewInt(83756500), StateSyncConfirmationDelay: map[string]uint64{ "44934656": 128, }, @@ -84,6 +86,7 @@ var mainnetBor = &Chain{ BurntContract: map[string]string{ "23850000": "0x70bca57f4579f58670ab2d18ef16e02c17553c38", "50523000": "0x7A8ed27F4C30512326878652d20fC85727401854", + "83756500": "0x3ef57def668054dd750bd260526105c4eeef104f", }, Coinbase: map[string]string{ "0": "0x0000000000000000000000000000000000000000", diff --git a/p2p/discover/v4wire/v4wire.go b/p2p/discover/v4wire/v4wire.go index 81aafbe8b3..3619f10236 100644 --- a/p2p/discover/v4wire/v4wire.go +++ b/p2p/discover/v4wire/v4wire.go @@ -306,6 +306,11 @@ func DecodePubkey(curve elliptic.Curve, e Pubkey) (*ecdsa.PublicKey, error) { p.X.SetBytes(e[:half]) p.Y.SetBytes(e[half:]) + // Reject non-canonical coordinates (X >= P or Y >= P) + if p.X.Cmp(p.Curve.Params().P) >= 0 || p.Y.Cmp(p.Curve.Params().P) >= 0 { + return nil, ErrBadPoint + } + if !p.Curve.IsOnCurve(p.X, p.Y) { return nil, ErrBadPoint } diff --git a/p2p/discover/v4wire/v4wire_test.go b/p2p/discover/v4wire/v4wire_test.go index 6115e15d27..6fb00d6583 100644 --- a/p2p/discover/v4wire/v4wire_test.go +++ b/p2p/discover/v4wire/v4wire_test.go @@ -18,6 +18,7 @@ package v4wire import ( "encoding/hex" + "math/big" "net" "reflect" "testing" @@ -137,3 +138,127 @@ func hexPubkey(h string) (ret Pubkey) { return ret } + +// TestDecodePubkeyNonCanonical verifies that DecodePubkey rejects non-canonical +// secp256k1 coordinates (X >= P or Y >= P) +func TestDecodePubkeyNonCanonical(t *testing.T) { + curve := crypto.S256() + P := curve.Params().P + + // Test case 1: X = P + t.Run("X_equals_P", func(t *testing.T) { + var pubkey Pubkey + // First half: X = P + P.FillBytes(pubkey[:32]) + // Second half: Y = 1 + pubkey[63] = 1 + + _, err := DecodePubkey(curve, pubkey) + if err == nil { + t.Fatal("expected error for X >= P, got nil") + } + if err != ErrBadPoint { + t.Fatalf("expected ErrBadPoint, got %v", err) + } + }) + + // Test case 2: X = P + 1 + t.Run("X_greater_than_P", func(t *testing.T) { + var pubkey Pubkey + xPlus1 := new(big.Int).Add(P, big.NewInt(1)) + xPlus1.FillBytes(pubkey[:32]) + pubkey[63] = 1 // Y = 1 + + _, err := DecodePubkey(curve, pubkey) + if err == nil { + t.Fatal("expected error for X > P, got nil") + } + if err != ErrBadPoint { + t.Fatalf("expected ErrBadPoint, got %v", err) + } + }) + + // Test case 3: Y = P + t.Run("Y_equals_P", func(t *testing.T) { + var pubkey Pubkey + pubkey[31] = 1 // X = 1 + P.FillBytes(pubkey[32:]) + + _, err := DecodePubkey(curve, pubkey) + if err == nil { + t.Fatal("expected error for Y >= P, got nil") + } + if err != ErrBadPoint { + t.Fatalf("expected ErrBadPoint, got %v", err) + } + }) + + // Test case 4: Valid coordinates + t.Run("valid_coordinates", func(t *testing.T) { + params := curve.Params() + var pubkey Pubkey + params.Gx.FillBytes(pubkey[:32]) + params.Gy.FillBytes(pubkey[32:]) + + pub, err := DecodePubkey(curve, pubkey) + if err != nil { + t.Fatalf("valid coordinates rejected: %v", err) + } + if pub.X.Cmp(params.Gx) != 0 || pub.Y.Cmp(params.Gy) != 0 { + t.Fatal("decoded coordinates don't match") + } + }) + + // Test case 5: Valid random key round-trip + t.Run("valid_random_key", func(t *testing.T) { + key, err := crypto.GenerateKey() + if err != nil { + t.Fatalf("key generation failed: %v", err) + } + + encoded := EncodePubkey(&key.PublicKey) + decoded, err := DecodePubkey(curve, encoded) + if err != nil { + t.Fatalf("decoding valid key failed: %v", err) + } + + if decoded.X.Cmp(key.X) != 0 || decoded.Y.Cmp(key.Y) != 0 { + t.Fatal("round-trip mismatch") + } + }) +} + +// TestNeighborsNonCanonicalNode verifies that a Neighbors packet with a node +// containing non-canonical coordinates is properly rejected +func TestNeighborsNonCanonicalNode(t *testing.T) { + curve := crypto.S256() + P := curve.Params().P + + // Create a Neighbors packet with X = P + var maliciousID Pubkey + P.FillBytes(maliciousID[:32]) + maliciousID[63] = 1 // Y = 1 + + neighbors := &Neighbors{ + Nodes: []Node{ + { + ID: maliciousID, + IP: net.ParseIP("127.0.0.1").To4(), + UDP: 30303, + TCP: 30303, + }, + }, + Expiration: 0, + } + + // Try to decode the node ID + _, err := DecodePubkey(curve, neighbors.Nodes[0].ID) + if err == nil { + t.Fatal("expected error for node with X >= P, got nil") + } + if err != ErrBadPoint { + t.Fatalf("expected ErrBadPoint, got %v", err) + } + + t.Log("Malicious node ID correctly rejected") +} diff --git a/params/config.go b/params/config.go index e121983e24..bba5889e6f 100644 --- a/params/config.go +++ b/params/config.go @@ -344,6 +344,7 @@ var ( MadhugiriProBlock: big.NewInt(29287400), DandeliBlock: big.NewInt(31890000), LisovoBlock: big.NewInt(33634700), + LisovoProBlock: big.NewInt(34062000), StateSyncConfirmationDelay: map[string]uint64{ "0": 128, }, @@ -428,6 +429,8 @@ var ( MadhugiriBlock: big.NewInt(80084800), MadhugiriProBlock: big.NewInt(80084800), DandeliBlock: big.NewInt(81424000), + LisovoBlock: big.NewInt(83756500), + LisovoProBlock: big.NewInt(83756500), StateSyncConfirmationDelay: map[string]uint64{ "44934656": 128, }, @@ -473,6 +476,7 @@ var ( BurntContract: map[string]string{ "23850000": "0x70bca57f4579f58670ab2d18ef16e02c17553c38", "50523000": "0x7A8ed27F4C30512326878652d20fC85727401854", + "83756500": "0x3ef57def668054dd750bd260526105c4eeef104f", }, Coinbase: map[string]string{ "0": "0x0000000000000000000000000000000000000000", @@ -731,6 +735,7 @@ var ( MadhugiriProBlock: big.NewInt(0), DandeliBlock: big.NewInt(0), LisovoBlock: big.NewInt(0), + LisovoProBlock: big.NewInt(0), }, } @@ -899,6 +904,11 @@ type BlockRangeOverrideValidatorSet struct { Validators []common.Address `json:"validators"` } +// DefaultSpanLength is the number of bor blocks in a span. This must match +// heimdall-v2's bor module Params.span_duration to ensure reorg protection +// boundaries stay consistent between the execution and consensus layers. +const DefaultSpanLength = 6400 + // BorConfig is the consensus engine configs for Matic bor based sealing. type BorConfig struct { Period map[string]uint64 `json:"period"` // Number of seconds between blocks to enforce @@ -931,6 +941,7 @@ type BorConfig struct { MadhugiriProBlock *big.Int `json:"madhugiriProBlock"` // MadhugiriPro switch block (nil = no fork, 0 = already on madhugiriPro) DandeliBlock *big.Int `json:"dandeliBlock"` // Dandeli switch block (nil = no fork, 0 = already on dandeli) LisovoBlock *big.Int `json:"lisovoBlock"` // Lisovo switch block (nil = no fork, 0 = already on lisovo) + LisovoProBlock *big.Int `json:"lisovoProBlock"` // LisovoPro switch block (nil = no fork, 0 = already on lisovoPro) } // String implements the stringer interface, returning the consensus engine details. @@ -998,6 +1009,10 @@ func (c *BorConfig) IsLisovo(number *big.Int) bool { return isBlockForked(c.LisovoBlock, number) } +func (c *BorConfig) IsLisovoPro(number *big.Int) bool { + return isBlockForked(c.LisovoProBlock, number) +} + // GetTargetGasPercentage returns the target gas percentage for gas limit calculation. // After Lisovo hard fork, this value can be configured via CLI flags (stored in BorConfig at runtime). // It validates the configured value and falls back to defaults if invalid or nil. @@ -1134,6 +1149,9 @@ func (c *ChainConfig) Description() string { if c.Bor.LisovoBlock != nil { banner += fmt.Sprintf(" - Lisovo: #%-8v\n", c.Bor.LisovoBlock) } + if c.Bor.LisovoProBlock != nil { + banner += fmt.Sprintf(" - Lisovo Pro: #%-8v\n", c.Bor.LisovoProBlock) + } return banner } @@ -1771,6 +1789,7 @@ type Rules struct { IsMadhugiri bool IsMadhugiriPro bool IsLisovo bool + IsLisovoPro bool } // Rules ensures c's ChainID is not nil. @@ -1805,5 +1824,6 @@ func (c *ChainConfig) Rules(num *big.Int, isMerge bool, _ uint64) Rules { IsMadhugiri: c.Bor != nil && c.Bor.IsMadhugiri(num), IsMadhugiriPro: c.Bor != nil && c.Bor.IsMadhugiriPro(num), IsLisovo: c.Bor != nil && c.Bor.IsLisovo(num), + IsLisovoPro: c.Bor != nil && c.Bor.IsLisovoPro(num), } } diff --git a/params/version.go b/params/version.go index 3e522e1d0a..c1dac7824a 100644 --- a/params/version.go +++ b/params/version.go @@ -24,8 +24,8 @@ import ( const ( VersionMajor = 2 // Major version component of the current release - VersionMinor = 5 // Minor version component of the current release - VersionPatch = 8 // Patch version component of the current release + VersionMinor = 6 // Minor version component of the current release + VersionPatch = 0 // Patch version component of the current release VersionMeta = "" // Version metadata to append to the version string ) diff --git a/tests/bor/bor_test.go b/tests/bor/bor_test.go index b2cb45a918..a7eac3e37f 100644 --- a/tests/bor/bor_test.go +++ b/tests/bor/bor_test.go @@ -30,6 +30,7 @@ import ( "github.com/ethereum/go-ethereum/consensus" "github.com/ethereum/go-ethereum/consensus/bor" "github.com/ethereum/go-ethereum/consensus/bor/clerk" + borMilestone "github.com/ethereum/go-ethereum/consensus/bor/heimdall/milestone" borSpan "github.com/ethereum/go-ethereum/consensus/bor/heimdall/span" "github.com/ethereum/go-ethereum/consensus/bor/valset" "github.com/ethereum/go-ethereum/consensus/ethash" @@ -2786,7 +2787,7 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { h2.EXPECT().GetSpan(gomock.Any(), uint64(1)).Return(&span1, nil).AnyTimes() h2.EXPECT().GetLatestSpan(gomock.Any()).Return(&span1, nil).AnyTimes() h2.EXPECT().FetchCheckpoint(gomock.Any(), int64(-1)).Return(nil, fmt.Errorf("no checkpoint available")).AnyTimes() - h2.EXPECT().FetchMilestone(gomock.Any()).Return(nil, fmt.Errorf("no milestone available")).AnyTimes() + h2.EXPECT().FetchMilestone(gomock.Any()).Return(&borMilestone.Milestone{EndBlock: 15}, nil).AnyTimes() h2.EXPECT().FetchStatus(gomock.Any()).Return(&ctypes.SyncInfo{CatchingUp: false}, nil).AnyTimes() h2.EXPECT().StateSyncEvents(gomock.Any(), gomock.Any(), gomock.Any()).Return([]*clerk.EventRecordWithTime{getSampleEventRecord(t)}, nil).AnyTimes() @@ -2799,17 +2800,6 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { borEngs[1].PurgeCache() log.Info("Purged caches on validator 2 to apply new span data") - // Set a milestone at block 15 on validator 2's chain - // This will cause verifyPendingHeaders to check blocks 16-18 - milestoneBlock := nodes[1].BlockChain().GetHeaderByNumber(15) - require.NotNil(t, milestoneBlock, "Milestone block 15 should exist") - - log.Info("Setting milestone at block 15", - "hash", milestoneBlock.Hash(), - "miner", milestoneBlock.Coinbase) - - nodes[1].Downloader().ChainValidator.ProcessMilestone(15, milestoneBlock.Hash()) - log.Info("Waiting for header verification loop to detect invalid headers...") timeout := time.After(30 * time.Second) @@ -2851,7 +2841,6 @@ func TestVerifyPendingHeadersSpanRotationReorg(t *testing.T) { block15 := nodes[1].BlockChain().GetHeaderByNumber(15) require.NotNil(t, block15, "Block 15 should still exist") - require.Equal(t, milestoneBlock.Hash(), block15.Hash(), "Block 15 hash should match milestone") for blockNum := uint64(16); blockNum <= uint64(18); blockNum++ { header := nodes[1].BlockChain().GetHeaderByNumber(blockNum)