ERC-8326 Document Bundle Hash Anchor Standard (Canonical Document Bundle Anchor)

Final · Other · created 2026-07-05

A standard that sorts multiple off-chain documents according to a fixed rule into a single hash (bundle hash), records it on-chain by subject and role, and preserves a history of supersessions.

Why it is needed

When a contract needs to reference multiple off-chain documents, such as contracts, receipts, or certificates, implementations typically bundle the documents' hashes into a single value. Without a common rule, however, each implementation differs in how it lists document entries, the sort order, versioning, and handling of replacements. As a result, two systems holding exactly the same documents can compute different bundle hashes and lose interoperability. Mixing the normalization step, which converts original files into standardized bytes, with the manifest step, which lists those results to produce a hash, also means the entire rule set shifts whenever format-specific handling changes. This standard separates the two steps and fixes how the manifest is built and recorded on-chain, so that anyone with the same set of documents arrives at the same hash.

What to implement

In depth

The five fields of a document entry

Each document is represented as a fixed-width struct, DocumentEntry, consisting of five bytes32 values. contentHash must be the keccak256 of the bytes produced by the normalization profile. role is the function the document serves within the bundle; six roles are defined (LEGAL_BASIS, EVIDENCE, CERTIFICATION, AGREEMENT, AMENDMENT, SUPPORTING), and applications may add more. mimeTypeHash is the keccak256 of the standard IANA media type in lowercase ASCII with parameters stripped. filenameHash is computed by treating / and \ as path separators, keeping only the portion after the last separator, lowercasing ASCII uppercase characters, applying Unicode NFC normalization and UTF-8 encoding, and then taking keccak256. normProfileId identifies which transformation produced the bytes, and consumers must not interpret contentHash without this value.

Three normalization profiles

PROFILE_RAW uses the input bytes unchanged. PROFILE_JSON_RFC8785 uses the UTF-8 output of RFC 8785 canonical serialization and must reject inputs that violate RFC 8785 and I-JSON constraints, such as duplicate keys, invalid Unicode, lone surrogates, or numbers outside the interoperable range. Unicode normalization is not applied to JSON string values. PROFILE_XML_C14N11 uses Canonical XML 1.1 output without comments and must disable resolution of external entities. Normalization for PDF, images, signed PDFs, and plain text is not defined by this standard, so using PROFILE_RAW is recommended absent separate agreement. Custom profiles are recommended to follow the format NORM:CUSTOM:<namespace>:<format>:<version> and must define exact transformation rules and test fixtures.

Canonical ordering and bundle hash derivation

Entries must be sorted lexicographically by comparing the five keys role, filenameHash, contentHash, mimeTypeHash, and normProfileId in ascending order in that sequence, moving to the next key only when all preceding keys are equal. Duplicate entries are preserved and affect the resulting hash. Each entry becomes a leaf by concatenating its five fields with abi.encodePacked and taking keccak256, and the bundle hash is the keccak256 of SCHEMA_V1 (keccak256("ERC-8326:BUNDLE:V1")) concatenated with all leaves. Since every element is fixed-width bytes32, there is no boundary ambiguity in the packed encoding. A bundle must contain at least one entry, and incompatible future schemas must use a new identifier rather than reusing SCHEMA_V1.

Anchoring rules and slots

A record is keyed by the triple (bundleHash, subjectId, role), so the same bundle hash can be anchored independently under different subjects or roles. A slot, defined by (subjectId, role), can hold at most one valid bundle at a time. anchorBundle rejects a zero hash, subject, role, or document count, and also rejects an existing triple or a slot that already has a valid bundle. Replacing an occupied slot is only possible via supersedeBundle. On success, anchoredBy is set to msg.sender and anchoredAt to uint64(block.timestamp), and BundleAnchored is emitted. metadataURI may be left empty, meaning there is no on-chain lookup pointer. Anchoring must be restricted to authorized callers, and the authorization method is left to the implementation but must be documented.

Supersession and the append-only history

supersedeBundle rejects a zero new hash, subject, role, or document count, and also rejects a case where the old and new hashes are identical. The old record must exist, must not already be superseded, and must be the valid bundle for that slot, and the new triple must not already exist. The replacement atomically sets the old record's superseded flag to true and supersededBy to the new hash, creates a new valid record, updates the slot, and emits both BundleSuperseded and BundleAnchored for the new record. The remaining fields of the old record stay unchanged and remain queryable, preserving the audit trail. A different authorized anchor must not hijack another party's slot.

Optional recovery extension

Anchor keys can be leaked or a slot can be occupied without authorization, and if an administrator attempts a direct replacement, the current owner could front-run it by superseding first. The recovery extension therefore atomically reassigns slot ownership instead. When this extension is implemented, the slot owner must be set to msg.sender on the first successful anchoring and on each successful supersession. assignSlotPrincipal may only be called by an authorized recovery administrator, and after reassignment the previous owner must not be able to supersede that slot using ordinary anchor permissions alone. Assigning ownership does not itself grant anchor permission, and because it requires trusting the administrator, it is kept separate from the core interface so deployments wanting immutable permissions can omit it.

Consumer verification procedure and ERC-165

Consumers relying on a valid bundle must follow six steps. Query activeBundle and reject if it is bytes32(0); retrieve the record via getAnchor and confirm its fields match the requested hash, subject, and role, that anchoredAt and documentCount are nonzero, and that superseded is false; then reproduce the manifest, ordering, and bundle hash off-chain. documentCount is a self-reported value, not one verified by the contract. Compliant registries must implement ERC-165 and return true for the IDocumentBundleAnchor interface ID, and must also return true for IDocumentBundleAnchorRecovery if recovery is implemented. ERC-165 only reports interface support; it does not guarantee correct hash derivation or document authenticity. This standard has been finalized as Final.

Things to watch out for

Original ERC · ← ERCs