doc-trail
doc-trail keeps an append-only, tamper-evident history of document hashes
for each business entity, such as a product, case or shipment. Each append
chains the new hash into the entity’s head. The documents themselves stay off
the ledger.
The machine id is exactly doc-trail. An entity id is any stable application
identifier.
At a glance
Section titled “At a glance”| Machine id | doc-trail |
| Maturity | stable |
| Command | [entityId, entryHash, reference]: entityId text, entryHash non-empty bytes, reference text ("" when none) |
| Settings | none |
| State | e/<entityId> → [count, headHash] |
| Head | head' = Blake2b-256(head ‖ entryHash ‖ sender), starting from 32 zero bytes |
| Proof subject | document-head-v1: coordinate entity-id; claims revision-exact, revision-minimum, digest-equals |
| Result codes | none: every well-formed append applies |
| Events | doc-trail.entry-appended.v1, in composites only |
How it works
Section titled “How it works”What you're seeing: two members append document hashes to one product. The head is a chain of hashes, so a verifier who recomputes it detects any change to the bytes, the order, or the authors.
- Start empty Entity
product-42has no trail yet. The first append chains from a genesis head of 32 zero bytes.doc-trail state Key Count Head empty - A appends v1 Member A appends the SHA-256 hash h1 of certificate v1, with a reference to where it is stored. The new head hashes the old head, the entry hash and A’s member key. The reference is not part of the head.
doc-trail state Key Count Head e/product-421 Blake2b( 00…00‖ h1 ‖ A) =3f1c… - B appends v2 Member B appends hash h2 of version 2. There is no owner check: any member may append to any entity. The head now commits to both entries, their order, and both authors.
doc-trail state Key Count Head e/product-422 Blake2b( 3f1c…‖ h2 ‖ B) =9a07… - Verify A verifier reads the two finalized commands from block history, recomputes the head from (h1, A) then (h2, B), and compares it with the proven head. They match.
- passes: The recomputed head equals the proven head
What if: A document is edited
Someone changes one byte of certificate v1 after it was appended.
- A appends v1 Member A appends the SHA-256 hash h1 of certificate v1, with a reference to where it is stored. The new head hashes the old head, the entry hash and A’s member key. The reference is not part of the head.
doc-trail state Key Count Head e/product-421 Blake2b( 00…00‖ h1 ‖ A) =3f1c… - B appends v2 Member B appends hash h2 of version 2. There is no owner check: any member may append to any entity. The head now commits to both entries, their order, and both authors.
doc-trail state Key Count Head e/product-422 Blake2b( 3f1c…‖ h2 ‖ B) =9a07… - Verify A verifier recomputes the head from the edited file’s hash h1′ and compares it with the proven head
9a07…. The edited file hashes to a different h1′, so every later head differs.- fails: The recomputed head equals the proven head
What if: The order is swapped
Someone claims version 2 came first.
- A appends v1 Member A appends the SHA-256 hash h1 of certificate v1, with a reference to where it is stored. The new head hashes the old head, the entry hash and A’s member key. The reference is not part of the head.
doc-trail state Key Count Head e/product-421 Blake2b( 00…00‖ h1 ‖ A) =3f1c… - B appends v2 Member B appends hash h2 of version 2. There is no owner check: any member may append to any entity. The head now commits to both entries, their order, and both authors.
doc-trail state Key Count Head e/product-422 Blake2b( 3f1c…‖ h2 ‖ B) =9a07… - Verify A verifier recomputes the head from (h2, B) then (h1, A) and compares it with the proven head
9a07…. The head chains entries in order, so a different order gives a different head.- fails: The recomputed head equals the proven head
What if: The author is swapped
Someone claims member B appended version 1.
- A appends v1 Member A appends the SHA-256 hash h1 of certificate v1, with a reference to where it is stored. The new head hashes the old head, the entry hash and A’s member key. The reference is not part of the head.
doc-trail state Key Count Head e/product-421 Blake2b( 00…00‖ h1 ‖ A) =3f1c… - B appends v2 Member B appends hash h2 of version 2. There is no owner check: any member may append to any entity. The head now commits to both entries, their order, and both authors.
doc-trail state Key Count Head e/product-422 Blake2b( 3f1c…‖ h2 ‖ B) =9a07… - Verify A verifier recomputes the head from (h1, B), (h2, B) and compares it with the proven head
9a07…. Each step hashes the sender’s member key, so a different author gives a different head.- fails: The recomputed head equals the proven head
What if: The reference is edited
Someone points version 1’s reference at another file.
- A appends v1 Member A appends the SHA-256 hash h1 of certificate v1, with a reference to where it is stored. The new head hashes the old head, the entry hash and A’s member key. The reference is not part of the head.
doc-trail state Key Count Head e/product-421 Blake2b( 00…00‖ h1 ‖ A) =3f1c… - B appends v2 Member B appends hash h2 of version 2. There is no owner check: any member may append to any entity. The head now commits to both entries, their order, and both authors.
doc-trail state Key Count Head e/product-422 Blake2b( 3f1c…‖ h2 ‖ B) =9a07… - Verify The recomputed head still matches: the reference is not hashed into it. Only the finalized command in block history shows the original reference, so check the document by its hash, not its location.
- passes: The recomputed head equals the proven head
- member node
- deterministic state machine
- verifier
- tampering detected
What the head covers, and what it does not:
- Covered: each entry hash, the order of entries, and the member key that submitted each one. Changing any of them changes the head.
- Not covered: the reference. It is kept in the finalized command in block history, but it is not hashed into the head. Authenticate a document by its hash, not by where the reference points.
- No ownership. Any member can append to any entity, and appending the same hash twice is simply two entries.
When to use it
Section titled “When to use it”Use doc-trail when several members need an ordered, tamper-evident history per
entity without replicating whole documents: product passports, supply-chain
histories, case files, document revisions, certificate histories.
Choose ordered-log for one global order of events, or
kv-registry for a mutable current value. Use a composite or
a plugin when appends need owners, roles, approvals or domain validation.
Configure
Section titled “Configure”doc-trail has no settings of its own. The stock local cluster has no
doc-trail chain; select the document-trail recipe for a new project, or add
a chain entry with the three
state-identity settings and a fresh
genesis id:
yano: app-chain: chains[3]: chain-id: "document-trail-chain" state-machine: doc-trail state: commitment-profile: mpf-blake2b256-v1 format-fingerprint: 91ee14091200f1e24659112d640e877e9177779dcc81dd06117f013e9190082b genesis-id: <64 lowercase hex characters, unique to this chain> membership: mode: governed block: interval-ms: 1000To add a chain to a generated project, use the
add-chain workflow. Do not reinterpret retained
data from another machine as doc-trail state.
Hash and submit a document through REST
Section titled “Hash and submit a document through REST”You choose the document hash algorithm; SHA-256 is common. The head itself always uses Blake2b-256. From the top-level directory of the extracted release:
TOOL=docs/appchain/tutorials/tools/stdlib_command.pyENTITY=product-42
ENTRY_HASH=$(python3 -c \ 'import hashlib; print(hashlib.sha256(b"quality certificate v1").hexdigest())')
APPEND_HEX=$(python3 "$TOOL" doc-trail "$ENTITY" "$ENTRY_HASH" \ --reference 's3://evidence/product-42/certificate-v1.pdf')
curl -sS -X POST \ http://127.0.0.1:7071/api/v1/app-chain/chains/document-trail-chain/messages \ -H 'Content-Type: application/json' \ -d "{\"topic\":\"doc-trail.command.v1\",\"bodyHex\":\"$APPEND_HEX\"}" | jq .Without --reference, the helper sends an empty reference, which the command
requires. 202 means queued; wait until the message is final before treating
the trail as advanced. Append the next revision the same way. Submitting it
through another member, such as port 7072, records a different author in the
head.
Submit from Java
Section titled “Submit from Java”implementation "org.yanoproject.x:yano-x-client:${yanoXVersion}"import org.yanoproject.x.client.AppChainClient;import org.yanoproject.x.client.StdlibAppChainClient;
import java.nio.charset.StandardCharsets;import java.security.MessageDigest;
var client = AppChainClient.builder("http://127.0.0.1:7071/api/v1") .chainId("document-trail-chain") .build();var documents = new StdlibAppChainClient(client);
byte[] document = "quality certificate v1".getBytes(StandardCharsets.UTF_8);byte[] entryHash = MessageDigest.getInstance("SHA-256").digest(document);
var submitted = documents.appendDocument("product-42", entryHash, "s3://evidence/product-42/certificate-v1.pdf");Store which hash algorithm you used next to the document or in a versioned
envelope. doc-trail treats entryHash as opaque bytes.
Read and prove a trail head
Section titled “Read and prove a trail head”STATE_KEY_HEX=$(python3 -c 'print("e/product-42".encode().hex())')
curl -sS \ "http://127.0.0.1:7070/api/v1/app-chain/chains/document-trail-chain/state/proof/$STATE_KEY_HEX" \ | jq '{committedHeight, stateRoot, presence, valueHex}'valueHex is CBOR [count, headHash]. It proves the trail summary, not that
the referenced documents still exist. In Java:
var trail = documents.documentTrail("product-42").orElseThrow().value();System.out.println("entries=" + trail.count());To verify the whole trail independently:
- Collect the finalized commands for the entity from block history, in order, with each command’s sender.
- Recompute the head with
DocTrailContract.computeHead(entryHashes, authors). - Compare it with the proven head, and verify that proof against a state root from pinned finality or a Cardano anchor.
- Fetch each document, hash it with your declared algorithm, and match it to
its
entryHash.
Useful history endpoints, relative to /api/v1/app-chain:
GET /chains/{chainId}/blocks?from=1&limit=100GET /chains/{chainId}/messages/by-topic/doc-trail.command.v1?fromHeight=0&limit=100GET /chains/{chainId}/messages/{messageId}Design notes
Section titled “Design notes”- Keep documents outside consensus. Submit hashes and short references, not files. Every member receives every command.
- Define canonical bytes. Two equivalent JSON documents can hash differently. Canonicalize before hashing, or hash the stored file exactly.
- Integrity is not availability. The trail proves an ordered digest; your storage must keep the document available.
Advanced
Section titled “Advanced”Composites
Section titled “Composites”In a declarative composite, an append emits doc-trail.entry-appended.v1 with
entityId, entryHash, reference, headHash, count and sender. A
trail whose count is already at the maximum rejects the append with
DOC_COUNT_OVERFLOW. To require approval before an append, combine
approvals and doc-trail in a committed composite.
Admission-rule views and facts
Section titled “Admission-rule views and facts”Admission rules can read a trail’s head (ADR-031.4, see admission rules):
- Value view (namespace
"", key: the entity id):count,headHash. - Post-state facts, after an approved append:
countAfterandfirst(the append created the trail).