Skip to content

fix: make the consumer and provider genesis round-trips restart-safe - #68

Open
giunatale wants to merge 1 commit into
giunatale/docs/refreshfrom
giunatale/fix/genesis-roundtrip
Open

fix: make the consumer and provider genesis round-trips restart-safe#68
giunatale wants to merge 1 commit into
giunatale/docs/refreshfrom
giunatale/fix/genesis-roundtrip

Conversation

@giunatale

@giunatale giunatale commented Jul 30, 2026

Copy link
Copy Markdown
Contributor

Fixes three export/restart correctness bugs, removes two vestigial per-block
maps, adds the missing restart runbook, and proves the consumer round-trip end
to end.

The bugs

  • The consumer's packet-dedup watermark was not exported. VSC packets are
    out of order and deduplicated via HighestValsetUpdateID; a restarted
    consumer came back with the watermark at zero and would re-apply stale
    updates. The watermark is now a genesis field, exported and restored.
  • The provider's stored per-consumer validator set was not exported. After
    a provider restart the stored set was empty, and diffing the live bonded set
    against an empty set emits only additions — a validator that unbonded during
    the outage would never get its power-0 removal and would keep consensus power
    on the consumer indefinitely. Instead of exporting the set,
    QueueVSCPackets now treats an empty stored set for a launched consumer as
    must-snapshot: the first post-restart epoch sends a full snapshot, which
    reconciles the consumer regardless of what it held. (The same path covers a
    consumer's very first epoch.)
  • consumer export emitted null validator pubkeys. CometBFT's genesis
    validation dereferences each pubkey on reload, so export-then-start was a
    broken round-trip. The export now carries every validator's consensus
    pubkey.

Also removed: two write-only per-block maps (provider ValsetUpdateBlockHeight
and consumer HeightValsetUpdateIDs) whose proto fields are kept reserved.

The consumer's in-debt flag survives a restart too

The consumer's in-debt flag gates its own transaction admission alongside the
VSC-staleness clock, but only the clock was round-tripped: a debt-gated consumer
that restarted from a state export came back ungated. Both arms are now
exported. The provider's copy of the flag is exported as well, rather than
relying on it being recomputed after import — the recomputation only runs at an
epoch boundary and only for launched consumers, so a provider restart while a
consumer was paused-and-in-debt, followed by a resume, would stamp "not in debt"
into the forced snapshot and clear the gate the consumer had just correctly
restored.

Genesis validation of withheld-fee records now checks the amount like every
sibling validator does: an omitted amount produced a nil big integer that
validated cleanly and then panicked during fee distribution, and a negative one
inflated the funds the pool believed it had.

Runbook

docs/genesis-restart-runbook.md documents the export/import contract of both
modules, what is deliberately re-derived after restart, and the halt/upgrade
procedure — including one operational constraint discovered while testing this
PR end to end: advancing an IBC client past a restart requires the relayer to
fetch validator sets at pre-restart heights from the restarted chain, so an
export-based restart with a fresh data dir stalls packet flow from the
restarted chain (after a consumer restart the ack-driven liveness clock on the
provider is affected too). The runbook documents the two escape hatches (keep
the pre-restart block store queryable, or governance client recovery).

Testing

  • Unit: full export -> reload round-trips for both modules, must-snapshot on an
    empty stored set, exported pubkeys survive CometBFT's reload validation.
  • A new Docker e2e subtest restarts the real consumer from its own export:
    quiesces the relayer, stops the container, asserts the exported genesis
    carries non-null pubkeys and the exact committed watermark, restarts a fresh
    container from the export on the same network identity, and asserts blocks
    resume, the consumer stays LAUNCHED on the provider, and validator-set
    updates converge with every post-restart update id strictly above the
    restored watermark. Full main e2e suite green with it.

@giunatale

Copy link
Copy Markdown
Contributor Author

Branched from giunatale/docs/refresh (#67) (hard dependency: its restart runbook slots into the refreshed documentation set). Opened against that branch; will retarget to main once it lands.

@giunatale
giunatale force-pushed the giunatale/docs/refresh branch from c6f2086 to cf9e63f Compare July 31, 2026 18:40
@giunatale
giunatale force-pushed the giunatale/fix/genesis-roundtrip branch from e594c11 to dda585e Compare July 31, 2026 18:41
- consumer: add highest_valset_update_id to the genesis proto and
  export/restore it, so a restarted consumer keeps deduplicating VSC packets
  by their update id instead of re-applying stale ones.
- provider: treat an empty stored ConsumerValSet for a LAUNCHED consumer as
  must-snapshot, so the next epoch re-establishes the set after a restart
  rather than sending an empty diff.
- consumer app export: set PubKey on each exported GenesisValidator so the set
  round-trips through genesis.
- consumer: add consumer_in_debt to the genesis proto and export/restore it,
  mirroring last_vsc_recv_time. Both are arms of the same tx-admission gate
  and IsConsumerInDebt reads an unset flag as "not in debt", so a debt-gated
  consumer used to come back admitting ordinary transactions until the next
  VSC packet re-asserted the flag.
- provider: add in_debt to ConsumerState and export/restore it too. Only the
  per-epoch fee distribution rewrites the flag, and only for LAUNCHED
  consumers, so it is not re-derivable at import the way the code comment and
  the runbook claimed: a PAUSED consumer resumed before its first post-restart
  distribution would be sent an immediate snapshot clearing a debt it still
  owes.
- provider: validate the withheld-fee genesis amount (nil, negative, bad
  denom) like every sibling validator does. A genesis omitting the amount
  subfield used to pass Validate and panic later on a nil big.Int the first
  time the record was read for payment or escrow accounting.
- drop the two vestigial per-block maps (ValsetUpdateBlockHeight,
  HeightValsetUpdateIDs) now that nothing reads them; their proto fields stay
  reserved.
- add docs/genesis-restart-runbook.md describing the export/import round-trip.
- e2e: restart the consumer from its exported genesis in TestVAAS, checking
  the exported valset pubkeys and dedup watermark and that VSC flow resumes.
- test: cover the consumer's new-chain provider-chain-id pin, which nothing
  exercised -- the existing test covers only the restart branch.
@giunatale
giunatale force-pushed the giunatale/fix/genesis-roundtrip branch from dda585e to 1c22145 Compare July 31, 2026 19:01
// HeightToValsetUpdateId nil on new chain, filled in on restart.
repeated HeightToValsetUpdateID height_to_valset_update_id = 4
[ (gogoproto.nullable) = false ];
reserved 4;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why not breaking the proto? backward compat isn't necessary

// empty for a new chain
repeated ValsetUpdateIdToHeight valset_update_id_to_height = 3
[ (gogoproto.nullable) = false ];
reserved 3;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ditto

consumertypes "github.com/allinbits/vaas/x/vaas/consumer/types"
)

// TestGetValidatorSetCarriesPubKeysAndReloads is the M6 property: the exported

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's simplify this. what is M6 (except a tv channel).

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants