Object Digital Passport — specification 0.7

Redesigned generation 0.7-redesign-8, deployed on Polygon mainnet on 2026-09-25 (§7). Not yet independently audited. The earlier 0.7-redesign-7 snapshot and its approved release bundle are history; they do not describe the current sources.

This document is normative for the sources in chain/contracts/. MUST, MUST NOT and SHOULD express requirements. The previous 0.7 ABI is incompatible. Its specification is preserved for audit in BASELINE_SPEC_5d1db8f.md, not as an alternative implementation target. The older audit/design documents describe inputs to this implementation; ODP_ASTRA_REVIEW.md records which claims survived independent reproduction. Deployment and client integration status live in ODP_07_IMPLEMENTATION.md. This English file is the only normative text. Translations under docs/<language>/ are informative and may contain mistakes; see docs/TRANSLATIONS.md.

1. Overview

ODP records an issuer's immutable description and hash commitments. It establishes that a wallet registered those commitments in a particular block. It does not establish physical possession, ownership, authenticity, legal title, or the real-world identity of a self-registered issuer. A displayed author name is a declaration.

The core has no administrator, owner, proxy, privileged deployer, freeze, mutable implementation, or satellite callback. Its internal library is compiled into its bytecode. Satellites read a constructor-pinned core; the core does not know their addresses. There is no protocol fee. Transactions incur network fees. Reads and offline checks require no transaction, but RPC and data providers can charge or become unavailable. Long-term verification requires the original chain state and document bytes to remain accessible. Verification is offline-capable, not offline-complete: a bundle can be checked against itself without a network, but confirming that it is the registered document needs the chain (§15.3). No images or files are stored on chain; the core holds a short card and hash commitments.

ODP complements human expertise and other standards; it does not replace them (§18). Spam and indexing noise are only partly limited (mint caps, independent directories). Visual design of apps, labels and websites is not specified here.

1.1 Terminology

Term Meaning in this specification
Passport ID The ODP-… identifier assigned by the core at mint (§2). The hashed passport.json carries passportId: null; the assigned ID lives in the receipt (§15) and on chain.
Profile ID The issuer identifier C-…, B-…, P-… or M-… (§3); creatorId on chain. A document MAY name profiles in authorship persons; such a value is a declaration.
Registration registerCreator(type): binds the calling wallet to one permanent profile ID. Network fee only.
Mint A transaction to mintPhysical, mintDigital or mintMixed that creates one immutable passport record: card, classification and hash commitments (§8). It uploads no document or file.
Generation One deployed set of a core and its satellites, identified by chain ID, addresses and ABI generation (§7). Profiles and passports belong to exactly one generation.
Passport The on-chain record plus, when available, the passport.json bytes and original files whose hashes it commits.
passport.json The canonical off-chain document (§9, §10); its SHA-256 is dataHash.
.odpass The ZIP bundle carrying passport.json, its original files and the service files that tie it to a generation (§15). It is the primary copy; network locations are conveniences (§19).
Gas The network fee in POL on Polygon. ODP charges no protocol fee.
Verification Read-only checks of chain records and locally available bytes (§11). It never needs a signature (CA-12.4) and yields per-part results, not one verdict (§22.16).
Revocation window The period after mint in which the original issuer may revoke: 72 hours for C, 24 hours for B/P/M, closed earlier by print finalization (§8).
Print finalization The irreversible finalizePassportForPrint mark that closes revocation before labels are printed (§8, CA-2).
Edition passport One B passport registering a production run, carrying a unit_key_set anchor (§20). Its anchors describe the run, not one item.
Unit One physical item of an edition, identified by its index. A unit has no passport of its own.
Unit key The key pair of one unit (§20.5). Its seed is the code printed under the concealment layer.
Activation The one-time public record that a unit key signed for its slot (§20.8–20.9). It is not a mint, not a verification and not ownership (CA-4).
Relayer Anyone who submits someone else's signed activation. It gains no rights over the unit (§19).

2. Passport ID

ODP-YYYY-MM-NNNNNNNNN: Gregorian UTC mint year/month and a nine-digit number unique within that month in this registry. It is not a secret or globally unique across chains/registries. Full identity is (chainId, registryAddress, passportId). The contract selects at most 25 candidates from a hash of block inputs, sender, nonce, bucket and gas. These are identifiers, not security randomness. No useful fairness or unpredictability claim is made. Exhaustion reverts atomically.

The client supplies the actual current Gregorian UTC year/month. A transaction mined in a different month reverts. Reprepare the document and its hashes after a month boundary; do not silently patch hashed bytes. The supported calendar is 1970 through 2570. A conforming client obtains the assigned ID from the non-indexed passportIdText in PassportMinted and checks the emitting registry and successful receipt.

Part Meaning Example
ODP Fixed prefix ODP
YYYY UTC year of the mint block 2026
MM UTC month (01–12) of the mint block 03
NNNNNNNNN Nine decimal digits, zero-padded 004829301

The reference core computes, with abi.encodePacked: key = year*100 + month; n = uint32(keccak256(block.timestamp, block.prevrandao, msg.sender, nonce+i, key, gasleft())) % 1e9 for i = 0…24, taking the first n not yet used in that key; otherwise it reverts EC(61). The number carries no order or count of registrations. The ID is assigned by the contract, never chosen by the issuer, and never changes. YYYY-MM states when the record was mined, not when the object was made; historical dates belong in creationDate or objectYear (§9). The chain enforces only consistency with the block time.

3. Profile ID

A wallet registers exactly once as C, B, P, or M. The ID is T-NNN-NNN-NNN-NNN and remains fixed. Types are self-selected: they are not accreditation. getCreator returns one tuple with exactly four components: (string creatorId, address wallet, bytes1 typePrefix, uint256 timestamp). Profile registration is permanent. There is no revokeCreator, CreatorRevoked, profile revokedAt, profile-stop rejection, hidden admin pause, wallet rotation or recovery authority. Passport revokedAt remains a separate field. Losing the key can permanently prevent new issuer actions.

C has 1000 mints and B has 100000 mints per approximate quota bucket; P/M have no mint cap. These are per issuing wallet, not Sybil resistance. Bucket calculation is exactly year = 1970 + floor(timestamp/31556952) and month = min(12, floor((timestamp % 31556952)/2629746)+1). This deliberately retained rule differs from Gregorian months (measured discrepancies in the audit). Clients MUST NOT label its reset as a Gregorian calendar promise. Passport IDs still use the real UTC calendar.

Type prefixes and format

Prefix Intended use Contract-enforced differences
C Individual creator: artist, photographer, maker 1000 mints per bucket; unique passports only; 72-hour revocation window
B Brand, company, studio, label 100000 mints per bucket; the only type that may issue editions (§8, §20)
P Expert, auction house, certification body No mint cap; institutional proofs and concerns (§4, §13)
M Museum or collection, including holdings by deceased authors Same as P

Museums and large inventories SHOULD register as M, not B or C. Only these four prefixes exist. registerCreator rejects any other byte (EC(54)) and a second registration of the same wallet (EC(53)). A new prefix requires a specification update and a new registry.

NNN-NNN-NNN-NNN is a twelve-digit number in four groups, for example C-482-930-174-005, B-029-384-751-224, P-001-293-847-119, M-204-839-112-441. The core computes n = uint64(keccak256(block.timestamp, block.prevrandao, msg.sender, nonce+i, gasleft())) % 1e12 for i = 0…24 and takes the first unused n; otherwise it reverts EC(62). The number carries no meaning.

Public identity

A profile has a short form, the profile ID, and a full form, the complete 42-character wallet address that the registry binds to it. A name is an off-chain label only; the registry stores no names. Profile IDs and wallets are public by design. Every issuer MUST publish both forms on at least one channel it controls (website, shop, packaging, documents handed over with the object), and organizations (B, P, M) MUST also serve /.well-known/odp.json (§22.10). The registry cannot enforce this; a conforming issuer follows it. Anyone can register, so a profile whose ID cannot be found in its claimed owner's own channels is unidentified. Clients show the full profile ID and ask the user to compare it (CA-16.5). A passport ID and profile ID printed together on packaging are a convenience, not an identity proof (§5).

Direct issuance and repeat protection

mintPhysical/mintDigital/mintMixed(m, operationId) always register the caller's own registered profile. There is no principal selector, mint-agent role, grant or tx.origin path. Issuance delegation is deferred. Contract wallets may issue as themselves if registered; forwarding a call does not impersonate another wallet. Publishing-location delegation remains a separate satellite permission and confers no mint rights.

operationId is a nonzero bytes32 identifier created and durably saved by the client BEFORE submission. Retries MUST reuse the same ID and exact mint parameters. Do not generate a fresh ID on a transport timeout. The core stores (digest, passportId) per (msg.sender, operationId) on successful mint only. Digest is keccak256(abi.encode("ODP-MINT-OPERATION-0.7", block.chainid, address(this), msg.sender, objectType, m)), where objectType is the entrypoint's physical/digital/mixed string and m is the entire PassportMintInputs tuple.

An exact replay reverts with AlreadyCommitted(operationId, passportId); changed parameters or entrypoint revert with MintOperationConflict(operationId). Neither creates another record or consumes quota. Failed initial mints reserve nothing. getMintOperation(issuer, operationId) returns the committed result, or a zero digest and empty passportId if absent. Recovery also works after passport revocation or a month boundary; these do not authorize a new mint. A new operation ID denotes a new operation: this is not global uniqueness of documents or physical objects. Reads/receipts still require the client's chain finality policy.

4. Institutional statements and affiliation

