diff --git a/.changeset/cmio-libcmt-v2.md b/.changeset/cmio-libcmt-v2.md index d1823a6a..bfe93daa 100644 --- a/.changeset/cmio-libcmt-v2.md +++ b/.changeset/cmio-libcmt-v2.md @@ -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. diff --git a/apps/docs/pages/cmio/guide/emitting-outputs.mdx b/apps/docs/pages/cmio/guide/emitting-outputs.mdx index 4a52beed..137c7105 100644 --- a/apps/docs/pages/cmio/guide/emitting-outputs.mdx +++ b/apps/docs/pages/cmio/guide/emitting-outputs.mdx @@ -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 | @@ -16,12 +17,12 @@ 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 @@ -29,24 +30,26 @@ const index = rollup.emitOutput( ); ``` -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 @@ -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. diff --git a/apps/docs/pages/cmio/guide/handling-requests.mdx b/apps/docs/pages/cmio/guide/handling-requests.mdx index 01280224..ea4a0224 100644 --- a/apps/docs/pages/cmio/guide/handling-requests.mdx +++ b/apps/docs/pages/cmio/guide/handling-requests.mdx @@ -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. diff --git a/apps/docs/pages/cmio/guide/testing.mdx b/apps/docs/pages/cmio/guide/testing.mdx index 4db40bb8..5d5772f9 100644 --- a/apps/docs/pages/cmio/guide/testing.mdx +++ b/apps/docs/pages/cmio/guide/testing.mdx @@ -31,52 +31,29 @@ 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 @@ -84,26 +61,13 @@ export function encodeEvmAdvance(input: { 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 @@ -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)}`, @@ -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'); }); ``` diff --git a/apps/docs/pages/cmio/reference/decode-advance.mdx b/apps/docs/pages/cmio/reference/decode-advance.mdx index 2b61444a..a7b0ccbb 100644 --- a/apps/docs/pages/cmio/reference/decode-advance.mdx +++ b/apps/docs/pages/cmio/reference/decode-advance.mdx @@ -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 diff --git a/apps/docs/pages/cmio/reference/emit-output.mdx b/apps/docs/pages/cmio/reference/emit-output.mdx index f01ee8b1..72e65a4d 100644 --- a/apps/docs/pages/cmio/reference/emit-output.mdx +++ b/apps/docs/pages/cmio/reference/emit-output.mdx @@ -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 @@ -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 }), @@ -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 diff --git a/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx b/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx new file mode 100644 index 00000000..c34c74db --- /dev/null +++ b/apps/docs/pages/cmio/reference/encode-asset-transfers.mdx @@ -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` | diff --git a/apps/docs/pages/cmio/reference/encode-voucher.mdx b/apps/docs/pages/cmio/reference/encode-call-voucher.mdx similarity index 69% rename from apps/docs/pages/cmio/reference/encode-voucher.mdx rename to apps/docs/pages/cmio/reference/encode-call-voucher.mdx index b22d98c0..4c35c910 100644 --- a/apps/docs/pages/cmio/reference/encode-voucher.mdx +++ b/apps/docs/pages/cmio/reference/encode-call-voucher.mdx @@ -1,19 +1,19 @@ -# encodeVoucher +# encodeCallVoucher -Encodes a **voucher** — a one-shot on-chain transaction authorized by the application: a destination, an ether value, and calldata. Once the epoch containing it settles, the voucher can be executed through the application contract, exactly once. +Encodes a **call voucher** — a one-shot on-chain transaction authorized by the application: a destination, an ether value, and calldata. Once the epoch containing it settles, the voucher can be executed through the application contract, exactly once. -`encodeVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a CALL voucher in an `Output2(bytes32[2],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable). +`encodeCallVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a `CallVoucher(address,uint256,bytes)` call, mirroring libcmt's `cmt_callvoucher_encode`). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable). ## Usage Plain value transfer: ```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 }), @@ -23,12 +23,12 @@ const index = rollup.emitOutput( Contract call (calldata in `payload`): ```ts twoslash -import { Rollup, encodeVoucher } from '@deroll/cmio'; +import { Rollup, encodeCallVoucher } from '@deroll/cmio'; const rollup = new Rollup(); declare const calldata: Uint8Array; // e.g. ERC-20 transfer(address,uint256) // ---cut--- rollup.emitOutput( - encodeVoucher({ + encodeCallVoucher({ destination: '0x6B175474E89094C44Da98b954EedeAC495271d0F', payload: calldata, }), diff --git a/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx b/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx deleted file mode 100644 index df6acbee..00000000 --- a/apps/docs/pages/cmio/reference/encode-delegate-call-voucher.mdx +++ /dev/null @@ -1,48 +0,0 @@ -# encodeDelegateCallVoucher - -Encodes a **delegate-call voucher** — like a [voucher](/cmio/reference/encode-voucher), but executed with `DELEGATECALL`: the destination's code runs in the application contract's own storage context, address and balance. An advanced primitive for library-style execution and upgrade patterns; with great power comes great responsibility over the application contract's state. - -`encodeDelegateCallVoucher` is a pure-JS helper that returns the EVM-ABI encoded bytes (a DELEGATECALL voucher in an `Output2(bytes32[2],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the voucher to the outputs merkle tree (provable). - -## Usage - -```ts twoslash -import { Rollup, encodeDelegateCallVoucher } from '@deroll/cmio'; -const rollup = new Rollup(); -declare const calldata: Uint8Array; -// ---cut--- -const index = rollup.emitOutput( - encodeDelegateCallVoucher({ - destination: '0x5FbDB2315678afecb367f032d93F642f64180aa3', // library contract - payload: calldata, - }), -); -``` - -The output index comes from [`emitOutput`](/cmio/reference/emit-output)'s return value. - -## Returns - -`Buffer` — the encoded delegate-call voucher, ready to pass to [`emitOutput`](/cmio/reference/emit-output). - -## Parameters - -### destination - -- Type: `AddressLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array` -- Required; must be exactly 20 bytes - -The contract whose code will run in the application contract's context. - -### payload - -- Type: `BytesLike` — 0x-prefixed hex string, `Buffer` or `Uint8Array` -- Default: empty - -Calldata for the delegate call. There is no `value` parameter — `DELEGATECALL` cannot transfer ether. - -## Errors - -| Condition | Error | -| --- | --- | -| `destination` not 20 bytes / malformed hex | `TypeError` | diff --git a/apps/docs/pages/cmio/reference/encode-notice.mdx b/apps/docs/pages/cmio/reference/encode-notice.mdx index 008fabb5..0696ffb5 100644 --- a/apps/docs/pages/cmio/reference/encode-notice.mdx +++ b/apps/docs/pages/cmio/reference/encode-notice.mdx @@ -2,7 +2,7 @@ Encodes a **notice** — a verifiable statement of fact about the application state, with no on-chain action attached. Off-chain consumers can validate a notice against the application's settled output root instead of re-executing the machine. -`encodeNotice` is a pure-JS helper that returns the EVM-ABI encoded bytes (an `Output1(bytes32[1],bytes)` envelope). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the notice to the outputs merkle tree (provable). +`encodeNotice` is a pure-JS helper that returns the EVM-ABI encoded bytes (a `Notice(bytes)` call, mirroring libcmt's `cmt_notice_encode`). Emit them with [`emitOutput`](/cmio/reference/emit-output) to add the notice to the outputs merkle tree (provable). ## Usage diff --git a/apps/docs/pages/cmio/reference/run.mdx b/apps/docs/pages/cmio/reference/run.mdx index 943db934..212f0bc8 100644 --- a/apps/docs/pages/cmio/reference/run.mdx +++ b/apps/docs/pages/cmio/reference/run.mdx @@ -21,7 +21,7 @@ await rollup.run({ }); ``` -Handlers receive the request with its **raw, undecoded payload**. For an advance, call [`decodeAdvance`](/cmio/reference/decode-advance) to read the input fields, and build provable outputs with [`encodeNotice`](/cmio/reference/encode-notice) / [`encodeVoucher`](/cmio/reference/encode-voucher) before passing them to [`emitOutput`](/cmio/reference/emit-output). +Handlers receive the request with its **raw, undecoded payload**. For an advance, call [`decodeAdvance`](/cmio/reference/decode-advance) to read the input fields, and build provable outputs with [`encodeNotice`](/cmio/reference/encode-notice) / [`encodeCallVoucher`](/cmio/reference/encode-call-voucher) before passing them to [`emitOutput`](/cmio/reference/emit-output). ## Returns diff --git a/apps/docs/pages/cmio/reference/types.mdx b/apps/docs/pages/cmio/reference/types.mdx index e279c437..f70c7560 100644 --- a/apps/docs/pages/cmio/reference/types.mdx +++ b/apps/docs/pages/cmio/reference/types.mdx @@ -5,16 +5,21 @@ Everything the package exports besides the [`Rollup`](/cmio/reference/rollup) cl ```ts twoslash import type { Advance, + AdvanceArgs, AddressLike, AdvanceRequest, BytesLike, - DelegateCallVoucher, + CallVoucher, + ERC20Transfer, + ERC721Transfer, + ERC1155SingleTransfer, + ERC1155BatchTransfer, Hex, InspectRequest, RollupRequest, RunHandlers, + U64Like, U256Like, - Voucher, } from '@deroll/cmio'; ``` @@ -25,26 +30,30 @@ Three "like" types describe what the binding accepts and converts automatically | Type | Accepts | Used for | | --- | --- | --- | | `BytesLike` | `Hex`, `Buffer`, `Uint8Array` | output and report payloads | -| `AddressLike` | `Hex`, `Buffer`, `Uint8Array` (20 bytes) | voucher destinations | -| `U256Like` | `bigint`, `number`, `Hex`, `Uint8Array` (32 bytes) | voucher values | +| `AddressLike` | `Hex`, `Buffer`, `Uint8Array` (20 bytes) | voucher destinations, transfer recipients and tokens | +| `U256Like` | `bigint`, `number`, `Hex`, `Uint8Array` (32 bytes) | voucher values, token amounts and ids | +| `U64Like` | `bigint`, `number` | `encodeAdvance` numeric fields | ## Encoder argument types -The two voucher encoders take a named options object, importable for typing helpers that build outputs: +The output encoders take a named options object, importable for typing helpers that build outputs: -- `Voucher` — argument of [`encodeVoucher`](/cmio/reference/encode-voucher): `destination` (`AddressLike`), optional `value` (`U256Like`, default `0n`) and `payload` (`BytesLike`, default empty). -- `DelegateCallVoucher` — argument of [`encodeDelegateCallVoucher`](/cmio/reference/encode-delegate-call-voucher): `destination` (`AddressLike`) and optional `payload` (`BytesLike`). No `value` — `DELEGATECALL` cannot transfer ether. +- `CallVoucher` — argument of [`encodeCallVoucher`](/cmio/reference/encode-call-voucher): `destination` (`AddressLike`), optional `value` (`U256Like`, default `0n`) and `payload` (`BytesLike`, default empty). +- `ERC20Transfer` / `ERC721Transfer` / `ERC1155SingleTransfer` / `ERC1155BatchTransfer` — arguments of the [asset transfer encoders](/cmio/reference/encode-asset-transfers). +- `AdvanceArgs` — argument of `encodeAdvance` (see [`decodeAdvance`](/cmio/reference/decode-advance)): the same fields as `Advance` with "like" input types. ```ts twoslash -import type { Voucher, DelegateCallVoucher } from '@deroll/cmio'; +import type { CallVoucher, ERC20Transfer } from '@deroll/cmio'; -const voucher: Voucher = { +const voucher: CallVoucher = { destination: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', value: 1_000_000_000_000_000_000n, }; -const delegateCall: DelegateCallVoucher = { - destination: '0x5FbDB2315678afecb367f032d93F642f64180aa3', +const transfer: ERC20Transfer = { + recipient: '0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266', + token: '0x6B175474E89094C44Da98b954EedeAC495271d0F', + value: 100n, }; ``` diff --git a/apps/docs/vocs.config.ts b/apps/docs/vocs.config.ts index e8ec0ac1..04f87a2d 100644 --- a/apps/docs/vocs.config.ts +++ b/apps/docs/vocs.config.ts @@ -270,10 +270,10 @@ export default defineConfig({ { text: "emitException", link: "/cmio/reference/emit-exception" }, { text: "progress", link: "/cmio/reference/progress" }, { text: "close", link: "/cmio/reference/close" }, - { text: "decodeAdvance", link: "/cmio/reference/decode-advance" }, + { text: "decodeAdvance / encodeAdvance", link: "/cmio/reference/decode-advance" }, { text: "encodeNotice", link: "/cmio/reference/encode-notice" }, - { text: "encodeVoucher", link: "/cmio/reference/encode-voucher" }, - { text: "encodeDelegateCallVoucher", link: "/cmio/reference/encode-delegate-call-voucher" }, + { text: "encodeCallVoucher", link: "/cmio/reference/encode-call-voucher" }, + { text: "Asset transfer encoders", link: "/cmio/reference/encode-asset-transfers" }, { text: "Types & Constants", link: "/cmio/reference/types" }, ], }, diff --git a/packages/bindings/cmio/README.md b/packages/bindings/cmio/README.md index ae687a75..c69e8c70 100644 --- a/packages/bindings/cmio/README.md +++ b/packages/bindings/cmio/README.md @@ -9,24 +9,28 @@ The package is a Node-API native addon: The right flavor is selected automatically by target architecture, so the same dapp code runs unchanged on the host and in the machine. -The API is **fully synchronous** on purpose: calls that wait on the emulator (`finish`, `gio`) yield the machine, which pauses the entire guest — including the Node.js event loop — so there is nothing to run concurrently while they wait. On the host mock they return immediately. +The API is **fully synchronous** on purpose: calls that wait on the emulator (`waitForInput`) yield the machine, which pauses the entire guest — including the Node.js event loop — so there is nothing to run concurrently while they wait. On the host mock they return immediately. + +The binding mirrors libcmt's own split between its **rollup** module (raw I/O) and its **codec** module (EVM-ABI wire formats): the `Rollup` class only moves raw bytes, and standalone `encode*`/`decodeAdvance` helpers translate between structured values and bytes. ## Usage ```js -import { Rollup } from '@deroll/cmio'; +import { Rollup, decodeAdvance, encodeCallVoucher, encodeNotice } from '@deroll/cmio'; const rollup = new Rollup(); await rollup.run({ advance(request, rollup) { - // request: { chainId, appContract, msgSender, blockNumber, + // request.payload is the raw EvmAdvance input; decode it + const advance = decodeAdvance(request.payload); + // advance: { chainId, appContract, msgSender, blockNumber, // blockTimestamp, prevRandao, index, payload } - rollup.emitNotice(request.payload); - rollup.emitVoucher({ - destination: request.msgSender, + rollup.emitOutput(encodeNotice(advance.payload)); + rollup.emitOutput(encodeCallVoucher({ + destination: advance.msgSender, value: 0n, payload: '0xdeadbeef', - }); + })); return true; // accept (default); return false to reject }, inspect(request, rollup) { @@ -41,7 +45,7 @@ Or drive the loop yourself: const rollup = new Rollup(); let accept = true; for (;;) { - const request = rollup.finish({ accept }); + const request = rollup.waitForInput({ accept }); accept = handle(request); // your logic } ``` @@ -50,30 +54,40 @@ Byte arguments accept `Buffer`, `Uint8Array` or 0x-prefixed hex strings. Address The package is dual ESM + CommonJS — `const { Rollup } = require('@deroll/cmio')` works too, and both entry points share the same native addon instance. -### API +### Rollup API (raw I/O) | Method | Description | | --- | --- | | `new Rollup()` | Opens the rollup device. Only **one** instance may be open at a time (`-EBUSY` otherwise); `close()` the previous one first. | -| `finish({ accept })` | Accepts/rejects the previous request, yields, and returns the next `{ type: 'advance' \| 'inspect', payload, ... }`. | -| `emitVoucher({ destination, value, payload })` | Emits `Voucher(address,uint256,bytes)`. Returns the output index. | -| `emitDelegateCallVoucher({ destination, payload })` | Emits `DelegateCallVoucher(address,bytes)`. Returns the output index. | -| `emitNotice(payload)` | Emits `Notice(bytes)`. Returns the output index. | +| `waitForInput({ accept })` | Accepts/rejects the previous request, yields, and returns the next `{ type: 'advance' \| 'inspect', payload }` with the raw, undecoded payload. | +| `emitOutput(bytes)` | Emits a raw, already EVM-ABI encoded output; adds it to the outputs merkle tree and returns its index. | | `emitReport(payload)` | Emits a report (raw bytes, not in the outputs merkle tree). | | `emitException(payload)` | Signals that the request could not be processed. | | `progress(value)` | Reports progress (raw uint32). | -| `gio({ domain, id })` | Generic IO request; returns `{ responseCode, responseData }`. | -| `saveMerkle(file)` / `loadMerkle(file)` / `resetMerkle()` | Persist/restore/reset the outputs merkle tree. | | `close()` | Releases the device. | -| `run({ advance, inspect })` | Convenience loop over `finish`; handlers may be async. Handler exceptions reject the input and are emitted as reports. | +| `run({ advance, inspect })` | Convenience loop over `waitForInput`; handlers may be async. Handler exceptions reject the input and are emitted as reports. | Failed libcmt calls throw a `RollupError` with the negative errno in `error.errno` and the failed call in `error.syscall`. -## Documentation +### Codec helpers (EVM-ABI wire formats) -Published at **** (deployed by the [Docs workflow](.github/workflows/docs.yml) on every push to `main`). +Each helper mirrors one entry of libcmt's `codec.h` and produces/consumes the exact same bytes: + +| Function | Wire format | +| --- | --- | +| `decodeAdvance(input)` / `encodeAdvance(fields)` | `EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes)` | +| `encodeNotice(payload)` | `Notice(bytes)` | +| `encodeCallVoucher({ destination, value, payload })` | `CallVoucher(address,uint256,bytes)` | +| `encodeERC20Transfer({ recipient, token, value })` | `ERC20Transfer(address,address,uint256)` | +| `encodeERC721Transfer({ recipient, token, tokenId, data })` | `ERC721Transfer(address,address,uint256,bytes)` | +| `encodeERC1155SingleTransfer({ recipient, token, tokenId, value, data })` | `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` | +| `encodeERC1155BatchTransfer({ recipient, token, tokenIdsAndValues, data })` | `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` | + +Compose them with the raw API: `rollup.emitOutput(encodeNotice(payload))`. `encodeAdvance` is the inverse of `decodeAdvance`, useful for crafting mock inputs when testing on the host. + +## Documentation -The site ([Vocs](https://vocs.dev)) has its pages in [`docs/pages/`](docs/pages/) and is configured by [`vocs.config.ts`](vocs.config.ts): `npm run docs:dev` / `docs:build` / `docs:preview`. +Published at **** (the `apps/docs` Vocs site in this monorepo, under *cmio*). ## Testing on the host (mock) @@ -84,7 +98,7 @@ CMT_INPUTS="0:advance.bin,1:inspect.bin" node my-dapp.js # -> advance.output-0.bin, advance.report-0.bin, ... ``` -Reason `0` is advance (EVM-ABI encoded `EvmAdvance`), `1` is inspect (raw payload); any other reason is a gio reply with that response code. Set `CMT_DEBUG=yes` for verbose logging. See the [libcmt README](https://github.com/cartesi/machine-guest-tools/tree/main/sys-utils/libcmt#testing) for how to generate inputs with foundry's `cast`, or `test/rollup.test.mjs` here for a pure-JS encoder. +Reason `0` is advance (EVM-ABI encoded `EvmAdvance` — build one with `encodeAdvance`), `1` is inspect (raw payload). Set `CMT_DEBUG=yes` for verbose logging. See the [libcmt README](https://github.com/cartesi/machine-guest-tools/tree/main/sys-utils/libcmt#testing) for how to generate inputs with foundry's `cast`, or `test/rollup.test.mjs` here for a pure-JS encoder. ## Testing inside a Cartesi Machine @@ -117,4 +131,4 @@ Publishing uses [npm trusted publishing](https://docs.npmjs.com/trusted-publishe ## License -The libcmt-node repository and all contributions are licensed under [APACHE 2.0](https://www.apache.org/licenses/LICENSE-2.0). Please review our [LICENSE](LICENSE) file. +This package and all contributions are licensed under [APACHE 2.0](https://www.apache.org/licenses/LICENSE-2.0). Please review our [LICENSE](LICENSE) file. diff --git a/packages/bindings/cmio/deps/machine-guest-tools b/packages/bindings/cmio/deps/machine-guest-tools index 99339046..7ffb2dae 160000 --- a/packages/bindings/cmio/deps/machine-guest-tools +++ b/packages/bindings/cmio/deps/machine-guest-tools @@ -1 +1 @@ -Subproject commit 993390463d1cda53ad86ed9c8d88e2cdffa43016 +Subproject commit 7ffb2daea30e94440d0f1a9b9b2de1b4ee64a467 diff --git a/packages/bindings/cmio/lib/index.d.ts b/packages/bindings/cmio/lib/index.d.ts index 7f6521cc..9f7fad5e 100644 --- a/packages/bindings/cmio/lib/index.d.ts +++ b/packages/bindings/cmio/lib/index.d.ts @@ -28,6 +28,9 @@ export type AddressLike = Hex | Uint8Array; /** Unsigned 256-bit value: bigint, number, or 32 bytes (hex string/Uint8Array). */ export type U256Like = bigint | number | Hex | Uint8Array; +/** Unsigned 64-bit value: bigint or number. */ +export type U64Like = bigint | number; + export const ADDRESS_LENGTH: 20; export const U256_LENGTH: 32; @@ -65,8 +68,28 @@ export interface Advance { payload: Buffer; } -/** Arguments for {@link encodeVoucher}. Encoded on-chain as a CALL voucher. */ -export interface Voucher { +/** Arguments for {@link encodeAdvance}, the inverse of {@link decodeAdvance}. */ +export interface AdvanceArgs { + /** Network chain id. */ + chainId: U64Like; + /** Application contract address (20 bytes). */ + appContract: AddressLike; + /** Input sender address (20 bytes). */ + msgSender: AddressLike; + /** Block number of this input. */ + blockNumber: U64Like; + /** Block timestamp of this input (UNIX epoch seconds). */ + blockTimestamp: U64Like; + /** RANDAO mix of the post beacon state of the previous block. */ + prevRandao: U256Like; + /** Input index relative to all inputs ever sent to the application. */ + index: U64Like; + /** Input payload. */ + payload: BytesLike; +} + +/** Arguments for {@link encodeCallVoucher}. Executed on-chain as a CALL. */ +export interface CallVoucher { /** Address the voucher executes against (20 bytes): an EOA for transfers, a contract for calls. */ destination: AddressLike; /** Amount of wei sent with the execution. Default: `0n`. */ @@ -75,28 +98,84 @@ export interface Voucher { payload?: BytesLike; } -/** Arguments for {@link encodeDelegateCallVoucher}. Encoded on-chain as a DELEGATECALL voucher. */ -export interface DelegateCallVoucher { - /** Contract whose code runs in the application contract's storage context (20 bytes). */ - destination: AddressLike; - /** Calldata for the delegate call. Default: empty. There is no `value` — `DELEGATECALL` cannot transfer ether. */ - payload?: BytesLike; +/** Arguments for {@link encodeERC20Transfer}. */ +export interface ERC20Transfer { + /** Address receiving the tokens (20 bytes). */ + recipient: AddressLike; + /** ERC-20 token contract address (20 bytes). */ + token: AddressLike; + /** Amount of tokens to transfer. */ + value: U256Like; +} + +/** Arguments for {@link encodeERC721Transfer}. */ +export interface ERC721Transfer { + /** Address receiving the token (20 bytes). */ + recipient: AddressLike; + /** ERC-721 token contract address (20 bytes). */ + token: AddressLike; + /** Token id to transfer. */ + tokenId: U256Like; + /** Extra data forwarded to the recipient. Default: empty. */ + data?: BytesLike; +} + +/** Arguments for {@link encodeERC1155SingleTransfer}. */ +export interface ERC1155SingleTransfer { + /** Address receiving the tokens (20 bytes). */ + recipient: AddressLike; + /** ERC-1155 token contract address (20 bytes). */ + token: AddressLike; + /** Token id to transfer. */ + tokenId: U256Like; + /** Amount of tokens to transfer. */ + value: U256Like; + /** Extra data forwarded to the recipient. Default: empty. */ + data?: BytesLike; +} + +/** Arguments for {@link encodeERC1155BatchTransfer}. */ +export interface ERC1155BatchTransfer { + /** Address receiving the tokens (20 bytes). */ + recipient: AddressLike; + /** ERC-1155 token contract address (20 bytes). */ + token: AddressLike; + /** `[tokenId, value]` pairs to transfer. */ + tokenIdsAndValues: readonly (readonly [U256Like, U256Like])[]; + /** Extra data forwarded to the recipient. Default: empty. */ + data?: BytesLike; } /** * Decode an `EvmAdvance` input (the raw payload of an advance request) into its - * structured fields. Mirrors libcmt's `cmt_decode_advance_state`. + * structured fields. Mirrors libcmt's `cmt_evmadvance_decode`. */ export function decodeAdvance(input: BytesLike): Advance; -/** Encode a notice into an `Output1(bytes32[1],bytes)` envelope. */ +/** + * Encode an `EvmAdvance` input from its structured fields, the inverse of + * {@link decodeAdvance}. Mirrors libcmt's `cmt_evmadvance_encode`; useful for + * crafting mock inputs (`CMT_INPUTS`) when testing on the host. + */ +export function encodeAdvance(advance: AdvanceArgs): Buffer; + +/** Encode a `Notice(bytes)` output. Mirrors libcmt's `cmt_notice_encode`. */ export function encodeNotice(payload: BytesLike): Buffer; -/** Encode a CALL voucher into an `Output2(bytes32[2],bytes)` envelope. */ -export function encodeVoucher(voucher: Voucher): Buffer; +/** Encode a `CallVoucher(address,uint256,bytes)` output. Mirrors libcmt's `cmt_callvoucher_encode`. */ +export function encodeCallVoucher(voucher: CallVoucher): Buffer; + +/** Encode an `ERC20Transfer(address,address,uint256)` output. Mirrors libcmt's `cmt_erc20transfer_encode`. */ +export function encodeERC20Transfer(transfer: ERC20Transfer): Buffer; + +/** Encode an `ERC721Transfer(address,address,uint256,bytes)` output. Mirrors libcmt's `cmt_erc721transfer_encode`. */ +export function encodeERC721Transfer(transfer: ERC721Transfer): Buffer; + +/** Encode an `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` output. Mirrors libcmt's `cmt_erc1155singletransfer_encode`. */ +export function encodeERC1155SingleTransfer(transfer: ERC1155SingleTransfer): Buffer; -/** Encode a DELEGATECALL voucher into an `Output2(bytes32[2],bytes)` envelope. */ -export function encodeDelegateCallVoucher(voucher: DelegateCallVoucher): Buffer; +/** Encode an `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` output. Mirrors libcmt's `cmt_erc1155batchtransfer_encode`. */ +export function encodeERC1155BatchTransfer(transfer: ERC1155BatchTransfer): Buffer; /** * Error thrown when a libcmt binding call fails (e.g. a too-large output, or diff --git a/packages/bindings/cmio/lib/index.js b/packages/bindings/cmio/lib/index.js index 4abc397f..89b414de 100644 --- a/packages/bindings/cmio/lib/index.js +++ b/packages/bindings/cmio/lib/index.js @@ -17,35 +17,35 @@ 'use strict'; const path = require('node:path'); -const { AbiFunction, AbiParameters, Bytes, Hash } = require('ox'); +const { AbiFunction, AbiParameters } = require('ox'); const binding = require('node-gyp-build')(path.join(__dirname, '..')); const ADDRESS_LENGTH = 20; const U256_LENGTH = 32; const EMPTY = Buffer.alloc(0); -// EVM-ABI encoders/decoders for libcmt's output envelopes, expressed with ox so -// the low-level word packing/padding stays battle-tested. The Output1/Output2 -// selectors ox derives from these signatures match libcmt's hardcoded funsels -// (0xaed682a1 / 0x50b41f12). -const OUTPUT1 = AbiFunction.from('function Output1(bytes32[1], bytes)'); -const OUTPUT2 = AbiFunction.from('function Output2(bytes32[2], bytes)'); +// EVM-ABI codecs for libcmt's wire formats, expressed with ox so the low-level +// word packing/padding stays battle-tested. One AbiFunction per codec.h entry; +// the selectors ox derives from these signatures match libcmt's hardcoded +// funsels (codec.h stores them as little-endian uint32, so e.g. Notice's +// 0xe5d658c2 there is wire bytes c2 58 d6 e5 = ox's 0xc258d6e5). +const NOTICE = AbiFunction.from('function Notice(bytes payload)'); // 0xc258d6e5 +const CALL_VOUCHER = AbiFunction.from('function CallVoucher(address destination, uint256 value, bytes payload)'); // 0x4691c2bc +const ERC20_TRANSFER = AbiFunction.from('function ERC20Transfer(address recipient, address token, uint256 value)'); // 0xe59fdd36 +const ERC721_TRANSFER = AbiFunction.from( + 'function ERC721Transfer(address recipient, address token, uint256 tokenId, bytes data)', +); // 0x2b0e47a0 +const ERC1155_SINGLE_TRANSFER = AbiFunction.from( + 'function ERC1155SingleTransfer(address recipient, address token, uint256 tokenId, uint256 value, bytes data)', +); // 0x8c76b598 +const ERC1155_BATCH_TRANSFER = AbiFunction.from( + 'function ERC1155BatchTransfer(address recipient, address token, uint256[2][] tokenIdsAndValues, bytes data)', +); // 0x2c38972f const EVM_ADVANCE = AbiFunction.from( - 'function EvmAdvance(uint256 chainId, address appContract, address msgSender, ' + - 'uint256 blockNumber, uint256 blockTimestamp, uint256 prevRandao, uint256 index, bytes payload)', -); + 'function EvmAdvance(uint64 chainId, address appContract, address msgSender, ' + + 'uint64 blockNumber, uint64 blockTimestamp, uint256 prevRandao, uint64 index, bytes payload)', +); // 0x233a0ebf const EVM_ADVANCE_SELECTOR = EVM_ADVANCE.hash.slice(0, 10); // 0x + 4 bytes -// CALL voucher dynamic content: abi.encode(uint256 value, bytes payload) -const CALL_VOUCHER_DATA = AbiParameters.from('uint256 value, bytes payload'); - -// Each output kind is tagged with a domain-separated identifier: -// keccak256("cartesi.output.v1."). These are the same 32-byte constants -// libcmt hardcodes in codec.c as CARTESI_OUTPUT_V1_*; we derive them so the -// source-of-truth string is visible rather than an opaque hash. -const outputTag = (kind) => Hash.keccak256(Bytes.fromString(`cartesi.output.v1.${kind}`), { as: 'Hex' }); -const TAG_NOTICE = outputTag('notice'); -const TAG_CALL_VOUCHER = outputTag('call-voucher'); -const TAG_DELEGATECALL_VOUCHER = outputTag('delegatecall-voucher'); /** * Error thrown when a libcmt binding call fails. Carries the negative errno @@ -112,36 +112,47 @@ function toAddress(value, name) { return bytes; } -// Validate an unsigned 256-bit value and return it as a bigint for ox. -function toU256(value, name) { +// Validate an unsigned integer of `bits` width and return it as a bigint for +// ox. 256-bit values also accept a 32-byte big-endian buffer/hex string. +function toUint(value, name, bits) { let v; if (typeof value === 'bigint' || typeof value === 'number') { v = BigInt(value); - } else { + } else if (bits === 256) { const bytes = toBytes(value, name); if (bytes.length !== U256_LENGTH) { throw new TypeError(`${name} must be ${U256_LENGTH} bytes long`); } v = bytes.length === 0 ? 0n : BigInt(toHex(bytes)); + } else { + throw new TypeError(`${name} must be a bigint or number`); } - if (v < 0n || v >= 1n << 256n) { - throw new RangeError(`${name} must fit in an unsigned 256-bit integer`); + if (v < 0n || v >= 1n << BigInt(bits)) { + throw new RangeError(`${name} must fit in an unsigned ${bits}-bit integer`); } return v; } +const toU256 = (value, name) => toUint(value, name, 256); +const toU64 = (value, name) => toUint(value, name, 64); + function toHex(bytes) { return `0x${bytes.toString('hex')}`; } -// Left-pad a validated 20-byte address into a 32-byte ABI word (0x-hex). -function addressToWord(bytes) { - return `0x${bytes.toString('hex').padStart(2 * U256_LENGTH, '0')}`; +// Validate a 20-byte address argument and return it as 0x-hex for ox. +function toAddressHex(value, name) { + return toHex(toAddress(value, name)); +} + +// Encode a call to `fn` with `args` and return the calldata as a Buffer. +function encodeData(fn, args) { + return Buffer.from(AbiFunction.encodeData(fn, args).slice(2), 'hex'); } /** * Decode an `EvmAdvance` input (the raw payload of an advance request) into its - * structured fields. Mirrors libcmt's `cmt_decode_advance_state`. + * structured fields. Mirrors libcmt's `cmt_evmadvance_decode`. */ function decodeAdvance(input) { const bytes = toBytes(input, 'input'); @@ -165,32 +176,103 @@ function decodeAdvance(input) { }; } -/** Encode a notice into an `Output1(bytes32[1],bytes)` envelope. */ +/** + * Encode an `EvmAdvance` input from its structured fields, the inverse of + * {@link decodeAdvance}. Mirrors libcmt's `cmt_evmadvance_encode`; useful for + * crafting mock inputs (`CMT_INPUTS`) when testing on the host. + */ +function encodeAdvance({ chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload }) { + return encodeData(EVM_ADVANCE, [ + toU64(chainId, 'chainId'), + toAddressHex(appContract, 'appContract'), + toAddressHex(msgSender, 'msgSender'), + toU64(blockNumber, 'blockNumber'), + toU64(blockTimestamp, 'blockTimestamp'), + toU256(prevRandao, 'prevRandao'), + toU64(index, 'index'), + toHex(toBytes(payload, 'payload')), + ]); +} + +/** Encode a `Notice(bytes)` output. Mirrors libcmt's `cmt_notice_encode`. */ function encodeNotice(payload) { - const data = toHex(toBytes(payload, 'payload')); - return Buffer.from(AbiFunction.encodeData(OUTPUT1, [[TAG_NOTICE], data]).slice(2), 'hex'); + return encodeData(NOTICE, [toHex(toBytes(payload, 'payload'))]); } /** - * Encode a CALL voucher into an `Output2(bytes32[2],bytes)` envelope, whose - * dynamic content is `abi.encode(uint256 value, bytes payload)`. + * Encode a `CallVoucher(address,uint256,bytes)` output: an on-chain CALL to + * `destination` with `value` wei and `payload` calldata. Mirrors libcmt's + * `cmt_callvoucher_encode`. */ -function encodeVoucher({ destination, value = 0n, payload = EMPTY }) { - const dest = addressToWord(toAddress(destination, 'destination')); - const val = toU256(value, 'value'); - const data = toHex(toBytes(payload, 'payload')); - const inner = AbiParameters.encode(CALL_VOUCHER_DATA, [val, data]); - return Buffer.from(AbiFunction.encodeData(OUTPUT2, [[TAG_CALL_VOUCHER, dest], inner]).slice(2), 'hex'); +function encodeCallVoucher({ destination, value = 0n, payload = EMPTY }) { + return encodeData(CALL_VOUCHER, [ + toAddressHex(destination, 'destination'), + toU256(value, 'value'), + toHex(toBytes(payload, 'payload')), + ]); } /** - * Encode a DELEGATECALL voucher into an `Output2(bytes32[2],bytes)` envelope. - * There is no `value` — `DELEGATECALL` cannot transfer ether. + * Encode an `ERC20Transfer(address,address,uint256)` output: transfer `value` + * of `token` to `recipient`. Mirrors libcmt's `cmt_erc20transfer_encode`. */ -function encodeDelegateCallVoucher({ destination, payload = EMPTY }) { - const dest = addressToWord(toAddress(destination, 'destination')); - const data = toHex(toBytes(payload, 'payload')); - return Buffer.from(AbiFunction.encodeData(OUTPUT2, [[TAG_DELEGATECALL_VOUCHER, dest], data]).slice(2), 'hex'); +function encodeERC20Transfer({ recipient, token, value }) { + return encodeData(ERC20_TRANSFER, [ + toAddressHex(recipient, 'recipient'), + toAddressHex(token, 'token'), + toU256(value, 'value'), + ]); +} + +/** + * Encode an `ERC721Transfer(address,address,uint256,bytes)` output: transfer + * `tokenId` of `token` to `recipient`. Mirrors libcmt's `cmt_erc721transfer_encode`. + */ +function encodeERC721Transfer({ recipient, token, tokenId, data = EMPTY }) { + return encodeData(ERC721_TRANSFER, [ + toAddressHex(recipient, 'recipient'), + toAddressHex(token, 'token'), + toU256(tokenId, 'tokenId'), + toHex(toBytes(data, 'data')), + ]); +} + +/** + * Encode an `ERC1155SingleTransfer(address,address,uint256,uint256,bytes)` + * output: transfer `value` of `tokenId` of `token` to `recipient`. Mirrors + * libcmt's `cmt_erc1155singletransfer_encode`. + */ +function encodeERC1155SingleTransfer({ recipient, token, tokenId, value, data = EMPTY }) { + return encodeData(ERC1155_SINGLE_TRANSFER, [ + toAddressHex(recipient, 'recipient'), + toAddressHex(token, 'token'), + toU256(tokenId, 'tokenId'), + toU256(value, 'value'), + toHex(toBytes(data, 'data')), + ]); +} + +/** + * Encode an `ERC1155BatchTransfer(address,address,uint256[2][],bytes)` output: + * transfer several `[tokenId, value]` pairs of `token` to `recipient`. Mirrors + * libcmt's `cmt_erc1155batchtransfer_encode`. + */ +function encodeERC1155BatchTransfer({ recipient, token, tokenIdsAndValues, data = EMPTY }) { + if (!Array.isArray(tokenIdsAndValues)) { + throw new TypeError('tokenIdsAndValues must be an array of [tokenId, value] pairs'); + } + const pairs = tokenIdsAndValues.map((pair, i) => { + if (!Array.isArray(pair) || pair.length !== 2) { + throw new TypeError(`tokenIdsAndValues[${i}] must be a [tokenId, value] pair`); + } + return [toU256(pair[0], `tokenIdsAndValues[${i}][0]`), toU256(pair[1], `tokenIdsAndValues[${i}][1]`)]; + }); + return encodeData(ERC1155_BATCH_TRANSFER, [ + toAddressHex(recipient, 'recipient'), + toAddressHex(token, 'token'), + pairs, + toHex(toBytes(data, 'data')), + ]); } class Rollup { @@ -261,9 +343,13 @@ module.exports = { Rollup, RollupError, decodeAdvance, + encodeAdvance, encodeNotice, - encodeVoucher, - encodeDelegateCallVoucher, + encodeCallVoucher, + encodeERC20Transfer, + encodeERC721Transfer, + encodeERC1155SingleTransfer, + encodeERC1155BatchTransfer, ADDRESS_LENGTH, U256_LENGTH, }; diff --git a/packages/bindings/cmio/lib/index.mjs b/packages/bindings/cmio/lib/index.mjs index 2f3cdefb..3a83770d 100644 --- a/packages/bindings/cmio/lib/index.mjs +++ b/packages/bindings/cmio/lib/index.mjs @@ -20,9 +20,13 @@ export { Rollup, RollupError, decodeAdvance, + encodeAdvance, encodeNotice, - encodeVoucher, - encodeDelegateCallVoucher, + encodeCallVoucher, + encodeERC20Transfer, + encodeERC721Transfer, + encodeERC1155SingleTransfer, + encodeERC1155BatchTransfer, ADDRESS_LENGTH, U256_LENGTH, } from './index.js'; diff --git a/packages/bindings/cmio/test/machine/abi.mjs b/packages/bindings/cmio/test/machine/abi.mjs index 59b65e72..48c17457 100644 --- a/packages/bindings/cmio/test/machine/abi.mjs +++ b/packages/bindings/cmio/test/machine/abi.mjs @@ -15,12 +15,12 @@ // // Minimal EVM-ABI helpers shared by encode-inputs.mjs and verify-outputs.mjs, -// enough to encode EvmAdvance inputs and decode Voucher/Notice outputs. +// enough to encode EvmAdvance inputs and CallVoucher/Notice outputs. // Mirrors the encoder in test/rollup.test.mjs. export const SELECTOR = { - evmAdvance: '415bf363', // EvmAdvance(uint256,address,address,uint256,uint256,uint256,uint256,bytes) - voucher: '237a816f', // Voucher(address,uint256,bytes) + evmAdvance: '233a0ebf', // EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes) + callVoucher: '4691c2bc', // CallVoucher(address,uint256,bytes) notice: 'c258d6e5', // Notice(bytes) }; @@ -62,9 +62,9 @@ export function encodeNotice(payload) { ]); } -export function encodeVoucher({ destination, value, payload }) { +export function encodeCallVoucher({ destination, value, payload }) { return Buffer.concat([ - Buffer.from(SELECTOR.voucher, 'hex'), + Buffer.from(SELECTOR.callVoucher, 'hex'), addressWord(destination), word(value), word(3 * 32), // offset of the payload `bytes` field diff --git a/packages/bindings/cmio/test/machine/app.mjs b/packages/bindings/cmio/test/machine/app.mjs index 4cc556c4..df049e0c 100644 --- a/packages/bindings/cmio/test/machine/app.mjs +++ b/packages/bindings/cmio/test/machine/app.mjs @@ -15,21 +15,24 @@ // // Echo dapp run inside the Cartesi Machine by test/machine/run.sh. For each -// advance it emits a notice and a voucher echoing the payload plus a report; -// for each inspect it reports the query payload back. +// advance it emits a notice and a call voucher echoing the payload plus a +// report; for each inspect it reports the query payload back. -import { Rollup } from '@deroll/cmio'; +import { Rollup, decodeAdvance, encodeCallVoucher, encodeNotice } from '@deroll/cmio'; const rollup = new Rollup(); await rollup.run({ advance(request, rollup) { - rollup.emitNotice(request.payload); - rollup.emitVoucher({ - destination: request.msgSender, - value: request.index, - payload: request.payload, - }); - rollup.emitReport(Buffer.from(`advance index=${request.index} chainId=${request.chainId}`)); + const advance = decodeAdvance(request.payload); + rollup.emitOutput(encodeNotice(advance.payload)); + rollup.emitOutput( + encodeCallVoucher({ + destination: advance.msgSender, + value: advance.index, + payload: advance.payload, + }), + ); + rollup.emitReport(Buffer.from(`advance index=${advance.index} chainId=${advance.chainId}`)); return true; }, inspect(request, rollup) { diff --git a/packages/bindings/cmio/test/machine/verify-outputs.mjs b/packages/bindings/cmio/test/machine/verify-outputs.mjs index d4753b8b..b0a701a3 100644 --- a/packages/bindings/cmio/test/machine/verify-outputs.mjs +++ b/packages/bindings/cmio/test/machine/verify-outputs.mjs @@ -23,7 +23,7 @@ import assert from 'node:assert/strict'; import fs from 'node:fs'; import path from 'node:path'; -import { ADVANCES, QUERY, encodeNotice, encodeVoucher } from './abi.mjs'; +import { ADVANCES, QUERY, encodeCallVoucher, encodeNotice } from './abi.mjs'; const dir = process.argv[2]; if (!dir) { @@ -38,7 +38,7 @@ ADVANCES.forEach((advance, i) => { assert.deepEqual(read(`input-${i}-output-0.bin`), encodeNotice(advance.payload), `input ${i}: notice mismatch`); assert.deepEqual( read(`input-${i}-output-1.bin`), - encodeVoucher({ destination: advance.msgSender, value: advance.index, payload: advance.payload }), + encodeCallVoucher({ destination: advance.msgSender, value: advance.index, payload: advance.payload }), `input ${i}: voucher mismatch`, ); assert.equal( diff --git a/packages/bindings/cmio/test/rollup.test.mjs b/packages/bindings/cmio/test/rollup.test.mjs index 149bdf95..5a68d292 100644 --- a/packages/bindings/cmio/test/rollup.test.mjs +++ b/packages/bindings/cmio/test/rollup.test.mjs @@ -24,22 +24,29 @@ import { Rollup, RollupError, decodeAdvance, + encodeAdvance, encodeNotice, - encodeVoucher, - encodeDelegateCallVoucher, + encodeCallVoucher, + encodeERC20Transfer, + encodeERC721Transfer, + encodeERC1155SingleTransfer, + encodeERC1155BatchTransfer, } from '../lib/index.mjs'; // keep the mock by-product files (advance.output-0.bin etc.) out of the repo const tmp = fs.mkdtempSync(path.join(os.tmpdir(), 'libcmt-node-')); process.chdir(tmp); -// EVM-ABI function selectors / output type tags, mirroring libcmt's codec.c. -const EVM_ADVANCE = '415bf363'; // EvmAdvance(uint256,address,address,...,bytes) -const FUNSEL = { output1: 'aed682a1', output2: '50b41f12' }; -const TAG = { - notice: 'e4f5829fb698a59fba2cf6128b6bf1e8ce1dc09d271c55b787781bd415db8eed', - callVoucher: 'd515b20044ba3bb84cfce3004f8b64ee11fb8ca22f936e4bc25a65e4b2133120', - delegateCallVoucher: 'e166d466bf2d7d71d7f3f69e4c68f516330d8073b6ddb408c507548b64a1f3bb', +// EVM-ABI function selectors, mirroring libcmt's codec.h funsels (which are +// stored there as little-endian uint32, so the byte order here is reversed). +const FUNSEL = { + evmAdvance: '233a0ebf', // EvmAdvance(uint64,address,address,uint64,uint64,uint256,uint64,bytes) + notice: 'c258d6e5', // Notice(bytes) + callVoucher: '4691c2bc', // CallVoucher(address,uint256,bytes) + erc20Transfer: 'e59fdd36', // ERC20Transfer(address,address,uint256) + erc721Transfer: '2b0e47a0', // ERC721Transfer(address,address,uint256,bytes) + erc1155SingleTransfer: '8c76b598', // ERC1155SingleTransfer(address,address,uint256,uint256,bytes) + erc1155BatchTransfer: '2c38972f', // ERC1155BatchTransfer(address,address,uint256[2][],bytes) }; function word(value) { @@ -55,9 +62,12 @@ function word(value) { const addressWord = (hex) => Buffer.concat([Buffer.alloc(12), Buffer.from(hex.slice(2), 'hex')]); const pad32 = (bytes) => Buffer.concat([bytes, Buffer.alloc((32 - (bytes.length % 32)) % 32)]); +// Hand-rolled EvmAdvance encoder, independent of the library's encodeAdvance +// so the two implementations cross-check each other. All heads are 32-byte +// words regardless of the field's declared width (uint64 fields included). function encodeEvmAdvance({ chainId, appContract, msgSender, blockNumber, blockTimestamp, prevRandao, index, payload }) { return Buffer.concat([ - Buffer.from(EVM_ADVANCE, 'hex'), + Buffer.from(FUNSEL.evmAdvance, 'hex'), word(chainId), addressWord(appContract), addressWord(msgSender), @@ -105,6 +115,9 @@ test('advance request, outputs and reports', async () => { const request = rollup.waitForInput(); assert.equal(request.type, 'advance'); + // the library encoder and the hand-rolled one above must agree + assert.deepEqual(encodeAdvance(ADVANCE), encodeEvmAdvance(ADVANCE)); + const advance = decodeAdvance(request.payload); assert.equal(advance.chainId, ADVANCE.chainId); assert.equal(advance.appContract, ADVANCE.appContract); @@ -123,30 +136,26 @@ test('advance request, outputs and reports', async () => { // outputs are EVM-ABI encoded in JS and emitted as raw bytes; the index is // the position in the outputs merkle tree. assert.equal(rollup.emitOutput(encodeNotice(noticePayload)), 0); - assert.equal(rollup.emitOutput(encodeVoucher({ destination, value: 1000n, payload: voucherPayload })), 1); + assert.equal(rollup.emitOutput(encodeCallVoucher({ destination, value: 1000n, payload: voucherPayload })), 1); rollup.emitReport(reportPayload); rollup.progress(500); - // notice: Output1 envelope (funsel | NOTICE tag | offset | length | payload) + // notice: Notice(bytes) = funsel | offset | length | payload const notice = fs.readFileSync(path.join(dir, 'advance.output-0.bin')); - assert.equal(hex(notice.subarray(0, 4)), FUNSEL.output1); - assert.equal(hex(notice.subarray(4, 36)), TAG.notice); - assert.deepEqual(notice.subarray(36, 68), word(0x40)); - assert.deepEqual(notice.subarray(68, 100), word(noticePayload.length)); - assert.deepEqual(notice.subarray(100, 100 + noticePayload.length), noticePayload); + assert.equal(hex(notice.subarray(0, 4)), FUNSEL.notice); + assert.deepEqual(notice.subarray(4, 36), word(0x20)); + assert.deepEqual(notice.subarray(36, 68), word(noticePayload.length)); + assert.deepEqual(notice.subarray(68, 68 + noticePayload.length), noticePayload); - // call voucher: Output2 envelope; dynamic content is abi.encode(value, payload) + // call voucher: CallVoucher(address,uint256,bytes) = + // funsel | destination | value | offset | length | payload const voucher = fs.readFileSync(path.join(dir, 'advance.output-1.bin')); - assert.equal(hex(voucher.subarray(0, 4)), FUNSEL.output2); - assert.equal(hex(voucher.subarray(4, 36)), TAG.callVoucher); - assert.deepEqual(voucher.subarray(36, 68), addressWord(destination)); + assert.equal(hex(voucher.subarray(0, 4)), FUNSEL.callVoucher); + assert.deepEqual(voucher.subarray(4, 36), addressWord(destination)); + assert.deepEqual(voucher.subarray(36, 68), word(1000n)); // value assert.deepEqual(voucher.subarray(68, 100), word(0x60)); - const used = 96 + pad32(voucherPayload).length; - assert.deepEqual(voucher.subarray(100, 132), word(used)); - assert.deepEqual(voucher.subarray(132, 164), word(1000n)); // value - assert.deepEqual(voucher.subarray(164, 196), word(0x40)); // inner offset - assert.deepEqual(voucher.subarray(196, 228), word(voucherPayload.length)); - assert.deepEqual(voucher.subarray(228, 228 + voucherPayload.length), voucherPayload); + assert.deepEqual(voucher.subarray(100, 132), word(voucherPayload.length)); + assert.deepEqual(voucher.subarray(132, 132 + voucherPayload.length), voucherPayload); // reports are raw assert.deepEqual(fs.readFileSync(path.join(dir, 'advance.report-0.bin')), reportPayload); @@ -180,23 +189,68 @@ test('inspect request', async () => { rollup.close(); }); -test('delegate call voucher', async () => { - const dir = writeInputs('dcv', [[0, 'advance.bin', encodeEvmAdvance(ADVANCE)]]); +test('asset transfer outputs', async () => { + const dir = writeInputs('transfers', [[0, 'advance.bin', encodeEvmAdvance(ADVANCE)]]); const rollup = new Rollup(); rollup.waitForInput(); - const destination = `0x${'bb'.repeat(20)}`; - const payload = Buffer.from('delegate-payload'); - assert.equal(rollup.emitOutput(encodeDelegateCallVoucher({ destination, payload })), 0); - - // Output2 envelope: funsel | DELEGATECALL tag | destination | offset | length | payload - const output = fs.readFileSync(path.join(dir, 'advance.output-0.bin')); - assert.equal(hex(output.subarray(0, 4)), FUNSEL.output2); - assert.equal(hex(output.subarray(4, 36)), TAG.delegateCallVoucher); - assert.deepEqual(output.subarray(36, 68), addressWord(destination)); - assert.deepEqual(output.subarray(68, 100), word(0x60)); - assert.deepEqual(output.subarray(100, 132), word(payload.length)); - assert.deepEqual(output.subarray(132, 132 + payload.length), payload); + const recipient = `0x${'bb'.repeat(20)}`; + const token = `0x${'cc'.repeat(20)}`; + const data = Buffer.from('transfer-data'); + + assert.equal(rollup.emitOutput(encodeERC20Transfer({ recipient, token, value: 1000n })), 0); + assert.equal(rollup.emitOutput(encodeERC721Transfer({ recipient, token, tokenId: 7n, data })), 1); + assert.equal(rollup.emitOutput(encodeERC1155SingleTransfer({ recipient, token, tokenId: 7n, value: 3n })), 2); + assert.equal( + rollup.emitOutput( + encodeERC1155BatchTransfer({ + recipient, + token, + tokenIdsAndValues: [ + [1n, 2n], + [3n, 4n], + ], + }), + ), + 3, + ); + + // ERC20Transfer(address,address,uint256): static, funsel + 3 words + const erc20 = fs.readFileSync(path.join(dir, 'advance.output-0.bin')); + assert.equal(hex(erc20.subarray(0, 4)), FUNSEL.erc20Transfer); + assert.deepEqual(erc20.subarray(4, 36), addressWord(recipient)); + assert.deepEqual(erc20.subarray(36, 68), addressWord(token)); + assert.deepEqual(erc20.subarray(68, 100), word(1000n)); + assert.equal(erc20.length, 100); + + // ERC721Transfer(address,address,uint256,bytes) = + // funsel | recipient | token | tokenId | offset | length | data + const erc721 = fs.readFileSync(path.join(dir, 'advance.output-1.bin')); + assert.equal(hex(erc721.subarray(0, 4)), FUNSEL.erc721Transfer); + assert.deepEqual(erc721.subarray(68, 100), word(7n)); + assert.deepEqual(erc721.subarray(100, 132), word(0x80)); + assert.deepEqual(erc721.subarray(132, 164), word(data.length)); + assert.deepEqual(erc721.subarray(164, 164 + data.length), data); + + // ERC1155SingleTransfer(address,address,uint256,uint256,bytes): empty data + const single = fs.readFileSync(path.join(dir, 'advance.output-2.bin')); + assert.equal(hex(single.subarray(0, 4)), FUNSEL.erc1155SingleTransfer); + assert.deepEqual(single.subarray(68, 100), word(7n)); // tokenId + assert.deepEqual(single.subarray(100, 132), word(3n)); // value + assert.deepEqual(single.subarray(132, 164), word(0xa0)); // data offset + assert.deepEqual(single.subarray(164, 196), word(0)); // data length + + // ERC1155BatchTransfer(address,address,uint256[2][],bytes): pairs are + // encoded as a dynamic array of static [tokenId, value] tuples + const batch = fs.readFileSync(path.join(dir, 'advance.output-3.bin')); + assert.equal(hex(batch.subarray(0, 4)), FUNSEL.erc1155BatchTransfer); + assert.deepEqual(batch.subarray(68, 100), word(0x80)); // pairs offset + assert.deepEqual(batch.subarray(100, 132), word(0x120)); // data offset + assert.deepEqual(batch.subarray(132, 164), word(2)); // pair count + assert.deepEqual(batch.subarray(164, 196), word(1n)); + assert.deepEqual(batch.subarray(196, 228), word(2n)); + assert.deepEqual(batch.subarray(228, 260), word(3n)); + assert.deepEqual(batch.subarray(260, 292), word(4n)); rollup.close(); }); @@ -253,9 +307,14 @@ test('input validation', async () => { const rollup = new Rollup(); rollup.waitForInput(); - assert.throws(() => encodeVoucher({ destination: '0x1234' }), /destination must be 20 bytes/); - assert.throws(() => encodeVoucher({ destination: 'not-hex' }), TypeError); - assert.throws(() => encodeVoucher({ destination: `0x${'aa'.repeat(20)}`, value: -1n }), RangeError); + assert.throws(() => encodeCallVoucher({ destination: '0x1234' }), /destination must be 20 bytes/); + assert.throws(() => encodeCallVoucher({ destination: 'not-hex' }), TypeError); + assert.throws(() => encodeCallVoucher({ destination: `0x${'aa'.repeat(20)}`, value: -1n }), RangeError); + assert.throws(() => encodeAdvance({ ...ADVANCE, chainId: 1n << 64n }), RangeError); // uint64 overflow + assert.throws( + () => encodeERC1155BatchTransfer({ recipient: `0x${'aa'.repeat(20)}`, token: `0x${'bb'.repeat(20)}`, tokenIdsAndValues: [[1n]] }), + TypeError, + ); assert.throws(() => rollup.emitOutput(42), TypeError); assert.throws(() => decodeAdvance(Buffer.alloc(3)), RangeError); // shorter than a selector assert.throws(() => decodeAdvance(Buffer.alloc(300)), TypeError); // long enough, wrong selector