docs: propose the Zaino driving port design#1414
Conversation
This commit carries the driving-port design corpus, apart from any code, so the design can be judged on its own: the CONTEXT.md glossary that fixes the port's language, and the port's three architecture decision records. ADR 0001 decouples the mempool from chain state, retiring the stream-closes-on-tip-change idiom in favor of an explicit tip-change subscription. ADR 0002 sends payloads across the boundary as consensus-serialized bytes, typing only identifiers and locators from zaino-primitives. ADR 0003 makes snapshot pinning unconditional: an engine must retain a pinned view while any clone of the snapshot lives. The design was pressure-tested by a draft implementation and a structured review of that draft; the executable half lives on the docs/driving-port-design branch and is deliberately not part of this proposal. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
I think there are some incorrect statements in docs/driving-port/0001-decouple-mempool-from-chain-state.md that need to be addressed, I think we should also consider not tying zallet into a set configuration and providing a modular system for them to use.
| @@ -0,0 +1,28 @@ | |||
| # Decouple the mempool from chain state in Zaino's driving port | |||
|
|
|||
| Zallet today learns of a new block through a side effect: Zaino's mempool | |||
There was a problem hiding this comment.
This is not a side effect, it is an API that was specifically designed and agreed upon in both zallet API meetings and the lightclient working group calls. This is misleading.
| stream ends when the chain tip changes, and the sync loop treats that closure | ||
| as its only new-block signal (zcash/zallet, | ||
| `zallet-core/src/components/sync.rs:136`). We decided that the driving port | ||
| Zallet will consume from Zaino must not carry this coupling. The port serves |
There was a problem hiding this comment.
This is confusing to me, the mempool requires this functionality in some form, even if not for zallet then for the light-client interface. #1416 separates this functionality from the core mempool service so that users that dont want it are not forced to use it.
If we are separating the mempool from the ChainIndex / ChainView as discussed then why would we tie zallet into any specific configuration? It should be up to the zallet devs to decide whether they want just the MempoolService or the CoherentMempoolService.
| the protocol itself imposes no structural relation between them: a transaction | ||
| commits to no block hash. Consensus binds a transaction only to chain | ||
| *state* — shielded anchors, nullifier non-membership, transparent prevouts, | ||
| expiry height, and the consensus branch id — so a mempool view is coherent |
There was a problem hiding this comment.
I think there is a bit of confusion about the issue / bug that lead to the greater tip tracking we have, it is not that a transaction must be tied to a particular block hash, the issue is that the mempool must be correct for the chain being served.
The first problem is, if a reorg occurs and the mempool and non-finalised state are out of sync, transactions could be missing completely (not shown in mempool or chain), or shown twice (both in mempool and chain), the fix that #1416 adds for the coherentmempool (freeze until the two re-agree) fixes this race condition.
The second is that clients that use the get_mempool_stream method as intended see a separate race condition: non-finalised state serves the chain_tip to clients, they then call get_mempool_stream with the given chain tip, and if out of sync with the mempool, return none, also fixed in #1416.
| The port therefore provides an explicit chain tip-change subscription, and its | ||
| mempool capability is an independent stream whose view is tagged with the tip | ||
| it was validated against. Zallet composes the two: on a tip event it resyncs | ||
| and resubscribes to the mempool. This costs Zallet a sync-loop rewrite during |
There was a problem hiding this comment.
See above, this is not just a Zallet API, it is also a part of the lightclient interface.
|
|
||
| ## Considered Options | ||
|
|
||
| We rejected canonizing the closure idiom (keeping "stream ends on tip change" |
There was a problem hiding this comment.
No we have not. we must keep this behaviour, just separate from core mempool functionality.
| @@ -0,0 +1,106 @@ | |||
| # The Zallet driving port | |||
There was a problem hiding this comment.
Are we sure we want a single bespoke driving port for zallet? what are the benefits / disadvantages vs providing generic, functionality based driving ports?
Eg. providing MempoolService, ChainView, BlockChainSource, etc and letting Zallet use what they want how they want, rather than tying them into a set configuration.
What this proposes
This pull request presents the design of Zaino's driving port — the contract through which consumers drive Zaino — as a code-free proposal, so the design can be discussed and judged on its own rather than inside an implementation diff. It adds one directory,
docs/driving-port/, holding two kinds of document:CONTEXT.md— the glossary that fixes the port's language: thirteen terms (Driving port, Snapshot, Conformance kit, Block locator, Reported upgrades, Broadcast, Tip event, Fork point, Treestate, Transaction status, Spend status, Mempool view, Transient failure), each with the meanings we committed to and the near-synonyms we deliberately avoid.The port spans snapshot-pinned chain reads, an explicit chain tip-change subscription, a mempool stream that stands apart from chain state, and transaction broadcast. Zallet is its first driver; zainod-for-lightclients is its expected second.
The three decisions
zaino-primitives. Consumers already parse consensus bytes into their own types, and typing payloads withzcash_primitiveswould put Zaino's engines in version-lockstep with the wallet stack.Lapsedanswer engines may give when they have forgotten a pinned view) would export the engine's storage policy into every driver's sync loop.How the design was pressure-tested
A complete first-draft implementation was built alongside these documents and then put through a structured review: eight independent finder passes (three correctness angles, three cleanup angles, altitude, and conventions) followed by adversarial verification of all 35 deduplicated candidates. The draft — a contract crate with a scriptable in-memory mock and a conformance kit that is the executable half of this contract — lives on the
docs/driving-port-designbranch and is deliberately not part of this pull request.All ten ranked findings of that review are implemented in the draft. The ones that fed back into the design itself:
Open questions for this discussion
The review surfaced five design questions we deliberately left unresolved for this proposal:
zallet-driving-port, but the glossary defines the port as the contract through which consumers drive Zaino — a port belongs to the hexagon, not to its first adapter. Rename tozaino-driving-port?BlockId,Outpoint, and treestate types structurally duplicate existing workspace types (zaino-state'sBlockIndex, two existing outpoint shapes, andzaino-primitives' ownTreestate/TreeBytes). Should the shared vocabulary be hoisted intozaino-primitives, which both sides already depend on?BackendErrorcarries only a retry classification and a message; adapters must stringify structured transport errors. Adding asourcechain preserves structure but costs thePartialEq/Eqderives.pubwith no in-repo external caller — plausibly the right testing API for out-of-tree adapter tests, but formally wider than anything requires.Relationship to code
No code changes in this pull request. The reference draft (crate, mock, conformance kit, 33 passing tests) is on
docs/driving-port-designfor readers who want to see the contract executed; it will be proposed separately once this design settles.🤖 Generated with Claude Code