Skip to content

Sling node hardening for the v0.22 generation - #299

Merged
GCdePaula merged 81 commits into
mainfrom
feature/sling-node-hardening
Oct 17, 2026
Merged

GCdePaula merged 81 commits into
mainfrom
feature/sling-node-hardening

Conversation

@GCdePaula

@GCdePaula GCdePaula commented Oct 15, 2026 •

Copy link
Copy Markdown
Member

Moves Dave to Cartesi Machine v0.22.0 and solidity-step v0.16.0 and hardens the sling node: louder operator signals, one storage generation, a simplification that removes about 4,600 lines (net about -2,700), and starting a node from the last settled state instead of replaying from genesis. Landed as granular commits; each phase was gated and adversarially reviewed before the next.

Generation impact. This is a new deployment generation for the state transition (CM_MARCHID 22, a new pristine uarch hash, machine archive version 8). Every node needs a fresh data directory: v0.22 refuses machines stored by v0.21, and the store format changed (older directories exit 40). Operators also change their invocation: the private key comes only from CARTESI_SLING_AUTH_PRIVATE_KEY or --auth-private-key-file (never argv), --blockchain-http-endpoint and --blockchain-id are required, and KMS leaves region and endpoint to the AWS SDK. Everything operator-facing is in CHANGELOG.md's Unreleased section and the node README's operator notes.

Reading guide

The commits group into five phases; each can be reviewed on its own (the GitHub commit list shows them in order).

  1. Emulator release candidate (from "build!: move to Cartesi Machine v0.22.0-test3 ..." through "docs: describe the v0.22 emulator and step pins"). The bump, the C-API reads that replace hand-declared uarch constants, and the harness running in parallel now that v0.22 opens files close-on-exec.

  2. Phase A: operator signals and participation fixes (from "refactor(node): use anyhow in the hero and the epoch manager" through "fix(node): address the Phase A review"):

    • Exit codes: 0 stop, 1 retry, 2 usage, 30 terminal, 40 rebuild, 101 panic (restart and page); RestartPreventExitStatus=30 40.
    • One paging rule: a failure that would cost a dispute logs WARN while it lasts and ERROR once it outlasts G (5 minutes).
    • ERROR on every missed obligation, a classified loss, divergence exits 30, and the sentry votes after a lost root too.
    • Fixes: a GC that could drop the runner's boundaries, a KMS call with no timeout, the cold refold of child tournaments, a 1 GiB free-space floor, a stale read endpoint.
  3. Phase B, storage generation (from "refactor(node): stamp the store with a hand-bumped format version" through "fix(node): address the storage-format review"): STORE_FORMAT with a pinned schema hash, snapshot paths derived from state hashes (a stopped data directory can move), write-once checks in Rust instead of 26 triggers, and an epoch roll in one transaction.

  4. Phase B, simplification (from "refactor(node): prepare each Hero action from the planning context" through "docs(node): address the simplification review"): behavior-preserving: about +2,000 / -4,600 lines. The Hero prepares from its planning context; the ArenaSender trait, pass-through error enums and dead surface go; the tournament layer stops re-proving what the observer built (a delegated child is bounded by a pure check against its parent instead of a chain read; every validator in front of an engine assert stays); engine leftovers. No leaf, root or proof byte changes: the spec oracles, the prototype differentials, the seam witness fixture and the release corpus all pin that.

  5. Phase C (from "build: pin the final Cartesi Machine v0.22.0 and solidity-step v0.16.0" to the end):

    • The final release pins. The releases ship the code the candidates did; only the package and corpus digests moved.
    • Start from a settled image: a new data directory seeds from the template (epoch 0) or from the state the current sealed epoch starts from, decided by hash against finalized chain state. A seeded directory owns its epoch and later ones; bonds below it are the operator's (README). A seeded directory never reads --template-path again.
    • Endpoints: each answers its chain once at startup or the start fails (exit 1 if it does not answer, 30 on another chain); a send the endpoint does not answer pages after G instead of logging ERROR every tick; relay requirements and a qualification runbook (docs/runbooks/submit-relay.md).
    • A template whose files the runner cannot write is refused at startup (v0.22 would otherwise fail every batch after seeding).
    • Hash-tree verification of seed images is staged, commented out together with its binding, as agreed with the emulator team: nothing calls cm_verify_hash_tree on v0.22.0, where it serves internal tests. Until the release that supports it, the docs say to trust a seed image's source like the template.
    • The startup line names the release tag. CHANGELOG, todo lines for the deferred items, and the v0.22 measurements.
    • Config surface: the private key is never a command-line flag, and its source is checked (and parsed) before any network call; KMS region and endpoint come from the AWS SDK; the read endpoint and chain id are required; the startup config logs as one line; snapshot_gap_inputs lives in the machine runner; smaller tidying.

    Two commits cancel out and can be skipped: "fix(node): a settled epoch's recovery walk stops at its successor's seal" and its revert. They stay so the history matches what was reviewed.

