Skip to content

fix: seven browser banners that asserted what the page never computed - #610

Merged
Zyrtnin merged 5 commits into
mainfrom
fix/browser-banner-claims
Sep 4, 2026
Merged

fix: seven browser banners that asserted what the page never computed#610
Zyrtnin merged 5 commits into
mainfrom
fix/browser-banner-claims

Conversation

@Zyrtnin

@Zyrtnin Zyrtnin commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Stacked on #597. The public inspect page is the widest-audience surface here, and it is a pure renderer — the data was already in the payload and simply not consulted.

# banner what it actually computed
1 HIGH — burn: tokens "removed from circulation", "cannot reference the burned ref" protocol.includes("6") alone — an operator-supplied CBOR marker. GlyphMetadata has no field naming which ref is burned. Every sibling marker carries "not enforced by script"; BURN alone dropped it
2 "Reveal metadata" heading on multi-glyph reveals neither of_n_payloads nor metadata_inputs — the exact defect #577 fixed for the CLI. One observed mainnet reveal minted 35 refs from 36 inputs
3 commit-nft "every Glyph FT deploy carries" applied to every commit-nft row; a plain NFT mint produces one, and the page's own sibling banner said so
4 dMint deploy "all sharing the same token_ref", supply = reward × max_height × N a plain count; the three fields were on every row, never compared
5 dMint claim "the FT lives in a separate ft output" never consulted the ft count it had already tallied
6 OP_RETURN "Does NOT carry value" printed six lines under the row's own sats figure
7 homoglyph "visually mimic Latin letters" a pure non-Latin category test — any Japanese/Chinese/Arabic name tripped it and was told it mimicked Latin

Two further omissions of the same family surfaced: metadata.classification and metadata.timelock were computed and rendered nowhere — the page had a TIMELOCK banner saying "a time-based condition" and never said what it was.

#7 is now driven by the reason string it is handed, which also keeps it correct when #604's TR39 findings start landing in the same field.

The harness could not see any of this

inspect_render_harness.mjs had no way to exercise renderFetchedTxCard, so every whole-transaction claim above ran outside a browser for the first time. +50 tests, 15 built through the real glue.inspect_txid_with_raw, plus a tx-card structural guard mirroring the row one with its own non-vacuity check.

It re-attacked its own fix and found a defect in it

The new "This transaction has NO ft output" sentence reads outputs, which isn't always the whole tx — classify_raw_tx(only_vout=…) returns one row while output_count reports all four. So the fix announced a missing reward output that exists. The completeness test existed but gated only the shape verdict; it now gates both.

Four plants, each caught — including reverting the whole file, where 25 of 48 new tests fail and the passers are exactly the preserved-behaviour ones.

Verified independently: 11,051 passed, 192 skipped, 1 xfailed; inspect.js parses.

🤖 Generated with Claude Code

Mudwood Labs and others added 5 commits September 3, 2026 01:42
The browser page rendered NONE of the OP_RETURN payload fields. A forged
HashMark v2 showed as an authoritative-looking `OP_RETURN-HASHMARK-V2` badge
with no signer and NO VERDICT: the affirmative half of the record survived and
the part that contradicts it did not. Same defect I just fixed in the CLI, on
the surface with the widest audience and no install step.

The data was never the problem. glue.py re-uses the same classify_raw_tx the CLI
uses, so hashmark, attestation, message and relationships were all in the
payload, crossing the bridge, and discarded by the renderer.

`renderOutputRow` already carried the rule in a comment - "a field that only the
standalone-script card shows is a field most readers never see" - so this is the
third instance of the shape that comment was written about. There is now ONE
`appendOpReturnPayload` used by both cards rather than a second copy.

Two smaller things the rename broke, both found by reading rather than reported:
the title map and `_structuralQualifierNote` key on the literal "op_return", so
renaming the type to `op_return-hashmark-v2` silently dropped the card title AND
the "unspendable data carrier" note from exactly the outputs that had just
gained content.

