M3.13e: decode_genesis and the version-seven node process - #217
Conversation
Version seven published `encode_genesis` with no inverse, so nothing could turn a genesis file into the struct the store opens a chain from. `decode_genesis` reads the 110 canonical octets and then re-encodes what it read, returning a genesis only when the result equals the input octet for octet. The validity rule is therefore stated exactly once, in the encoder: a decoder carrying its own copy of the four conditions would be a second opinion about what a genesis is, kept in step by discipline. Re-encoding also catches what a restatement would not — a field read at the wrong offset, a trailing octet, an unrecognised schema version. That last case is not hypothetical. `total_supply` is encoded before `fixed_transfer_fee` while the struct declares them the other way round, so a decoder reading in declaration order puts the fee where the supply belongs. The test uses a distinctive nonzero fee for exactly that reason; a zero fee would hide it. It changes no encoding, no state, and no accepted vector: nothing that existed before behaves differently.
`protocol-application` is version one's binary, so a version-seven application could not be started, connected to, or shut down. `protocol-application-v7` reads the genesis file, decodes it, opens or creates the store, makes the application, binds a private Unix socket, and serves in a loop with a signalfd for SIGINT and SIGTERM. `--genesis-identity` prints the chain identity and the height-zero application hash without touching a database, which are the two figures an operator puts into a consensus engine's configuration. Opening is attempted before creating, so a restart is the ordinary case rather than a special one, and a peer that hangs up badly loses its connection and nothing else: the application's terminal latch is what stops a node that has contradicted itself. `version-seven-headless-process` runs the built binary and checks it against the recorded vectors, which know nothing about sockets or processes. The identity mode must print the recorded chain identity and an application hash equal to the height-zero root read out of a recorded block header, since a header commits to its previous state root and at height one that is the genesis root. A short or empty genesis file is refused rather than becoming a chain. A first run creates its database and refuses a premature commit in a well-formed frame; a second run reopens it, accepts init_chain over the wire, and answers the same root. Both exit zero on SIGTERM and leave no socket behind.
ADR 0060 records why the decoder is defined as the encoder's inverse rather than carrying its own copy of the validity rule, what the round trip catches that a restatement would not, and why the size check on the genesis file is an allocation bound rather than a rule — which is why a probe that widened it passed and was right to. It records the four things still owed: the Go ABCI adapter and the replay handshake it must answer, the uptime schedule that is still null, the socket pathname bound the binary does not check, and the absence of fault injection through the process boundary.
`protocol_application_server_v7` was declared but never added to `PROTOCOL_STACK_TARGETS`, which is the only place the C++ standard, the warning flags, `-Werror`, and the sanitizer flags are applied. It built at the compiler's default standard, so `operator<=>` and `std::span` in the headers it includes stopped parsing, and the hosted matrix failed in all four jobs on a tree that compiled clean locally — because a local harness passes `-std=c++20` explicitly and can never reproduce it. The registration guard now requires every target the project builds itself to appear in that list, with the two imported dependency libraries named as the only exceptions. Removing the entry makes it fail.
VerificationPR run The first run failed, and the reason is the useful partRun No local check could have caught it, and that is worth stating plainly: the
Local, before pushingGCC 12 and Clang 14 at |
Closes #216.
A version-seven node is now a process: it can be started, connected to, and shut
down. The remaining piece before a running chain is the Go ABCI adapter.
The decoder is the encoder's inverse and checks itself
decode_genesisreads the 110 canonical octets and then re-encodes what itread, returning a genesis only when the result equals the input octet for
octet.
That makes the validity rule — a nonzero supply limit, a zero total supply, a
zero initial fee pool, no accounts — stated exactly once, inside
encode_genesis. A decoder carrying its own copy would be a second opinion aboutwhat a genesis is, kept in step by discipline. Re-encoding also catches what a
restatement would not: a trailing octet, an unrecognised schema version, and a
field read at the wrong offset.
That last case is not hypothetical.
total_supplyis encoded beforefixed_transfer_feewhile the struct declares them the other way round, so adecoder reading in declaration order puts the fee where the supply belongs. The
test uses a distinctive nonzero fee for exactly that reason — a zero fee would
hide it — and a probe that swaps the two offsets fails.
The binary is version one's with three substitutions
protocol-application-v7reads the genesis file, decodes it, opens or createsthe store, makes the application, binds a private Unix socket, and serves with a
signalfdforSIGINTandSIGTERM.--genesis-identityprints the chainidentity and the height-zero application hash without touching a database.
Opening is attempted before creating, so a restart is the ordinary case
rather than a special one. A peer that hangs up badly or speaks nonsense loses
its connection and nothing else — the application's own terminal latch is what
stops a node that has contradicted itself, and it is deliberately not the loop's
business.
Evidence
version-seven-headless-processruns the built binary and checks it againsttest-vectors/economy-transition-v7-execution.txt, which knows nothing aboutsockets or processes:
--genesis-identityon the recordedgenesis.bytesmust print the recordedgenesis.chain_idand an application hash equal to the height-zero root readout of the recorded
carried.block0.header— a header commits to itsprevious state root, and at height 1 that is the genesis root, so the figure is
a third source rather than a restatement.
the recorded genesis root, and refuses a premature
commitwithsequence_failurein a well-formed frame rather than by breaking theconnection.
init_chainover thewire with the same root, and refuses rubbish as
malformed_transaction.SIGTERM, leave no socket behind, and hold the socket atmode 0600 while it exists.
The decoder's own evidence rides in
economy-transition-v7-cpp: the round tripfield by field, the derived chain identity, a short file, a long file, a foreign
magic, an earlier schema version, and each of the four fields the encoder refuses
— every case mutated in the file rather than in the struct, because a file is
where they come from.
Probes
belongs → the recorded comparison fails.
u64s in declaration order → the round tripfails.
refusals fail.
from "exactly 110 octets" to "at most 4,096" broke nothing, because that check
is an allocation bound and not a validity rule — the rule lives only in the
decoder. The comment now says that rather than claiming a better error message.
at
init_chainis not visible here; it fails inversion-seven-application,which owns that rule, and re-running it there confirmed the coverage rather
than assuming it.
Owed, recorded in ADR 0060
The Go ABCI adapter and the replay handshake it must answer; the uptime schedule,
still
nullptr, so a chain run through this process writes no cycle assignmentand no seat accrues; the
sun_pathbound, which the binary does not check andreports only as a bind failure; and no fault injection through the process
boundary.
Verification
Local, before pushing: GCC 12 and Clang 14 at
-Wall -Wextra -Wpedantic -Werrorover every new and changed translation unit;
economy-transition-v7-cpp,economy-transition-v7-execution-cpp,version-seven-transport,version-seven-application, and the newversion-seven-headless-processallpassing;
test_registration_test.pyandverify_metadata.pyclean;git diff --check main HEADclean. The hosted matrix is the gate for this exactcommit.