Byte-verify ownership transfers and subasset reissuances; guard the borrow - #219
Merged
Merged
Conversation
…orrow An ownership transfer's message is byte-for-byte a reissuance: core carries the new owner only in an output paying transfer_destination, so declining to pack it verified nothing the standard layout could not already express. The message now packs -- a live oracle case confirms core composes identical data with and without the transfer -- and the output policy, which requires every output to be an address the request names, is what pins the ownership output. The same division of labor dispense already relies on. Subasset reissuances (update description, add supply) now byte-verify too, in the standard layout under an asset id borrowed from the composed message, since the longname resolves to its numeric id through the ledger. The field fallback is equally blind to a substituted id -- it compares the longname the user typed, which the response echoes faithfully -- so borrowing makes nothing worse and buys byte equality on everything the user authored. What the borrow now refuses, on both subasset paths: lock, reset and transfer. A substituted id can land the operation on a different numeric asset the user owns, and while issue-and-describe is recoverable there (destroy the supply, rewrite the description), those three are permanent against the wrong asset. This tightens the initial-issuance borrow shipped in the previous release, whose safety argument under-weighted the lock case; the analysis is recorded in the audit log, and the vector's real fix -- an independent ledger view or local composition -- is folded into the local-compose decision. Fixtures regenerated for the unlocked subasset shape; the compose oracle gains the transfer case. All 681 counterparty tests pass with the oracles live; the issuance, transfer-ownership and update-description E2E specs pass locally, one file at a time. Claude-Session: https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj
This was referenced Aug 3, 2026
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 #216. Closes two of the three deliberate byte-equality gaps, and tightens a borrow shipped in #215.
Ownership transfers now byte-verify
The decline was over-conservative. A transfer's message is byte-for-byte a reissuance — core carries the new owner only in an output paying
transfer_destination(issuance.pyreturns(source, [(destination, None)], data)). A new compose-oracle case proves it against a live node: core composes identical data with and without the transfer. So the message packs, equality covers every field, and the output policy — which already picks uptransfer_destinationviaaddressesNamedInand requires every output to be explained — is what pins the ownership output. Same division of labor dispense relies on.Subasset reissuances now byte-verify, under a borrowed id
Updating a subasset's description composes the standard layout under the ledger-resolved numeric id, which the request cannot determine. It's now borrowed from the composed message, like the initial issuance's random draw. The honest accounting: the field fallback is equally blind to a substituted id (it compares the longname the user typed, which the response echoes faithfully), so borrowing makes nothing worse and buys byte equality on the description, quantity, flags and MIME type.
The borrow refuses irreversible operations — on both subasset paths
A substituted id can land the operation on a different numeric asset the user owns (matching divisibility required; the range guard limits targets to numeric assets). For issue-and-describe the wrong outcome is recoverable — destroy the supply, rewrite the description. For lock, reset, and transfer it is permanent against the wrong asset, so the borrow refuses those and they stay on the field fallback: no blinder, but byte equality does not sign off on them.
This retroactively tightens #215's initial-issuance borrow, whose safety argument under-weighted the lock case. The full analysis is an addendum in the (deliberately uncommitted) audit log; the vector's real fix — an independent ledger view or local composition — is folded into the local-compose decision.
Verification
tsc --noEmitclean; all 681 counterparty tests pass with both oracles live againstapi.counterparty.io:4000, including the new transfer oracle case.issuance/index.spec.ts(10/10),transfer-ownership.spec.ts(3/3),update-description.spec.ts(3/3).https://claude.ai/code/session_01CcjnCrgosSeshymXLBxdGj