bip322-audit 0.12.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.
@@ -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,268 @@
1
+ Metadata-Version: 2.4
2
+ Name: bip322-audit
3
+ Version: 0.12.1
4
+ Summary: Proof of control of a wallet's coins at a block: BIP-322 proofs per funded address plus the on-chain checks an auditor runs on their own node
5
+ Author: Embedded Nation AB
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/embeddednation/bip322-audit
8
+ Project-URL: Changelog, https://github.com/embeddednation/bip322-audit/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
+ Requires-Dist: bip322-core<0.12,>=0.11.1
19
+ Provides-Extra: kernel
20
+ Requires-Dist: bip322-core[kernel]<0.12,>=0.11.1; extra == "kernel"
21
+ Provides-Extra: dev
22
+ Requires-Dist: pytest>=8; extra == "dev"
23
+ Dynamic: license-file
24
+
25
+ # bip322-audit
26
+
27
+ Proof of control of a wallet's coins at a block: one BIP-322 proof per funded
28
+ address, a block stamp inside the signed message, and the on-chain checks an
29
+ auditor runs on their own node. Built on
30
+ [bip322-core](https://github.com/embeddednation/bip322-core), which does the
31
+ BIP-322 work and never talks to a node; everything that does lives here, and
32
+ only through `bitcoin-cli`.
33
+
34
+ Two roles:
35
+
36
+ * **Owner**: `snapshot` (or `prove`, for given addresses), sign on the
37
+ cosigners' devices, `finalize`. Needs a node with the wallet loaded
38
+ (watch-only is enough).
39
+ * **Auditor**: `verify`, against any node that has the chain; `holdings` to
40
+ look up single outputs or addresses. No wallet, no index, no key material,
41
+ and nothing about the owner's wallet beyond the proven addresses.
42
+
43
+ The commands: `stamp`, `snapshot`, `prove`, `finalize`, `verify`, `holdings`,
44
+ `help`. With bip322-core installed they are also reachable as `bip322 audit ...`.
45
+
46
+ ## Install
47
+
48
+ One line, into a fresh venv, with bip322-core pulled in:
49
+
50
+ ```sh
51
+ python3 -m venv ~/.bip322 && ~/.bip322/bin/pip install "bip322-audit[kernel]"
52
+ export PATH="$HOME/.bip322/bin:$PATH"
53
+ ```
54
+
55
+ For a reproducible, hash-pinned install (what an auditor should do), clone and use the setup script:
56
+
57
+ ```sh
58
+ git clone https://github.com/embeddednation/bip322-audit.git && cd bip322-audit
59
+ ./setup.sh # venv, hash-pinned dependencies, bip322-core at the pinned tag, tests
60
+ export PATH="$PWD/.venv/bin:$PATH"
61
+ ```
62
+
63
+ `setup.sh` installs `bip322-core` from its git repository at the tag named
64
+ by `CORE_REF` in the script. `./setup.sh --core ../bip322-core` installs a local
65
+ checkout instead, which is the development setup and also the auditor's: clone both
66
+ repositories at the tags the owner names, check the commit hashes out of
67
+ band, and run `verify`.
68
+
69
+ The whole yearly flow across the three packages is in bip322-report's [handbook](https://github.com/embeddednation/bip322-report/blob/main/bip322report/handbook.md).
70
+
71
+ ## Workflow
72
+
73
+ For "we controlled these coins as of block N", repeatable whenever coins move:
74
+
75
+ ```sh
76
+ bip322-audit -w treasury snapshot --text "Annual audit {date}" # the node wallet's own descriptor
77
+ # -> snapshot-2026-09-14-912345/: snapshot.json, message.txt, to_sign/to_sign-01.psbt ... one per funded address
78
+ # sign every PSBT on the cosigners' devices, put the results into snapshot-.../signed/
79
+ bip322-audit finalize snapshot-2026-09-14-912345 # -> proofs.json (hand this to the auditor)
80
+ bip322-audit verify snapshot-2026-09-14-912345 --report audit-report.json
81
+ ```
82
+
83
+ `finalize` also asks the node wallet the coins came from (recorded in
84
+ `snapshot.json`) which listed outputs have been spent since, and records the
85
+ spending transactions in `proofs.json`. If coins move between the snapshot and
86
+ the audit, re-run `finalize` before handing `proofs.json` over; the proofs
87
+ themselves do not change. `--offline` skips that step and keeps the spends an
88
+ earlier run recorded. A PSBT that cannot be used is reported with its address
89
+ and file names, and files in `to_sign/` and `signed/` that were not used are
90
+ listed.
91
+
92
+ The node is reached with `--cli CMD` (default `bitcoin-cli`). `CMD` is split
93
+ like a shell command line, so a path with spaces is quoted inside it:
94
+ `--cli "'/opt/my node/bitcoin-cli' -signet"`. `--timeout SECONDS` bounds every
95
+ node call (default 600); a UTXO-set scan the tool gave up on keeps running on
96
+ the node. Chains: `main`, `test`, `testnet4`, `signet`, `regtest`.
97
+
98
+ `snapshot` reads the wallet's descriptor from the node wallet (`-w NAME`,
99
+ `listdescriptors`; `--descriptor FILE` overrides and is cross-checked against
100
+ the node), takes the block six behind the tip (`--depth`) as the stamp *and*
101
+ the snapshot height: the message ends with `block: HEIGHT HASH TIME` taken
102
+ from that block, and only outputs confirmed at that block are listed. Coins
103
+ come from `listunspent` on the node wallet (the only one loaded, or `-w NAME`)
104
+ or, when the node has no wallet, from a
105
+ `scantxoutset` of the descriptor (minutes on mainnet; the command says so
106
+ before it starts; `--source` forces either). The stamp, the coins, their
107
+ heights and block hashes are read under one tip: if a block arrives
108
+ meanwhile, they are read again. The template accepts `{date}`, `{time}`,
109
+ `{height}`, `{hash}` (date and time are the stamp block's header time, UTC);
110
+ anything else in braces is refused, as is a template with a `block:` line of
111
+ its own, and the message is checked against hardware signers' message rules.
112
+
113
+ `snapshot` covers one descriptor (`wsh(multi/sortedmulti)` or `wpkh`). Its
114
+ summary and `snapshot.json` say which (`descriptor`), and under `left_out`
115
+ how many outputs and how much the node listed that the bundle does not, and
116
+ why: coins of another descriptor in the same node wallet, coins on addresses
117
+ an earlier bundle proves. Descriptors of a kind it does not cover are named
118
+ in `notes`. The default directory is `snapshot-<date>-<height>`, with `-2`,
119
+ `-3` ... when that exists; `-o DIR --force` rewrites a bundle that holds no
120
+ signed PSBTs and no `proofs.json` yet, and nothing else.
121
+
122
+ `proofs.json` names addresses, not a wallet: the message, the stamp, and per
123
+ address the proof and its outputs, plus the policy string (`2 of 3`). It is
124
+ built from a fixed list of fields (`tool`, `chain`, `stamp`, `message`,
125
+ `message_hex`, `policy`, `finalized_utc`, `total_sat`, `total_btc`, `spends`,
126
+ `spends_utc`, and per proof `address`, `signature`, `variant`, `total_sat`
127
+ and the outputs' `txid`, `vout`, `amount_sat`, `height`, `blockhash`), with
128
+ the proofs in address order and only `smp` proofs. No
129
+ descriptor, no xpubs, no derivation paths and no node wallet name go in. The
130
+ proofs stand per address, and the xpubs would let the auditor derive every
131
+ address of the wallet, past and future, which no check of the claim needs.
132
+ `snapshot.json`, the owner's copy, keeps all of it.
133
+
134
+ `verify` re-checks everything on the auditor's node: each BIP-322 signature,
135
+ the stamp block (`getblockheader`: height, time, in main chain), that the
136
+ document is consistent with the signed message, and each listed output.
137
+ An output still unspent is checked with `gettxout` (amount, address,
138
+ creation height at or before the stamp), which also shows it was unspent at
139
+ the stamp. An output spent since is fetched by the block hash the snapshot
140
+ recorded (no `-txindex` needed): that shows it existed at the stamp with the
141
+ claimed amount and address. Whether it was still *unspent* at the stamp needs
142
+ the spending transaction, which only an address index or the owner's wallet
143
+ knows; `finalize` records it in `proofs.json` from the node wallet's history
144
+ (`listsinceblock` from the stamp block), and `verify` checks that it really
145
+ spends the output and was confirmed after the stamp block. A spend at or
146
+ before the stamp is a contradiction (`spent_before_snapshot`). A spent output
147
+ with no recorded spend, or with one the node cannot find or whose block left
148
+ the main chain, is `spent_time_unknown`: it existed, and nothing shows when
149
+ it was spent, so it is not counted as held. The report's rows are built from
150
+ what the node answered; nothing else in `proofs.json` is copied into them.
151
+ Totals are computed from the listed outputs: `claimed`, `held at the stamp`
152
+ (shown by the node) and `unspent now`. A `total_sat` that disagrees, an
153
+ output or an address listed twice, a recorded spend that does not spend its
154
+ output, and a document with no proofs are problems of the document.
155
+
156
+ The result is one of three:
157
+
158
+ * **OK**: the signatures and the stamp check out, the document is consistent,
159
+ and every listed output is shown held at the stamp block (unspent now, or
160
+ spent after the stamp by a checked spend).
161
+ * **INCOMPLETE**: nothing failed and nothing is contradicted, but some listed
162
+ output is not shown held at the stamp block. The owner re-runs `finalize`
163
+ on the node wallet to record the spends.
164
+ * **FAILED**: a signature, the stamp or the document does not check out, or
165
+ the node contradicts a listed output.
166
+
167
+ In the JSON report the verdict is `result` (`ok`, `incomplete`, `failed`).
168
+ The key `ok` is true only for `ok`; `totals.held_at_stamp_sat` is what the node
169
+ showed held.
170
+
171
+ `--offline` verifies signatures and the document only.
172
+
173
+ ## Exit codes
174
+
175
+ | command | 0 | 1 | 2 | 3 |
176
+ |---|---|---|---|---|
177
+ | `verify` | result OK | result FAILED | could not run | result INCOMPLETE |
178
+ | `stamp`, `snapshot`, `prove`, `finalize`, `holdings`, `help` | done | not used | could not run | not used |
179
+
180
+ "Could not run" is bad input (an unknown placeholder, a file that is not a
181
+ proofs document, a directory that exists), a node that cannot be reached or
182
+ answers something that is not JSON, or a bundle that cannot be finalized;
183
+ the message is one line on stderr starting with `error:`.
184
+
185
+ Completeness (that the listed addresses are all the holdings in scope) is not
186
+ something a key or a scan can establish, since nothing rules out a second
187
+ wallet. It comes from the audited party's representation and from
188
+ reconciling one year's spends to the next year's proofs, which the recorded
189
+ spends make possible.
190
+
191
+ `examples/audit_walkthrough.sh [DIR]` runs the whole thing on a throwaway
192
+ regtest node, including spending a coin after the snapshot (INCOMPLETE until
193
+ `finalize` is re-run). It writes into a new directory and refuses one that
194
+ exists and is not empty.
195
+
196
+ ## Checking holdings on chain
197
+
198
+ ```sh
199
+ bip322-audit holdings 7a1b...:0 3c9d...:1 --at 912345 # by output: a direct lookup, instant
200
+ bip322-audit holdings bc1q... --at 912345 # by address: a UTXO-set scan, minutes on mainnet
201
+ ```
202
+
203
+ For each output: the scriptPubKey it is locked to, its amount, and whether
204
+ it is unspent; with `--at`, whether it was held at that block (an output
205
+ confirmed at or before the block and unspent now was; `gettxout` only finds
206
+ unspent outputs). The JSON adds the address and the confirmation block and
207
+ time. This is how a reader of a statement checks its closing holdings. Coins spent since cannot
208
+ show; the owner's records name them and `verify` checks those. A bech32
209
+ address may be given in upper case.
210
+
211
+ ## Bundles over time: a ledger
212
+
213
+ Keep every bundle in one directory tree and it becomes the wallet's ledger of
214
+ proofs. Two ways to add to it:
215
+
216
+ ```sh
217
+ bip322-audit -w treasury snapshot --text "Proof of control {date}" --skip-proven ledger
218
+ # -> ledger/snapshot-<date>-<height>/ with only the outputs no earlier bundle proves (new change, new deposits)
219
+ bip322-audit -w treasury snapshot --text "Proof of control, audit FY2026, {date}" -o ledger/audit-2026
220
+ # -> every output the wallet holds, after the period's end, under a message that names the audit
221
+ ```
222
+
223
+ The block stamp in every message is the timestamp an audit needs: it shows
224
+ the proof was made after that block, it can be checked on any node at any
225
+ later date, and it cannot pass for another year. The auditor's request is
226
+ therefore simply "a full bundle with a stamp after the period's last block";
227
+ their engagement reference may go in the text if they want it in their file.
228
+
229
+ A third way, for an address that holds nothing yet:
230
+
231
+ ```sh
232
+ bip322-audit -w treasury prove bc1q...change... --text "Proof of control {date}" --ledger ledger
233
+ # -> a bundle for exactly that address; refused if it is not the wallet's
234
+ ```
235
+
236
+ That is the check to run on the change address of a spend before broadcasting
237
+ it: sign, `finalize`, and a valid proof means the quorum controls where the
238
+ change goes. A proof is about an address, so every output paid to it later
239
+ is covered too, and `--skip-proven` skips addresses proven that way.
240
+
241
+ `--skip-proven DIR` must exist, and a `proofs.json` under it that cannot be
242
+ read (a truncated file) is named in a warning, since it proves nothing.
243
+
244
+ Sign and `finalize` each as usual. `bip322-audit verify` checks one bundle;
245
+ [bip322-report](https://github.com/embeddednation/bip322-report) reads the
246
+ whole ledger to produce balance reports in which every output is backed by a
247
+ verified proof.
248
+
249
+ ## Layout
250
+
251
+ ```
252
+ bip322audit/stamp.py the block stamp: fetch, parse, compose the message, check
253
+ bip322audit/snapshot.py coins at the stamp block (listunspent / scantxoutset), PSBT per address, the bundle (snapshot, prove)
254
+ bip322audit/audit.py finalize (combine, finalize, self-verify, record spends) and verify (the report)
255
+ bip322audit/holdings.py what outputs or addresses hold now, and at a block (holdings)
256
+ bip322audit/ledger.py a directory tree of bundles: which addresses and outputs earlier proofs cover
257
+ bip322audit/rpc.py bitcoin-cli as a subprocess
258
+ bip322audit/cli.py the bip322-audit command
259
+ bip322audit/testing.py a throwaway regtest node for the regtest test and the walkthrough
260
+ tests/ fake-node tests (test_audit.py, test_review_fixes.py), the pin test, and an end-to-end test on a real regtest Bitcoin Core
261
+ examples/audit_walkthrough.sh the whole flow on a throwaway regtest node
262
+ ```
263
+
264
+ The regtest test and the walkthrough need a Bitcoin Core binary: set
265
+ `BITCOIN_CORE_DIR`, or run `refcheck/fetch.sh` in the bip322-core checkout
266
+ the venv was set up from. Without one the test skips.
267
+
268
+ CI checks out bip322-core at the pinned tag.
@@ -0,0 +1,244 @@
1
+ # bip322-audit
2
+
3
+ Proof of control of a wallet's coins at a block: one BIP-322 proof per funded
4
+ address, a block stamp inside the signed message, and the on-chain checks an
5
+ auditor runs on their own node. Built on
6
+ [bip322-core](https://github.com/embeddednation/bip322-core), which does the
7
+ BIP-322 work and never talks to a node; everything that does lives here, and
8
+ only through `bitcoin-cli`.
9
+
10
+ Two roles:
11
+
12
+ * **Owner**: `snapshot` (or `prove`, for given addresses), sign on the
13
+ cosigners' devices, `finalize`. Needs a node with the wallet loaded
14
+ (watch-only is enough).
15
+ * **Auditor**: `verify`, against any node that has the chain; `holdings` to
16
+ look up single outputs or addresses. No wallet, no index, no key material,
17
+ and nothing about the owner's wallet beyond the proven addresses.
18
+
19
+ The commands: `stamp`, `snapshot`, `prove`, `finalize`, `verify`, `holdings`,
20
+ `help`. With bip322-core installed they are also reachable as `bip322 audit ...`.
21
+
22
+ ## Install
23
+
24
+ One line, into a fresh venv, with bip322-core pulled in:
25
+
26
+ ```sh
27
+ python3 -m venv ~/.bip322 && ~/.bip322/bin/pip install "bip322-audit[kernel]"
28
+ export PATH="$HOME/.bip322/bin:$PATH"
29
+ ```
30
+
31
+ For a reproducible, hash-pinned install (what an auditor should do), clone and use the setup script:
32
+
33
+ ```sh
34
+ git clone https://github.com/embeddednation/bip322-audit.git && cd bip322-audit
35
+ ./setup.sh # venv, hash-pinned dependencies, bip322-core at the pinned tag, tests
36
+ export PATH="$PWD/.venv/bin:$PATH"
37
+ ```
38
+
39
+ `setup.sh` installs `bip322-core` from its git repository at the tag named
40
+ by `CORE_REF` in the script. `./setup.sh --core ../bip322-core` installs a local
41
+ checkout instead, which is the development setup and also the auditor's: clone both
42
+ repositories at the tags the owner names, check the commit hashes out of
43
+ band, and run `verify`.
44
+
45
+ The whole yearly flow across the three packages is in bip322-report's [handbook](https://github.com/embeddednation/bip322-report/blob/main/bip322report/handbook.md).
46
+
47
+ ## Workflow
48
+
49
+ For "we controlled these coins as of block N", repeatable whenever coins move:
50
+
51
+ ```sh
52
+ bip322-audit -w treasury snapshot --text "Annual audit {date}" # the node wallet's own descriptor
53
+ # -> snapshot-2026-09-14-912345/: snapshot.json, message.txt, to_sign/to_sign-01.psbt ... one per funded address
54
+ # sign every PSBT on the cosigners' devices, put the results into snapshot-.../signed/
55
+ bip322-audit finalize snapshot-2026-09-14-912345 # -> proofs.json (hand this to the auditor)
56
+ bip322-audit verify snapshot-2026-09-14-912345 --report audit-report.json
57
+ ```
58
+
59
+ `finalize` also asks the node wallet the coins came from (recorded in
60
+ `snapshot.json`) which listed outputs have been spent since, and records the
61
+ spending transactions in `proofs.json`. If coins move between the snapshot and
62
+ the audit, re-run `finalize` before handing `proofs.json` over; the proofs
63
+ themselves do not change. `--offline` skips that step and keeps the spends an
64
+ earlier run recorded. A PSBT that cannot be used is reported with its address
65
+ and file names, and files in `to_sign/` and `signed/` that were not used are
66
+ listed.
67
+
68
+ The node is reached with `--cli CMD` (default `bitcoin-cli`). `CMD` is split
69
+ like a shell command line, so a path with spaces is quoted inside it:
70
+ `--cli "'/opt/my node/bitcoin-cli' -signet"`. `--timeout SECONDS` bounds every
71
+ node call (default 600); a UTXO-set scan the tool gave up on keeps running on
72
+ the node. Chains: `main`, `test`, `testnet4`, `signet`, `regtest`.
73
+
74
+ `snapshot` reads the wallet's descriptor from the node wallet (`-w NAME`,
75
+ `listdescriptors`; `--descriptor FILE` overrides and is cross-checked against
76
+ the node), takes the block six behind the tip (`--depth`) as the stamp *and*
77
+ the snapshot height: the message ends with `block: HEIGHT HASH TIME` taken
78
+ from that block, and only outputs confirmed at that block are listed. Coins
79
+ come from `listunspent` on the node wallet (the only one loaded, or `-w NAME`)
80
+ or, when the node has no wallet, from a
81
+ `scantxoutset` of the descriptor (minutes on mainnet; the command says so
82
+ before it starts; `--source` forces either). The stamp, the coins, their
83
+ heights and block hashes are read under one tip: if a block arrives
84
+ meanwhile, they are read again. The template accepts `{date}`, `{time}`,
85
+ `{height}`, `{hash}` (date and time are the stamp block's header time, UTC);
86
+ anything else in braces is refused, as is a template with a `block:` line of
87
+ its own, and the message is checked against hardware signers' message rules.
88
+
89
+ `snapshot` covers one descriptor (`wsh(multi/sortedmulti)` or `wpkh`). Its
90
+ summary and `snapshot.json` say which (`descriptor`), and under `left_out`
91
+ how many outputs and how much the node listed that the bundle does not, and
92
+ why: coins of another descriptor in the same node wallet, coins on addresses
93
+ an earlier bundle proves. Descriptors of a kind it does not cover are named
94
+ in `notes`. The default directory is `snapshot-<date>-<height>`, with `-2`,
95
+ `-3` ... when that exists; `-o DIR --force` rewrites a bundle that holds no
96
+ signed PSBTs and no `proofs.json` yet, and nothing else.
97
+
98
+ `proofs.json` names addresses, not a wallet: the message, the stamp, and per
99
+ address the proof and its outputs, plus the policy string (`2 of 3`). It is
100
+ built from a fixed list of fields (`tool`, `chain`, `stamp`, `message`,
101
+ `message_hex`, `policy`, `finalized_utc`, `total_sat`, `total_btc`, `spends`,
102
+ `spends_utc`, and per proof `address`, `signature`, `variant`, `total_sat`
103
+ and the outputs' `txid`, `vout`, `amount_sat`, `height`, `blockhash`), with
104
+ the proofs in address order and only `smp` proofs. No
105
+ descriptor, no xpubs, no derivation paths and no node wallet name go in. The
106
+ proofs stand per address, and the xpubs would let the auditor derive every
107
+ address of the wallet, past and future, which no check of the claim needs.
108
+ `snapshot.json`, the owner's copy, keeps all of it.
109
+
110
+ `verify` re-checks everything on the auditor's node: each BIP-322 signature,
111
+ the stamp block (`getblockheader`: height, time, in main chain), that the
112
+ document is consistent with the signed message, and each listed output.
113
+ An output still unspent is checked with `gettxout` (amount, address,
114
+ creation height at or before the stamp), which also shows it was unspent at
115
+ the stamp. An output spent since is fetched by the block hash the snapshot
116
+ recorded (no `-txindex` needed): that shows it existed at the stamp with the
117
+ claimed amount and address. Whether it was still *unspent* at the stamp needs
118
+ the spending transaction, which only an address index or the owner's wallet
119
+ knows; `finalize` records it in `proofs.json` from the node wallet's history
120
+ (`listsinceblock` from the stamp block), and `verify` checks that it really
121
+ spends the output and was confirmed after the stamp block. A spend at or
122
+ before the stamp is a contradiction (`spent_before_snapshot`). A spent output
123
+ with no recorded spend, or with one the node cannot find or whose block left
124
+ the main chain, is `spent_time_unknown`: it existed, and nothing shows when
125
+ it was spent, so it is not counted as held. The report's rows are built from
126
+ what the node answered; nothing else in `proofs.json` is copied into them.
127
+ Totals are computed from the listed outputs: `claimed`, `held at the stamp`
128
+ (shown by the node) and `unspent now`. A `total_sat` that disagrees, an
129
+ output or an address listed twice, a recorded spend that does not spend its
130
+ output, and a document with no proofs are problems of the document.
131
+
132
+ The result is one of three:
133
+
134
+ * **OK**: the signatures and the stamp check out, the document is consistent,
135
+ and every listed output is shown held at the stamp block (unspent now, or
136
+ spent after the stamp by a checked spend).
137
+ * **INCOMPLETE**: nothing failed and nothing is contradicted, but some listed
138
+ output is not shown held at the stamp block. The owner re-runs `finalize`
139
+ on the node wallet to record the spends.
140
+ * **FAILED**: a signature, the stamp or the document does not check out, or
141
+ the node contradicts a listed output.
142
+
143
+ In the JSON report the verdict is `result` (`ok`, `incomplete`, `failed`).
144
+ The key `ok` is true only for `ok`; `totals.held_at_stamp_sat` is what the node
145
+ showed held.
146
+
147
+ `--offline` verifies signatures and the document only.
148
+
149
+ ## Exit codes
150
+
151
+ | command | 0 | 1 | 2 | 3 |
152
+ |---|---|---|---|---|
153
+ | `verify` | result OK | result FAILED | could not run | result INCOMPLETE |
154
+ | `stamp`, `snapshot`, `prove`, `finalize`, `holdings`, `help` | done | not used | could not run | not used |
155
+
156
+ "Could not run" is bad input (an unknown placeholder, a file that is not a
157
+ proofs document, a directory that exists), a node that cannot be reached or
158
+ answers something that is not JSON, or a bundle that cannot be finalized;
159
+ the message is one line on stderr starting with `error:`.
160
+
161
+ Completeness (that the listed addresses are all the holdings in scope) is not
162
+ something a key or a scan can establish, since nothing rules out a second
163
+ wallet. It comes from the audited party's representation and from
164
+ reconciling one year's spends to the next year's proofs, which the recorded
165
+ spends make possible.
166
+
167
+ `examples/audit_walkthrough.sh [DIR]` runs the whole thing on a throwaway
168
+ regtest node, including spending a coin after the snapshot (INCOMPLETE until
169
+ `finalize` is re-run). It writes into a new directory and refuses one that
170
+ exists and is not empty.
171
+
172
+ ## Checking holdings on chain
173
+
174
+ ```sh
175
+ bip322-audit holdings 7a1b...:0 3c9d...:1 --at 912345 # by output: a direct lookup, instant
176
+ bip322-audit holdings bc1q... --at 912345 # by address: a UTXO-set scan, minutes on mainnet
177
+ ```
178
+
179
+ For each output: the scriptPubKey it is locked to, its amount, and whether
180
+ it is unspent; with `--at`, whether it was held at that block (an output
181
+ confirmed at or before the block and unspent now was; `gettxout` only finds
182
+ unspent outputs). The JSON adds the address and the confirmation block and
183
+ time. This is how a reader of a statement checks its closing holdings. Coins spent since cannot
184
+ show; the owner's records name them and `verify` checks those. A bech32
185
+ address may be given in upper case.
186
+
187
+ ## Bundles over time: a ledger
188
+
189
+ Keep every bundle in one directory tree and it becomes the wallet's ledger of
190
+ proofs. Two ways to add to it:
191
+
192
+ ```sh
193
+ bip322-audit -w treasury snapshot --text "Proof of control {date}" --skip-proven ledger
194
+ # -> ledger/snapshot-<date>-<height>/ with only the outputs no earlier bundle proves (new change, new deposits)
195
+ bip322-audit -w treasury snapshot --text "Proof of control, audit FY2026, {date}" -o ledger/audit-2026
196
+ # -> every output the wallet holds, after the period's end, under a message that names the audit
197
+ ```
198
+
199
+ The block stamp in every message is the timestamp an audit needs: it shows
200
+ the proof was made after that block, it can be checked on any node at any
201
+ later date, and it cannot pass for another year. The auditor's request is
202
+ therefore simply "a full bundle with a stamp after the period's last block";
203
+ their engagement reference may go in the text if they want it in their file.
204
+
205
+ A third way, for an address that holds nothing yet:
206
+
207
+ ```sh
208
+ bip322-audit -w treasury prove bc1q...change... --text "Proof of control {date}" --ledger ledger
209
+ # -> a bundle for exactly that address; refused if it is not the wallet's
210
+ ```
211
+
212
+ That is the check to run on the change address of a spend before broadcasting
213
+ it: sign, `finalize`, and a valid proof means the quorum controls where the
214
+ change goes. A proof is about an address, so every output paid to it later
215
+ is covered too, and `--skip-proven` skips addresses proven that way.
216
+
217
+ `--skip-proven DIR` must exist, and a `proofs.json` under it that cannot be
218
+ read (a truncated file) is named in a warning, since it proves nothing.
219
+
220
+ Sign and `finalize` each as usual. `bip322-audit verify` checks one bundle;
221
+ [bip322-report](https://github.com/embeddednation/bip322-report) reads the
222
+ whole ledger to produce balance reports in which every output is backed by a
223
+ verified proof.
224
+
225
+ ## Layout
226
+
227
+ ```
228
+ bip322audit/stamp.py the block stamp: fetch, parse, compose the message, check
229
+ bip322audit/snapshot.py coins at the stamp block (listunspent / scantxoutset), PSBT per address, the bundle (snapshot, prove)
230
+ bip322audit/audit.py finalize (combine, finalize, self-verify, record spends) and verify (the report)
231
+ bip322audit/holdings.py what outputs or addresses hold now, and at a block (holdings)
232
+ bip322audit/ledger.py a directory tree of bundles: which addresses and outputs earlier proofs cover
233
+ bip322audit/rpc.py bitcoin-cli as a subprocess
234
+ bip322audit/cli.py the bip322-audit command
235
+ bip322audit/testing.py a throwaway regtest node for the regtest test and the walkthrough
236
+ tests/ fake-node tests (test_audit.py, test_review_fixes.py), the pin test, and an end-to-end test on a real regtest Bitcoin Core
237
+ examples/audit_walkthrough.sh the whole flow on a throwaway regtest node
238
+ ```
239
+
240
+ The regtest test and the walkthrough need a Bitcoin Core binary: set
241
+ `BITCOIN_CORE_DIR`, or run `refcheck/fetch.sh` in the bip322-core checkout
242
+ the venv was set up from. Without one the test skips.
243
+
244
+ CI checks out bip322-core at the pinned tag.