{% include "_style.html" %}
{% macro tx(t, role="") -%} {% if explorer %}{{ t }}{% else %}{{ t }}{% endif %} {%- endmacro %} {% macro spk(hex, t, role="") -%} {% if explorer and hex %}{{ hex }}{% else %}{{ hex or "?" }}{% endif %} {%- endmacro %} {% macro addr(a) -%} {% if explorer %}{{ a }}{% else %}{{ a }}{% endif %} {%- endmacro %} {% macro yesno(v) -%}{% if v %}✓{% else %}✗{% endif %}{%- endmacro %} {% macro when(a) -%}{% if a.received_first %}{{ a.received_first[:10] }}{% if a.received_last and a.received_last[:10] != a.received_first[:10] %} – {{ a.received_last[:10] }}{% endif %}{% endif %}{%- endmacro %}{{ doc }}
{% if r.holder %}{{ r.holder }} · {% endif %}{{ r.period.start.time_utc | utc }} – {{ r.period.end.time_utc | utc }}{% if r.chain != "main" %} · {{ r.chain }} chain{% endif %}
{# the summary: from the opening holdings to the closing ones, each split by wallet for a holder, and what is proven #} {% set who = "holder" if r.wallets else "wallet" %} {% macro split(key) -%} {% for w in r.wallets or [] %}| BTC | {% if r.fiat %}{{ r.fiat.currency }} | {{ r.fiat.currency }}/BTC | {% endif %}||
|---|---|---|---|---|
| Opening holdings, {{ r.period.start.time_utc | utc }} | {{ r.opening.total_btc }} | {% if r.fiat %}{{ r.fiat.opening.value | acct }} | {{ r.fiat.opening.rate or "" }} | {% endif %}{% if r.previous %}{% if r.previous.agrees %}the closing holdings of {{ r.previous.label }}, reference {{ r.previous.report_id }}{% else %}not the closing holdings of {{ r.previous.label }} (reference {{ r.previous.report_id }}), which were {{ r.previous.closing_btc }}{% endif %}{% endif %} |
| Received{% if r.wallets %} from others{% endif %} | {{ r.totals.received_btc | nil }} | {% if r.fiat %}{{ r.totals.fiat.received | acct }} | {% endif %} | |
| Sent{% if r.wallets %} to others{% endif %} | {{ r.totals.sent_btc | paren }} | {% if r.fiat %}{{ r.totals.fiat.sent | acct }} | {% endif %} | |
| Joint transactions, the {{ who }}'s net | {{ r.totals.mixed_btc | acct }} | {% if r.fiat %}{{ r.totals.fiat.mixed | acct }} | {% endif %} | {{ r.totals.mixed_count }}, funded by others too: their fees unknown |
| Network fees | {{ r.totals.fees_btc | paren }} | {% if r.fiat %}{{ r.totals.fiat.fees | acct }} | {% endif %} | |
| Closing holdings, {{ r.period.end.time_utc | utc }} | {{ r.closing.total_btc }} | {% if r.fiat %}{{ r.fiat.closing.value | acct }} | {{ r.fiat.closing.rate or "" }} | {% endif %}{% if r.closing.coins %}{{ r.coverage.total_count }} UTXO{{ "s" if r.coverage.total_count != 1 }} in {{ r.coverage.addresses_total }} address{{ "es" if r.coverage.addresses_total != 1 }} (section 2){% endif %} |
| Difference between the running balance and the holdings on the chain | {{ r.reconciliation.diff_sat | btc | acct }} | {% if r.fiat %}{% endif %} |
{{ proven }}
{% else %}Not every check passed; the failing ones are marked here, and the section concerned says more.
| {{ yesno(r.reconciliation.ok) }} | Opening holdings and the movements add up to the closing holdings (section 1) | {% if r.reconciliation.ok %}difference {{ r.reconciliation.diff_sat | btc }}{% else %}difference {{ r.reconciliation.diff_sat | btc | acct }}{% endif %} |
| {{ yesno(r.coverage.complete) }} | Proof of control for every closing UTXO (sections 2 and 3) | {{ r.coverage.addresses_covered }} of {{ r.coverage.addresses_total }} addresses{% if not r.coverage.complete %} · {{ r.coverage.uncovered_btc }} without proof{% endif %} |
| {% if r.period.to_tip or r.coverage.covered_after_period_count != r.coverage.total_count %}–{% else %}{{ yesno(true) }}{% endif %} | Proofs dated after the period's end | {% if r.period.to_tip %}n/a, the period runs to the tip{% else %}{{ r.coverage.addresses_after_period }} of {{ r.coverage.addresses_total }} addresses{% endif %} |
| {% if r.onchain.contradicted %}{{ yesno(false) }}{% elif r.onchain.not_run or r.onchain.spent_since or r.onchain.after_proof %}–{% else %}{{ yesno(true) }}{% endif %} | Closing UTXOs found on the chain at the block of their proof (section 3) | {{ r.onchain.matches }} of {{ r.onchain.outputs }}{% if r.onchain.after_proof %} · {{ r.onchain.after_proof }} received after the proof{% endif %}{% if r.onchain.spent_since %} · {{ r.onchain.spent_since }} spent since{% endif %}{% if r.onchain.contradicted %} · {{ r.onchain.contradicted }} contradicted{% endif %}{% if r.onchain.not_run %} · {{ r.onchain.not_run }} not run{% endif %} |
| {% if r.node_check.ok is none %}–{% else %}{{ yesno(r.node_check.ok) }}{% endif %} | Node wallet agrees with the closing UTXOs | {% if r.node_check.ok is none %}not run · {{ r.node_check.reason }}{% else %}{{ r.node_check.listunspent_count }} of {{ r.node_check.closing_count }}{% if r.node_check.spent_in_mempool %} · {{ r.node_check.spent_in_mempool | length }} spent by an unconfirmed transaction{% endif %}{% if r.node_check.immature %} · {{ r.node_check.immature | length }} immature coinbase{% endif %}{% endif %} |
| # | Date (UTC) | Kind | Counterparty | Amount | Fee | Balance | {% if r.fiat %}Amount {{ r.fiat.currency }} | {% endif %}
|---|---|---|---|---|---|---|---|
| Opening balance, {{ r.period.start.time_utc | utc }} | {{ b.opening_btc }} | {% if r.fiat %}{% endif %} | |||||
| {% if explorer %}{{ t.n }}{% else %}{{ t.n }}{% endif %} | {{ t.time_utc[:10] }} {{ t.time_utc[11:19] }} |
{{ kinds.get(t.kind, t.kind) }} | {{ t.counterpart }} | {% if t.amount_sat %}{{ t.amount_btc | acct }}{% endif %} | {% if t.kind == "mixed" %}unknown{% elif t.fee_btc %}({{ t.fee_btc }}){% endif %} | {{ t.balance_after_btc }} | {% if r.fiat %}{{ (t.fiat.amount | acct) if t.fiat.amount and t.amount_sat else "" }} | {% endif %}
| Closing balance, {{ r.period.end.time_utc | utc }}{% if not r.wallets %} · the holdings of section 2{% endif %} | {{ b.amount_btc | acct }} | ({{ b.fees_btc }}) | {{ b.closing_btc }} | {% if r.fiat %}{% endif %} | |||
| Difference between the running balance and the holdings on the chain | {{ r.reconciliation.diff_sat | btc | acct }} | {% if r.fiat %}{% endif %} | |||||
No transactions in the period: the closing balance, {{ b.closing_btc }}, equals the opening balance{% if not r.wallets and not r.reconciliation.ok %} but the holdings on the chain differ by {{ r.reconciliation.diff_sat | btc | acct }}{% endif %}.
{% endif %}{% if r.wallets %}The wallets' closing balances add up to the closing holdings of section 2, {{ r.closing.total_btc }}{% if not r.reconciliation.ok %}; the running balances differ from the holdings on the chain by {{ r.reconciliation.diff_sat | btc | acct }}{% endif %}. A move between the holder's wallets carries one number in both: sent by the one, received by the other, the fee with the sender. {% endif %}A movement's number is its transaction's in {{ r.name | default("report") }}.json{% if explorer %}, and links to it on the explorer{% endif %}.{% if r.totals.mixed_count %} A joint transaction was funded by others too: its fee is unknown and its amount is the {{ who }}'s net.{% endif %}{% if r.fiat %} {{ r.fiat.currency }} at the rate in force on the transaction's day.{% endif %}
| Address | Received | UTXOs | Amount | Proof of control | Page |
|---|---|---|---|---|---|
| {{ a.wallet }} | |||||
| {{ addr(a.address) }} | {{ when(a) }} | {{ a.outputs | length }} | {{ a.total_btc }} | {% if not a.proof %}NO PROOF{% elif a.proof.verified %}valid · block {{ a.proof.stamp.height }}{% if a.proof.before_coins == "all" %} · before the UTXO{{ "s" if a.outputs | length != 1 }}{% elif a.proof.before_coins == "some" %} · before some of the UTXOs{% endif %}{% else %}{{ a.proof.state | upper }} · block {{ a.proof.stamp.height }}{% endif %} | 3.{{ loop.index }} |
| Closing holdings | {{ r.coverage.total_count }} | {{ r.closing.total_btc }} | |||
One page per address follows: the proof of control of its scriptPubKey, then the chain's record of each UTXO at the block of the proof.
{% elif r.dust.count %}Nothing but dust at the end of the period (see the notes): nothing to prove.
{% else %}No holdings at the end of the period: nothing to prove.
{% endif %} {% if r.coverage.uncovered %}UTXOs without a verified proof hold {{ r.coverage.uncovered_btc }}.
{% endif %} {% if r.pending_snapshots %}Proofs awaiting signatures: {% for p in r.pending_snapshots %}{{ p }}{% if not loop.last %}, {% endif %}{% endfor %}.
Unconfirmed transactions, in no balance: {% for t in r.pending_transactions %}{{ tx(t.txid) }} ({{ kinds.get(t.kind, t.kind) | lower }}, {{ t.net_btc | acct }}){% if not loop.last %}, {% endif %}{% endfor %}.
{% endif %}{% if r.wallets %}{{ a.wallet }} · {% endif %}{{ a.outputs | length }} UTXO{{ "s" if a.outputs | length != 1 }}, {{ a.total_btc }} BTC{% if a.received_first %}, received {{ when(a) }}{% endif %}
| scriptPubKey | {{ spk(a.script.script_pubkey, a.outputs[0].txid, "hl-script") }} |
| message | {{ a.proof.message | hl([["block", stamp, "block: "]]) }} |
| proof | {{ a.proof.signature | breakable }} |
| verify |
{{ a.proof.command | hl([["script", a.script.script_pubkey], ["proof", a.proof.signature], ["block", stamp, "block: "]]) }}
{{ a.proof.output | hl([["bad", "INVALID"], ["bad", "INCONCLUSIVE"], ["ok", "VALID"]]) }} |
| verify | NO PROOF |
| utxo | {{ tx(o.txid, "hl-utxo") }}:{{ o.vout }} {{ o.amount_btc }} BTC{% if o.created_utc %}, received {{ o.created_utc[:10] }}{% endif %} |
| lookup |
{% if o.onchain %}
{% set at = o.onchain.at | string %}
{{ o.onchain.command | hl([["utxo", o.txid ~ ":" ~ o.vout], ["block", at, "--at "]]) }}
{{ o.onchain.output | hl([["script", a.script.script_pubkey, "locked to "], ["amount", o.amount_btc ~ " BTC"], ["block", at, "block "]]) }}bip322 audit holdings \
{{ o.txid }}:{{ o.vout }} --at {{ o.proof.stamp.height if o.proof else r.period.end.height }} |
A proof of control is a spend that can never happen: a made-up transaction locks nothing to the real UTXO's scriptPubKey, a second one spends it with exactly what a real spend would need, and that unlocking data is the proof. The mechanism is the same for every kind of scriptPubKey.
Coins and locks. A balance is a set of unspent transaction outputs, {{ U | safe }}s. Each was created by a transaction paying an amount to a {{ S | safe }}, the lock a later transaction must open to spend it, and is named by that transaction's id and the output's index. Whoever can open the lock controls every {{ U | safe }} behind it, so control is a property of the {{ S | safe }}, and that is what a proof is about. Locks come in several kinds, a single key, a script, a taproot output; the proof works the same way for all of them.
The message. The text on each {{ U | safe }} page: a title, and a line naming a {{ B | safe }} by height, hash and time. A block's hash cannot be known before the block exists, so the proof is no older than that {{ B | safe }}. The message is hashed (SHA-256 under the tag BIP0322-signed-message), and the {{ H | safe }} is what enters the transactions.
Two transactions that never reach the network. to_spend has an output with the same {{ S | safe }} as the real {{ U | safe }} and nothing in it, and an input that spends no coin at all, the null outpoint, carrying the {{ H | safe }}. to_sign spends that empty output, naming it by the {{ X | safe }}, and pays nothing to OP_RETURN, which no one can spend. Neither can ever be confirmed, since the coin they pass along does not exist: a proof cannot move funds.
The {{ P | safe }}. to_sign's input must be unlocked like any spend from that {{ S | safe }}, so the signers produce its witness, exactly what a real spend would carry; every signature in it commits to the {{ X | safe }}, and through it to the message. That witness, base64-encoded with the prefix smp (BIP-322's simple format), is the {{ P | safe }}. Nothing else needs to be kept: the verifier rebuilds both transactions from the message and the {{ S | safe }}.
Verification. The verifier rebuilds to_spend from the {{ H | safe }} and the {{ S | safe }}, rebuilds to_sign from the {{ X | safe }} with the {{ P | safe }} as its witness, and checks that input exactly as a node validating a block would: the {{ P | safe }} must unlock the {{ S | safe }} under the rules that govern every spend, whatever kind of lock it is. That is the verify command on each {{ U | safe }} page. The lookup beside it finds the {{ U | safe }} in the chain's set of unspent outputs, with its amount and the same {{ S | safe }}. The engines named in each output make the check independently; one of them is Bitcoin Core's own consensus library.
What it shows. That at the {{ B | safe }} named in the message the holder could produce a valid spend from the {{ S | safe }}, which is what controlling the {{ U | safe }}s locked to it means; a {{ U | safe }} confirmed after that {{ B | safe }} is covered by a later {{ P | safe }}. Control, not title: not who owns the coins, nor what liabilities stand against them. Nothing about any time after the {{ B | safe }}; a new {{ P | safe }} over a new {{ B | safe }} does.
Amounts in BTC to eight decimals, negatives in parentheses{% if r.fiat %}; {{ r.fiat.currency }} from {{ r.fiat.source }}, each movement at the rate on its day, the opening and closing holdings at the rate on theirs{% endif %}. Amount is what moved between the {{ who }} and others, received or (sent), before the fee; the balance moves by the amount and the fee together.{% if r.totals.mixed_count %} A joint transaction has inputs from others beside the {{ who }}'s: its fee is unknown and its amount is the {{ who }}'s net.{% endif %}{% if r.dust.count %} Outputs of at most {{ r.dust.threshold_sat }} sat received from others are dust and left out of every figure, as if they had never been the {{ who }}'s: at the closing block the {{ who }}'s addresses hold {{ r.dust.count }}, {{ r.dust.total_btc }} in all ({{ r.name | default("report") }}.json lists them). Spending them would cost more in fees than they hold, so they are left where they are; what is proven is control, not its absence.{% endif %}{% if r.dust.swept_count %} Dust the {{ who }} spent in the period ({{ r.dust.swept_count }}, {{ r.dust.swept_btc }} in all) is booked in the block that spent it, as a movement of its own named after the output.{% endif %}
{% if r.wallets %}Holder {{ r.label }}, {{ r.wallets | length }} wallet{{ "s" if r.wallets | length != 1 }} ({% for w in r.wallets %}{{ w.name }}{% if not loop.last %}, {% endif %}{% endfor %}); a move between them is sent by the one and received by the other, in neither the received nor the sent of the summary, its fee the holder's only cost{% else %}Wallet {{ r.label }}{% endif %}{% if r.policy == "1 of 1" %}, single key{% elif r.policy %}, {{ r.policy }} keys{% endif %}, {{ r.chain }} chain; opening figures at block {{ r.period.start.height }}, closing at block {{ r.period.end.height }}{% if r.period.to_tip %}, the tip when prepared{% endif %}. Holdings and movements as known to the holder's node; no descriptor or extended public key is disclosed.
Prepared {{ r.generated_utc | utc }} by {{ r.tool }}; reference {{ r.report_id }}. With this statement: {{ r.name | default("report") }}.json, {{ r.name | default("report") }}.csv{% if r.proofs_files %} and the proofs, {% for f in r.proofs_files %}{{ f.file }}{% if not loop.last %}, {% endif %}{% endfor %}{% endif %}; bip322 audit verify FILE checks a proofs file against a node{% if r.closing.coins %}, and the commands on the address pages paste as they stand{% endif %}. The tools: https://github.com/embeddednation/bip322-report.