ODPPassportProofRegistry accepts statements from registered P/M profiles about an existing nonrevoked passport. A document hash is optional; a URL requires a nonzero hash. URLs are at most 512 UTF-8 bytes. The statement payload is immutable. Its submitting wallet can call withdrawProof(proofId, reasonHash) once, including after passport revocation; a nonzero SHA-256 reason is required. Readers MUST read proofWithdrawnAt and proofWithdrawalReason alongside getProof; an existing record does not imply a current endorsement. IDs are PRF-YYYY-MM-NNNNNNNN; the current UTC month is checked. The contract does not assert that an examination occurred. Clients MUST establish the institution's identity independently. A statement does not modify the passport; statements accumulate beside it and stay readable after withdrawal.

getProof(proofId) returns ProofRecord(proofId, contractVersion, prover, passportId, documentHash, documentUrl, timestamp): prover is the submitting profile ID, documentHash is the SHA-256 of the institution's document or zero, documentUrl is empty when the hash is zero, and timestamp is the block time. contractVersion is 7 and MUST equal the core's CONTRACT_VERSION; the satellite derives it from the same SPEC_MAJOR/SPEC_MINOR rule (§14). The proof number is uint32(keccak256(block.timestamp, block.prevrandao, msg.sender, nonce+i, key, keccak256(passportId), gasleft())) % 1e8 with key = year*100 + month, first unused of 25 candidates, otherwise EC(60). Lists are read with getProofsForPassportPaged and getProofsByInstitutionPaged (§13). Submission costs only the network fee.

ODPRegistryRelations stores display-only affiliation for B/P/M. A child proposes; the parent confirms. Both profiles must be registered B/P/M when creating a relation. There is at most one active parent per child, 100 active children per parent and 100 pending parents per child. Confirmation rechecks cycles, walking at most eight ancestors and rejecting when no root is reached in that bound. This bound is a per-operation walk bound, not a global maximum tree height. Parent detach and child leave/cancel are cleanup operations available to their original participants. No rights, quota or trust are inherited. Children lists use swap-and-pop on detach; read multiple pages at the same block. Private Solidity storage is publicly observable.

A link that would close a cycle reverts EC(67); an ancestor walk that reaches no root within eight hops reverts EC(69). C profiles cannot take part on either side (EC(71)). Affiliation records that two profiles agreed to be linked and in which direction, nothing more: a parent confirms belonging, not quality. A verifier MUST NOT present a child as endorsed, certified or vouched for by its parent, and MUST NOT invent a relationship type (subsidiary, member, franchise) that the contract does not store. It SHOULD show only the immediate parent by default and MAY show the full chain on request. A parent can detach a child at any time.

5. Verification label

An optional label carries a readable passport ID and a QR locator. It is a convenience, not identity proof. An issuer profile printed on packaging is not an independent trust source. Copying a valid label is possible. A client SHOULD show issuer identity, integrity, revocation and individual statements separately. It MUST NOT transform a concern count or first activation into a verdict about the physical object.

If a label is used, it MUST carry:

Element Rule
QR code One of the payloads of §12 and CA-14.6, error correction level Q or higher
Passport ID The full ODP-… identifier as readable text, so the passport stays checkable without the QR or a website
Protocol mark ODP or Object Digital Passport

A label MAY add the profile ID, title, author name, year, edition number, a logo, an NFC tag or a seal number; none of these is a trust anchor. If a numbered seal is used, the label SHOULD overlap it so that removing the label removes the seal. Size, shape, colour, material, typography, layout and manufacturer are implementation decisions. Edition unit labels follow §20.7 and §22.14; a print-ready file follows CA-2.2.

6. Physical seal

NFC transport and NFC hardware verification are deferred and are not part of the current 0.7 release work. The following describes optional anchor semantics, not a shipped NFC feature.

Physical binding is an identification problem outside the core. Optional nfc, numbered_seal, fingerprint or dna anchors commit identification data. A seal can add evidence, not ownership rights. Cryptographic NFC checks depend on the specific tag protocol and provisioning. A public symmetric read key is not a public-key signature and does not make all observed NFC responses unforgeable. Master/write keys MUST NOT appear in the public document. Passive UID matching alone is not cryptographic authentication. Readers MUST distinguish a live verified response, a reported seal state, and an unavailable check. Detailed legacy hardware guides are informative and require separate implementation/hardware validation.

A seal is optional. The identification minimum of a physical or mixed object is photo, dimensions, materials and distinguishing_features (§8, §9); a seal anchor adds evidence on top of it. Digital objects use no seal; the file hash is their binding. Seal anchors have no dedicated on-chain fields: they are bound by dataHash and anchorsHash and visible as mask bits 256 (nfc) and 512 (numbered_seal).

numbered_seal anchor

A printed-number seal (holographic sticker, wax or lead seal, tamper-evident label) gives a physical reference, not cryptographic proof. data carries number (exactly as printed) and type (for example holographic sticker), and MAY carry color, size and notes. A verifier shows the committed number for a person to compare with the object; the check cannot be automated. The issuer chooses a seal that cannot be removed without visible damage.

nfc anchor (informative until NFC verification ships)

The data shape below appears in the conformance vectors, so its field names are fixed: uid (7-byte chip UID, lower-case hex), publicKey, model (NTAG424DNA or NTAG424DNA_TAGTAMPER), installedAt (ISO 8601 date) and optional notes (installation method and location). Generic Type 2 tags such as NTAG 213 are not a conforming nfc anchor. For NTAG 424 DNA, publicKey holds a 16-byte AES application key for EV2 mutual authentication (profile odp-ntag424-ev2-symmetric-cr-v1); the name is kept for future asymmetric ICs. Consequences:

An NFC-capable verifier obtains the anchor, checks it against dataHash or anchorsHash, and runs AuthenticateEV2First with that key. Success shows only that the chip holds the committed key. For NTAG424DNA_TAGTAMPER a high-assurance result additionally requires an authenticated tamper state of intact and a live UID equal to data.uid. Read_Sig shows manufacturer originality, not binding to a passport. A web verifier cannot perform this check and MUST report it as unsupported (§11, CA-16.3). Other tag technologies need their own specified model string and verification recipe before they can be claimed.

7. Networks and generations

The one approved generation of 0.7 is odp-0.7-redesign-8-polygon-20260925, deployed on Polygon mainnet (chainId 137) on 2026-09-25 from the owner-approved release bundle sha256:7392c821…a414d7 and approved by the project owner the same day. Its authenticated manifest is chain/generations.json; the full deployment record is chain/deploy/output/odp-0.7-redesign-8-polygon-20260925.json. Never substitute a 0.6 address or infer an address from the version byte. Polygon Amoy is not used (owner decision 2026-09-24).

Role Contract Address Deployment block
Registry (core) ObjectDigitalPassport 0x3281492981DCD492cc2F7c17398134e1D3B5CA1F 94434863
edition-units ODPEditionUnits 0x3DBf163b8Ec7c251565E4aCD7b4F6b310B60d0fe 94434870
author-attestation ODPAuthorAttestation 0x7973D5894f45755D87166518304654bFD56fFDD9 94434876
concerns ODPPassportConcerns 0xdda4740c9dFae9b827B1CA500533A555B0578C27 94434881
hosting ODPHosting 0x7cFB16693fFd0E26Fc6380fd7503b360BA02FCC3 94434888
profile-directory ODPProfileDirectory 0x5aA960C6608052b2249f47f5184FE7F8A4996a72 94434895
relations ODPRegistryRelations 0x9F27bcc97EDd3A8700991A6827a14bD9e53201F7 94434900
institutional-proofs ODPPassportProofRegistry 0x49472AAa75a667198C4FF86f8Fe7B359f417eA22 94434906
wallet-document-anchor ODPWalletDocumentAnchor 0x6Bc2fC372E38f03f9e2aaA53206bcd49B78A9CdE 94434912
statement-journal ODPStatementJournal 0xcf6375B52C5888e51eE20b1B61c623D08604859c 94434917

Its deployment manifest MUST bind the generation identifier to chain ID, registry address, deployed runtime hashes, ABI identities, deployment blocks and source/build identity. A generation identifier MUST NOT later be reassigned to different addresses. Clients may embed the authenticated fixed manifest; an online mutable generation directory is not required for this model. The current sources identify as 0.7-redesign-8 and match the deployed runtime above. Document version 0.7 alone does not identify an ABI.

Every issued .odpass MUST carry its generation and full contract addresses as specified in §15. An embedded manifest is an identification artifact, not its own trust root: clients MUST authenticate the deployment identity independently before reporting chain authenticity. A replacement satellite is a different namespace; it cannot silently replace the one committed by an existing edition. Historical records and activation state remain associated with their original addresses, regardless of later client software or hosting changes.

Profile IDs and passports belong to the generation that created them. A new generation does not carry them over: the same wallet registers again and receives a different profile ID. Every verification path ends at a read from this one chain; §15.3 states what remains possible without it.

Superseded lines (informative)

Earlier lines are separate registries with incompatible ABIs; their records do not migrate and are not ODP 0.7 passports. They remain readable on Polygon PoS (chain ID 137), and a document issued under them still verifies against their own on-chain hashes. They are listed only so that historical records can be read. A 0.7 client MUST NOT use them for issuance, MUST NOT substitute them for a 0.7 address and, under CA-12.1, does not report their records as verified 0.7 passports. The 0.7 audit findings were not rechecked against these deployments.