For a contracts reviewer: the only production Solidity change is CartesiStateTransition taking CM_MARCHID from solidity-step; between step v0.15 and v0.16 only EmulatorConstants.sol changes. just measure-prt-gas, the authoritative gas gate, passed on the final pins (clean tree, official Forge 1.5.1): all 22 Tournament witnesses and all 13 full-stack leaf-proof witnesses, so no allocation moves and, per the calibration runbook, no new record is due.

Validation

On macOS arm64 with the final releases (logs kept outside the repository):

  • just check; CI green on every push.
  • The anvil harness (28), the real-machine engine differentials (21), the reference-CLI goldens, and the release computation-hash corpus through both the CLI and Dave.
  • Both contract suites and the gas witnesses; the from-source emulator build CI's release job uses.
  • just e2e-smoke 5/5 with no ERROR lines.
  • The machine images rebuilt on the final release keep their roots.
  • just measure-node-vs-emulator on v0.22.0: the node stays at 1.01x the bare emulator for a cold join and 1.05x for a deep proof (docs/measurements/node-vs-emulator.md).
  • just test-kms against LocalStack.

Open, tracked in docs/todo.md

  • Uncomment the hash-tree verification on the emulator release that supports it (agreed with the emulator team).
  • Qualify a relay on mainnet with a funded key, then decide whether to require one.
  • The sequencer still pins v0.21.0, and the shared devshell keeps a v0.21 shell for it.

….0-test3

The emulator release candidate bumps MARCHID to 22, rebuilds the uarch
(a new pristine uarch hash) and moves stored machines to archive version 8,
so every machine root moves: the test images, the devnet and every fixture
are regenerated, after the engine's differentials passed against the new
library and before any fixture was trusted. It is a new deployment
generation for the state transition.

- CartesiStateTransition publishes solidity-step's exported CM_MARCHID
  instead of a local literal; its bytecode, and every address built from
  it, moves.
- The bindings take CMIO response lengths as u32 and refuse a longer
  payload rather than truncate it, mirror backing_store_config's new
  mark_pages_dirty, and expect version 0.22.
- The reference CLI tests use the new computation-hash flags and
  input_file_index keys; the corpus pins the release's archive, its new
  mcycle-overflow case, and uarch-near-limit-tail as error-no-hash, whose
  template the node still refuses.
- The seam-1 control test now pins the fixed collector: a rejection on the
  input budget's last cycle resets to the revert root.
- CI installs the release candidate's package, the gas script pins the
  rebuilt yield image, and the constants benchmark accepts the 0.22 module.
v0.22 exports cm_get_uarch_pristine_hash and the uarch state's start
address and log2 size, so the sys crate drops its hand-written
declarations of the library's internal symbols (and the provider
requirement they added), and the node and the bindings tests stop pinning
the coordinates. The guard test now also holds the linked library's
CM_MARCHID to solidity-step's export, so a mismatched emulator or step
fails in unit tests rather than at node startup.
The v0.22 collector resets to the revert root when an input is rejected
on its budget's last cycle, as the step does, so bulk collection takes
that cycle instead of leaving it to stepping. The control test becomes a
positive one: bulk collection returns the cycle and leaves the machine at
the pre-input checkpoint. The ruler's residual assert now names the only
input bulk collection still cannot take, one delivered at mcycle 2^64 - 1.
v0.22 opens the files it flocks with close-on-exec, so an anvil spawned
while another test stores a machine no longer inherits its locks. The
harness and the other anvil tests pass in parallel (three runs, 26 tests
each, about 36 s against 110 s serially).
Close the upstream asks v0.22.0-test3 and solidity-step v0.16.0-test3
deliver (the seam fixes, close-on-exec, the pristine uarch API and range,
the exported CM_MARCHID) and keep the open ones. Add the promotion step:
both release candidates report 0.22.0, so the final tags need rebuilt
images and fixtures and fresh data directories. The CLI exclusions narrow
to the uarch cycle overflow and an unverified halted-template lead, the
seam-1 guard leaves the glossary, the measurements stay labelled as v0.21
figures until re-measured, and version-specific wording in the justfiles,
READMEs and test comments becomes pin-neutral.
ReactError and EpochManagerError were transparent wrappers that nothing
matched on except one arm handing its anyhow error to the reader. Every
caller formats them with `{:#}`, so plain anyhow keeps every message and
error chain while dropping the conversions at each call site, and it lets a
typed cause anywhere in a chain be found by downcasting.
A supervisor could not tell a terminal refusal from an RPC blip: every
failure exited 1, and a panic or an unexpected worker stop reached only
stderr. The exit status now follows the sequencer's contract (fatal.rs):
1 retry, 30 terminal, 40 rebuild the data directory, and 101 for a panic,
restart and page, because a node restart is harmless and may clear a
nondeterministic fault while a dispute clock runs.

