#!/usr/bin/env python3
"""verify_anchor.py — prove one BlueFox checkpoint root is in the Bitcoin chain.

Download it, run it, edit nothing:

    curl -sS -O https://api.bluefoxedge.ai/verify_anchor.py
    curl -sS https://api.bluefoxedge.ai/verify_anchor.py.sha256 | shasum -a 256 -c -
    curl -sS "https://api.bluefoxedge.ai/v1/transparency/anchor\
?log_id=reliance/v1&tree_size=<the size your proof was served against>" \\
      | python3 verify_anchor.py

No checkpoint of your own in mind? This one is real, published and confirmed,
and the whole thing runs with no account, nothing installed, and no key of ours:

    curl -sS "https://api.bluefoxedge.ai/v1/transparency/anchor\
?log_id=reliance/v1&tree_size=4142" | python3 verify_anchor.py

WHY THIS FILE EXISTS. Its sibling, verify_inclusion.py, ends every run on the
log's own signature and says so out loud — either "cosignatures is empty, so
trust in the log key is trust in BlueFox", or, since the log began carrying an
independent witness cosignature on 2026-09-19, that it counted the witness
lines and did NOT check them. Every step that script takes is real arithmetic
and the last one lands on our signature. This script takes the other road. It
fetches no key set, checks
no signature, and carries no signature code at all — it asks the Bitcoin chain,
through public APIs nobody here operates, whether the root you name is committed
into a block and how deep that block is buried. If this company vanished
tomorrow and every key it holds were repudiated, this check would still run and
still answer.

Exit 0 means the proof checked out. Any non-zero exit means it did not, and the
reason is one sentence on stderr — never a stack trace.

DEPENDENCIES: none. Python 3.8+ standard library only. There is no optional
third-party path either, because there is no cryptographic signature anywhere in
this check — only SHA-256, which the standard library has always had.

═══════════════════════════════════════════════════════════════════════════════
THE RECIPE — what this script checks, and why each step is here
═══════════════════════════════════════════════════════════════════════════════

1. NO KEY OF OURS, ANYWHERE — AND THAT IS THE ENTIRE POINT. This script never
   fetches the published key set, never resolves a key hint, and never verifies
   an Ed25519 signature. Read it and you will not find that code. The claim it
   makes therefore does not weaken if our signing key is compromised, lost, or
   repudiated: it rests on SHA-256 arithmetic over bytes you can download from
   four places, and on Bitcoin's proof of work, which we have no part in.

   That is a NARROWER claim than the log makes, not a wider one. See item 9.

2. THE ROOT MUST BE YOURS, NOT OURS. Everything below proves a statement about
   one 32-byte Merkle root. If you take that root from the record this script is
   handed, then the strongest sentence available is "the root BlueFox says its
   checkpoint had is in Bitcoin" — and a record is unsigned JSON we serve, so
   that leg is our word. Pass ``--root sha256:<64 lower-case hex>`` with the root
   out of a signed checkpoint note you already hold (or straight out of
   verify_inclusion.py's own output), and this run REFUSES unless the record
   agrees with you byte for byte. Without the flag the run still completes and
   names the gap in its own output rather than letting a green line imply a pin
   that never happened.

   THE OTHER HALF OF THE SAME RULE: which bytes were STAMPED is not a free
   parameter either. For the ROOT class the stamped value is the 32 root bytes
   themselves, and the record's ``preimage_sha256`` is cross-checked against
   them. For the CHECKPOINT_NOTE class the bytes come from you, via
   ``--preimage-file`` — the one input a caller controls entirely — so they are
   bound three ways before a single operation runs: their SHA-256 must equal the
   record's ``preimage_sha256``; the note's OWN root and tree size must match
   the record's; and the note's ORIGIN — which in C2SP is one-to-one with a log,
   so it is the log's identity — must be the log this record describes, pinned
   exactly with ``--origin`` or, absent that, required to end in ``/`` plus the
   record's ``log_id``. The note is parsed byte-exactly and its SIGNATURE IS
   DELIBERATELY NOT CHECKED, because checking it would put our key back in the
   middle of this script: the parse is for BINDING, not for belief. Without all
   four, a proof over some other checkpoint's note — or another log's note whose
   root and size happen to coincide — could exit 0 while this report named your
   root and your log.

3. TWO PROOFS, CHAINED — THE STEP THAT COSTS AN AFTERNOON. Each calendar holds
   TWO proofs for one root, not one spliced file:

     the PENDING proof  carries your root  ->  the calendar's commitment
     the UPGRADED proof carries THAT commitment  ->  a Bitcoin block

   The upgraded proof's operations DO NOT begin at your root. They begin at the
   commitment the pending proof reaches from your root, because the calendar's
   upgrade endpoint is keyed by that commitment. Execute the upgraded proof from
   the raw root and every operation is legal, nothing raises, and you land on a
   garbage value that matches no block on earth — a silent, total, repeatable
   wrong answer. This exact composition error ran here for days against fifty
   good calendar proofs before it was found, so it is written down at the top of
   the file that a stranger reads: START THE SECOND PROOF WHERE THE FIRST ONE
   ENDED. This script fetches both proofs per calendar and refuses a lane whose
   pending proof is missing rather than walking from an unproven start.

4. THE HEIGHT COMES FROM THE PROOF, NOT FROM THE RECORD. A Bitcoin claim inside
   the proof names the block height in its own payload; the record repeats it as
   a convenience. This script reads the height OUT OF THE PROOF BYTES and, if the
   record names one too, requires the record's value to be among them. Where
   several heights are named the SHALLOWEST governs, because confirming on the
   deepest of disagreeing claims would be picking the answer we like best.

5. EXECUTE THE OPERATIONS, AND FAIL CLOSED ON ANYTHING UNKNOWN. An OpenTimestamps
   proof is a tree of operations: append and prepend carry an operand, sha256,
   sha1, reverse and hexlify do not. An unknown OPERATION tag is refused rather
   than stepped over — the walk cannot know whether it carries an operand, and a
   desynchronized walk would read some later stretch of bytes as a claim. An
   unknown CLAIM tag is different: claims are length-delimited, so one is stepped
   over intact and never read as a Bitcoin claim. RIPEMD-160 and KECCAK-256 are
   not executed: neither is guaranteed present in a standard-library build, and a
   branch this script cannot execute is reported unwalkable, never guessed at.

6. THE BYTE ORDER IS THE WHOLE TRAP. The value you land on is a Merkle root in
   Bitcoin's INTERNAL byte order. Every chain API prints Merkle roots REVERSED,
   in display order. Reverse before you compare, or two identical values will
   look like a mismatch forever. Worked pair, published so you can reproduce it
   without fetching anything:

     for the commitment (internal order)
       3d35e98a42e465553bf82e54915c714d9db582f41c5447880612039d695999fd
     every operator reports, for block 429172 (display order)
       fd9959699d0312068847541cf482b59d4d715c91542ef83b5565e4428ae9353d

   Those are the same 32 bytes read in opposite directions. If your own
   implementation compares un-reversed, you now know which turn you missed.

7. ASK SEVERAL OPERATORS AND BELIEVE ONLY AGREEMENT. This script asks four
   independently operated public chain APIs (below) for the block at the named
   height, and requires at least 3 of them to agree BYTE FOR BYTE on both its
   hash and its Merkle root, spanning at least 2 independently written
   implementations. blockstream.info and mempool.space are different companies in
   different countries running the SAME software (Esplora), so a bug in Esplora
   is not two independent mistakes — that is why implementations are counted and
   not just operators. An even split is UNDETERMINED and refuses: choosing
   between equal halves is a guess wearing a verdict's clothes. The four are
   blockstream.info and mempool.space (Esplora), api.blockcypher.com
   (BlockCypher) and Blockchain.com's haskoin-store (Haskoin) — three
   independently written implementations. (Until 2026-09-13 the fourth seat was
   api.bitaps.com, whose raw-header answer this script used to derive and
   self-check; it was demoted after running 9-12 blocks behind the others for
   hours at a time, and the log's own configuration names the same four this
   file pins.)

   These are the same four operators, the same 3-of-4, and the same 2-implementation
   floor the log's own confirmation path uses. This script is not a friendlier
   bar than the one we hold ourselves to; it is the same bar with our key taken
   out of it. Your own numbers, if you keep them, go in with --chain-file, and
   any threshold you lower is named in the output instead of quietly applied.

8. DEPTH, COMPUTED CONSERVATIVELY — FROM THE MAJORITY'S TIP. Depth is
   (tip - height + 1). The tip is the MAJORITY'S: the largest group of operators
   whose tips agree within a small window (6 blocks by default) governs, and the
   tip is the MINIMUM inside that group, so a fast indexer can never overstate
   how deeply buried a block is. An operator whose tip falls outside the
   majority's window is an OUTLIER: it is named on its own line of the report,
   it does not govern the tip, it does not veto the check, and its answer about
   the BLOCK still counts — being eleven blocks behind the tip says nothing
   about a block that is hundreds deep. No group of three within the window is
   UNDETERMINED, and an even split is a tie; both refuse. (Until 2026-09-13 the
   window was a bar over every answer, so one lagging operator could turn a
   confirmed anchor into NOT PROVED for hours; the number did not move, what it
   gates did.) The window exists because blocks propagate and demanding exact
   equality would make UNDETERMINED the normal answer. A block's IDENTITY gets
   no tolerance at all: there is no propagation story that explains two
   operators disagreeing about a block deep enough to confirm, and identity is
   what the whole claim rests on.

   THE REQUIRED DEPTH IS 6 BLOCKS, AND IT IS PINNED IN THIS FILE — deliberately
   not read from our API. It matches the log's own threshold today, and matching
   is not why it is here: a bar this script fetched from us would be a bar we
   could lower under you, which is the dependency this whole file exists to
   remove. So 6 is a number you can read before you run it. The consequence,
   stated rather than buried: if this log ever raises its OWN requirement above
   6, this script's 6 becomes a floor rather than a match, and --depth N is how
   you hold us to the higher one.

9. WHAT SUCCESS MEANS — AND WHAT IT DOES NOT. A passing run proves exactly this:
   the proof chain, executed from the root you named, lands on the Merkle root of
   a specific Bitcoin block, and that block is real and buried deep enough by the
   agreement of independent operators. Therefore your root existed no later than
   that block was mined, and no key of ours is load-bearing in that sentence.

   It does NOT prove that any receipt is in the tree with that root. That is the
   inclusion question, it has its own published checker, and the two compose:

     your receipt --[verify_inclusion.py]--> a tree root --[this script]--> Bitcoin

   Run only this one and you have proved that a number is old. Run both and you
   have a receipt bound to the chain with nothing of ours in the middle except
   the one inclusion signature, which the other script grades on its own line.
   This script says both of these things out loud, every run, on separate lines.

10. WHEN, READ NARROWLY. This run places your root BEFORE a named block, which is
   the strongest thing a Bitcoin anchor says. It does not tell you the wall-clock
   instant, and this script deliberately reads no block timestamp: a header's
   time field is miner-set and only loosely bounded, so turning "block 963758"
   into a date is a separate claim with its own error bars. Look the height up
   wherever you like. What was proved here is the ordering.
"""
from __future__ import annotations

