Repository navigation
refactor(cli): PRT nodes in cartesi.toml, with a sling claimer - #577
Conversation
🦋 Changeset detectedLatest commit: 8e19586 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
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 |
5954e82 to
db40096
Compare
Coverage Report
📁 File Coverage (20 files)
|
e162004 to
e2bdc4d
Compare
tuler
left a comment
There was a problem hiding this comment.
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--sentriesat 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.
- Replace
rolewithsentry = true(defaultfalse). An entry is a validator; sentry is the optional extra and means "appoint this address". - Make the reference node an explicit entry,
type = "reference", implied when omitted (as[drives.root]is). Reader mode becomesclaim = falseon that entry, instead of being inferred from a sibling. - Drop the one-claimer rule. If nothing the CLI runs claims, warn instead of failing: a claimer may run outside the CLI.
- Keep external entries as they are (no
type,addressrequired). They only make sense withsentry = 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.
|
|
||
| // the roles differ in funding, reserved accounts and the node's mode, so | ||
| // every entry says which one it is | ||
| if (value.role === undefined) { |
There was a problem hiding this comment.
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.
| : parseRequiredString(value.private_key_file, "private_key_file"); | ||
|
|
||
| // the CLI runs the claimer, and the rollups-node only reads | ||
| if (role === "claimer" && type === undefined) { |
There was a problem hiding this comment.
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.
| ([, other]) => other.role === "claimer", | ||
| ); | ||
| if (sameLabel >= 0) { | ||
| if (node.role === "claimer" && claimer !== undefined) { |
There was a problem hiding this comment.
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.
| * 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 = ( |
There was a problem hiding this comment.
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.
| port, | ||
| projectName, | ||
| prt, | ||
| claimSubmission: claimer === undefined, |
There was a problem hiding this comment.
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.
this is the right call and I'm taking it as proposed. You're right that a sling node always claims, so So the next revision of this PR:
One thing I'd add: with Two names to settle before I change anything: |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Summary
Note
This PR supersedes #569, closed in its favour, and targets
prerelease/v2-alphadirectly. 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, thecartesi runfixes, 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 undercartesi run --prt. Every entry is a validator.sentry = trueis the optional extra that appoints its address on the DaveConsensus. The rollups-node is an entry too,type = "reference", implied when omitted, andclaim = falseon 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--prtisn't given, andsentry_managerstays next to the slots it manages.Configuration
[prt]keyssentry_manager--sentry-manager.nodes[prt.nodes.<label>]entries below.[prt]is an error, as in the other sections.Without
--prtthe 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<label>(the key)prt-node-<label>, as shown in the run output andcartesi logs. The reference entry's label names nothing, the node staysrollups_node.-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"sling": acartesi-sling-nodecontainer."reference": the rollups-node itself, one entry, implied when the config has none. Withouttypethe entry is only appointed, and someone else runs it."reference".sentryfalse. Requiredtrueon an entry withouttype. Not supported on the reference node yet. Sentries are appointed in file order, so the firstsentry = trueentry is sentry 1.claimfalseis reader mode: no PRT signer, the node follows the tournaments and records settled epochs.true. Only on the reference entry. Not supported on a sling node yet, which always claims.addresstype. Withtype = "sling"andprivate_key_file, it must be the key's address. Not on the reference node.private_key_fileaddressthe address comes from the key. Reaches the container as an init-service copy under/run/secrets.type = "sling"and can't be empty. The format, and the match withaddress, 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:
address.private_key_file.cartesi runfunds with 10,000 ETH, in file order. The reference node takes none, it runs on the devnet accounts it always had.Entry rules
type = "sling"type = "sling"withsentry = truesentry = truewithaddressand notypetype = "reference"withclaim = falsetype = "reference"entryaddressand notype, withoutsentry = truesentry = trueon the reference nodeclaimon a sling node or an appointed entrytype = "reference"entriesaddressorprivate_key_fileon the reference nodeprivate_key_filewithouttypeWarnings, printed before anything starts:
claim = falsewith notype = "sling"entry, since nothing the CLI runs claims and a claimer must run elsewhere.claim = falsewith 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
claim = false)sendanddepositAppointed-only entries may use any account.
Effects of each setup
Who does what
claim = false)sentry = trueCARTESI_FEATURE_CLAIM_SUBMISSION_ENABLEDon the nodetrue(default)falseCARTESI_PRT_AUTH_*on the nodePRT submitter identityin the node's log--claim-staging-period)rollups_node,prt-node-<label>per sling entrycartesi logsprt-node-*containerIn 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
--prtparse
[prt]and resolve each entry → start the environment, the node in reader mode when the reference entry saysclaim = false→ fund the pool entries → deploy with--sentries/--sentry-manager→ start theprt-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 runtakes the volumes down on exit, so changingclaimbetween 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.tomlfor the sling entries and the manager, as today, and compares the reference entry'sclaimwith the one resolved at startup:claimchanged: 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 --prtagain. 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>]withtype,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'sclaim.compose/sentry.ts→compose/prt-node.ts: one sling service pertype = "sling"entry, namedprt-node-<label>, with the same env as before. The sentry flag is nothing but the deploy's--sentrieslist, since the sling node takes no flag and learns its slot from the chain.compose/node.ts: withclaim = false,CARTESI_FEATURE_CLAIM_SUBMISSION_ENABLED=falseand 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…orsentry 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 everyprt-node-*service.[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,claimon a sling, an appointed entry without the flag, the node rendered in reader mode with the host's PRT auth dropped).prt.test.tskeeps the node-as-claimer environment.prt-sling-claimer.test.tsruns the reference entry withclaim = 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.[[prt.sentries]]andsentry-<label>are reworded, since both PRs ship in one release. One new Minor: the reference entry andclaim = falsefor reader mode.