Skip to content

refactor(cli): PRT nodes in cartesi.toml, with a sling claimer - #577

Merged
brunomenezes merged 22 commits into
prerelease/v2-alphafrom
refactor/redesign-sentry-setup
Nov 12, 2026
Merged

brunomenezes merged 22 commits into
prerelease/v2-alphafrom
refactor/redesign-sentry-setup

Conversation

@brunomenezes

@brunomenezes brunomenezes commented Nov 12, 2026 •

Copy link
Copy Markdown
Member

Summary

Note

This PR supersedes #569, closed in its favour, and targets prerelease/v2-alpha directly. It carries that PR's 17 commits unchanged (rollups-node 2.0.0-alpha.14 through SDK 0.12.0-alpha.45, cartesi-machine 0.22, the cartesi run fixes, the signer separation and the first PRT sentries) plus the commits below, which replace its [[prt.sentries]] with [prt.nodes.<label>] before any of it is released. The discussion of the sentry design is in #569's comments and in tuler's review here.

Replaces [[prt.sentries]] with [prt.nodes.<label>], one table per PRT node beside the rollups-node under cartesi run --prt. Every entry is a validator. sentry = true is the optional extra that appoints its address on the DaveConsensus. The rollups-node is an entry too, type = "reference", implied when omitted, and claim = false on it runs the node in reader mode: it follows the tournaments and records what the chain settles, while the sling nodes claim. That is one setup production deployments may use. Others run the sling node only as a sentry, which the entries cover too. Without a reference entry, nothing changes: the rollups-node claims, as it does today.

PS: [[prt.sentries]] is unreleased (it is on the same stacked branch), so there is no migration.

Design (why)

PRT is permissionless. Any validator node claims and, on disagreement, disputes. The Dave sling node reads inputs, runs the machine, joins the tournament for its commitment, stages the result and accepts it, whether or not its signer holds a sentry slot. A slot only adds the sentry claim it submits per epoch. Its own startup log says so: a signer that isn't a sentry "disputes and settles, but casts no sentry claim". So "sentry" is an extra role of a validator, not a kind of node, and the config models it as one flag on any entry.

The rollups-node is a validator as well, with the matching switch, CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED. With it off, its PRT service builds no transaction sender, needs no PRT key, and runs only the observer loop that follows tournaments and records settled epochs. That is what the rest of the node (outputs, proofs, the epochs endpoints) consumes, so a reader node next to sling validators is a complete setup. The node's mode is its own setting, on its own entry, never inferred from a sibling.

The entries are a keyed table, like [drives.<name>] and [nvrams.<label>]. The key is the label and the container name, so uniqueness comes from TOML and the "label defaults to the position" rule is gone. Entries count in file order where order matters: sentry IDs on the consensus and pool accounts.

The table lives under [prt], as [prt.nodes.<label>], not at the top level as [nodes.<label>]. Every entry is a PRT process: a sling node claims on, or is a sentry of, a DaveConsensus and nothing else, so under an authority or quorum consensus the entries have no meaning, and a top-level table would suggest a generality the CLI doesn't have. Under [prt] there is one rule for the whole section, parsed always and ignored with a notice when --prt isn't given, and sentry_manager stays next to the slots it manages.

Configuration

[prt]
sentry_manager = "0x9965507D1a55bcC2695C58ba16FB37d819B0A4dc"   # optional

[prt.nodes.reference]                      # the rollups-node, implied when omitted
type = "reference"
claim = false                              # reader mode

[prt.nodes.blue]                           # a validator: devnet account 20, funded by the CLI
type = "sling"

[prt.nodes.green]                          # a validator that also holds a sentry slot
type = "sling"
sentry = true                              # devnet account 21, funded by the CLI

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

[prt.nodes.partner]
sentry = true
address = "0x…"                            # appointed only, run elsewhere
[prt] keys
Key What it does Rules
sentry_manager The address allowed to rotate the sentries on the DaveConsensus later. Passed to the deploy as --sentry-manager. Optional. A manager with no sentry gets a warning, since there is nothing to rotate.
nodes The [prt.nodes.<label>] entries below. A table of tables. Any other key in [prt] is an error, as in the other sections.