Line Contract Address
v0.6, deployed 2026-07-24, CONTRACT_VERSION 6 ObjectDigitalPassport 0x012aC6393464A73EC16131D701ff2e000695b91b
ODPPassportLib 0xB7D7B8485eeb385c375ABd91035F5a6914171ccE
ODPWalletDocumentAnchor 0x35df3773919D9F10e5F8838abaa453DE120e6Cb4
ODPCounterfeitConcern 0x692935d6c1532b47cE0459bF1E9549991d0eD2C9
ODPRegistryRelations 0x2ea6f05a050973afa14E61b1Ea19De92621e3661
ODPPassportProofRegistry 0x990FCc2E587d9f2cDb9c73083E9f90793CeF7F49
ODPExtensionMintRouter 0x3fa8f213399a2A9f7Da4bF7D8a9D7D42E8AEF822
ODPAuthorAttestation 0x1972E68D0A5B19C5ee2af54F8b792c426985F7d7
v0.5 and earlier ObjectDigitalPassport 0x413aEeBB2ac437483Bc68791EaAab492C2a4B346

Release notes of these lines are kept as written: docs/RELEASE_v0.6.md, docs/V0.6.md.

8. Immutable on-chain record

Three mint entrypoints take the same PassportMintInputs plus a nonzero bytes32 operationId:

Title/authorName are 1–128 UTF-8 bytes, description 1–256, domain 0–128. The contract bounds bytes; it cannot verify correspondence with an off-chain JSON file. Clients MUST check all commitments and fields. Core classification encodings are the one-based positions in these lists:

Field Values in numeric order
contentClass static, time_based, spatial, textual, composite, executable
lifecycleStatus concept, prototype, produced_object, archived
aiStatus none, assisted, generated
verificationMethod self_asserted, institutional, nfc, c2pa, hybrid
editionModel unique, limited, open, dynamic

dataHash and anchorsHash must be nonzero. Physical requires nonzero imageHash, zero fileHash and mask15. Digital requires nonzero fileHash and mask bit32; imageHash is optional. Mixed requires both hashes and all bits in mask47. The mask checks presence of required bits, not truth of the anchors. Edition anchor bits 4096/8192 and every non-unique editionModel (limited/open/dynamic), even without unit anchors, require a B issuer. C/P/M may mint unique passports only. The immutable creator, creatorId, card, hashes and classifications never change.

previewHash is the SHA-256 of the exact bytes of a lighter public copy of the primary photo (§9, §22.19), a JPEG of at most 1,048,576 bytes without GPS or other location metadata. Zero means that no such copy exists. A nonzero previewHash requires a nonzero imageHash (EC(142)) and MUST differ from it (EC(143)). The contract checks only these two relations; it cannot check size, format, metadata or that the copy shows the same picture. previewHash is immutable like the other hashes and is covered by the mint-operation digest (§3) as part of m. PassportMinted is unchanged; readers obtain the value from getPassportMedia, whose PassportMediaView places previewHash directly after imageHash.

revokePassport(id, reasonHash) is available only to the original issuer, at or before mintTimestamp + 259200 for C or mintTimestamp + 86400 for B/P/M, and only while print finalization is absent. Reason is nonzero keccak256 of an external UTF-8 explanation. It is one-shot. The revocation flag/time/reason change once and cannot be undone; immutable payload/card and prior history remain. Solidity enforces only a nonzero reason hash, not availability or truth of its preimage. There is no administrator revocation, transfer, replacement link, unit-passport mint, general event-history API, mutable lifecycleStatus classification, URL field or activation-triggered revocation lock in the core. After the deadline, a correction uses a new passport and an external explanatory statement. Earlier passports remain addressable.

finalizePassportForPrint(id) is an original-issuer-only, irreversible per-passport operation. It rejects missing/revoked passports, records the first block timestamp and emits PassportFinalizedForPrint. A same-issuer retry is a no-op and preserves that timestamp. There is no undo, including after failed printing. getPassportReleaseState(id) returns (revocationDeadline, printFinalizedAt) and rejects missing IDs. Finalization and revocation exclude each other according to chain order. Finalization is allowed after the window as well, applies to unique passports, and does not stop a profile or block author/expert withdrawals. Core does not observe printers or openEdition. The application MUST withhold final print export until confirmed finalization, nonrevoked status and, for unit editions, matching confirmed open/commitment; it must first durably preserve the exact job, archive and secrets. External printing cannot be prevented. Readers MUST pin the generation and current evidence before claiming that revocation is closed.

The stored record adds to the mint inputs: passportId, contractVersion (7), creator (the issuing wallet), creatorId, objectType (physical, digital or mixed, set by the entrypoint), timestamp (the mint block time, the only proof of the registration moment), and revoked, revokedAt, revocationReasonHash (zero until revocation). Block times are UTC; there is no stored time-zone field. lifecycleStatus is the state at registration and never changes.

getPassportHeader, getPassportClassification, getPassportMedia, getPassportReleaseState are defined by IODPRegistry.sol. Dynamic indexed strings in events are hashes; events also carry readable non-indexed IDs. No caller should recover an ID by treating an indexed string topic as text.

9. Passport JSON

schema/passport-0.7.schema.json defines the document shape. The canonical hashed document ALWAYS has passportId: null; the assigned ID is in an external receipt (§15). Card fields MUST match the normalized mint strings byte for byte. status is the state at registration. idGranularity is model, batch or item; it is independent of edition size. Canonical examples: physical, physical with a public photo copy, digital, mixed and edition; their exact canonical bytes and hashes are in schema/vectors/. The document follows the Object ID identification principle: object type, materials and technique, measurements, inscriptions and markings, distinguishing features, title, subject, date or period, maker and photographs are first-class fields and anchors, not an external mapping.

Field Required Rule
version yes "0.7"
passportId yes null in the hashed document
title, authorName, shortDescription, domain yes, yes, yes, no Card fields, equal to the mint strings; domain MAY be empty
description no Full description, unbounded
subject no What is depicted or what the object is about
creationDate no Date or period of the object's creation, not the mint date
objectYear no Integer historical year when it differs from the mint year
authorship no author and optional coAuthors (each name, optional full wallet and creatorId) and team; declarations (CA-3)
anchors yes Identification anchors, below
objectType yes physical, digital or mixed, equal to the mint entrypoint
status, contentClass, aiStatus, verificationMethod yes Controlled values of §8
edition yes model (§8 values), optional number ≤ total
idGranularity yes What the passport identifies
translations no Language-keyed title, shortDescription, description; the card stays in the original language
year, month yes UTC year and month of preparation, equal to the mint inputs
registeredAt, registration yes Preparation time, below
refinementTags no Free labels; not a replacement for the controlled values
physical no; yes for mixed Descriptive facts such as category, medium, weight. Identification facts belong in anchors[]
digital yes for digital and mixed fileHash (required), subtype, format, fileSize, optional c2pa
additionalMetadata no String-keyed string values not modelled elsewhere

physical.category and digital.subtype are secondary descriptors; classification comes from domain, objectType, status, contentClass and refinementTags. A mixed passport MUST contain both physical and digital blocks: it has a physical layer bound by its anchors and a digital layer bound by fileHash. mixed is not a fallback for uncertain classification.

contentClass describes the content, not a file format: static fixed still output, time_based fixed sequence over time, spatial 3D structure or geometry, textual symbolic content, composite structured multi-file bundle, executable logic that runs and produces output.

idGranularity says what the record points at: model any unit of a design, batch one production run, item one physical object. A verifier comparing an object with a model or batch passport matches the class, not the individual, and MUST NOT present the result as identifying that specific object.

Pre-registration history (previous owners, exhibitions, publications) is not a structured part of this standard; the protocol cannot check it. It MAY appear in description as issuer text. Optional fields and additionalMetadata are issuer statements: a price or similar value is not an offer.

For compatibility the field registeredAt retains its name but means document preparation time. It MUST agree with registration.utcIso8601 and registration.localIso8601 (UTC with +00:00), and ianaTimeZone MUST be UTC. It is NOT a claim about the future mining timestamp. The actual registration moment comes only from the chain. UTC year/month of preparation and mint must agree; see §2 for rollover.

Identification facts belong in anchors[]. Physical/mixed need photo, dimensions, materials and distinguishing_features. Dimensions use one unit (MMT, CMT, MTR, INH, FOT) and at least one positive width/height/depth/diameter. Materials/features must describe the object, not merely exist as empty placeholders. Digital/mixed need exactly one original file_hash matching digital.fileHash. The photo selected as primary MUST be unambiguous: one photo or exactly one data.role == "primary". Photo/file hashes refer to original bytes, not URLs. Metadata-only examples are not evidence of file possession.

The lighter public copy of the primary photo (§8 previewHash) is described by at most one anchor {"type": "photo", "data": {"role": "preview"}, "hash": "sha256:<hex>"}. It is allowed only when a photo anchor with data.role == "primary" exists, and it is never a primary candidate. The anchor is present if and only if previewHash is nonzero, and its hash MUST equal previewHash. It uses the existing photo bit; no mask bit is added. The copy is derived from the primary photo but is a separate file with its own hash; the primary photo and imageHash keep referring to the original bytes.

Anchor type Mask bit
photo 1
dimensions 2
materials 4
distinguishing_features 8
marks 16
file_hash 32
perceptual_hash 64
c2pa 128
nfc 256
numbered_seal 512
fingerprint 1024
dna 2048
unit_key_set 4096
unit_variant_commit 8192
custom / unknown type 2147483648

Bits14–30 are reserved. The reference tool assigns unknown types the unsigned custom bit (bit31), including names such as constructor, __proto__, toString and hasOwnProperty, using own-property lookup; it must not silently invent meaning for reserved bits. Array order is significant. Unit-key anchors additionally follow §20. Additional namespaces are allowed but participate in canonical hashing. Descriptions of custody, sales and restoration are issuer statements; no core transaction establishes that the real-world event occurred.

An anchor is {"type", "data"?, "hash"?, "verification"?}: type is required, hash is sha256: plus 64 lower-case hex digits of the committed bytes, and verification is free text or a profile ID saying how to check it. Type-specific content:

