Skip to content

CIP-0188? | Token-Owned Addresses - #1200

Merged
rphair merged 16 commits into
cardano-foundation:masterfrom
mariusgeorgescu:cip-token-owned-addresses
Sep 1, 2026
Merged

CIP-0188? | Token-Owned Addresses#1200
rphair merged 16 commits into
cardano-foundation:masterfrom
mariusgeorgescu:cip-token-owned-addresses

Conversation

@mariusgeorgescu

@mariusgeorgescu mariusgeorgescu commented May 23, 2026

Copy link
Copy Markdown
Contributor

Summary

Token-Owned Addresses (TOA) define a deterministic Cardano enterprise script address for a controlling asset class (policy_id, asset_name). The address is the hash of a canonical Plutus V3 validator applied to (toa_version, policy_id, asset_name), spendable by any transaction that consumes exactly one unit of the controlling NFT as input, produces exactly one unit as output, and does not mint or burn that asset class in the same transaction.

This is an off-chain interoperability CIP — no ledger changes are required. Conformance is determined by the compiled un-applied validator artifact, the canonical parameter encoding (CDDL), the address-derivation procedure, validator rules, and normative test vectors.

Status: Proposed.

Related work

  • Builds on: CIP-0014 (asset fingerprint), CIP-0019 (addresses), CIP-0067 / CIP-0068 (asset name labels and datum metadata), CIP-0069 (Plutus script type uniformization).
  • Complementary: CIP-0143 (Inactive) and CPS-0003 share the deterministic-derived-script-address pattern applied to programmable transfer behaviour for fungible tokens; TOA applies it to NFT-controlled vault addresses.
  • Forward-compatible with: CIP-0160 for future deposit-restriction extensions.
  • External prior art: ERC-6551 (Ethereum Token-Bound Accounts), Solana Associated Token Accounts.

Reference implementation and demo

Published at en7angled/toa@0.1.0:

  • Plinth validator + compiled un-applied UPLC artifact (476 bytes, template hash 129181a58ca3716aada61244d3d4210bff5a7235f709189af2596dc0, artifact blake2b-256 7f78c5524a00852eb51ee760f86a3f0cce4535b446e0c03d6cdc35eb6de0092b).
  • Six normative address-derivation test vectors covering ASCII / empty / max-length / CIP-67 label 100 / CIP-67 label 222 / toa_version isolation.
  • Atlas-based off-chain library + Servant HTTP API + Swagger UI.

Demo: toa.e7d.tech — runs the reference frontend against Cardano Preview and mainnet, exercising address derivation, deposit, spend, and NFT carry-through end-to-end.

Path to Active

7 of 9 acceptance criteria are satisfied by the reference implementation. Outstanding work is community-owned:

  • Validator-scenario test vectors + verification by an independent implementation on a different stack (lucid-evolution, MeshJS, cardano-transaction-lib, PyCardano, or similar).
  • Wallet and explorer/indexer integrations labelling TOA addresses and surfacing the policy classification.

Note on Discussions

The Discussions: field will be updated to this PR URL in a follow-up commit on the same branch immediately after this PR is opened, per CIP-0001 convention.


(rendered latest document)

Defines Token-Owned Addresses (TOA): a deterministic Cardano enterprise
script address derived from (toa_version, policy_id, asset_name) applied
to a canonical Plutus V3 validator. A TOA is spendable by any transaction
that consumes exactly one unit of the controlling NFT as input, produces
exactly one unit as output, and does not mint or burn that asset class
in the same transaction.

Authorisation is purely NFT-presence-based — no signer, wallet identity,
or external ownership registry required. The standard is an off-chain
interoperability CIP requiring no ledger changes; conformance is defined
by the compiled validator artifact, the canonical parameter encoding, the
address-derivation procedure, validator rules, and the normative test
vectors.

