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

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

{# the summary: from the opening holdings to the closing ones, and what is proven #} {% set who = "holder" if r.wallets else "wallet" %} {% if r.fiat %}{% endif %}{% if r.fiat %}{% endif %} {% if r.fiat %}{% endif %} {% if r.fiat %}{% endif %}{% if r.fiat %}{% endif %} {% if r.totals.mixed_count %} {% if r.fiat %}{% endif %} {% endif %} {% if r.fiat %}{% endif %} {% if r.fiat %}{% endif %} {% if not r.reconciliation.ok %} {% if r.fiat %}{% endif %} {% endif %}
BTC{{ r.fiat.currency }}{{ r.fiat.currency }}/BTC
Opening holdings, {{ r.period.start.time_utc | utc }}{{ r.opening.total_btc }}{{ r.fiat.opening.value | acct }}{{ r.fiat.opening.rate or "" }}{% 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 }}{{ r.totals.fiat.received | acct }}
Sent{% if r.wallets %} to others{% endif %}{{ r.totals.sent_btc | paren }}{{ r.totals.fiat.sent | acct }}
Joint transactions, the {{ who }}'s net{{ r.totals.mixed_btc | acct }}{{ r.totals.fiat.mixed | acct }}{{ r.totals.mixed_count }}, funded by others too: their fees unknown
Network fees{{ r.totals.fees_btc | paren }}{{ r.totals.fiat.fees | acct }}
Closing holdings, {{ r.period.end.time_utc | utc }}{{ r.closing.total_btc }}{{ r.fiat.closing.value | acct }}{{ r.fiat.closing.rate or "" }}{% 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.wallets %} {% for _ in range(6 + (1 if r.totals.mixed_count else 0)) %}{% endfor %}{% if r.totals.mixed_count %}{% endif %} {% for w in r.wallets %} {% if r.totals.mixed_count %}{% endif %} {% endfor %} {% set wt = r.wallets_totals %} {% if r.totals.mixed_count %}{% endif %}
WalletOpeningReceivedSentJointTransfersFeesClosing
{{ w.name }}{% if w.history and w.history.source == "file" %} history kept {{ w.history.fetched_utc[:10] }}{% endif %} {{ w.opening_btc | nil }}{{ w.received_btc | nil }}{{ w.sent_btc | paren }}{{ w.mixed_btc | acct | nil }}{{ w.transfers_btc | acct | nil }}{{ w.fees_btc | paren }}{{ w.closing_btc | nil }}
{{ r.wallets | length }} wallet{{ "s" if r.wallets | length != 1 }}{{ wt.opening_btc }}{{ wt.received_btc | nil }}{{ wt.sent_btc | paren }}{{ wt.mixed_btc | acct | nil }}{{ wt.transfers_btc | acct | nil }}{{ wt.fees_btc | paren }}{{ wt.closing_btc }}
{% endif %} {% if r.ok %}

{{ proven }}

{% else %}

Attention

Not every check passed; the failing ones are marked here, and the section concerned says more.

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

1. Movements in the period

{% if r.transactions %} {% if r.wallets %}{% endif %}{% if r.fiat %}{% endif %}{% if r.wallets %}{% endif %}{% if r.fiat %}{% endif %}{% if r.fiat %}{% endif %} {% for t in r.transactions %} {% if r.wallets %}{% endif %} {% if r.fiat %}{% endif %} {% endfor %} {% if r.fiat %}{% endif %} {% if not r.reconciliation.ok %} {% if r.fiat %}{% endif %} {% endif %}
#Date (UTC)KindWalletCounterpartyAmountFeeBalanceAmount {{ r.fiat.currency }}
Opening balance, {{ r.period.start.time_utc | utc }}{{ r.opening.total_btc }}
{{ loop.index }}{{ t.time_utc[:10] }}
{{ t.time_utc[11:19] }}
{{ kinds.get(t.kind, t.kind) }}{{ t.wallet | replace("->", "→") }}{{ 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 }}{{ (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 }}

The number of a movement finds its transaction in section 4.{% if r.wallets %} A transfer between the {{ who }}'s wallets is one movement, its fee the only cost; the wallet column reads from → to.{% 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 %}

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

{% if r.closing.addresses %} {% for a in r.closing.addresses %} {% if r.wallets and (loop.first or a.wallet != loop.previtem.wallet) %}{% endif %} {% endfor %}
AddressReceivedUTXOsAmountProof of controlPage
{{ 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 %}.

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

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 %}
{% for a in r.closing.addresses %}

3.{{ loop.index }} Address {{ addr(a.address) }}

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

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, 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"]]) }}
{% 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" if a.outputs | length == 1 else "these UTXOs" }}: it shows control of the script, and the lookup below says when each UTXO arrived.{% elif a.proof.before_coins == "some" %}The proof predates some of the UTXOs at this script.{% endif %}
verifyNO PROOF

On the chain

{% for o in a.outputs %} {% endfor %}
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 "]]) }}
{% 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 %} {% if r.transactions %}

4. The transactions

Each movement of section 1 by its number: the transaction's identifier and the block that confirmed it.

{% for t in r.transactions %} {% endfor %}
#Date (UTC)TransactionBlock
{{ loop.index }}{{ t.time_utc[:10] }}{{ tx(t.txid) }}{% if t.kind == "swept" %} dust swept: the output's transaction{% endif %}{{ t.height }}
{% endif %}
{% if r.closing.coins %}

5. 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 }}, 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 transfer between them is one movement, in neither received nor sent, 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.