import argparse
import base64
import binascii
import hashlib
import http.client
import json
import re
import socket
import sys
import urllib.error
import urllib.parse
import urllib.request

# ── The pins. Every address this script will contact lives in these bytes,
#    readable before you run it. Nothing here is ever taken from the record.
API_BASE = "https://api.bluefoxedge.ai"
PROOF_PATH = "/v1/transparency/anchor/proof"

#: name, implementation family, operator, base URL, response style. FAMILY is the
#: independently-written-implementation grouping, not the company (recipe item 7).
OPERATORS = (
    ("blockstream", "esplora", "Blockstream",
     "https://blockstream.info/api", "esplora"),
    ("mempool", "esplora", "mempool.space",
     "https://mempool.space/api", "esplora"),
    ("blockcypher", "blockcypher", "BlockCypher",
     "https://api.blockcypher.com/v1/btc/main", "blockcypher"),
    ("haskoin", "haskoin", "Blockchain.com",
     "https://api.blockchain.info/haskoin-store/btc", "haskoin"),
)

# The published bar, mirrored from the log's own confirmation path so this script
# is not a softer check wearing the same word.
DEFAULT_QUORUM = 3
DEFAULT_MIN_IMPLEMENTATIONS = 2
DEFAULT_MAX_TIP_SPREAD = 6
DEFAULT_DEPTH = 6
DEFAULT_TIMEOUT = 10.0

DIGEST_RE = re.compile(r"^sha256:[0-9a-f]{64}$")
HEX64_RE = re.compile(r"^[0-9a-f]{64}$")

EXIT_OK = 0
EXIT_REFUSED = 1

# Bounds. Each one refuses loudly; none of them truncates quietly.
MAX_RESPONSE_BYTES = 1 << 18       # a chain API's answer is a few hundred bytes
MAX_PROOF_BYTES = 1 << 20
MAX_MESSAGE_BYTES = 1 << 20        # append/prepend/hexlify GROW the buffer
MAX_VARUINT_BYTES = 9
MAX_TREE_DEPTH = 256
MAX_EXECUTED_OPS = 100_000
MAX_BITCOIN_MATCHES = 8
MAX_PENDING_COMMITMENTS = 64

# ══════════════════════════════════════════════════════════════════════════════
# Every negative outcome leaves through here — one sentence, a non-zero exit, and
# no traceback. There is no bare ``assert`` on a verification branch in this file:
# a stranger who catches a bad proof must be handed a verdict, not a stack trace.
# ══════════════════════════════════════════════════════════════════════════════
def refuse(sentence: str) -> "None":
    sys.stderr.write("NOT PROVED: " + sentence + "\n")
    sys.exit(EXIT_REFUSED)


def present_field(container: dict, key: str, what: str, required: bool) -> object:
    """ABSENT and PRESENT, told apart — because ``.get()`` cannot tell them apart.

    ``dict.get()`` collapses two different facts into one ``None``: the key is
    MISSING, and the key is THERE carrying JSON ``null``. A gate written
    ``if x is not None:`` therefore skips a PRESENT null exactly as it skips an
    absent key — and on a field this script promises to cross-check against the
    signed artifact, a skipped check reads exactly like a passed one.

    PRESENT-and-null REFUSES, always: nothing here reads a null as "absent".
    ABSENT refuses when the field is required, and is lawfully skippable —
    returning ``None`` — only where this script's own recipe above says the
    field is optional.
    """
    if key not in container:
        if required:
            return refuse(
                "there is no %r field here, and this script cross-checks it "
                "against the signed artifact rather than trusting it -- "
                "refused rather than skipped" % key)
        return None
    value = container[key]
    if value is None:
        return refuse(
            "%s is PRESENT and null. A null is not an absent field: this "
            "script cross-checks that value against the signed artifact, so "
            "it is refused rather than skipped -- a skipped check reads "
            "exactly like a passed one" % what)
    return value


class ProofError(ValueError):
    """These bytes are not a well-formed proof — refused, never guessed at."""


class ObservationError(RuntimeError):
    """One operator failed to answer. Recorded; never raised past the quorum."""


class Undetermined(RuntimeError):
    """The operators answered and CONTRADICTED each other, so nothing is claimed.

    Kept distinct from an outage on purpose. Both refuse; only this one means the
    world handed back conflicting evidence, and that is the state a careless
    checker resolves by picking a favourite. Nothing here picks a favourite.
    """


# ══════════════════════════════════════════════════════════════════════════════
# The OpenTimestamps proof format, walked. Framing only, plus the operations the
# standard library can execute. Nothing in this section observes anything.
# ══════════════════════════════════════════════════════════════════════════════
# Each constant below was read off a real artifact rather than recalled, because a
# protocol constant remembered by the same machine that wrote the parser is
# circular evidence. The two claim tags were read from live calendar responses and
# from the published example proofs, where the 8 tag bytes are immediately
# followed by the calendar's own URL (pending) or by a block height consistent
# with the example's age (bitcoin) — both self-confirming reads.
FILE_MAGIC = (b"\x00OpenTimestamps\x00\x00Proof\x00"
              b"\xbf\x89\xe2\xe8\x84\xe8\x92\x94")
FILE_MAJOR_VERSION = 1
PENDING_CLAIM_TAG = binascii.unhexlify(b"83dfe30d2ef90c8e")
BITCOIN_CLAIM_TAG = binascii.unhexlify(b"0588960d73d71901")

CLAIM_MARKER = 0x00
FORK_MARKER = 0xFF
CLAIM_TAG_LEN = 8

OP_SHA1 = 0x02
OP_RIPEMD160 = 0x03
OP_SHA256 = 0x08
OP_KECCAK256 = 0x67
OP_APPEND = 0xF0
OP_PREPEND = 0xF1
OP_REVERSE = 0xF2
OP_HEXLIFY = 0xF3

UNARY_OPS = frozenset({OP_SHA1, OP_RIPEMD160, OP_SHA256, OP_KECCAK256,
                       OP_REVERSE, OP_HEXLIFY})
BINARY_OPS = frozenset({OP_APPEND, OP_PREPEND})
# Item 5: RIPEMD-160 and KECCAK-256 are framing-known and execution-excluded.
EXECUTABLE_OPS = frozenset({OP_APPEND, OP_PREPEND, OP_SHA1, OP_SHA256,
                            OP_REVERSE, OP_HEXLIFY})


class Node:
    """One node of the proof tree: claims made here, plus onward operations."""

    __slots__ = ("claims", "ops")

    def __init__(self) -> None:
        self.claims = []   # list of (tag, payload)
        self.ops = []      # list of (tag, operand_or_None, child)


def _read_byte(buf: bytes, pos: int):
    if pos >= len(buf):
        raise ProofError("truncated proof: expected a byte at offset %d" % pos)
    return buf[pos], pos + 1


def _read_varuint(buf: bytes, pos: int):
    """Base-128 little-endian varuint with a high continuation bit."""
    value = 0
    shift = 0
    consumed = 0
    while True:
        byte, pos = _read_byte(buf, pos)
        consumed += 1
        if consumed > MAX_VARUINT_BYTES:
            raise ProofError(
                "a length field runs longer than %d bytes" % MAX_VARUINT_BYTES)
        value |= (byte & 0x7F) << shift
        if not byte & 0x80:
            return value, pos
        shift += 7


def _read_varbytes(buf: bytes, pos: int):
    length, pos = _read_varuint(buf, pos)
    if length > MAX_PROOF_BYTES:
        raise ProofError("a length field claims %d bytes, over the ceiling"
                         % length)
    end = pos + length
    if end > len(buf):
        raise ProofError(
            "truncated proof: a field wants %d bytes at offset %d and only %d "
            "remain" % (length, pos, len(buf) - pos))
    return buf[pos:end], end


def _read_item(buf: bytes, pos: int, depth: int, node: Node) -> int:
    tag, pos = _read_byte(buf, pos)
    if tag == CLAIM_MARKER:
        end = pos + CLAIM_TAG_LEN
        if end > len(buf):
            raise ProofError(
                "truncated proof: a claim tag at offset %d needs %d bytes and "
                "only %d remain" % (pos, CLAIM_TAG_LEN, len(buf) - pos))
        claim_tag = buf[pos:end]
        payload, pos = _read_varbytes(buf, end)
        node.claims.append((claim_tag, payload))
        return pos
    if tag in BINARY_OPS:
        operand, pos = _read_varbytes(buf, pos)
    elif tag in UNARY_OPS:
        operand = None
    else:
        # Item 5: fail closed. An unknown operation may or may not carry an
        # operand, so stepping over it risks reading later bytes as a claim.
        raise ProofError(
            "unknown operation tag 0x%02x at offset %d — refused rather than "
            "skipped, because skipping it could misread the rest of the proof"
            % (tag, pos - 1))
    child, pos = _parse_at(buf, pos, depth + 1)
    node.ops.append((tag, operand, child))
    return pos


