M3.13h: a version-seven chain through a real CometBFT process - #226
Conversation
Every recorded version-seven transaction is signed with a stand-in. The traces say so outright — a signature is an eight-octet counter padded to 64 octets, recorded in an oracle that verifies by exact-match lookup — and that is the right decision for a contract fixture: the model implements no cryptography and every message-binding claim stays testable. **It also means nothing in this repository could be broadcast to a node.** `protocol-application-v7` opens its store through `open_sqlite_ledger_v7`, whose verifier defaults to `protocol::v7::ed25519_verifier()`, so it would refuse every recorded input as `invalid_signature`. This is the first version-seven fixture that signs for real. **The model needed no change to accept it.** `execute_block` takes the signature oracle as an argument and only ever calls `verify(public_key, message, signature)`, so `Signer` — real Ed25519 through the pinned libsodium, keyed by seeds derived from labels — is a drop-in for the recorded table. It is version one's `tests/differential/cases.py` shape: build the transactions, run the same octets through the independent Python model, and learn what each block produces. **One transaction per block is a requirement rather than a simplification.** A state root commits to the whole block, so a fixture whose block holds four transactions can only be reproduced by getting all four into one block in one order, which broadcasting through a mempool does not give you. The chain is three contiguous blocks: two registrations, because a transfer to an unregistered recipient is refused, and then a confirmed transfer, because it is the first block that moves value and charges the fee, so a node that agreed to the first two and not the third is caught. `version-seven-chain-fixture` is the registered check, and it states what a node is being asked to reproduce: 110 canonical genesis octets, three contiguous heights, one input and one 56-octet version-seven receipt each with a SUCCESS result byte, four distinct state roots and three distinct block identifiers, byte-identical reproduction on a second build, and — the point of the whole thing — a trailing signature that is not a stand-in. **Six mutation probes, and the first is the argument for the slice.** Making `Signer.sign` issue stand-ins is refused by the model itself at admission with code 3, `invalid_signature`, which is the finding this fixture exists to answer, demonstrated rather than asserted. A stand-in shape spliced into a raw input, two blocks sharing a root, and a version-six receipt version are each caught by the check that names them. **One probe passed uncaught and was re-aimed**: a key that drifts only after the fifth derivation never reached the rebuild, because a rebuild derives exactly five keys; drifting from the first is caught, and the re-aimed probe reports how many times it ran so a later reader can see the mutation executed.
Nothing in this repository had ever run a version-seven chain. The kernel executed blocks, the store made them durable, the application reconciled the two, the transport carried them, the node process served them, and the adapter spoke ABCI — each against recorded vectors, none against a consensus engine. This is the first time a version-seven transaction is signed for real, broadcast to a CometBFT node, admitted by a mempool, proposed, finalized, and committed, with the engine required to report the state root the independent Python model says that block produces. **The claim is agreement between two implementations rather than self-consistency.** The fixture derives the chain identity, the height-zero root, and each block's root from the Python model; the binary derives the same two figures from the same genesis file through `--genesis-identity` and they must match; and the running node derives each block's root by executing the octets. Every comparison is one against the other. **Three things are checked at every height and they are different claims.** `/status` reports the app hash embedded in the latest header, which at height `H` is the state after `H - 1`; `/abci_info` reports the application's own durable head, which is the root this block produced; and `block_results` must publish the recorded block identifier as the `protocol_block` event the version-seven bridge emits — which is what proves the identifier ABCI has no field for survived the whole path rather than being decoded and dropped. **The third block is committed by a process that did not execute the first two.** Its root therefore depends on a state read back out of SQLite rather than one held in memory, which is the half of requirement 13's "through restart" a single run cannot show. The home is initialized twice before the first run, because a repeated initialization must exact-validate the version-seven genesis rather than silently rewrite it. `start_stack`, `stop_stack`, and `application_info` take a protocol version rather than being copied, and `inspect_identity` and `initialize_home` move out of version one's test into the shared harness so both versions drive the same three processes through one code path. `application_info` previously required the application to report version one; it now requires the version asked for, which catches a stack wired to the wrong binary.
ADR 0062 records why the slice that was going to emit octets built a fixture instead. **Every recorded version-seven transaction is signed with a stand-in**, and the ADR keeps the reason that is right — exact-match lookup makes every message-binding claim in the contract testable without the model implementing cryptography it would then have to be trusted about — beside the reason it is useless for a run: `protocol-application-v7` opens its store with `ed25519_verifier()` and would refuse every recorded input as `invalid_signature`. The rest is recorded with the reasoning a later session would otherwise have to rebuild: why two fixtures is not duplication, because they answer different questions; why one transaction per block is a requirement rather than a simplification; why three blocks and why those three; why every comparison in the run is one implementation against another rather than a value against itself; and why the restart is the third block rather than a separate case. Three things are owed rather than implied: the four-validator devnet is still version one, the uptime schedule is still `nullptr` so this chain executes correctly and pays nobody, and the fixture exercises three of fourteen kinds on purpose and should not grow into a second coverage claim without a reason. Three alternatives are recorded with why they lose, including the one this slice started as — emitting the recorded raw inputs — and giving the recorded traces real signatures, which would move every recorded root in both accepted version-seven vector files to buy a property only the integration run needs.
Hosted verificationRun 33504060503 on head A version-seven chain ran under a real CometBFT process on every one of them:
The new entry is registered and runs everywhere: 152 → 153 in the debug presets and 160 → 161 under The two existing integrations still pass on every preset, which is what proves The version-seven run costs about ten seconds per preset, the same as the other That the chain committed under both sanitizers matters more than the pass |
Closes #225.
The first time a version-seven transaction is signed for real, broadcast to a
CometBFT node, admitted by a mempool, proposed, finalized, and committed, with the
engine required to report the state root the independent Python model says that
block produces.
The finding this slice is built on
The handoff recorded the next step as "emit the recorded blocks' raw inputs into a
vector file". Checking that against the fixtures found it would produce a fixture
no chain can accept.
Every recorded version-seven transaction is signed with a stand-in.
simulation/economy_transition_v7/trace.pysays so outright — no signature iscomputed anywhere; a stand-in is an eight-octet counter padded to 64 octets,
recorded in an oracle that verifies by exact-match lookup — and
Signaturesintests/kernel/economy_v7_execution_fixture.hppissues byte-identical tokens so theC++ trace reproduces the model's exact bytes. That is the right decision for a
contract fixture: it makes every message-binding claim testable without the model
implementing cryptography.
But
protocol-application-v7opens its store throughopen_sqlite_ledger_v7,whose verifier defaults to
protocol::v7::ed25519_verifier(). It would refuseevery recorded input as
invalid_signature.So this is a fixture slice.
tests/integration/version_seven_chain.pyis the firstversion-seven fixture that signs for real — version one's
tests/differential/cases.pyshape, using the pinned libsodium. The model neededno change:
execute_blockalready takes the signature oracle as an argument andonly ever calls
verify(public_key, message, signature), so a libsodium-backedobject is a drop-in for the recorded table.
One transaction per block is a requirement
A state root commits to the whole block, so reproducing a recorded block that holds
four transactions requires all four to land in one block in the recorded order, and
broadcasting through a mempool does not give you that.
CreateEmptyBlocksisalready false, and version one's integration gets one transaction per block
precisely because it submits one at a time.
Three blocks: two registrations, because a transfer to an unregistered recipient is
refused, then a confirmed transfer, because it is the first block that moves value
and charges the fee — a node that agreed to two airdrops and then disagreed about a
fee would pass a two-block fixture.
What the run checks, and why each is a different claim
/status— the app hash embedded in the latest header, which at heightHisthe state after
H - 1;/abci_info— the application's own durable head, which is the root this blockproduced;
block_results— the recorded block identifier, published as theprotocol_blockevent M3.13g's bridge emits, which proves an identifier ABCI hasno field for survived the whole path rather than being decoded and dropped;
The chain identity and height-zero root are cross-checked between the model and
--genesis-identity, so every comparison is one implementation against another andnever a value against itself. The third block is committed by a process that did
not execute the first two, so its root comes from a state read back out of SQLite
— the half of requirement 13's "through restart" a single run cannot show.
Verification
Local, and unusually complete for a hosted-only area. The whole fixture and its
contract test were run against real Ed25519 on this machine: the system libsodium
is 1.0.18 against the repository's pinned 1.0.22, and Ed25519 is Ed25519, so a
scratch relaxation of the pin (never committed) exercised the real code path.
version-seven-chain-fixture's four checks — shape, distinctness, determinism,and real signatures — all pass.
Signer.signissue stand-ins is refused by themodel itself at admission with code 3,
invalid_signature— the findingdemonstrated rather than asserted. A stand-in spliced into a raw input, two
blocks sharing a root, and a version-six receipt version are each caught by the
check that names them. One probe passed uncaught and was re-aimed: a key that
drifts only after the fifth derivation never reached the executed path, because a
rebuild derives exactly five keys; drifting from the first is caught, and the
re-aimed probe reports how many times it ran.
python3 -B tests/tools/test_registration_test.py— passes after the CMake edit.python3 tools/verify_metadata.pyand thetests/toolsunittest suite — pass.git diff --check main HEAD— clean.One ctest entry is added,
version-seven-chain-fixture, so the suite goes from 152to 153 entries in the debug presets and from 160 to 161 under
clang-sanitizers.The integration run itself is hosted: it needs the built
protocol-application-v7,three Go binaries, the pinned CometBFT, and the pinned libsodium. Results recorded
below when the run reaches a terminal state.
Owed, and recorded in the ADR rather than implied
-protocol-versionits supervisor passes each bridge must move in one slice.nullptr. This chain executes correctly andpays nobody; three blocks open no cycle window, so the fixture is honest about
what it shows.
vectors already execute all fourteen; this is not a second coverage claim and
should not grow into one without a reason.