docs / verification

Verification

Verification is deterministic, offline-capable (for steps 1–3), and auditable by anyone with a Bitcoin library and a public chain explorer.

Inputs

A verifier needs:

  • addr — Bitcoin address.
  • msg — canonical message (UTF-8 text, LF endings).
  • sig — BIP-322 or legacy signature (base64 or hex).
  • scheme"bip322" or "legacy".

Optional per-call:

  • Expected origin for the aud: extension (phishing resistance).
  • Test-mode flag to accept network: testnet/signet addresses.

Algorithm

1. Canonical checks

  1. Decode msg if provided as base64url.
  2. Enforce exactly one trailing LF.
  3. Parse core lines; verify header (orangecheck), purpose:, and ack: are exact literals.
  4. Parse extensions; verify keys are lexicographically sorted.
  5. Confirm the address: line value equals addr.
  6. Validate nonce matches /^[0-9a-f]{32}$/.
  7. Validate issued_at parses as RFC-3339 UTC.
  8. Validate identities: field — each entry matches <protocol>:<identifier>.

Any failure → decode_error or equivalent status code.

2. Derive attestation ID

attestation_id = SHA-256(msg)

3. Signature verification

if scheme == "bip322":
    ok = verify_bip322(addr, msg, sig)
elif scheme == "legacy":
    if not is_p2pkh(addr):
        raise InvalidScheme
    ok = verify_legacy_signmessage(addr, msg, sig)
else:
    raise InvalidScheme
  • On failure → sig_invalid.
  • On success → sig_ok_bip322 or sig_ok_legacy.

4. Network selection

Read the network: extension:

  • mainnet (default) — require addr prefix in {bc1q, bc1p, 1, 3}.
  • testnet — require {tb1q, tb1p, m, n, 2}.
  • signet — same as testnet.

If the verifier is not in test-mode and network is testnet/signet, return network_testmode.

5. Fetch UTXOs

Query a public Esplora-compatible explorer (mempool.space, blockstream.info, your own node) for confirmed unspent outputs at addr.

6. Bond handling

if "bond:" extension is present:
    bond = int(extension)
    confirmed_balance = sum(utxo.value for utxo in confirmed_utxos)
    if confirmed_balance < bond:
        status = "bond_insufficient"  // invalid
    sats_bonded = bond  // surplus ignored
    days_unspent = greedy_oldest_first(confirmed_utxos, bond)
else:
    sats_bonded = sum(u.value for u in confirmed_utxos)
    first_seen = min(u.confirmed_at for u in confirmed_utxos) if confirmed_utxos else None
    days_unspent = floor((now - first_seen) / 86400) if first_seen else 0

if sats_bonded == 0:
    status = "bond_zero"
elif confirmed_utxos and any(unconfirmed):
    status += "bond_pending"
else:
    status = "bond_confirmed"

7. Policy checks

  • If aud: present and the RP enforces origin binding, aud: must equal the RP's origin.
  • If expires: is in the past, return expired (the RP decides whether to honor).

8. Return

{
  "ok": true,
  "codes": ["sig_ok_bip322", "bond_confirmed"],
  "network": "mainnet",
  "attestation_id": "a3f5b8c2...",
  "identities": [...],
  "metrics": {
    "sats_bonded": 125000,
    "days_unspent": 47,
    "score": 18.2
  }
}

Status codes

Signature

  • sig_ok_bip322 — BIP-322 signature valid.
  • sig_ok_legacy — legacy signmessage valid.
  • sig_invalid — signature did not verify.
  • sig_unsupported_script — address type doesn't allow the declared scheme.

Bond

  • bond_confirmed — at or above declared bond.
  • bond_zero — no confirmed balance.
  • bond_pending — unconfirmed UTXOs present (ignored for metrics).
  • bond_insufficient — balance < declared bond.

Policy

  • aud_mismatchaud: didn't match expected origin.
  • expiredexpires: is in the past.
  • network_testmode — test-network attestation but verifier not in test mode.

Transport

  • bad_request — missing or malformed inputs.
  • decode_error — base64url / UTF-8 / structural.
  • invalid_scheme — scheme value not recognized.

Offline vs online

Steps 1–3 are pure cryptographic work: parsing, hashing, signature verification. An air-gapped machine with bip322-js and nothing else can perform them.

Steps 4–7 need live chain state. A verifier that can reach any Esplora-compatible endpoint (public or private) can do them. Steps 4–7 produce metrics; step 3 produces the authority.

This separation means a proof is permanently verifiable — you can always re-check a signed message against whatever chain state is current. No proof goes stale except when the user spends the bonded UTXOs.

Caching rules (for the hosted verifier)

  • /api/check caches verification outcomes for 60 seconds. Bond state changes at Bitcoin's block cadence — 60 s of staleness is well below the block time.
  • /api/verify (raw verification) returns no-store.
  • Clients SHOULD treat metrics as fresh for ~60 s for UX but MUST NOT persist them as ground truth.