AI agent documentation index: llms.txt. Raw markdown for any page is available by appending .md to the URL. Full content snapshot: llms-full.txt.
llms.txt — complete Cartesi documentation index. Append .md to any page URL for raw Markdown (e.g. /cartesi-rollups/2.0/development/building-an-application.md).
Skip to main content

Running applications on a forked network

Introduction

When building Cartesi applications, it is often necessary to interact with contracts and services that are already deployed on a live blockchain network. While a local clean chain is useful during early development, it does not always reflect real world conditions.

To address this, the Cartesi CLI introduces support for running applications on a forked network. This feature allows you to replicate the state of an existing blockchain locally and run your application against it.

This is enabled through two flags available in the cartesi run command:

  • --fork-url
  • --fork-block-number

Using these flags, the CLI forks a network from a given RPC endpoint at a specific block height, load that state into your local Anvil environment, then runs your Cartesi application on top of it.

This approach makes it possible to develop and test locally while interacting with real on chain data from networks such as testnets or mainnets.

Understanding the fork options

--fork-url

This flag specifies the RPC endpoint that will serve as the source of truth for the forked network.

Example:

--fork-url https://sample-rpc.sepolia.com

The CLI uses this endpoint to fetch blockchain state and replicate it locally.

--fork-block-number

This flag defines the exact block height used as the snapshot point for the fork.

Example:

--fork-block-number 32768

Using a fixed block number ensures deterministic behavior, which is especially useful when debugging or running repeatable tests.

Basic usage

To run your application on a forked network, execute:

cartesi run --fork-url https://sample-rpc.sepolia.com --fork-block-number 32768

When this command is executed, the Cartesi CLI performs the following steps:

  1. Connects to the specified RPC endpoint
  2. Fetches blockchain state at the given block number
  3. Initializes a local network using the forked state
  4. Deploys required Cartesi infrastructure contracts, including portals, InputBox, and test tokens
  5. Starts the Cartesi node stack, allowing your application to run in this environment

Why use a forked network

Running your application on a forked network provides several advantages:

  • Enables interaction with contracts that already exist on the source network
  • Allows reproduction of issues using a known blockchain state
  • Improves development speed by enabling local iteration with realistic data
  • Helps validate integration logic before deploying to public test networks

Best practices

  • Prefer stable and reliable HTTPS RPC endpoints
  • Use a fixed block number during debugging to ensure consistent results
  • Update the block number when you need access to more recent state
  • Validate that the RPC endpoint is reachable if the command fails to start

Deploying and interacting with a sample application

In this section, you will build a simple end to end example that demonstrates how a Cartesi application can interact with a contract that already exists on a forked network.

The workflow follows a practical sequence:

  • set up your development environment
  • create a new Cartesi application
  • deploy a contract to Sepolia
  • run your application on a forked network
  • interact with the deployed contract through the fork

Set up your environment

Before getting started, ensure the following tools are installed:

Create an application template using the Cartesi CLI

Use the Cartesi CLI to generate a starter application:

cartesi create fork-seploia --template javascript

This command creates a new directory named fork-seploia. Depending on the selected language, the directory includes a basic project structure and an entry point for your application logic.

Create and deploy a Solidity contract to Sepolia

Next, you will create and deploy a Solidity contract named InputRelayer to Sepolia.

This contract acts as a simple relay layer. It receives a user request and forwards it to the Cartesi InputBox contract. Your Cartesi application will later receive this input, decode it, and process the message.

In a real world scenario, this contract could represent an existing protocol such as a liquidity pool manager, a swap router, or any deployed smart contract that your application integrates with.

Create and implement contract logic

Inside the fork-seploia directory, create a contracts folder and add the InputRelayer.sol file:

mkdir contracts
touch contracts/InputRelayer.sol

Then copy the Solidity implementation into the file. This contract accepts a destination, an InputBox address, and user input. It encodes the request and forwards it to the specified InputBox.

// SPDX-License-Identifier: MIT
pragma solidity ^0.8.0;

interface IInputBox {
function addInput(address appContract, bytes calldata payload) external returns (bytes32);
}