def _parse_at(buf: bytes, pos: int, depth: int):
    if depth > MAX_TREE_DEPTH:
        raise ProofError("the proof nests deeper than %d levels"
                         % MAX_TREE_DEPTH)
    node = Node()
    while True:
        tag, _ = _read_byte(buf, pos)
        # Every item but the last is prefixed with 0xff: "one more sibling".
        if tag != FORK_MARKER:
            return node, _read_item(buf, pos, depth, node)
        pos = _read_item(buf, pos + 1, depth, node)


def parse_proof(raw: bytes):
    """Parse the bytes the proof endpoint serves, or a detached ``.ots`` file.

    Returns ``(node, file_digest_or_None)``. Trailing bytes are an error rather
    than slack: a proof we only half-understand is a proof we must not act on.
    """
    if not raw:
        raise ProofError("empty proof")
    if len(raw) > MAX_PROOF_BYTES:
        raise ProofError("the proof is %d bytes, over the %d ceiling"
                         % (len(raw), MAX_PROOF_BYTES))
    digest = None
    pos = 0
    if raw.startswith(FILE_MAGIC):
        pos = len(FILE_MAGIC)
        major, pos = _read_varuint(raw, pos)
        if major != FILE_MAJOR_VERSION:
            raise ProofError("unsupported proof file version %d" % major)
        hash_op, pos = _read_byte(raw, pos)
        if hash_op != OP_SHA256:
            raise ProofError(
                "this proof file stamps a 0x%02x digest; this lane stamps "
                "SHA-256 only" % hash_op)
        end = pos + 32
        if end > len(raw):
            raise ProofError("truncated proof: the file digest is short")
        digest = raw[pos:end]
        pos = end
    node, pos = _parse_at(raw, pos, 0)
    if pos != len(raw):
        raise ProofError("%d trailing byte(s) after the proof — refused"
                         % (len(raw) - pos))
    return node, digest


def _apply(tag: int, operand, msg: bytes) -> bytes:
    """One operation, refusing any result over ``MAX_MESSAGE_BYTES``.

    The ceiling is not decoration: append, prepend and hexlify all GROW the
    message and hexlify doubles it, so a proof that nests hexlify operations —
    well inside the 1 MB input limit — would expand the buffer exponentially and
    exhaust memory before the depth guard ever fired. A remote calendar controls
    those bytes, so the bound belongs on the operation, not only on the input.
    """
    if tag == OP_APPEND:
        out = msg + (operand or b"")
    elif tag == OP_PREPEND:
        out = (operand or b"") + msg
    elif tag == OP_SHA256:
        return hashlib.sha256(msg).digest()
    elif tag == OP_SHA1:
        return hashlib.sha1(msg).digest()
    elif tag == OP_REVERSE:
        return msg[::-1]
    elif tag == OP_HEXLIFY:
        if len(msg) * 2 > MAX_MESSAGE_BYTES:
            raise ProofError(
                "one operation would expand the message to %d bytes, over the "
                "%d ceiling" % (len(msg) * 2, MAX_MESSAGE_BYTES))
        out = binascii.hexlify(msg)
    else:
        raise ProofError("operation 0x%02x is not executable here" % tag)
    if len(out) > MAX_MESSAGE_BYTES:
        raise ProofError(
            "one operation grew the message to %d bytes, over the %d ceiling"
            % (len(out), MAX_MESSAGE_BYTES))
    return out


def count_reachable_claims(node: Node, depth: int = 0) -> int:
    """How many claims are reachable without computing a single commitment.

    Counted structurally so the honest total is known BEFORE any collection
    limit applies — a truncated view must never wear the costume of a fact.
    """
    if depth > MAX_TREE_DEPTH:
        raise ProofError("the proof nests deeper than %d levels"
                         % MAX_TREE_DEPTH)
    total = len(node.claims)
    for tag, _operand, child in node.ops:
        if tag in EXECUTABLE_OPS:
            total += count_reachable_claims(child, depth + 1)
    return total


def pending_commitments(node: Node, start: bytes) -> "list[bytes]":
    """Every value this proof, executed from ``start``, submits to a calendar.

    These are the messages the UPGRADED proof's operations begin at — recipe
    item 3. Collected rather than tested one at a time because the caller needs
    all of them; the collection is capped, and the cap is REPORTED by the caller
    when ``count_reachable_claims`` says the proof carries more.
    """
    out = []
    _walk_pending(node, start, out, 0, [MAX_EXECUTED_OPS])
    return out


def _walk_pending(node: Node, msg: bytes, out, depth: int, budget) -> None:
    if depth > MAX_TREE_DEPTH:
        raise ProofError("the proof nests deeper than %d levels"
                         % MAX_TREE_DEPTH)
    for tag, _payload in node.claims:
        if len(out) >= MAX_PENDING_COMMITMENTS:
            return
        if tag == PENDING_CLAIM_TAG and msg not in out:
            out.append(msg)
    for tag, operand, child in node.ops:
        if len(out) >= MAX_PENDING_COMMITMENTS:
            return
        if tag not in EXECUTABLE_OPS:
            continue
        if budget[0] <= 0:
            raise ProofError(
                "the proof demands more than %d operations to walk — refused "
                "rather than computed" % MAX_EXECUTED_OPS)
        budget[0] -= 1
        _walk_pending(child, _apply(tag, operand, msg), out, depth + 1, budget)


def bitcoin_heights(node: Node, depth: int = 0) -> "list[int]":
    """Every block height this proof NAMES, structurally — no execution at all.

    Named, not observed. Item 4: the height is read out of the proof, so the
    record cannot move it.
    """
    if depth > MAX_TREE_DEPTH:
        raise ProofError("the proof nests deeper than %d levels"
                         % MAX_TREE_DEPTH)
    found = []
    for tag, payload in node.claims:
        if tag != BITCOIN_CLAIM_TAG:
            continue
        try:
            height, pos = _read_varuint(payload, 0)
        except ProofError:
            continue
        if pos == len(payload) and height not in found:
            found.append(height)
    for _tag, _operand, child in node.ops:
        for height in bitcoin_heights(child, depth + 1):
            if height not in found:
                found.append(height)
    return found


def merkle_roots_at(node: Node, start: bytes, height: int) -> "list[str]":
    """Every Merkle root this proof commits ``start`` to AT ``height``, display order.

    A TARGETED walk: each commitment is tested and dropped as the walk passes it,
    so one buffer is live at a time and there is no window for a claim to hide
    behind. Empty is itself the answer — this proof commits ``start`` to nothing
    at that height, which is an unverifiable claim rather than a confirmable one.

    The bounds that remain can only REFUSE the whole proof loudly, never hide a
    claim: the depth guard, the per-operation size ceiling, the total operation
    budget, and — since the R9-1 gate round, where a review bot caught this
    function returning a partial set while its own docstring promised otherwise —
    the distinct-root ceiling, which now raises instead of truncating.
    """
    out = []
    _walk_bitcoin(node, start, height, out, 0, [MAX_EXECUTED_OPS])
    return out


def _walk_bitcoin(node: Node, msg: bytes, height: int, out, depth: int,
                  budget) -> None:
    if depth > MAX_TREE_DEPTH:
        raise ProofError("the proof nests deeper than %d levels"
                         % MAX_TREE_DEPTH)
    for tag, payload in node.claims:
        if tag != BITCOIN_CLAIM_TAG:
            continue
        try:
            named, pos = _read_varuint(payload, 0)
        except ProofError:
            continue
        if pos != len(payload) or named != height:
            continue
        if len(msg) != 32:
            # Not a Merkle root. Skipped rather than raised: the rest of the
            # proof may still carry a well-formed claim, and this one proves
            # nothing either way.
            continue
        # Item 6: internal order on the wire, display order in every explorer.
        root = msg[::-1].hex()
        if root not in out:
            if len(out) >= MAX_BITCOIN_MATCHES:
                # LOUD, never a partial set. Returning early here would drop a
                # matching claim that sits later in the proof and refuse a good
                # anchor while claiming completeness — a silent truncation
                # wearing a verdict's clothes.
                raise ProofError(
                    "the proof commits your root to more than %d distinct "
                    "values at block %d — refused rather than compared against "
                    "a truncated set of them" % (MAX_BITCOIN_MATCHES, height))
            out.append(root)
    for tag, operand, child in node.ops:
        if tag not in EXECUTABLE_OPS:
            continue
        if budget[0] <= 0:
            raise ProofError(
                "the proof demands more than %d operations to walk — refused "
                "rather than computed" % MAX_EXECUTED_OPS)
        budget[0] -= 1
        _walk_bitcoin(child, _apply(tag, operand, msg), height, out, depth + 1,
                      budget)


# ══════════════════════════════════════════════════════════════════════════════
# HTTP. Bounded reads, no redirects, and an opener built explicitly — the default
# one installs file:// and ftp:// handlers and follows redirects anywhere.
# ══════════════════════════════════════════════════════════════════════════════
_USER_AGENT = "bluefox-verify-anchor/1.0 (standard library only)"


class _NoRedirects(urllib.request.HTTPRedirectHandler):
    def redirect_request(self, req, fp, code, msg, headers, newurl):
        return None


_OPENER = urllib.request.build_opener(
    urllib.request.HTTPHandler, urllib.request.HTTPSHandler, _NoRedirects)