Without --prt the whole [prt] section is parsed, so a typo still fails early, and then ignored with a notice. A sling node only makes sense on a DaveConsensus.

[prt.nodes.<label>] keys
Key What it does Rules
<label> (the key) Names the container, prt-node-<label>, as shown in the run output and cartesi logs. The reference entry's label names nothing, the node stays rollups_node. Starts with a lowercase letter, then lowercase letters, digits, - and _. Unique by construction. A label can't start with a digit: the CLI reads entries in file order, and TOML parsers put integer-like keys first.
type The node the CLI runs for this entry. "sling": a cartesi-sling-node container. "reference": the rollups-node itself, one entry, implied when the config has none. Without type the entry is only appointed, and someone else runs it. Any other value is an error listing the supported types. At most one "reference".
sentry The entry's address holds a sentry slot on the DaveConsensus and submits a sentry claim per epoch. Boolean, default false. Required true on an entry without type. Not supported on the reference node yet. Sentries are appointed in file order, so the first sentry = true entry is sentry 1.
claim Whether the rollups-node submits claims. false is reader mode: no PRT signer, the node follows the tournaments and records settled epochs. Boolean, default true. Only on the reference entry. Not supported on a sling node yet, which always claims.
address The entry's address, as appointed on the consensus. Not the zero address. Unique across entries. Required without type. With type = "sling" and private_key_file, it must be the key's address. Not on the reference node.
private_key_file A host file with the entry's hex private key. The sling node signs with it, and without address the address comes from the key. Reaches the container as an init-service copy under /run/secrets. Needs type = "sling" and can't be empty. The format, and the match with address, are checked before any container starts. Not a devnet account's key.

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

  1. address.
  2. The key in private_key_file.
  3. The PRT pool: the next unused devnet account from 20 to 29, which cartesi run funds with 10,000 ETH, in file order. The reference node takes none, it runs on the devnet accounts it always had.
Entry rules
Entry Result
type = "sling" A validator the CLI runs. It joins tournaments, stages and accepts results, recovers bonds.
type = "sling" with sentry = true The same, and its address holds a sentry slot.
sentry = true with address and no type Appointed only. The CLI runs nothing for it.
type = "reference" with claim = false The rollups-node reads. It follows the tournaments and records settled epochs, with no PRT signer.
no type = "reference" entry The rollups-node claims, as today.
address and no type, without sentry = true Error. The entry would do nothing.
sentry = true on the reference node Error, not supported yet.
claim on a sling node or an appointed entry Error, not supported yet. Only the rollups-node reads instead of claiming.
two type = "reference" entries Error. The rollups-node is one entry.
address or private_key_file on the reference node Error. The CLI runs it on the devnet accounts.
private_key_file without type Error. The CLI only reads the key of a node it runs.

Warnings, printed before anything starts: claim = false with no type = "sling" entry, since nothing the CLI runs claims and a claimer must run elsewhere. claim = false with every sentry a sling the CLI runs, since a sling sentry votes for its own claim and the staged result is accepted as soon as its authors vote. A sling on devnet account 8 or the guardian, whose transactions may collide with the CLI's.

Reserved accounts
Account Reference node claims Reference node reads (claim = false)
0, the node's claimer signer error for any sling entry error for any sling entry
6, the node's PRT signer error for any sling entry free: the node has no PRT signer in reader mode
8, the default sender of send and deposit warning warning
the guardian warning warning
20 to 29, the PRT pool allocated in file order allocated in file order

Appointed-only entries may use any account.

Effects of each setup

