Skip to content

M3.13d: the version-seven transport - #214

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

M3.13d: the version-seven transport#214
kaikisegfault merged 4 commits into
mainfrom
feat/213-version-seven-transport

Conversation

@kaikisegfault

Copy link
Copy Markdown
Owner

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_connection simply gains an overload taking an ApplicationV7&. The V1
in UnixSocketServerV1 is 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_id is a tagged ChainId and therefore a distinct type
from version seven's Octets32. It is converted in one place rather than either
type being loosened — the tag is what stops a state root being passed where a
chain identity belongs.

Evidence

The carried scenario's four contiguous blocks driven through the whole frame
pipeline 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_root and block_id; every response header must answer the
kind 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_chain and a whole block driven through it — which is the only
thing 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

  • Omitting the block identifier → caught by the recorded-identifier comparison.
  • Removing the encoder's code-versus-receipt check → caught by the encoder
    refusals.
  • A dispatcher that ignores the application and always accepts a proposal
    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.
  • A socket wired to the wrong dispatcher aborted rather than failed, which
    was a defect in the test: an assertion inside the client block left the server
    thread unjoined and std::thread's destructor called std::terminate, hiding
    the 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-application is 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 the sun_path bound, which is why the registered test's
directory name is short.

Verification

Local, before pushing: GCC 12 and Clang 14 at -Wall -Wextra -Wpedantic -Werror
over every new and changed translation unit; version-seven-transport,
version-seven-application, application-v1, and application-unix-server-v1
all passing — the last two because the connection loop they share was
refactored; 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.

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

Copy link
Copy Markdown
Owner Author

Verification

PR run 33438537070 on head 440c214 passed the complete hosted matrix — scope
classification full, GCC and Clang debug, both sanitizer presets, and the
aggregate Verification required check — with 150 of 150 ctest entries
passing in the debug presets and 158 of 158 under clang-sanitizers, up from
149 and 157. All four job logs confirm version-seven-transport running and
passing, which means the socket case ran under both sanitizers as well as the
plain builds.

Local, before pushing: GCC 12 and Clang 14 at -Wall -Wextra -Wpedantic -Werror over every new and changed translation unit; version-seven-transport,
version-seven-application, application-v1, and application-unix-server-v1
passing — the last two because the connection loop they share was refactored;
test_registration_test.py and verify_metadata.py clean; git diff --check main HEAD clean.

Four mutation probes, each made to fail on purpose first. Two found tests
that did not exist rather than tests that were wrong, and both fixes are in this
branch: a proposal one height ahead that must come back as a vote against, and a
socket test that reports a failure instead of aborting the process on it.

@kaikisegfault
kaikisegfault merged commit 115c295 into main Aug 31, 2026
6 checks passed
@kaikisegfault
kaikisegfault deleted the feat/213-version-seven-transport branch August 31, 2026 21:06
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.13d: the version-seven transport

1 participant