bip322-report 0.9.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (41) hide show
  1. bip322_report-0.9.1/LICENSE +21 -0
  2. bip322_report-0.9.1/PKG-INFO +302 -0
  3. bip322_report-0.9.1/README.md +273 -0
  4. bip322_report-0.9.1/bip322_report.egg-info/PKG-INFO +302 -0
  5. bip322_report-0.9.1/bip322_report.egg-info/SOURCES.txt +39 -0
  6. bip322_report-0.9.1/bip322_report.egg-info/dependency_links.txt +1 -0
  7. bip322_report-0.9.1/bip322_report.egg-info/entry_points.txt +2 -0
  8. bip322_report-0.9.1/bip322_report.egg-info/requires.txt +12 -0
  9. bip322_report-0.9.1/bip322_report.egg-info/top_level.txt +1 -0
  10. bip322_report-0.9.1/bip322report/__init__.py +18 -0
  11. bip322_report-0.9.1/bip322report/_version.py +1 -0
  12. bip322_report-0.9.1/bip322report/cli.py +482 -0
  13. bip322_report-0.9.1/bip322report/coverage.py +285 -0
  14. bip322_report-0.9.1/bip322report/fiat.py +82 -0
  15. bip322_report-0.9.1/bip322report/fonts/JetBrainsMono-Bold.woff2 +0 -0
  16. bip322_report-0.9.1/bip322report/fonts/JetBrainsMono-Regular.woff2 +0 -0
  17. bip322_report-0.9.1/bip322report/fonts/LICENSE.md +94 -0
  18. bip322_report-0.9.1/bip322report/fonts/SourceSans3-Bold.woff2 +0 -0
  19. bip322_report-0.9.1/bip322report/fonts/SourceSans3-It.woff2 +0 -0
  20. bip322_report-0.9.1/bip322report/fonts/SourceSans3-Regular.woff2 +0 -0
  21. bip322_report-0.9.1/bip322report/fonts/SourceSans3-Semibold.woff2 +0 -0
  22. bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Bold.woff2 +0 -0
  23. bip322_report-0.9.1/bip322report/fonts/SourceSerif4-It.woff2 +0 -0
  24. bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Regular.woff2 +0 -0
  25. bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Semibold.woff2 +0 -0
  26. bip322_report-0.9.1/bip322report/handbook.md +212 -0
  27. bip322_report-0.9.1/bip322report/history.py +376 -0
  28. bip322_report-0.9.1/bip322report/period.py +143 -0
  29. bip322_report-0.9.1/bip322report/render.py +375 -0
  30. bip322_report-0.9.1/bip322report/report.py +669 -0
  31. bip322_report-0.9.1/bip322report/templates/report.html +401 -0
  32. bip322_report-0.9.1/pyproject.toml +55 -0
  33. bip322_report-0.9.1/setup.cfg +4 -0
  34. bip322_report-0.9.1/tests/test_cli.py +376 -0
  35. bip322_report-0.9.1/tests/test_history.py +242 -0
  36. bip322_report-0.9.1/tests/test_pins.py +35 -0
  37. bip322_report-0.9.1/tests/test_render.py +135 -0
  38. bip322_report-0.9.1/tests/test_report.py +614 -0
  39. bip322_report-0.9.1/tests/test_report_regtest.py +157 -0
  40. bip322_report-0.9.1/tests/test_shell.py +141 -0
  41. bip322_report-0.9.1/tests/test_walkthrough.py +33 -0
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Embedded Nation AB
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,302 @@
1
+ Metadata-Version: 2.4
2
+ Name: bip322-report
3
+ Version: 0.9.1
4
+ Summary: The statement of a period in which every coin is backed by a verified BIP-322 proof of control: holdings, movements, reconciliation, HTML/CSV/PDF, on top of bip322-audit
5
+ Author: Embedded Nation AB
6
+ License-Expression: MIT AND OFL-1.1
7
+ Project-URL: Homepage, https://github.com/embeddednation/bip322-report
8
+ Project-URL: Changelog, https://github.com/embeddednation/bip322-report/blob/main/CHANGELOG.md
9
+ Keywords: bitcoin,bip322,proof of control,audit,multisig,psbt
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Programming Language :: Python :: 3
12
+ Classifier: Intended Audience :: Financial and Insurance Industry
13
+ Classifier: Topic :: Security :: Cryptography
14
+ Classifier: Topic :: Office/Business :: Financial :: Accounting
15
+ Requires-Python: >=3.11
16
+ Description-Content-Type: text/markdown
17
+ License-File: LICENSE
18
+ License-File: bip322report/fonts/LICENSE.md
19
+ Requires-Dist: bip322-audit<0.13,>=0.12.1
20
+ Requires-Dist: bip322-core<0.12,>=0.11.1
21
+ Requires-Dist: jinja2>=3.1
22
+ Provides-Extra: kernel
23
+ Requires-Dist: bip322-audit[kernel]<0.13,>=0.12.1; extra == "kernel"
24
+ Provides-Extra: pdf
25
+ Requires-Dist: weasyprint>=60; extra == "pdf"
26
+ Provides-Extra: dev
27
+ Requires-Dist: pytest>=8; extra == "dev"
28
+ Dynamic: license-file
29
+
30
+ # bip322-report
31
+
32
+ The statement of a period, in which every coin is backed by a verified BIP-322
33
+ proof of control. Built on [bip322-audit](https://github.com/embeddednation/bip322-audit)
34
+ (the proof bundles) and [bip322-core](https://github.com/embeddednation/bip322-core)
35
+ (the BIP-322 work), for the owner's side: it reads the node wallet's transaction
36
+ history and the ledger of proof bundles, and writes the statement of a period.
37
+
38
+ A statement answers, for a wallet and a period:
39
+
40
+ * what the wallet held at the start and at the end (as of two block heights),
41
+ * every movement in between, with the wallet's side of each transaction and the exact fee (unknown only when others funded the transaction too),
42
+ * that opening + net = closing,
43
+ * and for each closing coin, the proof of control that lists it, re-verified against the node when the report is made.
44
+
45
+ Two ways of working with it, the same tool for both:
46
+
47
+ * **Continuously, for yourself.** Before broadcasting a spend, prove its change address (`bip322-audit prove ADDRESS --ledger LEDGER`, sign, finalize): a valid proof means the quorum controls where the change goes. New deposit addresses the same way, or with `snapshot --skip-proven LEDGER` after the fact. A report can then be produced at any time, with every coin backed by your own signed message; a coin that arrived after its proof is marked as such (the proof shows control, not the holding).
48
+ * **On demand, for an auditor.** After the period's end, take one full bundle (no `--skip-proven`) over every output under a message that names the audit, and produce the report for the year. Every closing coin then has a proof dated after the closing block, and the chain lookup at that block shows the coin held. The auditor verifies the bundles and the report on their own node with `bip322-audit verify` and `bitcoin-cli`.
49
+
50
+ The whole flow, for the holder and for the auditor, is in the [handbook](bip322report/handbook.md); `bip322-report handbook` prints it.
51
+
52
+ ## Install
53
+
54
+ One line, into a fresh venv; bip322-audit and bip322-core come along, and the
55
+ three commands land in the venv's `bin`:
56
+
57
+ ```sh
58
+ python3 -m venv ~/.bip322 && ~/.bip322/bin/pip install "bip322-report[kernel,pdf]"
59
+ export PATH="$HOME/.bip322/bin:$PATH"
60
+ bip322 engines && bip322-audit help && bip322-report help
61
+ ```
62
+
63
+ Leave out `[kernel]` on anything but CPython 3.12 / Linux x86_64 (btclib
64
+ remains as the verifier). For a reproducible, hash-pinned install, clone and
65
+ use the setup script:
66
+
67
+ ```sh
68
+ git clone https://github.com/embeddednation/bip322-report.git && cd bip322-report
69
+ ./setup.sh # venv, hash-pinned dependencies, bip322-core and bip322-audit at their pinned tags, tests
70
+ export PATH="$PWD/.venv/bin:$PATH"
71
+ ```
72
+
73
+ `setup.sh` installs the two packages from git at the tags named in the script
74
+ (`CORE_REF`, `AUDIT_REF`); `./setup.sh --core ../bip322-core --audit ../bip322-audit`
75
+ uses local checkouts, which is the development setup. Nothing here needs the
76
+ wallet descriptor: the history comes from the node wallet, the proofs from the
77
+ ledger.
78
+
79
+ PDF output is optional. It needs WeasyPrint's system libraries, then the
80
+ renderer itself, hash-pinned like everything else:
81
+
82
+ ```sh
83
+ sudo apt install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b # Debian/Ubuntu
84
+ ./setup.sh --pdf # installs requirements-pdf.lock
85
+ ```
86
+
87
+ Without it `report` still writes the HTML (print it to PDF from a browser)
88
+ plus the JSON and the CSV; `--pdf` then says what is missing.
89
+
90
+ ## The flow
91
+
92
+ ```sh
93
+ # a ledger: any directory where the bip322-audit bundles accumulate.
94
+ # before broadcasting a spend: prove its change address
95
+ bip322-audit -w treasury prove bc1q...change... --text "Proof of control {date}" --ledger ledger
96
+ # sign to_sign/*.psbt on the cosigners' devices, put the results in signed/, then
97
+ bip322-audit finalize ledger/snapshot-<date>-<height> # valid -> broadcast
98
+ # anything that arrived on addresses not yet proven (deposits):
99
+ bip322-audit -w treasury snapshot --text "Proof of control {date}" --skip-proven ledger
100
+
101
+ # the year's statement
102
+ bip322-report -w treasury --year 2026 --ledger ledger
103
+ # -> treasury-2026/treasury-2026.json, .html, .csv (and .pdf with --pdf); -o DIR names both
104
+ ```
105
+
106
+ `bip322-report` prints a summary on stderr and the directory on stdout. For the
107
+ year, made in January from the year-end bundle:
108
+
109
+ ```
110
+ treasury: 2026 blocks 878000 -> 931234 ref 009ab805de61d672
111
+ opening 1.23400000 BTC closing 1.03398110 BTC net -0.20001890 BTC (3 transactions, fees 0.00001890 BTC)
112
+ reconciliation: ok
113
+ proof coverage: 2/2 closing outputs, 1.03398110 BTC; stamped after the period's end: 2/2
114
+ bundle 2026-year-end/proofs.json: stamp 931871, signatures 2/2, verified, used for 2
115
+ other bundles in the ledger, not used: 2
116
+ on chain (holdings by UTXO at the block of its proof): 2/2 held when proven
117
+ RESULT: OK
118
+ ```
119
+
120
+ `ref` identifies the facts stated (heights, coins, movements, the proofs
121
+ chosen): the same facts give the same reference whenever the report is made.
122
+ A period whose end is still in the future says `(to tip)`; when the period
123
+ ends at the tip the history was read at, a line
124
+ `node check (listunspent vs closing coins): ok` follows.
125
+
126
+ `RESULT: ATTENTION` means the reconciliation failed, a closing UTXO has no
127
+ verified proof, a lookup on the chain contradicts the statement, or the node
128
+ wallet disagrees with the closing UTXOs; the summary and the report say
129
+ which. An uncovered UTXO is fixed by a new bundle with `--skip-proven`.
130
+ `report` exits 0 when the result is OK, 1 when it is ATTENTION and 2 on an
131
+ error.
132
+
133
+ ## Commands
134
+
135
+ ```
136
+ bip322-report [--cli CMD] [-w NAME] history [-o history.json]
137
+ bip322-report [--cli CMD] block WHEN
138
+ bip322-report [--cli CMD] [-w NAME] balance [--at WHEN | --height H] [--history FILE]
139
+ bip322-report [--cli CMD] [-w NAME] (--year Y | --from WHEN --to WHEN | --from-height H --to-height H)
140
+ [--ledger DIR]... [--holder NAME] [--history FILE]
141
+ [--rates CSV | --rate N] [--currency CODE] [--dust SATS [--dust-force]] [--explorer URL] [--pdf] [--theme journal|light|paper|dark] [--heading STYLE] [-o DIR]
142
+ bip322-report help [COMMAND]
143
+ ```
144
+
145
+ The statement is the default command (`report` names it explicitly, as in `help report`).
146
+
147
+ * `history` reads every transaction of the node wallet once and writes it as
148
+ JSON; `balance` and `report` accept it with `--history`, so reports can be
149
+ regenerated without the wallet, or exactly reproduced from the same data.
150
+ A history of another chain, or one whose tip block is not in the node's
151
+ chain (a reorganisation, another network), is refused.
152
+ * `block` resolves a UTC date or instant to the last block before it, the
153
+ rule every period uses. `--year 2026` opens on the last block before
154
+ 2026-01-01 and closes on the last block before 2027-01-01, or on the tip when
155
+ that is still in the future (the tip the history was read at). The heights
156
+ used are printed in the report, and `--from-height/--to-height` reproduce a
157
+ report exactly. The period is given one way only. When the end is in the
158
+ past and the node has no block after it yet (still syncing, or behind), the
159
+ closing block is not settled and `report` refuses.
160
+ * `--ledger` takes a directory tree of bundles, one bundle directory or one
161
+ `proofs.json`, and may repeat. A path that does not exist or holds no
162
+ bundle is an error, so a mistyped path does not read as "no proofs".
163
+ * `--rates date,rate.csv` or `--rate N` with `--currency` adds a fiat value
164
+ per movement (the rate in force is the latest dated row on or before the
165
+ transaction's day). Nothing is fetched; the rates are yours. `--rates`
166
+ and `--rate` exclude each other, `--currency` needs one of them, and a
167
+ rate or a row that cannot be read is an error naming the file and line.
168
+ * `--explorer URL` sets the block explorer behind the links in the HTML and
169
+ PDF. The default is https://mempool.space on mainnet and no links on any
170
+ other chain; `''` for none.
171
+
172
+ ## What is in the report
173
+
174
+ The report is laid out as a statement the holder prepares for examination,
175
+ following the conventions accountants expect: units declared once in the
176
+ header, negatives in parentheses, a single rule above and a double rule under
177
+ totals, right-aligned tabular figures with all eight decimals, a running
178
+ balance in the movements table, a reference number on every page, and a
179
+ section on the basis of preparation and its limitations (point in time,
180
+ control is not title, completeness rests on the holder's representation).
181
+ The wallet is named by the node wallet's name; `--holder NAME` puts the holder's name in the header. Sections 1 and 2 are kept whole when they fit on a page.
182
+
183
+ * Period: the instants asked for and the two blocks they resolved to.
184
+ * First, in the style of a bank statement, the movements: opening balance,
185
+ each transaction with its running balance, closing balance, which is the
186
+ holdings on the chain; a difference from the running balance is printed
187
+ in red, agreement in one quiet sentence.
188
+ * Holdings at the end of the period: the wallet's UTXOs with the
189
+ scriptPubKey each is locked to, then one page per UTXO in two steps, each
190
+ a pasteable command with what it printed. *Proof of control of the scriptPubKey*: message, proof, and
191
+ `bip322 verifymessage <scriptPubKey> ...` with its verdict. *On the
192
+ chain*: `bip322 audit holdings TXID:VOUT --at <block of the proof>`, the
193
+ block named in the proof's message, which reports the same scriptPubKey as
194
+ the UTXO's lock, the amount, and that it was held at that block (confirmed
195
+ by then and unspent now). Control and holding are thus shown at one block.
196
+ The values a reader matches by eye are coloured alike wherever they appear
197
+ on the page (scriptPubKey, block, proof, UTXO, amount; the theme's
198
+ palette), and the commands and their output are set in monospace.
199
+ `--theme` picks `journal` (the default): a warm canvas,
200
+ greys, ruled tables, the values in the palette's base colours with red
201
+ for the block, set in a serif for reading and a sans for labels and
202
+ tables; or `light`, `dark` (Solarized) or `paper` (white). `--heading`
203
+ picks how section headings are set: `number` (the number in red), `tab`
204
+ (a red tab over a grey rule), `redrule`, or `underline`.
205
+ * Typefaces: Adobe's Source Serif 4 and Source Sans 3 and JetBrains Mono
206
+ (SIL Open Font License) are bundled and embedded in the PDF, so the
207
+ statement looks the same on every machine; `report` copies them to
208
+ `<out>/fonts/` for the HTML. The commands' output is the finding;
209
+ a badge follows only when something needs saying (INVALID, NO PROOF,
210
+ CONTRADICTED, a proof that predates its UTXO). BIP-322 defines
211
+ the proof for a scriptPubKey ("the key script to be proven"); an address
212
+ is that script encoded, which is why the statement can verify on the
213
+ bytes themselves. A coin that arrived after its proof (a change address
214
+ proven before the spend) is marked so; the year-end bundle gives it a
215
+ proof dated after it.
216
+ * `--dust SATS`: an output of at most SATS satoshi is dust when others
217
+ paid it to the wallet in a transaction that spends nothing of the
218
+ wallet's. Dust is left out of every figure, as if it had never been the
219
+ wallet's: balances, movements, reconciliation, coverage and the node
220
+ cross-check all ignore it, so the statement agrees with books that never
221
+ recorded it. The wallet's own change and consolidations are never dust,
222
+ whatever their size. Dust the wallet does spend is booked in the block
223
+ that spends it, as a movement of its own (`dust swept`) just before the
224
+ spending transaction, whose amounts and fee stay exact; an opening
225
+ balance is therefore always the closing balance of the statement
226
+ before, whatever happens to dust later. The dust held at
227
+ the closing block is listed in the JSON and counted in a note at the
228
+ end; spending it would cost more in fees than it holds, so a holder
229
+ leaves it (and spending unsolicited dust links addresses). What is
230
+ proven is control, not its absence. A negative threshold is refused,
231
+ and one above 10000 satoshi needs `--dust-force`.
232
+ * Exceptions only: nothing in the statement says OK beyond the figures.
233
+ Four checks decide the result: opening + net = closing; every closing
234
+ UTXO has a verified proof; no lookup on the chain contradicts the
235
+ statement (another scriptPubKey or amount); and, when the period ends
236
+ at the tip the history was read at, the node wallet's `listunspent`
237
+ agrees with the closing UTXOs. When one fails, an ATTENTION mark and a
238
+ checklist with the failing item appear on the first page. Shown on the
239
+ UTXO's page but not failed checks: a proof whose block is before the
240
+ period's end (the page names the proof's block), a UTXO received after
241
+ its proof, and a UTXO spent after the period (SPENT SINCE, with the
242
+ spending transaction; `spent_by` in the JSON). A lookup or a node check
243
+ that could not run is shown as not run, with the reason.
244
+ * The node cross-check counts only while the node's tip is the history's,
245
+ before and after `listunspent`; otherwise it is not run. An output that
246
+ an unconfirmed transaction spends, or a coinbase output not yet mature,
247
+ is missing from `listunspent` but still in the chain (`gettxout`): it is
248
+ noted, not a mismatch.
249
+ * At the end, on a page of its own, section 4: how a BIP-322 proof of
250
+ control works, for any kind of scriptPubKey, with a figure of the two
251
+ virtual transactions field by field: the message hashed into to_spend's
252
+ input, to_spend's txid consumed by to_sign's input, the proof as its
253
+ witness, the UTXO on the chain under the same scriptPubKey, and the
254
+ verifier's checks; the prose to match, with this wallet's script as the
255
+ example, and what a proof shows and does not; then
256
+ brief notes: units, wallet and blocks, preparation, the files alongside
257
+ and the tools that check them.
258
+ * Every movement: date, txid, kind, amount (received, or sent before the
259
+ fee), fee and the running balance; the total row's amount is the sum of
260
+ the rows. Kinds: `receive` (nothing of the wallet's spent), `send`,
261
+ `internal` (everything came back), and `mixed` for a transaction funded
262
+ by the wallet and by others, whose fee is unknown and shown as such and
263
+ whose amount is the wallet's net. Within a block, movements follow their
264
+ position in the block. The CSV and the JSON add the net and the outputs
265
+ on each side: for a payment the address paid, or no address for an
266
+ output that has none (`no-address` in the CSV, null in the JSON).
267
+ Unconfirmed transactions are listed apart and count in no balance;
268
+ abandoned and replaced ones are not listed.
269
+
270
+ The report names no descriptor, no xpub and no derivation path; the bundles
271
+ it cites carry none either. Bundle names are relative to the ledger, never
272
+ absolute paths: `<bundle dir>/proofs.json` also when `--ledger` is itself a
273
+ bundle directory or a `proofs.json`, with the ledger directory's name in
274
+ front when two ledgers hold bundles of the same name (an error if that
275
+ does not tell them apart). Only the bundles cited on a closing UTXO's page
276
+ are listed; whatever else the ledger holds (another wallet's bundles, older
277
+ ones) appears as a bare count.
278
+
279
+ **What to hand over:** the report directory. Besides the HTML, JSON and
280
+ CSV (and the PDF), all named after the directory, it holds `ledger/` with
281
+ a copy of each `proofs.json` the statement cites, and no other, under the
282
+ name the report uses, so a reader can run `bip322-audit verify` on each.
283
+ Never hand over the ledger itself: the bundles' PSBT files carry the
284
+ wallet's xpubs. `--no-proofs` skips the copy.
285
+
286
+ ## Layout
287
+
288
+ ```
289
+ bip322report/history.py the node wallet's transactions; coins and balance at any height
290
+ bip322report/period.py a period resolved to block heights
291
+ bip322report/coverage.py the ledger's bundles, re-verified; which proof backs which coin
292
+ bip322report/fiat.py optional valuation from user-supplied rates
293
+ bip322report/report.py the report as data, and the text summary
294
+ bip322report/render.py HTML (Jinja2 template), CSV, PDF (WeasyPrint), themes, bundled fonts
295
+ bip322report/cli.py the bip322-report command
296
+ tests/ a fake node covering both packages' RPCs; an end-to-end test on regtest Core
297
+ examples/report_walkthrough.sh the flow on a throwaway regtest node, into a fresh directory (or a new or empty DIR)
298
+ ```
299
+
300
+ The regtest test and the walkthrough need a Bitcoin Core binary: set
301
+ `BITCOIN_CORE_DIR`, or run `refcheck/fetch.sh` in the bip322-core checkout the
302
+ venv was set up from. CI checks out both dependencies at their pinned tags.
@@ -0,0 +1,273 @@
1
+ # bip322-report
2
+
3
+ The statement of a period, in which every coin is backed by a verified BIP-322
4
+ proof of control. Built on [bip322-audit](https://github.com/embeddednation/bip322-audit)
5
+ (the proof bundles) and [bip322-core](https://github.com/embeddednation/bip322-core)
6
+ (the BIP-322 work), for the owner's side: it reads the node wallet's transaction
7
+ history and the ledger of proof bundles, and writes the statement of a period.
8
+
9
+ A statement answers, for a wallet and a period:
10
+
11
+ * what the wallet held at the start and at the end (as of two block heights),
12
+ * every movement in between, with the wallet's side of each transaction and the exact fee (unknown only when others funded the transaction too),
13
+ * that opening + net = closing,
14
+ * and for each closing coin, the proof of control that lists it, re-verified against the node when the report is made.
15
+
16
+ Two ways of working with it, the same tool for both:
17
+
18
+ * **Continuously, for yourself.** Before broadcasting a spend, prove its change address (`bip322-audit prove ADDRESS --ledger LEDGER`, sign, finalize): a valid proof means the quorum controls where the change goes. New deposit addresses the same way, or with `snapshot --skip-proven LEDGER` after the fact. A report can then be produced at any time, with every coin backed by your own signed message; a coin that arrived after its proof is marked as such (the proof shows control, not the holding).
19
+ * **On demand, for an auditor.** After the period's end, take one full bundle (no `--skip-proven`) over every output under a message that names the audit, and produce the report for the year. Every closing coin then has a proof dated after the closing block, and the chain lookup at that block shows the coin held. The auditor verifies the bundles and the report on their own node with `bip322-audit verify` and `bitcoin-cli`.
20
+
21
+ The whole flow, for the holder and for the auditor, is in the [handbook](bip322report/handbook.md); `bip322-report handbook` prints it.
22
+
23
+ ## Install
24
+
25
+ One line, into a fresh venv; bip322-audit and bip322-core come along, and the
26
+ three commands land in the venv's `bin`:
27
+
28
+ ```sh
29
+ python3 -m venv ~/.bip322 && ~/.bip322/bin/pip install "bip322-report[kernel,pdf]"
30
+ export PATH="$HOME/.bip322/bin:$PATH"
31
+ bip322 engines && bip322-audit help && bip322-report help
32
+ ```
33
+
34
+ Leave out `[kernel]` on anything but CPython 3.12 / Linux x86_64 (btclib
35
+ remains as the verifier). For a reproducible, hash-pinned install, clone and
36
+ use the setup script:
37
+
38
+ ```sh
39
+ git clone https://github.com/embeddednation/bip322-report.git && cd bip322-report
40
+ ./setup.sh # venv, hash-pinned dependencies, bip322-core and bip322-audit at their pinned tags, tests
41
+ export PATH="$PWD/.venv/bin:$PATH"
42
+ ```
43
+
44
+ `setup.sh` installs the two packages from git at the tags named in the script
45
+ (`CORE_REF`, `AUDIT_REF`); `./setup.sh --core ../bip322-core --audit ../bip322-audit`
46
+ uses local checkouts, which is the development setup. Nothing here needs the
47
+ wallet descriptor: the history comes from the node wallet, the proofs from the
48
+ ledger.
49
+
50
+ PDF output is optional. It needs WeasyPrint's system libraries, then the
51
+ renderer itself, hash-pinned like everything else:
52
+
53
+ ```sh
54
+ sudo apt install libpango-1.0-0 libpangoft2-1.0-0 libharfbuzz0b # Debian/Ubuntu
55
+ ./setup.sh --pdf # installs requirements-pdf.lock
56
+ ```
57
+
58
+ Without it `report` still writes the HTML (print it to PDF from a browser)
59
+ plus the JSON and the CSV; `--pdf` then says what is missing.
60
+
61
+ ## The flow
62
+
63
+ ```sh
64
+ # a ledger: any directory where the bip322-audit bundles accumulate.
65
+ # before broadcasting a spend: prove its change address
66
+ bip322-audit -w treasury prove bc1q...change... --text "Proof of control {date}" --ledger ledger
67
+ # sign to_sign/*.psbt on the cosigners' devices, put the results in signed/, then
68
+ bip322-audit finalize ledger/snapshot-<date>-<height> # valid -> broadcast
69
+ # anything that arrived on addresses not yet proven (deposits):
70
+ bip322-audit -w treasury snapshot --text "Proof of control {date}" --skip-proven ledger
71
+
72
+ # the year's statement
73
+ bip322-report -w treasury --year 2026 --ledger ledger
74
+ # -> treasury-2026/treasury-2026.json, .html, .csv (and .pdf with --pdf); -o DIR names both
75
+ ```
76
+
77
+ `bip322-report` prints a summary on stderr and the directory on stdout. For the
78
+ year, made in January from the year-end bundle:
79
+
80
+ ```
81
+ treasury: 2026 blocks 878000 -> 931234 ref 009ab805de61d672
82
+ opening 1.23400000 BTC closing 1.03398110 BTC net -0.20001890 BTC (3 transactions, fees 0.00001890 BTC)
83
+ reconciliation: ok
84
+ proof coverage: 2/2 closing outputs, 1.03398110 BTC; stamped after the period's end: 2/2
85
+ bundle 2026-year-end/proofs.json: stamp 931871, signatures 2/2, verified, used for 2
86
+ other bundles in the ledger, not used: 2
87
+ on chain (holdings by UTXO at the block of its proof): 2/2 held when proven
88
+ RESULT: OK
89
+ ```
90
+
91
+ `ref` identifies the facts stated (heights, coins, movements, the proofs
92
+ chosen): the same facts give the same reference whenever the report is made.
93
+ A period whose end is still in the future says `(to tip)`; when the period
94
+ ends at the tip the history was read at, a line
95
+ `node check (listunspent vs closing coins): ok` follows.
96
+
97
+ `RESULT: ATTENTION` means the reconciliation failed, a closing UTXO has no
98
+ verified proof, a lookup on the chain contradicts the statement, or the node
99
+ wallet disagrees with the closing UTXOs; the summary and the report say
100
+ which. An uncovered UTXO is fixed by a new bundle with `--skip-proven`.
101
+ `report` exits 0 when the result is OK, 1 when it is ATTENTION and 2 on an
102
+ error.
103
+
104
+ ## Commands
105
+
106
+ ```
107
+ bip322-report [--cli CMD] [-w NAME] history [-o history.json]
108
+ bip322-report [--cli CMD] block WHEN
109
+ bip322-report [--cli CMD] [-w NAME] balance [--at WHEN | --height H] [--history FILE]
110
+ bip322-report [--cli CMD] [-w NAME] (--year Y | --from WHEN --to WHEN | --from-height H --to-height H)
111
+ [--ledger DIR]... [--holder NAME] [--history FILE]
112
+ [--rates CSV | --rate N] [--currency CODE] [--dust SATS [--dust-force]] [--explorer URL] [--pdf] [--theme journal|light|paper|dark] [--heading STYLE] [-o DIR]
113
+ bip322-report help [COMMAND]
114
+ ```
115
+
116
+ The statement is the default command (`report` names it explicitly, as in `help report`).
117
+
118
+ * `history` reads every transaction of the node wallet once and writes it as
119
+ JSON; `balance` and `report` accept it with `--history`, so reports can be
120
+ regenerated without the wallet, or exactly reproduced from the same data.
121
+ A history of another chain, or one whose tip block is not in the node's
122
+ chain (a reorganisation, another network), is refused.
123
+ * `block` resolves a UTC date or instant to the last block before it, the
124
+ rule every period uses. `--year 2026` opens on the last block before
125
+ 2026-01-01 and closes on the last block before 2027-01-01, or on the tip when
126
+ that is still in the future (the tip the history was read at). The heights
127
+ used are printed in the report, and `--from-height/--to-height` reproduce a
128
+ report exactly. The period is given one way only. When the end is in the
129
+ past and the node has no block after it yet (still syncing, or behind), the
130
+ closing block is not settled and `report` refuses.
131
+ * `--ledger` takes a directory tree of bundles, one bundle directory or one
132
+ `proofs.json`, and may repeat. A path that does not exist or holds no
133
+ bundle is an error, so a mistyped path does not read as "no proofs".
134
+ * `--rates date,rate.csv` or `--rate N` with `--currency` adds a fiat value
135
+ per movement (the rate in force is the latest dated row on or before the
136
+ transaction's day). Nothing is fetched; the rates are yours. `--rates`
137
+ and `--rate` exclude each other, `--currency` needs one of them, and a
138
+ rate or a row that cannot be read is an error naming the file and line.
139
+ * `--explorer URL` sets the block explorer behind the links in the HTML and
140
+ PDF. The default is https://mempool.space on mainnet and no links on any
141
+ other chain; `''` for none.
142
+
143
+ ## What is in the report
144
+
145
+ The report is laid out as a statement the holder prepares for examination,
146
+ following the conventions accountants expect: units declared once in the
147
+ header, negatives in parentheses, a single rule above and a double rule under
148
+ totals, right-aligned tabular figures with all eight decimals, a running
149
+ balance in the movements table, a reference number on every page, and a
150
+ section on the basis of preparation and its limitations (point in time,
151
+ control is not title, completeness rests on the holder's representation).
152
+ The wallet is named by the node wallet's name; `--holder NAME` puts the holder's name in the header. Sections 1 and 2 are kept whole when they fit on a page.
153
+
154
+ * Period: the instants asked for and the two blocks they resolved to.
155
+ * First, in the style of a bank statement, the movements: opening balance,
156
+ each transaction with its running balance, closing balance, which is the
157
+ holdings on the chain; a difference from the running balance is printed
158
+ in red, agreement in one quiet sentence.
159
+ * Holdings at the end of the period: the wallet's UTXOs with the
160
+ scriptPubKey each is locked to, then one page per UTXO in two steps, each
161
+ a pasteable command with what it printed. *Proof of control of the scriptPubKey*: message, proof, and
162
+ `bip322 verifymessage <scriptPubKey> ...` with its verdict. *On the
163
+ chain*: `bip322 audit holdings TXID:VOUT --at <block of the proof>`, the
164
+ block named in the proof's message, which reports the same scriptPubKey as
165
+ the UTXO's lock, the amount, and that it was held at that block (confirmed
166
+ by then and unspent now). Control and holding are thus shown at one block.
167
+ The values a reader matches by eye are coloured alike wherever they appear
168
+ on the page (scriptPubKey, block, proof, UTXO, amount; the theme's
169
+ palette), and the commands and their output are set in monospace.
170
+ `--theme` picks `journal` (the default): a warm canvas,
171
+ greys, ruled tables, the values in the palette's base colours with red
172
+ for the block, set in a serif for reading and a sans for labels and
173
+ tables; or `light`, `dark` (Solarized) or `paper` (white). `--heading`
174
+ picks how section headings are set: `number` (the number in red), `tab`
175
+ (a red tab over a grey rule), `redrule`, or `underline`.
176
+ * Typefaces: Adobe's Source Serif 4 and Source Sans 3 and JetBrains Mono
177
+ (SIL Open Font License) are bundled and embedded in the PDF, so the
178
+ statement looks the same on every machine; `report` copies them to
179
+ `<out>/fonts/` for the HTML. The commands' output is the finding;
180
+ a badge follows only when something needs saying (INVALID, NO PROOF,
181
+ CONTRADICTED, a proof that predates its UTXO). BIP-322 defines
182
+ the proof for a scriptPubKey ("the key script to be proven"); an address
183
+ is that script encoded, which is why the statement can verify on the
184
+ bytes themselves. A coin that arrived after its proof (a change address
185
+ proven before the spend) is marked so; the year-end bundle gives it a
186
+ proof dated after it.
187
+ * `--dust SATS`: an output of at most SATS satoshi is dust when others
188
+ paid it to the wallet in a transaction that spends nothing of the
189
+ wallet's. Dust is left out of every figure, as if it had never been the
190
+ wallet's: balances, movements, reconciliation, coverage and the node
191
+ cross-check all ignore it, so the statement agrees with books that never
192
+ recorded it. The wallet's own change and consolidations are never dust,
193
+ whatever their size. Dust the wallet does spend is booked in the block
194
+ that spends it, as a movement of its own (`dust swept`) just before the
195
+ spending transaction, whose amounts and fee stay exact; an opening
196
+ balance is therefore always the closing balance of the statement
197
+ before, whatever happens to dust later. The dust held at
198
+ the closing block is listed in the JSON and counted in a note at the
199
+ end; spending it would cost more in fees than it holds, so a holder
200
+ leaves it (and spending unsolicited dust links addresses). What is
201
+ proven is control, not its absence. A negative threshold is refused,
202
+ and one above 10000 satoshi needs `--dust-force`.
203
+ * Exceptions only: nothing in the statement says OK beyond the figures.
204
+ Four checks decide the result: opening + net = closing; every closing
205
+ UTXO has a verified proof; no lookup on the chain contradicts the
206
+ statement (another scriptPubKey or amount); and, when the period ends
207
+ at the tip the history was read at, the node wallet's `listunspent`
208
+ agrees with the closing UTXOs. When one fails, an ATTENTION mark and a
209
+ checklist with the failing item appear on the first page. Shown on the
210
+ UTXO's page but not failed checks: a proof whose block is before the
211
+ period's end (the page names the proof's block), a UTXO received after
212
+ its proof, and a UTXO spent after the period (SPENT SINCE, with the
213
+ spending transaction; `spent_by` in the JSON). A lookup or a node check
214
+ that could not run is shown as not run, with the reason.
215
+ * The node cross-check counts only while the node's tip is the history's,
216
+ before and after `listunspent`; otherwise it is not run. An output that
217
+ an unconfirmed transaction spends, or a coinbase output not yet mature,
218
+ is missing from `listunspent` but still in the chain (`gettxout`): it is
219
+ noted, not a mismatch.
220
+ * At the end, on a page of its own, section 4: how a BIP-322 proof of
221
+ control works, for any kind of scriptPubKey, with a figure of the two
222
+ virtual transactions field by field: the message hashed into to_spend's
223
+ input, to_spend's txid consumed by to_sign's input, the proof as its
224
+ witness, the UTXO on the chain under the same scriptPubKey, and the
225
+ verifier's checks; the prose to match, with this wallet's script as the
226
+ example, and what a proof shows and does not; then
227
+ brief notes: units, wallet and blocks, preparation, the files alongside
228
+ and the tools that check them.
229
+ * Every movement: date, txid, kind, amount (received, or sent before the
230
+ fee), fee and the running balance; the total row's amount is the sum of
231
+ the rows. Kinds: `receive` (nothing of the wallet's spent), `send`,
232
+ `internal` (everything came back), and `mixed` for a transaction funded
233
+ by the wallet and by others, whose fee is unknown and shown as such and
234
+ whose amount is the wallet's net. Within a block, movements follow their
235
+ position in the block. The CSV and the JSON add the net and the outputs
236
+ on each side: for a payment the address paid, or no address for an
237
+ output that has none (`no-address` in the CSV, null in the JSON).
238
+ Unconfirmed transactions are listed apart and count in no balance;
239
+ abandoned and replaced ones are not listed.
240
+
241
+ The report names no descriptor, no xpub and no derivation path; the bundles
242
+ it cites carry none either. Bundle names are relative to the ledger, never
243
+ absolute paths: `<bundle dir>/proofs.json` also when `--ledger` is itself a
244
+ bundle directory or a `proofs.json`, with the ledger directory's name in
245
+ front when two ledgers hold bundles of the same name (an error if that
246
+ does not tell them apart). Only the bundles cited on a closing UTXO's page
247
+ are listed; whatever else the ledger holds (another wallet's bundles, older
248
+ ones) appears as a bare count.
249
+
250
+ **What to hand over:** the report directory. Besides the HTML, JSON and
251
+ CSV (and the PDF), all named after the directory, it holds `ledger/` with
252
+ a copy of each `proofs.json` the statement cites, and no other, under the
253
+ name the report uses, so a reader can run `bip322-audit verify` on each.
254
+ Never hand over the ledger itself: the bundles' PSBT files carry the
255
+ wallet's xpubs. `--no-proofs` skips the copy.
256
+
257
+ ## Layout
258
+
259
+ ```
260
+ bip322report/history.py the node wallet's transactions; coins and balance at any height
261
+ bip322report/period.py a period resolved to block heights
262
+ bip322report/coverage.py the ledger's bundles, re-verified; which proof backs which coin
263
+ bip322report/fiat.py optional valuation from user-supplied rates
264
+ bip322report/report.py the report as data, and the text summary
265
+ bip322report/render.py HTML (Jinja2 template), CSV, PDF (WeasyPrint), themes, bundled fonts
266
+ bip322report/cli.py the bip322-report command
267
+ tests/ a fake node covering both packages' RPCs; an end-to-end test on regtest Core
268
+ examples/report_walkthrough.sh the flow on a throwaway regtest node, into a fresh directory (or a new or empty DIR)
269
+ ```
270
+
271
+ The regtest test and the walkthrough need a Bitcoin Core binary: set
272
+ `BITCOIN_CORE_DIR`, or run `refcheck/fetch.sh` in the bip322-core checkout the
273
+ venv was set up from. CI checks out both dependencies at their pinned tags.