fix: seven browser banners that asserted what the page never computed - #610
Merged
Conversation
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.
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.
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.
protocol.includes("6")alone — an operator-supplied CBOR marker.GlyphMetadatahas no field naming which ref is burned. Every sibling marker carries "not enforced by script"; BURN alone dropped itof_n_payloadsnormetadata_inputs— the exact defect #577 fixed for the CLI. One observed mainnet reveal minted 35 refs from 36 inputsftcount it had already talliedTwo further omissions of the same family surfaced:
metadata.classificationandmetadata.timelockwere 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.mjshad no way to exerciserenderFetchedTxCard, so every whole-transaction claim above ran outside a browser for the first time. +50 tests, 15 built through the realglue.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 whileoutput_countreports 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.jsparses.🤖 Generated with Claude Code