Transparency Offline Verification Guide
For third-party auditors, watchdogs, and users who care about ledger integrity. Requires only Python 3.11+ and the blake3 package — no source code needed.
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).