Skip to content

Commit 02ed6ce

Browse files
committed
round-trip the liveness clock through genesis export/import
Export and re-import the per-consumer liveness state so a genesis export/import (a state-export restart) preserves it instead of resetting it: - provider ConsumerState gains last_ack_time and highest_sent / highest_acked vsc ids, exported for launched consumers and restored at InitGenesis. - consumer GenesisState gains last_vsc_recv_time, so the VSC-staleness clock survives a restart rather than falling back to the current block time. Previously these fields reset on import -- safe by fallback (no immediate sweep, no forced snapshot, never-stale until the first VSC) but not faithful. Genesis round-tripping is foundational state handling, not a version migration, so the liveness clock is now preserved end-to-end (covered by the provider and consumer genesis round-trip tests).
1 parent 3d746ce commit 02ed6ce

8 files changed

Lines changed: 426 additions & 103 deletions

File tree

proto/vaas/consumer/v1/genesis.proto

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,11 @@ message GenesisState {
2929
// Flag indicating whether the consumer VAAS module starts in pre-VAAS state
3030
bool preVAAS = 5;
3131
vaas.v1.ProviderInfo provider = 6 [ (gogoproto.nullable) = false ];
32+
// LastVSCRecvTime is the block time of the last VSC packet the consumer
33+
// received; it drives IsVSCStale (safe mode). Round-tripped on restart so a
34+
// state-export upgrade preserves the staleness clock. Absent for a new chain
35+
// that has not received a VSC yet.
36+
google.protobuf.Timestamp last_vsc_recv_time = 7 [ (gogoproto.stdtime) = true ];
3237
}
3338

3439
// HeightValsetUpdateID represents a mapping internal to the consumer VAAS module

proto/vaas/provider/v1/genesis.proto

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -86,6 +86,17 @@ message ConsumerState {
8686
// absent otherwise. Used to re-derive the keeper's removal-time queue.
8787
google.protobuf.Timestamp removal_time = 12
8888
[ (gogoproto.stdtime) = true ];
89+
90+
// Liveness clock, present once a consumer has launched: LastAckTime is the
91+
// block time of the consumer's most recent successful VSC acknowledgement
92+
// (the provider's per-consumer grace clock), and HighestSentVscId /
93+
// HighestAckedVscId are the resync counters that decide whether the consumer
94+
// is behind. Round-tripped so a state-export restart preserves the grace
95+
// window and resync state instead of resetting them.
96+
google.protobuf.Timestamp last_ack_time = 13
97+
[ (gogoproto.stdtime) = true ];
98+
uint64 highest_sent_vsc_id = 14;
99+
uint64 highest_acked_vsc_id = 15;
89100
}
90101

91102
// ValsetUpdateIdToHeight defines the genesis information for the mapping

x/vaas/consumer/keeper/genesis.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,12 @@ func (k Keeper) InitGenesis(ctx sdk.Context, state *types.GenesisState) []abci.V
6161
return []abci.ValidatorUpdate{}
6262
}
6363

64+
// Restore the VSC staleness clock on a restart (see ExportGenesis); absent
65+
// (new chain / never received a VSC) leaves the never-stale default.
66+
if state.LastVscRecvTime != nil {
67+
k.SetLastVSCRecvTime(ctx, *state.LastVscRecvTime)
68+
}
69+
6470
// populate cross chain validators states with initial valset
6571
k.ApplyCCValidatorChanges(ctx, state.Provider.InitialValSet)
6672
return state.Provider.InitialValSet
@@ -88,5 +94,17 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) (genesis *types.GenesisState) {
8894
params,
8995
)
9096

97+
// Preserve the VSC staleness clock across a restart (see IsVSCStale): export
98+
// the last-VSC-recv time only when actually recorded, so a consumer that has
99+
// not received a VSC keeps the absent-default (never stale) on import.
100+
has, err := k.LastVSCRecvTime.Has(ctx)
101+
if err != nil {
102+
panic(err)
103+
}
104+
if has {
105+
t := k.GetLastVSCRecvTime(ctx)
106+
genesis.LastVscRecvTime = &t
107+
}
108+
91109
return genesis
92110
}

x/vaas/consumer/keeper/genesis_test.go

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -193,6 +193,44 @@ func TestExportGenesis(t *testing.T) {
193193
}
194194
}
195195

