Skip to content

Commit a0ec0fb

Browse files
authored
BM-271: Add deliver and deliverBatch for "altruistic fulfillment" (#107)
- split out the ProofDelivered event - update IProofMarket in the RFC - rename verifyDelivery and add some basic tests - update rfc.md Supersedes #103 Closes github#85
1 parent 699383d commit a0ec0fb

File tree

5 files changed

+154
-27
lines changed

5 files changed

+154
-27
lines changed

contracts/src/IProofMarket.sol

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -99,8 +99,13 @@ interface IProofMarket {
9999
event RequestSubmitted(ProvingRequest request, bytes clientSignature);
100100
/// Event logged when a request is locked in by the given prover.
101101
event RequestLockedin(uint192 indexed requestId, address prover);
102-
/// Event logged when a request is fulfilled, outside of a batch.
103-
event RequestFulfilled(uint192 indexed requestId, bytes journal, bytes seal);
102+
/// Event logged when a request is fulfilled.
103+
event RequestFulfilled(uint192 indexed requestId);
104+
/// @notice Event logged when a proof is delivered that satisfies the requests requirements.
105+
/// @dev It is possible for this event to be logged multiple times for a single request. This
106+
/// is usually logged as part of order fulfillment, however it can also be logged by a prover
107+
/// sending the proof without payment.
108+
event ProofDelivered(uint192 indexed requestId, bytes journal, bytes seal);
104109
/// Event when prover stake is burned for failing to fulfill a request by the deadline.
105110
event LockinStakeBurned(uint192 indexed requestId, uint96 stake);
106111
/// Event when a deposit is made to the proof market.
@@ -172,10 +177,24 @@ interface IProofMarket {
172177
/// Note that this can differ from the address of the prover that locked the
173178
/// request. When they differ, the locked-in prover is the one that received payment.
174179
function fulfill(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external;
175-
176180
/// @notice Fulfills a batch of locked requests. See IProofMarket.fulfill for more information.
177181
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) external;
178182

183+
/// @notice Delivers a proof satisfying a referenced request, without modifying contract state.
184+
/// In particular, calling this method will not result in payment being sent to the prover, or
185+
/// marking the request as fulfilled.
186+
/// @dev This method is useful for when an interested third party wants to delivery a proof for
187+
/// a request even if they will not be paid for doing so.
188+
/// @param fill The fulfillment information, including the journal and seal.
189+
/// @param assessorSeal The seal from the Assessor guest, which is verified to confirm the
190+
/// request's requirements are met.
191+
/// @param prover The address of the prover that produced the fulfillment.
192+
/// Note that this can differ from the address of the prover that locked the
193+
/// request.
194+
function deliver(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external;
195+
/// @notice Delivers a batch of proofs. See IProofMarket.deliver for more information.
196+
function deliverBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) external;
197+
179198
/// @notice Combined function to submit a new merkle root to the set-verifier and call fulfillBatch.
180199
/// @dev Useful to reduce the transaction count for fulfillments
181200
function submitRootAndFulfillBatch(

contracts/src/ProofMarket.sol

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -241,8 +241,10 @@ contract ProofMarket is IProofMarket, EIP712 {
241241
emit RequestLockedin(request.id, prover);
242242
}
243243

244-
// TODO(victor): Add a path that allows a prover to fuilfill a request without first sending a lock-in.
245-
function fulfill(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external {
244+
/// Verify the application and assessor receipts, ensuring that the provided fulfillment
245+
/// satisfies the request.
246+
// TODO(#165) Return or check the request checksum here.
247+
function verifyDelivery(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) public view {
246248
// Verify the application guest proof. We need to verify it here, even though the market
247249
// guest already verified that the prover has knowledge of a verifying receipt, because
248250
// we need to make sure the _delivered_ seal is valid.
@@ -266,13 +268,14 @@ contract ProofMarket is IProofMarket, EIP712 {
266268
);
267269
// Verification of the assessor seal does not need to comply with FULFILL_MAX_GAS_FOR_VERIFY.
268270
VERIFIER.verify(assessorSeal, ASSESSOR_ID, assessorJournalDigest);
269-
270-
_fulfillVerified(fill.id);
271-
272-
emit RequestFulfilled(fill.id, fill.journal, fill.seal);
273271
}
274272

275-
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) public {
273+
/// Verify the application and assessor receipts for the batch, ensuring that the provided
274+
/// fulfillments satisfy the requests.
275+
function verifyBatchDelivery(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover)
276+
public
277+
view
278+
{
276279
// TODO(victor): Figure out how much the memory here is costing. If it's significant, we can do some tricks to reduce memory pressure.
277280
bytes32[] memory claimDigests = new bytes32[](fills.length);
278281
uint192[] memory ids = new uint192[](fills.length);
@@ -297,14 +300,28 @@ contract ProofMarket is IProofMarket, EIP712 {
297300
);
298301
// Verification of the assessor seal does not need to comply with FULFILL_MAX_GAS_FOR_VERIFY.
299302
VERIFIER.verify(assessorSeal, ASSESSOR_ID, assessorJournalDigest);
303+
}
304+
305+
function fulfill(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external {
306+
verifyDelivery(fill, assessorSeal, prover);
307+
_fulfillVerified(fill.id);
308+
309+
// TODO(victor): Potentially this should be (re)combined with RequestFulfilled. It would make
310+
// the logic to watch for a proof a bit more complex, but the gas usage a little less (by
311+
// about 1000 gas per fulfill based on benchmarks)
312+
emit ProofDelivered(fill.id, fill.journal, fill.seal);
313+
}
314+
315+
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) public {
316+
verifyBatchDelivery(fills, assessorSeal, prover);
300317

301318
// NOTE: It would be slightly more efficient to keep balances and request flags in memory until a single
302319
// batch update to storage. However, updating the the same storage slot twice only costs 100 gas, so
303320
// this savings is marginal, and will be outweighed by complicated memory management if not careful.
304321
for (uint256 i = 0; i < fills.length; i++) {
305-
_fulfillVerified(ids[i]);
322+
_fulfillVerified(fills[i].id);
306323

307-
emit RequestFulfilled(fills[i].id, fills[i].journal, fills[i].seal);
324+
emit ProofDelivered(fills[i].id, fills[i].journal, fills[i].seal);
308325
}
309326
}
310327

@@ -336,6 +353,20 @@ contract ProofMarket is IProofMarket, EIP712 {
336353
// Mark the request as fulfilled and pay the prover.
337354
accounts[client].setRequestFulfilled(idx);
338355
accounts[lock.prover].balance += lock.price + lock.stake;
356+
357+
emit RequestFulfilled(id);
358+
}
359+
360+
function deliver(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external {
361+
verifyDelivery(fill, assessorSeal, prover);
362+
emit ProofDelivered(fill.id, fill.journal, fill.seal);
363+
}
364+
365+
function deliverBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) external {
366+
verifyBatchDelivery(fills, assessorSeal, prover);
367+
for (uint256 i = 0; i < fills.length; i++) {
368+
emit ProofDelivered(fills[i].id, fills[i].journal, fills[i].seal);
369+
}
339370
}
340371

341372
function slash(uint192 requestId) external {

contracts/test/ProofMarket.t.sol

Lines changed: 44 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -430,6 +430,11 @@ contract ProofMarketTest is Test {
430430
vm.startPrank(PROVER_WALLET.addr);
431431
proofMarket.lockin(request, clientSignature);
432432
(Fulfillment memory fill, bytes memory assessorSeal) = fulfillRequest(request, APP_JOURNAL, PROVER_WALLET.addr);
433+
434+
vm.expectEmit(true, true, true, true);
435+
emit IProofMarket.RequestFulfilled(request.id);
436+
vm.expectEmit(true, true, true, false);
437+
emit IProofMarket.ProofDelivered(request.id, hex"", hex"");
433438
proofMarket.fulfill(fill, assessorSeal, PROVER_WALLET.addr);
434439
// console2.log("fulfill - Gas used:", vm.gasUsed());
435440
vm.stopPrank();
@@ -526,6 +531,13 @@ contract ProofMarketTest is Test {
526531

527532
(Fulfillment[] memory fills, bytes memory assessorSeal) =
528533
fulfillRequestBatch(requests, journals, PROVER_WALLET.addr);
534+
535+
for (uint256 i = 0; i < fills.length; i++) {
536+
vm.expectEmit(true, true, true, true);
537+
emit IProofMarket.RequestFulfilled(fills[i].id);
538+
vm.expectEmit(true, true, true, false);
539+
emit IProofMarket.ProofDelivered(fills[i].id, hex"", hex"");
540+
}
529541
proofMarket.fulfillBatch(fills, assessorSeal, PROVER_WALLET.addr);
530542

531543
for (uint256 i = 0; i < fills.length; i++) {
@@ -620,7 +632,7 @@ contract ProofMarketTest is Test {
620632
checkProofMarketBalance();
621633
}
622634

623-
function testFulfillfExpired() public {
635+
function testFulfillExpired() public {
624636
Offer memory offer = Offer({
625637
minPrice: 1 ether,
626638
maxPrice: 2 ether,
@@ -650,6 +662,36 @@ contract ProofMarketTest is Test {
650662
checkProofMarketBalance();
651663
}
652664

665+
function testDeliver() public {
666+
// Submit request
667+
Vm.Wallet memory client = createClient(1);
668+
ProvingRequest memory request = defaultRequest(client.addr, 1);
669+
(Fulfillment memory fill, bytes memory assessorSeal) = fulfillRequest(request, APP_JOURNAL, PROVER_WALLET.addr);
670+
671+
vm.expectEmit(true, true, true, false);
672+
emit IProofMarket.ProofDelivered(request.id, hex"", hex"");
673+
proofMarket.deliver(fill, assessorSeal, PROVER_WALLET.addr);
674+
675+
// Check that the proof is still marked as unfulfilled.
676+
assertFalse(proofMarket.requestIsFulfilled(fill.id), "Request should not have fulfilled status");
677+
}
678+
679+
function testDeliverBatch() public {
680+
(ProvingRequest[] memory requests, bytes[] memory journals) = newBatch(5);
681+
(Fulfillment[] memory fills, bytes memory assessorSeal) =
682+
fulfillRequestBatch(requests, journals, PROVER_WALLET.addr);
683+
684+
for (uint256 i = 0; i < fills.length; i++) {
685+
vm.expectEmit(true, true, true, false);
686+
emit IProofMarket.ProofDelivered(fills[i].id, hex"", hex"");
687+
}
688+
proofMarket.deliverBatch(fills, assessorSeal, PROVER_WALLET.addr);
689+
690+
for (uint256 j = 0; j < fills.length; j++) {
691+
assertFalse(proofMarket.requestIsFulfilled(fills[j].id), "Request should not have fulfilled status");
692+
}
693+
}
694+
653695
function testSlash() public {
654696
Offer memory offer = Offer({
655697
minPrice: 1 ether,
@@ -662,7 +704,7 @@ contract ProofMarketTest is Test {
662704
Vm.Wallet memory client = createClient(1);
663705
ProvingRequest memory request = newRequest(offer, client.addr, 1);
664706

665-
testFulfillfExpired();
707+
testFulfillExpired();
666708

667709
// Slash the request
668710
vm.expectEmit(true, true, false, true);

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -488,7 +488,7 @@ where
488488
.context("Failed to get latest block number")?)
489489
}
490490

491-
/// Query the RequestFulfilled event based on request ID and block options.
491+
/// Query the ProofDelivered event based on request ID and block options.
492492
/// For each iteration, we query a range of blocks.
493493
/// If the event is not found, we move the range down and repeat until we find the event.
494494
/// If the event is not found after the configured max iterations, we return an error.
@@ -517,7 +517,7 @@ where
517517
let lower_block = upper_block.saturating_sub(self.event_query_config.block_range);
518518

519519
// Set up the event filter for the specified block range
520-
let mut event_filter = self.instance.RequestFulfilled_filter();
520+
let mut event_filter = self.instance.ProofDelivered_filter();
521521
event_filter.filter = event_filter
522522
.filter
523523
.topic1(request_id)

docs/src/market/rfc.md

Lines changed: 46 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -156,8 +156,13 @@ interface IProofMarket {
156156
event RequestSubmitted(ProvingRequest request, bytes clientSignature);
157157
/// Event logged when a request is locked in by the given prover.
158158
event RequestLockedin(uint192 indexed requestId, address prover);
159-
/// Event logged when a request is fulfilled, outside of a batch.
160-
event RequestFulfilled(uint192 indexed requestId, bytes journal, bytes seal);
159+
/// Event logged when a request is fulfilled.
160+
event RequestFulfilled(uint192 indexed requestId);
161+
/// @notice Event logged when a proof is delivered that satisfies the requests requirements.
162+
/// @dev It is possible for this event to be logged multiple times for a single request. This
163+
/// is usually logged as part of order fulfillment, however it can also be logged by a prover
164+
/// sending the proof without payment.
165+
event ProofDelivered(uint192 indexed requestId, bytes journal, bytes seal);
161166
/// Event when prover stake is burned for failing to fulfill a request by the deadline.
162167
event LockinStakeBurned(uint192 indexed requestId, uint96 stake);
163168
/// Event when a deposit is made to the proof market.
@@ -200,14 +205,14 @@ interface IProofMarket {
200205
/// @notice Submit a request such that it is publicly available for provers to evaluate and bid on.
201206
/// Any `msg.value` sent with the call will be added to the balance of `msg.sender`.
202207
/// @dev Submitting the transaction only broadcasting it, and is not a required step.
203-
function submitRequest(ProvingRequest calldata request, bytes memory clientSignature) external payable;
208+
function submitRequest(ProvingRequest calldata request, bytes calldata clientSignature) external payable;
204209
205210
/// @notice Lock the proving request to the prover, giving them exclusive rights to be paid to
206211
/// fulfill this request, and also making them subject to slashing penalties if they fail to
207212
/// deliver. At this point, the price for fulfillment is also set, based on the reverse Dutch
208213
/// auction parameters and the block at which this transaction is processed.
209214
/// @dev This method should be called from the address of the prover.
210-
function lockin(ProvingRequest calldata request, bytes memory clientSignature) external;
215+
function lockin(ProvingRequest calldata request, bytes calldata clientSignature) external;
211216
212217
/// @notice Lock the proving request to the prover, giving them exclusive rights to be paid to
213218
/// fulfill this request, and also making them subject to slashing penalties if they fail to
@@ -216,16 +221,46 @@ interface IProofMarket {
216221
/// @dev This method uses the provided signature to authenticate the prover.
217222
function lockinWithSig(
218223
ProvingRequest calldata request,
219-
bytes memory clientSignature,
224+
bytes calldata clientSignature,
220225
bytes calldata proverSignature
221226
) external;
222227
223-
/// Fulfill a locked request by delivering the proof for the application.
224-
/// Upon proof verification, the prover will be paid.
225-
function fulfill(Fulfillment calldata fill, bytes calldata assessorSeal) external;
226-
227-
/// Fulfills a batch of locked requests
228-
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal) external;
228+
/// @notice Fulfill a locked request by delivering the proof for the application.
229+
/// Upon proof verification, the prover that locked the request will be paid.
230+
/// @param fill The fulfillment information, including the journal and seal.
231+
/// @param assessorSeal The seal from the Assessor guest, which is verified to confirm the
232+
/// request's requirements are met.
233+
/// @param prover The address of the prover that produced the fulfillment.
234+
/// Note that this can differ from the address of the prover that locked the
235+
/// request. When they differ, the locked-in prover is the one that received payment.
236+
function fulfill(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external;
237+
/// @notice Fulfills a batch of locked requests. See IProofMarket.fulfill for more information.
238+
function fulfillBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) external;
239+
240+
/// @notice Delivers a proof satisfying a referenced request, without modifying contract state.
241+
/// In particular, calling this method will not result in payment being sent to the prover, or
242+
/// marking the request as fulfilled.
243+
/// @dev This method is useful for when an interested third party wants to delivery a proof for
244+
/// a request even if they will not be paid for doing so.
245+
/// @param fill The fulfillment information, including the journal and seal.
246+
/// @param assessorSeal The seal from the Assessor guest, which is verified to confirm the
247+
/// request's requirements are met.
248+
/// @param prover The address of the prover that produced the fulfillment.
249+
/// Note that this can differ from the address of the prover that locked the
250+
/// request.
251+
function deliver(Fulfillment calldata fill, bytes calldata assessorSeal, address prover) external;
252+
/// @notice Delivers a batch of proofs. See IProofMarket.deliver for more information.
253+
function deliverBatch(Fulfillment[] calldata fills, bytes calldata assessorSeal, address prover) external;
254+
255+
/// @notice Combined function to submit a new merkle root to the set-verifier and call fulfillBatch.
256+
/// @dev Useful to reduce the transaction count for fulfillments
257+
function submitRootAndFulfillBatch(
258+
bytes32 root,
259+
bytes calldata seal,
260+
Fulfillment[] calldata fills,
261+
bytes calldata assessorSeal,
262+
address prover
263+
) external;
229264
230265
/// When a prover fails to fulfill a request by the deadline, this method can be used to burn
231266
/// the associated prover stake.

0 commit comments

Comments
 (0)