Type Content
photo hash of the image file (required); data.role primary, preview (§8) or another descriptive role such as detail
dimensions data.unit and measures, below
materials data.list, at least one non-empty material or technique
distinguishing_features data.text: defects, craquelure, repairs, what a copy would not reproduce
marks Signatures, stamps, serial numbers and where they are
file_hash hash of the digital original (required), equal to digital.fileHash and on-chain fileHash
perceptual_hash data.algorithm (for example phash-dct-64, pdq) and data.value; supplementary only
c2pa hash of a C2PA manifest, optional data.activeManifest; supplementary only
nfc, numbered_seal Seal data (§6)
fingerprint Measurable object fingerprint: method, hash of the measurement, methodology reference
dna Synthetic DNA tag or microdot marking
unit_key_set, unit_variant_commit Edition commitments (§20); B only
custom or any other name Outside the registry; a custom anchor SHOULD name its type in data.customType

Supplementary anchors never satisfy the identification minimum, and neither do the edition anchors: an edition passport still describes the run with photo, dimensions, materials and distinguishing features.

Dimensions carry a UN/CEFACT Recommendation 20 code in data.unit (MMT millimetre, CMT centimetre, MTR metre, INH inch, FOT foot); every measure in the anchor uses it. Free text such as "cm" is not conforming. Optional upperTolerance and lowerTolerance, in the same unit, state how far a measured object may exceed or fall short of the figures and still match. Without them a verifier applies its own judgement and MUST NOT treat a small discrepancy alone as a failed check. A client SHOULD display the unit in the reader's language (60 × 40 cm, not 60 × 40 CMT); the code is what the document carries.

If any card field, object type or UTC month in passport.json differs from the chain record, a verifier MUST report the integrity check as failed, not as a warning (§11).

C2PA: when a file carries an embedded C2PA manifest, fileHash or imageHash over the unmodified bytes covers content and manifest together. An issuer MAY also commit the manifest separately in digital.c2pa.manifestHash or a c2pa anchor, so that a C2PA-capable verifier can show that provenance beside ODP results. Presence of C2PA data SHOULD be determined by a C2PA parser, not by byte signatures; without it, digital.c2pa is omitted. verificationMethod: "c2pa" is an issuer declaration (CA-16.3). ODP defines no C2PA assertion of its own.

10. Hashing (normative)

chain/tools/canonical.mjs and schema/vectors/ provide conformance vectors.

  1. Decode input bytes with fatal UTF-8 validation and reject BOM (including service JSON and statement payloads). Parse strict JSON. Reject duplicate keys, including duplicates created by NFC normalization. Reject unpaired UTF-16 surrogates, NaN, Infinity and non-JSON values. Do not silently replace characters.
  2. Normalize every string value and object key to Unicode NFC. Do not otherwise trim or case-fold text.
  3. Sort normalized object keys lexicographically by UTF-16 code units at every level. Emit keys directly in that order; constructing a JS object and calling JSON.stringify can reorder integer-like keys.
  4. Preserve array order. Use ECMAScript JSON string escapes and finite binary64 number serialization: negative zero is 0, 1e21 is 1e+21, 1e-7 is 1e-7. Arbitrary precision values must use strings.
  5. Emit compact UTF-8 with no BOM, spaces outside strings, or trailing newline.
  6. dataHash = SHA256(canonical(passport.json)); passportId MUST already be null. Do not replace a supplied assigned ID silently. anchorsHash = SHA256(canonical(passport.json.anchors)) independently.

The receipt, generation manifest, ZIP container and external transport paths/URLs are not included in dataHash. Any path or URL inside passport.json IS included in its canonical hash. Original image/file bytes have their own SHA-256. A reader MUST recompute hashes from the available bytes; trusting a declared hash alone is not a file integrity check. Pretty-print changes can canonicalize identically; semantic changes produce a different commitment. The protocol is not RFC8785 verbatim because it additionally requires NFC.

Two failures seen in practice: a serializer that escapes / (Foundation JSONSerialization writes \/) cannot reproduce the hash of a title such as Портрет 1/25, and one that prints 17 significant digits renders 0.1 as 0.10000000000000001. Compare canonical bytes with the vectors before comparing hashes; a byte diff locates the fault.

fileHash, imageHash and previewHash are the SHA-256 of the exact file bytes. Do not recompress, resize, strip or otherwise modify a file before hashing it; the preview copy is a separate file with its own hash (§22.19). In passport.json a hash is written sha256: plus lower-case hex; on chain it is the raw bytes32. anchorsHash lets a verifier check the identification block alone, for example a compact payload delivered by a low-bandwidth carrier, without the full document; dataHash still requires the whole canonical document.

11. Verification algorithm

  1. Select an approved generation using chain, registry and ABI/runtime pins; never passportId alone.
  2. Read the immutable card/classification/media. A missing record, unavailable chain and revoked record are different results. Read a consistent block, handle reorgs/finality, and display observation time.
  3. Parse/validate/canonicalize the document. Check dataHash, anchorsHash, mask, all card fields, object type, UTC month and numeric classifications. Check previewHash against the preview anchor (§9); they are either both absent or both present and equal. Check original file bytes where available. Report missing files.
  4. Compare receipt identity with the selected registry and on-chain ID; receipt is a locator, not authority.
  5. Establish issuer identity from independently trusted public sources. Profile type, name, domain declaration, authorSigner and affiliation are not sufficient alone. Check passport revocation and each statement lifecycle separately.
  6. For statements, read bounded pages at a fixed block and show institution, document hash, date and withdrawal. Verify documents when available. A count is not a vote, ranking or automatic decision.
  7. For editions, verify §20 anchor/openEdition equality and pinned namespace, then proof/signature/activation. Offline checks cannot assert the latest online activation state; label cached state with its block/time.

If a reader cannot perform a declared verification method or interpret an anchor type, it MUST explicitly report that check as unsupported (user-facing example: “Не умею выполнять эту проверку”). It MAY still show readable data and independently verified integrity results. It MUST NOT label an unperformed check as passed or turn partial integrity success into a complete verification claim. Unsupported is distinct from failed, missing evidence, and a counterfeit verdict.

verifyPassport in chain/tools/passport.mjs checks document integrity/card against supplied chain views. It deliberately does not authenticate the RPC/generation, fetch files or decide identity/trust. A client must implement the remaining steps. Hosting unavailability must not invalidate a locally held bundle.

Checks against the object

Chain and hash checks establish what was registered. Whether the object in front of the reader is the one described is a human comparison, and a client MUST ask for it (CA-16.2):

On-chain creator, creatorId and hashes show which wallet registered which bytes under which generation. They are not copyright, moral rights or title to an object (§16).

Creator wallet proof (optional, off-chain)

An issuer MAY prove control of the issuing wallet without a transaction by signing, with EIP-191 personal_sign, the UTF-8 text:

Object Digital Passport — creator wallet proof (EIP-191) v1

passportId: <Passport ID>
chainId: <decimal chain ID>
contract: <registry address>
nonce: <random unique string>

The verifier recovers the address, compares it with creator from getPassportHeader, and checks that passportId, chainId and contract match what it is verifying and that the nonce is fresh. A match proves key control, not authorship. For an issuer registered to a contract wallet such as a Safe, the verifier MUST instead call isValidSignature(bytes32 hash, bytes signature) (ERC-1271) on creator, with hash the same EIP-191 message hash, and accept only the return value 0x1626ba7e. The answer reflects the wallet's current owners and threshold at the block read, not those at mint time; the verifier MUST record that block. It is a request to the issuer; a verifier MUST NOT ask the person checking a passport to sign anything (CA-12.4).

Wallet document anchor

ODPWalletDocumentAnchor.attestExternalDocument(documentHash, documentUri) lets any registered profile commit the SHA-256 of any file (for example a contract PDF) once per (wallet, documentHash), with an optional URI of at most 512 bytes (EC(50) zero hash, EC(51) long URI, EC(52) repeat). getExternalDocumentAttestation(wallet, documentHash) returns (attested, creatorId, timestamp, documentUri). ExternalDocumentAttested indexes documentHash, so a verifier can find attestations of a locally computed hash in the logs. A match means that wallet recorded that hash at that time; it is not a qualified electronic signature. A document about a specific passport belongs in an institutional proof (§4) or a journal statement (§13).

12. Locators and QR

odp://ODP-YYYY-MM-NNNNNNNNN is the preferred QR payload. An issuer MAY instead print its own https:// link that carries the same ID in exactly one odp query parameter (§22.14). Either way the generation context is the single embedded deployment (§22.12); the link's website is never a verification authority. A resolver MUST NOT silently select a registry when several match. The portable receipt explicitly carries chainId, registry, passportId, transaction hash and block number. HTTPS resolvers are optional carriers and not authorities. A QR can be photocopied; possession of the locator gives no record rights. Unit labels additionally carry the edition index and sufficient generation/satellite context (§20). Never put the concealed activation code in a public QR, URL query, analytics log or public address list.

The odp scheme has one unencoded authority token: a Passport ID (§2), for example odp://ODP-2026-03-004829301, or a Profile ID (§3), for example odp://P-482-930-174-005. A client tells them apart by prefix (ODP- versus C-, B-, P-, M-). Other paths and query keys are reserved, except the unit-label form still to be fixed (§22.14); implementations MUST NOT rely on them. A printed QR uses UTF-8 and error correction level Q or higher. The odp URI names a record; resolving it always uses the embedded generation (§22.12). There is no on-chain index from a file hash to a passport; such a lookup needs an off-chain indexer and is not part of conformance.

13. SDK and satellite requirements

Use the compiled ABI for 0.7-redesign-8, not an earlier redesign or the historical version-byte-only ABI. Build mint inputs from the same canonical document that will be distributed. Independently decode receipts and verify them. Page reads are capped at100 and tolerate arbitrary offset/limit without overflow. Zero limit gives an empty page and total. Unbounded core/proof/concern list reads are absent. The affiliation full list is bounded100.