196+
// TestGenesisRoundTripLastVSCRecvTime verifies the consumer's VSC-staleness
197+
// clock survives an export/import restart: ExportGenesis carries the recorded
198+
// last-VSC-recv time, and InitGenesis restores it on a fresh keeper (rather than
199+
// falling back to the current block time, which would reset the safe-mode clock).
200+
func TestGenesisRoundTripLastVSCRecvTime(t *testing.T) {
201+
provClientID := "tendermint-07"
202+
params := vaastypes.DefaultConsumerParams()
203+
params.Enabled = true
204+
205+
pubKey := ed25519.GenPrivKey().PubKey()
206+
tmPK, err := cryptocodec.ToCmtPubKeyInterface(pubKey)
207+
require.NoError(t, err)
208+
validator := tmtypes.NewValidator(tmPK, 1)
209+
210+
lastRecv := time.Unix(1_850_000_000, 0).UTC()
211+
212+
// Export half: a keeper with a recorded last-VSC-recv time exports it.
213+
ck, ctx, ctrl, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
214+
defer ctrl.Finish()
215+
ck.SetParams(ctx, params)
216+
ck.SetProviderClientID(ctx, provClientID)
217+
cVal, err := consumertypes.NewCCValidator(validator.Address.Bytes(), 1, pubKey)
218+
require.NoError(t, err)
219+
ck.SetCCValidator(ctx, cVal)
220+
ck.SetHeightValsetUpdateID(ctx, 0, 0)
221+
ck.SetLastVSCRecvTime(ctx, lastRecv)
222+
223+
exported := ck.ExportGenesis(ctx)
224+
require.NotNil(t, exported.LastVscRecvTime, "export must carry last_vsc_recv_time")
225+
require.Equal(t, lastRecv, *exported.LastVscRecvTime)
226+
227+
// Import half: a fresh keeper restores the exact time, not the block-time fallback.
228+
ck2, ctx2, ctrl2, _ := testkeeper.GetConsumerKeeperAndCtx(t, testkeeper.NewInMemKeeperParams(t))
229+
defer ctrl2.Finish()
230+
ck2.InitGenesis(ctx2, exported)
231+
require.Equal(t, lastRecv, ck2.GetLastVSCRecvTime(ctx2))
232+
}
233+
196234
func assertProviderClientID(t *testing.T, ctx sdk.Context, ck *consumerkeeper.Keeper, clientID string) {
197235
t.Helper()
198236
cid, ok := ck.GetProviderClientID(ctx)

x/vaas/consumer/types/genesis.pb.go

Lines changed: 98 additions & 30 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

x/vaas/provider/keeper/genesis.go

Lines changed: 36 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,19 @@ func (k Keeper) InitGenesis(ctx sdk.Context, genState *types.GenesisState) []abc
8181
panic(fmt.Errorf("init: set removal time for %d: %w", consumerId, err))
8282
}
8383
}
84+
// Restore the liveness clock (see ExportGenesis): only when present, so
85+
// a consumer that never launched keeps the absent-defaults.
86+
if cs.LastAckTime != nil {
87+
if err := k.SetConsumerLastAckTime(ctx, consumerId, *cs.LastAckTime); err != nil {
88+
panic(fmt.Errorf("init: set last ack time for %d: %w", consumerId, err))
89+
}
90+
}
91+
if cs.HighestSentVscId != 0 {
92+
k.SetConsumerHighestSentVscId(ctx, consumerId, cs.HighestSentVscId)
93+
}
94+
if cs.HighestAckedVscId != 0 {
95+
k.SetConsumerHighestAckedVscId(ctx, consumerId, cs.HighestAckedVscId)
96+
}
8497
if len(cs.PendingValsetChanges) > 0 {
8598
k.AppendPendingVSCPackets(ctx, consumerId, cs.PendingValsetChanges...)
8699
}
@@ -281,13 +294,10 @@ func (k Keeper) InitGenesisValUpdates(ctx sdk.Context) []abci.ValidatorUpdate {
281294
// and per-consumer debt are NOT exported because they are derivable from
282295
// the per-consumer fields above and / or other module state at InitGenesis.
283296
//
284-
// The liveness clock is also NOT exported and resets to its fresh state on
285-
// import: the last-ack time defaults to the current block time (so no launched
286-
// consumer is swept before it has had a fresh grace window), and the
287-
// highest-sent / highest-acked VSC ids default to 0 and equal (so no consumer
288-
// is treated as "behind", and the next epoch sends an ordinary diff rather than
289-
// a snapshot). This is a deliberate reset for a state-export restart, not
290-
// preserved audit state.
297+
// The liveness clock (last-ack time and the highest-sent / highest-acked VSC
298+
// ids) IS exported per consumer and restored at InitGenesis, so a state-export
299+
// restart preserves each consumer's grace window and resync counters rather
300+
// than resetting them.
291301
func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
292302
allConsumerIds := k.GetAllConsumerIds(ctx)
293303

@@ -339,6 +349,25 @@ func (k Keeper) ExportGenesis(ctx sdk.Context) *types.GenesisState {
339349
panic(fmt.Errorf("export: failed to read removal time for consumer %d: %w", consumerId, err))
340350
}
341351

352+
// Liveness clock: export the last-ack time only when actually recorded
353+
// (GetConsumerLastAckTime falls back to block time when absent, which we
354+
// must not persist), and the resync counters only when non-zero (zero is
355+
// their absent default).
356+
hasAck, err := k.ConsumerLastAckTime.Has(ctx, consumerId)
357+
if err != nil {
358+
panic(fmt.Errorf("export: failed to check last ack time for consumer %d: %w", consumerId, err))
359+
}
360+
if hasAck {
361+
ackCopy := k.GetConsumerLastAckTime(ctx, consumerId)
362+
cs.LastAckTime = &ackCopy
363+
}
364+
if v := k.GetConsumerHighestSentVscId(ctx, consumerId); v != 0 {
365+
cs.HighestSentVscId = v
366+
}
367+
if v := k.GetConsumerHighestAckedVscId(ctx, consumerId); v != 0 {
368+
cs.HighestAckedVscId = v
369+
}
370+
342371
consumerStates = append(consumerStates, cs)
343372
}
344373

0 commit comments

Comments
 (0)