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/signetaddresses.
Algorithm
1. Canonical checks
- Decode
msgif provided as base64url. - Enforce exactly one trailing LF.
- Parse core lines; verify header (
orangecheck),purpose:, andack:are exact literals. - Parse extensions; verify keys are lexicographically sorted.
- Confirm the
address:line value equalsaddr. - Validate
noncematches/^[0-9a-f]{32}$/. - Validate
issued_atparses as RFC-3339 UTC. - 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_bip322orsig_ok_legacy.
4. Network selection
Read the network: extension:
mainnet(default) — requireaddrprefix 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, returnexpired(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_mismatch—aud:didn't match expected origin.expired—expires: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/checkcaches 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) returnsno-store.- Clients SHOULD treat metrics as fresh for ~60 s for UX but MUST NOT persist them as ground truth.