Skip to content

fix(dlv): G1's missing width conjuncts, and G1-G4 enforced where a bundle is READ - #838

Merged
cryptskii merged 1 commit into
mainfrom
fix/g1-field-widths-and-consumer-side-enforcement
Sep 10, 2026
Merged

fix(dlv): G1's missing width conjuncts, and G1-G4 enforced where a bundle is READ#838
cryptskii merged 1 commit into
mainfrom
fix/g1-field-widths-and-consumer-side-enforcement

Conversation

@cryptskii

Copy link
Copy Markdown
Collaborator

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 confirmed from source, not taken on the reviewer's word.

1. G1 was missing two conjuncts the spec lists, and the attack it enables

I implemented G1 as "decodes and consumes all bytes" and stopped. 2c-B lists two more 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 sigmawhose 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 not paid.

The decoder width-checks the digest-shaped fields and sigma, but reads vault_id, both public keys and the signature with an unbounded read. Those four are checked here or nowhere — and the grammar writes them length-prefixed, so a short field re-encodes to exactly the bytes it came from. G2 passes. 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 — the evidence that G2 and G4 genuinely cannot catch it.

2. Nothing on the consuming side ran G1–G4

check_market_evidence had one non-test caller: the producer's own self-check. No consumer ran it.

Survivable while the route refused. #836 lifted that, 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. Exactly the point 2c-C4 §2.1 describes, "refused before any verifier reads it" — the surrounding comment already said so about the checks beside it.

A test binds a bundle perfect in every other respect (canonical, hashing to the record, naming this parent, right generation) and wrong only in its carried successor, and requires Unresolvable/Invalid.

Mutation control: removing the consuming-side call turns exactly that test red.

Also from the review

finding fix
three doc comments still said the route "refuses at emission" rewritten; market_bundle_at is no longer a stand-in for a missing producer but a deliberate shortcut past the trader advance
foreign-trader test asserted only the absence of a vault-keyed fence — which no fence at all would satisfy now asserts the positive half: the fence is on the foreign trader's own chain and still awaiting acceptance
a comment reading "NOTHING WAS EMITTED … the refusal came before the first mutating op" sat above assertions that the trade bound and the trader advanced corrected
TrailingBytes is unreachable kept as a guard, but the docs now say so and note that a trailing-byte preimage reports Undecodable

Verification

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; safety checks green.

This is not a clean bill

The review was incomplete: 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.

…ndle is READ

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.
@cryptskii
cryptskii merged commit db95606 into main Sep 10, 2026
16 checks passed
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.

1 participant