def _fetch(url: str, timeout: float, accept: str) -> bytes:
    request = urllib.request.Request(url, method="GET")
    request.add_header("Accept", accept)
    request.add_header("User-Agent", _USER_AGENT)
    try:
        with _OPENER.open(request, timeout=timeout) as response:
            body = response.read(MAX_RESPONSE_BYTES + 1)
    except urllib.error.HTTPError as exc:
        raise ObservationError("HTTP %d" % exc.code)
    except socket.timeout:
        raise ObservationError("no response in %ss" % timeout)
    except (urllib.error.URLError, OSError, http.client.HTTPException) as exc:
        # http.client.HTTPException does NOT derive from OSError, so without it
        # in this tuple one truncated chunked response escapes here and aborts
        # the whole run — a quorum that would still hold refuses instead. Per-
        # operator isolation is the point of asking four of them.
        if isinstance(getattr(exc, "reason", None), socket.timeout):
            raise ObservationError("no response in %ss" % timeout)
        raise ObservationError("%s: %s" % (type(exc).__name__, exc))
    if len(body) > MAX_RESPONSE_BYTES:
        raise ObservationError("response over %d bytes" % MAX_RESPONSE_BYTES)
    return body


def _fetch_text(url: str, timeout: float) -> str:
    try:
        return _fetch(url, timeout, "application/json").decode("utf-8")
    except UnicodeDecodeError:
        raise ObservationError("response was not valid UTF-8")


def _as_height(raw: str) -> int:
    """A bare-integer body, parsed strictly — a body we cannot read as a height
    is a failure, never a zero."""
    try:
        height = int(raw.strip())
    except ValueError:
        raise ObservationError("not a height: %r" % raw.strip()[:60])
    if height <= 0:
        raise ObservationError("implausible tip height %d" % height)
    return height


def _as_object(raw: str) -> dict:
    try:
        parsed = json.loads(raw)
    except ValueError:
        raise ObservationError("response was not JSON")
    if not isinstance(parsed, dict):
        raise ObservationError("expected a JSON object")
    return parsed


def _as_array(raw: str) -> list:
    try:
        parsed = json.loads(raw)
    except ValueError:
        raise ObservationError("response was not JSON")
    if not isinstance(parsed, list):
        raise ObservationError("expected a JSON array")
    return parsed


def _hex64(value, what: str) -> str:
    """A 32-byte hex string, lower-cased.

    Case is normalised HERE and only here, so one operator printing upper-case
    hex cannot read as disagreement later. Anything else is a failure.
    """
    if not isinstance(value, str):
        raise ObservationError("%s was not a string" % what)
    text = value.strip().lower()
    if not HEX64_RE.match(text):
        raise ObservationError("%s is not 32-byte hex" % what)
    return text


def fetch_tip(operator, timeout: float) -> int:
    _name, _family, _who, base, style = operator
    if style == "esplora":
        return _as_height(_fetch_text(base + "/blocks/tip/height", timeout))
    if style == "blockcypher":
        return _as_height(str(_as_object(_fetch_text(base, timeout)).get("height")))
    if style == "haskoin":
        best = _as_object(_fetch_text(base + "/block/best?notx=true", timeout))
        if best.get("mainchain") is not True:
            raise ObservationError("the best block is not flagged mainchain")
        return _as_height(str(best.get("height")))
    raise ObservationError("unknown response style %r" % style)


def _haskoin_block(operator, height: int, timeout: float):
    """This operator lists EVERY block it knows at a height, orphans included,
    each flagged ``mainchain``. Exactly one main-chain block is an answer; none
    is a failure to describe the block (a height above its tip comes back as an
    empty list with HTTP 200), and two is an answer that contradicts itself.
    ``?notx=true`` is pinned into the URL: it is what keeps a block to a few
    hundred bytes instead of the whole transaction list.
    """
    _name, _family, _who, base, _style = operator
    entries = _as_array(_fetch_text("%s/block/height/%d?notx=true" % (base, height),
                                    timeout))
    main = [e for e in entries if isinstance(e, dict) and e.get("mainchain") is True]
    if not main:
        raise ObservationError(
            "no main-chain block at height %d in this operator's answer (%d "
            "block(s) listed)" % (height, len(entries)))
    if len(main) > 1:
        raise ObservationError(
            "%d main-chain blocks listed at height %d -- an answer that "
            "contradicts itself, refused rather than chosen from"
            % (len(main), height))
    block = main[0]
    if block.get("height") != height:
        raise ObservationError("asked for height %d, the answer describes %r"
                               % (height, block.get("height")))
    return _hex64(block.get("hash"), "hash"), _hex64(block.get("merkle"), "merkle")


def fetch_block(operator, height: int, timeout: float):
    """One operator's ``(block_hash, merkle_root)`` for the block at ``height``.

    Every style cross-checks the height the operator ECHOES against the height we
    asked for. Without it an off-by-one or a redirected index would be read as a
    fact about the wrong block — and since the whole point is depth, the wrong
    block is precisely the mistake that must not pass quietly.
    """
    _name, _family, _who, base, style = operator
    if style == "esplora":
        block_hash = _hex64(_fetch_text("%s/block-height/%d" % (base, height),
                                        timeout).strip(), "block hash")
        block = _as_object(_fetch_text("%s/block/%s" % (base, block_hash), timeout))
        observed = block.get("height")
        merkle = _hex64(block.get("merkle_root"), "merkle_root")
        block_id = _hex64(block.get("id"), "id")
        if block_id != block_hash:
            raise ObservationError("asked for block %s, got %s"
                                   % (block_hash, block_id))
    elif style == "blockcypher":
        block = _as_object(_fetch_text("%s/blocks/%d" % (base, height), timeout))
        observed = block.get("height")
        merkle = _hex64(block.get("mrkl_root"), "mrkl_root")
        block_hash = _hex64(block.get("hash"), "hash")
    elif style == "haskoin":
        return _haskoin_block(operator, height, timeout)
    else:
        raise ObservationError("unknown response style %r" % style)
    if observed != height:
        raise ObservationError("asked for height %d, the answer describes %r"
                              % (height, observed))
    return block_hash, merkle


# ══════════════════════════════════════════════════════════════════════════════
# The quorum. Recipe items 7 and 8 — and the third state that keeps it honest.
# ══════════════════════════════════════════════════════════════════════════════
class Chain:
    """The chain as several operators describe it, believed only when they agree.

    ``offline`` holds observations a caller supplied with --chain-file; when it is
    present nothing here touches the network, and the SAME arithmetic runs over
    those numbers. A checker whose offline mode skips the quorum would be proving
    a different thing than its online mode does.
    """

    def __init__(self, operators, quorum, min_families, max_spread, timeout,
                 offline=None):
        self.operators = operators
        self.quorum = quorum
        self.min_families = min_families
        self.max_spread = max_spread
        self.timeout = timeout
        self.offline = offline
        self.notes = []
        if quorum < 1:
            raise ValueError("a quorum of fewer than 1 operator is not a quorum")
        if len(operators) < quorum:
            raise ValueError(
                "%d chain API(s) selected but the quorum needs %d to agree, so "
                "this run could never confirm anything"
                % (len(operators), quorum))
        families = {op[1] for op in operators}
        if len(families) < min_families:
            raise ValueError(
                "the selected APIs span %d implementation family/families and "
                "%d are required" % (len(families), min_families))

    def _families(self, names):
        return sorted({op[1] for op in self.operators if op[0] in names})

    def _why(self, failures):
        if not failures:
            return "no failures recorded"
        return "; ".join("%s: %s" % (n, w) for n, w in sorted(failures.items()))

    @staticmethod
    def _tip_clusters(heights, window):
        """Every maximal group of operators whose tips lie within ``window``
        blocks of one another, lowest group first. Three at N and one at N-11
        with a window of 6 yields {N-11} and {N, N, N}; the larger is the
        majority. Overlapping equally large groups resolve to the LOWER one —
        the reading that can only understate depth."""
        ordered = sorted(heights.items(), key=lambda kv: (kv[1], kv[0]))
        clusters = []
        for i, (_, low) in enumerate(ordered):
            members = dict((n, h) for n, h in ordered[i:] if h - low <= window)
            if clusters and set(members) <= set(clusters[-1]):
                continue
            clusters.append(members)
        return clusters

    def tip_height(self):
        """The MAJORITY'S conservative view of the tip (recipe item 8): the
        largest group of operators agreeing within the window governs, its
        MINIMUM height is the tip, and an operator outside the group is an
        outlier -- named, not counted, not a veto."""
        heights = {}
        failures = {}
        if self.offline is not None:
            supplied = self.offline.get("tips") or {}
            for op in self.operators:
                if op[0] in supplied:
                    heights[op[0]] = _as_height(str(supplied[op[0]]))
                else:
                    failures[op[0]] = "no tip supplied in --chain-file"
        else:
            for op in self.operators:
                try:
                    heights[op[0]] = fetch_tip(op, self.timeout)
                except ObservationError as exc:
                    failures[op[0]] = str(exc)
        if len(heights) < self.quorum:
            raise ObservationError(
                "only %d of %d chain API(s) reported a tip and the quorum needs "
                "%d (%s)" % (len(heights), len(self.operators), self.quorum,
                             self._why(failures)))
        answers = ", ".join("%s=%d" % kv for kv in sorted(heights.items()))
        clusters = self._tip_clusters(heights, self.max_spread)
        largest = max(len(c) for c in clusters)
        leading = [c for c in clusters if len(c) == largest]
        if any(set(a).isdisjoint(b) for a in leading for b in leading if a is not b):
            raise Undetermined(
                "the chain APIs split evenly about the tip, with no group larger "
                "than another (window %d blocks): %s -- UNDETERMINED, because "
                "choosing between equal halves would be a guess"
                % (self.max_spread, answers))
        majority = min(leading, key=lambda c: min(c.values()))
        if len(majority) < self.quorum:
            raise Undetermined(
                "no %d of the %d chain API(s) that answered agree about the tip "
                "within %d blocks: %s -- the tip is UNDETERMINED and nothing is "
                "claimed" % (self.quorum, len(heights), self.max_spread, answers))
        families = self._families(majority)
        if len(families) < self.min_families:
            raise Undetermined(
                "the %d operator(s) agreeing about the tip span %d implementation "
                "family/families and %d are required -- the answers are not "
                "independent enough to compute depth against"
                % (len(majority), len(families), self.min_families))
        agreed = min(majority.values())
        outliers = dict((n, h) for n, h in sorted(heights.items()) if n not in majority)
        self.notes.append({
            "question": "tip_height", "agreed": agreed,
            "agreeing": sorted(majority), "families": families,
            "answers": dict(heights), "failures": failures, "outliers": outliers})
        return agreed

    def block_at(self, height: int):
        """The quorum's byte-exact view of one block. No tolerance at all here."""
        seen = {}
        failures = {}
        if self.offline is not None:
            supplied = (self.offline.get("blocks") or {}).get(str(height)) or {}
            for op in self.operators:
                record = supplied.get(op[0])
                if not isinstance(record, dict):
                    failures[op[0]] = ("no observation of block %d supplied in "
                                       "--chain-file" % height)
                    continue
                try:
                    seen[op[0]] = (_hex64(record.get("block_hash"), "block_hash"),
                                   _hex64(record.get("merkle_root"), "merkle_root"))
                except ObservationError as exc:
                    failures[op[0]] = str(exc)
        else:
            for op in self.operators:
                try:
                    seen[op[0]] = fetch_block(op, height, self.timeout)
                except ObservationError as exc:
                    failures[op[0]] = str(exc)
        if len(seen) < self.quorum:
            raise ObservationError(
                "only %d of %d chain API(s) could describe block %d and the "
                "quorum needs %d (%s)"
                % (len(seen), len(self.operators), height, self.quorum,
                   self._why(failures)))
        groups = {}
        for name, answer in seen.items():
            groups.setdefault(answer, []).append(name)
        answer, agreeing = max(groups.items(), key=lambda kv: len(kv[1]))
        # A TIE distinguishes nothing, so nothing may be picked out of it. With
        # the published 3-of-4 the quorum check below already refuses every tie;
        # this makes the refusal true at ANY threshold, so lowering one cannot
        # quietly buy a coin flip.
        if sum(1 for names in groups.values() if len(names) == len(agreeing)) > 1:
            raise Undetermined(
                "the observers of block %d split evenly with no group larger "
                "than another — UNDETERMINED, because choosing between equal "
                "halves would be a guess" % height)
        if len(agreeing) < self.quorum:
            raise Undetermined(
                "the chain APIs do not agree on the identity of block %d and %d "
                "must: %s — UNDETERMINED, and nothing is claimed on a block "
                "whose observers contradict each other"
                % (height, self.quorum,
                   "; ".join("%s=%s/%s" % (n, v[0], v[1])
                             for n, v in sorted(seen.items()))))
        families = self._families(agreeing)
        if len(families) < self.min_families:
            raise Undetermined(
                "the operators agreeing about block %d span %d implementation "
                "family/families and %d are required — not independent enough"
                % (height, len(families), self.min_families))
        self.notes.append({
            "question": "block_at(%d)" % height, "agreed": "%s/%s" % answer,
            "agreeing": sorted(agreeing), "families": families,
            "answers": {n: "%s/%s" % v for n, v in seen.items()},
            "failures": failures})
        return answer[0], answer[1], sorted(agreeing), families