WHY THE DRIFT GUARD DID NOT CATCH IT. tests/web asserts that every key the
classifier emits appears in the rendered text - which is structural about FIELDS
and hand-kept about SHAPES. The check only ever saw the payloads that
`_SHAPE_NAMES` produces, and no shape in that tuple produced a hashmark or a
message, so "every key must be rendered" was vacuously true for the keys that
had just been added. A hand-kept list of what a guard covers, for the third time
in this session.

Fixed at that level too: the corpus is now checked against the `type` values
recovered from the classifier's own source, both directions - a type with no
corpus shape means every test in that file is blind to it, and a corpus shape
whose type no longer exists is guarding nothing. The v2 shape carries a random
signature deliberately, so it decodes and does NOT verify: "does not verify" is
the line that was missing from this page entirely, and a corpus of only valid
records would not have demanded it.

The guard also now recurses into nested payload blocks rather than asserting the
repr of a dict, which no renderer could ever emit. Leaves rendered as prose are
listed with reasons, the same rule the top-level omission tables already use.

Also fixed: glue.py truncated `hex` and `data_hex` to 200 chars while inspect.js
told the reader "the JSON drawer carries the full bytes", so the drawer and the
Copy JSON button held a prefix, beside a header reporting the true byte count.
Newly material rather than untidy - `data_hex` is now the only place a HashMark
or msg record's raw bytes appear, and the UI points at it. The row stays
scannable because inspect.js truncates for DISPLAY at 64 chars on its own, which
is the right layer: only the display needs to be short.

Verified by planting three defects, each caught: removing the shared renderer
from either card (2 failures each), and forgetting to add the new shapes to the
corpus, which the coverage check names explicitly.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`_required_evidence` recurses into the nested payload blocks and skips leaves
listed in `_OMITTED_NESTED_KEYS`. If a block's every leaf ended up listed there,
it would demand nothing and the block would count as "rendered" while the card
showed none of it - the same shape as the corpus gap that let these fields go
unrendered in the first place, one level down. The omission table is the right
mechanism; it needed a floor.

Each nested block must now demand a minimum number of evidence strings, and two
specific leaves are named because they are the ones whose absence turns the
feature back into decoration: the DIGEST, which identifies which file was
marked, and the ATTESTATION DETAIL, which is the verdict's reason.

Measured before asserting: hashmark demands 5 evidence strings today and message
2, so the floors of 3 and 1 have real headroom rather than being set to whatever
happened to pass.

Verified by planting - listing every meaningful leaf as omitted fails 3.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Seven banners and notes on `docs/inspect_static/inspect/inspect.js` — the
widest-audience surface this project has — stated facts no code computed. The
data was in the payload every time and simply not read. A test can pin that a
sentence is emitted; nothing pins that it is TRUE, so these drifted in silence.

BURN (the worst): `protocol.includes("6")` — one operator-supplied integer in a
CBOR envelope — rendered as "tokens ... are removed from circulation; subsequent
transfers cannot reference the burned ref". Nothing read the outputs, and nothing
could: `GlyphMetadata` has no field naming which ref is burned. A tx can carry
protocol 6 and hand every ref straight back out to an ordinary FT output. Its
sibling markers (CONTAINER, ENCRYPTED, TIMELOCK, …) already said "purely a CBOR
metadata flag"; BURN was the one branch that dropped the caveat and the one
making the largest claim. It now says what the marker is.

