---
eip: 8326
title: Canonical Document Bundle Anchor
description: A deterministic document-entry bundle hash and subject-scoped anchoring interface with supersession history
author: Chris Turner <c.turner@kula.com>, David Hay (@david-hay), Reagan Simpson (@krumg111), Collins Musyimi (@Musyimi97)
discussions-to: https://ethereum-magicians.org/t/erc-8326-canonical-document-bundle-anchor/28935
status: Review
type: Standards Track
category: ERC
created: 2026-07-05
requires: 165
---

## Abstract

This ERC defines a deterministic manifest and anchoring interface for
commitments to bundles of off-chain documents. Each document is represented by
a fixed-width entry containing a content hash, document role, media-type hash,
filename hash, and normalization-profile identifier. Entries are placed in a
total order and hashed under a schema-version prefix to produce one `bytes32`
bundle commitment.

The on-chain interface anchors a bundle hash in a `(subjectId, role)` namespace,
records declarative metadata, and preserves an append-only supersession history.
An optional recovery interface permits administrative reassignment of authority
over a contested namespace without rewriting anchored records.

This ERC standardizes manifest construction and commitment anchoring. It does
not prove document authenticity, legal effect, off-chain availability, or the
correct application of a normalization profile.

## Motivation

Contracts and applications frequently commit to multiple off-chain documents,
including agreements, certifications, evidence, amendments, and supporting
records. Without a common manifest, implementations differ in entry encoding,
ordering, version separation, and supersession behavior. Two systems can hold
the same canonical document representations but derive incompatible bundle
commitments.

Document commitment has two distinct layers. Normalization transforms a raw
format into canonical bytes. Manifesting describes and orders the resulting
document commitments before deriving a bundle hash. Normalization is
format-specific and evolves independently; manifesting and on-chain anchoring
can remain stable.

This ERC provides a common manifest and anchoring surface while making that
boundary explicit. Compatible implementations derive the same bundle hash only
when they use the same canonical document bytes, entry fields, normalization
profile identifiers, schema version, and ordering rules.

## Specification