# ══════════════════════════════════════════════════════════════════════════════
# Input.
# ══════════════════════════════════════════════════════════════════════════════
def read_record(path) -> dict:
    if path:
        try:
            with open(path, "rb") as handle:
                raw = handle.read()
        except OSError as exc:
            return refuse("the record file %s could not be read (%s)"
                          % (path, exc))
    else:
        raw = sys.stdin.buffer.read()
    if not raw.strip():
        return refuse(
            "no anchor record was supplied -- pipe the /v1/transparency/anchor "
            "response in on stdin, or pass --file")
    try:
        parsed = json.loads(raw.decode("utf-8"))
    except (UnicodeDecodeError, ValueError) as exc:
        return refuse("the record is not valid JSON (%s)" % exc)
    if not isinstance(parsed, dict):
        return refuse("the record is not a JSON object")
    # The service wraps its answers in {"data": ..., "meta": ...}; a bare data
    # object is accepted too, so a holder who saved either shape can run this
    # unedited.
    inner = parsed.get("data")
    if isinstance(inner, dict) and "target_root" in inner:
        return inner
    if "target_root" in parsed:
        return parsed
    if isinstance(parsed.get("error"), dict) or "type" in parsed:
        detail = parsed.get("detail") or parsed.get("title") or "no detail given"
        return refuse("this is a refusal from the service, not an anchor record "
                      "(%s) -- there is nothing to check" % detail)
    return refuse(
        "the JSON carries no 'target_root' field, so it is not an anchor record "
        "from /v1/transparency/anchor")


def read_bytes_file(path: str, what: str) -> bytes:
    try:
        with open(path, "rb") as handle:
            return handle.read()
    except OSError as exc:
        return refuse("the %s file %s could not be read (%s)" % (what, path, exc))


def read_chain_file(path: str) -> dict:
    raw = read_bytes_file(path, "chain observation")
    try:
        parsed = json.loads(raw.decode("utf-8"))
    except (UnicodeDecodeError, ValueError) as exc:
        return refuse("the chain observation file is not valid JSON (%s)" % exc)
    if not isinstance(parsed, dict):
        return refuse("the chain observation file is not a JSON object")
    return parsed


def proof_url(api_base: str, log_id: str, tree_size: int, calendar: str,
              kind: str) -> str:
    query = urllib.parse.urlencode({
        "log_id": log_id, "tree_size": tree_size, "calendar": calendar,
        "kind": kind})
    return "%s%s?%s" % (api_base.rstrip("/"), PROOF_PATH, query)


# ══════════════════════════════════════════════════════════════════════════════
# The check.
# ══════════════════════════════════════════════════════════════════════════════
def note_origin_root_and_size(raw: bytes) -> "tuple[str, str, int]":
    """The root and tree size inside a C2SP signed note, read byte-exactly.

    NO SIGNATURE IS CHECKED HERE, and that is deliberate: checking it would put
    our key back in the middle of this script. What the parse is for is BINDING —
    the note you hand over must be a note about the checkpoint this record
    describes, so an anchor proof over some other note cannot be reported as this
    root's. The body is at least three newline-terminated lines: origin, decimal
    tree size, base64 of the 32 root bytes.
    """
    try:
        text = raw.decode("utf-8")
    except UnicodeDecodeError:
        return refuse("the note bytes you supplied are not valid UTF-8, so they "
                      "are not a checkpoint note")
    body = text.split("\n\n", 1)[0]
    lines = body.split("\n")
    if len(lines) < 3:
        return refuse(
            "the note bytes you supplied carry fewer than the three lines a "
            "checkpoint note's body must have (origin, tree size, root)")
    try:
        size = int(lines[1])
    except ValueError:
        return refuse("the second line of the note you supplied is not a decimal "
                      "tree size (got %r)" % lines[1][:40])
    root = b64_bytes(lines[2], "the note's root line")
    if len(root) != 32:
        return refuse(
            "the note you supplied carries a %d-byte root where 32 are required"
            % len(root))
    if not lines[0]:
        return refuse("the note you supplied has an empty origin line, and the "
                      "origin is the log's identity")
    return lines[0], "sha256:" + root.hex(), size


def b64_bytes(text: str, what: str) -> bytes:
    try:
        return base64.b64decode(text, validate=True)
    except (binascii.Error, ValueError):
        return refuse("%s is not valid base64, so the note cannot be read" % what)


