Repository navigation
feat: checkpoint keeper watch progress so a restart cannot resettle (#380) - #447
Merged
karagozemin merged 3 commits intoOct 1, 2026
Conversation
…ence The in-memory settlement guard dies with the process: a keeper that crashes after broadcasting clear/settle has no record of the in-flight transaction and the next process rebroadcasts it. Persist the per-round watch cursor locally so a restart cannot resettle. - checkpoint.ts: durable cursor (round id, completed steps, last completed step, transaction hash, network, contract id) with pure planners, a file store, and dry-run (in-memory, never writes) mode. - Startup validation: refuse to start when the checkpoint network or contract id differs from the process config; re-verify recorded transaction hashes and reconcile hashless entries against the on-chain status. - keeper.ts: skip steps the cursor records as complete (reveal, clear, settle, void) and record each completed step, including when the contract confirms the step idempotently. open-reveal stays on-chain-driven so a round cannot strand. - dry-run.ts/run.ts: print the checkpoint a live pass would write, plus any binding conflict, without submitting a transaction or touching disk. - watch/serve: construct the checkpoint (mismatch aborts the process). Closes Sub-Rosa-Issue#380
|
@adeboladee Great news! 🎉 Based on an automated assessment of this PR, the linked Wave issue(s) no longer count against your application limits. You can now already apply to more issues while waiting for a review of this PR. Keep up the great work! 🚀 |
4 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The keeper's per-round watch progress is now durable. The in-memory settlement guard dies with the process, so a keeper that crashed after broadcasting
clear/settlehad no record of the in-flight transaction and the next process rebroadcasted it. This adds a local watch checkpoint and makes the watch loop, the one-shot keeper, and the dry-run planner share it.services/keeper/src/checkpoint.ts— schema, pure planners (planCheckpointStep/planCheckpointRollback), file store (KeeperCheckpointStore,KEEPER_CHECKPOINT_PATH, default.keeper-checkpoint.json), and adryRunmode that tracks progress in memory and never writes.services/keeper/src/keeper.ts—KeeperDeps.checkpoint: skip the steps the cursor records as complete and record each step on success (including when the contract confirms it idempotently, e.g.AlreadySettled).services/keeper/src/watch-loop.ts—resumeCheckpoint()startup gate, wired intorunWatchLoop, pluscheckpoint/verifyTransactionparams.services/keeper/src/dry-run.ts+run.ts— the dry-run summary now carries the checkpoint a live pass would write, and warns when a live keeper would refuse to start.watch.ts/serve.ts— construct the checkpoint; a binding mismatch aborts the process.Checkpoint schema
{ "version": 1, "network": "Test SDF Network ; September 2015", "contractId": "C...", "rounds": { "1": { "roundId": "1", "completedSteps": ["open-reveal", "reveal", "clear", "settle"], "lastCompletedStep": "settle", "lastTransactionHash": "0x…", "stepHashes": { "settle": "0x…" }, "updatedAt": "2026-09-30T00:00:00.000Z" } } }Steps tracked:
open-reveal,reveal,clear,settle,void. The file is bound to onenetwork+contractIdpair. The preview the dry-run prints is produced by the same planner the store uses, so it is byte-for-byte what a live pass would persist (asserted by a test).Validation checks on startup
KeeperCheckpointMismatchErrorwhen the storednetworkorcontractIddiffers from the process config (also true for an unsupportedversion); a corrupt/unparseable file is backed up to*.corrupted.<ts>and the keeper starts from a fresh cursor instead of guessing.stepHashesentry is re-checked.failed/missingrolls the step back so it is retried;confirmedis trusted even when the RPC replica still reports the pre-step status. A failing hash lookup keeps the cursor (an unreachable RPC must not discard durable progress).STEP_SATISFIED_BY_STATUS). If the chain cannot prove a step happened, the entry is dropped rather than stranding the round.open-revealis recorded for auditability but is not skip-eligible (CHECKPOINT_SKIP_STEPS): whether the reveal window is open is already authoritative on-chain, so trusting the cursor there could strand an Open round. The reveal step is only advanced when every bidder ended up revealed, keeping undecryptable seals retryable.Dry-run behavior
KEEPER_DRY_RUN=true npm run startprints the checkpoint it would write —path,network,contractId,proposedStep,mismatch, and the fullproposedFile— inside the summary. It submits nothing (transactionsSubmitted: 0), writes nothing (checkpoint.filesWritten: 0), andKeeperCheckpointStore({ dryRun: true })never touches the filesystem. A checkpoint that would block a live keeper is reported ascheckpoint.mismatch("network"or"contractId").Acceptance criteria → tests
checkpoint-restart.test.ts→ a confirmed settle is not submitted again by a lagging replica (also asserted end-to-end throughrunWatchLoopin a resumed watch loop does not settle a round the cursor already settled)checkpoint.test.ts→ refuses to start when the checkpoint contract id does not match (and the network variant)dry-run.test.ts→ writes nothing to the checkpoint path and submits nothing, pluscheckpoint.test.ts→ tracks progress in memory and never writes to diskcheckpoint-restart.test.tsdescribes restart after the checkpoint was written (4 tests) and restart before the checkpoint was written (4 tests), all oncreateFakeTimewith an in-memory chain; the crash-before flow walks settle → idempotent skip → recorded → never rebroadcastcheckpoint.test.ts→ persists round id, last completed step, transaction hash, and networkTest execution evidence
(73 pre-existing tests + 43 new: 27 in
checkpoint.test.ts, 12 incheckpoint-restart.test.ts, 4 added todry-run.test.ts.)Also green:
pnpm time:guardstill fails, but only on pre-existing violations inapps/web/src/hooks/useDashboardData.test.tsandpackages/sdk/src/status-client.ts— verified identical onmain(stashed) and untouched by this PR.All tests are offline: no RPC, no Drand, no signing material. The live testnet scripts (
keeper:e2e,lifecycle:e2e) were not run — they require a funded testnet account and a live round.Closes #380