M3.13d: the version-seven transport - #214
Conversation
…wire The version-seven application could only be driven in-process, so no adapter could speak to it. The frame format is version one's and is reused unchanged: the header and all five request payloads carry no ledger-version meaning, so a second wire would differ from the first in nothing but its name while doubling the places a framing rule can be wrong. What version seven adds is the half that is genuinely its own. A finalized block carries a block identifier version one's does not, because an adapter that could not name the block it just executed could not tell a peer which one it agreed to, and its receipts are version seven's fifty-six octets rather than version one's forty-seven. Every response is validated on the way out rather than merely serialised. A receipt whose declared code and encoded result byte disagree, a mempool answer carrying a receipt, a response of the wrong type for its kind: each is refused rather than written, because the adapter on the other side has no ledger, no kernel, and no vectors, and cannot tell a wrong answer from a right one. The chain identity is converted explicitly rather than by loosening either type: `InitChainRequest` carries a tagged `ChainId`, and the tag is what stops a state root being passed where a chain identity belongs.
Accepting, framing, the duplicate-request-identifier rule, the shutdown descriptor, and the clean-EOF path are properties of the wire rather than of a ledger version, so the connection loop becomes one function over a dispatcher and `serve_connection` gains an overload taking an `ApplicationV7&`. The only version-specific step is which dispatcher a decoded request is handed to. The `V1` in `UnixSocketServerV1` is the wire's version rather than the ledger's, and the header now says so. Renaming it would touch version one's tests, its binary, and its adapter to express what one comment expresses exactly as well.
The transport is checked against bytes rather than against objects: every request is built as the octets an adapter would send, decoded by the shared wire, dispatched, and read back out of the response payload by a reader that refuses to finish with anything left over. The `carried` scenario's four contiguous blocks must reproduce their recorded roots and block identifiers, and each admitted input's framed code must be its own receipt's, decoded back through `decode_receipt`. The same frames also go over a real socket, which is the only thing that exercises the version-seven overload, and a client that closes cleanly must end the connection without an error. Two probes found missing tests rather than wrong ones. A dispatcher that ignored the application and always accepted a proposal passed, because the suite only ever sent proposals that should be accepted; a proposal one height ahead now has to come back as a vote against, sent before the block is staged so that only the rule under test can refuse it. And a socket handed to the wrong dispatcher aborted rather than failed, because an assertion inside the client block left the server thread unjoined and `std::thread`'s destructor called `std::terminate`; the block now captures the exception, lets the client's destructor close the socket so the server returns, joins, and rethrows. The registered directory name is short on purpose: `sun_path` caps a socket pathname near 108 octets.
ADR 0059 records what the slice declines to add and why: no new frame format, no second connection loop, and no rename of a class whose version suffix names the wire rather than the ledger. It records why every response is validated on the way out — the adapter cannot tell a wrong answer from a right one — and the four things still owed, including that there is no version-seven server binary and that the uptime schedule is still null.
VerificationPR run Local, before pushing: GCC 12 and Clang 14 at Four mutation probes, each made to fail on purpose first. Two found tests |
Closes #213.
M3.13c made a block a consensus engine decided into a block the store commits.
Nothing carried those calls, so no adapter could speak to it. This is the
transport, and most of the decision is what it declines to add.
The wire is version one's, reused unchanged
The 20-octet header, the seven message kinds, the seven wire errors, and all
five request payloads carry no ledger-version meaning — a height, a
transaction list, a byte budget, an app state, a raw transaction. A second frame
format would differ from the first in nothing but its name while doubling the
places a framing rule can be wrong.
The same argument makes the socket's connection loop one function over a
dispatcher: accepting, framing, the duplicate-request-identifier rule, the
shutdown descriptor, and the clean-EOF path are all properties of the wire, so
serve_connectionsimply gains an overload taking anApplicationV7&. TheV1in
UnixSocketServerV1is the wire's version, and the header now says so.What is genuinely version-specific
A finalized block carries a block identifier version one's does not, because
an adapter that could not name the block it just executed could not tell a peer
which one it agreed to. Its receipts are version seven's fifty-six octets with a
version field of 7, and its result codes are version seven's thirty-three.
Every response is validated on the way out rather than merely serialised. A
receipt whose declared code and encoded result byte disagree, a mempool answer
carrying a receipt, a finalized block with more results than it could have held,
a response of the wrong type for its kind — each is refused as
invalid_payload.The adapter on the other side has no ledger, no kernel and no vectors; it cannot
tell a wrong answer from a right one, so the encoder is the last place a
disagreement can be caught, and catching it costs one comparison per result.
InitChainRequest::chain_idis a taggedChainIdand therefore a distinct typefrom version seven's
Octets32. It is converted in one place rather than eithertype being loosened — the tag is what stops a state root being passed where a
chain identity belongs.
Evidence
The
carriedscenario's four contiguous blocks driven through the whole framepipeline as the octets an adapter would send, decoded by the shared wire,
dispatched, and read back out of the response payload by a reader that refuses
to finish with anything left over. Every block must reproduce its recorded
resulting_state_rootandblock_id; every response header must answer thekind and request identifier it was asked about with a declared payload size
equal to what arrived; and each admitted input's framed code must be its own
receipt's, decoded back through
decode_receipt.The same frames also go over a real socket — a server bound, a client
connected,
init_chainand a whole block driven through it — which is the onlything that exercises the version-seven overload. A client that closes cleanly
must end the connection without an error, because that is what an adapter
restarting looks like from this side.
Refusals are exercised on both sides of the boundary: an application error
becomes a status in a well-formed response frame, while a malformed frame
never reaches the application at all (a commit carrying a block payload, a
truncated frame, an unknown kind, a zero request identifier).
Four probes, two of which found missing tests
refusals.
passed. The suite only ever sent proposals that should be accepted. A
proposal one height ahead must now come back as a vote against — and must be
sent before the block is staged, or a staged block refuses it for a different
reason and the test would pass for the wrong one.
was a defect in the test: an assertion inside the client block left the server
thread unjoined and
std::thread's destructor calledstd::terminate, hidingthe message. The block now captures the exception, lets the client's destructor
close the socket so the server returns, joins, and rethrows.
Owed, recorded in ADR 0059
No version-seven server binary —
protocol-applicationis still version one's,and a version-seven one needs its genesis source, socket path, and shutdown
handling settled, which are operational questions. The Go ABCI adapter, which
must also answer the replay handshake ADR 0058 records. The uptime schedule,
still
nullptr. And thesun_pathbound, which is why the registered test'sdirectory name is short.
Verification
Local, before pushing: GCC 12 and Clang 14 at
-Wall -Wextra -Wpedantic -Werrorover every new and changed translation unit;
version-seven-transport,version-seven-application,application-v1, andapplication-unix-server-v1all passing — the last two because the connection loop they share was
refactored;
test_registration_test.pyandverify_metadata.pyclean;git diff --check main HEADclean. The hosted matrix is the gate for this exactcommit.