def stamped_bytes(record: dict, preimage_path, log_id: str,
                  origin_pin) -> "tuple[bytes, str]":
    """The exact digest a calendar was handed, and a sentence naming it.

    Recipe item 2's other half: WHICH bytes were stamped is a property of the
    record's own class, and a class this script cannot reconstruct is refused
    rather than approximated.

    Both classes are cross-checked against the record's own ``preimage_sha256``
    when it is present — the log's §5.5 rule, applied from the outside. It is the
    one field on this record that is a hash of something, so it is the one field
    that can be checked rather than believed.
    """
    target_root = record.get("target_root")
    if not isinstance(target_root, str) or not DIGEST_RE.match(target_root):
        return refuse(
            "the record's target_root is not a 'sha256:' + 64 lower-case hex "
            "digest, so there is nothing this script can safely stamp against "
            "-- upper-case hex and short digests are rejected, never normalised "
            "(got %r)" % (target_root,))
    # F-7: PRESENT-and-null refuses here rather than reading as absent. The
    # field stays lawfully OPTIONAL-WHEN-ABSENT, exactly as this function's
    # docstring says ("when it is present") -- what changes is that a null no
    # longer impersonates absence and silently disarms the cross-check below.
    declared = present_field(record, "preimage_sha256",
                             "this record's preimage_sha256", False)
    kind = record.get("anchored_preimage")

    if kind == "root":
        digest = bytes.fromhex(target_root[len("sha256:"):])
        # The ROOT class stamps the 32 root bytes THEMSELVES (they already are a
        # digest), and preimage_sha256 is a cross-check OVER them -- not the
        # stamped value. Measured against the live lane, not assumed.
        if declared is not None:
            # PRESENT means CHECKED. Skipping a malformed value would let
            # corrupted metadata pass while this report implies a cross-check
            # that never ran — the silence is the defect, not the malformation.
            if not isinstance(declared, str) or not DIGEST_RE.match(declared):
                return refuse(
                    "this record carries a preimage_sha256 that is not a "
                    "'sha256:' + 64 lower-case hex digest (%r), so the "
                    "cross-check cannot run -- refused rather than skipped, "
                    "because a skipped check reads exactly like a passed one"
                    % (declared,))
            recomputed = "sha256:" + hashlib.sha256(digest).hexdigest()
            if recomputed != declared:
                return refuse(
                    "this record's preimage_sha256 is %s and the SHA-256 of the "
                    "root it names is %s -- refused, because the record "
                    "disagrees with itself about what was stamped"
                    % (declared, recomputed))
        return digest, "the 32 raw root bytes, un-nonced"

    if kind == "checkpoint_note":
        if not preimage_path:
            return refuse(
                "this anchor stamps the checkpoint NOTE rather than the raw root "
                "bytes, and the note is not in this record -- pass "
                "--preimage-file with the exact note bytes you hold, or this "
                "run would be guessing at what was stamped")
        raw = read_bytes_file(preimage_path, "stamped pre-image")
        # THREE bindings, because bytes handed over on the command line are the
        # one input a caller controls entirely, and an unbound pre-image would
        # let a proof over note B exit 0 while this report names root A.
        note_digest = hashlib.sha256(raw).digest()
        if not isinstance(declared, str) or not DIGEST_RE.match(declared):
            return refuse(
                "this record stamps the checkpoint note but carries no usable "
                "preimage_sha256, so the note bytes you supplied cannot be bound "
                "to it -- refused rather than walked from unbound bytes")
        if "sha256:" + note_digest.hex() != declared:
            return refuse(
                "the note bytes you supplied hash to sha256:%s and this record "
                "says the stamped bytes hash to %s -- these are not the bytes "
                "this anchor covers" % (note_digest.hex(), declared))
        note_origin, note_root, note_size = note_origin_root_and_size(raw)
        # THE FOURTH BINDING, and the one that carries log IDENTITY. A C2SP
        # origin is 1:1 with a log, so without it a note from ANOTHER log whose
        # root and size happen to coincide satisfies every other check and gets
        # reported under this record's log_id. --origin is the holder's exact
        # out-of-band pin, the same posture as --root; absent it, the structural
        # 1:1 relation is enforced and the report says which of the two ran.
        if origin_pin is not None:
            if note_origin != origin_pin:
                return refuse(
                    "the note you supplied names origin %r and you pinned %r "
                    "-- refused, because an origin is a log's identity and "
                    "these are not the same log"
                    % (note_origin, origin_pin))
        elif not note_origin.endswith("/" + log_id):
            return refuse(
                "the note you supplied names origin %r, which does not end in "
                "'/%s' -- a C2SP origin is one-to-one with its log, so this "
                "note is not a note about the log this record describes. Pin "
                "the exact origin with --origin if yours is spelled differently"
                % (note_origin, log_id))
        if note_root != target_root:
            return refuse(
                "the note you supplied is a note about root %s and this record "
                "describes %s -- refused, because a proof over another "
                "checkpoint's note is not a proof about this one"
                % (note_root, target_root))
        record_size = record.get("tree_size")
        # type(), not isinstance(): see the tree_size gate in check().
        if type(record_size) is int and note_size != record_size:
            return refuse(
                "the note you supplied is for tree size %d and this record "
                "describes size %d" % (note_size, record_size))
        # The stamped VALUE for this class is the note's SHA-256 -- a calendar
        # timestamps a 32-byte digest, and for arbitrary-length bytes that digest
        # is their hash. The raw note is the pre-image, never the start.
        return (note_digest,
                "sha256 of the %d note bytes you supplied, bound to this "
                "record's preimage_sha256, its root and its tree size, and to "
                "origin %r (%s)"
                % (len(raw), note_origin,
                   "exactly, as you pinned it with --origin" if origin_pin
                   else "structurally: a C2SP origin is one-to-one with its "
                        "log, so it must end in '/' + this record's log_id"))

    return refuse(
        "this record names an anchored_preimage class this script does not know "
        "(%r), so it cannot know which bytes were stamped -- refused rather "
        "than assumed" % (kind,))