Principal reads, all without a transaction:

Contract Reads
Core getCreator(creatorId), getCreatorByWallet(wallet), passportExists(id), getPassportHeader, getPassportClassification, getPassportMedia, getPassportReleaseState, getMintOperation(issuer, operationId), getPassportsByCreatorPaged(wallet, offset, limit), CONTRACT_VERSION
ODPPassportProofRegistry getProof, getProofsForPassportPaged, getProofsByInstitutionPaged, proofWithdrawnAt, proofWithdrawalReason, proofOperations
ODPPassportConcerns getConcern(passportId, raisedBy), activeConcernCount, getConcernRaisersPaged, getConcernsByRaiserPaged
ODPStatementJournal getStatement, getStatementsForPassportPaged, getStatementsByAuthorPaged, statementOperations
ODPAuthorAttestation getAuthorAttestation, authorWithdrawalAt, authorWithdrawalReason, hashAuthorAttestation, domainSeparator
ODPHosting getLocations, getPublishingDelegation
ODPProfileDirectory getDomain
ODPRegistryRelations getAffiliatedParent, getAffiliatedChildrenPaged, getAffiliationAudit, isAffiliationPending
ODPWalletDocumentAnchor getExternalDocumentAttestation
ODPEditionUnits getEdition, getActivation, isActivated, commitmentFor, editionPassportByNonce, unitLeaf, labelPayloadHash, activationPayloadHash

The exact signatures are those of the compiled ABI; this table is a guide, not a replacement for it.

Hosting

ODPHosting stores mutable data/image locations up to512 bytes each. Only the original issuer or its own unexpired publishing agent may update. Expiry is exclusive (now < expiresAt). Publishing grants do not grant mint authority; issuers can revoke them, at any time. URLs can be cleared and can be updated for a revoked passport to keep explanations accessible, without changing issuer rights. Hashes never change. A new hosting satellite does not have authority over old hashes. Treat URLs as untrusted transport input. The contract does not parse the strings. By convention (§22.19, CA-19.6) each field MAY hold several ipfs://, ar:// or https:// addresses of the same bytes, separated by single spaces, within the 512-byte limit.

Profile directory

ODPProfileDirectory lets registered B/P/M declare or clear a lower-case ASCII DNS name (IDNs use A-labels). No scheme/path, empty labels, leading/trailing hyphens, >63-byte labels or >253-byte names. The contract verifies syntax, not DNS/HTTPS control. Names/descriptions stay off-chain. A client trust directory is separate and cannot delegate rights in the core.

Concerns

ODPPassportConcerns allows registered P/M to raise a nonzero SHA-256 reason document hash and optional URL up to512 bytes for any existing passport, including revoked ones. One active record per passport/raiser. Only that raiser may withdraw and later raise a new episode. Active counts track unwithdrawn episodes; clients MUST surface statement lifecycle independently from identity. There is no profile-wide concern. Re-raising after withdrawal reuses the pair, never duplicates either index, and increments the active counter once. State is latest episode; full episode history is in readable event logs. Missing state is detected by empty raisedBy, not withdrawnAt == 0 alone. Core has no dependency on concerns or their availability.

Author and wallet attestations

ODPAuthorAttestation is one-shot per passport, submitted only by its original issuer for a nonrevoked passport. The EIP712 message binds passportId, dataHash, creatorId and authorSigner; domain is name Object Digital Passport, version1, chainId, satellite address. Low-s ECDSA signatures are required. A known author key gives an independent signal; an arbitrary freshly declared key proves no human identity. The satellite reads dataHash and creatorId from the core rather than from calldata, so a signature cannot be pointed at other bytes. The attestation is optional: its absence is not a negative signal. The signature path accepts EOA ECDSA keys; ERC1271 off-chain signature execution is not implemented. The authorSigner can independently call withdrawAuthorAttestation once with a nonzero SHA-256 reason, even after passport revocation. getAuthorAttestation().attested means historically published; readers MUST additionally check authorWithdrawalAt and authorWithdrawalReason. Issuer cannot withdraw another signer's consent. Independent author publication, including by contract-wallet transaction callers, uses the journal below. ODPWalletDocumentAnchor allows any registered profile to anchor nonzero SHA-256 bytes once per (wallet, hash), with a <=512-byte URI. It is not a passport-specific institutional proof.

Statement journal (normative lifecycle)

ODPStatementJournal is an immutable satellite pinned to one core; generation role statement-journal. It has no owner, upgrade, forwarding signatures, core write access or deletion. All publication is directly transaction-authorized by a registered profile (contract-wallet callers can register and call). A payload is a canonical JSON document following schema/statement-0.7.schema.json; payloadHash is its SHA-256. Solidity commits the hash and typed envelope; clients MUST check that envelope against the full payload. It cannot verify JSON availability, semantic truth or human identity.

publishStatement(passportId, kind, payloadHash, previousId, operationId) returns a local sequential statement ID. Journal and institutional-proof appends require a nonzero bytes32 operationId, persisted before submission. submitProof(passportId, documentHash, documentUrl, year, month, operationId) uses the same retry model. The public statementOperations(author, operationId) / proofOperations(author, operationId) getters return digest and original ID (zero/empty when absent). Exact replay reverts StatementAlreadyCommitted or ProofAlreadyCommitted; changed inputs revert StatementOperationConflict or ProofOperationConflict. Replay/conflict is checked before registration, subject lifecycle and calendar validation. Failed calls reserve nothing. A new ID deliberately permits independent repeated evidence. Successful calls emit StatementOperationCommitted / ProofOperationCommitted in addition to their existing publication events.

Digests use keccak256(abi.encode(...)) with these ordered fields and types:

Scope is the caller within this deployed satellite (and thus this chain/registry namespace); the same ID in another satellite or wallet is a different operation. Exact replay still identifies the original record after proof withdrawal, statement retraction/supersession, passport revocation or proof calendar rollover. It never republishes or reactivates the record. The journal has no calendar input/check. These checks apply to ABI-decodable calls; invalid enum encoding can fail before function execution.

The operation ID is the lookup key, not a digest field. Clients must authenticate the selected satellite, compare the full digest, and reconcile receipts at their required finality before treating a replay as success.

Global identity is (chainId, registry, journal, statementId); IDs from different journals never alias. Kinds: 1 issuer correction, 2 author declaration, 3 institutional assessment. Kind 0/unknown and zero payloadHash are invalid. The record snapshots caller wallet, profile ID and immutable core dataHash.

previousId=0 creates a new independent claim. A nonzero previousId must be an Active statement of the same wallet, kind and passport in this journal. Publication atomically marks that predecessor Superseded and records both directions of the link. Competing replacements of one predecessor are serialized: the first succeeds, the stale second reverts StatementNotActive. No timestamp-based branch resolution or implicit selection among independent claims is permitted. Retracting a successor does not reactivate its predecessor. Revisions of withdrawn claims start a new independent chain; clients retain all history.

retractStatement(id, reasonHash) is allowed only to its original caller, with a nonzero SHA-256 reason, for an Active statement, including after passport revocation. A terminal statement cannot be retracted/replaced again. Status: Missing=0, Active=1, Retracted=2, Superseded=3. getStatement rejects a missing ID and returns immutable Statement plus Lifecycle; lifecycle time is block inclusion time, not an assertion of when a real-world event occurred. Pagination by passport/wallet caps results at 100 and includes terminal records. Read all pages and events at a fixed independently verified block.

Existing proof/concern/one-shot author satellites remain separate namespaces. Their own withdrawal APIs change only their own lifecycle; a journal declaration never silently retracts another namespace. The client must report the namespace and completeness of history. Full reason/payload bytes require separate availability; on-chain hashes do not restore missing documents.

14. Versioning

CONTRACT_VERSION is the historical packed nibble major*16+minor, currently7. Values are only meaningful when both components are <16. It is not a general semantic version and cannot identify a build or ABI. The redesigned core and retired core both report7; their authenticated generation manifests MUST distinguish them. New document/satellite versions may coexist when old interpretations and namespaces remain available. Changing immutable core semantics requires a new registry. There is no upgrade promise or record migration. Derive major as CONTRACT_VERSION >> 4 and minor as CONTRACT_VERSION & 0x0f; the core exposes no separate getters for them or for the mint caps of §3. Satellites that record a version byte (§4) MUST report the same byte as the core they are pinned to. Every passport.json carries version; a breaking change to the document shape changes it. The stable line will be 1.0, which will define any migration or dual reading explicitly. Profile type prefixes change only through a specification update (§3).

15. .odpass bundle

15.1 Format

An issued .odpass MUST be a ZIP container with exactly named root JSON files passport.json, generation.json, receipt.json, and manifest.json; original referenced files reside in files/. JSON encoding MUST be UTF-8 without BOM and duplicate keys MUST be rejected. Names are case-sensitive. The external archive filename is not authoritative. Bundle 0.7 format defines the directory layout and semantic checks; its JSON Schemas define the exact service-file templates. passport.json MUST follow the existing passport schema and canonical encoding (§10). A pre-mint draft is not an issued passport bundle. Prepare/hash the document before mint; after a successful mint add receipt/manifest WITHOUT changing passport.json or original files.

generation.json MUST include:

The manifest MUST reproduce the authenticated fixed deployment identities of §7; runtime hashes, deployment blocks and source/build identity MUST also be retained from that deployment manifest. These are actual deployed identities, not unlinked bytecode templates or placeholders. No such addresses are approved by this document.

15.2 Verification rules

