Makoto
Menu
Worked walkthrough · one dataset, start to finish

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.

Raw customer records are attested by an origin statement, normalized into a second dataset attested by a transformation statement, reduced to a public-safe dataset attested by a third statement, and then handed to a receiver that re-hashes the bytes it was given. Every arrow points forward. Each step writes a file; under each file is the signed statement for it, and each statement names the one before it as its predecessor. The receiver comes last. Nothing is edited in place: each step writes a new file with a new digest, and every earlier file stays exactly as it was. For a table rewritten in place, see the in-place walkthrough.
  1. FileRawcustomers.raw.json
    4e90181e…Stays as it arrived.
    Signs the file aboveOrigin statement014b4252…Subject: the raw file. No predecessor; the graph starts here.
  2. normalize

    New fileNormalizedcustomers.normalized.json
    421e19cf…Written beside the raw file, which is untouched.
    Signs the file aboveTransformation statement9ef9051c…Input: the raw digest. Predecessor: the origin statement.
  3. public-safe derivative

    New filePublic-safecustomers.public.json
    ecd821db…Both earlier files remain readable.
    Signs the file aboveTransformation statement6c8f8061…Input: the normalized digest. Predecessor: the normalize statement.
ReceiverReceives the public-safe file and its three statements, re-hashes the bytes, and checks the chain from the root statement to the head.
6c8f8061…
  1. 014b4252… root
  2. 9ef9051c…
  3. 6c8f8061… head
  1. 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.

    File
    customers.raw.json
    Size
    311 bytes
    SHA-256
    4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919
  2. 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. subject names the artifact and pins its digest, so the statement is bound to content rather than to a filename. source records 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 under extensions, 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.

    Subject digest
    4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919
    Statement digest
    014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11
    Document
    origin-example.json

    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.

  3. The data

    The dataset changes

    A new file with a new digest: customers.normalized.json. The original customers.raw.json is 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.
    FieldBeforeAfterWhat 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.

    Digest before
    4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919
    Digest after
    421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f8

    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.

  4. 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 inputs block 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:

    Input digest
    4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919
    inputs[0].digest: the bytes this step consumed. A receiver checks this against the predecessor's subject digest; if they differ, something was swapped between the steps.
    Predecessor
    014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11
    inputs[0].provenance.statementDigest: the identity of the origin statement from step 02. This is the edge in the graph.

    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.

    Subject digest
    421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f8
    Statement digest
    9ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55
    Parameters
    c463deba9e6da45408de8cc72cb352026f0f527d9fbd1f6d33f45266862002b2
    transform-parameters.json
    Document
    transform-example.json
  5. 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 US region. Region is what lines the two rows up — the identifier no longer can. Scroll sideways on a narrow screen.
    FieldBeforeAfterWhat happened
    email"[email protected]"not presentDirect identifier removed outright.
    customer_id"C-1001""4956cf12a64fd24a0815"Replaced by a pseudonym; the original value is not recoverable from these bytes.
    age34not presentGeneralised.
    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.

    Digest before
    421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f8
    Digest after
    ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e
  6. What Makoto adds

    Re-attest the transformed dataset

    The same pattern, one link further along. The inputs block 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.

    Subject digest
    ecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e
    Statement digest
    6c8f8061f0173e9ee0011fdb5ec604dde4f93597d29d0b8f5098ea05683ea33d
    Points back to
    9ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55
    Document
    public-transform-example.json
  7. 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.
    StepArtifact digestStatement digestDeclares its predecessor as
    Origin4e90181e7f75ccdf0e4d81c83d281f53806bed241787cdc751e22b2d4cd19919014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11— root of the graph
    Normalize421e19cf002aa25cf6f2f9ed788e04c9badfd9cbc6b00030b0b089810b2ae4f89ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55014b4252977a17c8f2423210fc4c123a464efcc36eefa70e4203c3adee1a3b11
    Public-safeecd821dbe6f4204c4aad64878643d7954cb8c94abbf8d0b28f9c2bfc272f781e6c8f8061f0173e9ee0011fdb5ec604dde4f93597d29d0b8f5098ea05683ea33d9ef9051c4f0229f9238cfc8541e76d9f2517ddc69a6e3ccf8e69604899527a55

    The 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.