Skip to content
Logo

Handling Requests

A Cartesi application processes two kinds of requests:

  • Advance — an input submitted on-chain. It can change application state and produce provable outputs (vouchers and notices).
  • Inspect — an off-chain, read-only query. It can only answer with reports.

This page shows the request lifecycle; Emitting Outputs covers what you can produce while handling them.

The request cycle

Processing follows a strict cycle, inherited from how the machine and the blockchain interact:

  1. The application calls finish, telling the machine whether it accepts or rejects the previous request.
  2. The call blocks (the whole machine yields) until the next request arrives.
  3. The application handles the request, emitting outputs along the way.
  4. Back to step 1.

run implements this cycle; your handlers plug into step 3.

Advance requests

Advance handlers receive the decoded input metadata along with the payload:

import {  } from "@cartesi/rollup";
 
const  = new ();
 
await .({
    (, ) {
        .msgSenderArrow

Input sender address (0x-prefixed hex).

;
// every input is identified by its index and carries the L1 context // it was included in: block number, timestamp and prevRandao .( .( `input ${.} from ${.} ` + `at block ${.} (chain ${.})`, ), ); return true; }, });

Addresses arrive as 0x-prefixed hex strings, numeric fields as bigint, and the payload as a Buffer.

Inspect requests

Inspect handlers only get a payload — the query — and should answer with reports:

import {  } from "@cartesi/rollup";
 
const  = new ();
const  = new <string, bigint>();
 
await .({
    (, ) {
        const  = ..();
        const  = .() ?? 0n;
        .(.(.()));
    },
});

Inside the machine, any state changes made while handling an inspect are reverted afterwards — inspects are queries, not transactions.

Accepting and rejecting

A handler's return value decides the input's fate:

  • true or undefined (no return) — accept: outputs become part of the application's verifiable history.
  • falsereject: the machine reverts to the state before the input; its outputs are discarded.
  • throwing — the exception is caught, emitted as a report (so you can see it from the outside), and the input is rejected.
import {  } from "@cartesi/rollup";
 
const  = new ();
 
await .({
    (, ) {
        if (.. === 0) {
            return false; // reject empty inputs — state is rolled back
        }
        if (.. > 1024) {
            // also rejects, and the message lands in a report
            throw new ("payload too large");
        }
        .(.);
        return true;
    },
});

These are the rules for the handler run calls. When that handler is a composition of several — see Composing Handlers — the ones inside it answer a narrower question, "did I claim this request?", and only the composition's answer decides the input's fate.

Async handlers

Handlers may be asyncrun awaits them before finishing the request:

import {  } from "node:fs/promises";
import {  } from "@cartesi/rollup";
 
const  = new ();
 
await .({
    async (, ) {
        const  = await ("/etc/app/config.json", "utf8");
        .(.(`config loaded: ${.} bytes`));
        return true;
    },
});

Remember that the machine is paused between requests, not within them: timers and I/O work normally while your handler runs, but nothing executes while the application waits for the next request.

Driving the loop yourself

run is a thin convenience. If you prefer explicit control — custom dispatch, graceful shutdown, request batching — call finish directly:

import {  } from "@cartesi/rollup";
 
const  = new ();
 
let  = true;
for (;;) {
    const requestArrow
= .({ });
switch (.) { case "advance": .(.); = true; break; case "inspect": .(.); = true; break; } }

The request is a discriminated union — narrowing on type gives you the right fields in each branch.

When the loop ends

run returns a Promise<void>, but under normal operation inside the machine the loop never ends — the promise simply never settles. It rejects when finish fails for a real reason, such as after close is called: let that propagate (the process exits) or catch it if you have cleanup to do.

It resolves in exactly one case: the host mock ran out of the inputs listed in CMT_INPUTS, the normal end of a test session. That is decided by the driver the binding was built against, not by the environment, so inside a real machine an exhaustion-looking errno is still an error.