A receipt MUST contain matching generationId, chainId, registry, assigned passportId, the successful nonzero operationId, transactionHash, blockNumber and blockHash, with format: "odp-bundle-receipt-0.7". Readers MUST reject conflicting identities between receipt, generation manifest, selected chain record and any hash-bound satellite references such as unit_key_set.data.satellite. They MUST NOT repair conflicts by substituting their current default contracts. Unknown generation/ABI means unsupported or unauthenticated, not permission to guess. A copied manifest or receipt alone is not proof of chain state.

The full original photo/files MUST be available within a complete bundle; reading that local bundle does not require fetching a separate photo URL. When previewHash is nonzero, the lighter public copy (§9) is a required payload as well, so a complete bundle holds both the original primary photo and its copy. Mirrors may distribute the same document/files or a repackaged bundle, but MUST NOT change the committed content. Receipt, generation manifest, external transport locations and ZIP metadata are outside dataHash; every field inside passport.json remains subject to canonical hashing (§10).

manifest.json uses format: "odp-bundle-manifest-0.7" and lists payload paths, SHA-256 hashes and byte lengths. Payload path is files/ followed by its 64 lowercase SHA-256 hex digits. Readers MUST validate file bytes and required payload completeness independently of the editable manifest. Required payloads follow from passport.json, never from a manifest flag: the file of every photo and file_hash anchor, digital.fileHash and, for an edition, the address list named by unit_key_set.data.addressListHash. The address list is public and secret-free; it MUST travel in the edition's .odpass, because without it no verifier can rebuild the tree and no unit can be activated. addressListUrl is a mirror, never the source of truth.

Harden archive extraction against traversal, duplicate paths and oversized payloads. Machine-readable service-file schemas are supplied in schema/bundle-0.7. chain/tools/bundle.mjs implements validation of already extracted entries: strict JSON, canonical passport, cross-file identity, role/address completeness, payload hashes/lengths and required originals, full edition address-list tree, and optional exact comparison with an independently trusted generation manifest. It does not parse ZIP or authenticate chain evidence. The safe ZIP importer/exporter and client integration remain required before claiming complete bundle conformance. Storage, publication and retrieval follow §22.19; the format does not mandate a hosting service or provider.

15.3 Trust model and limitations

A bundle is untrusted input and is treated as data only (CA-13). It does not replace chain state: verification rests on the on-chain commitments. Every path ends at a read from one chain, and this is stated here rather than left for an implementer to discover.

With no chain access a verifier can still open the bundle and check its structure, recompute the hashes of passport.json, its anchors and every payload, and check them against each other, that is, confirm that the bundle is internally consistent. It can show those hashes for comparison with a value obtained elsewhere, or compare them with chain data it authenticated earlier (§19). It cannot establish that the document was registered, by whom or when, or whether the passport was later revoked or finalized for print. A forger can build an internally consistent bundle. Such a result MUST be shown as unconfirmed, with the time of any cached chain data (CA-11.4, CA-17.1), and MUST NOT be worded so that it reads as a positive verdict (CA-16.1).

A verifier MUST NOT depend on a single RPC endpoint. It MUST be able to try more than one and SHOULD let the user supply their own; a self-hosted node depends on nobody. "Could not reach the registry" is an ordinary event and MUST NOT be shown as "invalid" or as "not an ODP passport". If Polygon PoS itself stopped, records would remain in its history but live verification would stop for everyone; this line has no fallback anchor.

16. Limits and external statements

The protocol does not adjudicate provenance, intellectual property, authenticity, ownership, or disputes. Issuer corrections after the applicable window or print finalization can be published through the journal (§13), committing an external payload with explicit original/new passport references, or distributed off-chain. Independent P/M proof documents can add assessments. The core has no general issuer-event history API or on-chain replacement pointer; the journal provides its own history and never changes core content or extends the revocation window. Public data is permanent: clients SHOULD avoid personal information that should later be erased.

This specification also does not define: who stores or hosts .odpass files and originals (§16.1, §22.19); the visual design of clients and labels (§5); pricing, sales or marketplace mechanics; networks other than the single deployment of §22.12; human-readable names for profiles; which seal product to use (§6); or C2PA integration beyond hash commitments (§9).

16.1 Durable hosting (normative SHOULD)

Not choosing a host is not the same as having no view on how. An issuer or holder that publishes passport.json, the photo copy or an .odpass SHOULD use locations that are:

A personal file-sharing link meets none of these. When every online copy is gone, the on-chain card (title, author name, short description, domain), the classification and the hashes remain readable. What is lost is the identification evidence in anchors[] and the originals, unless someone holds the .odpass. That is a degradation, not a revocation, and a client MUST present it as such (CA-9.1, CA-19.7).

17. Wallet and key management

Keep real issuer/master keys outside public documents, logs, tests and the repository. An issuer who has lost their key has no administrator recovery path. Separate author keys have only their explicitly assigned attestation role; mint delegation is absent. Compromise of an issuer can mint misleading descriptions and revoke its recent passports within the role-specific window unless finalized for print. It cannot edit immutable older records. Permissionless registration enables impersonation and Sybil flooding: independent identity and client filtering remain necessary.

The protocol requires only an Ethereum-compatible account that can send transactions; how its key is made and kept is the holder's choice and responsibility. A key MUST be generated on the holder's own device or hardware and MUST NOT be sent to ODP, a client vendor or any server; release clients hold no keys (CA-20.1). An issuer SHOULD use a wallet dedicated to ODP, not one used for balances, trading or daily payments. Common arrangements, all compatible: a software wallet backed by a seed phrase kept offline on paper in more than one place; a hardware device, also backed by a seed phrase; seed-less hardware, which needs a second device bought before registration because losing all devices loses the profile; and, for B, P and M, a multisig account (CA-5.6, CA-5.7). No wallet brand is normative.

If an issuer loses its key: its profile ID and every passport it issued stay on chain and remain verifiable; it can no longer mint, revoke within a window, finalize for print, open an edition, raise or withdraw its own statements, or declare a domain under that profile. A new wallet is a new profile (CA-1.4).

18. Interoperability

Hashes, full IDs, generation context and algorithm versions are required at integration boundaries. All original data and historical addresses must remain exportable. Public statements may be independently indexed, but an indexer omission is not evidence of absence. The core supports Ethereum-style ABI readers; client SDKs, NFC hardware, DID methods and legal attestations require their own conformance work.

ODP is meant as a verifiable registry beside, not instead of, regulatory Digital Product Passports, GS1 identifiers, IIIF manifests, C2PA content credentials and institutional catalogues.

18.0 Position relative to the EU DPP (ESPR)

The regulatory DPP is the one established by Regulation (EU) 2024/1781 (Ecodesign for Sustainable Products Regulation, ESPR), Chapter III. An ODP passport is not an ESPR DPP. It is no conformity route, product-group delegated acts do not apply to it, it has no unique identifier registered with the Commission, it is not connected to the DPP registry (Art. 13) or the web portal (Art. 14), and it makes no claim about the sustainability information ESPR requires. Nothing here helps an economic operator meet an ESPR obligation, and an implementation MUST NOT present an ODP passport as satisfying one.

Two points of Art. 11 describe properties ODP happens to share, and the overlap stops there:

ESPR expects service providers and a registry; ODP deliberately has neither. That is a difference in kind.

18.1 Optional passport.json content

Issuers MAY add namespaces such as sustainability, compliance, identifiers.gtin (a GS1 GTIN) or iiif.manifest, or string values in additionalMetadata. Everything inside passport.json is part of dataHash (§10) and is an issuer statement: the protocol checks that the bytes are unchanged, not that the content is true. A GTIN or other identifier does not prove authenticity and does not replace the passport ID. A GS1 Digital Link or other URL is not an ODP label (§22.14). Additional photos are further photo anchors.

18.2 The did:odp naming convention (informative)

odp is not a registered DID method. There is no method specification, no resolver and no entry in the W3C DID Specification Registries. A did:odp:… string is a naming convention for documents produced by this project, nothing more. Software that requires a resolvable DID (a Verifiable Credentials issuer or verifier, a wallet, a profile that mandates did:web or did:webvh) cannot consume it, and an implementation MUST NOT present ODP as offering DID support. Within that limit, tooling MAY use did:odp:passport:<Passport ID> and did:odp:profile:<Profile ID> and MAY export a DID-document-shaped JSON built from public chain data at any time, without a transaction. Such a file asserts nothing the chain record does not. Only the odp:// scheme (§12) carries protocol meaning.

18.3 Verifiable Credentials

Institutional proofs and journal statements MAY be mapped to credential-style claims in wallets or catalogues. The on-chain records and their lifecycle (§4, §13) remain what a verifier checks.

19. Availability and client policy

Offline integrity verification uses locally held bytes and previously authenticated chain data. Fresh chain status needs a node/RPC and finality policy. Institutions, domain registries, hosting providers, label printers and relayers are separate trust surfaces. Relayers cannot forge a unit signature, but can delay/censor submission, observe a submitted activation or consume its one-shot record earlier than another courier. Submission time is not acquisition time. A paid relay is optional: any caller can submit a valid signed activation.

Storage networks, pinning and upload services, gateways and sponsors are replaceable conveniences, not trust sources. The owner's .odpass is the primary copy; a published file is accepted only when its bytes match the committed hash, whatever route delivered them (CA-14.4). A client MUST NOT depend on one gateway or one service, and the loss of every online copy MUST NOT be reported as a change of the chain record (§22.19). A gas sponsor can stop funding a wallet but cannot act for it (§22.20).

20. Edition keys and activation

20.1 Scope

The original registered B issuer may open an existing nonrevoked passport with editionModel limited(2) or open(3), unit_key_set bit4096, nonzero root and nonce, and unitCount1..1048576. Opening is issuer-only and write-once per satellite/passport. It does not create unit passports, assign ownership or alter the core.

