feat(dlv): the live market path binds, and stops at bound-but-unrealized (RE-OPENED — lost from #834's merge) - #836
Merged
Conversation
…nforce them on Step 4 requirement 3. 2c-B froze four conjuncts over MarketTerms field 6 and deferred them for a stated reason: their operand did not exist, and nothing was permitted to fabricate one to make a vector pass. The producer supplied the operand; this is the half that checks it, and the half a foreign verifier runs. G1 operation_bytes decode under DlvSettleOperationPreimageV1, all bytes consumed G2 canonical re-encode reproduces the carried bytes exactly G3 discriminator 26 and mode Unilateral G4 relationship_chain_tip_v2 over the carried inputs == trader_successor Placement follows the two rulings that constrain it rather than convenience. NOT the generic CCB decoder: the owner ruled the chain-tip recomputation is successor-evidence validity rather than byte decoding, and putting it there would make every decode of any bundle hash a foreign grammar. NOT the C4 walk: 2c-C4 says these gate the bundle's structural validity, not the economic walk, which treats the bytes as opaque. The sibling conjunct that IS in-bundle structure, embedded_parent == trader_parent, stays where it was, enforced at construction and decode. Errors are structured, one arm per conjunct, so the arm IS the finding. G4 carries both the carried and the recomputed successor, because "these differ" is the whole content of that refusal. The discriminator is read before decoding, so a close preimage arriving where a settle belongs reports as a wrong tag rather than as an undecodable blob. The producer now verifies its own output. G1-G4 hold there by construction, so the check can only fire if the construction stops being what it claims — which is exactly when a silent divergence would otherwise begin. It also means this is not a verifier sitting uncalled: a producer that cannot pass the check a foreign party runs has no business emitting a bundle. Teeth, including the two requirement 10 names: - a single altered operation byte is rejected. The test flips byte 10, inside vault_id, deliberately: the bytes stay decodable and canonically re-encodable so G1-G3 all pass and G4 is the conjunct actually under test. My first attempt flipped the last byte, which is mode, and proved G3 instead — the failure is worth recording because a mutation test that fires the wrong gate looks identical to one that works. - a supplied successor different from the recomputed one is rejected, and the refusal reports both values. - trailing bytes, a wrong discriminator, empty bytes, and a bilateral settle are each refused separately. The bilateral case re-derives the tip first so ONLY the mode is wrong, or G4 would fire and the test would prove nothing about G3. dsm dlv::market 15/15, ccb 18/18, conformance 12/12. make lint green; production safety checks green. Scope: this does NOT lift the market emission refusal. Requirements 1, 2 and 4-10 — the live bind path to bound-but-unrealized — are next, and realization stays unreachable until 2c-D.
Step 4, requirements 1, 2, 4, 5, 8 and part of 10. The refusal that stood here was
correct for exactly as long as its reason held: a canonical market bundle needs the
trader's prepared successor and its evidence, and no producer could make them. 5c-2
Step 2 built that producer, so the reason is spent and keeping the refusal would itself
have become the stale thing.
What the route now does, in this order and not another:
sign the settle the signature is INSIDE the bytes the chain tip hashes, so a
settle cannot be signed after the advance
prepare, purely no writes, no head install — it exists to learn the embedded
parent and the entropy the tip covers, both the device's own
produce prepare_market_successor RECOMPUTES the successor from those
exact bytes; market_terms runs G1-G4 over its own output
cross-check the producer's successor must equal the prepare's own. They
compute it by the same rule from the same inputs, so a
disagreement means one is not doing what it says, and binding a
successor this device would not advance to is worth refusing
hardest
derive the successor from the AUTHENTICATED V_n, by the rule every verifier applies
publish, fence, bind all inside bind_settlement, which publishes the canonical bundle
to a quorum of the vault's COMMITTED storage set and refuses
before any fence or binding round if that is not durable
Requirement 4 needed no new code: publication-before-binding with a quorum, and a failed
publication leaving no bind, no fence and no rival excluded, were already implemented
inside bind_settlement ahead of place_fence. The live path uses that ordering rather
than reimplementing it.
Requirement 5: the fence is keyed on the TRADER's own chain and parent, never the
vault's. A market settle consumes the trader's sovereign chain position; reusing the
close path's vault-keyed identity would fence the wrong thing. Both tests assert the
absence of a vault-keyed row, which is the checkable half of that.
Requirement 2 needed plumbing, because the operands died at a scope boundary. The
composition's owner identity and the vault's fee are now carried on SettleTerms, and the
checked amounts come from the re-simulation that proved them rather than a second
narrowing — the narrowing's own comment says a second site is how the difference gets
minted. AmmVerifyOutcome returns them instead of discarding them.
WHAT THIS DOES NOT LIFT. Realization. No balance moves, no head advances, no reserves
move, no receipt is published, and the fence is not released. Ruling R1 and V3 put the
acceptance witness, fence release, receipt publication and the realized frontier behind
2c-D, and nothing here reaches any of them. Binding a trade is not settling it.
The two tests that asserted the refusal now assert the end state instead, both halves
separately, because "it bound" and "it did not realize" fail in opposite directions.
The foreign-trader test is the one that matters most: a device holding no owner record
and no owner signature binds a trade against the owner's liquidity while the owner is
offline, and still cannot realize it.
A re-submission of the same trade is now accepted rather than refused, and that is
asserted: the generation is bound by that very trade, and a trader retrying after a
dropped response must not be told the vault is taken by someone else.
The three operator docs banded earlier said market settlement was deployment-blocked.
That is no longer true, so their banners are rewritten rather than left to mislead in
the opposite direction from before.
dsm_sdk: dlv_routes 37/37, vault_state_composition 22/22, route_commit_sdk 43/43,
settlement_bind 7/7, binding_occupancy 10/10, settlement_resume 2/2 — all serial.
make lint green; production safety checks green.
Still owed in Step 4: requirement 6's second half (running the accepted-successor and
economic-admission path after COMMIT), and the remaining requirement 10 controls. The
trader's advance is deliberately NOT committed here, so nothing it would credit exists
yet to be mistaken for realization.
cryptskii
added a commit
that referenced
this pull request
Sep 10, 2026
…ndle is READ (#838) An adversarial review of the wired Step 4 path against the ten requirements found two real gaps in requirement 3, both in work I wrote. Both are confirmed from source, not taken on the reviewer's word. FIRST: G1 was implemented as "decodes and consumes all bytes" and stopped there. 2c-B lists two more conjuncts in the same normative block — "every fixed-length semantic field has its required length" and "signature has the schema-1 SPX256f signature length" — and its hostile-case clause names the attack directly: An operation_bytes with trailing bytes after a valid decode, a wrong discriminator, or a short sigma — whose trader_successor was computed over those same bytes so the chain-tip equality passes — must be refused by the conjunct. Owed at implementation. It was owed and it was not paid. The decoder width-checks the digest-shaped fields and sigma via get_arr32, but reads vault_id, both public keys and the signature with an unbounded get_bytes. Those four are checked here or nowhere, and the grammar writes them with a length prefix, so a SHORT field re-encodes to exactly the bytes it came from: G2 passes, and G4 passes too whenever the attacker computed the successor over those same short bytes. A truncated settler key or signature was self-consistent. Two tests build precisely that: truncate the field inside operation_bytes, RECOMPUTE the tip over the result so G4 cannot be what fires, and require the width refusal. Mutation control: removing the width rule turns exactly those two red and nothing else, which is the evidence that G2 and G4 genuinely cannot catch it. SECOND: check_market_evidence had one non-test caller — the producer's own self-check. No consumer ran it. That was survivable while the market route refused; #836 lifted the refusal, so market bundles now reach consumers for the first time, and a producer that means harm simply does not self-check. A self-check gates nothing on the consuming side. It now runs in binding_occupancy, where a foreign bundle first becomes readable: fetched from the register, canonically decoded, hashed to the record's identity. That is exactly the point 2c-C4 §2.1 describes — "refused before any verifier reads it" — and the surrounding comment already said so about the checks beside it. A test binds a bundle that is perfect in every other respect (canonical, hashing to the record, naming this parent, right generation) and wrong only in its carried successor, and requires it to be Unresolvable/Invalid. Mutation control: removing the consuming-side call turns exactly that test red. Also from the review, smaller and all confirmed: - Three doc comments still said dlv.unlockRouted "refuses at emission", which stopped being true when #836 landed. Rewritten to say what each thing now is, including market_bundle_at, which is no longer a stand-in for a missing producer but a deliberate test shortcut past the trader advance. - The foreign-trader test asserted only the ABSENCE of a vault-keyed fence, which no fence at all would also satisfy. It now asserts the positive half too: the fence is on the foreign trader's own chain and is still CommittedAwaitingAcceptance after its advance. - A comment reading "NOTHING WAS EMITTED ... the refusal came before the first mutating op" sat directly above assertions that the trade bound and the trader advanced. - EvidenceInvalid::TrailingBytes is unreachable, because from_bytes refuses trailing input itself and a successful decode always re-encodes to the same length. Kept as a guard on both facts, but the docs now say it is unreachable and that a trailing-byte preimage reports Undecodable — rather than implying it fires. dsm dlv::market 17/17, conformance 12/12, settle provenance 15/15; dsm_sdk dlv_routes 37/37, binding_occupancy 11/11, settlement_bind 7/7, settlement_resume 2/2 — all serial. make lint green; production safety checks green. The review was INCOMPLETE and this is not a clean bill: 18 of its 39 agents died on usage credits, and an unverified finding is dropped rather than promoted, so an unknown number went unexamined. Two of requirement 10's controls also remain owed — publication below quorum driven through the route, and a rival bundle after COMMIT.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why this is being opened again
This work was already reviewed, verified and pushed, and it did not reach
main.#834's merge took commit
3f0e5db7— theG1–G4enforcement only. The second commit on that branch,e804e662, carrying the live market path, never appeared on the pull request object at all:pulls/834recordedhead.sha = 3f0e5db7and its commit list has exactly one entry.The push itself succeeded.
originstill holds the branch ate804e662— which is how this is recoverable at all. What failed is the thing this repository already documents:That is now three times.
My part in it: after pushing, I printed
git rev-parse HEAD— my local head — and treated it as evidence of what the PR would merge. It is not, and the repo rule says so in as many words. I did runcheck-pr-head-sync.sh 834, but in the same command as the PR body edit, and its output was cut off by the pipe. So the one check that would have caught this produced no visible result and I proceeded anyway.mainis not broken by this: it simply lacks the change, and the old refusal is still live there.What the change is
Unchanged from its review. The market emission refusal is lifted, and the route carries a trade through signing, a pure prepare, production, publication, the trader-parent fence and QuorumBind to a committed binding — stopping at bound-but-unrealized.
Requirement 4 needed no new code. Requirement 5 fences the trader's own chain, never the vault's. Requirement 2's operands are plumbed out of the composition block rather than re-derived.
Nothing realizes: no balance moves, no head advances, no reserves move, no receipt publishes, the fence is not released.
Verification
merge-treeconfirms it still merges cleanly onto currentmain, which has since taken #835.Re-verified before this PR was opened, and the head-sync check will be run on its own this time, with its output read.