def check(record, args, chain) -> dict:
    result = {"weakened": []}
    log_id = record.get("log_id")
    tree_size = record.get("tree_size")
    if not isinstance(log_id, str) or not log_id:
        return refuse("the record names no log_id, so its proofs cannot be fetched")
    # NOT isinstance: bool is a subclass of int in Python, so JSON `true` would
    # sail through as tree size 1 and compare equal to a size-1 note.
    if type(tree_size) is not int or tree_size < 1:
        return refuse("the record names no usable tree_size (got %r)"
                      % (tree_size,))

    # ── item 2: whose root is this? ──────────────────────────────────────────
    target_root = record.get("target_root")
    if args.root is not None:
        pinned = args.root.strip().lower()
        if not DIGEST_RE.match(pinned):
            return refuse(
                "--root must be a 'sha256:' + 64 lower-case hex digest, which "
                "is the spelling every BlueFox surface prints (got %r)"
                % (args.root,))
        if pinned != target_root:
            return refuse(
                "the root you pinned with --root is not the root this record "
                "describes: you named %s and the record says %s -- refused, "
                "because checking the record's root would answer a question you "
                "did not ask" % (pinned, target_root))
        result["root_pinned"] = True
    else:
        result["root_pinned"] = False

    digest, digest_note = stamped_bytes(record, args.preimage_file, log_id,
                                       args.origin)
    result.update({
        "log_id": log_id, "tree_size": tree_size, "root": target_root,
        "stamped": digest_note, "lane_status": record.get("lane_status"),
    })

    # ── the proof lanes ─────────────────────────────────────────────────────
    lanes = []
    if args.pending_proof or args.upgraded_proof:
        if not (args.pending_proof and args.upgraded_proof):
            return refuse(
                "--pending-proof and --upgraded-proof come as a pair: the "
                "upgraded proof's operations begin where the pending proof's "
                "end, so one without the other cannot be walked from your root")
        lanes.append(("(local files)",
                      read_bytes_file(args.pending_proof, "pending proof"),
                      read_bytes_file(args.upgraded_proof, "upgraded proof")))
    else:
        served = record.get("proofs")
        if not isinstance(served, list) or not served:
            return refuse(
                "this record lists no stored proofs, so there is nothing to "
                "walk -- an anchor with no proof bytes proves nothing")
        pending_names, upgraded_names = [], []
        for entry in served:
            if not isinstance(entry, dict):
                continue
            calendar = entry.get("calendar")
            if not isinstance(calendar, str) or not calendar:
                continue
            if entry.get("kind") == "pending" and calendar not in pending_names:
                pending_names.append(calendar)
            elif entry.get("kind") == "upgraded" and calendar not in upgraded_names:
                upgraded_names.append(calendar)
        both = [c for c in upgraded_names if c in pending_names]
        if not both:
            return refuse(
                "no calendar in this record holds BOTH a pending and an "
                "upgraded proof, so no chain runs from this root to a block "
                "yet -- %d pending and %d upgraded proof(s) are listed"
                % (len(pending_names), len(upgraded_names)))
        for calendar in both:
            try:
                pending = _fetch(
                    proof_url(args.api_base, log_id, tree_size, calendar,
                              "pending"),
                    args.timeout, "application/vnd.opentimestamps.v1")
                upgraded = _fetch(
                    proof_url(args.api_base, log_id, tree_size, calendar,
                              "upgraded"),
                    args.timeout, "application/vnd.opentimestamps.v1")
            except ObservationError as exc:
                result.setdefault("lane_problems", []).append(
                    {"calendar": calendar, "reason": "proof download failed",
                     "detail": str(exc)})
                continue
            lanes.append((calendar, pending, upgraded))
        if not lanes:
            return refuse(
                "every proof download failed, so nothing could be walked (%s)"
                % "; ".join("%s: %s" % (p["calendar"], p["detail"])
                            for p in result.get("lane_problems", [])))

    # ── items 3, 4, 5, 6: the chained walk, per lane ─────────────────────────
    committed = {}      # display-hex root -> list of calendars that reach it
    heights_named = []
    # A lane recorded as DISQUALIFIED here must not be counted by the second
    # pass below. Without this set the same calendar could print as both "lane
    # not counted" and "reached by" in one passing run.
    disqualified = set()
    for calendar, pending_raw, upgraded_raw in lanes:
        try:
            pending_node, pending_digest = parse_proof(pending_raw)
            upgraded_node, _ = parse_proof(upgraded_raw)
        except ProofError as exc:
            disqualified.add(calendar)
            result.setdefault("lane_problems", []).append(
                {"calendar": calendar, "reason": "proof unreadable",
                 "detail": str(exc)})
            continue
        if pending_digest is not None and pending_digest != digest:
            disqualified.add(calendar)
            result.setdefault("lane_problems", []).append(
                {"calendar": calendar, "reason": "proof file stamps other bytes",
                 "detail": ("this detached proof file names the digest %s, and "
                            "the bytes this record stamps hash differently"
                            % pending_digest.hex())})
            continue
        try:
            starts = pending_commitments(pending_node, digest)
            reachable = count_reachable_claims(pending_node)
            named = bitcoin_heights(upgraded_node)
        except ProofError as exc:
            disqualified.add(calendar)
            result.setdefault("lane_problems", []).append(
                {"calendar": calendar, "reason": "proof not walkable",
                 "detail": str(exc)})
            continue
        # The honest signal that the derived starts are a partial view is that
        # the COLLECTION actually hit its ceiling — not that the proof happens to
        # carry many claims of every kind. Counting all reachable claims against
        # a pending-commitment cap mixes two units and reports a truncation that
        # never happened (caught at the R9-1 gate round).
        if len(starts) >= MAX_PENDING_COMMITMENTS:
            # A truncated view must be named, never worn as a fact — but this is
            # a NOTE, not a disqualification: the lane still counts, and filing
            # it beside the disqualifying problems made report() print a counted
            # lane as dropped (caught at the R9-1 gate round, second pass).
            result.setdefault("lane_notes", []).append(
                {"calendar": calendar, "reason": "derivation window truncated",
                 "detail": ("the collection of submitted commitments hit "
                            "this script's %d-commitment ceiling (the pending "
                            "proof carries %d reachable claims in total), so "
                            "the derived starts are a partial view"
                            % (MAX_PENDING_COMMITMENTS, reachable))})
        if not starts:
            disqualified.add(calendar)
            result.setdefault("lane_problems", []).append(
                {"calendar": calendar, "reason": "no commitment from your root",
                 "detail": ("the pending proof reaches no calendar claim when "
                            "executed from the bytes this record stamps, so "
                            "the upgraded proof has no proven start")})
            continue
        for height in named:
            if height not in heights_named:
                heights_named.append(height)
        result.setdefault("lanes", []).append(
            {"calendar": calendar, "pending_bytes": len(pending_raw),
             "upgraded_bytes": len(upgraded_raw),
             "commitments": [s.hex() for s in starts],
             "heights_named": sorted(named)})

    if not heights_named:
        problems = ((result.get("lane_problems") or [])
                    + (result.get("lane_notes") or []))
        if problems:
            return refuse(
                "no proof in this record could be walked to a Bitcoin block, "
                "and here is why, by lane: %s -- this is not an anchor waiting "
                "on a calendar, it is a proof this script could not use"
                % "; ".join("%s: %s (%s)"
                            % (p["calendar"], p["reason"], p["detail"])
                            for p in problems))
        return refuse(
            "no upgraded proof in this record names a Bitcoin block, so there "
            "is no block to look for -- an anchor still waiting on a calendar "
            "proves nothing yet, and retrying later is the cure")

    # Item 4: the shallowest named height governs.
    height = min(heights_named)
    result["height"] = height
    result["heights_named"] = sorted(heights_named)
    claimed = record.get("bitcoin_block_height")
    # THE THIRD SITE of the bool-is-an-int class, and the one no reviewer named:
    # it was found by taking the finding as a CLASS rather than as its two
    # instances. Fail-closed today only by luck of the numbers — a JSON `true`
    # here reads as height 1, which refuses against any real block, and would
    # have silently PASSED the cross-check against a proof naming block 1.
    # Correct by accident is not correct.
    if type(claimed) is int and claimed not in heights_named:
        return refuse(
            "this record says the block is %d and its own proof bytes name %s "
            "-- refused, because the record and the evidence beside it do not "
            "agree about which block to look in"
            % (claimed, ", ".join(str(h) for h in sorted(heights_named))))

    for calendar, pending_raw, upgraded_raw in lanes:
        if calendar in disqualified:
            continue    # already recorded as a lane problem above, by name
        try:
            pending_node, _ = parse_proof(pending_raw)
            upgraded_node, _ = parse_proof(upgraded_raw)
            starts = pending_commitments(pending_node, digest)
        except ProofError:
            continue    # already recorded as a lane problem above
        # BUFFERED, then committed only if the WHOLE lane completes. Inserting
        # per start meant a lane whose third start refused had already had its
        # first two counted, so the same calendar printed as both "reached by"
        # and "lane not counted" (R9-1 gate round, second pass).
        lane_roots = []
        walkable = True
        for start in starts:
            try:
                lane_roots.extend(merkle_roots_at(upgraded_node, start, height))
            except ProofError as exc:
                disqualified.add(calendar)
                result.setdefault("lane_problems", []).append(
                    {"calendar": calendar, "reason": "proof not walkable",
                     "detail": str(exc)})
                walkable = False
                break
        if not walkable:
            continue
        for root in lane_roots:
            committed.setdefault(root, [])
            if calendar not in committed[root]:
                committed[root].append(calendar)

    if not committed:
        problems = ((result.get("lane_problems") or [])
                    + (result.get("lane_notes") or []))
        if problems:
            # A loud refusal must not arrive wearing a vague one. The ceiling and
            # the budget guards raise on purpose; swallowing them into "commits
            # nothing" would hide the very thing they were built to shout.
            return refuse(
                "no proof in this record could be walked to a value at block "
                "%d, and here is why, by lane: %s"
                % (height, "; ".join("%s: %s (%s)"
                                     % (p["calendar"], p["reason"], p["detail"])
                                     for p in problems)))
        return refuse(
            "no proof in this record commits your root to any value at block "
            "%d, so there is nothing the chain could confirm -- the SHALLOWEST "
            "block any proof names is the one that governs (item 4), and either "
            "these are not the bytes that were stamped or the proof that named "
            "block %d does not commit them" % (height, height))
    result["committed"] = {k: sorted(v) for k, v in committed.items()}

    # ── items 7 and 8: ask the chain ────────────────────────────────────────
    try:
        tip = chain.tip_height()
    except Undetermined as exc:
        return refuse(str(exc))
    except ObservationError as exc:
        return refuse("the chain tip could not be observed, so depth cannot be "
                      "computed: %s" % exc)
    depth = tip - height + 1
    result.update({"tip": tip, "depth": depth, "required_depth": args.depth,
                   "tip_outliers": chain.notes[-1].get("outliers", {})})
    if depth < args.depth:
        return refuse(
            "block %d is %d block(s) deep and %d are required, so this anchor "
            "is not settled yet -- this is a wait, not a failure; the same "
            "command answers differently in about an hour"
            % (height, depth, args.depth))

    try:
        block_hash, observed, agreeing, families = chain.block_at(height)
    except Undetermined as exc:
        return refuse(str(exc))
    except ObservationError as exc:
        return refuse("block %d could not be observed, so nothing is claimed: "
                      "%s" % (height, exc))
    result.update({
        "block_hash": block_hash, "observed_root": observed,
        "agreeing": agreeing, "families": families,
        "operators_asked": [op[0] for op in chain.operators],
    })

    if observed not in committed:
        # LOUD. The block is real and deep enough, and it does not contain the
        # commitment. Depth alone would have believed this.
        return refuse(
            "block %d is real and %d block(s) deep, and it does NOT contain "
            "your root: the proof commits it to %s and the block's merkle root "
            "is %s -- refused, and this is the failure depth alone would have "
            "read as a confirmation"
            % (height, depth, ", ".join(sorted(committed)), observed))
    result["reached_by"] = committed[observed]

    # Any bar below the published default is named, never quietly applied.
    if args.quorum < DEFAULT_QUORUM:
        result["weakened"].append(
            "you lowered the agreement threshold to %d of %d (published bar: %d)"
            % (args.quorum, len(chain.operators), DEFAULT_QUORUM))
    if args.min_implementations < DEFAULT_MIN_IMPLEMENTATIONS:
        result["weakened"].append(
            "you lowered the independent-implementation floor to %d (published "
            "bar: %d)" % (args.min_implementations, DEFAULT_MIN_IMPLEMENTATIONS))
    if args.depth < DEFAULT_DEPTH:
        result["weakened"].append(
            "you lowered the required depth to %d block(s) (published bar: %d)"
            % (args.depth, DEFAULT_DEPTH))
    if args.max_tip_spread > DEFAULT_MAX_TIP_SPREAD:
        result["weakened"].append(
            "you widened the tip agreement window to %d blocks (published bar: "
            "%d)" % (args.max_tip_spread, DEFAULT_MAX_TIP_SPREAD))
    if len(chain.operators) < len(OPERATORS):
        result["weakened"].append(
            "you asked %d of the %d published operators (%s)"
            % (len(chain.operators), len(OPERATORS),
               ", ".join(op[0] for op in chain.operators)))
    if chain.offline is not None:
        result["offline"] = True
    return result


