BM-2957: feat(contracts): decouple verification via BoundlessRouter#1982
BM-2957: feat(contracts): decouple verification via BoundlessRouter#1982jonastheis wants to merge 43 commits into
Conversation
Introduces the verification engine that decouples per-class verification dispatch from BoundlessMarket. The router owns a two-mapping registry (entries + classes) with namespace invariants, dispatches per-fill verification on interfaceTag (verifier or joint), and forwards to the class's required assessor when the verifier class is per-fill. UUPS upgradeable, governance-gated `addClass` / `instantiate` / `removeClass` / `removeEntry`, ERC-165 conformance check at instantiation, gas-capped adapter calls so a misbehaving impl can self-rug its sub-batch but not starve sibling sub-batches in the same transaction. Three seam interfaces: - IBoundlessVerifier: per-fill cryptographic check (claim digest only). - IBoundlessJointVerifierAssessor: per-fill combined check + binding. - IBoundlessAssessor: per-batch binding seam. Tests will land in a follow-up.
Phase B of the verifier-router-and-assessor-decoupling epic. Adds two adapter contracts that bridge the existing R0 STARK verification path into the router's universal seam interfaces, so today's R0 Groth16, set-inclusion, and Blake3-Groth16 selectors plug into the new dispatch unchanged. R0BoundlessVerifierAdapter: thin wrapper around any IRiscZeroVerifier. One adapter per BoundlessRouter selector entry, each pinned to a specific underlying verifier — no transitive trust of the upstream R0 router's selector set. Phase C deployment script wires up one adapter per existing R0 selector under the R0_VERIFIER class. R0BoundlessAssessorAdapter: reconstructs today's assessor journal digest verbatim from a seal envelope, then forwards to IRiscZeroVerifier.verify against the pinned image id. The narrow IBoundlessAssessor interface stays universal (rds, cds, seal); journal extras (per-fill id and fulfillmentDataDigest; per-batch callbacks, selectors, prover) ride inside the seal as an envelope, so other assessor classes (signature-batch, threshold-attested, future SP1) plug into the same interface without inheriting R0-STARK-specific fields. The adapter is fully immutable — image-id rotation happens by deploying a new adapter and registering a new R0_ASSESSOR selector in parallel, then tombstoning the old one when ready (mirrors today's DEPRECATED_ASSESSOR_EXPIRES_AT pattern but managed via governance rather than an in-contract timestamp). A TODO calls out post-Phase C guest cleanup (drop redundant journal fields once the market sources them from the verified ProofRequest) for the next image rotation. Also includes a one-line whitespace fix on BoundlessRouter.sol from forge fmt. Tests will land in a follow-up.
… seams Widens IBoundlessAssessor.verifyAssessor and IBoundlessJointVerifierAssessor.verifyJoint to take `address prover` as a universal arg, alongside the existing request/claim digests and seal. The router's verifySubBatch gains the arg and forwards it to both per-fill (joint) and per-batch (assessor) dispatch sites. The market needs a trusted prover address for crediting and slashing. Today's R0 STARK assessor binds it via the journal commitment, but that's adapter-specific — a future signature-based assessor would have to commit to it in the signing payload, threshold-attested impls in their committee message, etc. Making `prover` part of the universal interface forces every adapter to verify the binding via its own mechanism, so the market can trust the value uniformly across class types. R0BoundlessAssessorAdapter: drops `prover` from Envelope (it's now an arg), uses the arg directly in journal reconstruction. The R0 STARK fails if the seal was produced against a different prover than the one passed by the caller. Tests will land in a follow-up.
…sRouter Reshapes the market entrypoints around `SubBatch[]` and ProofRequest- based fulfill, dispatching all verification through the router. Constructor: drops the verifier / applicationVerifier / assessorId / deprecatedAssessorId / deprecatedDuration immutables. Now takes `(BoundlessRouter router, address collateralToken)`. The router holds the verification engine; image-id rotation is adapter-level. Entrypoints: `fulfill`, `fulfillAndWithdraw`, `verifyDelivery`, and the four `submitRootAndFulfill*` variants now take `SubBatch[]`. Each sub-batch carries its own per-fill `ProofRequest[]` and `Fulfillment[]` plus a single `bytes assessorSeal` and `address prover`. The market re-derives `requestDigest` from each request, asserts integrity against the lock (locked path) or signature (priceAndFulfill, where `bytes[][] clientSignatures` is provided per sub-batch), and forwards `(rds, cds, seals, signedSelectors, prover, assessorSeal)` to `router.verifySubBatch`. `signedSelectors` and per-fill callbacks are sourced from the verified `ProofRequest`, not from any assessor journal commitment — `AssessorReceipt` is dropped entirely. The internal `_fulfillAndPay*` helpers take the verified `requestDigest` rather than reading `fill.requestDigest`. The new `MismatchedRequestId` error guards against a `Fulfillment.id` that disagrees with `request.id`. `verifyDelivery` is now just a per-sub-batch loop into the router; the old per-fill merkle reconstruction lives in `R0BoundlessAssessorAdapter`. The deprecated-assessor try/catch fallback at the market level is gone — image rotation is handled by deploying a fresh adapter under a new `R0_ASSESSOR` selector and manually tombstoning the old one when ready. Also: - `imageInfo()`, `setImageUrl()`, and the `imageUrl` storage variable are removed (slot reserved as `__deprecated_imageUrl` to preserve storage layout for upgrades). - `BoundlessMarketLib.encodeConstructorArgs` updated to the new shape. - `Deploy.s.sol` / `Manage.s.sol` updated to read `BOUNDLESS_ROUTER` from the env until the deployment.toml schema is updated to carry it. Tests will land in a follow-up.
…Router
Splits BoundlessRouter operational tooling into its own scripts so the
market scripts stay focused on the market lifecycle.
`Deploy.Router.s.sol` — bootstrap-only. Deploys the router UUPS proxy
and registers the two curated R0 classes:
- `R0_ASSESSOR` (id 0xAA000002) — terminal assessor seam.
- `R0_VERIFIER` (id 0xAA000001) — chain default; required assessor
class is `R0_ASSESSOR`.
Reads `ROUTER_ADMIN` and `DEPLOYER_PRIVATE_KEY` from env.
`Manage.Router.s.sol` — three operations as separate Script contracts:
- `RegisterR0Verifier`: deploy an `R0BoundlessVerifierAdapter` for
one R0 selector and `instantiate` it under `R0_VERIFIER`. Looks up
the underlying impl via the upstream `RiscZeroVerifierRouter`.
- `RegisterR0Assessor`: deploy an `R0BoundlessAssessorAdapter`
pinned to one image id and `instantiate` it under `R0_ASSESSOR` at
a chosen selector. Brokers put that selector in the first 4 bytes
of the assessor seal.
- `RemoveEntry`: tombstone an entry (e.g. a deprecated adapter
after a broker rollover).
The market scripts (`Deploy.s.sol`, `Manage.s.sol`) stay untouched
here — they consume an already-deployed router via the BOUNDLESS_ROUTER
env var. Market upgrades to the router-aware implementation use
`Manage.s.sol::UpgradeBoundlessMarket` after this script set has run
to set up the router infrastructure.
Also: rename the deprecated `__deprecated_imageUrl` storage slot back
to its original `imageUrl` name. The market no longer reads or writes
this field, but keeping the original name preserves the storage
layout for the OZ Upgrades safety check without needing a rename
annotation.
Mirrors the contract-layer rewrite: - IBoundlessMarket.sol artifact picks up the SubBatch-based entrypoints and drops AssessorReceipt + imageInfo. - SubBatch.sol artifact is new. - bytecode.rs regenerated for the new market implementation. CI verifies these checked-in artifacts haven't drifted from the source contracts, so they need to land alongside the contract changes.
| function verifyAssessor( | ||
| bytes32[] calldata requestDigests, | ||
| bytes32[] calldata claimDigests, | ||
| address prover, | ||
| bytes calldata assessorSeal | ||
| ) external view; |
There was a problem hiding this comment.
Thinking a bit about this, in the new path, BoundlessMarket._verifySubBatch passes only requestDigests, claimDigests, seals, and signedSelectors to the router. The R0 assessor adapter then reconstructs leaves using env.fulfillmentDataDigests supplied inside the prover-controlled assessor envelope. But the market later decodes callback data from fill.fulfillmentData.
Before, the market computed fill.fulfillmentDataDigest() itself before building assessor leaves. With this change we are losing that binding. A prover can satisfy the assessor against one fulfillment-data digest while passing different fulfillmentData to the callback path.
The market should compute fulfillment-data digests from sb.fills[I] and pass them through the router/assessor interface. So maybe we could extend IBoundlessAssessor.verifyAssessor and BoundlessRouter.verifySubBatch to include bytes32[] fulfillmentDataDigests?
| for (uint256 i = 0; i < sb.fills.length; i++) { | ||
| Fulfillment calldata fill = sb.fills[i]; | ||
| ProofRequest calldata request = sb.requests[i]; | ||
| bytes32 requestDigest = request.eip712Digest(); |
There was a problem hiding this comment.
after deriving this, the contract should either check fill.requestDigest == requestDigest, as we emit the provided one rather than the one we recompute
| function fulfill(SubBatch[] calldata subBatches) public returns (bytes[] memory paymentError) { | ||
| verifyDelivery(subBatches); | ||
|
|
||
| // Total fill count across all sub-batches; flatten for the return array. |
There was a problem hiding this comment.
Previously for fulfill and submitRootAndFulfill it wasn't required for the prover to post the ProofRequest which was intended to save gas on the hot path (LockAndFulfill). It seems now every fulfill path takes SubBatch which includes the full ProofRequest, so the hot path will take a gas hit
Was this required for the refactor? If not maybe we introduce a
struct LockedSubBatch {
Fulfillment[] fills;
bytes assessorSeal;
address prover;
}
or something to keep the previous behavior?
There was a problem hiding this comment.
It adds indeed quite some gas. I just checked for ~100 requests:
- Mean ABI-encoded ProofRequest size: ~1024 bytes (sample is dominated by order-generator traffic which is fairly uniform, but it's representative of
the actual hot-path workload). - Calldata cost per fill: ~1024 bytes × ~14 gas/byte (mixed nonzero/zero) ≈ ~14k gas/fill.
- Plus EIP-712 re-derivation per fill: ~5k gas/fill.
- Total: ~19k extra gas per fill vs. a LockedSubBatch-shaped hot path.
| /// Terminal seam. Classes with this `interfaceTag` are referenced by other | ||
| /// classes' `requiredAssessorClass` and MUST never be selected as a verifier | ||
| /// class — the router rejects this at `verifySubBatch`. | ||
| interface IBoundlessAssessor { |
There was a problem hiding this comment.
(Related to my other comment about passing ProofRequest into all the fulfill endpoints)
Previously the assessor did two things:
1/ proving each claimDigest is a valid answer for its requestDigest, and prover is who produced it.
2/ surfacing the selector and callback.{addr, gasLimit} to the market so it could dispatch the callback and feed the router.
In this PR the market now gets selector/callback from ProofRequest[] in calldata, which is now possible because the full ProofRequest are now also posted in fulfill and submitRootAndFulfill
If we address my other comment and end up making ProofRequest not required, I think we need to change verifyAssessor to return the selectors/callbacks like:
function verifyAssessor(
bytes32[] calldata requestDigests,
bytes32[] calldata claimDigests,
address prover,
bytes calldata assessorSeal
) external view returns (
bytes4[] memory signedSelectors,
Callback[] memory callbacks
);
There was a problem hiding this comment.
2/ surfacing the selector and callback.{addr, gasLimit} to the market so it could dispatch the callback and feed the router.
This is the main reason I wanted to move the proof request to the calldata. I don't think this is a clean separation of concerns.
I get the concern about the gas cost. So there's clear motivation to avoid this. Especially for every request when most not even use the callback.
Extending the interface to return this data is imo also not super clean as this implies that every Assessor implementation needs to pass this data via seal and decode it internally.
Another option might be to store this data on lock request conditionally if the request has a callback and then load it in fullfill. This way the assessor doesn't need to provide it and most requests don't pay for it.
| /// @notice Add a new class. Governance-only. | ||
| /// @dev Enforces namespace invariants: `classId` is non-zero, non-tombstoned, and | ||
| /// not already in either map. Validates `interfaceTag` is one of the three | ||
| /// accepted values and that `requiredAssessorClass` matches the tag's | ||
| /// expectations (mandatory for verifier classes; absent otherwise). | ||
| function addClass(bytes4 classId, ClassMetadata calldata metadata) external onlyRole(ADMIN_ROLE) { | ||
| if (classId == CHAIN_DEFAULT_SENTINEL) revert ZeroSelectorReserved(); | ||
| if (tombstoned[classId]) revert ClassRemoved(classId); | ||
| if (classes[classId].interfaceTag != bytes4(0)) revert ClassInUse(classId); | ||
| if (entries[classId].impl != address(0)) revert EntryInUse(classId); | ||
|
|
||
| bytes4 tag = metadata.interfaceTag; | ||
| if (!_isVerifierTag(tag) && !_isJointTag(tag) && !_isAssessorTag(tag)) { |
There was a problem hiding this comment.
I don't fully understand the need for the class layer in the router, it seems it gives us:
- Verifier -> assessor binding, gas limit, schema artifact set once per class.
- A distinction between permissionless and non-permissionless verifiers
- A way to bulk disable verifiers
But seems to add a lot of code + complexity, especially as verifiers are rarely added / removed so the potential duplication of things like assessor address, verifier etc doesn't seem like a big deal?
Am i missing something as to why this is important?
There was a problem hiding this comment.
The class layer buys us four things (the last 3 you mentioned already):
- Sign-many selector semantics. A requestor can sign a class id and accept any entry under that class. Without classes, every requestor pins one specific selector (or signs
0x00and gets exactly the one default impl) — they can't say "any R0 verifier flavor is fine." - Required-assessor binding. The verifier↔assessor pairing lives on the class, not on each entry. Adding a 4th R0 verifier flavor doesn't risk it being wired to the wrong assessor.
- Permissionless-tier convenience. A reserved-prefix policy (we already have
RESERVED_PREFIX_MASK = 0xff000000) gates who can register just as well in a flat design. What classes still buy you is bundling the conformance constraints — permissionless registrants inherit(interface tag, required assessor, gas limit, schema)from the class instead of picking those values themselves. Without classes a permissionless registrant could pair their impl with a bogus assessor, but that just self-rugs at verify time, not a soundness issue. - Bulk disable + permissionless gate. One
removeClasstombstones the whole group; onepermissionlessInstantiateflag gates a whole group.
If we want to simplify, we'd flatten to one mapping: each entry carries its own (assessor, gasLimit, schema, permissionless), shrinks the contract by ~100 LOC, and means adding a 4th R0 verifier means duplicating the assessor pointer in storage — which, as you point out, isn't a big deal at v1 scale.
imo the class is really only necessary if we want to enable the flexibility of 1.
Replace ProofRequest in SubBatch with a slim per-fill payload carrying only what the market and assessor need at fulfill time. The market reconstructs each requestDigest from the slim payload and asserts it matches the value stored at lock time (or via FulfillmentContext for the priced path) before dispatching, so downstream consumers can trust the payload without re-verification. Highlights: - New SlimRequest type + reconstruction library; predicate / callback / selector in full, plus pre-computed imageUrlHash / inputDigest / offerDigest. - SubBatch.requests is now SlimRequest[]. - Fulfillment drops the redundant id and requestDigest fields. - IBoundlessAssessor.verifyAssessor widened to (SlimRequest[], Fulfillment[], requestDigests[], prover, seal); BoundlessRouter.verifySubBatch matches. - BoundlessMarket inlines verifyDelivery into fulfill, adds explicit _verifyBinding before router dispatch, and drops the ProofRequest arg from _fulfillAndPay. - priceAndFulfill / submitRootAndPriceAndFulfill take a parallel ProofRequest[][] for the priced path (slim alone can't verify client signatures). - R0BoundlessAssessorAdapter fits the new interface, sourcing every journal field (ids, callbacks, selectors, fulfillment-data digests) from the trusted slim payload; the seal carries only the inner STARK proof. - FulfillmentLibrary gains a calldata-friendly fulfillmentDataDigest overload.
Native Solidity implementation of IBoundlessAssessor that evaluates each
fill's predicate directly on-chain. No zkVM, no merkle tree, no STARK
proof. The market binds each SlimRequest to a signed lock before dispatch,
so the adapter trusts the supplied predicate.
Per-fill checks:
- Predicate satisfaction via PredicateLibrary.eval (DigestMatch, PrefixMatch,
ClaimDigestMatch).
- Claim-digest binding: ReceiptClaimLib.ok(imageId, sha256(journal)) must
reconstruct to fill.claimDigest. Without this, the prover could submit a
valid seal for a different computation entirely.
Per sub-batch:
- Prover signature: ECDSA over the EIP-712 SubBatchAuth(prover, requestDigests,
claimDigests) carried in assessorSeal. The adapter recovers the signer and
asserts it equals the supplied prover address. This is the on-chain
equivalent of the R0 STARK adapter's prover commitment in the journal.
Ships with a Foundry gas bench measuring per-fill cost across N in
{1, 5, 10, 50, 100} for both DigestMatch and ClaimDigestMatch predicates,
through both direct-call and BoundlessRouter-dispatch paths, plus
regression tests for the four revert paths (binding mismatch, predicate
failure, claim-digest mismatch, prover-signature mismatch).
willpote
left a comment
There was a problem hiding this comment.
Overall looks good, will give a more in depth review once we have test / new gas snapshot numbers. For now just feedback on the API design / naming
| ProofRequest[][] calldata priceRequests, | ||
| bytes[][] calldata clientSignatures, |
There was a problem hiding this comment.
Thoughts on having a wrapper to 1/ avoid these 2d arrays, 2/ more symmetry to SubBatch?
e.g.
struct ProofRequestBatch {
requests: ProofRequest[]
signatures: bytes[]
}
There was a problem hiding this comment.
Also how about rename SubBatch -> FulfillmentBatch?
Then API is more intuitive and clearer imo:
priceAndFulfill(
ProofRequestBatch[] requestBatches;
FulfillmentBatch[] fulfillmentBatches;
)
…ced-path args
Address review feedback on the priced-fulfillment API:
- Rename `SubBatch` -> `FulfillmentBatch` across types, externals, router, and
market for clearer naming. `verifySubBatch` -> `verifyBatch`,
`MixedClassWithinSubBatch` -> `MixedClassWithinBatch`,
`EmptySubBatch` -> `EmptyBatch`, `SubBatchAuth` -> `FulfillmentBatchAuth`.
- Introduce `ProofRequestBatch { ProofRequest[] requests; bytes[] signatures }`
and update `priceAndFulfill` / `priceAndFulfillAndWithdraw` /
`submitRootAndPriceAndFulfill*` to take `ProofRequestBatch[]` instead of
parallel `ProofRequest[][] + bytes[][]` arrays. Symmetric to the
`FulfillmentBatch[]` argument so the priced-path API reads cleanly:
priceAndFulfill(
ProofRequestBatch[] requestBatches,
FulfillmentBatch[] fulfillmentBatches
)
Replace the single OnChainAssessorBench file with three focused files sharing a common base: - `BenchBase` (abstract) — router/adapter setup, prover wallet, fixture builders, and three harnesses (DirectHarness, RouterHarness, MultiCallRouterHarness). Registers three sibling assessor entries (OnChainAssessor, R0BoundlessAssessorAdapter via mock IRiscZeroVerifier, NullAssessor) under one assessor class to enable cross-adapter comparison through the router. - `AdapterBench` — measures the assessor adapters in isolation (direct call, no router). Includes DigestMatch and ClaimDigestMatch per-fill gas sweeps for OnChain vs R0, plus a journal-size sweep showing how Steel-style large journals affect per-fill cost. Order- generator commits a 16-byte journal (crates/order-generator/src/main.rs:356), used as the default fixture; 128 B and 512 B variants are also measured. - `RouterBench` — measures the router architecture cost from the market's perspective: "what does the market pay per batch to drive the verification engine, vs. the absolute minimum it could pay if it hardcoded a single assessor adapter and skipped routing entirely?" Includes a framing-cost row and a cold-vs-warm comparison. Harnesses no longer perform the binding check (that's market work; out of scope for adapter/router measurement). Callers pre-compute requestDigests once at fixture build time.
- Slim `_classOf` to `_classTagOf`: read only slot 0 of `ClassMetadata`
instead of copying all 5 slots into memory. The hot path only needs
`interfaceTag` (and `requiredAssessorClass`, also in slot 0).
- Defer the `tombstoned[]` check to the error path in `_entryOf` and
`_classTagOf` — a registered selector cannot simultaneously be
tombstoned (remove clears the value before tombstoning), so the
happy path skips one SLOAD per lookup.
- Add `signedSel == sealSel` and `signedSel == sealClassId` fast paths
to `_matchSignedSelector`: the common case now does zero SLOADs.
- Hoist the verifier/joint tag dispatch out of the per-fill loop and
reuse `firstEntry` for i=0, removing the redundant `_entryOf` call
for the first fill. Loop counter uses `unchecked { ++i; }`.
B.1 router framing overhead (NullVerifier + NullAssessor):
N=1 44,252 -> 24,203 (-45%)
N=10 95,400 -> 68,130 (-29%)
N=50 330,146 -> 270,783 (-18%)
- Cache the last-seen (sealSel, Entry) across the per-fill loop so a batch sharing one selector pays one entry lookup instead of N. This is the common case when a single verifier serves a whole batch. - Use `_isVerifierTag` / `_isJointTag` / `_isAssessorTag` helpers in the hot path instead of inlining `type(I).interfaceId` comparisons. Zero runtime cost (interface ids are compile-time constants), reads cleaner. - Tighten `_entryOf`'s error diagnostics so a malformed seal whose first 4 bytes resolve to a class id or the chain-default sentinel reverts with `EntryIsClass` / `ZeroSelectorReserved` instead of the generic `EntryUnknown`. Cold path only — no hot-path SLOADs added. B.1 router framing overhead vs prior commit (NullVerifier + NullAssessor): N=10 68,130 -> 62,884 (-7.7%) N=50 270,783 -> 240,760 (-11.1%) B.2 cold/warm vs prior commit: N=10 cold 90,740 -> 85,494 (-5.8%); warm 71,680 -> 66,434 (-7.3%) N=50 cold 368,974 -> 338,951 (-8.1%); warm 342,813 -> 312,790 (-8.8%) N=100 cold 740,709 -> 676,445 (-8.7%); warm 698,781 -> 634,517 (-9.2%) Per-fill warm cost drops from ~6,700 to ~6,100 gas.
Add `_forwardCalldataAsStaticCall(impl, gasLimit, selector)` and use it for the per-batch assessor dispatch. The helper writes only the 4-byte destination selector into scratch memory, then `calldatacopy`s the entry-point's calldata tail into the outgoing call -- never copying or re-encoding the args Solidity would otherwise traverse. Reverts bubble verbatim via `returndatacopy + revert`. This works inside an `internal` helper because in the EVM calldata belongs to the current message-call frame, not to a Solidity function; internal calls are JUMPs within the same frame, so `calldatasize()` still references the outer (entry-point) calldata -- exactly the bytes we want to forward. ABI-stability invariant: `verifyBatch` and `IBoundlessAssessor.verifyAssessor` must keep byte-identical calldata tails. The OnChainAssessor and R0BoundlessAssessorAdapter end-to-end tests catch any drift because those adapters fully decode the forwarded args. via_ir inlines the helper at the single call site, so the bench numbers are identical to the equivalent inline-assembly form: B.1 N=50 router-framing overhead 270,783 -> 157,506 (-30.7%) B.2 N=50 cold 338,951 -> 255,697 (-24.6%); warm 312,790 -> 229,536 (-26.6%) Cumulative vs pre-optimization baseline at N=50: framing 330,146 -> 157,506 (-52.3%); per-fill warm ~7,500 -> ~4,700 gas.
…_000 The router and its adapters are the hot path on every market settlement and are deployed once. Bumping their optimizer_runs from the size-tuned default of 100 to 1_000_000 trades a small bytecode-size increase for faster runtime. Rest of the project unchanged. B.1 router framing overhead vs prior commit: N=1 21,807 -> 21,309 (-2.3%) N=10 45,630 -> 43,377 (-4.9%) N=50 157,506 -> 147,453 (-6.4%) B.2 cold/warm vs prior commit: N=10 cold 68,240 -> 65,075 (-4.6%); warm 49,180 -> 46,027 (-6.4%) N=50 cold 255,697 -> 241,372 (-5.6%); warm 229,536 -> 215,223 (-6.2%) N=100 cold 510,691 -> 482,416 (-5.5%); warm 468,763 -> 440,500 (-5.7%) Per-fill warm cost: ~4,700 -> ~4,400 gas. Cumulative vs pre-optimization baseline at N=50: B.1 overhead 330,146 -> 147,453 (-55.3%) B.2 warm 386,176 -> 215,223 (-44.3%)
Extract the always-passing IBoundlessVerifier / IBoundlessAssessor / IRiscZeroVerifier mocks from BenchBase into a shared contracts/test/mocks/RouterMocks.sol so both bench files and future unit-test files can reuse them. Move the OnChainAssessor sanity tests (predicate-failure revert, prover-signature mismatch, claim-digest mismatch, slim-payload reconstruction parity, single-fill happy path) out of AdapterBench.t.sol into a dedicated contracts/test/router/adapters/OnChainAssessor.t.sol. Remove the matching test_router_singleFill_passes from RouterBench.t.sol (belongs in router unit tests; the bench's own pass/fail is sufficient sanity here). Net effect: AdapterBench.t.sol and RouterBench.t.sol now contain only test_bench_* gas-measurement functions; correctness tests live in adapter- and router-specific unit files.
Bring the test file's setUp + harness back into compiling shape against the slim/router architecture. All 133 tests are wrapped in a single TODO(MIGRATE-MARKET) block comment so they can be ported incrementally without compile errors blocking the rest of the suite. Setup now: - Deploys a BoundlessRouter UUPS proxy. - Registers NullVerifier under a default verifier class and NullAssessor under its required-assessor class. Market state-machine tests don't exercise real cryptographic verification; the mocks short-circuit verifier + assessor dispatch so each test runs through the production fulfill path without paying for a STARK. - Deploys BoundlessMarket with the new (BoundlessRouter, collateralToken) constructor. Old AssessorReceipt-based helpers (createFills, createFillAndSubmitRoot, submitRoot, createDeprecatedFills) are also commented out — they relied on AssessorReceipt + set-builder root inclusion proofs that no longer exist. A minimal createFulfillmentBatch helper will be added before the first fulfill test is restored. Inherited helpers preserved verbatim: - Client / SmartContractClient / prover funding and snapshotting - expectMarketBalanceUnchanged, snapshot/expect collateral helpers - newBatch* (build locked-request batches; locks don't touch fulfill, so they port cleanly)
Restore 32 tests that don't depend on the (still TODO) fulfill helper: - 13 account / admin tests (deposit, depositTo, deposits, withdraw, withdrawals, collateral variants, stake withdraw, bytecode size, admin role setup). - 19 lock + submit-request tests covering both the EOA-signed lockRequest path and the lockRequestWithSignature path: happy paths, already-locked / already-fulfilled, bad client signature, prover signature variants (wrong-request, wrong-domain), insufficient funds, expired/lock-expired, and the two invalid-request shapes. Two prover-signature regression tests (testLockRequestWith- SignatureProverSignatureIncorrectRequest /IncorrectDomain) had hardcoded recovered-signer addresses that change with deploy nonce. Switched them to `expectPartialRevert` so they keep their regression purpose without breaking on contract-layout changes.
`BoundlessMarket._lockRequest` writes the domain-bound `requestHash` into `RequestLock.requestDigest`, but the post-refactor `_verifyBinding` was comparing it against the raw EIP-712 struct hash produced by `SlimRequestLibrary.reconstructRequestDigest`. Result: every locked fulfill reverted with `RequestIsNotLockedOrPriced` because the two sides hashed differently. Fix: keep the slim library producing the pure struct hash (its natural output), but have the market wrap each reconstruction with `_hashTypedDataV4` once per fill before comparing. This matches what both `lockRequest` and `priceRequest` write into storage. The priced path inside `_verifyBinding` no longer needs its own `_hashTypedDataV4` call either — both branches compare directly. To absorb the extra local variables the wrap introduces without tripping the Yul stack-too-deep limit, `fulfill` now delegates to two new internal helpers (`_bindAndCollectDigests` and `_settleBatch`). NatSpec on `SlimRequestLibrary.reconstructRequestDigest` and `_verifyBinding` updated to document the struct-hash vs. domain-bound contract. Also ports the first fulfill helper (`_testFulfillSameBlock`) and three tests that consume it (`testFulfillLockedRequest`, `testFulfillLockedRequestWithSig`, `testFulfillNeverLocked`) — these served as the regression check that caught the binding mismatch. New test-side helpers (`createFulfillmentBatch`, `_asArray` overloads for single-element batches) live alongside.
- ClaimDigestMatch fills now post FulfillmentDataType.None with empty fulfillmentData, matching the production shape where the journal doesn't need to be on-chain. Result: ClaimDigestMatch per-fill cost is now perfectly journal-independent (14,375 across 16/128/512 B). - Pad the journal tail with non-zero bytes (0x80..0xff) so any tx-intrinsic gas measurement (4 vs 16 gas per zero/non-zero byte) reflects real journals instead of getting the zero-byte discount. Inner-frame bench numbers don't move (precompile + memory costs are value-independent), but the fixture no longer misleads tx-level measurements. - Add N=2 row to test_bench_adapters and shrink the journalSize sweep to n=1 so the cost of journal-length itself isolates cleanly.
Unwrap and migrate the fulfill/slash families of BoundlessMarket.t.sol to the new FulfillmentBatch + ProofRequestBatch wire shape. Tests retain their original line positions and call into the existing _testFulfillSameBlock / _testFulfillRepeatIndex / _testFulfillAlreadyFulfilled helpers so the diff is bound to body changes, not restructuring. Brings the suite from 36 to 73 passing tests: ranges + large-journal, other-prover-fulfills, already-fulfilled, fully-expired, multiple-same-index, the wasLocked family (incl. stake-rollover, double-fulfill, locker-after-other), the neverLocked family, the dedicated testSlash* block, and the invalid-smart-contract-signature path.
Extends BoundlessMarket.t.sol from 73 to 96 passing tests:
* batch tests (testFulfillLockedRequests, …NoJournal, …AndWithdraw),
* smart-contract-signature tests (priceRequest + lockRequest +
priceAndFulfill variants),
* single-request priceAndFulfill,
* callback / claim-digest tests (11 ports).
Registers `R0BoundlessVerifierAdapter(setVerifier)` in the router under
setVerifier.SELECTOR() so callback fixtures produce one seal that
satisfies both the router's per-fill verifier dispatch and the
BoundlessMarketCallback re-verify. Modifies `createFills` /
`createFillsAndSubmitRoot` / `createFillAndSubmitRoot` in place to
return `FulfillmentBatch`, build set-builder seals over the slim
payload, and drop the assessor-journal aggregation (selector + callback
now live on `SlimRequest` per fill). The deprecated-assessor helper
variants are gone — replaced by router tombstones.
Tests that don't need callback verification keep using the cheap
NullVerifier path under VERIFIER_ENTRY_SEL.
Brings the suite from 96 to 103 passing tests: * `_testSubmitRootAndFulfillSameBlock` + AndWithdraw helpers (in place), * `testSubmitRootAndFulfillLockedRequest`, …WithSig, …AndWithdraw, * `testSubmitRootAndFulfillNeverLocked` + …ProverNoStake, * `testSubmitRootAndPriceAndFulfillLockedRequest`, * `testSubmitRootAndFulfill` (2-request batch). Splits the set-builder fixture back into `createFills` (pure compute, returns `(FulfillmentBatch, bytes32 root)`) and `createFillsAndSubmitRoot` (wrapper that also submits the root via setVerifier), mirroring the original layout. Helpers reuse `_asArray` overloads for singleton calls. Deprecated-assessor helpers and the matching test are restored wrapped (not deleted) so the migration retains a paper trail until equivalent coverage exists at the router-tombstone level.
Wires `R0BoundlessAssessorAdapter(setVerifier, ASSESSOR_IMAGE_ID)` into
the router under `ASSESSOR_R0_SEL = 0x24` (alongside `NullAssessor`)
and adds a broker+guest fixture (`createFillAndSubmitRootR0`,
`createFillsAndSubmitRootR0`) that produces what a broker would hand
the market: per-fill set-builder seals + a journal-bound STARK seal
over the assessor's `(root, callbacks, selectors, prover)` commitment.
The market then drives both adapters end-to-end.
Per-fill construction is now a shared `_buildFillsAndSlim` helper, used
by both `createFills` (NullAssessor path) and the R0 fixture, so the
loop lives in one place.
Ports three tests to the R0 path:
* testPriceAndFulfillWithSelector — happy-path with signed selector,
* testFulfillLockedRequestProverAddressNotMatchAssessorReceipt —
tampered `batch.prover` desyncs the journal digest from the
broker's seal → setVerifier rejects with `VerificationFailed`,
* testFulfillShuffleFills — swapped claimDigest/fulfillmentData
desyncs each per-fill seal from its claim → `VerifierFailed`.
`testFulfillShuffleIds` is dropped (with note): slim-id tampering now
breaks `_verifyBinding` first, already covered by
`testFulfillLockedRequestMultipleRequestsSameIndex`.
`testFulfillRequestWrongSelector` and the two
`*VerificationGasLimit*` tests stay wrapped — selector mismatch is
enforced by `BoundlessRouter._matchSignedSelector` and the per-fill
gas budget is the router entry's `gasLimit`; equivalent coverage
belongs in `BoundlessRouter.t.sol`.
103 → 106 passing tests.
Updates `testUnsafeUpgrade` to the new market constructor `(BoundlessRouter, collateralToken)` and single-arg `initialize(owner)`. The pre-upgrade `imageInfo()` invariant is gone with the slim-payload refactor — both market versions are constructed with the same router and collateral token, and the test just asserts the implementation address rotated. `testGrantAdminRole` is unchanged (admin role lives on the market directly).
Wires the 20 `testBench*` entrypoints to drive the production verification path — `R0BoundlessAssessorAdapter` + setVerifier inclusion proofs — by routing the 3 bench helpers (`benchFulfill`, `benchFulfillWithSelector`, `benchFulfillWithCallback`) through `createFillsAndSubmitRootR0` and the new `fulfill(FulfillmentBatch[])` ABI. Snapshot labels carry a `:v2` suffix so the new numbers coexist with the legacy entries in `BoundlessMarketBench.json` for side-by-side review. Side-by-side: the new fulfill path costs ~40% more per batch than the legacy market — overhead is dominated by `_bindAndCollectDigests` (the slim-payload security gain that the market now does on-chain instead of trusting the assessor STARK), the adapter's per-fill `AssessorCommitment` reconstruction (redundant until the next assessor-image rotation, see adapter NatSpec), larger calldata, and the two-hop market → router → adapter dispatch. The % delta shrinks with callbacks (+22% at N=32) because their fixed `verifyIntegrity` cost dilutes the routing overhead.
…verifyAssessor Aligns `IBoundlessRouter.verifyBatch` and `IBoundlessAssessor.verifyAssessor` on `(FulfillmentBatch calldata batch, bytes32[] calldata requestDigests)`, collapsing the previous five-arg form (slim requests, fills, digests, prover, assessor seal). The market's call site becomes `ROUTER.verifyBatch(batch, requestDigests)` instead of unpacking the batch field-by-field. Both interfaces stay shape-identical so the router can continue to forward its calldata tail verbatim to the assessor adapter via `_forwardCalldataAsStaticCall`. All three adapters (Null, OnChain, R0) and the bench harnesses (DirectHarness, RouterHarness, MultiCallRouterHarness) updated to match. A `_makeBatch(slim, fills, prover, seal)` helper in BenchBase keeps the call sites in AdapterBench / RouterBench / OnChainAssessor.t.sol short. Trade-off: the market->router hop now copies the FulfillmentBatch struct into a contiguous top-level calldata layout (previously each inner calldata field was passed by pointer), costing ~2-3% gas on fulfill. Acceptable for now -- the loop-in-both-router-and-assessor refactor that would recover this is a separate, larger change.
Widens `IBoundlessJointVerifierAssessor.verifyJoint` from `(requestDigest, claimDigest, prover, seal)` to `(SlimRequest request, Fulfillment fill, bytes32 requestDigest, address prover)`. The adapter receives the entire per-fill payload — the slim request (selector, callback, predicate, pre-computed digests) and the full fulfillment (claimDigest, fulfillmentDataType, fulfillmentData, seal) — and chooses what its mechanism actually needs. Rationale: keeps the joint seam future-proof for adapters that need more than just `(requestDigest, claimDigest)` — e.g. predicate-aware joint verifiers, attestation paths that bind to the callback, or journal-reconstructing implementations. Avoids interface churn each time a new joint mechanism wants visibility into another field. Router's call site collapses to `IBoundlessJointVerifierAssessor.verifyJoint(batch.requests[i], batch.fills[i], requestDigests[i], batch.prover)`, removing the separate `claimDigest` and `seal` extractions. No production adapter implements the joint interface yet; existing tests don't exercise this path, so behavior is unchanged for current fulfillments.
…ndlessRouter
* `forge fmt` over the touched contracts/tests (line wraps in multi-arg
function signatures, trailing commas, etc.).
* Extract `IBoundlessRouter` (one method: `verifyBatch`). The market
now depends on the abstract seam; `BoundlessRouter` declares
`implements IBoundlessRouter`. Admin/registration entry points
(`addClass`, `instantiate`, `removeClass`, `removeEntry`) stay on the
concrete contract -- admin tooling only.
* Regenerate Rust artifacts in `crates/boundless-market/src/contracts/`:
- Copy `SlimRequest.sol`, `FulfillmentBatch.sol`, `ProofRequestBatch.sol`,
`IBoundlessRouter.sol` into the artifact folder (build.rs).
- Delete stale `AssessorReceipt.sol` + `SubBatch.sol`.
- Refresh `Fulfillment.sol`, `IBoundlessMarket.sol`, `bytecode.rs` to
reflect the new ABI.
Known follow-up: the Rust SDK at `crates/boundless-market/src/contracts/
boundless_market.rs` still uses the old market ABI and has 13 compile
errors (`Fulfillment.id`, `fulfill(fills, receipt)`, arity drift on
`submitRootAndFulfill` / `priceAndFulfill`). Tracked as Phase D
(broker/SDK port); to be tackled in a focused follow-up PR.
Moves `OnChainAssessor` (the native Solidity assessor adapter) and its unit tests off this branch to keep the audit scope tight. They live on `jonas/onchain-assessor` (branched from this commit's parent) for a follow-up PR. * Delete `contracts/src/router/adapters/OnChainAssessor.sol`. * Delete `contracts/test/router/adapters/OnChainAssessor.t.sol`. * `BenchBase`: drop the `OnChainAssessor` adapter wiring, the `directOnChain` harness, the `ASSESSOR_ON_CHAIN_SEL` selector, and the `_buildOnChainSeal` ECDSA seal builder. * `AdapterBench`: collapse the side-by-side OnChain vs R0 comparison to R0-only. * `IBoundlessAssessor` / `BoundlessRouter`: trim NatSpec references to OnChainAssessor. R0 adapter remains as the v1 assessor. Router stays pluggable, so a re-introduction in a future PR only needs a fresh `instantiate` call against the same `ASSESSOR_CLASS_ID`.
Stand up the router unit-test scaffolding: a shared `RouterTestBase` deploying an empty UUPS proxy, an extended mock catalog covering all three interface tags + the ERC-165 / revert / gas-hog edge cases, and the Section A registry suite (57 tests) covering `addClass`, `removeClass`, default-class state machine, `instantiate`, and `removeEntry` plus their error branches.
Section B of the router test plan: 53 tests covering `verifyBatch` end-to-end — length/shape guards, first-seal resolution, per-fill verifier and joint dispatch (with multi-fill cache and gas-cap behavior), mixed-class detection, assessor dispatch, dispatch ordering, signed-selector resolution (incl. bounded fuzz), and the load-bearing assessor calldata-forward byte-equality + ABI-stability check.
Adds an `entriesPerClass` counter incremented on `instantiate` and decremented on `removeEntry`. `removeClass` now reverts with `ClassHasEntries(classId, live)` if the counter is non-zero, forcing admins to remove pinned impls before tombstoning the class. The router no longer carries entries whose `classId` resolves to a non-live class — `_classTagOf`'s `ClassRemoved` branch becomes defense in depth. Also simplifies the verifyBatch per-fill loop header.
The router's NatSpec on `Entry.gasLimit`, `VerifierFailed`, and `verifyBatch` claimed that the per-fill try/catch isolates sibling fulfillment batches in the same tx. The catch re-reverts and the market doesn't wrap `ROUTER.verifyBatch`, so one bad fill reverts the whole fulfill() call. Tighten the docs to describe what the try/catch actually buys (gas bound + structured VerifierFailed) and add a regression test in BoundlessMarket.t.sol that pins the multi-batch revert behavior. Also drops the unused `PermissionlessNotAllowed` error.
Adds `InvalidGasLimit` and rejects classes registered with
`defaultGasLimit == 0`. Without this check, any `instantiate` caller
that passes `gasLimit == 0` would silently produce an entry pinned at
`staticcall{gas: 0}` — every dispatch OOGs immediately and the failure
mode only surfaces at first fulfillment. The check closes the
governance fat-finger loop at registration time.
Removes the duplicated NatSpec block at the assessor-dispatch site and keeps the full explanation at `_forwardCalldataAsStaticCall`, where the assembly lives. The dispatch site now points at the helper for the ABI-equality invariant, and the helper carries the load-bearing framing along with a note that a unit test pins the byte-equality.
Guards against drift between SlimRequestLibrary.reconstructRequestDigest and ProofRequestLibrary.eip712Digest by mutating each signed field on the slim payload and asserting _verifyBinding reverts.
`testFulfillRevertsOnAnyMutatedSlimField` exhaustively covers per-field tampering across selector, callback, predicate, imageUrlHash, inputDigest, and offerDigest — every EIP-712-bound field except `id` itself. Extend it with a 9th case that swaps `slim.id` to another locked request's id. The new case exercises the binding check's digest comparison against a real second lock rather than a never-locked id: both ids exist in `requestLocks` with non-zero digests, so a regression that only checked `requestLocks[id].requestDigest != 0` would silently accept the swap. The digest reconstructed from the original request's other fields under the swapped id doesn't match the target lock's stored digest, so the comparison still rejects.
Two cleanup items plus a small derivability tweak.
1. Drop wrapped /* */ blocks and TODO(MIGRATE-MARKET) comments now
that equivalent component-level coverage exists in the router
suite. Removed:
- Deprecated-assessor helpers and tests
(createDeprecatedFillAndSubmitRoot,
_testFulfillDeprecatedAssessor, testFulfillDeprecatedAssessor)
— the feature is gone; router-level tombstoning replaces it,
covered by BoundlessRouter.registry.t.sol.
- testFulfillRequestWrongSelector and the two
ApplicationVerificationGasLimit tests — signed-selector
enforcement and per-fill gas budgets are router concerns,
covered by BoundlessRouter.dispatch.t.sol.
- The migration TODO header above BoundlessMarketBasicTest, the
inline "incrementally unwrapped" pointer, and the stale
bench/upgrade migration TODO (BoundlessMarketBench and
BoundlessMarketUpgradeTest already exist).
No active test bodies changed.
2. Move r0JournalDigest — the R0 assessor guest stand-in that
reconstructs the AssessorJournal commitment for test fixtures —
from BoundlessMarket.t.sol to TestUtils.sol alongside the
existing mockSetBuilder / hashLeaf / mockAssessorSeal helpers.
Byte-identical output; the BoundlessMarket.t.sol call site
references the shared symbol.
3. Mark BoundlessMarketTest.setUp() virtual so derived fixtures can
extend it.
R0BoundlessAssessorAdapter.sol documents the operational pattern for
rotating the assessor guest image: deploy a new adapter pinned to the
new image id, instantiate it under the existing assessor class at a
fresh selector, run both selectors in parallel, then removeEntry the
old selector once brokers have migrated. The mechanism is
router-level tombstoning rather than a time-based deprecated-assessor
flag.
Add a dedicated fixture that inherits from BoundlessMarketTest,
registers a second R0BoundlessAssessorAdapter pinned to a fresh
image id alongside the production one, and covers the two
load-bearing scenarios:
* Parallel operation: locks settled via either selector succeed
while both are live.
* Post-tombstone lifecycle on a single locked request: a stale
broker on the old selector hits BoundlessRouter.EntryRemoved
while a migrated broker on the new selector settles the same
lock — the lock binds no assessor selector, so the path to
payment survives as long as the required assessor class has
any live entry.
Helpers reuse TestUtils.r0JournalDigest + mockSetBuilder. A small
_buildBatchFor variant of the base fixture's createFillsAndSubmitRootR0
parameterizes on image id + selector so the same path produces
batches for either adapter.
R0BoundlessAssessorAdapter has only been exercised end-to-end through
the market fixture so far. Add a standalone suite that drives the
adapter directly with a NullRiscZeroVerifier and pins the behavior
the market path obscures:
* Input shape gating — LengthMismatch on requests/fills/digests
length divergence; MalformedSeal on assessorSeal under 4 bytes;
exactly-4-byte seal forwards an empty innerSeal (the lower edge
of the gate).
* Inner-seal stripping + journal forwarding — vm.expectCall asserts
the underlying verify receives the verbatim post-prefix bytes and
the expected journalDigest, computed via TestUtils.r0JournalDigest
so the reference reconstruction stays in sync with the adapter.
* Sparse callback/selector arrays — none/some/all of three fills,
with non-contiguous indices to exercise the index field as
distinct from the array position.
* Tamper detection — independently perturb prover, slim.id,
fill.claimDigest, and fill.fulfillmentData; assert each shifts
the journalDigest the adapter forwards, confirming every
journal-bound field actually binds.
All happy paths use vm.expectCall against the controllable null R0
verifier so failures surface as a divergence between the adapter's
output and TestUtils.r0JournalDigest rather than as a downstream
STARK error.
Pairs with testFulfillMultiBatchRevertingFillRevertsWholeTx (negative
case: one bad fill kills the tx) by pinning the positive shape: a
single fulfill() call settles three batches that each take a
different dispatch path through the router.
* batch A — NullVerifier + NullAssessor (mock).
* batch B — R0 setVerifier + R0 assessor (production path).
* batch C — NullJoint under a freshly registered joint class
(no assessor seam; assessorSeal must be empty).
The fixture only registers verifier + assessor classes today, so the
joint path needs a NullJoint entry instantiated inline. The
requestor of batch C signs the joint class id directly so the
signed-selector check matches under that class.
Confirms the router walks each batch's dispatch tree independently
and the market settles every fill in one tx — each client paid
1 ether at lock time, the prover collects 3 ether at fulfillment.
CI's forge fmt --check was failing on five test files added or touched in this branch. Run forge fmt to bring them in line. No behavioral changes — pure whitespace and line-wrapping adjustments.
Triaged the inline TODOs left in the router test suite:
* Most were stale review questions answered by adjacent tests
(signed-selector depth, joint dispatch happy path, single-class
rationale, fuzz mechanics) — replaced with brief inline notes
where context was actually missing, otherwise removed.
* `test_signedSelector_revertsOnSignedEntryMismatch`: variable name
was misleading (claimed "same class" but the entry lives in a
separate class so the entry-mismatch branch is reachable);
renamed to `otherEntry` and expanded the inline comment to
explain why a separate class is required.
* `test_instantiate_revertsForNonAdminOnReservedPrefix_permissionless`:
expanded the comment to spell out that the reserved-prefix policy
is entry-selector-only — class ids in the 0x00xxxxxx range can
perfectly well be permissionless.
* BenchBase R0 seal: was 200 zero bytes, which understates calldata
gas (4 gas/byte vs 16 gas/byte for non-zero). Fill with 0xAA so
the bench numbers match production seal cost.
Summary
Decouple per-class verification dispatch from
BoundlessMarketby introducing a governance-controlledBoundlessRouter. The market becomes a thin orchestrator: it binds each fill's slim payload to a client-signed request digest, then hands the batch to the router. The router resolves the seal's first-4-byte selector to a registered adapter, dispatches to the right per-fill interface, and tail-calls the class's required assessor adapter once per batch.Adding new verifier classes — alternate zkVMs, signature-backed joint verifiers, future proof systems — no longer touches
BoundlessMarket.sol. New classes plug in by registering an adapter address under a class id.Architecture
Entry point is
BoundlessMarket.fulfill(FulfillmentBatch[]). Each batch carries(SlimRequest[], Fulfillment[], assessorSeal, prover).Three interfaces define the seam taxonomy:
IBoundlessVerifier— per-fill cryptographic check; sees(seal, claimDigest)only. Pairs with a separate assessor seam.IBoundlessJointVerifierAssessor— per-fill combined seal verification + request-claim-prover binding. Skips the assessor seam.IBoundlessAssessor— per-batch fulfillment-check binding. Terminal — referenced by verifier classes viarequiredAssessorClass, never selected as a verifier class.Contracts in this PR
src/router/BoundlessRouter.solentries+classes), namespace invariants, permanent tombstoning, governance-gated mutations. UUPS-upgradeable.src/router/interfaces/IBoundlessRouter.solsrc/router/interfaces/IBoundlessVerifier.solsrc/router/interfaces/IBoundlessAssessor.solsrc/router/interfaces/IBoundlessJointVerifierAssessor.solsrc/router/adapters/R0BoundlessVerifierAdapter.solsrc/router/adapters/R0BoundlessAssessorAdapter.solproverarg.src/types/SlimRequest.solsrc/types/FulfillmentBatch.sol{ requests, fills, assessorSeal, prover }.src/types/ProofRequestBatch.solsrc/BoundlessMarket.sol(BoundlessRouter, collateralToken).fulfill / priceAndFulfill / submitRootAndFulfill*takeFulfillmentBatch[].AssessorReceiptgone; request-derived data sourced from binding-verified slim payloads.src/IBoundlessMarket.solscripts/Deploy.Router.s.solscripts/Manage.Router.s.solRegisterR0Verifier/RegisterR0Assessor/RemoveEntry.Plus updates to
scripts/Deploy.s.sol,scripts/Manage.s.sol,src/libraries/BoundlessMarketLib.sol,src/types/Fulfillment.sol; deletion ofsrc/types/AssessorReceipt.sol; regeneratedcrates/boundless-market/src/contracts/artifacts/+bytecode.rs.Tests
test/router/BoundlessRouter.registry.t.soladdClass/instantiate/removeClass/removeEntryinvariants.test/router/BoundlessRouter.dispatch.t.solverifyBatchdispatch tree: class resolution, signed-selector matching, per-fill catch, assessor seam.test/router/RouterTestBase.soltest/router/BenchBase.soltest/router/RouterBench.t.soltest/router/AdapterBench.t.soltest/mocks/RouterMocks.solNullRiscZeroVerifier.test/router/R0BoundlessAssessorAdapter.t.soltest/router/R0AssessorImageRotation.t.solThe rewritten
test/BoundlessMarket.t.sol(1979 changed lines) ports the existing market suite onto the new architecture — locked / never-locked / partial-payment paths, smart-contract signatures, callbacks, set-builder R0 fixtures, slim-payload binding coverage.Key design decisions
SlimRequestcarries predicate, callback, and selector in full plusimageUrlHash,inputDigest,offerDigest. The market reconstructs the EIP-712 struct hash from these fields and compares against the digest stored at lock time (or in transient storage frompriceRequest). Once the comparison passes, every slim field is bound to the client-signed request — downstream consumers (router, adapters, callback dispatch) trust the payload without re-verification.proverarg. Threaded through the router into bothIBoundlessJointVerifierAssessor.verifyJointandIBoundlessAssessor.verifyAssessoras the address the market will credit / slash. Each adapter binds it via its own mechanism (R0 STARK journal commitment, signature payload, attestation message). The market trusts the value uniformly.entries[selector]pins a concrete impl;classes[classId]groups conformant adapters with shared interface tag, required assessor class, and gas defaults. Both maps share abytes4namespace; tombstoning is permanent so an EIP-712-signed request can never be silently repointed by a later registration.removeClassrefuses to tombstone a class with live entries (entriesPerClasscounter is the guard).BoundlessRouter.verifyBatch(FulfillmentBatch, bytes32[])andIBoundlessAssessor.verifyAssessor(FulfillmentBatch, bytes32[])share an ABI tail, so the assessor staticcall reuses verbatim calldata via a singlecalldatacopy— skipping ABI re-encoding for the batch payload.R0BoundlessAssessorAdapteris immutable per image id. To rotate: deploy a fresh adapter at a new selector under the existingR0_ASSESSORclass, run both in parallel during broker transition, thenremoveEntry(oldSelector)to tombstone. The lock binds no assessor selector, so brokers holding locks can switch selectors at fulfillment time without re-locking.IBoundlessJointVerifierAssessorinterface ships as part of the seam taxonomy; no production impl in this PR. The dispatch path is fully covered byRouterMocks.NullJoint/RevertingJointin the test suite.