Attest it. Change it. Attest it again.
A dataset is attested, then edited, then transformed again. Each change produces different bytes, so each change needs its own statement, and each new statement carries the identity of the one before it. This page follows a single set of customer records through all three states and shows the evidence each state produced.
Step through this in the Explorer
Every value on this page is real. The records are the fixtures checked into the runnable proof, and every digest was computed from those exact bytes. Nothing here is illustrative filler. This page presents evidence; it does not perform verification.
-
FileRaw
customers.raw.json4e90181e…Stays as it arrived.Signs the file aboveOrigin statement014b4252…Subject: the raw file. No predecessor; the graph starts here. -
normalize
New fileNormalizedcustomers.normalized.json421e19cf…Written beside the raw file, which is untouched.Signs the file aboveTransformation statement9ef9051c…Input: the raw digest. Predecessor: the origin statement. -
public-safe derivative
New filePublic-safecustomers.public.jsonecd821db…Both earlier files remain readable.Signs the file aboveTransformation statement6c8f8061…Input: the normalized digest. Predecessor: the normalize statement.
- 014b4252… root
- 9ef9051c…
- 6c8f8061… head
The data
The dataset as it arrives
Three customer records, straight off a partner API. They have the defects real intake data has: padded whitespace, inconsistent case, and a consent field holding three different spellings of the same two answers.
[{"age":34,"customer_id":"C-1001","email":" [email protected] ","marketing_consent":" YES ","region":"us"},{"age":67,"customer_id":"C-1002","email":"[email protected]","marketing_consent":"false","region":"gb"},{"age":17,"customer_id":"C-1003","email":" [email protected]","marketing_consent":"1","region":"ca"}]These exact bytes hash to one value, and that value is the only thing an attestation can usefully commit to. A file path can be reused; a name can be reassigned. The digest cannot.
What Makoto adds
Attest it for the first time
The first statement is an origin. It is the only kind of statement with no predecessor, so it is where a lineage graph is allowed to begin. It answers one question: where did these specific bytes come from?
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "customers.raw.json", "digest": { "sha256": "4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919" } } ], "predicateType": "https://usemakoto.dev/predicate/v0.2/origin", "predicate": { "schemaVersion": "0.2", "event": { "id": "urn:uuid:9f1c6a02-5d74-4f0b-9a3e-1c6b4d8e77a1", "occurredAt": "2026-09-16T16:00:00Z" }, "source": { "kind": "urn:makoto:example:source:partner-api", "uri": "https://api.partner-bank.example/v2/customers", "name": "Partner Bank customer extract", "mediaType": "application/json", "retrievedAt": "2026-09-16T15:58:12Z", "version": "2026-09-16" }, "profiles": [], "extensions": { "urn:makoto:example:collection": { "method": "scheduled-pull", "collector": "urn:makoto:example:collector:prod-west-01", "collectorVersion": "1.4.2", "environment": "production", "recordsCollected": 3, "recordsDropped": 0 }, "urn:makoto:example:governance": { "originGeography": "US-WEST-2", "lawfulBasis": "contractual", "agreementReference": "https://legal.example.com/dpa/partner-bank-2026", "agreementEffective": "2026-01-15T00:00:00Z", "confidentiality": "confidential", "intendedUse": "fraud-detection model training" } } } }Read it from the top.
subjectnames the artifact and pins its digest, so the statement is bound to content rather than to a filename.sourcerecords what the bytes were pulled from and when. The operational and governance detail that the earlier format carried as first-class fields — collector identity, origin geography, lawful basis — lives underextensions, which the specification leaves open for exactly this.This statement now has an identity of its own, and it is a hash too: the SHA-256 of its canonical serialization. That number is how every later statement will refer back to it.
The statement digest is taken over the canonical form of the statement — keys sorted, no insignificant whitespace — not over the pretty-printed file above. That is what makes the identity stable: reformatting a statement, or reordering its keys, cannot change what it is. Reproduce it yourself below.
The data
The dataset changes
A new file with a new digest:
customers.normalized.json. The originalcustomers.raw.jsonis untouched.Now the records get normalized. Whitespace goes, email case is folded, consent becomes a real boolean, region codes are upper-cased.
[{"age":34,"customer_id":"C-1001","email":"[email protected]","marketing_consent":true,"region":"US"},{"age":67,"customer_id":"C-1002","email":"[email protected]","marketing_consent":false,"region":"GB"},{"age":17,"customer_id":"C-1003","email":"[email protected]","marketing_consent":true,"region":"CA"}]Field-by-field, for the first record. Scroll sideways on a narrow screen. Field Before After What happened email" [email protected] ""[email protected]"Surrounding spaces stripped, case folded. marketing_consent" YES "trueString token resolved to a boolean. region"us""US"Upper-cased to match the reference list. age3434Carried through unchanged. customer_id"C-1001""C-1001"Carried through unchanged. Different bytes, different digest. This is the moment the record would go stale in a system that tracked datasets by name.
The origin statement is not invalidated by this. It was never a claim about a file path or about the current contents of a table; it was a claim about one specific sequence of bytes, and that claim stays true forever. What the change creates is not a contradiction but a gap: there is now a dataset in play that no statement covers.
What Makoto adds
Re-attest the changed dataset
The gap closes with a transformation statement. It looks much like the origin one at the top, and then it does the thing origins cannot do.
{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "customers.normalized.json", "digest": { "sha256": "421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f8" } } ], "predicateType": "https://usemakoto.dev/predicate/v0.2/transform", "predicate": { "schemaVersion": "0.2", "event": { "id": "urn:uuid:4b0d2e11-8a37-4c62-b5d9-2f7e0a9c3b44", "occurredAt": "2026-09-16T16:00:00Z" }, "operation": { "type": "urn:makoto:example:operation:normalize", "name": "Normalize customer records", "tool": { "name": "makoto-example-normalizer", "version": "1.2.0", "uri": "git+https://github.com/makoto-project/makoto.git" }, "parametersDigest": { "sha256": "c463deba9e6da45408de8cc72cb352026f0f527d9fbd1f6d33f45266862002b2" } }, "profiles": [], "extensions": { "urn:makoto:example:execution": { "invocationId": "transform-20260916-160000-4b0d2e", "startedAt": "2026-09-16T16:00:00Z", "finishedAt": "2026-09-16T16:00:01Z", "recordsIn": 3, "recordsOut": 3, "recordsDropped": 0, "deterministic": true } }, "inputs": [ { "name": "raw", "digest": { "sha256": "4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919" }, "provenance": { "statementDigest": { "sha256": "014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11" }, "subjectName": "customers.raw.json" } } ] } }Look at the bottom of the document. The
inputsblock is the new information, and it is what makes this a link rather than an isolated assertion. It carries two digests, and they do different jobs:Because that second value is the hash of the whole previous statement, the edge cannot be rewired quietly. Editing the origin statement in any way — changing a timestamp, adding a field, even reordering it in a way that survives canonicalization — changes its digest, and this pointer stops resolving. Anyone re-signing the origin to cover their tracks has to re-sign every statement downstream of it.
Note also
operation.parametersDigest. The configuration that drove the run is not copied into the statement; it is hashed, so the parameters can be published, withheld, or kept internal without changing what the statement commits to.The data
Transform it again
Another new file:
customers.public.json. The normalized and raw files stay as they were.The normalized data still carries direct identifiers, so it cannot leave. A second transformation produces a public-safe derivative: email dropped, customer id replaced by a pseudonym, exact age generalised into a band.
[{"age_band":"25-34","customer_id":"4956cf12a64fd24a0815","marketing_consent":true,"region":"US"},{"age_band":"0-17","customer_id":"b8610bcb16fd5eb07f52","marketing_consent":true,"region":"CA"},{"age_band":"65+","customer_id":"d7ab36aae7b1e4237278","marketing_consent":false,"region":"GB"}]Before and after the public-safe step, for the one record in the USregion. Region is what lines the two rows up — the identifier no longer can. Scroll sideways on a narrow screen.Field Before After What happened email"[email protected]"not present Direct identifier removed outright. customer_id"C-1001""4956cf12a64fd24a0815"Replaced by a pseudonym; the original value is not recoverable from these bytes. age34not present Generalised. age_bandnot present "25-34"Replaces the exact age. marketing_consenttruetrueCarried through unchanged. region"US""US"Carried through unchanged. The shape of the data changed this time, not just its formatting. That is worth attesting precisely because it is the step a downstream consumer most wants assurance about.
What Makoto adds
Re-attest the transformed dataset
The same pattern, one link further along. The
inputsblock at the foot now points at the statement from step 04 rather than the one from step 02.{ "_type": "https://in-toto.io/Statement/v1", "subject": [ { "name": "customers.public.json", "digest": { "sha256": "ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e" } } ], "predicateType": "https://usemakoto.dev/predicate/v0.2/transform", "predicate": { "schemaVersion": "0.2", "event": { "id": "urn:uuid:7c5a9d38-6e41-4bb0-8f27-3d81c0f5ea62", "occurredAt": "2026-09-16T16:00:00Z" }, "operation": { "type": "urn:makoto:example:operation:public-safe", "name": "Remove direct identifiers and bucket ages" }, "profiles": [], "extensions": { "urn:makoto:example:privacy": { "directIdentifiersRemoved": [ "email" ], "pseudonymised": [ "customer_id" ], "generalised": [ "age" ], "reviewed": true } }, "inputs": [ { "name": "normalized", "digest": { "sha256": "421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f8" }, "provenance": { "statementDigest": { "sha256": "9ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55" }, "subjectName": "customers.normalized.json" } } ] } }Three statements, and the chain reads backwards from any point without consulting a server: the public-safe statement names the normalization statement, which names the origin statement, which names nothing, because it is the root.
What a receiver now sees
A receiver gets the three statements and one file of bytes. It does not get, and does not need, access to the producer's systems. From the head of the chain it walks backwards to the root, and at every hop it has something checkable.
The complete chain as a receiver reconstructs it. Scroll sideways on a narrow screen. Step Artifact digest Statement digest Declares its predecessor as Origin 4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11— root of the graph Normalize 421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f89ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11Public-safe ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e6c8f8061f0173e9ee0011fdb5ec604dde4f93597d29d0b8f5098ea05683ea33d9ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55The checks it can run on that, alone, at its own desk:
- Hash the file that actually arrived and compare it to the head statement's subject digestBYTES
- Hash each statement's canonical form and compare it to the digest its successor claimsGRAPH
- Confirm every step but the root declares a predecessor, and that the graph terminates at an originCONTINUITY
Three things this page deliberately does not establish. The statements here are unsigned, so there is no authenticity claim: anyone could have written them. Without signatures there is nothing to authorize, so there is no authorization claim either — and authorization is a receiver policy decision even when signatures are present, not a property of the document. And a chain that is internally consistent can still be an old one replayed, so freshness needs a separate anchor.
The runnable proof adds all three: Ed25519 signatures over each statement, a receiver-held trust policy that decides which key may sign which kind of step, and an expected manifest, head, and artifact that pin the handoff to this delivery rather than an earlier one. It also carries seven executable tampering cases, each of which breaks exactly one of the checks above.
For transport, the three statements and the final bytes go into a bundle index, which maps statement digests to paths without adding any authority of its own: dbom-example.json. The set of files themselves can be listed with digests and sizes in a dataset manifest: dataset-manifest-example.json.
Reproduce every digest on this page
No tooling required beyond a shell and Python. Artifact digests are plain file hashes:
curl -sO https://usemakoto.dev/demos/end-to-end/artifacts/data/customers.raw.json
shasum -a 256 customers.raw.json
Statement digests are taken over the canonical serialization, so hash that rather than the file:
python3 - <<'EOF'
import hashlib, json, urllib.request
BASE = "https://usemakoto.dev/attestations/"
for name in ("origin-example", "transform-example", "public-transform-example"):
raw = urllib.request.urlopen(BASE + name + ".json").read()
canonical = json.dumps(json.loads(raw), sort_keys=True, separators=(",", ":"))
print(hashlib.sha256(canonical.encode()).hexdigest(), name)
EOF
That prints the three statement digests quoted above, in order. If any of them differs, this page and the documents it serves have drifted apart, and the documents are the ones to believe.
This walkthrough stops short of signing. The runnable proof does not.
Same three statements, plus signatures, a receiver trust policy, a signed handoff, and seven tampering cases that each produce a denial with a named error code.