{% 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 yesno(v) -%}{% if v %}✓{% else %}✗{% endif %}{%- endmacro %}

Statement of bitcoin holdings and movements

{{ r.label }} · {{ r.period.label }}

{% if not r.ok %}ATTENTION{% endif %}

{% 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 %}

Attention

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 %}
{% if r.onchain %} {% endif %} {% if r.node_check %} {% 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 %}
{% endif %}

1. Movements in the period

{% if r.transactions %} {% if r.fiat %}{% endif %}{% if r.fiat %}{% endif %}{% if r.fiat %}{% endif %} {% for t in r.transactions %} {% if r.fiat %}{% endif %} {% endfor %} {% if r.fiat %}{% endif %} {% if not r.reconciliation.ok %} {% if r.fiat %}{% endif %} {% endif %}
Date (UTC)TransactionKindAmountFeeBalanceAmount {{ r.fiat.currency }}
Opening balance, {{ r.period.start.time_utc | utc }}{{ r.opening.total_btc }}
{{ 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 }}{{ (t.fiat.amount | acct) if t.fiat.amount and t.amount_sat else "" }}
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 }}
Difference between the running balance and the holdings on the chain{{ r.reconciliation.diff_sat | btc | acct }}

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 %}

2. Holdings at the end of the period · {{ r.period.end.time_utc | utc }}

{% for a in r.closing.addresses %}{% for o in a.outputs %} {% endfor %}{% endfor %}
UTXOLocked 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 }}
{% if r.closing.coins %}

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 %}.

{% endif %} {% if r.pending_transactions %}

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 %}
{% set n = namespace(i=0) %} {% for a in r.closing.addresses %}{% for o in a.outputs %}{% set n.i = n.i + 1 %}

3.{{ n.i }} UTXO {{ tx(o.txid) }}:{{ o.vout }}

Proof of control of the scriptPubKey

{% if a.proof %} {% set stamp = a.proof.stamp.height ~ " " ~ a.proof.stamp.hash %} {% else %} {% 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"]]) }}
{% if not a.proof.verified %}{{ a.proof.state | upper }} the proof does not verify; see the output above{% endif %} {% if a.proof.before_coins == "all" %}The proof predates this UTXO: it shows control of the script, and the lookup below says when the UTXO arrived.{% elif a.proof.before_coins == "some" %}The proof predates some of the UTXOs at this script.{% endif %}
verifyNO PROOF

On the chain

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 "]]) }}
{% if o.onchain.status == "after_proof" %}AFTER THE PROOF received after block {{ o.onchain.at }}, so this proof predates the UTXO; a proof dated later, for example the year-end one, closes that {% elif o.onchain.status == "spent_since" %}SPENT SINCE {% if o.spent_by %}spent after the period, at block {{ o.spent_by.height }}, by {{ tx(o.spent_by.txid) }}{% else %}not in the UTXO set any more; spent after the history for this statement was read{% endif %} {% elif o.onchain.status == "contradicted" %}CONTRADICTED the chain disagrees with the statement {% elif o.onchain.status != "matches" %}NOT RUN the lookup failed when this statement was prepared; see the output above{% endif %} {% else %}
bip322 audit holdings \
{{ o.txid }}:{{ o.vout }} --at {{ o.proof.stamp.height if o.proof else r.period.end.height }}
NOT RUN not run when this statement was prepared {% endif %}
{% endfor %}{% endfor %}
{% if r.closing.coins %}

4. How a proof of control works

{% set m = t.muted %}{% set k = t.ink %}{% set st = t.strong %}{% set ln = t.rule_strong %}{% set mono = t.font_mono %}{# the stacks are escaped in attributes: their quotes would end one #} {# the figure's grid, in the units of its viewBox: three columns of boxes (the left edge of each) and two rows (their top); the lines of text inside a box, and what sits between the boxes, are placed by hand from these #} {% set left = 1 %}{% set mid = 191 %}{% set right = 454 %}{% set top = 50 %}{% set low = 270 %} {% set narrow = 166 %}{% set wide = 246 %}{# the width of a box in the left column, and in the other two #} {% set pad = 10 %}{% set indent = 55 %}{% set step = 22 %}{# from a box's edge: its text, a field's name, a numbered step's second line #} {% set value_dx = 81 %}{# from a field's name to its value: 15 characters of the 9px mono, the longest name ("scriptSig OP_0") and a space #} {% macro val(x, y, text, role) -%} {{ text }} {%- endmacro %} {% macro pill(x, y, w, text, size=8.5) -%} {{ text }} {%- endmacro %} {% macro field(x, y, name, text, role=None) -%} {{ name }}{% if role %}{{ val(x + value_dx, y, text, role) }}{% else %}{{ text }}{% endif %} {%- endmacro %} {% macro txbox(x, y, h, title, sub) -%} {{ title }} {{ sub }} input output {%- endmacro %} Message {{ fig.title }} block: {{ fig.block }} the block line dates the proof {{ pill(34, 142, 100, "SHA-256, tagged") }} {{ val(38, 180, "", "hash") }} {{ txbox(mid, top, 156, "to_spend", "a transaction never broadcast") }} {{ field(mid + indent, 90, "prevout", "0000…00:ffffffff") }} the null outpoint: no such coin {{ field(mid + indent, 118, "scriptSig OP_0", "", "hash") }} the message, committed to the transaction {{ field(mid + indent, 160, "value", "0 sat") }} {{ field(mid + indent, 174, "scriptPubKey", "", "script") }} the lock being proven, with nothing in it {{ txbox(right, top, 156, "On the chain", "a real transaction, confirmed") }} {{ field(right + indent, 90, "prevout", "") }} whoever paid the wallet {{ field(right + indent, 118, "witness", "") }} a payment to the wallet {{ field(right + indent, 160, "value", "", "amount") }} {{ field(right + indent, 174, "scriptPubKey", "", "script") }} the same lock, with coins in it {{ pill(221, 218, 186, "SHA-256 twice, over the raw transaction") }} {{ val(265, 254, "", "txid") }} {{ pill(484, 218, 186, "SHA-256 twice, over the raw transaction") }} the UTXO {{ val(541, 254, ":", "utxo") }} {{ txbox(mid, low, 240, "to_sign", "a transaction never broadcast") }} {{ field(mid + indent, 310, "prevout", "", "txid") }} output 0 of to_spend, the empty one {{ field(mid + indent, 338, "witness", "", "proof") }} what unlocks the scriptPubKey {{ field(mid + indent, 380, "value", "0 sat") }} {{ field(mid + indent, 394, "scriptPubKey", "OP_RETURN") }} nothing can spend it every signature in the witness commits to the txid above, and so to the message The proof to_sign's witness, base64- encoded with the prefix smp: smpBABHMEQCI… Verifier 1 rebuilds to_spend from {{ val(right + step, 321, "", "hash") }} and {{ val(498, 334, "", "script") }} 2 rebuilds to_sign from {{ val(right + step, 367, "", "txid") }} and {{ val(498, 380, "", "proof") }} 3 checks, as a node would for any spend, that {{ val(500, 413, "", "proof") }} unlocks {{ val(right + step, 426, "", "script") }} 4 finds the UTXO on the chain: {{ val(right + step, 459, ":", "utxo") }} unspent, holding {{ val(right + step, 472, "", "amount") }} behind {{ val(right + step, 485, "", "script") }} 1–3: the verify command; 4: the lookup

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.

{% set S = 'scriptPubKey' %} {% set H = 'hash of the message' %} {% set X = 'txid of to_spend' %} {% set P = 'proof' %} {% set U = 'UTXO' %} {% set B = 'block' %}

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.

{% endif %}

Notes

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.