diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md index 226c3399..1d7327e4 100644 --- a/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/counter.md @@ -148,7 +148,7 @@ cartesi build ``` - Expected Logs: - + ```shell user@user-MacBook-Pro counter % cartesi build (node:4460) ExperimentalWarning: Importing JSON modules is an experimental feature and might change at any time @@ -183,7 +183,7 @@ Storing machine: please wait The build command compiles your application then builds a Cartesi machine that contains your application. -This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. wound next be started by running the command: +This recently built machine alongside other necessary service, like an Anvil network, inspect service, etc. would next be started by running the command: ```bash cartesi run @@ -197,10 +197,10 @@ user@user-MacBook-Pro counter % cartesi run (Use `node --trace-warnings ...` to show where the warning was created) WARNING: default block is set to 'latest', production configuration will likely use 'finalized' [+] Pulling 4/0 - ✔ database Skipped - Image is already present locally - ✔ rollups-node Skipped - Image is already present locally - ✔ anvil Skipped - Image is already present locally - ✔ proxy Skipped - Image is already present locally + ✔ database Skipped - Image is already present locally + ✔ rollups-node Skipped - Image is already present locally + ✔ anvil Skipped - Image is already present locally + ✔ proxy Skipped - Image is already present locally ✔ counter 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 @@ -221,7 +221,7 @@ We start by querying the current count value, this is done by making an inspect ```bash curl -X POST http://127.0.0.1:6751/inspect/counter \ -H "Content-Type: application/json" \ - -d '{""}' + -d '{""}' ``` :::note Inspect endpoint diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/running-applications-on-a-forked-network.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/running-applications-on-a-forked-network.md new file mode 100644 index 00000000..c942c6b7 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/running-applications-on-a-forked-network.md @@ -0,0 +1,316 @@ +--- +id: running-applications-on-a-forked-network +title: Running applications on a forked network +resources: +--- + +## 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: + +```bash +--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: + +```bash +--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: + +```bash +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: + +- Cartesi CLI: A simple tool for building applications on Cartesi. [Install Cartesi CLI for your OS of choice](../development/installation.md). + +- Docker Desktop 4.x: The tool you need to run the Cartesi Machine and its dependencies. [Install Docker for your OS of choice](https://www.docker.com/products/docker-desktop/). + +### Create an application template using the Cartesi CLI + +Use the Cartesi CLI to generate a starter application: + +import Tabs from '@theme/Tabs'; +import TabItem from '@theme/TabItem'; + + + +

+
+```shell
+cartesi create fork-seploia --template javascript
+```
+
+
+
+ + +

+
+```shell
+cartesi create fork-seploia --template python
+```
+
+
+
+ + +

+
+```shell
+cartesi create fork-seploia --template rust
+```
+
+
+
+
+ +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: + +```bash +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. + +import InputReaderSolidity from './snippets/inputRelayer-sol.md'; + + + +

+
+
+
+
+
+
+ +#### Deploy contract to Base Sepolia + +Deploy the contract to Base Sepolia using the following command: + +```bash +forge create contracts/InputRelayer.sol:InputRelayer --rpc-url --private-key --broadcast -v +``` + +This returns an output similar to: + +```bash +[⠊] 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. + +import ForkSepoliaRs from './snippets/fork-sepolia-rs.md'; +import ForkSepoliaJs from './snippets/fork-sepolia-js.md'; + + + +

+
+
+
+
+
+ + +

+
+
+
+
+
+
+ +## Build and run your application + +Once your application logic is in place, build the project by running: + +```shell +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: + +```bash +cartesi run --fork-url --fork-block-number 39988953 +``` + +### Expected logs + +```shell +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: + + + + +

+
+```bash
+cast send  "relayInput(address,address,bytes)"   --rpc-url  --private-key 
+```
+
+
+
+ + +