- Startup refusals of a flag, the template or the deployment are terminal;
  schema, semantics and emulator, consensus or geometry pin mismatches ask
  for a rebuild; transport failures stay retryable.
- A root that starts from a state the node did not compute (a divergence
  it lost, or corrupt local state) stops the node with exit 30 instead of a
  panic loop; the Hero meets it only after the settled epoch's refunds.
- A panic hook logs every panic at ERROR, main logs the final error once,
  and a worker failing during a signal-requested drain no longer exits 0.
Persistent failures that would lose a dispute stayed at WARN forever (a
full disk, a dead or lagging provider, a relay holding a transaction, a
failing dispute tick), while two narrow rules escalated after one repeat.
One rule now covers them (escalation.rs): WARN while a failure lasts, ERROR
once it has lasted G, the 5-minute response window past which a due
response draws on the censorship budget; a transient blip never pages.

- The reader, the runner and the manager's tick and dispute, settlement and
  refund planning use it, as does a sealed epoch the runner has not
  finished, which delays the root join.
- Every call the manager plans is tracked across ticks: one still planned
  after G has not landed, whatever the lane's verdict, and the error carries
  the revert reason when it reverts at estimation. It replaces the
  consecutive-tick revert and log-check rules.
- G is hardcoded, with the reasoning and when to revisit it: the node knows
  no block time to convert the table's budgets.
- Ingestion waits silently until the deployment finalizes instead of failing
  every tick, which would now page every fresh deployment.
The planner already knew how the local commitment failed, but the node
logged four missed-obligation waits at debug and folded every loss into one
undifferentiated ERROR.

- A wait that means an obligation was already missed (joins closed before
  the node joined, its clock expired, its match or its match's child is
  eliminable) logs ERROR every tick.
- A loss names its case: elimination with its reason, another class's
  child win, a root won by a commitment with this node's final state (the
  correct state settles), or a divergence, with both final states and the
  guardian's remedy.
- A sentry claims the state it computed once the root has a winner, its own
  or not; staging and acceptance still follow only its own win. A harness
  test has a node miss its root, lose it to a wrong commitment, and still
  claim.
The gap GC deleted runner-epoch boundaries that were not multiples of the
gap. Since the runner publishes only whole-gap batch anchors it removed
nothing in steady state, but after --snapshot-gap-inputs changed to a value
that does not divide the last anchor, every later anchor was a non-multiple
and each commit deleted the previous one, leaving the open epoch with its
start and its newest boundary. A dispute on that epoch then replayed from
the start, far past the documented gap - 1 inputs, on the response clock.
Epoch GC still frees every boundary once the epoch completes.
aws-config bounds only the connect by default, so a KMS endpoint that
accepted a sign call and never answered parked the epoch manager's tick
forever: no action left the node, and nothing was logged. The client now
sets a 20-second operation timeout, the RPC client's per-attempt bound, so
the tick fails and the paging rule reports a persistent outage.
…agging head

- A cold fold (a restart, or a dropped Solid) read every live child
  tournament from the root's creation block, though a clone has no logs
  before it. Behind a provider that caps eth_getLogs ranges, a restart late
  in a long dispute with many children issued hundreds of sequential range
  queries against a 5-minute response window. Each child is now read from
  the block of the NewInnerTournament that created it.
- A finalized head behind the one Solid holds (backends behind a load
  balancer) failed the Hero's whole tick. Solid is still final, so the
  reader keeps it, as ingestion treats the same head as nothing new.
Working clones are mapped shared and grow as the guest writes, so a full
filesystem would most likely kill the node by SIGBUS mid-batch, with no
log line, and every respawn would replay into the same death. The runner
now refuses a batch or a roll below 1 GiB free, about one stored machine,
with an error the paging rule reports, while the Hero keeps defending
computed epochs.

