Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
33 changes: 19 additions & 14 deletions .changeset/cmio-libcmt-v2.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,29 +2,34 @@
"@deroll/cmio": minor
---

Port the binding to the libcmt v2 API overhaul (machine-guest-tools `feature/libcmt-v2`).
Port the binding to the libcmt v2 API overhaul (machine-guest-tools
`7ffb2da`, "output indexing" era).

This is a **breaking** change. libcmt v2 made the rollup layer raw I/O only and moved
all EVM-ABI encoding/decoding into a separate `codec` module, so the binding now mirrors
all EVM-ABI encoding/decoding into a separate `codec` module, so the binding mirrors
that split:

- **`Rollup` is now a thin, raw wrapper.** `finish()` is replaced by
- **`Rollup` is a thin, raw wrapper.** `finish()` is replaced by
`waitForInput({ accept })`, which returns `{ type, payload }` with the **raw, undecoded**
input bytes. Outputs are emitted with `emitOutput(bytes)` (returns the output index);
`emitReport`/`emitException`/`progress`/`close`/`run` are unchanged in spirit.
- **ABI encoding/decoding moved to JS helpers.** New functions
`decodeAdvance(input)`, `encodeNotice(payload)`, `encodeVoucher({ destination, value, payload })`
and `encodeDelegateCallVoucher({ destination, payload })` mirror libcmt's codec module,
implemented with [`ox`](https://oxlib.sh) (a new runtime dependency) for the EVM-ABI work.
- **ABI encoding/decoding lives in JS helpers**, one per libcmt `codec.h` entry,
implemented with [`ox`](https://oxlib.sh) (a new runtime dependency) and verified
byte-for-byte against the C encoders: `decodeAdvance`/`encodeAdvance`
(`EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)`),
`encodeNotice` (`Notice(bytes)`), `encodeCallVoucher` (`CallVoucher(address,uint256,bytes)`),
and the new asset-transfer outputs `encodeERC20Transfer`, `encodeERC721Transfer`,
`encodeERC1155SingleTransfer` and `encodeERC1155BatchTransfer`.
Compose them with the raw API, e.g. `rollup.emitOutput(encodeNotice(payload))` and
`decodeAdvance(request.payload)`.
- **The on-chain output format changed.** Notices and vouchers are now encoded as the
`Output1..Output4(bytes32[N],bytes)` envelope with a 32-byte type tag, not the old
`Notice(bytes)` / `Voucher(address,uint256,bytes)` selectors. This tracks a newer
rollups-contracts version.
- **Removed:** `gio()` (libcmt v2 dropped generic IO support entirely) and the
`loadMerkle`/`saveMerkle`/`resetMerkle` methods (no longer part of the rollup API;
`cmt_merkle_reset` is also absent from the v2 sources).
- **The wire formats track the new rollups contracts.** `EvmAdvance` narrows
`chainId`/`blockNumber`/`blockTimestamp`/`index` to `uint64` (new selector), and
outputs use plain per-type selectors (`Notice`, `CallVoucher`, `ERC*Transfer`)
instead of the previous `Voucher(address,uint256,bytes)` format.
- **Removed:** delegate-call vouchers (dropped by libcmt v2 along with the
`Output1..Output4` envelope design), `gio()` (libcmt v2 dropped generic IO support),
and the `loadMerkle`/`saveMerkle`/`resetMerkle` methods (no longer part of the
rollup API).
- The high-level `emitVoucher`/`emitNotice`/`emitDelegateCallVoucher` methods and the
decoded `AdvanceRequest` fields on `finish()` are gone; use the `encode*`/`decodeAdvance`
helpers instead.
31 changes: 17 additions & 14 deletions apps/docs/pages/cmio/guide/emitting-outputs.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@ While handling a request your application can emit four kinds of outputs. They d

| Output | Provable | Purpose |
| --- | --- | --- |
| [Voucher](/cmio/reference/encode-voucher) | yes | Execute a transaction on-chain (transfer assets, call a contract) |
| [Call voucher](/cmio/reference/encode-call-voucher) | yes | Execute a transaction on-chain (transfer ether, call a contract) |
| [Asset transfers](/cmio/reference/encode-asset-transfers) | yes | Typed ERC-20/721/1155 transfers out of the application |
| [Notice](/cmio/reference/encode-notice) | yes | Attest a fact about the application state |
| [Report](/cmio/reference/emit-report) | no | Logs, diagnostics, inspect responses |
| [Exception](/cmio/reference/emit-exception) | no | Signal that the request could not be processed |
Expand All @@ -16,37 +17,39 @@ Provable outputs are built in two steps: an `encode*` helper produces the EVM-AB
A voucher is a one-shot transaction the application authorizes: when the epoch containing it settles, anyone can execute it through the application contract. Use it to transfer assets out, call DeFi protocols, or trigger any on-chain effect:

```ts twoslash
import { Rollup, encodeVoucher } from '@deroll/cmio';
import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
// transfer 1 ETH from the application contract to a user
const index = rollup.emitOutput(
encodeVoucher({
encodeCallVoucher({
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n, // wei
payload: '0x', // no calldata — plain value transfer
}),
);
```

To call a contract, put the EVM calldata in `payload` — for example an ERC-20 `transfer(address,uint256)`:
To call a contract, put the EVM calldata in `payload`. For plain token withdrawals, prefer the typed asset-transfer outputs below.

## Asset transfers: withdraw tokens

The new rollups contracts understand first-class asset-transfer outputs, so an application transfers ERC-20/721/1155 tokens out without hand-crafting calldata:

```ts twoslash
import { Rollup, encodeVoucher } from '@deroll/cmio';
import { Rollup, encodeERC20Transfer } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
const recipient = 'f39Fd6e51aad88F6F4ce6aB8827279cffFb92266'.padStart(64, '0');
const amount = (10n ** 18n).toString(16).padStart(64, '0');

rollup.emitOutput(
encodeVoucher({
destination: '0x6B175474E89094C44Da98b954EedeAC495271d0F', // token contract
payload: `0xa9059cbb${recipient}${amount}`, // transfer(address,uint256) selector + args
encodeERC20Transfer({
recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
token: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
value: 10n ** 18n,
}),
);
```

There is also a [delegate-call variant](/cmio/reference/encode-delegate-call-voucher) that executes the target's code in the application contract's own storage context — an advanced tool for upgrade-style patterns.
See the [asset transfer encoders](/cmio/reference/encode-asset-transfers) for the ERC-721 and ERC-1155 (single and batch) variants.

## Notices: attest facts

Expand Down Expand Up @@ -102,8 +105,8 @@ rollup.emitReport(new Uint8Array([1, 2, 3])); // Uint8Array

The `encode*` helpers validate their structured arguments:

- **Addresses** (`destination`) must be exactly 20 bytes; as strings they are 0x-prefixed hex.
- **256-bit values** (`value`) accept `bigint`, `number`, or 32 bytes.
- **Addresses** (`destination`, `recipient`, `token`) must be exactly 20 bytes; as strings they are 0x-prefixed hex.
- **256-bit values** (`value`, `tokenId`) accept `bigint`, `number`, or 32 bytes.
- Outgoing data is always a `Buffer`; decoded advance numbers are `bigint` and addresses are 0x-hex strings.

Hex strings are typed as `` `0x${string}` `` ([`Hex`](/cmio/reference/types#input-types)), so values from [viem](https://viem.sh) — `Hex` from `encodeFunctionData`, `Address`, `bigint` from `parseEther` — pass in directly, and decoded request addresses pass back into viem without casts.
Expand Down
2 changes: 1 addition & 1 deletion apps/docs/pages/cmio/guide/handling-requests.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

A Cartesi application processes two kinds of requests:

- **Advance** — an input submitted on-chain. It can change application state and produce provable outputs ([vouchers](/cmio/reference/encode-voucher) and [notices](/cmio/reference/encode-notice)).
- **Advance** — an input submitted on-chain. It can change application state and produce provable outputs ([vouchers](/cmio/reference/encode-call-voucher), [asset transfers](/cmio/reference/encode-asset-transfers) and [notices](/cmio/reference/encode-notice)).
- **Inspect** — an off-chain, read-only query. It can only answer with [reports](/cmio/reference/emit-report).

This page shows the request lifecycle; [Emitting Outputs](/cmio/guide/emitting-outputs) covers what you can produce while handling them.
Expand Down
88 changes: 24 additions & 64 deletions apps/docs/pages/cmio/guide/testing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -31,79 +31,43 @@ advance.report-0.bin # first report
advance.exception-0.bin # first exception
```

Provable outputs are EVM-ABI encoded in the `Output1..Output4(bytes32[N],bytes)` envelope: a 4-byte function selector, a 32-byte type tag identifying the output kind (notice, call voucher, …), then the ABI-encoded fields. Reports and exceptions are raw bytes.
Provable outputs are EVM-ABI encoded calls with a per-type function selector — `Notice(bytes)`, `CallVoucher(address,uint256,bytes)`, `ERC20Transfer(address,address,uint256)`, … — exactly the bytes the `encode*` helpers produce. Reports and exceptions are raw bytes.

## Generating an advance input

An advance input file is the ABI encoding of the `EvmAdvance(uint256,address,address,uint256,uint256,uint256,uint256,bytes)` call that the on-chain input box would produce. You can craft one with foundry's `cast calldata`, or in a few lines of TypeScript:
An advance input file is the ABI encoding of the `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` call that the on-chain input box would produce. [`encodeAdvance`](/cmio/reference/decode-advance) builds one:

```ts twoslash
// @filename: encode.ts
const word = (value: bigint) => {
const bytes = Buffer.alloc(32);
let v = value;
for (let i = 31; i >= 0 && v > 0n; i--) {
bytes[i] = Number(v & 0xffn);
v >>= 8n;
}
return bytes;
};
const addressWord = (hex: string) =>
Buffer.concat([Buffer.alloc(12), Buffer.from(hex.slice(2), 'hex')]);
const pad32 = (bytes: Buffer) =>
Buffer.concat([bytes, Buffer.alloc((32 - (bytes.length % 32)) % 32)]);

export function encodeEvmAdvance(input: {
chainId: bigint;
appContract: string;
msgSender: string;
blockNumber: bigint;
blockTimestamp: bigint;
prevRandao: bigint;
index: bigint;
payload: Buffer;
}) {
return Buffer.concat([
Buffer.from('415bf363', 'hex'), // EvmAdvance selector
word(input.chainId),
addressWord(input.appContract),
addressWord(input.msgSender),
word(input.blockNumber),
word(input.blockTimestamp),
word(input.prevRandao),
word(input.index),
word(8n * 32n), // offset of the payload `bytes` field
word(BigInt(input.payload.length)),
pad32(input.payload),
]);
}
import fs from 'node:fs';
import { encodeAdvance } from '@deroll/cmio';

fs.writeFileSync(
'input.bin',
encodeAdvance({
chainId: 31337n,
appContract: `0x${'02'.repeat(20)}`,
msgSender: `0x${'03'.repeat(20)}`,
blockNumber: 1n,
blockTimestamp: 1700000000n,
prevRandao: 0n,
index: 0n,
payload: Buffer.from('hello'),
}),
);
```

## A complete test

Putting it together with `node:test` — encode an input, point `CMT_INPUTS` at it, run the application, and assert on the output files:

```ts twoslash
// @filename: encode.ts
export declare function encodeEvmAdvance(input: {
chainId: bigint;
appContract: string;
msgSender: string;
blockNumber: bigint;
blockTimestamp: bigint;
prevRandao: bigint;
index: bigint;
payload: Buffer;
}): Buffer;

// @filename: app.test.ts
import assert from 'node:assert/strict';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import test from 'node:test';
import { decodeAdvance, encodeNotice } from '@deroll/cmio';
import { encodeEvmAdvance } from './encode.js';
import { decodeAdvance, encodeAdvance, encodeNotice } from '@deroll/cmio';

test('echoes the payload as a notice', async () => {
// run in a temp dir: the mock writes output files next to the inputs
Expand All @@ -113,7 +77,7 @@ test('echoes the payload as a notice', async () => {
const input = path.join(dir, 'input.bin');
fs.writeFileSync(
input,
encodeEvmAdvance({
encodeAdvance({
chainId: 31337n,
appContract: `0x${'02'.repeat(20)}`,
msgSender: `0x${'03'.repeat(20)}`,
Expand All @@ -140,15 +104,11 @@ test('echoes the payload as a notice', async () => {
})
.catch(() => {}); // inputs exhausted — session over

// Output1 envelope: funsel (4) + notice type tag (32) + offset (32) +
// length (32) + padded payload — so the payload starts at byte 100.
// Notice(bytes): funsel (4) + offset (32) + length (32) + padded
// payload — so the payload starts at byte 68.
const notice = fs.readFileSync(path.join(dir, 'input.output-0.bin'));
assert.equal(notice.subarray(0, 4).toString('hex'), 'aed682a1');
assert.equal(
notice.subarray(4, 36).toString('hex'),
'e4f5829fb698a59fba2cf6128b6bf1e8ce1dc09d271c55b787781bd415db8eed',
);
assert.equal(notice.subarray(100, 100 + 5).toString(), 'hello');
assert.equal(notice.subarray(0, 4).toString('hex'), 'c258d6e5');
assert.equal(notice.subarray(68, 68 + 5).toString(), 'hello');
});
```

Expand Down
4 changes: 3 additions & 1 deletion apps/docs/pages/cmio/reference/decode-advance.mdx
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# decodeAdvance

Decodes the raw payload of an [advance request](/cmio/reference/wait-for-input) — an EVM-ABI encoded `EvmAdvance` input — into its structured fields. This is a pure-JS helper that mirrors libcmt's `cmt_decode_advance_state`; it touches no device state.
Decodes the raw payload of an [advance request](/cmio/reference/wait-for-input) — an EVM-ABI encoded `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` input — into its structured fields. This is a pure-JS helper that mirrors libcmt's `cmt_evmadvance_decode`; it touches no device state.

The inverse, `encodeAdvance`, builds the input bytes from the same fields (mirroring `cmt_evmadvance_encode`) — useful for crafting mock inputs (`CMT_INPUTS`) when [testing on the host](/cmio/guide/testing).

## Usage

Expand Down
10 changes: 5 additions & 5 deletions apps/docs/pages/cmio/reference/emit-output.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ Emits a **provable output** — a raw, already EVM-ABI encoded output that is ad
`emitOutput` takes the encoded bytes directly. Build them with one of the standalone encoders:

- [`encodeNotice`](/cmio/reference/encode-notice) — attest a fact about the application state.
- [`encodeVoucher`](/cmio/reference/encode-voucher) — authorize an on-chain transaction (transfer assets, call a contract).
- [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher) — a `DELEGATECALL` voucher.
- [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) — authorize an on-chain transaction (transfer ether, call a contract).
- [asset transfer encoders](/cmio/reference/encode-asset-transfers) — typed ERC-20/721/1155 transfer outputs.

## Usage

Expand All @@ -22,11 +22,11 @@ const index = rollup.emitOutput(
A voucher is emitted the same way:

```ts twoslash
import { Rollup, encodeVoucher } from '@deroll/cmio';
import { Rollup, encodeCallVoucher } from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
const index = rollup.emitOutput(
encodeVoucher({
encodeCallVoucher({
destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266',
value: 1_000_000_000_000_000_000n, // 1 ETH in wei
}),
Expand All @@ -44,7 +44,7 @@ const index = rollup.emitOutput(
- Type: `BytesLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array`
- Required

The already-encoded output bytes. Produce them with [`encodeNotice`](/cmio/reference/encode-notice), [`encodeVoucher`](/cmio/reference/encode-voucher) or [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher).
The already-encoded output bytes. Produce them with [`encodeNotice`](/cmio/reference/encode-notice), [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) or the [asset transfer encoders](/cmio/reference/encode-asset-transfers).

## Errors

Expand Down
86 changes: 86 additions & 0 deletions apps/docs/pages/cmio/reference/encode-asset-transfers.mdx
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
# Asset transfer encoders

Encoders for the **typed asset-transfer outputs** introduced by the new rollups contracts: instead of hand-crafting a [call voucher](/cmio/reference/encode-call-voucher) with ERC-20/721/1155 calldata, the application emits a first-class transfer output that the contracts know how to execute. Each helper mirrors one entry of libcmt's codec module and produces the exact same bytes.

| Function | Wire format |
| --- | --- |
| `encodeERC20Transfer` | `ERC20Transfer(address,address,uint256)` |
| `encodeERC721Transfer` | `ERC721Transfer(address,address,uint256,bytes)` |
| `encodeERC1155SingleTransfer` | `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` |
| `encodeERC1155BatchTransfer` | `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` |

All of them return a `Buffer`, ready to pass to [`emitOutput`](/cmio/reference/emit-output).

## Usage

```ts twoslash
import {
Rollup,
encodeERC20Transfer,
encodeERC721Transfer,
encodeERC1155SingleTransfer,
encodeERC1155BatchTransfer,
} from '@deroll/cmio';
const rollup = new Rollup();
// ---cut---
const recipient = '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266';

// 100 units of an ERC-20 token
rollup.emitOutput(
encodeERC20Transfer({
recipient,
token: '0x6B175474E89094C44Da98b954EedeAC495271d0F',
value: 100n,
}),
);

// one ERC-721 token
rollup.emitOutput(
encodeERC721Transfer({
recipient,
token: '0x5FbDB2315678afecb367f032d93F642f64180aa3',
tokenId: 7n,
}),
);

// ERC-1155: a single (id, value) or a batch of [id, value] pairs
rollup.emitOutput(
encodeERC1155SingleTransfer({
recipient,
token: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
tokenId: 7n,
value: 3n,
}),
);
rollup.emitOutput(
encodeERC1155BatchTransfer({
recipient,
token: '0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512',
tokenIdsAndValues: [
[1n, 2n],
[3n, 4n],
],
}),
);
```

## Parameters

All four take a named options object:

| Field | Type | Applies to | Description |
| --- | --- | --- | --- |
| `recipient` | `AddressLike` | all | Address receiving the assets (20 bytes) |
| `token` | `AddressLike` | all | Token contract address (20 bytes) |
| `value` | `U256Like` | ERC-20, ERC-1155 single | Amount of tokens to transfer |
| `tokenId` | `U256Like` | ERC-721, ERC-1155 single | Token id to transfer |
| `tokenIdsAndValues` | `[U256Like, U256Like][]` | ERC-1155 batch | `[tokenId, value]` pairs |
| `data` | `BytesLike`, default empty | ERC-721, ERC-1155 | Extra data forwarded to the recipient |

## Errors

| Condition | Error |
| --- | --- |
| `recipient`/`token` not 20 bytes / malformed hex | `TypeError` |
| `value`/`tokenId` negative or ≥ 2²⁵⁶ | `RangeError` |
| `tokenIdsAndValues` entry not a `[tokenId, value]` pair | `TypeError` |
Loading