Every additional production run/reprint MUST be a new edition with a new passport and a fresh editionNonce and key set. The original edition's count, root, description and original files MUST NOT be extended or rewritten. An open classification does not authorize appending units to an existing passport. Each issued key set is finite and immutable; any later run is independently identified and verified. A new edition MUST NOT inherit activation state from an earlier edition. No mutable umbrella-series contract is required.

An edition passport carries exactly one unit_key_set anchor and at most one unit_variant_commit anchor. When edition.total is present it MUST equal unitCount. Its identification anchors describe the run, not any one unit. Only B may issue editions because the mechanism needs a controlled key process and secure printing, and because a mis-issued edition cannot be corrected unit by unit.

20.2 Prepare before mint

Choose a cryptographically random32-byte master seed and independent nonzero32-byte editionNonce. Select the registry and permanent activation satellite BEFORE deriving keys. Commit these in unit_key_set.data: editionNonce, chainId (decimal string), registry, satellite, keyDerivation: "odp-unit-v2", merkleRoot, unitCount, hashAlg, leafFormat, addressListHash and optional labelSignerKey/addressListUrl. The root exists before passportId; mint hashes the completed document. Before mint, also compute: editionCommitment = keccak256(abi.encode("ODP-EDITION-COMMITMENT-0.7", uint256(chainId), address(registry), address(issuer), address(satellite), bytes32(editionNonce), bytes32(merkleRoot), uint32(unitCount), address(labelSigner))). An omitted labelSigner is address(0). The domain fixes the algorithms of §20.3/20.5; changing those requires a new commitment format. This commitment is persisted in the core and included in operation replay protection. commitmentFor on the satellite and tools/edition-commitment.mjs expose it.

After mint, the original registered issuer calls openEdition. The satellite MUST reject any disclosure whose computed commitment differs from the core, including another issuer or satellite. A (issuer, editionNonce) can open only once in that satellite, even after revocation; editionPassportByNonce identifies its original passport. This uniqueness does not extend across different satellites or registries. Invalid openings reserve nothing.

Verifiers MUST still compare ALL parameters against the hash-bound JSON anchor and selected namespace. The contract does not parse JSON: it enforces core-to-satellite equality, not semantic truth of the document. Clients MUST complete mint → confirmed open → anchor/commitment equality checks before distributing final edition labels. No profile stop can strand opening; key loss, passport revocation and invalid commitments can still prevent completion.

20.3 Merkle tree

Index is zero-based uint32 big-endian. Leaf = SHA256(index4 || unitAddress20). Internal node = SHA256(left32 || right32). Duplicate the last node at each odd level. Proof siblings are ordered bottom-up; index bits select left/right. The canonical proof has ceil(log2(unitCount)) levels. The contract caps proof length32, while the count cap needs at most20 levels. Clients MUST construct/check the canonical count/tree. Root is a commitment, not an encrypted address list. An optional published address list is lower-case full addresses in index order, one per line with LF including final LF; its SHA256 is addressListHash. It MUST NOT contain codes/private keys. It travels in the edition's .odpass (§15). The whole set costs one 32-byte root on chain, whatever its size.

Indexes MUST be assigned independently of cartons, regions, distribution batches and release waves, for example by shuffling at labelling time. Activations are public, so indexes that follow packing order would let anyone read regional sell-through from the chain.

Rebuilding the root from the published list checks the edition: that the list is the one committed at mint, including against replacement by the issuer. It says nothing about the unit in front of the reader, because every index below unitCount is in the tree by construction. A verifier MUST NOT present a successful membership check as evidence that a particular object is genuine.

20.4 Optional variant commitment

A concealed variant can use a separate Merkle root over SHA256(index4 || byteLength(variant)2 || NFC_UTF8(variant) || randomSalt32), using the same tree rules. Salt remains inside the package, not the public document. No on-chain variant evaluation is implemented. The variant unitCount MUST agree with the edition total (if present) and its unit_key_set count (if present). A reader checks the disclosed variant/proof against its separate hash-bound anchor. Distinct root domains and leaf formats MUST NOT be interchanged.

Each randomSalt32 comes from a CSPRNG and is unique per unit. It MUST NOT be derivable from the unit key, the index or anything readable without opening the sealed package: variant vocabularies are small, so whoever holds the salt can test every candidate, and a salt derived from the outer code would let a reseller learn the contents by scratching the label. The salt MUST be carried inside the sealed package, for example on an enclosed card; the issuer MUST NOT publish it before the unit is opened and MAY keep copies as an optional recovery path, never as a dependency. The length prefix and fixed salt size make the commitment open to one variant only; a substituted salt card fails to verify rather than proving another variant.

20.5 Key derivation v2

All concatenations below are byte concatenation. chainId32 is unsigned big-endian; registry is20 bytes. context = ASCII("ODP-EDITION-v2") || chainId32 || registry20 || editionNonce32. secret_i = HKDF-SHA256(masterSeed32, emptySalt, context || uint32be(i), 32). Take the first100 bits: first12 bytes and high4 bits of byte12; low4 bits of byte12 are zero. This is seed13. For counter0,1,... derive h = SHA256(ASCII("ODP-UNIT-KEY-v2") || seed13 || context || uint32be(counter)). Use the first scalar with 0 < uint256(h) < secp256k1_order; otherwise retry. Derive its EVM address normally. This is intentionally incompatible with the circular passportId-based v1 derivation. Do not mix label batches. At N=2^20, generic any-of-N search on100-bit secrets is about2^80; larger unit sets require a new format.

20.6 Printed code

Encode exactly100 bits MSB-first as20 Crockford Base32 characters using 0123456789ABCDEFGHJKMNPQRSTVWXYZ. Append5 checksum characters encoding the first25 bits of SHA256(ASCII(payload20)). Print five groups of five. Reader uppercases, removes spaces/hyphens, maps I/L→1 and O→0, checks length/alphabet/checksum, and reconstructs seed13. Checksum is typo detection, not authentication. Keep the code under tamper-evident concealment; whoever learns it can sign. Store the master securely and do not reuse context/master combinations across distinct production runs. The alphabet and checksum are global and MUST NOT be localized. The code length is a security parameter: an attacker can test guesses offline against the public list and needs any valid unit, not a particular one, so an implementation MUST NOT shorten the code for usability.

20.7 Outer labels

Optional label signature uses personal_sign over the32-byte keccak256 of ASCII("ODP-UNIT-LABEL-v1") || chainId32 || satellite20 || UTF8(passportId) || index4 || merkleRoot32. The expected labelSigner is the one committed before mint and registered at open; zero means plain labels. Readers can verify labels offline with an authenticated edition bundle. A genuine signature can be copied. It proves only that the specified key signed that label, not that the attached object matches the description. Signing prevents fabricated labels, not copied ones. A verifier MUST NOT downgrade an edition with plain labels.

The outer carrier, readable before purchase, MUST make the edition passport ID and the unit index recoverable, and MUST also print both as readable text, so a unit stays checkable when a symbol is damaged or its encoding changes. It MUST NOT carry the unit code or anything derived from it. The concealed code has its own printed form under the concealment layer; its symbol format is being fixed with print tests (review/qr-07). The label MUST be applied so that removing or moving it is visibly destructive, for example across a package seam; this is the only physical binding in the mechanism, and no cryptographic property replaces it.

20.8–20.9 Activation

Unit key signs personal_sign over keccak256 of ASCII("ODP-UNIT-ACTIVATE-v1") || chainId32 || satellite20 || UTF8(passportId) || index4. Any courier may call activate(id,index,proof,signature). Low-s ECDSA and valid-v are required; index must be in range, root proof valid, and slot unused. First valid submission stores unitAddress and block timestamp. Duplicate attempts revert. This is not replay-proof physical ownership: leaked/cloned keys permit early use. Because a duplicate reverts rather than succeeding as a no-op, a courier's dry run rejects it before any fee is spent. The signature binds chain, satellite, edition and index, so it is valid for one slot only. A signature MAY be produced offline and submitted later from any device, by anyone. An activation carries no verdict: an earlier activation can mean a cloned code or a legitimate resale, and the protocol cannot tell which. A verifier reports the facts (index, unit address, block time) and MUST NOT rank or label them as counterfeit or stolen; no concern is raised automatically (§13).

No ODP-operated service, repository or maintainer may hold a master seed, a share of it or a unit key for any edition. The reference tools derive keys offline and store nothing. A P or M profile MAY witness the key process by submitting an ordinary institutional proof (§4) on the edition passport.

20.10–20.13 Lifecycle and removal of unit passports

There is no unit-passport mint or transfer. Edition activation remains possible for an already opened nonrevoked edition. Passport revocation blocks new activations but preserves recorded ones. First activation does not close the B issuer 24h revocation window. Only expiry or explicit print finalization closes revocation. Display activation, revocation and print finalization independently. Readers must retain the pinned satellite even after client defaults change. No satellite can write to the core. A later issuer correction (§13) that points to a new edition is prose about the run. It MUST NOT be turned into a machine-readable "superseded" or "invalid" state for the earlier edition: its units keep their committed keys and verify as before.

20.14 Stated limits

A client and its marketing MUST NOT claim assurances the mechanism does not give. In particular:

  1. The issuer knows every unit key when it generates them and can activate units itself; no outside party can verify that the master seed was destroyed.
  2. The print vendor necessarily sees the codes; this is controlled physically, not cryptographically.
  3. Anyone who knows the codes can activate units they do not hold, including an insider before shipping. Every activation has a public time; judging whether it is plausible is left to people, and an issuer can explain a poisoned run with a correction statement (§13).
  4. Before its concealment layer is removed, a sealed counterfeit carrying a copied code looks the same as the real unit; the activation state is the only pre-purchase signal.
  5. A unit key binds the package, not the object inside it; a variant commitment (§20.4) binds only the variant.
  6. The activation log is public commercial data: with the on-chain unitCount anyone can reconstruct run size and sell-through over time. An issuer MUST be told this before it opens an edition. Index shuffling (§20.3) limits the leak to totals.

