feat(economic): the beta root register is pinned to the register histories the real fleet holds - #771
Merged
Merged
Conversation
…ories the real fleet holds #770 gave a vault's birth commitment `(member_id, register_incarnation_id)` pairs, but the network-wide root register still derived its set id from whatever candidate the catalog offered, constrained only to the right member ids. "Who chose the incarnation values?" answered "the resolved catalog candidate" — and `derive_set_id(&candidate) -> Result<[u8; 32]>` returned the id derived from its own input, which both call sites then used as the expected id. A checker that returns what it derived hands the authority to its input. `RootRegisterProfile` now carries `storage_set_id` as a pinned commitment, and `verify_candidate` replaces `derive_set_id` at every call site: the candidate's membership must be the network's AND the id re-derived from its pairs must equal the pin. A member that rebuilt its register passes the first and fails the second, and is refused (`SetIdIsNotThePinnedOne`) rather than silently resolved to a different register. Endpoints remain transport metadata. The pin is the three real `(member, incarnation)` pairs, not an opaque digest: member list and set id both derive from the one source, so they cannot drift, and an auditor can read which register histories are trusted. `pinned_root_register_members()` is public — a commitment, not a secret — and every fixture builds from it, so nothing holds parallel values that must match. Before the values existed the placeholder was all-zero, which the encoder already refuses; an unprovisioned network fails closed by construction (`ProfileNotDerivable(ZeroRegisterIncarnation)`), not by anyone remembering to check. The values came off the provisioned nodes in the owner's order. Each member: forensic `pg_dump`, `DROP SCHEMA public CASCADE`, boot on merged `f00d1e0c` with `[storage_set]` absent, mint and log the incarnation. Then the collected pairs were written as `[[storage_set.members]]` to every node and to the client config, and every node restarted into the set logging the identical id `E05YS8101EJH33KY2CG625JJE8A0Z4GJNSEM335TX1XVTWM9RR8G` with no refusal. Two tests pin this end to end: the pinned bytes render to exactly the strings each node logged (cross-checked against the raw `register_incarnation` rows), and core's derivation equals the id every node logged. Mutation control: disabling the pin comparison turned `one_member_rebuilding_its_register_changes_the_set_id` red (`got Ok(())` — the rebuilt member was accepted); restored on an identical tree stamp. Not changed, surfaced: no node sets `node.network_id`, so the faucet-ticket register is inactive fleet-wide until it is configured. A fail-open in `main.rs` (optional config + a defaulted `database.url` that quietly points at localhost) is filed separately; it turned a wrong-image build into a crash loop during provisioning rather than a refusal. Verification (Rust 1.98.0, the version rust-toolchain.toml declares): - root `make lint` exit 0 - workspace board 3984 passed, 0 failed - node board (local-dev,strict / SQLite) 288 passed, 0 failed - node board (default features / Postgres 14) 258 passed, 0 failed - production_safety_checks (incl. TLA+), ci_scan, no_clock_and_no_json, check_forbidden_symbols all exit 0
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Item B.1 of the locked order: pin the root-register authority set, so D runs against a tree with no open authority assumption.
What #770 left open, and what this closes
#770 gave a vault's birth commitment
(member_id, register_incarnation_id)pairs, but the network-wide root register still derived its set id from whatever candidate the catalog offered, constrained only to the right member ids. The adversarial gate would rightly ask who chose the incarnation values? — and the answer was "the resolved catalog candidate."Now
RootRegisterProfilecarriesstorage_set_idas a pinned commitment, andverify_candidatereplacesderive_set_idat every call site — two conjuncts, neither sufficient alone:A member that rebuilt its register passes 1 and fails 2 → new refusal
SetIdIsNotThePinnedOne, never a silent resolution to a different register. Endpoints stay transport metadata.The pin is the three real
(member, incarnation)pairs, not an opaque digest. One source: the member list and the id both derive from them, so they cannot drift; an auditor reads which register histories are trusted.pinned_root_register_members()is public — a commitment, not a secret — and every fixture builds from it, so nothing duplicates values that must match.Unprovisioned fails closed by construction. The placeholder was all-zero, which #770 already refuses as the "not yet established" sentinel; confirmed
ProfileNotDerivable(ZeroRegisterIncarnation)before the values landed.Provisioning — the values came off the real nodes, in the order you set
Per node (
dsm-node-1/2/3):pg_dumpsnapshot (forensic only,pre-b1-wipe-*.sql.gz) →DROP SCHEMA public CASCADE→ boot mergedf00d1e0cwith[storage_set]absent → node mints and logs its incarnation → collect all three → write[[storage_set.members]]into every node and the client config → restart. Every node then logged the same set id,E05YS8101EJH33KY2CG625JJE8A0Z4GJNSEM335TX1XVTWM9RR8G, and no node refused (the #770 refusal for a config/DB incarnation mismatch was live throughout).dsm-node-1DXWR7W9J2E5ASQ5BJBYF13ZZEK1VFTZFYNWAYPF1KNT8C33YPVM0dsm-node-2H4ZSDG34M1BSQQH8T9WWWZ65Y90YW9QY2CYRR2EG3H621VDGJ3W0dsm-node-3VW3REAWA7PR608Y4AY3VX18M8BE4828PFPNVTG380XV18HKF8SSGTwo tests pin this end to end:
beta_root_register_pins_render_to_the_logged_values(the pinned bytes render to exactly those strings — also cross-checked against the rawregister_incarnationrows) andthe_pinned_beta_set_id_is_the_one_every_provisioned_member_logged(core's derivation equals the id every node logged).Mutation control
verify_candidatestops comparing against the pin →one_member_rebuilding_its_register_changes_the_set_idred (got Ok(())— the rebuilt member was accepted). Restored on an identical tree stamp.Verification (Rust 1.98.0)
make lint— exit 0production_safety_checks.sh(incl. TLA+),ci_scan.sh,no_clock_and_no_json.sh,check_forbidden_symbols.sh— all exit 0What went wrong on the way, stated
ci/docker/StorageNode.Dockerfile(distroless, noCMD) instead ofDockerfile.cloud. The binary defaulted to--config config.toml, found nothing, andmain.rssilently fell back tolocalhost:5432→ crash loop. Recovered by wrapping the already-compiled binary inDockerfile.cloud's runtime stage (trixie-slimto match the builder's glibc — the one deviation). Nothing was minted by the failed boot (schema still empty), so the wipe-then-mint ordering held.required(false)config + defaulteddatabase.url): a node that can't read its config should refuse, not quietly connect to localhost. Filed as a follow-up, out of this PR's scope.node.network_id, so all three logfaucet-ticket register is INACTIVE. Economic admission cannot complete on this fleet untilnetwork_id = "dsm-testnet"is configured. Your call; I did not change it.