Skip to content

Extend byte equality to broadcast and initial subasset issuance - #215

Merged
droplister merged 1 commit into
mainfrom
harden/byte-equality-broadcast-subasset
Aug 3, 2026
Merged

Extend byte equality to broadcast and initial subasset issuance#215
droplister merged 1 commit into
mainfrom
harden/byte-equality-broadcast-subasset

Conversation

@droplister

Copy link
Copy Markdown
Member

Continues the verification architecture (#214, ADR-019): two more compose types move from field-by-field comparison to whole-message byte equality.

Broadcast

CBOR [timestamp, value, fee_fraction_int, mime_type, text] after type id 30. Two format facts checked against counterparty-core (ca2496d) rather than recalled:

  • value is always an 8-byte double. The API coerces the param with float() and core calls cbor2.dumps with default options — default, not canonical, meaning finite floats are never shortened to float16/32. Verified empirically under cbor2==5.9.0, the version core pins. The pack-side CBOR encoder gains float64 support (number = float, bigint = int) and refuses non-finite values, which core's compose rejects anyway.
  • The timestamp never reaches the packer's params. The form doesn't carry one; composeBroadcast stamps the wallet's own clock into the request. It is borrowed from the decoded message through the Observed channel, bounded to ≤ 1 hour ahead of the same clock that stamped it — an honest response echoes a timestamp taken moments earlier, while a substituted future timestamp settles a feed's open bets before their deadline (broadcast.py settles once timestamp >= deadline). verifyBroadcast applies the same bound on the field-comparison fallback, so a refused borrow doesn't become an allowance. An explicit timestamp=0 (server continues the feed from ledger state) is declined.

Initial subasset issuance

Nine-element CBOR layout under LR_SUBASSET (23), with two byte-level details that differ from the standard layout:

  • Flags are ints. Core's subasset branch writes 1 if divisible else 0 where the standard branch passes booleans — different bytes in CBOR.
  • The longname is compacted by a new base-68 encoder mirroring the existing decoder in unpack/messages/issuance.ts, emitting minimal big-endian bytes as core's canonical_subasset_compact gate requires.

The numeric asset id is drawn by random.randint at compose time — server-chosen, so it's borrowed from the composed message under a range guard pinning it to the space core draws from. Safety argument (verified in core's validate()): an id naming someone else's asset is consensus-rejected ("issued by another address"); an id naming an asset the user already owns degrades the transaction into a reissuance of the user's own asset to themselves. Neither pays an attacker, and the longname, quantity, flags and description all stay byte-compared.

Still deliberately declined to the field fallback: subasset reissuance (standard layout, ledger-resolved id), ord-inscription composes, and non-text MIME types (core restructures or hex-decodes their content) — the last now declined across standard issuance too, where the packer previously assumed UTF-8.

Oracles and tests

  • Compose oracle: broadcast cases with explicit timestamps, plus a subasset case that decodes core's response and borrows the random asset id from it exactly as production does — equality then proves every other byte.
  • Round-trip oracle: rebuilds recent on-chain broadcasts and subasset issuances (the issuance decoder now surfaces the wire's mime_type — CBOR layouts only, never invented — which the glue and display can both use).
  • Unit fixtures generated with cbor2==5.9.0 plus core's own compaction arithmetic, embedded as byte-exact expectations, alongside borrow-bound and decline tests.

Verification

  • tsc --noEmit clean; counterparty unit suite 638 passing.
  • Both oracles run against api.counterparty.io:4000: 49/49, including the new cases.
  • E2E locally, one file at a time: compose/broadcast/index.spec.ts (10/10), compose/issuance/index.spec.ts (10/10).

https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj

Broadcast packs as CBOR [timestamp, value, fee_fraction_int, mime_type,
text], with the value as an 8-byte double: core calls cbor2.dumps with
default options, which never shortens a finite float (verified under
cbor2==5.9.0, core's pin), so the encoder gains float64 support and
refuses non-finite values, which core's compose rejects anyway. The
timestamp never reaches the packer's params -- composeBroadcast stamps
the wallet's own clock into the request -- so it is borrowed from the
decoded message, bounded to at most an hour ahead of the same clock
that stamped it, because a substituted future timestamp settles a
feed's open bets before their deadline. verifyBroadcast applies the
same bound on the field-comparison fallback so a refused borrow does
not become an allowance.

Initial subasset issuance packs the nine-element CBOR layout under
LR_SUBASSET, with the flags as ints -- core's subasset branch writes
1/0 where the standard branch passes booleans -- and the longname
compacted by a new base-68 encoder mirroring the existing decoder,
emitting minimal big-endian bytes as core's canonical_subasset_compact
gate requires. The numeric asset id is drawn randomly by core at
compose time, so it is borrowed from the composed message under a
range guard; a wrong id cannot pay an attacker (someone else's asset
is consensus-rejected as "issued by another address", the user's own
degrades to a self-reissuance) while the longname, quantity, flags and
description all stay byte-compared. A subasset reissuance composes in
the standard layout with a ledger-resolved id and is still declined to
field comparison, as are ord-inscription composes and non-text MIME
types across issuance and broadcast, whose content core restructures
or hex-decodes.

The issuance decoder now surfaces the wire's mime_type (CBOR layouts
only, never invented), which display and the round-trip oracle both
use. Both nightly oracles cover the new types: the compose oracle
gains broadcast cases and a subasset case that decodes core's response
and borrows from it exactly as production does, and the round-trip
oracle rebuilds recent on-chain broadcasts and subasset issuances.
Unit fixtures were generated with cbor2 5.9.0 plus core's own
compaction arithmetic. All 49 pack tests pass against a live node;
the broadcast and issuance compose E2E specs pass locally.

Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
@droplister
droplister merged commit 95e6caf into main Aug 3, 2026
38 checks passed
@droplister
droplister deleted the harden/byte-equality-broadcast-subasset branch August 3, 2026 19:31
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