Multi-glyph reveals (#577): `renderFetchedTxCard` rendered `payload.metadata`
under a bare "Reveal metadata" heading and read neither `of_n_payloads` nor
`metadata_inputs`, both of which the Python emits and the CLI prints. One
observed mainnet reveal mints 35 refs from 36 inputs, so 34 refs were shown
another token's name, ticker and media as if it were the transaction's. The
heading now says which glyph and of how many, and the others are listed by input.

commit-nft: "the NFT singleton anchor that every Glyph FT deploy carries … not a
separately-mintable collectible" was printed for EVERY commit-nft row. A plain
NFT mint produces one — `prepare_commit` sets `is_nft` from
`GlyphProtocol.NFT in metadata.protocol` and emits OP_2/SINGLETON — so the note
told a collectible's holder their commit output was somebody else's FT-deploy
artifact, contradicting this page's own banner for that shape. The note now
states what the bytes say; the FT-deploy framing stays on the branch that has
actually checked for a commit-ft beside it.

dMint deploy: "N contracts all sharing the same token_ref" and "total supply is
reward × max_height × N" came from a plain count of dmint rows, though every row
carries `token_ref_outpoint`, `reward` and `max_height`. It now compares them and
says which way it came out — a tx may carry dmint outputs for unrelated tokens.
(The agreement check requires each field PRESENT, so it cannot pass vacuously.)

dMint claim: "the freshly-minted FT lives in a separate ft output in this same tx"
and the canonical 4-output shape were asserted without consulting the ft count or
`outputs.length`. Both are now checked — the shape positionally — and a truncated
outputs list gets no verdict at all.

OP_RETURN: "Does NOT carry value", six lines under a header printing
`<satoshis> sats`. For a funded OP_RETURN the sentence is false and buries the
fact worth stating: no scriptSig can satisfy OP_RETURN, so those photons are
destroyed.

Homoglyph banner: "contains characters that visually mimic Latin letters", for
every entry in `display_warnings`. One producing branch — `glue.py`
`_suspicious_reason` — is a pure category test (every Letter is non-Latin,
therefore flag) with no confusability check in it, so any name in Japanese,
Chinese or Arabic was accused of mimicry. The banner now renders the reason it
was handed, which also keeps it correct when the TR39 skeleton check starts
landing findings in the same field.

Two omissions found while wiring the guard, same family: `metadata.classification`
and `metadata.timelock` were computed by the Python and rendered nowhere — the
page carried a banner saying a TIMELOCK reveal is "subject to a time-based
condition" and never said what the condition was. Both now render. No
unlocked/locked verdict: that needs a chain tip, as the Python and the CLI both
say.

Guard: `inspect_render_harness.mjs` gains a `tx` key, so `renderFetchedTxCard` —
where every whole-transaction claim above lives — is executed outside a browser
for the first time. The new cases go through the real browser entry point
(`glue.inspect_txid_with_raw`), which is where `display_warnings` is attached; a
hand-built dict would have exercised the renderer against a payload the browser
never produces. Each claim is pinned on BOTH sides of its new check, and
`_OMITTED_FROM_TX_CARD` plus a non-vacuity test keep the field guard from passing
by having nothing to iterate.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Re-attacking the previous commit: the new "This transaction has NO ft output"
sentence was computed from `outputs`, and `outputs` is not always the whole
transaction. `classify_raw_tx` takes an `only_vout` that returns ONE row while
`output_count` still reports the full count, so off a partial list the sentence
announces a missing reward output that exists and simply was not classified —
the same defect the sentence was written to remove, one revision later. The
completeness test was already there and gated only the shape verdict.

It now gates both, and the case is pinned: the canonical 4-output claim tx
classified at vout 0 alone, built through the real `classify_raw_tx` rather than
a hand-trimmed dict, asserting the card decides neither where the FT sits nor
whether the shape matches.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Three real integration issues that only merging surfaced, all fixed rather than
asserted around:

* `display_warnings` was emitted unconditionally by the classifier, so 'flagged'
  and 'checked and clean' were indistinguishable to a caller testing for the key.
  It now follows glue.py's contract - the key appears only when there is
  something to say - and the browser drift guard is what caught it.
* `signer_address` was added to the attestation payload and rendered nowhere in
  the browser. The structural guard caught that the moment the two branches met;
  the card now shows it.
* the mixed-script case is now reported by the TR39 skeleton check rather than by
  _suspicious_reason's heuristic. Both fire on it, the classifier's lands first
  and glue.py uses setdefault, so the more SPECIFIC reason wins - which is the
  intended precedence and the reason for wiring TR39 at all. The drift test's
  expectation is updated to match, with the reasoning in place.
@Zyrtnin
Zyrtnin merged commit 3c151e1 into main Sep 4, 2026
15 checks passed
@Zyrtnin
Zyrtnin deleted the fix/browser-banner-claims branch September 4, 2026 08:32
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