Skip to content

Commit 4a2f4a3

Browse files
mothrannategraf
andauthored
BM-279: Added single entry point for submitMerkle + fulfillBatches (github#91)
This PR introduces a single transaction entry point for fulfillment of orders to simplify and reduce errors between merkle submission and order fulfillment. It does so by adding a optional method on the proof market to perform both actions in a single TXN. --------- Co-authored-by: Victor Graf <[email protected]>
1 parent e7293b1 commit 4a2f4a3

File tree

6 files changed

+226
-43
lines changed

6 files changed

+226
-43
lines changed

contracts/src/IProofMarket.sol

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -168,6 +168,15 @@ interface IProofMarket {
168168
/// Fulfills a batch of locked requests
169169
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal) external;
170170

171+
/// Optional path to combine submitting a new merkle root to the set-verifier and then calling fulfillBatch
172+
/// Useful to reduce the transaction count for fulfillments
173+
function submitRootAndFulfillBatch(
174+
bytes32 root,
175+
bytes calldata seal,
176+
Fulfillment[] calldata fills,
177+
bytes calldata assessorSeal
178+
) external;
179+
171180
/// When a prover fails to fulfill a request by the deadline, this method can be used to burn
172181
/// the associated prover stake.
173182
function slash(uint192 requestId) external;

contracts/src/ProofMarket.sol

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import {SafeCast} from "@openzeppelin/contracts/utils/math/SafeCast.sol";
99
import {EIP712} from "@openzeppelin/contracts/utils/cryptography/EIP712.sol";
1010
import {ECDSA} from "@openzeppelin/contracts/utils/cryptography/ECDSA.sol";
1111
import {IRiscZeroVerifier, Receipt, ReceiptClaim, ReceiptClaimLib} from "risc0/IRiscZeroVerifier.sol";
12+
import {IRiscZeroSetVerifier} from "./IRiscZeroSetVerifier.sol";
1213

1314
import {IProofMarket, ProvingRequest, Offer, Fulfillment, AssessorJournal} from "./IProofMarket.sol";
1415
import {ProofMarketLib} from "./ProofMarketLib.sol";
@@ -266,7 +267,7 @@ contract ProofMarket is IProofMarket, EIP712 {
266267
emit RequestFulfilled(fill.id, fill.journal, fill.seal);
267268
}
268269

269-
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal) external {
270+
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal) public {
270271
// TODO(victor): Figure out how much the memory here is costing. If it's significant, we can do some tricks to reduce memory pressure.
271272
bytes32[] memory claimDigests = new bytes32[](fills.length);
272273
uint192[] memory ids = new uint192[](fills.length);
@@ -362,6 +363,17 @@ contract ProofMarket is IProofMarket, EIP712 {
362363
function imageInfo() external view returns (bytes32, string memory) {
363364
return (ASSESSOR_ID, imageUrl);
364365
}
366+
367+
function submitRootAndFulfillBatch(
368+
bytes32 root,
369+
bytes calldata seal,
370+
Fulfillment[] calldata fills,
371+
bytes calldata assessorSeal
372+
) external {
373+
IRiscZeroSetVerifier setVerifier = IRiscZeroSetVerifier(address(VERIFIER));
374+
setVerifier.submitMerkleRoot(root, seal);
375+
fulfillBatch(fills, assessorSeal);
376+
}
365377
}
366378

367379
// Functions copied from OZ MerkleProof library to allow building the Merkle tree above.

contracts/test/ProofMarket.t.sol

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -160,9 +160,9 @@ contract ProofMarketTest is Test {
160160
return (fills[0], seal);
161161
}
162162

163-
function fulfillRequestBatch(ProvingRequest[] memory requests, bytes[] memory journals)
163+
function createFills(ProvingRequest[] memory requests, bytes[] memory journals)
164164
internal
165-
returns (Fulfillment[] memory fills, bytes memory assessorSeal)
165+
returns (Fulfillment[] memory fills, bytes memory assessorSeal, bytes32 root)
166166
{
167167
// initialize the fullfillments; one for each request;
168168
// the seal is filled in later, by calling fillInclusionProof
@@ -182,15 +182,25 @@ contract ProofMarketTest is Test {
182182
TestUtils.mockAssessor(fills, ASSESSOR_IMAGE_ID, proofMarket.eip712DomainSeparator());
183183
// compute the batchRoot of the batch Merkle Tree (without the assessor)
184184
(bytes32 batchRoot, bytes32[][] memory tree) = TestUtils.mockSetBuilder(fills);
185-
// Join the batchRoot with the assessor digest.
186-
bytes32 root = MerkleProofish._hashPair(batchRoot, assessorClaim.digest());
187-
// submit the root to the set verifier
188-
publishRoot(root);
185+
186+
root = MerkleProofish._hashPair(batchRoot, assessorClaim.digest());
187+
189188
// compute all the inclusion proofs for the fullfillments
190189
TestUtils.fillInclusionProofs(setVerifier, fills, assessorClaim.digest(), tree);
191190
// compute the assessor seal
192191
assessorSeal = TestUtils.mockAssessorSeal(setVerifier, batchRoot);
193192

193+
return (fills, assessorSeal, root);
194+
}
195+
196+
function fulfillRequestBatch(ProvingRequest[] memory requests, bytes[] memory journals)
197+
internal
198+
returns (Fulfillment[] memory fills, bytes memory assessorSeal)
199+
{
200+
bytes32 root;
201+
(fills, assessorSeal, root) = createFills(requests, journals);
202+
// submit the root to the set verifier
203+
publishRoot(root);
194204
return (fills, assessorSeal);
195205
}
196206

@@ -749,6 +759,19 @@ contract ProofMarketTest is Test {
749759
benchFulfillBatch(128);
750760
}
751761

762+
function testsubmitRootAndFulfillBatch() public {
763+
(ProvingRequest[] memory requests, bytes[] memory journals) = newBatch(2);
764+
(Fulfillment[] memory fills, bytes memory assessorSeal, bytes32 root) = createFills(requests, journals);
765+
766+
bytes memory seal =
767+
verifier.mockProve(SET_BUILDER_IMAGE_ID, sha256(abi.encodePacked(SET_BUILDER_IMAGE_ID, root))).seal;
768+
proofMarket.submitRootAndFulfillBatch(root, seal, fills, assessorSeal);
769+
770+
for (uint256 j = 0; j < fills.length; j++) {
771+
assertTrue(proofMarket.requestIsFulfilled(fills[j].id), "Request should have fulfilled status");
772+
}
773+
}
774+
752775
function testProcessTree2() public pure {
753776
bytes32[] memory leaves = new bytes32[](2);
754777
leaves[0] = 0x6a428060b5d51f04583182f2ff1b565f9db661da12ee7bdc003e9ab6d5d91ba9;

crates/boundless-market/src/contracts/proof_market.rs

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -383,6 +383,32 @@ where
383383
Ok(())
384384
}
385385

386+
pub async fn submit_merkle_and_fulfill(
387+
&self,
388+
root: B256,
389+
seal: Bytes,
390+
fulfillments: Vec<Fulfillment>,
391+
assessor_seal: Bytes,
392+
) -> Result<(), MarketError> {
393+
tracing::debug!("Calling submitRootAndFulfillBatch({root:?}, {seal:x}, {fulfillments:?}, {assessor_seal:x})");
394+
let call = self
395+
.instance
396+
.submitRootAndFulfillBatch(root, seal, fulfillments, assessor_seal)
397+
.from(self.caller);
398+
tracing::debug!("Calldata: {}", call.calldata());
399+
let pending_tx = call.send().await.map_err(IProofMarketErrors::decode_error)?;
400+
tracing::debug!("Broadcasting tx {}", pending_tx.tx_hash());
401+
let tx_hash = pending_tx
402+
.with_timeout(Some(self.timeout))
403+
.watch()
404+
.await
405+
.context("failed to confirm tx")?;
406+
407+
tracing::info!("Submitted merkle root and proof for batch {}", tx_hash);
408+
409+
Ok(())
410+
}
411+
386412
/// Checks if a request is locked in.
387413
pub async fn is_locked_in(&self, request_id: U256) -> Result<bool, MarketError> {
388414
tracing::debug!("Calling requestIsLocked({})", request_id);
@@ -889,4 +915,59 @@ mod tests {
889915
assert_eq!(journal, fulfillment.journal);
890916
assert_eq!(seal, fulfillment.seal);
891917
}
918+
919+
#[tokio::test]
920+
#[traced_test]
921+
async fn test_e2e_merged_submit_fulfill() {
922+
// Setup anvil
923+
let anvil = Anvil::new().spawn();
924+
925+
let ctx = TestCtx::new(&anvil).await.unwrap();
926+
927+
let eip712_domain = eip712_domain! {
928+
name: "IProofMarket",
929+
version: "1",
930+
chain_id: anvil.chain_id(),
931+
verifying_contract: *ctx.customer_market.instance().address(),
932+
};
933+
934+
let request = new_request(1, &ctx).await;
935+
936+
let request_id =
937+
ctx.customer_market.submit_request(&request, &ctx.customer_signer).await.unwrap();
938+
939+
// fetch logs to retrieve the customer signature from the event
940+
let logs = ctx.customer_market.instance().RequestSubmitted_filter().query().await.unwrap();
941+
942+
let (_, log) = logs.first().unwrap();
943+
let log = log.log_decode::<IProofMarket::RequestSubmitted>().unwrap();
944+
let request = log.inner.data.request;
945+
let customer_sig = log.inner.data.clientSignature;
946+
947+
// Deposit prover balances
948+
ctx.prover_market.deposit(parse_ether("1").unwrap()).await.unwrap();
949+
950+
// Lockin the request
951+
ctx.prover_market.lockin_request(&request, &customer_sig, None).await.unwrap();
952+
assert!(ctx.customer_market.is_locked_in(request_id).await.unwrap());
953+
assert!(ctx.customer_market.get_status(request_id).await.unwrap() == ProofStatus::Locked);
954+
955+
// mock the fulfillment
956+
let (root, set_verifier_seal, fulfillment, market_seal) =
957+
mock_singleton(request_id, eip712_domain);
958+
959+
let fulfillments = vec![fulfillment];
960+
// publish the committed root + fulfillments
961+
ctx.prover_market
962+
.submit_merkle_and_fulfill(root, set_verifier_seal, fulfillments.clone(), market_seal)
963+
.await
964+
.unwrap();
965+
966+
// retrieve journal and seal from the fulfilled request
967+
let (journal, seal) =
968+
ctx.customer_market.get_request_fulfillment(request_id).await.unwrap();
969+
970+
assert_eq!(journal, fulfillments[0].journal);
971+
assert_eq!(seal, fulfillments[0].seal);
972+
}
892973
}

crates/broker/src/config.rs

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,11 @@ pub struct BatcherConfig {
120120
pub block_deadline_buffer_secs: u64,
121121
/// Timeout, in seconds for transaction confirmations
122122
pub txn_timeout: Option<u64>,
123+
/// Use the single TXN submission that batchs submit_merkle / fulfill_batch into
124+
/// A single transaction. Requires the `submitRootAndFulfillBatch` method
125+
/// be present on the deployed contract
126+
#[serde(default)]
127+
pub single_txn_fulfill: bool,
123128
}
124129

125130
impl Default for BatcherConfig {
@@ -130,6 +135,7 @@ impl Default for BatcherConfig {
130135
batch_max_fees: None,
131136
block_deadline_buffer_secs: 120,
132137
txn_timeout: None,
138+
single_txn_fulfill: false,
133139
}
134140
}
135141
}
@@ -335,7 +341,8 @@ req_retry_count = 0
335341
batch_max_time = 300
336342
batch_size = 2
337343
block_deadline_buffer_secs = 120
338-
txn_timeout = 45"#;
344+
txn_timeout = 45
345+
single_txn_fulfill = true"#;
339346

340347
const BAD_CONFIG: &str = r#"
341348
[market]
@@ -421,6 +428,7 @@ error = ?"#;
421428
assert_eq!(config.prover.status_poll_ms, 1000);
422429
assert!(config.prover.bonsai_r0_zkvm_ver.is_none());
423430
assert_eq!(config.batcher.txn_timeout, Some(45));
431+
assert_eq!(config.batcher.single_txn_fulfill, true);
424432
}
425433
tracing::debug!("closing...");
426434
}

0 commit comments

Comments
 (0)