| name | cartesi-backend-py |
| version | 0.1.0 |
| description | Build Cartesi Rollups v2 backend applications in Python with production-shaped structure: finish loop, advance/inspect handlers, payload codecs, output emitters, state modules, tests, and operational checks. Use when the target backend stack is Python. |
Cartesi Rollups Python Backend
Objective
Use this skill for Python-based Cartesi backend implementation and refactoring.
cartesi-backend-core is the source of truth for backend fundamental design principles (determinism, state transition model, payload contracts, output semantics, and finish-loop correctness). Use this skill for Python-specific implementation choices only.
In scope:
/finish processing loop and request dispatch;
- Advance/inspect handlers and deterministic state updates;
- Hex/UTF-8 and JSON payload codecs;
- Reports/notices/vouchers helpers;
- Python tests for handler behavior.
Out of scope:
- Self-hosted deployment and node operations (use
cartesi-deploy);
- Frontend wallet and JSON-RPC UI concerns;
- Solidity contract authoring.
Authoritative references
Scaffold from Cartesi CLI template
Start new projects with Cartesi CLI before applying architecture rules in this skill.
cartesi create <app-name> --template python
Replace <app-name> with your target project name.
Use the directory and file structure generated by the Cartesi CLI template as the default.
Only recommend structural refactors (new files/folders/module splits) when the developer explicitly asks for them.
RISC-V Wheel Index Integration
When this section applies: only when requirements.txt includes Python
packages with native (C/Rust) extensions that are not already available as
linux_riscv64 wheels on the upstream RISC-V index used by the Cartesi base
image. Pure-Python dependencies and packages already shipped as RISC-V
wheels do not require a local index — the default Cartesi CLI template
layout is sufficient for those cases.
When the project does need a local wheel index, it MUST include a
riscv-wheel-index/ directory at the project root with the structure below.
This is a controlled, opt-in extension to the Cartesi CLI Python template;
it does NOT replace the template's other files.
Required structure:
riscv-wheel-index/
├── index.json
├── wheels/<python-tag>/<package>/<wheel>.whl
├── metadata/ (optional)
├── sources/
index.json MUST track wheel availability for the build, and wheels/<python-tag>/ MUST contain all wheel artifacts needed by requirements.txt.
Dependency resolution rules
These rules apply when the project uses a local riscv-wheel-index/ as
described above. Projects that do not need a local index can skip this
section and rely on the Cartesi base image's default Python package
resolution.
- All Python dependencies MUST be installed from the local wheel index.
pip install MUST use:
--no-index --find-links=/opt/riscv-wheel-index/wheels/<python-tag>
- During Docker builds, direct installs from PyPI, GitHub, or other network package sources MUST NOT be used for native dependencies.
- If a dependency is missing from the local index, the developer MUST:
- build the wheel manually;
- add the wheel to
riscv-wheel-index/wheels/<python-tag>/...;
- update
riscv-wheel-index/index.json accordingly.
Dockerfile patch instructions
Do NOT replace the Cartesi template Dockerfile. Add only the following patch lines required for local index installation:
COPY riscv-wheel-index/ /opt/riscv-wheel-index/
ARG PYTHON_TAG=cp310
RUN pip install \
--no-index \
--find-links=/opt/riscv-wheel-index/wheels/${PYTHON_TAG} \
-r requirements.txt
Wheel sourcing policy
- The local wheel index is the authoritative source for build-time Python dependencies.
- Upstream repositories (for example
prototyp3, tuler, think-dev) are ONLY allowed for populating riscv-wheel-index/ ahead of time.
- Upstream repositories MUST NOT be queried during Docker image build.
Determinism and failure handling
- Builds MUST be reproducible given the same
requirements.txt and the same riscv-wheel-index/.
- Builds MUST NOT require network dependency resolution.
- If a required wheel is missing, the build MUST fail and trigger the manual wheel build workflow.
- Silent fallback to PyPI or any external index MUST NOT be allowed.
Manual Wheel Build Workflow (RISC-V)
This workflow MUST be followed when a required dependency is not present in riscv-wheel-index/.
Preconditions
- Target Python tag is known (for example
cp310).
- Package version is pinned in
requirements.txt.
- Build runs in a RISC-V-compatible environment (native RISC-V or Docker/QEMU RISC-V).
Step 1 - Classify dependency
- Pure-Python dependency: prefer adding a compatible wheel artifact to the local index.
- Native extension dependency: developer MUST build a
linux_riscv64 wheel manually using the steps below.
Step 2 - Prepare build environment
Install baseline build tooling:
apt-get update && apt-get install -y \
build-essential \
python3-dev \
python3-pip \
python3-setuptools \
python3-wheel \
python3-venv
Upgrade Python build tools:
pip install --upgrade pip build wheel setuptools
Step 3 - Install package-specific system dependencies
Developer MUST install required system libraries before building.
Examples:
cryptography: libssl-dev, libffi-dev
numpy/scipy: libopenblas-dev, gfortran
pillow: libjpeg-dev, zlib1g-dev
If dependencies are unknown, attempt build, inspect errors, install missing packages, and retry.
Step 4 - Build wheel
Preferred:
pip wheel <package>==<version> --no-deps
Alternative:
python -m build
Step 5 - Validate and test wheel
Developer MUST verify:
- wheel file exists with
linux_riscv64 tag;
- Python ABI matches target tag (
cp310, etc.).
Developer MUST test:
pip install <wheel-file>
python -c "import <package>"
Import MUST succeed without ImportError or missing shared library errors.
Step 6 - Add wheel to index
Place the wheel under:
riscv-wheel-index/wheels/<python-tag>/<package>/<wheel>.whl
Update riscv-wheel-index/index.json with package, version, file path, ABI, platform linux_riscv64, and source metadata (for example manual-build).
Optional metadata MAY be added at:
riscv-wheel-index/metadata/<package>.json
Step 7 - Rebuild Cartesi image
Re-run:
cartesi build
Build MUST succeed without network dependency resolution and without silent fallback behavior.
Manual-build constraints
- Developer MUST build inside a RISC-V-compatible environment.
- Developer MUST NOT rely on wheels built for non-RISC-V architectures.
- Developer MUST ensure reproducibility before committing wheel/index updates.
Finish-loop behavior requirements
- Keep an explicit
finish = {"status": "accept"} object lifecycle.
- On each iteration: POST
/finish, inspect request_type, dispatch handler.
- Persist handler outcome into
finish.status; do not blindly reset to accept.
- Handle unknown request types defensively and observably.
Payload and route design
- Decode inputs consistently from hex to bytes/string/json.
- Validate schema before state mutations.
- Keep inspect route vocabulary stable (
health, stats, items/<id>, etc.).
- Return JSON reports with predictable top-level keys.
Error-handling policy
- Distinguish malformed input from domain-rule rejection.
- Emit reports for recoverable diagnostic context.
- Keep reject paths deterministic and explicit.
- Ensure exception handling does not hide status transitions.
Testing policy (Python)
Prefer pytest. Minimum suite:
- codec roundtrip tests (hex/json);
- advance happy path and reject path;
- inspect route behavior and response shape;
- finish-loop status propagation under handler success/failure.
Include fixture payloads that mirror real sender formats.
If asset-handling is in scope, add:
- deposit payload decoding for each supported asset type (packed prefix byte offsets);
- voucher encoding round-trip (decode the emitted calldata back to the expected function and args);
- insufficient-balance and unknown-operation reject paths.
Performance and reliability guidance
- Keep inspect read-only and lightweight.
- Avoid unbounded in-memory growth for history views.
- Use explicit pagination/slicing for inspect responses if collections grow.
- Ensure log lines are structured enough for troubleshooting.
Completion contract
When finishing Python backend tasks, return:
- Whether template structure was kept as-is or changed on request;
- Payload schema + inspect route catalog;
- Test commands and outcomes;
- Any assumptions on runtime/env vars.
Asset handling
Primary reference: Asset handling.
Cartesi exposes Portal contracts on L1 that move assets into the application. Each deposit arrives as an advance_state input whose metadata["msg_sender"] equals the Portal contract address. Withdrawals run the other direction: only the application can initiate them by emitting a voucher; the voucher is executed on L1 once its epoch settles.
Resolve Portal addresses for your environment with:
cartesi address-book
Pin them as constants in dapp.py and dispatch on data["metadata"]["msg_sender"] (case-insensitive).
Deposit payload encoding
Portals deliver deposit payloads as packed ABI-encoded fields, optionally followed by a standard ABI-encoded tail for variable-length data such as baseLayerData and execLayerData.
| Asset | Packed fields (byte offsets) | Standard-ABI tail |
|---|
| Ether | address sender [0..20), uint256 value [20..52) | bytes execLayerData |
| ERC-20 | address token [0..20), address sender [20..40), uint256 amount [40..72) | bytes execLayerData |
| ERC-721 | address token [0..20), address sender [20..40), uint256 tokenId [40..72) | bytes baseLayerData, bytes execLayerData |
| ERC-1155 single | address token [0..20), address sender [20..40), uint256 tokenId [40..72), uint256 value [72..104) | bytes baseLayerData, bytes execLayerData |
| ERC-1155 batch | address token [0..20), address sender [20..40) | uint256[] tokenIds, uint256[] values, bytes baseLayerData, bytes execLayerData |
Decoding a deposit
The advance handler receives the deposit payload as a hex string. Decode the packed prefix with raw byte slicing; use eth_abi.decode for the standard-ABI tail when it contains variable-length data.
Example for ERC-20 (token(20) + sender(20) + amount(32) = 72 bytes, then optional execLayerData):
def decode_erc20_deposit(payload_hex: str) -> dict:
data = bytes.fromhex(
payload_hex[2:] if payload_hex.startswith("0x") else payload_hex
)
if len(data) < 72:
raise ValueError("invalid ERC-20 deposit payload")
token = "0x" + data[0:20].hex()
sender = "0x" + data[20:40].hex()
amount = int.from_bytes(data[40:72], "big")
exec_layer_data = data[72:]
return {"token": token, "sender": sender, "amount": amount, "exec_layer_data": exec_layer_data}
Apply the same pattern for Ether (sender[0:20], value[20:52]) and ERC-721 (token[0:20], sender[20:40], tokenId[40:72]). For ERC-1155 batch the tail carries arrays; decode them with eth_abi.decode.
Withdrawing ERC-20 tokens
Encode the transfer(address,uint256) calldata manually using the 4-byte selector and eth_abi, then emit a voucher with value set to the zero hash (no Ether attached):
import eth_abi
ERC20_TRANSFER_SELECTOR = bytes.fromhex("a9059cbb")
def encode_erc20_withdraw(token: str, recipient: str, amount: int) -> dict:
args = eth_abi.encode(["address", "uint256"], [recipient, amount])
payload = "0x" + ERC20_TRANSFER_SELECTOR.hex() + args.hex()
return {
"destination": token,
"payload": payload,
"value": "0x" + "00" * 32,
}
The application contract is the msg.sender during voucher execution, so the token contract sees transfer as coming from the app's own balance.
Withdrawing Ether
The Application contract executes vouchers via a safeCall to destination, forwarding value (wei) and payload (calldata). For a plain Ether transfer leave payload as "0x"; to call a payable function on the receiver, set payload to the encoded calldata.
def encode_ether_withdraw(recipient: str, amount: int) -> dict:
return {
"destination": recipient,
"payload": "0x",
"value": hex(amount)[2:].zfill(64),
}
Emitting a voucher
import requests, os
rollup_server = os.environ["ROLLUP_HTTP_SERVER_URL"]
def emit_voucher(voucher: dict):
requests.post(
rollup_server + "/voucher",
json=voucher,
headers={"Content-Type": "application/json"},
)
Voucher destinations and function signatures
| Asset | Destination | Function signature |
|---|
| Ether | dApp contract | withdrawEther(address,uint256) |
| ERC-20 | Token contract | transfer(address,uint256) / transferFrom(address,address,uint256) |
| ERC-721 | Token contract | safeTransferFrom(address,address,uint256) / safeTransferFrom(address,address,uint256,bytes) |
| ERC-1155 single | Token contract | safeTransferFrom(address,address,uint256,uint256,data) |
| ERC-1155 batch | Token contract | safeBatchTransferFrom(address,address,uint256[],uint256[],data) |
For withdrawals that need the application's own address as a from/sender argument (for example safeTransferFrom), read it from data["metadata"]["app_contract"] inside the handler.
Epoch length and voucher execution
Cartesi nodes close one epoch every 7200 blocks by default. Vouchers can only be executed after the epoch they were emitted in has settled, so withdrawals do not appear on L1 immediately. For dev/test workflows you can manually shorten the epoch length.
Determinism and accounting rules
- Keep per-account state in plain dicts or typed dataclasses; avoid external databases or file I/O.
- Normalize addresses to lowercase before using them as dict keys.
- Validate amounts (no negative deltas, no underflow) before mutating state.
- Emit a notice on deposit and on transfer; emit a voucher only on withdrawal.
- Decrease balance before emitting the withdrawal voucher so replay produces identical state and outputs.
- Vouchers MUST be deterministic functions of input and current state (no time, randomness, or external calls).
- Inspect handlers MUST NOT mutate balances; they only read and emit reports.
eth_abi wheel availability
eth_abi is a pure-Python package and ships as a wheel in most RISC-V wheel indexes. Confirm it is present in riscv-wheel-index/ before adding it to requirements.txt. If it is missing, follow the manual wheel build workflow in this skill.
Skill boundaries
For deployment and node bring-up tasks, load cartesi-deploy in addition to this skill.