Skip to content

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

Merged
omersadika merged 26 commits into
mainfrom
test/simtest-flow-coverage
Jul 14, 2026
Merged

Conversation

@omersadika

@omersadika omersadika commented Jul 11, 2026

Copy link
Copy Markdown
Contributor

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=false after 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 new sim_user_flows test:

  1. The checkpoint pusher permanently lost the newest checkpoints of every pruner tick (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.)
  2. The dynamic-fields walk permanently dropped live-listed bag children whose defining checkpoint was pruned (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 a session_events entry 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 through verified_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)

IkaTestCluster methods, builders from ika_sui_client::ika_dwallet_transactions, user-side crypto from dwallet-mpc-centralized-party (which forwards dwallet-mpc-unsafe-mock, so both 2PC halves agree under test):

  • request_global_presign / request_dedicated_presign + wait_for_presign — routing matches the mainnet-shape Full genesis presign config: DKG dwallets on secp256k1-ECDSA are global-presign-only, imported-key ones dedicated-presign-only, so the two dwallets cover both paths
  • sign — centralized signature → request_sign_tx / request_imported_key_sign_tx (an ImportedKeyDWalletCap type-mismatches the DKG entry point) → poll the SignSession behind the event's sign_id
  • future_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 dwallets

Harness 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; never state.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.
  • Re-enabled (previously #[ignore]d): both presign-traffic close-race reproducers and sim_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

@omersadika

Copy link
Copy Markdown
Contributor Author

CI links:

@omersadika

Copy link
Copy Markdown
Contributor Author

Main-health baseline: all four suites green

  • Test Cluster: 29153674218
  • Integration Tests: 29153676252
  • Simtest: first run 29153675568 was externally canceled (SIGTERM mid-run, 4 tests killed — not a test failure); clean re-run 29162431939
  • TS Integration: first run 29153674855 hit the known cumulative-load flake (5 timeouts at ~620s each in all-combinations-future-sign.test.ts, late in the suite); clean re-run 29162432790

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 {type, fields} with no variant tag, so the CLI-mirrored state.variant polls can never match, and fieldless variants (e.g. NetworkVerificationCompleted) are entirely unobservable via object JSON. Fixed in 014f600 by matching on output-field presence, retrying accept/fulfill transactions (the abort is the pending signal), and a BCS-read wait_for_user_sessions_drained barrier. Full-suite gate: 29171042830.

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.

@omersadika

omersadika commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

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 + sim_future_sign_across_boundary) in ~11 min wall on the large runner.

Iteration trail for the record: full-suite on hosted runner died of resource starvationmoved to ika-k8s-large, 7/8imported-key secret encoding fixed, 7/8debug run pinned the quiet-epoch starvation600s budgets, dedicated presign wedgedsecond debug run proved the epoch pin (session_locked=true all_epoch_sessions_finished=false for 9.5+ virtual minutes) → sim_user_flows preserved as an #[ignore]d seeded reproducer of the known close-lock wedge, findings ledger updated in the fault-matrix plan.

@omersadika omersadika force-pushed the test/simtest-flow-coverage branch 2 times, most recently from 1f4394a to 9fff691 Compare July 12, 2026 05:37
@omersadika

Copy link
Copy Markdown
Contributor Author

Do the new main commits or #1818/#1819 fix the two findings? No — verified empirically with deterministic A/B runs of the sim_user_flows reproducer (single-test dispatches, identical config):

Variant Run Result
main @ 496bc2f (baseline) 29180813257 ❌ wedged — imported-key verification never completes; epoch 5 pinned (session_locked=true, all_epoch_sessions_finished=false)
main + #1818 + #1819 29180813947 ❌ wedged — got further (import passed), then the dedicated presign never completes; epoch 6 pinned, same signature

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 dev-docs/plans/simtest-fault-matrix.md.

Branch rebased onto post-#1818 main (9fff691); re-validation gate: 29181447878. Experiment branches deleted.

omersadika and others added 10 commits July 12, 2026 09:04
…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
@omersadika omersadika force-pushed the test/simtest-flow-coverage branch from 9fff691 to 9d5d069 Compare July 12, 2026 06:04
omersadika and others added 7 commits July 12, 2026 09:53
…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>
@omersadika

Copy link
Copy Markdown
Contributor Author