Who does what
Reference node claims (no reference entry, today's behaviour) Reference node reads (claim = false)
runs the machine for inputs and outputs the rollups-node the rollups-node
joins tournaments, stages and accepts results, recovers bonds the rollups-node, signing with account 6, and every sling node every sling node
submits sentry claims the sling nodes with sentry = true the same
records settled epochs for outputs and proofs the rollups-node, from its own transactions and the chain the rollups-node, from the chain only
CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED on the node true (default) false
CARTESI_PRT_AUTH_* on the node the devnet mnemonic, account 6 not set. A host value is dropped with a warning.
PRT submitter identity in the node's log present absent
early acceptance (inside --claim-staging-period) when every sentry claimed the staged result the same rule, applied by whichever sling node accepts
containers rollups_node, prt-node-<label> per sling entry the same
cartesi logs the node and every prt-node-* container the same

In both setups every PRT participant runs the application's machine over the same inputs. A reader node with three sling nodes is three machines beside the node's own, which is fine on a devnet and is the reason the pool has ten accounts.

Order under --prt

parse [prt] and resolve each entry → start the environment, the node in reader mode when the reference entry says claim = false → fund the pool entries → deploy with --sentries/--sentry-manager → start the prt-node-<label> containers → on a shell rebuild, stop them → undeploy → redeploy on a new DaveConsensus → start them again.

Rebuilds and the sticky reader mode

The rollups-node saves the submission mode in its database the first time its submitter starts, and refuses to start if the database was created in the other mode. cartesi run takes the volumes down on exit, so changing claim between runs is fine. A shell rebuild is different: it keeps anvil, the database and the node container as they are, and only rebuilds, undeploys and redeploys the application. The node's mode can't change there, since both its env and its database would have to be recreated, which is a restart in all but name.

So a rebuild re-reads cartesi.toml for the sling entries and the manager, as today, and compares the reference entry's claim with the one resolved at startup:

  • Same value: the rebuild proceeds as before, with the sling entries as they are in the file now. Their containers are recreated on every rebuild, so they can change freely.
  • claim changed: a yellow line says "the rollups-node's claim setting changed to claim = false in cartesi.toml. Restart cartesi run to apply it. This rebuild keeps the [prt] setup it started with." The rebuild proceeds on the startup setup, so the application change still lands.

To apply the new mode: Ctrl-C, which removes the containers and volumes, then cartesi run --prt again. The node then starts in the mode the config asks for, on a fresh database.

The same was already true of settings the rebuild can't apply to a running environment, such as --epoch-length. The sentry list and the manager are re-read, because each redeploy creates a new DaveConsensus.

Changes

  • config/toml/prt.ts, config/types.ts: [prt.nodes.<label>] with type, sentry, claim, address, private_key_file. The entry rules above are parse-time errors naming the entry, [prt.nodes.green].
  • sentries.ts → prt.ts: resolution of every entry, the reference entry implied when omitted, the shared pool, the reserved accounts per mode, funding, the warnings, and the rebuild check of the reference entry's claim.
  • compose/sentry.ts → compose/prt-node.ts: one sling service per type = "sling" entry, named prt-node-<label>, with the same env as before. The sentry flag is nothing but the deploy's --sentries list, since the sling node takes no flag and learns its slot from the chain.
  • compose/node.ts: with claim = false, CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED=false and no PRT auth variables.
  • commands/run.ts: the output block prints the rollups-node's mode first, then the entries in file order, validator blue 0x… or sentry 1 green 0x…, and the rebuild warning. An appointed-only entry reads (appointed only, devnet account 3) or (appointed only), so what the CLI does comes before where the account is from.
  • commands/logs.ts: the default selection is the node and every prt-node-* service.
  • Tests: the [prt] unit cases rewritten to the table, cases for the sentry flag, the reference entry and its rules, the reserved accounts per mode, the warnings and the rebuild check (readerModeChanged), the node's reader-mode env, and dry runs (a second reference entry, claim on a sling, an appointed entry without the flag, the node rendered in reader mode with the host's PRT auth dropped). prt.test.ts keeps the node-as-claimer environment. prt-sling-claimer.test.ts runs the reference entry with claim = false, a sling validator and a sling sentry: the node is healthy in reader mode without a submitter identity, the validator's log says it disputes and settles, and an epoch is accepted inside a 1,000,000-block staging period, which only the sling nodes can have done.
  • Changesets: the three of feat(cli): Add support to rollups-node alpha.14 and PRT sentries [Setup + Local runs] #569 that name [[prt.sentries]] and sentry-<label> are reworded, since both PRs ship in one release. One new Minor: the reference entry and claim = false for reader mode.