The roll's disk line summed apparent sizes, which counts every reflinked
or sparse clone at full length and never said how much room was left; it
now logs the filesystem's free space.
A provider stuck at an old head (a node that lost its peers, a dead backend
behind a load balancer) answers every read consistently, so nothing fails:
ingestion sees no new blocks and the Hero waits on turns already taken
while the clocks run. The reader now logs ERROR on each tick whose latest
block is older than G by its timestamp. It is outside the trust model, so
the node reports it and works around nothing.
The startup line now names the node, the linked Cartesi Machine version and
its MARCHID, so an operator can tell which emulator a running binary links.
Startup also says whether the signer is a sentry of the consensus, read
once, since a sentry that silently casts no claim is the one surprise an
operator would not see in the logs.
…rules

Name what each exit status asks of a supervisor (and the systemd setting
that stops a terminal loop), what to alert on beyond ERROR lines, why the
data directory belongs on a reflink filesystem and stays put, and that the
signer must accept ETH transfers within the payment gas.
- Log the first worker failure when it happens, not only after the drain,
  and keep its exit status if a second stop signal cuts the drain short.
- Check a fatal error before the shutdown arm, so a divergence seen during
  a requested stop still exits 30.
- Make only the template's deterministic refusals terminal; a transient
  load failure (memory, file descriptors) retries instead of holding the
  node down under RestartPreventExitStatus.
- Page the runner wait only once the runner has reached the sealed epoch,
  so a fresh data directory's replay of history does not page for hours.
- Vote as a sentry only once the root is decided, as documented.
- Restart the runner's paging clock on each committed batch and roll.
- Word the loss and stale-head errors accurately, check the claim value in
  the sentry test, and fix stale doc comments and the README's exit 40.
The node version never discriminated (the crate version is frozen) and the
raw schema fingerprint refused stores over comment edits. node_metadata now
holds STORE_FORMAT and COMMITMENT_SEMANTICS; a test pins schema.sql's hash so
every schema edit decides whether to bump the format.
It only kept GC off the template's directory, which epoch 0's start row
already does while anything can need it; a pinned directory never seeds
again.
The database stored absolute snapshot paths, so a data directory could not
move. A boundary row now names only its state hash; its directory is
snapshots/0x<hash>, derived on every read and in GC. The CAS table that
held the paths goes: GC releases boundary rows and, in the same
transaction, keeps the states a remaining row still names. The plan and
crossings derive their paths from the hash, the harness adversary links the
node's snapshots into its private store, and the e2e seam derives the path
the same way.
The trigger layer duplicated the writers' checks and turned two of them
into string-matched panics. The writers now hold every class alone: the
boundary row compares with its stored hash and panics on a disagreement,
as settlements and quartet nodes already did, and completion refuses an
epoch ingestion has not seen. escalate_tripwires goes with the triggers;
raw writes stay unsupported.
…once

The roll checked readiness twice, the window-root prefix three times and
the final boundary's hash four times, and stored a machine that finishing
an epoch never changes. It now loads the verified final boundary, folds
the settlement root, and in one transaction re-checks that the cursor ends
the sealed epoch, registers (e+1, 0) on the same snapshot and inserts the
settlement. A settlement row decodes in one place with one panic; the
tests that bypassed the schema's CHECKs to fake impossible rows go.
GC finds its orphans in one set-based query before the delete. The docs no
longer claim the engine config panics on drift (startup refuses it with
exit 30 or 40), the staging comment names the publishers that still race,
and STORE_FORMAT says a number is never reused.
The actor prepares every intent on the context that planned it, so nine
error variants re-checked the intent against itself and fired only on
hand-built intents. Preparation now reads the engaged match from the
context and panics if it disagrees; the engine tripwires stay errors.
Preparation no longer reads the match payloads the intents copied, so each
intent keeps only the tournaments its call targets, and the large-variant
allowance goes. A join still names its commitment: Solid must plan that
exact join, which decision equality now checks.
LevelMaterial repeated the snapshot's descriptor and local commitment next
to the engine coordinates, so preparation compared values assembly had
taken from one variable. The context now keeps only the coordinates, and
the dead root_tournament and levels accessors go.
…request