Re-review findings (fresh pass, 2026-07-12). One fix pushed, the rest need a decision:

Pushed (2d036528dc): removed keys/devnet-pre-alpha-dwallet-keypair.json — a raw 64-byte ed25519 keypair, referenced nowhere, almost certainly a CLI-run artifact. Note it remains in this branch's git history; if that key was ever used anywhere, rotate it (scrubbing history would need a force-push — say the word).

Needs a decision — this PR is not what its title says. Five production fix commits are buried under the test(simtest) title (they exist only on this branch; I checked #1820#1827):

  • push_worker.rs — checkpoint-fetch failure semantics reworked (advance-past-forever → pending_gaps retry with a 600s deadline)
  • lib.rs — pusher cadence 2s → 250ms
  • dwallet_mpc_service.rs + authority_perpetual_tables.rs — new perpetual table dwallet_mpc_own_output_transactions + cross-epoch re-submission of persisted MPC outputs (epoch-close/quorum-adjacent behavior)
  • proof_provider.rs — new wire field skipped_entry_ids on VerifiedDynamicFieldsPageResponse

Issues with shipping them as-is:

  1. Consensus-adjacent paths (dwallet_mpc/**, sui_connector) with no consensus-focused review and no spec updates (ocs-verified-sui-reads.md covers the pusher skip behavior and the dynamic-fields omission policing this changes).
  2. The own-output re-submit fix has zero targeted tests and the new perpetual table is never pruned (one serialized ConsensusTransaction per session, forever — the sibling table stores ()).
  3. The PR body is stale: it still describes sim_user_flows as an #[ignore]d reproducer of an open wedge; the branch removes every #[ignore] and declares the wedge fixed. The fault-matrix doc has the same present-tense stale text.

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 #[sim_test] use); minor: rebase to pick up actions/checkout@v7.

…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
@omersadika

Copy link
Copy Markdown
Contributor Author

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):

Run Head Result
29194687865 (13:35) pre-batch sim_user_flows_across_boundaries (sim_user_flows.rs:166, 745s)
29195338671 (13:56) pre-batch
29204115374 (20:14) 2d036528dc sim_two_laggards_one_epoch (sim_fault_two_laggards.rs:95, 1802s timeout; repro: MSIM_TEST_SEED=1)

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:

  1. sim_user_flows_across_boundaries failing intermittently contradicts the PR body's claim that the epoch-close wedge is fixed — this is the reproducer for the known session-astride-the-close variant, and the branch removed its #[ignore]. Either the fix is incomplete (the race is seed/schedule-dependent) or the test's pass criteria are wrong; both need resolving before the #[ignore] removal is justified.
  2. sim_two_laggards_one_epoch timed out with both laggards spinning on verified_reader: listed dynamic-field child with a pruned defining checkpoint could not be resolved from the verified cache (Transaction … not found) for the rest of the run — the exact subsystem the buried production commits modify (proof_provider.rs skipped_entry_ids + the verified_reader.rs resolution pass). That failure mode reads like the laggard-catchup path having no recovery when the upstream pruned an object and the cache lacks it — which is precisely the kind of behavior change that needs its own reviewed PR with a spec update, per the earlier comment.

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
@omersadika

omersadika commented Jul 12, 2026

Copy link
Copy Markdown
Contributor Author

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 #[ignore]s (final gate 29208084278). Every previously-ignored reproducer is re-enabled and green: both presign-traffic close-race reproducers, the wide-degradation backlog, the dual-laggard halt, and the user-flows test.

The close-lock wedge turned out to be three stacked product bugs in the OCS/sui-connector pipeline, all fixed here:

  1. Checkpoint pusher lost the newest 2-3 checkpoints of every pruner tick (~10s cadence) — its 2s poll trailed the fullnode's pruning watermark, and a failed fetch skipped the checkpoint forever, leaving permanent verified-cache gaps. Fixed: 250ms poll + failed fetches become retried pending gaps folded late (version-safe), never silent skips (4021e72a48, 79fce8e272).
  2. The dynamic-fields walk silently dropped live-listed bag children whose defining checkpoint was pruned — every tick, forever, so a session_events entry created astride an epoch boundary never reached the next epoch's MPC manager. Fixed: the provider reports unprovable ids; the verified reader resolves them through verified_object's committee-verified cache fallback (47b78622ea).
  3. MPC output quorum split across the epoch boundary — outputs sequenced in the dying epoch die with its tally, retries carry the rest into the new epoch (observed 2+2 of 4, quorum 3 never reached), and the durable computation-completed flag suppressed recomputation on re-pull. Fixed: the own-output consensus transaction is persisted at submission and re-submitted once per epoch on re-pull (6b247ec132).

The "full consensus halt never recovers" finding was an artifact of the two-laggards test itself: nodes boot at epoch 1, so its wait_for_epoch(1) was a no-op and it stopped 2-of-4 ~0.3s after genesis — mid network DKG. Injected into a warm cluster it recovers and passes (7aa22c6774).

Test-driver fixes along the way: imported-key dwallets sign through request_imported_key_sign (d34e13d381), bounded recovery waits (d39e795f37). Findings ledger updated in dev-docs/plans/simtest-fault-matrix.md.

omersadika and others added 2 commits July 13, 2026 18:35
…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
@omersadika

Copy link
Copy Markdown
Contributor Author

Merge-ready: all four suites green on the final branch state (ff106288ed, includes the revert of the speculative output-resubmission fix):

Suite Run Result
Simtest (11/11, zero ignores) 29263090774
Test Cluster 29263092582
Integration Tests 29263094591
TS Integration 29269077888 ✅ (first attempt 29263096240 hit the known cumulative-load flake — single 623s timeout in all-combinations-future-sign.test.ts, same shape that hit main on 2026-07-11 and cleared on re-run)

Design-review outcome on the "output quorum split across the epoch boundary" claim: retracted and reverted (42a6bd8861). No MPC protocol spans an epoch boundary by design — a session inside the closing epoch's lock target holds the epoch open until its on-chain completion, and a session outside the target never computed in that epoch — and re-reading the probe trace showed the session in question reached quorum moments later (starvation latency, not a lost tally). The simtest gate re-ran on the revert as the falsification check: 11/11 still passes, confirming the two real fixes (checkpoint-pusher gap repair + pruned-children resolution in the dynamic-fields walk) carry the suite. Findings ledger updated accordingly.

omersadika and others added 2 commits July 13, 2026 21:32
…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
@omersadika

Copy link
Copy Markdown
Contributor Author

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:

  • Keypair: removal commit kept (2d036528dc). It remains in branch history — @omersadika's call on rotation/history-scrub before or after merge.
  • Own-output re-submission + unpruned perpetual table: resolved by removal — the fix was retracted after design review (no MPC protocol spans an epoch boundary; the probe trace misread starvation latency as a quorum split) and reverted in 42a6bd8861. The falsification gate stayed 11/11 without it.
  • Spec updates: done in 7cb6d1e2d9ocs-verified-sui-reads.md now specifies the fold loop's fetch-failure semantics (250ms cadence vs the pruning watermark, pending-gap retry with version-safe late folds), the new bag-entry defense layer (skipped_entry_ids resolved through verified_object, mirrored-node exclusion, cache-miss residual), and the anchor-TTL paragraph's relationship to the repair.
  • Consensus-focused review: a dedicated consensus-reviewer pass over the three surviving product commits is running now; results will be posted here.
  • Stale PR body / fault-matrix text: both rewritten (eb64a5b84a + the PR description).
  • Split recommendation: surfaced to @omersadika with the deployment-risk framing (the touched pipeline is the not-yet-live OCS machinery); merging as one PR is their call.

Triage observations — both cited failures were pre-fix commits and are since root-caused:

  • 29194687865 failed at sim_user_flows.rs:166 = the imported-key sign PTB type-mismatch, fixed in d34e13d381 (imported-key dwallets sign via request_imported_key_sign).
  • 29204115374 was the two-laggards genesis-injection artifact (wait_for_epoch(1) is a no-op — nodes boot at epoch 1 — so it decapitated the cluster mid-network-DKG), fixed in 7aa22c6774. The "could not be resolved from the verified cache" spinning you saw is that scenario's expected residual (nodes restarted with pre-fold state while upstream pruned) — now documented in the spec; it is bounded by session completion and does not occur in the warm-cluster shape.
  • Post-fix stability: two independent full gates at 11/11 (29208084278, 29263090774) plus the single-test probes.

…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
@omersadika

Copy link
Copy Markdown
Contributor Author

Consensus-review pass complete (dedicated consensus-reviewer over the three surviving product commits + the revert). Verdicts:

  • Determinism: clean — every changed path is per-validator input/submission side; nothing writes checkpoint content or consensus-agreed state; late folds are version-safe (monotone-by-version cache, fetch_max heads).
  • Skipped-id membership trust: consistent with the reader's existing trust model (direct-only, own-fullnode listing; the membership binding defends against relays, which don't exist on this path; resolved objects remain committee-verified).
  • Epoch-close invariants: no new hazard — late delivery re-enters the deduped re-pull path; no new completion/submission path.
  • Out-of-order folds vs committee capture: safe — installs are head-guarded, BLS-verified, monotone; EOE retention idempotent.
  • Revert of the retracted fix: clean — exact inverse, no surviving references.

Findings fixed in d306bc3af9:

  1. (medium) retry_pending_gaps dropped a gap on ANY fold failure as "deterministic" — but MissingCommittee is transient (committee-follower lag at a Sui epoch boundary / restart near one). Fold failures now retry under the same 600s deadline as fetch failures.
  2. (low) A late gap fold persisted the cache head with its older source seq, regressing the restored head after a restart; the persisted head write is now max-semantics.
  3. (low) The skipped-child resolution leaked a high-water entry per short-lived bag id (against the map's documented policy) — now forgotten after use; and the unresolvable-child warn fires once per id instead of on every ~50ms pump walk.

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.

@omersadika

Copy link
Copy Markdown
Contributor Author

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 sim_presign_traffic_across_boundaries: comment shifts alone produce a different msim schedule, and this schedule pins an epoch again (all_epoch_sessions_finished=false for 14+ virtual minutes). Forensics from that run:

  • All 47 presign sessions were served (popped) — delivery is fine up to that point.
  • Checkpoint creation/certification stops at seq 29 (02:09:17); the executor acts exactly once afterward and then goes silent for the entire pinned window.
  • The window floods with listed dynamic-field child with a pruned defining checkpoint could not be resolved from the verified cache either — the documented residual of the pruned-children fix (entry live on-chain, defining checkpoint pruned upstream, and the local verified cache lacks it too), recurring at walk rate.

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.

@omersadika omersadika changed the title test(simtest): user-flow drivers and happy-path sim tests across epoch boundaries fix(sui-connector): stop permanently losing pruned checkpoints and bag entries (epoch-close wedge); add user-flow sim tests, re-enable all simtest reproducers Jul 13, 2026
…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
@omersadika

Copy link
Copy Markdown
Contributor Author

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 (num_epochs_to_retain_for_checkpoints = Some(2)) — ~40 virtual seconds at the 20s test epochs. Under msim, proof-building burns virtual time while the fullnode keeps producing and pruning, so the OCS pusher occasionally loses an individual contents-lagged checkpoint to pruning before it materializes; if a session_events bag entry rode it, the entry is unresolvable from both upstream and the cache and the epoch pins. The dropped seqs were scattered with no fast-forward/stall lines, i.e. the pusher was keeping up with the head — this was 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.

Fix (ceea1ffe41): disable fullnode checkpoint pruning in the test cluster, restoring that fidelity so the tests exercise epoch-boundary delivery instead of a pruning race. Same class as the wait_for_epoch(1) injection-point correction — a misconfigured test environment, not a product bug. (The product code's pruned-defining-checkpoint residual is genuine and stays documented as defense-in-depth; this just stops the test from manufacturing it artificially.)

Schedule stability confirmed:

  • 8-seed sweep of the previously-flaky sim_presign_traffic_across_boundaries (MSIM_TEST_SEED=1 MSIM_TEST_NUM=8): all 8 schedules pass (29280076112).
  • Full suite 11/11 on the fix SHA (29285586551).

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.

@omersadika

Copy link
Copy Markdown
Contributor Author

✅ Merge-ready — all four suites green on the final tree (ceea1ffe41), schedule-stable

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.

@omersadika omersadika merged commit 8d249fa into main Jul 14, 2026
16 checks passed
@omersadika omersadika deleted the test/simtest-flow-coverage branch July 14, 2026 09:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants