Statement of bitcoin holdings and movements
{% 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 %}
{% if not r.ok %}Not every check passed; the failing ones are marked below, and the section concerned says more.
| Opening balance, {{ r.period.start.time_utc | utc }} | {{ r.opening.total_btc }} |
| Net of the {{ r.totals.transactions }} transactions in the period (section 1) | {{ r.totals.net_btc | acct }} |
| Closing balance, {{ r.period.end.time_utc | utc }} (section 2) | {{ r.closing.total_btc }} |
| Difference | {% if r.reconciliation.ok %}{{ r.reconciliation.diff_sat | btc }}{% else %}{{ r.reconciliation.diff_sat | btc | acct }}{% endif %} |
| {{ yesno(r.coverage.complete) }} | Proof of control for every closing UTXO (section 3) | {{ r.coverage.addresses_covered }} of {{ r.coverage.addresses_total }} scripts{% 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 }} scripts{% 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) | Transaction | Kind | Amount | Fee | Balance | {% if r.fiat %}Amount {{ r.fiat.currency }} | {% endif %}
|---|---|---|---|---|---|---|
| Opening balance, {{ r.period.start.time_utc | utc }} | {{ r.opening.total_btc }} | {% if r.fiat %}{% endif %} | ||||
| {{ t.time_utc[:10] }} {{ t.time_utc[11:19] }} |
{{ tx(t.txid) }} | {{ "dust swept" if t.kind == "swept" else t.kind }} | {% 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 }} · the holdings of section 2 | {{ r.totals.amount_btc | acct }} | ({{ r.totals.fees_btc }}) | {{ r.closing.total_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 %} | ||||
Received {{ r.totals.received_btc }} · paid out {{ r.totals.sent_btc }} · fees {{ r.totals.fees_btc }}{% if r.totals.mixed_count %} · mixed {{ r.totals.mixed_btc | acct }}, fee unknown{% endif %}{% if r.reconciliation.ok %} · difference to the holdings on the chain {{ r.reconciliation.diff_sat | btc }}{% endif %}.{% if r.fiat %} {{ r.fiat.currency }} at the rate in force on the transaction's day.{% endif %}
{% else %}No transactions in the period: the closing balance, {{ r.closing.total_btc }}, equals the opening balance{% if not r.reconciliation.ok %} but the holdings on the chain differ by {{ r.reconciliation.diff_sat | btc | acct }}{% endif %}.
{% endif %}| UTXO | Locked to (scriptPubKey) | Amount |
|---|---|---|
| {{ tx(o.txid, "hl-utxo") }}:{{ o.vout }} | {{ spk(a.script.script_pubkey, o.txid, "hl-script") }} | {{ o.amount_btc }} |
| Closing balance | {{ r.closing.total_btc }} | |
One page per UTXO follows: the proof of control of its scriptPubKey, then the chain's record of the 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) }} ({{ t.kind }}, {{ t.net_btc | acct }}){% if not loop.last %}, {% endif %}{% endfor %}.
{% endif %}| scriptPubKey | {{ spk(a.script.script_pubkey, o.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 }} |
| 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 }} at the rate on each transaction's day{% endif %}. Amount is what moved between the wallet and others, received or (sent), before the fee; the balance moves by the amount and the fee together.{% if r.totals.mixed_count %} A mixed transaction has inputs from others beside the wallet's: its fee is unknown and its amount is the wallet'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 wallet's: at the closing block the wallet'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 wallet 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 %}
Wallet {{ r.label }}{% 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 UTXO pages paste as they stand{% endif %}. The tools: https://github.com/embeddednation/bip322-report.