Skip to content

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.

Machine iddoc-trail
Maturitystable
Command[entityId, entryHash, reference]: entityId text, entryHash non-empty bytes, reference text ("" when none)
Settingsnone
Statee/<entityId> → [count, headHash]
Headhead' = Blake2b-256(head ‖ entryHash ‖ sender), starting from 32 zero bytes
Proof subjectdocument-head-v1: coordinate entity-id; claims revision-exact, revision-minimum, digest-equals
Result codesnone: every well-formed append applies
Eventsdoc-trail.entry-appended.v1, in composites only
A chained document trailIllustrative data

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.

  1. Start empty Entity product-42 has no trail yet. The first append chains from a genesis head of 32 zero bytes.
    doc-trail state
    KeyCountHead
    empty
  2. 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
    KeyCountHead
    e/product-421Blake2b(00…00 ‖ h1 ‖ A) = 3f1c…
  3. 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
    KeyCountHead
    e/product-422Blake2b(3f1c… ‖ h2 ‖ B) = 9a07…
  4. 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.

  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
    KeyCountHead
    e/product-421Blake2b(00…00 ‖ h1 ‖ A) = 3f1c…
  2. 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
    KeyCountHead
    e/product-422Blake2b(3f1c… ‖ h2 ‖ B) = 9a07…
  3. 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.

  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
    KeyCountHead
    e/product-421Blake2b(00…00 ‖ h1 ‖ A) = 3f1c…
  2. 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
    KeyCountHead
    e/product-422Blake2b(3f1c… ‖ h2 ‖ B) = 9a07…
  3. 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.

  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
    KeyCountHead
    e/product-421Blake2b(00…00 ‖ h1 ‖ A) = 3f1c…
  2. 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
    KeyCountHead
    e/product-422Blake2b(3f1c… ‖ h2 ‖ B) = 9a07…
  3. 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.

  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
    KeyCountHead
    e/product-421Blake2b(00…00 ‖ h1 ‖ A) = 3f1c…
  2. 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
    KeyCountHead
    e/product-422Blake2b(3f1c… ‖ h2 ‖ B) = 9a07…
  3. 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.

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.

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: 1000

To add a chain to a generated project, use the add-chain workflow. Do not reinterpret retained data from another machine as doc-trail state.

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:

Terminal window
TOOL=docs/appchain/tutorials/tools/stdlib_command.py
ENTITY=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.

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.

Terminal window
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:

  1. Collect the finalized commands for the entity from block history, in order, with each command’s sender.
  2. Recompute the head with DocTrailContract.computeHead(entryHashes, authors).
  3. Compare it with the proven head, and verify that proof against a state root from pinned finality or a Cardano anchor.
  4. 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=100
GET /chains/{chainId}/messages/by-topic/doc-trail.command.v1?fromHeight=0&limit=100
GET /chains/{chainId}/messages/{messageId}
  • 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.

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 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: countAfter and first (the append created the trail).