fix(sui-connector): stop permanently losing pruned checkpoints and bag entries (epoch-close wedge); add user-flow sim tests, re-enable all simtest reproducers#1809
Conversation
|
CI links:
|
|
Main-health baseline: all four suites green ✅
Branch iteration — the first branch simtest run failed both new tests at the presign wait. An instrumented deterministic re-run (29162252813) pinned it: the presign completed on-chain — but the pinned Sui renders Move enum values as Side note from the forensics: a global presign requested astride an epoch close is correctly held, but in a quiet epoch the serve vote stayed held for the entire following epoch (the off-chain close-target only advanced at the next close). It completed, so it's liveness-not-safety, but the epoch-long latency shape may deserve a look — details in the run logs above. |
|
Note Superseded — the "quiet-epoch close-target starvation" side note below was later explained as the stale-cache face of the pusher/dynamic-fields bugs fixed in this PR; see the PR description for the final state. Merge gate green: 29179597487 — 7/7 passing (6 fault tests + Iteration trail for the record: full-suite on hosted runner died of resource starvation → moved to ika-k8s-large, 7/8 → imported-key secret encoding fixed, 7/8 → debug run pinned the quiet-epoch starvation → 600s budgets, dedicated presign wedged → second debug run proved the epoch pin ( |
1f4394a to
9fff691
Compare
|
Do the new main commits or #1818/#1819 fix the two findings? No — verified empirically with deterministic A/B runs of the
The victim session shifting between runs is the known schedule sensitivity; the terminal state is the same epoch pin. #1818/#1819 fix a different family — system-session (internal presign) entry-window races — while the wedge here pins a user session in the close-lock target, and the quiet-epoch close-target starvation is on-chain target math neither PR touches. Both remain open; ledger updated in Branch rebased onto post-#1818 main (9fff691); re-validation gate: 29181447878. Experiment branches deleted. |
…h boundaries Add Rust-native drivers for every user-facing dwallet flow to ika-test-cluster (new flows.rs module), and two #[sim_test]s that exercise them successfully across epoch boundaries under the deterministic crypto mock — the happy-path twin of the fault-simulation suite. Purely additive: no existing tokio test is modified. Drivers (IkaTestCluster methods, shapes mirrored from the ika CLI): - request_global_presign / request_dedicated_presign + wait_for_presign (routing matches the mainnet-shape Full genesis config: DKG dwallets on secp256k1-ECDSA are global-presign-only, imported-key ones dedicated-presign-only) - sign (centralized signature -> request_sign_tx -> network signature) - future_sign + wait_for_partial_signature_verified + future_sign_fulfill - import_key_dwallet (centralized step, verification, share acceptance) - accept_dwallet_share (AwaitingKeyHolderSignature -> Active) - make_share_public + wait_for_public_share - transfer_share (re-encrypt, network verification, destination accept) - DwalletSigner: common signer view over DKG-created and imported-key dwallets DwalletDkgHandle now also carries dwallet_cap_id, the encrypted-share id, the user secret share, and the curve so the flows can run without re-deriving them. Tests: - sim_user_flows_across_boundaries: DKG + accept + global-presign sign, imported-key + dedicated-presign sign, then across a boundary: transfer, make-public, and a fresh post-boundary sign; ends with the coordinator user-session drain check (started == completed). - sim_future_sign_across_boundary: the pre-commit half in one epoch, the fulfill half in the next — the two halves of future-sign separated by a reconfiguration, which no other test covers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…sages The first simtest run failed both flow tests at the global-presign wait with a bare timeout — indistinguishable between the network never completing the session on-chain and the test-side object reads never observing the completion. Track and report per-poll observations (last variant, state observations vs fetch errors) in every state poll's timeout message, plus a periodic progress log line, so the deterministic rerun pins down which side failed. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…l-signature polls Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…ops enum variant tags
The instrumented run pinned the failures: the presign COMPLETED on-chain
(state.fields.presign present in every poll's own dump), but the pinned
Sui renders Move enum values as {type, fields} with NO variant tag — the
same quirk wait_for_dwallet_dkg_complete already documents — so the
CLI-mirrored 'state.variant == Completed' polls could never match, and
fieldless variants (NetworkVerificationCompleted) are entirely
unobservable through object JSON.
Rework the waits accordingly:
- presign/sign completion: match on the output-bytes field's presence
(variant tag still honored where a node renders one);
- share acceptance and future-sign fulfill: retry the transaction until
it stops aborting — the abort IS the still-pending signal;
- partial-signature verification barrier and the end-of-test drain
checks: a shared wait_for_user_sessions_drained helper reading the
coordinator via BCS (immune to the JSON rendering).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
… DWalletSession The second run advanced both flow tests past the presign wait and failed at the sign wait with 'object not found' on every poll: the event wrapper's session_object_id names the bookkeeping DWalletSession object, which complete_session unpacks and deletes exactly when the sign succeeds. The signature lives on the SignSession object behind the event's sign_id — poll that instead. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…starvation The 4-vCPU hosted runner ran the grown suite in waves and this run died mid-suite with 'runner lost communication ... starves it for CPU/Memory' (run 29173514605). Each #[sim_test] is a single-OS-thread process and nextest schedules one per core, so the large self-hosted runner (the same one the integration/TS suites use) runs the whole suite simultaneously. Guard sudo like the other k8s workflows. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…scalar encoding create_imported_dwallet_centralized_step_inner_v2 BCS-decodes the secret as a curve scalar; the raw 32 test bytes failed with 'unexpected end of input'. Sample the keypair inside the driver via sample_dwallet_keypair_inner — the same way the ika-core imported-key integration test does (secp256k1-only, guarded). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…och starvation Deterministic finding from the debug run (seed 1): a user session requested astride an epoch close is excluded from the close target (correct), but in a quiet epoch nothing recomputes the target until the NEXT close, so the session starves for the remainder of that epoch and completes ~one epoch late. The imported-key verification hit exactly this and reached quorum ~295 virtual seconds after the request, losing the 300-second budget by seconds. The tests assert eventual success, so the budget now tolerates the known latency shape; the starvation itself is reported as a product finding on the PR. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…e close-lock wedge The flow runs ~ten user sessions against 20-second epochs, so on most msim schedules one lands astride an epoch close — and a user MPC session locked into the close target astride the close pins the next epoch permanently (session_locked=true, all_epoch_sessions_finished= false, 9.5+ stuck virtual minutes; the victim session varies by schedule). This is the same open close/serve wedge the ignored presign-traffic reproducer documents, now reachable from a single request. future-sign coverage stays enabled (green on every run). Findings ledger updated in the fault-matrix plan, including the quiet-epoch close-target starvation (an epoch of latency per astride request) and the variant-less enum JSON rendering the drivers now handle. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
… close the close-lock wedge Deterministic A/B on the sim_user_flows reproducer: baseline main and main+#1818+#1819 both pin the epoch (session_locked=true, all_epoch_sessions_finished=false); only the victim session shifts. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
9fff691 to
9d5d069
Compare
…usher scan and retry Root cause of the msim close-lock wedge (cornered by the sim_user_flows reproducer with sui_connector debug logging): the OCS checkpoint pusher advanced its cursor past any checkpoint whose full contents failed to fetch. The newest checkpoints of every poll window routinely 404 — contents materialize after the summary certifies — so every ~10s poll permanently dropped 2-3 checkpoints from the verified state cache, in identical bursts on all validators. A session_events bag entry whose creating checkpoint was skipped never entered any cache, the bag event pump never delivered the request to the fresh epoch's manager, the session never ran, and all_epoch_sessions_finished=false pinned the epoch forever. The same stale reads explain the quiet-epoch close-target starvation latency (stale synced coordinator). The pusher now stops the scan at a failed fetch and retries the same checkpoint next tick; in-order folding is preserved, and the existing FAR_BEHIND fast-forward remains the explicit escape valve for genuinely pruned history. Contract unit tests rewritten: a near-head NotFound is retried (cursor holds), a materializing checkpoint is folded on a later tick, and an end-of-epoch checkpoint on a previously failed seq is RECOVERED (committee captured + retained) instead of silently lost. All four #[ignore]d simtest reproducers (user flows, presign traffic, wide degradation backlog, dual-laggard halt) are re-enabled to guard the fix end-to-end. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…stead of stalling the scan The stop-and-retry shape regressed the suite: some checkpoints' contents materialize tens of seconds after their summaries certify under msim load, so blocking the in-order scan behind one froze the cache (stall -> far-behind fast-forward -> ~900-checkpoint spans sacrificed, repeatedly), taking down even sim_smoke. New shape: the scan continues past a failed fetch — later checkpoints usually ARE fetchable — and the missing seq is queued as a pending gap, retried at the top of every tick and folded LATE when it materializes. Out-of-order folding is safe because the verified state cache is monotonic-by-version (an old fold can only fill gaps, never regress state) and its fold head is monotone-max. A gap that outlives a generous deadline is dropped with a loud warn (genuinely pruned); gaps inside a far-behind fast-forward's sacrificed span go with it. Contract tests updated: a gap does not stall the scan, is not silently lost, survives ticks while unavailable, and a late-materializing end-of-epoch checkpoint recovers its committee capture and retention. The swallowed fast-forward boundary-walk test is restored unchanged. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…pruning watermark The gap-retry probe proved the missing checkpoints never materialize: they are pruned. The fullnode's lowest-available watermark trails the executed head by about the pusher's own 2s poll cadence, so every pruner tick permanently vaporized the 2-3 newest checkpoints the pusher hadn't fetched yet. At 250ms the pusher stays within a checkpoint or two of the head, inside the watermark's trail; the pending-gap retries remain the backstop for anything that still slips. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…on-completed session is re-pulled The last leg of the close-lock wedge (probe with full debug logging, pusher already fixed): a session computing astride an epoch boundary gets its output quorum SPLIT across the two epochs' consensus — outputs sequenced in the dying epoch die with that epoch's tally, the consensus-adapter retries carry the rest into the new epoch (observed: 2 + 2 of 4, quorum 3 never reached), and the durable computation-completed flag suppresses recomputation on the re-pull, so nobody ever completes the session on-chain and the epoch-close gate pins the epoch forever. Fix: persist the validator's own MPC output consensus transaction (success and rejection alike) in the perpetual tables when it is first submitted; when a computation-completed session is re-pulled — which means it is still uncompleted on-chain — re-submit the persisted output once per epoch. The per-authority output tally dedups, so the merged tally reaches quorum in the current epoch. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…heckpoint was pruned The last silent-drop leg of the close-lock wedge: the bag event pump's dynamic-fields walk lists children from the live fullnode, but building each entry's inclusion proof needs the child's DEFINING checkpoint — and when that checkpoint is already pruned (observed cadence ~10s in the sim), the provider skipped the entry as 'transient' on every tick FOREVER. A session_events entry created astride an epoch boundary was therefore invisible to the fresh epoch's manager despite being live on-chain, the session never ran, and the epoch-close gate pinned the epoch. The provider now reports the ids it listed but could not prove (skipped_entry_ids, serde-default for wire compat), and the verified reader resolves each through verified_object — which carries its own proof verification, currency check, and the committee-verified cache fallback that serves exactly these pruned-defining-tx reads. Trusted listings only: on a mirrored node the relay's skipped ids stay omitted (no membership binding without a proof) and the existing size-vs-listed omission policing covers them. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…try point An ImportedKeyDWalletCap passed to request_sign aborts with a PTB TypeMismatch — imported-key dwallets sign via request_imported_key_sign. DwalletSigner now carries the distinction and sign() picks the matching builder. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
keys/devnet-pre-alpha-dwallet-keypair.json is a raw 64-byte ed25519 keypair referenced nowhere in the repo (keys/ does not exist on main) — an artifact of a CLI run that slipped into the branch. Committed private key material does not belong in the repo regardless of the network it targets; anyone who used this key on any network should treat it as compromised and rotate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Re-review findings (fresh pass, 2026-07-12). One fix pushed, the rest need a decision: Pushed ( Needs a decision — this PR is not what its title says. Five production fix commits are buried under the
Issues with shipping them as-is:
Recommendation: split the five fix commits into their own PR(s) with spec updates + targeted tests + a consensus review pass, keep this PR test-only, and rewrite the body. The sim tests themselves reviewed clean (deterministic, virtual time, defensible |
…onds An unbounded wait on a fully-halted cluster burns hours of wall clock under msim (dense retry timers) before the sim-level budget fires — the cancelled gates never even captured the test's output. Recovery after the heal is sub-minute when it works; 600s is generous. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…nesis-DKG The 'full consensus halt never recovers' finding was an artifact of the test's own injection point: nodes boot AT epoch 1, so wait_for_epoch(1) returned immediately and the test stopped 2-of-4 ~0.3s after genesis — during network DKG, with Mysticeti still bootstrapping. Wait for a real crossed boundary (epoch 2) plus the network key before injecting, which is the documented scenario. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Simtest triage — the suite on this branch is intermittently failing its own tests (three runs today, two different failures; exit 100 both times, i.e. real test failures, not runner deaths):
The only delta in the last run is the keypair-file deletion (referenced nowhere — cannot affect msim), so both failures predate/are independent of today's changes. Two observations that tie back to the earlier review:
Both failures strengthen the split recommendation: land the sim tests once their subjects actually hold, and give the production commits their own review cycle. |
…d reproducers remain Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Note Partially superseded — of the three fixes described below, the third (output-quorum split across the epoch boundary / own-output resubmission) was later retracted and reverted after design review: no MPC protocol spans an epoch boundary, and the falsification re-run stayed 11/11 green without it. The two real fixes are the checkpoint-pusher gap repair and the pruned-children resolution. See the merge-ready comment and the PR description for the final state. All simtests pass — 11/11, zero The close-lock wedge turned out to be three stacked product bugs in the OCS/sui-connector pipeline, all fixed here:
The "full consensus halt never recovers" finding was an artifact of the two-laggards test itself: nodes boot at epoch 1, so its Test-driver fixes along the way: imported-key dwallets sign through |
…omputation-completed session is re-pulled" This reverts commit 6b247ec132504fa0e2a06a2a63c9d10efa77b043. Design review caught the claim over-reach: no MPC protocol spans an epoch boundary by design. A session inside the closing epoch's lock target holds the epoch open until its completion lands on-chain, and a session outside the target never computed in that epoch at all — so the "output quorum split across the boundary" state the reverted commit guarded against should be unreachable, and re-reading the probe trace confirms the session there reached quorum moments later (the failure was starvation latency, not a split). The empirically load-bearing fixes for the close-lock wedge are the checkpoint-pusher gap repair and the dynamic-fields pruned-children resolution; this one was never isolated as necessary. Removing the speculative mechanism (a perpetual table + a consensus-submission path) rather than shipping it unproven; the full simtest gate re-runs on this revert as the falsification check. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…dger Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Merge-ready: all four suites green on the final branch state (
Design-review outcome on the "output quorum split across the epoch boundary" claim: retracted and reverted ( |
…e resolved state - presign-traffic sim tests: module and test docs no longer describe open bugs and ignored reproducers — they document the fixed close-lock wedge they now guard, and a leftover comment describing a degradation window the pure-traffic test does not have is corrected - sim_user_flows: the fix summary names both product legs and the actual pusher fix shape (gap repair, not the reverted stop-and-retry) - flow-test budgets: the 600s rationale is the legitimate re-pull-next-epoch latency, not the retracted starvation finding - simtest workflow header: the suite runs under the crypto mock in ~25-35 minutes on the large runner (stale claims about sequential class-groups crypto and a nonexistent CLAUDE.md section removed) - mpc-stall playbook: the never-re-pulled wedge bullet records the root cause and fixes, with post-fix diagnostic greps - fault-matrix plan: status header reflects completion; the dual-restart finding records the genesis-injection artifact; the stale TOP FOLLOW-UPS list is annotated with what closed Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
…hildren resolution ocs-verified-sui-reads.md covered the folder's skip-on-failure behavior and the bag-walk omission policing that PR #1809's fixes changed; per the spec-maintenance rule these land in the same PR: the fold loop's fetch-failure semantics (250ms cadence vs the pruning watermark, pending-gap retry with version-safe late folds, the far-behind valve), a new bag-entry defense layer (skipped_entry_ids resolved through verified_object on trusted listings, with the mirrored-node exclusion and the cache-miss residual), and the anchor-TTL paragraph now notes the repair as the primary defense with the TTL as depth. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Disposition of the re-review findings and the triage observations (both predate the fix batch; thanks — two of the points drove real work): Re-review findings:
Triage observations — both cited failures were pre-fix commits and are since root-caused:
|
…ir batch - retry_pending_gaps: a fold failure no longer drops the gap immediately — verify_before_fold legitimately fails with MissingCommittee while the checkpoint's epoch committee hasn't installed yet (follower lag at a Sui epoch boundary, or a restart near one), the same transient the in-order scan retries by pinning its cursor. Fold failures now retry under the same 600s deadline as fetch failures, warning each tick, so a genuinely deterministic failure still expires loudly. - write_verified_object_cache: the persisted cache head is now max-semantics — a late gap fold persists with its own older source seq, and writing that through would regress the restored head below what is durably folded (under-firing the staleness tripwire after a restart). The persist() premise comment is updated to match. - skipped-child resolution: forget the high-water entry a resolution records (bag-entry ids are short-lived and the map's policy is not to track them), and warn once per unresolvable id instead of on every ~50ms pump walk. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Consensus-review pass complete (dedicated consensus-reviewer over the three surviving product commits + the revert). Verdicts:
Findings fixed in
Accepted with documented backstops (no code change): the two-EOE-gaps-out-of-order edge (requires two Sui epoch boundaries inside one 600s gap window — unreachable at production epoch lengths; ratchet re-fetch + archive fallback cover it) and the pre-existing scan-path deterministic-fold pin-until-fast-forward asymmetry. Definitive gate re-running on the final SHA; will report here. |
|
Hold the merge — a residual leg of the wedge is live on some schedules. An intermediate gate on a docs-only delta (29275195858, 10/11) failed
So the two fixes closed the common paths, but a schedule exists where bag entries end up unservable from both upstream and the cache, and something downstream of the last certified checkpoint stalls completions. The three prior 11/11 gates were all on one binary schedule; this is a different roll of the dice — i.e. the suite is green per-binary, not schedule-stable yet. Next: single-test debug probe on the current SHA to trace one unresolvable entry end-to-end (why the cache lacks it — dropped gap? never folded? currency-rejected?) and the post-checkpoint-29 stall. The definitive gate on the final SHA (29277054847) is still running; its verdict is one more schedule sample either way. |
…uster Root cause of the schedule-dependent presign-traffic wedge (10/11 on one msim schedule while 11/11 on others): Sui's msim pruning default retains checkpoints for only 2 epochs (num_epochs_to_retain_for_checkpoints = Some(2)) — ~40 virtual seconds at these 20s test epochs. Under msim, proof-building advances virtual time while the fullnode keeps producing and pruning, so the OCS pusher can lose an individual contents-lagged checkpoint to pruning before it materializes; if a session_events bag entry rode that checkpoint, the entry becomes unresolvable from both upstream and the local cache (the documented residual of the pruned-children fix), and the epoch pins. The dropped seqs were scattered with no fast-forward or stall lines, so the pusher was keeping up with the head — this is contents-lag racing an unrealistically tight prune window, not the pusher falling behind. Real fullnodes serving an ika direct node retain far more than the pusher's fold lag. Disabling pruning restores that fidelity: contents-lagged 404s eventually materialize and the gap-repair folds them, so the tests exercise epoch-boundary delivery instead of a pruning race. (Same class of fix as the wait_for_epoch(1) injection-point correction — a misconfigured test environment, not a product bug.) Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN
|
Hold lifted — the schedule-dependent wedge is root-caused and fixed, and the fix is schedule-stable. Root cause of the intermittent presign-traffic failure: Sui's msim pruning default retains checkpoints for only 2 epochs ( Fix ( Schedule stability confirmed:
Re-running the other three suites on this final tree (the pruning change is in shared test-cluster construction); will confirm all-green and mark merge-ready. |
✅ Merge-ready — all four suites green on the final tree (
|
| Suite | Run | Result |
|---|---|---|
| Simtest (11/11) | 29285586551 | ✅ |
| Simtest — 8-seed sweep of the previously-flaky test | 29280076112 | ✅ 8/8 schedules |
| Test Cluster | 29288004530 | ✅ |
| Integration Tests | 29288005979 | ✅ |
| TS Integration | 29288007438 | ✅ |
Everything from this review cycle is resolved: the two product fixes (pusher gap-repair, pruned-children resolution) with spec updates and a clean consensus-review pass; the retracted output-split hypothesis reverted; the three consensus-review findings fixed; the schedule-dependent flake root-caused to the msim fullnode's 2-epoch checkpoint pruning and fixed by disabling it in the test cluster (with an 8-seed sweep proving stability); the PR title and body rewritten to lead with the fix; stale comments bannered. No open items.
What
Follow-up to #1808 (fault-simulation suite): that suite proved the machinery survives adversity; this PR proves the product works — and in doing so it cornered and fixed the open close-lock wedge, so every previously-
#[ignore]d simtest reproducer is re-enabled and the whole suite passes: 11/11, zero ignores (final gate).Contents, in dependency order: Rust-native drivers for every user-facing dwallet flow, two new happy-path sim tests that cross epoch boundaries, the simtest CI moved to the large runner, two product fixes in the OCS/sui-connector pipeline, and the re-enablement of all four reproducers (with one of them fixed as a test bug).
No existing tokio test is modified or removed.
Product fixes (the close-lock wedge, root-caused)
The wedge — an epoch permanently pinned with
session_locked=true, all_epoch_sessions_finished=falseafter a user session lands astride an epoch close — was two stacked state-loss bugs in the verified-state pipeline, cornered with deterministic debug runs of the newsim_user_flowstest:push_worker.rs). The fullnode's lowest-available watermark trails its executed head by roughly the pusher's old 2s poll, so every ~10s pruner tick vaporized the 2–3 newest checkpoints before the pusher fetched them — and a failed fetch skipped the checkpoint forever, leaving permanent gaps in the verified state cache. Fixed: 250ms poll, and failed fetches become pending gaps retried every tick and folded late when they materialize (safe: the cache is monotonic-by-version), never silent skips. Contract unit tests rewritten accordingly, including recovery of an end-of-epoch checkpoint on a previously failed seq. (An intermediate stop-and-retry shape was tried and rejected — it froze the whole cache behind one unfetchable checkpoint.)proof_provider.rs/verified_reader.rs). Building an entry's inclusion proof needs its defining checkpoint; once pruned, the provider skipped the entry as "transient" on every tick, forever — so asession_eventsentry created astride an epoch boundary never reached the next epoch's MPC manager, the session never ran, and the close gate pinned the epoch. Fixed: the provider reports the ids it listed but could not prove (skipped_entry_ids, wire-compatible), and the reader resolves each throughverified_object— proof verification, currency check, and the committee-verified cache fallback included. Trusted listings only: a mirrored node's relay-supplied skipped ids stay omitted (no membership binding without a proof).A third hypothesis — "output quorum split across the epoch boundary" — was implemented, then retracted and reverted after design review (
42a6bd8861): no MPC protocol spans an epoch boundary (in-target sessions hold the epoch open; out-of-target sessions never computed in it), and the falsification re-run stayed 11/11 green without it.Two findings dissolved under scrutiny: the "full consensus halt never recovers" result was the two-laggards test stopping 2-of-4 validators ~0.3s after genesis (
wait_for_epoch(1)is a no-op — nodes boot at epoch 1); injected into a warm cluster it recovers and passes. The "quiet-epoch close-target starvation" was the stale-cache face of the two bugs above.Flow drivers (
crates/ika-test-cluster/src/flows.rs)IkaTestClustermethods, builders fromika_sui_client::ika_dwallet_transactions, user-side crypto fromdwallet-mpc-centralized-party(which forwardsdwallet-mpc-unsafe-mock, so both 2PC halves agree under test):request_global_presign/request_dedicated_presign+wait_for_presign— routing matches the mainnet-shapeFullgenesis presign config: DKG dwallets on secp256k1-ECDSA are global-presign-only, imported-key ones dedicated-presign-only, so the two dwallets cover both pathssign— centralized signature →request_sign_tx/request_imported_key_sign_tx(anImportedKeyDWalletCaptype-mismatches the DKG entry point) → poll theSignSessionbehind the event'ssign_idfuture_sign+future_sign_fulfill(fulfill retries while the partial signature awaits verification — the abort is the pending signal)import_key_dwallet(samples a canonical BCS-encoded secp256k1 keypair),accept_dwallet_share(AwaitingKeyHolderSignature → Active),make_share_public+wait_for_public_share,transfer_share(re-encrypt + accept-retry)wait_for_user_sessions_drained— BCS-read coordinator barrier (started == completed)DwalletSigner— common signer view over DKG-created and imported-key dwalletsHarness knowledge baked in: this Sui version renders Move enum values in object JSON without a variant tag (
{type, fields}), so fieldless states are unobservable — completion waits match output-field presence, retry the follow-up transaction, or read BCS state; neverstate.variant.Tests
sim_future_sign_across_boundary(new) — pre-commit in one epoch, fulfill in the next, a full reconfiguration between the two halves. No other test covers this split.sim_user_flows_across_boundaries(new) — the full flow chain (DKG+accept, global-presign sign, imported-key + dedicated-presign sign, transfer, make-public, post-boundary sign, drain check). Its ~ten sessions against 20s epochs reliably put one astride a close, which is what cornered the wedge; it now guards both fixes end-to-end.#[ignore]d): both presign-traffic close-race reproducers andsim_two_laggards_one_epoch(fixed to inject into a warm cluster).CI
Simtest workflow moved to
ika-k8s-large(the runner the other heavy suites use): each#[sim_test]is a single-OS-thread process and nextest runs one per core — full suite ~25–35 min wall (the 4-vCPU hosted runner ran it in waves and died of resource starvation once the suite grew).Verification
All four suites green on the final tree (
ff106288ed): Simtest 11/11, Test Cluster, Integration, TS Integration (first attempt hit the known cumulative-load flake; clean on re-run). Docs updated in the same PR:dev-docs/plans/simtest-fault-matrix.md(full forensic record),dev-docs/playbooks/mpc-stall-postmortem.md(the never-re-pulled wedge bullet now records the root cause and post-fix diagnostics).🤖 Generated with Claude Code
https://claude.ai/code/session_01Bi6Vr4KcRA4KpKHikcBhDN