Skip to content

fix(validator): fail closed on signing-protection persistence gaps (A-1315, A-1317, A-1318)#24565

Merged
spalladino merged 3 commits into
merge-train/spartan-v5from
spl/a1315-a1317-a1318-db
Jul 7, 2026
Merged

fix(validator): fail closed on signing-protection persistence gaps (A-1315, A-1317, A-1318)#24565
spalladino merged 3 commits into
merge-train/spartan-v5from
spl/a1315-a1317-a1318-db

Conversation

@spalladino

@spalladino spalladino commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Context

A store that must never silently become empty — the single-node signing-protection LMDB, and (for the ordering fix) world-state — could be wiped or bypassed by the DatabaseVersionManager / store-creation path.

  • A-1315: writeVersion() ran before onOpen(), and was a plain non-atomic writeFile. A crash in between left a "valid" marker over an empty/partial data dir; on restart the self-healing reset was skipped forever (a stable wedge for world-state, which opens several native stores in onOpen).
  • A-1318: any non-ENOENT read/parse failure of the version file (EACCES, EIO, truncation) fell back to DatabaseVersion.empty(), which unconditionally triggered a reset — so a transient permissions/disk error at startup rm-rf'd the signing-protection DB. The schemaVersionMismatchPolicy: 'throw' added by A-1029 only guarded the numeric-mismatch branches, not this one.
  • A-1317: a missing dataDirectory silently selected a fresh ephemeral tmp store on every start, giving no double-signing protection across restarts, with only a debug log.

Approach

  • A-1315: DatabaseVersionManager.open() now opens the database before writing the version marker, making the marker a post-commit record — a crash before a durable open leaves no marker, so the next start re-runs the reset. writeVersion is now an atomic durable write (temp file → fsync → rename → best-effort directory fsync). The marker is only (re)written when it would actually change (first boot, reset, upgrade), which also avoids leaking a freshly opened DB if the write fails and drops the per-boot fsync.
  • A-1318: new versionFileReadFailurePolicy ('reset' default, preserving existing behavior for archiver/p2p/world-state; 'throw' for signing protection). On 'throw', an unreadable version file fails startup with an operator-actionable error and leaves data untouched. Threaded through the kv-store createStore options.
  • A-1317: createLocalSignerWithProtection now throws when no data directory is configured, unless allowEphemeralSigningProtection (env VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION, default false) is set, in which case it warns loudly. The local network sets the flag by default; the production default is strict fail-fast.

The rollup-address-change reset is intentionally left as-is: the LMDB slashing DB relies on it (see the cleanupOutdatedRollupDuties no-op).

Reviewed by Codex and a second model; both confirmed the atomic-write mechanics, ENOENT branching, migration idempotency, and the schema-composition decision (a cross-field Zod refine was avoided because it would break the downstream .merge()/.extend() chain, so the invariant is enforced at the factory).

Operator-facing changes are documented in the v5 changelog.

Fixes A-1315
Fixes A-1317
Fixes A-1318

@spalladino spalladino added the wip Work in progress label Jul 6, 2026
…-1315, A-1317, A-1318)

Protection stores (signing protection, and for the ordering fix world-state) must never be
silently emptied or bypassed by the store-creation path. Three related fixes:

- A-1315: DatabaseVersionManager now opens the database before writing the version marker, so
  the marker is a post-commit record. A crash between reset and a durable open leaves no marker,
  and the next start re-runs the reset instead of trusting a marker over empty data. writeVersion
  is now an atomic durable write (temp file, fsync, rename, best-effort dir fsync).

- A-1318: a version file that exists but cannot be read (permissions, IO error, truncation) no
  longer defaults to an empty version — which would reset the data directory. A new
  versionFileReadFailurePolicy ('reset' default, 'throw' for signing protection) makes the local
  signing-protection store refuse to open and leave data untouched on a transient read failure.
  This closes the gap left by the A-1029 schema-mismatch 'throw' policy.

- A-1317: createLocalSignerWithProtection now throws when no data directory is configured, since
  an ephemeral store drops all double-signing protection across restarts. Dev/test networks can
  opt in via VALIDATOR_ALLOW_EPHEMERAL_SIGNING_PROTECTION; the local network sets it by default.
@spalladino spalladino force-pushed the spl/a1315-a1317-a1318-db branch from e19a807 to 1a477f2 Compare July 7, 2026 12:31
@spalladino spalladino enabled auto-merge (squash) July 7, 2026 12:32
@spalladino spalladino removed the wip Work in progress label Jul 7, 2026
@spalladino spalladino merged commit 26ba4a7 into merge-train/spartan-v5 Jul 7, 2026
13 checks passed
@spalladino spalladino deleted the spl/a1315-a1317-a1318-db branch July 7, 2026 13:08
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