# chain-anchor/v1 — batched-anchoring sample (live, anchored 2026-05-12)

This sample is anchored on BSV mainnet via
`POST https://app.satsignal.cloud/api/v1/anchors` against the
`agent-runs` matter. The cryptographic objects in each `*.json` file
here are byte-for-byte identical to what the server hashed into the
on-chain transaction — the client-recomputed Merkle root matches the
server's response exactly, and the leaf-2 inclusion proof walks
deterministically to that on-chain root.

## Scenario

Four AAR receipts ([Agent Action Receipt v1.0](https://github.com/Cyberweasel777/agent-action-receipt-spec))
are batched under a single BSV anchor. Each receipt embeds a
`chain-anchor/v1` `evidenceRef` entry per the
[AAR PR](https://github.com/Cyberweasel777/agent-action-receipt-spec/pull/1),
carrying its merkle-tree leaf and the inclusion proof from that leaf
to the on-chain root.

This shape lets an issuer amortize one BSV transaction across N
receipts while preserving per-receipt independent verifiability — an
auditor with just one receipt can prove (via the inclusion proof)
that the receipt's content hash was included in the on-chain
commitment, without seeing the other receipts.

## The batch

- **bundle_id:** `4300b1ccb6884c2c`
- **txid:** [`1ba57f41fe81fab8383b5ede81d2de03a13ec5f3b7e2bf40ede91dd04115783e`](https://whatsonchain.com/tx/1ba57f41fe81fab8383b5ede81d2de03a13ec5f3b7e2bf40ede91dd04115783e)
- **root:** `1c724e6fb05e735557c3e5969964100c98a9258bc80e460461cc453b861d0989`
- **chain:** `bsv-mainnet`
- **system:** `satsignal`
- **leaf_count:** 4
- **leaf hashing:** `leaf = sha256(JCS({label, sha256_hex}))` per manifest mode in [`/spec-mbnt`](https://proof.satsignal.cloud/spec-mbnt) — binds `(label, sha256_hex)` together so a swapped label can't preserve the leaf even if the underlying sha is the same.

| Leaf index | Label           | Underlying sha256_hex (the AAR receipt's content hash placeholder)  | Leaf hash (`sha256(JCS({label, sha256_hex}))`)                     |
|------------|-----------------|---------------------------------------------------------------------|---------------------------------------------------------------------|
| 0          | aar-receipt-1   | `d3ba9979113fe3338f09303f435d38dc38409a43c198d1db39aae97c5fbad9ab`  | `1f02244356c8bc0dfb247a893ddb1612e3dae71aaf825d6dfad73dadf573a65c` |
| 1          | aar-receipt-2   | `24ce406a2300aac2bcc275b95bb146c1f13b2b4375f2588c82787303526f6b8f`  | `58f4e8cf60d07485e5c0c486d3dbffa1048f91010e57477a3a03088298cdbbb6` |
| 2          | aar-receipt-3   | `6adc7ec9f4030290e02e560d3a6f02f2fb182be02da1769cc6f63a56bcfd33bd`  | `3d4916f1dd9ad7b1ff12a0534c544b3a63e1631df9c3edec4037a3f3a9751ea3` |
| 3          | aar-receipt-4   | `1e62ca9e87ee87323953c5a1eab87b38e7b7f2a0dba21c62c5fc6f06a3dc1800`  | `e6b0ea1163734b4a84bf6ee38b9fc0400e86fa45e72a248efcedcecb2795d9dd` |

The underlying `sha256_hex` values are deterministic placeholders
(each is `sha256(b"aar-receipt-synthetic-<i>")` for i ∈ {1..4}), so
the entire merkle tree is client-reproducible.

## Artifacts in this directory

- `batch-anchor.json` — the batch-level metadata: real txid, real
  root, all four leaves with both their underlying sha256_hex and
  the server's computed leaf_hash.
- `anchor-response.json` — the actual API 200 response from
  `POST /api/v1/anchors`.
- `aar-receipt-2.json` — leaf #1 (`aar-receipt-2`), shown as a
  complete AAR receipt with a `chain-anchor/v1` `evidenceRef` entry
  carrying `leafHash` + `inclusionProof` from leaf to root.
- `aar-receipt-2-dualattest.json` — the same AAR receipt with **two**
  `evidenceRef` entries: an `rfc3161/v1` entry carrying a real RFC
  3161 timestamp token from `freetsa.org` over the canonical receipt
  hash, alongside the existing `chain-anchor/v1` entry over the BSV
  anchor. Independent trust roots; either path verifies on its own.
  See the *Dual-attest variant* section below.

## Reproducing the leaves and root locally

```python
import hashlib, json

def sha(b): return hashlib.sha256(b).digest()
def pair(a, b): return sha(a + b)

# Server-compatible JCS canonicalization (sorted keys, no whitespace,
# UTF-8 — full impl in scripts/merkle_row.py canonicalize()).
def jcs(o):
    return json.dumps(o, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False, allow_nan=False).encode("utf-8")

items = [{"label": f"aar-receipt-{i}",
          "sha256_hex": sha(f"aar-receipt-synthetic-{i}".encode()).hex()}
         for i in range(1, 5)]
leaves = [sha(jcs({"label": it["label"], "sha256_hex": it["sha256_hex"]}))
          for it in items]
level1 = [pair(leaves[0], leaves[1]), pair(leaves[2], leaves[3])]
root = pair(level1[0], level1[1]).hex()
assert root == "1c724e6fb05e735557c3e5969964100c98a9258bc80e460461cc453b861d0989"
```

## Verifying leaf #1's inclusion proof locally

```python
def walk(leaf, proof):
    carry = leaf
    for step in proof:
        sib = bytes.fromhex(step["sib"])
        if step["side"] == "L":
            carry = sha(sib + carry)
        else:
            carry = sha(carry + sib)
    return carry

leaf = bytes.fromhex("58f4e8cf60d07485e5c0c486d3dbffa1048f91010e57477a3a03088298cdbbb6")
proof = [
    {"sib": "1f02244356c8bc0dfb247a893ddb1612e3dae71aaf825d6dfad73dadf573a65c", "side": "L"},
    {"sib": "4d236d4c49242ddd74b70d3f183c9c894b8cc445f96cb78fc8d65a72cc9a0db9", "side": "R"},
]
assert walk(leaf, proof).hex() == "1c724e6fb05e735557c3e5969964100c98a9258bc80e460461cc453b861d0989"
```

The proof shape `{sib, side}` and walk procedure are specified in
[`/spec-merkle-row`](https://proof.satsignal.cloud/spec-merkle-row) §2.2;
the leaf-hashing rule (`sha256(JCS({label, sha256_hex}))`) is from
[`/spec-mbnt`](https://proof.satsignal.cloud/spec-mbnt) manifest mode;
chain-anchor-v1 composes both at the cross-system batching layer per
[`/spec-chain-anchor`](https://proof.satsignal.cloud/spec-chain-anchor) §4.1.

## Verifying against the chain

Hit the existing notary `/verify` page with the bundle (or any of the
artifacts above as a JSON paste) and it will resolve the txid,
extract the on-chain root, and walk the proof end-to-end.

A lighter-weight check: fetch the transaction at
[whatsonchain](https://whatsonchain.com/tx/1ba57f41fe81fab8383b5ede81d2de03a13ec5f3b7e2bf40ede91dd04115783e),
extract the OP_RETURN payload per [`/spec-mbnt`](https://proof.satsignal.cloud/spec-mbnt)
§3 (TLV-encoded root + subtype), confirm the root bytes equal
`1c724e6fb05e735557c3e5969964100c98a9258bc80e460461cc453b861d0989`.

## Dual-attest variant — TSA path

`aar-receipt-2-dualattest.json` carries the same canonical receipt
as `aar-receipt-2.json` plus one extra `evidenceRef` entry of type
`rfc3161/v1`. The TSA token was issued by `freetsa.org` over the
canonical receipt's sha256 hash:

- **TSA:** `freetsa.org`
- **TSA timestamp:** `2026-05-12T16:23:39Z`
- **messageImprintHex:** `be89a8815752d6e369817f1b2e2dcb2ab30c3d55a33965a0142405860d02c39b`
- **tsaToken encoding:** `rfc3161-tsr-asn1-der-base64` — a full RFC 3161
  `TimeStampResponse` (ASN.1 DER), base64-encoded so it slots into
  JSON cleanly. To verify with stock OpenSSL, base64-decode back to
  binary first.

The TSA path and the chain path are independent — either is
sufficient evidence the receipt's canonical bytes existed by the
attested time. See [`/spec-chain-anchor`](https://proof.satsignal.cloud/spec-chain-anchor) §4.2
for the embedding shape, failure modes, and verifier rules.

### Order-of-operations note

The TSA token was issued *over* the receipt at a specific point in
time. The canonical bytes the TSA signed are obtained by stripping
the `rfc3161/v1` entry itself, the top-level `_note` (not part of
the AAR spec), and `signature.sig` (per AAR §5.1), then
JCS-canonicalizing the rest. A verifier reconstructs those bytes
the same way before checking the token.

### Verifying the TSA path locally

```bash
# 1. Pull the dual-attest receipt + freetsa's CA bundle.
curl -sO https://satsignal.cloud/samples/chain-anchor/aar-receipt-2-dualattest.json
curl -sO https://freetsa.org/files/cacert.pem
curl -sO https://freetsa.org/files/tsa.crt

# 2. Extract the TSA token (base64-decode evidenceRef[0].tsaToken back to DER).
jq -r '.evidenceRef[] | select(.type=="rfc3161/v1") | .tsaToken' \
   aar-receipt-2-dualattest.json | base64 -d > tsa-token.tsr

# 3. Reconstruct the canonical receipt bytes a TSA verifier needs:
#    drop _note, drop signature.sig, drop the rfc3161/v1 entry,
#    JCS-canonicalize the rest. (Below uses scripts/merkle_row.py
#    canonicalize() from the satsignal repo; pure stdlib otherwise.)
python3 - <<'PY' > canonical-receipt.bin
import json, copy, sys
sys.path.insert(0, '<path-to-satsignal-repo>/scripts')
from merkle_row import canonicalize
r = json.load(open('aar-receipt-2-dualattest.json'))
r.pop('_note', None)
r['signature'].pop('sig', None)
r['evidenceRef'] = [e for e in r['evidenceRef'] if e['type']!='rfc3161/v1']
sys.stdout.buffer.write(canonicalize(r))
PY

# 4. Verify.
openssl ts -verify -in tsa-token.tsr -data canonical-receipt.bin \
   -CAfile cacert.pem -untrusted tsa.crt
# → "Verification: OK"
```

## Cross-domain anchor — three real public artifacts in one tx

`cross-domain.json` demonstrates the same batched-anchoring shape as
`batch-anchor.json` above, but with three leaves drawn from three
*different* public systems instead of four synthetic AAR receipts.
The on-chain root commits, in a single BSV transaction, to:

- an Ethereum mainnet block hash,
- a HuggingFace model card's README at a pinned commit, and
- a C2PA-credentialed sample image.

Each leaf is independently reproducible by a third party — fetch the
named source, sha256 it, recompute the JCS-canonical leaf hash,
walk the inclusion proof, get the same on-chain root.

- **bundle_id:** `5895200028814c73`
- **txid:** [`027c5b69ba5955e17ee470ff2ac5f8825c551ab61be601773b8910b0c444f075`](https://whatsonchain.com/tx/027c5b69ba5955e17ee470ff2ac5f8825c551ab61be601773b8910b0c444f075)
- **root:** `686c620b9b0e7219bf943bf31f8ffa3be2b23fc21384aef4ae63a163ff3ebfb7`
- **chain:** `bsv-mainnet`
- **leaf_count:** 3
- **leaf hashing:** same manifest-mode rule as the AAR batch above
  (`leaf = sha256(JCS({label, sha256_hex}))`).

| Idx | Label                                            | Underlying sha256_hex                                                | Source                                                                                                |
|-----|--------------------------------------------------|----------------------------------------------------------------------|--------------------------------------------------------------------------------------------------------|
| 0   | `ethereum-mainnet-block-25080361`                | `47c20c0086f3ff41144d495a5baa9394ecb42585c5a80960390ff1edd87c2772`   | Ethereum mainnet block 25080361 — `sha256_hex` IS the 32-byte block hash itself.                       |
| 1   | `huggingface-openai-whisper-large-v3-readme-main`| `b05416eb578e9fc65ce137f3401bafdf3bc2feac7448e7dd180dcd367b8cfb52`   | [`whisper-large-v3/README.md @ 06f233fe`](https://huggingface.co/openai/whisper-large-v3/raw/06f233fe06e710322aca913c1bc4249a0d71fce1/README.md), 21,829 bytes. |
| 2   | `c2pa-public-testfile-CA-jpg`                    | `e71bff58fc57640803e6e65f7534e2fb0c2f99018c85276cc14b30f04427cc76`   | [`contentauth/c2pa-rs sdk/tests/fixtures/CA.jpg`](https://raw.githubusercontent.com/contentauth/c2pa-rs/main/sdk/tests/fixtures/CA.jpg), 166,864 bytes, C2PA manifest embedded. |

### Reproducing all three leaves and the root locally

```python
import hashlib, json, urllib.request

def sha(b): return hashlib.sha256(b).digest()
def jcs(o):
    return json.dumps(o, sort_keys=True, separators=(",", ":"),
                      ensure_ascii=False, allow_nan=False).encode("utf-8")
def pair(a, b): return sha(a + b)
def get(url):
    return urllib.request.urlopen(
        urllib.request.Request(url, headers={"User-Agent":"Mozilla/5.0"}),
        timeout=15
    ).read()

# Leaf 0 — Ethereum block hash (any mainnet RPC; here a public one).
import urllib.request as r
import json as j
body = j.dumps({"jsonrpc":"2.0","method":"eth_getBlockByNumber",
                "params":[hex(25080361), False],"id":1}).encode()
req = r.Request("https://1rpc.io/eth", data=body,
                headers={"Content-Type":"application/json"})
eth_block_hash_hex = j.loads(r.urlopen(req, timeout=15).read())["result"]["hash"][2:]
assert eth_block_hash_hex == "47c20c0086f3ff41144d495a5baa9394ecb42585c5a80960390ff1edd87c2772"

# Leaf 1 — HuggingFace README at pinned commit.
readme = get("https://huggingface.co/openai/whisper-large-v3/raw/"
             "06f233fe06e710322aca913c1bc4249a0d71fce1/README.md")
hf_sha = sha(readme).hex()
assert hf_sha == "b05416eb578e9fc65ce137f3401bafdf3bc2feac7448e7dd180dcd367b8cfb52"

# Leaf 2 — C2PA-credentialed JPEG.
jpg = get("https://raw.githubusercontent.com/contentauth/c2pa-rs/"
          "main/sdk/tests/fixtures/CA.jpg")
c2pa_sha = sha(jpg).hex()
assert c2pa_sha == "e71bff58fc57640803e6e65f7534e2fb0c2f99018c85276cc14b30f04427cc76"

# Manifest-mode leaf hashing + merkle row (dup-last-when-odd).
items = [
    {"label": "ethereum-mainnet-block-25080361",                 "sha256_hex": eth_block_hash_hex},
    {"label": "huggingface-openai-whisper-large-v3-readme-main", "sha256_hex": hf_sha},
    {"label": "c2pa-public-testfile-CA-jpg",                     "sha256_hex": c2pa_sha},
]
leaves = [sha(jcs({"label": it["label"], "sha256_hex": it["sha256_hex"]}))
          for it in items]
level = leaves[:]
while len(level) > 1:
    if len(level) % 2 == 1:
        level.append(level[-1])
    level = [pair(level[i], level[i+1]) for i in range(0, len(level), 2)]
assert level[0].hex() == "686c620b9b0e7219bf943bf31f8ffa3be2b23fc21384aef4ae63a163ff3ebfb7"
```

### Why this shape matters

The three leaves bottom out in three independent trust roots — Ethereum
PoS consensus, HuggingFace's git storage + commit SHA1 chain, and the
contentauth C2PA test-fixture set — but the BSV anchor commits to all
three under one root in one transaction. A verifier can hand any one
leaf to an auditor along with its inclusion proof, and the auditor can
prove that *this specific artifact's hash* was committed on BSV at
the block time of `027c5b69…f075` without needing the other two.

This is the operational shape `chain-anchor-v1` §4.1 specifies for
"different receipt formats, same anchor envelope" — the
[chain-anchor-v1 spec](https://proof.satsignal.cloud/spec-chain-anchor)
intentionally leaves the leaf's content shape unconstrained, so an
issuer can batch heterogeneous evidence under one cost.

### Verifying against the chain

Same `/verify` flow as the AAR batch above — paste any one leaf's
content hash plus its `inclusion_proof` and the verifier walks to
the root, then resolves the txid to confirm the on-chain commitment.

## C2PA-credentialed image — chain-anchor riding inside a C2PA manifest

`c2pa-credentialed.jpg` is the c2patool reference distribution's
`sample/image.jpg` (Canon EOS REBEL T3, 1024×683, 61,720 bytes,
sha256 `f999fd78…0f23`) re-signed by us with a C2PA manifest that
carries a custom `com.chain-anchor` assertion. The assertion points
at a real BSV anchor over the pristine image's canonical receipt.

This demonstrates the **other** direction of C2PA interop — not
"a C2PA-credentialed image is a leaf in a Satsignal anchor" (which
`cross-domain.json` already shows) but "a Satsignal anchor lives
inside a C2PA manifest as a custom assertion." Both directions are
supported; pick whichever your receipt format already uses.

- **txid:** [`6e61c940abff8d994d834ee984fcff87f7c7943fd7d5dd3c50041759679cc5dc`](https://whatsonchain.com/tx/6e61c940abff8d994d834ee984fcff87f7c7943fd7d5dd3c50041759679cc5dc)
- **bundle_id:** `e4a8f09e04b24829`
- **on-chain doc_hash:** `42642052794cbd717c46a171b2fd1df44e13b243` (20 bytes — MBNT writes the first 20 of sha256(canonical_doc) into OP_RETURN per [`/spec-mbnt`](https://proof.satsignal.cloud/spec-mbnt))
- **canonical-doc full sha256:** `42642052794cbd717c46a171b2fd1df44e13b243c1435d8140582b3cfd05dc6f`
- **block:** 948821 / `00000000…3852b0`
- **C2PA assertion label:** `com.chain-anchor` (bare — c2pa-rs treats trailing `.vN` as a version indicator; version stays inside the envelope's `v` field per chain-anchor-v1 §7)
- **C2PA signing cert:** c2patool's bundled `C2PA Test Signing Cert` (ES256). **Intentionally not in the C2PA production trust list** — the whole point of the demo is that the cert path fails (`signingCredential.untrusted`) while the chain path still verifies.

### Artifacts in this directory

- `c2pa-credentialed.jpg` — the signed image (125,762 bytes; manifest store ≈ 51% of file).
- `c2pa-credentialed.json` — chain-anchor-v1 envelope mirroring the cross-domain shape, with C2PA-specific extensions (asset source, signing cert, two-path verification record).
- `c2pa-manifest.json` — the c2patool manifest-definition file we fed to `c2patool sign`. Reproducible by anyone who installs c2patool v0.26.58.
- `canonical-receipt-c2pa.json` — the server-canonical receipt bytes the BSV anchor commits to (extracted from the receipt bundle). Reviewers re-sha256 these bytes to recover the on-chain `doc_hash`. **Do not reformat** — the file is byte-identical to what was hashed.
- `c2pa-bundle-manifest.json` — the bundle-level manifest (txid, ARC acceptance metadata, doc_hash_expected).

### Verifying both paths

```bash
# 1. Install c2patool (or use any C2PA-aware reader).
curl -sL https://github.com/contentauth/c2pa-rs/releases/download/c2patool-v0.26.58/c2patool-v0.26.58-x86_64-unknown-linux-gnu.tar.gz \
  | tar -xz -C /tmp && sudo install -m755 /tmp/c2patool/c2patool /usr/local/bin/

# 2. Pull the signed image + canonical receipt.
curl -sO https://satsignal.cloud/samples/chain-anchor/c2pa-credentialed.jpg
curl -sO https://satsignal.cloud/samples/chain-anchor/canonical-receipt-c2pa.json

# 3. Verify the C2PA path.
#    Expect: validation_state=Valid, 7 successes, 1 failure
#    (signingCredential.untrusted — the demo cert, as designed).
c2patool c2pa-credentialed.jpg

# 4. Extract the chain-anchor assertion.
c2patool c2pa-credentialed.jpg | jq '.manifests | to_entries[0].value.assertions[]
    | select(.label=="com.chain-anchor") | .data' > anchor.json
TXID=$(jq -r .txid anchor.json)
ROOT=$(jq -r .root_hash anchor.json)
ASSET=$(jq -r .binds.asset_sha256_hex anchor.json)

# 5. Independently confirm the canonical-doc sha matches root_hash.
test "$(sha256sum canonical-receipt-c2pa.json | awk '{print $1}')" = "$ROOT" && echo "[canonical sha matches]"

# 6. Confirm the canonical doc carries the asset's hash inside.
jq -e --arg s "$ASSET" '.subject.proofs.byte_exact.hash == $s' canonical-receipt-c2pa.json \
  && echo "[canonical doc binds asset bytes]"

# 7. Confirm OP_RETURN doc_hash == first 20 bytes of root.
curl -sS "https://api.whatsonchain.com/v1/bsv/main/tx/hash/$TXID" \
  | python3 -c "
import sys, json
tx = json.load(sys.stdin)
for v in tx['vout']:
    asm = v['scriptPubKey']['asm']
    if 'OP_RETURN' in asm:
        h = v['scriptPubKey']['hex']
        # MBNT layout: 6a <len> 4d424e54 v st tlv_len doc_hash[20] tlvs
        # data starts after the pushdata header
        i = h.index('4d424e54')  # MBNT magic
        doc_hash_hex = h[i+16:i+16+40]  # skip 4(MBNT)+1(v)+1(st)+2(tlv_len) bytes = 8 hex chars * 2... actually 16 hex chars
        # 4 bytes magic + 1 v + 1 subtype + 2 tlv_len = 8 bytes = 16 hex chars
        # then 20 bytes doc_hash = 40 hex chars
        print('OP_RETURN doc_hash:', doc_hash_hex)
        print('root[:40]         :', '$ROOT'[:40])
        print('match:', doc_hash_hex == '$ROOT'[:40])
"
```

A reader following these steps gets two independent verifications:
the c2pa manifest's signature (sound but untrusted in production)
and the BSV chain commitment (sound *and* trust-rooted in PoW). Per
the chain-anchor-v1 §4.2 verifier rules, single-path success is
still strong evidence — and in this case the strong path is the
chain one.

## Visa Trusted Agent Protocol (TAP) interop

`tap-interop.json` demonstrates chain-anchor-v1 stacking on
[Visa's Trusted Agent Protocol](https://github.com/visa/trusted-agent-protocol),
the open framework Visa unveiled in 2025 for cryptographically
distinguishing legitimate AI agents from malicious bots at the
merchant boundary. TAP's wire format is
[RFC 9421 HTTP Message Signatures](https://www.rfc-editor.org/rfc/rfc9421.html) —
agents sign their HTTP requests; merchants verify against a public
key in an agent registry.

**What TAP provides:** real-time agent identity verification at the
transaction moment. The merchant gets a fresh signature, checks it
against the registry's current key, decides whether to accept.

**What chain-anchor adds:** post-hoc auditability of *what the
registry said about this agent at time T*, and *what this agent
signed at time T*, neither of which depends on the registry
remaining online or trusted forever.

The sample anchors two leaves in one BSV transaction:

- `tap-agent-registration.json` — a TAP-style agent-registry record
  carrying the agent's `key_id`, `alg=ed25519`, and `public_key_b64`.
  Anchored sha provides chain-rooted evidence that this exact
  registration existed at the anchor's block time.
- `tap-signed-request.json` — a real RFC 9421-signed HTTP request
  produced by the same agent. The signature covers `("@authority"
  "@path")` per TAP's convention, with `created`, `expires`,
  `keyId`, `alg`, `nonce`, and `tag` parameters in TAP-spec field
  order. Anchored sha provides chain-rooted evidence that the
  signed request envelope existed at the anchor's block time.

- **bundle_id:** `65b3b34eccf24fcf`
- **txid:** [`1e4abc4a18f73e1000d210ec4c6f3041e9176ae7862adb9c06888c3b237ed4e4`](https://whatsonchain.com/tx/1e4abc4a18f73e1000d210ec4c6f3041e9176ae7862adb9c06888c3b237ed4e4)
- **root:** `e84d93b1340cbbe60c21d96f8d82015cada0b5eb3c1f02cda467c27b4b10f97e`
- **leaf hashing:** manifest mode (`leaf = sha256(JCS({label, sha256_hex}))`).

### Source posture

The Visa TAP repository's code is released under [Visa Developer
Center Terms of Use](https://developer.visa.com/terms), not a
permissive OSS license. No code from `visa/trusted-agent-protocol`
was copied or redistributed in this demo. The signature shape is
built directly from the public RFC 9421 standard, using
`python-cryptography` for the Ed25519 primitive. `tap-demo.py` in
this directory is the full reproduction script — re-running it
with the same Python + cryptography versions produces byte-identical
records.

### Reproducing both leaves locally

```bash
curl -sO https://satsignal.cloud/samples/chain-anchor/tap-demo.py
pip install cryptography
python3 tap-demo.py
# Expected output (relevant lines):
#   registration sha256_hex     : 913f75c4b21681b2ff7adf6be750efd93b2dfeeb436276bd8d0e343bcd8cbff6
#   signed_request sha256_hex   : 5b40ec22c1ddd901974d4f5ad8ad2ef147fa4ac66e36910cf7183fe9d9d67e9d
#   merkle root (locally)       : e84d93b1340cbbe60c21d96f8d82015cada0b5eb3c1f02cda467c27b4b10f97e
#   ed25519 verify              : True
```

### What the two-path verification proves

A merchant or auditor with just one signed request weeks later can:

1. **RFC 9421 path** — re-verify the Ed25519 signature using the
   public key from `tap-agent-registration.json`. Confirms the
   signature was produced by the holder of the key.
2. **chain-anchor path** — walk the inclusion proof from the
   signed-request leaf (or the registration leaf) to the on-chain
   root. Confirms both records existed by the block time of
   `1e4abc4a…d4e4`, independently of the registry's then-state or
   the merchant's log retention.

If TAP's registry rotates keys, goes offline, or has its
attestations rewritten, the chain-anchor leaf still proves what
the agent presented at the anchor's block time — which is the
forensic evidence floor regulated commerce will eventually need.