TournamentResult was HeroTerminal plus Running, and a tick's wave never
held more than one request. A tick now carries an optional terminal and an
optional request, selecting a cleanup only in the arm where the Hero waits,
so one request per tick holds by construction.
The tick already gates on the epoch's write-once settlement row, yet the
sentry-claim, stage and accept planners and the loss report each read it
again behind a None arm that could not run. They now take the tick's row.
The sentry claim reads its epoch from canAcceptStagedTournamentResult,
which it needed anyway, instead of getCurrentSealedEpoch.
…functions

Every caller handed the arena sender the same provider the Hero's chain
already holds, and the only other implementation was a test fake that
ignored the bond read's pin. The builders now take the chain's provider,
the join's bond read lives with the other pinned reads in the observer,
and the test checks the real requests over a recording provider.
…moves to its test

Production always set the region and endpoint, so the builder's
us-east-1 defaults and its setters only obscured the one call path.
Key creation is a LocalStack fixture, not something the node may do
in an operator's account, so it moves into the test module, and
lazy_static gives way to std's LazyLock.
Refusing a silent submit endpoint at boot makes eth_chainId a relay
requirement, which the runbook now states and checks with cast. Its runs move
to mainnet: the team found Flashbots' Sepolia endpoint does not include
transactions. The README also prices the recovery walk of a template-seeded
directory behind a provider that caps log ranges.
Cloning keeps each file's mode and hard-links one without write bits, so a
read-only template seeded fine and then every advance batch failed to open
its working clone (EACCES; as root, the emulator's read-only refusal), a
liveness failure that paged only after 5 minutes. Startup now refuses such an
image with exit 30 before anything is written, naming the file and the fix.
Only files a shared load opens for writing need the bit, read from the stored
config, so the PMAs and read-only drives may stay read-only.
A load takes the root hash from the image's stored hash tree and does not
check it on pages marked clean, so a corrupt or forged image keeps a root its
contents do not have; the docs ask operators to trust the image's source.
The emulator team plans to support cm_verify_hash_tree in a later release
(v0.22.0 documents it as for internal tests). The binding, the refusal
(commented out in Template::inspect) and its ignored test land now, so that
release is a bump, an uncomment and an un-ignore; todo.md tracks it, and the
trust notes say they hold until then.
The line exists to identify a binary in bug reports, but it logged the
workspace version 2.0.0, which no release carries in its name
(v3.0.0-alpha.5 also built as 2.0.0). The release build now injects the tag
through CARTESI_SLING_RELEASE, and any other build says it is unreleased.
Cargo tracks the option_env! read, so a changed tag rebuilds the crate.
The anvil tests' reasons still blamed the emulator's leaked machine file
locks, which v0.22 fixed by opening those files close-on-exec; the harness
docs already dropped that paragraph. They now just say they spawn anvil and
name the recipe, and the two corpus tests name theirs too. args.rs keeps two
such reasons until another branch's edits there merge.
reviews/README.md requires unfinished review work to have a todo line or an
owner doc. The small debts deferred until another branch's args.rs edits
merge are now listed under node-architecture.md's Known debts, which no
longer says none are open, with one todo line for their cleanup; the reader
test for several seals in one chunk and the review's untaken decisions get
todo lines too.
todo.md makes the Unreleased section this generation's release notes, yet it
missed most of the node's operator-visible changes on this branch: the exit
contract, the paging rule, the loss and missed-obligation ERRORs, the
sentry's vote after a lost root, the disk floor, the KMS bound, the stale
read endpoint, the startup line, the store format stamp, and the refusals of
an unanswering endpoint and an unwritable template. The entries point at the
README's operator notes rather than repeating them.
The node's overhead over the bare emulator holds on v0.22: a cold join at
1.01x and a deep proof at 1.05x. The cold join's disk delta rose from 18 to
34 MiB; it is a free-space delta on a shared machine and was not
investigated further.
… out

The emulator team asked that nothing call cm_verify_hash_tree on v0.22.0: it
is there for the emulator's internal tests and unoptimized. The binding and
its test now wait, commented out, beside the check in Template::inspect for
the release that supports it.
Any local user can read a process's arguments (ps, /proc/<pid>/cmdline), and
--auth-private-key put the signer's key there. The pk signer now takes its
key only from CARTESI_SLING_AUTH_PRIVATE_KEY or --auth-private-key-file; the
flag is gone, so passing the key on the command line is a usage error. clap
cannot read an argument from the environment alone, so the signer reads the
variable itself, and help never shows it. Neither or both sources set is
terminal (exit 30). The e2e harness passes its key through the environment,
and the Rust tests through a key file.
--aws-region defaulted to us-east-1 and the node always passed
https://kms.{region}.amazonaws.com, overriding the SDK's own resolution: a
validator that takes its region from a profile or instance metadata signed
in us-east-1, and FIPS endpoints and other partitions (aws-cn) needed an
explicit URL. Both flags are now optional overrides with no default, and
without them the SDK resolves as for any AWS client. They no longer read
AWS_REGION and AWS_ENDPOINT_URL themselves: the SDK does, with its own
precedence (AWS_ENDPOINT_URL_KMS first). A start where no region resolves
still fails, naming --aws-region. The operation timeout is unchanged.
--blockchain-http-endpoint defaulted to anvil's http://127.0.0.1:8545 and
--blockchain-id to its 31337, values a production start never wants. A
forgotten flag already failed loudly through the chain check, but only
after the node had connected somewhere it was not told to. Both are now
required, so a start without them is a usage error. The e2e harness passes
anvil's chain id, and the argument tests pass both.
The config dump was one INFO record over a dozen lines, which a
line-oriented log pipeline splits into lines without a level or a target.
It is now one line of comma-separated fields under the same labels, with
the endpoints redacted as before; the setup test pins both.
run took a ShutdownSignal that its one caller, main, always created fresh,
and nothing requests shutdown before run does after the first exit. So
worker_failure's graceful arm, for a worker that returned under an earlier
request, could not be reached outside tests, and no test reached it. run
now creates the signal, and the first exit is always a failure, exactly as
before.
Chain::finalized_block_number read the same tag as finalized_head without
its zero-hash check, so the reader's tick, the seed probe and startup
sampled the durable boundary through a laxer path than the rest of
ingestion. They, and the tests, now take finalized_head's number, and the
duplicate is gone. The mocked heads carry a hash, as a provider's do.
Every Storage handle, the reader's, the manager's and the Hero's included,
carried snapshot_gap_inputs, set by NodeConfig::storage, although only the
runner's planning reads it. The runner now holds the gap and passes it to
advance_plan, and its default lives beside it; --snapshot-gap-inputs and
the runner's behavior are unchanged. Storage tests that set a gap their
batch never read drop it, and the stopped-runner differential restarts at
its own gap instead of the default a fresh handle carried.
run's startup hygiene promised to grow into another project's ritual, an
aspiration no check backs. args.rs justified reading the linked pristine
uarch hash outside an error context, but Machine::uarch_pristine_hash is
infallible, so there is no local fault to misreport. sync.rs's module doc
retold the signal's predecessor; the contract that errors do not travel
through the signal stays.
harness/settled_start.rs drives the seeding gate through
NodeConfig::setup_with and the seeded node's workers on the deterministic
chain, but test-harness.md's inventory never named it, so a reader looking
for where seeding is tested against the contracts would not find it.
The other anvil tests stopped blaming the emulator's leaked machine file
locks, which v0.22 fixed, while another branch's edits to args.rs were
pending; these two kept the stale reason until now.
Operators read the README's Run section and the help block, and the
changelog is this generation's release notes. Both now say where the pk
signer takes its key and why no flag does, that the endpoint and chain id
have no default, how the KMS signer resolves its region and endpoint, and
that the startup config is one log line. The help block is regenerated
from the binary.
The deferred config-surface debts, the run signal, the finalized-head
duplicate, the runner's gap, the comment noise, args.rs's stale ignore
reasons and the harness inventory line have landed, so their todo line goes
and Known debts keeps only the chain-name gate, which stays by decision.
@GCdePaula
GCdePaula requested a review from stephenctw October 16, 2026 19:44
A start with no private key, or with both sources, failed only after both
endpoints had been asked their chain. The key source is now checked first,
as configuration to fix before anything reaches the network.
It refuses nothing a supported chain needs, and removing it would touch
several signatures for no change in behavior; it is a decision, not a
debt.
The early check now parses the key, so a key that does not parse exits 30
before any endpoint is asked, instead of 1 after, which a supervisor
restarts forever. The key file's path leaves its read error: clap suggests
the file flag to an operator who still passes the key itself. The docs add
exit 2 to RestartPreventExitStatus, since the required flags make a usage
error the likely upgrade mistake.
@GCdePaula
GCdePaula marked this pull request as ready for review October 16, 2026 21:02

@stephenctw stephenctw 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.

Amazing work!

@GCdePaula
GCdePaula merged commit de12cb4 into main Oct 17, 2026
9 checks passed
@GCdePaula
GCdePaula deleted the feature/sling-node-hardening branch October 17, 2026 09:11
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants