Skip to content

M3.13e: decode_genesis and the version-seven node process - #217

Merged
kaikisegfault merged 4 commits into
mainfrom
feat/216-version-seven-node
Aug 31, 2026
Merged

M3.13e: decode_genesis and the version-seven node process#217
kaikisegfault merged 4 commits into
mainfrom
feat/216-version-seven-node

Conversation

@kaikisegfault

Copy link
Copy Markdown
Owner

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_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
.

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 about
what 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_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 — and a probe that swaps the two offsets fails.

The binary is version one's with three substitutions

protocol-application-v7 reads the genesis file, decodes it, opens or creates
the store, makes the application, binds a private Unix socket, and serves with a
signalfd for SIGINT and SIGTERM. --genesis-identity prints the chain
identity 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-process runs the built binary and checks it against
test-vectors/economy-transition-v7-execution.txt, which knows nothing about
sockets or processes:

  • --genesis-identity on the recorded genesis.bytes must print the recorded
    genesis.chain_id and an application hash equal to the height-zero root read
    out of the recorded carried.block0.header — a header commits to its
    previous state root, and at height 1 that is the genesis root, so the figure is
    a third source rather than a restatement.
  • A short genesis file and an empty one are refused rather than becoming chains.
  • A first run creates its database, reports protocol version 7 at height 0 with
    the recorded genesis root, and refuses a premature commit with
    sequence_failure in a well-formed frame rather than by breaking the
    connection.
  • A second run reopens what the first created, accepts init_chain over the
    wire with the same root, and refuses rubbish as malformed_transaction.
  • Both exit zero on SIGTERM, leave no socket behind, and hold the socket at
    mode 0600 while it exists.

The decoder's own evidence rides in economy-transition-v7-cpp: the round trip
field 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

  • A binary that always creates rather than reopening → the second run fails.
  • An identity mode that prints the chain identity where the application hash
    belongs → the recorded comparison fails.
  • A decoder that reads the two u64s in declaration order → the round trip
    fails.
  • A decoder that does not re-encode to check itself → the four file-level field
    refusals fail.
  • One probe passed and was right to. Widening the genesis file's size check
    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.
  • One probe passed and belonged elsewhere. Accepting version one's app state
    at init_chain is not visible here; it fails in version-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 assignment
and no seat accrues; the sun_path bound, which the binary does not check and
reports 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 -Werror
over every new and changed translation unit; economy-transition-v7-cpp,
economy-transition-v7-execution-cpp, version-seven-transport,
version-seven-application, and the new version-seven-headless-process all
passing; test_registration_test.py and verify_metadata.py clean;
git diff --check main HEAD clean. The hosted matrix is the gate for this exact
commit.

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

Copy link
Copy Markdown
Owner Author

Verification

PR run 33442267440 on head b388656 passed the complete hosted matrix — scope
classification full, GCC and Clang debug, both sanitizer presets, and the
aggregate Verification required check — with 151 of 151 ctest entries
passing in the debug presets and 159 of 159 under clang-sanitizers, up from
150 and 158. All four job logs confirm version-seven-headless-process running
and passing, so the binary is sanitizer-clean as a process, not only as a
translation unit.

The first run failed, and the reason is the useful part

Run 33441137560 on 611e7b6 failed in all four jobs with operator<=> and
std::span failing to parse inside headers that have been in the tree for
months. The cause was not those headers: protocol_application_server_v7 was
declared as a target and never added to PROTOCOL_STACK_TARGETS
, which is the
only place this project applies the C++ standard, the warning flags, -Werror,
and the sanitizers. It built at the compiler's default standard.

No local check could have caught it, and that is worth stating plainly: the
scratch harness passes -std=c++20 on every invocation, so a target missing the
standard compiles perfectly there. This is the same shape as the
-Wdangling-reference finding recorded in the handoff — the matrix is the gate
for a reason.

b388656 fixes it and adds the guard that makes it non-repeatable:
test_every_built_target_takes_the_project_build_flags 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, which
was checked rather than assumed.

Local, before pushing

GCC 12 and Clang 14 at -Wall -Wextra -Wpedantic -Werror over every new and
changed translation unit; economy-transition-v7-cpp,
economy-transition-v7-execution-cpp, version-seven-transport,
version-seven-application, and version-seven-headless-process all passing;
test_registration_test.py (now fourteen tests) and verify_metadata.py clean;
git diff --check main HEAD clean.

@kaikisegfault
kaikisegfault merged commit 8ffc2bd into main Aug 31, 2026
6 checks passed
@kaikisegfault
kaikisegfault deleted the feat/216-version-seven-node branch August 31, 2026 21:48
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.

M3.13e: decode_genesis and the version-seven node process

1 participant