contract InputRelayer {
struct RelayRecord {
address destination;
address inputBox;
bytes inputBody;
bytes32 inputHash;
uint256 timestamp;
}

RelayRecord[] private _records;

function relayInput(
address destination,
address input_box,
bytes calldata input_body
) external returns (bytes32) {
bytes32 inputHash = IInputBox(input_box).addInput(destination, input_body);

_records.push(RelayRecord({
destination: destination,
inputBox: input_box,
inputBody: input_body,
inputHash: inputHash,
timestamp: block.timestamp
}));

return inputHash;
}

function getRecordCount() external view returns (uint256) {
return _records.length;
}

function getRecord(uint256 index) external view returns (RelayRecord memory) {
require(index < _records.length, "InputRelayer: index out of bounds");
return _records[index];
}

function getAllRecords() external view returns (RelayRecord[] memory) {
return _records;
}

function getRecordsByDestination(address destination) external view returns (RelayRecord[] memory) {
uint256 count = 0;
for (uint256 i = 0; i < _records.length; i++) {
if (_records[i].destination == destination) count++;
}

RelayRecord[] memory result = new RelayRecord[](count);
uint256 j = 0;
for (uint256 i = 0; i < _records.length; i++) {
if (_records[i].destination == destination) {
result[j++] = _records[i];
}
}
return result;
}
}

Deploy contract to Base Sepolia

Deploy the contract to Base Sepolia using the following command:

forge create contracts/InputRelayer.sol:InputRelayer --rpc-url <RPC-Url> --private-key <Deployment Private Key with Eth token> --broadcast -v

This returns an output similar to:

[⠊] Compiling...
No files changed, compilation skipped
Deployer: 0xbD8Eba8Bf9e56ad92F4C4Fc89D6CB88902535749
Deployed to: 0x06eBAF6d44B65d76C8BDcB1701E68f44C22B1057
Transaction hash: 0xa5c073210568d1d8b11b2ff6bed0d7bd0d8058cfcf0ec561f51b5de9c8e21644

After deployment, inspect the transaction using a block explorer. Take note of the block number in which the transaction was included, as this will be used later when configuring the fork.

Implementing the Cartesi application logic

Now implement the logic for your Cartesi application.

This application will:

  • receive inputs from the onchain relayer
  • decode the payload
  • extract the original sender and message
  • store the data in memory
  • log a structured summary of the received input

To set this up, replace the contents of the src/ directory inside fork-seploia with the appropriate snippet for your chosen language.

const rollup_server = process.env.ROLLUP_HTTP_SERVER_URL;
console.log("HTTP rollup_server url is " + rollup_server);

// In-memory store of all advance messages received from the InputRelayer
const messages = [];

/**
* Decodes a hex string to a UTF-8 string.
* Accepts with or without "0x" prefix.
*/
function hexToUtf8(hex) {
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
const bytes = Buffer.from(clean, "hex");
return bytes.toString("utf8");
}

/**
* Decodes a hex string to a Buffer.
*/
function hexToBuffer(hex) {
const clean = hex.startsWith("0x") ? hex.slice(2) : hex;
return Buffer.from(clean, "hex");
}

/**
* Encodes a string to a "0x"-prefixed hex string.
*/
function utf8ToHex(str) {
return "0x" + Buffer.from(str, "utf8").toString("hex");
}

async function handle_advance(data) {
console.log("Received advance request data " + JSON.stringify(data));

const relayer = data.metadata.msg_sender;
const inputIndex = data.metadata.input_index;
const payload = data.payload;

// Decode payload: first 20 bytes = original sender address (abi.encodePacked),
// remaining bytes = UTF-8 message.
const bytes = hexToBuffer(payload);

if (bytes.length < 20) {
console.error(
"Payload too short to contain a 20-byte sender address, rejecting",
);
return "reject";
}

const originalSender = "0x" + bytes.slice(0, 20).toString("hex");
const message = bytes.slice(20).toString("utf8");

console.log(
`[InputRelayer] input_index=${inputIndex} relayer=${relayer} original_sender=${originalSender} message="${message}"`,
);

messages.push({
input_index: inputIndex,
relayer,
original_sender: originalSender,
message,
});

return "accept";
}

