v2 Upgrade Summary

The /transparency page outputs a schema_version field indicating the current version. New writes go only to v2; old v1 roots can still be verified independently (the domain separator tag differs).

combined_root_v2 = BLAKE3("BAN-TRANSPARENCY-v2"
                          || ledger_root
                          || trust_root
                          || contract_root
                          || oracle_root)

oracle_root = RFC 6962 Merkle root over oracle_payloads.fingerprint (ascending id, 32B per row). Leaf bytes are computed by oracle_payload_repository_sa at ingest time as BLAKE3(canonicalize_for_signing(payload)).

§1 · What This Document Answers

"Does the combined_root the platform publishes truly correspond to the current database — or has it silently modified some ledger / trust / contract entry?" Recomputation flow: fetch the published root + fetch all leaf sets → recompute locally using the same rules → compare. Any mismatch is evidence of tampering.

§2 · Three-Source + Combined Root Definition

combined_root = BLAKE3( "BAN-TRANSPARENCY-v1"
                       || merkle_root(ledger_leaves)
                       || merkle_root(trust_leaves)
                       || merkle_root(contract_leaves) )

Where merkle_root(·) is an RFC 6962 §2.1 binary Merkle Hash Tree with SHA-256 replaced by BLAKE3-256:

MTH({})         = HASH("")
MTH({d})        = HASH(0x00 || d)
MTH({d_1..d_n}) = HASH(0x01 || MTH(left) || MTH(right))
                  where k = largest power of 2 ≤ (n-1)
                  left = d_1..d_k, right = d_{k+1}..d_n

The domain separators 0x00 / 0x01 prevent leaf/node second-preimage attacks.

§3 · Leaf Construction Rules for the Three Sources

Source Leaf Bytes (32B) Order
ledger_entries LedgerEntry.fingerprint Ascending by internal id
trust_events BLAKE3(canonical_json({id,user_id,delta,reason,…})) Ascending by id
contracts BLAKE3(canonical_json({id,outcome_id,bettor_id,…})) Lexicographic order by ContractId string

⚠️ The .bin endpoints provided by the platform already return pre-processed 32B leaves (i.e., LedgerEntry.fingerprint, BLAKE3(canonical_json(trust)), BLAKE3(canonical_json(contract))). Third parties do not need to perform canonical_json themselves — simply pass each 32B chunk as a leaf into merkle_root.

§4 · Fetching Data

# current root + metadata (same-origin JSON)
curl -fsSL https://betonanything.net/transparency.json -o digest.json

# three source leaf sets (raw binary, 32B per leaf, no separator)
curl -fsSL "https://betonanything.net/api/transparency/ledger-leaves.bin?from=0&limit=10000" -o ledger.bin
curl -fsSL "https://betonanything.net/api/transparency/trust-leaves.bin?from=0&limit=10000"  -o trust.bin

# contracts use cursor pagination — loop X-Beton-Next-Cursor until empty:
: > contract.bin
cursor=""
while :; do
  hdr=$(mktemp)
  curl -fsSL -D "$hdr" \
       "https://betonanything.net/api/transparency/contract-leaves.bin?limit=10000&after=$cursor" \
       >> contract.bin
  cursor=$(awk -F': ' '/^X-Beton-Next-Cursor:/{gsub(/\r/,"");print $2}' "$hdr")
  rm "$hdr"
  [ -z "$cursor" ] && break
done

Also loop ?from= for ledger / trust until the response is 0 bytes (leaf count = Content-Length / 32).

📌 The ETag header equals combined_root. If the database receives new writes mid-fetch, the ETag changes. To ensure a consistent snapshot: first fetch digest.json to lock the target combined_root; then when fetching each .bin, send If-None-Match: <combined_root> — if you receive a 200 but the ETag differs, that page came from an updated snapshot; start over.

§5 · Local Recomputation Script

Requires only blake3 (pip install blake3):

#!/usr/bin/env python3
"""verify_digest.py — offline combined_root recomputation."""
import json, sys, argparse
from pathlib import Path
from blake3 import blake3

DOMAIN_TAG  = b"BAN-TRANSPARENCY-v1"
LEAF_PREFIX = b"\x00"
NODE_PREFIX = b"\x01"

def h(b: bytes) -> bytes:
    return blake3(b).digest()

def merkle_root(leaves: list[bytes]) -> bytes:
    if not leaves:
        return h(b"")
    if len(leaves) == 1:
        return h(LEAF_PREFIX + leaves[0])
    k = 1 << ((len(leaves) - 1).bit_length() - 1)
    return h(NODE_PREFIX + merkle_root(leaves[:k]) + merkle_root(leaves[k:]))

def chunks_of_32(data: bytes) -> list[bytes]:
    if len(data) % 32 != 0:
        sys.exit(f"leaf set length {len(data)} not a multiple of 32 — truncated?")
    return [data[i:i + 32] for i in range(0, len(data), 32)]

def main() -> int:
    ap = argparse.ArgumentParser()
    ap.add_argument("--digest",   required=True)
    ap.add_argument("--ledger",   required=True)
    ap.add_argument("--trust",    required=True)
    ap.add_argument("--contract", required=True)
    args = ap.parse_args()

    digest        = json.loads(Path(args.digest).read_text())
    ledger_root   = merkle_root(chunks_of_32(Path(args.ledger).read_bytes()))
    trust_root    = merkle_root(chunks_of_32(Path(args.trust).read_bytes()))
    contract_root = merkle_root(chunks_of_32(Path(args.contract).read_bytes()))
    combined      = h(DOMAIN_TAG + ledger_root + trust_root + contract_root)

    expected = digest["combined_root"]
    actual   = combined.hex()
    ok = actual == expected
    print(f"ledger_root   {ledger_root.hex()}  expect {digest['ledger_root']}")
    print(f"trust_root    {trust_root.hex()}  expect {digest['trust_root']}")
    print(f"contract_root {contract_root.hex()}  expect {digest['contract_root']}")
    print(f"combined_root {actual}  expect {expected}")
    print("OK" if ok else "MISMATCH — platform ledger inconsistent with published root")
    return 0 if ok else 1

if __name__ == "__main__":
    raise SystemExit(main())
python verify_digest.py \
    --digest   digest.json \
    --ledger   ledger.bin \
    --trust    trust.bin \
    --contract contract.bin

Exit code 0 = verification passed; 1 = mismatch.

§6 · Possible Causes of a Mismatch (by likelihood)

Cause Debugging
Data fetch time differs from digest time Re-fetch the digest and all .bin files, ensuring all response ETags equal digest.combined_root
A page was missed during pagination Check Content-Length / 32 = X-Beton-Count; loop X-Beton-Next-Cursor for contracts until empty
Wrong local BLAKE3 version Use the official pip install blake3 (based on the Rust reference implementation)
Platform tampered with ledger Publish digest.json + the three .bin files + the script output as evidence

§7 · Watchdog Automation Suggestion

*/10 * * * * /opt/betonanything-watch/poll.sh   # every 10 minutes

poll.sh fetches the digest, recomputes, and pushes an alert (Slack / Matrix / email) on mismatch. Also archive each digest.json locally to build a third-party time series — any future retroactive ledger alteration can be disproved using the local archive.

§8 · Protocol Version

The BAN-TRANSPARENCY-v1 literal embeds the version number. If the platform adds a fourth source in the future (e.g., dpm_positions), it will upgrade the tag to BAN-TRANSPARENCY-v2, and this script must update DOMAIN_TAG accordingly. Version changes are explicitly marked in the digest.schema field (from betonanything.transparency.v1 to …v2).