21. Pre-release validation boundaries (ABI 0.7-redesign-8)

JSON input bytes MUST be decoded with fatal UTF-8 validation; replacement decoding and BOM are forbidden. An edition Merkle root MUST be nonzero. Issuance preflight MUST obtain the complete address-list bytes (from local storage or an independently permitted transport), check exact lower-case address lines with a final LF, declared count and SHA256, and rebuild the positional tree (including odd-node duplication) before minting. A syntactically valid typed commitment is insufficient. preparePassport checks shape and commitments; prepareIssuance additionally requires the edition list. Bundle integrity includes this tree check but does not prove possession, activation, authenticity or identity. Unsupported verification methods/anchors MUST remain explicitly unsupported in the per-capability report.

21.1 Reference statement validation and ABI errors

verifyStatement(bytes, {chainId, registry, journal}, record) in chain/tools/statement.mjs requires strict UTF-8, schema-valid canonical bytes, and equality of namespace, passportId, subject dataHash, author wallet, kind, previousId and payloadHash. The supplied record/context must be authenticated separately. Its successful report verifies content integrity only; author identity/lifecycle remain unverified and truth unsupported. Schema validation alone is insufficient for these semantic checks or for edition/address-list validation.

The following is a focused integration table for ABI 0.7-redesign-8, not an exhaustive list of Solidity errors. Decode against the selected contract ABI in chain/abi/; the same EC number can have context-specific uses. The earlier 0.7-redesign-7 line had no previewHash and emitted neither EC(142) nor EC(143). The 0.7-redesign-6 line before it emitted neither PassportPrintFinalized nor the non-unique-edition rejection below, and used a single 72-hour window for every profile type.

Call / condition Result in 0.7-redesign-8
Zero operationId: mint / journal / proof InvalidOperationId() / InvalidStatementOperation() / InvalidProofOperation()
Exact committed mint AlreadyCommitted(bytes32 operationId,string passportId)
Exact committed journal / proof StatementAlreadyCommitted(bytes32 operationId,uint256 statementId) / ProofAlreadyCommitted(bytes32 operationId,string proofId)
Changed digest under committed ID MintOperationConflict(bytes32) / StatementOperationConflict(bytes32) / ProofOperationConflict(bytes32)
revokePassport: missing / already revoked / wrong caller / expired / zero reason EC(12) / EC(18) / EC(17) / EC(132) / EC(16) respectively
revokePassport after print finalization PassportPrintFinalized(), checked before the deadline test
finalizePassportForPrint: missing / wrong caller / revoked EC(12) / EC(17) / EC(18); a repeat on an already finalized passport succeeds as a no-op
getPassportReleaseState on a missing passport EC(12)
Non-unique editionModel (limited/open/dynamic) or bits 4096/8192 from a non-B profile EC(121)
Mint with nonzero previewHash and zero imageHash EC(142)
Mint with nonzero previewHash equal to imageHash EC(143)
New mint/proof in a different UTC month EC(68) (committed operation lookup precedes this check)
Replacement of own terminal statement StatementNotActive()
Wrong edition disclosure / reused issuer nonce EditionCommitmentMismatch() / EditionNonceAlreadyUsed(string passportId)
Historical profile-stop error EC(131) Not emitted by any production contract in this generation; no stopped-profile state exists

21.2 Deployment evidence boundary

The ten-contract roster is the core plus concerns, hosting, profile directory, institutional proofs, author attestation, wallet document anchor, relations, edition units and statement journal. A release bundle pins complete compiler input/output and exact ABI/creation/runtime/immutable bindings. The bundle for 0.7-redesign-8 is review/v07-abi8-release/ (hash approved by the owner on 2026-09-24), and the generation in §7 was deployed from it on 2026-09-25. The earlier bundles describe earlier sources: 0.7-redesign-6 (sealed audit package) and 0.7-redesign-7 (review/v07-abi7-release/). Approving a bundle does not by itself authorize a deployment. Deployment uses pinned release bytes with compilation disabled, a checked manifest/resume identity, write-ahead transaction plans, exclusive manifest lock, nonce sequence and explicit gas/fee/total spend caps. On Polygon, two separate RPC services verify transactions, canonical receipts, runtime and finalized blocks; all ten contracts are rechecked before a generation candidate is emitted. Unknown submissions must be reconciled, never blindly redeployed. Two RPCs remain a trust assumption, not a light-client proof. See deployment procedure. A local candidate is not production approval; the deployed generation was approved separately (§7).

22. Client application requirements (normative)

Contracts cannot see screens, printers or people. These requirements bind any application that issues, prints or verifies ODP 0.7 passports; a client that breaks one of them is non-conformant even if every transaction is valid. Rationale and acceptance scenarios: usage-safety audit, decisions: closure log.

22.1 Issuer keys and recovery (D1)

22.2 Revocation window and print finalization (D2)

22.3 Authorship (D3)

22.4 Activation and handover (D4)

22.5 Issuance job integrity (A1)

22.6 Interrupted issuance and recovery (A2)

22.7 Edition secrets and the .odpsecret file (A3)

A valid Merkle root proves that the public address list was not altered. It does not prove that the secrets were kept, that every key is distinct, or that the right code was printed on the right label.

22.8 Hand-out before print finalization (A4)

With CA-2.2 a conforming issuer cannot hand out edition labels while the passport is still revocable. The remaining gap is labels printed outside a conforming client.

22.9 What each recovery path restores (A5)

The chain holds the card and commitments, not the originals. A QR restores the chain card; an issuer key decrypts nothing and restores no files; only byte-identical copies restore the originals.

22.10 Institution identity and independent directories (A6)

Anyone may register a P/M profile, declare any syntactically valid domain (ODPProfileDirectory) and publish any number of statements. A domain check proves control of that domain only; a look-alike phishing domain passes it too. ODP has no central trust list. Identity comes from independent, operator-run directories in the ODP Profile Directory format, where the operator decides which domain is the organization's real one.

Domain publication endpoint: an organization publishes its profile IDs at https://<domain>/.well-known/odp.json as a JSON object {"odp": 1, "chainId": <number>, "registry": "0x…", "profiles": [{"profileId": "…", "wallet": "0x…"}]}. odp is the format version and MUST be 1; an entry MAY override chainId/registry. Each entry SHOULD carry wallet, the full 42-character address bound to the profile (§3, public identity). A profile counts as published only when profileId, chainId and registry all match the generation being verified; when wallet is present it MUST also equal getCreator(profileId).wallet (case-insensitive), otherwise the entry does not count. The file is served over HTTPS as application/json from the organization's own domain, not a shared hosting or social-network domain; a static file at this reserved path (RFC 8615) is readable without a browser engine and writable only by whoever controls the server. The endpoint is advisory: no mint, statement or verification result depends on it, and a missing file invalidates nothing; it only leaves the identity unconfirmed.

22.11 Reading lifecycle and history completely (A7)

22.12 One network, one registry (A8)

ODP 0.7 uses exactly one deployment: Polygon mainnet (chainId 137), one registry and its satellites, fixed by the single approved entry in chain/generations.json once deployed (§7). There is no second network and no alternative registry. A self-consistent .odpass can describe a fake deployment, so its own manifest and receipt are never a trust root.

22.13 Importing untrusted .odpass files (A9)

A matching hash proves the bytes are unchanged since issuance, not that they are safe to open.

An ODP QR has one of two forms. The preferred form is odp://<passportId> for a passport and, for an edition unit, the edition passport ID with the unit index in the same odp: form (exact unit-label encoding to be fixed with physical print tests, review/qr-07). The issuer link form is the issuer's own https:// address that carries the same identifier in an odp query parameter, for example https://issuer.example/p?odp=ODP-2026-09-123456789: the system camera opens the issuer's website or app, and an ODP scanner reads the identifier and verifies it without the website. GS1 Digital Link and other https:// payloads without a valid odp parameter are not ODP labels. A counterfeit label can still carry any URL, including a look-alike domain with a genuine passport ID, and the phone's camera, not the ODP app, reads it; passports also contain hosting and proof URLs that their publishers can change.

22.15 Permanent publication and privacy (A11)

Chain data, events and distributed archives cannot be erased. A hash is a commitment, not encryption; a guessable personal value can be recovered from its hash by trying candidates.

22.16 No overall verdict (A12)

Each check is honest on its own; merged into one success it invites the false conclusion "this object is genuine". A real label moved onto a counterfeit passes every chain check. Classification fields such as verificationMethod are issuer declarations, not performed checks.

22.17 Accepted residual risks (R1–R3)

These risks remain by design and are accepted with the limits below.

22.18 Warnings before irreversible actions

ODP has no support desk that can undo an action. As with a crypto wallet, the client is the only place where a person learns what cannot be taken back. Warnings are therefore required at a few fixed moments, not everywhere, so that they are still read.

22.19 Photo copy, storage and publication

The chain holds commitments, never files. The owner's .odpass is the primary copy of every file; each network location is a replaceable convenience that is checked by hash. Publication is permanent in practice. Arweave data cannot be deleted, and IPFS copies held by others cannot be recalled.

22.20 Wallet connection and gas sponsorship

ODP runs no servers and no relayer in 0.7. A sponsor, such as a university or publisher, pays for gas only by sending POL from its own wallet to the addresses of the people it supports.


Object Digital Passport is open source under the MIT License. Gaps and ambiguities in this specification are reported as a Standard gap issue, in English.