def report(result: dict) -> None:
    """Exactly what was proved and — separately, never merged into the same line
    — exactly what was not."""
    print("  log                    : %s" % result["log_id"])
    print("  checkpoint             : tree size %d" % result["tree_size"])
    print("  root (checked)         : %s" % result["root"])
    if result["root_pinned"]:
        print("  root provenance        : PINNED BY YOU with --root; this "
              "record was required to agree with it")
    else:
        print("  root provenance        : read from this record, which is "
              "BlueFox's word (see the NOT PROVED line below)")
    print("  stamped bytes          : %s" % result["stamped"])
    for lane in result.get("lanes", []):
        print("  proof chain            : %s" % lane["calendar"])
        print("    pending %d B -> commitment %s"
              % (lane["pending_bytes"], ", ".join(lane["commitments"])))
        print("    upgraded %d B -> block(s) %s"
              % (lane["upgraded_bytes"],
                 ", ".join(str(h) for h in lane["heights_named"])))
    print("  bitcoin block          : %d  [named by the proof, not by the "
          "record]" % result["height"])
    print("  merkle root (computed) : %s"
          % ", ".join(sorted(result["committed"])))
    print("  merkle root (observed) : %s" % result["observed_root"])
    print("  reached by             : %s  [any ONE calendar's chain is enough; "
          "a lane that names a different block is listed above and counted for "
          "that block, not this one]" % ", ".join(result["reached_by"]))
    print("  block hash (observed)  : %s" % result["block_hash"])
    print("  observed by            : %s  [%d of %d agreeing, %d independent "
          "implementation(s): %s]"
          % (", ".join(result["agreeing"]), len(result["agreeing"]),
             len(result["operators_asked"]), len(result["families"]),
             ", ".join(result["families"])))
    if result.get("offline"):
        print("  observation source     : YOUR OWN numbers, from --chain-file; "
              "no chain API was contacted by this run")
    print("  tip / depth            : %d / %d block(s)  [required: %d]"
          % (result["tip"], result["depth"], result["required_depth"]))
    for name, reported in sorted(result.get("tip_outliers", {}).items()):
        gap = reported - result["tip"]
        print("  tip outlier            : %s=%d, %d block(s) %s the agreeing tip "
              "-- recorded, not counted toward the tip; its block observation "
              "still counts" % (name, reported, abs(gap),
                                "behind" if gap < 0 else "ahead of"))
    for problem in result.get("lane_problems", []):
        print("  lane not counted       : %s -- %s (%s)"
              % (problem["calendar"], problem["reason"], problem["detail"]))
    for note in result.get("lane_notes", []):
        # Distinct label on purpose: this lane WAS counted, and something about
        # the view of it is worth saying. Printing it as "not counted" beside a
        # "reached by" line naming the same calendar is a self-contradicting
        # report, which for this checker is a defect and not a cosmetic.
        print("  lane counted, with note: %s -- %s (%s)"
              % (note["calendar"], note["reason"], note["detail"]))
    print("")
    if result.get("offline"):
        # The observations were the caller's own. Saying "public chain APIs
        # agree" here would attribute a local JSON file to four operators who
        # were never asked — a false verdict with a true disclosure three lines
        # down, which is the composition this file refuses to ship.
        observers = (
            "the same quorum arithmetic was applied to the %d observation(s) "
            "YOU supplied, which agreed byte-for-byte across %d independently "
            "written implementation(s); NO chain API was contacted by this run"
            % (len(result["agreeing"]), len(result["families"])))
    else:
        observers = (
            "%d of %d public chain API(s), across %d independently written "
            "implementation(s), agree byte-for-byte on that block's hash and "
            "merkle root"
            % (len(result["agreeing"]), len(result["operators_asked"]),
               len(result["families"])))
    print(
        "VERIFIED -- bitcoin anchor: the proof chain was executed from the root "
        "named above and lands on the merkle root of block %d; %s; and the "
        "block sits %d block(s) deep, against the %d required. Your root "
        "therefore existed no later than that block was mined. "
        "NO BLUEFOX KEY WAS FETCHED, CHECKED OR TRUSTED ANYWHERE IN THIS RUN."
        % (result["height"], observers, result["depth"],
           result["required_depth"])
    )
    if not result["root_pinned"]:
        print(
            "NOT PROVED by this run -- WHICH ROOT: the root above was read from "
            "the record this run was handed, which is unsigned JSON BlueFox "
            "serves, so that one leg is our word. The cure is one flag: pass "
            "--root sha256:<64hex> with the root out of a signed checkpoint "
            "note you hold, or out of verify_inclusion.py's own output, and "
            "this run refuses unless the record agrees with you."
        )
    print(
        "NOT VERIFIED by this run -- YOUR RECEIPT, AND ITS PLACE IN THE TREE: "
        "this run says nothing about any receipt. It proves a 32-byte root is "
        "in Bitcoin; that your receipt is a leaf of the tree with THAT root is "
        "a separate claim with its own published checker, "
        "https://api.bluefoxedge.ai/verify_inclusion.py. The two compose -- "
        "receipt to tree root by that script, tree root to Bitcoin by this one "
        "-- and running only this one proves that a number is old."
    )
    if result.get("offline"):
        print(
            "NOT VERIFIED by this run -- THE CHAIN ITSELF: no Bitcoin block was "
            "validated here and no node was run, by you or by us. This run "
            "asked nobody: the block's identity and the tip came from the file "
            "you passed with --chain-file, so what it trusts is whatever you "
            "trusted to write those numbers down. Drop the flag and the same "
            "arithmetic runs over %d independently operated public chain APIs "
            "instead." % len(OPERATORS)
        )
    else:
        print(
            "NOT VERIFIED by this run -- THE CHAIN ITSELF: no Bitcoin block was "
            "validated here and no node was run, by you or by us. What this run "
            "trusts is that %d separately operated HTTPS services, running %d "
            "independently written implementation(s), are not simultaneously "
            "wrong about the same block -- weaker than running your own node, "
            "stronger than asking one explorer. If you keep your own "
            "observations, --chain-file takes them and this script does the "
            "same arithmetic over your numbers instead of ours."
            % (len(result["agreeing"]), len(result["families"]))
        )
    print(
        "NOT PROVED by this run -- THE CLOCK: this places your root BEFORE a "
        "named block, which is the strongest thing a Bitcoin anchor says. It is "
        "not a wall-clock instant, and this script deliberately reads no block "
        "timestamp: a header's time field is miner-set and only loosely "
        "bounded, so turning a height into a date is a separate claim with its "
        "own error bars. What was proved here is the ordering."
    )
    for weakened in result.get("weakened", []):
        print("BAR LOWERED BY YOU -- %s. The verdict above is true against the "
              "bar you set, not against the published one." % weakened)


def build_parser() -> argparse.ArgumentParser:
    parser = argparse.ArgumentParser(
        prog="verify_anchor.py",
        description=(
            "Check a BlueFox anchor against the public Bitcoin record: execute "
            "the stored proof chain from the checkpoint root, land on a block's "
            "merkle root, and confirm that block through a quorum of "
            "independently operated public chain APIs at the required depth. No "
            "key set is fetched and no signature is checked anywhere in this "
            "run -- that is the point of the file. Exit 0 = proved; any "
            "non-zero = not proved, with the reason in one sentence on stderr."
        ),
        epilog=(
            "The anchor record comes in on stdin, or from --file. The proof "
            "bytes are downloaded from the record's own proof endpoint, or "
            "supplied with --pending-proof/--upgraded-proof; either way they "
            "are evidence, not testimony -- bytes that do not walk to the real "
            "block make this script refuse. THE ONE STEP AN INDEPENDENT "
            "IMPLEMENTATION LOSES AN AFTERNOON ON: the upgraded proof's "
            "operations begin at the commitment the PENDING proof reaches from "
            "your root, not at the root itself, so both proofs are needed and "
            "the second starts where the first ended. The other trap is byte "
            "order: the value you land on is internal-order and every chain API "
            "prints display-order, so reverse before comparing. Read the recipe "
            "at the top of this file for the whole construction, including the "
            "published worked pair for that reversal."
        ),
    )
    parser.add_argument("--file", metavar="PATH", default=None,
                        help="read the anchor record from PATH instead of stdin")
    parser.add_argument("--root", metavar="SHA256", default=None,
                        help="pin the checkpoint root out of band as "
                             "sha256:<64hex>; the record must agree with it or "
                             "this run refuses")
    parser.add_argument("--pending-proof", metavar="PATH", default=None,
                        help="read one calendar's pending proof from PATH "
                             "instead of downloading every lane")
    parser.add_argument("--upgraded-proof", metavar="PATH", default=None,
                        help="read that calendar's upgraded proof from PATH; "
                             "the pair goes together")
    parser.add_argument("--preimage-file", metavar="PATH", default=None,
                        help="the exact stamped bytes, for an anchor whose "
                             "pre-image class is the checkpoint note rather "
                             "than the raw root bytes. They are BOUND before "
                             "use: their SHA-256 must equal the record's "
                             "preimage_sha256, and the note's own root and tree "
                             "size must match the record's, or the run refuses")
    parser.add_argument("--origin", metavar="STRING", default=None,
                        help="pin the log's C2SP origin out of band, for the "
                             "checkpoint-note pre-image class. Without it the "
                             "note's origin must still end in '/' + the "
                             "record's log_id, because an origin is one-to-one "
                             "with a log and an unbound one lets another log's "
                             "note be reported as this one's")
    parser.add_argument("--chain-file", metavar="PATH", default=None,
                        help="observations you gathered yourself, as "
                             '{"tips": {"<operator>": <height>}, "blocks": '
                             '{"<height>": {"<operator>": {"block_hash": '
                             '"<64hex>", "merkle_root": "<64hex>"}}}} -- the '
                             "same quorum arithmetic runs over your numbers "
                             "and no chain API is contacted")
    parser.add_argument("--api", metavar="NAME", action="append", default=None,
                        help="ask only this chain API (repeatable). Known: "
                             + ", ".join(op[0] for op in OPERATORS))
    parser.add_argument("--api-base", metavar="URL", default=API_BASE,
                        help="where to download the proof bytes from (default: "
                             "the pinned address in this file)")
    parser.add_argument("--quorum", metavar="N", type=int, default=DEFAULT_QUORUM,
                        help="how many operators must agree byte-for-byte "
                             "(default and published bar: %d)" % DEFAULT_QUORUM)
    parser.add_argument("--min-implementations", metavar="N", type=int,
                        default=DEFAULT_MIN_IMPLEMENTATIONS,
                        help="how many independently written implementations "
                             "must be among them (default and published bar: "
                             "%d)" % DEFAULT_MIN_IMPLEMENTATIONS)
    parser.add_argument("--depth", metavar="N", type=int, default=DEFAULT_DEPTH,
                        help="required proof-of-work depth in blocks (default "
                             "and published bar: %d)" % DEFAULT_DEPTH)
    parser.add_argument("--max-tip-spread", metavar="N", type=int,
                        default=DEFAULT_MAX_TIP_SPREAD,
                        help="the tip agreement window: how far apart agreeing "
                             "operators' tips may be; the largest group inside "
                             "one window is the majority and governs the tip, "
                             "an operator outside it is an outlier that is named "
                             "and not counted (default and published bar: %d)"
                             % DEFAULT_MAX_TIP_SPREAD)
    parser.add_argument("--timeout", metavar="SECONDS", type=float,
                        default=DEFAULT_TIMEOUT,
                        help="per-request timeout (default: %s)"
                             % DEFAULT_TIMEOUT)
    return parser


def select_operators(names):
    if not names:
        return list(OPERATORS)
    chosen = []
    known = {op[0]: op for op in OPERATORS}
    for name in names:
        if name not in known:
            return refuse(
                "--api names %r, which is not one of the chain APIs pinned in "
                "this file (%s) -- refused rather than skipped, because a typo "
                "that silently shrank the quorum would weaken the answer by "
                "exactly the amount nobody noticed"
                % (name, ", ".join(sorted(known))))
        if known[name] not in chosen:
            chosen.append(known[name])
    return chosen


def main(argv=None) -> int:
    args = build_parser().parse_args(argv)
    try:
        record = read_record(args.file)
        offline = read_chain_file(args.chain_file) if args.chain_file else None
        operators = select_operators(args.api)
        try:
            chain = Chain(operators, args.quorum, args.min_implementations,
                          args.max_tip_spread, args.timeout, offline)
        except ValueError as exc:
            return refuse("this run was configured so that nothing could ever "
                          "be confirmed: %s" % exc)
        result = check(record, args, chain)
    except SystemExit:
        raise
    except KeyboardInterrupt:
        return refuse("interrupted before the check finished")
    except Exception as exc:  # the backstop: a sentence, never a traceback
        return refuse("this anchor could not be checked -- %s: %s"
                      % (type(exc).__name__, exc))
    report(result)
    return EXIT_OK


if __name__ == "__main__":
    sys.exit(main())
