diff --git a/consensus/bor/bor.go b/consensus/bor/bor.go index 2b0ca4fcb7..d88415d66c 100644 --- a/consensus/bor/bor.go +++ b/consensus/bor/bor.go @@ -1760,12 +1760,32 @@ func (c *Bor) CommitStates( // Wait for heimdall to be synced before fetching state sync events c.spanStore.waitUntilHeimdallIsSynced(c.ctx) - 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) + if c.config.IsDeterministicStateSync(header.Number) { + log.Info("Using deterministic state sync", "cutoff", to.Unix()) + + eventRecords, err = c.HeimdallClient.StateSyncEventsByTime(c.ctx, from, to.Unix()) + if err != nil { + // Liveness-over-safety tradeoff (matches pre-fork behavior): + // FetchWithRetry already retries aggressively and MultiHeimdallClient + // provides failover, so errors reaching here are persistent. Returning + // empty lets the proposer build a block with 0 state syncs. If other + // validators succeed, they will derive a different state root and reject + // this block — the proposer misses a slot but no silent divergence + // occurs. Skipped events are retried at the next sprint since `from` + // is derived from the on-chain LastStateId. + log.Error("Error fetching deterministic state sync events", "fromID", from, "to", to.Unix(), "err", err) - stateSyncs := make([]*types.StateSyncData, 0) - return stateSyncs, nil + return make([]*types.StateSyncData, 0), nil + } + } else { + eventRecords, err = c.HeimdallClient.StateSyncEvents(c.ctx, from, to.Unix()) + if err != nil { + // Pre-fork: preserve existing behavior (returning empty, no error) + log.Error("Error occurred when fetching state sync events", "fromID", from, "to", to.Unix(), "err", err) + + stateSyncs := make([]*types.StateSyncData, 0) + return stateSyncs, nil + } } // This if statement checks if there are any state sync record overrides configured for the current block number. diff --git a/consensus/bor/bor_test.go b/consensus/bor/bor_test.go index 4db081af2c..b1983a141c 100644 --- a/consensus/bor/bor_test.go +++ b/consensus/bor/bor_test.go @@ -119,6 +119,9 @@ func (f *failingHeimdallClient) FetchMilestoneCount(ctx context.Context) (int64, func (f *failingHeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return nil, errors.New("fetch status failed") } +func (f *failingHeimdallClient) StateSyncEventsByTime(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, errors.New("state sync events by time failed") +} // newStateDBForTest creates a fresh state database for testing. func newStateDBForTest(t *testing.T, root common.Hash) *state.StateDB { @@ -2974,6 +2977,9 @@ func (m *mockHeimdallClient) FetchMilestoneCount(ctx context.Context) (int64, er func (m *mockHeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (m *mockHeimdallClient) StateSyncEventsByTime(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return m.events, nil +} func TestEncodeSigHeader_WithBaseFee(t *testing.T) { t.Parallel() h := &types.Header{ @@ -5259,3 +5265,182 @@ func TestVerifyHeader_PreGiugliano_NoCheck(t *testing.T) { require.NotErrorIs(t, err, errMissingGiuglianoFields) } } + +// trackingHeimdallClient records which IHeimdallClient methods were called. +// It returns configurable results and tracks call counts for assertions. +type trackingHeimdallClient struct { + // Call counters + stateSyncEventsCalled int + stateSyncEventsByTimeCalled int + + // Configurable return values + events []*clerk.EventRecordWithTime + eventsErr error + eventsByTime []*clerk.EventRecordWithTime + eventsByTimeErr error +} + +func (t *trackingHeimdallClient) Close() {} +func (t *trackingHeimdallClient) StateSyncEvents(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + t.stateSyncEventsCalled++ + return t.events, t.eventsErr +} +func (t *trackingHeimdallClient) GetSpan(context.Context, uint64) (*borTypes.Span, error) { + return nil, nil +} +func (t *trackingHeimdallClient) GetLatestSpan(context.Context) (*borTypes.Span, error) { + return nil, nil +} +func (t *trackingHeimdallClient) FetchCheckpoint(context.Context, int64) (*checkpoint.Checkpoint, error) { + return nil, nil +} +func (t *trackingHeimdallClient) FetchCheckpointCount(context.Context) (int64, error) { + return 0, nil +} +func (t *trackingHeimdallClient) FetchMilestone(context.Context) (*milestone.Milestone, error) { + return nil, nil +} +func (t *trackingHeimdallClient) FetchMilestoneCount(context.Context) (int64, error) { + return 0, nil +} +func (t *trackingHeimdallClient) FetchStatus(context.Context) (*ctypes.SyncInfo, error) { + return &ctypes.SyncInfo{CatchingUp: false}, nil +} +func (t *trackingHeimdallClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + t.stateSyncEventsByTimeCalled++ + return t.eventsByTime, t.eventsByTimeErr +} + +// deterministicBorConfig returns a BorConfig with DeterministicStateSyncBlock set. +func deterministicBorConfig(forkBlock int64) *params.BorConfig { + return ¶ms.BorConfig{ + Sprint: map[string]uint64{"0": 16}, + Period: map[string]uint64{"0": 2}, + IndoreBlock: big.NewInt(0), + StateSyncConfirmationDelay: map[string]uint64{"0": 0}, + RioBlock: big.NewInt(1000000), + DeterministicStateSyncBlock: big.NewInt(forkBlock), + } +} + +func TestCommitStates_DeterministicForkSwitch(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + mockGC := &mockGenesisContractForCommitStatesIndore{lastStateID: 0} + + // Fork activates at block 100 + borCfg := deterministicBorConfig(100) + genesisTime := uint64(time.Now().Unix()) - 200 + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, genesisTime) + b.GenesisContractsClient = mockGC + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + now := time.Now() + + // Pre-fork: block 16 should use StateSyncEvents (old legacy path) + tracker := &trackingHeimdallClient{ + events: []*clerk.EventRecordWithTime{}, + } + b.SetHeimdallClient(tracker) + + stateDb := newStateDBForTest(t, genesis.Root) + h := &types.Header{Number: big.NewInt(16), ParentHash: genesis.Hash(), Time: uint64(now.Unix())} + + _, err := b.CommitStates(stateDb, h, statefull.ChainContext{Chain: chain.HeaderChain(), Bor: b}) + require.NoError(t, err) + require.Equal(t, 1, tracker.stateSyncEventsCalled, "pre-fork should call StateSyncEvents") + require.Equal(t, 0, tracker.stateSyncEventsByTimeCalled, "pre-fork should not call StateSyncEventsByTime") + + // Post-fork: block 112 should use StateSyncEventsByTime (deterministic state sync) + tracker2 := &trackingHeimdallClient{ + eventsByTime: []*clerk.EventRecordWithTime{}, + } + b.SetHeimdallClient(tracker2) + + stateDb2 := newStateDBForTest(t, genesis.Root) + h2 := &types.Header{Number: big.NewInt(112), ParentHash: genesis.Hash(), Time: uint64(now.Unix())} + + _, err = b.CommitStates(stateDb2, h2, statefull.ChainContext{Chain: chain.HeaderChain(), Bor: b}) + require.NoError(t, err) + require.Equal(t, 0, tracker2.stateSyncEventsCalled, "post-fork should not call StateSyncEvents") + require.Equal(t, 1, tracker2.stateSyncEventsByTimeCalled, "post-fork should call StateSyncEventsByTime") +} + +func TestCommitStates_ResilientPostFork(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + mockGC := &mockGenesisContractForCommitStatesIndore{lastStateID: 0} + + // Fork activates at block 0 so all blocks are post-fork + borCfg := deterministicBorConfig(0) + genesisTime := uint64(time.Now().Unix()) - 200 + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, genesisTime) + b.GenesisContractsClient = mockGC + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + now := time.Now() + + // StateSyncEventsByTime returns an error + tracker := &trackingHeimdallClient{ + eventsByTimeErr: errors.New("heimdall state sync by time failed"), + } + b.SetHeimdallClient(tracker) + + stateDb := newStateDBForTest(t, genesis.Root) + h := &types.Header{Number: big.NewInt(16), ParentHash: genesis.Hash(), Time: uint64(now.Unix())} + + result, err := b.CommitStates(stateDb, h, statefull.ChainContext{Chain: chain.HeaderChain(), Bor: b}) + + // Post-fork errors are resilient: log + return empty, no error + require.NoError(t, err, "post-fork should not return error on StateSyncEventsByTime failure") + require.Empty(t, result, "post-fork should return empty on StateSyncEventsByTime failure") + + // StateSyncEventsByTime should have been called + require.Equal(t, 1, tracker.stateSyncEventsByTimeCalled, + "StateSyncEventsByTime should have been called once") + // Must not fallback to StateSyncEvents + require.Equal(t, 0, tracker.stateSyncEventsCalled, + "post-fork should NOT fall back to StateSyncEvents on error") +} + +func TestCommitStates_ResilientPostFork_ReturnsEmptyOnError(t *testing.T) { + t.Parallel() + + addr1 := common.HexToAddress("0x1") + sp := &fakeSpanner{vals: []*valset.Validator{{Address: addr1, VotingPower: 1}}} + mockGC := &mockGenesisContractForCommitStatesIndore{lastStateID: 0} + + borCfg := deterministicBorConfig(0) + genesisTime := uint64(time.Now().Unix()) - 200 + chain, b := newChainAndBorForTest(t, sp, borCfg, true, addr1, genesisTime) + b.GenesisContractsClient = mockGC + + genesis := chain.HeaderChain().GetHeaderByNumber(0) + now := time.Now() + + // StateSyncEventsByTime fails with an HTTP error + tracker := &trackingHeimdallClient{ + eventsByTimeErr: errors.New("HTTP 503: service unavailable"), + } + b.SetHeimdallClient(tracker) + + stateDb := newStateDBForTest(t, genesis.Root) + h := &types.Header{Number: big.NewInt(16), ParentHash: genesis.Hash(), Time: uint64(now.Unix())} + + result, err := b.CommitStates(stateDb, h, statefull.ChainContext{Chain: chain.HeaderChain(), Bor: b}) + + // Post-fork is resilient: returns empty on error, does not propagate + require.NoError(t, err, "post-fork should not return error on StateSyncEventsByTime failure") + require.Empty(t, result, "post-fork should return empty on StateSyncEventsByTime failure") + + // StateSyncEventsByTime should have been called + require.Equal(t, 1, tracker.stateSyncEventsByTimeCalled, + "StateSyncEventsByTime should have been called") + // Old path should not have been called as fallback + require.Equal(t, 0, tracker.stateSyncEventsCalled, + "post-fork should not fall back to StateSyncEvents") +} diff --git a/consensus/bor/heimdall.go b/consensus/bor/heimdall.go index 37405e9cee..a1b9f3a8bc 100644 --- a/consensus/bor/heimdall.go +++ b/consensus/bor/heimdall.go @@ -14,6 +14,7 @@ import ( //go:generate mockgen -source=heimdall.go -destination=../../tests/bor/mocks/IHeimdallClient.go -package=mocks type IHeimdallClient interface { StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) + StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) GetSpan(ctx context.Context, spanID uint64) (*types.Span, error) GetLatestSpan(ctx context.Context) (*types.Span, error) FetchCheckpoint(ctx context.Context, number int64) (*checkpoint.Checkpoint, error) diff --git a/consensus/bor/heimdall/client.go b/consensus/bor/heimdall/client.go index d8a4878d83..2ec976db96 100644 --- a/consensus/bor/heimdall/client.go +++ b/consensus/bor/heimdall/client.go @@ -95,6 +95,8 @@ const ( fetchLatestSpan = "bor/spans/latest" fetchStatus = "/status" + + fetchStateSyncsByTimePath = "clerk/state-syncs-by-time" ) // StateSyncEvents fetches the state sync events from heimdall @@ -265,6 +267,63 @@ func (h *HeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, err return response, nil } +// StateSyncsByTimeResponse uses the proto-generated response type from heimdall-v2. +type StateSyncsByTimeResponse = clerkTypes.StateSyncsByTimeResponse + +// StateSyncEventsByTime fetches state sync events using the combined endpoint that +// resolves the Heimdall height from the cutoff time internally. +func (h *HeimdallClient) StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + // Global timeout bounding the entire paginated fetch, matching the gRPC + // implementation's stateSyncTotalTimeout (1 minute). + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + ctx = WithRequestType(ctx, StateSyncByTimeRequest) + + eventRecords := make([]*clerk.EventRecordWithTime, 0) + + for { + u, err := stateSyncsByTimeURL(h.urlString, fromID, toTime) + if err != nil { + return nil, err + } + + log.Debug("Fetching state sync events by time", "queryParams", u.RawQuery) + + response, err := FetchWithRetry[StateSyncsByTimeResponse](ctx, h.client, u, h.closeCh) + if err != nil { + return nil, err + } + + for _, e := range response.EventRecords { + record := &clerk.EventRecordWithTime{ + EventRecord: clerk.EventRecord{ + ID: e.Id, + ChainID: e.BorChainId, + Contract: common.HexToAddress(e.Contract), + Data: e.Data, + LogIndex: e.LogIndex, + TxHash: common.HexToHash(e.TxHash), + }, + Time: e.RecordTime, + } + eventRecords = append(eventRecords, record) + } + + if len(response.EventRecords) < stateFetchLimit { + break + } + + fromID += uint64(stateFetchLimit) + } + + sort.SliceStable(eventRecords, func(i, j int) bool { + return eventRecords[i].ID < eventRecords[j].ID + }) + + return eventRecords, nil +} + func FetchOnce[T any](ctx context.Context, client http.Client, url *url.URL, closeCh chan struct{}) (*T, error) { request := &Request{client: client, url: url, start: time.Now()} return Fetch[T](ctx, request) @@ -437,6 +496,15 @@ func statusURL(urlString string) (*url.URL, error) { return makeURL(urlString, fetchStatus, "") } +func stateSyncsByTimeURL(urlString string, fromID uint64, toTime int64) (*url.URL, error) { + t := time.Unix(toTime, 0).UTC() + params := url.Values{} + params.Set("from_id", fmt.Sprintf("%d", fromID)) + params.Set("to_time", t.Format(time.RFC3339Nano)) + params.Set("pagination.limit", fmt.Sprintf("%d", stateFetchLimit)) + return makeURL(urlString, fetchStateSyncsByTimePath, params.Encode()) +} + func makeURL(urlString, rawPath, rawQuery string) (*url.URL, error) { u, err := url.Parse(urlString) if err != nil { diff --git a/consensus/bor/heimdall/failover_client.go b/consensus/bor/heimdall/failover_client.go index 9b20269ff2..73a28080bc 100644 --- a/consensus/bor/heimdall/failover_client.go +++ b/consensus/bor/heimdall/failover_client.go @@ -29,6 +29,7 @@ const ( // running into Go's covariant-slice restriction. type Endpoint interface { StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) + StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) GetSpan(ctx context.Context, spanID uint64) (*types.Span, error) GetLatestSpan(ctx context.Context) (*types.Span, error) FetchCheckpoint(ctx context.Context, number int64) (*checkpoint.Checkpoint, error) @@ -109,6 +110,17 @@ func (f *MultiHeimdallClient) StateSyncEvents(ctx context.Context, fromID uint64 }) } +func (f *MultiHeimdallClient) StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + // Set a 1-minute global timeout for the paginated fetch BEFORE callWithFailover + // applies its per-attempt attemptTimeout cap. + ctx, cancel := context.WithTimeout(ctx, 1*time.Minute) + defer cancel() + + return callWithFailover(f, ctx, func(ctx context.Context, c Endpoint) ([]*clerk.EventRecordWithTime, error) { + return c.StateSyncEventsByTime(ctx, fromID, toTime) + }) +} + func (f *MultiHeimdallClient) GetSpan(ctx context.Context, spanID uint64) (*types.Span, error) { return callWithFailover(f, ctx, func(ctx context.Context, c Endpoint) (*types.Span, error) { return c.GetSpan(ctx, spanID) diff --git a/consensus/bor/heimdall/failover_client_test.go b/consensus/bor/heimdall/failover_client_test.go index 1ed5740ddd..410a60ee3e 100644 --- a/consensus/bor/heimdall/failover_client_test.go +++ b/consensus/bor/heimdall/failover_client_test.go @@ -24,16 +24,17 @@ import ( // mockHeimdallClient is a configurable mock implementing the Endpoint interface. type mockHeimdallClient struct { - getSpanFn func(ctx context.Context, spanID uint64) (*types.Span, error) - getLatestSpanFn func(ctx context.Context) (*types.Span, error) - stateSyncEventsFn func(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) - fetchCheckpointFn func(ctx context.Context, number int64) (*checkpoint.Checkpoint, error) - fetchCheckpointCntFn func(ctx context.Context) (int64, error) - fetchMilestoneFn func(ctx context.Context) (*milestone.Milestone, error) - fetchMilestoneCntFn func(ctx context.Context) (int64, error) - fetchStatusFn func(ctx context.Context) (*ctypes.SyncInfo, error) - closeFn func() - hits atomic.Int32 + getSpanFn func(ctx context.Context, spanID uint64) (*types.Span, error) + getLatestSpanFn func(ctx context.Context) (*types.Span, error) + stateSyncEventsFn func(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) + fetchCheckpointFn func(ctx context.Context, number int64) (*checkpoint.Checkpoint, error) + fetchCheckpointCntFn func(ctx context.Context) (int64, error) + fetchMilestoneFn func(ctx context.Context) (*milestone.Milestone, error) + fetchMilestoneCntFn func(ctx context.Context) (int64, error) + fetchStatusFn func(ctx context.Context) (*ctypes.SyncInfo, error) + stateSyncEventsByTimeFn func(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) + closeFn func() + hits atomic.Int32 } func (m *mockHeimdallClient) StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) { @@ -122,6 +123,16 @@ func (m *mockHeimdallClient) Close() { } } +func (m *mockHeimdallClient) StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + m.hits.Add(1) + + if m.stateSyncEventsByTimeFn != nil { + return m.stateSyncEventsByTimeFn(ctx, fromID, toTime) + } + + return []*clerk.EventRecordWithTime{}, nil +} + // testConnErr is a reusable connection-refused error for tests. var testConnErr = &net.OpError{Op: "dial", Net: "tcp", Err: errors.New("connection refused")} @@ -254,12 +265,18 @@ func TestFailover_NoSwitchOnContextCanceled(t *testing.T) { } func TestFailover_NoSwitchOnServiceUnavailable(t *testing.T) { + var secondaryCalled atomic.Bool primary := &mockHeimdallClient{ getSpanFn: func(_ context.Context, _ uint64) (*types.Span, error) { return nil, ErrServiceUnavailable }, } - secondary := &mockHeimdallClient{} + secondary := &mockHeimdallClient{ + getSpanFn: func(_ context.Context, _ uint64) (*types.Span, error) { + secondaryCalled.Store(true) + return &types.Span{Id: 1}, nil + }, + } fc, err := NewMultiHeimdallClient(primary, secondary) require.NoError(t, err) @@ -273,7 +290,7 @@ func TestFailover_NoSwitchOnServiceUnavailable(t *testing.T) { _, err = fc.GetSpan(context.Background(), 1) require.Error(t, err) assert.True(t, errors.Is(err, ErrServiceUnavailable)) - assert.Equal(t, int32(0), secondary.hits.Load(), "should not failover on 503") + assert.False(t, secondaryCalled.Load(), "should not failover on 503") } func TestFailover_NoSwitchOnShutdownDetected(t *testing.T) { @@ -747,13 +764,19 @@ func TestFailover_ThreeClients_ProbeBackToPrimary(t *testing.T) { // Active client returns non-failover error: should return directly, no cascade. func TestFailover_ActiveNonFailoverError(t *testing.T) { + var tertiaryCalled atomic.Bool primary := &mockHeimdallClient{} secondary := &mockHeimdallClient{ getSpanFn: func(_ context.Context, _ uint64) (*types.Span, error) { return nil, ErrShutdownDetected }, } - tertiary := &mockHeimdallClient{} + tertiary := &mockHeimdallClient{ + getSpanFn: func(_ context.Context, _ uint64) (*types.Span, error) { + tertiaryCalled.Store(true) + return &types.Span{Id: 1}, nil + }, + } fc, err := NewMultiHeimdallClient(primary, secondary, tertiary) require.NoError(t, err) @@ -770,7 +793,7 @@ func TestFailover_ActiveNonFailoverError(t *testing.T) { _, err = fc.GetSpan(context.Background(), 1) require.Error(t, err) assert.True(t, errors.Is(err, ErrShutdownDetected)) - assert.Equal(t, int32(0), tertiary.hits.Load(), "should not cascade to tertiary on non-failover error") + assert.False(t, tertiaryCalled.Load(), "should not cascade to tertiary on non-failover error") } // Active client returns failover error: cascade should try by priority. @@ -1255,3 +1278,164 @@ func TestRegistry_InformedCascade_RespectsCooldown(t *testing.T) { // Should prefer tertiary (cooled) over primary (uncooled) assert.Equal(t, 2, fc.registry.Active(), "should prefer cooled tertiary over uncooled primary") } + +// --- StateSyncEventsByTime failover tests --- + +func TestMultiFailover_StateSyncEventsByTime_SwitchOnPrimaryDown(t *testing.T) { + connErr := &net.OpError{Op: "dial", Net: "tcp", Err: errors.New("connection refused")} + expected := []*clerk.EventRecordWithTime{{EventRecord: clerk.EventRecord{ID: 42}}} + + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, connErr + }, + } + secondary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return expected, nil + }, + } + + fc := newInstantMulti(primary, secondary) + defer fc.Close() + + result, err := fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.NoError(t, err) + assert.Equal(t, expected, result) +} + +func TestMultiFailover_StateSyncEventsByTime_NoSwitchOnServiceUnavailable(t *testing.T) { + var secondaryCalled atomic.Bool + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, ErrServiceUnavailable + }, + } + secondary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + secondaryCalled.Store(true) + return []*clerk.EventRecordWithTime{}, nil + }, + } + + fc, err := NewMultiHeimdallClient(primary, secondary) + require.NoError(t, err) + + fc.attemptTimeout = 100 * time.Millisecond + fc.registry.HealthCheckInterval = 1 * time.Hour + fc.registry.ConsecutiveThreshold = 1 + fc.registry.PromotionCooldown = 0 + defer fc.Close() + + _, err = fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrServiceUnavailable)) + assert.False(t, secondaryCalled.Load(), "should not failover on 503") +} + +func TestMultiFailover_StateSyncEventsByTime_NoSwitchOnShutdownDetected(t *testing.T) { + var secondaryCalled atomic.Bool + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, ErrShutdownDetected + }, + } + secondary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + secondaryCalled.Store(true) + return []*clerk.EventRecordWithTime{}, nil + }, + } + + fc, err := NewMultiHeimdallClient(primary, secondary) + require.NoError(t, err) + + fc.attemptTimeout = 100 * time.Millisecond + fc.registry.HealthCheckInterval = 1 * time.Hour + fc.registry.ConsecutiveThreshold = 1 + fc.registry.PromotionCooldown = 0 + defer fc.Close() + + _, err = fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.Error(t, err) + assert.True(t, errors.Is(err, ErrShutdownDetected)) + assert.False(t, secondaryCalled.Load(), "should not failover on shutdown") +} + +func TestMultiFailover_StateSyncEventsByTime_ThreeClients_CascadeToTertiary(t *testing.T) { + connErr := &net.OpError{Op: "dial", Net: "tcp", Err: errors.New("connection refused")} + expected := []*clerk.EventRecordWithTime{{EventRecord: clerk.EventRecord{ID: 99}}} + + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, connErr + }, + } + secondary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, connErr + }, + } + tertiary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return expected, nil + }, + } + + fc := newInstantMulti(primary, secondary, tertiary) + defer fc.Close() + + result, err := fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.NoError(t, err) + assert.Equal(t, expected, result) + + assert.GreaterOrEqual(t, primary.hits.Load(), int32(1), "primary should have been tried") + assert.GreaterOrEqual(t, secondary.hits.Load(), int32(1), "secondary should have been tried") + assert.GreaterOrEqual(t, tertiary.hits.Load(), int32(1), "tertiary should have been called") +} + +func TestMultiFailover_StateSyncEventsByTime_UsesGlobalPaginationDeadlineWhenAttemptTimeoutExceedsIt(t *testing.T) { + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(ctx context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + deadline, ok := ctx.Deadline() + require.True(t, ok, "paginated call should have a deadline") + + remaining := time.Until(deadline) + assert.LessOrEqual(t, remaining, time.Minute+2*time.Second, "global pagination deadline should cap per-attempt timeout") + assert.Greater(t, remaining, 55*time.Second, "global pagination deadline should be close to 1 minute") + + return []*clerk.EventRecordWithTime{}, nil + }, + } + + fc, err := NewMultiHeimdallClient(primary) + require.NoError(t, err) + defer fc.Close() + + fc.attemptTimeout = 2 * time.Minute + + _, err = fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.NoError(t, err) +} + +func TestMultiFailover_StateSyncEventsByTime_UsesDefaultPerAttemptTimeoutWhenSmallerThanGlobalDeadline(t *testing.T) { + primary := &mockHeimdallClient{ + stateSyncEventsByTimeFn: func(ctx context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + deadline, ok := ctx.Deadline() + require.True(t, ok, "paginated call should have a deadline") + + remaining := time.Until(deadline) + assert.LessOrEqual(t, remaining, defaultAttemptTimeout+2*time.Second, "default per-attempt timeout should bound the call") + assert.Greater(t, remaining, 25*time.Second, "default per-attempt timeout should be close to 30 seconds") + + return []*clerk.EventRecordWithTime{}, nil + }, + } + + fc, err := NewMultiHeimdallClient(primary) + require.NoError(t, err) + defer fc.Close() + + _, err = fc.StateSyncEventsByTime(context.Background(), 1, 9999) + require.NoError(t, err) +} diff --git a/consensus/bor/heimdall/metrics.go b/consensus/bor/heimdall/metrics.go index 7d0e2a65a3..05ac753d41 100644 --- a/consensus/bor/heimdall/metrics.go +++ b/consensus/bor/heimdall/metrics.go @@ -29,6 +29,7 @@ const ( MilestoneLastNoAckRequest requestType = "milestone-last-no-ack" MilestoneIDRequest requestType = "milestone-id" StatusRequest requestType = "status" + StateSyncByTimeRequest requestType = "state-sync-by-time" ) func WithRequestType(ctx context.Context, reqType requestType) context.Context { @@ -112,6 +113,13 @@ var ( }, timer: metrics.NewRegisteredTimer("client/requests/milestoneid/duration", nil), }, + StateSyncByTimeRequest: { + request: map[bool]*metrics.Meter{ + true: metrics.NewRegisteredMeter("client/requests/statesyncbytime/valid", nil), + false: metrics.NewRegisteredMeter("client/requests/statesyncbytime/invalid", nil), + }, + timer: metrics.NewRegisteredTimer("client/requests/statesyncbytime/duration", nil), + }, } ) diff --git a/consensus/bor/heimdall/state_sync_url_test.go b/consensus/bor/heimdall/state_sync_url_test.go new file mode 100644 index 0000000000..026cd5184a --- /dev/null +++ b/consensus/bor/heimdall/state_sync_url_test.go @@ -0,0 +1,49 @@ +package heimdall + +import ( + "fmt" + "strings" + "testing" + "time" + + "github.com/stretchr/testify/require" +) + +func TestStateSyncsByTimeURL_Format(t *testing.T) { + t.Parallel() + + fromID := uint64(42) + toTime := int64(1700000000) // 2023-11-14T22:13:20Z + + u, err := stateSyncsByTimeURL("http://bor0", fromID, toTime) + require.NoError(t, err) + + // Path must be clerk/state-syncs-by-time + require.True(t, strings.HasSuffix(u.Path, "clerk/state-syncs-by-time"), + "expected path to end with clerk/state-syncs-by-time, got %s", u.Path) + + // Validate query parameters + q := u.Query() + + expectedTime := time.Unix(toTime, 0).UTC().Format(time.RFC3339Nano) + require.Equal(t, expectedTime, q.Get("to_time"), "to_time should be RFC3339Nano formatted") + require.Equal(t, fmt.Sprintf("%d", fromID), q.Get("from_id")) + require.Equal(t, fmt.Sprintf("%d", stateFetchLimit), q.Get("pagination.limit")) + + // Should NOT have heimdall_height (resolved internally by heimdall) + require.Empty(t, q.Get("heimdall_height"), "combined endpoint should not send heimdall_height") +} + +func TestStateSyncURL_ToTimeIsRFC3339(t *testing.T) { + t.Parallel() + + fromID := uint64(10) + to := int64(100) + + u, err := stateSyncURL("http://bor0", fromID, to) + require.NoError(t, err) + + expectedTime := time.Unix(to, 0).UTC().Format(time.RFC3339Nano) + require.Contains(t, u.RawQuery, fmt.Sprintf("to_time=%s", expectedTime), + "to_time should be RFC3339Nano formatted") +} diff --git a/consensus/bor/heimdallapp/state_sync.go b/consensus/bor/heimdallapp/state_sync.go index ab8ba4dfab..76bfb9d4c6 100644 --- a/consensus/bor/heimdallapp/state_sync.go +++ b/consensus/bor/heimdallapp/state_sync.go @@ -2,12 +2,14 @@ package heimdallapp import ( "context" + "sort" "time" + "github.com/0xPolygon/heimdall-v2/x/clerk/keeper" + "github.com/0xPolygon/heimdall-v2/x/clerk/types" + "github.com/cosmos/cosmos-sdk/types/query" "github.com/ethereum/go-ethereum/common" "github.com/ethereum/go-ethereum/consensus/bor/clerk" - - "github.com/0xPolygon/heimdall-v2/x/clerk/types" ) func (h *HeimdallAppClient) StateSyncEvents(ctx context.Context, fromID uint64, to int64) ([]*clerk.EventRecordWithTime, error) { @@ -36,6 +38,41 @@ func (h *HeimdallAppClient) StateSyncEvents(ctx context.Context, fromID uint64, return totalRecords, nil } +// StateSyncEventsByTime fetches state sync events using the combined endpoint that +// resolves the Heimdall height from the cutoff time internally. +func (h *HeimdallAppClient) StateSyncEventsByTime(_ context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + totalRecords := make([]*clerk.EventRecordWithTime, 0) + + queryServer := keeper.NewQueryServer(&h.hApp.ClerkKeeper) + + for { + req := &types.StateSyncsByTimeRequest{ + FromId: fromID, + ToTime: time.Unix(toTime, 0), + Pagination: query.PageRequest{Limit: stateFetchLimit}, + } + + res, err := queryServer.GetStateSyncsByTime(h.NewContext(), req) + if err != nil { + return nil, err + } + + totalRecords = append(totalRecords, toEvents(res.EventRecords)...) + + if len(res.EventRecords) < stateFetchLimit { + break + } + + fromID += uint64(stateFetchLimit) + } + + sort.SliceStable(totalRecords, func(i, j int) bool { + return totalRecords[i].ID < totalRecords[j].ID + }) + + return totalRecords, nil +} + func toEvents(hdEvents []types.EventRecord) []*clerk.EventRecordWithTime { events := make([]*clerk.EventRecordWithTime, len(hdEvents)) diff --git a/consensus/bor/heimdallgrpc/state_sync.go b/consensus/bor/heimdallgrpc/state_sync.go index fa4098ddd3..fd1ec88eb0 100644 --- a/consensus/bor/heimdallgrpc/state_sync.go +++ b/consensus/bor/heimdallgrpc/state_sync.go @@ -2,6 +2,7 @@ package heimdallgrpc import ( "context" + "sort" "time" "github.com/cosmos/cosmos-sdk/types/query" @@ -84,3 +85,68 @@ func (h *HeimdallGRPCClient) StateSyncEvents(ctx context.Context, fromID uint64, return eventRecords, nil } + +// StateSyncEventsByTime fetches state sync events using the combined endpoint that +// resolves the Heimdall height from the cutoff time internally. +func (h *HeimdallGRPCClient) StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + log.Info("Fetching state sync events by time (gRPC)", "fromID", fromID, "toTime", toTime) + + var err error + + globalCtx, cancel := context.WithTimeout(ctx, stateSyncTotalTimeout) + defer cancel() + + start := time.Now() + ctx = heimdall.WithRequestType(globalCtx, heimdall.StateSyncByTimeRequest) + + defer func() { + heimdall.SendMetrics(ctx, start, err == nil) + }() + + eventRecords := make([]*clerk.EventRecordWithTime, 0) + + for { + req := &types.StateSyncsByTimeRequest{ + FromId: fromID, + ToTime: time.Unix(toTime, 0), + Pagination: query.PageRequest{Limit: stateFetchLimit}, + } + + var res *types.StateSyncsByTimeResponse + pageCtx, pageCancel := context.WithTimeout(ctx, defaultTimeout) + res, err = h.clerkQueryClient.GetStateSyncsByTime(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, + } + eventRecords = append(eventRecords, eventRecord) + } + + if len(events) < stateFetchLimit { + break + } + + fromID += uint64(stateFetchLimit) + } + + sort.SliceStable(eventRecords, func(i, j int) bool { + return eventRecords[i].ID < eventRecords[j].ID + }) + + return eventRecords, nil +} diff --git a/consensus/bor/span_store_test.go b/consensus/bor/span_store_test.go index d62a0995c3..39713e1263 100644 --- a/consensus/bor/span_store_test.go +++ b/consensus/bor/span_store_test.go @@ -84,6 +84,9 @@ func (h *MockHeimdallClient) GetLatestSpan(ctx context.Context) (*types.Span, er func (h *MockHeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (h *MockHeimdallClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func TestSpanStore_SpanById(t *testing.T) { spanStore := NewSpanStore(&MockHeimdallClient{}, nil, "1337") @@ -397,6 +400,9 @@ func (h *MockOverlappingHeimdallClient) Close() { func (h *MockOverlappingHeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (h *MockOverlappingHeimdallClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func TestSpanStore_SpanByBlockNumber_OverlappingSpans(t *testing.T) { spanStore := NewSpanStore(&MockOverlappingHeimdallClient{}, nil, "1337") @@ -958,6 +964,9 @@ func (d *dynamicHeimdallClient) Close() {} func (d *dynamicHeimdallClient) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (d *dynamicHeimdallClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func makeTestSpan(id, start, end uint64, producerAddr string) *types.Span { producer := stakeTypes.Validator{ @@ -1075,6 +1084,9 @@ func (m *MockSyncStatusClient) FetchMilestoneID(ctx context.Context, milestoneID panic("not implemented") } func (m *MockSyncStatusClient) Close() {} +func (m *MockSyncStatusClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func TestSpanStore_WaitUntilHeimdallIsSynced(t *testing.T) { t.Run("heimdall already synced", func(t *testing.T) { @@ -1459,6 +1471,9 @@ func (h *TimeoutHeimdallClient) FetchMilestoneCount(ctx context.Context) (int64, panic("not implemented") } func (h *TimeoutHeimdallClient) Close() {} +func (h *TimeoutHeimdallClient) StateSyncEventsByTime(context.Context, uint64, int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func TestSpanStore_HeimdallDownTimeout(t *testing.T) { t.Run("heimdallStatus set to nil on FetchStatus error", func(t *testing.T) { diff --git a/core/forkid/forkid.go b/core/forkid/forkid.go index fc287a71ee..cf03f4e5ad 100644 --- a/core/forkid/forkid.go +++ b/core/forkid/forkid.go @@ -335,6 +335,7 @@ func GatherForks(config *params.ChainConfig, genesisTime uint64) (heightForks [] config.Bor.LisovoBlock, config.Bor.LisovoProBlock, config.Bor.GiuglianoBlock, + config.Bor.DeterministicStateSyncBlock, } { if fork != nil { heightForks = append(heightForks, fork.Uint64()) diff --git a/eth/ethconfig/config_test.go b/eth/ethconfig/config_test.go index 302a570834..10edfa75ee 100644 --- a/eth/ethconfig/config_test.go +++ b/eth/ethconfig/config_test.go @@ -49,6 +49,9 @@ func (m *mockHeimdallClient) FetchMilestoneCount(context.Context) (int64, error) func (m *mockHeimdallClient) FetchStatus(context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (m *mockHeimdallClient) StateSyncEventsByTime(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} // newTestBorChainConfig creates a minimal Bor chain config for testing func newTestBorChainConfig() *params.ChainConfig { diff --git a/eth/handler_bor_test.go b/eth/handler_bor_test.go index 4ecc83b3c5..754bec368b 100644 --- a/eth/handler_bor_test.go +++ b/eth/handler_bor_test.go @@ -64,6 +64,9 @@ func (m *mockHeimdall) Close() {} func (m *mockHeimdall) FetchStatus(ctx context.Context) (*ctypes.SyncInfo, error) { return &ctypes.SyncInfo{CatchingUp: false}, nil } +func (m *mockHeimdall) StateSyncEventsByTime(_ context.Context, _ uint64, _ int64) ([]*clerk.EventRecordWithTime, error) { + return nil, nil +} func TestFetchWhitelistCheckpointAndMilestone(t *testing.T) { t.Parallel() diff --git a/go.mod b/go.mod index f618297e80..35bd568904 100644 --- a/go.mod +++ b/go.mod @@ -351,6 +351,8 @@ require ( replace ( cosmossdk.io/client/v2 => github.com/0xPolygon/cosmos-sdk/client/v2 v2.0.0-beta.6 // Same as heimdall-v2. + // TODO marcello update to final version once released + github.com/0xPolygon/heimdall-v2 => github.com/0xPolygon/heimdall-v2 v0.6.1-0.20260406091052-f1ea38254ac0 github.com/Masterminds/goutils => github.com/Masterminds/goutils v1.1.1 github.com/cometbft/cometbft => github.com/0xPolygon/cometbft v0.3.6-polygon github.com/cometbft/cometbft-db => github.com/0xPolygon/cometbft-db v0.14.1-polygon diff --git a/go.sum b/go.sum index 07b3445317..3ca6b3536c 100644 --- a/go.sum +++ b/go.sum @@ -86,8 +86,8 @@ github.com/0xPolygon/cosmos-sdk/client/v2 v2.0.0-beta.6 h1:+6AxZcMTWHaRHV0HILf/r github.com/0xPolygon/cosmos-sdk/client/v2 v2.0.0-beta.6/go.mod h1:4p0P6o0ro+FizakJUYS9SeM94RNbv0thLmkHRw5o5as= github.com/0xPolygon/crand v1.0.3 h1:BYYflmgLhmGPEgqtopG4muq6wV6DOkwD8uPymNz5WeQ= github.com/0xPolygon/crand v1.0.3/go.mod h1:km4366oC7EVFl1xNUCwzxUXNM10swZqd8LZ0E5SgbAE= -github.com/0xPolygon/heimdall-v2 v0.6.0 h1:rA8RISMnns1w08PxTLvDBS5WiaTOFHJGSrhDWDJLtHc= -github.com/0xPolygon/heimdall-v2 v0.6.0/go.mod h1:fVkGiODG6cGLaDyrE3qxIrvz1rbUr4Zdrr3dOm2SPgg= +github.com/0xPolygon/heimdall-v2 v0.6.1-0.20260406091052-f1ea38254ac0 h1:rxebnDDy61MkxnrzQ3jvZ3xZqJfi+0wAC6J6kcYDVR0= +github.com/0xPolygon/heimdall-v2 v0.6.1-0.20260406091052-f1ea38254ac0/go.mod h1:wap2yjeiFg1j6Sew0UtCxr9qx+bxi1u/NMH4oStEg5M= github.com/0xPolygon/polyproto v0.0.7 h1:Ody+kFyCRK4QXRPXbsP5pdxKrDgwAAXtFB8NPgaIxRs= github.com/0xPolygon/polyproto v0.0.7/go.mod h1:2Iw93k2LismvckKKeXQITuhJH9vLbqOa212AMskH6no= github.com/99designs/go-keychain v0.0.0-20191008050251-8e49817e8af4 h1:/vQbFIOMbk2FiG/kXiLl8BRyzTWDw7gX/Hz7Dd5eDMs= diff --git a/params/config.go b/params/config.go index b03fdbd325..33572561b6 100644 --- a/params/config.go +++ b/params/config.go @@ -954,6 +954,8 @@ type BorConfig struct { 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) GiuglianoBlock *big.Int `json:"giuglianoBlock"` // Giugliano switch block (nil = no fork, 0 = already on giugliano) + // TODO marcello define block numbers + DeterministicStateSyncBlock *big.Int `json:"deterministicStateSyncBlock"` // DeterministicStateSync switch block (nil = no fork, 0 = already active) } // String implements the stringer interface, returning the consensus engine details. @@ -1029,6 +1031,10 @@ func (c *BorConfig) IsGiugliano(number *big.Int) bool { return isBlockForked(c.GiuglianoBlock, number) } +func (c *BorConfig) IsDeterministicStateSync(number *big.Int) bool { + return isBlockForked(c.DeterministicStateSyncBlock, 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. @@ -1237,6 +1243,9 @@ func (c *ChainConfig) Description() string { if c.Bor.GiuglianoBlock != nil { banner += fmt.Sprintf(" - Giugliano: #%-8v\n", c.Bor.GiuglianoBlock) } + if c.Bor.DeterministicStateSyncBlock != nil { + banner += fmt.Sprintf(" - Deterministic State Sync: #%-8v\n", c.Bor.DeterministicStateSyncBlock) + } return banner } diff --git a/params/config_test.go b/params/config_test.go index bdd6b18842..d7e1c77cf0 100644 --- a/params/config_test.go +++ b/params/config_test.go @@ -1329,3 +1329,47 @@ func TestGetDynamicTargetGasPercentage_InvalidMinMax(t *testing.T) { } }) } + +func TestIsDeterministicStateSync(t *testing.T) { + t.Parallel() + + config := &BorConfig{ + DeterministicStateSyncBlock: big.NewInt(100), + } + + t.Run("nil block is not active", func(t *testing.T) { + t.Parallel() + if config.IsDeterministicStateSync(nil) { + t.Error("expected IsDeterministicStateSync(nil) to return false") + } + }) + + t.Run("block before fork is not active", func(t *testing.T) { + t.Parallel() + if config.IsDeterministicStateSync(big.NewInt(99)) { + t.Error("expected IsDeterministicStateSync(99) to return false for fork at 100") + } + }) + + t.Run("block at fork is active", func(t *testing.T) { + t.Parallel() + if !config.IsDeterministicStateSync(big.NewInt(100)) { + t.Error("expected IsDeterministicStateSync(100) to return true for fork at 100") + } + }) + + t.Run("block after fork is active", func(t *testing.T) { + t.Parallel() + if !config.IsDeterministicStateSync(big.NewInt(200)) { + t.Error("expected IsDeterministicStateSync(200) to return true for fork at 100") + } + }) + + t.Run("nil fork block means not active", func(t *testing.T) { + t.Parallel() + noForkConfig := &BorConfig{DeterministicStateSyncBlock: nil} + if noForkConfig.IsDeterministicStateSync(big.NewInt(100)) { + t.Error("expected IsDeterministicStateSync to return false when fork block is nil") + } + }) +} diff --git a/tests/bor/mocks/IHeimdallClient.go b/tests/bor/mocks/IHeimdallClient.go index 3608823bd4..11f510e45b 100644 --- a/tests/bor/mocks/IHeimdallClient.go +++ b/tests/bor/mocks/IHeimdallClient.go @@ -1,5 +1,5 @@ // Code generated by MockGen. DO NOT EDIT. -// Source: heimdall.go +// Source: consensus/bor/heimdall.go // Package mocks is a generated GoMock package. package mocks @@ -8,13 +8,12 @@ import ( context "context" reflect "reflect" - gomock "go.uber.org/mock/gomock" - types "github.com/0xPolygon/heimdall-v2/x/bor/types" - coretypes "github.com/cometbft/cometbft/rpc/core/types" + types0 "github.com/cometbft/cometbft/rpc/core/types" clerk "github.com/ethereum/go-ethereum/consensus/bor/clerk" checkpoint "github.com/ethereum/go-ethereum/consensus/bor/heimdall/checkpoint" milestone "github.com/ethereum/go-ethereum/consensus/bor/heimdall/milestone" + gomock "go.uber.org/mock/gomock" ) // MockIHeimdallClient is a mock of IHeimdallClient interface. @@ -113,10 +112,10 @@ func (mr *MockIHeimdallClientMockRecorder) FetchMilestoneCount(ctx interface{}) } // FetchStatus mocks base method. -func (m *MockIHeimdallClient) FetchStatus(ctx context.Context) (*coretypes.SyncInfo, error) { +func (m *MockIHeimdallClient) FetchStatus(ctx context.Context) (*types0.SyncInfo, error) { m.ctrl.T.Helper() ret := m.ctrl.Call(m, "FetchStatus", ctx) - ret0, _ := ret[0].(*coretypes.SyncInfo) + ret0, _ := ret[0].(*types0.SyncInfo) ret1, _ := ret[1].(error) return ret0, ret1 } @@ -172,6 +171,21 @@ func (mr *MockIHeimdallClientMockRecorder) StateSyncEvents(ctx, fromID, to inter return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateSyncEvents", reflect.TypeOf((*MockIHeimdallClient)(nil).StateSyncEvents), ctx, fromID, to) } +// StateSyncEventsByTime mocks base method. +func (m *MockIHeimdallClient) StateSyncEventsByTime(ctx context.Context, fromID uint64, toTime int64) ([]*clerk.EventRecordWithTime, error) { + m.ctrl.T.Helper() + ret := m.ctrl.Call(m, "StateSyncEventsByTime", ctx, fromID, toTime) + ret0, _ := ret[0].([]*clerk.EventRecordWithTime) + ret1, _ := ret[1].(error) + return ret0, ret1 +} + +// StateSyncEventsByTime indicates an expected call of StateSyncEventsByTime. +func (mr *MockIHeimdallClientMockRecorder) StateSyncEventsByTime(ctx, fromID, toTime interface{}) *gomock.Call { + mr.mock.ctrl.T.Helper() + return mr.mock.ctrl.RecordCallWithMethodType(mr.mock, "StateSyncEventsByTime", reflect.TypeOf((*MockIHeimdallClient)(nil).StateSyncEventsByTime), ctx, fromID, toTime) +} + // MockIHeimdallWSClient is a mock of IHeimdallWSClient interface. type MockIHeimdallWSClient struct { ctrl *gomock.Controller