Skip to content

BM-3091: feat(cli): default prover fulfill to the on-chain assessor - #2064

Merged
jonastheis merged 5 commits into
mainfrom
jonas/cli-onchain-assessor
Jul 16, 2026
Merged

BM-3091: feat(cli): default prover fulfill to the on-chain assessor#2064
jonastheis merged 5 commits into
mainfrom
jonas/cli-onchain-assessor

Conversation

@jonastheis

@jonastheis jonastheis commented Jul 14, 2026

Copy link
Copy Markdown
Contributor

Summary

boundless prover fulfill was hardwired to the R0 zkVM assessor: --assessor-selector was a required flag and the assessor guest was always proven, even though the router-native market ships a cheaper native assessor. The flag now defaults to the on-chain assessor (0x00000022), bringing the CLI in line with the broker's preferred path (#2005, #2040). Any other selector (e.g. 0x00000024) keeps the previous guest-based flow, so the old behavior remains one flag away.

Main changes

  • AssessorMode enum on OrderFulfiller (crates/boundless-cli/src/lib.rs): R0 { selector } proves the assessor guest and seals with its set-inclusion proof (unchanged behavior); Onchain { selector, adapter, signer } skips the assessor guest entirely — no ELF download, no proof, order claims only in the set-builder aggregation — and seals the batch with an EIP-712 FulfillmentBatchAuth signature via the SDK's build_onchain_assessor_seal.
  • prover fulfill (commands/prover/fulfill.rs): --assessor-selector defaults to ONCHAIN_ASSESSOR_SELECTOR; on the default the OnChainAssessor adapter address is discovered through the market's router (router_entry_impl), with a clear error pointing at the R0 selector when the market has no on-chain assessor registered. The selected assessor is shown in the command output.
  • Mechanical call-site adaptations: boundless-ffi (unchanged behavior — its Forge deployment test pins the R0 selector explicitly), slasher and indexer test helpers.
  • New test test_fulfill_onchain_assessor: asserts the seal is selector ‖ 65-byte signature recovering to the prover over the exact hash OnChainAssessor reconstructs, and covers the single-leaf aggregation with the assessor guest skipped.

Seal construction per mode

orders ──► prove order guests ──► set-builder aggregation ──► per-fill set-inclusion seals
                │
                ├─ R0 (override):  prove assessor guest ──► include in aggregation ──► assessorSeal = 0x00000024 ‖ set-inclusion proof
                │
                └─ Onchain (default):  sign FulfillmentBatchAuth(prover, requestDigests, claimDigests)
                                       ──► assessorSeal = 0x00000022 ‖ 65-byte ECDSA sig

Commit-reveal fix (SDK)

Live testing on Base Sepolia staging surfaced a client-side race in the open-path commit-reveal flow, fixed here in boundless-market: the reveal's gas estimation simulates against the current head, and on nodes where the pending tag aliases latest (op-geth) that is the commit's own block — committedBlock + COMMIT_REVEAL_MIN_BLOCKS > block.number holds and the estimation reverts MissingFulfillmentCommitment before the reveal is ever broadcast. The fix retries the reveal dispatch (bounded, 15s) when its estimation reverts MissingFulfillmentCommitment: nodes that estimate on a next-block env (anvil, L1 geth) pass on the first attempt with no added latency, while op-geth-style nodes retry until the head passes the commit block. Verified live on Base staging (estimation revert observed, one 500ms retry, reveal landed two blocks after the commit) and against the anvil test suite (no timing impact — an earlier pre-wait variant of this fix deterministically lost the test_slash_fulfilled race to the slasher's poll loop, which is why the retry design won). This affects every open-path submitter — the CLI fulfiller and the broker alike.

Live validation (Base Sepolia staging, market 0x7abb…ca8e)

Ran the T6 cells of docs/2026-07-09_staging-router-proof-matrix-runbook.md against the shared staging market:

Cell Assessor Commit Reveal Checks
T6a never-locked, CLI default 00000022 (sig) 0x8319…0823 0x1723…b756 fulfilled ✓ seal prefix ✓ commitment consumed ✓ replay reverts ✓
T6a never-locked, CLI --assessor-selector 0x00000024 00000024 (STARK guest) 0xc8d0…27ad 0xdead…e0fd same, incl. deployed-assessor-guest proving (#2060) ✓
T6b lock-expired, broker 00000022 (sig) 0xac3e…f907 0x2b42…29a1 fulfilled after lock expiry ✓ commit from broker ≠ locker ✓ replay reverts ✓

Every reveal landed ≥ 1 block after its commit, and replaying any reveal's exact calldata reverts MissingFulfillmentCommitment (0x4b46580b).

Related: #1982 (router decoupling, migrated the CLI to the batched ABI), #2005 (OnChainAssessor), #2040 (broker assessor selection), #2060 (Digest wire format, validated live by the second cell).

The fulfill command's --assessor-selector flag now defaults to the
on-chain assessor (0x00000022). In that mode the assessor guest is
neither downloaded nor proven: only the order claims are aggregated,
and the batch is sealed with an EIP-712 FulfillmentBatchAuth signature
verified by the OnChainAssessor adapter, whose address is discovered
through the market's router. Passing any other selector (e.g.
0x00000024) keeps the previous guest-based R0 assessor path.
@github-actions github-actions Bot changed the title feat(cli): default prover fulfill to the on-chain assessor BM-3091: feat(cli): default prover fulfill to the on-chain assessor Jul 14, 2026
@linear

linear Bot commented Jul 14, 2026

Copy link
Copy Markdown

BM-3091

The open-path reveal is broadcast via a send() whose gas estimation
simulates against the current head. On nodes where the pending tag
aliases latest (op-geth), that is the commit's own block, where
committedBlock + COMMIT_REVEAL_MIN_BLOCKS > block.number holds and the
estimation reverts MissingFulfillmentCommitment before the reveal is
ever broadcast. Awaiting the commit receipt only guarantees the reveal
is mined in a later block, not that it is estimated against one.

After recording the commitment, wait (bounded) for the chain head to
pass the commit block so the estimation context satisfies the same
condition the mined transaction will. Chains that only mine on demand
(anvil) time out of the grace period and proceed; their estimation
runs on a next-block env and passes anyway.

Found live on Base Sepolia staging, where the race hit on every
first attempt; verified fixed there via both the CLI fulfiller and
the broker open-path submission.
…e head

The unconditional post-commit wait burned its full grace period on
chains that only mine on demand (anvil), delaying every open-path
reveal by ~10s and handing the slasher's poll loop a guaranteed win
over the reveal in test_slash_fulfilled - turning a pre-existing flaky
race into a deterministic CI failure.

Drop the wait and retry the reveal dispatch (bounded) when its gas
estimation reverts MissingFulfillmentCommitment: nodes that estimate
on a next-block env (anvil, L1 geth) pass on the first attempt with no
added latency, while nodes where the pending tag aliases latest
(op-geth) retry until the head passes the commit block - the case
observed live on Base Sepolia staging.
@jonastheis
jonastheis merged commit 3835bc4 into main Jul 16, 2026
17 checks passed
@jonastheis
jonastheis deleted the jonas/cli-onchain-assessor branch July 16, 2026 02:20
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