@changeset-bot

changeset-bot Bot commented Nov 12, 2026 •

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8e19586

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 moved this to 👁 In Review in Rollups Tooling Nov 12, 2026
@brunomenezes
brunomenezes force-pushed the feat/rollups-node-changes branch from 5954e82 to db40096 Compare November 12, 2026 14:13
@github-actions

github-actions Bot commented Nov 12, 2026 •

Copy link
Copy Markdown
Contributor

Coverage Report

Status Category Percentage Covered / Total
🟢 Lines 88.4% (🎯 0%) 13811 / 15624
🔵 Statements 88.4% 13811 / 15624
🔵 Functions 82.64% 357 / 432
🔵 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 🔴 16.51% 🔴 16.51% 🔴 8.33% 🔴 0% 76-95, 99-278, 282-289, 296...
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 force-pushed the refactor/redesign-sentry-setup branch 2 times, most recently from e162004 to e2bdc4d Compare November 12, 2026 15:17

@tuler tuler left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a clear step forward from #569: one table of nodes, keyed labels, and reader mode for the rollups-node are all things I want to keep. I think it is one small step short of the model, though, and the gap is the role key.

The model I have in mind. PRT is permissionless: any validator node claims and, if there is disagreement, disputes. Being a sentry is an optional extra role of a validator. We have two validator implementations today, the Go reference node and the Rust sling node, and in principle either can claim and either can be a sentry.

What role does today. It doesn't configure the entry it sits on:

  • role = "sentry" only adds the entry's address to --sentries at deploy. The sling container is identical for both roles, as the description says ("the role is nothing but the signer").
  • role = "claimer" only switches the rollups-node to reader mode and frees account 6.
  • A sling node always participates, and learns it is a sentry from the chain. So every role = "sentry" sling is already a claimer as well.

So the two values are answers to two different questions ("is this address appointed?" and "does the rollups-node submit?"), and making them one enum rules out valid setups:

Rollups-node Sling Today
claims sentry role = "sentry"
reads not a sentry role = "claimer"
claims not a sentry can't be written
reads sentry blocked for a single node

The third row is the plain permissionless case: two validators, no sentries.

Proposal.

  1. Replace role with sentry = true (default false). An entry is a validator; sentry is the optional extra and means "appoint this address".
  2. Make the reference node an explicit entry, type = "reference", implied when omitted (as [drives.root] is). Reader mode becomes claim = false on that entry, instead of being inferred from a sibling.
  3. Drop the one-claimer rule. If nothing the CLI runs claims, warn instead of failing: a claimer may run outside the CLI.
  4. Keep external entries as they are (no type, address required). They only make sense with sentry = true.
[prt.nodes.reference]     # implied when omitted: type = "reference", claim = true
type = "reference"
claim = false             # reader mode

[prt.nodes.blue]          # plain validator
type = "sling"

[prt.nodes.green]
type = "sling"
sentry = true

[prt.nodes.partner]       # external, appointed only
sentry = true
address = "0x…"

Combinations the implementations don't support yet become parse errors we can lift later without a schema change: sentry = true on the reference node, and claim = false on a sling.

