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.
- bip322_report-0.9.1/LICENSE +21 -0
- bip322_report-0.9.1/PKG-INFO +302 -0
- bip322_report-0.9.1/README.md +273 -0
- bip322_report-0.9.1/bip322_report.egg-info/PKG-INFO +302 -0
- bip322_report-0.9.1/bip322_report.egg-info/SOURCES.txt +39 -0
- bip322_report-0.9.1/bip322_report.egg-info/dependency_links.txt +1 -0
- bip322_report-0.9.1/bip322_report.egg-info/entry_points.txt +2 -0
- bip322_report-0.9.1/bip322_report.egg-info/requires.txt +12 -0
- bip322_report-0.9.1/bip322_report.egg-info/top_level.txt +1 -0
- bip322_report-0.9.1/bip322report/__init__.py +18 -0
- bip322_report-0.9.1/bip322report/_version.py +1 -0
- bip322_report-0.9.1/bip322report/cli.py +482 -0
- bip322_report-0.9.1/bip322report/coverage.py +285 -0
- bip322_report-0.9.1/bip322report/fiat.py +82 -0
- bip322_report-0.9.1/bip322report/fonts/JetBrainsMono-Bold.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/JetBrainsMono-Regular.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/LICENSE.md +94 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSans3-Bold.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSans3-It.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSans3-Regular.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSans3-Semibold.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Bold.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSerif4-It.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Regular.woff2 +0 -0
- bip322_report-0.9.1/bip322report/fonts/SourceSerif4-Semibold.woff2 +0 -0
- bip322_report-0.9.1/bip322report/handbook.md +212 -0
- bip322_report-0.9.1/bip322report/history.py +376 -0
- bip322_report-0.9.1/bip322report/period.py +143 -0
- bip322_report-0.9.1/bip322report/render.py +375 -0
- bip322_report-0.9.1/bip322report/report.py +669 -0
- bip322_report-0.9.1/bip322report/templates/report.html +401 -0
- bip322_report-0.9.1/pyproject.toml +55 -0
- bip322_report-0.9.1/setup.cfg +4 -0
- bip322_report-0.9.1/tests/test_cli.py +376 -0
- bip322_report-0.9.1/tests/test_history.py +242 -0
- bip322_report-0.9.1/tests/test_pins.py +35 -0
- bip322_report-0.9.1/tests/test_render.py +135 -0
- bip322_report-0.9.1/tests/test_report.py +614 -0
- bip322_report-0.9.1/tests/test_report_regtest.py +157 -0
- bip322_report-0.9.1/tests/test_shell.py +141 -0
- 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.
|