Skip to content

feat(cli): Add support to rollups-node alpha.14 and PRT sentries [Setup + Local runs] - #569

Closed
brunomenezes wants to merge 17 commits into
prerelease/v2-alphafrom
feat/rollups-node-changes
Closed

brunomenezes wants to merge 17 commits into
prerelease/v2-alphafrom
feat/rollups-node-changes

Conversation

@brunomenezes

@brunomenezes brunomenezes commented Oct 20, 2026 •

Copy link
Copy Markdown
Member

Summary

Adapts the CLI to rollups-node 2.0.0-alpha.14 (cartesi/rollups-node#809 → cartesi/rollups-node#815) and Dave 3.0.0-alpha.6, fixes cartesi run bugs found on the way, and lets cartesi run --prt appoint and run PRT sentries from cartesi.toml.

The branch is based on prerelease/v2-alpha with the alpha.14 node released: SDK 0.12.0-alpha.45 (#562) carries it, and the last two commits make it the CLI's default SDK and bump the explorer to 2.0.0-alpha.6, which supports the alpha.14 node.

Sentry configuration Design (why)

Tip

✅ Confirmed by Guilherme from the Dave team: the paragraph below is his description of how sentries take part in claim acceptance.

How sentries fit in the stack: under PRT, each epoch is decided by a tournament. Once it has a winner, the result is staged on the application's DaveConsensus and can be accepted only after the claim staging period, which gives the guardian time to foreclose if the result is wrong. Sentries are addresses set at deployment on the DaveConsensus (the sentry manager can rotate them). Each one runs the application's machine over the same inputs and submits the post-epoch state hash it computed. If every sentry submitted the staged hash, the result can be accepted without waiting out the staging period. If any sentry is missing or disagrees, or there are no sentries, the full period applies. Sentries cannot change which result is staged or block it. But when they all agree, the guardian's window is skipped, so the fast path is only as trustworthy as the sentries are independent of whoever won the tournament.

The list comes from the contract, not the CLI. deploy application --prt takes --sentries a,b,c, and the DaveConsensus keeps a set of sentries. A claim is accepted before the staging period ends only when all of them agree. [[prt.sentries]] is that list in cartesi.toml. The minimal config is one entry with only type = "sling".

Locally, two sling sentries on the same snapshot replay the same deterministic machine and compute the same claim. A second one adds no extra evidence that the claim is right. More than one is still useful:

  1. It matches a real deployment. There, sentries are independent parties: different operators, keys and machines, and later different implementations (the rollups-node as a sentry, the Lua one). The config describes who the sentries are and which of them the CLI runs locally. An entry with only an address is appointed but run by someone else, as in a rehearsal of a real topology on a fork.
  2. Each entry carries its own data: where its key comes from (the pool, a key file or an external address), its label and its type. A plain count couldn't express "one I run with my own key, plus another operator's address".
  3. It lets you see what "all must agree" means before deploying. With two sentries the CLI runs, stopping one (docker compose -p <project> stop sentry-2) makes acceptance fall back to the staging period. Adding an address-only entry that nobody runs has the same effect, which is the case of another operator being slow or offline.
[[prt.sentries]] keys
Key What it does Rules
type The CLI runs a node for this sentry. Only "sling" exists today: a cartesi-sling-node container. Without type the sentry is only appointed, and someone else runs it. Any other value is an error listing the supported types.
address The sentry's address, appointed on the app's consensus. Required without type. Not the zero address. Unique across entries.
private_key_file A host file with the sentry's hex private key. The sentry signs with it, and without address the address comes from the key. Reaches the container as an init-service copy. Needs type and can't be empty. Format, and the match with address, are checked before any container starts.
label Names the container sentry-<label>, as shown in the run output and cartesi logs. Lowercase letters, digits, - and _. Unique. Default: the 1-based position, which is also the sentry's ID on the consensus.

Each sentry's address comes from the first of these that is set:

  1. address.
  2. The key in private_key_file.
  3. The CLI's pool: the next unused devnet account from 20 to 29, which cartesi run funds. For example, three type = "sling" entries with nothing else set get accounts 20, 21 and 22.

Sentries are appointed in the order of the entries, so the first one is sentry 1.

[prt] also takes an optional sentry_manager, the address allowed to replace the sentries later. Without --prt, cartesi run ignores the whole [prt] section.

Environment of a sling sentry container
Variable Value Source
CARTESI_SLING_APP_ADDRESS the deployed app the deploy's result, so sentries start after it
CARTESI_SLING_BLOCKCHAIN_HTTP_ENDPOINT http://anvil:8545 the project's anvil, same as the node
CARTESI_SLING_BLOCKCHAIN_ID 31337 viem's anvil.id, also right in fork mode (anvil runs with --chain-id 31337)
CARTESI_SLING_DATA_DIR /var/lib/cartesi-rollups-node/snapshots inside the container, no volume, so a recreated container starts clean
CARTESI_SLING_POLLING_INTERVAL anvil's block time, at least 1 cartesi run --block-time
CARTESI_SLING_TEMPLATE_PATH /var/lib/cartesi-rollups-node/template/image ./.cartesi mounted read-only, the machine the node deployed
CARTESI_SLING_AUTH_PRIVATE_KEY_FILE /run/secrets/sentry-<label> with private_key_file: the init service's copy
CARTESI_SLING_AUTH_PRIVATE_KEY the devnet account's key otherwise, for a pool or explicit devnet account. The key is public.

Changes

Node alpha.14

  • app remove refuses without --allow-abandonment, so every shell rebuild failed at undeploy. The CLI now passes the flag.
  • The node no longer starts with --default-block pending. The CLI drops the choice.
  • The machine requirement moves to cartesi-machine 0.22 (machines stored by 0.21 can't be loaded).
  • The PRT contracts move to Dave alpha.6 through @cartesi/wagmi-plugin 1.0.0-alpha.9.
  • Two integration cases skipped on alpha.13 (withdrawal from nvram accounts drives, an account with no balance next to others) now run.
  • The default SDK moves to 0.12.0-alpha.45, the first with the alpha.14 node, and the explorer service to 2.0.0-alpha.6.

cartesi run fixes

  • A shell redeploy on the first deploy's consensus failed with CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS: variable not defined, since the node was never given the ApplicationFactory.
  • Docker Compose read $name in forwarded CARTESI_* values and in --fork-url as variables, so a password or API key in a URL arrived changed.
  • CARTESI_*_FILE key files were forwarded as paths only, so the node couldn't open them. They now reach the container as copies owned by the node's user, installed by a root init service, which also works on native Linux where a bind mount keeps the host owner.
  • An unsupported CARTESI_BLOCKCHAIN_HTTP_ENDPOINT failed only once the node container started, with the value hidden. It is now rejected before anything starts, with the same rules as the node and without echoing the value.
  • Under --prt, the shell's redeploy no longer passes the previous application's consensus. The node ignored it, since a PRT deploy always creates its own DaveConsensus for the new machine, but the command read as if the consensus were reused.

Signer separation

  • cartesi send and cartesi deposit defaulted to devnet account 0, the account the node signs its claims with, so user inputs shared the claimer's nonces. They now default to account 8, and --from still picks another.

PRT sentries

  • deploy application --prt has taken --sentries and --sentry-manager since alpha.13, but the CLI couldn't set them, and nothing ran a sentry.
  • A [prt] section in cartesi.toml names an optional sentry_manager and [[prt.sentries]] entries, appointed on the DaveConsensus in that order.
  • An entry with type = "sling" is run by the CLI as a cartesi-sling-node container, sentry-<label>, on the same machine snapshot the application was deployed with.
  • Its account comes from private_key_file (your key, you fund it) or, with neither address nor key file, from the CLI's sentry pool, devnet accounts 20 to 29, funded with 10,000 ETH by cartesi run.
  • An entry with only an address is appointed and run elsewhere.
  • With the sentries agreeing on a claim, the node accepts it without waiting out --claim-staging-period.
  • Without --prt the section is ignored.
  • cartesi logs, and the shell's View logs, show the sentries next to the node, and cartesi logs sentry-green shows one.

Order under --prt: parse [prt] and resolve each sentry → start the environment → fund the pool sentries → deploy with --sentries/--sentry-manager → start sentry-<label> containers → on a shell rebuild, stop the sentries → undeploy (--allow-abandonment) → redeploy on a new DaveConsensus → start the sentries again.

PRT sentries config example
[prt]
sentry_manager = "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"

[[prt.sentries]]
type = "sling"                            # devnet account 20, funded by the CLI, container sentry-1

[[prt.sentries]]
type = "sling"
label = "mine"
private_key_file = "secrets/sentry.key"   # your key: the address comes from it, you fund it

[[prt.sentries]]
address = "0x…"                           # appointed only, run elsewhere

Checks before anything starts

  • A private_key_file must hold a hex private key on its first line, match address when both are given, and not be a devnet account's key.
  • A running sentry can't be the node's claimer (account 0) or PRT signer (account 6), and gets a warning on account 8 or the guardian.
  • Duplicate addresses and labels are rejected.
  • The key is never printed and reaches the container as a copy under /run/secrets, never as an environment variable.
Detailed changes
  • commands/run.ts: --allow-abandonment on undeploy, and pending removed from --default-block. [prt] resolution, sentry funding before the deploy, and sentry containers after it and across rebuilds. Once the sentries run, it prints the manager and each sentry with its account and origin, aligned with the deploy's check marks.
  • compose/node.ts: CARTESI_CONTRACTS_APPLICATION_FACTORY_ADDRESS in the node's defaults (a host value still overrides it). $ escaped in forwarded values. A node-secret-init service and a node_secrets volume when a CARTESI_*_FILE variable is set, with the env pointing at /run/secrets/<name>.
  • compose/secrets.ts (new): fileSecret and secretInitService, the root init service that installs cartesi:cartesi 0400 copies. Shared by the node and the sentries.
  • compose/sentry.ts (new): the sentry-<label> service on cartesi/rollups-runtime, running cartesi-sling-node pk against the project's anvil and polling every block. The key comes through env for devnet accounts, or as a secret copy for key files.
  • commands/logs.ts: cartesi logs [service...] over docker compose logs. It shows the node and every sentry by default, or the services named, with a prefix per service when more than one is shown.
  • compose/anvil.ts, compose/common.ts: escapeComposeValue, applied to --fork-url.
  • validations.ts: assertBlockchainEndpoint (mirrors Go's url.Parse plus the node's http(s)-with-host rule, checked against the alpha.14 binary) and assertSecretFiles.
  • config/toml/prt.ts (new), config/types.ts, config/errors.ts, config/defaults.ts, config.ts: the [prt] table (sentry_manager, [[prt.sentries]] with address, type, label, private_key_file), unknown keys rejected like the other tables.
  • sentries.ts (new): the sentry pool, resolveSentries (addresses, key files, reserved accounts, duplicates, warnings), checkSentries (funding and balances), describeSentry.
  • exec/rollups.ts: --sentries and --sentry-manager on deploy, getDaveConsensus (contract consensus --json), composeEnvironment factored out of startEnvironment, and startSentries/stopSentries.
  • wallet.ts: getDefaultSender (account 8) and getDevnetAccount (address and key of mnemonic accounts 0–29).
  • commands/send.ts, commands/deposit*.ts: default sender through getDefaultSender, and the --from help names devnet account 8.
  • exec/cartesi-machine.ts: require cartesi-machine 0.22.
  • package.json: @cartesi/wagmi-plugin 1.0.0-alpha.9 (Dave alpha.6 PRT contracts).
  • config/constants.ts: DEFAULT_SDK_VERSION 0.12.0-alpha.45. exec/rollups.ts: the explorer image 2.0.0-alpha.6.
  • Tests: Unit tests for every module above. Integration tests: undeploy.test.ts (undeploy and redeploy), run.test.ts (dry runs: default block, $, key files, endpoint, [prt] errors), send.test.ts (default sender), prt.test.ts (sentries appointed, sling nodes running on funded accounts, a claim accepted far inside a 1,000,000-block staging period, a claim that waits while one sentry is stopped and is accepted once it is back, cartesi logs showing them), a key-file case in node-startup.test.ts (the node reads 0600 key files, their copies are cartesi:cartesi 0400, the host files are untouched), and the two previously skipped withdrawal cases.
  • Changesets: Minor for the machine requirement, the endpoint check, the default sender, sentry appointment, sling sentries, the sentries in cartesi logs and the default SDK. Patch for the redeploy fix, --default-block, $ escaping, key files, the contracts bump and the explorer bump.

@github-actions

github-actions Bot commented Oct 20, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 88.56% (🎯 0%) 13728 / 15501
🔵 Statements 88.56% 13728 / 15501
🔵 Functions 82.27% 348 / 423
🔵 Branches 0% 0 / 0
📁 File Coverage (20 files)
File Lines Statements Functions Branches Uncovered Lines
apps/cli/src/base.ts 🔴 38.6% 🔴 38.6% 🟡 64.71% 🔴 0% 62, 70-74, 79, 101-174, 200...
apps/cli/src/builder/directory.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/docker.ts 🟢 87.12% 🟢 87.12% 🟡 66.67% 🔴 0% 77-79, 81, 114-116, 174-183
apps/cli/src/builder/empty.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/none.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/builder/nvram.ts 🟢 97.14% 🟢 97.14% 🟢 100% 🔴 0% 38
apps/cli/src/builder/tar.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/commands/foreclose.ts 🔴 49.09% 🔴 49.09% 🔴 50% 🔴 0% 69, 91-145
apps/cli/src/commands/logs.ts 🟡 66.67% 🟡 66.67% 🟡 66.67% 🔴 0% 13-24, 106-124
apps/cli/src/commands/refund.ts 🔴 38.2% 🔴 38.2% 🟡 66.67% 🔴 0% 55-109
apps/cli/src/commands/run.ts 🔴 17.47% 🔴 17.47% 🔴 8.33% 🔴 0% 69-88, 92-259, 263-270, 277...
apps/cli/src/commands/withdraw.ts 🔴 46.58% 🔴 46.58% 🟡 73.33% 🔴 0% 95-123, 277-278, 301-356, 3...
apps/cli/src/compose/anvil.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/builder.ts 🟢 99.79% 🟢 99.79% 🟢 100% 🔴 0% 228
apps/cli/src/compose/bundler.ts 🔴 4.82% 🔴 4.82% 🔴 0% 🔴 0% 8-40, 44-75, 79-92
apps/cli/src/compose/common.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/database.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/explorer.ts 🔴 6.67% 🔴 6.67% 🔴 0% 🔴 0% 10-38, 43-55, 59-72
apps/cli/src/compose/node.ts 🟢 100% 🟢 100% 🟢 100% 🔴 0% -
apps/cli/src/compose/passkey.ts 🔴 8.33% 🔴 8.33% 🔴 0% 🔴 0% 9-18, 23-42, 46-59

@brunomenezes brunomenezes moved this to 🧑‍💻 In Progress in Rollups Tooling Oct 21, 2026
@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from 99e6d83 to 6ce1d09 Compare October 29, 2026 16:53
@changeset-bot

changeset-bot Bot commented Oct 29, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: db40096

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@cartesi/cli Minor

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from 6ce1d09 to ddc54c8 Compare November 3, 2026 15:42
@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from ddc54c8 to 4f3d67a Compare November 4, 2026 13:55
@socket-security

socket-security Bot commented Nov 4, 2026 •

Copy link
Copy Markdown

Review the following changes in direct dependencies. Learn more about Socket for GitHub.

Diff Package Supply Chain
Security
Vulnerability Quality Maintenance License
Updated@​cartesi/​wagmi-plugin@​1.0.0-alpha.7 ⏵ 1.0.0-alpha.973 +110098 +196 +1100

View full report

@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch 2 times, most recently from 2529ff4 to a78996a Compare November 5, 2026 15:26
@brunomenezes

brunomenezes commented Nov 6, 2026 •

Copy link
Copy Markdown
Member Author

Hey @endersonmaia could you have a look in the following commits, check if see any problems or improvements in the following commits
b474393, bfb4d91, 2902204. They touch the compose for secret copy and some fixes for escaping the $ when forward values to avoid corruption of these values when docker could mistakenly apply variable “interpolation”.

@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from c3a5992 to 2902204 Compare November 6, 2026 18:53
@endersonmaia

Copy link
Copy Markdown
Member

I see you're using new containers for the sentry, but have you tried scaling them instead of new instances?

So, instead of creating a new instance sentry-1, sentry-2 that will show sentry-1-1 and sentry-2-1,

Use the docker compose scale sentry=2, then we leverage what compose already provides to scale services instead of wiring it from the outside.


On another end, we could leverage nitro that's already inside cartesi/rollups-runtime image., and use it to start new instances for sentries, using templates like /etc/nitro/cartesi-sling-node@

This way we wouldn't need to spin up a new container.

This would consume less resources BUT may be more complicated since there's already a Service abstraction that controls the containers.


The test I did with sentries works, with 2 sentries and sending a couple of inputs and I can see logs and stuff.

The suggestions above are just suggestion, nothing blocker.

@endersonmaia

Copy link
Copy Markdown
Member

In see you used a bind mount to expose the secret content, that's what docker compose already does via its secrets:

Just like we already do with config:

Is there any reason why you didn't use the secrets:?

https://docs.docker.com/compose/how-tos/use-secrets/

@brunomenezes

Copy link
Copy Markdown
Member Author

I see you're using new containers for the sentry, but have you tried scaling them instead of new instances?

So, instead of creating a new instance sentry-1, sentry-2 that will show sentry-1-1 and sentry-2-1,

Use the docker compose scale sentry=2, then we leverage what compose already provides to scale services instead of wiring it from the outside.

I did not think on using "scaling" for that. However, the sentries aren't identical replicas. Each one has its own signer (a devnet account or a key file), data dir and label, and a later type may run a different binary (ref-node as sentry and Diego's Lua named Crisium) that is why the config is designed with type. Compose has no per-replica environment, so scale can't give replica 2 a different key. Distinct services keep each sentry's config, logs and cleanup separate, the -1 suffix is compose's replica numbering and I can set container_name to drop but this suffix is everywhere. Instead of the fallback sentry index you can use the optional label so it would be sentry-green-1 like the screenshot below.

Sentries with label Screenshot 2026-11-07 at 07 24 36

Are you thinking on using replicas when running sentries on your environment? Same signer and configs? On this branch I am separating devnet accounts to avoid the lately wide spread problems with nonces. I also defined a pool of high accounts just for sentries, I reserved accounts from index 20 to 29 (that is more than enough I assume, for testing using devnet)

On another end, we could leverage nitro that's already inside cartesi/rollups-runtime image., and use it to start new instances for sentries, using templates like /etc/nitro/cartesi-sling-node@

This way we wouldn't need to spin up a new container.

This would consume less resources BUT may be more complicated since there's already a Service abstraction that controls the containers.

I think you are spot on in the "more complicated" and not as much of again to be honest. But being a bit more deep on my answer; the @ template would save the rescan on start, but nitro only passes $1 to run and has no per-service environment, so the app address, endpoint and key would still have to be written into the running node container through docker exec, and the same for stop/wipe/restart across a shell rebuild and for reading logs back. The keys would also sit in the node's container, readable by every node process (I know devnet, mostly of the time). The template itself would live in the image's skel/etc/nitro, written for the CLI's conventions, so the CLI would only work with runtime versions that ship it. The runtime image should stay a neutral bundle of binaries that any orchestrator can use, with the orchestration knowledge in the CLI. As I see it the suggestion creates that coupling for little gain, one cgroup per sentry, given the lifecycle work above. compose gives all of it as up/down of a named service, so I kept them as services.

@brunomenezes

Copy link
Copy Markdown
Member Author

In see you used a bind mount to expose the secret content, that's what docker compose already does via its secrets:

Just like we already do with config:

Is there any reason why you didn't use the secrets:?

https://docs.docker.com/compose/how-tos/use-secrets/

There is a problem identified by the rollups-node team a couple of weeks/months ago. The compose file secrets: are plain bind mounts, and uid/gid/mode are ignored outside Swarm. On Docker Desktop that's invisible because ownership is remapped, but on Linux a 0600 key owned by the host user is unreadable to the node's uid 102, and cartesi/rollups-node#790 would reject a key the node doesn't own. So an init service copies each key into a volume as cartesi:cartesi 0400, which is also what cartesi/rollups-node#790 compose.yaml does. configs: is fine for the traefik file because it's world-readable, so ownership never mattered there.

node-startup.test.ts covers it. The node starts on 0600 key files owned by the test's user and must log the identity read from them, which a plain bind mount can't give on Linux CI, where that user isn't uid 102. I'll extend it to check the copies are cartesi:cartesi 0400 and the host files keep their owner and mode, so it also fails on macOS, where Docker Desktop remaps ownership, and guards the copy against regressions.

@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch 2 times, most recently from 813d17d to 5954e82 Compare November 11, 2026 10:59
@brunomenezes brunomenezes changed the title feat: changes for upcoming rollups-node alpha.14 feat(cli): Add support to rollups-node alpha.14 and PRT sentries [Setup + Local runs] Nov 11, 2026
@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from 5954e82 to db40096 Compare November 12, 2026 14:13
@brunomenezes
brunomenezes marked this pull request as ready for review November 12, 2026 16:26
@brunomenezes
brunomenezes requested a review from tuler November 12, 2026 16:26
@brunomenezes

Copy link
Copy Markdown
Member Author

Closing in favour of #577 that use that one as base branch.

@github-project-automation github-project-automation Bot moved this from Todo to Done in Rollups SDK Nov 12, 2026
@github-project-automation github-project-automation Bot moved this from 🧑‍💻 In Progress to 📦 Done in Rollups Tooling Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

Status: Done
Status: 📦 Done

Development

Successfully merging this pull request may close these issues.

2 participants