From 29c2297b82f2a6c70d16c4ad26c16cbeb77597da Mon Sep 17 00:00:00 2001 From: Luke Rohenaz Date: Fri, 4 Sep 2026 17:09:45 -0400 Subject: [PATCH 1/4] docs: propose BRC-185 1Sat Ordinal Collections --- README.md | 1 + SUMMARY.md | 1 + tokens/0185.md | 234 +++++++++++++++++++++++++++++++++++++++++++++++ tokens/README.md | 1 + 4 files changed, 237 insertions(+) create mode 100644 tokens/0185.md diff --git a/README.md b/README.md index 100a9bb..5029e6c 100644 --- a/README.md +++ b/README.md @@ -238,6 +238,7 @@ BRC | Standard 174 | [Consensus-Unique Name Tokens — Identity Binding and Verified Resolution](./tokens/0174.md) 176 | [BSV-21 — Validity Proofs](./tokens/0176.md) 177 | [Wallet-Enforced Expiry for `noSend` Actions](./wallet/0177.md) +185 | [1Sat Ordinal Collections](./tokens/0185.md) 190 | [Access Gates for Metanet Rooms](./apps/0190.md) 210 | [Derived Collectibles](./apps/0210.md) 218 | [Chat-Native Command Grammar for the Metanet](./apps/0218.md) diff --git a/SUMMARY.md b/SUMMARY.md index 11501cf..9b487dc 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -130,6 +130,7 @@ * [P1Sat Permission Scheme for Basket `1sat`](./tokens/0165.md) * [Consensus-Unique Name Tokens — Identity Binding and Verified Resolution](./tokens/0174.md) * [BSV-21 — Validity Proofs](./tokens/0176.md) +* [1Sat Ordinal Collections](./tokens/0185.md) * [Miner-Enforced Resale-Royalty Covenant Tokens (OP_PUSH_TX)](./tokens/0226.md) ## Overlays diff --git a/tokens/0185.md b/tokens/0185.md new file mode 100644 index 0000000..32625a0 --- /dev/null +++ b/tokens/0185.md @@ -0,0 +1,234 @@ +# BRC-185: 1Sat Ordinal Collections + +Open Protocol Labs (info@opl.dev) + +**Authors:** Luke Rohenaz (luke@opl.dev), David Case (dcase@opl.dev) + +**Contributors:** Kurt Wuckert Jr. (kurt@opl.dev), Michael Boyd (root@opl.dev), Dan Wagner (dan@opl.dev) + +## Abstract + +This BRC defines **collections** of 1Sat inscribed ordinals: a **collection** origin inscription and **collection item** inscriptions that reference it. + +The **user-minted** model (wallet / app mints) uses a [BRC-160](./0160.md) envelope on a 1-sat output, **MAP** metadata on the same output after `OP_RETURN`, and **Sigma** for issuer binding. + +The proposed **parent** path links an item through field 3 and an exact collection-origin spend. This can be useful when a separate publisher signature is unavailable. + +## Motivation + +Markets and wallets group ordinals into collections. Historical docs used MAP `subType` values `collection` / `collectionItem`. This BRC standardizes that MAP model with **Sigma** for issuer-bound membership for user-minted items, and documents how contract mints link items via parent when that path is unavailable. + +## Relationship to other documents + +| Concern | Document | +|---------|----------| +| 1-sat chains | [BRC-159](./0159.md) | +| Envelopes + basic MAP | [BRC-160](./0160.md) | +| Sigma wire format and verification | [BRC-183](https://github.com/opldotdev/BRCs/blob/codex/brc-183-sigma/scripts/0183.md) | +| Wallet storage / P1Sat | [BRC-147](./0147.md) / [BRC-165](./0165.md) | +| Historical docs | [1sat-ordinals collections](https://docs.1satordinals.com) | + +## Status and prior art + +This is a proposed verification profile for existing collection metadata. Parsing a collection claim is already implemented; full verification of both paths below is not established as universal deployed behavior. [BRC-159](./0159.md) defines token identity, [BRC-160](./0160.md) defines envelopes and parent spend proof, and this proposal adds collection vocabulary and issuer checks. [BRC-161](./0161.md), [BRC-162](./0162.md), [BRC-163](./0163.md) and [BRC-176](./0176.md) concern fungible BSV-21 tokens and do not establish collection membership. + +## Specification + +The key words MUST, SHOULD and MAY define conformance to this proposed profile. Existing unsigned metadata remains readable as a collection claim; it is not retroactively a verified issuer endorsement. + +This document defines one relationship: whether a given inscription is a **member** of a given collection. There are two ways to establish it, and a reader admits an output as a member if **either** succeeds. + +* **MAP path** — the item declares the collection in MAP and carries a Sigma signature from the collection's issuer. This is the user-minted case, where a publisher signs each item. +* **Parent path** — the item names the collection origin as its inscription parent and **spends** that origin in the mint transaction. The spend of the collection origin is the authorization in this path. + +Both paths are specified below, followed by the membership rule that reads them. + +### User-minted (MAP + Sigma) + +#### Same-output layout + +Collection and item mints put the inscription and MAP on **one** 1-satoshi output. For user-minted items, MAP follows the envelope after `OP_RETURN` ([BRC-160](./0160.md)). + +**User-minted shape:** + +``` +OP_FALSE OP_IF + "ord" + OP_1 "image/png" + OP_0 +OP_ENDIF + +OP_RETURN + 1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5 + SET + type "ord" + name "The Awesome Collection" + subType "collection" + subTypeData "{\"description\":\"…\"}" +| +``` + +**Item (absolute `collectionId`):** + +``` +… +OP_RETURN + 1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5 + SET + type "ord" + name "Item #3" + subType "collectionItem" + subTypeData "{\"collectionId\":\"\",\"mintNumber\":3}" +| +``` + +Lock/envelope order may vary as allowed by BRC-160. Collection and item mints on this path MUST carry a Sigma from the issuer, appended after MAP on the same output (`|` bitcom separator). + +#### Collection inscription + +MAP fields: + +| Key | Requirement | Value | +|-----|-------------|--------| +| `app` | MAY | Minting app id | +| `type` | MUST | `ord` | +| `name` | MUST | Collection display name | +| `subType` | MUST | `collection` | +| `subTypeData` | MUST | Stringified JSON (below) | + +**`subTypeData` (collection):** + +| Field | Requirement | Meaning | +|-------|-------------|---------| +| `description` | MUST | Short description | +| `quantity` | MAY | Declared size when known (string per MAP); omit if open-ended | +| `rarityLabels` | MAY | Issuer-defined rarity map (JSON) | +| `traits` | MAY | Issuer-defined trait schema (JSON) | + +Unknown keys are ignored. Inscription body SHOULD be an image type for previews. + +Mint the collection **before** items that reference it (unless using same-tx relative ids below). A verifier MUST establish that the referenced output is a collection origin with the collection metadata above. An unavailable transaction is unresolved, not proof that the collection does not exist. + +#### Collection item inscription + +MAP fields: + +| Key | Requirement | Value | +|-----|-------------|--------| +| `app` | MAY | Minting app id | +| `type` | MUST | `ord` | +| `name` | MUST | Item display name | +| `subType` | MUST | `collectionItem` | +| `subTypeData` | MUST | Stringified JSON (below) | + +**`subTypeData` (collection item):** + +| Field | Requirement | Meaning | +|-------|-------------|---------| +| `collectionId` | MUST | Collection origin outpoint (see forms below) | +| `mintNumber` | MAY | Position in the set | +| `rank` | MAY | Rank within the set | +| `rarityLabel` | MAY | Label from the collection's scheme | +| `traits` | MAY | Trait instances | +| `attachments` | MAY | Extra linked resources | + +#### `collectionId` forms + +`collectionId` identifies the collection **origin** outpoint. + +1. **Absolute** — an outpoint reference per [BRC-184](https://github.com/opldotdev/BRCs/blob/codex/brc-184-content-addressing/outpoints/0184.md) (`txid_vout`; dot form denotes the same decoded outpoint; preserve dot form at BRC-36 boundaries). +2. **Same-transaction relative** — `_N` where `N` is a decimal output index in **this** transaction (e.g. `_0` if the collection is output 0 of the same mint tx). + +Indexers MUST expand `_N` to `{this_txid}_N` using the item's transaction id before comparing or storing membership. Implementations in 1sat-stack use this rule (`NormalizeRelativeOutpoint`). + +Top-level MAP key `collectionId` (outside `subTypeData`) is **not** used for membership; only `subTypeData.collectionId` under `subType=collectionItem`. + +#### Issuer authenticity (Sigma) + +User-minted collection and item mints use **Sigma** so the signature is bound to this output's script / transaction. + +Each valid Sigma signer covering the collection metadata is an eligible **collection issuer** in this profile. Signatures that do not cover the membership-bearing fields MUST NOT authorize membership. Item membership on the MAP path requires a valid Sigma on the item from **that same signer** (see [Collection membership](#collection-membership)). + +Sigma wire format and verification are defined in [BRC-183](https://github.com/opldotdev/BRCs/blob/codex/brc-183-sigma/scripts/0183.md). + +#### Wallet filing (informative) + +Collectables use storage basket `1sat` under [BRC-147](./0147.md). [BRC-165](./0165.md) defines P1Sat permission requests; `p 1sat ` is a permission route, not the storage basket. A membership claim or overlay admission alone does not establish verified membership for wallet authorization. + +### Contract-minted (parent) + +A mint may use the [BRC-160](./0160.md) parent / child pattern when authorization is established by a collection-origin spend: + +1. The parent is the **collection origin** (the 1-sat collection inscription outpoint). +2. The item mint **spends that parent** as an input. +3. The item inscription includes **field 3 (parent)** set to that outpoint (encoding and spend proof per BRC-160). + +Field 3 without spending the parent is not a valid link. For collection membership via this path, the parent MUST be the collection origin. Authorization is whatever the contract enforces on that spend — not owner Sigma. + +This exact-origin path can authorize children in only the transaction that spends the origin. Once spent, that outpoint cannot be spent again. Continued minting from a successor would need an additional profile that proves successor-to-origin ancestry; this proposal does not silently treat a successor as the origin. A contract can authorize output creation and signatures through its design; there is no general Bitcoin prohibition on contract-created MAP. The distinction here is the evidence used for membership. + +In-envelope MAP fields are an extension permitted structurally by BRC-160, but their decoding and precedence are not established by the inspected collection parser. They are outside this profile's MAP verification path. A reader may expose such fields as extension metadata, without using them to override the post-`OP_RETURN` membership fields. + +### Collection membership + +An output is a **member** of a collection if **either** path fully succeeds: + +1. **MAP path** — all of: + * The same-output post-`OP_RETURN` MAP has `subType=collectionItem` and `subTypeData.collectionId` naming the collection origin (including `_N` expansion), and + * the item carries a **valid Sigma** whose signer is the **same** as a valid Sigma signer covering the collection metadata on the collection origin. + +2. **Parent path** — inscription field 3 names that collection origin and the mint spends that outpoint ([BRC-160](./0160.md)). + +Either path alone is enough. Writers typically use one (MAP + matching Sigma for user-minted items; parent for contract mints). Readers do not require both. + +MAP `collectionId` without a matching collection-issuer Sigma is **not** membership on the MAP path — only a claim. + +### Validation and failure states + +Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and the required collection fields; an arbitrary spent parent is not automatically a collection. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. + +Relative `_N` references use unsigned decimal output indexes with no leading zeros, bounded by uint32 and the actual output count; they cannot be expanded without the item's transaction. `quantity`, rank and mint number are descriptive: they neither enforce supply nor uniqueness. JSON values inside `subTypeData` retain their JSON types; they are not all MAP strings. + +A reader MUST distinguish a parsed claim, unresolved evidence, verified membership and failed verification. Missing source transactions, missing Sigma verification inputs or incomplete chain proofs produce unresolved evidence. A signature mismatch produces failed MAP-path verification; an independently valid parent path can still succeed. Verify the actual Sigma bytes and covered output prefix under BRC-183. Comparing signer strings without verifying signatures is insufficient. Exact signer-address equality is used; BAP rotation or issuer delegation requires an additional profile. + +Membership is assessed at inscription creation. It does not establish current ownership or transfer validity, which require BRC-159 tracking. Chain validation and reorganization handling remain necessary for accepted mint transactions. + +### Verification examples + +| Evidence | Result | +|----------|--------| +| Item names collection, matching valid covered Sigma on both | Verified MAP-path member | +| Item names collection, valid Sigma from unrelated signer | Claim; failed MAP path | +| Item names collection, collection transaction unavailable | Unresolved | +| Parent field names valid collection origin, exact origin spent in mint | Verified parent-path member | +| Parent field names origin, mint spends only its successor | Fails this profile's exact-origin path | +| `_2` names output 2 of same mint; output is a valid collection | Expand then verify Sigma; relative syntax alone is not authorization | + +## What this document does not cover + +* Full rarity percentage validation — indexer policy +* On-chain royalty enforcement +* OrdFS / streams +* Non-collection MAP subTypes +* (Sigma wire format — [BRC-183](https://github.com/opldotdev/BRCs/blob/codex/brc-183-sigma/scripts/0183.md)) + +## Security considerations + +* **False `collectionId`** — anyone can write the string; MAP-path membership also requires valid Sigma from the collection issuer. +* **Parent path** — trust is the spend of the collection origin under contract rules. +* **Order** — items referencing a non-existent collection are not members under strict indexing. +* **Relative ids** — `_N` is only valid in the mint transaction that contains output `N`. + +## Implementations + +* [docs.1satordinals.com](https://docs.1satordinals.com) +* [b-open-io/1sat-stack](https://github.com/b-open-io/1sat-stack) — `ParseCollection` extracts claims and expands relative ids. Its [item topic manager](https://github.com/b-open-io/1sat-stack/blob/589884a6a445a57b62e79057d27040660bdaec37/pkg/collection/topic_item.go) requires valid Sigma but explicitly does not match the collection issuer at ingest. Overlay admission is therefore not full verification under this proposal. +* [b-open-io/1sat-sdk](https://github.com/b-open-io/1sat-sdk) — collection tooling; a cross-implementation conformance claim for this proposed verification profile requires shared fixtures. + +## References + +1. [BRC-159](./0159.md) — 1Sat core +2. [BRC-160](./0160.md) — Inscriptions and basic MAP +3. [BRC-183](https://github.com/opldotdev/BRCs/blob/codex/brc-183-sigma/scripts/0183.md) — Sigma +4. [BRC-184](https://github.com/opldotdev/BRCs/blob/codex/brc-184-content-addressing/outpoints/0184.md) — Outpoint Content Addressing +5. 1Sat Ordinals metadata — diff --git a/tokens/README.md b/tokens/README.md index cf93efa..7ad46b1 100644 --- a/tokens/README.md +++ b/tokens/README.md @@ -23,4 +23,5 @@ BRC | Standard 165 | [P1Sat Permission Scheme for Basket `1sat`](./0165.md) 174 | [Consensus-Unique Name Tokens — Identity Binding and Verified Resolution](./0174.md) 176 | [BSV-21 — Validity Proofs](./0176.md) +185 | [1Sat Ordinal Collections](./0185.md) 226 | [Miner-Enforced Resale-Royalty Covenant Tokens (OP_PUSH_TX)](./0226.md) From f70787dcebd0b9ccebe7c64c75318fa6576be322 Mon Sep 17 00:00:00 2001 From: Luke Rohenaz Date: Fri, 4 Sep 2026 18:15:36 -0400 Subject: [PATCH 2/4] docs(collections): ground issuer verification in historical transactions --- tokens/0185.md | 107 +++++++++- tokens/media/0185-musashi-evidence.json | 105 ++++++++++ tokens/media/0185-verify-musashi.py | 257 ++++++++++++++++++++++++ 3 files changed, 459 insertions(+), 10 deletions(-) create mode 100644 tokens/media/0185-musashi-evidence.json create mode 100644 tokens/media/0185-verify-musashi.py diff --git a/tokens/0185.md b/tokens/0185.md index 32625a0..3963460 100644 --- a/tokens/0185.md +++ b/tokens/0185.md @@ -92,7 +92,7 @@ MAP fields: |-----|-------------|--------| | `app` | MAY | Minting app id | | `type` | MUST | `ord` | -| `name` | MUST | Collection display name | +| `name` | SHOULD (writers) | Collection display name; not an authorization condition | | `subType` | MUST | `collection` | | `subTypeData` | MUST | Stringified JSON (below) | @@ -100,9 +100,9 @@ MAP fields: | Field | Requirement | Meaning | |-------|-------------|---------| -| `description` | MUST | Short description | -| `quantity` | MAY | Declared size when known (string per MAP); omit if open-ended | -| `rarityLabels` | MAY | Issuer-defined rarity map (JSON) | +| `description` | SHOULD (writers) | Short description; not an authorization condition | +| `quantity` | MAY | Declared size when known; see descriptive metadata rules below; omit if open-ended | +| `rarityLabels` | MAY | Issuer-defined descriptive JSON; deployed object and array forms are preserved | | `traits` | MAY | Issuer-defined trait schema (JSON) | Unknown keys are ignored. Inscription body SHOULD be an image type for previews. @@ -117,7 +117,7 @@ MAP fields: |-----|-------------|--------| | `app` | MAY | Minting app id | | `type` | MUST | `ord` | -| `name` | MUST | Item display name | +| `name` | SHOULD (writers) | Item display name; not an authorization condition | | `subType` | MUST | `collectionItem` | | `subTypeData` | MUST | Stringified JSON (below) | @@ -132,14 +132,37 @@ MAP fields: | `traits` | MAY | Trait instances | | `attachments` | MAY | Extra linked resources | +#### Descriptive metadata compatibility + +Names, descriptions, quantities, ranks, mint numbers and rarity labels describe +an output; they do not authorize membership. Writers SHOULD emit a nonempty +top-level MAP `name` and a collection `description`. For display, readers MUST +prefer a nonempty top-level `name`, then a nonempty string `subTypeData.name`, +then the output's canonical outpoint. An absent or invalid descriptive field +MUST NOT invalidate an otherwise valid membership proof. + +When emitted, new `quantity`, `rank` and `mintNumber` values MUST be JSON +nonnegative integers no larger than `9007199254740991` (`2^53 - 1`). Readers MUST +also accept historical strings matching `0|[1-9][0-9]*` within that same bound. +Negative, fractional, padded, overflowing or wrong-type values are ignored for +display; they MUST NOT erase a valid `collectionId` or its authority evidence. +These values neither enforce supply nor establish uniqueness. Object and array +forms of `rarityLabels` are descriptive opaque JSON; readers MUST preserve +either form without making its shape a membership condition. + +These are interpretations for display and comparison. Readers MUST verify the +original signed script bytes; they MUST NOT rewrite metadata or reserialize +JSON before checking Sigma. JSON values inside `subTypeData` retain their JSON +types even though the outer MAP value is a string. + #### `collectionId` forms `collectionId` identifies the collection **origin** outpoint. -1. **Absolute** — an outpoint reference per [BRC-184](https://github.com/opldotdev/BRCs/blob/codex/brc-184-content-addressing/outpoints/0184.md) (`txid_vout`; dot form denotes the same decoded outpoint; preserve dot form at BRC-36 boundaries). -2. **Same-transaction relative** — `_N` where `N` is a decimal output index in **this** transaction (e.g. `_0` if the collection is output 0 of the same mint tx). +1. **Absolute** — exactly 64 hexadecimal txid characters, followed by `_` or `.`, followed by an index matching `0|[1-9][0-9]*` and bounded by uint32 (`4294967295`). No signs, whitespace, leading zeros or trailing characters are accepted. Normalize to lowercase `txid_vout` for comparison and indexing, without changing the signed source. Dot form denotes the same decoded outpoint; preserve dot form at BRC-36 boundaries. See [BRC-184](https://github.com/opldotdev/BRCs/blob/codex/brc-184-content-addressing/outpoints/0184.md). +2. **Same-transaction relative** — `_N` with the same canonical decimal uint32 index, identifying a different output in **this** transaction (e.g. `_0` when a later item output references collection output 0). The actual output must exist and be a collection origin; a reference to the item itself fails. -Indexers MUST expand `_N` to `{this_txid}_N` using the item's transaction id before comparing or storing membership. Implementations in 1sat-stack use this rule (`NormalizeRelativeOutpoint`). +Indexers MUST expand `_N` to `{this_txid}_N` using the item's transaction id before comparing or storing membership. Verification requires `N < output_count`, a distinct referenced output and the collection-origin checks; syntax alone does not establish those facts. `1sat-stack` implements relative expansion (`NormalizeRelativeOutpoint`), but that helper alone does not perform all these validation checks. Top-level MAP key `collectionId` (outside `subTypeData`) is **not** used for membership; only `subTypeData.collectionId` under `subType=collectionItem`. @@ -163,6 +186,12 @@ A mint may use the [BRC-160](./0160.md) parent / child pattern when authorizatio 2. The item mint **spends that parent** as an input. 3. The item inscription includes **field 3 (parent)** set to that outpoint (encoding and spend proof per BRC-160). +For the 36-byte parent form, serialize the display txid's 32 decoded bytes in +reverse order, followed by the output index as four little-endian bytes. +[BRC-160](./0160.md) is authoritative for parent encoding and spend proof. +This parent serialization must not be substituted for Sigma's input-hash +preimage, whose separate rules are in BRC-183. + Field 3 without spending the parent is not a valid link. For collection membership via this path, the parent MUST be the collection origin. Authorization is whatever the contract enforces on that spend — not owner Sigma. This exact-origin path can authorize children in only the transaction that spends the origin. Once spent, that outpoint cannot be spent again. Continued minting from a successor would need an additional profile that proves successor-to-origin ancestry; this proposal does not silently treat a successor as the origin. A contract can authorize output creation and signatures through its design; there is no general Bitcoin prohibition on contract-created MAP. The distinction here is the evidence used for membership. @@ -185,9 +214,9 @@ MAP `collectionId` without a matching collection-issuer Sigma is **not** members ### Validation and failure states -Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and the required collection fields; an arbitrary spent parent is not automatically a collection. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. +Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and object-valued `subTypeData`; an arbitrary spent parent is not automatically a collection. Names and descriptions are not required authorization fields. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string item `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. -Relative `_N` references use unsigned decimal output indexes with no leading zeros, bounded by uint32 and the actual output count; they cannot be expanded without the item's transaction. `quantity`, rank and mint number are descriptive: they neither enforce supply nor uniqueness. JSON values inside `subTypeData` retain their JSON types; they are not all MAP strings. +Relative `_N` references cannot be resolved without the item's transaction. The referenced output must be distinct from the item and within the actual output count. Invalid optional display metadata follows the compatibility rules above; invalid membership-bearing metadata fails verification. A reader MUST distinguish a parsed claim, unresolved evidence, verified membership and failed verification. Missing source transactions, missing Sigma verification inputs or incomplete chain proofs produce unresolved evidence. A signature mismatch produces failed MAP-path verification; an independently valid parent path can still succeed. Verify the actual Sigma bytes and covered output prefix under BRC-183. Comparing signer strings without verifying signatures is insufficient. Exact signer-address equality is used; BAP rotation or issuer delegation requires an additional profile. @@ -204,6 +233,57 @@ Membership is assessed at inscription creation. It does not establish current ow | Parent field names origin, mint spends only its successor | Fails this profile's exact-origin path | | `_2` names output 2 of same mint; output is a valid collection | Expand then verify Sigma; relative syntax alone is not authorization | +### Historical example and reproducible evidence + +The historical **Musashi** collection provides a pinned example: + +| Role | Origin outpoint | +|------|-----------------| +| Collection | `ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde_0` | +| Item | `34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9_0` | + +The item names that exact collection in `subTypeData.collectionId`. Both raw +transactions contain one-satoshi outputs with MAP before Sigma. Verification +of the raw script prefixes and compact signatures recovers the same public +key, `037da89fcbeab8c3fe819bb48eb46beada585f1a5b3532f3a729b24780138806dd`, +and signer address `12KP5KzkBwtsc1UrTrsBCJzgqKn8UqaYQq`. This establishes +cryptographically verified issuer binding for the supplied bytes, rather than +merely matching unverified signer strings. + +The root has top-level name `Musashi`, string quantity `"240"` and object +`rarityLabels`. The item's name `Musashi 42` exists only in `subTypeData.name`, +and its mint number is string `"42"`. These historical descriptive forms do +not defeat issuer binding under this profile. No historical string rank is +asserted by this example. + +[Evidence JSON](./media/0185-musashi-evidence.json) records the pinned +transaction IDs, metadata, signature bytes, prefix lengths and hashes. +[The Python verifier](./media/0185-verify-musashi.py) uses only the standard +library: it parses supplied raw transactions, recomputes their IDs and Sigma +preimages, checks the signed MAP and collection reference, and recovers and +verifies the secp256k1 signer. It requires the raw transactions and does not +treat the digest list alone as proof. It is a verifier for these fixtures, +not a general BRC-160/185 indexer or chain validator. + +The item bytes are pinned in [1sat-stack's fixture](https://github.com/b-open-io/1sat-stack/blob/589884a6a445a57b62e79057d27040660bdaec37/pkg/template/bitcom/testdata/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9.hex). +Raw transaction retrieval sources for the [root](https://api.whatsonchain.com/v1/bsv/main/tx/ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde/hex) +and [item](https://api.whatsonchain.com/v1/bsv/main/tx/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9/hex) +are also listed in the JSON. Save the hex strings as `item.hex` and `root.hex`, then run: + +```sh +python3 tokens/media/0185-verify-musashi.py --raw-item item.hex --raw-root root.hex +``` + +A parsed collection reference is a **claim**. The checks above establish +**cryptographic issuer binding**. Accepted on-chain membership additionally +requires chain inclusion, transaction validity and origin evidence under +BRC-159/160. The evidence and verifier make no assertion of current canonical +chain inclusion, current ownership or complete membership validation. + +### Fungible-token wrapper boundary + +A BSV-21 deployment inscription can also carry collection-item MAP and Sigma. In that composition, this profile assesses the deployment inscription's relationship to the collection. It does not grant membership automatically to every fungible unit or subsequent BSV-21 transfer output. Token supply and transfer validity continue to follow their own BRCs; collection-specific metadata belongs outside the generic token JSON. + ## What this document does not cover * Full rarity percentage validation — indexer policy @@ -225,6 +305,13 @@ Membership is assessed at inscription creation. It does not establish current ow * [b-open-io/1sat-stack](https://github.com/b-open-io/1sat-stack) — `ParseCollection` extracts claims and expands relative ids. Its [item topic manager](https://github.com/b-open-io/1sat-stack/blob/589884a6a445a57b62e79057d27040660bdaec37/pkg/collection/topic_item.go) requires valid Sigma but explicitly does not match the collection issuer at ingest. Overlay admission is therefore not full verification under this proposal. * [b-open-io/1sat-sdk](https://github.com/b-open-io/1sat-sdk) — collection tooling; a cross-implementation conformance claim for this proposed verification profile requires shared fixtures. +The SDK's standalone `mintCollectionItem({ ref })` creates only the item output, +so it cannot use `_N` to create sibling default content; `_0` would refer to +itself. Use an existing absolute content reference with that helper. Lower-level +multi-output ORDFS builders can support relative content references when the +corresponding content outputs are actually composed. This content-reference +limitation is separate from same-transaction collection IDs in this profile. + ## References 1. [BRC-159](./0159.md) — 1Sat core diff --git a/tokens/media/0185-musashi-evidence.json b/tokens/media/0185-musashi-evidence.json new file mode 100644 index 0000000..6cb2e0d --- /dev/null +++ b/tokens/media/0185-musashi-evidence.json @@ -0,0 +1,105 @@ +{ + "schema": "brc-185-musashi-evidence-v1", + "purpose": "Pinned raw-transaction and Sigma issuer-binding evidence; not full chain or membership validation.", + "sources": { + "item": "https://raw.githubusercontent.com/b-open-io/1sat-stack/589884a6a445a57b62e79057d27040660bdaec37/pkg/template/bitcom/testdata/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9.hex", + "root": "https://api.whatsonchain.com/v1/bsv/main/tx/ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde/hex", + "item_api": "https://api.whatsonchain.com/v1/bsv/main/tx/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9/hex" + }, + "records": [ + { + "role": "item", + "txid": "34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9", + "outputIndex": 0, + "satoshis": 1, + "scriptLength": 14256, + "contentType": "image/webp", + "map": { + "app": "ZoideNFT", + "type": "ord", + "subType": "collectionItem", + "subTypeData": "{\"name\":\"Musashi 42\",\"collectionId\":\"ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde_0\",\"mintNumber\":\"42\",\"rarityLabel\":\"RARE\"}", + "royalties": "[{\"type\":\"paymail\",\"destination\":\"1HjN545U6hJewz5JiQX7QbTNPNUyqxhaSf\",\"percentage\":0.1}]" + }, + "subTypeData": { + "name": "Musashi 42", + "collectionId": "ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde_0", + "mintNumber": "42", + "rarityLabel": "RARE" + }, + "sigma": [ + { + "algorithm": "BSM", + "declaredSigner": "12KP5KzkBwtsc1UrTrsBCJzgqKn8UqaYQq", + "vin": 0, + "resolvedVin": 0, + "sourceTXID": "ce616907bb8d19f1c3a4f7767863e41b258afb5204278e7fb9ef07bc711a6342", + "sourceVout": 3, + "inputBytesHex": "ce616907bb8d19f1c3a4f7767863e41b258afb5204278e7fb9ef07bc711a634203000000", + "inputHash": "03a45f2a9557c8623e292468ff3837d755997d2ef018abfd6e3e5254aeac1f80", + "signedPrefixLength": 14141, + "signedPrefixSHA256": "2b2e513080bfdfbd07a0b32b1b147253f8aeea8aa182847dc57f1a36912688dc", + "sigmaSeparatorOffset": 14141, + "separatorOpcode": 1, + "messageHash": "1ac4e472b4a8d5614e61197d515e691f5f02bd38b4139763d4c805ed724f09c4", + "bitcoinSignedMessageDigest": "8aac6522f6609d3feae6704b54580b5b6be4a094d9c575c5788892d2f28c1e64", + "compactSignatureHex": "1fa86118c68c274c244148dc9b6d79e4bc812dcfdfebea511cb714a54a2ab2c8fe74472aaf9c87dac70129263467d4601ddd3aeb145b2c204c3c3bb6f41bbcbcf7", + "compactSignatureBase64": "H6hhGMaMJ0wkQUjcm2155LyBLc/f6+pRHLcUpUoqssj+dEcqr5yH2scBKSY0Z9RgHd066xRbLCBMPDu29Bu8vPc=", + "recovery": 0, + "recoveredPublicKey": "037da89fcbeab8c3fe819bb48eb46beada585f1a5b3532f3a729b24780138806dd", + "recoveredAddress": "12KP5KzkBwtsc1UrTrsBCJzgqKn8UqaYQq" + } + ] + }, + { + "role": "root", + "txid": "ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde", + "outputIndex": 0, + "satoshis": 1, + "scriptLength": 1279928, + "contentType": "image/png", + "map": { + "app": "ZoideNFT", + "type": "ord", + "subType": "collection", + "name": "Musashi", + "subTypeData": "{\"description\":\"A collection inspired by the solitude of a Samurai.\",\"quantity\":\"240\",\"rarityLabels\":{},\"traits\":{}}" + }, + "subTypeData": { + "description": "A collection inspired by the solitude of a Samurai.", + "quantity": "240", + "rarityLabels": {}, + "traits": {} + }, + "sigma": [ + { + "algorithm": "BSM", + "declaredSigner": "12KP5KzkBwtsc1UrTrsBCJzgqKn8UqaYQq", + "vin": 0, + "resolvedVin": 0, + "sourceTXID": "d57877f8b326e280b06d314bb419b443762def4cd855f59c897c4d5231f67aa7", + "sourceVout": 1, + "inputBytesHex": "d57877f8b326e280b06d314bb419b443762def4cd855f59c897c4d5231f67aa701000000", + "inputHash": "759fcc7978324c256c3c46d5cfa07a11ce0d439e61e3f681a6384d5d92fa6ed6", + "signedPrefixLength": 1279813, + "signedPrefixSHA256": "9e7b74dfbefe93fb4bfba4da052d4293cb57eecaddad76dc32bc6d9a4f1283db", + "sigmaSeparatorOffset": 1279813, + "separatorOpcode": 1, + "messageHash": "7217c217737f7713a4c4cccef9d8953b43932bd5b7518ddca70b8ef67e3042b5", + "bitcoinSignedMessageDigest": "783aa27839cec0cf083cd7e6fc0a65808201bbe22f8270a36bc3a06c445950f8", + "compactSignatureHex": "2017c1831b041446e82eaa68249802bad86e6a184b8861e4ad7a78d04ebfa6d4ef4907463f945917196d9eb4b5e2a7b1e738f856f3abd4e47b401221ecd499264a", + "compactSignatureBase64": "IBfBgxsEFEboLqpoJJgCuthuahhLiGHkrXp40E6/ptTvSQdGP5RZFxltnrS14qex5zj4VvOr1OR7QBIh7NSZJko=", + "recovery": 1, + "recoveredPublicKey": "037da89fcbeab8c3fe819bb48eb46beada585f1a5b3532f3a729b24780138806dd", + "recoveredAddress": "12KP5KzkBwtsc1UrTrsBCJzgqKn8UqaYQq" + } + ] + } + ], + "interpretation": { + "claim": "Item subTypeData.collectionId names the pinned root output.", + "issuerBinding": "Both compact signatures over the actual covered MAP prefixes recover the same signer; verifier requires raw transactions.", + "descriptiveCompatibility": "Item name is nested; root quantity and item mintNumber are canonical decimal strings. Root rarityLabels is an object. These descriptive forms do not invalidate issuer binding.", + "chainValidation": "Not performed by this fixture verifier; chain inclusion, transaction validity, origin status and ownership require separate evidence." + } +} diff --git a/tokens/media/0185-verify-musashi.py b/tokens/media/0185-verify-musashi.py new file mode 100644 index 0000000..03a023f --- /dev/null +++ b/tokens/media/0185-verify-musashi.py @@ -0,0 +1,257 @@ +#!/usr/bin/env python3 +"""Verify the pinned Musashi raw transactions and Sigma issuer binding. + +Python 3.8+ standard library only; no network access or SDK imports. This is a +fixture verifier, not a general inscription parser, script interpreter, SPV +client or ownership tracker. Raw hex files (or JSON-quoted hex) are required. +Public retrieval URLs are in 0185-musashi-evidence.json beside this script. +""" + +import argparse +import hashlib +import json +from pathlib import Path + + +P = 2**256 - 2**32 - 977 +N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 +G = ( + 55066263022277343669578718895168534326250603453777594175500187360389116729240, + 32670510020758816978083085130507043184471273380659243275938904335757337482424, +) +MAP = b"1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5" + + +def require(condition, message): + if not condition: + raise ValueError(message) + + +def sha(data): + return hashlib.sha256(data).digest() + + +def add(a, b): + if a is None: + return b + if b is None: + return a + x, y = a + X, Y = b + if x == X and (y + Y) % P == 0: + return None + slope = ((3 * x * x) * pow(2 * y, -1, P) if a == b + else (Y - y) * pow(X - x, -1, P)) % P + q = (slope * slope - x - X) % P + return q, (slope * (x - q) - y) % P + + +def mul(k, point): + require(k >= 0, "negative scalar") + result = None + while k: + if k & 1: + result = add(result, point) + point = add(point, point) + k >>= 1 + return result + + +def address(public_key): + payload = b"\x00" + hashlib.new("ripemd160", sha(public_key)).digest() + payload += sha(sha(payload))[:4] + alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" + value, encoded = int.from_bytes(payload, "big"), "" + while value: + value, digit = divmod(value, 58) + encoded = alphabet[digit] + encoded + return "1" * (len(payload) - len(payload.lstrip(b"\x00"))) + encoded + + +def recover(compact, message): + require(len(compact) == 65 and 27 <= compact[0] <= 34, "compact signature") + header = compact[0] - 27 + recovery, compressed = header & 3, bool(header & 4) + r, s = int.from_bytes(compact[1:33], "big"), int.from_bytes(compact[33:], "big") + require(1 <= r < N and 1 <= s < N, "signature scalar range") + x = r + (recovery >> 1) * N + require(x < P, "recovery x range") + square = (x**3 + 7) % P + y = pow(square, (P + 1) // 4, P) + require(y * y % P == square, "recovery point is not on curve") + if y & 1 != recovery & 1: + y = P - y + R = (x, y) + require(mul(N, R) is None, "recovery point order") + require(len(message) == 32, "Sigma message length") + digest = sha(sha(b"\x18Bitcoin Signed Message:\n\x20" + message)) + z = int.from_bytes(digest, "big") + Q = mul(pow(r, -1, N), add(mul(s, R), mul((-z) % N, G))) + require(Q is not None, "recovered infinity") + w = pow(s, -1, N) + check = add(mul(z * w % N, G), mul(r * w % N, Q)) + require(check is not None and check[0] % N == r, "ECDSA verification") + public_key = (bytes([2 + (Q[1] & 1)]) + Q[0].to_bytes(32, "big") if compressed + else b"\x04" + Q[0].to_bytes(32, "big") + Q[1].to_bytes(32, "big")) + return public_key, digest + + +class Reader: + def __init__(self, data): + self.data, self.pos = data, 0 + + def take(self, size): + require(0 <= size <= len(self.data) - self.pos, "truncated bytes") + result = self.data[self.pos:self.pos + size] + self.pos += size + return result + + def integer(self, size): + return int.from_bytes(self.take(size), "little") + + def compact_size(self): + first = self.integer(1) + if first < 253: + return first + size = {253: 2, 254: 4, 255: 8}[first] + value = self.integer(size) + require(value >= {253: 253, 254: 65536, 255: 4294967296}[first], + "noncanonical CompactSize") + return value + + +def transaction(raw): + reader = Reader(raw) + reader.take(4) # version + count = reader.compact_size() + require(0 < count <= len(raw) // 41, "invalid legacy input count") + inputs = [] + for _ in range(count): + inputs.append((reader.take(32)[::-1], reader.integer(4))) + reader.take(reader.compact_size()) + reader.take(4) # sequence + count = reader.compact_size() + require(0 < count <= len(raw) // 9, "invalid output count") + outputs = [] + for _ in range(count): + value = reader.integer(8) + outputs.append((value, reader.take(reader.compact_size()))) + reader.take(4) # locktime + require(reader.pos == len(raw), "trailing transaction bytes") + return sha(sha(raw))[::-1].hex(), inputs, outputs + + +def script_ops(script): + reader, result = Reader(script), [] + while reader.pos < len(script): + start, opcode = reader.pos, reader.integer(1) + size = opcode if 1 <= opcode <= 75 else 0 + if opcode in (76, 77, 78): + size = reader.integer({76: 1, 77: 2, 78: 4}[opcode]) + data = reader.take(size) if opcode <= 78 else None + result.append((start, opcode, data)) + return result + + +def unique_object(pairs): + result = {} + for key, value in pairs: + require(key not in result, "duplicate JSON key") + result[key] = value + return result + + +def metadata_and_sigma(script): + ops = script_ops(script) + depth, tape_start, content_type = 0, None, None + for i, (_, opcode, data) in enumerate(ops): + if data == b"ord" and i >= 2 and ops[i - 1][1] == 99 and ops[i - 2][1] == 0: + require(ops[i + 1][1] == 81, "fixture content-type tag") + content_type = ops[i + 2][2].decode("utf8") + if opcode in (99, 100): + depth += 1 + elif opcode == 104: + depth -= 1 + require(depth >= 0, "unbalanced fixture conditionals") + elif opcode == 106 and depth == 0: + tape_start = i + 1 + break + require(tape_start is not None and content_type is not None, "fixture envelope/tape") + segments, segment = [], [] + for op in ops[tape_start:]: + require(op[2] is not None, "fixture tape must be push-only") + if op[2] == b"|": + segments.append(segment) + segment = [op] # retain exact separator byte offset + else: + segment.append(op) + segments.append(segment) + require(len(segments) == 2, "expected one MAP and one Sigma segment") + fields = [op[2] for op in segments[0]] + require(fields[:2] == [MAP, b"SET"] and len(fields) % 2 == 0, "MAP SET shape") + values = unique_object([(fields[i].decode("utf8"), fields[i + 1].decode("utf8")) + for i in range(2, len(fields), 2)]) + sub = json.loads(values["subTypeData"], object_pairs_hook=unique_object) + require(isinstance(sub, dict), "subTypeData object") + sigma = segments[1] + require(len(sigma) == 6 and [op[2] for op in sigma[1:3]] == [b"SIGMA", b"BSM"], + "fixture Sigma shape") + return values, sub, content_type, sigma[0][0], [op[2] for op in sigma[3:]] + + +def verify_record(record, path): + text = path.read_text().strip() + if text.startswith('"'): + text = json.loads(text) + raw = bytes.fromhex(text) + txid, inputs, outputs = transaction(raw) + require(txid == record["txid"], record["role"] + " txid mismatch") + satoshis, script = outputs[record["outputIndex"]] + require(satoshis == record["satoshis"] == 1, "one-satoshi output") + require(len(script) == record["scriptLength"], "script length") + values, sub, media, offset, (signer, compact, vin_bytes) = metadata_and_sigma(script) + require(values == record["map"] and sub == record["subTypeData"], "pinned metadata") + require(media == record["contentType"], "pinned media type") + expected = record["sigma"][0] + vin = int(vin_bytes.decode("ascii")) + vin = record["outputIndex"] if vin == -1 else vin + require(0 <= vin < len(inputs), "Sigma input range") + txid_bytes, vout = inputs[vin] + preimage = txid_bytes + vout.to_bytes(4, "little") + input_hash, data_hash = sha(preimage), sha(script[:offset]) + message = sha(input_hash + data_hash) + require(offset == expected["signedPrefixLength"] == expected["sigmaSeparatorOffset"], + "signed prefix length") + for actual, key in [(preimage.hex(), "inputBytesHex"), (input_hash.hex(), "inputHash"), + (data_hash.hex(), "signedPrefixSHA256"), (message.hex(), "messageHash"), + (compact.hex(), "compactSignatureHex"), (signer.decode(), "declaredSigner")]: + require(actual == expected[key], key + " mismatch") + key, digest = recover(compact, message) + require(key.hex() == expected["recoveredPublicKey"], "recovered public key") + require(address(key) == signer.decode() == expected["recoveredAddress"], "signer address") + require(digest.hex() == expected["bitcoinSignedMessageDigest"], "BSM digest") + return txid, values, sub, signer.decode() + + +def main(): + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("--raw-item", required=True, type=Path) + parser.add_argument("--raw-root", required=True, type=Path) + args = parser.parse_args() + evidence = json.loads(Path(__file__).with_name("0185-musashi-evidence.json").read_text()) + records = {record["role"]: record for record in evidence["records"]} + item = verify_record(records["item"], args.raw_item) + root = verify_record(records["root"], args.raw_root) + require(root[1]["type"] == item[1]["type"] == "ord", "MAP type") + require(root[1]["subType"] == "collection" and item[1]["subType"] == "collectionItem", + "collection roles") + require(item[2]["collectionId"] == root[0] + "_0", "collection reference") + require(item[3] == root[3], "issuer equality") + print(json.dumps({"raw_transaction_ids": "verified", "signed_MAP_prefixes": "verified", + "issuer_binding": "verified", "signer": item[3], + "chain_inclusion": "not_checked", "full_membership": "not_checked", + "current_ownership": "not_checked"}, indent=2)) + + +if __name__ == "__main__": + main() From 2c4639512387bb4788ad7008f79fc7ddd3a03af7 Mon Sep 17 00:00:00 2001 From: Luke Rohenaz Date: Fri, 4 Sep 2026 19:19:17 -0400 Subject: [PATCH 3/4] docs(collections): retain original schema without legacy coercion --- tokens/0185.md | 82 +++++++++++++++++++++++++++++--------------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/tokens/0185.md b/tokens/0185.md index 3963460..f31c18c 100644 --- a/tokens/0185.md +++ b/tokens/0185.md @@ -92,7 +92,7 @@ MAP fields: |-----|-------------|--------| | `app` | MAY | Minting app id | | `type` | MUST | `ord` | -| `name` | SHOULD (writers) | Collection display name; not an authorization condition | +| `name` | MUST | Collection display name (string) | | `subType` | MUST | `collection` | | `subTypeData` | MUST | Stringified JSON (below) | @@ -100,9 +100,9 @@ MAP fields: | Field | Requirement | Meaning | |-------|-------------|---------| -| `description` | SHOULD (writers) | Short description; not an authorization condition | -| `quantity` | MAY | Declared size when known; see descriptive metadata rules below; omit if open-ended | -| `rarityLabels` | MAY | Issuer-defined descriptive JSON; deployed object and array forms are preserved | +| `description` | MUST | Short description (string) | +| `quantity` | MAY | Declared size as a string, as in the published collection schema; omit if open-ended | +| `rarityLabels` | MAY | Rarity label-to-percentage object; see metadata rules below | | `traits` | MAY | Issuer-defined trait schema (JSON) | Unknown keys are ignored. Inscription body SHOULD be an image type for previews. @@ -117,7 +117,7 @@ MAP fields: |-----|-------------|--------| | `app` | MAY | Minting app id | | `type` | MUST | `ord` | -| `name` | SHOULD (writers) | Item display name; not an authorization condition | +| `name` | MUST | Item display name (string) | | `subType` | MUST | `collectionItem` | | `subTypeData` | MUST | Stringified JSON (below) | @@ -126,34 +126,44 @@ MAP fields: | Field | Requirement | Meaning | |-------|-------------|---------| | `collectionId` | MUST | Collection origin outpoint (see forms below) | -| `mintNumber` | MAY | Position in the set | -| `rank` | MAY | Rank within the set | +| `mintNumber` | MAY | JSON integer giving the position in the set | +| `rank` | MAY | JSON integer starting at 1; 1 is most rare | | `rarityLabel` | MAY | Label from the collection's scheme | | `traits` | MAY | Trait instances | | `attachments` | MAY | Extra linked resources | -#### Descriptive metadata compatibility - -Names, descriptions, quantities, ranks, mint numbers and rarity labels describe -an output; they do not authorize membership. Writers SHOULD emit a nonempty -top-level MAP `name` and a collection `description`. For display, readers MUST -prefer a nonempty top-level `name`, then a nonempty string `subTypeData.name`, -then the output's canonical outpoint. An absent or invalid descriptive field -MUST NOT invalidate an otherwise valid membership proof. - -When emitted, new `quantity`, `rank` and `mintNumber` values MUST be JSON -nonnegative integers no larger than `9007199254740991` (`2^53 - 1`). Readers MUST -also accept historical strings matching `0|[1-9][0-9]*` within that same bound. -Negative, fractional, padded, overflowing or wrong-type values are ignored for -display; they MUST NOT erase a valid `collectionId` or its authority evidence. -These values neither enforce supply nor establish uniqueness. Object and array -forms of `rarityLabels` are descriptive opaque JSON; readers MUST preserve -either form without making its shape a membership condition. - -These are interpretations for display and comparison. Readers MUST verify the -original signed script bytes; they MUST NOT rewrite metadata or reserialize -JSON before checking Sigma. JSON values inside `subTypeData` retain their JSON -types even though the outer MAP value is a string. +#### Metadata types and conformance + +This profile retains the published schema's required top-level string `name`, +required collection string `description`, optional string `quantity`, and +optional integer `mintNumber` and `rank` (`rank` starts at 1). A numeric string +is not an integer. Readers are not required to coerce values, recover names +from other fields, or accept malformed metadata. Optional fields may be +omitted; when present, they MUST have their specified types. + +For this profile, `rarityLabels` is a JSON object mapping labels to percentage +strings, using the published object example. If provided, its percentages MUST +sum to 100, as required by the original indexer rule. This does not enforce +supply or guarantee that actual mints follow the declared distribution. The +historical documentation calls this field “stringified JSON”; this profile +places the object inside the already stringified `subTypeData` object, rather +than adding a second serialization layer. It does not require accepting arrays +as an alternative. Other ambiguities in historical trait schemas are not +resolved by accepting arbitrary values as conforming. + +The [collection schema](https://docs.1satordinals.com/adding-metadata/collections), +[item schema](https://docs.1satordinals.com/adding-metadata/collectionitem-subtype) +and [base ord schema](https://docs.1satordinals.com/adding-metadata/ord-schema-type) +are the sources for these requirements. Those pages contain inconsistent +field-placement examples, and specify AIP signatures. This proposal explicitly +uses `subTypeData.collectionId` and Sigma for its MAP path; it does not claim to +be an unchanged transcription of the historical AIP profile. + +A valid signature establishes who signed particular bytes; it does not make +wrong-type or missing required metadata conforming. Implementations MAY expose +nonconforming historical records for inspection, but that is an application +choice and MUST NOT be reported as conformance to this profile. Verify the +original signed script bytes without rewriting metadata or reserializing JSON. #### `collectionId` forms @@ -214,9 +224,9 @@ MAP `collectionId` without a matching collection-issuer Sigma is **not** members ### Validation and failure states -Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and object-valued `subTypeData`; an arbitrary spent parent is not automatically a collection. Names and descriptions are not required authorization fields. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string item `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. +Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and object-valued `subTypeData`; an arbitrary spent parent is not automatically a collection. Collection and item metadata MUST satisfy the required fields and types above. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string item `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. -Relative `_N` references cannot be resolved without the item's transaction. The referenced output must be distinct from the item and within the actual output count. Invalid optional display metadata follows the compatibility rules above; invalid membership-bearing metadata fails verification. +Relative `_N` references cannot be resolved without the item's transaction. The referenced output must be distinct from the item and within the actual output count. Wrong-type optional metadata is nonconforming; invalid membership-bearing metadata fails verification. A reader MUST distinguish a parsed claim, unresolved evidence, verified membership and failed verification. Missing source transactions, missing Sigma verification inputs or incomplete chain proofs produce unresolved evidence. A signature mismatch produces failed MAP-path verification; an independently valid parent path can still succeed. Verify the actual Sigma bytes and covered output prefix under BRC-183. Comparing signer strings without verifying signatures is insufficient. Exact signer-address equality is used; BAP rotation or issuer delegation requires an additional profile. @@ -252,9 +262,11 @@ merely matching unverified signer strings. The root has top-level name `Musashi`, string quantity `"240"` and object `rarityLabels`. The item's name `Musashi 42` exists only in `subTypeData.name`, -and its mint number is string `"42"`. These historical descriptive forms do -not defeat issuer binding under this profile. No historical string rank is -asserted by this example. +and its mint number is string `"42"`. The string quantity matches the published type. The string mint number and +nested-only item name do not satisfy the published schema; the empty rarity +object does not satisfy its percentage-total rule. These signatures remain +useful cryptographic evidence, but these records are not positive metadata +conformance fixtures. No historical string rank is asserted by this example. [Evidence JSON](./media/0185-musashi-evidence.json) records the pinned transaction IDs, metadata, signature bytes, prefix lengths and hashes. @@ -286,7 +298,7 @@ A BSV-21 deployment inscription can also carry collection-item MAP and Sigma. In ## What this document does not cover -* Full rarity percentage validation — indexer policy +* Enforcement of actual mint frequencies against declared rarity percentages * On-chain royalty enforcement * OrdFS / streams * Non-collection MAP subTypes From 8a014b99c4ead225ff7c71924860e2ce6f557c83 Mon Sep 17 00:00:00 2001 From: Luke Rohenaz Date: Fri, 4 Sep 2026 19:45:03 -0400 Subject: [PATCH 4/4] docs: focus proposal on specification and review examples --- tokens/0185.md | 36 ++-- tokens/media/0185-verify-musashi.py | 257 ---------------------------- 2 files changed, 14 insertions(+), 279 deletions(-) delete mode 100644 tokens/media/0185-verify-musashi.py diff --git a/tokens/0185.md b/tokens/0185.md index f31c18c..dedd258 100644 --- a/tokens/0185.md +++ b/tokens/0185.md @@ -90,7 +90,7 @@ MAP fields: | Key | Requirement | Value | |-----|-------------|--------| -| `app` | MAY | Minting app id | +| `app` | MUST | Minting app id (string); descriptive, not authenticated app identity | | `type` | MUST | `ord` | | `name` | MUST | Collection display name (string) | | `subType` | MUST | `collection` | @@ -103,7 +103,7 @@ MAP fields: | `description` | MUST | Short description (string) | | `quantity` | MAY | Declared size as a string, as in the published collection schema; omit if open-ended | | `rarityLabels` | MAY | Rarity label-to-percentage object; see metadata rules below | -| `traits` | MAY | Issuer-defined trait schema (JSON) | +| `traits` | MAY | JSON object describing the trait schema; contents are application-defined | Unknown keys are ignored. Inscription body SHOULD be an image type for previews. @@ -115,7 +115,7 @@ MAP fields: | Key | Requirement | Value | |-----|-------------|--------| -| `app` | MAY | Minting app id | +| `app` | MUST | Minting app id (string); descriptive, not authenticated app identity | | `type` | MUST | `ord` | | `name` | MUST | Item display name (string) | | `subType` | MUST | `collectionItem` | @@ -129,12 +129,12 @@ MAP fields: | `mintNumber` | MAY | JSON integer giving the position in the set | | `rank` | MAY | JSON integer starting at 1; 1 is most rare | | `rarityLabel` | MAY | Label from the collection's scheme | -| `traits` | MAY | Trait instances | -| `attachments` | MAY | Extra linked resources | +| `traits` | MAY | JSON array of trait objects; contents are application-defined | +| `attachments` | MAY | JSON array of attachment objects; contents are application-defined | #### Metadata types and conformance -This profile retains the published schema's required top-level string `name`, +This profile retains the published schema's required top-level strings `app` and `name`, required collection string `description`, optional string `quantity`, and optional integer `mintNumber` and `rank` (`rank` starts at 1). A numeric string is not an integer. Readers are not required to coerce values, recover names @@ -143,13 +143,12 @@ omitted; when present, they MUST have their specified types. For this profile, `rarityLabels` is a JSON object mapping labels to percentage strings, using the published object example. If provided, its percentages MUST -sum to 100, as required by the original indexer rule. This does not enforce +sum to 100, as required by the original indexer rule. For this proposed profile, each percentage string MUST match `(?:0|[1-9][0-9]*)(?:\.[0-9]+)?` and represent a value from 0 through 100 inclusive. Sum exact decimal values, not binary floating-point approximations; signs, whitespace and exponent notation are invalid. The object MUST be nonempty. For example, `{"common":"3.00","rare":"97.00"}` is valid, whereas `{}` and `{"rare":"1e2"}` are not. This does not enforce supply or guarantee that actual mints follow the declared distribution. The historical documentation calls this field “stringified JSON”; this profile places the object inside the already stringified `subTypeData` object, rather than adding a second serialization layer. It does not require accepting arrays -as an alternative. Other ambiguities in historical trait schemas are not -resolved by accepting arbitrary values as conforming. +as an alternative. For `traits` and `attachments`, this profile fixes only the container types in the tables. Interpretation and validation of their contents belong to application schemas; no rarity, attachment availability or trait-schema conformance is implied. The historical trait documentation has conflicting array/object descriptions; the container choices here are explicit proposal decisions. The [collection schema](https://docs.1satordinals.com/adding-metadata/collections), [item schema](https://docs.1satordinals.com/adding-metadata/collectionitem-subtype) @@ -196,6 +195,8 @@ A mint may use the [BRC-160](./0160.md) parent / child pattern when authorizatio 2. The item mint **spends that parent** as an input. 3. The item inscription includes **field 3 (parent)** set to that outpoint (encoding and spend proof per BRC-160). +The parent item does not need collection-item MAP, `subTypeData.collectionId`, or Sigma under this path. The referenced collection origin MUST satisfy the collection metadata schema above, but it does not need Sigma for the parent path. The exact-origin spend supplies authorization. If the item also claims MAP-path conformance, evaluate that path and its metadata separately; malformed optional MAP does not manufacture or remove a valid parent-spend proof. + For the 36-byte parent form, serialize the display txid's 32 decoded bytes in reverse order, followed by the output index as four little-endian bytes. [BRC-160](./0160.md) is authoritative for parent encoding and spend proof. @@ -224,11 +225,11 @@ MAP `collectionId` without a matching collection-issuer Sigma is **not** members ### Validation and failure states -Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and object-valued `subTypeData`; an arbitrary spent parent is not automatically a collection. Collection and item metadata MUST satisfy the required fields and types above. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, a non-string item `collectionId`, duplicate JSON member names or conflicting membership-bearing MAP fields MUST NOT yield verified membership. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. +Both collection and item outputs MUST be valid 1-satoshi inscriptions under BRC-160. A claimed collection MUST have `type=ord`, `subType=collection` and object-valued `subTypeData`; an arbitrary spent parent is not automatically a collection. Collection-origin metadata MUST satisfy the collection schema above. MAP-path items MUST satisfy the item schema; parent-path items follow the separate requirements in the parent section. Parse `subTypeData` as a UTF-8 JSON object. Malformed JSON, duplicate JSON member names or conflicting membership-bearing MAP fields invalidate the affected MAP schema. A non-string item `collectionId` fails the MAP path; it does not invalidate an independently valid parent path. Unknown metadata is ignored. Writers SHOULD emit one MAP `SET` segment and unique keys for this profile. Relative `_N` references cannot be resolved without the item's transaction. The referenced output must be distinct from the item and within the actual output count. Wrong-type optional metadata is nonconforming; invalid membership-bearing metadata fails verification. -A reader MUST distinguish a parsed claim, unresolved evidence, verified membership and failed verification. Missing source transactions, missing Sigma verification inputs or incomplete chain proofs produce unresolved evidence. A signature mismatch produces failed MAP-path verification; an independently valid parent path can still succeed. Verify the actual Sigma bytes and covered output prefix under BRC-183. Comparing signer strings without verifying signatures is insufficient. Exact signer-address equality is used; BAP rotation or issuer delegation requires an additional profile. +A reader MUST distinguish a parsed claim, unresolved evidence, verified membership and failed verification. Missing evidence required by the evaluated path produces unresolved evidence; unavailable Sigma inputs affect only MAP-path verification. A signature mismatch produces failed MAP-path verification; an independently valid parent path can still succeed. Verify the actual Sigma bytes and covered output prefix under BRC-183. Comparing signer strings without verifying signatures is insufficient. Exact signer-address equality is used; BAP rotation or issuer delegation requires an additional profile. Membership is assessed at inscription creation. It does not establish current ownership or transfer validity, which require BRC-159 tracking. Chain validation and reorganization handling remain necessary for accepted mint transactions. @@ -270,21 +271,12 @@ conformance fixtures. No historical string rank is asserted by this example. [Evidence JSON](./media/0185-musashi-evidence.json) records the pinned transaction IDs, metadata, signature bytes, prefix lengths and hashes. -[The Python verifier](./media/0185-verify-musashi.py) uses only the standard -library: it parses supplied raw transactions, recomputes their IDs and Sigma -preimages, checks the signed MAP and collection reference, and recovers and -verifies the secp256k1 signer. It requires the raw transactions and does not -treat the digest list alone as proof. It is a verifier for these fixtures, -not a general BRC-160/185 indexer or chain validator. +The recorded intermediate values allow independent reproduction of the signing calculation from the raw transactions. They are cryptographic examples, not a general BRC-160/185 validator. The item bytes are pinned in [1sat-stack's fixture](https://github.com/b-open-io/1sat-stack/blob/589884a6a445a57b62e79057d27040660bdaec37/pkg/template/bitcom/testdata/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9.hex). Raw transaction retrieval sources for the [root](https://api.whatsonchain.com/v1/bsv/main/tx/ee8c1a403ad4d9396df261b96f53a30209c2ba419d0e9b41f4930a4602e72cde/hex) and [item](https://api.whatsonchain.com/v1/bsv/main/tx/34adf92c766e11a656d3ff3508df7b1a31405821bf734bc9bef9fb43fcf701f9/hex) -are also listed in the JSON. Save the hex strings as `item.hex` and `root.hex`, then run: - -```sh -python3 tokens/media/0185-verify-musashi.py --raw-item item.hex --raw-root root.hex -``` +are also listed in the JSON. A parsed collection reference is a **claim**. The checks above establish **cryptographic issuer binding**. Accepted on-chain membership additionally diff --git a/tokens/media/0185-verify-musashi.py b/tokens/media/0185-verify-musashi.py deleted file mode 100644 index 03a023f..0000000 --- a/tokens/media/0185-verify-musashi.py +++ /dev/null @@ -1,257 +0,0 @@ -#!/usr/bin/env python3 -"""Verify the pinned Musashi raw transactions and Sigma issuer binding. - -Python 3.8+ standard library only; no network access or SDK imports. This is a -fixture verifier, not a general inscription parser, script interpreter, SPV -client or ownership tracker. Raw hex files (or JSON-quoted hex) are required. -Public retrieval URLs are in 0185-musashi-evidence.json beside this script. -""" - -import argparse -import hashlib -import json -from pathlib import Path - - -P = 2**256 - 2**32 - 977 -N = 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141 -G = ( - 55066263022277343669578718895168534326250603453777594175500187360389116729240, - 32670510020758816978083085130507043184471273380659243275938904335757337482424, -) -MAP = b"1PuQa7K62MiKCtssSLKy1kh56WWU7MtUR5" - - -def require(condition, message): - if not condition: - raise ValueError(message) - - -def sha(data): - return hashlib.sha256(data).digest() - - -def add(a, b): - if a is None: - return b - if b is None: - return a - x, y = a - X, Y = b - if x == X and (y + Y) % P == 0: - return None - slope = ((3 * x * x) * pow(2 * y, -1, P) if a == b - else (Y - y) * pow(X - x, -1, P)) % P - q = (slope * slope - x - X) % P - return q, (slope * (x - q) - y) % P - - -def mul(k, point): - require(k >= 0, "negative scalar") - result = None - while k: - if k & 1: - result = add(result, point) - point = add(point, point) - k >>= 1 - return result - - -def address(public_key): - payload = b"\x00" + hashlib.new("ripemd160", sha(public_key)).digest() - payload += sha(sha(payload))[:4] - alphabet = "123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz" - value, encoded = int.from_bytes(payload, "big"), "" - while value: - value, digit = divmod(value, 58) - encoded = alphabet[digit] + encoded - return "1" * (len(payload) - len(payload.lstrip(b"\x00"))) + encoded - - -def recover(compact, message): - require(len(compact) == 65 and 27 <= compact[0] <= 34, "compact signature") - header = compact[0] - 27 - recovery, compressed = header & 3, bool(header & 4) - r, s = int.from_bytes(compact[1:33], "big"), int.from_bytes(compact[33:], "big") - require(1 <= r < N and 1 <= s < N, "signature scalar range") - x = r + (recovery >> 1) * N - require(x < P, "recovery x range") - square = (x**3 + 7) % P - y = pow(square, (P + 1) // 4, P) - require(y * y % P == square, "recovery point is not on curve") - if y & 1 != recovery & 1: - y = P - y - R = (x, y) - require(mul(N, R) is None, "recovery point order") - require(len(message) == 32, "Sigma message length") - digest = sha(sha(b"\x18Bitcoin Signed Message:\n\x20" + message)) - z = int.from_bytes(digest, "big") - Q = mul(pow(r, -1, N), add(mul(s, R), mul((-z) % N, G))) - require(Q is not None, "recovered infinity") - w = pow(s, -1, N) - check = add(mul(z * w % N, G), mul(r * w % N, Q)) - require(check is not None and check[0] % N == r, "ECDSA verification") - public_key = (bytes([2 + (Q[1] & 1)]) + Q[0].to_bytes(32, "big") if compressed - else b"\x04" + Q[0].to_bytes(32, "big") + Q[1].to_bytes(32, "big")) - return public_key, digest - - -class Reader: - def __init__(self, data): - self.data, self.pos = data, 0 - - def take(self, size): - require(0 <= size <= len(self.data) - self.pos, "truncated bytes") - result = self.data[self.pos:self.pos + size] - self.pos += size - return result - - def integer(self, size): - return int.from_bytes(self.take(size), "little") - - def compact_size(self): - first = self.integer(1) - if first < 253: - return first - size = {253: 2, 254: 4, 255: 8}[first] - value = self.integer(size) - require(value >= {253: 253, 254: 65536, 255: 4294967296}[first], - "noncanonical CompactSize") - return value - - -def transaction(raw): - reader = Reader(raw) - reader.take(4) # version - count = reader.compact_size() - require(0 < count <= len(raw) // 41, "invalid legacy input count") - inputs = [] - for _ in range(count): - inputs.append((reader.take(32)[::-1], reader.integer(4))) - reader.take(reader.compact_size()) - reader.take(4) # sequence - count = reader.compact_size() - require(0 < count <= len(raw) // 9, "invalid output count") - outputs = [] - for _ in range(count): - value = reader.integer(8) - outputs.append((value, reader.take(reader.compact_size()))) - reader.take(4) # locktime - require(reader.pos == len(raw), "trailing transaction bytes") - return sha(sha(raw))[::-1].hex(), inputs, outputs - - -def script_ops(script): - reader, result = Reader(script), [] - while reader.pos < len(script): - start, opcode = reader.pos, reader.integer(1) - size = opcode if 1 <= opcode <= 75 else 0 - if opcode in (76, 77, 78): - size = reader.integer({76: 1, 77: 2, 78: 4}[opcode]) - data = reader.take(size) if opcode <= 78 else None - result.append((start, opcode, data)) - return result - - -def unique_object(pairs): - result = {} - for key, value in pairs: - require(key not in result, "duplicate JSON key") - result[key] = value - return result - - -def metadata_and_sigma(script): - ops = script_ops(script) - depth, tape_start, content_type = 0, None, None - for i, (_, opcode, data) in enumerate(ops): - if data == b"ord" and i >= 2 and ops[i - 1][1] == 99 and ops[i - 2][1] == 0: - require(ops[i + 1][1] == 81, "fixture content-type tag") - content_type = ops[i + 2][2].decode("utf8") - if opcode in (99, 100): - depth += 1 - elif opcode == 104: - depth -= 1 - require(depth >= 0, "unbalanced fixture conditionals") - elif opcode == 106 and depth == 0: - tape_start = i + 1 - break - require(tape_start is not None and content_type is not None, "fixture envelope/tape") - segments, segment = [], [] - for op in ops[tape_start:]: - require(op[2] is not None, "fixture tape must be push-only") - if op[2] == b"|": - segments.append(segment) - segment = [op] # retain exact separator byte offset - else: - segment.append(op) - segments.append(segment) - require(len(segments) == 2, "expected one MAP and one Sigma segment") - fields = [op[2] for op in segments[0]] - require(fields[:2] == [MAP, b"SET"] and len(fields) % 2 == 0, "MAP SET shape") - values = unique_object([(fields[i].decode("utf8"), fields[i + 1].decode("utf8")) - for i in range(2, len(fields), 2)]) - sub = json.loads(values["subTypeData"], object_pairs_hook=unique_object) - require(isinstance(sub, dict), "subTypeData object") - sigma = segments[1] - require(len(sigma) == 6 and [op[2] for op in sigma[1:3]] == [b"SIGMA", b"BSM"], - "fixture Sigma shape") - return values, sub, content_type, sigma[0][0], [op[2] for op in sigma[3:]] - - -def verify_record(record, path): - text = path.read_text().strip() - if text.startswith('"'): - text = json.loads(text) - raw = bytes.fromhex(text) - txid, inputs, outputs = transaction(raw) - require(txid == record["txid"], record["role"] + " txid mismatch") - satoshis, script = outputs[record["outputIndex"]] - require(satoshis == record["satoshis"] == 1, "one-satoshi output") - require(len(script) == record["scriptLength"], "script length") - values, sub, media, offset, (signer, compact, vin_bytes) = metadata_and_sigma(script) - require(values == record["map"] and sub == record["subTypeData"], "pinned metadata") - require(media == record["contentType"], "pinned media type") - expected = record["sigma"][0] - vin = int(vin_bytes.decode("ascii")) - vin = record["outputIndex"] if vin == -1 else vin - require(0 <= vin < len(inputs), "Sigma input range") - txid_bytes, vout = inputs[vin] - preimage = txid_bytes + vout.to_bytes(4, "little") - input_hash, data_hash = sha(preimage), sha(script[:offset]) - message = sha(input_hash + data_hash) - require(offset == expected["signedPrefixLength"] == expected["sigmaSeparatorOffset"], - "signed prefix length") - for actual, key in [(preimage.hex(), "inputBytesHex"), (input_hash.hex(), "inputHash"), - (data_hash.hex(), "signedPrefixSHA256"), (message.hex(), "messageHash"), - (compact.hex(), "compactSignatureHex"), (signer.decode(), "declaredSigner")]: - require(actual == expected[key], key + " mismatch") - key, digest = recover(compact, message) - require(key.hex() == expected["recoveredPublicKey"], "recovered public key") - require(address(key) == signer.decode() == expected["recoveredAddress"], "signer address") - require(digest.hex() == expected["bitcoinSignedMessageDigest"], "BSM digest") - return txid, values, sub, signer.decode() - - -def main(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument("--raw-item", required=True, type=Path) - parser.add_argument("--raw-root", required=True, type=Path) - args = parser.parse_args() - evidence = json.loads(Path(__file__).with_name("0185-musashi-evidence.json").read_text()) - records = {record["role"]: record for record in evidence["records"]} - item = verify_record(records["item"], args.raw_item) - root = verify_record(records["root"], args.raw_root) - require(root[1]["type"] == item[1]["type"] == "ord", "MAP type") - require(root[1]["subType"] == "collection" and item[1]["subType"] == "collectionItem", - "collection roles") - require(item[2]["collectionId"] == root[0] + "_0", "collection reference") - require(item[3] == root[3], "issuer equality") - print(json.dumps({"raw_transaction_ids": "verified", "signed_MAP_prefixes": "verified", - "issuer_binding": "verified", "signer": item[3], - "chain_inclusion": "not_checked", "full_membership": "not_checked", - "current_ownership": "not_checked"}, indent=2)) - - -if __name__ == "__main__": - main()