Skip to content

Callbacks

Prerequisite Reading: Request a Proof, Proof Lifecycle

Automatic Proof Delivery

Boundless supports proof delivery to application contracts through callbacks. When requesting a proof, you can specify a contract address that implements the IBoundlessMarketCallback interface. When the proof is fulfilled, the Boundless Market will automatically call this contract with the proof data.

The callback contract must implement the following interface:

interface IBoundlessMarketCallback {
    function handleProof(bytes32 imageId, bytes calldata journal, bytes calldata seal) external;
}

We provide a template for implementing the IBoundlessMarketCallback interface: BoundlessMarketCallback.sol. This implements best practices for implementing the handleProof function.

Considerations

  1. Boundless does not guarantee the success of callback execution.

Callbacks are executed as part of a try-catch block using the specified gas limit. Successful execution of the callback is not required for a request to be marked as fulfilled. It is important for requestors to set a high enough gas limit to ensure the callback can execute to completion.

  1. Callbacks have at-least-once delivery semantics.

Proof request submission is permissionless in Boundless. Any user can submit a request that causes any contract's callback to be executed, potentially re-using proofs that have been previously submitted. It is important for callbacks to be robust to multiple invocations.

  1. Callback invocation does not specify which Requirements were used to generate the proof.

Proofs are submitted to the callback with the journal and seal generated by the prover. The callback cannot inspect the Requirements used to generate the proof. It is important for the callback to verify the image ID and journal are as expected before accepting the callback as valid.

Example: Counter with callback

The Counter with callback example submits a request to the market for a proof that "4" is an even number, and specifies that the proof should be delivered to the Counter contract.

When creating the proof request, the requestor specifies the callback contract address and a gas limit:













let request = ProofRequest::builder()
    .with_image_url(image_url)
    .with_input(input_url)
    .with_requirements(
        Requirements::new(ECHO_ID, Predicate::digest_match(journal.digest()))
            .with_callback(Callback {
                addr: counter_address,
                gasLimit: U96::from(100_000)
            }),
    )
    .with_offer(
        Offer::default()
            .with_min_price_per_mcycle(parse_ether("0.001")?, mcycles_count)
            .with_max_price_per_mcycle(parse_ether("0.002")?, mcycles_count)
            .with_timeout(1000)
            .with_lock_timeout(1000),
    )
    .build()?;

Our Counter contract implements the handleProof function, which checks if we've already seen this proof, and if not, increments a counter and emits an event:

Counter.sol
function _handleProof(bytes32 imageId, bytes calldata journal, bytes calldata seal) internal override {
    // Since a callback can be triggered by any requestor sending a valid request to the Boundless Market,
    // we need to perform some checks on the proof before proceeding.
    // First, the validation of the proof (e.g., seal is valid, the caller of the callback is the BoundlessMarket)
    // is done in the parent contract, the `BoundlessMarketCallback`.
    // Here we can add additional checks if needed.
    // For example, we can check if the proof has already been verified,
    // so that the same proof cannot be used more than once to run the callback logic.
    bytes32 journalAndSeal = keccak256(abi.encode(journal, seal));
    if (verified[journalAndSeal]) {
        revert AlreadyVerified();
    }
    // Mark the proof as verified.
    verified[journalAndSeal] = true;
 
    // run the callback logic
    count += 1;
    emit CounterCallbackCalled(imageId, journal, seal);
}

Once the proof is fulfilled, our example checks the counter contract and confirms that the value was incremented by the callback:






#



 
alloy::sol! {
    #[sol(rpc)]
    interface ICounter {
        function count() external view returns (uint256);
    }
}
 
let (_journal, _seal) =
    boundless_client.wait_for_request_fulfillment(request_id, Duration::from_secs(5), expires_at).await?;
 
// We interact with the Counter contract by calling the getCount function to check that the callback
// was executed correctly.
let counter_address = address!("0x000000000000000000000000000000000c0077e5");
let counter = ICounter::ICounterInstance::new(counter_address, boundless_client.provider().clone());
let count = counter
    .count()
    .call()
    .await?
    ._0;

Relevant links: Boundless Foundry Template, Journal, Seal