async function handle_inspect(data) {
console.log("Received inspect request data " + JSON.stringify(data));

const route = hexToUtf8(data.payload).replace(/^\//, "");
console.log(`Inspect route: "${route}"`);

let reportBody;
if (route === "messages" || route === "") {
reportBody = JSON.stringify(messages);
} else if (route === "messages/count") {
reportBody = JSON.stringify({ count: messages.length });
} else {
reportBody = JSON.stringify({ error: "unknown route", route });
}

const resp = await fetch(rollup_server + "/report", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ payload: utf8ToHex(reportBody) }),
});
console.log("Report response status: " + resp.status);

return "accept";
}

var handlers = {
advance_state: handle_advance,
inspect_state: handle_inspect,
};

var finish = { status: "accept" };

(async () => {
while (true) {
const finish_req = await fetch(rollup_server + "/finish", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify({ status: finish["status"] }),
});

console.log("Received finish status " + finish_req.status);

if (finish_req.status == 202) {
console.log("No pending rollup request, trying again");
} else {
const rollup_req = await finish_req.json();
var handler = handlers[rollup_req["request_type"]];
finish["status"] = await handler(rollup_req["data"]);
}
}
})();

Build and run your application

Once your application logic is in place, build the project by running:

cartesi build

This command compiles your application and produces a Cartesi Machine image that includes your code.

Next, run the application using a forked network. Provide the RPC endpoint for Base Sepolia and the block number where the InputRelayer contract was deployed:

cartesi run --fork-url <Valid RPC URL> --fork-block-number 39988953

Expected logs

WARNING: default block is set to 'latest', production configuration will likely use 'finalized'
✔ fork-sepolia-r starting at http://127.0.0.1:6751
✔ anvil service ready at http://127.0.0.1:6751/anvil
✔ rpc service ready at http://127.0.0.1:6751/rpc
✔ inspect service ready at http://127.0.0.1:6751/inspect/fork-sepolia-r
✔ fork-sepolia-r machine hash is 0xa5b369b0d19766005d12369d0fa588925093ffe24c44c64c76155734270b7ac7
✔ fork-sepolia-r contract deployed at 0xa831a9883abc2ae26c643b3cab57498b8c6fcb52
(l) View logs (b) Build and redeploy (q) Quit

At this point, your application is running on a local network that mirrors the state of Base Sepolia at the specified block.

Interacting with the Cartesi application through the forked contract

Because the network was forked at a block where the InputRelayer contract already exists, you can interact with your application through this contract as if you were on the original network.

Use the following command to send an input through the relayer:

cast send <InputRelayer address> "relayInput(address,address,bytes)" <application address> <input-box address> --rpc-url <application local rpc_url> --private-key <private key>

This command calls the relayInput(address,address,bytes) function on the relayer contract, passing:

  • the Cartesi application address
  • the InputBox address
  • the encoded user input

Ensure that you replace the application address and RPC URL with the values returned when you executed cartesi run.

Expected logs

[INFO  rollup_http_server::http_service] received new request of type ADVANCE
[INFO actix_web::middleware::logger] 127.0.0.1 "POST /finish HTTP/1.1" 200 434 "-" "-" 0.018560
Received finish status 200 OK
Received advance request data {"request_type":"advance_state","data":{"metadata":{"chain_id":31337,"app_contract":"0xa831a9883abc2ae26c643b3cab57498b8c6fcb52","msg_sender":"0x06ebaf6d44b65d76c8bdcb1701e68f44c22b1057","block_number":39989157,"block_timestamp":1775746604,"prev_randao":"0x50244e200a55ed7d32e48e6f0c32710fe89c537345036bf55a02608736d25cc2","input_index":1},"payload":"0xd8da6bf26964af9d7eed9e03e53415d37aa9604548656c6c6f2066726f6d204361727465736921"}}
[InputRelayer] input_index=1 relayer=0x06ebaf6d44b65d76c8bdcb1701e68f44c22b1057 original_sender=0xd8da6bf26964af9d7eed9e03e53415d37aa96045 message="Hello from Cartesi!"
Sending finish

Summary

In this section, you successfully:

  • deployed a contract to Sepolia
  • forked the network locally at a specific block
  • ran a Cartesi application on top of that fork
  • interacted with the application through an already deployed contract

This demonstrates how the Cartesi CLI can be used to replicate real blockchain environments locally, enabling integration and interaction with already deployed contracts during development and testing.

We use cookies to ensure that we give you the best experience on our website. By using the website, you agree to the use of cookies.