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.
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.
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 and RFC 8174.
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.
Each document MUST be represented as:
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.
Implementations MUST use the following schema identifier:
bytes32 constant SCHEMA_V1 = keccak256("ERC-8326:BUNDLE:V1");
The following role identifiers are defined:
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:
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. Implementations
MUST reject inputs that cannot be processed under the RFC 8785 and I-JSON
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
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:
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.
Entries MUST be ordered lexicographically by their raw bytes32 values using
the following keys, each ascending:
rolefilenameHashcontentHashmimeTypeHashnormProfileIdThe 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.
A bundle MUST contain at least one entry.
For each canonically ordered entry, derive:
bytes32 leaf = keccak256(
abi.encodePacked(
entry.contentHash,
entry.role,
entry.mimeTypeHash,
entry.filenameHash,
entry.normProfileId
)
);
The bundle hash is:
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.
The following signatures describe two conforming reference paths:
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.
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);
}
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:
anchoredBy to msg.sender;anchoredAt to uint64(block.timestamp);superseded to false;supersededBy to bytes32(0);BundleAnchored.The registry MUST restrict anchoring to authorized callers. Its authorization mechanism is implementation-defined and MUST be documented.
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:
superseded field to true;supersededBy field to newBundleHash;BundleSuperseded; andBundleAnchored for the new record.The old record's remaining fields MUST NOT change and the old record MUST remain queryable.
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.
Administrative slot recovery is OPTIONAL. A registry that supports principal reassignment MUST implement:
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.
Compliant registries MUST implement ERC-165 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.
A consumer relying on an active bundle MUST:
activeBundle(subjectId, role) and reject bytes32(0).getAnchor(bundleHash, subjectId, role).anchoredAt and documentCount are nonzero.superseded is false.documentCount is declarative and MUST NOT be treated as independently verified
by the anchoring contract.
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.
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.
The schema identifier separates incompatible manifest versions. Without it, a future change to entry interpretation or hash derivation could reuse the same hash domain.
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.
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.
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.
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.
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.
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.
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.
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 defines document signing and verification. This ERC defines deterministic bundle commitments and supersession rather than a signing workflow.
ERC-5732 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 defines general on-chain data containers. This ERC defines a document-specific commitment and lifecycle interface.
ERC-7578 includes document URI storage in a physical-asset redemption flow but does not define deterministic bundle manifesting.
ERC-3668 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 defines deterministic JSON
serialization and is used directly by PROFILE_JSON_RFC8785.
Canonical XML 1.1
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.
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.
Implementations should test at least:
Exact normalization inputs, canonical bytes, and expected content and bundle hashes should be published before this ERC advances to Review.
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.
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.
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.
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.
The anchoring contract cannot verify that documentCount matches the committed
manifest. Consumers should count the reproduced entries rather than trusting
the stored value independently.
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.
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.
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.
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.
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 and related rights waived via CC0.