The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL NOT",
"SHOULD", "SHOULD NOT", "RECOMMENDED", "NOT RECOMMENDED", "MAY", and
"OPTIONAL" in this document are to be interpreted as described in
[RFC 2119](https://www.rfc-editor.org/rfc/rfc2119) and
[RFC 8174](https://www.rfc-editor.org/rfc/rfc8174).

### Definitions

A **document entry** is a fixed-width record describing one normalized document
commitment.

A **normalization profile** defines how raw document input is converted to the
bytes committed by `contentHash`.

A **bundle** is a non-empty multiset of document entries. Duplicate entries are
retained and affect the resulting bundle hash.

A **bundle hash** is the schema-versioned commitment derived from the canonically
ordered entries.

A **slot** is the `(subjectId, role)` namespace containing at most one active
bundle hash.

A **superseded record** is a historical anchor that has been replaced in its
slot. Supersession does not delete or rewrite its immutable fields.

### Document Entry

Each document MUST be represented as:

```solidity
struct DocumentEntry {
    bytes32 contentHash;
    bytes32 role;
    bytes32 mimeTypeHash;
    bytes32 filenameHash;
    bytes32 normProfileId;
}
```

`contentHash` MUST be the `keccak256` hash of the exact output bytes produced by
the selected normalization profile.

`role` identifies the function of the document within the bundle.

`mimeTypeHash` MUST be `keccak256` of the canonical IANA media type encoded as
lowercase ASCII without parameters. For example, `application/json;
charset=utf-8` is represented by `keccak256("application/json")`.

`filenameHash` MUST be derived by treating U+002F (`/`) and U+005C (`\`) as
path separators, retaining the substring after the final separator, applying
ASCII lowercase conversion to `A` through `Z`, applying Unicode NFC
normalization, encoding the result as UTF-8, and applying `keccak256`.

`normProfileId` identifies the transformation used to produce the committed
bytes. Consumers MUST NOT interpret `contentHash` without considering its
normalization profile.

### Schema, Role, and Profile Identifiers

Implementations MUST use the following schema identifier:

```solidity
bytes32 constant SCHEMA_V1 = keccak256("ERC-8326:BUNDLE:V1");
```

The following role identifiers are defined:

```solidity
bytes32 constant LEGAL_BASIS = keccak256("LEGAL_BASIS");
bytes32 constant EVIDENCE = keccak256("EVIDENCE");
bytes32 constant CERTIFICATION = keccak256("CERTIFICATION");
bytes32 constant AGREEMENT = keccak256("AGREEMENT");
bytes32 constant AMENDMENT = keccak256("AMENDMENT");
bytes32 constant SUPPORTING = keccak256("SUPPORTING");
```

Applications MAY define additional role identifiers. Custom roles SHOULD use a
documented namespace and version to prevent semantic collisions.

The following normalization profiles are defined:

```solidity
bytes32 constant PROFILE_RAW = keccak256("NORM:RAW:V1");
bytes32 constant PROFILE_JSON_RFC8785 =
    keccak256("NORM:JSON:RFC8785:V1");
bytes32 constant PROFILE_XML_C14N11 =
    keccak256("NORM:XML:C14N11:V1");
```

For `PROFILE_RAW`, the output bytes are the raw input bytes without
transformation.

For `PROFILE_JSON_RFC8785`, the output bytes are the UTF-8 serialization
produced by [RFC 8785](https://www.rfc-editor.org/rfc/rfc8785). Implementations
MUST reject inputs that cannot be processed under the RFC 8785 and
[I-JSON](https://www.rfc-editor.org/rfc/rfc7493) constraints, including
duplicate object keys, invalid Unicode, lone surrogates, and numbers outside
the interoperable range. JSON string values are preserved;
this profile does not apply Unicode normalization to them.

For `PROFILE_XML_C14N11`, the output bytes are produced by
[Canonical XML 1.1](https://www.w3.org/TR/2008/REC-xml-c14n11-20080502/)
without comments. Implementations MUST disable external entity resolution.
This profile is distinct from Exclusive XML Canonicalization.

PDF, image, signed-PDF, and plain-text normalization are not defined by this
ERC. Such documents SHOULD use `PROFILE_RAW` unless another precisely specified
profile is agreed by the producer and verifier.

Custom profile identifiers SHOULD use:

```text
NORM:CUSTOM:<namespace>:<format>:<version>
```

A custom profile specification MUST define exact transformation rules and test
fixtures. The namespace MUST identify the defining organization or protocol.

### Canonical Ordering

Entries MUST be ordered lexicographically by their raw `bytes32` values using
the following keys, each ascending:

1. `role`
2. `filenameHash`
3. `contentHash`
4. `mimeTypeHash`
5. `normProfileId`

The comparison proceeds to the next key only when all preceding keys are equal.
Equal entries remain duplicated. No timestamp or preparer-controlled value is
included in a document entry.

### Bundle Hash Derivation

A bundle MUST contain at least one entry.

For each canonically ordered entry, derive:

```solidity
bytes32 leaf = keccak256(
    abi.encodePacked(
        entry.contentHash,
        entry.role,
        entry.mimeTypeHash,
        entry.filenameHash,
        entry.normProfileId
    )
);
```

The bundle hash is:

```solidity
bundleHash = keccak256(
    abi.encodePacked(SCHEMA_V1, leaf0, leaf1, ..., leafN)
);
```

All encoded elements are fixed-width `bytes32` values, so packed encoding does
not introduce variable-length boundary ambiguity.

Future incompatible manifest schemas MUST use a new schema identifier. They
MUST NOT reuse `SCHEMA_V1`.

### Reference Hashing Functions

The following signatures describe two conforming reference paths:

```solidity
function computeCanonicalBundleHash(DocumentEntry[] memory entries)
    internal pure returns (bytes32);

function computeBundleHash(DocumentEntry[] memory entries)
    internal pure returns (bytes32);
```

`computeCanonicalBundleHash` MUST sort entries according to the total order
before deriving the hash.

`computeBundleHash` MUST require already sorted entries and MUST revert for
unsorted input. Both functions MUST revert for an empty bundle and MUST produce
the derivation specified above.

The reference `computeCanonicalBundleHash` uses an in-memory quadratic sort for
clarity. Production systems SHOULD normalize, sort, and hash off-chain. Large
bundles verified on-chain SHOULD use pre-sorted entries with
`computeBundleHash` or a more gas-efficient algorithm that produces the same
total order.

### Anchor Interface

```solidity
interface IDocumentBundleAnchor {
    struct AnchorRecord {
        bytes32 bundleHash;
        bytes32 subjectId;
        bytes32 role;
        address anchoredBy;
        uint64 anchoredAt;
        uint256 documentCount;
        string metadataURI;
        bool superseded;
        bytes32 supersededBy;
    }

    event BundleAnchored(
        bytes32 indexed bundleHash,
        bytes32 indexed subjectId,
        bytes32 indexed role,
        uint256 documentCount
    );

    event BundleSuperseded(
        bytes32 indexed oldBundleHash,
        bytes32 indexed newBundleHash,
        bytes32 indexed subjectId,
        bytes32 role
    );

    function anchorBundle(
        bytes32 bundleHash,
        bytes32 subjectId,
        bytes32 role,
        uint256 documentCount,
        string calldata metadataURI
    ) external;

    function supersedeBundle(
        bytes32 oldBundleHash,
        bytes32 newBundleHash,
        bytes32 subjectId,
        bytes32 role,
        uint256 documentCount,
        string calldata metadataURI
    ) external;

    function getAnchor(
        bytes32 bundleHash,
        bytes32 subjectId,
        bytes32 role
    ) external view returns (AnchorRecord memory);

    function isAnchored(
        bytes32 bundleHash,
        bytes32 subjectId,
        bytes32 role
    ) external view returns (bool);

    function activeBundle(
        bytes32 subjectId,
        bytes32 role
    ) external view returns (bytes32);
}
```

### Anchoring

`anchorBundle` MUST reject a zero `bundleHash`, zero `subjectId`, zero `role`, or
zero `documentCount`.

`metadataURI` MAY be empty. An empty value indicates that the anchor does not
provide an on-chain retrieval pointer. Applications requiring availability
SHOULD enforce a non-empty URI before calling the registry.

Applications without an existing subject identifier SHOULD derive a nonzero,
domain-separated `subjectId` from application context rather than sharing a
common placeholder value.

Each record MUST be keyed by the `(bundleHash, subjectId, role)` triple. The same
bundle hash MAY be anchored under different subjects or roles, producing
independent records.

`anchorBundle` MUST reject a duplicate triple and MUST reject a slot that already
has an active bundle. Replacement of an occupied slot MUST use
`supersedeBundle`.

On success, the registry MUST:

- store all supplied values;
- set `anchoredBy` to `msg.sender`;
- set `anchoredAt` to `uint64(block.timestamp)`;
- initialize `superseded` to `false`;
- initialize `supersededBy` to `bytes32(0)`;
- set the active bundle for the slot; and
- emit `BundleAnchored`.

The registry MUST restrict anchoring to authorized callers. Its authorization
mechanism is implementation-defined and MUST be documented.

### Supersession

`supersedeBundle` MUST reject a zero `newBundleHash`, zero `subjectId`, zero
`role`, or zero `documentCount`. It MUST reject `oldBundleHash ==
newBundleHash`.

The old record MUST exist, MUST NOT already be superseded, and MUST be the active
bundle for the specified slot. The new triple MUST NOT already exist.

The caller MUST be authorized to supersede that slot. Authorization is
implementation-defined, but an unrelated authorized anchorer MUST NOT be able to
take over another principal's slot.

Supersession MUST atomically:

- set the old record's `superseded` field to `true`;
- set its `supersededBy` field to `newBundleHash`;
- create the new active record;
- update the active slot;
- emit `BundleSuperseded`; and
- emit `BundleAnchored` for the new record.

The old record's remaining fields MUST NOT change and the old record MUST remain
queryable.

### Queries

`getAnchor` MUST return the complete record for a triple and MUST revert when no
record exists.

`isAnchored` MUST return `true` for every existing record, including a
superseded record, and `false` for an unknown triple.

`activeBundle` MUST return the active bundle hash for a slot or `bytes32(0)` if
the slot has never been occupied.

### Recovery Interface

Administrative slot recovery is OPTIONAL. A registry that supports principal
reassignment MUST implement:

```solidity
interface IDocumentBundleAnchorRecovery {
    event SlotPrincipalAssigned(
        bytes32 indexed subjectId,
        bytes32 indexed role,
        address indexed principal
    );

    function slotPrincipal(
        bytes32 subjectId,
        bytes32 role
    ) external view returns (address);

    function assignSlotPrincipal(
        bytes32 subjectId,
        bytes32 role,
        address principal
    ) external;
}
```

`slotPrincipal` MUST return the address currently authorized as principal for
the slot or `address(0)` when none is assigned.

When the recovery extension is implemented, successful first anchoring and
supersession MUST set the slot principal to `msg.sender`.

`assignSlotPrincipal` MUST be restricted to an authorized recovery
administrator and MUST reject zero `subjectId`, zero `role`, and a zero
`principal`. Assignment MUST emit `SlotPrincipalAssigned`.

Principal assignment does not itself grant general anchoring authority. A
designated principal MUST also satisfy the implementation's authorization
policy before anchoring or superseding.

If a principal is assigned before first anchoring, `anchorBundle` MUST reject
any other caller for that slot. After reassignment, the former principal MUST
NOT be able to supersede that slot solely by retaining general anchoring
authority.

### Interface Detection

Compliant registries MUST implement [ERC-165](./eip-165.md) and return `true`
for `type(IDocumentBundleAnchor).interfaceId`.

Registries implementing recovery MUST also return `true` for
`type(IDocumentBundleAnchorRecovery).interfaceId`.

ERC-165 reports interface support. It does not establish that a bundle was
derived correctly, that referenced documents are authentic or available, or
that an operator is trustworthy.

### Consumer Verification

A consumer relying on an active bundle MUST:

1. Query `activeBundle(subjectId, role)` and reject `bytes32(0)`.
2. Retrieve the corresponding record with
   `getAnchor(bundleHash, subjectId, role)`.
3. Verify that the returned fields match the requested hash, subject, and role.
4. Verify that `anchoredAt` and `documentCount` are nonzero.
5. Verify that `superseded` is `false`.
6. Reproduce the manifest, canonical ordering, and bundle hash off-chain.

`documentCount` is declarative and MUST NOT be treated as independently verified
by the anchoring contract.

## Rationale

### Why Separate Manifesting from Normalization?

Combining universal document normalization with bundle hashing would overstate
what one ERC can guarantee. The manifest gives normalized document commitments
a common structure. Profiles define how a particular input format produces the
committed bytes and can evolve independently.

### Why a Total Order Over All Five Fields?

Ordering by only role, filename, and content leaves insertion order as a hidden
tie-breaker when entries differ only by media type or normalization profile.
Including all five fields gives every distinct entry a deterministic position.

### Why Include a Schema Identifier?

The schema identifier separates incompatible manifest versions. Without it, a
future change to entry interpretation or hash derivation could reuse the same
hash domain.

### Why Use Subject and Role Slots?

The same document set can be relevant to multiple subjects or serve different
purposes. Keying records by `(bundleHash, subjectId, role)` preserves those
independent records, while `(subjectId, role)` identifies the one current bundle
for a particular purpose.

### Why Permit an Empty Metadata URI?

The bundle commitment remains valid without an on-chain retrieval pointer. Some
deployments distribute documents through private or regulated channels, while
others use content-addressed public storage. Availability policy belongs to the
application and is not implied by a syntactically non-empty URI.

### Why Preserve Superseded Records?

Document sets evolve through amendments, renewals, and corrections. Mutating or
deleting the prior record would remove the audit trail. Supersession changes
only the prior record's forward pointer and status.

### Why a Slot-Principal Recovery Extension?

An anchoring key can be compromised, revoked, or used to squat a slot. Directly
attempting an administrative supersession is front-runnable because the current
principal can supersede first and invalidate the administrator's expected old
hash. Atomic principal reassignment removes that race while preserving all
bundle records.

Recovery introduces administrative trust, so it is separated from the core
interface. Deployments preferring immutable authority can omit it.

### Why Compute Bundle Hashes Off-Chain?

Normalization and sorting can be expensive. The reference canonical convenience
path uses a quadratic sort and repeated memory encoding, which is suitable for
tests and small bundles but inefficient for large on-chain sets. The anchoring
contract accepts a precomputed `bundleHash`; consumers reproduce it off-chain.

### Why Not Use a Merkle Root?

This ERC commits to the complete ordered manifest and is optimized for
reproducing one bundle identifier, not proving membership of a single document
without the rest of the manifest. Applications requiring compact membership
proofs can commit a separately specified Merkle root as a document or extension
field, but that construction is outside this ERC.

### Why Defer PDF and Image Normalization?

PDF and image files contain format-specific metadata, incremental updates,
compression choices, object ordering, color profiles, and renderer-dependent
behavior. A credible profile requires exact binary fixtures and specialized
review. Until then, byte-identical raw hashing is the only defined profile for
those files.

### Prior Art

The Document Management proposal numbered 1643 defines document references for security tokens. It stores individual
named documents rather than a deterministic, schema-versioned bundle manifest.

[ERC-5289](./eip-5289.md) defines document signing and verification. This ERC
defines deterministic bundle commitments and supersession rather than a signing
workflow.

[ERC-5732](./eip-5732.md) defines a generic commit-reveal mechanism. It can be
used as a privacy layer around publication but does not define document entries
or bundle hashing.

[ERC-7208](./eip-7208.md) defines general on-chain data containers. This ERC
defines a document-specific commitment and lifecycle interface.

[ERC-7578](./eip-7578.md) includes document URI storage in a physical-asset
redemption flow but does not define deterministic bundle manifesting.

[ERC-3668](./eip-3668.md) defines an off-chain data retrieval and verification
flow. It can retrieve material referenced by an anchor but does not determine
how a document bundle commitment is derived.

The Ethereum Attestation Service provides generic schema-based attestations.
Such attestations can reference a bundle hash, but they do not define this
manifest or supersession model.

[RFC 8785](https://www.rfc-editor.org/rfc/rfc8785) defines deterministic JSON
serialization and is used directly by `PROFILE_JSON_RFC8785`.

[Canonical XML 1.1](https://www.w3.org/TR/2008/REC-xml-c14n11-20080502/)
defines deterministic XML serialization and is used directly by
`PROFILE_XML_C14N11`.

W3C Verifiable Credential Data Integrity defines proof transformation,
canonicalization, hashing, and verification pipelines for verifiable
credentials. This ERC applies a document-entry manifest and Ethereum anchoring
interface to arbitrary document commitments rather than credential proofs.

## Backwards Compatibility

This ERC introduces new interfaces and does not change existing token or
registry standards. Existing systems can store the resulting `bytes32` bundle
hash without changing their storage type, but ad hoc bundle hashes are not
necessarily compatible with this derivation.

Existing document registries can integrate by storing this bundle hash as a
document commitment or by deploying a companion `IDocumentBundleAnchor`
registry. Composition with any particular asset registry is optional;
`subjectId` is an application-defined nonzero identifier.

## Test Cases

Implementations should test at least:

- raw-byte content hashing;
- RFC 8785 canonical serialization and invalid-input rejection;
- Canonical XML 1.1 serialization with external entities disabled;
- all profile, role, and schema constants;
- ordering differences at each of the five fields;
- permutation independence through the canonical hashing path;
- rejection of unsorted input through the pre-sorted path;
- empty, single-entry, and duplicate-entry bundles;
- schema-version separation;
- anchoring and querying independent triples;
- rejection of occupied active slots;
- multi-step supersession history;
- independent use of the same bundle hash by different subjects or roles;
- empty and non-empty metadata URIs;
- positive and negative ERC-165 detection; and
- contested-slot recovery and pre-assignment.

### Normative Test Vectors

The hexadecimal byte strings in this section are authoritative. Text renderings
are included only for readability. None of the inputs or canonical outputs has
an implicit byte-order mark or trailing newline unless its hexadecimal form
includes one.

The schema identifier for these vectors is:

```text
SCHEMA_V1
0x1853dddb0c73884633f2ff8e736679ec654a11f704ed868aacca79d8ae4caf67
```

#### JSON Entry

The filename input contains `e` followed by U+0301 COMBINING ACUTE ACCENT.
Filename processing selects the basename, folds ASCII uppercase characters,
and applies NFC, producing the UTF-8 bytes for `café.json`.

```text
input document:       {"b":2,"a":1}
input bytes:          0x7b2262223a322c2261223a317d
canonical document:   {"a":1,"b":2}
canonical bytes:      0x7b2261223a312c2262223a327d
filename input:       records/Cafe\u0301.JSON
filename input bytes: 0x7265636f7264732f43616665cc812e4a534f4e
canonical filename:   café.json
filename bytes:       0x636166c3a92e6a736f6e
role preimage:         AGREEMENT
media type preimage:  application/json
profile preimage:     NORM:JSON:RFC8785:V1
contentHash:          0xb8ffb64722137f4b100665a52e3c943f8066e8ab8ba3b427e6f4b404defd82b0
role:                 0x566614d5b403a4ea71e1ef1027b77ff1e1a13a54c7f393aa64a1368de23a5f92
mimeTypeHash:         0x82e6a468c95da6cfe399f69ee0782fd009e354a8030ea5636ea9c7db0edcf7f5
filenameHash:         0x3b40ecd25f3375868ddf559a0ef47c2dc15863a529ac592bec5e1b618bcbaf3e
normProfileId:        0x464861b0846e795db3d9c52e9c49870c7e83f2bb07f73764f7e4850151994f40
leaf:                 0xe78934e3ee972b7eae660a945de9780c77e5656203bfe437ab117413adf3ad2b
```

#### XML Entry

The backslash in the filename input is a path separator. Canonical XML 1.1
orders the unqualified attributes lexicographically. The canonical output has
no trailing newline.

```text
input document:       <doc b="2" a="1"></doc>
input bytes:          0x3c646f6320623d22322220613d2231223e3c2f646f633e
canonical document:   <doc a="1" b="2"></doc>
canonical bytes:      0x3c646f6320613d22312220623d2232223e3c2f646f633e
filename input:       Evidence\Proof.XML
filename input bytes: 0x45766964656e63655c50726f6f662e584d4c
canonical filename:   proof.xml
filename bytes:       0x70726f6f662e786d6c
role preimage:         EVIDENCE
media type preimage:  application/xml
profile preimage:     NORM:XML:C14N11:V1
contentHash:          0xde64c753c807c4620bf010c7e855bcd38bd389e980c4054b81abd5d44d45eab1
role:                 0x7477535acdef313b25d16b4871e7023fac62af68d6312bbdbdb96203a4710dc3
mimeTypeHash:         0x37aaf14a93fea5695fd8577aacfb548c98692a106d439219bfb9b83e3011ea2f
filenameHash:         0x696b36bf7095c1b1564382a37b8f5ba7d259b36be7639a7a1b50c97cd13cfe39
normProfileId:        0x72efa7a47196f4ad021a5a3758b19b14d8e09d7b7b211bf4745b35cba62e49c2
leaf:                 0x8c72daea8a7297c8d307dd41e24038ff71065f9b0932a07e9016942abbc5c9ac
```

#### Raw Entry

The input and canonical output end with one U+000A LINE FEED byte.

```text
input document:       Hello, ERC-8326!<LF>
input bytes:          0x48656c6c6f2c204552432d38333236210a
canonical document:   Hello, ERC-8326!<LF>
canonical bytes:      0x48656c6c6f2c204552432d38333236210a
filename input:       README.TXT
filename input bytes: 0x524541444d452e545854
canonical filename:   readme.txt
filename bytes:       0x726561646d652e747874
role preimage:         SUPPORTING
media type preimage:  text/plain
profile preimage:     NORM:RAW:V1
contentHash:          0x06750728a91d155294f77f992fec49acabb0470481439ed5e3bb59854df82ec9
role:                 0xb0e9b5730d97b99270ce15f439eec98a4f9580e1dfbfb8f5c9e0e3ab71d4bca6
mimeTypeHash:         0xb25570cad408307f58d995c1dadde60bc76e94924d640305a148c9a11f8303bf
filenameHash:         0x31f491635b16d6fb45a7d770fcbcc8cbb6eae32ac98ab622631f4bc4a8c7e9ce
normProfileId:        0xbe97b35c60bb0caee86a5a99022973ef2aa47cbf9586dd34065141c6668b430b
leaf:                 0xa418892b0b93cf88e9d840cf38d36f5bfa16813774f22f5cf02d6b4fcc48375a
```

#### Bundle Vector

The entries sort in JSON, XML, raw order by their `role` values. Concatenating
`SCHEMA_V1` with the three leaf values in that order produces:

```text
bundleHash:
0xbe712c4a5eb51d9eb303f1a5c896417a8407a420936fa210626bb66b1a6d0613
```

## Reference Implementation

A Solidity reference implementation, hashing library, unit tests, Medusa
property tests, consumer verifier, and independent audit are linked from the
discussion referenced in the preamble. The implementation performs manifest
ordering and hashing over supplied entries; raw JSON and XML normalization
occurs off-chain.

## Security Considerations

### Normalization Profile Trust

The determinism guarantee is only as strong as the profile implementation.
`PROFILE_RAW` performs no normalization. Custom profiles can be ambiguous,
malicious, or incompletely specified. Consumers should verify the selected
profile and reproduce the exact transformation.

### Canonicalization Boundary

The Solidity hashing library receives document-entry fields and does not parse
or canonicalize JSON, XML, PDF, images, filenames, or MIME types. A caller can
submit a hash while falsely claiming that a profile was followed. Consumers
must reproduce normalization and entry derivation off-chain.

### Document Availability and Privacy

An anchored hash does not make its preimage available. `metadataURI` can become
unavailable, change content, or expose sensitive information. Empty URIs are
permitted. Applications should use durable retrieval policies and must not
place personal, confidential, or legally restricted information directly in a
public URI.

### Declarative Document Count

The anchoring contract cannot verify that `documentCount` matches the committed
manifest. Consumers should count the reproduced entries rather than trusting
the stored value independently.

### Authorization and Slot Squatting

A caller with anchoring authority can occupy an unassigned slot. Implementations
should scope authorization appropriately or pre-assign principals for protected
slots. Recovery administrators can redirect legitimate authority and therefore
require strong operational controls.

### Supersession Front-Running

An administrator attempting direct supersession of a contested slot can be
front-run by the current principal. Implementations supporting recovery should
use atomic principal reassignment before the legitimate principal supersedes the
active bundle.

### Hash and Encoding Assumptions

The construction relies on `keccak256` collision resistance. Packed encoding is
used only for fixed-width fields. Implementations must not substitute
variable-length fields into the leaf or bundle encoding without introducing
unambiguous length encoding and a new schema identifier.

### Filename and Unicode Handling

ASCII case folding does not provide Unicode case equivalence. NFC normalization
requires a conforming Unicode implementation. Different path or Unicode
handling will produce different filename hashes.

### No Authenticity or Legal-Effect Guarantee

A reproducible bundle hash proves agreement on committed bytes, not that a
document is authentic, current, authorized, legally effective, or associated
with the claimed subject.

## Copyright

Copyright and related rights waived via [CC0](../LICENSE.md).