The reference entry is a little irregular (exactly one, its label doesn't name a container, no address/private_key_file for now). I think that is a fair price for not having an implicit participant. The code change looks small: sentriesOf filters on the flag, claimerOf/claimerMoved become a check on the reference entry's claim, and the reserved-account logic keys off the same value.

Happy to talk it through before you change anything.

Comment thread apps/cli/src/config/toml/prt.ts Outdated

// the roles differ in funding, reserved accounts and the node's mode, so
// every entry says which one it is
if (value.role === undefined) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is where I'd swap role for an optional sentry boolean. The comment says the roles differ in funding, reserved accounts and the node's mode, but only the last two differ, and both are about the rollups-node, not this entry.

Comment thread apps/cli/src/config/toml/prt.ts Outdated
: parseRequiredString(value.private_key_file, "private_key_file");

// the CLI runs the claimer, and the rollups-node only reads
if (role === "claimer" && type === undefined) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With reader mode stated on the reference node itself, this rule goes away. "The rollups-node reads and nothing the CLI runs claims" is worth a warning, not an error: running a claimer outside the CLI (a dev build of either node, for instance) is a setup I'd like to keep possible.

Comment thread apps/cli/src/config/toml/prt.ts Outdated
([, other]) => other.role === "claimer",
);
if (sameLabel >= 0) {
if (node.role === "claimer" && claimer !== undefined) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PRT is permissionless, so more than one claimer is the normal case. It is also already what runs: the rollups-node plus N sling sentries is N+1 participants, since a sling sentry joins tournaments too.

Comment thread apps/cli/src/prt.ts Outdated
* it, so the placement can only change with a fresh `cartesi run`.
* @returns the warning, or undefined when the claimer is where it was
*/
export const claimerMoved = (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This compares labels, so moving the claimer from one sling entry to another asks for a restart, although the rollups-node's mode doesn't change and the sling containers are recreated on a rebuild anyway. Only a change of the rollups-node's own mode needs the restart.

Comment thread apps/cli/src/commands/run.ts Outdated
port,
projectName,
prt,
claimSubmission: claimer === undefined,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This line is the implicit entry I find confusing: the rollups-node's mode is derived from whether some other entry says role = "claimer". I'd read it from the reference node's own setting.

@brunomenezes

Copy link
Copy Markdown
Member Author

Happy to talk it through before you change anything.

this is the right call and I'm taking it as proposed.

You're right that a sling node always claims, so role was a false choice between two things that aren't exclusive. The comment about funding was wrong too, both draw from the same pool. What differs is account 6 and the rollups-node's mode, and those belong on the node's own entry, as you say.

So the next revision of this PR:

  • role goes. Any entry is a validator, sentry = true appoints its address. Sentry IDs still follow file order.
  • The rollups-node gets an explicit entry, type = "reference", implied when omitted. claim = false puts it in reader mode. Exactly one is allowed, and address or private_key_file on it is an error for now.
  • The one-claimer rule goes. When nothing the CLI runs claims, a warning, since the claimer may run outside the CLI.
  • External entries stay as they are, and sentry = true becomes required on them, since without a type and without the flag the entry does nothing.
  • The rebuild check only looks at the reference entry's claim, which is the one thing the node keeps in its database. Sling entries can move freely between rebuilds.
  • Unsupported combinations are parse errors for now: sentry = true on the reference entry, claim = false on a sling.

One thing I'd add: with claim = false and a single sling that is also the only sentry, the contract accepts that sling's claims as soon as it votes for them, so the staging period stops meaning anything. I'd keep that writable, it's a fair single-node dev setup, but print a warning.

Two names to settle before I change anything: "reference" or "rollups-node" for the type, and claim or claim_submission for the key, after the node's variable. I lean to "reference" and claim. Let me know if you prefer otherwise.

@brunomenezes
brunomenezes changed the base branch from feat/rollups-node-changes to prerelease/v2-alpha November 12, 2026 16:34
@socket-security

socket-security Bot commented Nov 12, 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

@github-project-automation github-project-automation Bot moved this from 👁 In Review to 👍 Waiting Merge in Rollups Tooling Nov 12, 2026
@github-project-automation github-project-automation Bot moved this from Todo to Waiting Merge in Rollups SDK Nov 12, 2026
@brunomenezes
brunomenezes merged commit 580a58f into prerelease/v2-alpha Nov 12, 2026
4 checks passed
@brunomenezes
brunomenezes deleted the refactor/redesign-sentry-setup branch November 12, 2026 19:05
@github-project-automation github-project-automation Bot moved this from 👍 Waiting Merge to 📦 Done in Rollups Tooling Nov 12, 2026
@github-project-automation github-project-automation Bot moved this from Waiting Merge to Done in Rollups SDK 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