+
+```bash
+cast send 0x06eBAF6d44B65d76C8BDcB1701E68f44C22B1057 "relayInput(address,address,bytes)" 0xa831a9883abc2ae26c643b3cab57498b8c6fcb52 0x1b51e2992A2755Ba4D6F7094032DF91991a0Cfac "0xd8da6bf26964af9d7eed9e03e53415d37aa9604548656c6c6f2066726f6d204361727465736921"  --rpc-url http://127.0.0.1:6751/anvil --private-key 0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80
+```
+
+
+
+ +
+ +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 + +```bash +[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. diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-js.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-js.md new file mode 100644 index 00000000..c3caa019 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-js.md @@ -0,0 +1,121 @@ +```javascript +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"]); + } + } +})(); +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-rs.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-rs.md new file mode 100644 index 00000000..e5447635 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/fork-sepolia-rs.md @@ -0,0 +1,173 @@ +```rust +use json::{object, JsonValue}; +use std::env; +use std::sync::Mutex; + +#[derive(Clone)] +struct AdvanceMessage { + relayer: String, + original_sender: String, + message: String, + input_index: u64, +} + +static MESSAGES: Mutex> = Mutex::new(Vec::new()); + +fn hex_decode(hex: &str) -> Result, Box> { + let hex = hex.strip_prefix("0x").unwrap_or(hex); + if hex.len() % 2 != 0 { + return Err("hex string has odd length".into()); + } + (0..hex.len()) + .step_by(2) + .map(|i| u8::from_str_radix(&hex[i..i + 2], 16).map_err(|e| Box::new(e) as _)) + .collect() +} + +fn hex_encode(bytes: &[u8]) -> String { + bytes.iter().map(|b| format!("{:02x}", b)).collect() +} + +pub async fn handle_advance( + _client: &hyper::Client, + _server_addr: &str, + request: JsonValue, +) -> Result<&'static str, Box> { + println!("Received advance request data {}", &request); + + let payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + let relayer = request["data"]["metadata"]["msg_sender"] + .as_str() + .unwrap_or("unknown") + .to_string(); + + let input_index = request["data"]["metadata"]["input_index"] + .as_u64() + .unwrap_or(0); + + // Decode payload: first 20 bytes = original sender address (abi.encodePacked), + // remaining bytes = UTF-8 message. + let bytes = hex_decode(payload)?; + + if bytes.len() < 20 { + eprintln!("Payload too short to contain a 20-byte sender address, rejecting"); + return Ok("reject"); + } + + let original_sender = format!("0x{}", hex_encode(&bytes[0..20])); + let message = std::str::from_utf8(&bytes[20..]) + .unwrap_or("") + .to_string(); + + println!( + "[InputRelayer] input_index={} relayer={} original_sender={} message=\"{}\"", + input_index, relayer, original_sender, message + ); + + MESSAGES.lock().unwrap().push(AdvanceMessage { + relayer, + original_sender, + message, + input_index, + }); + + Ok("accept") +} + +pub async fn handle_inspect( + client: &hyper::Client, + server_addr: &str, + request: JsonValue, +) -> Result<&'static str, Box> { + println!("Received inspect request data {}", &request); + + let payload = request["data"]["payload"] + .as_str() + .ok_or("Missing payload")?; + + let route_bytes = hex_decode(payload)?; + let route = std::str::from_utf8(&route_bytes) + .unwrap_or("") + .trim_start_matches('/'); + + println!("Inspect route: \"{}\"", route); + + let report_body = match route { + "messages" | "" => { + let messages = MESSAGES.lock().unwrap(); + let mut arr = JsonValue::new_array(); + for msg in messages.iter() { + let _ = arr.push(object! { + "input_index" => msg.input_index, + "relayer" => msg.relayer.clone(), + "original_sender" => msg.original_sender.clone(), + "message" => msg.message.clone(), + }); + } + arr.dump() + } + "messages/count" => { + let count = MESSAGES.lock().unwrap().len(); + format!("{{\"count\":{}}}", count) + } + _ => { + format!("{{\"error\":\"unknown route\",\"route\":\"{}\"}}", route) + } + }; + + let hex_payload = format!("0x{}", hex_encode(report_body.as_bytes())); + let report = object! { "payload" => hex_payload }; + let req = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/report", server_addr)) + .body(hyper::Body::from(report.dump()))?; + let resp = client.request(req).await?; + println!("Report response status: {}", resp.status()); + + Ok("accept") +} + +#[tokio::main] +async fn main() -> Result<(), Box> { + let client = hyper::Client::new(); + let server_addr = env::var("ROLLUP_HTTP_SERVER_URL")?; + + let mut status = "accept"; + loop { + println!("Sending finish"); + let response = object! {"status" => status}; + let request = hyper::Request::builder() + .method(hyper::Method::POST) + .header(hyper::header::CONTENT_TYPE, "application/json") + .uri(format!("{}/finish", &server_addr)) + .body(hyper::Body::from(response.dump()))?; + let response = client.request(request).await?; + println!("Received finish status {}", response.status()); + + if response.status() == hyper::StatusCode::ACCEPTED { + println!("No pending rollup request, trying again"); + } else { + let body = hyper::body::to_bytes(response).await?; + let utf = std::str::from_utf8(&body)?; + let req = json::parse(utf)?; + + let request_type = req["request_type"] + .as_str() + .ok_or("request_type is not a string")?; + status = match request_type { + "advance_state" => handle_advance(&client, &server_addr[..], req).await?, + "inspect_state" => handle_inspect(&client, &server_addr[..], req).await?, + &_ => { + eprintln!("Unknown request type"); + "reject" + } + }; + } + } +} + +``` diff --git a/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/inputRelayer-sol.md b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/inputRelayer-sol.md new file mode 100644 index 00000000..4f56fa14 --- /dev/null +++ b/cartesi-rollups_versioned_docs/version-2.0/tutorials/snippets/inputRelayer-sol.md @@ -0,0 +1,67 @@ +```rust +// 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; + } +} +``` diff --git a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json index 99facf74..a01c2d92 100644 --- a/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json +++ b/cartesi-rollups_versioned_sidebars/version-2.0-sidebars.json @@ -183,7 +183,8 @@ "tutorials/erc-721-token-wallet", "tutorials/react-frontend-application", "tutorials/cli-account-abstraction-feauture", - "tutorials/utilizing-the-cli-test-tokens" + "tutorials/utilizing-the-cli-test-tokens", + "tutorials/running-applications-on-a-forked-network" ] }, {