Reference implementation, compiled un-applied Plutus V3 validator
artifact (476 bytes, template hash 129181a5...96dc0), and six normative
address-derivation test vectors published externally at en7angled/toa
tag 0.1.0. Demo frontend exercising address derivation, deposit, spend,
and NFT carry-through hosted at toa.e7d.tech (mainnet and Preview).
@rphair rphair changed the title Add CIP-? | Token-Owned Addresses CIP-???? | Token-Owned Addresses May 23, 2026

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @mariusgeorgescu for submitting a progressive & well defined proposal with apparently great commercial potential: marking ready for Triage (preliminary technical assessment only: enough to confirm CIP candidacy) for the next meeting (https://hackmd.io/@cip-editors/135) which you & other proposal advocates are welcome to attend.

Also thanks for your rigour in cross-referencing the foundational CIPs in your OP & elaborating the "prior art" through the CIP itself: not all CIP authors do this, it saves editors a lot of work, and provides an excellent starting point in tagging potential reviewers.

Since it appears your method has already been demonstrated as workable, I think the next important step would be to verify that the great amount of implementation detail in your document would still leave the Cardano community with a standard that can be implemented by enterprises other than your own:

  • Rather than just a "take it or leave it" stage ("independent implementation" pending in your Acceptance Criteria) we should now exercise due diligence to confirm that all of the detail in the Specification (and methods to demonstrate compliance) is both:
    • necessary for mutual interoperability;
    • mutually agreeable by reviewers as "best practice" rather than an arbitrary choice of your own reference implementation.
  • TL;DR the reviewing community should agree that this is not overspecified. (Maybe you have already explained this & we just need to highlight those demonstrations.)

Though operatively about Tokens I'm not categorising this yet because it has the scope of other Tools CIPs before it: to discuss below & also likely settle at the upcoming CIP meeting. Tagging, in the meantime, roughly by stakeholder category:

Comment thread CIP-0188/README.md
Comment thread CIP-token-owned-addresses/README.md Outdated
Comment thread CIP-token-owned-addresses/README.md Outdated
Comment thread CIP-0188/README.md
Comment thread CIP-token-owned-addresses/README.md Outdated
Comment thread CIP-token-owned-addresses/README.md Outdated
Comment thread CIP-token-owned-addresses/README.md Outdated
@rphair rphair added the State: Triage Applied to new PR afer editor cleanup on GitHub, pending CIP meeting introduction. label May 23, 2026
mariusgeorgescu and others added 3 commits May 24, 2026 21:02
- Motivation/Rationale: capital first word after colon
- Acceptance Criteria: regroup; rephrase to not depend on checkbox rendering
- Implementation Plan: drop implicit editor-signoff step

Co-authored-by: Robert Phair <rphair@cosd.com>
Addresses @rphair's review comment by leading the related-work comparison
with CIP-0113, clarifying the derivation-model distinction with TOA, and
adding CIP-0113 to References.
Moves JSON envelope example and per-vector field semantics out of the CIP
body into en7angled/toa@0.1.1:test-vectors/README.md, identified by
blake2b-256 content hash. Conformance gates (a)/(b) split and Coverage MUSTs
stay inline.

Bumps all reference-repo links from 0.1.0 to 0.1.1. UPLC and test-vector
JSON content unchanged from 0.1.0 (verified by hash).

Addresses @rphair review comment on PR thread cardano-foundation#4.
@rphair rphair added the Category: Tokens Proposals belonging to the 'Tokens' category. label May 24, 2026
Motivation: under the previous apply_params-based derivation, the TOA
address could not be derived on-chain (Plutus V3 exposes no primitive
for apply_params + ledger serialisation), and off-chain implementations
in TypeScript, Rust, Python, etc. had to depend on Plutus tooling. R
removes both obstacles: it is implementable on-chain using only
standard Plutus V3 builtins (appendByteString, consByteString,
lengthOfByteString, serialiseData, blake2b_224) and off-chain in any
language with bytestring concatenation and blake2b-224. The semantic
identity of TOA v1 remains anchored in the same compiled validator
artifact -- the byte-level constants are extracted from it -- but
conformance is now expressed in a form that is verifiable uniformly
on-chain and off-chain.

Replaces the apply_params-based derivation procedure with the byte-level
function R, executable on-chain via standard Plutus V3 builtins and
implementable off-chain in any language with bytestring concatenation
and blake2b-224.

The compiled UPLC artifact and template hash become audit references;
FLAT_PREFIX_TOA_V1 (528 B), FLAT_SUFFIX_TOA_V1 (1 B), the CBOR
bytestring-header encoder, and the canonical PlutusData CBOR parameter
encoder become the normative derivation inputs, pinned at
en7angled/toa@0.2.0 with blake2b-256 content hashes.

The reference recipe wraps params_cbor in a single-chunk
chunked-bytestring frame (1-byte length prefix + paramCbor + 0x00
terminator), matching the flat-encoded UPLC layout the validator
expects when applied via a Constant Data UPLC term (cardano-api
applyArguments semantics).

Implements cerinta sections 3.1, 3.2, 3.3, 3.8.
Adds:
  - Rationale entry "Why is the canonical address derivation specified
    at the byte level (R) rather than via apply_params?" — explains why
    on-chain executability + non-Haskell implementability justify the
    byte-level specification, and documents the empirical precondition
    (byte-aligned decomposition of the applied flat-encoded script).
  - Open Question cardano-foundation#6 "Stability of FLAT_PREFIX_TOA_V1 and
    FLAT_SUFFIX_TOA_V1" — frames byte-aligned decomposition as a
    property of the compiled artifact, not of the Plinth source;
    recompilation requires a new toa_version.
  - Acceptance Criteria items for canonical R implementation and for
    toa-verify-reconstruction passing on every artifact release.

Also adjusts the high-level Specification intro to be neutral about
which procedure produces the applied script bytes (R reconstructs
them rather than calling apply_params).

Implements cerinta sections 3.5, 3.6, 3.7.
Keeps the conformance-vs-documentation cut agreed in PR review
discussion r3293465306 (cardano-foundation#1200 (comment)):
conformance gates stay inline in the CIP; schema documentation lives
in en7angled/toa@0.2.0:test-vectors/README.md (referenced by content
hash).

Changes inline in the CIP:
  - Clarifies that per-vector fields `params_cbor_hex`,
    `applied_script_cbor_hex`, `applied_script_bytes`, and
    `flat_body_length` are diagnostic aids; conformance for category
    (a) is determined by `expected_script_hash`,
    `expected_address_mainnet`, `expected_address_testnet`.
  - Adds the CBOR-threshold rationale paragraph: lengths 23 and 24
    bracket the single-byte vs length-prefixed CBOR bytestring header
    transition, and lengths 31 and 32 cover the upper boundary of the
    permitted asset_name domain.
  - Adds two coverage MUST bullets: single-policy_id-change vector
    pairs and single-asset_name-change vector pairs, useful for
    isolating which region of R depends on which input.

The schema documentation for the new `flat_body_length` field is
in en7angled/toa@0.2.0:test-vectors/README.md, already content-hashed
in the preceding paragraph.

Implements cerinta section 3.4 under the conformance-vs-documentation
cut.
Removes paragraphs and sections that are non-normative noise under the
R-canonical derivation:

  - The "TextEnvelope encoding mistakes" warning in Address Derivation:
    irrelevant under R, which never goes through cardano-cli text
    envelopes — implementers compute applied bytes directly from
    bytestring concatenation, not from a serialised wrapper.
  - The Tx3 paragraph in Public API and Transaction Shapes and the
    "Generated bindings (supplementary)" subsection: Tx3 was a
    non-normative reference file that was never published; the mentions
    added noise without normative or informative value.
  - The "L2 Interoperability" subsection: a disclaimer paragraph noting
    cross-ledger semantics are out of scope; the Versioning section
    already establishes scope.

Condenses two redundancies:

  - The repeated MUST NOT in Datum and Redeemer Schema becomes a single
    cross-reference to Definitions — TOA deposit, which already
    establishes the normative rule.
  - "What lives where" no longer restates content-hashing strategy
    already covered in Mandatory Normative Artifacts; the two bullets
    naming the CIP folder contents are preserved.

Updates two orphan references left behind: the "outside Tx3" sentence
in Address derivation helper, and the "any toa.tx3 file" mention in
Reference Artifacts — Normative components.

Net: 22 deletions, 5 insertions; 611 → 593 lines.
@mariusgeorgescu

Copy link
Copy Markdown
Contributor Author

Update — TOA addresses can now be derived on-chain

The address-derivation procedure has been restructured into a byte-level function R (4 new commits).

Core benefit: TOA addresses are now derivable on-chain by other validators. Plutus V3 exposes no primitive that applies parameters to an unapplied script and serialises the result, so under apply_params + ledger_serialise no on-chain code could derive a TOA address. R closes that gap using only appendByteString, consByteString, lengthOfByteString, serialiseData, blake2b_224. A generic validator can now check "is this output at the TOA for the NFT in this transaction?" without being pre-parameterised per controlling NFT — one deployed validator serves every TOA. (R is also implementable in any off-chain language with bytestring concatenation and blake2b-224, ~50 lines, no Plutus tooling.)

Technical cost. TOA v1's validator was recompiled to accept BuiltinData so parameters delivered as a Constant Data UPLC term (cardano-api's applyArguments semantics) execute correctly. Predicate logic and trace codes unchanged — all 38 CLB validator scenarios still pass — but every expected_script_hash and expected_address_* differs from 0.1.1. TOA v1 is Proposed with no mainnet deployments; the address change is in-scope.

R is content-hashed at en7angled/toa@0.2.0; toa-verify-reconstruction empirically proves R reproduces apply_params + ledger_serialise byte-for-byte. The conformance-vs-documentation split from discussion_r3293465306 is preserved.

Reading order: cf279d4 (rationale) → cc4220f (procedure) → 79f1d96 (test vectors) → 200d03e (trims).

cc @rphair @Ryun1.

@rphair

rphair commented May 26, 2026

Copy link
Copy Markdown
Collaborator

thanks @mariusgeorgescu — it seems like at least the change TOA addresses are now derivable on-chain by other validators supports the premise I mentioned in #1200 (review) that a CIP more generally usable by third parties will be more acceptable as a "standard" ... I'll bring this up at the the CIP meeting today (in about 45 minutes).

@colll78

colll78 commented May 26, 2026

Copy link
Copy Markdown
Contributor

Very cool CIP, and a great use-case of onchain parameter validation.

Perhaps you will find our Aiken Design Patterns library useful for this, specifically the parameter validation module which exposes high-level abstractions for performing onchain parameter validation:

https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/parameter-validation.ak

example uses:
https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/parameter-validation.ak

@rphair rphair changed the title CIP-???? | Token-Owned Addresses CIP-0188? | Token-Owned Addresses May 26, 2026
@rphair rphair added State: Confirmed Candiate with CIP number (new PR) or update under review. and removed State: Triage Applied to new PR afer editor cleanup on GitHub, pending CIP meeting introduction. labels May 26, 2026

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mariusgeorgescu as you've observed, the CIP meeting today has declared this a candidate: please rename the containing directory to CIP-0188 and update the "rendered latest document" link in your OP accordingly. 🎉

We had a longer than usual Triage for this one because of concerns (introduced by @perturbing) that this data structure for each token-owned address would provide standardisation for an "entity" on the Cardano chain which all wallets would then be compelled to recognise and process in user wallets: a query-intensive operation whose expense and development difficulty might not be worth the benefits gained from it.

However @colll78 made a number of arguments that presented technical usefulness even under these conditions, including:

  • Other Cardano contributors, without this CIP presented roughly as conceived, would eventually submit their own CIPs — or worse, continue without standardisation — to achieve same purpose (of saying "this address is owned by this NFT: which can serve as a witness instead of a conventional signature").
  • ... Therefore it would make sense to continue with this spec to have a common definition: rather than the chaos that would ensue if each data structure developed for this purpose had a mutually incompatible surface. This is the same rationale behind pursuing CIP-0113 - Programmable Tokens.
  • The off-chain development that this would entail is not strictly a part of the CIP (therefore also confirming the category as Tokens rather the the possibility of Tools raised above) and therefore it shouldn't be inadmissible because of the degree of complexity or expense that might lurk there.
  • ... We adopted a similar rationale when accepting the Beacon Tokens CIP: i.e. even though the primary use case of such tokens — signaling for conditional transactions (again, my own words) — has not been broadly accepted.

We should be able to continue this CIP's review with expectations set as above: assuming I've described these correctly. I've already tagged those who I hope are a representative set of experts across the applicability of this CIP & believe we should consider suitability as @perturbing & @colll78 have defined it above before merging.

Therefore please anyone else that was at the CIP meeting today correct my non-expert language in defining the issues & assessment above: @PapaGoose @Cerkoryn @fallen-icarus @nemo83 @Ryun1

Comment thread CIP-token-owned-addresses/README.md Outdated
@mariusgeorgescu

Copy link
Copy Markdown
Contributor Author

Thanks for the triage outcome and the number assignment, @rphair. Folder rename to CIP-0188 and the rendered-document link in the PR description are now in place; your ef9e434 already set the preamble correctly.

Comment thread CIP-0188/README.md Outdated

**Within the Aiken / Plutus design-patterns ecosystem:**

- The [`parameter-validation`](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/lib/aiken-design-patterns/parameter-validation.ak) module of Anastasia Labs' [aiken-design-patterns](https://github.com/Anastasia-Labs/aiken-design-patterns) library captures, in a more general form, the byte-level technique TOA's R uses to reconstruct a parameterised script's hash without invoking `apply_params` at runtime — splicing precomputed prefix and suffix bytes around the parameter region and hashing the result with `blake2b_224`. Example usage of the library is at [`validators/examples/parameter-validation.ak`](https://github.com/Anastasia-Labs/aiken-design-patterns/blob/main/validators/examples/parameter-validation.ak). TOA's R is conceptually a specific instance of the same pattern, specialised to a single composite parameter and pinned to a single canonical artifact. The encoding choices differ: the library hashes each parameter to a constant 28-byte footprint (`blake2b_224(serialise(param))`) so that arbitrary parameter types — and, in particular, multiple interleaved parameters — share a uniform on-chain layout; TOA embeds the raw `serialiseData` output of a single composite parameter `Constr 0 [toa_version, policy_id, asset_name]`, variable in length, framed by a fixed 1-byte suffix. The two encodings reflect different design goals — generality across parameter shapes versus a specialised single-parameter form — not a quality judgement. See *Why raw parameter embedding rather than hashing parameters?* in *Rationale — Design Decisions* for the specific reasons TOA chose the latter.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good feedback, I will make sure we add variants of the library functions which provide direct argument support such that when the caller is certain that the argument they are parameterizing the contract with is byte aligned already (as is the case for Constr BuiltinData) then no hashing is required, likewise if the argument is a currency symbol, script hash or other hashed already hashed or byte aligned parameter.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @colll78 — I noticed you actually built this the very next day: 2eb280f9 on develop (parameter-validation/advanced.ak, variable-length Data params). That's essentially CIP-0188's R, generalised to any prefix.

If/when it lands on main: feeding advanced.apply_param the pinned FLAT_PREFIX_TOA_V1 and Constr(0, [toa_version, policy_id, asset_name]) should reproduce every expected_script_hash in the six vectors at en7angled/toa@0.2.1 — a free byte-for-byte test target. Happy to reference the module from the CIP once it's released.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@keyan-m What's the progress on that? Can we merge into main?

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@colll78 @mariusgeorgescu this is now merged and included in v1.8.0

@mariusgeorgescu

Copy link
Copy Markdown
Contributor Author

Independent clean-room verification of R · erratum fix in diagnostic vector fields (0.2.1) · spec alignment pass

Three related updates, just pushed to this branch and released as reference-repo tag 0.2.1.

1. R verified implementable from the CIP text alone

To test the overspecification concern from the less-demanding direction, I wrote a clean-room Python implementation of R from the CIP text only — ~40 lines of core logic, standard library only (hashlib; no cbor2, no Plutus tooling, no reference-repo code). It reproduces all six published address-derivation vectors byte-for-byte: expected_script_hash, both bech32 addresses, and (after the erratum fix below) every diagnostic field as well. Full script in the fold below.

The exercise surfaced exactly two places where I had to reach outside the CIP text: the CBOR encoding of the integer field (the CIP specified cbor_bytestring_header inline but delegated integers to an RFC 8949 citation), and the bech32 algorithm (previously uncited). Both are now specified in this push: smallest-form integer bytes are stated inline in Canonical Parameter Encoding, and BIP-173 (checksum constant 1, not bech32m) is cited in Address Derivation and References.

To be clear about what this is not: Path-to-Active steps 7–8 require verification by independent external parties, and category-(b) validator-scenario vectors remain outstanding. This exercise claims neither — it only demonstrates that the byte-level spec is complete enough to implement in any language without Plutus dependencies.

2. Erratum: params_cbor_hex and flat_body_length in the published vectors — fixed in 0.2.1

The clean-room run caught a genuine erratum in the 0.2.0 vector file: the diagnostic field params_cbor_hex was emitted with definite-length CBOR (d8 79 83 …) — the exact encoding the CIP declares non-conforming — while the applied script bytes correctly embed the indefinite-length serialiseData form (d8 79 9f … ff). Consequence: flat_body_length was off by one in all six vectors. The schema doc says a mismatch in these fields "points to a parameter-encoding bug", so the fields meant to catch encoding bugs were telling a correct implementation it had one. Per the CIP's own rule (Reference Artifacts: a conflict among normative components is a specification erratum and MUST be corrected), this is fixed in tag 0.2.1:

  • params_cbor_hex is now emitted from the same serialiseData identity that produces the applied script, and the generator hard-fails unless (a) params_cbor_hex is a substring of applied_script_cbor_hex, and (b) flat_body_length equals the outer CBOR bytestring-header payload;
  • unchanged: expected_script_hash, both addresses, and applied_script_cbor_hex for all six vectors; the normative binaries ToaV1.uplc / FLAT_PREFIX_TOA_V1.bin / FLAT_SUFFIX_TOA_V1.bin are byte-identical to 0.2.0 (the stability commitment holds);
  • CIP pins bumped accordingly: toa-v1.json blake2b-256 → 03ebfb996f8e2b6cde462d30a8292dc6e8c403c224074c4cbdb86aa247b77a81, tag URLs 0.2.00.2.1.

3. Spec alignment pass: conformance = R + Validator Rules + test vectors

Responding directly to the overspecification concern, this push aligns RFC 2119 levels to the declared conformance surface:

  • Policy classification (ProvenClosed / KnownOpen / Unknown) is now a RECOMMENDED vocabulary; per-TOA assignment is SHOULD. The one remaining MUST is the safety outcome: never present a TOA as safely controlled by a unique NFT without verified policy closure.
  • UI-wording and display obligations dropped from MUST to SHOULD (TOA-version surfacing, self-deposit description wording); the min-UTxO item no longer restates a ledger-enforced rule as a wallet obligation.
  • Obligations addressed to the frozen validator artifact ("the validator MUST NOT reject…") rephrased as properties — the artifact is immutable and has no implementor.
  • Wallet Behaviour now states explicitly that the MUST items (four) are the minimum to claim TOA v1 support — address correctness plus not misrepresenting control safety — while SHOULD/MAY describe the full recommended experience. This is also meant to lower the integration cost raised at Triage.
  • Acceptance Criteria rewritten as conditions-at-activation, dropping the GitHub checkbox syntax (per the earlier editorial note); status tracking lives in Implementation Plan.
  • Security analysis extended: control cycles and nested TOAs (self-deposit is the 1-cycle; transitive control is the general case), multi-TOA composition in one transaction (no double-satisfaction), guidance for consumer contracts deriving TOA addresses on-chain (constants provenance, output identification, payee semantics), a CIP-0068 label-100 control caveat, and non-spending-purpose behaviour — each with corresponding (outstanding) category-(b) coverage entries.
Clean-room Python implementation of R (stdlib only, from CIP text)
#!/usr/bin/env python3
"""Independent implementation of TOA v1 address derivation R, from CIP-0188 text ONLY.

No cbor2, no Plutus libraries. Every encoding rule below is annotated with the
CIP section it was taken from, or flagged AMBIGUITY where the text was not enough.
"""
import hashlib, json

# --- Parameter encoder: canonical PlutusData CBOR per "Canonical Parameter Encoding"
# and "Mandatory Normative Artifacts - Identity of the parameter encoder":
#   Constr 0 = tag 121 = 0xd8 0x79, fields in an INDEFINITE-length array 0x9f...0xff,
#   integers and bytestrings in "smallest CBOR major-type form".

def cbor_uint(n: int) -> bytes:
    # AMBIGUITY: the CIP specifies cbor_bytestring_header(n) inline but gives NO
    # analogous inline table for integers -- only the phrase "smallest CBOR
    # major-type form" plus an RFC 8949 citation in References. The rules below
    # (major type 0: value in initial byte for 0..23, 0x18/0x19/0x1a/0x1b beyond)
    # come from RFC 8949, not from the CIP text.
    if n < 0:
        raise ValueError("toa_version is uint per CDDL")
    if n <= 0x17:
        return bytes([n])
    if n <= 0xFF:
        return bytes([0x18, n])
    if n <= 0xFFFF:
        return bytes([0x19]) + n.to_bytes(2, "big")
    if n <= 0xFFFFFFFF:
        return bytes([0x1A]) + n.to_bytes(4, "big")
    return bytes([0x1B]) + n.to_bytes(8, "big")

def cbor_bytestring_header(n: int) -> bytes:
    # Verbatim from "Address Derivation" (major type 2 headers, big-endian lengths).
    if n <= 0x17:
        return bytes([0x40 + n])
    if n <= 0xFF:
        return bytes([0x58, n])
    if n <= 0xFFFF:
        return bytes([0x59]) + n.to_bytes(2, "big")
    if n <= 0xFFFFFFFF:
        return bytes([0x5A]) + n.to_bytes(4, "big")
    raise ValueError("unsupported length")

def cbor_bytes(b: bytes) -> bytes:
    # "bytestrings use smallest CBOR major-type form" -- same header shape as
    # cbor_bytestring_header, which the CIP does define inline (for the outer
    # script wrap); reusing it for inner bytestrings is a (small) inference.
    return cbor_bytestring_header(len(b)) + b

def params_cbor(toa_version: int, policy_id: bytes, asset_name: bytes) -> bytes:
    # toa_params_v1 = #6.121([toa_version, policy_id, asset_name])
    # #6.121 -> 0xd8 0x79 ("Identity of the parameter encoder"); fields wrapped
    # in indefinite-length array 0x9f ... 0xff.
    return (b"\xd8\x79" + b"\x9f"
            + cbor_uint(toa_version)
            + cbor_bytes(policy_id)
            + cbor_bytes(asset_name)
            + b"\xff")

# --- R, steps 1-5 of "Address Derivation"

def R(toa_version: int, policy_id: bytes, asset_name: bytes, prefix: bytes, suffix: bytes):
    pc = params_cbor(toa_version, policy_id, asset_name)                 # step 1
    assert len(pc) <= 255, "single-chunk frame precondition (CIP: len <= 255)"
    chunked = bytes([len(pc)]) + pc + b"\x00"                            # step 2
    flat_body = prefix + chunked + suffix                                # step 3
    script_bytes = cbor_bytestring_header(len(flat_body)) + flat_body   # step 4
    h = hashlib.blake2b(b"\x03" + script_bytes, digest_size=28).digest()  # step 5
    return pc, flat_body, script_bytes, h

# --- steps 6-8: enterprise address + bech32.
# Header bytes 0x70 (testnet) / 0x71 (mainnet) are stated concretely in the CIP.
# AMBIGUITY: bech32 itself is not specified in the CIP (delegated to CIP-0019,
# which in turn points at BIP-173); charset/checksum below are from BIP-173.

B32CHARS = "qpzry9x8gf2tvdw0s3jn54khce6mua7l"

def _polymod(values):
    GEN = [0x3B6A57B2, 0x26508E6D, 0x1EA119FA, 0x3D4233DD, 0x2A1462B3]
    chk = 1
    for v in values:
        top = chk >> 25
        chk = (chk & 0x1FFFFFF) << 5 ^ v
        for i in range(5):
            chk ^= GEN[i] if ((top >> i) & 1) else 0
    return chk

def _hrp_expand(hrp):
    return [ord(c) >> 5 for c in hrp] + [0] + [ord(c) & 31 for c in hrp]

def bech32_encode(hrp: str, data: bytes) -> str:
    # convert 8-bit -> 5-bit
    acc = bits = 0
    fivebit = []
    for b in data:
        acc = (acc << 8) | b
        bits += 8
        while bits >= 5:
            bits -= 5
            fivebit.append((acc >> bits) & 31)
    if bits:
        fivebit.append((acc << (5 - bits)) & 31)
    checksum_input = _hrp_expand(hrp) + fivebit + [0, 0, 0, 0, 0, 0]
    polymod = _polymod(checksum_input) ^ 1  # bech32 constant 1 (not bech32m)
    chk = [(polymod >> 5 * (5 - i)) & 31 for i in range(6)]
    return hrp + "1" + "".join(B32CHARS[d] for d in fivebit + chk)

def addresses(script_hash: bytes):
    mainnet = bech32_encode("addr", bytes([0x71]) + script_hash)
    testnet = bech32_encode("addr_test", bytes([0x70]) + script_hash)
    return mainnet, testnet

# --- test harness

def main():
    # Run from the root of en7angled/toa @ 0.2.1
    prefix = open("validators/FLAT_PREFIX_TOA_V1.bin", "rb").read()
    suffix = open("validators/FLAT_SUFFIX_TOA_V1.bin", "rb").read()
    assert len(prefix) == 528 and suffix == b"\x01"

    vectors = json.load(open("test-vectors/toa-v1.json"))["vectors"]
    print(f"{len(vectors)} vectors\n")
    failures = 0
    for v in vectors:
        pid = bytes.fromhex(v["policy_id"])
        an = bytes.fromhex(v["asset_name_hex"])
        pc, flat_body, script_bytes, h = R(v["toa_version"], pid, an, prefix, suffix)
        mn, tn = addresses(h)

        ok_hash = h.hex() == v["expected_script_hash"]
        ok_mn = mn == v["expected_address_mainnet"]
        ok_tn = tn == v["expected_address_testnet"]
        diag_pc = v.get("params_cbor_hex")
        ok_diag_pc = (diag_pc is None) or (pc.hex() == diag_pc)
        diag_fbl = v.get("flat_body_length")
        ok_diag_fbl = (diag_fbl is None) or (len(flat_body) == diag_fbl)
        diag_asb = v.get("applied_script_bytes")
        ok_diag_asb = (diag_asb is None) or (len(script_bytes) == diag_asb)
        diag_cbor = v.get("applied_script_cbor_hex")
        ok_diag_cbor = (diag_cbor is None) or (script_bytes.hex() == diag_cbor)

        print(f"[{v['name']}] toa_version={v['toa_version']} asset_name_len={len(an)}")
        print(f"  script_hash   {'OK ' if ok_hash else 'MISMATCH'} got={h.hex()}")
        if not ok_hash:
            print(f"                            exp={v['expected_script_hash']}")
        print(f"  addr mainnet  {'OK' if ok_mn else 'MISMATCH got=' + mn}")
        print(f"  addr testnet  {'OK' if ok_tn else 'MISMATCH got=' + tn}")
        print(f"  diag params_cbor_hex      {'match' if ok_diag_pc else 'DIVERGES'}")
        if not ok_diag_pc:
            print(f"      mine: {pc.hex()}")
            print(f"      file: {diag_pc}")
        print(f"  diag flat_body_length     {'match' if ok_diag_fbl else f'DIVERGES mine={len(flat_body)} file={diag_fbl}'}")
        print(f"  diag applied_script_bytes {'match' if ok_diag_asb else f'DIVERGES mine={len(script_bytes)} file={diag_asb}'}")
        print(f"  diag applied_script_cbor  {'match' if ok_diag_cbor else 'DIVERGES'}")
        if not ok_diag_cbor and diag_cbor:
            mine, file = script_bytes.hex(), diag_cbor
            i = next((k for k in range(min(len(mine), len(file))) if mine[k] != file[k]), None)
            print(f"      first hex divergence at char {i}")
        if not (ok_hash and ok_mn and ok_tn):
            failures += 1
        print()
    print("RESULT:", "ALL PASS" if failures == 0 else f"{failures} FAILURES")

if __name__ == "__main__":
    main()

…end security analysis

Overspecification pass (conformance = R + Validator Rules + test vectors):
- Policy classification becomes a RECOMMENDED vocabulary; per-TOA assignment
  is SHOULD; the single remaining MUST is the safety outcome (never present
  a TOA as safely controlled by a unique NFT without verified policy closure).
- UI wording and display obligations lowered to SHOULD; min-UTxO no longer
  restated as a wallet obligation; obligations addressed to the frozen
  validator artifact rephrased as properties.
- Wallet Behaviour states the minimal 'TOA-aware' bar explicitly.
- Acceptance Criteria rewritten as conditions-at-activation (checkbox syntax
  dropped); status tracking consolidated in Implementation Plan.

Test-vector erratum (reference repo 0.2.1): params_cbor_hex was emitted
definite-length in 0.2.0 vectors and flat_body_length was off by one in all
six; script hashes, addresses and applied bytes unchanged; normative
binaries byte-identical. Pins bumped: toa-v1.json blake2b-256 03ebfb99...,
tag URLs 0.2.0 -> 0.2.1.

Precision: n = 531 + len(params_cbor) in [567, 600]; integer smallest-form
bytes inline; serialiseData empty-Constr exception noted; BIP-173 cited for
bech32 (checksum constant 1); aiken-design-patterns links pinned to v1.7.0;
Validator Rules list indentation fixed; phantom 'R-canonic' term unified.

Security analysis: control cycles and nested TOAs (self-deposit as the
1-cycle); multi-TOA composition without double-satisfaction; consumer
contracts deriving TOA addresses on-chain; CIP-0068 label-100 control
caveat; non-spending purpose behaviour (trace T3); corresponding
category-(b) coverage entries added as outstanding.
@mariusgeorgescu
mariusgeorgescu force-pushed the cip-token-owned-addresses branch from cb277a2 to 9d809ee Compare August 13, 2026 12:46

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@mariusgeorgescu (and/or @colll78) please ping the editors in a comment here when the work in progress appears to stabilise — particularly, anything that would be documented in the CIP — so we can put this through final review.

…cross-check of vectors

Anastasia Labs released the raw-embedding parameter-application form
(parameter-validation/advanced.apply_param) in v1.8.0. Related Work now
describes it as the same construction as R generalised to any flat
prefix, and records that, called with FLAT_PREFIX_TOA_V1 and
Constr 0 [toa_version, policy_id, asset_name], it reproduces all six
published address-derivation vectors byte-for-byte (Aiken 1.1.23;
harness pinned in the reference repository). Implementation Plan step 5
notes the independently authored implementation; links bumped
v1.7.0 -> v1.8.0.
@mariusgeorgescu
mariusgeorgescu force-pushed the cip-token-owned-addresses branch from 45871b5 to 8821694 Compare August 28, 2026 11:42
@mariusgeorgescu

Copy link
Copy Markdown
Contributor Author

Thanks @rphair (cc @Ryun1) — pinging as requested: the document has stabilised and I'd like to request final review.

The one substantive change since my last comment is the v1.8.0 reference. Anastasia Labs released the same byte-level construction as R in aiken-design-patterns v1.8.0 (advanced.apply_param, see the thread with @colll78), so Related Work now references it. Fed FLAT_PREFIX_TOA_V1 and the TOA parameter it reproduces the expected script hash of all six published vectors; I've noted that under Implementation Plan step 5 with a reproducible harness in the reference repo. I ran that check myself, so I'm not counting it towards the external verifications in steps 7–8.

The remaining Path-to-Active items (validator-scenario vectors, ports, integrations) are tracked in the Implementation Plan; completing them will only bump pins and status markers, not the normative content. CI validation is green on the current head.

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

that's great @mariusgeorgescu and I will readily approve this myself & mark Last Check for the next CIP meeting after your consideration of the change below: as you say, with Path to Active issues being independent of the normative content.

@colll78 we have tagged this around the ecosystem already in #1200 (review) and you are the only one who's shown a productive interest. It looks like implementations have already been facilitated by the work you began in #1200 (comment) with compatibility verified in #1200 (comment).

@mariusgeorgescu the CIP editors don't have the need or the ability to audit this — so this would not be a deeply technical review — though I can appreciate (and will note in the meeting review) that:

  • audit is expected beginning with the next item on the Implementation Plan
  • data structure has been provided to facilitate audit (i.e. exposing intermediate levels of data).

@Crypto2099 I've also seen you RSVP for this next meeting so I thought you might also be interested in a look at this beforehand & please tag anyone else you'd like to notify that the review is actively proceeding.

Comment thread CIP-0188/README.md Outdated

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

p.s. in my last review I meant to suggest using a link when referring to a commonly quoted part of your spec (used 8 times in your document, but only these 2 are forward references & therefore more of a courtesy to the reader):

Comment thread CIP-0188/README.md Outdated
Comment thread CIP-0188/README.md Outdated
@rphair
rphair requested review from Ryun1 and colll78 August 28, 2026 21:13
…atus markers

- Implementation Plan closing line per rphair's suggestion: editors
  verify completion and apply the Active tag; steps 7-9 remain
  ecosystem-owned.
- Forward references to Mandatory Normative Artifacts become anchor
  links (Canonical Parameter Encoding, Address Derivation).
- Done/outstanding status markers in Implementation Plan bolded for
  scannability (keeping the no-tickbox format).
@mariusgeorgescu
mariusgeorgescu force-pushed the cip-token-owned-addresses branch from 775c6cc to f1765d6 Compare August 31, 2026 07:48
@mariusgeorgescu

Copy link
Copy Markdown
Contributor Author

Thank you @rphair! All three suggestions applied in f1765d6, and I bolded the Done/outstanding markers in the Implementation Plan as well. Kept the format without tickboxes, for the rendering reasons you mentioned. Ready for Last Check on my side.

@rphair rphair left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

great work @mariusgeorgescu ⭐ and I look forward to hearing what the other editors think (cc @Ryun1 @perturbing) — and if @colll78 has any last words — around Last Check tomorrow: https://hackmd.io/@cip-editors/142

@rphair rphair added State: Last Check Review favourable with disputes resolved; staged for merging. and removed State: Confirmed Candiate with CIP number (new PR) or update under review. labels Aug 31, 2026
@rphair
rphair requested review from colll78 and perturbing and removed request for colll78 August 31, 2026 14:59

@Ryun1 Ryun1 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM
thanks for the contribution @mariusgeorgescu

@rphair
rphair merged commit c9e4524 into cardano-foundation:master Sep 1, 2026
1 check passed
@rphair rphair removed the State: Last Check Review favourable with disputes resolved; staged for merging. label Sep 1, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Category: Tokens Proposals belonging to the 'Tokens' category.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

5 participants