Evidence: chains, anchors, transitions, and exports

Copy Markdown View Source

Verifying a request answers "may these bytes be accepted now?". Evidence answers a later question: "what did this service accept, and can anyone show that the record was not altered?". The protocol defines four evidence formats for that, and verifies each of them with the same posture as credentials: pure functions, caller-supplied keys and expectations, redacted facts, and the single {:error, :invalid}.

FormatWhat it isProduceVerifyFacts
Consumption chainHash-linked rows, one per accepted invocationencode_consumption_entry/2check_chain/2ChainFacts
Boundary anchorA signed statement that a chain had a given head at a given time (typ: "ba+chain-anchor")boundary_anchor_signing_input/2, assemble_compact/2verify_historical_anchor/3AnchorFacts
Key transitionThe current archive key authenticating its successor (typ: "ba+key-transition")key_transition_signing_input/2, assemble_compact/2verify_key_transition/4KeyTransitionFacts
Anchored exportOne archive object holding rows, anchors, and transitionsencode_anchored_export/2verify_anchored_export/3AnchoredExportFacts

Every contract-major has all eight functions in its own namespace, with its own domain separators (BAP1-CHAIN, BAP2-CHAIN, BAP3-CHAIN, and the matching archive prefixes); under contract-major 3 the anchors and transitions are ES256-signed. The normative rules are in spec/bap-v1.md ยง 15.

ChainFacts, AnchorFacts, and KeyTransitionFacts carry trust: :not_evaluated; AnchoredExportFacts carries trust: :not_evaluated and authorization: :not_evaluated. They say the bytes are consistent with what you supplied. They do not say the archive is complete, that you should trust its keys, or that anything it records was authorized.

A consumption chain

A row binds a chain id, a sequence number, the previous row's hash, and a 32-byte commitment. The host computes the commitment over whatever it recorded for the invocation, for example the envelope facts and the outcome, and keeps the preimage private: the public row carries only the digest. The first row links to the all-zero hash.

alias BoundedAuthorityProtocol.V1
alias BoundedAuthorityProtocol.V1.{ChainInput, ConsumptionEntry, ExpectedChain}

chain_id = "urn:example:chain:model-evaluations"

{rows, head} =
  ["eval run 41", "eval run 42", "eval run 43"]
  |> Enum.with_index(1)
  |> Enum.map_reduce(<<0::256>>, fn {record, sequence}, previous ->
    entry = %ConsumptionEntry{
      chain_id: chain_id,
      sequence: sequence,
      previous_hash: previous,
      commitment: :crypto.hash(:sha256, record)
    }

    {:ok, encoded} = V1.encode_consumption_entry(entry, %{})
    {encoded.bytes, encoded.hash}
  end)

expected_chain = %ExpectedChain{
  chain_id: chain_id,
  first_sequence: 1,
  last_sequence: 3,
  row_count: 3,
  previous_hash: <<0::256>>,
  last_hash: head,
  bounds: %{}
}

{:ok, chain_facts} = V1.check_chain(%ChainInput{rows: rows}, expected_chain)
[first, _second, third] = rows

{chain_facts.row_count, chain_facts.verification,
 V1.check_chain(%ChainInput{rows: [first, third]}, expected_chain)}

The value is {3, :boundary_consistent, {:error, :invalid}}: the chain verifies, and the same chain with a row dropped does not.

Chain verification checks consistency between the boundaries you name, and only that. A chain that was validly truncated or rebuilt from some earlier row still links. What makes it evidence is that the expected head (last_hash) and the predecessor come from your own records or from signed anchors, never from the rows you are checking (usage rules 10 and 11).

Anchors and key transitions

An archive key signs a boundary anchor at the chain's start (sequence 0, all-zero hash) and at its end (the last sequence and head). Archive keys are historical keys: each HistoricalPublicKey carries valid_from and valid_before (or :unbounded), and an anchor verifies only when its anchored_at falls inside the window of the key that signed it.

When the archive moves to a new key, the current key signs a key transition naming the next key by id and RFC 7638 fingerprint, effective at a time inside both windows. A verifier that trusts the first key in the history can then follow the archive onto later keys without trusting each one out of band. verify_key_transition/4 takes the current key, the next key, and the expected transition; it refuses a transition signed by the next key.

Anchored exports

encode_anchored_export/2 frames the rows, the start anchor, the transitions, and the end anchor into one archive object. It first checks them against the same expectations a verifier will use, so it refuses to build an archive that would not verify. It returns the object as a list of chunks, its SHA-256 digest, and its byte count.

The host writes the chunks to an object store and records two values from outside the archive: the digest and the object version the store reports. To verify the archive later, verify_anchored_export/3 takes the bytes read back (ArchivedObject, any chunking), the ordered key history (HistoricalKeyChain), and an ExpectedAnchoredExport built from the host's own records. It checks every byte to the exact end of the object, the digest, the object version, the chain, every anchor, and every transition.

The chains and archives Livebook builds all four formats end to end with ephemeral keys and shows the attacks each one refuses: a dropped, reordered, or edited row, a truncated head, an anchor outside its key's window, a transition signed by the wrong key, a trailing byte, another object version, and a missing key in the history.

What the host owns

The package checks evidence; it does not keep it. The host:

  • decides what each commitment covers and keeps the preimages;
  • appends rows atomically, so two accepted invocations never share a sequence number;
  • decides when to anchor and which archive keys to use, and protects those private keys;
  • stores archives, and records each digest and object version where an attacker who can write the archive cannot also rewrite them;
  • decides which archive keys it trusts, for which windows, and whether a verified archive is enough for its audit.