Credential Anchoring
Chain: Base mainnet Prefix: MolTrust/VC/v1
Hash: SHA-256 Specification URL: https://moltrust.ch/anchoring.html
The transaction calldata is a string
This is the part that costs people the most time, so it comes first. The anchoring transaction carries its payload as UTF-8 text, not as ABI-encoded arguments:
MolTrust/VC/v1/<merkle_root>
Decoding the transaction input with an ABI decoder finds no arguments and
returns nothing, which reads as an empty anchor. An external auditor reached
exactly that conclusion in September 2026. Read the input as bytes, decode as
UTF-8, split on the last /.
The root is 64 lowercase hex characters. Everything before it is the version prefix and is there so a reader can tell what the number is without a lookup.
The leaf
One credential makes one leaf. The preimage is five fields joined by a pipe, in this order, with no padding and no whitespace:
leaf = SHA-256( credential_id | subject_did | credential_type | issued_at | proof_value )
issued_at is an ISO-8601 timestamp as stored.
proof_value is the credential signature; an unsigned credential
contributes an empty string rather than being skipped.
All five are needed. Leave any one out and the anchor stops binding the thing
it is meant to bind — drop subject_did and the same anchor
covers a credential about someone else.
The tree
Pairs are hashed left-then-right with SHA-256 over the concatenated raw bytes, not over their hex text:
parent = SHA-256( left_bytes || right_bytes )
A level with an odd number of nodes duplicates its last node to make the count even, and the duplicate is part of the level. This matters for verification: a proof for the last leaf of an odd level includes that duplicated sibling. An implementation that pads only while computing the root, and not while producing proofs, emits proofs that do not replay — we shipped that bug once and fixed it.
Replaying a proof
GET /identity/verify/<did> returns, for every credential,
the anchor transaction, the root, and the proof:
"anchor": {
"tx_hash": "0x0fc09fb8…",
"merkle_root": "e3174fd3…",
"merkle_proof": { "leaf": "53f9d163…", "path": [ { "hash": "…", "position": "left" }, … ] },
"calldata_format": "utf8:MolTrust/VC/v1/<merkle_root>",
"leaf_rule": "https://moltrust.ch/anchoring.html#leaf"
}
To check it:
- Recompute the leaf from the credential with the rule above and compare it
with
merkle_proof.leaf. - Fold the path into the leaf: for each step, concatenate the raw bytes with
the sibling on the side its
positionnames, and hash. - Compare the result with
merkle_root. - Fetch the transaction, decode its input as UTF-8, and check that the root in the calldata is the same root.
The proof travels with the credential rather than behind a second request. A verifier that has to fetch the proof from another endpoint cannot check us while that endpoint is unavailable, which is when checking matters most.
Two anchors, two tables
Credentials and interaction proof records are anchored by the same machinery and are not the same artefact. A credential anchor says we issued this credential about this subject at this time. An interaction proof record anchors an output an agent produced. An agent can hold anchored credentials and have no interaction proof records at all, and a count of zero for one says nothing about the other.
Which wallet pays
Anchoring is paid for from a dedicated address, never the wallet that receives payments. The anchor transaction and the payment rail share no key.
MolTrust / CryptoKRI GmbH, Zurich | hello@moltrust.ch | moltrust.ch Released under Creative Commons Attribution 4.0 International (CC BY 4.0)