Compare commits
674 Commits
v0.21.0
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c01983d02a | ||
|
|
fef70d5e8f | ||
|
|
c3544c9b8c | ||
|
|
b290b29502 | ||
|
|
d77a7f2ff1 | ||
|
|
3d44ffaef2 | ||
|
|
2efa299d04 | ||
|
|
2647aff4bc | ||
|
|
c151d8fd23 | ||
|
|
2c324d3759 | ||
|
|
50c549b5ac | ||
|
|
8379839010 | ||
|
|
5489f905a4 | ||
|
|
420e929463 | ||
|
|
13ab5a835d | ||
|
|
728e26f223 | ||
|
|
dbbd514242 | ||
|
|
ae00e1ee7b | ||
|
|
adc95137ac | ||
|
|
022d5a21cf | ||
|
|
7aca88474a | ||
|
|
b3278a4c29 | ||
|
|
552f11cb5f | ||
|
|
d8f74dc5e4 | ||
|
|
8d93fad778 | ||
|
|
9bb39a3a3f | ||
|
|
9e098a5b6d | ||
|
|
c6b9ed3b76 | ||
|
|
1c15cb2f91 | ||
|
|
89a7ddca7f | ||
|
|
097d818d4c | ||
|
|
f11d663b7e | ||
|
|
4679ca1df7 | ||
|
|
64a90192d9 | ||
|
|
ba7624781d | ||
|
|
d597f4c761 | ||
|
|
f099b42005 | ||
|
|
ce8c617c9d | ||
|
|
8ad52f720f | ||
|
|
c5afbaa95d | ||
|
|
929b5ddb0c | ||
|
|
070fffb95c | ||
|
|
216648bcfd | ||
|
|
5299db34cb | ||
|
|
8375bb8d39 | ||
|
|
63fa710319 | ||
|
|
d4276a1c32 | ||
|
|
6a03e0f209 | ||
|
|
38b728ae52 | ||
|
|
d162208d95 | ||
|
|
e687c27096 | ||
|
|
5611c9e42a | ||
|
|
07116df541 | ||
|
|
48b28e3abc | ||
|
|
51bd01b3dd | ||
|
|
285ff46a49 | ||
|
|
8305e64849 | ||
|
|
66dc34e75a | ||
|
|
fbd1d65618 | ||
|
|
c4d5f2ccd8 | ||
|
|
52c77b8451 | ||
|
|
99661be5f3 | ||
|
|
914db84824 | ||
|
|
f8f371c8d8 | ||
|
|
232a172c32 | ||
|
|
8d916d7a10 | ||
|
|
3fa44a58ec | ||
|
|
6f824cf325 | ||
|
|
f05e8502e6 | ||
|
|
25653d71b8 | ||
|
|
e6433fb2c1 | ||
|
|
0bee46e75b | ||
|
|
08b745ec9f | ||
|
|
0a2a57060b | ||
|
|
d33acc1466 | ||
|
|
d1ea0ef3d1 | ||
|
|
60abd87a32 | ||
|
|
71fff1613d | ||
|
|
b6a58d4f9b | ||
|
|
cf0c333744 | ||
|
|
7c0f4653b2 | ||
|
|
3829fc18c7 | ||
|
|
d494f63d08 | ||
|
|
83e7b7ec40 | ||
|
|
9294e30943 | ||
|
|
b74c2e2622 | ||
|
|
81aeaba48a | ||
|
|
c7b47af72f | ||
|
|
d9501187ef | ||
|
|
a4f28c079e | ||
|
|
8ec65f0b8e | ||
|
|
a7d01dc39a | ||
|
|
e0512acf94 | ||
|
|
8f2d4d9d40 | ||
|
|
9467cad55d | ||
|
|
d3e5095df1 | ||
|
|
2b61a122ff | ||
|
|
40f0765d30 | ||
|
|
bf67519768 | ||
|
|
b6422f7ffc | ||
|
|
eb1714aee0 | ||
|
|
705690ee8f | ||
|
|
c871764670 | ||
|
|
a3aa8b6682 | ||
|
|
cd602430ee | ||
|
|
264bb85efc | ||
|
|
761189ab2b | ||
|
|
5b77942993 | ||
|
|
f9dad51ae1 | ||
|
|
8f6dad76ef | ||
|
|
887e112e8f | ||
|
|
21d8875826 | ||
|
|
6e6bad9223 | ||
|
|
105d70e974 | ||
|
|
9efaead8f1 | ||
|
|
1ff9d5ce8f | ||
|
|
8694624bd5 | ||
|
|
003271117c | ||
|
|
f6418ba911 | ||
|
|
028caa9f8c | ||
|
|
d71829914a | ||
|
|
a1d34afa24 | ||
|
|
9cc03324f4 | ||
|
|
de54e710ed | ||
|
|
95d34854f4 | ||
|
|
ed91a4bdb4 | ||
|
|
179cfeff51 | ||
|
|
7eff024213 | ||
|
|
1def76f1f1 | ||
|
|
c9467dcbb2 | ||
|
|
bc796f412a | ||
|
|
4fd539b647 | ||
|
|
01698ae5ec | ||
|
|
f4863c6314 | ||
|
|
b5612f269a | ||
|
|
e7fbc8bcf3 | ||
|
|
2251b8d416 | ||
|
|
b13505c1c3 | ||
|
|
0adff9c35f | ||
|
|
908b0f9f5e | ||
|
|
169385bb5b | ||
|
|
f741122ffb | ||
|
|
959b4f8172 | ||
|
|
55b680c194 | ||
|
|
43aed386bc | ||
|
|
cb713e5b8c | ||
|
|
2c4e90a76f | ||
|
|
18bd329617 | ||
|
|
9e681b39fb | ||
|
|
6817ca9bcb | ||
|
|
73862be3ba | ||
|
|
02fa340896 | ||
|
|
4ee41dbc40 | ||
|
|
278210bb89 | ||
|
|
6fb45d8a73 | ||
|
|
e803ee9010 | ||
|
|
82632897aa | ||
|
|
46d39beb2c | ||
|
|
00ec19ef2d | ||
|
|
77f9977c02 | ||
|
|
9e7d99e3bf | ||
|
|
cc552c5f91 | ||
|
|
27a63abd1e | ||
|
|
bc8d6a396b | ||
|
|
f1b112e8f9 | ||
|
|
9a250baf62 | ||
|
|
79b84bed0e | ||
|
|
06a956ad20 | ||
|
|
c3265e2514 | ||
|
|
96f1d94e2c | ||
|
|
1886dc4fe7 | ||
|
|
24994a3ed4 | ||
|
|
d294e2e318 | ||
|
|
7c6cbc4d9f | ||
|
|
6cf3963c6c | ||
|
|
7d5f31f6cc | ||
|
|
5998a22819 | ||
|
|
d6a0cf0795 | ||
|
|
6e27e66738 | ||
|
|
f382fa9230 | ||
|
|
e71770f93e | ||
|
|
298f6cb1e8 | ||
|
|
3fdab87ee7 | ||
|
|
855c61a6ab | ||
|
|
0112c67b60 | ||
|
|
1010efd8d6 | ||
|
|
991cb77b6f | ||
|
|
e553231eae | ||
|
|
0a7b60f0f7 | ||
|
|
0ecc0280c0 | ||
|
|
afbf83c8b0 | ||
|
|
2f2f138595 | ||
|
|
95250fc44e | ||
|
|
f17df1e133 | ||
|
|
3569acca0b | ||
|
|
2e4bc3c5e2 | ||
|
|
6ebdd195e2 | ||
|
|
d5c87c49a8 | ||
|
|
009408d243 | ||
|
|
38d69c947c | ||
|
|
67eec36db4 | ||
|
|
85c62532a5 | ||
|
|
b69c13ddf6 | ||
|
|
5f34df8489 | ||
|
|
57590e0a1f | ||
|
|
6d4b33ef91 | ||
|
|
4f5695d43a | ||
|
|
150d6f8ab6 | ||
|
|
4f10463d9e | ||
|
|
a73dac2d91 | ||
|
|
bb7424d11d | ||
|
|
240657b167 | ||
|
|
43bc813c64 | ||
|
|
b3db5ca9df | ||
|
|
f795a43cc7 | ||
|
|
b1461f05d0 | ||
|
|
77cde96229 | ||
|
|
1db3f87a48 | ||
|
|
4a65a12c4f | ||
|
|
4ee11aae12 | ||
|
|
a7dbc22df1 | ||
|
|
6d601a7e88 | ||
|
|
48ca95b541 | ||
|
|
59a2403e28 | ||
|
|
6e511473a5 | ||
|
|
62de55f12d | ||
|
|
5e79b81a6a | ||
|
|
a3e8480ad9 | ||
|
|
4742d88ea3 | ||
|
|
2f26eca607 | ||
|
|
486e0e1437 | ||
|
|
9bd528607a | ||
|
|
f28e665c7d | ||
|
|
417963f168 | ||
|
|
edfd4c236d | ||
|
|
fe654310d7 | ||
|
|
37d5e5319f | ||
|
|
ea6411c685 | ||
|
|
4a1b96dcc4 | ||
|
|
bf9a425849 | ||
|
|
d35668e76a | ||
|
|
31d52e12c9 | ||
|
|
6a5c9d7a00 | ||
|
|
4d1a9fd47a | ||
|
|
f95506ba6a | ||
|
|
e6519e3a52 | ||
|
|
43fb0b20df | ||
|
|
94f8fa530b | ||
|
|
c20a4da9fc | ||
|
|
1ff806c67f | ||
|
|
d43ae0231f | ||
|
|
59fc1b341b | ||
|
|
20900218ce | ||
|
|
e89cf5a16a | ||
|
|
4104206980 | ||
|
|
c56728ff13 | ||
|
|
32c40ac939 | ||
|
|
a28748c339 | ||
|
|
f42f8b8ff1 | ||
|
|
68572bfd2e | ||
|
|
2392e50fd9 | ||
|
|
7c12dc9942 | ||
|
|
6bcbb93233 | ||
|
|
2867e88b64 | ||
|
|
e48b911c8d | ||
|
|
a7a1d9b2fb | ||
|
|
8321aaa5c7 | ||
|
|
cc1a43c495 | ||
|
|
f41cc1cb37 | ||
|
|
da8cfd39e9 | ||
|
|
93e8eaf7ee | ||
|
|
5fb5061645 | ||
|
|
dd5b8d7599 | ||
|
|
465d53cc88 | ||
|
|
036299803f | ||
|
|
d443fe7f66 | ||
|
|
b4c31cd5ba | ||
|
|
e5fb1ec7ff | ||
|
|
18e8da3937 | ||
|
|
8f978f86b8 | ||
|
|
21206fe773 | ||
|
|
381c560c10 | ||
|
|
c753584379 | ||
|
|
6125062a5b | ||
|
|
2263a58448 | ||
|
|
11ac26f6b2 | ||
|
|
fb5cfa3c25 | ||
|
|
fa0bead024 | ||
|
|
8c4eeb56c0 | ||
|
|
3b5b829086 | ||
|
|
4f37b2a293 | ||
|
|
2d543475e2 | ||
|
|
d6bcd9b725 | ||
|
|
62f253103c | ||
|
|
80f5ecf3be | ||
|
|
ea50b6a932 | ||
|
|
480c2730de | ||
|
|
0cd2348166 | ||
|
|
b0b91b7418 | ||
|
|
feafaaca31 | ||
|
|
1da3b304bb | ||
|
|
792b39fa92 | ||
|
|
b73385dbd2 | ||
|
|
3dac3f9bba | ||
|
|
2949bdc7b8 | ||
|
|
468d2a0a3b | ||
|
|
b8ac16d03c | ||
|
|
6c29e53ee8 | ||
|
|
6eb079576f | ||
|
|
91b0f0ba29 | ||
|
|
f4e3ba3265 | ||
|
|
853d361751 | ||
|
|
d73669e8fa | ||
|
|
933056706c | ||
|
|
b206a985cf | ||
|
|
bea8e5aff4 | ||
|
|
db15e03bdc | ||
|
|
95312d4d05 | ||
|
|
8bf7a997f7 | ||
|
|
315e7e0b4b | ||
|
|
af705da1a8 | ||
|
|
eabeb6ccb1 | ||
|
|
8f38e96e45 | ||
|
|
ffb7c795e1 | ||
|
|
56b8eea643 | ||
|
|
cb626e9fc8 | ||
|
|
e17f03e755 | ||
|
|
f5074ee3ae | ||
|
|
f4d2a76661 | ||
|
|
81c7613391 | ||
|
|
26ade11726 | ||
|
|
5d1f922b3b | ||
|
|
7ab84be9c7 | ||
|
|
b184e351e5 | ||
|
|
352f5b29ab | ||
|
|
fa54a2e3a5 | ||
|
|
97d542cf1c | ||
|
|
75f8b81d58 | ||
|
|
cff92111d5 | ||
|
|
a7668a2f3e | ||
|
|
ac80829caa | ||
|
|
1c3cbefa4d | ||
|
|
5860704b2d | ||
|
|
2952341e52 | ||
|
|
78a7920ba3 | ||
|
|
92709d03ce | ||
|
|
50425e979b | ||
|
|
a78967e51b | ||
|
|
6a1ac7f80a | ||
|
|
f55974a64b | ||
|
|
2e3cee4bd0 | ||
|
|
7261669c09 | ||
|
|
aba88130d9 | ||
|
|
e69fccb15f | ||
|
|
8641847e6c | ||
|
|
22b8a48842 | ||
|
|
9bc7fe855d | ||
|
|
10b4b6c665 | ||
|
|
796f433f6c | ||
|
|
ac3759254a | ||
|
|
e30919ba3a | ||
|
|
1d55943fa1 | ||
|
|
df74b23f31 | ||
|
|
725eee8c92 | ||
|
|
c7a045fa54 | ||
|
|
34d60870ac | ||
|
|
ed89de752c | ||
|
|
fb75aa94a9 | ||
|
|
96b1075132 | ||
|
|
e01d17d59b | ||
|
|
05d353c0ad | ||
|
|
4963240599 | ||
|
|
2aa08a5898 | ||
|
|
e3c137043f | ||
|
|
065c64a675 | ||
|
|
a56d289eef | ||
|
|
2ccc116eda | ||
|
|
4ae727a1fb | ||
|
|
085bf9413d | ||
|
|
e413d3e424 | ||
|
|
c61995ca97 | ||
|
|
10fe32e6f1 | ||
|
|
6bc5f33ded | ||
|
|
702fe7ac5e | ||
|
|
ce5ae3eac4 | ||
|
|
c1cffe9333 | ||
|
|
5be7c1c50d | ||
|
|
29055658a6 | ||
|
|
139e3d3802 | ||
|
|
b799a5728b | ||
|
|
8cd0328eec | ||
|
|
911af34f50 | ||
|
|
e536307e5c | ||
|
|
217ea3321a | ||
|
|
f101dde09b | ||
|
|
1b152647c5 | ||
|
|
ecc74ce4cd | ||
|
|
ac336aa32f | ||
|
|
165b874dfe | ||
|
|
f3e7b67bf1 | ||
|
|
03c128311a | ||
|
|
34a7bf5afe | ||
|
|
6c49570742 | ||
|
|
1003fe2ee6 | ||
|
|
7175a82c04 | ||
|
|
8e36a2e5f6 | ||
|
|
81436fcd72 | ||
|
|
5e026cfd03 | ||
|
|
001efdd1cb | ||
|
|
10ab77c549 | ||
|
|
7d92337b93 | ||
|
|
a7fbe0ac67 | ||
|
|
ee1060f2ff | ||
|
|
611d2e3ea2 | ||
|
|
bff80ec378 | ||
|
|
24cd8c5cc7 | ||
|
|
ddd5e951f5 | ||
|
|
da4cef044d | ||
|
|
89cfa4d78e | ||
|
|
6e59dce10b | ||
|
|
ebd6103e65 | ||
|
|
a7eaebbb77 | ||
|
|
c09cd2afce | ||
|
|
7810059ed0 | ||
|
|
a63ffe9739 | ||
|
|
a1172def7d | ||
|
|
8c906170c9 | ||
|
|
468701a129 | ||
|
|
34d0277e44 | ||
|
|
3440a05711 | ||
|
|
236c50fa7b | ||
|
|
e902c10295 | ||
|
|
313965d8c8 | ||
|
|
db7883d813 | ||
|
|
d0a2aa83be | ||
|
|
6cbb18d409 | ||
|
|
784cd34e3d | ||
|
|
43b648fee0 | ||
|
|
61a8606fbc | ||
|
|
5ae5fe30eb | ||
|
|
5a090fac90 | ||
|
|
f99eb32ac5 | ||
|
|
30c11904a7 | ||
|
|
82f9caddab | ||
|
|
3b68a7bcc0 | ||
|
|
919e74aa8d | ||
|
|
72b1e2a485 | ||
|
|
2ae69ca10b | ||
|
|
877b658787 | ||
|
|
82f5d9c81e | ||
|
|
24df03afd6 | ||
|
|
cd4945af3a | ||
|
|
bc3e05c6c6 | ||
|
|
352f95f558 | ||
|
|
2fcf9c4adb | ||
|
|
5dd4ce74cf | ||
|
|
ae9b19d84c | ||
|
|
def0c9ed39 | ||
|
|
26ab2e2d6c | ||
|
|
ab9242d10d | ||
|
|
0aaf420f6d | ||
|
|
47faa881fb | ||
|
|
9d26121dbc | ||
|
|
eddd748870 | ||
|
|
0505cd7242 | ||
|
|
de9457fce6 | ||
|
|
69cf6d7924 | ||
|
|
b3836cb308 | ||
|
|
b082932268 | ||
|
|
d267517dbd | ||
|
|
0c7a0abb19 | ||
|
|
dfcbafd6b1 | ||
|
|
0ba41c5751 | ||
|
|
a38f63359d | ||
|
|
38ef170ed1 | ||
|
|
3a5d727899 | ||
|
|
96d932c830 | ||
|
|
5708bf0c8c | ||
|
|
5acee82496 | ||
|
|
8c9bcebc71 | ||
|
|
c61b3604e1 | ||
|
|
1805bd35c0 | ||
|
|
3f5a78ae3b | ||
|
|
303a1703c9 | ||
|
|
b5559767db | ||
|
|
2e82cd8c04 | ||
|
|
c069b0fb41 | ||
|
|
949608ab1f | ||
|
|
03deafb553 | ||
|
|
37dfa77d9d | ||
|
|
f2188f9dcd | ||
|
|
1c970a9295 | ||
|
|
94a084aafd | ||
|
|
9edbdf54c9 | ||
|
|
20e45b7af0 | ||
|
|
6d05598407 | ||
|
|
b60820a7b5 | ||
|
|
22bec6d363 | ||
|
|
8a6de3aa2d | ||
|
|
fdfc9b9ede | ||
|
|
e1eb0253cf | ||
|
|
3baf9721ec | ||
|
|
b310a7afdd | ||
|
|
5985706c1a | ||
|
|
57538e53e4 | ||
|
|
a40da9ba6c | ||
|
|
aab2b12f7a | ||
|
|
544c397a38 | ||
|
|
ced2d05e64 | ||
|
|
843807b08f | ||
|
|
231a1fba61 | ||
|
|
74119e70c3 | ||
|
|
8b2943c49b | ||
|
|
a1a70a5011 | ||
|
|
2d173a17f7 | ||
|
|
5b9e0e392a | ||
|
|
c2a42493fd | ||
|
|
0c2570ae07 | ||
|
|
e83bb7c4dc | ||
|
|
46273fe72f | ||
|
|
0b26fa75dc | ||
|
|
35bbe2beef | ||
|
|
9e7bad8afa | ||
|
|
4ada11f358 | ||
|
|
cf8cd2f2b4 | ||
|
|
147a4ed141 | ||
|
|
97f8fe3fd1 | ||
|
|
f0cec015b5 | ||
|
|
ff72078095 | ||
|
|
e678aad3c7 | ||
|
|
41dc7f7d0d | ||
|
|
6b92a169ab | ||
|
|
45d41416ed | ||
|
|
9019793bd4 | ||
|
|
32912eaa05 | ||
|
|
2e7a220e39 | ||
|
|
b02bfb347d | ||
|
|
0cce1ce982 | ||
|
|
fb76c9ed9a | ||
|
|
3a782b3b0d | ||
|
|
eac739d395 | ||
|
|
6e5873ebba | ||
|
|
3205f0c16d | ||
|
|
5f0870a741 | ||
|
|
5a483472c1 | ||
|
|
8d4cc3920a | ||
|
|
14bc9c0e35 | ||
|
|
2451c00268 | ||
|
|
4cad18bbca | ||
|
|
634a0575cb | ||
|
|
d3d07564f2 | ||
|
|
0b768d6f0b | ||
|
|
ec9aefac6b | ||
|
|
d72aa7ebc0 | ||
|
|
99930af12e | ||
|
|
d6e730f18a | ||
|
|
d1e5b87bfc | ||
|
|
c101dea460 | ||
|
|
9ddd502538 | ||
|
|
a5d345fff2 | ||
|
|
11dcc14374 | ||
|
|
4c5ceaff14 | ||
|
|
b5fcddcf1a | ||
|
|
d570ff2c65 | ||
|
|
21c96c9c81 | ||
|
|
c51d544932 | ||
|
|
5e56c3b3c1 | ||
|
|
235961a934 | ||
|
|
df905a8d5e | ||
|
|
8b68cf9546 | ||
|
|
150f4d6f41 | ||
|
|
1c95ca33a8 | ||
|
|
108edc3a6b | ||
|
|
f99a6b9f43 | ||
|
|
aedbc8c97d | ||
|
|
5c42102c79 | ||
|
|
5d5b2fb88c | ||
|
|
9cb6f70fc0 | ||
|
|
5720e38033 | ||
|
|
e9bbb8724f | ||
|
|
648282e602 | ||
|
|
c7a43d941f | ||
|
|
1ffd59d469 | ||
|
|
ae4f4e5416 | ||
|
|
9854fd34ea | ||
|
|
60057a7bf7 | ||
|
|
ea47d7a35b | ||
|
|
f2181f5467 | ||
|
|
34987d58ec | ||
|
|
1c76084db8 | ||
|
|
68dd6d2031 | ||
|
|
1437e1ecfe | ||
|
|
1a71eb1f47 | ||
|
|
0695e9fb3e | ||
|
|
a4a43ea860 | ||
|
|
b627455b8f | ||
|
|
1331193800 | ||
|
|
7de8be46c0 | ||
|
|
55145f57a1 | ||
|
|
8e8fd49e04 | ||
|
|
d7bfe68e2d | ||
|
|
b11c86d074 | ||
|
|
e2a4a5884b | ||
|
|
fd34956c29 | ||
|
|
b5b92248c7 | ||
|
|
cf2bc388f2 | ||
|
|
5baf46f84d | ||
|
|
a8cf34e809 | ||
|
|
af0b3698c6 | ||
|
|
92ad4876c4 | ||
|
|
b14e4ee3a0 | ||
|
|
e6f2d029fa | ||
|
|
bbf524b3f9 | ||
|
|
dbf6bf5fdf | ||
|
|
aff41d6e1c | ||
|
|
e2bf9734b1 | ||
|
|
c3faf05be9 | ||
|
|
aad5461ee1 | ||
|
|
0a7a1f4ef2 | ||
|
|
5e9965fca7 | ||
|
|
54d768412a | ||
|
|
97b6fb06aa | ||
|
|
da7670801b | ||
|
|
e1fa0b6695 | ||
|
|
dfeb08fa00 | ||
|
|
8963e8c9f4 | ||
|
|
562cb81cad | ||
|
|
b12dec3620 | ||
|
|
e65edbf53c | ||
|
|
88307045b0 | ||
|
|
e06c3f945c | ||
|
|
ab41679368 | ||
|
|
7b12f35698 | ||
|
|
aa0ea6aeff | ||
|
|
c3a7bbb3ff | ||
|
|
1c4d47825b | ||
|
|
fa998de4b1 | ||
|
|
06310f1dd0 | ||
|
|
8dd02094df | ||
|
|
0010ecd94a | ||
|
|
690411722e | ||
|
|
7001b14b4c | ||
|
|
13cf72ffa7 | ||
|
|
d7163c3a97 | ||
|
|
cf13c80991 | ||
|
|
7c57965999 | ||
|
|
3d69f1c291 | ||
|
|
3451d1c12e | ||
|
|
4fbd8520e6 | ||
|
|
bfd7b2f65d | ||
|
|
061f15af00 | ||
|
|
369e17b801 | ||
|
|
2bff4e5e56 | ||
|
|
138acc3b7d | ||
|
|
d6e1dd1040 | ||
|
|
76034772cb | ||
|
|
12507c707f | ||
|
|
de358f8cdc | ||
|
|
08668ac462 | ||
|
|
0a3734ed2b | ||
|
|
a5d1a3d65c | ||
|
|
7bc2980905 | ||
|
|
34e792e193 | ||
|
|
7b1ad1b629 | ||
|
|
a8f9f6c43a | ||
|
|
c9b1b6d076 | ||
|
|
cd078903a7 | ||
|
|
588c17ff69 | ||
|
|
baf7eaace6 | ||
|
|
8026bd9476 | ||
|
|
e2bd96012a | ||
|
|
9f9ffd0efd | ||
|
|
d9adfbe047 | ||
|
|
c5952dd09a |
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
17
.github/ISSUE_TEMPLATE/enhancement_request.md
vendored
Normal file
@@ -0,0 +1,17 @@
|
||||
---
|
||||
name: Enhancement request
|
||||
about: Request a new feature or change to an existing feature
|
||||
title: ''
|
||||
labels: 'enhancement'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
**Describe the enhancement**
|
||||
<!-- A clear and concise description of what you would like added or changed. -->
|
||||
|
||||
**Use case**
|
||||
<!-- Tell us how you or others will use this new feature or change to an existing feature. -->
|
||||
|
||||
**Additional context**
|
||||
<!-- Add any other context about the enhancement here. -->
|
||||
99
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
99
.github/ISSUE_TEMPLATE/minor_release.md
vendored
Normal file
@@ -0,0 +1,99 @@
|
||||
---
|
||||
name: Minor Release
|
||||
about: Create a new minor release [for release managers only]
|
||||
title: 'Release MAJOR.MINOR+1.0'
|
||||
labels: 'release'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Create a new minor release
|
||||
|
||||
### Summary
|
||||
|
||||
<--release summary to be used in announcements-->
|
||||
|
||||
### Commit
|
||||
|
||||
<--latest commit ID to include in this release-->
|
||||
|
||||
### Changelog
|
||||
|
||||
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||
|
||||
### Checklist
|
||||
|
||||
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||
branch **development** version is *MAJOR.MINOR.0*.
|
||||
|
||||
#### On the day of the feature freeze
|
||||
|
||||
Change the `master` branch to the next MINOR+1 version:
|
||||
|
||||
- [ ] Switch to the `master` branch.
|
||||
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR+1`, eg. `bump_dev_0_22`.
|
||||
- [ ] Bump the `bump_dev_MAJOR_MINOR+1` branch to the next development MINOR+1 version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
|
||||
- Update the `CHANGELOG.md` file.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
|
||||
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR+1` branch to `master`.
|
||||
- Title PR "Bump version to MAJOR.MINOR+1.0".
|
||||
|
||||
Create a new release branch and release candidate tag:
|
||||
|
||||
- [ ] Double check that your local `master` is up-to-date with the upstream repo.
|
||||
- [ ] Create a new branch called `release/MAJOR.MINOR+1` from `master`.
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.1".
|
||||
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||
- The tag name should be `vMAJOR.MINOR+1.0-rc.1`
|
||||
- Use message "Release MAJOR.MINOR+1.0 rc.1".
|
||||
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||
- [ ] Push the `release/MAJOR.MINOR` branch and new tag to the `bitcoindevkit/bdk` repo.
|
||||
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.1` tag.
|
||||
|
||||
If any issues need to be fixed before the *MAJOR.MINOR+1.0* version is released:
|
||||
|
||||
- [ ] Merge fix PRs to the `master` branch.
|
||||
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR+1` branch.
|
||||
- [ ] Verify fixes in `release/MAJOR.MINOR+1` branch.
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0-rc.x+1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0-rc.x+1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0-rc.x+1".
|
||||
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||
- The tag name should be `vMAJOR.MINOR+1.0-rc.x+1`, where x is the current release candidate number.
|
||||
- Use tag message "Release MAJOR.MINOR+1.0 rc.x+1".
|
||||
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||
- Use `git push --tags` option to push the new `vMAJOR.MINOR+1.0-rc.x+1` tag.
|
||||
|
||||
#### On the day of the release
|
||||
|
||||
Tag and publish new release:
|
||||
|
||||
- [ ] Bump the `release/MAJOR.MINOR+1` branch to `MAJOR.MINOR+1.0` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR+1.0`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR+1.0".
|
||||
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR+1` branch.
|
||||
- The tag name should be `vMAJOR.MINOR+1.0`
|
||||
- The first line of the tag message should be "Release MAJOR.MINOR+1.0".
|
||||
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||
- [ ] Wait for the CI to finish one last time.
|
||||
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||
- [ ] Publish **all** the updated crates to crates.io.
|
||||
- [ ] Create the release on GitHub.
|
||||
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||
- Set the title to `Release MAJOR.MINOR+1.0`.
|
||||
- In the release notes body put the **Summary** and **Changelog**.
|
||||
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||
- [ ] Celebrate 🎉
|
||||
|
||||
[Semantic Versioning]: https://semver.org/
|
||||
[crates.io]: https://crates.io/crates/bdk
|
||||
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||
71
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
71
.github/ISSUE_TEMPLATE/patch_release.md
vendored
Normal file
@@ -0,0 +1,71 @@
|
||||
---
|
||||
name: Patch Release
|
||||
about: Create a new patch release [for release managers only]
|
||||
title: 'Release MAJOR.MINOR.PATCH+1'
|
||||
labels: 'release'
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
## Create a new patch release
|
||||
|
||||
### Summary
|
||||
|
||||
<--release summary to be used in announcements-->
|
||||
|
||||
### Commit
|
||||
|
||||
<--latest commit ID to include in this release-->
|
||||
|
||||
### Changelog
|
||||
|
||||
<--add notices from PRs merged since the prior release, see ["keep a changelog"]-->
|
||||
|
||||
### Checklist
|
||||
|
||||
Release numbering must follow [Semantic Versioning]. These steps assume the current `master`
|
||||
branch **development** version is *MAJOR.MINOR.PATCH*.
|
||||
|
||||
### On the day of the patch release
|
||||
|
||||
Change the `master` branch to the new PATCH+1 version:
|
||||
|
||||
- [ ] Switch to the `master` branch.
|
||||
- [ ] Create a new PR branch called `bump_dev_MAJOR_MINOR_PATCH+1`, eg. `bump_dev_0_22_1`.
|
||||
- [ ] Bump the `bump_dev_MAJOR_MINOR` branch to the next development PATCH+1 version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR.PATCH+1`.
|
||||
- Update the `CHANGELOG.md` file.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
|
||||
- [ ] Create PR and merge the `bump_dev_MAJOR_MINOR_PATCH+1` branch to `master`.
|
||||
- Title PR "Bump version to MAJOR.MINOR.PATCH+1".
|
||||
|
||||
Cherry-pick, tag and publish new PATCH+1 release:
|
||||
|
||||
- [ ] Merge fix PRs to the `master` branch.
|
||||
- [ ] Git cherry-pick fix commits to the `release/MAJOR.MINOR` branch to be patched.
|
||||
- [ ] Verify fixes in `release/MAJOR.MINOR` branch.
|
||||
- [ ] Bump the `release/MAJOR.MINOR.PATCH+1` branch to `MAJOR.MINOR.PATCH+1` version.
|
||||
- Change the `Cargo.toml` version value to `MAJOR.MINOR.MINOR.PATCH+1`.
|
||||
- The commit message should be "Bump version to MAJOR.MINOR.PATCH+1".
|
||||
- [ ] Add a tag to the `HEAD` commit in the `release/MAJOR.MINOR` branch.
|
||||
- The tag name should be `vMAJOR.MINOR.PATCH+1`
|
||||
- The first line of the tag message should be "Release MAJOR.MINOR.PATCH+1".
|
||||
- In the body of the tag message put a copy of the **Summary** and **Changelog** for the release.
|
||||
- Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||
- [ ] Wait for the CI to finish one last time.
|
||||
- [ ] Push the new tag to the `bitcoindevkit/bdk` repo.
|
||||
- [ ] Publish **all** the updated crates to crates.io.
|
||||
- [ ] Create the release on GitHub.
|
||||
- Go to "tags", click on the dots on the right and select "Create Release".
|
||||
- Set the title to `Release MAJOR.MINOR.PATCH+1`.
|
||||
- In the release notes body put the **Summary** and **Changelog**.
|
||||
- Use the "+ Auto-generate release notes" button to add details from included PRs.
|
||||
- Until we reach a `1.0.0` release check the "Pre-release" box.
|
||||
- [ ] Make sure the new release shows up on [crates.io] and that the docs are built correctly on [docs.rs].
|
||||
- [ ] Announce the release, using the **Summary**, on Discord, Twitter and Mastodon.
|
||||
- [ ] Celebrate 🎉
|
||||
|
||||
[Semantic Versioning]: https://semver.org/
|
||||
[crates.io]: https://crates.io/crates/bdk
|
||||
[docs.rs]: https://docs.rs/bdk/latest/bdk
|
||||
["keep a changelog"]: https://keepachangelog.com/en/1.0.0/
|
||||
8
.github/dependabot.yml
vendored
Normal file
8
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,8 @@
|
||||
# Set update schedule for GitHub Actions
|
||||
version: 2
|
||||
updates:
|
||||
- package-ecosystem: "github-actions"
|
||||
directory: "/"
|
||||
schedule:
|
||||
# Check for updates to GitHub Actions every week
|
||||
interval: "weekly"
|
||||
6
.github/pull_request_template.md
vendored
6
.github/pull_request_template.md
vendored
@@ -9,6 +9,11 @@
|
||||
<!-- In this section you can include notes directed to the reviewers, like explaining why some parts
|
||||
of the PR were done in a specific way -->
|
||||
|
||||
### Changelog notice
|
||||
|
||||
<!-- Notice the release manager should include in the release tag message changelog -->
|
||||
<!-- See https://keepachangelog.com/en/1.0.0/ for examples -->
|
||||
|
||||
### Checklists
|
||||
|
||||
#### All Submissions:
|
||||
@@ -21,7 +26,6 @@ of the PR were done in a specific way -->
|
||||
|
||||
* [ ] I've added tests for the new feature
|
||||
* [ ] I've added docs for the new feature
|
||||
* [ ] I've updated `CHANGELOG.md`
|
||||
|
||||
#### Bugfixes:
|
||||
|
||||
|
||||
68
.github/workflows/code_coverage.yml
vendored
68
.github/workflows/code_coverage.yml
vendored
@@ -1,37 +1,57 @@
|
||||
on: [push]
|
||||
on: [push, pull_request]
|
||||
|
||||
name: Code Coverage
|
||||
|
||||
jobs:
|
||||
|
||||
Codecov:
|
||||
name: Code Coverage
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
RUSTFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
RUSTDOCFLAGS: '-Zprofile -Ccodegen-units=1 -Cinline-threshold=0 -Clink-dead-code -Coverflow-checks=off'
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
RUSTDOCFLAGS: "-Cinstrument-coverage"
|
||||
LLVM_PROFILE_FILE: "./target/coverage/%p-%m.profraw"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install rustup
|
||||
run: curl https://sh.rustup.rs -sSf | sh -s -- -y
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features all-keys,compiler,esplora,ureq,compact_filters --no-default-features
|
||||
|
||||
- id: coverage
|
||||
name: Generate coverage
|
||||
uses: actions-rs/grcov@v0.1.5
|
||||
|
||||
- name: Upload coverage to Codecov
|
||||
uses: codecov/codecov-action@v2
|
||||
- name: Install lcov tools
|
||||
run: sudo apt-get install lcov -y
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
file: ${{ steps.coverage.outputs.report }}
|
||||
directory: ./coverage/reports/
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
components: llvm-tools-preview
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Install grcov
|
||||
run: if [[ ! -e ~/.cargo/bin/grcov ]]; then cargo install grcov; fi
|
||||
# TODO: re-enable the hwi tests
|
||||
- name: Build simulator image
|
||||
run: docker build -t hwi/ledger_emulator ./ci -f ci/Dockerfile.ledger
|
||||
- name: Run simulator image
|
||||
run: docker run --name simulator --network=host hwi/ledger_emulator &
|
||||
- name: Install Python
|
||||
uses: actions/setup-python@v4
|
||||
with:
|
||||
python-version: '3.9'
|
||||
- name: Install python dependencies
|
||||
run: pip install hwi==2.1.1 protobuf==3.20.1
|
||||
- name: Test
|
||||
run: cargo test --all-features
|
||||
- name: Make coverage directory
|
||||
run: mkdir coverage
|
||||
- name: Run grcov
|
||||
run: grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --keep-only '**/crates/**' --ignore '**/tests/**' --ignore '**/examples/**' -o ./coverage/lcov.info
|
||||
- name: Generate HTML coverage report
|
||||
run: genhtml -o coverage-report.html --ignore-errors source ./coverage/lcov.info
|
||||
- name: Coveralls upload
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
github-token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
name: coverage-report
|
||||
path: coverage-report.html
|
||||
|
||||
198
.github/workflows/cont_integration.yml
vendored
198
.github/workflows/cont_integration.yml
vendored
@@ -10,118 +10,61 @@ jobs:
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- version: 1.60.0 # STABLE
|
||||
- version: stable
|
||||
clippy: true
|
||||
- version: 1.56.1 # MSRV
|
||||
- version: 1.63.0 # MSRV
|
||||
features:
|
||||
- default
|
||||
- minimal
|
||||
- all-keys
|
||||
- minimal,use-esplora-ureq
|
||||
- key-value-db
|
||||
- electrum
|
||||
- compact_filters
|
||||
- esplora,ureq,key-value-db,electrum
|
||||
- compiler
|
||||
- rpc
|
||||
- verify
|
||||
- async-interface
|
||||
- use-esplora-reqwest
|
||||
- sqlite
|
||||
- sqlite-bundled
|
||||
- --no-default-features
|
||||
- --all-features
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Generate cache key
|
||||
run: echo "${{ matrix.rust.version }} ${{ matrix.features }}" | tee .cache_key
|
||||
- name: cache
|
||||
uses: actions/cache@v2
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('.cache_key') }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default ${{ matrix.rust.version }}
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add clippy
|
||||
if: ${{ matrix.rust.clippy }}
|
||||
run: rustup component add clippy
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
toolchain: ${{ matrix.rust.version }}
|
||||
override: true
|
||||
profile: minimal
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Pin dependencies for MSRV
|
||||
if: matrix.rust.version == '1.63.0'
|
||||
run: |
|
||||
cargo update -p zstd-sys --precise "2.0.8+zstd.1.5.5"
|
||||
cargo update -p time --precise "0.3.20"
|
||||
cargo update -p home --precise "0.5.5"
|
||||
- name: Build
|
||||
run: cargo build --features ${{ matrix.features }} --no-default-features
|
||||
- name: Clippy
|
||||
if: ${{ matrix.rust.clippy }}
|
||||
run: cargo clippy --all-targets --features ${{ matrix.features }} --no-default-features -- -D warnings
|
||||
run: cargo build ${{ matrix.features }}
|
||||
- name: Test
|
||||
run: cargo test --features ${{ matrix.features }} --no-default-features
|
||||
run: cargo test ${{ matrix.features }}
|
||||
|
||||
test-readme-examples:
|
||||
name: Test README.md examples
|
||||
check-no-std:
|
||||
name: Check no_std
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-test-md-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features test-md-docs --no-default-features -- doctest::ReadmeDoctests
|
||||
|
||||
test-blockchains:
|
||||
name: Blockchain ${{ matrix.blockchain.features }}
|
||||
runs-on: ubuntu-20.04
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
blockchain:
|
||||
- name: electrum
|
||||
testprefix: blockchain::electrum::test
|
||||
features: test-electrum,verify
|
||||
- name: rpc
|
||||
testprefix: blockchain::rpc::test
|
||||
features: test-rpc
|
||||
- name: rpc-legacy
|
||||
testprefix: blockchain::rpc::test
|
||||
features: test-rpc-legacy
|
||||
- name: esplora
|
||||
testprefix: esplora
|
||||
features: test-esplora,use-esplora-reqwest,verify
|
||||
- name: esplora
|
||||
testprefix: esplora
|
||||
features: test-esplora,use-esplora-ureq,verify
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Setup rust toolchain
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
- name: Test
|
||||
run: cargo test --no-default-features --features ${{ matrix.blockchain.features }} ${{ matrix.blockchain.testprefix }}::bdk_blockchain_tests
|
||||
profile: minimal
|
||||
# target: "thumbv6m-none-eabi"
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Check bdk_chain
|
||||
working-directory: ./crates/chain
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,hashbrown
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
# TODO "--target thumbv6m-none-eabi" should work but currently does not
|
||||
run: cargo check --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown
|
||||
|
||||
check-wasm:
|
||||
name: Check WASM
|
||||
@@ -132,29 +75,25 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: ${{ runner.os }}-cargo-${{ github.job }}-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
# Install a recent version of clang that supports wasm32
|
||||
- run: wget -O - https://apt.llvm.org/llvm-snapshot.gpg.key | sudo apt-key add - || exit 1
|
||||
- run: sudo apt-add-repository "deb http://apt.llvm.org/focal/ llvm-toolchain-focal-10 main" || exit 1
|
||||
- run: sudo apt-get update || exit 1
|
||||
- run: sudo apt-get install -y libclang-common-10-dev clang-10 libc6-dev-i386 || exit 1
|
||||
- name: Set default toolchain
|
||||
run: rustup default 1.56.1 # STABLE
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add target wasm32
|
||||
run: rustup target add wasm32-unknown-unknown
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Check
|
||||
run: cargo check --target wasm32-unknown-unknown --features use-esplora-reqwest --no-default-features
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
target: "wasm32-unknown-unknown"
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Check bdk
|
||||
working-directory: ./crates/bdk
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,dev-getrandom-wasm
|
||||
- name: Check esplora
|
||||
working-directory: ./crates/esplora
|
||||
run: cargo check --target wasm32-unknown-unknown --no-default-features --features bitcoin/no-std,miniscript/no-std,bdk_chain/hashbrown,async
|
||||
|
||||
fmt:
|
||||
name: Rust fmt
|
||||
@@ -162,13 +101,28 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Add rustfmt
|
||||
run: rustup component add rustfmt
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
override: true
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- name: Check fmt
|
||||
run: cargo fmt --all -- --config format_code_in_doc_comments=true --check
|
||||
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features --all-targets -- -D warnings
|
||||
|
||||
16
.github/workflows/nightly_docs.yml
vendored
16
.github/workflows/nightly_docs.yml
vendored
@@ -9,22 +9,18 @@ jobs:
|
||||
steps:
|
||||
- name: Checkout sources
|
||||
uses: actions/checkout@v2
|
||||
- name: Setup cache
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/registry
|
||||
~/.cargo/git
|
||||
target
|
||||
key: nightly-docs-${{ hashFiles('**/Cargo.toml','**/Cargo.lock') }}
|
||||
- name: Set default toolchain
|
||||
run: rustup default nightly-2022-01-25
|
||||
run: rustup default nightly-2022-12-14
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Rust Cache
|
||||
uses: Swatinem/rust-cache@v2.2.1
|
||||
- name: Build docs
|
||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,ureq,compact_filters,key-value-db,all-keys,sqlite -- --cfg docsrs -Dwarnings
|
||||
run: cargo doc --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: '--cfg docsrs -Dwarnings'
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -1,5 +1,9 @@
|
||||
/target
|
||||
Cargo.lock
|
||||
/.vscode
|
||||
|
||||
*.swp
|
||||
.idea
|
||||
|
||||
# Example persisted files.
|
||||
*.db
|
||||
|
||||
214
CHANGELOG.md
214
CHANGELOG.md
@@ -1,12 +1,155 @@
|
||||
# Changelog
|
||||
All notable changes to this project will be documented in this file.
|
||||
|
||||
All notable changes to this project can be found here and in each release's git tag and can be viewed with `git tag -ln100 "v*"`. See also [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
|
||||
|
||||
Contributors do not need to change this file but do need to add changelog details in their PR descriptions. The person making the next release will collect changelog details from included PRs and edit this file prior to each release.
|
||||
|
||||
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
|
||||
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [v0.21.0] - [v0.20.0]
|
||||
## [v0.27.1]
|
||||
|
||||
### Summary
|
||||
|
||||
Fixes [RUSTSEC-2022-0090], this issue is only applicable if you are using the optional sqlite database feature.
|
||||
|
||||
[RUSTSEC-2022-0090]: https://rustsec.org/advisories/RUSTSEC-2022-0090
|
||||
|
||||
### Changed
|
||||
|
||||
- Update optional sqlite dependency from 0.27.0 to 0.28.0. #867
|
||||
|
||||
## [v0.27.0]
|
||||
|
||||
### Summary
|
||||
|
||||
A maintenance release with a bump in project MSRV to 1.57.0, updated dependence and a few developer oriented improvements. Improvements include better error formatting, don't default to async/await for wasm32 and adding derived PartialEq and Eq on SyncTime.
|
||||
|
||||
### Changed
|
||||
|
||||
- Improve display error formatting #814
|
||||
- Don't default to use async/await on wasm32 #831
|
||||
- Project MSRV changed from 1.56.1 to 1.57.0 #842
|
||||
- Update rust-miniscript dependency to latest bug fix release 9.0 #844
|
||||
|
||||
### Added
|
||||
|
||||
- Derive PartialEq, Eq on SyncTime #837
|
||||
|
||||
## [v0.26.0]
|
||||
|
||||
### Summary
|
||||
|
||||
This release improves Fulcrum electrum server compatibility and fixes public descriptor template key origin paths. We also snuck in small enhancements to configure the electrum client to validate the domain using SSL and sort TransactionDetails by block height and timestamp.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Make electrum blockchain client `save_tx` function order independent to work with Fulcrum servers. #808
|
||||
- Fix wrong testnet key origin path in public descriptor templates. #818
|
||||
- Make README.md code examples compile without errors. #820
|
||||
|
||||
### Changed
|
||||
|
||||
- Bump `hwi` dependency to `0.4.0`. #825
|
||||
- Bump `esplora-client` dependency to `0.3` #830
|
||||
|
||||
### Added
|
||||
|
||||
- For electrum blockchain client, allow user to configure whether to validate the domain using SSL. #805
|
||||
- Implement ordering for `TransactionDetails`. #812
|
||||
|
||||
## [v0.25.0]
|
||||
|
||||
### Summary
|
||||
|
||||
This release fixes slow sync time and big script_pubkeys table with SQLite, the wallet rescan height for the FullyNodedExport and setting the network for keys in the KeyMap when using descriptor templates. Also added are new blockchain and mnemonic examples.
|
||||
|
||||
### Fixed
|
||||
|
||||
- Slow sync time and big script_pubkeys table with SQLite.
|
||||
- Wallet rescan height for the FullyNodedExport.
|
||||
- Setting the network for keys in the KeyMap when using descriptor templates.
|
||||
|
||||
### Added
|
||||
|
||||
- Examples for connecting to Esplora, Electrum Server, Neutrino and Bitcoin Core.
|
||||
- Example for using a mnemonic in a descriptors.
|
||||
|
||||
## [v0.24.0]
|
||||
|
||||
### Summary
|
||||
|
||||
This release contains important dependency updates for `rust-bitcoin` to `0.29` and `rust-miniscript` to `8.0`, plus related crates that also depend on the latest version of `rust-bitcoin`. The release also includes a breaking change to the BDK signer which now produces low-R signatures by default, saving one byte. A bug was found in the `get_checksum` and `get_checksum_bytes` functions, which are now deprecated in favor of fixed versions called `calc_checksum` and `calc_checksum_bytes`. And finally a new `hardware-signer` features was added that re-exports the `hwi` crate, along with a new `hardware_signers.rs` example file.
|
||||
|
||||
### Changed
|
||||
|
||||
- Updated dependency versions for `rust-bitcoin` to `0.29` and `rust-miniscript` to `8.0`, plus all related crates. @afilini #770
|
||||
- BDK Signer now produces low-R signatures by default, saving one byte. If you want to preserve the original behavior, set allow_grinding in the SignOptions to false. @vladimirfomene #779
|
||||
- Deprecated `get_checksum`and `get_checksum_bytes` due to bug where they calculates the checksum of a descriptor that already has a checksum. Use `calc_checksum` and `calc_checksum_bytes` instead. @evanlinjin #765
|
||||
- Remove deprecated "address validators". @afilini #770
|
||||
|
||||
### Added
|
||||
|
||||
- New `calc_checksum` and `calc_checksum_bytes`, replace deprecated `get_checksum` and `get_checksum_bytes`. @evanlinjin #765
|
||||
- Re-export the hwi crate when the feature hardware-signer is on. @danielabrozzoni #758
|
||||
- New examples/hardware_signer.rs. @danielabrozzoni #758
|
||||
- Make psbt module public to expose PsbtUtils trait to downstream projects. @notmandatory #782
|
||||
|
||||
## [v0.23.0]
|
||||
|
||||
### Summary
|
||||
|
||||
This release brings new utilities functions on PSBTs like `fee_amount()` and `fee_rate()` and migrates BDK to use our new external esplora client library.
|
||||
As always many bug fixes, docs and tests improvement are also included.
|
||||
|
||||
### Changed
|
||||
|
||||
- Update electrum-client to 0.11.0 by @afilini in https://github.com/bitcoindevkit/bdk/pull/737
|
||||
- Change configs for source-base code coverage by @wszdexdrf in https://github.com/bitcoindevkit/bdk/pull/708
|
||||
- Improve docs regarding PSBT finalization by @tnull in https://github.com/bitcoindevkit/bdk/pull/753
|
||||
- Update compiler example to a Policy example by @rajarshimaitra in https://github.com/bitcoindevkit/bdk/pull/730
|
||||
- Fix the release process by @afilini in https://github.com/bitcoindevkit/bdk/pull/754
|
||||
- Remove redundant duplicated keys check by @afilini in https://github.com/bitcoindevkit/bdk/pull/761
|
||||
- Remove genesis_block lazy initialization by @shobitb in https://github.com/bitcoindevkit/bdk/pull/756
|
||||
- Fix `Wallet::descriptor_checksum` to actually return the checksum by @evanlinjin in https://github.com/bitcoindevkit/bdk/pull/763
|
||||
- Use the esplora client crate by @afilini in https://github.com/bitcoindevkit/bdk/pull/764
|
||||
|
||||
### Added
|
||||
|
||||
- Run code coverage on every PR by @danielabrozzoni in https://github.com/bitcoindevkit/bdk/pull/747
|
||||
- Add psbt_signer.rs example by @notmandatory in https://github.com/bitcoindevkit/bdk/pull/744
|
||||
- Add fee_amount() and fee_rate() functions to PsbtUtils trait by @notmandatory in https://github.com/bitcoindevkit/bdk/pull/728
|
||||
- Add tests to improve coverage by @vladimirfomene in https://github.com/bitcoindevkit/bdk/pull/745
|
||||
- Enable signing taproot transactions with only `non_witness_utxos` by @afilini in https://github.com/bitcoindevkit/bdk/pull/757
|
||||
- Add datatype for is_spent sqlite column by @vladimirfomene in https://github.com/bitcoindevkit/bdk/pull/713
|
||||
- Add vscode filter to gitignore by @evanlinjin in https://github.com/bitcoindevkit/bdk/pull/762
|
||||
|
||||
## [v0.22.0]
|
||||
|
||||
### Summary
|
||||
|
||||
This release brings support for hardware signers on desktop through the HWI library.
|
||||
It also includes fixes and improvements which are part of our ongoing effort of integrating
|
||||
BDK and LDK together.
|
||||
|
||||
### Changed
|
||||
|
||||
- FeeRate function name as_sat_vb to as_sat_per_vb. #678
|
||||
- Verify signatures after signing. #718
|
||||
- Dependency electrum-client to 0.11.0. #737
|
||||
|
||||
### Added
|
||||
|
||||
- Functions to create FeeRate from sats/kvbytes and sats/kwu. #678
|
||||
- Custom hardware wallet signer HwiSigner in wallet::hardwaresigner module. #682
|
||||
- Function allow_dust on TxBuilder. #689
|
||||
- Implementation of Deref<Target=UrlClient> for EsploraBlockchain. #722
|
||||
- Implementation of Deref<Target=Client> for ElectrumBlockchain #705
|
||||
- Implementation of Deref<Target=Client> for RpcBlockchain. #731
|
||||
|
||||
## [v0.21.0]
|
||||
|
||||
- Add `descriptor::checksum::get_checksum_bytes` method.
|
||||
- Add `Excess` enum to handle remaining amount after coin selection.
|
||||
@@ -15,29 +158,29 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add the ability to specify which leaves to sign in a taproot transaction through `TapLeavesOptions` in `SignOptions`
|
||||
- Add the ability to specify whether a taproot transaction should be signed using the internal key or not, using `sign_with_tap_internal_key` in `SignOptions`
|
||||
- Consolidate params `fee_amount` and `amount_needed` in `target_amount` in `CoinSelectionAlgorithm::coin_select` signature.
|
||||
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees asociated with the utxos in the `selected` field of `CoinSelectionResult`.
|
||||
- Change the meaning of the `fee_amount` field inside `CoinSelectionResult`: from now on the `fee_amount` will represent only the fees associated with the utxos in the `selected` field of `CoinSelectionResult`.
|
||||
- New `RpcBlockchain` implementation with various fixes.
|
||||
- Return balance in separate categories, namely `confirmed`, `trusted_pending`, `untrusted_pending` & `immature`.
|
||||
|
||||
## [v0.20.0] - [v0.19.0]
|
||||
## [v0.20.0]
|
||||
|
||||
- New MSRV set to `1.56.1`
|
||||
- Fee sniping discouraging through nLockTime - if the user specifies a `current_height`, we use that as a nlocktime, otherwise we use the last sync height (or 0 if we never synced)
|
||||
- Fix hang when `ElectrumBlockchainConfig::stop_gap` is zero.
|
||||
- Set coin type in BIP44, BIP49, and BIP84 templates
|
||||
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
|
||||
- Get block hash given a block height - A `get_block_hash` method is now defined on the `GetBlockHash` trait and implemented on every blockchain backend. This method expects a block height and returns the corresponding block hash.
|
||||
- Add `remove_partial_sigs` and `try_finalize` to `SignOptions`
|
||||
- Deprecate `AddressValidator`
|
||||
- Fix Electrum wallet sync potentially causing address index decrement - compare proposed index and current index before applying batch operations during sync.
|
||||
|
||||
## [v0.19.0] - [v0.18.0]
|
||||
## [v0.19.0]
|
||||
|
||||
- added `OldestFirstCoinSelection` impl to `CoinSelectionAlgorithm`
|
||||
- New MSRV set to `1.56`
|
||||
- Unpinned tokio to `1`
|
||||
- Add traits to reuse `Blockchain`s across multiple wallets (`BlockchainFactory` and `StatelessBlockchain`).
|
||||
- Upgrade to rust-bitcoin `0.28`
|
||||
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
|
||||
- If using the `sqlite-db` feature all cached wallet data is deleted due to a possible UTXO inconsistency, a wallet.sync will recreate it
|
||||
- Update `PkOrF` in the policy module to become an enum
|
||||
- Add experimental support for Taproot, including:
|
||||
- Support for `tr()` descriptors with complex tapscript trees
|
||||
@@ -46,7 +189,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Support for `tr()` descriptors in the `descriptor!()` macro
|
||||
- Add support for Bitcoin Core 23.0 when using the `rpc` blockchain
|
||||
|
||||
## [v0.18.0] - [v0.17.0]
|
||||
## [v0.18.0]
|
||||
|
||||
- Add `sqlite-bundled` feature for deployments that need a bundled version of sqlite, i.e. for mobile platforms.
|
||||
- Added `Wallet::get_signers()`, `Wallet::descriptor_checksum()` and `Wallet::get_address_validators()`, exposed the `AsDerived` trait.
|
||||
@@ -54,9 +197,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Add `keychain: KeychainKind` to `wallet::AddressInfo`.
|
||||
- Improve key generation traits
|
||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
||||
- Bump `miniscript` dependency version to `^6.1`.
|
||||
- Bump `miniscript` dependency version to `^6.1`.
|
||||
|
||||
## [v0.17.0] - [v0.16.1]
|
||||
## [v0.17.0]
|
||||
|
||||
- Removed default verification from `wallet::sync`. sync-time verification is added in `script_sync` and is activated by `verify` feature flag.
|
||||
- `verify` flag removed from `TransactionDetails`.
|
||||
@@ -77,45 +220,45 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||
- Removed `max_addresses` sync parameter which determined how many addresses to cache before syncing since this can just be done with `ensure_addresses_cached`.
|
||||
- remove `flush` method from the `Database` trait.
|
||||
|
||||
## [v0.16.1] - [v0.16.0]
|
||||
## [v0.16.1]
|
||||
|
||||
- Pin tokio dependency version to ~1.14 to prevent errors due to their new MSRV 1.49.0
|
||||
|
||||
## [v0.16.0] - [v0.15.0]
|
||||
## [v0.16.0]
|
||||
|
||||
- Disable `reqwest` default features.
|
||||
- Added `reqwest-default-tls` feature: Use this to restore the TLS defaults of reqwest if you don't want to add a dependency to it in your own manifest.
|
||||
- Use dust_value from rust-bitcoin
|
||||
- Fixed generating WIF in the correct network format.
|
||||
|
||||
## [v0.15.0] - [v0.14.0]
|
||||
## [v0.15.0]
|
||||
|
||||
- Overhauled sync logic for electrum and esplora.
|
||||
- Unify ureq and reqwest esplora backends to have the same configuration parameters. This means reqwest now has a timeout parameter and ureq has a concurrency parameter.
|
||||
- Fixed esplora fee estimation.
|
||||
|
||||
## [v0.14.0] - [v0.13.0]
|
||||
## [v0.14.0]
|
||||
|
||||
- BIP39 implementation dependency, in `keys::bip39` changed from tiny-bip39 to rust-bip39.
|
||||
- Add new method on the `TxBuilder` to embed data in the transaction via `OP_RETURN`. To allow that a fix to check the dust only on spendable output has been introduced.
|
||||
- Update the `Database` trait to store the last sync timestamp and block height
|
||||
- Rename `ConfirmationTime` to `BlockTime`
|
||||
|
||||
## [v0.13.0] - [v0.12.0]
|
||||
## [v0.13.0]
|
||||
|
||||
- Exposed `get_tx()` method from `Database` to `Wallet`.
|
||||
|
||||
## [v0.12.0] - [v0.11.0]
|
||||
## [v0.12.0]
|
||||
|
||||
- Activate `miniscript/use-serde` feature to allow consumers of the library to access it via the re-exported `miniscript` crate.
|
||||
- Add support for proxies in `EsploraBlockchain`
|
||||
- Added `SqliteDatabase` that implements `Database` backed by a sqlite database using `rusqlite` crate.
|
||||
|
||||
## [v0.11.0] - [v0.10.0]
|
||||
## [v0.11.0]
|
||||
|
||||
- Added `flush` method to the `Database` trait to explicitly flush to disk latest changes on the db.
|
||||
|
||||
## [v0.10.0] - [v0.9.0]
|
||||
## [v0.10.0]
|
||||
|
||||
- Added `RpcBlockchain` in the `AnyBlockchain` struct to allow using Rpc backend where `AnyBlockchain` is used (eg `bdk-cli`)
|
||||
- Removed hard dependency on `tokio`.
|
||||
@@ -129,21 +272,21 @@ To decouple the `Wallet` from the `Blockchain` we've made major changes:
|
||||
- Removed `stop_gap` from `Blockchain` trait and added it to only `ElectrumBlockchain` and `EsploraBlockchain` structs.
|
||||
- Added a `ureq` backend for use when not using feature `async-interface` or target WASM. `ureq` is a blocking HTTP client.
|
||||
|
||||
## [v0.9.0] - [v0.8.0]
|
||||
## [v0.9.0]
|
||||
|
||||
### Wallet
|
||||
|
||||
- Added Bitcoin core RPC added as blockchain backend
|
||||
- Added a `verify` feature that can be enable to verify the unconfirmed txs we download against the consensus rules
|
||||
|
||||
## [v0.8.0] - [v0.7.0]
|
||||
## [v0.8.0]
|
||||
|
||||
### Wallet
|
||||
- Added an option that must be explicitly enabled to allow signing using non-`SIGHASH_ALL` sighashes (#350)
|
||||
#### Changed
|
||||
`get_address` now returns an `AddressInfo` struct that includes the index and derefs to `Address`.
|
||||
|
||||
## [v0.7.0] - [v0.6.0]
|
||||
## [v0.7.0]
|
||||
|
||||
### Policy
|
||||
#### Changed
|
||||
@@ -158,7 +301,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
|
||||
- Require and validate `non_witness_utxo` for SegWit signatures by default, can be adjusted with `SignOptions`
|
||||
- Replace the opt-in builder option `force_non_witness_utxo` with the opposite `only_witness_utxo`. From now on we will provide the `non_witness_utxo`, unless explicitly asked not to.
|
||||
|
||||
## [v0.6.0] - [v0.5.1]
|
||||
## [v0.6.0]
|
||||
|
||||
### Misc
|
||||
#### Changed
|
||||
@@ -182,13 +325,13 @@ Timelocks are considered (optionally) in building the `satisfaction` field
|
||||
#### Fixed
|
||||
- Fixed `coin_select` calculation for UTXOs where `value < fee` that caused over-/underflow errors.
|
||||
|
||||
## [v0.5.1] - [v0.5.0]
|
||||
## [v0.5.1]
|
||||
|
||||
### Misc
|
||||
#### Changed
|
||||
- Pin `hyper` to `=0.14.4` to make it compile on Rust 1.45
|
||||
|
||||
## [v0.5.0] - [v0.4.0]
|
||||
## [v0.5.0]
|
||||
|
||||
### Misc
|
||||
#### Changed
|
||||
@@ -198,7 +341,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
|
||||
#### Changed
|
||||
- `FeeRate` constructors `from_sat_per_vb` and `default_min_relay_fee` are now `const` functions
|
||||
|
||||
## [v0.4.0] - [v0.3.0]
|
||||
## [v0.4.0]
|
||||
|
||||
### Keys
|
||||
#### Changed
|
||||
@@ -227,7 +370,7 @@ Timelocks are considered (optionally) in building the `satisfaction` field
|
||||
- Removed unneeded `Result<(), PolicyError>` return type for `Satisfaction::finalize()`
|
||||
- Removed the `TooManyItemsSelected` policy error (see commit message for more details)
|
||||
|
||||
## [v0.3.0] - [v0.2.0]
|
||||
## [v0.3.0]
|
||||
|
||||
### Descriptor
|
||||
#### Changed
|
||||
@@ -264,7 +407,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
#### Changed
|
||||
- Remove `cli.rs` module, `cli-utils` feature and `repl.rs` example; moved to new [`bdk-cli`](https://github.com/bitcoindevkit/bdk-cli) repository
|
||||
|
||||
## [v0.2.0] - [0.1.0-beta.1]
|
||||
## [v0.2.0]
|
||||
|
||||
### Project
|
||||
#### Added
|
||||
@@ -306,7 +449,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
#### Changed
|
||||
- Simplify the architecture of blockchain traits
|
||||
- Improve sync
|
||||
- Remove unused varaint HeaderParseFail
|
||||
- Remove unused variant `HeaderParseFail`
|
||||
|
||||
### CLI
|
||||
#### Added
|
||||
@@ -374,7 +517,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Default to SIGHASH_ALL if not specified
|
||||
- Replace ChangeSpendPolicy::filter_utxos with a predicate
|
||||
- Make 'unspendable' into a HashSet
|
||||
- Stop implicitly enforcing manaul selection by .add_utxo
|
||||
- Stop implicitly enforcing manual selection by .add_utxo
|
||||
- Rename DumbCS to LargestFirstCoinSelection
|
||||
- Rename must_use_utxos to required_utxos
|
||||
- Rename may_use_utxos to optional_uxtos
|
||||
@@ -386,7 +529,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Use TXIN_DEFAULT_WEIGHT constant in coin selection
|
||||
- Replace `must_use` with `required` in coin selection
|
||||
- Take both spending policies into account in create_tx
|
||||
- Check last derivation in cache to avoid recomputation
|
||||
- Check last derivation in cache to avoid recomputing
|
||||
- Use the branch-and-bound cs by default
|
||||
- Make coin_select return UTXOs instead of TxIns
|
||||
- Build output lookup inside complete transaction
|
||||
@@ -407,7 +550,7 @@ final transaction is created by calling `finish` on the builder.
|
||||
- Require esplora feature for repl example
|
||||
|
||||
#### Security
|
||||
- Use dirs-next instead of dirs since the latter is unmantained
|
||||
- Use dirs-next instead of dirs since the latter is unmaintained
|
||||
|
||||
## [0.1.0-beta.1] - 2020-09-08
|
||||
|
||||
@@ -492,4 +635,11 @@ final transaction is created by calling `finish` on the builder.
|
||||
[v0.19.0]: https://github.com/bitcoindevkit/bdk/compare/v0.18.0...v0.19.0
|
||||
[v0.20.0]: https://github.com/bitcoindevkit/bdk/compare/v0.19.0...v0.20.0
|
||||
[v0.21.0]: https://github.com/bitcoindevkit/bdk/compare/v0.20.0...v0.21.0
|
||||
[unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...HEAD
|
||||
[v0.22.0]: https://github.com/bitcoindevkit/bdk/compare/v0.21.0...v0.22.0
|
||||
[v0.23.0]: https://github.com/bitcoindevkit/bdk/compare/v0.22.0...v0.23.0
|
||||
[v0.24.0]: https://github.com/bitcoindevkit/bdk/compare/v0.23.0...v0.24.0
|
||||
[v0.25.0]: https://github.com/bitcoindevkit/bdk/compare/v0.24.0...v0.25.0
|
||||
[v0.26.0]: https://github.com/bitcoindevkit/bdk/compare/v0.25.0...v0.26.0
|
||||
[v0.27.0]: https://github.com/bitcoindevkit/bdk/compare/v0.26.0...v0.27.0
|
||||
[v0.27.1]: https://github.com/bitcoindevkit/bdk/compare/v0.27.0...v0.27.1
|
||||
[Unreleased]: https://github.com/bitcoindevkit/bdk/compare/v0.27.1...HEAD
|
||||
|
||||
@@ -28,7 +28,7 @@ The codebase is maintained using the "contributor workflow" where everyone
|
||||
without exception contributes patch proposals using "pull requests". This
|
||||
facilitates social contribution, easy testing and peer review.
|
||||
|
||||
To contribute a patch, the worflow is a as follows:
|
||||
To contribute a patch, the workflow is as follows:
|
||||
|
||||
1. Fork Repository
|
||||
2. Create topic branch
|
||||
@@ -46,15 +46,15 @@ Every new feature should be covered by functional tests where possible.
|
||||
When refactoring, structure your PR to make it easy to review and don't
|
||||
hesitate to split it into multiple small, focused PRs.
|
||||
|
||||
The Minimal Supported Rust Version is 1.46 (enforced by our CI).
|
||||
The Minimal Supported Rust Version is **1.57.0** (enforced by our CI).
|
||||
|
||||
Commits should cover both the issue fixed and the solution's rationale.
|
||||
These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind.
|
||||
These [guidelines](https://chris.beams.io/posts/git-commit/) should be kept in mind. Commit messages should follow the ["Conventional Commits 1.0.0"](https://www.conventionalcommits.org/en/v1.0.0/) to make commit histories easier to read by humans and automated tools.
|
||||
|
||||
To facilitate communication with other contributors, the project is making use
|
||||
of GitHub's "assignee" field. First check that no one is assigned and then
|
||||
comment suggesting that you're working on it. If someone is already assigned,
|
||||
don't hesitate to ask if the assigned party or previous commenters are still
|
||||
don't hesitate to ask if the assigned party or previous commenter are still
|
||||
working on it if it has been awhile.
|
||||
|
||||
Deprecation policy
|
||||
@@ -91,7 +91,7 @@ This is also enforced by the CI.
|
||||
Security
|
||||
--------
|
||||
|
||||
Security is a high priority of BDK; disclosure of security vulnerabilites helps
|
||||
Security is a high priority of BDK; disclosure of security vulnerabilities helps
|
||||
prevent user loss of funds.
|
||||
|
||||
Note that BDK is currently considered "pre-production" during this time, there
|
||||
|
||||
146
Cargo.toml
146
Cargo.toml
@@ -1,124 +1,24 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.21.0-rc.1"
|
||||
edition = "2018"
|
||||
authors = ["Alekos Filini <alekos.filini@gmail.com>", "Riccardo Casatta <riccardo@casatta.it>"]
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
|
||||
[dependencies]
|
||||
bdk-macros = "^0.6"
|
||||
log = "^0.4"
|
||||
miniscript = { version = "7.0", features = ["use-serde"] }
|
||||
bitcoin = { version = "0.28.1", features = ["use-serde", "base64", "rand"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.7"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.10", optional = true }
|
||||
rusqlite = { version = "0.27.0", optional = true }
|
||||
ahash = { version = "0.7.6", optional = true }
|
||||
reqwest = { version = "0.11", optional = true, default-features = false, features = ["json"] }
|
||||
ureq = { version = "~2.2.0", features = ["json"], optional = true }
|
||||
futures = { version = "0.3", optional = true }
|
||||
async-trait = { version = "0.1", optional = true }
|
||||
rocksdb = { version = "0.14", default-features = false, features = ["snappy"], optional = true }
|
||||
cc = { version = ">=1.0.64", optional = true }
|
||||
socks = { version = "0.3", optional = true }
|
||||
lazy_static = { version = "1.4", optional = true }
|
||||
|
||||
bip39 = { version = "1.0.1", optional = true }
|
||||
bitcoinconsensus = { version = "0.19.0-3", optional = true }
|
||||
|
||||
# Needed by bdk_blockchain_tests macro and the `rpc` feature
|
||||
bitcoincore-rpc = { version = "0.15", optional = true }
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
async-trait = "0.1"
|
||||
js-sys = "0.3"
|
||||
rand = { version = "^0.7", features = ["wasm-bindgen"] }
|
||||
|
||||
[features]
|
||||
minimal = []
|
||||
compiler = ["miniscript/compiler"]
|
||||
verify = ["bitcoinconsensus"]
|
||||
default = ["key-value-db", "electrum"]
|
||||
sqlite = ["rusqlite", "ahash"]
|
||||
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
|
||||
compact_filters = ["rocksdb", "socks", "lazy_static", "cc"]
|
||||
key-value-db = ["sled"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
rpc = ["bitcoincore-rpc"]
|
||||
|
||||
# We currently provide mulitple implementations of `Blockchain`, all are
|
||||
# blocking except for the `EsploraBlockchain` which can be either async or
|
||||
# blocking, depending on the HTTP client in use.
|
||||
#
|
||||
# - Users wanting asynchronous HTTP calls should enable `async-interface` to get
|
||||
# access to the asynchronous method implementations. Then, if Esplora is wanted,
|
||||
# enable `esplora` AND `reqwest` (`--features=use-esplora-reqwest`).
|
||||
# - Users wanting blocking HTTP calls can use any of the other blockchain
|
||||
# implementations (`compact_filters`, `electrum`, or `esplora`). Users wanting to
|
||||
# use Esplora should enable `esplora` AND `ureq` (`--features=use-esplora-ureq`).
|
||||
#
|
||||
# WARNING: Please take care with the features below, various combinations will
|
||||
# fail to build. We cannot currently build `bdk` with `--all-features`.
|
||||
async-interface = ["async-trait"]
|
||||
electrum = ["electrum-client"]
|
||||
# MUST ALSO USE `--no-default-features`.
|
||||
use-esplora-reqwest = ["esplora", "reqwest", "reqwest/socks", "futures"]
|
||||
use-esplora-ureq = ["esplora", "ureq", "ureq/socks"]
|
||||
# Typical configurations will not need to use `esplora` feature directly.
|
||||
esplora = []
|
||||
|
||||
# Use below feature with `use-esplora-reqwest` to enable reqwest default TLS support
|
||||
reqwest-default-tls = ["reqwest/default-tls"]
|
||||
|
||||
# Debug/Test features
|
||||
test-blockchains = ["bitcoincore-rpc", "electrum-client"]
|
||||
test-electrum = ["electrum", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||
test-rpc = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||
test-rpc-legacy = ["rpc", "electrsd/electrs_0_8_10", "electrsd/bitcoind_0_20_0", "test-blockchains"]
|
||||
test-esplora = ["electrsd/legacy", "electrsd/esplora_a33e97e1", "electrsd/bitcoind_22_0", "test-blockchains"]
|
||||
test-md-docs = ["electrum"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4"
|
||||
env_logger = "0.7"
|
||||
clap = "2.33"
|
||||
electrsd = "0.19.1"
|
||||
|
||||
[[example]]
|
||||
name = "address_validator"
|
||||
[[example]]
|
||||
name = "compact_filters_balance"
|
||||
required-features = ["compact_filters"]
|
||||
|
||||
[[example]]
|
||||
name = "miniscriptc"
|
||||
path = "examples/compiler.rs"
|
||||
required-features = ["compiler"]
|
||||
|
||||
[[example]]
|
||||
name = "rpcwallet"
|
||||
path = "examples/rpcwallet.rs"
|
||||
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
[package.metadata.docs.rs]
|
||||
features = ["compiler", "electrum", "esplora", "ureq", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify"]
|
||||
# defines the configuration attribute `docsrs`
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/bdk",
|
||||
"crates/chain",
|
||||
"crates/file_store",
|
||||
"crates/electrum",
|
||||
"crates/esplora",
|
||||
"crates/bitcoind_rpc",
|
||||
"crates/hwi",
|
||||
"example-crates/example_cli",
|
||||
"example-crates/example_electrum",
|
||||
"example-crates/example_esplora",
|
||||
"example-crates/example_bitcoind_rpc_polling",
|
||||
"example-crates/wallet_electrum",
|
||||
"example-crates/wallet_esplora_blocking",
|
||||
"example-crates/wallet_esplora_async",
|
||||
"example-crates/wallet_rpc",
|
||||
"nursery/tmp_plan",
|
||||
"nursery/coin_select"
|
||||
]
|
||||
|
||||
[workspace.package]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
|
||||
@@ -1,46 +1,16 @@
|
||||
# Development Cycle
|
||||
|
||||
This project follows a regular releasing schedule similar to the one [used by the Rust language](https://doc.rust-lang.org/book/appendix-07-nightly-rust.html). In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
||||
This project follows a regular releasing schedule similar to the one [used by the Rust language]. In short, this means that a new release is made at a regular cadence, with all the feature/bugfixes that made it to `master` in time. This ensures that we don't keep delaying releases waiting for "just one more little thing".
|
||||
|
||||
This project uses [Semantic Versioning], but is currently at MAJOR version zero (0.y.z) meaning it is still in initial development. Anything MAY change at any time. The public API SHOULD NOT be considered stable. Until we reach version `1.0.0` we will do our best to document any breaking API changes in the changelog info attached to each release tag.
|
||||
|
||||
We decided to maintain a faster release cycle while the library is still in "beta", i.e. before release `1.0.0`: since we are constantly adding new features and, even more importantly, fixing issues, we want developers to have access to those updates as fast as possible. For this reason we will make a release **every 4 weeks**.
|
||||
|
||||
Once the project will have reached a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
||||
Once the project reaches a more mature state (>= `1.0.0`), we will very likely switch to longer release cycles of **6 weeks**.
|
||||
|
||||
The "feature freeze" will happen **one week before the release date**. This means a new branch will be created originating from the `master` tip at that time, and in that branch we will stop adding new features and only focus on ensuring the ones we've added are working properly.
|
||||
|
||||
```
|
||||
master: - - - - * - - - * - - - - - - * - - - * ...
|
||||
| / | |
|
||||
release/0.x.0: * - - # | |
|
||||
| /
|
||||
release/0.y.0: * - - #
|
||||
```
|
||||
To create a new release a release manager will create a new issue using the `Release` template and follow the template instructions.
|
||||
|
||||
As soon as the release is tagged and published, the `release` branch will be merged back into `master` to update the version in the `Cargo.toml` to apply the new `Cargo.toml` version and all the other fixes made during the feature freeze window.
|
||||
|
||||
## Making the Release
|
||||
|
||||
What follows are notes and procedures that maintainers can refer to when making releases. All the commits and tags must be signed and, ideally, also [timestamped](https://github.com/opentimestamps/opentimestamps-client/blob/master/doc/git-integration.md).
|
||||
|
||||
Pre-`v1.0.0` our "major" releases only affect the "minor" semver value. Accordingly, our "minor" releases will only affect the "patch" value.
|
||||
|
||||
1. Create a new branch called `release/x.y.z` from `master`. Double check that your local `master` is up-to-date with the upstream repo before doing so.
|
||||
2. Make a commit on the release branch to bump the version to `x.y.z-rc.1`. The message should be "Bump version to x.y.z-rc.1".
|
||||
3. Push the new branch to `bitcoindevkit/bdk` on GitHub.
|
||||
4. During the one week of feature freeze run additional tests on the release branch.
|
||||
5. If a bug is found:
|
||||
- If it's a minor issue you can just fix it in the release branch, since it will be merged back to `master` eventually
|
||||
- For bigger issues you can fix them on `master` and then *cherry-pick* the commit to the release branch
|
||||
6. Update the changelog with the new release version.
|
||||
7. Update `src/lib.rs` with the new version (line ~43)
|
||||
8. On release day, make a commit on the release branch to bump the version to `x.y.z`. The message should be "Bump version to x.y.z".
|
||||
9. Add a tag to this commit. The tag name should be `vx.y.z` (for example `v0.5.0`), and the message "Release x.y.z". Make sure the tag is signed, for extra safety use the explicit `--sign` flag.
|
||||
10. Push the new commits to the upstream release branch, wait for the CI to finish one last time.
|
||||
11. Publish **all** the updated crates to crates.io.
|
||||
12. Make a new commit to bump the version value to `x.y.(z+1)-dev`. The message should be "Bump version to x.y.(z+1)-dev".
|
||||
13. Merge the release branch back into `master`.
|
||||
14. If the `master` branch contains any unreleased changes to the `bdk-macros` crate, change the `bdk` Cargo.toml `[dependencies]` to point to the local path (i.e. `bdk-macros = { path = "./macros"}`)
|
||||
15. Create the release on GitHub: go to "tags", click on the dots on the right and select "Create Release". Then set the title to `vx.y.z` and write down some brief release notes.
|
||||
16. Make sure the new release shows up on crates.io and that the docs are built correctly on docs.rs.
|
||||
17. Announce the release on Twitter, Discord and Telegram.
|
||||
18. Celebrate :tada:
|
||||
[used by the Rust language]: https://doc.rust-lang.org/book/appendix-07-nightly-rust.html
|
||||
[Semantic Versioning]: https://semver.org/
|
||||
|
||||
195
README.md
195
README.md
@@ -1,3 +1,5 @@
|
||||
# The Bitcoin Dev Kit
|
||||
|
||||
<div align="center">
|
||||
<h1>BDK</h1>
|
||||
|
||||
@@ -11,9 +13,9 @@
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://codecov.io/gh/bitcoindevkit/bdk"><img src="https://codecov.io/gh/bitcoindevkit/bdk/branch/master/graph/badge.svg"/></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2021/11/01/Rust-1.56.1.html"><img alt="Rustc Version 1.56.1+" src="https://img.shields.io/badge/rustc-1.56.1%2B-lightgrey.svg"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
@@ -26,164 +28,67 @@
|
||||
|
||||
## About
|
||||
|
||||
The `bdk` library aims to be the core building block for Bitcoin wallets of any kind.
|
||||
The `bdk` libraries aims to provide well engineered and reviewed components for Bitcoin based applications.
|
||||
It is built upon the excellent [`rust-bitcoin`] and [`rust-miniscript`] crates.
|
||||
|
||||
* It uses [Miniscript](https://github.com/rust-bitcoin/rust-miniscript) to support descriptors with generalized conditions. This exact same library can be used to build
|
||||
single-sig wallets, multisigs, timelocked contracts and more.
|
||||
* It supports multiple blockchain backends and databases, allowing developers to choose exactly what's right for their projects.
|
||||
* It's built to be cross-platform: the core logic works on desktop, mobile, and even WebAssembly.
|
||||
* It's very easy to extend: developers can implement customized logic for blockchain backends, databases, signers, coin selection, and more, without having to fork and modify this library.
|
||||
> ⚠ The Bitcoin Dev Kit developers are in the process of releasing a `v1.0` which is a fundamental re-write of how the library works.
|
||||
> See for some background on this project: https://bitcoindevkit.org/blog/road-to-bdk-1/ (ignore the timeline 😁)
|
||||
> For a release timeline see the [`BDK 1.0 project page`].
|
||||
|
||||
## Examples
|
||||
## Architecture
|
||||
|
||||
### Sync the balance of a descriptor
|
||||
The project is split up into several crates in the `/crates` directory:
|
||||
|
||||
```rust,no_run
|
||||
use bdk::Wallet;
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
use bdk::SyncOptions;
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::bitcoin::Network;
|
||||
- [`bdk`](./crates/bdk): Contains the central high level `Wallet` type that is built from the low-level mechanisms provided by the other components
|
||||
- [`chain`](./crates/chain): Tools for storing and indexing chain data
|
||||
- [`file_store`](./crates/file_store): A (experimental) persistence backend for storing chain data in a single file.
|
||||
- [`esplora`](./crates/esplora): Extends the [`esplora-client`] crate with methods to fetch chain data from an esplora HTTP server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
- [`electrum`](./crates/electrum): Extends the [`electrum-client`] crate with methods to fetch chain data from an electrum server in the form that [`bdk_chain`] and `Wallet` can consume.
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
Fully working examples of how to use these components are in `/example-crates`:
|
||||
- [`example_cli`](./example-crates/example_cli): Library used by the `example_*` crates. Provides utilities for syncing, showing the balance, generating addresses and creating transactions without using the bdk `Wallet`.
|
||||
- [`example_electrum`](./example-crates/example_electrum): A command line Bitcoin wallet application built on top of `example_cli` and the `electrum` crate. It shows the power of the bdk tools (`chain` + `file_store` + `electrum`), without depending on the main `bdk` library.
|
||||
- [`example_esplora`](./example-crates/example_esplora): A command line Bitcoin wallet application built on top of `example_cli` and the `esplora` crate. It shows the power of the bdk tools (`chain` + `file_store` + `esplora`), without depending on the main `bdk` library.
|
||||
- [`example_bitcoind_rpc_polling`](./example-crates/example_bitcoind_rpc_polling): A command line Bitcoin wallet application built on top of `example_cli` and the `bitcoind_rpc` crate. It shows the power of the bdk tools (`chain` + `file_store` + `bitcoind_rpc`), without depending on the main `bdk` library.
|
||||
- [`wallet_esplora_blocking`](./example-crates/wallet_esplora_blocking): Uses the `Wallet` to sync and spend using the Esplora blocking interface.
|
||||
- [`wallet_esplora_async`](./example-crates/wallet_esplora_async): Uses the `Wallet` to sync and spend using the Esplora asynchronous interface.
|
||||
- [`wallet_electrum`](./example-crates/wallet_electrum): Uses the `Wallet` to sync and spend using Electrum.
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
[`BDK 1.0 project page`]: https://github.com/orgs/bitcoindevkit/projects/14
|
||||
[`rust-miniscript`]: https://github.com/rust-bitcoin/rust-miniscript
|
||||
[`rust-bitcoin`]: https://github.com/rust-bitcoin/rust-bitcoin
|
||||
[`esplora-client`]: https://docs.rs/esplora-client/
|
||||
[`electrum-client`]: https://docs.rs/electrum-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
|
||||
println!("Descriptor balance: {} SAT", wallet.get_balance()?);
|
||||
## Minimum Supported Rust Version (MSRV)
|
||||
This library should compile with any combination of features with Rust 1.63.0.
|
||||
|
||||
Ok(())
|
||||
}
|
||||
To build with the MSRV you will need to pin dependencies as follows:
|
||||
|
||||
```shell
|
||||
# zip 0.6.3 has MSRV 1.64.0
|
||||
cargo update -p zip --precise "0.6.2"
|
||||
# time 0.3.21 has MSRV 1.65.0
|
||||
cargo update -p time --precise "0.3.20"
|
||||
# jobserver 0.1.27 has MSRV 1.66.0
|
||||
cargo update -p jobserver --precise "0.1.26"
|
||||
# home 0.5.9 has MSRV 1.70.0
|
||||
cargo update -p home --precise "0.5.5"
|
||||
```
|
||||
|
||||
### Generate a few addresses
|
||||
|
||||
```rust
|
||||
use bdk::{Wallet, database::MemoryDatabase};
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
println!("Address #0: {}", wallet.get_address(New)?);
|
||||
println!("Address #1: {}", wallet.get_address(New)?);
|
||||
println!("Address #2: {}", wallet.get_address(New)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Create a transaction
|
||||
|
||||
```rust,no_run
|
||||
use bdk::{FeeRate, Wallet, SyncOptions};
|
||||
use bdk::database::MemoryDatabase;
|
||||
use bdk::blockchain::ElectrumBlockchain;
|
||||
|
||||
use bdk::electrum_client::Client;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::serialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?);
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
wallet.sync(&blockchain, SyncOptions::default())?;
|
||||
|
||||
let send_to = wallet.get_address(New)?;
|
||||
let (psbt, details) = {
|
||||
let mut builder = wallet.build_tx();
|
||||
builder
|
||||
.add_recipient(send_to.script_pubkey(), 50_000)
|
||||
.enable_rbf()
|
||||
.do_not_spend_change()
|
||||
.fee_rate(FeeRate::from_sat_per_vb(5.0));
|
||||
builder.finish()?
|
||||
};
|
||||
|
||||
println!("Transaction details: {:#?}", details);
|
||||
println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
### Sign a transaction
|
||||
|
||||
```rust,no_run
|
||||
use bdk::{Wallet, SignOptions, database::MemoryDatabase};
|
||||
|
||||
use bitcoin::base64;
|
||||
use bitcoin::consensus::deserialize;
|
||||
|
||||
fn main() -> Result<(), bdk::Error> {
|
||||
let wallet = Wallet::new(
|
||||
"wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)",
|
||||
Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"),
|
||||
bitcoin::Network::Testnet,
|
||||
MemoryDatabase::default(),
|
||||
)?;
|
||||
|
||||
let psbt = "...";
|
||||
let mut psbt = deserialize(&base64::decode(psbt).unwrap())?;
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, SignOptions::default())?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
```
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit testing
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
### Integration testing
|
||||
|
||||
Integration testing require testing features, for example:
|
||||
|
||||
```bash
|
||||
cargo test --features test-electrum
|
||||
```
|
||||
|
||||
The other options are `test-esplora`, `test-rpc` or `test-rpc-legacy` which runs against an older version of Bitcoin Core.
|
||||
Note that `electrs` and `bitcoind` binaries are automatically downloaded (on mac and linux), to specify you already have installed binaries you must use `--no-default-features` and provide `BITCOIND_EXE` and `ELECTRS_EXE` as environment variables.
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0
|
||||
([LICENSE-APACHE](LICENSE-APACHE) or http://www.apache.org/licenses/LICENSE-2.0)
|
||||
* MIT license
|
||||
([LICENSE-MIT](LICENSE-MIT) or http://opensource.org/licenses/MIT)
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
## Contribution
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally submitted
|
||||
for inclusion in the work by you, as defined in the Apache-2.0 license, shall be
|
||||
dual licensed as above, without any additional terms or conditions.
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
9
ci/Dockerfile.ledger
Normal file
9
ci/Dockerfile.ledger
Normal file
@@ -0,0 +1,9 @@
|
||||
# Taken from bitcoindevkit/rust-hwi
|
||||
FROM ghcr.io/ledgerhq/speculos
|
||||
|
||||
RUN apt-get update
|
||||
RUN apt-get install wget -y
|
||||
RUN wget "https://github.com/LedgerHQ/speculos/blob/master/apps/nanos%23btc%232.1%231c8db8da.elf?raw=true" -O /speculos/btc.elf
|
||||
ADD automation.json /speculos/automation.json
|
||||
|
||||
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanos", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
|
||||
30
ci/automation.json
Normal file
30
ci/automation.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"version": 1,
|
||||
"rules": [
|
||||
{
|
||||
"regexp": "Address \\(\\d/\\d\\)|Message hash \\(\\d/\\d\\)|Confirm|Fees|Review|Amount",
|
||||
"actions": [
|
||||
[ "button", 2, true ],
|
||||
[ "button", 2, false ]
|
||||
]
|
||||
},
|
||||
{
|
||||
"text": "Sign",
|
||||
"conditions": [
|
||||
[ "seen", false ]
|
||||
],
|
||||
"actions": [
|
||||
[ "button", 2, true ],
|
||||
[ "button", 2, false ],
|
||||
[ "setbool", "seen", true ]
|
||||
]
|
||||
},
|
||||
{
|
||||
"regexp": "Approve|Sign|Accept",
|
||||
"actions": [
|
||||
[ "button", 3, true ],
|
||||
[ "button", 3, false ]
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@@ -0,0 +1 @@
|
||||
msrv="1.63.0"
|
||||
13
codecov.yaml
13
codecov.yaml
@@ -1,13 +0,0 @@
|
||||
coverage:
|
||||
status:
|
||||
project:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 1%
|
||||
base: auto
|
||||
informational: false
|
||||
patch:
|
||||
default:
|
||||
target: auto
|
||||
threshold: 100%
|
||||
base: auto
|
||||
61
crates/bdk/Cargo.toml
Normal file
61
crates/bdk/Cargo.toml
Normal file
@@ -0,0 +1,61 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
version = "1.0.0-alpha.7"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk"
|
||||
description = "A modern, lightweight, descriptor-based wallet library"
|
||||
keywords = ["bitcoin", "wallet", "descriptor", "psbt"]
|
||||
readme = "README.md"
|
||||
license = "MIT OR Apache-2.0"
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
|
||||
[dependencies]
|
||||
rand = "^0.8"
|
||||
miniscript = { version = "10.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.30.0", features = ["serde", "base64", "rand-std"], default-features = false }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = ["miniscript", "serde"], default-features = false }
|
||||
|
||||
# Optional dependencies
|
||||
bip39 = { version = "2.0", optional = true }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
js-sys = "0.3"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "miniscript/std", "bdk_chain/std"]
|
||||
compiler = ["miniscript/compiler"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
|
||||
# This feature is used to run `cargo check` in our CI targeting wasm. It's not recommended
|
||||
# for libraries to explicitly include the "getrandom/js" feature, so we only do it when
|
||||
# necessary for running our CI. See: https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support
|
||||
dev-getrandom-wasm = ["getrandom/js"]
|
||||
|
||||
[dev-dependencies]
|
||||
lazy_static = "1.4"
|
||||
assert_matches = "1.5.0"
|
||||
tempfile = "3"
|
||||
bdk_file_store = { path = "../file_store" }
|
||||
anyhow = "1"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
all-features = true
|
||||
rustdoc-args = ["--cfg", "docsrs"]
|
||||
|
||||
[[example]]
|
||||
name = "mnemonic_to_descriptors"
|
||||
path = "examples/mnemonic_to_descriptors.rs"
|
||||
required-features = ["all-keys"]
|
||||
|
||||
[[example]]
|
||||
name = "miniscriptc"
|
||||
path = "examples/compiler.rs"
|
||||
required-features = ["compiler"]
|
||||
226
crates/bdk/README.md
Normal file
226
crates/bdk/README.md
Normal file
@@ -0,0 +1,226 @@
|
||||
<div align="center">
|
||||
<h1>BDK</h1>
|
||||
|
||||
<img src="https://raw.githubusercontent.com/bitcoindevkit/bdk/master/static/bdk.png" width="220" />
|
||||
|
||||
<p>
|
||||
<strong>A modern, lightweight, descriptor-based wallet library written in Rust!</strong>
|
||||
</p>
|
||||
|
||||
<p>
|
||||
<a href="https://crates.io/crates/bdk"><img alt="Crate Info" src="https://img.shields.io/crates/v/bdk.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/blob/master/LICENSE"><img alt="MIT or Apache-2.0 Licensed" src="https://img.shields.io/badge/license-MIT%2FApache--2.0-blue.svg"/></a>
|
||||
<a href="https://github.com/bitcoindevkit/bdk/actions?query=workflow%3ACI"><img alt="CI Status" src="https://github.com/bitcoindevkit/bdk/workflows/CI/badge.svg"></a>
|
||||
<a href="https://coveralls.io/github/bitcoindevkit/bdk?branch=master"><img src="https://coveralls.io/repos/github/bitcoindevkit/bdk/badge.svg?branch=master"/></a>
|
||||
<a href="https://docs.rs/bdk"><img alt="API Docs" src="https://img.shields.io/badge/docs.rs-bdk-green"/></a>
|
||||
<a href="https://blog.rust-lang.org/2022/08/11/Rust-1.63.0.html"><img alt="Rustc Version 1.63.0+" src="https://img.shields.io/badge/rustc-1.63.0%2B-lightgrey.svg"/></a>
|
||||
<a href="https://discord.gg/d7NkDKm"><img alt="Chat on Discord" src="https://img.shields.io/discord/753336465005608961?logo=discord"></a>
|
||||
</p>
|
||||
|
||||
<h4>
|
||||
<a href="https://bitcoindevkit.org">Project Homepage</a>
|
||||
<span> | </span>
|
||||
<a href="https://docs.rs/bdk">Documentation</a>
|
||||
</h4>
|
||||
</div>
|
||||
|
||||
## `bdk`
|
||||
|
||||
The `bdk` crate provides the [`Wallet`](`crate::Wallet`) type which is a simple, high-level
|
||||
interface built from the low-level components of [`bdk_chain`]. `Wallet` is a good starting point
|
||||
for many simple applications as well as a good demonstration of how to use the other mechanisms to
|
||||
construct a wallet. It has two keychains (external and internal) which are defined by
|
||||
[miniscript descriptors][`rust-miniscript`] and uses them to generate addresses. When you give it
|
||||
chain data it also uses the descriptors to find transaction outputs owned by them. From there, you
|
||||
can create and sign transactions.
|
||||
|
||||
For more information, see the [`Wallet`'s documentation](https://docs.rs/bdk/latest/bdk/wallet/struct.Wallet.html).
|
||||
|
||||
### Blockchain data
|
||||
|
||||
In order to get blockchain data for `Wallet` to consume, you have to put it into particular form.
|
||||
Right now this is [`KeychainScan`] which is defined in [`bdk_chain`].
|
||||
|
||||
This can be created manually or from blockchain-scanning crates.
|
||||
|
||||
**Blockchain Data Sources**
|
||||
|
||||
* [`bdk_esplora`]: Grabs blockchain data from Esplora for updating BDK structures.
|
||||
* [`bdk_electrum`]: Grabs blockchain data from Electrum for updating BDK structures.
|
||||
|
||||
**Examples**
|
||||
|
||||
* [`example-crates/wallet_esplora`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora)
|
||||
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
||||
|
||||
### Persistence
|
||||
|
||||
To persist the `Wallet` on disk, `Wallet` needs to be constructed with a
|
||||
[`Persist`](https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainPersist.html) implementation.
|
||||
|
||||
**Implementations**
|
||||
|
||||
* [`bdk_file_store`]: a simple flat-file implementation of `Persist`.
|
||||
|
||||
**Example**
|
||||
|
||||
```rust
|
||||
use bdk::{bitcoin::Network, wallet::{AddressIndex, Wallet}};
|
||||
|
||||
fn main() {
|
||||
// a type that implements `Persist`
|
||||
let db = ();
|
||||
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdy6LMhUtFHAgpocR8GC6QmwMSFpZs7h6Eziw3SpThFfczTDh5rW2krkqffa11UpX3XkeTTB2FvzZKWXqPY54Y6Rq4AQ5R8L/84'/0'/0'/0/*)";
|
||||
let mut wallet = Wallet::new(descriptor, None, db, Network::Testnet).expect("should create");
|
||||
|
||||
// get a new address (this increments revealed derivation index)
|
||||
println!("revealed address: {}", wallet.get_address(AddressIndex::New));
|
||||
println!("staged changes: {:?}", wallet.staged());
|
||||
// persist changes
|
||||
wallet.commit().expect("must save");
|
||||
}
|
||||
```
|
||||
|
||||
<!-- ### Sync the balance of a descriptor -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
<!-- use bdk::SyncOptions; -->
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
<!-- )?; -->
|
||||
|
||||
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
|
||||
|
||||
<!-- println!("Descriptor balance: {} SAT", wallet.get_balance()?); -->
|
||||
|
||||
<!-- Ok(()) -->
|
||||
<!-- } -->
|
||||
<!-- ``` -->
|
||||
<!-- ### Generate a few addresses -->
|
||||
|
||||
<!-- ```rust -->
|
||||
<!-- use bdk::Wallet; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
<!-- )?; -->
|
||||
|
||||
<!-- println!("Address #0: {}", wallet.get_address(New)); -->
|
||||
<!-- println!("Address #1: {}", wallet.get_address(New)); -->
|
||||
<!-- println!("Address #2: {}", wallet.get_address(New)); -->
|
||||
|
||||
<!-- Ok(()) -->
|
||||
<!-- } -->
|
||||
<!-- ``` -->
|
||||
|
||||
<!-- ### Create a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::{FeeRate, Wallet, SyncOptions}; -->
|
||||
<!-- use bdk::blockchain::ElectrumBlockchain; -->
|
||||
|
||||
<!-- use bdk::electrum_client::Client; -->
|
||||
<!-- use bdk::wallet::AddressIndex::New; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk::bitcoin::consensus::serialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let blockchain = ElectrumBlockchain::from(Client::new("ssl://electrum.blockstream.info:60002")?); -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tpubDDYkZojQFQjht8Tm4jsS3iuEmKjTiEGjG6KnuFNKKJb5A6ZUCUZKdvLdSDWofKi4ToRCwb9poe1XdqfUnP4jaJjCB2Zwv11ZLgSbnZSNecE/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
<!-- )?; -->
|
||||
|
||||
<!-- wallet.sync(&blockchain, SyncOptions::default())?; -->
|
||||
|
||||
<!-- let send_to = wallet.get_address(New); -->
|
||||
<!-- let (psbt, details) = { -->
|
||||
<!-- let mut builder = wallet.build_tx(); -->
|
||||
<!-- builder -->
|
||||
<!-- .add_recipient(send_to.script_pubkey(), 50_000) -->
|
||||
<!-- .enable_rbf() -->
|
||||
<!-- .do_not_spend_change() -->
|
||||
<!-- .fee_rate(FeeRate::from_sat_per_vb(5.0)); -->
|
||||
<!-- builder.finish()? -->
|
||||
<!-- }; -->
|
||||
|
||||
<!-- println!("Transaction details: {:#?}", details); -->
|
||||
<!-- println!("Unsigned PSBT: {}", base64::encode(&serialize(&psbt))); -->
|
||||
|
||||
<!-- Ok(()) -->
|
||||
<!-- } -->
|
||||
<!-- ``` -->
|
||||
|
||||
<!-- ### Sign a transaction -->
|
||||
|
||||
<!-- ```rust,no_run -->
|
||||
<!-- use bdk::{Wallet, SignOptions}; -->
|
||||
|
||||
<!-- use bitcoin::base64; -->
|
||||
<!-- use bdk::bitcoin::consensus::deserialize; -->
|
||||
<!-- use bdk::bitcoin::Network; -->
|
||||
|
||||
<!-- fn main() -> Result<(), bdk::Error> { -->
|
||||
<!-- let wallet = Wallet::new_no_persist( -->
|
||||
<!-- "wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/0/*)", -->
|
||||
<!-- Some("wpkh([c258d2e4/84h/1h/0h]tprv8griRPhA7342zfRyB6CqeKF8CJDXYu5pgnj1cjL1u2ngKcJha5jjTRimG82ABzJQ4MQe71CV54xfn25BbhCNfEGGJZnxvCDQCd6JkbvxW6h/1/*)"), -->
|
||||
<!-- Network::Testnet, -->
|
||||
<!-- )?; -->
|
||||
|
||||
<!-- let psbt = "..."; -->
|
||||
<!-- let mut psbt = deserialize(&base64::decode(psbt).unwrap())?; -->
|
||||
|
||||
<!-- let _finalized = wallet.sign(&mut psbt, SignOptions::default())?; -->
|
||||
|
||||
<!-- Ok(()) -->
|
||||
<!-- } -->
|
||||
<!-- ``` -->
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit testing
|
||||
|
||||
```bash
|
||||
cargo test
|
||||
```
|
||||
|
||||
## License
|
||||
|
||||
Licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](../../LICENSE-APACHE) or <https://www.apache.org/licenses/LICENSE-2.0>)
|
||||
* MIT license ([LICENSE-MIT](../../LICENSE-MIT) or <https://opensource.org/licenses/MIT>)
|
||||
|
||||
at your option.
|
||||
|
||||
### Contribution
|
||||
|
||||
Unless you explicitly state otherwise, any contribution intentionally
|
||||
submitted for inclusion in the work by you, as defined in the Apache-2.0
|
||||
license, shall be dual licensed as above, without any additional terms or
|
||||
conditions.
|
||||
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
[`bdk_file_store`]: https://docs.rs/bdk_file_store/latest
|
||||
[`bdk_electrum`]: https://docs.rs/bdk_electrum/latest
|
||||
[`bdk_esplora`]: https://docs.rs/bdk_esplora/latest
|
||||
[`KeychainScan`]: https://docs.rs/bdk_chain/latest/bdk_chain/keychain/struct.KeychainScan.html
|
||||
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
||||
66
crates/bdk/examples/compiler.rs
Normal file
66
crates/bdk/examples/compiler.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bitcoin;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miniscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
/// rust-miniscript provides a `compile()` function that can be used to compile any miniscript policy
|
||||
/// into a descriptor. This descriptor then in turn can be used in bdk a fully functioning wallet
|
||||
/// can be derived from the policy.
|
||||
///
|
||||
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
// We start with a generic miniscript policy string
|
||||
let policy_str = "or(10@thresh(4,pk(029ffbe722b147f3035c87cb1c60b9a5947dd49c774cc31e94773478711a929ac0),pk(025f05815e3a1a8a83bfbb03ce016c9a2ee31066b98f567f6227df1d76ec4bd143),pk(025625f41e4a065efc06d5019cbbd56fe8c07595af1231e7cbc03fafb87ebb71ec),pk(02a27c8b850a00f67da3499b60562673dcf5fdfb82b7e17652a7ac54416812aefd),pk(03e618ec5f384d6e19ca9ebdb8e2119e5bef978285076828ce054e55c4daf473e2)),1@and(older(4209713),thresh(2,pk(03deae92101c790b12653231439f27b8897264125ecb2f46f48278603102573165),pk(033841045a531e1adf9910a6ec279589a90b3b8a904ee64ffd692bd08a8996c1aa),pk(02aebf2d10b040eb936a6f02f44ee82f8b34f5c1ccb20ff3949c2b28206b7c1068))))";
|
||||
println!("Compiling policy: \n{}", policy_str);
|
||||
|
||||
// Parse the string as a [`Concrete`] type miniscript policy.
|
||||
let policy = Concrete::<String>::from_str(policy_str)?;
|
||||
|
||||
// Create a `wsh` type descriptor from the policy.
|
||||
// `policy.compile()` returns the resulting miniscript from the policy.
|
||||
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
|
||||
|
||||
println!("Compiled into following Descriptor: \n{}", descriptor);
|
||||
|
||||
// Create a new wallet from this descriptor
|
||||
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
|
||||
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.get_address(New)
|
||||
);
|
||||
|
||||
// BDK also has it's own `Policy` structure to represent the spending condition in a more
|
||||
// human readable json format.
|
||||
let spending_policy = wallet.policies(KeychainKind::External)?;
|
||||
println!(
|
||||
"The BDK spending policy: \n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
59
crates/bdk/examples/mnemonic_to_descriptors.rs
Normal file
59
crates/bdk/examples/mnemonic_to_descriptors.rs
Normal file
@@ -0,0 +1,59 @@
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use anyhow::anyhow;
|
||||
use bdk::bitcoin::bip32::DerivationPath;
|
||||
use bdk::bitcoin::secp256k1::Secp256k1;
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor;
|
||||
use bdk::descriptor::IntoWalletDescriptor;
|
||||
use bdk::keys::bip39::{Language, Mnemonic, WordCount};
|
||||
use bdk::keys::{GeneratableKey, GeneratedKey};
|
||||
use bdk::miniscript::Tap;
|
||||
use std::str::FromStr;
|
||||
|
||||
/// This example demonstrates how to generate a mnemonic phrase
|
||||
/// using BDK and use that to generate a descriptor string.
|
||||
fn main() -> Result<(), anyhow::Error> {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
// In this example we are generating a 12 words mnemonic phrase
|
||||
// but it is also possible generate 15, 18, 21 and 24 words
|
||||
// using their respective `WordCount` variant.
|
||||
let mnemonic: GeneratedKey<_, Tap> =
|
||||
Mnemonic::generate((WordCount::Words12, Language::English))
|
||||
.map_err(|_| anyhow!("Mnemonic generation error"))?;
|
||||
|
||||
println!("Mnemonic phrase: {}", *mnemonic);
|
||||
let mnemonic_with_passphrase = (mnemonic, None);
|
||||
|
||||
// define external and internal derivation key path
|
||||
let external_path = DerivationPath::from_str("m/86h/1h/0h/0").unwrap();
|
||||
let internal_path = DerivationPath::from_str("m/86h/1h/0h/1").unwrap();
|
||||
|
||||
// generate external and internal descriptor from mnemonic
|
||||
let (external_descriptor, ext_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase.clone(), external_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
let (internal_descriptor, int_keymap) =
|
||||
descriptor!(tr((mnemonic_with_passphrase, internal_path)))?
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
println!("tpub external descriptor: {}", external_descriptor);
|
||||
println!("tpub internal descriptor: {}", internal_descriptor);
|
||||
println!(
|
||||
"tprv external descriptor: {}",
|
||||
external_descriptor.to_string_with_secret(&ext_keymap)
|
||||
);
|
||||
println!(
|
||||
"tprv internal descriptor: {}",
|
||||
internal_descriptor.to_string_with_secret(&int_keymap)
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
60
crates/bdk/examples/policy.rs
Normal file
60
crates/bdk/examples/policy.rs
Normal file
@@ -0,0 +1,60 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
use std::error::Error;
|
||||
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::descriptor::{policy::BuildSatisfaction, ExtractPolicy, IntoWalletDescriptor};
|
||||
use bdk::wallet::signer::SignersContainer;
|
||||
|
||||
/// This example describes the use of the BDK's [`bdk::descriptor::policy`] module.
|
||||
///
|
||||
/// Policy is higher abstraction representation of the wallet descriptor spending condition.
|
||||
/// This is useful to express complex miniscript spending conditions into more human readable form.
|
||||
/// The resulting `Policy` structure can be used to derive spending conditions the wallet is capable
|
||||
/// to spend from.
|
||||
///
|
||||
/// This example demos a Policy output for a 2of2 multisig between between 2 parties, where the wallet holds
|
||||
/// one of the Extend Private key.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
let secp = bitcoin::secp256k1::Secp256k1::new();
|
||||
|
||||
// The descriptor used in the example
|
||||
// The form is "wsh(multi(2, <privkey>, <pubkey>))"
|
||||
let desc = "wsh(multi(2,tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
|
||||
// Use the descriptor string to derive the full descriptor and a keymap.
|
||||
// The wallet descriptor can be used to create a new bdk::wallet.
|
||||
// While the `keymap` can be used to create a `SignerContainer`.
|
||||
//
|
||||
// The `SignerContainer` can sign for `PSBT`s.
|
||||
// a bdk::wallet internally uses these to handle transaction signing.
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
println!("Example Descriptor for policy analysis : {}", wallet_desc);
|
||||
|
||||
// Create the signer with the keymap and descriptor.
|
||||
let signers_container = SignersContainer::build(keymap, &wallet_desc, &secp);
|
||||
|
||||
// Extract the Policy from the given descriptor and signer.
|
||||
// Note that Policy is a wallet specific structure. It depends on the the descriptor, and
|
||||
// what the concerned wallet with a given signer can sign for.
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
|
||||
.expect("We expect a policy");
|
||||
|
||||
println!("Derived Policy for the descriptor {:#?}", policy);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
147
crates/bdk/src/descriptor/checksum.rs
Normal file
147
crates/bdk/src/descriptor/checksum.rs
Normal file
@@ -0,0 +1,147 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Descriptor checksum
|
||||
//!
|
||||
//! This module contains a re-implementation of the function used by Bitcoin Core to calculate the
|
||||
//! checksum of a descriptor
|
||||
|
||||
use crate::descriptor::DescriptorError;
|
||||
use alloc::string::String;
|
||||
|
||||
const INPUT_CHARSET: &[u8] = b"0123456789()[],'/*abcdefgh@:$%{}IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";
|
||||
const CHECKSUM_CHARSET: &[u8] = b"qpzry9x8gf2tvdw0s3jn54khce6mua7l";
|
||||
|
||||
fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
let c0 = c >> 35;
|
||||
c = ((c & 0x7ffffffff) << 5) ^ val;
|
||||
if c0 & 1 > 0 {
|
||||
c ^= 0xf5dee51989
|
||||
};
|
||||
if c0 & 2 > 0 {
|
||||
c ^= 0xa9fdca3312
|
||||
};
|
||||
if c0 & 4 > 0 {
|
||||
c ^= 0x1bab10e32d
|
||||
};
|
||||
if c0 & 8 > 0 {
|
||||
c ^= 0x3706b1677a
|
||||
};
|
||||
if c0 & 16 > 0 {
|
||||
c ^= 0x644d626ffd
|
||||
};
|
||||
|
||||
c
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(mut desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
let mut c = 1;
|
||||
let mut cls = 0;
|
||||
let mut clscount = 0;
|
||||
|
||||
let mut original_checksum = None;
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
let pos = INPUT_CHARSET
|
||||
.iter()
|
||||
.position(|b| b == ch)
|
||||
.ok_or(DescriptorError::InvalidDescriptorCharacter(*ch))? as u64;
|
||||
c = poly_mod(c, pos & 31);
|
||||
cls = cls * 3 + (pos >> 5);
|
||||
clscount += 1;
|
||||
if clscount == 3 {
|
||||
c = poly_mod(c, cls);
|
||||
cls = 0;
|
||||
clscount = 0;
|
||||
}
|
||||
}
|
||||
if clscount > 0 {
|
||||
c = poly_mod(c, cls);
|
||||
}
|
||||
(0..8).for_each(|_| c = poly_mod(c, 0));
|
||||
c ^= 1;
|
||||
|
||||
let mut checksum = [0_u8; 8];
|
||||
for j in 0..8 {
|
||||
checksum[j] = CHECKSUM_CHARSET[((c >> (5 * (7 - j))) & 31) as usize];
|
||||
}
|
||||
|
||||
// if input data already had a checksum, check calculated checksum against original checksum
|
||||
if let Some(original_checksum) = original_checksum {
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes(desc).map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::descriptor::calc_checksum;
|
||||
use assert_matches::assert_matches;
|
||||
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core
|
||||
#[test]
|
||||
fn test_calc_checksum() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
}
|
||||
|
||||
// test calc_checksum() function; it should return the same value as Bitcoin Core even if the
|
||||
// descriptor string includes a checksum hash
|
||||
#[test]
|
||||
fn test_calc_checksum_with_checksum_hash() {
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc62";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "tqz0nc62");
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmfs";
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc_checksum_invalid_character() {
|
||||
let sparkle_heart = unsafe { core::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||
let invalid_desc = format!("wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcL{}fjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)", sparkle_heart);
|
||||
|
||||
assert_matches!(
|
||||
calc_checksum(&invalid_desc),
|
||||
Err(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -23,7 +23,7 @@ macro_rules! impl_top_level_sh {
|
||||
};
|
||||
|
||||
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti $( $inner:tt )* ) => {{
|
||||
use std::marker::PhantomData;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
|
||||
use $crate::miniscript::$ctx;
|
||||
@@ -35,7 +35,7 @@ macro_rules! impl_top_level_sh {
|
||||
$crate::impl_sortedmulti!(build_desc, sortedmulti $( $inner )*)
|
||||
}};
|
||||
( $inner_struct:ident, $constructor:ident, $sortedmulti_constructor:ident, $ctx:ident, sortedmulti_vec $( $inner:tt )* ) => {{
|
||||
use std::marker::PhantomData;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use $crate::miniscript::descriptor::{$inner_struct, Descriptor, DescriptorPublicKey};
|
||||
use $crate::miniscript::$ctx;
|
||||
@@ -203,8 +203,8 @@ macro_rules! impl_node_opcode_two {
|
||||
a_keymap.extend(b_keymap.into_iter());
|
||||
|
||||
let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
|
||||
std::sync::Arc::new(a_minisc),
|
||||
std::sync::Arc::new(b_minisc),
|
||||
$crate::alloc::sync::Arc::new(a_minisc),
|
||||
$crate::alloc::sync::Arc::new(b_minisc),
|
||||
))?;
|
||||
|
||||
minisc.check_miniscript()?;
|
||||
@@ -234,9 +234,9 @@ macro_rules! impl_node_opcode_three {
|
||||
let networks = $crate::keys::merge_networks(&networks, &c_networks);
|
||||
|
||||
let minisc = $crate::miniscript::Miniscript::from_ast($crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
|
||||
std::sync::Arc::new(a_minisc),
|
||||
std::sync::Arc::new(b_minisc),
|
||||
std::sync::Arc::new(c_minisc),
|
||||
$crate::alloc::sync::Arc::new(a_minisc),
|
||||
$crate::alloc::sync::Arc::new(b_minisc),
|
||||
$crate::alloc::sync::Arc::new(c_minisc),
|
||||
))?;
|
||||
|
||||
minisc.check_miniscript()?;
|
||||
@@ -263,7 +263,7 @@ macro_rules! impl_sortedmulti {
|
||||
)*
|
||||
];
|
||||
|
||||
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
||||
keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
|
||||
.map_err($crate::descriptor::DescriptorError::Key)
|
||||
.and_then(|keys| $crate::keys::make_sortedmulti($thresh, keys, $build_desc, &secp))
|
||||
});
|
||||
@@ -274,7 +274,7 @@ macro_rules! impl_sortedmulti {
|
||||
#[macro_export]
|
||||
macro_rules! parse_tap_tree {
|
||||
( @merge $tree_a:expr, $tree_b:expr) => {{
|
||||
use std::sync::Arc;
|
||||
use $crate::alloc::sync::Arc;
|
||||
use $crate::miniscript::descriptor::TapTree;
|
||||
|
||||
$tree_a
|
||||
@@ -318,7 +318,7 @@ macro_rules! parse_tap_tree {
|
||||
|
||||
// Single leaf
|
||||
( $op:ident ( $( $minisc:tt )* ) ) => {{
|
||||
use std::sync::Arc;
|
||||
use $crate::alloc::sync::Arc;
|
||||
use $crate::miniscript::descriptor::TapTree;
|
||||
|
||||
$crate::fragment!( $op ( $( $minisc )* ) )
|
||||
@@ -337,7 +337,7 @@ macro_rules! apply_modifier {
|
||||
.and_then(|(minisc, keymap, networks)| {
|
||||
let minisc = $crate::miniscript::Miniscript::from_ast(
|
||||
$crate::miniscript::miniscript::decode::Terminal::$terminal_variant(
|
||||
std::sync::Arc::new(minisc),
|
||||
$crate::alloc::sync::Arc::new(minisc),
|
||||
),
|
||||
)?;
|
||||
|
||||
@@ -374,8 +374,8 @@ macro_rules! apply_modifier {
|
||||
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
|
||||
$crate::impl_leaf_opcode_value_two!(
|
||||
AndV,
|
||||
std::sync::Arc::new(a_minisc),
|
||||
std::sync::Arc::new($crate::fragment!(true).unwrap().0)
|
||||
$crate::alloc::sync::Arc::new(a_minisc),
|
||||
$crate::alloc::sync::Arc::new($crate::fragment!(true).unwrap().0)
|
||||
)
|
||||
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
|
||||
})
|
||||
@@ -384,8 +384,8 @@ macro_rules! apply_modifier {
|
||||
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
|
||||
$crate::impl_leaf_opcode_value_two!(
|
||||
OrI,
|
||||
std::sync::Arc::new($crate::fragment!(false).unwrap().0),
|
||||
std::sync::Arc::new(a_minisc)
|
||||
$crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0),
|
||||
$crate::alloc::sync::Arc::new(a_minisc)
|
||||
)
|
||||
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
|
||||
})
|
||||
@@ -394,8 +394,8 @@ macro_rules! apply_modifier {
|
||||
$inner.and_then(|(a_minisc, a_keymap, a_networks)| {
|
||||
$crate::impl_leaf_opcode_value_two!(
|
||||
OrI,
|
||||
std::sync::Arc::new(a_minisc),
|
||||
std::sync::Arc::new($crate::fragment!(false).unwrap().0)
|
||||
$crate::alloc::sync::Arc::new(a_minisc),
|
||||
$crate::alloc::sync::Arc::new($crate::fragment!(false).unwrap().0)
|
||||
)
|
||||
.map(|(minisc, _, _)| (minisc, a_keymap, a_networks))
|
||||
})
|
||||
@@ -495,6 +495,8 @@ macro_rules! apply_modifier {
|
||||
/// let (descriptor, key_map, networks) = bdk::descriptor!(wpkh(my_key))?;
|
||||
/// # Ok::<(), Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`Vec`]: alloc::vec::Vec
|
||||
#[macro_export]
|
||||
macro_rules! descriptor {
|
||||
( bare ( $( $minisc:tt )* ) ) => ({
|
||||
@@ -514,13 +516,14 @@ macro_rules! descriptor {
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
$crate::impl_top_level_pk!(Pkh, $crate::miniscript::Legacy, $key)
|
||||
.and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
|
||||
.map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Pkh(a), b, c))
|
||||
});
|
||||
( wpkh ( $key:expr ) ) => ({
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
$crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
|
||||
.and_then(|(a, b, c)| Ok((a?, b, c)))
|
||||
.and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
|
||||
.map(|(a, b, c)| (Descriptor::<DescriptorPublicKey>::Wpkh(a), b, c))
|
||||
});
|
||||
( sh ( wpkh ( $key:expr ) ) ) => ({
|
||||
@@ -530,7 +533,7 @@ macro_rules! descriptor {
|
||||
use $crate::miniscript::descriptor::{Descriptor, DescriptorPublicKey, Sh};
|
||||
|
||||
$crate::impl_top_level_pk!(Wpkh, $crate::miniscript::Segwitv0, $key)
|
||||
.and_then(|(a, b, c)| Ok((a?, b, c)))
|
||||
.and_then(|(a, b, c)| Ok((a.map_err(|e| miniscript::Error::from(e))?, b, c)))
|
||||
.and_then(|(a, b, c)| Ok((Descriptor::<DescriptorPublicKey>::Sh(Sh::new_wpkh(a.into_inner())?), b, c)))
|
||||
});
|
||||
( sh ( $( $minisc:tt )* ) ) => ({
|
||||
@@ -599,7 +602,7 @@ macro_rules! group_multi_keys {
|
||||
)*
|
||||
];
|
||||
|
||||
keys.into_iter().collect::<Result<Vec<_>, _>>()
|
||||
keys.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
|
||||
.map_err($crate::descriptor::DescriptorError::Key)
|
||||
}};
|
||||
}
|
||||
@@ -700,10 +703,10 @@ macro_rules! fragment {
|
||||
$crate::keys::make_pkh($key, &secp)
|
||||
});
|
||||
( after ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(After, $value)
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
|
||||
});
|
||||
( older ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Older, $value)
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||
});
|
||||
( sha256 ( $hash:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Sha256, $hash)
|
||||
@@ -744,8 +747,8 @@ macro_rules! fragment {
|
||||
( thresh_vec ( $thresh:expr, $items:expr ) ) => ({
|
||||
use $crate::miniscript::descriptor::KeyMap;
|
||||
|
||||
let (items, key_maps_networks): (Vec<_>, Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
|
||||
let items = items.into_iter().map(std::sync::Arc::new).collect();
|
||||
let (items, key_maps_networks): ($crate::alloc::vec::Vec<_>, $crate::alloc::vec::Vec<_>) = $items.into_iter().map(|(a, b, c)| (a, (b, c))).unzip();
|
||||
let items = items.into_iter().map($crate::alloc::sync::Arc::new).collect();
|
||||
|
||||
let (key_maps, valid_networks) = key_maps_networks.into_iter().fold((KeyMap::default(), $crate::keys::any_network()), |(mut keys_acc, net_acc), (key, net)| {
|
||||
keys_acc.extend(key.into_iter());
|
||||
@@ -760,7 +763,7 @@ macro_rules! fragment {
|
||||
( thresh ( $thresh:expr, $( $inner:tt )* ) ) => ({
|
||||
let items = $crate::fragment_internal!( @v $( $inner )* );
|
||||
|
||||
items.into_iter().collect::<Result<Vec<_>, _>>()
|
||||
items.into_iter().collect::<Result<$crate::alloc::vec::Vec<_>, _>>()
|
||||
.and_then(|items| $crate::fragment!(thresh_vec($thresh, items)))
|
||||
});
|
||||
( multi_vec ( $thresh:expr, $keys:expr ) ) => ({
|
||||
@@ -793,21 +796,19 @@ macro_rules! fragment {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use bitcoin::hashes::hex::ToHex;
|
||||
use alloc::string::ToString;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::{Descriptor, Legacy, Segwitv0};
|
||||
|
||||
use std::str::FromStr;
|
||||
use core::str::FromStr;
|
||||
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey, ValidNetworks};
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::PrivateKey;
|
||||
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
|
||||
// test the descriptor!() macro
|
||||
|
||||
// verify descriptor generates expected script(s) (if bare or pk) or address(es)
|
||||
@@ -817,24 +818,19 @@ mod test {
|
||||
is_fixed: bool,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||
for i in 0..expected.len() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
};
|
||||
let child_desc = desc
|
||||
.at_derivation_index(i as u32)
|
||||
.expect("i is not hardened");
|
||||
let address = child_desc.address(Regtest);
|
||||
if let Ok(address) = address {
|
||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||
} else {
|
||||
let script = child_desc.script_pubkey();
|
||||
assert_eq!(script.to_hex().as_str(), *expected.get(i).unwrap());
|
||||
assert_eq!(script.to_hex_string(), *expected.get(i).unwrap());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1179,9 +1175,7 @@ mod test {
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(
|
||||
expected = "Miniscript(ContextError(CompressedOnly(\"04b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a87378ec38ff91d43e8c2092ebda601780485263da089465619e0358a5c1be7ac91f4\")))"
|
||||
)]
|
||||
#[should_panic(expected = "Miniscript(ContextError(UncompressedKeysNotAllowed))")]
|
||||
fn test_dsl_miniscript_checks() {
|
||||
let mut uncompressed_pk =
|
||||
PrivateKey::from_wif("L5EZftvrYaSudiozVRzTqLcHLNDoVn7H5HSfM9BAN6tMJX8oTWz6").unwrap();
|
||||
123
crates/bdk/src/descriptor/error.rs
Normal file
123
crates/bdk/src/descriptor/error.rs
Normal file
@@ -0,0 +1,123 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Descriptor errors
|
||||
use core::fmt;
|
||||
|
||||
/// Errors related to the parsing and usage of descriptors
|
||||
#[derive(Debug)]
|
||||
pub enum Error {
|
||||
/// Invalid HD Key path, such as having a wildcard but a length != 1
|
||||
InvalidHdKeyPath,
|
||||
/// The provided descriptor doesn't match its checksum
|
||||
InvalidDescriptorChecksum,
|
||||
/// The descriptor contains hardened derivation steps on public extended keys
|
||||
HardenedDerivationXpub,
|
||||
/// The descriptor contains multipath keys
|
||||
MultiPath,
|
||||
|
||||
/// Error thrown while working with [`keys`](crate::keys)
|
||||
Key(crate::keys::KeyError),
|
||||
/// Error while extracting and manipulating policies
|
||||
Policy(crate::descriptor::policy::PolicyError),
|
||||
|
||||
/// Invalid byte found in the descriptor checksum
|
||||
InvalidDescriptorCharacter(u8),
|
||||
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::bip32::Error),
|
||||
/// Error during base58 decoding
|
||||
Base58(bitcoin::base58::Error),
|
||||
/// Key-related error
|
||||
Pk(bitcoin::key::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
/// Hex decoding error
|
||||
Hex(bitcoin::hashes::hex::Error),
|
||||
}
|
||||
|
||||
impl From<crate::keys::KeyError> for Error {
|
||||
fn from(key_error: crate::keys::KeyError) -> Error {
|
||||
match key_error {
|
||||
crate::keys::KeyError::Miniscript(inner) => Error::Miniscript(inner),
|
||||
crate::keys::KeyError::Bip32(inner) => Error::Bip32(inner),
|
||||
e => Error::Key(e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidHdKeyPath => write!(f, "Invalid HD key path"),
|
||||
Self::InvalidDescriptorChecksum => {
|
||||
write!(f, "The provided descriptor doesn't match its checksum")
|
||||
}
|
||||
Self::HardenedDerivationXpub => write!(
|
||||
f,
|
||||
"The descriptor contains hardened derivation steps on public extended keys"
|
||||
),
|
||||
Self::MultiPath => write!(
|
||||
f,
|
||||
"The descriptor contains multipath keys, which are not supported yet"
|
||||
),
|
||||
Self::Key(err) => write!(f, "Key error: {}", err),
|
||||
Self::Policy(err) => write!(f, "Policy error: {}", err),
|
||||
Self::InvalidDescriptorCharacter(char) => {
|
||||
write!(f, "Invalid descriptor character: {}", char)
|
||||
}
|
||||
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
|
||||
Self::Base58(err) => write!(f, "Base58 error: {}", err),
|
||||
Self::Pk(err) => write!(f, "Key-related error: {}", err),
|
||||
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
|
||||
Self::Hex(err) => write!(f, "Hex decoding error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for Error {}
|
||||
|
||||
impl From<bitcoin::bip32::Error> for Error {
|
||||
fn from(err: bitcoin::bip32::Error) -> Self {
|
||||
Error::Bip32(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::base58::Error> for Error {
|
||||
fn from(err: bitcoin::base58::Error) -> Self {
|
||||
Error::Base58(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::key::Error> for Error {
|
||||
fn from(err: bitcoin::key::Error) -> Self {
|
||||
Error::Pk(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<miniscript::Error> for Error {
|
||||
fn from(err: miniscript::Error) -> Self {
|
||||
Error::Miniscript(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bitcoin::hashes::hex::Error> for Error {
|
||||
fn from(err: bitcoin::hashes::hex::Error) -> Self {
|
||||
Error::Hex(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::descriptor::policy::PolicyError> for Error {
|
||||
fn from(err: crate::descriptor::policy::PolicyError) -> Self {
|
||||
Error::Policy(err)
|
||||
}
|
||||
}
|
||||
@@ -14,33 +14,35 @@
|
||||
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
||||
//! from [`miniscript`].
|
||||
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::ops::Deref;
|
||||
use crate::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||
use bitcoin::util::{psbt, taproot};
|
||||
use bitcoin::{secp256k1, PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::{Network, Script, TxOut};
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPubKey, Fingerprint, KeySource};
|
||||
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
|
||||
use bitcoin::{psbt, taproot};
|
||||
use bitcoin::{Network, TxOut};
|
||||
|
||||
use miniscript::descriptor::{DescriptorType, InnerXKey, SinglePubKey};
|
||||
pub use miniscript::{
|
||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
use miniscript::descriptor::{
|
||||
DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType,
|
||||
DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard,
|
||||
};
|
||||
use miniscript::{DescriptorTrait, ForEachKey, TranslatePk};
|
||||
pub use miniscript::{
|
||||
Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
};
|
||||
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
|
||||
|
||||
use crate::descriptor::policy::BuildSatisfaction;
|
||||
|
||||
pub mod checksum;
|
||||
pub mod derived;
|
||||
#[doc(hidden)]
|
||||
pub mod dsl;
|
||||
pub mod error;
|
||||
pub mod policy;
|
||||
pub mod template;
|
||||
|
||||
pub use self::checksum::get_checksum;
|
||||
pub use self::derived::{AsDerived, DerivedDescriptorKey};
|
||||
pub use self::checksum::calc_checksum;
|
||||
use self::checksum::calc_checksum_bytes;
|
||||
pub use self::error::Error as DescriptorError;
|
||||
pub use self::policy::Policy;
|
||||
use self::template::DescriptorTemplateOut;
|
||||
@@ -52,21 +54,21 @@ use crate::wallet::utils::SecpCtx;
|
||||
pub type ExtendedDescriptor = Descriptor<DescriptorPublicKey>;
|
||||
|
||||
/// Alias for a [`Descriptor`] that contains extended **derived** keys
|
||||
pub type DerivedDescriptor<'s> = Descriptor<DerivedDescriptorKey<'s>>;
|
||||
pub type DerivedDescriptor = Descriptor<DefiniteDescriptorKey>;
|
||||
|
||||
/// Alias for the type of maps that represent derivation paths in a [`psbt::Input`] or
|
||||
/// [`psbt::Output`]
|
||||
///
|
||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||
/// [`psbt::Input`]: bitcoin::psbt::Input
|
||||
/// [`psbt::Output`]: bitcoin::psbt::Output
|
||||
pub type HdKeyPaths = BTreeMap<secp256k1::PublicKey, KeySource>;
|
||||
|
||||
/// Alias for the type of maps that represent taproot key origins in a [`psbt::Input`] or
|
||||
/// [`psbt::Output`]
|
||||
///
|
||||
/// [`psbt::Input`]: bitcoin::util::psbt::Input
|
||||
/// [`psbt::Output`]: bitcoin::util::psbt::Output
|
||||
pub type TapKeyOrigins = BTreeMap<bitcoin::XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
|
||||
/// [`psbt::Input`]: bitcoin::psbt::Input
|
||||
/// [`psbt::Output`]: bitcoin::psbt::Output
|
||||
pub type TapKeyOrigins = BTreeMap<XOnlyPublicKey, (Vec<taproot::TapLeafHash>, KeySource)>;
|
||||
|
||||
/// Trait for types which can be converted into an [`ExtendedDescriptor`] and a [`KeyMap`] usable by a wallet in a specific [`Network`]
|
||||
pub trait IntoWalletDescriptor {
|
||||
@@ -84,19 +86,15 @@ impl IntoWalletDescriptor for &str {
|
||||
secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let descriptor = if self.contains('#') {
|
||||
let parts: Vec<&str> = self.splitn(2, '#').collect();
|
||||
if !get_checksum(parts[0])
|
||||
.ok()
|
||||
.map(|computed| computed == parts[1])
|
||||
.unwrap_or(false)
|
||||
{
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
let descriptor = match self.split_once('#') {
|
||||
Some((desc, original_checksum)) => {
|
||||
let checksum = calc_checksum_bytes(desc)?;
|
||||
if original_checksum.as_bytes() != checksum {
|
||||
return Err(DescriptorError::InvalidDescriptorChecksum);
|
||||
}
|
||||
desc
|
||||
}
|
||||
|
||||
parts[0]
|
||||
} else {
|
||||
self
|
||||
None => self,
|
||||
};
|
||||
|
||||
ExtendedDescriptor::parse_descriptor(secp, descriptor)?
|
||||
@@ -132,28 +130,77 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
use crate::keys::DescriptorKey;
|
||||
|
||||
let check_key = |pk: &DescriptorPublicKey| {
|
||||
let (pk, _, networks) = if self.0.is_witness() {
|
||||
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
} else {
|
||||
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
};
|
||||
struct Translator<'s, 'd> {
|
||||
secp: &'s SecpCtx,
|
||||
descriptor: &'d ExtendedDescriptor,
|
||||
network: Network,
|
||||
}
|
||||
|
||||
if networks.contains(&network) {
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
impl<'s, 'd> miniscript::Translator<DescriptorPublicKey, String, DescriptorError>
|
||||
for Translator<'s, 'd>
|
||||
{
|
||||
fn pk(&mut self, pk: &DescriptorPublicKey) -> Result<String, DescriptorError> {
|
||||
let secp = &self.secp;
|
||||
|
||||
let (_, _, networks) = if self.descriptor.is_taproot() {
|
||||
let descriptor_key: DescriptorKey<miniscript::Tap> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
} else if self.descriptor.is_witness() {
|
||||
let descriptor_key: DescriptorKey<miniscript::Segwitv0> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
} else {
|
||||
let descriptor_key: DescriptorKey<miniscript::Legacy> =
|
||||
pk.clone().into_descriptor_key()?;
|
||||
descriptor_key.extract(secp)?
|
||||
};
|
||||
|
||||
if networks.contains(&self.network) {
|
||||
Ok(Default::default())
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
}
|
||||
};
|
||||
fn sha256(
|
||||
&mut self,
|
||||
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash256(
|
||||
&mut self,
|
||||
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn ripemd160(
|
||||
&mut self,
|
||||
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash160(
|
||||
&mut self,
|
||||
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
// check the network for the keys
|
||||
let translated = self.0.translate_pk(check_key, check_key)?;
|
||||
use miniscript::TranslateErr;
|
||||
match self.0.translate_pk(&mut Translator {
|
||||
secp,
|
||||
network,
|
||||
descriptor: &self.0,
|
||||
}) {
|
||||
Ok(_) => {}
|
||||
Err(TranslateErr::TranslatorErr(e)) => return Err(e),
|
||||
Err(TranslateErr::OuterError(e)) => return Err(e.into()),
|
||||
}
|
||||
|
||||
Ok((translated, self.1))
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,10 +210,17 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
_secp: &SecpCtx,
|
||||
network: Network,
|
||||
) -> Result<(ExtendedDescriptor, KeyMap), DescriptorError> {
|
||||
let valid_networks = &self.2;
|
||||
struct Translator {
|
||||
network: Network,
|
||||
}
|
||||
|
||||
let fix_key = |pk: &DescriptorPublicKey| {
|
||||
if valid_networks.contains(&network) {
|
||||
impl miniscript::Translator<DescriptorPublicKey, DescriptorPublicKey, DescriptorError>
|
||||
for Translator
|
||||
{
|
||||
fn pk(
|
||||
&mut self,
|
||||
pk: &DescriptorPublicKey,
|
||||
) -> Result<DescriptorPublicKey, DescriptorError> {
|
||||
// workaround for xpubs generated by other key types, like bip39: since when the
|
||||
// conversion is made one network has to be chosen, what we generally choose
|
||||
// "mainnet", but then override the set of valid networks to specify that all of
|
||||
@@ -175,7 +229,7 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
let pk = match pk {
|
||||
DescriptorPublicKey::XPub(ref xpub) => {
|
||||
let mut xpub = xpub.clone();
|
||||
xpub.xkey.network = network;
|
||||
xpub.xkey.network = self.network;
|
||||
|
||||
DescriptorPublicKey::XPub(xpub)
|
||||
}
|
||||
@@ -183,15 +237,47 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
};
|
||||
|
||||
Ok(pk)
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
miniscript::translate_hash_clone!(
|
||||
DescriptorPublicKey,
|
||||
DescriptorPublicKey,
|
||||
DescriptorError
|
||||
);
|
||||
}
|
||||
|
||||
let (desc, keymap, networks) = self;
|
||||
|
||||
if !networks.contains(&network) {
|
||||
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it in the descriptor
|
||||
use miniscript::TranslateErr;
|
||||
let translated = match desc.translate_pk(&mut Translator { network }) {
|
||||
Ok(descriptor) => descriptor,
|
||||
Err(TranslateErr::TranslatorErr(e)) => return Err(e),
|
||||
Err(TranslateErr::OuterError(e)) => return Err(e.into()),
|
||||
};
|
||||
// ...and in the key map
|
||||
let fixed_keymap = keymap
|
||||
.into_iter()
|
||||
.map(|(mut k, mut v)| {
|
||||
match (&mut k, &mut v) {
|
||||
(DescriptorPublicKey::XPub(xpub), DescriptorSecretKey::XPrv(xprv)) => {
|
||||
xpub.xkey.network = network;
|
||||
xprv.xkey.network = network;
|
||||
}
|
||||
(_, DescriptorSecretKey::Single(key)) => {
|
||||
key.key.network = network;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it
|
||||
let translated = self.0.translate_pk(fix_key, fix_key)?;
|
||||
(k, v)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((translated, self.1))
|
||||
Ok((translated, fixed_keymap))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,7 +296,7 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
derivation_path,
|
||||
wildcard,
|
||||
..
|
||||
}) = k.as_key()
|
||||
}) = k
|
||||
{
|
||||
return *wildcard == Wildcard::Hardened
|
||||
|| derivation_path.into_iter().any(ChildNumber::is_hardened);
|
||||
@@ -222,24 +308,14 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
return Err(DescriptorError::HardenedDerivationXpub);
|
||||
}
|
||||
|
||||
// Ensure that there are no duplicated keys
|
||||
let mut found_keys = HashSet::new();
|
||||
let descriptor_contains_duplicated_keys = descriptor.for_any_key(|k| {
|
||||
if let DescriptorPublicKey::XPub(xkey) = k.as_key() {
|
||||
let fingerprint = xkey.root_fingerprint(secp);
|
||||
if found_keys.contains(&fingerprint) {
|
||||
return true;
|
||||
}
|
||||
|
||||
found_keys.insert(fingerprint);
|
||||
}
|
||||
|
||||
false
|
||||
});
|
||||
if descriptor_contains_duplicated_keys {
|
||||
return Err(DescriptorError::DuplicatedKeys);
|
||||
if descriptor.is_multipath() {
|
||||
return Err(DescriptorError::MultiPath);
|
||||
}
|
||||
|
||||
// Run miniscript's sanity check, which will look for duplicated keys and other potential
|
||||
// issues
|
||||
descriptor.sanity_check()?;
|
||||
|
||||
Ok((descriptor, keymap))
|
||||
}
|
||||
|
||||
@@ -271,35 +347,13 @@ pub trait ExtractPolicy {
|
||||
}
|
||||
|
||||
pub(crate) trait XKeyUtils {
|
||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath;
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||
}
|
||||
|
||||
impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
impl<T> XKeyUtils for DescriptorMultiXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
{
|
||||
fn full_path(&self, append: &[ChildNumber]) -> DerivationPath {
|
||||
let full_path = match self.origin {
|
||||
Some((_, ref path)) => path
|
||||
.into_iter()
|
||||
.chain(self.derivation_path.into_iter())
|
||||
.cloned()
|
||||
.collect(),
|
||||
None => self.derivation_path.clone(),
|
||||
};
|
||||
|
||||
if self.wildcard != Wildcard::None {
|
||||
full_path
|
||||
.into_iter()
|
||||
.chain(append.iter())
|
||||
.cloned()
|
||||
.collect()
|
||||
} else {
|
||||
full_path
|
||||
}
|
||||
}
|
||||
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
@@ -308,76 +362,43 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DerivedDescriptorMeta {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths;
|
||||
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins;
|
||||
impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
{
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
None => self.xkey.xkey_fingerprint(secp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) trait DescriptorMeta {
|
||||
fn is_witness(&self) -> bool;
|
||||
fn is_taproot(&self) -> bool;
|
||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError>;
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>>;
|
||||
fn derive_from_hd_keypaths(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
fn derive_from_tap_key_origins<'s>(
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_tap_key_origins(
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_key_origins(
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
fn derive_from_psbt_input<'s>(
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_input(
|
||||
&self,
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>>;
|
||||
}
|
||||
|
||||
pub(crate) trait DescriptorScripts {
|
||||
fn psbt_redeem_script(&self) -> Option<Script>;
|
||||
fn psbt_witness_script(&self) -> Option<Script>;
|
||||
}
|
||||
|
||||
impl<'s> DescriptorScripts for DerivedDescriptor<'s> {
|
||||
fn psbt_redeem_script(&self) -> Option<Script> {
|
||||
match self.desc_type() {
|
||||
DescriptorType::ShWpkh => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::ShWsh => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
||||
DescriptorType::Sh => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::Bare => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::ShSortedMulti => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::ShWshSortedMulti => Some(self.explicit_script().unwrap().to_v0_p2wsh()),
|
||||
DescriptorType::Pkh
|
||||
| DescriptorType::Wpkh
|
||||
| DescriptorType::Tr
|
||||
| DescriptorType::Wsh
|
||||
| DescriptorType::WshSortedMulti => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn psbt_witness_script(&self) -> Option<Script> {
|
||||
match self.desc_type() {
|
||||
DescriptorType::Wsh => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::ShWsh => Some(self.explicit_script().unwrap()),
|
||||
DescriptorType::WshSortedMulti | DescriptorType::ShWshSortedMulti => {
|
||||
Some(self.explicit_script().unwrap())
|
||||
}
|
||||
DescriptorType::Bare
|
||||
| DescriptorType::Sh
|
||||
| DescriptorType::Pkh
|
||||
| DescriptorType::Wpkh
|
||||
| DescriptorType::ShSortedMulti
|
||||
| DescriptorType::Tr
|
||||
| DescriptorType::ShWpkh => None,
|
||||
}
|
||||
}
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
}
|
||||
|
||||
impl DescriptorMeta for ExtendedDescriptor {
|
||||
@@ -397,25 +418,25 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
self.desc_type() == DescriptorType::Tr
|
||||
}
|
||||
|
||||
fn get_extended_keys(&self) -> Result<Vec<DescriptorXKey<ExtendedPubKey>>, DescriptorError> {
|
||||
fn get_extended_keys(&self) -> Vec<DescriptorXKey<ExtendedPubKey>> {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
self.for_each_key(|pk| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk.as_key() {
|
||||
if let DescriptorPublicKey::XPub(xpub) = pk {
|
||||
answer.push(xpub.clone());
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
Ok(answer)
|
||||
answer
|
||||
}
|
||||
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
fn derive_from_psbt_key_origins(
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||
path: &DerivationPath,
|
||||
@@ -437,7 +458,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
|
||||
// using `for_any_key` should make this stop as soon as we return `true`
|
||||
self.for_any_key(|key| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key {
|
||||
// Check if the key matches one entry in our `key_origins`. If it does, `matches()` will
|
||||
// return the "prefix" that matched, so we remove that prefix from the full path
|
||||
// found in `key_origins` and save it in `derive_path`. We expect this to be a derivation
|
||||
@@ -467,11 +488,6 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
) {
|
||||
Some(derive_path)
|
||||
} else {
|
||||
log::debug!(
|
||||
"Key `{}` derived with {} yields an unexpected key",
|
||||
root_fingerprint,
|
||||
derive_path
|
||||
);
|
||||
None
|
||||
}
|
||||
});
|
||||
@@ -495,14 +511,17 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
false
|
||||
});
|
||||
|
||||
path_found.map(|path| self.as_derived(path, secp))
|
||||
path_found.map(|path| {
|
||||
self.at_derivation_index(path)
|
||||
.expect("We ignore hardened wildcards")
|
||||
})
|
||||
}
|
||||
|
||||
fn derive_from_hd_keypaths<'s>(
|
||||
fn derive_from_hd_keypaths(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
||||
let key_origins = hd_keypaths
|
||||
.iter()
|
||||
@@ -516,11 +535,11 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
self.derive_from_psbt_key_origins(key_origins, secp)
|
||||
}
|
||||
|
||||
fn derive_from_tap_key_origins<'s>(
|
||||
fn derive_from_tap_key_origins(
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "Convert" a tap_key_origins map to the format required by `derive_from_psbt_key_origins`
|
||||
let key_origins = tap_key_origins
|
||||
.iter()
|
||||
@@ -529,24 +548,24 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
self.derive_from_psbt_key_origins(key_origins, secp)
|
||||
}
|
||||
|
||||
fn derive_from_psbt_input<'s>(
|
||||
fn derive_from_psbt_input(
|
||||
&self,
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
) -> Option<DerivedDescriptor<'s>> {
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if let Some(derived) = self.derive_from_tap_key_origins(&psbt_input.tap_key_origins, secp) {
|
||||
return Some(derived);
|
||||
}
|
||||
if self.is_deriveable() {
|
||||
if self.has_wildcard() {
|
||||
// We can't try to bruteforce the derivation index, exit here
|
||||
return None;
|
||||
}
|
||||
|
||||
let descriptor = self.as_derived_fixed(secp);
|
||||
let descriptor = self.at_derivation_index(0).expect("0 is not hardened");
|
||||
match descriptor.desc_type() {
|
||||
// TODO: add pk() here
|
||||
DescriptorType::Pkh
|
||||
@@ -580,95 +599,16 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
}
|
||||
}
|
||||
|
||||
impl<'s> DerivedDescriptorMeta for DerivedDescriptor<'s> {
|
||||
fn get_hd_keypaths(&self, secp: &SecpCtx) -> HdKeyPaths {
|
||||
let mut answer = BTreeMap::new();
|
||||
self.for_each_key(|key| {
|
||||
if let DescriptorPublicKey::XPub(xpub) = key.as_key().deref() {
|
||||
let derived_pubkey = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, &xpub.derivation_path)
|
||||
.expect("Derivation can't fail");
|
||||
|
||||
answer.insert(
|
||||
derived_pubkey.public_key,
|
||||
(xpub.root_fingerprint(secp), xpub.full_path(&[])),
|
||||
);
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
answer
|
||||
}
|
||||
|
||||
fn get_tap_key_origins(&self, secp: &SecpCtx) -> TapKeyOrigins {
|
||||
use miniscript::ToPublicKey;
|
||||
|
||||
let mut answer = BTreeMap::new();
|
||||
let mut insert_path = |pk: &DerivedDescriptorKey<'_>, lh| {
|
||||
let key_origin = match pk.deref() {
|
||||
DescriptorPublicKey::XPub(xpub) => {
|
||||
Some((xpub.root_fingerprint(secp), xpub.full_path(&[])))
|
||||
}
|
||||
DescriptorPublicKey::SinglePub(_) => None,
|
||||
};
|
||||
|
||||
// If this is the internal key, we only insert the key origin if it's not None.
|
||||
// For keys found in the tap tree we always insert a key origin (because the signer
|
||||
// looks for it to know which leaves to sign for), even though it may be None
|
||||
match (lh, key_origin) {
|
||||
(None, Some(ko)) => {
|
||||
answer
|
||||
.entry(pk.to_x_only_pubkey())
|
||||
.or_insert_with(|| (vec![], ko));
|
||||
}
|
||||
(Some(lh), origin) => {
|
||||
answer
|
||||
.entry(pk.to_x_only_pubkey())
|
||||
.or_insert_with(|| (vec![], origin.unwrap_or_default()))
|
||||
.0
|
||||
.push(lh);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
};
|
||||
|
||||
if let Descriptor::Tr(tr) = &self {
|
||||
// Internal key first, then iterate the scripts
|
||||
insert_path(tr.internal_key(), None);
|
||||
|
||||
for (_, ms) in tr.iter_scripts() {
|
||||
// Assume always the same leaf version
|
||||
let leaf_hash = taproot::TapLeafHash::from_script(
|
||||
&ms.encode(),
|
||||
taproot::LeafVersion::TapScript,
|
||||
);
|
||||
|
||||
for key in ms.iter_pk_pkh() {
|
||||
let key = match key {
|
||||
miniscript::miniscript::iter::PkPkh::PlainPubkey(pk) => pk,
|
||||
miniscript::miniscript::iter::PkPkh::HashedPubkey(pk) => pk,
|
||||
};
|
||||
|
||||
insert_path(&key, Some(leaf_hash));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
answer
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
use alloc::string::ToString;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::{bip32, psbt};
|
||||
use bitcoin::Script;
|
||||
use bitcoin::ScriptBuf;
|
||||
use bitcoin::{bip32, psbt::Psbt};
|
||||
|
||||
use super::*;
|
||||
use crate::psbt::PsbtUtils;
|
||||
@@ -679,7 +619,7 @@ mod test {
|
||||
"wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)",
|
||||
)
|
||||
.unwrap();
|
||||
let psbt: psbt::PartiallySignedTransaction = deserialize(
|
||||
let psbt = Psbt::deserialize(
|
||||
&Vec::<u8>::from_hex(
|
||||
"70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\
|
||||
11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\
|
||||
@@ -702,7 +642,7 @@ mod test {
|
||||
"pkh([0f056943/44h/0h/0h]tpubDDpWvmUrPZrhSPmUzCMBHffvC3HyMAPnWDSAQNBTnj1iZeJa7BZQEttFiP4DS4GCcXQHezdXhn86Hj6LHX5EDstXPWrMaSneRWM8yUf6NFd/10/*)",
|
||||
)
|
||||
.unwrap();
|
||||
let psbt: psbt::PartiallySignedTransaction = deserialize(
|
||||
let psbt = Psbt::deserialize(
|
||||
&Vec::<u8>::from_hex(
|
||||
"70736274ff010053010000000145843b86be54a3cd8c9e38444e1162676c00df\
|
||||
e7964122a70df491ea12fd67090100000000ffffffff01c19598000000000017\
|
||||
@@ -733,7 +673,7 @@ mod test {
|
||||
"wsh(and_v(v:pk(03b6633fef2397a0a9de9d7b6f23aef8368a6e362b0581f0f0af70d5ecfd254b14),older(6)))",
|
||||
)
|
||||
.unwrap();
|
||||
let psbt: psbt::PartiallySignedTransaction = deserialize(
|
||||
let psbt = Psbt::deserialize(
|
||||
&Vec::<u8>::from_hex(
|
||||
"70736274ff01005302000000011c8116eea34408ab6529223c9a176606742207\
|
||||
67a1ff1d46a6e3c4a88243ea6e01000000000600000001109698000000000017\
|
||||
@@ -757,7 +697,7 @@ mod test {
|
||||
"sh(and_v(v:pk(021403881a5587297818fcaf17d239cefca22fce84a45b3b1d23e836c4af671dbb),after(630000)))",
|
||||
)
|
||||
.unwrap();
|
||||
let psbt: psbt::PartiallySignedTransaction = deserialize(
|
||||
let psbt = Psbt::deserialize(
|
||||
&Vec::<u8>::from_hex(
|
||||
"70736274ff0100530100000001bc8c13df445dfadcc42afa6dc841f85d22b01d\
|
||||
a6270ebf981740f4b7b1d800390000000000feffffff01ba9598000000000017\
|
||||
@@ -787,23 +727,40 @@ mod test {
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3c3gF1DUWpWNr2SG2XrG8oYPpqYh7hoWsJy9NjabErnzriJPpnGHyKz5NgdXmq1KVbqS1r4NXdCoKitWg5e86zqXHa8kxyB").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
// here `to_descriptor_key` will set the valid networks for the key to only mainnet, since
|
||||
// we are using an "xpub"
|
||||
let key = (xpub, path).into_descriptor_key().unwrap();
|
||||
let key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
// override it with any. this happens in some key conversions, like bip39
|
||||
let key = key.override_valid_networks(any_network());
|
||||
|
||||
// make a descriptor out of it
|
||||
let desc = crate::descriptor!(wpkh(key)).unwrap();
|
||||
// this should convert the key that supports "any_network" to the right network (testnet)
|
||||
let (wallet_desc, _) = desc
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubDEnoLuPdBep9bzw5LoGYpsxUQYheRQ9gcgrJhJEcdKFB9cWQRyYmkCyRoTqeD4tJYiVVgt6A3rN6rWn9RYhR9sBsGxji29LYWHuKKbdb1ev/0/*)#y8p7e8kk");
|
||||
let mut xprv_testnet = xprv;
|
||||
xprv_testnet.network = Network::Testnet;
|
||||
|
||||
let xpub_testnet = bip32::ExtendedPubKey::from_priv(&secp, &xprv_testnet);
|
||||
let desc_pubkey = DescriptorPublicKey::XPub(DescriptorXKey {
|
||||
xkey: xpub_testnet,
|
||||
origin: None,
|
||||
derivation_path: path,
|
||||
wildcard: Wildcard::Unhardened,
|
||||
});
|
||||
|
||||
assert_eq!(wallet_desc.to_string(), "wpkh(tpubD6NzVbkrYhZ4XtJzoDja5snUjBNQRP5B3f4Hyn1T1x6PVPxzzVjvw6nJx2D8RBCxog9GEVjZoyStfepTz7TtKoBVdkCtnc7VCJh9dD4RAU9/0/*)#a3svx0ha");
|
||||
assert_eq!(
|
||||
keymap
|
||||
.get(&desc_pubkey)
|
||||
.map(|key| key.to_public(&secp).unwrap()),
|
||||
Some(desc_pubkey)
|
||||
);
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from &str with and without checksum appended
|
||||
@@ -829,17 +786,11 @@ mod test {
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#67ju93jw"
|
||||
.into_wallet_descriptor(&secp, Network::Testnet);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::InvalidDescriptorChecksum));
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from &str with keys from right and wrong network
|
||||
@@ -873,17 +824,11 @@ mod test {
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)"
|
||||
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
|
||||
|
||||
let desc = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/2/*)"
|
||||
.into_wallet_descriptor(&secp, Network::Bitcoin);
|
||||
assert!(matches!(
|
||||
desc.err(),
|
||||
Some(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
));
|
||||
assert_matches!(desc, Err(DescriptorError::Key(KeyError::InvalidNetwork)));
|
||||
}
|
||||
|
||||
// test IntoWalletDescriptor trait from the output of the descriptor!() macro
|
||||
@@ -917,25 +862,23 @@ mod test {
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0'/1/2/*)";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
DescriptorError::HardenedDerivationXpub
|
||||
));
|
||||
assert_matches!(result, Err(DescriptorError::HardenedDerivationXpub));
|
||||
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/1/*))";
|
||||
let descriptor = "wpkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/<0;1>/*)";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
|
||||
assert_matches!(result, Err(DescriptorError::MultiPath));
|
||||
|
||||
// repeated pubkeys
|
||||
let descriptor = "wsh(multi(2,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*,tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/0/*))";
|
||||
let result = into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet);
|
||||
|
||||
assert!(result.is_err());
|
||||
assert!(matches!(
|
||||
result.unwrap_err(),
|
||||
DescriptorError::DuplicatedKeys
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sh_wsh_sortedmulti_redeemscript() {
|
||||
use super::{AsDerived, DescriptorScripts};
|
||||
use miniscript::psbt::PsbtInputExt;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
@@ -943,11 +886,16 @@ mod test {
|
||||
let (descriptor, _) =
|
||||
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
||||
|
||||
let descriptor = descriptor.as_derived(0, &secp);
|
||||
let descriptor = descriptor.at_derivation_index(0).unwrap();
|
||||
|
||||
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||
let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||
|
||||
assert_eq!(descriptor.psbt_redeem_script(), Some(script.to_v0_p2wsh()));
|
||||
assert_eq!(descriptor.psbt_witness_script(), Some(script));
|
||||
let mut psbt_input = psbt::Input::default();
|
||||
psbt_input
|
||||
.update_with_descriptor_unchecked(&descriptor)
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(psbt_input.redeem_script, Some(script.to_v0_p2wsh()));
|
||||
assert_eq!(psbt_input.witness_script, Some(script));
|
||||
}
|
||||
}
|
||||
@@ -32,38 +32,41 @@
|
||||
//!
|
||||
//! let signers = Arc::new(SignersContainer::build(key_map, &extended_desc, &secp));
|
||||
//! let policy = extended_desc.extract_policy(&signers, BuildSatisfaction::None, &secp)?;
|
||||
//! println!("policy: {}", serde_json::to_string(&policy)?);
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! println!("policy: {}", serde_json::to_string(&policy).unwrap());
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::cmp::max;
|
||||
use std::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use std::fmt;
|
||||
use crate::collections::{BTreeMap, HashSet, VecDeque};
|
||||
use alloc::string::String;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::max;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use serde::ser::SerializeMap;
|
||||
use serde::{Serialize, Serializer};
|
||||
|
||||
use bitcoin::hashes::*;
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::{PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::bip32::Fingerprint;
|
||||
use bitcoin::hashes::{hash160, ripemd160, sha256};
|
||||
use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorSinglePub, ShInner, SinglePubKey, SortedMultiVec, WshInner,
|
||||
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
|
||||
};
|
||||
use miniscript::hash256;
|
||||
use miniscript::{
|
||||
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
|
||||
};
|
||||
use miniscript::{Descriptor, Miniscript, MiniscriptKey, Satisfier, ScriptContext, Terminal};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use crate::descriptor::ExtractPolicy;
|
||||
use crate::keys::ExtScriptContext;
|
||||
use crate::wallet::signer::{SignerId, SignersContainer};
|
||||
use crate::wallet::utils::{self, After, Older, SecpCtx};
|
||||
use crate::wallet::utils::{After, Older, SecpCtx};
|
||||
|
||||
use super::checksum::get_checksum;
|
||||
use super::checksum::calc_checksum;
|
||||
use super::error::Error;
|
||||
use super::XKeyUtils;
|
||||
use bitcoin::util::psbt::{Input as PsbtInput, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use miniscript::psbt::PsbtInputSatisfier;
|
||||
|
||||
/// A unique identifier for a key
|
||||
@@ -81,15 +84,18 @@ pub enum PkOrF {
|
||||
impl PkOrF {
|
||||
fn from_key(k: &DescriptorPublicKey, secp: &SecpCtx) -> Self {
|
||||
match k {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(pk),
|
||||
..
|
||||
}) => PkOrF::Pubkey(*pk),
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(pk),
|
||||
..
|
||||
}) => PkOrF::XOnlyPubkey(*pk),
|
||||
DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
|
||||
DescriptorPublicKey::MultiXPub(multi) => {
|
||||
PkOrF::Fingerprint(multi.root_fingerprint(secp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -111,7 +117,7 @@ pub enum SatisfiableItem {
|
||||
/// Double SHA256 preimage hash
|
||||
Hash256Preimage {
|
||||
/// The digest value
|
||||
hash: sha256d::Hash,
|
||||
hash: hash256::Hash,
|
||||
},
|
||||
/// RIPEMD160 preimage hash
|
||||
Ripemd160Preimage {
|
||||
@@ -125,13 +131,13 @@ pub enum SatisfiableItem {
|
||||
},
|
||||
/// Absolute timeclock timestamp
|
||||
AbsoluteTimelock {
|
||||
/// The timestamp value
|
||||
value: u32,
|
||||
/// The timelock value
|
||||
value: absolute::LockTime,
|
||||
},
|
||||
/// Relative timelock locktime
|
||||
RelativeTimelock {
|
||||
/// The locktime value
|
||||
value: u32,
|
||||
/// The timelock value
|
||||
value: Sequence,
|
||||
},
|
||||
/// Multi-signature public keys with threshold count
|
||||
Multisig {
|
||||
@@ -165,7 +171,7 @@ impl SatisfiableItem {
|
||||
|
||||
/// Returns a unique id for the [`SatisfiableItem`]
|
||||
pub fn id(&self) -> String {
|
||||
get_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
|
||||
calc_checksum(&serde_json::to_string(self).expect("Failed to serialize a SatisfiableItem"))
|
||||
.expect("Failed to compute a SatisfiableItem id")
|
||||
}
|
||||
}
|
||||
@@ -438,32 +444,33 @@ pub struct Policy {
|
||||
}
|
||||
|
||||
/// An extra condition that must be satisfied but that is out of control of the user
|
||||
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Default, Serialize)]
|
||||
/// TODO: use `bitcoin::LockTime` and `bitcoin::Sequence`
|
||||
#[derive(Hash, Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Default, Serialize)]
|
||||
pub struct Condition {
|
||||
/// Optional CheckSequenceVerify condition
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub csv: Option<u32>,
|
||||
pub csv: Option<Sequence>,
|
||||
/// Optional timelock condition
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timelock: Option<u32>,
|
||||
pub timelock: Option<absolute::LockTime>,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
fn merge_nlocktime(a: u32, b: u32) -> Result<u32, PolicyError> {
|
||||
if (a < utils::BLOCKS_TIMELOCK_THRESHOLD) != (b < utils::BLOCKS_TIMELOCK_THRESHOLD) {
|
||||
fn merge_nlocktime(
|
||||
a: absolute::LockTime,
|
||||
b: absolute::LockTime,
|
||||
) -> Result<absolute::LockTime, PolicyError> {
|
||||
if !a.is_same_unit(b) {
|
||||
Err(PolicyError::MixedTimelockUnits)
|
||||
} else if a > b {
|
||||
Ok(a)
|
||||
} else {
|
||||
Ok(max(a, b))
|
||||
Ok(b)
|
||||
}
|
||||
}
|
||||
|
||||
fn merge_nsequence(a: u32, b: u32) -> Result<u32, PolicyError> {
|
||||
let mask = utils::SEQUENCE_LOCKTIME_TYPE_FLAG | utils::SEQUENCE_LOCKTIME_MASK;
|
||||
|
||||
let a = a & mask;
|
||||
let b = b & mask;
|
||||
|
||||
if (a < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) != (b < utils::SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
||||
fn merge_nsequence(a: Sequence, b: Sequence) -> Result<Sequence, PolicyError> {
|
||||
if a.is_time_locked() != b.is_time_locked() {
|
||||
Err(PolicyError::MixedTimelockUnits)
|
||||
} else {
|
||||
Ok(max(a, b))
|
||||
@@ -511,10 +518,18 @@ pub enum PolicyError {
|
||||
|
||||
impl fmt::Display for PolicyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
match self {
|
||||
Self::NotEnoughItemsSelected(err) => write!(f, "Not enough items selected: {}", err),
|
||||
Self::IndexOutOfRange(index) => write!(f, "Index out of range: {}", index),
|
||||
Self::AddOnLeaf => write!(f, "Add on leaf"),
|
||||
Self::AddOnPartialComplete => write!(f, "Add on partial complete"),
|
||||
Self::MixedTimelockUnits => write!(f, "Mixed timelock units"),
|
||||
Self::IncompatibleConditions => write!(f, "Incompatible conditions"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for PolicyError {}
|
||||
|
||||
impl Policy {
|
||||
@@ -651,11 +666,11 @@ impl Policy {
|
||||
(0..*threshold).collect()
|
||||
}
|
||||
SatisfiableItem::Multisig { keys, .. } => (0..keys.len()).collect(),
|
||||
_ => vec![],
|
||||
_ => HashSet::new(),
|
||||
};
|
||||
let selected = match path.get(&self.id) {
|
||||
Some(arr) => arr,
|
||||
_ => &default,
|
||||
let selected: HashSet<_> = match path.get(&self.id) {
|
||||
Some(arr) => arr.iter().copied().collect(),
|
||||
_ => default,
|
||||
};
|
||||
|
||||
match &self.item {
|
||||
@@ -663,14 +678,24 @@ impl Policy {
|
||||
let mapped_req = items
|
||||
.iter()
|
||||
.map(|i| i.get_condition(path))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// if all the requirements are null we don't care about `selected` because there
|
||||
// are no requirements
|
||||
if mapped_req.iter().all(Condition::is_null) {
|
||||
if mapped_req
|
||||
.iter()
|
||||
.all(|cond| matches!(cond, Ok(c) if c.is_null()))
|
||||
{
|
||||
return Ok(Condition::default());
|
||||
}
|
||||
|
||||
// make sure all the indexes in the `selected` list are within range
|
||||
for index in &selected {
|
||||
if *index >= items.len() {
|
||||
return Err(PolicyError::IndexOutOfRange(*index));
|
||||
}
|
||||
}
|
||||
|
||||
// if we have something, make sure we have enough items. note that the user can set
|
||||
// an empty value for this step in case of n-of-n, because `selected` is set to all
|
||||
// the elements above
|
||||
@@ -679,23 +704,18 @@ impl Policy {
|
||||
}
|
||||
|
||||
// check the selected items, see if there are conflicting requirements
|
||||
let mut requirements = Condition::default();
|
||||
for item_index in selected {
|
||||
requirements = requirements.merge(
|
||||
mapped_req
|
||||
.get(*item_index)
|
||||
.ok_or(PolicyError::IndexOutOfRange(*item_index))?,
|
||||
)?;
|
||||
}
|
||||
|
||||
Ok(requirements)
|
||||
mapped_req
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| selected.contains(index))
|
||||
.try_fold(Condition::default(), |acc, (_, cond)| acc.merge(&cond?))
|
||||
}
|
||||
SatisfiableItem::Multisig { keys, threshold } => {
|
||||
if selected.len() < *threshold {
|
||||
return Err(PolicyError::NotEnoughItemsSelected(self.id.clone()));
|
||||
}
|
||||
if let Some(item) = selected.iter().find(|i| **i >= keys.len()) {
|
||||
return Err(PolicyError::IndexOutOfRange(*item));
|
||||
if let Some(item) = selected.into_iter().find(|&i| i >= keys.len()) {
|
||||
return Err(PolicyError::IndexOutOfRange(item));
|
||||
}
|
||||
|
||||
Ok(Condition::default())
|
||||
@@ -720,16 +740,20 @@ impl From<SatisfiableItem> for Policy {
|
||||
}
|
||||
|
||||
fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
||||
// For consistency we always compute the key hash in "ecdsa" form (with the leading sign
|
||||
// prefix) even if we are in a taproot descriptor. We just want some kind of unique identifier
|
||||
// for a key, so it doesn't really matter how the identifier is computed.
|
||||
match key {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(pk),
|
||||
..
|
||||
}) => pk.to_pubkeyhash().into(),
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(pk),
|
||||
..
|
||||
}) => pk.to_pubkeyhash().into(),
|
||||
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||
DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -767,9 +791,9 @@ fn make_generic_signature<M: Fn() -> SatisfiableItem, F: Fn(&Psbt) -> bool>(
|
||||
fn generic_sig_in_psbt<
|
||||
// C is for "check", it's a closure we use to *check* if a psbt input contains the signature
|
||||
// for a specific key
|
||||
C: Fn(&PsbtInput, &SinglePubKey) -> bool,
|
||||
C: Fn(&psbt::Input, &SinglePubKey) -> bool,
|
||||
// E is for "extract", it extracts a key from the bip32 derivations found in the psbt input
|
||||
E: Fn(&PsbtInput, Fingerprint) -> Option<SinglePubKey>,
|
||||
E: Fn(&psbt::Input, Fingerprint) -> Option<SinglePubKey>,
|
||||
>(
|
||||
psbt: &Psbt,
|
||||
key: &DescriptorPublicKey,
|
||||
@@ -779,7 +803,7 @@ fn generic_sig_in_psbt<
|
||||
) -> bool {
|
||||
//TODO check signature validity
|
||||
psbt.inputs.iter().all(|input| match key {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub { key, .. }) => check(input, key),
|
||||
DescriptorPublicKey::Single(SinglePub { key, .. }) => check(input, key),
|
||||
DescriptorPublicKey::XPub(xpub) => {
|
||||
//TODO check actual derivation matches
|
||||
match extract(input, xpub.root_fingerprint(secp)) {
|
||||
@@ -787,6 +811,13 @@ fn generic_sig_in_psbt<
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
DescriptorPublicKey::MultiXPub(xpub) => {
|
||||
//TODO check actual derivation matches
|
||||
match extract(input, xpub.root_fingerprint(secp)) {
|
||||
Some(pubkey) => check(input, &pubkey),
|
||||
None => false,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -891,10 +922,13 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
Some(Ctx::make_signature(pubkey_hash, signers, build_sat, secp))
|
||||
}
|
||||
Terminal::After(value) => {
|
||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock { value: *value }.into();
|
||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
|
||||
value: (*value).into(),
|
||||
}
|
||||
.into();
|
||||
policy.contribution = Satisfaction::Complete {
|
||||
condition: Condition {
|
||||
timelock: Some(*value),
|
||||
timelock: Some((*value).into()),
|
||||
csv: None,
|
||||
},
|
||||
};
|
||||
@@ -905,9 +939,11 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
} = build_sat
|
||||
{
|
||||
let after = After::new(Some(current_height), false);
|
||||
let after_sat = Satisfier::<bitcoin::PublicKey>::check_after(&after, *value);
|
||||
let inputs_sat = psbt_inputs_sat(psbt)
|
||||
.all(|sat| Satisfier::<bitcoin::PublicKey>::check_after(&sat, *value));
|
||||
let after_sat =
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&after, (*value).into());
|
||||
let inputs_sat = psbt_inputs_sat(psbt).all(|sat| {
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&sat, (*value).into())
|
||||
});
|
||||
if after_sat && inputs_sat {
|
||||
policy.satisfaction = policy.contribution.clone();
|
||||
}
|
||||
@@ -999,6 +1035,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
|
||||
Policy::make_thresh(mapped, threshold)?
|
||||
}
|
||||
|
||||
// Unsupported
|
||||
Terminal::RawPkH(_) => None,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1124,16 +1163,15 @@ mod test {
|
||||
use crate::descriptor::{ExtractPolicy, IntoWalletDescriptor};
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::policy::SatisfiableItem::{EcdsaSignature, Multisig, Thresh};
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||
use crate::wallet::signer::SignersContainer;
|
||||
use alloc::{string::ToString, sync::Arc};
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::Network;
|
||||
use miniscript::DescriptorTrait;
|
||||
use std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use core::str::FromStr;
|
||||
|
||||
const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
|
||||
const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
|
||||
@@ -1172,8 +1210,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::None);
|
||||
|
||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||
let (wallet_desc, keymap) = desc
|
||||
@@ -1185,10 +1223,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||
);
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
|
||||
}
|
||||
|
||||
// 2 pub keys descriptor, required 2 prv keys
|
||||
@@ -1207,19 +1243,16 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
// TODO should this be "Satisfaction::None" since we have no prv keys?
|
||||
// TODO should items and conditions not be empty?
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
&& m == &2usize
|
||||
&& items.is_empty()
|
||||
&& conditions.is_empty()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1238,18 +1271,15 @@ mod test {
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2usize
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
assert_matches!(&policy.contribution, Satisfaction::Partial { n, m, items, conditions, ..} if n == &2usize
|
||||
&& m == &2usize
|
||||
&& items.len() == 1
|
||||
&& conditions.contains_key(&0)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1271,18 +1301,15 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &1
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0])
|
||||
&& conditions.contains_key(&vec![1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1303,18 +1330,15 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
||||
assert_matches!(&policy.item, Multisig { keys, threshold } if threshold == &2
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0,1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1329,31 +1353,27 @@ mod test {
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = single_key
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(matches!(&policy.contribution, Satisfaction::None));
|
||||
assert_matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint);
|
||||
assert_matches!(&policy.contribution, Satisfaction::None);
|
||||
|
||||
let desc = descriptor!(wpkh(prvkey)).unwrap();
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = single_key
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(matches!(&policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == &fingerprint));
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::Complete {condition} if condition.csv == None && condition.timelock == None)
|
||||
);
|
||||
assert_matches!(policy.item, EcdsaSignature(PkOrF::Fingerprint(f)) if f == fingerprint);
|
||||
assert_matches!(policy.contribution, Satisfaction::Complete {condition} if condition.csv.is_none() && condition.timelock.is_none());
|
||||
}
|
||||
|
||||
// single key, 1 prv and 1 pub key descriptor, required 1 prv keys
|
||||
@@ -1368,25 +1388,21 @@ mod test {
|
||||
let (wallet_desc, keymap) = desc
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let single_key = wallet_desc.derive(0);
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = single_key
|
||||
let policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Multisig { keys, threshold } if threshold == &1
|
||||
assert_matches!(policy.item, Multisig { keys, threshold } if threshold == 1
|
||||
&& keys[0] == PkOrF::Fingerprint(fingerprint0)
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1))
|
||||
&& keys[1] == PkOrF::Fingerprint(fingerprint1)
|
||||
);
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &2
|
||||
&& m == &1
|
||||
assert_matches!(policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == 2
|
||||
&& m == 1
|
||||
&& items.len() == 2
|
||||
&& conditions.contains_key(&vec![0])
|
||||
&& conditions.contains_key(&vec![1])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1418,18 +1434,14 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2)
|
||||
);
|
||||
assert_matches!(&policy.item, Thresh { items, threshold } if items.len() == 3 && threshold == &2);
|
||||
|
||||
assert!(
|
||||
matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
|
||||
assert_matches!(&policy.contribution, Satisfaction::PartialComplete { n, m, items, conditions, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items.len() == 3
|
||||
&& conditions.get(&vec![0,1]).unwrap().iter().next().unwrap().csv.is_none()
|
||||
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
|
||||
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(sequence)
|
||||
)
|
||||
&& conditions.get(&vec![0,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
|
||||
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1452,12 +1464,12 @@ mod test {
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = wallet_desc
|
||||
let _policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!("desc policy = {:?}", policy); // TODO remove
|
||||
// TODO how should this fail with mixed timelocks?
|
||||
// println!("desc policy = {:?}", policy); // TODO remove
|
||||
// TODO how should this fail with mixed timelocks?
|
||||
}
|
||||
|
||||
// - multiple timelocks of the same type should be correctly merged together
|
||||
@@ -1477,12 +1489,12 @@ mod test {
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = wallet_desc
|
||||
let _policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
println!("desc policy = {:?}", policy); // TODO remove
|
||||
// TODO how should this merge timelocks?
|
||||
// println!("desc policy = {:?}", policy); // TODO remove
|
||||
// TODO how should this merge timelocks?
|
||||
let (prvkey1, _pubkey1, _fingerprint1) = setup_keys(TPRV0_STR, PATH, &secp);
|
||||
let locktime_seconds0 = 500000100;
|
||||
let locktime_seconds1 = 500000200;
|
||||
@@ -1495,12 +1507,12 @@ mod test {
|
||||
.into_wallet_descriptor(&secp, Network::Testnet)
|
||||
.unwrap();
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
let policy = wallet_desc
|
||||
let _policy = wallet_desc
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
println!("desc policy = {:?}", policy); // TODO remove
|
||||
// println!("desc policy = {:?}", policy); // TODO remove
|
||||
|
||||
// TODO how should this merge timelocks?
|
||||
}
|
||||
@@ -1574,7 +1586,8 @@ mod test {
|
||||
.unwrap();
|
||||
|
||||
let addr = wallet_desc
|
||||
.as_derived(0, &secp)
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1592,11 +1605,9 @@ mod test {
|
||||
.unwrap();
|
||||
//println!("{}", serde_json::to_string(&policy_alice_psbt).unwrap());
|
||||
|
||||
assert!(
|
||||
matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_alice_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![0]
|
||||
)
|
||||
);
|
||||
|
||||
let psbt = Psbt::from_str(BOB_SIGNED_PSBT).unwrap();
|
||||
@@ -1606,11 +1617,9 @@ mod test {
|
||||
.unwrap();
|
||||
//println!("{}", serde_json::to_string(&policy_bob_psbt).unwrap());
|
||||
|
||||
assert!(
|
||||
matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_bob_psbt.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![1]
|
||||
)
|
||||
);
|
||||
|
||||
let psbt = Psbt::from_str(ALICE_BOB_SIGNED_PSBT).unwrap();
|
||||
@@ -1618,11 +1627,9 @@ mod test {
|
||||
.extract_policy(&signers_container, BuildSatisfaction::Psbt(&psbt), &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
|
||||
assert_matches!(&policy_alice_bob_psbt.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &2
|
||||
&& m == &2
|
||||
&& items == &vec![0, 1]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1646,7 +1653,8 @@ mod test {
|
||||
let signers_container = Arc::new(SignersContainer::build(keymap, &wallet_desc, &secp));
|
||||
|
||||
let addr = wallet_desc
|
||||
.as_derived(0, &secp)
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1666,11 +1674,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items.is_empty()
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy).unwrap());
|
||||
|
||||
@@ -1684,11 +1690,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat_expired, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy_expired.satisfaction, Satisfaction::Partial { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items == &vec![0]
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy_expired).unwrap());
|
||||
|
||||
@@ -1704,11 +1708,9 @@ mod test {
|
||||
.extract_policy(&signers_container, build_sat_expired_signed, &secp)
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
assert!(
|
||||
matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
|
||||
assert_matches!(&policy_expired_signed.satisfaction, Satisfaction::PartialComplete { n, m, items, .. } if n == &3
|
||||
&& m == &2
|
||||
&& items == &vec![0, 1]
|
||||
)
|
||||
);
|
||||
//println!("{}", serde_json::to_string(&policy_expired_signed).unwrap());
|
||||
}
|
||||
@@ -1783,12 +1785,8 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1])
|
||||
);
|
||||
assert_matches!(policy.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy.contribution, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![1]);
|
||||
|
||||
let alice_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(alice_fing));
|
||||
let bob_sig = SatisfiableItem::SchnorrSignature(PkOrF::Fingerprint(bob_fing));
|
||||
@@ -1880,19 +1878,11 @@ mod test {
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
|
||||
assert!(
|
||||
matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty())
|
||||
);
|
||||
assert_matches!(policy_unsigned.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy_unsigned.satisfaction, Satisfaction::Partial { n: 2, m: 1, items, .. } if items.is_empty());
|
||||
|
||||
assert!(
|
||||
matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2)
|
||||
);
|
||||
assert!(
|
||||
matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1])
|
||||
);
|
||||
assert_matches!(policy_signed.item, SatisfiableItem::Thresh { ref items, threshold: 1 } if items.len() == 2);
|
||||
assert_matches!(policy_signed.satisfaction, Satisfaction::PartialComplete { n: 2, m: 1, items, .. } if items == vec![0, 1]);
|
||||
|
||||
let satisfied_items = match policy_signed.item {
|
||||
SatisfiableItem::Thresh { items, .. } => items,
|
||||
@@ -14,10 +14,10 @@
|
||||
//! This module contains the definition of various common script templates that are ready to be
|
||||
//! used. See the documentation of each template for an example.
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::Network;
|
||||
|
||||
use miniscript::{Legacy, Segwitv0};
|
||||
use miniscript::{Legacy, Segwitv0, Tap};
|
||||
|
||||
use super::{ExtendedDescriptor, IntoWalletDescriptor, KeyMap};
|
||||
use crate::descriptor::DescriptorError;
|
||||
@@ -73,22 +73,16 @@ impl<T: DescriptorTemplate> IntoWalletDescriptor for T {
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Pkh;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Pkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Pkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -107,22 +101,16 @@ impl<K: IntoDescriptorKey<Legacy>> DescriptorTemplate for P2Pkh<K> {
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// # use bdk::Wallet;
|
||||
/// use bdk::template::P2Wpkh_P2Sh;
|
||||
/// use bdk::wallet::AddressIndex;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Wpkh_P2Sh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh_P2Sh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// wallet.get_address(AddressIndex::New).to_string(),
|
||||
/// "2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -143,21 +131,15 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh_P2Sh<K> {
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2Wpkh;
|
||||
/// use bdk::wallet::AddressIndex::New;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// P2Wpkh(key),
|
||||
/// None,
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default(),
|
||||
/// )?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2Wpkh(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New)?.to_string(),
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "tb1q4525hmgw265tl3drrl8jjta7ayffu6jf68ltjd"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
@@ -170,6 +152,34 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
}
|
||||
}
|
||||
|
||||
/// P2TR template. Expands to a descriptor `tr(key)`
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::Wallet;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::P2TR;
|
||||
///
|
||||
/// let key =
|
||||
/// bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(P2TR(key), None, Network::Testnet)?;
|
||||
///
|
||||
/// assert_eq!(
|
||||
/// wallet.get_address(New).to_string(),
|
||||
/// "tb1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xq7hps46"
|
||||
/// );
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct P2TR<K: IntoDescriptorKey<Tap>>(pub K);
|
||||
|
||||
impl<K: IntoDescriptorKey<Tap>> DescriptorTemplate for P2TR<K> {
|
||||
fn build(self, _network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
descriptor!(tr(self.0))
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP44 template. Expands to `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
@@ -182,20 +192,18 @@ impl<K: IntoDescriptorKey<Segwitv0>> DescriptorTemplate for P2Wpkh<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip44(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip44(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "mmogjc7HJEZkrLqyQYqJmxUqFaC7i4uf89");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDCuorCpzvYS2LCD75BR46KHE8GdDeg1wsAgNZeNr6DaB5gQK1o14uErKwKLuFmeemkQ6N2m3rNgvctdJLyr7nwu2yia7413Hhg8WWE44cgT/0/*)#5wrnv0xt");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44<K: DerivableKey<Legacy>>(pub K, pub KeychainKind);
|
||||
@@ -221,28 +229,29 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip44Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip44Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip44Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "pkh([c55b303f/44'/0'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#xgaaevjx");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "miNG7dJTzJqNbFS19svRdTCisC65dsubtR");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "pkh([c55b303f/44'/1'/0']tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU/0/*)#cfhumdqz");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip44Public<K: DerivableKey<Legacy>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Pkh(legacy::make_bipxx_public(44, self.0, self.1, self.2)?).build(network)
|
||||
P2Pkh(legacy::make_bipxx_public(
|
||||
44, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -258,20 +267,18 @@ impl<K: DerivableKey<Legacy>> DescriptorTemplate for Bip44Public<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip49(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip49(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N4zkWAoGdUv4NXhSsU8DvS5MB36T8nKHEB");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDDYr4kdnZgjjShzYNjZUZXUUtpXaofdkMaipyS8ThEh45qFmhT4hKYways7UXmg6V7het1QiFo9kf4kYUXyDvV4rHEyvSpys9pjCB3pukxi/0/*))#s9vxlc8e");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
@@ -284,7 +291,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
|
||||
/// BIP49 public template. Expands to `sh(wpkh(key/{0,1}/*))`
|
||||
///
|
||||
/// This assumes that the key used has already been derived with `m/49'/0'/0'`.
|
||||
/// This assumes that the key used has already been derived with `m/49'/0'/0'` for Mainnet or `m/49'/1'/0'` for Testnet.
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
@@ -297,28 +304,29 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip49Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip49Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip49Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "sh(wpkh([c55b303f/49'/0'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#gsmdv4xr");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "sh(wpkh([c55b303f/49'/1'/0']tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L/0/*))#3tka9g0q");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip49Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(49, self.0, self.1, self.2)?).build(network)
|
||||
P2Wpkh_P2Sh(segwit_v0::make_bipxx_public(
|
||||
49, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -334,20 +342,18 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip49Public<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip84(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip84(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qhl85z42h7r4su5u37rvvw0gk8j2t3n9y7zsg4n");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDDc5mum24DekpNw92t6fHGp8Gr2JjF9J7i4TZBtN6Vp8xpAULG5CFaKsfugWa5imhrQQUZKXe261asP5koDHo5bs3qNTmf3U3o4v9SaB8gg/0/*)#6kfecsmr");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84<K: DerivableKey<Segwitv0>>(pub K, pub KeychainKind);
|
||||
@@ -360,7 +366,7 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
|
||||
/// BIP84 public template. Expands to `wpkh(key/{0,1}/*)`
|
||||
///
|
||||
/// This assumes that the key used has already been derived with `m/84'/0'/0'`.
|
||||
/// This assumes that the key used has already been derived with `m/84'/0'/0'` for Mainnet or `m/84'/1'/0'` for Testnet.
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
@@ -373,28 +379,104 @@ impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84<K> {
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::database::MemoryDatabase;
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip84Public;
|
||||
///
|
||||
/// let key = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let wallet = Wallet::new(
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip84Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip84Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// MemoryDatabase::default()
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New)?.to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External)?.unwrap().to_string(), "wpkh([c55b303f/84\'/0\'/0\']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#nkk5dtkg");
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2pr6y4qc7");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "wpkh([c55b303f/84'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#dhu402yv");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip84Public<K: DerivableKey<Segwitv0>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Segwitv0>> DescriptorTemplate for Bip84Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(84, self.0, self.1, self.2)?).build(network)
|
||||
P2Wpkh(segwit_v0::make_bipxx_public(
|
||||
84, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP86 template. Expands to `tr(key/86'/{0,1}'/0'/{0,1}/*)`
|
||||
///
|
||||
/// Since there are hardened derivation steps, this template requires a private derivable key (generally a `xprv`/`tprv`).
|
||||
///
|
||||
/// See [`Bip86Public`] for a template that can work with a `xpub`/`tpub`.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86;
|
||||
///
|
||||
/// let key = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPeZRHk4rTG6orPS2CRNFX3njhUXx5vj9qGog5ZMH4uGReDWN5kCkY3jmWEtWause41CDvBRXD1shKknAMKxT99o9qUTRVC6m")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip86(key.clone(), KeychainKind::External),
|
||||
/// Some(Bip86(key, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1p5unlj09djx8xsjwe97269kqtxqpwpu2epeskgqjfk4lnf69v4tnqpp35qu");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDCiHofpEs47kx358bPdJmTZHmCDqQ8qw32upCSxHrSEdeeBs2T5Mq6QMB2ukeMqhNBiyhosBvJErteVhfURPGXPv3qLJPw5MVpHUewsbP2m/0/*)#dkgvr5hm");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip86<K: DerivableKey<Tap>>(pub K, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2TR(segwit_v1::make_bipxx_private(86, self.0, self.1, network)?).build(network)
|
||||
}
|
||||
}
|
||||
|
||||
/// BIP86 public template. Expands to `tr(key/{0,1}/*)`
|
||||
///
|
||||
/// This assumes that the key used has already been derived with `m/86'/0'/0'` for Mainnet or `m/86'/1'/0'` for Testnet.
|
||||
///
|
||||
/// This template requires the parent fingerprint to populate correctly the metadata of PSBTs.
|
||||
///
|
||||
/// See [`Bip86`] for a template that does the full derivation, but requires private data
|
||||
/// for the key.
|
||||
///
|
||||
/// ## Example
|
||||
///
|
||||
/// ```
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bdk::bitcoin::{PrivateKey, Network};
|
||||
/// # use bdk::{Wallet, KeychainKind};
|
||||
/// # use bdk::wallet::AddressIndex::New;
|
||||
/// use bdk::template::Bip86Public;
|
||||
///
|
||||
/// let key = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q")?;
|
||||
/// let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f")?;
|
||||
/// let mut wallet = Wallet::new_no_persist(
|
||||
/// Bip86Public(key.clone(), fingerprint, KeychainKind::External),
|
||||
/// Some(Bip86Public(key, fingerprint, KeychainKind::Internal)),
|
||||
/// Network::Testnet,
|
||||
/// )?;
|
||||
///
|
||||
/// assert_eq!(wallet.get_address(New).to_string(), "tb1pwjp9f2k5n0xq73ecuu0c5njvgqr3vkh7yaylmpqvsuuaafymh0msvcmh37");
|
||||
/// assert_eq!(wallet.public_descriptor(KeychainKind::External).unwrap().to_string(), "tr([c55b303f/86'/1'/0']tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q/0/*)#2p65srku");
|
||||
/// # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
/// ```
|
||||
pub struct Bip86Public<K: DerivableKey<Tap>>(pub K, pub bip32::Fingerprint, pub KeychainKind);
|
||||
|
||||
impl<K: DerivableKey<Tap>> DescriptorTemplate for Bip86Public<K> {
|
||||
fn build(self, network: Network) -> Result<DescriptorTemplateOut, DescriptorError> {
|
||||
P2TR(segwit_v1::make_bipxx_public(
|
||||
86, self.0, self.1, self.2, network,
|
||||
)?)
|
||||
.build(network)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -409,7 +491,7 @@ macro_rules! expand_make_bipxx {
|
||||
keychain: KeychainKind,
|
||||
network: Network,
|
||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||
let mut derivation_path = Vec::with_capacity(4);
|
||||
let mut derivation_path = alloc::vec::Vec::with_capacity(4);
|
||||
derivation_path.push(bip32::ChildNumber::from_hardened_idx(bip)?);
|
||||
|
||||
match network {
|
||||
@@ -440,6 +522,7 @@ macro_rules! expand_make_bipxx {
|
||||
key: K,
|
||||
parent_fingerprint: bip32::Fingerprint,
|
||||
keychain: KeychainKind,
|
||||
network: Network,
|
||||
) -> Result<impl IntoDescriptorKey<$ctx>, DescriptorError> {
|
||||
let derivation_path: bip32::DerivationPath = match keychain {
|
||||
KeychainKind::External => vec![bip32::ChildNumber::from_normal_idx(0)?].into(),
|
||||
@@ -448,7 +531,10 @@ macro_rules! expand_make_bipxx {
|
||||
|
||||
let source_path = bip32::DerivationPath::from(vec![
|
||||
bip32::ChildNumber::from_hardened_idx(bip)?,
|
||||
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
match network {
|
||||
Network::Bitcoin => bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
_ => bip32::ChildNumber::from_hardened_idx(1)?,
|
||||
},
|
||||
bip32::ChildNumber::from_hardened_idx(0)?,
|
||||
]);
|
||||
|
||||
@@ -460,53 +546,53 @@ macro_rules! expand_make_bipxx {
|
||||
|
||||
expand_make_bipxx!(legacy, Legacy);
|
||||
expand_make_bipxx!(segwit_v0, Segwitv0);
|
||||
expand_make_bipxx!(segwit_v1, Tap);
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
// test existing descriptor templates, make sure they are expanded to the right descriptors
|
||||
|
||||
use std::str::FromStr;
|
||||
use alloc::{string::ToString, vec::Vec};
|
||||
use core::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
use crate::descriptor::derived::AsDerived;
|
||||
use crate::descriptor::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, DescriptorTrait, KeyMap};
|
||||
use assert_matches::assert_matches;
|
||||
use miniscript::descriptor::{DescriptorPublicKey, KeyMap};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
// BIP44 `pkh(key/44'/{0,1}'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_template_cointype() {
|
||||
use bitcoin::util::bip32::ChildNumber::{self, Hardened};
|
||||
use bitcoin::bip32::ChildNumber::{self, Hardened};
|
||||
|
||||
let xprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||
let xprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K2fpbqApQL69a4oKdGVnVN52R82Ft7d1pSqgKmajF62acJo3aMszZb6qQ22QsVECSFxvf9uyxFUvFYQMq3QbtwtRSMjLAhMf").unwrap();
|
||||
assert_eq!(Network::Bitcoin, xprvkey.network);
|
||||
let xdesc = Bip44(xprvkey, KeychainKind::Internal)
|
||||
.build(Network::Bitcoin)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = xdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||
let purpose = path.first().unwrap();
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 0 }));
|
||||
assert_matches!(coin_type, Hardened { index: 0 });
|
||||
}
|
||||
|
||||
let tprvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let tprvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
assert_eq!(Network::Testnet, tprvkey.network);
|
||||
let tdesc = Bip44(tprvkey, KeychainKind::Internal)
|
||||
.build(Network::Testnet)
|
||||
.unwrap();
|
||||
|
||||
if let ExtendedDescriptor::Pkh(pkh) = tdesc.0 {
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().into();
|
||||
let purpose = path.get(0).unwrap();
|
||||
assert!(matches!(purpose, Hardened { index: 44 }));
|
||||
let path: Vec<ChildNumber> = pkh.into_inner().full_derivation_path().unwrap().into();
|
||||
let purpose = path.first().unwrap();
|
||||
assert_matches!(purpose, Hardened { index: 44 });
|
||||
let coin_type = path.get(1).unwrap();
|
||||
assert!(matches!(coin_type, Hardened { index: 1 }));
|
||||
assert_matches!(coin_type, Hardened { index: 1 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -514,22 +600,23 @@ mod test {
|
||||
fn check(
|
||||
desc: Result<(Descriptor<DescriptorPublicKey>, KeyMap, ValidNetworks), DescriptorError>,
|
||||
is_witness: bool,
|
||||
is_taproot: bool,
|
||||
is_fixed: bool,
|
||||
network: Network,
|
||||
expected: &[&str],
|
||||
) {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let (desc, _key_map, _networks) = desc.unwrap();
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
assert_eq!(!desc.is_deriveable(), is_fixed);
|
||||
assert_eq!(desc.is_taproot(), is_taproot);
|
||||
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||
for i in 0..expected.len() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.is_deriveable() {
|
||||
desc.as_derived_fixed(&secp)
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0).unwrap()
|
||||
} else {
|
||||
desc.as_derived(index, &secp)
|
||||
desc.at_derivation_index(index).unwrap()
|
||||
};
|
||||
let address = child_desc.address(Regtest).unwrap();
|
||||
let address = child_desc.address(network).unwrap();
|
||||
assert_eq!(address.to_string(), *expected.get(i).unwrap());
|
||||
}
|
||||
}
|
||||
@@ -543,7 +630,9 @@ mod test {
|
||||
check(
|
||||
P2Pkh(prvkey).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||
);
|
||||
|
||||
@@ -554,7 +643,9 @@ mod test {
|
||||
check(
|
||||
P2Pkh(pubkey).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||
);
|
||||
}
|
||||
@@ -568,7 +659,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||
);
|
||||
|
||||
@@ -579,7 +672,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||
);
|
||||
}
|
||||
@@ -593,7 +688,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||
);
|
||||
|
||||
@@ -604,19 +701,52 @@ mod test {
|
||||
check(
|
||||
P2Wpkh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["bcrt1qngw83fg8dz0k749cg7k3emc7v98wy0c7azaa6h"],
|
||||
);
|
||||
}
|
||||
|
||||
// P2TR `tr(key)`
|
||||
#[test]
|
||||
fn test_p2tr_template() {
|
||||
let prvkey =
|
||||
bitcoin::PrivateKey::from_wif("cTc4vURSzdx6QE6KVynWGomDbLaA75dNALMNyfjh3p8DRRar84Um")
|
||||
.unwrap();
|
||||
check(
|
||||
P2TR(prvkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["bcrt1pvjf9t34fznr53u5tqhejz4nr69luzkhlvsdsdfq9pglutrpve2xqnwtkqq"],
|
||||
);
|
||||
|
||||
let pubkey = bitcoin::PublicKey::from_str(
|
||||
"03a34b99f22c790c4e36b2b3c2c35a36db06226e41c692fc82b8b56ac1c540c5bd",
|
||||
)
|
||||
.unwrap();
|
||||
check(
|
||||
P2TR(pubkey).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["bcrt1pw74tdcrxlzn5r8z6ku2vztr86fgq0m245s72mjktf4afwzsf8ugs4evwdf"],
|
||||
);
|
||||
}
|
||||
|
||||
// BIP44 `pkh(key/44'/0'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5",
|
||||
"mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP",
|
||||
@@ -627,6 +757,8 @@ mod test {
|
||||
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"muHF98X9KxEzdKrnFAX85KeHv96eXopaip",
|
||||
"n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR",
|
||||
@@ -638,12 +770,14 @@ mod test {
|
||||
// BIP44 public `pkh(key/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip44_public_template() {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDDDzQ31JkZB7VxUr9bjvBivDdqoFLrDPyLWtLapArAi51ftfmCb2DPxwLQzX65iNcXz1DGaVvyvo6JQ6rTU73r2gqdEo8uov9QKRb7nKCSU").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"miNG7dJTzJqNbFS19svRdTCisC65dsubtR",
|
||||
"n2UqaDbCjWSFJvpC84m3FjUk5UaeibCzYg",
|
||||
@@ -654,6 +788,8 @@ mod test {
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H",
|
||||
"ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG",
|
||||
@@ -665,11 +801,13 @@ mod test {
|
||||
// BIP49 `sh(wpkh(key/49'/0'/0'/{0,1}/*))`
|
||||
#[test]
|
||||
fn test_bip49_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV",
|
||||
"2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS",
|
||||
@@ -680,6 +818,8 @@ mod test {
|
||||
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG",
|
||||
"2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p",
|
||||
@@ -691,12 +831,14 @@ mod test {
|
||||
// BIP49 public `sh(wpkh(key/{0,1}/*))`
|
||||
#[test]
|
||||
fn test_bip49_public_template() {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC49r947KGK52X5rBWS4BLs5m9SRY3pYHnvRrm7HcybZ3BfdEsGFyzCMzayi1u58eT82ZeyFZwH7DD6Q83E3fM9CpfMtmnTygnLfP59jL9L").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2N3K4xbVAHoiTQSwxkZjWDfKoNC27pLkYnt",
|
||||
"2NCTQfJ1sZa3wQ3pPseYRHbaNEpC3AquEfX",
|
||||
@@ -707,6 +849,8 @@ mod test {
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ",
|
||||
"2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH",
|
||||
@@ -718,11 +862,13 @@ mod test {
|
||||
// BIP84 `wpkh(key/84'/0'/0'/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip84_template() {
|
||||
let prvkey = bitcoin::util::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s",
|
||||
"bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp",
|
||||
@@ -733,6 +879,8 @@ mod test {
|
||||
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa",
|
||||
"bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45",
|
||||
@@ -744,12 +892,14 @@ mod test {
|
||||
// BIP84 public `wpkh(key/{0,1}/*)`
|
||||
#[test]
|
||||
fn test_bip84_public_template() {
|
||||
let pubkey = bitcoin::util::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::util::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("tpubDC2Qwo2TFsaNC4ju8nrUJ9mqVT3eSgdmy1yPqhgkjwmke3PRXutNGRYAUo6RCHTcVQaDR3ohNU9we59brGHuEKPvH1ags2nevW5opEE9Z5Q").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("c55b303f").unwrap();
|
||||
check(
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qedg9fdlf8cnnqfd5mks6uz5w4kgpk2prcdvd0h",
|
||||
"bcrt1q3lncdlwq3lgcaaeyruynjnlccr0ve0kakh6ana",
|
||||
@@ -760,6 +910,8 @@ mod test {
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2",
|
||||
"bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp",
|
||||
@@ -767,4 +919,67 @@ mod test {
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// BIP86 `tr(key/86'/0'/0'/{0,1}/*)`
|
||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
#[test]
|
||||
fn test_bip86_template() {
|
||||
let prvkey = bitcoin::bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3GJpoapnV8SFfukcVBSfeCficPSGfubmSFDxo1kuHnLisriDvSnRRuL2Qrg5ggqHKNVpxR86QEC8w35uxmGoggxtQTPvfUu").unwrap();
|
||||
check(
|
||||
Bip86(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Network::Bitcoin,
|
||||
&[
|
||||
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
|
||||
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
|
||||
"bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip86(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Network::Bitcoin,
|
||||
&[
|
||||
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
|
||||
"bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
|
||||
"bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// BIP86 public `tr(key/{0,1}/*)`
|
||||
// Used addresses in test vector in https://github.com/bitcoin/bips/blob/master/bip-0086.mediawiki
|
||||
#[test]
|
||||
fn test_bip86_public_template() {
|
||||
let pubkey = bitcoin::bip32::ExtendedPubKey::from_str("xpub6BgBgsespWvERF3LHQu6CnqdvfEvtMcQjYrcRzx53QJjSxarj2afYWcLteoGVky7D3UKDP9QyrLprQ3VCECoY49yfdDEHGCtMMj92pReUsQ").unwrap();
|
||||
let fingerprint = bitcoin::bip32::Fingerprint::from_str("73c5da0a").unwrap();
|
||||
check(
|
||||
Bip86Public(pubkey, fingerprint, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Network::Bitcoin,
|
||||
&[
|
||||
"bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr",
|
||||
"bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh",
|
||||
"bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8",
|
||||
],
|
||||
);
|
||||
check(
|
||||
Bip86Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
true,
|
||||
false,
|
||||
Network::Bitcoin,
|
||||
&[
|
||||
"bc1p3qkhfews2uk44qtvauqyr2ttdsw7svhkl9nkm9s9c3x4ax5h60wqwruhk7",
|
||||
"bc1ptdg60grjk9t3qqcqczp4tlyy3z47yrx9nhlrjsmw36q5a72lhdrs9f00nj",
|
||||
"bc1pgcwgsu8naxp7xlp5p7ufzs7emtfza2las7r2e7krzjhe5qj5xz2q88kmk5",
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -14,7 +14,8 @@
|
||||
// TODO: maybe write our own implementation of bip39? Seems stupid to have an extra dependency for
|
||||
// something that should be fairly simple to re-implement.
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
use alloc::string::String;
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::Network;
|
||||
|
||||
use miniscript::ScriptContext;
|
||||
@@ -141,7 +142,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
|
||||
(word_count, language): Self::Options,
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
let entropy = &entropy.as_ref()[..(word_count as usize / 8)];
|
||||
let entropy = &entropy[..(word_count as usize / 8)];
|
||||
let mnemonic = Mnemonic::from_entropy_in(language, entropy)?;
|
||||
|
||||
Ok(GeneratedKey::new(mnemonic, any_network()))
|
||||
@@ -150,9 +151,10 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for Mnemonic {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
use alloc::string::ToString;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::bip32;
|
||||
|
||||
use bip39::{Language, Mnemonic};
|
||||
|
||||
@@ -11,21 +11,24 @@
|
||||
|
||||
//! Key formats
|
||||
|
||||
use std::any::TypeId;
|
||||
use std::collections::HashSet;
|
||||
use std::marker::PhantomData;
|
||||
use std::ops::Deref;
|
||||
use std::str::FromStr;
|
||||
use crate::collections::HashSet;
|
||||
use alloc::string::{String, ToString};
|
||||
use alloc::vec::Vec;
|
||||
use core::any::TypeId;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
use core::ops::Deref;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bitcoin::secp256k1::{self, Secp256k1, Signing};
|
||||
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::{Network, PrivateKey, PublicKey, XOnlyPublicKey};
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::{key::XOnlyPublicKey, Network, PrivateKey, PublicKey};
|
||||
|
||||
use miniscript::descriptor::{Descriptor, DescriptorXKey, Wildcard};
|
||||
pub use miniscript::descriptor::{
|
||||
DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorSinglePub, KeyMap,
|
||||
SinglePubKey, SortedMultiVec,
|
||||
DescriptorPublicKey, DescriptorSecretKey, KeyMap, SinglePriv, SinglePub, SinglePubKey,
|
||||
SortedMultiVec,
|
||||
};
|
||||
pub use miniscript::ScriptContext;
|
||||
use miniscript::{Miniscript, Terminal};
|
||||
@@ -40,7 +43,7 @@ pub mod bip39;
|
||||
/// Set of valid networks for a key
|
||||
pub type ValidNetworks = HashSet<Network>;
|
||||
|
||||
/// Create a set containing mainnet, testnet and regtest
|
||||
/// Create a set containing mainnet, testnet, signet, and regtest
|
||||
pub fn any_network() -> ValidNetworks {
|
||||
vec![
|
||||
Network::Bitcoin,
|
||||
@@ -95,7 +98,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
}
|
||||
|
||||
// This method is used internally by `bdk::fragment!` and `bdk::descriptor!`. It has to be
|
||||
// public because it is effectively called by external crates, once the macros are expanded,
|
||||
// public because it is effectively called by external crates once the macros are expanded,
|
||||
// but since it is not meant to be part of the public api we hide it from the docs.
|
||||
#[doc(hidden)]
|
||||
pub fn extract(
|
||||
@@ -110,7 +113,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
let mut key_map = KeyMap::with_capacity(1);
|
||||
|
||||
let public = secret
|
||||
.as_public(secp)
|
||||
.to_public(secp)
|
||||
.map_err(|e| miniscript::Error::Unexpected(e.to_string()))?;
|
||||
key_map.insert(public.clone(), secret);
|
||||
|
||||
@@ -224,8 +227,8 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
///
|
||||
/// use bdk::keys::{
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, DescriptorSinglePub,
|
||||
/// IntoDescriptorKey, KeyError, ScriptContext, SinglePubKey,
|
||||
/// mainnet_network, DescriptorKey, DescriptorPublicKey, IntoDescriptorKey, KeyError,
|
||||
/// ScriptContext, SinglePub, SinglePubKey,
|
||||
/// };
|
||||
///
|
||||
/// pub struct MyKeyType {
|
||||
@@ -235,7 +238,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
/// impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for MyKeyType {
|
||||
/// fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
/// Ok(DescriptorKey::from_public(
|
||||
/// DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
/// DescriptorPublicKey::Single(SinglePub {
|
||||
/// origin: None,
|
||||
/// key: SinglePubKey::FullKey(self.pubkey),
|
||||
/// }),
|
||||
@@ -277,7 +280,7 @@ impl<Ctx: ScriptContext + 'static> ExtScriptContext for Ctx {
|
||||
///
|
||||
/// ```compile_fail
|
||||
/// use bdk::bitcoin::PublicKey;
|
||||
/// use std::str::FromStr;
|
||||
/// use core::str::FromStr;
|
||||
///
|
||||
/// use bdk::keys::{DescriptorKey, IntoDescriptorKey, KeyError};
|
||||
///
|
||||
@@ -375,7 +378,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// `(DerivableKey, KeySource, DerivationPath)` tuples.
|
||||
///
|
||||
/// For key types that don't encode any indication about the path to use (like bip39), it's
|
||||
/// generally recommended to implemented this trait instead of [`IntoDescriptorKey`]. The same
|
||||
/// generally recommended to implement this trait instead of [`IntoDescriptorKey`]. The same
|
||||
/// rules regarding script context and valid networks apply.
|
||||
///
|
||||
/// ## Examples
|
||||
@@ -385,12 +388,12 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::util::bip32;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{DerivableKey, ExtendedKey, KeyError, ScriptContext};
|
||||
///
|
||||
/// struct MyCustomKeyType {
|
||||
/// key_data: bitcoin::PrivateKey,
|
||||
/// chain_code: Vec<u8>,
|
||||
/// chain_code: [u8; 32],
|
||||
/// network: bitcoin::Network,
|
||||
/// }
|
||||
///
|
||||
@@ -401,7 +404,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// depth: 0,
|
||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||
/// private_key: self.key_data.inner,
|
||||
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||
/// chain_code: bip32::ChainCode::from(&self.chain_code),
|
||||
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||
/// };
|
||||
///
|
||||
@@ -410,20 +413,20 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// }
|
||||
/// ```
|
||||
///
|
||||
/// Types that don't internally encode the [`Network`](bitcoin::Network) in which they are valid need some extra
|
||||
/// Types that don't internally encode the [`Network`] in which they are valid need some extra
|
||||
/// steps to override the set of valid networks, otherwise only the network specified in the
|
||||
/// [`ExtendedPrivKey`] or [`ExtendedPubKey`] will be considered valid.
|
||||
///
|
||||
/// ```
|
||||
/// use bdk::bitcoin;
|
||||
/// use bdk::bitcoin::util::bip32;
|
||||
/// use bdk::bitcoin::bip32;
|
||||
/// use bdk::keys::{
|
||||
/// any_network, DerivableKey, DescriptorKey, ExtendedKey, KeyError, ScriptContext,
|
||||
/// };
|
||||
///
|
||||
/// struct MyCustomKeyType {
|
||||
/// key_data: bitcoin::PrivateKey,
|
||||
/// chain_code: Vec<u8>,
|
||||
/// chain_code: [u8; 32],
|
||||
/// }
|
||||
///
|
||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||
@@ -433,7 +436,7 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// depth: 0,
|
||||
/// parent_fingerprint: bip32::Fingerprint::default(),
|
||||
/// private_key: self.key_data.inner,
|
||||
/// chain_code: bip32::ChainCode::from(self.chain_code.as_ref()),
|
||||
/// chain_code: bip32::ChainCode::from(&self.chain_code),
|
||||
/// child_number: bip32::ChildNumber::Normal { index: 0 },
|
||||
/// };
|
||||
///
|
||||
@@ -460,12 +463,11 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
|
||||
pub trait DerivableKey<Ctx: ScriptContext = miniscript::Legacy>: Sized {
|
||||
/// Consume `self` and turn it into an [`ExtendedKey`]
|
||||
///
|
||||
/// This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
|
||||
/// like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
|
||||
#[cfg_attr(
|
||||
feature = "keys-bip39",
|
||||
doc = r##"
|
||||
This can be used to get direct access to `xprv`s and `xpub`s for types that implement this trait,
|
||||
like [`Mnemonic`](bip39::Mnemonic) when the `keys-bip39` feature is enabled.
|
||||
```rust
|
||||
use bdk::bitcoin::Network;
|
||||
use bdk::keys::{DerivableKey, ExtendedKey};
|
||||
@@ -619,7 +621,7 @@ pub trait GeneratableKey<Ctx: ScriptContext>: Sized {
|
||||
/// Extra options required by the `generate_with_entropy`
|
||||
type Options;
|
||||
/// Returned error in case of failure
|
||||
type Error: std::fmt::Debug;
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// Generate a key given the extra options and the entropy
|
||||
fn generate_with_entropy(
|
||||
@@ -752,7 +754,7 @@ fn expand_multi_keys<Pk: IntoDescriptorKey<Ctx>, Ctx: ScriptContext>(
|
||||
let (key_map, valid_networks) = key_maps_networks.into_iter().fold(
|
||||
(KeyMap::default(), any_network()),
|
||||
|(mut keys_acc, net_acc), (key, net)| {
|
||||
keys_acc.extend(key.into_iter());
|
||||
keys_acc.extend(key);
|
||||
let net_acc = merge_networks(&net_acc, &net);
|
||||
|
||||
(keys_acc, net_acc)
|
||||
@@ -842,7 +844,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorKey<Ctx> {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match self {
|
||||
DescriptorPublicKey::SinglePub(_) => any_network(),
|
||||
DescriptorPublicKey::Single(_) => any_network(),
|
||||
DescriptorPublicKey::XPub(DescriptorXKey { xkey, .. })
|
||||
if xkey.network == Network::Bitcoin =>
|
||||
{
|
||||
@@ -857,7 +859,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorPublicKey {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::FullKey(self),
|
||||
origin: None,
|
||||
})
|
||||
@@ -867,7 +869,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PublicKey {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorPublicKey::SinglePub(DescriptorSinglePub {
|
||||
DescriptorPublicKey::Single(SinglePub {
|
||||
key: SinglePubKey::XOnly(self),
|
||||
origin: None,
|
||||
})
|
||||
@@ -878,7 +880,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for XOnlyPublicKey {
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for DescriptorSecretKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
let networks = match &self {
|
||||
DescriptorSecretKey::SinglePriv(sk) if sk.key.network == Network::Bitcoin => {
|
||||
DescriptorSecretKey::Single(sk) if sk.key.network == Network::Bitcoin => {
|
||||
mainnet_network()
|
||||
}
|
||||
DescriptorSecretKey::XPrv(DescriptorXKey { xkey, .. })
|
||||
@@ -903,7 +905,7 @@ impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for &'_ str {
|
||||
|
||||
impl<Ctx: ScriptContext> IntoDescriptorKey<Ctx> for PrivateKey {
|
||||
fn into_descriptor_key(self) -> Result<DescriptorKey<Ctx>, KeyError> {
|
||||
DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self,
|
||||
origin: None,
|
||||
})
|
||||
@@ -925,25 +927,42 @@ pub enum KeyError {
|
||||
Message(String),
|
||||
|
||||
/// BIP32 error
|
||||
Bip32(bitcoin::util::bip32::Error),
|
||||
Bip32(bitcoin::bip32::Error),
|
||||
/// Miniscript error
|
||||
Miniscript(miniscript::Error),
|
||||
}
|
||||
|
||||
impl_error!(miniscript::Error, Miniscript, KeyError);
|
||||
impl_error!(bitcoin::util::bip32::Error, Bip32, KeyError);
|
||||
|
||||
impl std::fmt::Display for KeyError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
impl From<miniscript::Error> for KeyError {
|
||||
fn from(err: miniscript::Error) -> Self {
|
||||
KeyError::Miniscript(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<bip32::Error> for KeyError {
|
||||
fn from(err: bip32::Error) -> Self {
|
||||
KeyError::Bip32(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for KeyError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidScriptContext => write!(f, "Invalid script context"),
|
||||
Self::InvalidNetwork => write!(f, "Invalid network"),
|
||||
Self::InvalidChecksum => write!(f, "Invalid checksum"),
|
||||
Self::Message(err) => write!(f, "{}", err),
|
||||
Self::Bip32(err) => write!(f, "BIP32 error: {}", err),
|
||||
Self::Miniscript(err) => write!(f, "Miniscript error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for KeyError {}
|
||||
|
||||
#[cfg(test)]
|
||||
pub mod test {
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::bip32;
|
||||
|
||||
use super::*;
|
||||
|
||||
47
crates/bdk/src/lib.rs
Normal file
47
crates/bdk/src/lib.rs
Normal file
@@ -0,0 +1,47 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
// only enables the `doc_cfg` feature when the `docsrs` configuration attribute is defined
|
||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||
#![cfg_attr(
|
||||
docsrs,
|
||||
doc(html_logo_url = "https://github.com/bitcoindevkit/bdk/raw/master/static/bdk.png")
|
||||
)]
|
||||
#![no_std]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[macro_use]
|
||||
extern crate std;
|
||||
|
||||
#[doc(hidden)]
|
||||
#[macro_use]
|
||||
pub extern crate alloc;
|
||||
|
||||
pub extern crate bitcoin;
|
||||
pub extern crate miniscript;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
|
||||
#[cfg(feature = "keys-bip39")]
|
||||
extern crate bip39;
|
||||
|
||||
pub mod descriptor;
|
||||
pub mod keys;
|
||||
pub mod psbt;
|
||||
pub(crate) mod types;
|
||||
pub mod wallet;
|
||||
|
||||
pub use descriptor::template;
|
||||
pub use descriptor::HdKeyPaths;
|
||||
pub use types::*;
|
||||
pub use wallet::signer;
|
||||
pub use wallet::signer::SignOptions;
|
||||
pub use wallet::tx_builder::TxBuilder;
|
||||
pub use wallet::Wallet;
|
||||
|
||||
/// Get the version of BDK at runtime
|
||||
pub fn version() -> &'static str {
|
||||
env!("CARGO_PKG_VERSION", "unknown")
|
||||
}
|
||||
|
||||
pub use bdk_chain as chain;
|
||||
pub(crate) use bdk_chain::collections;
|
||||
71
crates/bdk/src/psbt/mod.rs
Normal file
71
crates/bdk/src/psbt/mod.rs
Normal file
@@ -0,0 +1,71 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Additional functions on the `rust-bitcoin` `PartiallySignedTransaction` structure.
|
||||
|
||||
use crate::FeeRate;
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
// TODO upstream the functions here to `rust-bitcoin`?
|
||||
|
||||
/// Trait to add functions to extract utxos and calculate fees.
|
||||
pub trait PsbtUtils {
|
||||
/// Get the `TxOut` for the specified input index, if it doesn't exist in the PSBT `None` is returned.
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut>;
|
||||
|
||||
/// The total transaction fee amount, sum of input amounts minus sum of output amounts, in sats.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_amount(&self) -> Option<u64>;
|
||||
|
||||
/// The transaction's fee rate. This value will only be accurate if calculated AFTER the
|
||||
/// `PartiallySignedTransaction` is finalized and all witness/signature data is added to the
|
||||
/// transaction.
|
||||
/// If the PSBT is missing a TxOut for an input returns None.
|
||||
fn fee_rate(&self) -> Option<FeeRate>;
|
||||
}
|
||||
|
||||
impl PsbtUtils for Psbt {
|
||||
fn get_utxo_for(&self, input_index: usize) -> Option<TxOut> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let input = self.inputs.get(input_index)?;
|
||||
|
||||
match (&input.witness_utxo, &input.non_witness_utxo) {
|
||||
(Some(_), _) => input.witness_utxo.clone(),
|
||||
(_, Some(_)) => input.non_witness_utxo.as_ref().map(|in_tx| {
|
||||
in_tx.output[tx.input[input_index].previous_output.vout as usize].clone()
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn fee_amount(&self) -> Option<u64> {
|
||||
let tx = &self.unsigned_tx;
|
||||
let utxos: Option<Vec<TxOut>> = (0..tx.input.len()).map(|i| self.get_utxo_for(i)).collect();
|
||||
|
||||
utxos.map(|inputs| {
|
||||
let input_amount: u64 = inputs.iter().map(|i| i.value).sum();
|
||||
let output_amount: u64 = self.unsigned_tx.output.iter().map(|o| o.value).sum();
|
||||
input_amount
|
||||
.checked_sub(output_amount)
|
||||
.expect("input amount must be greater than output amount")
|
||||
})
|
||||
}
|
||||
|
||||
fn fee_rate(&self) -> Option<FeeRate> {
|
||||
let fee_amount = self.fee_amount();
|
||||
fee_amount.map(|fee| {
|
||||
let weight = self.clone().extract_tx().weight();
|
||||
FeeRate::from_wu(fee, weight)
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -9,20 +9,22 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use std::convert::AsRef;
|
||||
use std::ops::Sub;
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
use core::ops::Sub;
|
||||
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Transaction, TxOut};
|
||||
use bitcoin::{hash_types::Txid, util::psbt};
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::{psbt, Weight};
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of keychains
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub enum KeychainKind {
|
||||
/// External
|
||||
/// External keychain, used for deriving recipient addresses.
|
||||
External = 0,
|
||||
/// Internal, usually used for change outputs
|
||||
/// Internal keychain, used for deriving change addresses.
|
||||
Internal = 1,
|
||||
}
|
||||
|
||||
@@ -63,6 +65,16 @@ impl FeeRate {
|
||||
FeeRate(value)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kwu
|
||||
pub fn from_sat_per_kwu(sat_per_kwu: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kwu / 250.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in sats/kvb
|
||||
pub fn from_sat_per_kvb(sat_per_kvb: f32) -> Self {
|
||||
FeeRate::new_checked(sat_per_kvb / 1000.0_f32)
|
||||
}
|
||||
|
||||
/// Create a new instance of [`FeeRate`] given a float fee rate in btc/kvbytes
|
||||
///
|
||||
/// ## Panics
|
||||
@@ -87,8 +99,8 @@ impl FeeRate {
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and weight units (`wu`).
|
||||
pub fn from_wu(fee: u64, wu: usize) -> FeeRate {
|
||||
Self::from_vb(fee, wu.vbytes())
|
||||
pub fn from_wu(fee: u64, wu: Weight) -> FeeRate {
|
||||
Self::from_vb(fee, wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// Calculate fee rate from `fee` and `vbytes`.
|
||||
@@ -98,22 +110,27 @@ impl FeeRate {
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/vbyte
|
||||
pub fn as_sat_vb(&self) -> f32 {
|
||||
pub fn as_sat_per_vb(&self) -> f32 {
|
||||
self.0
|
||||
}
|
||||
|
||||
/// Return the value as satoshi/kwu
|
||||
pub fn sat_per_kwu(&self) -> f32 {
|
||||
self.0 * 250.0_f32
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in weight units.
|
||||
pub fn fee_wu(&self, wu: usize) -> u64 {
|
||||
self.fee_vb(wu.vbytes())
|
||||
pub fn fee_wu(&self, wu: Weight) -> u64 {
|
||||
self.fee_vb(wu.to_vbytes_ceil() as usize)
|
||||
}
|
||||
|
||||
/// Calculate absolute fee in Satoshis using size in virtual bytes.
|
||||
pub fn fee_vb(&self, vbytes: usize) -> u64 {
|
||||
(self.as_sat_vb() * vbytes as f32).ceil() as u64
|
||||
(self.as_sat_per_vb() * vbytes as f32).ceil() as u64
|
||||
}
|
||||
}
|
||||
|
||||
impl std::default::Default for FeeRate {
|
||||
impl Default for FeeRate {
|
||||
fn default() -> Self {
|
||||
FeeRate::default_min_relay_fee()
|
||||
}
|
||||
@@ -144,7 +161,7 @@ impl Vbytes for usize {
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LocalUtxo {
|
||||
pub struct LocalOutput {
|
||||
/// Reference to a transaction output
|
||||
pub outpoint: OutPoint,
|
||||
/// Transaction output
|
||||
@@ -153,10 +170,14 @@ pub struct LocalUtxo {
|
||||
pub keychain: KeychainKind,
|
||||
/// Whether this UTXO is spent or not
|
||||
pub is_spent: bool,
|
||||
/// The derivation index for the script pubkey in the wallet
|
||||
pub derivation_index: u32,
|
||||
/// The confirmation time for transaction containing this utxo
|
||||
pub confirmation_time: ConfirmationTime,
|
||||
}
|
||||
|
||||
/// A [`Utxo`] with its `satisfaction_weight`.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct WeightedUtxo {
|
||||
/// The weight of the witness data and `scriptSig` expressed in [weight units]. This is used to
|
||||
/// properly maintain the feerate when adding this input to a transaction during coin selection.
|
||||
@@ -167,15 +188,17 @@ pub struct WeightedUtxo {
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
Local(LocalUtxo),
|
||||
Local(LocalOutput),
|
||||
/// A UTXO owned by another wallet.
|
||||
Foreign {
|
||||
/// The location of the output.
|
||||
outpoint: OutPoint,
|
||||
/// The nSequence value to set for this input.
|
||||
sequence: Option<Sequence>,
|
||||
/// The information about the input we require to add it to a PSBT.
|
||||
// Box it to stop the type being too big.
|
||||
psbt_input: Box<psbt::Input>,
|
||||
@@ -198,6 +221,7 @@ impl Utxo {
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
..
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
@@ -211,116 +235,16 @@ impl Utxo {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A wallet transaction
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct TransactionDetails {
|
||||
/// Optional transaction
|
||||
pub transaction: Option<Transaction>,
|
||||
/// Transaction id
|
||||
pub txid: Txid,
|
||||
|
||||
/// Received value (sats)
|
||||
/// Sum of owned outputs of this transaction.
|
||||
pub received: u64,
|
||||
/// Sent value (sats)
|
||||
/// Sum of owned inputs of this transaction.
|
||||
pub sent: u64,
|
||||
/// Fee value (sats) if confirmed.
|
||||
/// The availability of the fee depends on the backend. It's never `None` with an Electrum
|
||||
/// Server backend, but it could be `None` with a Bitcoin RPC node without txindex that receive
|
||||
/// funds while offline.
|
||||
pub fee: Option<u64>,
|
||||
/// If the transaction is confirmed, contains height and timestamp of the block containing the
|
||||
/// transaction, unconfirmed transaction contains `None`.
|
||||
pub confirmation_time: Option<BlockTime>,
|
||||
}
|
||||
|
||||
/// Block height and timestamp of a block
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Default)]
|
||||
pub struct BlockTime {
|
||||
/// confirmation block height
|
||||
pub height: u32,
|
||||
/// confirmation block timestamp
|
||||
pub timestamp: u64,
|
||||
}
|
||||
|
||||
/// **DEPRECATED**: Confirmation time of a transaction
|
||||
///
|
||||
/// The structure has been renamed to `BlockTime`
|
||||
#[deprecated(note = "This structure has been renamed to `BlockTime`")]
|
||||
pub type ConfirmationTime = BlockTime;
|
||||
|
||||
impl BlockTime {
|
||||
/// Returns `Some` `BlockTime` if both `height` and `timestamp` are `Some`
|
||||
pub fn new(height: Option<u32>, timestamp: Option<u64>) -> Option<Self> {
|
||||
match (height, timestamp) {
|
||||
(Some(height), Some(timestamp)) => Some(BlockTime { height, timestamp }),
|
||||
_ => None,
|
||||
/// Get the sequence number if an explicit sequence number has to be set for this input.
|
||||
pub fn sequence(&self) -> Option<Sequence> {
|
||||
match self {
|
||||
Utxo::Local(_) => None,
|
||||
Utxo::Foreign { sequence, .. } => *sequence,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance differentiated in various categories
|
||||
#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone, Default)]
|
||||
pub struct Balance {
|
||||
/// All coinbase outputs not yet matured
|
||||
pub immature: u64,
|
||||
/// Unconfirmed UTXOs generated by a wallet tx
|
||||
pub trusted_pending: u64,
|
||||
/// Unconfirmed UTXOs received from an external wallet
|
||||
pub untrusted_pending: u64,
|
||||
/// Confirmed and immediately spendable balance
|
||||
pub confirmed: u64,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
/// Get sum of trusted_pending and confirmed coins
|
||||
pub fn get_spendable(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending
|
||||
}
|
||||
|
||||
/// Get the whole balance visible to the wallet
|
||||
pub fn get_total(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
|
||||
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::ops::Add for Balance {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
immature: self.immature + other.immature,
|
||||
trusted_pending: self.trusted_pending + other.trusted_pending,
|
||||
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
|
||||
confirmed: self.confirmed + other.confirmed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::iter::Sum for Balance {
|
||||
fn sum<I: Iterator<Item = Self>>(iter: I) -> Self {
|
||||
iter.fold(
|
||||
Balance {
|
||||
..Default::default()
|
||||
},
|
||||
|a, b| a + b,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@@ -358,4 +282,35 @@ mod tests {
|
||||
fn test_valid_feerate_pos_zero() {
|
||||
let _ = FeeRate::from_sat_per_vb(0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_btc_per_kvb() {
|
||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_vbyte() {
|
||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_default_min_relay_fee() {
|
||||
let fee = FeeRate::default_min_relay_fee();
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_kvb() {
|
||||
let fee = FeeRate::from_sat_per_kvb(1000.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sat_per_kwu() {
|
||||
let fee = FeeRate::from_sat_per_kwu(250.0);
|
||||
assert!((fee.as_sat_per_vb() - 1.0).abs() < f32::EPSILON);
|
||||
assert_eq!(fee.sat_per_kwu(), 250.0);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
292
crates/bdk/src/wallet/error.rs
Normal file
292
crates/bdk/src/wallet/error.rs
Normal file
@@ -0,0 +1,292 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! Errors that can be thrown by the [`Wallet`](crate::wallet::Wallet)
|
||||
|
||||
use crate::descriptor::policy::PolicyError;
|
||||
use crate::descriptor::DescriptorError;
|
||||
use crate::wallet::coin_selection;
|
||||
use crate::{descriptor, FeeRate, KeychainKind};
|
||||
use alloc::string::String;
|
||||
use bitcoin::{absolute, psbt, OutPoint, Sequence, Txid};
|
||||
use core::fmt;
|
||||
|
||||
/// Errors returned by miniscript when updating inconsistent PSBTs
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum MiniscriptPsbtError {
|
||||
/// Descriptor key conversion error
|
||||
Conversion(miniscript::descriptor::ConversionError),
|
||||
/// Return error type for PsbtExt::update_input_with_descriptor
|
||||
UtxoUpdate(miniscript::psbt::UtxoUpdateError),
|
||||
/// Return error type for PsbtExt::update_output_with_descriptor
|
||||
OutputUpdate(miniscript::psbt::OutputUpdateError),
|
||||
}
|
||||
|
||||
impl fmt::Display for MiniscriptPsbtError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Conversion(err) => write!(f, "Conversion error: {}", err),
|
||||
Self::UtxoUpdate(err) => write!(f, "UTXO update error: {}", err),
|
||||
Self::OutputUpdate(err) => write!(f, "Output update error: {}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for MiniscriptPsbtError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::finish`]
|
||||
///
|
||||
/// [`TxBuilder::finish`]: crate::wallet::tx_builder::TxBuilder::finish
|
||||
pub enum CreateTxError<P> {
|
||||
/// There was a problem with the descriptors passed in
|
||||
Descriptor(DescriptorError),
|
||||
/// We were unable to write wallet data to the persistence backend
|
||||
Persist(P),
|
||||
/// There was a problem while extracting and manipulating policies
|
||||
Policy(PolicyError),
|
||||
/// Spending policy is not compatible with this [`KeychainKind`]
|
||||
SpendingPolicyRequired(KeychainKind),
|
||||
/// Requested invalid transaction version '0'
|
||||
Version0,
|
||||
/// Requested transaction version `1`, but at least `2` is needed to use OP_CSV
|
||||
Version1Csv,
|
||||
/// Requested `LockTime` is less than is required to spend from this script
|
||||
LockTime {
|
||||
/// Requested `LockTime`
|
||||
requested: absolute::LockTime,
|
||||
/// Required `LockTime`
|
||||
required: absolute::LockTime,
|
||||
},
|
||||
/// Cannot enable RBF with a `Sequence` >= 0xFFFFFFFE
|
||||
RbfSequence,
|
||||
/// Cannot enable RBF with `Sequence` given a required OP_CSV
|
||||
RbfSequenceCsv {
|
||||
/// Given RBF `Sequence`
|
||||
rbf: Sequence,
|
||||
/// Required OP_CSV `Sequence`
|
||||
csv: Sequence,
|
||||
},
|
||||
/// When bumping a tx the absolute fee requested is lower than replaced tx absolute fee
|
||||
FeeTooLow {
|
||||
/// Required fee absolute value (satoshi)
|
||||
required: u64,
|
||||
},
|
||||
/// When bumping a tx the fee rate requested is lower than required
|
||||
FeeRateTooLow {
|
||||
/// Required fee rate (satoshi/vbyte)
|
||||
required: FeeRate,
|
||||
},
|
||||
/// `manually_selected_only` option is selected but no utxo has been passed
|
||||
NoUtxosSelected,
|
||||
/// Output created is under the dust limit, 546 satoshis
|
||||
OutputBelowDustLimit(usize),
|
||||
/// The `change_policy` was set but the wallet does not have a change_descriptor
|
||||
ChangePolicyDescriptor,
|
||||
/// There was an error with coin selection
|
||||
CoinSelection(coin_selection::Error),
|
||||
/// Wallet's UTXO set is not enough to cover recipient's requested plus fee
|
||||
InsufficientFunds {
|
||||
/// Sats needed for some transaction
|
||||
needed: u64,
|
||||
/// Sats available for spending
|
||||
available: u64,
|
||||
},
|
||||
/// Cannot build a tx without recipients
|
||||
NoRecipients,
|
||||
/// Partially signed bitcoin transaction error
|
||||
Psbt(psbt::Error),
|
||||
/// In order to use the [`TxBuilder::add_global_xpubs`] option every extended
|
||||
/// key in the descriptor must either be a master key itself (having depth = 0) or have an
|
||||
/// explicit origin provided
|
||||
///
|
||||
/// [`TxBuilder::add_global_xpubs`]: crate::wallet::tx_builder::TxBuilder::add_global_xpubs
|
||||
MissingKeyOrigin(String),
|
||||
/// Happens when trying to spend an UTXO that is not in the internal database
|
||||
UnknownUtxo,
|
||||
/// Missing non_witness_utxo on foreign utxo for given `OutPoint`
|
||||
MissingNonWitnessUtxo(OutPoint),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
}
|
||||
|
||||
impl<P> fmt::Display for CreateTxError<P>
|
||||
where
|
||||
P: fmt::Display,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::Descriptor(e) => e.fmt(f),
|
||||
Self::Persist(e) => {
|
||||
write!(
|
||||
f,
|
||||
"failed to write wallet data to persistence backend: {}",
|
||||
e
|
||||
)
|
||||
}
|
||||
Self::Policy(e) => e.fmt(f),
|
||||
CreateTxError::SpendingPolicyRequired(keychain_kind) => {
|
||||
write!(f, "Spending policy required: {:?}", keychain_kind)
|
||||
}
|
||||
CreateTxError::Version0 => {
|
||||
write!(f, "Invalid version `0`")
|
||||
}
|
||||
CreateTxError::Version1Csv => {
|
||||
write!(
|
||||
f,
|
||||
"TxBuilder requested version `1`, but at least `2` is needed to use OP_CSV"
|
||||
)
|
||||
}
|
||||
CreateTxError::LockTime {
|
||||
requested,
|
||||
required,
|
||||
} => {
|
||||
write!(f, "TxBuilder requested timelock of `{:?}`, but at least `{:?}` is required to spend from this script", required, requested)
|
||||
}
|
||||
CreateTxError::RbfSequence => {
|
||||
write!(f, "Cannot enable RBF with a nSequence >= 0xFFFFFFFE")
|
||||
}
|
||||
CreateTxError::RbfSequenceCsv { rbf, csv } => {
|
||||
write!(
|
||||
f,
|
||||
"Cannot enable RBF with nSequence `{:?}` given a required OP_CSV of `{:?}`",
|
||||
rbf, csv
|
||||
)
|
||||
}
|
||||
CreateTxError::FeeTooLow { required } => {
|
||||
write!(f, "Fee to low: required {} sat", required)
|
||||
}
|
||||
CreateTxError::FeeRateTooLow { required } => {
|
||||
write!(
|
||||
f,
|
||||
"Fee rate too low: required {} sat/vbyte",
|
||||
required.as_sat_per_vb()
|
||||
)
|
||||
}
|
||||
CreateTxError::NoUtxosSelected => {
|
||||
write!(f, "No UTXO selected")
|
||||
}
|
||||
CreateTxError::OutputBelowDustLimit(limit) => {
|
||||
write!(f, "Output below the dust limit: {}", limit)
|
||||
}
|
||||
CreateTxError::ChangePolicyDescriptor => {
|
||||
write!(
|
||||
f,
|
||||
"The `change_policy` can be set only if the wallet has a change_descriptor"
|
||||
)
|
||||
}
|
||||
CreateTxError::CoinSelection(e) => e.fmt(f),
|
||||
CreateTxError::InsufficientFunds { needed, available } => {
|
||||
write!(
|
||||
f,
|
||||
"Insufficient funds: {} sat available of {} sat needed",
|
||||
available, needed
|
||||
)
|
||||
}
|
||||
CreateTxError::NoRecipients => {
|
||||
write!(f, "Cannot build tx without recipients")
|
||||
}
|
||||
CreateTxError::Psbt(e) => e.fmt(f),
|
||||
CreateTxError::MissingKeyOrigin(err) => {
|
||||
write!(f, "Missing key origin: {}", err)
|
||||
}
|
||||
CreateTxError::UnknownUtxo => {
|
||||
write!(f, "UTXO not found in the internal database")
|
||||
}
|
||||
CreateTxError::MissingNonWitnessUtxo(outpoint) => {
|
||||
write!(f, "Missing non_witness_utxo on foreign utxo {}", outpoint)
|
||||
}
|
||||
CreateTxError::MiniscriptPsbt(err) => {
|
||||
write!(f, "Miniscript PSBT error: {}", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<descriptor::error::Error> for CreateTxError<P> {
|
||||
fn from(err: descriptor::error::Error) -> Self {
|
||||
CreateTxError::Descriptor(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<PolicyError> for CreateTxError<P> {
|
||||
fn from(err: PolicyError) -> Self {
|
||||
CreateTxError::Policy(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<MiniscriptPsbtError> for CreateTxError<P> {
|
||||
fn from(err: MiniscriptPsbtError) -> Self {
|
||||
CreateTxError::MiniscriptPsbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<psbt::Error> for CreateTxError<P> {
|
||||
fn from(err: psbt::Error) -> Self {
|
||||
CreateTxError::Psbt(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl<P> From<coin_selection::Error> for CreateTxError<P> {
|
||||
fn from(err: coin_selection::Error) -> Self {
|
||||
CreateTxError::CoinSelection(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl<P: core::fmt::Display + core::fmt::Debug> std::error::Error for CreateTxError<P> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`Wallet::build_fee_bump`]
|
||||
///
|
||||
/// [`Wallet::build_fee_bump`]: super::Wallet::build_fee_bump
|
||||
pub enum BuildFeeBumpError {
|
||||
/// Happens when trying to spend an UTXO that is not in the internal database
|
||||
UnknownUtxo(OutPoint),
|
||||
/// Thrown when a tx is not found in the internal database
|
||||
TransactionNotFound(Txid),
|
||||
/// Happens when trying to bump a transaction that is already confirmed
|
||||
TransactionConfirmed(Txid),
|
||||
/// Trying to replace a tx that has a sequence >= `0xFFFFFFFE`
|
||||
IrreplaceableTransaction(Txid),
|
||||
/// Node doesn't have data to estimate a fee rate
|
||||
FeeRateUnavailable,
|
||||
}
|
||||
|
||||
impl fmt::Display for BuildFeeBumpError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnknownUtxo(outpoint) => write!(
|
||||
f,
|
||||
"UTXO not found in the internal database with txid: {}, vout: {}",
|
||||
outpoint.txid, outpoint.vout
|
||||
),
|
||||
Self::TransactionNotFound(txid) => {
|
||||
write!(
|
||||
f,
|
||||
"Transaction not found in the internal database with txid: {}",
|
||||
txid
|
||||
)
|
||||
}
|
||||
Self::TransactionConfirmed(txid) => {
|
||||
write!(f, "Transaction already confirmed with txid: {}", txid)
|
||||
}
|
||||
Self::IrreplaceableTransaction(txid) => {
|
||||
write!(f, "Transaction can't be replaced with txid: {}", txid)
|
||||
}
|
||||
Self::FeeRateUnavailable => write!(f, "Fee rate unavailable"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for BuildFeeBumpError {}
|
||||
@@ -20,7 +20,6 @@
|
||||
//! ```
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! let import = r#"{
|
||||
@@ -30,43 +29,38 @@
|
||||
//! }"#;
|
||||
//!
|
||||
//! let import = FullyNodedExport::from_str(import)?;
|
||||
//! let wallet = Wallet::new(
|
||||
//! let wallet = Wallet::new_no_persist(
|
||||
//! &import.descriptor(),
|
||||
//! import.change_descriptor().as_ref(),
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default(),
|
||||
//! )?;
|
||||
//! # Ok::<_, bdk::Error>(())
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
//!
|
||||
//! ### Export a `Wallet`
|
||||
//! ```
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::wallet::export::*;
|
||||
//! # use bdk::*;
|
||||
//! let wallet = Wallet::new(
|
||||
//! let wallet = Wallet::new_no_persist(
|
||||
//! "wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/0/*)",
|
||||
//! Some("wpkh([c258d2e4/84h/1h/0h]tpubDD3ynpHgJQW8VvWRzQ5WFDCrs4jqVFGHB3vLC3r49XHJSqP8bHKdK4AriuUKLccK68zfzowx7YhmDN8SiSkgCDENUFx9qVw65YyqM78vyVe/1/*)"),
|
||||
//! Network::Testnet,
|
||||
//! MemoryDatabase::default()
|
||||
//! )?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true)
|
||||
//! .map_err(ToString::to_string)
|
||||
//! .map_err(bdk::Error::Generic)?;
|
||||
//! let export = FullyNodedExport::export_wallet(&wallet, "exported wallet", true).unwrap();
|
||||
//!
|
||||
//! println!("Exported: {}", export.to_string());
|
||||
//! # Ok::<_, bdk::Error>(())
|
||||
//! # Ok::<_, Box<dyn std::error::Error>>(())
|
||||
//! ```
|
||||
|
||||
use std::str::FromStr;
|
||||
use core::str::FromStr;
|
||||
|
||||
use alloc::string::{String, ToString};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use miniscript::descriptor::{ShInner, WshInner};
|
||||
use miniscript::{Descriptor, ScriptContext, Terminal};
|
||||
|
||||
use crate::database::BatchDatabase;
|
||||
use crate::types::KeychainKind;
|
||||
use crate::wallet::Wallet;
|
||||
|
||||
@@ -116,7 +110,7 @@ impl FullyNodedExport {
|
||||
///
|
||||
/// If the database is empty or `include_blockheight` is false, the `blockheight` field
|
||||
/// returned will be `0`.
|
||||
pub fn export_wallet<D: BatchDatabase>(
|
||||
pub fn export_wallet<D>(
|
||||
wallet: &Wallet<D>,
|
||||
label: &str,
|
||||
include_blockheight: bool,
|
||||
@@ -131,18 +125,15 @@ impl FullyNodedExport {
|
||||
let descriptor = remove_checksum(descriptor);
|
||||
Self::is_compatible_with_core(&descriptor)?;
|
||||
|
||||
let blockheight = match wallet.database.borrow().iter_txs(false) {
|
||||
_ if !include_blockheight => 0,
|
||||
Err(_) => 0,
|
||||
Ok(txs) => {
|
||||
let mut heights = txs
|
||||
.into_iter()
|
||||
.map(|tx| tx.confirmation_time.map(|c| c.height).unwrap_or(0))
|
||||
.collect::<Vec<_>>();
|
||||
heights.sort_unstable();
|
||||
|
||||
*heights.last().unwrap_or(&0)
|
||||
}
|
||||
let blockheight = if include_blockheight {
|
||||
wallet.transactions().next().map_or(0, |canonical_tx| {
|
||||
match canonical_tx.chain_position {
|
||||
bdk_chain::ChainPosition::Confirmed(a) => a.confirmation_height,
|
||||
bdk_chain::ChainPosition::Unconfirmed(_) => 0,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
let export = FullyNodedExport {
|
||||
@@ -151,11 +142,7 @@ impl FullyNodedExport {
|
||||
blockheight,
|
||||
};
|
||||
|
||||
let change_descriptor = match wallet
|
||||
.public_descriptor(KeychainKind::Internal)
|
||||
.map_err(|_| "Invalid change descriptor")?
|
||||
.is_some()
|
||||
{
|
||||
let change_descriptor = match wallet.public_descriptor(KeychainKind::Internal).is_some() {
|
||||
false => None,
|
||||
true => {
|
||||
let descriptor = wallet
|
||||
@@ -225,36 +212,43 @@ impl FullyNodedExport {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bitcoin::{Network, Txid};
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{BlockHash, Network, Transaction};
|
||||
|
||||
use super::*;
|
||||
use crate::database::{memory::MemoryDatabase, BatchOperations};
|
||||
use crate::types::TransactionDetails;
|
||||
use crate::wallet::Wallet;
|
||||
use crate::BlockTime;
|
||||
|
||||
fn get_test_db() -> MemoryDatabase {
|
||||
let mut db = MemoryDatabase::new();
|
||||
db.set_tx(&TransactionDetails {
|
||||
transaction: None,
|
||||
txid: Txid::from_str(
|
||||
"4ddff1fa33af17f377f62b72357b43107c19110a8009b36fb832af505efed98a",
|
||||
fn get_test_wallet(
|
||||
descriptor: &str,
|
||||
change_descriptor: Option<&str>,
|
||||
network: Network,
|
||||
) -> Wallet<()> {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change_descriptor, network).unwrap();
|
||||
let transaction = Transaction {
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
version: 0,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
};
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 5001,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_tx(
|
||||
transaction,
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 5000,
|
||||
time: 0,
|
||||
},
|
||||
)
|
||||
.unwrap(),
|
||||
|
||||
received: 100_000,
|
||||
sent: 0,
|
||||
fee: Some(500),
|
||||
confirmation_time: Some(BlockTime {
|
||||
timestamp: 12345678,
|
||||
height: 5000,
|
||||
}),
|
||||
})
|
||||
.unwrap();
|
||||
|
||||
db
|
||||
.unwrap();
|
||||
wallet
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -262,13 +256,7 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
@@ -286,7 +274,7 @@ mod test {
|
||||
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
|
||||
let wallet = Wallet::new(descriptor, None, Network::Bitcoin, get_test_db()).unwrap();
|
||||
let wallet = get_test_wallet(descriptor, None, Network::Bitcoin);
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
@@ -299,13 +287,7 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/50'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
}
|
||||
|
||||
@@ -322,13 +304,7 @@ mod test {
|
||||
[c98b1535/48'/0'/0'/2']tpubDCDi5W4sP6zSnzJeowy8rQDVhBdRARaPhK1axABi8V1661wEPeanpEXj4ZLAUEoikVtoWcyK26TKKJSecSfeKxwHCcRrge9k1ybuiL71z4a/1/*\
|
||||
))";
|
||||
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Testnet,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Testnet);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.descriptor(), descriptor);
|
||||
@@ -342,13 +318,7 @@ mod test {
|
||||
let descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/0/*)";
|
||||
let change_descriptor = "wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44'/0'/0'/1/*)";
|
||||
|
||||
let wallet = Wallet::new(
|
||||
descriptor,
|
||||
Some(change_descriptor),
|
||||
Network::Bitcoin,
|
||||
get_test_db(),
|
||||
)
|
||||
.unwrap();
|
||||
let wallet = get_test_wallet(descriptor, Some(change_descriptor), Network::Bitcoin);
|
||||
let export = FullyNodedExport::export_wallet(&wallet, "Test Label", true).unwrap();
|
||||
|
||||
assert_eq!(export.to_string(), "{\"descriptor\":\"wpkh(xprv9s21ZrQH143K4CTb63EaMxja1YiTnSEWKMbn23uoEnAzxjdUJRQkazCAtzxGm4LSoTSVTptoV9RbchnKPW9HxKtZumdyxyikZFDLhogJ5Uj/44\'/0\'/0\'/0/*)\",\"blockheight\":5000,\"label\":\"Test Label\"}");
|
||||
98
crates/bdk/src/wallet/hardwaresigner.rs
Normal file
98
crates/bdk/src/wallet/hardwaresigner.rs
Normal file
@@ -0,0 +1,98 @@
|
||||
// Bitcoin Dev Kit
|
||||
// Written in 2020 by Alekos Filini <alekos.filini@gmail.com>
|
||||
//
|
||||
// Copyright (c) 2020-2021 Bitcoin Dev Kit Developers
|
||||
//
|
||||
// This file is licensed under the Apache License, Version 2.0 <LICENSE-APACHE
|
||||
// or http://www.apache.org/licenses/LICENSE-2.0> or the MIT license
|
||||
// <LICENSE-MIT or http://opensource.org/licenses/MIT>, at your option.
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
//! HWI Signer
|
||||
//!
|
||||
//! This module contains HWISigner, an implementation of a [TransactionSigner] to be
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut devices = HWIClient::enumerate()?;
|
||||
//! if devices.is_empty() {
|
||||
//! panic!("No devices found!");
|
||||
//! }
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new_no_persist(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
//! # )?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(custom_signer),
|
||||
//! );
|
||||
//!
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use bitcoin::bip32::Fingerprint;
|
||||
use bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
use hwi::HWIClient;
|
||||
|
||||
use crate::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Custom signer for Hardware Wallets
|
||||
///
|
||||
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
|
||||
pub struct HWISigner {
|
||||
fingerprint: Fingerprint,
|
||||
client: HWIClient,
|
||||
}
|
||||
|
||||
impl HWISigner {
|
||||
/// Create a instance from the specified device and chain
|
||||
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
|
||||
let client = HWIClient::get_client(device, false, chain)?;
|
||||
Ok(HWISigner {
|
||||
fingerprint: device.fingerprint,
|
||||
client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for HWISigner {
|
||||
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||
SignerId::Fingerprint(self.fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
/// This implementation ignores `sign_options`
|
||||
impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut PartiallySignedTransaction,
|
||||
_sign_options: &crate::SignOptions,
|
||||
_secp: &crate::wallet::utils::SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
psbt.combine(self.client.sign_tx(psbt)?.psbt)
|
||||
.expect("Failed to combine HW signed psbt with passed PSBT");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
2604
crates/bdk/src/wallet/mod.rs
Normal file
2604
crates/bdk/src/wallet/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,13 +15,12 @@
|
||||
//! through the [`Wallet::add_signer`](super::Wallet::add_signer) function.
|
||||
//!
|
||||
//! ```
|
||||
//! # use std::sync::Arc;
|
||||
//! # use std::str::FromStr;
|
||||
//! # use alloc::sync::Arc;
|
||||
//! # use core::str::FromStr;
|
||||
//! # use bitcoin::secp256k1::{Secp256k1, All};
|
||||
//! # use bitcoin::*;
|
||||
//! # use bitcoin::util::psbt;
|
||||
//! # use bitcoin::psbt;
|
||||
//! # use bdk::signer::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::*;
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct CustomHSM;
|
||||
@@ -70,39 +69,42 @@
|
||||
//! let custom_signer = CustomSigner::connect();
|
||||
//!
|
||||
//! let descriptor = "wpkh(tpubD6NzVbkrYhZ4Xferm7Pz4VnjdcDPFyjVu5K4iZXQ4pVN8Cks4pHVowTBXBKRhX64pkRyJZJN5xAKj4UDNnLPb5p2sSKXhewoYx5GbTdUFWq/*)";
|
||||
//! let mut wallet = Wallet::new(descriptor, None, Network::Testnet, MemoryDatabase::default())?;
|
||||
//! let mut wallet = Wallet::new_no_persist(descriptor, None, Network::Testnet)?;
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(custom_signer)
|
||||
//! );
|
||||
//!
|
||||
//! # Ok::<_, bdk::Error>(())
|
||||
//! # Ok::<_, anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::cmp::Ordering;
|
||||
use std::collections::BTreeMap;
|
||||
use std::fmt;
|
||||
use std::ops::{Bound::Included, Deref};
|
||||
use std::sync::Arc;
|
||||
use crate::collections::BTreeMap;
|
||||
use alloc::string::String;
|
||||
use alloc::sync::Arc;
|
||||
use alloc::vec::Vec;
|
||||
use core::cmp::Ordering;
|
||||
use core::fmt;
|
||||
use core::ops::{Bound::Included, Deref};
|
||||
|
||||
use bitcoin::blockdata::opcodes;
|
||||
use bitcoin::blockdata::script::Builder as ScriptBuilder;
|
||||
use bitcoin::hashes::{hash160, Hash};
|
||||
use bitcoin::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||
use bitcoin::hashes::hash160;
|
||||
use bitcoin::secp256k1::Message;
|
||||
use bitcoin::util::bip32::{ChildNumber, DerivationPath, ExtendedPrivKey, Fingerprint};
|
||||
use bitcoin::util::{ecdsa, psbt, schnorr, sighash, taproot};
|
||||
use bitcoin::{secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::{EcdsaSighashType, PrivateKey, PublicKey, SchnorrSighashType, Script};
|
||||
use bitcoin::sighash::{EcdsaSighashType, TapSighash, TapSighashType};
|
||||
use bitcoin::{ecdsa, psbt, sighash, taproot};
|
||||
use bitcoin::{key::TapTweak, key::XOnlyPublicKey, secp256k1};
|
||||
use bitcoin::{PrivateKey, PublicKey};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorSinglePriv, DescriptorXKey,
|
||||
KeyMap, SinglePubKey,
|
||||
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
||||
InnerXKey, KeyMap, SinglePriv, SinglePubKey,
|
||||
};
|
||||
use miniscript::{Legacy, MiniscriptKey, Segwitv0, Tap};
|
||||
use miniscript::{Legacy, Segwitv0, SigType, Tap, ToPublicKey};
|
||||
|
||||
use super::utils::SecpCtx;
|
||||
use crate::descriptor::{DescriptorMeta, XKeyUtils};
|
||||
use crate::psbt::PsbtUtils;
|
||||
use crate::wallet::error::MiniscriptPsbtError;
|
||||
|
||||
/// Identifier of a signer in the `SignersContainers`. Used as a key to find the right signer among
|
||||
/// multiple of them
|
||||
@@ -129,7 +131,7 @@ impl From<Fingerprint> for SignerId {
|
||||
}
|
||||
|
||||
/// Signing error
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
#[derive(Debug)]
|
||||
pub enum SignerError {
|
||||
/// The private key is missing for the required public key
|
||||
MissingKey,
|
||||
@@ -159,6 +161,12 @@ pub enum SignerError {
|
||||
InvalidSighash,
|
||||
/// Error while computing the hash to sign
|
||||
SighashError(sighash::Error),
|
||||
/// Miniscript PSBT error
|
||||
MiniscriptPsbt(MiniscriptPsbtError),
|
||||
/// To be used only by external libraries implementing [`InputSigner`] or
|
||||
/// [`TransactionSigner`], so that they can return their own custom errors, without having to
|
||||
/// modify [`SignerError`] in BDK.
|
||||
External(String),
|
||||
}
|
||||
|
||||
impl From<sighash::Error> for SignerError {
|
||||
@@ -169,10 +177,26 @@ impl From<sighash::Error> for SignerError {
|
||||
|
||||
impl fmt::Display for SignerError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
match self {
|
||||
Self::MissingKey => write!(f, "Missing private key"),
|
||||
Self::InvalidKey => write!(f, "The private key in use has the right fingerprint but derives differently than expected"),
|
||||
Self::UserCanceled => write!(f, "The user canceled the operation"),
|
||||
Self::InputIndexOutOfRange => write!(f, "Input index out of range"),
|
||||
Self::MissingNonWitnessUtxo => write!(f, "Missing non-witness UTXO"),
|
||||
Self::InvalidNonWitnessUtxo => write!(f, "Invalid non-witness UTXO"),
|
||||
Self::MissingWitnessUtxo => write!(f, "Missing witness UTXO"),
|
||||
Self::MissingWitnessScript => write!(f, "Missing witness script"),
|
||||
Self::MissingHdKeypath => write!(f, "Missing fingerprint and derivation path"),
|
||||
Self::NonStandardSighash => write!(f, "The psbt contains a non standard sighash"),
|
||||
Self::InvalidSighash => write!(f, "Invalid SIGHASH for the signing context in use"),
|
||||
Self::SighashError(err) => write!(f, "Error while computing the hash to sign: {}", err),
|
||||
Self::MiniscriptPsbt(err) => write!(f, "Miniscript PSBT error: {}", err),
|
||||
Self::External(err) => write!(f, "{}", err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for SignerError {}
|
||||
|
||||
/// Signing context
|
||||
@@ -191,7 +215,7 @@ pub enum SignerContext {
|
||||
},
|
||||
}
|
||||
|
||||
/// Wrapper structure to pair a signer with its context
|
||||
/// Wrapper to pair a signer with its context
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct SignerWrapper<S: Sized + fmt::Debug + Clone> {
|
||||
signer: S,
|
||||
@@ -356,13 +380,55 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
fn multikey_to_xkeys<K: InnerXKey + Clone>(
|
||||
multikey: DescriptorMultiXKey<K>,
|
||||
) -> Vec<DescriptorXKey<K>> {
|
||||
multikey
|
||||
.derivation_paths
|
||||
.into_paths()
|
||||
.into_iter()
|
||||
.map(|derivation_path| DescriptorXKey {
|
||||
origin: multikey.origin.clone(),
|
||||
xkey: multikey.xkey.clone(),
|
||||
derivation_path,
|
||||
wildcard: multikey.wildcard,
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.public_key(secp).to_pubkeyhash())
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
Some(DescriptorSecretKey::SinglePriv(DescriptorSinglePriv {
|
||||
Some(DescriptorSecretKey::MultiXPrv(self.signer.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<DescriptorMultiXKey<ExtendedPrivKey>> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
let xkeys = multikey_to_xkeys(self.signer.clone());
|
||||
for xkey in xkeys {
|
||||
SignerWrapper::new(xkey, self.ctx).sign_input(psbt, input_index, sign_options, secp)?
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.public_key(secp).to_pubkeyhash(SigType::Ecdsa))
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
Some(DescriptorSecretKey::Single(SinglePriv {
|
||||
key: self.signer,
|
||||
origin: None,
|
||||
}))
|
||||
@@ -391,20 +457,23 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
let x_only_pubkey = XOnlyPublicKey::from(pubkey.inner);
|
||||
|
||||
if let SignerContext::Tap { is_internal_key } = self.ctx {
|
||||
if is_internal_key
|
||||
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||
&& sign_options.sign_with_tap_internal_key
|
||||
{
|
||||
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
None,
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
);
|
||||
if let Some(psbt_internal_key) = psbt.inputs[input_index].tap_internal_key {
|
||||
if is_internal_key
|
||||
&& psbt.inputs[input_index].tap_key_sig.is_none()
|
||||
&& sign_options.sign_with_tap_internal_key
|
||||
&& x_only_pubkey == psbt_internal_key
|
||||
{
|
||||
let (hash, hash_ty) = Tap::sighash(psbt, input_index, None)?;
|
||||
sign_psbt_schnorr(
|
||||
&self.inner,
|
||||
x_only_pubkey,
|
||||
None,
|
||||
&mut psbt.inputs[input_index],
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some((leaf_hashes, _)) =
|
||||
@@ -450,8 +519,16 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
}
|
||||
|
||||
let (hash, hash_ty) = match self.ctx {
|
||||
SignerContext::Segwitv0 => Segwitv0::sighash(psbt, input_index, ())?,
|
||||
SignerContext::Legacy => Legacy::sighash(psbt, input_index, ())?,
|
||||
SignerContext::Segwitv0 => {
|
||||
let (h, t) = Segwitv0::sighash(psbt, input_index, ())?;
|
||||
let h = h.to_raw_hash();
|
||||
(h, t)
|
||||
}
|
||||
SignerContext::Legacy => {
|
||||
let (h, t) = Legacy::sighash(psbt, input_index, ())?;
|
||||
let h = h.to_raw_hash();
|
||||
(h, t)
|
||||
}
|
||||
_ => return Ok(()), // handled above
|
||||
};
|
||||
sign_psbt_ecdsa(
|
||||
@@ -461,6 +538,7 @@ impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
hash,
|
||||
hash_ty,
|
||||
secp,
|
||||
sign_options.allow_grinding,
|
||||
);
|
||||
|
||||
Ok(())
|
||||
@@ -471,16 +549,21 @@ fn sign_psbt_ecdsa(
|
||||
secret_key: &secp256k1::SecretKey,
|
||||
pubkey: PublicKey,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: bitcoin::Sighash,
|
||||
hash: impl bitcoin::hashes::Hash + bitcoin::secp256k1::ThirtyTwoByteHash,
|
||||
hash_ty: EcdsaSighashType,
|
||||
secp: &SecpCtx,
|
||||
allow_grinding: bool,
|
||||
) {
|
||||
let sig = secp.sign_ecdsa(
|
||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
||||
secret_key,
|
||||
);
|
||||
let msg = &Message::from(hash);
|
||||
let sig = if allow_grinding {
|
||||
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||
} else {
|
||||
secp.sign_ecdsa(msg, secret_key)
|
||||
};
|
||||
secp.verify_ecdsa(msg, &sig, &pubkey.inner)
|
||||
.expect("invalid or corrupted ecdsa signature");
|
||||
|
||||
let final_signature = ecdsa::EcdsaSig { sig, hash_ty };
|
||||
let final_signature = ecdsa::Signature { sig, hash_ty };
|
||||
psbt_input.partial_sigs.insert(pubkey, final_signature);
|
||||
}
|
||||
|
||||
@@ -490,26 +573,24 @@ fn sign_psbt_schnorr(
|
||||
pubkey: XOnlyPublicKey,
|
||||
leaf_hash: Option<taproot::TapLeafHash>,
|
||||
psbt_input: &mut psbt::Input,
|
||||
hash: taproot::TapSighashHash,
|
||||
hash_ty: SchnorrSighashType,
|
||||
hash: TapSighash,
|
||||
hash_ty: TapSighashType,
|
||||
secp: &SecpCtx,
|
||||
) {
|
||||
use schnorr::TapTweak;
|
||||
|
||||
let keypair = secp256k1::KeyPair::from_seckey_slice(secp, secret_key.as_ref()).unwrap();
|
||||
let keypair = match leaf_hash {
|
||||
None => keypair
|
||||
.tap_tweak(secp, psbt_input.tap_merkle_root)
|
||||
.into_inner(),
|
||||
.to_inner(),
|
||||
Some(_) => keypair, // no tweak for script spend
|
||||
};
|
||||
|
||||
let sig = secp.sign_schnorr(
|
||||
&Message::from_slice(&hash.into_inner()[..]).unwrap(),
|
||||
&keypair,
|
||||
);
|
||||
let msg = &Message::from(hash);
|
||||
let sig = secp.sign_schnorr(msg, &keypair);
|
||||
secp.verify_schnorr(&sig, msg, &XOnlyPublicKey::from_keypair(&keypair).0)
|
||||
.expect("invalid or corrupted schnorr signature");
|
||||
|
||||
let final_signature = schnorr::SchnorrSig { sig, hash_ty };
|
||||
let final_signature = taproot::Signature { sig, hash_ty };
|
||||
|
||||
if let Some(lh) = leaf_hash {
|
||||
psbt_input
|
||||
@@ -528,7 +609,7 @@ fn sign_psbt_schnorr(
|
||||
#[derive(Debug, Clone, PartialOrd, PartialEq, Ord, Eq)]
|
||||
pub struct SignerOrdering(pub usize);
|
||||
|
||||
impl std::default::Default for SignerOrdering {
|
||||
impl Default for SignerOrdering {
|
||||
fn default() -> Self {
|
||||
SignerOrdering(100)
|
||||
}
|
||||
@@ -559,7 +640,7 @@ impl SignersContainer {
|
||||
self.0
|
||||
.values()
|
||||
.filter_map(|signer| signer.descriptor_secret_key())
|
||||
.filter_map(|secret| secret.as_public(secp).ok().map(|public| (public, secret)))
|
||||
.filter_map(|secret| secret.to_public(secp).ok().map(|public| (public, secret)))
|
||||
.collect()
|
||||
}
|
||||
|
||||
@@ -584,8 +665,13 @@ impl SignersContainer {
|
||||
};
|
||||
|
||||
match secret {
|
||||
DescriptorSecretKey::SinglePriv(private_key) => container.add_external(
|
||||
SignerId::from(private_key.key.public_key(secp).to_pubkeyhash()),
|
||||
DescriptorSecretKey::Single(private_key) => container.add_external(
|
||||
SignerId::from(
|
||||
private_key
|
||||
.key
|
||||
.public_key(secp)
|
||||
.to_pubkeyhash(SigType::Ecdsa),
|
||||
),
|
||||
SignerOrdering::default(),
|
||||
Arc::new(SignerWrapper::new(private_key.key, ctx)),
|
||||
),
|
||||
@@ -594,6 +680,11 @@ impl SignersContainer {
|
||||
SignerOrdering::default(),
|
||||
Arc::new(SignerWrapper::new(xprv, ctx)),
|
||||
),
|
||||
DescriptorSecretKey::MultiXPrv(xprv) => container.add_external(
|
||||
SignerId::from(xprv.root_fingerprint(secp)),
|
||||
SignerOrdering::default(),
|
||||
Arc::new(SignerWrapper::new(xprv, ctx)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -661,7 +752,7 @@ pub struct SignOptions {
|
||||
/// Whether the signer should trust the `witness_utxo`, if the `non_witness_utxo` hasn't been
|
||||
/// provided
|
||||
///
|
||||
/// Defaults to `false` to mitigate the "SegWit bug" which chould trick the wallet into
|
||||
/// Defaults to `false` to mitigate the "SegWit bug" which should trick the wallet into
|
||||
/// paying a fee larger than expected.
|
||||
///
|
||||
/// Some wallets, especially if relatively old, might not provide the `non_witness_utxo` for
|
||||
@@ -686,14 +777,14 @@ pub struct SignOptions {
|
||||
/// Defaults to `false` which will only allow signing using `SIGHASH_ALL`.
|
||||
pub allow_all_sighashes: bool,
|
||||
|
||||
/// Whether to remove partial_sigs from psbt inputs while finalizing psbt.
|
||||
/// Whether to remove partial signatures from the PSBT inputs while finalizing PSBT.
|
||||
///
|
||||
/// Defaults to `true` which will remove partial_sigs after finalizing.
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to try finalizing psbt input after the inputs are signed.
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try fianlizing psbt after inputs are signed.
|
||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||
pub try_finalize: bool,
|
||||
|
||||
/// Specifies which Taproot script-spend leaves we should sign for. This option is
|
||||
@@ -707,12 +798,18 @@ pub struct SignOptions {
|
||||
///
|
||||
/// Defaults to `true`, i.e., we always try to sign with the taproot internal key.
|
||||
pub sign_with_tap_internal_key: bool,
|
||||
|
||||
/// Whether we should grind ECDSA signature to ensure signing with low r
|
||||
/// or not.
|
||||
/// Defaults to `true`, i.e., we always grind ECDSA signature to sign with low r.
|
||||
pub allow_grinding: bool,
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
#[derive(Default, Debug, Clone, PartialEq, Eq)]
|
||||
pub enum TapLeavesOptions {
|
||||
/// The signer will sign all the leaves it has a key for.
|
||||
#[default]
|
||||
All,
|
||||
/// The signer won't sign leaves other than the ones specified. Note that it could still ignore
|
||||
/// some of the specified leaves, if it doesn't have the right key to sign them.
|
||||
@@ -723,13 +820,6 @@ pub enum TapLeavesOptions {
|
||||
None,
|
||||
}
|
||||
|
||||
impl Default for TapLeavesOptions {
|
||||
fn default() -> Self {
|
||||
TapLeavesOptions::All
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::derivable_impls)]
|
||||
impl Default for SignOptions {
|
||||
fn default() -> Self {
|
||||
SignOptions {
|
||||
@@ -740,6 +830,7 @@ impl Default for SignOptions {
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
allow_grinding: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -758,7 +849,7 @@ pub(crate) trait ComputeSighash {
|
||||
|
||||
impl ComputeSighash for Legacy {
|
||||
type Extra = ();
|
||||
type Sighash = bitcoin::Sighash;
|
||||
type Sighash = sighash::LegacySighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
@@ -805,19 +896,9 @@ impl ComputeSighash for Legacy {
|
||||
}
|
||||
}
|
||||
|
||||
fn p2wpkh_script_code(script: &Script) -> Script {
|
||||
ScriptBuilder::new()
|
||||
.push_opcode(opcodes::all::OP_DUP)
|
||||
.push_opcode(opcodes::all::OP_HASH160)
|
||||
.push_slice(&script[2..])
|
||||
.push_opcode(opcodes::all::OP_EQUALVERIFY)
|
||||
.push_opcode(opcodes::all::OP_CHECKSIG)
|
||||
.into_script()
|
||||
}
|
||||
|
||||
impl ComputeSighash for Segwitv0 {
|
||||
type Extra = ();
|
||||
type Sighash = bitcoin::Sighash;
|
||||
type Sighash = sighash::SegwitV0Sighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
@@ -864,14 +945,21 @@ impl ComputeSighash for Segwitv0 {
|
||||
Some(ref witness_script) => witness_script.clone(),
|
||||
None => {
|
||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&utxo.script_pubkey)
|
||||
utxo.script_pubkey
|
||||
.p2wpkh_script_code()
|
||||
.expect("We check above that the spk is a p2wpkh")
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.map(Script::is_v0_p2wpkh)
|
||||
.map(|s| s.is_v0_p2wpkh())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
|
||||
psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.unwrap()
|
||||
.p2wpkh_script_code()
|
||||
.expect("We check above that the spk is a p2wpkh")
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessScript);
|
||||
}
|
||||
@@ -892,14 +980,14 @@ impl ComputeSighash for Segwitv0 {
|
||||
|
||||
impl ComputeSighash for Tap {
|
||||
type Extra = Option<taproot::TapLeafHash>;
|
||||
type Sighash = taproot::TapSighashHash;
|
||||
type SighashType = SchnorrSighashType;
|
||||
type Sighash = TapSighash;
|
||||
type SighashType = TapSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, SchnorrSighashType), SignerError> {
|
||||
) -> Result<(Self::Sighash, TapSighashType), SignerError> {
|
||||
if input_index >= psbt.inputs.len() || input_index >= psbt.unsigned_tx.input.len() {
|
||||
return Err(SignerError::InputIndexOutOfRange);
|
||||
}
|
||||
@@ -908,14 +996,11 @@ impl ComputeSighash for Tap {
|
||||
|
||||
let sighash_type = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| SchnorrSighashType::Default.into())
|
||||
.schnorr_hash_ty()
|
||||
.unwrap_or_else(|| TapSighashType::Default.into())
|
||||
.taproot_hash_ty()
|
||||
.map_err(|_| SignerError::InvalidSighash)?;
|
||||
let witness_utxos = psbt
|
||||
.inputs
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|i| i.witness_utxo)
|
||||
let witness_utxos = (0..psbt.inputs.len())
|
||||
.map(|i| psbt.get_utxo_for(i))
|
||||
.collect::<Vec<_>>();
|
||||
let mut all_witness_utxos = vec![];
|
||||
|
||||
@@ -973,11 +1058,12 @@ mod signers_container_tests {
|
||||
use crate::descriptor;
|
||||
use crate::descriptor::IntoWalletDescriptor;
|
||||
use crate::keys::{DescriptorKey, IntoDescriptorKey};
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::Network;
|
||||
use core::str::FromStr;
|
||||
use miniscript::ScriptContext;
|
||||
use std::str::FromStr;
|
||||
|
||||
fn is_equal(this: &Arc<dyn TransactionSigner>, that: &Arc<DummySigner>) -> bool {
|
||||
let secp = Secp256k1::new();
|
||||
@@ -1042,17 +1128,17 @@ mod signers_container_tests {
|
||||
signers.add_external(id2.clone(), SignerOrdering(2), signer2.clone());
|
||||
signers.add_external(id3.clone(), SignerOrdering(3), signer3.clone());
|
||||
|
||||
assert!(matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1)));
|
||||
assert!(matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2)));
|
||||
assert!(matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3)));
|
||||
assert_matches!(signers.find(id1), Some(signer) if is_equal(signer, &signer1));
|
||||
assert_matches!(signers.find(id2), Some(signer) if is_equal(signer, &signer2));
|
||||
assert_matches!(signers.find(id3.clone()), Some(signer) if is_equal(signer, &signer3));
|
||||
|
||||
// The `signer4` has the same ID as `signer3` but lower ordering.
|
||||
// It should be found by `id3` instead of `signer3`.
|
||||
signers.add_external(id3.clone(), SignerOrdering(2), signer4.clone());
|
||||
assert!(matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4)));
|
||||
assert_matches!(signers.find(id3), Some(signer) if is_equal(signer, &signer4));
|
||||
|
||||
// Can't find anything with ID that doesn't exist
|
||||
assert!(matches!(signers.find(id_nonexistent), None));
|
||||
assert_matches!(signers.find(id_nonexistent), None);
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -17,9 +17,13 @@
|
||||
//! # use std::str::FromStr;
|
||||
//! # use bitcoin::*;
|
||||
//! # use bdk::*;
|
||||
//! # use bdk::wallet::ChangeSet;
|
||||
//! # use bdk::wallet::error::CreateTxError;
|
||||
//! # use bdk::wallet::tx_builder::CreateTx;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
//! # let wallet = doctest_wallet!();
|
||||
//! # use bdk_chain::PersistBackend;
|
||||
//! # use anyhow::Error;
|
||||
//! # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
//! # let mut wallet = doctest_wallet!();
|
||||
//! // create a TxBuilder from a wallet
|
||||
//! let mut tx_builder = wallet.build_tx();
|
||||
//!
|
||||
@@ -27,33 +31,34 @@
|
||||
//! // Create a transaction with one output to `to_address` of 50_000 satoshi
|
||||
//! .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
//! // With a custom fee rate of 5.0 satoshi/vbyte
|
||||
//! .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
//! .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||
//! // Only spend non-change outputs
|
||||
//! .do_not_spend_change()
|
||||
//! // Turn on RBF signaling
|
||||
//! .enable_rbf();
|
||||
//! let (psbt, tx_details) = tx_builder.finish()?;
|
||||
//! # Ok::<(), bdk::Error>(())
|
||||
//! let psbt = tx_builder.finish()?;
|
||||
//! # Ok::<(), anyhow::Error>(())
|
||||
//! ```
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
use std::collections::HashSet;
|
||||
use std::default::Default;
|
||||
use std::marker::PhantomData;
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::collections::HashSet;
|
||||
use alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use bdk_chain::PersistBackend;
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{OutPoint, Script, Transaction};
|
||||
|
||||
use miniscript::descriptor::DescriptorTrait;
|
||||
use bitcoin::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{absolute, script::PushBytes, OutPoint, ScriptBuf, Sequence, Transaction, Txid};
|
||||
|
||||
use super::coin_selection::{CoinSelectionAlgorithm, DefaultCoinSelectionAlgorithm};
|
||||
use crate::{database::BatchDatabase, Error, Utxo, Wallet};
|
||||
use crate::{
|
||||
types::{FeeRate, KeychainKind, LocalUtxo, WeightedUtxo},
|
||||
TransactionDetails,
|
||||
};
|
||||
use super::ChangeSet;
|
||||
use crate::types::{FeeRate, KeychainKind, LocalOutput, WeightedUtxo};
|
||||
use crate::wallet::CreateTxError;
|
||||
use crate::{Utxo, Wallet};
|
||||
|
||||
/// Context in which the [`TxBuilder`] is valid
|
||||
pub trait TxBuilderContext: std::fmt::Debug + Default + Clone {}
|
||||
pub trait TxBuilderContext: core::fmt::Debug + Default + Clone {}
|
||||
|
||||
/// Marker type to indicate the [`TxBuilder`] is being used to create a new transaction (as opposed
|
||||
/// to bumping the fee of an existing one).
|
||||
@@ -80,11 +85,15 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// # use bdk::wallet::tx_builder::*;
|
||||
/// # use bitcoin::*;
|
||||
/// # use core::str::FromStr;
|
||||
/// # let wallet = doctest_wallet!();
|
||||
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// # let addr1 = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap().assume_checked();
|
||||
/// # let addr2 = addr1.clone();
|
||||
/// // chaining
|
||||
/// let (psbt1, details) = {
|
||||
/// let psbt1 = {
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder
|
||||
/// .ordering(TxOrdering::Untouched)
|
||||
@@ -94,7 +103,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// };
|
||||
///
|
||||
/// // non-chaining
|
||||
/// let (psbt2, details) = {
|
||||
/// let psbt2 = {
|
||||
/// let mut builder = wallet.build_tx();
|
||||
/// builder.ordering(TxOrdering::Untouched);
|
||||
/// for addr in &[addr1, addr2] {
|
||||
@@ -104,7 +113,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// };
|
||||
///
|
||||
/// assert_eq!(psbt1.unsigned_tx.output[..2], psbt2.unsigned_tx.output[..2]);
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// At the moment [`coin_selection`] is an exception to the rule as it consumes `self`.
|
||||
@@ -118,7 +127,7 @@ impl TxBuilderContext for BumpFee {}
|
||||
/// [`coin_selection`]: Self::coin_selection
|
||||
#[derive(Debug)]
|
||||
pub struct TxBuilder<'a, D, Cs, Ctx> {
|
||||
pub(crate) wallet: &'a Wallet<D>,
|
||||
pub(crate) wallet: Rc<RefCell<&'a mut Wallet<D>>>,
|
||||
pub(crate) params: TxParams,
|
||||
pub(crate) coin_selection: Cs,
|
||||
pub(crate) phantom: PhantomData<Ctx>,
|
||||
@@ -128,9 +137,9 @@ pub struct TxBuilder<'a, D, Cs, Ctx> {
|
||||
//TODO: TxParams should eventually be exposed publicly.
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub(crate) struct TxParams {
|
||||
pub(crate) recipients: Vec<(Script, u64)>,
|
||||
pub(crate) recipients: Vec<(ScriptBuf, u64)>,
|
||||
pub(crate) drain_wallet: bool,
|
||||
pub(crate) drain_to: Option<Script>,
|
||||
pub(crate) drain_to: Option<ScriptBuf>,
|
||||
pub(crate) fee_policy: Option<FeePolicy>,
|
||||
pub(crate) internal_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||
pub(crate) external_policy_path: Option<BTreeMap<String, Vec<usize>>>,
|
||||
@@ -139,7 +148,7 @@ pub(crate) struct TxParams {
|
||||
pub(crate) manually_selected_only: bool,
|
||||
pub(crate) sighash: Option<psbt::PsbtSighashType>,
|
||||
pub(crate) ordering: TxOrdering,
|
||||
pub(crate) locktime: Option<u32>,
|
||||
pub(crate) locktime: Option<absolute::LockTime>,
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
@@ -147,7 +156,8 @@ pub(crate) struct TxParams {
|
||||
pub(crate) add_global_xpubs: bool,
|
||||
pub(crate) include_output_redeem_witness_script: bool,
|
||||
pub(crate) bumping_fee: Option<PreviousFee>,
|
||||
pub(crate) current_height: Option<u32>,
|
||||
pub(crate) current_height: Option<absolute::LockTime>,
|
||||
pub(crate) allow_dust: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
@@ -162,16 +172,16 @@ pub(crate) enum FeePolicy {
|
||||
FeeAmount(u64),
|
||||
}
|
||||
|
||||
impl std::default::Default for FeePolicy {
|
||||
impl Default for FeePolicy {
|
||||
fn default() -> Self {
|
||||
FeePolicy::FeeRate(FeeRate::default_min_relay_fee())
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
impl<'a, D, Cs: Clone, Ctx> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
fn clone(&self) -> Self {
|
||||
TxBuilder {
|
||||
wallet: self.wallet,
|
||||
wallet: self.wallet.clone(),
|
||||
params: self.params.clone(),
|
||||
coin_selection: self.coin_selection.clone(),
|
||||
phantom: PhantomData,
|
||||
@@ -180,16 +190,33 @@ impl<'a, Cs: Clone, Ctx, D> Clone for TxBuilder<'a, D, Cs, Ctx> {
|
||||
}
|
||||
|
||||
// methods supported by both contexts, for any CoinSelectionAlgorithm
|
||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
TxBuilder<'a, D, Cs, Ctx>
|
||||
{
|
||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate
|
||||
/// The fee_rate method sets the mining fee paid by the transaction as a rate on its size.
|
||||
/// This means that the total fee paid is equal to this rate * size of the transaction in virtual Bytes (vB) or Weight Unit (wu).
|
||||
/// This rate is internally expressed in satoshis-per-virtual-bytes (sats/vB) using FeeRate::from_sat_per_vb, but can also be set by:
|
||||
/// * sats/kvB (1000 sats/kvB == 1 sats/vB) using FeeRate::from_sat_per_kvb
|
||||
/// * btc/kvB (0.00001000 btc/kvB == 1 sats/vB) using FeeRate::from_btc_per_kvb
|
||||
/// * sats/kwu (250 sats/kwu == 1 sats/vB) using FeeRate::from_sat_per_kwu
|
||||
/// Default is 1 sat/vB (see min_relay_fee)
|
||||
///
|
||||
/// Note that this is really a minimum feerate -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
/// excess might not be viable.
|
||||
pub fn fee_rate(&mut self, fee_rate: FeeRate) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeRate(fee_rate));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set an absolute fee
|
||||
/// The fee_absolute method refers to the absolute transaction fee in satoshis (sats).
|
||||
/// If anyone sets both the fee_absolute method and the fee_rate method,
|
||||
/// the FeePolicy enum will be set by whichever method was called last,
|
||||
/// as the FeeRate and FeeAmount are mutually exclusive.
|
||||
///
|
||||
/// Note that this is really a minimum absolute fee -- it's possible to
|
||||
/// overshoot it slightly since adding a change output to drain the remaining
|
||||
/// excess might not be viable.
|
||||
pub fn fee_absolute(&mut self, fee_amount: u64) -> &mut Self {
|
||||
self.params.fee_policy = Some(FeePolicy::FeeAmount(fee_amount));
|
||||
self
|
||||
@@ -242,8 +269,11 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
/// # use std::collections::BTreeMap;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
/// # let wallet = doctest_wallet!();
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut path = BTreeMap::new();
|
||||
/// path.insert("aabbccdd".to_string(), vec![0, 1]);
|
||||
///
|
||||
@@ -252,7 +282,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
/// .add_recipient(to_address.script_pubkey(), 50_000)
|
||||
/// .policy_path(path, KeychainKind::External);
|
||||
///
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
pub fn policy_path(
|
||||
&mut self,
|
||||
@@ -274,19 +304,27 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
||||
/// the "utxos" and the "unspendable" list, it will be spent.
|
||||
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, Error> {
|
||||
let utxos = outpoints
|
||||
.iter()
|
||||
.map(|outpoint| self.wallet.get_utxo(*outpoint)?.ok_or(Error::UnknownUtxo))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
pub fn add_utxos(&mut self, outpoints: &[OutPoint]) -> Result<&mut Self, AddUtxoError> {
|
||||
{
|
||||
let wallet = self.wallet.borrow();
|
||||
let utxos = outpoints
|
||||
.iter()
|
||||
.map(|outpoint| {
|
||||
wallet
|
||||
.get_utxo(*outpoint)
|
||||
.ok_or(AddUtxoError::UnknownUtxo(*outpoint))
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for utxo in utxos {
|
||||
let descriptor = self.wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
||||
self.params.utxos.push(WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
});
|
||||
for utxo in utxos {
|
||||
let descriptor = wallet.get_descriptor_for_keychain(utxo.keychain);
|
||||
#[allow(deprecated)]
|
||||
let satisfaction_weight = descriptor.max_satisfaction_weight().unwrap();
|
||||
self.params.utxos.push(WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
@@ -296,7 +334,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// These have priority over the "unspendable" utxos, meaning that if a utxo is present both in
|
||||
/// the "utxos" and the "unspendable" list, it will be spent.
|
||||
pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, Error> {
|
||||
pub fn add_utxo(&mut self, outpoint: OutPoint) -> Result<&mut Self, AddUtxoError> {
|
||||
self.add_utxos(&[outpoint])
|
||||
}
|
||||
|
||||
@@ -328,6 +366,10 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// This is an **EXPERIMENTAL** feature, API and other major changes are expected.
|
||||
///
|
||||
/// In order to use [`Wallet::calculate_fee`] or [`Wallet::calculate_fee_rate`] for a transaction
|
||||
/// created with foreign UTXO(s) you must manually insert the corresponding TxOut(s) into the tx
|
||||
/// graph using the [`Wallet::insert_txout`] function.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method returns errors in the following circumstances:
|
||||
@@ -347,23 +389,38 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
) -> Result<&mut Self, Error> {
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
self.add_foreign_utxo_with_sequence(
|
||||
outpoint,
|
||||
psbt_input,
|
||||
satisfaction_weight,
|
||||
Sequence::MAX,
|
||||
)
|
||||
}
|
||||
|
||||
/// Same as [add_foreign_utxo](TxBuilder::add_foreign_utxo) but allows to set the nSequence value.
|
||||
pub fn add_foreign_utxo_with_sequence(
|
||||
&mut self,
|
||||
outpoint: OutPoint,
|
||||
psbt_input: psbt::Input,
|
||||
satisfaction_weight: usize,
|
||||
sequence: Sequence,
|
||||
) -> Result<&mut Self, AddForeignUtxoError> {
|
||||
if psbt_input.witness_utxo.is_none() {
|
||||
match psbt_input.non_witness_utxo.as_ref() {
|
||||
Some(tx) => {
|
||||
if tx.txid() != outpoint.txid {
|
||||
return Err(Error::Generic(
|
||||
"Foreign utxo outpoint does not match PSBT input".into(),
|
||||
));
|
||||
return Err(AddForeignUtxoError::InvalidTxid {
|
||||
input_txid: tx.txid(),
|
||||
foreign_utxo: outpoint,
|
||||
});
|
||||
}
|
||||
if tx.output.len() <= outpoint.vout as usize {
|
||||
return Err(Error::InvalidOutpoint(outpoint));
|
||||
return Err(AddForeignUtxoError::InvalidOutpoint(outpoint));
|
||||
}
|
||||
}
|
||||
None => {
|
||||
return Err(Error::Generic(
|
||||
"Foreign utxo missing witness_utxo or non_witness_utxo".into(),
|
||||
))
|
||||
return Err(AddForeignUtxoError::MissingUtxo);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -372,6 +429,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Foreign {
|
||||
outpoint,
|
||||
sequence: Some(sequence),
|
||||
psbt_input: Box::new(psbt_input),
|
||||
},
|
||||
});
|
||||
@@ -425,7 +483,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
/// Use a specific nLockTime while creating the transaction
|
||||
///
|
||||
/// This can cause conflicts if the wallet's descriptors contain an "after" (OP_CLTV) operator.
|
||||
pub fn nlocktime(&mut self, locktime: u32) -> &mut Self {
|
||||
pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self {
|
||||
self.params.locktime = Some(locktime);
|
||||
self
|
||||
}
|
||||
@@ -464,7 +522,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
self
|
||||
}
|
||||
|
||||
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::util::psbt::Input::witness_utxo) field when spending from
|
||||
/// Only Fill-in the [`psbt::Input::witness_utxo`](bitcoin::psbt::Input::witness_utxo) field when spending from
|
||||
/// SegWit descriptors.
|
||||
///
|
||||
/// This reduces the size of the PSBT, but some signers might reject them due to the lack of
|
||||
@@ -474,8 +532,8 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
self
|
||||
}
|
||||
|
||||
/// Fill-in the [`psbt::Output::redeem_script`](bitcoin::util::psbt::Output::redeem_script) and
|
||||
/// [`psbt::Output::witness_script`](bitcoin::util::psbt::Output::witness_script) fields.
|
||||
/// Fill-in the [`psbt::Output::redeem_script`](bitcoin::psbt::Output::redeem_script) and
|
||||
/// [`psbt::Output::witness_script`](bitcoin::psbt::Output::witness_script) fields.
|
||||
///
|
||||
/// This is useful for signers which always require it, like ColdCard hardware wallets.
|
||||
pub fn include_output_redeem_witness_script(&mut self) -> &mut Self {
|
||||
@@ -501,10 +559,10 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
|
||||
/// Choose the coin selection algorithm
|
||||
///
|
||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`](super::coin_selection::DefaultCoinSelectionAlgorithm).
|
||||
/// Overrides the [`DefaultCoinSelectionAlgorithm`].
|
||||
///
|
||||
/// Note that this function consumes the builder and returns it so it is usually best to put this as the first call on the builder.
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm<D>>(
|
||||
pub fn coin_selection<P: CoinSelectionAlgorithm>(
|
||||
self,
|
||||
coin_selection: P,
|
||||
) -> TxBuilder<'a, D, P, Ctx> {
|
||||
@@ -516,15 +574,6 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
}
|
||||
}
|
||||
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns the [`BIP174`] "PSBT" and summary details about the transaction.
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<(Psbt, TransactionDetails), Error> {
|
||||
self.wallet.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
|
||||
/// Enable signaling RBF
|
||||
///
|
||||
/// This will use the default nSequence value of `0xFFFFFFFD`.
|
||||
@@ -540,7 +589,7 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// If the `nsequence` is higher than `0xFFFFFFFD` an error will be thrown, since it would not
|
||||
/// be a valid nSequence to signal RBF.
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: u32) -> &mut Self {
|
||||
pub fn enable_rbf_with_sequence(&mut self, nsequence: Sequence) -> &mut Self {
|
||||
self.params.rbf = Some(RbfValue::Value(nsequence));
|
||||
self
|
||||
}
|
||||
@@ -557,27 +606,136 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// In both cases, if you don't provide a current height, we use the last sync height.
|
||||
pub fn current_height(&mut self, height: u32) -> &mut Self {
|
||||
self.params.current_height = Some(height);
|
||||
self.params.current_height =
|
||||
Some(absolute::LockTime::from_height(height).expect("Invalid height"));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set whether or not the dust limit is checked.
|
||||
///
|
||||
/// **Note**: by avoiding a dust limit check you may end up with a transaction that is non-standard.
|
||||
pub fn allow_dust(&mut self, allow_dust: bool) -> &mut Self {
|
||||
self.params.allow_dust = allow_dust;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Finish building the transaction.
|
||||
///
|
||||
/// Returns a new [`Psbt`] per [`BIP174`].
|
||||
///
|
||||
/// [`BIP174`]: https://github.com/bitcoin/bips/blob/master/bip-0174.mediawiki
|
||||
pub fn finish(self) -> Result<Psbt, CreateTxError<D::WriteError>>
|
||||
where
|
||||
D: PersistBackend<ChangeSet>,
|
||||
{
|
||||
self.wallet
|
||||
.borrow_mut()
|
||||
.create_tx(self.coin_selection, self.params)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::add_utxo`] and [`TxBuilder::add_utxos`]
|
||||
pub enum AddUtxoError {
|
||||
/// Happens when trying to spend an UTXO that is not in the internal database
|
||||
UnknownUtxo(OutPoint),
|
||||
}
|
||||
|
||||
impl fmt::Display for AddUtxoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::UnknownUtxo(outpoint) => write!(
|
||||
f,
|
||||
"UTXO not found in the internal database for txid: {} with vout: {}",
|
||||
outpoint.txid, outpoint.vout
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AddUtxoError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::add_foreign_utxo`].
|
||||
pub enum AddForeignUtxoError {
|
||||
/// Foreign utxo outpoint txid does not match PSBT input txid
|
||||
InvalidTxid {
|
||||
/// PSBT input txid
|
||||
input_txid: Txid,
|
||||
/// Foreign UTXO outpoint
|
||||
foreign_utxo: OutPoint,
|
||||
},
|
||||
/// Requested outpoint doesn't exist in the tx (vout greater than available outputs)
|
||||
InvalidOutpoint(OutPoint),
|
||||
/// Foreign utxo missing witness_utxo or non_witness_utxo
|
||||
MissingUtxo,
|
||||
}
|
||||
|
||||
impl fmt::Display for AddForeignUtxoError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::InvalidTxid {
|
||||
input_txid,
|
||||
foreign_utxo,
|
||||
} => write!(
|
||||
f,
|
||||
"Foreign UTXO outpoint txid: {} does not match PSBT input txid: {}",
|
||||
foreign_utxo.txid, input_txid,
|
||||
),
|
||||
Self::InvalidOutpoint(outpoint) => write!(
|
||||
f,
|
||||
"Requested outpoint doesn't exist for txid: {} with vout: {}",
|
||||
outpoint.txid, outpoint.vout,
|
||||
),
|
||||
Self::MissingUtxo => write!(f, "Foreign utxo missing witness_utxo or non_witness_utxo"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AddForeignUtxoError {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Error returned from [`TxBuilder::allow_shrinking`]
|
||||
pub enum AllowShrinkingError {
|
||||
/// Script/PubKey was not in the original transaction
|
||||
MissingScriptPubKey(ScriptBuf),
|
||||
}
|
||||
|
||||
impl fmt::Display for AllowShrinkingError {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self {
|
||||
Self::MissingScriptPubKey(script_buf) => write!(
|
||||
f,
|
||||
"Script/PubKey was not in the original transaction: {}",
|
||||
script_buf,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AllowShrinkingError {}
|
||||
|
||||
impl<'a, D, Cs: CoinSelectionAlgorithm> TxBuilder<'a, D, Cs, CreateTx> {
|
||||
/// Replace the recipients already added with a new list
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(Script, u64)>) -> &mut Self {
|
||||
pub fn set_recipients(&mut self, recipients: Vec<(ScriptBuf, u64)>) -> &mut Self {
|
||||
self.params.recipients = recipients;
|
||||
self
|
||||
}
|
||||
|
||||
/// Add a recipient to the internal list
|
||||
pub fn add_recipient(&mut self, script_pubkey: Script, amount: u64) -> &mut Self {
|
||||
pub fn add_recipient(&mut self, script_pubkey: ScriptBuf, amount: u64) -> &mut Self {
|
||||
self.params.recipients.push((script_pubkey, amount));
|
||||
self
|
||||
}
|
||||
|
||||
/// Add data as an output, using OP_RETURN
|
||||
pub fn add_data(&mut self, data: &[u8]) -> &mut Self {
|
||||
let script = Script::new_op_return(data);
|
||||
pub fn add_data<T: AsRef<PushBytes>>(&mut self, data: &T) -> &mut Self {
|
||||
let script = ScriptBuf::new_op_return(data);
|
||||
self.add_recipient(script, 0u64);
|
||||
self
|
||||
}
|
||||
@@ -606,9 +764,16 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
||||
/// # use std::str::FromStr;
|
||||
/// # use bitcoin::*;
|
||||
/// # use bdk::*;
|
||||
/// # use bdk::wallet::ChangeSet;
|
||||
/// # use bdk::wallet::error::CreateTxError;
|
||||
/// # use bdk::wallet::tx_builder::CreateTx;
|
||||
/// # let to_address = Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt").unwrap();
|
||||
/// # let wallet = doctest_wallet!();
|
||||
/// # use bdk_chain::PersistBackend;
|
||||
/// # use anyhow::Error;
|
||||
/// # let to_address =
|
||||
/// Address::from_str("2N4eQYCbKUHCCTUjBJeHcJp9ok6J2GZsTDt")
|
||||
/// .unwrap()
|
||||
/// .assume_checked();
|
||||
/// # let mut wallet = doctest_wallet!();
|
||||
/// let mut tx_builder = wallet.build_tx();
|
||||
///
|
||||
/// tx_builder
|
||||
@@ -616,24 +781,24 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>> TxBuilder<'a, D, Cs, C
|
||||
/// .drain_wallet()
|
||||
/// // Send the excess (which is all the coins minus the fee) to this address.
|
||||
/// .drain_to(to_address.script_pubkey())
|
||||
/// .fee_rate(FeeRate::from_sat_per_vb(5.0))
|
||||
/// .fee_rate(bdk::FeeRate::from_sat_per_vb(5.0))
|
||||
/// .enable_rbf();
|
||||
/// let (psbt, tx_details) = tx_builder.finish()?;
|
||||
/// # Ok::<(), bdk::Error>(())
|
||||
/// let psbt = tx_builder.finish()?;
|
||||
/// # Ok::<(), anyhow::Error>(())
|
||||
/// ```
|
||||
///
|
||||
/// [`allow_shrinking`]: Self::allow_shrinking
|
||||
/// [`add_recipient`]: Self::add_recipient
|
||||
/// [`add_utxos`]: Self::add_utxos
|
||||
/// [`drain_wallet`]: Self::drain_wallet
|
||||
pub fn drain_to(&mut self, script_pubkey: Script) -> &mut Self {
|
||||
pub fn drain_to(&mut self, script_pubkey: ScriptBuf) -> &mut Self {
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
// methods supported only by bump_fee
|
||||
impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
impl<'a, D> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpFee> {
|
||||
/// Explicitly tells the wallet that it is allowed to reduce the amount of the output matching this
|
||||
/// `script_pubkey` in order to bump the transaction fee. Without specifying this the wallet
|
||||
/// will attempt to find a change output to shrink instead.
|
||||
@@ -644,7 +809,10 @@ impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpF
|
||||
///
|
||||
/// Returns an `Err` if `script_pubkey` can't be found among the recipients of the
|
||||
/// transaction we are bumping.
|
||||
pub fn allow_shrinking(&mut self, script_pubkey: Script) -> Result<&mut Self, Error> {
|
||||
pub fn allow_shrinking(
|
||||
&mut self,
|
||||
script_pubkey: ScriptBuf,
|
||||
) -> Result<&mut Self, AllowShrinkingError> {
|
||||
match self
|
||||
.params
|
||||
.recipients
|
||||
@@ -656,18 +824,16 @@ impl<'a, D: BatchDatabase> TxBuilder<'a, D, DefaultCoinSelectionAlgorithm, BumpF
|
||||
self.params.drain_to = Some(script_pubkey);
|
||||
Ok(self)
|
||||
}
|
||||
None => Err(Error::Generic(format!(
|
||||
"{} was not in the original transaction",
|
||||
script_pubkey
|
||||
))),
|
||||
None => Err(AllowShrinkingError::MissingScriptPubKey(script_pubkey)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Ordering of the transaction's inputs and outputs
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum TxOrdering {
|
||||
/// Randomized (default)
|
||||
#[default]
|
||||
Shuffle,
|
||||
/// Unchanged
|
||||
Untouched,
|
||||
@@ -675,12 +841,6 @@ pub enum TxOrdering {
|
||||
Bip69Lexicographic,
|
||||
}
|
||||
|
||||
impl Default for TxOrdering {
|
||||
fn default() -> Self {
|
||||
TxOrdering::Shuffle
|
||||
}
|
||||
}
|
||||
|
||||
impl TxOrdering {
|
||||
/// Sort transaction inputs and outputs by [`TxOrdering`] variant
|
||||
pub fn sort_tx(&self, tx: &mut Transaction) {
|
||||
@@ -688,14 +848,8 @@ impl TxOrdering {
|
||||
TxOrdering::Untouched => {}
|
||||
TxOrdering::Shuffle => {
|
||||
use rand::seq::SliceRandom;
|
||||
#[cfg(test)]
|
||||
use rand::SeedableRng;
|
||||
|
||||
#[cfg(not(test))]
|
||||
let mut rng = rand::thread_rng();
|
||||
#[cfg(test)]
|
||||
let mut rng = rand::rngs::StdRng::seed_from_u64(0);
|
||||
|
||||
tx.input.shuffle(&mut rng);
|
||||
tx.output.shuffle(&mut rng);
|
||||
}
|
||||
TxOrdering::Bip69Lexicographic => {
|
||||
@@ -727,22 +881,23 @@ impl Default for Version {
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub(crate) enum RbfValue {
|
||||
Default,
|
||||
Value(u32),
|
||||
Value(Sequence),
|
||||
}
|
||||
|
||||
impl RbfValue {
|
||||
pub(crate) fn get_value(&self) -> u32 {
|
||||
pub(crate) fn get_value(&self) -> Sequence {
|
||||
match self {
|
||||
RbfValue::Default => 0xFFFFFFFD,
|
||||
RbfValue::Default => Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
RbfValue::Value(v) => *v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Policy regarding the use of change outputs when creating a transaction
|
||||
#[derive(Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
#[derive(Default, Debug, Ord, PartialOrd, Eq, PartialEq, Hash, Clone, Copy)]
|
||||
pub enum ChangeSpendPolicy {
|
||||
/// Use both change and non-change outputs (default)
|
||||
#[default]
|
||||
ChangeAllowed,
|
||||
/// Only use change outputs (see [`TxBuilder::only_spend_change`])
|
||||
OnlyChange,
|
||||
@@ -750,14 +905,8 @@ pub enum ChangeSpendPolicy {
|
||||
ChangeForbidden,
|
||||
}
|
||||
|
||||
impl Default for ChangeSpendPolicy {
|
||||
fn default() -> Self {
|
||||
ChangeSpendPolicy::ChangeAllowed
|
||||
}
|
||||
}
|
||||
|
||||
impl ChangeSpendPolicy {
|
||||
pub(crate) fn is_satisfied_by(&self, utxo: &LocalUtxo) -> bool {
|
||||
pub(crate) fn is_satisfied_by(&self, utxo: &LocalOutput) -> bool {
|
||||
match self {
|
||||
ChangeSpendPolicy::ChangeAllowed => true,
|
||||
ChangeSpendPolicy::OnlyChange => utxo.keychain == KeychainKind::Internal,
|
||||
@@ -781,6 +930,7 @@ mod test {
|
||||
};
|
||||
}
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::consensus::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
|
||||
@@ -806,15 +956,25 @@ mod test {
|
||||
let original_tx = ordering_test_tx!();
|
||||
let mut tx = original_tx.clone();
|
||||
|
||||
TxOrdering::Shuffle.sort_tx(&mut tx);
|
||||
(0..40)
|
||||
.find(|_| {
|
||||
TxOrdering::Shuffle.sort_tx(&mut tx);
|
||||
original_tx.input != tx.input
|
||||
})
|
||||
.expect("it should have moved the inputs at least once");
|
||||
|
||||
assert_eq!(original_tx.input, tx.input);
|
||||
assert_ne!(original_tx.output, tx.output);
|
||||
let mut tx = original_tx.clone();
|
||||
(0..40)
|
||||
.find(|_| {
|
||||
TxOrdering::Shuffle.sort_tx(&mut tx);
|
||||
original_tx.output != tx.output
|
||||
})
|
||||
.expect("it should have moved the outputs at least once");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_output_ordering_bip69() {
|
||||
use std::str::FromStr;
|
||||
use core::str::FromStr;
|
||||
|
||||
let original_tx = ordering_test_tx!();
|
||||
let mut tx = original_tx;
|
||||
@@ -844,29 +1004,41 @@ mod test {
|
||||
);
|
||||
|
||||
assert_eq!(tx.output[0].value, 800);
|
||||
assert_eq!(tx.output[1].script_pubkey, From::from(vec![0xAA]));
|
||||
assert_eq!(tx.output[2].script_pubkey, From::from(vec![0xAA, 0xEE]));
|
||||
assert_eq!(tx.output[1].script_pubkey, ScriptBuf::from(vec![0xAA]));
|
||||
assert_eq!(
|
||||
tx.output[2].script_pubkey,
|
||||
ScriptBuf::from(vec![0xAA, 0xEE])
|
||||
);
|
||||
}
|
||||
|
||||
fn get_test_utxos() -> Vec<LocalUtxo> {
|
||||
fn get_test_utxos() -> Vec<LocalOutput> {
|
||||
use bitcoin::hashes::Hash;
|
||||
|
||||
vec![
|
||||
LocalUtxo {
|
||||
LocalOutput {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 0,
|
||||
},
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
derivation_index: 0,
|
||||
},
|
||||
LocalUtxo {
|
||||
LocalOutput {
|
||||
outpoint: OutPoint {
|
||||
txid: Default::default(),
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 1,
|
||||
},
|
||||
txout: Default::default(),
|
||||
keychain: KeychainKind::Internal,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Confirmed {
|
||||
height: 32,
|
||||
time: 42,
|
||||
},
|
||||
derivation_index: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -9,23 +9,11 @@
|
||||
// You may not use this file except in accordance with one or both of these
|
||||
// licenses.
|
||||
|
||||
use bitcoin::blockdata::script::Script;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::{absolute, Script, Sequence};
|
||||
|
||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||
|
||||
// MSB of the nSequence. If set there's no consensus-constraint, so it must be disabled when
|
||||
// spending using CSV in order to enforce CSV rules
|
||||
pub(crate) const SEQUENCE_LOCKTIME_DISABLE_FLAG: u32 = 1 << 31;
|
||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
// Mask for the bits used to express the timelock
|
||||
pub(crate) const SEQUENCE_LOCKTIME_MASK: u32 = 0x0000FFFF;
|
||||
|
||||
// Threshold for nLockTime to be considered a block-height-based timelock rather than time-based
|
||||
pub(crate) const BLOCKS_TIMELOCK_THRESHOLD: u32 = 500000000;
|
||||
|
||||
/// Trait to check if a value is below the dust limit.
|
||||
/// We are performing dust value calculation for a given script public key using rust-bitcoin to
|
||||
/// keep it compatible with network dust rate
|
||||
@@ -38,7 +26,7 @@ pub trait IsDust {
|
||||
|
||||
impl IsDust for u64 {
|
||||
fn is_dust(&self, script: &Script) -> bool {
|
||||
*self < script.dust_value().as_sat()
|
||||
*self < script.dust_value().to_sat()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,19 +44,15 @@ impl After {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
||||
// This flag cannot be set in the nSequence when spending using OP_CSV
|
||||
if rbf & SEQUENCE_LOCKTIME_DISABLE_FLAG != 0 {
|
||||
pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
|
||||
// The RBF value must enable relative timelocks
|
||||
if !rbf.is_relative_lock_time() {
|
||||
return false;
|
||||
}
|
||||
|
||||
let mask = SEQUENCE_LOCKTIME_TYPE_FLAG | SEQUENCE_LOCKTIME_MASK;
|
||||
let rbf = rbf & mask;
|
||||
let csv = csv & mask;
|
||||
|
||||
// Both values should be represented in the same unit (either time-based or
|
||||
// block-height based)
|
||||
if (rbf < SEQUENCE_LOCKTIME_TYPE_FLAG) != (csv < SEQUENCE_LOCKTIME_TYPE_FLAG) {
|
||||
if rbf.is_time_locked() != csv.is_time_locked() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -80,24 +64,10 @@ pub(crate) fn check_nsequence_rbf(rbf: u32, csv: u32) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
pub(crate) fn check_nlocktime(nlocktime: u32, required: u32) -> bool {
|
||||
// Both values should be expressed in the same unit
|
||||
if (nlocktime < BLOCKS_TIMELOCK_THRESHOLD) != (required < BLOCKS_TIMELOCK_THRESHOLD) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// The value should be at least `required`
|
||||
if nlocktime < required {
|
||||
return false;
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
||||
fn check_after(&self, n: u32) -> bool {
|
||||
fn check_after(&self, n: absolute::LockTime) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
current_height >= n
|
||||
current_height >= n.to_consensus_u32()
|
||||
} else {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -125,10 +95,15 @@ impl Older {
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for Older {
|
||||
fn check_older(&self, n: u32) -> bool {
|
||||
fn check_older(&self, n: Sequence) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
// TODO: test >= / >
|
||||
current_height as u64 >= self.create_height.unwrap_or(0) as u64 + n as u64
|
||||
current_height
|
||||
>= self
|
||||
.create_height
|
||||
.unwrap_or(0)
|
||||
.checked_add(n.to_consensus_u32())
|
||||
.expect("Overflowing addition")
|
||||
} else {
|
||||
self.assume_height_reached
|
||||
}
|
||||
@@ -139,17 +114,19 @@ pub(crate) type SecpCtx = Secp256k1<All>;
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::{
|
||||
check_nlocktime, check_nsequence_rbf, IsDust, BLOCKS_TIMELOCK_THRESHOLD,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG,
|
||||
};
|
||||
use crate::bitcoin::Address;
|
||||
use crate::types::FeeRate;
|
||||
use std::str::FromStr;
|
||||
// When nSequence is lower than this flag the timelock is interpreted as block-height-based,
|
||||
// otherwise it's time-based
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
|
||||
use super::{check_nsequence_rbf, IsDust};
|
||||
use crate::bitcoin::{Address, Network, Sequence};
|
||||
use core::str::FromStr;
|
||||
|
||||
#[test]
|
||||
fn test_is_dust() {
|
||||
let script_p2pkh = Address::from_str("1GNgwA8JfG7Kc8akJ8opdNWJUihqUztfPe")
|
||||
.unwrap()
|
||||
.require_network(Network::Bitcoin)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
assert!(script_p2pkh.is_p2pkh());
|
||||
@@ -157,6 +134,8 @@ mod test {
|
||||
assert!(!546.is_dust(&script_p2pkh));
|
||||
|
||||
let script_p2wpkh = Address::from_str("bc1qxlh2mnc0yqwas76gqq665qkggee5m98t8yskd8")
|
||||
.unwrap()
|
||||
.require_network(Network::Bitcoin)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
assert!(script_p2wpkh.is_v0_p2wpkh());
|
||||
@@ -164,86 +143,42 @@ mod test {
|
||||
assert!(!294.is_dust(&script_p2wpkh));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_btc_per_kb() {
|
||||
let fee = FeeRate::from_btc_per_kvb(1e-5);
|
||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_from_sats_vbyte() {
|
||||
let fee = FeeRate::from_sat_per_vb(1.0);
|
||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fee_default_min_relay_fee() {
|
||||
let fee = FeeRate::default_min_relay_fee();
|
||||
assert!((fee.as_sat_vb() - 1.0).abs() < 0.0001);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_msb_set() {
|
||||
let result = check_nsequence_rbf(0x80000000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(0x80000000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_lt_csv() {
|
||||
let result = check_nsequence_rbf(4000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(4000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_different_unit() {
|
||||
let result = check_nsequence_rbf(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000, 5000);
|
||||
let result =
|
||||
check_nsequence_rbf(Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000), Sequence(5000));
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_mask() {
|
||||
let result = check_nsequence_rbf(0x3f + 10_000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(0x3f + 10_000), Sequence(5000));
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_blocks() {
|
||||
let result = check_nsequence_rbf(10_000, 5000);
|
||||
let result = check_nsequence_rbf(Sequence(10_000), Sequence(5000));
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nsequence_rbf_same_unit_time() {
|
||||
let result = check_nsequence_rbf(
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000,
|
||||
SEQUENCE_LOCKTIME_TYPE_FLAG + 5000,
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_lt_cltv() {
|
||||
let result = check_nlocktime(4000, 5000);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_different_unit() {
|
||||
let result = check_nlocktime(BLOCKS_TIMELOCK_THRESHOLD + 5000, 5000);
|
||||
assert!(!result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_same_unit_blocks() {
|
||||
let result = check_nlocktime(10_000, 5000);
|
||||
assert!(result);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_check_nlocktime_same_unit_time() {
|
||||
let result = check_nlocktime(
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 10_000,
|
||||
BLOCKS_TIMELOCK_THRESHOLD + 5000,
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 10_000),
|
||||
Sequence(SEQUENCE_LOCKTIME_TYPE_FLAG + 5000),
|
||||
);
|
||||
assert!(result);
|
||||
}
|
||||
156
crates/bdk/tests/common.rs
Normal file
156
crates/bdk/tests/common.rs
Normal file
@@ -0,0 +1,156 @@
|
||||
#![allow(unused)]
|
||||
|
||||
use bdk::{wallet::AddressIndex, KeychainKind, LocalOutput, Wallet};
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
use bdk_chain::{BlockId, ConfirmationTime};
|
||||
use bitcoin::hashes::Hash;
|
||||
use bitcoin::{Address, BlockHash, Network, OutPoint, Transaction, TxIn, TxOut, Txid};
|
||||
use std::str::FromStr;
|
||||
|
||||
// Return a fake wallet that appears to be funded for testing.
|
||||
//
|
||||
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||
// sats are the transaction fee.
|
||||
pub fn get_funded_wallet_with_change(
|
||||
descriptor: &str,
|
||||
change: Option<&str>,
|
||||
) -> (Wallet, bitcoin::Txid) {
|
||||
let mut wallet = Wallet::new_no_persist(descriptor, change, Network::Regtest).unwrap();
|
||||
let change_address = wallet.get_address(AddressIndex::New).address;
|
||||
let sendto_address = Address::from_str("bcrt1q3qtze4ys45tgdvguj66zrk4fu6hq3a3v9pfly5")
|
||||
.expect("address")
|
||||
.require_network(Network::Regtest)
|
||||
.unwrap();
|
||||
|
||||
let tx0 = Transaction {
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: Txid::all_zeros(),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Default::default(),
|
||||
sequence: Default::default(),
|
||||
witness: Default::default(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 76_000,
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
}],
|
||||
};
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx0.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
script_sig: Default::default(),
|
||||
sequence: Default::default(),
|
||||
witness: Default::default(),
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 50_000,
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
},
|
||||
TxOut {
|
||||
value: 25_000,
|
||||
script_pubkey: sendto_address.script_pubkey(),
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 1_000,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_checkpoint(BlockId {
|
||||
height: 2_000,
|
||||
hash: BlockHash::all_zeros(),
|
||||
})
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_tx(
|
||||
tx0,
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 1_000,
|
||||
time: 100,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
wallet
|
||||
.insert_tx(
|
||||
tx1.clone(),
|
||||
ConfirmationTime::Confirmed {
|
||||
height: 2_000,
|
||||
time: 200,
|
||||
},
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
(wallet, tx1.txid())
|
||||
}
|
||||
|
||||
// Return a fake wallet that appears to be funded for testing.
|
||||
//
|
||||
// The funded wallet containing a tx with a 76_000 sats input and two outputs, one spending 25_000
|
||||
// to a foreign address and one returning 50_000 back to the wallet as change. The remaining 1000
|
||||
// sats are the transaction fee.
|
||||
pub fn get_funded_wallet(descriptor: &str) -> (Wallet, bitcoin::Txid) {
|
||||
get_funded_wallet_with_change(descriptor, None)
|
||||
}
|
||||
|
||||
pub fn get_test_wpkh() -> &'static str {
|
||||
"wpkh(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW)"
|
||||
}
|
||||
|
||||
pub fn get_test_single_sig_csv() -> &'static str {
|
||||
// and(pk(Alice),older(6))
|
||||
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),older(6)))"
|
||||
}
|
||||
|
||||
pub fn get_test_a_or_b_plus_csv() -> &'static str {
|
||||
// or(pk(Alice),and(pk(Bob),older(144)))
|
||||
"wsh(or_d(pk(cRjo6jqfVNP33HhSS76UhXETZsGTZYx8FMFvR9kpbtCSV1PmdZdu),and_v(v:pk(cMnkdebixpXMPfkcNEjjGin7s94hiehAH4mLbYkZoh9KSiNNmqC8),older(144))))"
|
||||
}
|
||||
|
||||
pub fn get_test_single_sig_cltv() -> &'static str {
|
||||
// and(pk(Alice),after(100000))
|
||||
"wsh(and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100000)))"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_single_sig() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG)"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_with_taptree() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_with_taptree_both_priv() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{pk(cPZzKuNmpuUjD1e8jUU4PVzy2b5LngbSip8mBsxf4e7rSFZVb4Uh),pk(cNaQCDwmmh4dS9LzCgVtyy1e1xjCJ21GUDHe9K98nzb689JvinGV)})"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_repeated_key() -> &'static str {
|
||||
"tr(b511bd5771e47ee27558b1765e87b541668304ec567721c7b880edc0a010da55,{and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(100)),and_v(v:pk(cVpPVruEDdmutPzisEsYvtST1usBR3ntr8pXSyt6D2YYqXRyPcFW),after(200))})"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_single_sig_xprv() -> &'static str {
|
||||
"tr(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*)"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_with_taptree_xprv() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(tprv8ZgxMBicQKsPdDArR4xSAECuVxeX1jwwSXR4ApKbkYgZiziDc4LdBy2WvJeGDfUSE4UT4hHhbgEwbdq8ajjUHiKDegkwrNU6V55CxcxonVN/*),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
|
||||
pub fn get_test_tr_dup_keys() -> &'static str {
|
||||
"tr(cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG,{pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642),pk(8aee2b8120a5f157f1223f72b5e62b825831a27a9fdf427db7cc697494d4a642)})"
|
||||
}
|
||||
192
crates/bdk/tests/psbt.rs
Normal file
192
crates/bdk/tests/psbt.rs
Normal file
@@ -0,0 +1,192 @@
|
||||
use bdk::bitcoin::TxIn;
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{psbt, FeeRate, SignOptions};
|
||||
use bitcoin::psbt::PartiallySignedTransaction as Psbt;
|
||||
use core::str::FromStr;
|
||||
mod common;
|
||||
use common::*;
|
||||
|
||||
// from bip 174
|
||||
const PSBT_STR: &str = "cHNidP8BAKACAAAAAqsJSaCMWvfEm4IS9Bfi8Vqz9cM9zxU4IagTn4d6W3vkAAAAAAD+////qwlJoIxa98SbghL0F+LxWrP1wz3PFTghqBOfh3pbe+QBAAAAAP7///8CYDvqCwAAAAAZdqkUdopAu9dAy+gdmI5x3ipNXHE5ax2IrI4kAAAAAAAAGXapFG9GILVT+glechue4O/p+gOcykWXiKwAAAAAAAEHakcwRAIgR1lmF5fAGwNrJZKJSGhiGDR9iYZLcZ4ff89X0eURZYcCIFMJ6r9Wqk2Ikf/REf3xM286KdqGbX+EhtdVRs7tr5MZASEDXNxh/HupccC1AaZGoqg7ECy0OIEhfKaC3Ibi1z+ogpIAAQEgAOH1BQAAAAAXqRQ1RebjO4MsRwUPJNPuuTycA5SLx4cBBBYAFIXRNTfy4mVAWjTbr6nj3aAfuCMIAAAA";
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_psbt_input_legacy() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_psbt_input_segwit() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.inputs.push(psbt_bip.inputs[1].clone());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic(expected = "InputIndexOutOfRange")]
|
||||
fn test_psbt_malformed_tx_input() {
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
psbt.unsigned_tx.input.push(TxIn::default());
|
||||
let options = SignOptions {
|
||||
trust_witness_utxo: true,
|
||||
..Default::default()
|
||||
};
|
||||
let _ = wallet.sign(&mut psbt, options).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_sign_with_finalized() {
|
||||
let psbt_bip = Psbt::from_str(PSBT_STR).unwrap();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_wpkh());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
|
||||
// add a finalized input
|
||||
psbt.inputs.push(psbt_bip.inputs[0].clone());
|
||||
psbt.unsigned_tx
|
||||
.input
|
||||
.push(psbt_bip.unsigned_tx.input[0].clone());
|
||||
|
||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_witness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
|
||||
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wallet.get_address(New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let fee_amount = psbt.fee_amount();
|
||||
assert!(fee_amount.is_some());
|
||||
let unfinalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
|
||||
let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
let finalized_fee_rate = psbt.fee_rate().unwrap();
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate.as_sat_per_vb() < unfinalized_fee_rate.as_sat_per_vb());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_missing_txout() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = 1.2345;
|
||||
|
||||
let (mut wpkh_wallet, _) = get_funded_wallet("wpkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = wpkh_wallet.get_address(New);
|
||||
let mut builder = wpkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut wpkh_psbt = builder.finish().unwrap();
|
||||
|
||||
wpkh_psbt.inputs[0].witness_utxo = None;
|
||||
wpkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
assert!(wpkh_psbt.fee_amount().is_none());
|
||||
assert!(wpkh_psbt.fee_rate().is_none());
|
||||
|
||||
let (mut pkh_wallet, _) = get_funded_wallet("pkh(tprv8ZgxMBicQKsPd3EupYiPRhaMooHKUHJxNsTfYuScep13go8QFfHdtkG9nRkFGb7busX4isf6X9dURGCoKgitaApQ6MupRhZMcELAxTBRJgS/*)");
|
||||
let addr = pkh_wallet.get_address(New);
|
||||
let mut builder = pkh_wallet.build_tx();
|
||||
builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
builder.fee_rate(FeeRate::from_sat_per_vb(expected_fee_rate));
|
||||
let mut pkh_psbt = builder.finish().unwrap();
|
||||
|
||||
pkh_psbt.inputs[0].non_witness_utxo = None;
|
||||
assert!(pkh_psbt.fee_amount().is_none());
|
||||
assert!(pkh_psbt.fee_rate().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_multiple_internalkey_signers() {
|
||||
use bdk::signer::{SignerContext, SignerOrdering, SignerWrapper};
|
||||
use bdk::KeychainKind;
|
||||
use bitcoin::{secp256k1::Secp256k1, PrivateKey};
|
||||
use miniscript::psbt::PsbtExt;
|
||||
use std::sync::Arc;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let (mut wallet, _) = get_funded_wallet(get_test_tr_single_sig());
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.add_recipient(send_to.script_pubkey(), 10_000);
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
// Adds a signer for the wrong internal key, bdk should not use this key to sign
|
||||
wallet.add_signer(
|
||||
KeychainKind::External,
|
||||
// A signerordering lower than 100, bdk will use this signer first
|
||||
SignerOrdering(0),
|
||||
Arc::new(SignerWrapper::new(
|
||||
PrivateKey::from_wif("5J5PZqvCe1uThJ3FZeUUFLCh2FuK9pZhtEK4MzhNmugqTmxCdwE").unwrap(),
|
||||
SignerContext::Tap {
|
||||
is_internal_key: true,
|
||||
},
|
||||
)),
|
||||
);
|
||||
let _ = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
// Checks that we signed using the right key
|
||||
assert!(
|
||||
psbt.finalize_mut(&secp).is_ok(),
|
||||
"The wrong internal key was used"
|
||||
);
|
||||
}
|
||||
3678
crates/bdk/tests/wallet.rs
Normal file
3678
crates/bdk/tests/wallet.rs
Normal file
File diff suppressed because it is too large
Load Diff
28
crates/bitcoind_rpc/Cargo.toml
Normal file
28
crates/bitcoind_rpc/Cargo.toml
Normal file
@@ -0,0 +1,28 @@
|
||||
[package]
|
||||
name = "bdk_bitcoind_rpc"
|
||||
version = "0.6.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_bitcoind_rpc"
|
||||
description = "This crate is used for emitting blockchain data from the `bitcoind` RPC interface."
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.17" }
|
||||
bdk_chain = { path = "../chain", version = "0.11", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bitcoind = { version = "0.33", features = ["25_0"] }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "bdk_chain/std"]
|
||||
serde = ["bitcoin/serde", "bdk_chain/serde"]
|
||||
3
crates/bitcoind_rpc/README.md
Normal file
3
crates/bitcoind_rpc/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# BDK Bitcoind RPC
|
||||
|
||||
This crate is used for emitting blockchain data from the `bitcoind` RPC interface.
|
||||
328
crates/bitcoind_rpc/src/lib.rs
Normal file
328
crates/bitcoind_rpc/src/lib.rs
Normal file
@@ -0,0 +1,328 @@
|
||||
//! This crate is used for emitting blockchain data from the `bitcoind` RPC interface. It does not
|
||||
//! use the wallet RPC API, so this crate can be used with wallet-disabled Bitcoin Core nodes.
|
||||
//!
|
||||
//! [`Emitter`] is the main structure which sources blockchain data from [`bitcoincore_rpc::Client`].
|
||||
//!
|
||||
//! To only get block updates (exclude mempool transactions), the caller can use
|
||||
//! [`Emitter::next_block`] or/and [`Emitter::next_header`] until it returns `Ok(None)` (which means
|
||||
//! the chain tip is reached). A separate method, [`Emitter::mempool`] can be used to emit the whole
|
||||
//! mempool.
|
||||
#![warn(missing_docs)]
|
||||
|
||||
use bdk_chain::{local_chain::CheckPoint, BlockId};
|
||||
use bitcoin::{block::Header, Block, BlockHash, Transaction};
|
||||
pub use bitcoincore_rpc;
|
||||
use bitcoincore_rpc::bitcoincore_rpc_json;
|
||||
|
||||
/// The [`Emitter`] is used to emit data sourced from [`bitcoincore_rpc::Client`].
|
||||
///
|
||||
/// Refer to [module-level documentation] for more.
|
||||
///
|
||||
/// [module-level documentation]: crate
|
||||
pub struct Emitter<'c, C> {
|
||||
client: &'c C,
|
||||
start_height: u32,
|
||||
|
||||
/// The checkpoint of the last-emitted block that is in the best chain. If it is later found
|
||||
/// that the block is no longer in the best chain, it will be popped off from here.
|
||||
last_cp: CheckPoint,
|
||||
|
||||
/// The block result returned from rpc of the last-emitted block. As this result contains the
|
||||
/// next block's block hash (which we use to fetch the next block), we set this to `None`
|
||||
/// whenever there are no more blocks, or the next block is no longer in the best chain. This
|
||||
/// gives us an opportunity to re-fetch this result.
|
||||
last_block: Option<bitcoincore_rpc_json::GetBlockResult>,
|
||||
|
||||
/// The latest first-seen epoch of emitted mempool transactions. This is used to determine
|
||||
/// whether a mempool transaction is already emitted.
|
||||
last_mempool_time: usize,
|
||||
|
||||
/// The last emitted block during our last mempool emission. This is used to determine whether
|
||||
/// there has been a reorg since our last mempool emission.
|
||||
last_mempool_tip: Option<u32>,
|
||||
}
|
||||
|
||||
impl<'c, C: bitcoincore_rpc::RpcApi> Emitter<'c, C> {
|
||||
/// Construct a new [`Emitter`].
|
||||
///
|
||||
/// `last_cp` informs the emitter of the chain we are starting off with. This way, the emitter
|
||||
/// can start emission from a block that connects to the original chain.
|
||||
///
|
||||
/// `start_height` starts emission from a given height (if there are no conflicts with the
|
||||
/// original chain).
|
||||
pub fn new(client: &'c C, last_cp: CheckPoint, start_height: u32) -> Self {
|
||||
Self {
|
||||
client,
|
||||
start_height,
|
||||
last_cp,
|
||||
last_block: None,
|
||||
last_mempool_time: 0,
|
||||
last_mempool_tip: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Emit mempool transactions, alongside their first-seen unix timestamps.
|
||||
///
|
||||
/// This method emits each transaction only once, unless we cannot guarantee the transaction's
|
||||
/// ancestors are already emitted.
|
||||
///
|
||||
/// To understand why, consider a receiver which filters transactions based on whether it
|
||||
/// alters the UTXO set of tracked script pubkeys. If an emitted mempool transaction spends a
|
||||
/// tracked UTXO which is confirmed at height `h`, but the receiver has only seen up to block
|
||||
/// of height `h-1`, we want to re-emit this transaction until the receiver has seen the block
|
||||
/// at height `h`.
|
||||
pub fn mempool(&mut self) -> Result<Vec<(Transaction, u64)>, bitcoincore_rpc::Error> {
|
||||
let client = self.client;
|
||||
|
||||
// This is the emitted tip height during the last mempool emission.
|
||||
let prev_mempool_tip = self
|
||||
.last_mempool_tip
|
||||
// We use `start_height - 1` as we cannot guarantee that the block at
|
||||
// `start_height` has been emitted.
|
||||
.unwrap_or(self.start_height.saturating_sub(1));
|
||||
|
||||
// Mempool txs come with a timestamp of when the tx is introduced to the mempool. We keep
|
||||
// track of the latest mempool tx's timestamp to determine whether we have seen a tx
|
||||
// before. `prev_mempool_time` is the previous timestamp and `last_time` records what will
|
||||
// be the new latest timestamp.
|
||||
let prev_mempool_time = self.last_mempool_time;
|
||||
let mut latest_time = prev_mempool_time;
|
||||
|
||||
let txs_to_emit = client
|
||||
.get_raw_mempool_verbose()?
|
||||
.into_iter()
|
||||
.filter_map({
|
||||
let latest_time = &mut latest_time;
|
||||
move |(txid, tx_entry)| -> Option<Result<_, bitcoincore_rpc::Error>> {
|
||||
let tx_time = tx_entry.time as usize;
|
||||
if tx_time > *latest_time {
|
||||
*latest_time = tx_time;
|
||||
}
|
||||
|
||||
// Avoid emitting transactions that are already emitted if we can guarantee
|
||||
// blocks containing ancestors are already emitted. The bitcoind rpc interface
|
||||
// provides us with the block height that the tx is introduced to the mempool.
|
||||
// If we have already emitted the block of height, we can assume that all
|
||||
// ancestor txs have been processed by the receiver.
|
||||
let is_already_emitted = tx_time <= prev_mempool_time;
|
||||
let is_within_height = tx_entry.height <= prev_mempool_tip as _;
|
||||
if is_already_emitted && is_within_height {
|
||||
return None;
|
||||
}
|
||||
|
||||
let tx = match client.get_raw_transaction(&txid, None) {
|
||||
Ok(tx) => tx,
|
||||
// the tx is confirmed or evicted since `get_raw_mempool_verbose`
|
||||
Err(err) if err.is_not_found_error() => return None,
|
||||
Err(err) => return Some(Err(err)),
|
||||
};
|
||||
|
||||
Some(Ok((tx, tx_time as u64)))
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
self.last_mempool_time = latest_time;
|
||||
self.last_mempool_tip = Some(self.last_cp.height());
|
||||
|
||||
Ok(txs_to_emit)
|
||||
}
|
||||
|
||||
/// Emit the next block height and header (if any).
|
||||
pub fn next_header(&mut self) -> Result<Option<BlockEvent<Header>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block_header(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
}
|
||||
|
||||
/// Emit the next block height and block (if any).
|
||||
pub fn next_block(&mut self) -> Result<Option<BlockEvent<Block>>, bitcoincore_rpc::Error> {
|
||||
Ok(poll(self, |hash| self.client.get_block(hash))?
|
||||
.map(|(checkpoint, block)| BlockEvent { block, checkpoint }))
|
||||
}
|
||||
}
|
||||
|
||||
/// A newly emitted block from [`Emitter`].
|
||||
#[derive(Debug)]
|
||||
pub struct BlockEvent<B> {
|
||||
/// Either a full [`Block`] or [`Header`] of the new block.
|
||||
pub block: B,
|
||||
|
||||
/// The checkpoint of the new block.
|
||||
///
|
||||
/// A [`CheckPoint`] is a node of a linked list of [`BlockId`]s. This checkpoint is linked to
|
||||
/// all [`BlockId`]s originally passed in [`Emitter::new`] as well as emitted blocks since then.
|
||||
/// These blocks are guaranteed to be of the same chain.
|
||||
///
|
||||
/// This is important as BDK structures require block-to-apply to be connected with another
|
||||
/// block in the original chain.
|
||||
pub checkpoint: CheckPoint,
|
||||
}
|
||||
|
||||
impl<B> BlockEvent<B> {
|
||||
/// The block height of this new block.
|
||||
pub fn block_height(&self) -> u32 {
|
||||
self.checkpoint.height()
|
||||
}
|
||||
|
||||
/// The block hash of this new block.
|
||||
pub fn block_hash(&self) -> BlockHash {
|
||||
self.checkpoint.hash()
|
||||
}
|
||||
|
||||
/// The [`BlockId`] of a previous block that this block connects to.
|
||||
///
|
||||
/// This either returns a [`BlockId`] of a previously emitted block or from the chain we started
|
||||
/// with (passed in as `last_cp` in [`Emitter::new`]).
|
||||
///
|
||||
/// This value is derived from [`BlockEvent::checkpoint`].
|
||||
pub fn connected_to(&self) -> BlockId {
|
||||
match self.checkpoint.prev() {
|
||||
Some(prev_cp) => prev_cp.block_id(),
|
||||
// there is no previous checkpoint, so just connect with itself
|
||||
None => self.checkpoint.block_id(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum PollResponse {
|
||||
Block(bitcoincore_rpc_json::GetBlockResult),
|
||||
NoMoreBlocks,
|
||||
/// Fetched block is not in the best chain.
|
||||
BlockNotInBestChain,
|
||||
AgreementFound(bitcoincore_rpc_json::GetBlockResult, CheckPoint),
|
||||
/// Force the genesis checkpoint down the receiver's throat.
|
||||
AgreementPointNotFound(BlockHash),
|
||||
}
|
||||
|
||||
fn poll_once<C>(emitter: &Emitter<C>) -> Result<PollResponse, bitcoincore_rpc::Error>
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
{
|
||||
let client = emitter.client;
|
||||
|
||||
if let Some(last_res) = &emitter.last_block {
|
||||
let next_hash = if last_res.height < emitter.start_height as _ {
|
||||
// enforce start height
|
||||
let next_hash = client.get_block_hash(emitter.start_height as _)?;
|
||||
// make sure last emission is still in best chain
|
||||
if client.get_block_hash(last_res.height as _)? != last_res.hash {
|
||||
return Ok(PollResponse::BlockNotInBestChain);
|
||||
}
|
||||
next_hash
|
||||
} else {
|
||||
match last_res.nextblockhash {
|
||||
None => return Ok(PollResponse::NoMoreBlocks),
|
||||
Some(next_hash) => next_hash,
|
||||
}
|
||||
};
|
||||
|
||||
let res = client.get_block_info(&next_hash)?;
|
||||
if res.confirmations < 0 {
|
||||
return Ok(PollResponse::BlockNotInBestChain);
|
||||
}
|
||||
|
||||
return Ok(PollResponse::Block(res));
|
||||
}
|
||||
|
||||
for cp in emitter.last_cp.iter() {
|
||||
let res = match client.get_block_info(&cp.hash()) {
|
||||
// block not in best chain
|
||||
Ok(res) if res.confirmations < 0 => continue,
|
||||
Ok(res) => res,
|
||||
Err(e) if e.is_not_found_error() => {
|
||||
if cp.height() > 0 {
|
||||
continue;
|
||||
}
|
||||
// if we can't find genesis block, we can't create an update that connects
|
||||
break;
|
||||
}
|
||||
Err(e) => return Err(e),
|
||||
};
|
||||
|
||||
// agreement point found
|
||||
return Ok(PollResponse::AgreementFound(res, cp));
|
||||
}
|
||||
|
||||
let genesis_hash = client.get_block_hash(0)?;
|
||||
Ok(PollResponse::AgreementPointNotFound(genesis_hash))
|
||||
}
|
||||
|
||||
fn poll<C, V, F>(
|
||||
emitter: &mut Emitter<C>,
|
||||
get_item: F,
|
||||
) -> Result<Option<(CheckPoint, V)>, bitcoincore_rpc::Error>
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
F: Fn(&BlockHash) -> Result<V, bitcoincore_rpc::Error>,
|
||||
{
|
||||
loop {
|
||||
match poll_once(emitter)? {
|
||||
PollResponse::Block(res) => {
|
||||
let height = res.height as u32;
|
||||
let hash = res.hash;
|
||||
let item = get_item(&hash)?;
|
||||
|
||||
let new_cp = emitter
|
||||
.last_cp
|
||||
.clone()
|
||||
.push(BlockId { height, hash })
|
||||
.expect("must push");
|
||||
emitter.last_cp = new_cp.clone();
|
||||
emitter.last_block = Some(res);
|
||||
return Ok(Some((new_cp, item)));
|
||||
}
|
||||
PollResponse::NoMoreBlocks => {
|
||||
emitter.last_block = None;
|
||||
return Ok(None);
|
||||
}
|
||||
PollResponse::BlockNotInBestChain => {
|
||||
emitter.last_block = None;
|
||||
continue;
|
||||
}
|
||||
PollResponse::AgreementFound(res, cp) => {
|
||||
let agreement_h = res.height as u32;
|
||||
|
||||
// The tip during the last mempool emission needs to in the best chain, we reduce
|
||||
// it if it is not.
|
||||
if let Some(h) = emitter.last_mempool_tip.as_mut() {
|
||||
if *h > agreement_h {
|
||||
*h = agreement_h;
|
||||
}
|
||||
}
|
||||
|
||||
// get rid of evicted blocks
|
||||
emitter.last_cp = cp;
|
||||
emitter.last_block = Some(res);
|
||||
continue;
|
||||
}
|
||||
PollResponse::AgreementPointNotFound(genesis_hash) => {
|
||||
emitter.last_cp = CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: genesis_hash,
|
||||
});
|
||||
emitter.last_block = None;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends [`bitcoincore_rpc::Error`].
|
||||
pub trait BitcoindRpcErrorExt {
|
||||
/// Returns whether the error is a "not found" error.
|
||||
///
|
||||
/// This is useful since [`Emitter`] emits [`Result<_, bitcoincore_rpc::Error>`]s as
|
||||
/// [`Iterator::Item`].
|
||||
fn is_not_found_error(&self) -> bool;
|
||||
}
|
||||
|
||||
impl BitcoindRpcErrorExt for bitcoincore_rpc::Error {
|
||||
fn is_not_found_error(&self) -> bool {
|
||||
if let bitcoincore_rpc::Error::JsonRpc(bitcoincore_rpc::jsonrpc::Error::Rpc(rpc_err)) = self
|
||||
{
|
||||
rpc_err.code == -5
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
864
crates/bitcoind_rpc/tests/test_emitter.rs
Normal file
864
crates/bitcoind_rpc/tests/test_emitter.rs
Normal file
@@ -0,0 +1,864 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, BlockHash, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, Block, CompactTarget, OutPoint, ScriptBuf, ScriptHash, Transaction,
|
||||
TxIn, TxOut, WScriptHash,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
|
||||
struct TestEnv {
|
||||
#[allow(dead_code)]
|
||||
daemon: bitcoind::BitcoinD,
|
||||
client: bitcoincore_rpc::Client,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> anyhow::Result<Self> {
|
||||
let daemon = match std::env::var_os("TEST_BITCOIND") {
|
||||
Some(bitcoind_path) => bitcoind::BitcoinD::new(bitcoind_path),
|
||||
None => bitcoind::BitcoinD::from_downloaded(),
|
||||
}?;
|
||||
let client = bitcoincore_rpc::Client::new(
|
||||
&daemon.rpc_url(),
|
||||
bitcoincore_rpc::Auth::CookieFile(daemon.params.cookie_file.clone()),
|
||||
)?;
|
||||
Ok(Self { daemon, client })
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self.client.get_new_address(None, None)?.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
|
||||
fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||
let bt = self.client.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let txdata = vec![Transaction {
|
||||
version: 1,
|
||||
lock_time: bitcoin::absolute::LockTime::from_height(0)?,
|
||||
input: vec![TxIn {
|
||||
previous_output: bitcoin::OutPoint::default(),
|
||||
script_sig: ScriptBuf::builder()
|
||||
.push_int(bt.height as _)
|
||||
// randomn number so that re-mining creates unique block
|
||||
.push_int(random())
|
||||
.into_script(),
|
||||
sequence: bitcoin::Sequence::default(),
|
||||
witness: bitcoin::Witness::new(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 0,
|
||||
script_pubkey: ScriptBuf::new_p2sh(&ScriptHash::all_zeros()),
|
||||
}],
|
||||
}];
|
||||
|
||||
let bits: [u8; 4] = bt
|
||||
.bits
|
||||
.clone()
|
||||
.try_into()
|
||||
.expect("rpc provided us with invalid bits");
|
||||
|
||||
let mut block = Block {
|
||||
header: Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash: bt.previous_block_hash,
|
||||
merkle_root: TxMerkleNode::all_zeros(),
|
||||
time: Ord::max(bt.min_time, std::time::UNIX_EPOCH.elapsed()?.as_secs()) as u32,
|
||||
bits: CompactTarget::from_consensus(u32::from_be_bytes(bits)),
|
||||
nonce: 0,
|
||||
},
|
||||
txdata,
|
||||
};
|
||||
|
||||
block.header.merkle_root = block.compute_merkle_root().expect("must compute");
|
||||
|
||||
for nonce in 0..=u32::MAX {
|
||||
block.header.nonce = nonce;
|
||||
if block.header.target().is_met_by(block.block_hash()) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
self.client.submit_block(&block)?;
|
||||
Ok((bt.height as usize, block.block_hash()))
|
||||
}
|
||||
|
||||
fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||
let mut hash = self.client.get_best_block_hash()?;
|
||||
for _ in 0..count {
|
||||
let prev_hash = self.client.get_block_info(&hash)?.previousblockhash;
|
||||
self.client.invalidate_block(&hash)?;
|
||||
match prev_hash {
|
||||
Some(prev_hash) => hash = prev_hash,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = self.mine_blocks(count, None);
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||
let start_height = self.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = (0..count)
|
||||
.map(|_| self.mine_empty_block())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
self.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||
let txid = self
|
||||
.client
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ensure that blocks are emitted in order even after reorg.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
/// 2. Emit blocks from [`Emitter`] and update the [`LocalChain`].
|
||||
/// 3. Reorg highest 6 blocks.
|
||||
/// 4. Emit blocks from [`Emitter`] and re-update the [`LocalChain`].
|
||||
#[test]
|
||||
pub fn test_sync_local_chain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(&env.client, local_chain.tip(), 0);
|
||||
|
||||
// mine some blocks and returned the actual block hashes
|
||||
let exp_hashes = {
|
||||
let mut hashes = vec![env.client.get_block_hash(0)?]; // include genesis block
|
||||
hashes.extend(env.mine_blocks(101, None)?);
|
||||
hashes
|
||||
};
|
||||
|
||||
// see if the emitter outputs the right blocks
|
||||
println!("first sync:");
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
assert_eq!(
|
||||
emission.block_hash(),
|
||||
exp_hashes[height as usize],
|
||||
"emitted block hash is unexpected"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
BTreeMap::from([(height, Some(hash))]),
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
local_chain.blocks(),
|
||||
&exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect(),
|
||||
"final local_chain state is unexpected",
|
||||
);
|
||||
|
||||
// perform reorg
|
||||
let reorged_blocks = env.reorg(6)?;
|
||||
let exp_hashes = exp_hashes
|
||||
.iter()
|
||||
.take(exp_hashes.len() - reorged_blocks.len())
|
||||
.chain(&reorged_blocks)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// see if the emitter outputs the right blocks
|
||||
println!("after reorg:");
|
||||
let mut exp_height = exp_hashes.len() - reorged_blocks.len();
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let hash = emission.block_hash();
|
||||
assert_eq!(
|
||||
height, exp_height as u32,
|
||||
"emitted block has unexpected height"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
hash, exp_hashes[height as usize],
|
||||
"emitted block is unexpected"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
local_chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?,
|
||||
if exp_height == exp_hashes.len() - reorged_blocks.len() {
|
||||
core::iter::once((height, Some(hash)))
|
||||
.chain((height + 1..exp_hashes.len() as u32).map(|h| (h, None)))
|
||||
.collect::<bdk_chain::local_chain::ChangeSet>()
|
||||
} else {
|
||||
BTreeMap::from([(height, Some(hash))])
|
||||
},
|
||||
"chain update changeset is unexpected",
|
||||
);
|
||||
|
||||
exp_height += 1;
|
||||
}
|
||||
|
||||
assert_eq!(
|
||||
local_chain.blocks(),
|
||||
&exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect(),
|
||||
"final local_chain state is unexpected after reorg",
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that [`EmittedUpdate::into_tx_graph_update`] behaves appropriately for both mempool and
|
||||
/// block updates.
|
||||
///
|
||||
/// [`EmittedUpdate::into_tx_graph_update`]: bdk_bitcoind_rpc::EmittedUpdate::into_tx_graph_update
|
||||
#[test]
|
||||
fn test_into_tx_graph() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
println!("getting new addresses!");
|
||||
let addr_0 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_1 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let addr_2 = env.client.get_new_address(None, None)?.assume_checked();
|
||||
println!("got new addresses!");
|
||||
|
||||
println!("mining block!");
|
||||
env.mine_blocks(101, None)?;
|
||||
println!("mined blocks!");
|
||||
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut indexed_tx_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut index = SpkTxOutIndex::<usize>::default();
|
||||
index.insert_spk(0, addr_0.script_pubkey());
|
||||
index.insert_spk(1, addr_1.script_pubkey());
|
||||
index.insert_spk(2, addr_2.script_pubkey());
|
||||
index
|
||||
});
|
||||
|
||||
let emitter = &mut Emitter::new(&env.client, chain.tip(), 0);
|
||||
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.is_empty());
|
||||
}
|
||||
|
||||
// send 3 txs to a tracked address, these txs will be in the mempool
|
||||
let exp_txids = {
|
||||
let mut txids = BTreeSet::new();
|
||||
for _ in 0..3 {
|
||||
txids.insert(env.client.send_to_address(
|
||||
&addr_0,
|
||||
Amount::from_sat(10_000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)?);
|
||||
}
|
||||
txids
|
||||
};
|
||||
|
||||
// expect that the next block should be none and we should get 3 txs from mempool
|
||||
{
|
||||
// next block should be `None`
|
||||
assert!(emitter.next_block()?.is_none());
|
||||
|
||||
let mempool_txs = emitter.mempool()?;
|
||||
let indexed_additions = indexed_tx_graph.batch_insert_unconfirmed(mempool_txs);
|
||||
assert_eq!(
|
||||
indexed_additions
|
||||
.graph
|
||||
.txs
|
||||
.iter()
|
||||
.map(|tx| tx.txid())
|
||||
.collect::<BTreeSet<Txid>>(),
|
||||
exp_txids,
|
||||
"changeset should have the 3 mempool transactions",
|
||||
);
|
||||
assert!(indexed_additions.graph.anchors.is_empty());
|
||||
}
|
||||
|
||||
// mine a block that confirms the 3 txs
|
||||
let exp_block_hash = env.mine_blocks(1, None)?[0];
|
||||
let exp_block_height = env.client.get_block_info(&exp_block_hash)?.height as u32;
|
||||
let exp_anchors = exp_txids
|
||||
.iter()
|
||||
.map({
|
||||
let anchor = BlockId {
|
||||
height: exp_block_height,
|
||||
hash: exp_block_hash,
|
||||
};
|
||||
move |&txid| (anchor, txid)
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
// must receive mined block which will confirm the transactions.
|
||||
{
|
||||
let emission = emitter.next_block()?.expect("must get mined block");
|
||||
let height = emission.block_height();
|
||||
let _ = chain.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})?;
|
||||
let indexed_additions = indexed_tx_graph.apply_block_relevant(&emission.block, height);
|
||||
assert!(indexed_additions.graph.txs.is_empty());
|
||||
assert!(indexed_additions.graph.txouts.is_empty());
|
||||
assert_eq!(indexed_additions.graph.anchors, exp_anchors);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure next block emitted after reorg is at reorg height.
|
||||
///
|
||||
/// After a reorg, if the last-emitted block height is equal or greater than the reorg height, and
|
||||
/// the fallback height is equal to or lower than the reorg height, the next block/header emission
|
||||
/// should be at the reorg height.
|
||||
///
|
||||
/// TODO: If the reorg height is lower than the fallback height, how do we find a block height to
|
||||
/// emit that can connect with our receiver chain?
|
||||
#[test]
|
||||
fn ensure_block_emitted_after_reorg_is_at_reorg_height() -> anyhow::Result<()> {
|
||||
const EMITTER_START_HEIGHT: usize = 100;
|
||||
const CHAIN_TIP_HEIGHT: usize = 110;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
EMITTER_START_HEIGHT as _,
|
||||
);
|
||||
|
||||
env.mine_blocks(CHAIN_TIP_HEIGHT, None)?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
for reorg_count in 1..=10 {
|
||||
let replaced_blocks = env.reorg_empty_blocks(reorg_count)?;
|
||||
let next_emission = emitter.next_header()?.expect("must emit block after reorg");
|
||||
assert_eq!(
|
||||
(
|
||||
next_emission.block_height() as usize,
|
||||
next_emission.block_hash()
|
||||
),
|
||||
replaced_blocks[0],
|
||||
"block emitted after reorg should be at the reorg height"
|
||||
);
|
||||
while emitter.next_header()?.is_some() {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn process_block(
|
||||
recv_chain: &mut LocalChain,
|
||||
recv_graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
|
||||
block: Block,
|
||||
block_height: u32,
|
||||
) -> anyhow::Result<()> {
|
||||
recv_chain
|
||||
.apply_update(CheckPoint::from_header(&block.header, block_height).into_update(false))?;
|
||||
let _ = recv_graph.apply_block(block, block_height);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn sync_from_emitter<C>(
|
||||
recv_chain: &mut LocalChain,
|
||||
recv_graph: &mut IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
|
||||
emitter: &mut Emitter<C>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
C: bitcoincore_rpc::RpcApi,
|
||||
{
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
process_block(recv_chain, recv_graph, emission.block, height)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<BlockId, SpkTxOutIndex<()>>,
|
||||
) -> anyhow::Result<Balance> {
|
||||
let chain_tip = recv_chain.tip().block_id();
|
||||
let outpoints = recv_graph.index.outpoints().clone();
|
||||
let balance = recv_graph
|
||||
.graph()
|
||||
.balance(recv_chain, chain_tip, outpoints, |_, _| true);
|
||||
Ok(balance)
|
||||
}
|
||||
|
||||
/// If a block is reorged out, ensure that containing transactions that do not exist in the
|
||||
/// replacement block(s) become unconfirmed.
|
||||
#[test]
|
||||
fn tx_can_become_unconfirmed_after_reorg() -> anyhow::Result<()> {
|
||||
const PREMINE_COUNT: usize = 101;
|
||||
const ADDITIONAL_COUNT: usize = 11;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// setup addresses
|
||||
let addr_to_mine = env.client.get_new_address(None, None)?.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_v0_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bitcoin::Network::Regtest)?;
|
||||
|
||||
// setup receiver
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<BlockId, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
});
|
||||
|
||||
// mine and sync receiver up to tip
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr_to_mine))?;
|
||||
|
||||
// create transactions that are tracked by our receiver
|
||||
for _ in 0..ADDITIONAL_COUNT {
|
||||
let txid = env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
|
||||
// lock outputs that send to `addr_to_track`
|
||||
let outpoints_to_lock = env
|
||||
.client
|
||||
.get_transaction(&txid, None)?
|
||||
.transaction()?
|
||||
.output
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(_, txo)| txo.script_pubkey == spk_to_track)
|
||||
.map(|(vout, _)| OutPoint::new(txid, vout as _))
|
||||
.collect::<Vec<_>>();
|
||||
env.client.lock_unspent(&outpoints_to_lock)?;
|
||||
|
||||
let _ = env.mine_blocks(1, None)?;
|
||||
}
|
||||
|
||||
// get emitter up to tip
|
||||
sync_from_emitter(&mut recv_chain, &mut recv_graph, &mut emitter)?;
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * ADDITIONAL_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
);
|
||||
|
||||
// perform reorgs with different depths
|
||||
for reorg_count in 1..=ADDITIONAL_COUNT {
|
||||
env.reorg_empty_blocks(reorg_count)?;
|
||||
sync_from_emitter(&mut recv_chain, &mut recv_graph, &mut emitter)?;
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * (ADDITIONAL_COUNT - reorg_count) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * reorg_count as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
reorg_count,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure avoid-re-emission-logic is sound when [`Emitter`] is synced to tip.
|
||||
///
|
||||
/// The receiver (bdk_chain structures) is synced to the chain tip, and there is txs in the mempool.
|
||||
/// When we call Emitter::mempool multiple times, mempool txs should not be re-emitted, even if the
|
||||
/// chain tip is extended.
|
||||
#[test]
|
||||
fn mempool_avoids_re_emission() -> anyhow::Result<()> {
|
||||
const BLOCKS_TO_MINE: usize = 101;
|
||||
const MEMPOOL_TX_COUNT: usize = 2;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks and sync up emitter
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(BLOCKS_TO_MINE, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
// have some random txs in mempool
|
||||
let exp_txids = (0..MEMPOOL_TX_COUNT)
|
||||
.map(|_| env.send(&addr, Amount::from_sat(2100)))
|
||||
.collect::<Result<BTreeSet<Txid>, _>>()?;
|
||||
|
||||
// the first emission should include all transactions
|
||||
let emitted_txids = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<Txid>>();
|
||||
assert_eq!(
|
||||
emitted_txids, exp_txids,
|
||||
"all mempool txs should be emitted"
|
||||
);
|
||||
|
||||
// second emission should be empty
|
||||
assert!(
|
||||
emitter.mempool()?.is_empty(),
|
||||
"second emission should be empty"
|
||||
);
|
||||
|
||||
// mine empty blocks + sync up our emitter -> we should still not re-emit
|
||||
for _ in 0..BLOCKS_TO_MINE {
|
||||
env.mine_empty_block()?;
|
||||
}
|
||||
while emitter.next_header()?.is_some() {}
|
||||
assert!(
|
||||
emitter.mempool()?.is_empty(),
|
||||
"third emission, after chain tip is extended, should also be empty"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure mempool tx is still re-emitted if [`Emitter`] has not reached the tx's introduction
|
||||
/// height.
|
||||
///
|
||||
/// We introduce a mempool tx after each block, where blocks are empty (does not confirm previous
|
||||
/// mempool txs). Then we emit blocks from [`Emitter`] (intertwining `mempool` calls). We check
|
||||
/// that `mempool` should always re-emit txs that have introduced at a height greater than the last
|
||||
/// emitted block height.
|
||||
#[test]
|
||||
fn mempool_re_emits_if_tx_introduction_height_not_reached() -> anyhow::Result<()> {
|
||||
const PREMINE_COUNT: usize = 101;
|
||||
const MEMPOOL_TX_COUNT: usize = 21;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance, sync emitter up to tip
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
while emitter.next_header()?.is_some() {}
|
||||
|
||||
// mine blocks to introduce txs to mempool at different heights
|
||||
let tx_introductions = (0..MEMPOOL_TX_COUNT)
|
||||
.map(|_| -> anyhow::Result<_> {
|
||||
let (height, _) = env.mine_empty_block()?;
|
||||
let txid = env.send(&addr, Amount::from_sat(2100))?;
|
||||
Ok((height, txid))
|
||||
})
|
||||
.collect::<anyhow::Result<BTreeSet<_>>>()?;
|
||||
|
||||
assert_eq!(
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
|
||||
"first mempool emission should include all txs",
|
||||
);
|
||||
assert_eq!(
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
tx_introductions.iter().map(|&(_, txid)| txid).collect(),
|
||||
"second mempool emission should still include all txs",
|
||||
);
|
||||
|
||||
// At this point, the emitter has seen all mempool transactions. It should only re-emit those
|
||||
// that have introduction heights less than the emitter's last-emitted block tip.
|
||||
while let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
// We call `mempool()` twice.
|
||||
// The second call (at height `h`) should skip the tx introduced at height `h`.
|
||||
for try_index in 0..2 {
|
||||
let exp_txids = tx_introductions
|
||||
.range((height as usize + try_index, Txid::all_zeros())..)
|
||||
.map(|&(_, txid)| txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let emitted_txids = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
emitted_txids, exp_txids,
|
||||
"\n emission {} (try {}) must only contain txs introduced at that height or lower: \n\t missing: {:?} \n\t extra: {:?}",
|
||||
height,
|
||||
try_index,
|
||||
exp_txids
|
||||
.difference(&emitted_txids)
|
||||
.map(|txid| (txid, tx_introductions.iter().find_map(|(h, id)| if id == txid { Some(h) } else { None }).unwrap()))
|
||||
.collect::<Vec<_>>(),
|
||||
emitted_txids
|
||||
.difference(&exp_txids)
|
||||
.map(|txid| (txid, tx_introductions.iter().find_map(|(h, id)| if id == txid { Some(h) } else { None }).unwrap()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure we force re-emit all mempool txs after reorg.
|
||||
#[test]
|
||||
fn mempool_during_reorg() -> anyhow::Result<()> {
|
||||
const TIP_DIFF: usize = 10;
|
||||
const PREMINE_COUNT: usize = 101;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance
|
||||
let addr = env.client.get_new_address(None, None)?.assume_checked();
|
||||
env.mine_blocks(PREMINE_COUNT, Some(addr.clone()))?;
|
||||
|
||||
// introduce mempool tx at each block extension
|
||||
for _ in 0..TIP_DIFF {
|
||||
env.mine_empty_block()?;
|
||||
env.send(&addr, Amount::from_sat(2100))?;
|
||||
}
|
||||
|
||||
// sync emitter to tip, first mempool emission should include all txs (as we haven't emitted
|
||||
// from the mempool yet)
|
||||
while emitter.next_header()?.is_some() {}
|
||||
assert_eq!(
|
||||
emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>(),
|
||||
env.client
|
||||
.get_raw_mempool()?
|
||||
.into_iter()
|
||||
.collect::<BTreeSet<_>>(),
|
||||
"first mempool emission should include all txs",
|
||||
);
|
||||
|
||||
// perform reorgs at different heights, these reorgs will not confirm transactions in the
|
||||
// mempool
|
||||
for reorg_count in 1..TIP_DIFF {
|
||||
println!("REORG COUNT: {}", reorg_count);
|
||||
env.reorg_empty_blocks(reorg_count)?;
|
||||
|
||||
// This is a map of mempool txids to tip height where the tx was introduced to the mempool
|
||||
// we recalculate this at every loop as reorgs may evict transactions from mempool. We use
|
||||
// the introduction height to determine whether we expect a tx to appear in a mempool
|
||||
// emission.
|
||||
// TODO: How can have have reorg logic in `TestEnv` NOT blacklast old blocks first?
|
||||
let tx_introductions = dbg!(env
|
||||
.client
|
||||
.get_raw_mempool_verbose()?
|
||||
.into_iter()
|
||||
.map(|(txid, entry)| (txid, entry.height as usize))
|
||||
.collect::<BTreeMap<_, _>>());
|
||||
|
||||
// `next_header` emits the replacement block of the reorg
|
||||
if let Some(emission) = emitter.next_header()? {
|
||||
let height = emission.block_height();
|
||||
println!("\t- replacement height: {}", height);
|
||||
|
||||
// the mempool emission (that follows the first block emission after reorg) should only
|
||||
// include mempool txs introduced at reorg height or greater
|
||||
let mempool = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_mempool = tx_introductions
|
||||
.iter()
|
||||
.filter(|(_, &intro_h)| intro_h >= (height as usize))
|
||||
.map(|(&txid, _)| txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
mempool, exp_mempool,
|
||||
"the first mempool emission after reorg should only include mempool txs introduced at reorg height or greater"
|
||||
);
|
||||
|
||||
let mempool = emitter
|
||||
.mempool()?
|
||||
.into_iter()
|
||||
.map(|(tx, _)| tx.txid())
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_mempool = tx_introductions
|
||||
.iter()
|
||||
.filter(|&(_, &intro_height)| intro_height > (height as usize))
|
||||
.map(|(&txid, _)| txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
mempool, exp_mempool,
|
||||
"following mempool emissions after reorg should exclude mempool introduction heights <= last emitted block height: \n\t missing: {:?} \n\t extra: {:?}",
|
||||
exp_mempool
|
||||
.difference(&mempool)
|
||||
.map(|txid| (txid, tx_introductions.get(txid).unwrap()))
|
||||
.collect::<Vec<_>>(),
|
||||
mempool
|
||||
.difference(&exp_mempool)
|
||||
.map(|txid| (txid, tx_introductions.get(txid).unwrap()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
}
|
||||
|
||||
// sync emitter to tip
|
||||
while emitter.next_header()?.is_some() {}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// If blockchain re-org includes the start height, emit new start height block
|
||||
///
|
||||
/// 1. mine 101 blocks
|
||||
/// 2. emit blocks 99a, 100a
|
||||
/// 3. invalidate blocks 99a, 100a, 101a
|
||||
/// 4. mine new blocks 99b, 100b, 101b
|
||||
/// 5. emit block 99b
|
||||
///
|
||||
/// The block hash of 99b should be different than 99a, but their previous block hashes should
|
||||
/// be the same.
|
||||
#[test]
|
||||
fn no_agreement_point() -> anyhow::Result<()> {
|
||||
const PREMINE_COUNT: usize = 101;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
// start height is 99
|
||||
let mut emitter = Emitter::new(
|
||||
&env.client,
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.client.get_block_hash(0)?,
|
||||
}),
|
||||
(PREMINE_COUNT - 2) as u32,
|
||||
);
|
||||
|
||||
// mine 101 blocks
|
||||
env.mine_blocks(PREMINE_COUNT, None)?;
|
||||
|
||||
// emit block 99a
|
||||
let block_header_99a = emitter.next_header()?.expect("block 99a header").block;
|
||||
let block_hash_99a = block_header_99a.block_hash();
|
||||
let block_hash_98a = block_header_99a.prev_blockhash;
|
||||
|
||||
// emit block 100a
|
||||
let block_header_100a = emitter.next_header()?.expect("block 100a header").block;
|
||||
let block_hash_100a = block_header_100a.block_hash();
|
||||
|
||||
// get hash for block 101a
|
||||
let block_hash_101a = env.client.get_block_hash(101)?;
|
||||
|
||||
// invalidate blocks 99a, 100a, 101a
|
||||
env.client.invalidate_block(&block_hash_99a)?;
|
||||
env.client.invalidate_block(&block_hash_100a)?;
|
||||
env.client.invalidate_block(&block_hash_101a)?;
|
||||
|
||||
// mine new blocks 99b, 100b, 101b
|
||||
env.mine_blocks(3, None)?;
|
||||
|
||||
// emit block header 99b
|
||||
let block_header_99b = emitter.next_header()?.expect("block 99b header").block;
|
||||
let block_hash_99b = block_header_99b.block_hash();
|
||||
let block_hash_98b = block_header_99b.prev_blockhash;
|
||||
|
||||
assert_ne!(block_hash_99a, block_hash_99b);
|
||||
assert_eq!(block_hash_98a, block_hash_98b);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
30
crates/chain/Cargo.toml
Normal file
30
crates/chain/Cargo.toml
Normal file
@@ -0,0 +1,30 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.11.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_chain"
|
||||
description = "Collection of core structures for Bitcoin Dev Kit."
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
# For no-std, remember to enable the bitcoin/no-std feature
|
||||
bitcoin = { version = "0.30.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive"] }
|
||||
|
||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bitcoin/std", "miniscript/std"]
|
||||
serde = ["serde_crate", "bitcoin/serde"]
|
||||
3
crates/chain/README.md
Normal file
3
crates/chain/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# BDK Chain
|
||||
|
||||
BDK keychain tracker, tools for storing and indexing chain data.
|
||||
332
crates/chain/src/chain_data.rs
Normal file
332
crates/chain/src/chain_data.rs
Normal file
@@ -0,0 +1,332 @@
|
||||
use bitcoin::{hashes::Hash, BlockHash, OutPoint, TxOut, Txid};
|
||||
|
||||
use crate::{Anchor, AnchorFromBlockPosition, COINBASE_MATURITY};
|
||||
|
||||
/// Represents the observed position of some chain data.
|
||||
///
|
||||
/// The generic `A` should be a [`Anchor`] implementation.
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, core::hash::Hash)]
|
||||
pub enum ChainPosition<A> {
|
||||
/// The chain data is seen as confirmed, and in anchored by `A`.
|
||||
Confirmed(A),
|
||||
/// The chain data is not confirmed and last seen in the mempool at this timestamp.
|
||||
Unconfirmed(u64),
|
||||
}
|
||||
|
||||
impl<A> ChainPosition<A> {
|
||||
/// Returns whether [`ChainPosition`] is confirmed or not.
|
||||
pub fn is_confirmed(&self) -> bool {
|
||||
matches!(self, Self::Confirmed(_))
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Clone> ChainPosition<&A> {
|
||||
/// Maps a [`ChainPosition<&A>`] into a [`ChainPosition<A>`] by cloning the contents.
|
||||
pub fn cloned(self) -> ChainPosition<A> {
|
||||
match self {
|
||||
ChainPosition::Confirmed(a) => ChainPosition::Confirmed(a.clone()),
|
||||
ChainPosition::Unconfirmed(last_seen) => ChainPosition::Unconfirmed(last_seen),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor> ChainPosition<A> {
|
||||
/// Determines the upper bound of the confirmation height.
|
||||
pub fn confirmation_height_upper_bound(&self) -> Option<u32> {
|
||||
match self {
|
||||
ChainPosition::Confirmed(a) => Some(a.confirmation_height_upper_bound()),
|
||||
ChainPosition::Unconfirmed(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Block height and timestamp at which a transaction is confirmed.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub enum ConfirmationTime {
|
||||
/// The transaction is confirmed
|
||||
Confirmed {
|
||||
/// Confirmation height.
|
||||
height: u32,
|
||||
/// Confirmation time in unix seconds.
|
||||
time: u64,
|
||||
},
|
||||
/// The transaction is unconfirmed
|
||||
Unconfirmed {
|
||||
/// The last-seen timestamp in unix seconds.
|
||||
last_seen: u64,
|
||||
},
|
||||
}
|
||||
|
||||
impl ConfirmationTime {
|
||||
/// Construct an unconfirmed variant using the given `last_seen` time in unix seconds.
|
||||
pub fn unconfirmed(last_seen: u64) -> Self {
|
||||
Self::Unconfirmed { last_seen }
|
||||
}
|
||||
|
||||
/// Returns whether [`ConfirmationTime`] is the confirmed variant.
|
||||
pub fn is_confirmed(&self) -> bool {
|
||||
matches!(self, Self::Confirmed { .. })
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ChainPosition<ConfirmationTimeHeightAnchor>> for ConfirmationTime {
|
||||
fn from(observed_as: ChainPosition<ConfirmationTimeHeightAnchor>) -> Self {
|
||||
match observed_as {
|
||||
ChainPosition::Confirmed(a) => Self::Confirmed {
|
||||
height: a.confirmation_height,
|
||||
time: a.confirmation_time,
|
||||
},
|
||||
ChainPosition::Unconfirmed(last_seen) => Self::Unconfirmed { last_seen },
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A reference to a block in the canonical chain.
|
||||
///
|
||||
/// `BlockId` implements [`Anchor`]. When a transaction is anchored to `BlockId`, the confirmation
|
||||
/// block and anchor block are the same block.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct BlockId {
|
||||
/// The height of the block.
|
||||
pub height: u32,
|
||||
/// The hash of the block.
|
||||
pub hash: BlockHash,
|
||||
}
|
||||
|
||||
impl Anchor for BlockId {
|
||||
fn anchor_block(&self) -> Self {
|
||||
*self
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorFromBlockPosition for BlockId {
|
||||
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||
block_id
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for BlockId {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
height: Default::default(),
|
||||
hash: BlockHash::all_zeros(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(u32, BlockHash)> for BlockId {
|
||||
fn from((height, hash): (u32, BlockHash)) -> Self {
|
||||
Self { height, hash }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockId> for (u32, BlockHash) {
|
||||
fn from(block_id: BlockId) -> Self {
|
||||
(block_id.height, block_id.hash)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<(&u32, &BlockHash)> for BlockId {
|
||||
fn from((height, hash): (&u32, &BlockHash)) -> Self {
|
||||
Self {
|
||||
height: *height,
|
||||
hash: *hash,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation height of the transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationHeightAnchor {
|
||||
/// The exact confirmation height of the transaction.
|
||||
///
|
||||
/// It is assumed that this value is never larger than the height of the anchor block.
|
||||
pub confirmation_height: u32,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationHeightAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.anchor_block
|
||||
}
|
||||
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.confirmation_height
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorFromBlockPosition for ConfirmationHeightAnchor {
|
||||
fn from_block_position(_block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||
Self {
|
||||
anchor_block: block_id,
|
||||
confirmation_height: block_id.height,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Anchor`] implementation that also records the exact confirmation time and height of the
|
||||
/// transaction.
|
||||
///
|
||||
/// Note that the confirmation block and the anchor block can be different here.
|
||||
///
|
||||
/// Refer to [`Anchor`] for more details.
|
||||
#[derive(Debug, Default, Clone, PartialEq, Eq, Copy, PartialOrd, Ord, core::hash::Hash)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate")
|
||||
)]
|
||||
pub struct ConfirmationTimeHeightAnchor {
|
||||
/// The confirmation height of the transaction being anchored.
|
||||
pub confirmation_height: u32,
|
||||
/// The confirmation time of the transaction being anchored.
|
||||
pub confirmation_time: u64,
|
||||
/// The anchor block.
|
||||
pub anchor_block: BlockId,
|
||||
}
|
||||
|
||||
impl Anchor for ConfirmationTimeHeightAnchor {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
self.anchor_block
|
||||
}
|
||||
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.confirmation_height
|
||||
}
|
||||
}
|
||||
|
||||
impl AnchorFromBlockPosition for ConfirmationTimeHeightAnchor {
|
||||
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, _tx_pos: usize) -> Self {
|
||||
Self {
|
||||
anchor_block: block_id,
|
||||
confirmation_height: block_id.height,
|
||||
confirmation_time: block.header.time as _,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `TxOut` with as much data as we can retrieve about it
|
||||
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct FullTxOut<A> {
|
||||
/// The position of the transaction in `outpoint` in the overall chain.
|
||||
pub chain_position: ChainPosition<A>,
|
||||
/// The location of the `TxOut`.
|
||||
pub outpoint: OutPoint,
|
||||
/// The `TxOut`.
|
||||
pub txout: TxOut,
|
||||
/// The txid and chain position of the transaction (if any) that has spent this output.
|
||||
pub spent_by: Option<(ChainPosition<A>, Txid)>,
|
||||
/// Whether this output is on a coinbase transaction.
|
||||
pub is_on_coinbase: bool,
|
||||
}
|
||||
|
||||
impl<A: Anchor> FullTxOut<A> {
|
||||
/// Whether the `txout` is considered mature.
|
||||
///
|
||||
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
|
||||
/// method may return false-negatives. In other words, interpreted confirmation count may be
|
||||
/// less than the actual value.
|
||||
///
|
||||
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
|
||||
pub fn is_mature(&self, tip: u32) -> bool {
|
||||
if self.is_on_coinbase {
|
||||
let tx_height = match &self.chain_position {
|
||||
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
|
||||
ChainPosition::Unconfirmed(_) => {
|
||||
debug_assert!(false, "coinbase tx can never be unconfirmed");
|
||||
return false;
|
||||
}
|
||||
};
|
||||
let age = tip.saturating_sub(tx_height);
|
||||
if age + 1 < COINBASE_MATURITY {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
|
||||
/// Whether the utxo is/was/will be spendable with chain `tip`.
|
||||
///
|
||||
/// This method does not take into account the lock time.
|
||||
///
|
||||
/// Depending on the implementation of [`confirmation_height_upper_bound`] in [`Anchor`], this
|
||||
/// method may return false-negatives. In other words, interpreted confirmation count may be
|
||||
/// less than the actual value.
|
||||
///
|
||||
/// [`confirmation_height_upper_bound`]: Anchor::confirmation_height_upper_bound
|
||||
pub fn is_confirmed_and_spendable(&self, tip: u32) -> bool {
|
||||
if !self.is_mature(tip) {
|
||||
return false;
|
||||
}
|
||||
|
||||
let confirmation_height = match &self.chain_position {
|
||||
ChainPosition::Confirmed(anchor) => anchor.confirmation_height_upper_bound(),
|
||||
ChainPosition::Unconfirmed(_) => return false,
|
||||
};
|
||||
if confirmation_height > tip {
|
||||
return false;
|
||||
}
|
||||
|
||||
// if the spending tx is confirmed within tip height, the txout is no longer spendable
|
||||
if let Some((ChainPosition::Confirmed(spending_anchor), _)) = &self.spent_by {
|
||||
if spending_anchor.anchor_block().height <= tip {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn chain_position_ord() {
|
||||
let unconf1 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(10);
|
||||
let unconf2 = ChainPosition::<ConfirmationHeightAnchor>::Unconfirmed(20);
|
||||
let conf1 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 9,
|
||||
anchor_block: BlockId {
|
||||
height: 20,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
let conf2 = ChainPosition::Confirmed(ConfirmationHeightAnchor {
|
||||
confirmation_height: 12,
|
||||
anchor_block: BlockId {
|
||||
height: 15,
|
||||
..Default::default()
|
||||
},
|
||||
});
|
||||
|
||||
assert!(unconf2 > unconf1, "higher last_seen means higher ord");
|
||||
assert!(unconf1 > conf1, "unconfirmed is higher ord than confirmed");
|
||||
assert!(
|
||||
conf2 > conf1,
|
||||
"confirmation_height is higher then it should be higher ord"
|
||||
);
|
||||
}
|
||||
}
|
||||
25
crates/chain/src/chain_oracle.rs
Normal file
25
crates/chain/src/chain_oracle.rs
Normal file
@@ -0,0 +1,25 @@
|
||||
use crate::BlockId;
|
||||
|
||||
/// Represents a service that tracks the blockchain.
|
||||
///
|
||||
/// The main method is [`is_block_in_chain`] which determines whether a given block of [`BlockId`]
|
||||
/// is an ancestor of the `chain_tip`.
|
||||
///
|
||||
/// [`is_block_in_chain`]: Self::is_block_in_chain
|
||||
pub trait ChainOracle {
|
||||
/// Error type.
|
||||
type Error: core::fmt::Debug;
|
||||
|
||||
/// Determines whether `block` of [`BlockId`] exists as an ancestor of `chain_tip`.
|
||||
///
|
||||
/// If `None` is returned, it means the implementation cannot determine whether `block` exists
|
||||
/// under `chain_tip`.
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error>;
|
||||
|
||||
/// Get the best chain's chain tip.
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error>;
|
||||
}
|
||||
18
crates/chain/src/descriptor_ext.rs
Normal file
18
crates/chain/src/descriptor_ext.rs
Normal file
@@ -0,0 +1,18 @@
|
||||
use crate::miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
/// A trait to extend the functionality of a miniscript descriptor.
|
||||
pub trait DescriptorExt {
|
||||
/// Returns the minimum value (in satoshis) at which an output is broadcastable.
|
||||
/// Panics if the descriptor wildcard is hardened.
|
||||
fn dust_value(&self) -> u64;
|
||||
}
|
||||
|
||||
impl DescriptorExt for Descriptor<DescriptorPublicKey> {
|
||||
fn dust_value(&self) -> u64 {
|
||||
self.at_derivation_index(0)
|
||||
.expect("descriptor can't have hardened derivation")
|
||||
.script_pubkey()
|
||||
.dust_value()
|
||||
.to_sat()
|
||||
}
|
||||
}
|
||||
30
crates/chain/src/example_utils.rs
Normal file
30
crates/chain/src/example_utils.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
#![allow(unused)]
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{
|
||||
consensus,
|
||||
hashes::{hex::FromHex, Hash},
|
||||
Transaction,
|
||||
};
|
||||
|
||||
use crate::BlockId;
|
||||
|
||||
pub const RAW_TX_1: &str = "0200000000010116d6174da7183d70d0a7d4dc314d517a7d135db79ad63515028b293a76f4f9d10000000000feffffff023a21fc8350060000160014531c405e1881ef192294b8813631e258bf98ea7a1027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c024730440220591b1a172a122da49ba79a3e79f98aaa03fd7a372f9760da18890b6a327e6010022013e82319231da6c99abf8123d7c07e13cf9bd8d76e113e18dc452e5024db156d012102318a2d558b2936c52e320decd6d92a88d7f530be91b6fe0af5caf41661e77da3ef2e0100";
|
||||
pub const RAW_TX_2: &str = "02000000000101a688607020cfae91a61e7c516b5ef1264d5d77f17200c3866826c6c808ebf1620000000000feffffff021027000000000000225120a60869f0dbcf1dc659c9cecbaf8050135ea9e8cdc487053f1dc6880949dc684c20fd48ff530600001600146886c525e41d4522042bd0b159dfbade2504a6bb024730440220740ff7e665cd20565d4296b549df8d26b941be3f1e3af89a0b60e50c0dbeb69a02206213ab7030cf6edc6c90d4ccf33010644261e029950a688dc0b1a9ebe6ddcc5a012102f2ac6b396a97853cb6cd62242c8ae4842024742074475023532a51e9c53194253e760100";
|
||||
pub const RAW_TX_3: &str = "0200000000010135d67ee47b557e68b8c6223958f597381965ed719f1207ee2b9e20432a24a5dc0100000000feffffff021027000000000000225120a82f29944d65b86ae6b5e5cc75e294ead6c59391a1edc5e016e3498c67fc7bbb62215a5055060000160014070df7671dea67a50c4799a744b5c9be8f4bac690247304402207ebf8d29f71fd03e7e6977b3ea78ca5fcc5c49a42ae822348fc401862fdd766c02201d7e4ff0684ecb008b6142f36ead1b0b4d615524c4f58c261113d361f4427e25012103e6a75e2fab85e5ecad641afc4ffba7222f998649d9f18cac92f0fcc8618883b3ee760100";
|
||||
pub const RAW_TX_4: &str = "02000000000101d00e8f76ed313e19b339ee293c0f52b0325c95e24c8f3966fa353fb2bedbcf580100000000feffffff021027000000000000225120882d74e5d0572d5a816cef0041a96b6c1de832f6f9676d9605c44d5e9a97d3dc9cda55fe53060000160014852b5864b8edd42fab4060c87f818e50780865ff0247304402201dccbb9bed7fba924b6d249c5837cc9b37470c0e3d8fbea77cb59baba3efe6fa0220700cc170916913b9bfc2bc0fefb6af776e8b542c561702f136cddc1c7aa43141012103acec3fc79dbbca745815c2a807dc4e81010c80e308e84913f59cb42a275dad97f3760100";
|
||||
|
||||
pub fn tx_from_hex(s: &str) -> Transaction {
|
||||
let raw = Vec::from_hex(s).expect("data must be in hex");
|
||||
consensus::deserialize(raw.as_slice()).expect("must deserialize")
|
||||
}
|
||||
|
||||
pub fn new_hash<H: Hash>(s: &str) -> H {
|
||||
<H as bitcoin::hashes::Hash>::hash(s.as_bytes())
|
||||
}
|
||||
|
||||
pub fn new_block_id(height: u32, hash: &str) -> BlockId {
|
||||
BlockId {
|
||||
height,
|
||||
hash: new_hash(hash),
|
||||
}
|
||||
}
|
||||
354
crates/chain/src/indexed_tx_graph.rs
Normal file
354
crates/chain/src/indexed_tx_graph.rs
Normal file
@@ -0,0 +1,354 @@
|
||||
//! Contains the [`IndexedTxGraph`] and associated types. Refer to the
|
||||
//! [`IndexedTxGraph`] documentation for more.
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::{Block, OutPoint, Transaction, TxOut, Txid};
|
||||
|
||||
use crate::{
|
||||
keychain,
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, AnchorFromBlockPosition, Append, BlockId,
|
||||
};
|
||||
|
||||
/// The [`IndexedTxGraph`] combines a [`TxGraph`] and an [`Indexer`] implementation.
|
||||
///
|
||||
/// It ensures that [`TxGraph`] and [`Indexer`] are updated atomically.
|
||||
#[derive(Debug)]
|
||||
pub struct IndexedTxGraph<A, I> {
|
||||
/// Transaction index.
|
||||
pub index: I,
|
||||
graph: TxGraph<A>,
|
||||
}
|
||||
|
||||
impl<A, I: Default> Default for IndexedTxGraph<A, I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
index: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, I> IndexedTxGraph<A, I> {
|
||||
/// Construct a new [`IndexedTxGraph`] with a given `index`.
|
||||
pub fn new(index: I) -> Self {
|
||||
Self {
|
||||
index,
|
||||
graph: TxGraph::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference of the internal transaction graph.
|
||||
pub fn graph(&self) -> &TxGraph<A> {
|
||||
&self.graph
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I> {
|
||||
/// Applies the [`ChangeSet`] to the [`IndexedTxGraph`].
|
||||
pub fn apply_changeset(&mut self, changeset: ChangeSet<A, I::ChangeSet>) {
|
||||
self.index.apply_changeset(changeset.indexer);
|
||||
|
||||
for tx in &changeset.graph.txs {
|
||||
self.index.index_tx(tx);
|
||||
}
|
||||
for (&outpoint, txout) in &changeset.graph.txouts {
|
||||
self.index.index_txout(outpoint, txout);
|
||||
}
|
||||
|
||||
self.graph.apply_changeset(changeset.graph);
|
||||
}
|
||||
|
||||
/// Determines the [`ChangeSet`] between `self` and an empty [`IndexedTxGraph`].
|
||||
pub fn initial_changeset(&self) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.initial_changeset();
|
||||
let indexer = self.index.initial_changeset();
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
|
||||
where
|
||||
I::ChangeSet: Default + Append,
|
||||
{
|
||||
fn index_tx_graph_changeset(
|
||||
&mut self,
|
||||
tx_graph_changeset: &tx_graph::ChangeSet<A>,
|
||||
) -> I::ChangeSet {
|
||||
let mut changeset = I::ChangeSet::default();
|
||||
for added_tx in &tx_graph_changeset.txs {
|
||||
changeset.append(self.index.index_tx(added_tx));
|
||||
}
|
||||
for (&added_outpoint, added_txout) in &tx_graph_changeset.txouts {
|
||||
changeset.append(self.index.index_txout(added_outpoint, added_txout));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
/// Apply an `update` directly.
|
||||
///
|
||||
/// `update` is a [`TxGraph<A>`] and the resultant changes is returned as [`ChangeSet`].
|
||||
pub fn apply_update(&mut self, update: TxGraph<A>) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.apply_update(update);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
|
||||
/// Insert a floating `txout` of given `outpoint`.
|
||||
pub fn insert_txout(&mut self, outpoint: OutPoint, txout: TxOut) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.insert_txout(outpoint, txout);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
|
||||
/// Insert and index a transaction into the graph.
|
||||
pub fn insert_tx(&mut self, tx: Transaction) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.insert_tx(tx);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
|
||||
/// Insert an `anchor` for a given transaction.
|
||||
pub fn insert_anchor(&mut self, txid: Txid, anchor: A) -> ChangeSet<A, I::ChangeSet> {
|
||||
self.graph.insert_anchor(txid, anchor).into()
|
||||
}
|
||||
|
||||
/// Insert a unix timestamp of when a transaction is seen in the mempool.
|
||||
///
|
||||
/// This is used for transaction conflict resolution in [`TxGraph`] where the transaction with
|
||||
/// the later last-seen is prioritized.
|
||||
pub fn insert_seen_at(&mut self, txid: Txid, seen_at: u64) -> ChangeSet<A, I::ChangeSet> {
|
||||
self.graph.insert_seen_at(txid, seen_at).into()
|
||||
}
|
||||
|
||||
/// Batch insert transactions, filtering out those that are irrelevant.
|
||||
///
|
||||
/// Relevancy is determined by the [`Indexer::is_tx_relevant`] implementation of `I`. Irrelevant
|
||||
/// transactions in `txs` will be ignored. `txs` do not need to be in topological order.
|
||||
pub fn batch_insert_relevant<'t>(
|
||||
&mut self,
|
||||
txs: impl IntoIterator<Item = (&'t Transaction, impl IntoIterator<Item = A>)>,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
// The algorithm below allows for non-topologically ordered transactions by using two loops.
|
||||
// This is achieved by:
|
||||
// 1. insert all txs into the index. If they are irrelevant then that's fine it will just
|
||||
// not store anything about them.
|
||||
// 2. decide whether to insert them into the graph depending on whether `is_tx_relevant`
|
||||
// returns true or not. (in a second loop).
|
||||
let txs = txs.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let mut indexer = I::ChangeSet::default();
|
||||
for (tx, _) in &txs {
|
||||
indexer.append(self.index.index_tx(tx));
|
||||
}
|
||||
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx, anchors) in txs {
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.txid();
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
for anchor in anchors {
|
||||
graph.append(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
|
||||
/// Batch insert unconfirmed transactions, filtering out those that are irrelevant.
|
||||
///
|
||||
/// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
|
||||
/// Irrelevant transactions in `txs` will be ignored.
|
||||
///
|
||||
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
|
||||
/// *last seen* communicates when the transaction is last seen in the mempool which is used for
|
||||
/// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details).
|
||||
pub fn batch_insert_relevant_unconfirmed<'t>(
|
||||
&mut self,
|
||||
unconfirmed_txs: impl IntoIterator<Item = (&'t Transaction, u64)>,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
// The algorithm below allows for non-topologically ordered transactions by using two loops.
|
||||
// This is achieved by:
|
||||
// 1. insert all txs into the index. If they are irrelevant then that's fine it will just
|
||||
// not store anything about them.
|
||||
// 2. decide whether to insert them into the graph depending on whether `is_tx_relevant`
|
||||
// returns true or not. (in a second loop).
|
||||
let txs = unconfirmed_txs.into_iter().collect::<Vec<_>>();
|
||||
|
||||
let mut indexer = I::ChangeSet::default();
|
||||
for (tx, _) in &txs {
|
||||
indexer.append(self.index.index_tx(tx));
|
||||
}
|
||||
|
||||
let graph = self.graph.batch_insert_unconfirmed(
|
||||
txs.into_iter()
|
||||
.filter(|(tx, _)| self.index.is_tx_relevant(tx))
|
||||
.map(|(tx, seen_at)| (tx.clone(), seen_at)),
|
||||
);
|
||||
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
|
||||
/// Batch insert unconfirmed transactions.
|
||||
///
|
||||
/// Items of `txs` are tuples containing the transaction and a *last seen* timestamp. The
|
||||
/// *last seen* communicates when the transaction is last seen in the mempool which is used for
|
||||
/// conflict-resolution in [`TxGraph`] (refer to [`TxGraph::insert_seen_at`] for details).
|
||||
///
|
||||
/// To filter out irrelevant transactions, use [`batch_insert_relevant_unconfirmed`] instead.
|
||||
///
|
||||
/// [`batch_insert_relevant_unconfirmed`]: IndexedTxGraph::batch_insert_relevant_unconfirmed
|
||||
pub fn batch_insert_unconfirmed(
|
||||
&mut self,
|
||||
txs: impl IntoIterator<Item = (Transaction, u64)>,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
let graph = self.graph.batch_insert_unconfirmed(txs);
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods are available if the anchor (`A`) implements [`AnchorFromBlockPosition`].
|
||||
impl<A: Anchor, I: Indexer> IndexedTxGraph<A, I>
|
||||
where
|
||||
I::ChangeSet: Default + Append,
|
||||
A: AnchorFromBlockPosition,
|
||||
{
|
||||
/// Batch insert all transactions of the given `block` of `height`, filtering out those that are
|
||||
/// irrelevant.
|
||||
///
|
||||
/// Each inserted transaction's anchor will be constructed from
|
||||
/// [`AnchorFromBlockPosition::from_block_position`].
|
||||
///
|
||||
/// Relevancy is determined by the internal [`Indexer::is_tx_relevant`] implementation of `I`.
|
||||
/// Irrelevant transactions in `txs` will be ignored.
|
||||
pub fn apply_block_relevant(
|
||||
&mut self,
|
||||
block: &Block,
|
||||
height: u32,
|
||||
) -> ChangeSet<A, I::ChangeSet> {
|
||||
let block_id = BlockId {
|
||||
hash: block.block_hash(),
|
||||
height,
|
||||
};
|
||||
let mut changeset = ChangeSet::<A, I::ChangeSet>::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
changeset.indexer.append(self.index.index_tx(tx));
|
||||
if self.index.is_tx_relevant(tx) {
|
||||
let txid = tx.txid();
|
||||
let anchor = A::from_block_position(block, block_id, tx_pos);
|
||||
changeset.graph.append(self.graph.insert_tx(tx.clone()));
|
||||
changeset
|
||||
.graph
|
||||
.append(self.graph.insert_anchor(txid, anchor));
|
||||
}
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
/// Batch insert all transactions of the given `block` of `height`.
|
||||
///
|
||||
/// Each inserted transaction's anchor will be constructed from
|
||||
/// [`AnchorFromBlockPosition::from_block_position`].
|
||||
///
|
||||
/// To only insert relevant transactions, use [`apply_block_relevant`] instead.
|
||||
///
|
||||
/// [`apply_block_relevant`]: IndexedTxGraph::apply_block_relevant
|
||||
pub fn apply_block(&mut self, block: Block, height: u32) -> ChangeSet<A, I::ChangeSet> {
|
||||
let block_id = BlockId {
|
||||
hash: block.block_hash(),
|
||||
height,
|
||||
};
|
||||
let mut graph = tx_graph::ChangeSet::default();
|
||||
for (tx_pos, tx) in block.txdata.iter().enumerate() {
|
||||
let anchor = A::from_block_position(&block, block_id, tx_pos);
|
||||
graph.append(self.graph.insert_anchor(tx.txid(), anchor));
|
||||
graph.append(self.graph.insert_tx(tx.clone()));
|
||||
}
|
||||
let indexer = self.index_tx_graph_changeset(&graph);
|
||||
ChangeSet { graph, indexer }
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents changes to an [`IndexedTxGraph`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "A: Ord + serde::Deserialize<'de>, IA: serde::Deserialize<'de>",
|
||||
serialize = "A: Ord + serde::Serialize, IA: serde::Serialize"
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct ChangeSet<A, IA> {
|
||||
/// [`TxGraph`] changeset.
|
||||
pub graph: tx_graph::ChangeSet<A>,
|
||||
/// [`Indexer`] changeset.
|
||||
pub indexer: IA,
|
||||
}
|
||||
|
||||
impl<A, IA: Default> Default for ChangeSet<A, IA> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
indexer: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A: Anchor, IA: Append> Append for ChangeSet<A, IA> {
|
||||
fn append(&mut self, other: Self) {
|
||||
self.graph.append(other.graph);
|
||||
self.indexer.append(other.indexer);
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
self.graph.is_empty() && self.indexer.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, IA: Default> From<tx_graph::ChangeSet<A>> for ChangeSet<A, IA> {
|
||||
fn from(graph: tx_graph::ChangeSet<A>) -> Self {
|
||||
Self {
|
||||
graph,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, K> From<keychain::ChangeSet<K>> for ChangeSet<A, keychain::ChangeSet<K>> {
|
||||
fn from(indexer: keychain::ChangeSet<K>) -> Self {
|
||||
Self {
|
||||
graph: Default::default(),
|
||||
indexer,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Utilities for indexing transaction data.
|
||||
///
|
||||
/// Types which implement this trait can be used to construct an [`IndexedTxGraph`].
|
||||
/// This trait's methods should rarely be called directly.
|
||||
pub trait Indexer {
|
||||
/// The resultant "changeset" when new transaction data is indexed.
|
||||
type ChangeSet;
|
||||
|
||||
/// Scan and index the given `outpoint` and `txout`.
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet;
|
||||
|
||||
/// Scans a transaction for relevant outpoints, which are stored and indexed internally.
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet;
|
||||
|
||||
/// Apply changeset to itself.
|
||||
fn apply_changeset(&mut self, changeset: Self::ChangeSet);
|
||||
|
||||
/// Determines the [`ChangeSet`] between `self` and an empty [`Indexer`].
|
||||
fn initial_changeset(&self) -> Self::ChangeSet;
|
||||
|
||||
/// Determines whether the transaction should be included in the index.
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool;
|
||||
}
|
||||
175
crates/chain/src/keychain.rs
Normal file
175
crates/chain/src/keychain.rs
Normal file
@@ -0,0 +1,175 @@
|
||||
//! Module for keychain related structures.
|
||||
//!
|
||||
//! A keychain here is a set of application-defined indexes for a miniscript descriptor where we can
|
||||
//! derive script pubkeys at a particular derivation index. The application's index is simply
|
||||
//! anything that implements `Ord`.
|
||||
//!
|
||||
//! [`KeychainTxOutIndex`] indexes script pubkeys of keychains and scans in relevant outpoints (that
|
||||
//! has a `txout` containing an indexed script pubkey). Internally, this uses [`SpkTxOutIndex`], but
|
||||
//! also maintains "revealed" and "lookahead" index counts per keychain.
|
||||
//!
|
||||
//! [`SpkTxOutIndex`]: crate::SpkTxOutIndex
|
||||
|
||||
use crate::{collections::BTreeMap, Append};
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod txout_index;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use txout_index::*;
|
||||
|
||||
/// Represents updates to the derivation index of a [`KeychainTxOutIndex`].
|
||||
/// It maps each keychain `K` to its last revealed index.
|
||||
///
|
||||
/// It can be applied to [`KeychainTxOutIndex`] with [`apply_changeset`]. [`ChangeSet`]s are
|
||||
/// monotone in that they will never decrease the revealed derivation index.
|
||||
///
|
||||
/// [`KeychainTxOutIndex`]: crate::keychain::KeychainTxOutIndex
|
||||
/// [`apply_changeset`]: crate::keychain::KeychainTxOutIndex::apply_changeset
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(
|
||||
crate = "serde_crate",
|
||||
bound(
|
||||
deserialize = "K: Ord + serde::Deserialize<'de>",
|
||||
serialize = "K: Ord + serde::Serialize"
|
||||
)
|
||||
)
|
||||
)]
|
||||
#[must_use]
|
||||
pub struct ChangeSet<K>(pub BTreeMap<K, u32>);
|
||||
|
||||
impl<K> ChangeSet<K> {
|
||||
/// Get the inner map of the keychain to its new derivation index.
|
||||
pub fn as_inner(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Ord> Append for ChangeSet<K> {
|
||||
/// Append another [`ChangeSet`] into self.
|
||||
///
|
||||
/// If the keychain already exists, increase the index when the other's index > self's index.
|
||||
/// If the keychain did not exist, append the new keychain.
|
||||
fn append(&mut self, mut other: Self) {
|
||||
self.0.iter_mut().for_each(|(key, index)| {
|
||||
if let Some(other_index) = other.0.remove(key) {
|
||||
*index = other_index.max(*index);
|
||||
}
|
||||
});
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
self.0.extend(other.0);
|
||||
}
|
||||
|
||||
/// Returns whether the changeset are empty.
|
||||
fn is_empty(&self) -> bool {
|
||||
self.0.is_empty()
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> Default for ChangeSet<K> {
|
||||
fn default() -> Self {
|
||||
Self(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> AsRef<BTreeMap<K, u32>> for ChangeSet<K> {
|
||||
fn as_ref(&self) -> &BTreeMap<K, u32> {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Balance, differentiated into various categories.
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Default)]
|
||||
#[cfg_attr(
|
||||
feature = "serde",
|
||||
derive(serde::Deserialize, serde::Serialize),
|
||||
serde(crate = "serde_crate",)
|
||||
)]
|
||||
pub struct Balance {
|
||||
/// All coinbase outputs not yet matured
|
||||
pub immature: u64,
|
||||
/// Unconfirmed UTXOs generated by a wallet tx
|
||||
pub trusted_pending: u64,
|
||||
/// Unconfirmed UTXOs received from an external wallet
|
||||
pub untrusted_pending: u64,
|
||||
/// Confirmed and immediately spendable balance
|
||||
pub confirmed: u64,
|
||||
}
|
||||
|
||||
impl Balance {
|
||||
/// Get sum of trusted_pending and confirmed coins.
|
||||
///
|
||||
/// This is the balance you can spend right now that shouldn't get cancelled via another party
|
||||
/// double spending it.
|
||||
pub fn trusted_spendable(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending
|
||||
}
|
||||
|
||||
/// Get the whole balance visible to the wallet.
|
||||
pub fn total(&self) -> u64 {
|
||||
self.confirmed + self.trusted_pending + self.untrusted_pending + self.immature
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Balance {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{{ immature: {}, trusted_pending: {}, untrusted_pending: {}, confirmed: {} }}",
|
||||
self.immature, self.trusted_pending, self.untrusted_pending, self.confirmed
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl core::ops::Add for Balance {
|
||||
type Output = Self;
|
||||
|
||||
fn add(self, other: Self) -> Self {
|
||||
Self {
|
||||
immature: self.immature + other.immature,
|
||||
trusted_pending: self.trusted_pending + other.trusted_pending,
|
||||
untrusted_pending: self.untrusted_pending + other.untrusted_pending,
|
||||
confirmed: self.confirmed + other.confirmed,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn append_keychain_derivation_indices() {
|
||||
#[derive(Ord, PartialOrd, Eq, PartialEq, Clone, Debug)]
|
||||
enum Keychain {
|
||||
One,
|
||||
Two,
|
||||
Three,
|
||||
Four,
|
||||
}
|
||||
let mut lhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
let mut rhs_di = BTreeMap::<Keychain, u32>::default();
|
||||
lhs_di.insert(Keychain::One, 7);
|
||||
lhs_di.insert(Keychain::Two, 0);
|
||||
rhs_di.insert(Keychain::One, 3);
|
||||
rhs_di.insert(Keychain::Two, 5);
|
||||
lhs_di.insert(Keychain::Three, 3);
|
||||
rhs_di.insert(Keychain::Four, 4);
|
||||
|
||||
let mut lhs = ChangeSet(lhs_di);
|
||||
let rhs = ChangeSet(rhs_di);
|
||||
lhs.append(rhs);
|
||||
|
||||
// Exiting index doesn't update if the new index in `other` is lower than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::One), Some(&7));
|
||||
// Existing index updates if the new index in `other` is higher than `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Two), Some(&5));
|
||||
// Existing index is unchanged if keychain doesn't exist in `other`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Three), Some(&3));
|
||||
// New keychain gets added if the keychain is in `other` but not in `self`.
|
||||
assert_eq!(lhs.0.get(&Keychain::Four), Some(&4));
|
||||
}
|
||||
}
|
||||
677
crates/chain/src/keychain/txout_index.rs
Normal file
677
crates/chain/src/keychain/txout_index.rs
Normal file
@@ -0,0 +1,677 @@
|
||||
use crate::{
|
||||
collections::*,
|
||||
indexed_tx_graph::Indexer,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
spk_iter::BIP32_MAX_INDEX,
|
||||
SpkIterator, SpkTxOutIndex,
|
||||
};
|
||||
use bitcoin::{OutPoint, Script, Transaction, TxOut, Txid};
|
||||
use core::{
|
||||
fmt::Debug,
|
||||
ops::{Bound, RangeBounds},
|
||||
};
|
||||
|
||||
use crate::Append;
|
||||
|
||||
const DEFAULT_LOOKAHEAD: u32 = 25;
|
||||
|
||||
/// [`KeychainTxOutIndex`] controls how script pubkeys are revealed for multiple keychains, and
|
||||
/// indexes [`TxOut`]s with them.
|
||||
///
|
||||
/// A single keychain is a chain of script pubkeys derived from a single [`Descriptor`]. Keychains
|
||||
/// are identified using the `K` generic. Script pubkeys are identified by the keychain that they
|
||||
/// are derived from `K`, as well as the derivation index `u32`.
|
||||
///
|
||||
/// # Revealed script pubkeys
|
||||
///
|
||||
/// Tracking how script pubkeys are revealed is useful for collecting chain data. For example, if
|
||||
/// the user has requested 5 script pubkeys (to receive money with), we only need to use those
|
||||
/// script pubkeys to scan for chain data.
|
||||
///
|
||||
/// Call [`reveal_to_target`] or [`reveal_next_spk`] to reveal more script pubkeys.
|
||||
/// Call [`revealed_keychain_spks`] or [`revealed_spks`] to iterate through revealed script pubkeys.
|
||||
///
|
||||
/// # Lookahead script pubkeys
|
||||
///
|
||||
/// When an user first recovers a wallet (i.e. from a recovery phrase and/or descriptor), we will
|
||||
/// NOT have knowledge of which script pubkeys are revealed. So when we index a transaction or
|
||||
/// txout (using [`index_tx`]/[`index_txout`]) we scan the txouts against script pubkeys derived
|
||||
/// above the last revealed index. These additionally-derived script pubkeys are called the
|
||||
/// lookahead.
|
||||
///
|
||||
/// The [`KeychainTxOutIndex`] is constructed with the `lookahead` and cannot be altered. The
|
||||
/// default `lookahead` count is 1000. Use [`new`] to set a custom `lookahead`.
|
||||
///
|
||||
/// # Unbounded script pubkey iterator
|
||||
///
|
||||
/// For script-pubkey-based chain sources (such as Electrum/Esplora), an initial scan is best done
|
||||
/// by iterating though derived script pubkeys one by one and requesting transaction histories for
|
||||
/// each script pubkey. We will stop after x-number of script pubkeys have empty histories. An
|
||||
/// unbounded script pubkey iterator is useful to pass to such a chain source.
|
||||
///
|
||||
/// Call [`unbounded_spk_iter`] to get an unbounded script pubkey iterator for a given keychain.
|
||||
/// Call [`all_unbounded_spk_iters`] to get unbounded script pubkey iterators for all keychains.
|
||||
///
|
||||
/// # Change sets
|
||||
///
|
||||
/// Methods that can update the last revealed index will return [`super::ChangeSet`] to report
|
||||
/// these changes. This can be persisted for future recovery.
|
||||
///
|
||||
/// ## Synopsis
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_chain::keychain::KeychainTxOutIndex;
|
||||
/// # use bdk_chain::{ miniscript::{Descriptor, DescriptorPublicKey} };
|
||||
/// # use core::str::FromStr;
|
||||
///
|
||||
/// // imagine our service has internal and external addresses but also addresses for users
|
||||
/// #[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
/// enum MyKeychain {
|
||||
/// External,
|
||||
/// Internal,
|
||||
/// MyAppUser {
|
||||
/// user_id: u32
|
||||
/// }
|
||||
/// }
|
||||
///
|
||||
/// let mut txout_index = KeychainTxOutIndex::<MyKeychain>::default();
|
||||
///
|
||||
/// # let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
/// # let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
/// # let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
/// # let (descriptor_for_user_42, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/2/*)").unwrap();
|
||||
/// txout_index.add_keychain(MyKeychain::External, external_descriptor);
|
||||
/// txout_index.add_keychain(MyKeychain::Internal, internal_descriptor);
|
||||
/// txout_index.add_keychain(MyKeychain::MyAppUser { user_id: 42 }, descriptor_for_user_42);
|
||||
///
|
||||
/// let new_spk_for_user = txout_index.reveal_next_spk(&MyKeychain::MyAppUser{ user_id: 42 });
|
||||
/// ```
|
||||
///
|
||||
/// [`Ord`]: core::cmp::Ord
|
||||
/// [`SpkTxOutIndex`]: crate::spk_txout_index::SpkTxOutIndex
|
||||
/// [`Descriptor`]: crate::miniscript::Descriptor
|
||||
/// [`reveal_to_target`]: KeychainTxOutIndex::reveal_to_target
|
||||
/// [`reveal_next_spk`]: KeychainTxOutIndex::reveal_next_spk
|
||||
/// [`revealed_keychain_spks`]: KeychainTxOutIndex::revealed_keychain_spks
|
||||
/// [`revealed_spks`]: KeychainTxOutIndex::revealed_spks
|
||||
/// [`index_tx`]: KeychainTxOutIndex::index_tx
|
||||
/// [`index_txout`]: KeychainTxOutIndex::index_txout
|
||||
/// [`new`]: KeychainTxOutIndex::new
|
||||
/// [`unbounded_spk_iter`]: KeychainTxOutIndex::unbounded_spk_iter
|
||||
/// [`all_unbounded_spk_iters`]: KeychainTxOutIndex::all_unbounded_spk_iters
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct KeychainTxOutIndex<K> {
|
||||
inner: SpkTxOutIndex<(K, u32)>,
|
||||
// descriptors of each keychain
|
||||
keychains: BTreeMap<K, Descriptor<DescriptorPublicKey>>,
|
||||
// last revealed indexes
|
||||
last_revealed: BTreeMap<K, u32>,
|
||||
// lookahead settings for each keychain
|
||||
lookahead: u32,
|
||||
}
|
||||
|
||||
impl<K> Default for KeychainTxOutIndex<K> {
|
||||
fn default() -> Self {
|
||||
Self::new(DEFAULT_LOOKAHEAD)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> Indexer for KeychainTxOutIndex<K> {
|
||||
type ChangeSet = super::ChangeSet<K>;
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
|
||||
match self.inner.scan_txout(outpoint, txout).cloned() {
|
||||
Some((keychain, index)) => self.reveal_to_target(&keychain, index).1,
|
||||
None => super::ChangeSet::default(),
|
||||
}
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &bitcoin::Transaction) -> Self::ChangeSet {
|
||||
let mut changeset = super::ChangeSet::<K>::default();
|
||||
for (op, txout) in tx.output.iter().enumerate() {
|
||||
changeset.append(self.index_txout(OutPoint::new(tx.txid(), op as u32), txout));
|
||||
}
|
||||
changeset
|
||||
}
|
||||
|
||||
fn initial_changeset(&self) -> Self::ChangeSet {
|
||||
super::ChangeSet(self.last_revealed.clone())
|
||||
}
|
||||
|
||||
fn apply_changeset(&mut self, changeset: Self::ChangeSet) {
|
||||
self.apply_changeset(changeset)
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &bitcoin::Transaction) -> bool {
|
||||
self.inner.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K> KeychainTxOutIndex<K> {
|
||||
/// Construct a [`KeychainTxOutIndex`] with the given `lookahead`.
|
||||
///
|
||||
/// The `lookahead` is the number of script pubkeys to derive and cache from the internal
|
||||
/// descriptors over and above the last revealed script index. Without a lookahead the index
|
||||
/// will miss outputs you own when processing transactions whose output script pubkeys lie
|
||||
/// beyond the last revealed index. In certain situations, such as when performing an initial
|
||||
/// scan of the blockchain during wallet import, it may be uncertain or unknown what the index
|
||||
/// of the last revealed script pubkey actually is.
|
||||
///
|
||||
/// Refer to [struct-level docs](KeychainTxOutIndex) for more about `lookahead`.
|
||||
pub fn new(lookahead: u32) -> Self {
|
||||
Self {
|
||||
inner: SpkTxOutIndex::default(),
|
||||
keychains: BTreeMap::new(),
|
||||
last_revealed: BTreeMap::new(),
|
||||
lookahead,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods that are *re-exposed* from the internal [`SpkTxOutIndex`].
|
||||
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Return a reference to the internal [`SpkTxOutIndex`].
|
||||
///
|
||||
/// **WARNING:** The internal index will contain lookahead spks. Refer to
|
||||
/// [struct-level docs](KeychainTxOutIndex) for more about `lookahead`.
|
||||
pub fn inner(&self) -> &SpkTxOutIndex<(K, u32)> {
|
||||
&self.inner
|
||||
}
|
||||
|
||||
/// Get a reference to the set of indexed outpoints.
|
||||
pub fn outpoints(&self) -> &BTreeSet<((K, u32), OutPoint)> {
|
||||
self.inner.outpoints()
|
||||
}
|
||||
|
||||
/// Iterate over known txouts that spend to tracked script pubkeys.
|
||||
pub fn txouts(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> + ExactSizeIterator {
|
||||
self.inner
|
||||
.txouts()
|
||||
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
|
||||
}
|
||||
|
||||
/// Finds all txouts on a transaction that has previously been scanned and indexed.
|
||||
pub fn txouts_in_tx(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> impl DoubleEndedIterator<Item = (K, u32, OutPoint, &TxOut)> {
|
||||
self.inner
|
||||
.txouts_in_tx(txid)
|
||||
.map(|((k, i), op, txo)| (k.clone(), *i, op, txo))
|
||||
}
|
||||
|
||||
/// Return the [`TxOut`] of `outpoint` if it has been indexed.
|
||||
///
|
||||
/// The associated keychain and keychain index of the txout's spk is also returned.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::txout`] internally.
|
||||
pub fn txout(&self, outpoint: OutPoint) -> Option<(K, u32, &TxOut)> {
|
||||
self.inner
|
||||
.txout(outpoint)
|
||||
.map(|((k, i), txo)| (k.clone(), *i, txo))
|
||||
}
|
||||
|
||||
/// Return the script that exists under the given `keychain`'s `index`.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::spk_at_index`] internally.
|
||||
pub fn spk_at_index(&self, keychain: K, index: u32) -> Option<&Script> {
|
||||
self.inner.spk_at_index(&(keychain, index))
|
||||
}
|
||||
|
||||
/// Returns the keychain and keychain index associated with the spk.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::index_of_spk`] internally.
|
||||
pub fn index_of_spk(&self, script: &Script) -> Option<(K, u32)> {
|
||||
self.inner.index_of_spk(script).cloned()
|
||||
}
|
||||
|
||||
/// Returns whether the spk under the `keychain`'s `index` has been used.
|
||||
///
|
||||
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||
/// never scanned a transaction output with it.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::is_used`] internally.
|
||||
pub fn is_used(&self, keychain: K, index: u32) -> bool {
|
||||
self.inner.is_used(&(keychain, index))
|
||||
}
|
||||
|
||||
/// Marks the script pubkey at `index` as used even though the tracker hasn't seen an output
|
||||
/// with it.
|
||||
///
|
||||
/// This only has an effect when the `index` had been added to `self` already and was unused.
|
||||
///
|
||||
/// Returns whether the `index` was initially present as `unused`.
|
||||
///
|
||||
/// This is useful when you want to reserve a script pubkey for something but don't want to add
|
||||
/// the transaction output using it to the index yet. Other callers will consider `index` on
|
||||
/// `keychain` used until you call [`unmark_used`].
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::mark_used`] internally.
|
||||
///
|
||||
/// [`unmark_used`]: Self::unmark_used
|
||||
pub fn mark_used(&mut self, keychain: K, index: u32) -> bool {
|
||||
self.inner.mark_used(&(keychain, index))
|
||||
}
|
||||
|
||||
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
|
||||
/// `unused`.
|
||||
///
|
||||
/// Note that if `self` has scanned an output with this script pubkey, then this will have no
|
||||
/// effect.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::unmark_used`] internally.
|
||||
///
|
||||
/// [`mark_used`]: Self::mark_used
|
||||
pub fn unmark_used(&mut self, keychain: K, index: u32) -> bool {
|
||||
self.inner.unmark_used(&(keychain, index))
|
||||
}
|
||||
|
||||
/// Computes total input value going from script pubkeys in the index (sent) and the total output
|
||||
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
|
||||
/// correctly, the output being spent must have already been scanned by the index. Calculating
|
||||
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
|
||||
/// not been scanned.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::sent_and_received`] internally.
|
||||
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||
self.inner.sent_and_received(tx)
|
||||
}
|
||||
|
||||
/// Computes the net value that this transaction gives to the script pubkeys in the index and
|
||||
/// *takes* from the transaction outputs in the index. Shorthand for calling
|
||||
/// [`sent_and_received`] and subtracting sent from received.
|
||||
///
|
||||
/// This calls [`SpkTxOutIndex::net_value`] internally.
|
||||
///
|
||||
/// [`sent_and_received`]: Self::sent_and_received
|
||||
pub fn net_value(&self, tx: &Transaction) -> i64 {
|
||||
self.inner.net_value(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<K: Clone + Ord + Debug> KeychainTxOutIndex<K> {
|
||||
/// Return a reference to the internal map of keychain to descriptors.
|
||||
pub fn keychains(&self) -> &BTreeMap<K, Descriptor<DescriptorPublicKey>> {
|
||||
&self.keychains
|
||||
}
|
||||
|
||||
/// Add a keychain to the tracker's `txout_index` with a descriptor to derive addresses.
|
||||
///
|
||||
/// Adding a keychain means you will be able to derive new script pubkeys under that keychain
|
||||
/// and the txout index will discover transaction outputs with those script pubkeys.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if a different `descriptor` is introduced to the same `keychain`.
|
||||
pub fn add_keychain(&mut self, keychain: K, descriptor: Descriptor<DescriptorPublicKey>) {
|
||||
let old_descriptor = &*self
|
||||
.keychains
|
||||
.entry(keychain.clone())
|
||||
.or_insert_with(|| descriptor.clone());
|
||||
assert_eq!(
|
||||
&descriptor, old_descriptor,
|
||||
"keychain already contains a different descriptor"
|
||||
);
|
||||
self.replenish_lookahead(&keychain, self.lookahead);
|
||||
}
|
||||
|
||||
/// Get the lookahead setting.
|
||||
///
|
||||
/// Refer to [`new`] for more information on the `lookahead`.
|
||||
///
|
||||
/// [`new`]: Self::new
|
||||
pub fn lookahead(&self) -> u32 {
|
||||
self.lookahead
|
||||
}
|
||||
|
||||
/// Store lookahead scripts until `target_index` (inclusive).
|
||||
///
|
||||
/// This does not change the global `lookahead` setting.
|
||||
pub fn lookahead_to_target(&mut self, keychain: &K, target_index: u32) {
|
||||
let (next_index, _) = self.next_index(keychain);
|
||||
|
||||
let temp_lookahead = (target_index + 1)
|
||||
.checked_sub(next_index)
|
||||
.filter(|&index| index > 0);
|
||||
|
||||
if let Some(temp_lookahead) = temp_lookahead {
|
||||
self.replenish_lookahead(keychain, temp_lookahead);
|
||||
}
|
||||
}
|
||||
|
||||
fn replenish_lookahead(&mut self, keychain: &K, lookahead: u32) {
|
||||
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
|
||||
let next_store_index = self.next_store_index(keychain);
|
||||
let next_reveal_index = self.last_revealed.get(keychain).map_or(0, |v| *v + 1);
|
||||
|
||||
for (new_index, new_spk) in
|
||||
SpkIterator::new_with_range(descriptor, next_store_index..next_reveal_index + lookahead)
|
||||
{
|
||||
let _inserted = self
|
||||
.inner
|
||||
.insert_spk((keychain.clone(), new_index), new_spk);
|
||||
debug_assert!(_inserted, "replenish lookahead: must not have existing spk: keychain={:?}, lookahead={}, next_store_index={}, next_reveal_index={}", keychain, lookahead, next_store_index, next_reveal_index);
|
||||
}
|
||||
}
|
||||
|
||||
fn next_store_index(&self, keychain: &K) -> u32 {
|
||||
self.inner()
|
||||
.all_spks()
|
||||
// This range is filtering out the spks with a keychain different than
|
||||
// `keychain`. We don't use filter here as range is more optimized.
|
||||
.range((keychain.clone(), u32::MIN)..(keychain.clone(), u32::MAX))
|
||||
.last()
|
||||
.map_or(0, |((_, index), _)| *index + 1)
|
||||
}
|
||||
|
||||
/// Get an unbounded spk iterator over a given `keychain`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// This will panic if the given `keychain`'s descriptor does not exist.
|
||||
pub fn unbounded_spk_iter(&self, keychain: &K) -> SpkIterator<Descriptor<DescriptorPublicKey>> {
|
||||
SpkIterator::new(
|
||||
self.keychains
|
||||
.get(keychain)
|
||||
.expect("keychain does not exist")
|
||||
.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
/// Get unbounded spk iterators for all keychains.
|
||||
pub fn all_unbounded_spk_iters(
|
||||
&self,
|
||||
) -> BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>> {
|
||||
self.keychains
|
||||
.iter()
|
||||
.map(|(k, descriptor)| (k.clone(), SpkIterator::new(descriptor.clone())))
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Iterate over revealed spks of all keychains.
|
||||
pub fn revealed_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
|
||||
self.keychains.keys().flat_map(|keychain| {
|
||||
self.revealed_keychain_spks(keychain)
|
||||
.map(|(i, spk)| (keychain.clone(), i, spk))
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over revealed spks of the given `keychain`.
|
||||
pub fn revealed_keychain_spks(
|
||||
&self,
|
||||
keychain: &K,
|
||||
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
|
||||
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
|
||||
self.inner
|
||||
.all_spks()
|
||||
.range((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
|
||||
.map(|((_, i), spk)| (*i, spk.as_script()))
|
||||
}
|
||||
|
||||
/// Iterate over revealed, but unused, spks of all keychains.
|
||||
pub fn unused_spks(&self) -> impl DoubleEndedIterator<Item = (K, u32, &Script)> + Clone {
|
||||
self.keychains.keys().flat_map(|keychain| {
|
||||
self.unused_keychain_spks(keychain)
|
||||
.map(|(i, spk)| (keychain.clone(), i, spk))
|
||||
})
|
||||
}
|
||||
|
||||
/// Iterate over revealed, but unused, spks of the given `keychain`.
|
||||
pub fn unused_keychain_spks(
|
||||
&self,
|
||||
keychain: &K,
|
||||
) -> impl DoubleEndedIterator<Item = (u32, &Script)> + Clone {
|
||||
let next_i = self.last_revealed.get(keychain).map_or(0, |&i| i + 1);
|
||||
self.inner
|
||||
.unused_spks((keychain.clone(), u32::MIN)..(keychain.clone(), next_i))
|
||||
.map(|((_, i), spk)| (*i, spk))
|
||||
}
|
||||
|
||||
/// Get the next derivation index for `keychain`. The next index is the index after the last revealed
|
||||
/// derivation index.
|
||||
///
|
||||
/// The second field in the returned tuple represents whether the next derivation index is new.
|
||||
/// There are two scenarios where the next derivation index is reused (not new):
|
||||
///
|
||||
/// 1. The keychain's descriptor has no wildcard, and a script has already been revealed.
|
||||
/// 2. The number of revealed scripts has already reached 2^31 (refer to BIP-32).
|
||||
///
|
||||
/// Not checking the second field of the tuple may result in address reuse.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `keychain` does not exist.
|
||||
pub fn next_index(&self, keychain: &K) -> (u32, bool) {
|
||||
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
|
||||
let last_index = self.last_revealed.get(keychain).cloned();
|
||||
|
||||
// we can only get the next index if the wildcard exists.
|
||||
let has_wildcard = descriptor.has_wildcard();
|
||||
|
||||
match last_index {
|
||||
// if there is no index, next_index is always 0.
|
||||
None => (0, true),
|
||||
// descriptors without wildcards can only have one index.
|
||||
Some(_) if !has_wildcard => (0, false),
|
||||
// derivation index must be < 2^31 (BIP-32).
|
||||
Some(index) if index > BIP32_MAX_INDEX => {
|
||||
unreachable!("index is out of bounds")
|
||||
}
|
||||
Some(index) if index == BIP32_MAX_INDEX => (index, false),
|
||||
// get the next derivation index.
|
||||
Some(index) => (index + 1, true),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the last derivation index that is revealed for each keychain.
|
||||
///
|
||||
/// Keychains with no revealed indices will not be included in the returned [`BTreeMap`].
|
||||
pub fn last_revealed_indices(&self) -> &BTreeMap<K, u32> {
|
||||
&self.last_revealed
|
||||
}
|
||||
|
||||
/// Get the last derivation index revealed for `keychain`.
|
||||
pub fn last_revealed_index(&self, keychain: &K) -> Option<u32> {
|
||||
self.last_revealed.get(keychain).cloned()
|
||||
}
|
||||
|
||||
/// Convenience method to call [`Self::reveal_to_target`] on multiple keychains.
|
||||
pub fn reveal_to_target_multi(
|
||||
&mut self,
|
||||
keychains: &BTreeMap<K, u32>,
|
||||
) -> (
|
||||
BTreeMap<K, SpkIterator<Descriptor<DescriptorPublicKey>>>,
|
||||
super::ChangeSet<K>,
|
||||
) {
|
||||
let mut changeset = super::ChangeSet::default();
|
||||
let mut spks = BTreeMap::new();
|
||||
|
||||
for (keychain, &index) in keychains {
|
||||
let (new_spks, new_changeset) = self.reveal_to_target(keychain, index);
|
||||
if !new_changeset.is_empty() {
|
||||
spks.insert(keychain.clone(), new_spks);
|
||||
changeset.append(new_changeset.clone());
|
||||
}
|
||||
}
|
||||
|
||||
(spks, changeset)
|
||||
}
|
||||
|
||||
/// Reveals script pubkeys of the `keychain`'s descriptor **up to and including** the
|
||||
/// `target_index`.
|
||||
///
|
||||
/// If the `target_index` cannot be reached (due to the descriptor having no wildcard and/or
|
||||
/// the `target_index` is in the hardened index range), this method will make a best-effort and
|
||||
/// reveal up to the last possible index.
|
||||
///
|
||||
/// This returns an iterator of newly revealed indices (alongside their scripts) and a
|
||||
/// [`super::ChangeSet`], which reports updates to the latest revealed index. If no new script
|
||||
/// pubkeys are revealed, then both of these will be empty.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `keychain` does not exist.
|
||||
pub fn reveal_to_target(
|
||||
&mut self,
|
||||
keychain: &K,
|
||||
target_index: u32,
|
||||
) -> (
|
||||
SpkIterator<Descriptor<DescriptorPublicKey>>,
|
||||
super::ChangeSet<K>,
|
||||
) {
|
||||
let descriptor = self.keychains.get(keychain).expect("keychain must exist");
|
||||
let has_wildcard = descriptor.has_wildcard();
|
||||
|
||||
let target_index = if has_wildcard { target_index } else { 0 };
|
||||
let next_reveal_index = self
|
||||
.last_revealed
|
||||
.get(keychain)
|
||||
.map_or(0, |index| *index + 1);
|
||||
|
||||
debug_assert!(next_reveal_index + self.lookahead >= self.next_store_index(keychain));
|
||||
|
||||
// If the target_index is already revealed, we are done
|
||||
if next_reveal_index > target_index {
|
||||
return (
|
||||
SpkIterator::new_with_range(
|
||||
descriptor.clone(),
|
||||
next_reveal_index..next_reveal_index,
|
||||
),
|
||||
super::ChangeSet::default(),
|
||||
);
|
||||
}
|
||||
|
||||
// We range over the indexes that are not stored and insert their spks in the index.
|
||||
// Indexes from next_reveal_index to next_reveal_index + lookahead are already stored (due
|
||||
// to lookahead), so we only range from next_reveal_index + lookahead to target + lookahead
|
||||
let range = next_reveal_index + self.lookahead..=target_index + self.lookahead;
|
||||
for (new_index, new_spk) in SpkIterator::new_with_range(descriptor, range) {
|
||||
let _inserted = self
|
||||
.inner
|
||||
.insert_spk((keychain.clone(), new_index), new_spk);
|
||||
debug_assert!(_inserted, "must not have existing spk");
|
||||
debug_assert!(
|
||||
has_wildcard || new_index == 0,
|
||||
"non-wildcard descriptors must not iterate past index 0"
|
||||
);
|
||||
}
|
||||
|
||||
let _old_index = self.last_revealed.insert(keychain.clone(), target_index);
|
||||
debug_assert!(_old_index < Some(target_index));
|
||||
(
|
||||
SpkIterator::new_with_range(descriptor.clone(), next_reveal_index..target_index + 1),
|
||||
super::ChangeSet(core::iter::once((keychain.clone(), target_index)).collect()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Attempts to reveal the next script pubkey for `keychain`.
|
||||
///
|
||||
/// Returns the derivation index of the revealed script pubkey, the revealed script pubkey and a
|
||||
/// [`super::ChangeSet`] which represents changes in the last revealed index (if any).
|
||||
///
|
||||
/// When a new script cannot be revealed, we return the last revealed script and an empty
|
||||
/// [`super::ChangeSet`]. There are two scenarios when a new script pubkey cannot be derived:
|
||||
///
|
||||
/// 1. The descriptor has no wildcard and already has one script revealed.
|
||||
/// 2. The descriptor has already revealed scripts up to the numeric bound.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the `keychain` does not exist.
|
||||
pub fn reveal_next_spk(&mut self, keychain: &K) -> ((u32, &Script), super::ChangeSet<K>) {
|
||||
let (next_index, _) = self.next_index(keychain);
|
||||
let changeset = self.reveal_to_target(keychain, next_index).1;
|
||||
let script = self
|
||||
.inner
|
||||
.spk_at_index(&(keychain.clone(), next_index))
|
||||
.expect("script must already be stored");
|
||||
((next_index, script), changeset)
|
||||
}
|
||||
|
||||
/// Gets the next unused script pubkey in the keychain. I.e., the script pubkey with the lowest
|
||||
/// index that has not been used yet.
|
||||
///
|
||||
/// This will derive and reveal a new script pubkey if no more unused script pubkeys exist.
|
||||
///
|
||||
/// If the descriptor has no wildcard and already has a used script pubkey or if a descriptor
|
||||
/// has used all scripts up to the derivation bounds, then the last derived script pubkey will be
|
||||
/// returned.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if `keychain` has never been added to the index
|
||||
pub fn next_unused_spk(&mut self, keychain: &K) -> ((u32, &Script), super::ChangeSet<K>) {
|
||||
let need_new = self.unused_keychain_spks(keychain).next().is_none();
|
||||
// this rather strange branch is needed because of some lifetime issues
|
||||
if need_new {
|
||||
self.reveal_next_spk(keychain)
|
||||
} else {
|
||||
(
|
||||
self.unused_keychain_spks(keychain)
|
||||
.next()
|
||||
.expect("we already know next exists"),
|
||||
super::ChangeSet::default(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
|
||||
/// `keychain`.
|
||||
///
|
||||
/// Use [`keychain_outpoints_in_range`](KeychainTxOutIndex::keychain_outpoints_in_range) to
|
||||
/// iterate over a specific derivation range.
|
||||
pub fn keychain_outpoints(
|
||||
&self,
|
||||
keychain: &K,
|
||||
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
|
||||
self.keychain_outpoints_in_range(keychain, ..)
|
||||
}
|
||||
|
||||
/// Iterate over [`OutPoint`]s that point to `TxOut`s with script pubkeys derived from
|
||||
/// `keychain` in a given derivation `range`.
|
||||
pub fn keychain_outpoints_in_range(
|
||||
&self,
|
||||
keychain: &K,
|
||||
range: impl RangeBounds<u32>,
|
||||
) -> impl DoubleEndedIterator<Item = (u32, OutPoint)> + '_ {
|
||||
let start = match range.start_bound() {
|
||||
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
|
||||
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
let end = match range.end_bound() {
|
||||
Bound::Included(i) => Bound::Included((keychain.clone(), *i)),
|
||||
Bound::Excluded(i) => Bound::Excluded((keychain.clone(), *i)),
|
||||
Bound::Unbounded => Bound::Unbounded,
|
||||
};
|
||||
self.inner
|
||||
.outputs_in_range((start, end))
|
||||
.map(|((_, i), op)| (*i, op))
|
||||
}
|
||||
|
||||
/// Returns the highest derivation index of the `keychain` where [`KeychainTxOutIndex`] has
|
||||
/// found a [`TxOut`] with it's script pubkey.
|
||||
pub fn last_used_index(&self, keychain: &K) -> Option<u32> {
|
||||
self.keychain_outpoints(keychain).last().map(|(i, _)| i)
|
||||
}
|
||||
|
||||
/// Returns the highest derivation index of each keychain that [`KeychainTxOutIndex`] has found
|
||||
/// a [`TxOut`] with it's script pubkey.
|
||||
pub fn last_used_indices(&self) -> BTreeMap<K, u32> {
|
||||
self.keychains
|
||||
.iter()
|
||||
.filter_map(|(keychain, _)| {
|
||||
self.last_used_index(keychain)
|
||||
.map(|index| (keychain.clone(), index))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Applies the derivation changeset to the [`KeychainTxOutIndex`], extending the number of
|
||||
/// derived scripts per keychain, as specified in the `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: super::ChangeSet<K>) {
|
||||
let _ = self.reveal_to_target_multi(&changeset.0);
|
||||
}
|
||||
}
|
||||
101
crates/chain/src/lib.rs
Normal file
101
crates/chain/src/lib.rs
Normal file
@@ -0,0 +1,101 @@
|
||||
//! This crate is a collection of core structures for [Bitcoin Dev Kit].
|
||||
//!
|
||||
//! The goal of this crate is to give wallets the mechanisms needed to:
|
||||
//!
|
||||
//! 1. Figure out what data they need to fetch.
|
||||
//! 2. Process the data in a way that never leads to inconsistent states.
|
||||
//! 3. Fully index that data and expose it to be consumed without friction.
|
||||
//!
|
||||
//! Our design goals for these mechanisms are:
|
||||
//!
|
||||
//! 1. Data source agnostic -- nothing in `bdk_chain` cares about where you get data from or whether
|
||||
//! you do it synchronously or asynchronously. If you know a fact about the blockchain, you can just
|
||||
//! tell `bdk_chain`'s APIs about it, and that information will be integrated, if it can be done
|
||||
//! consistently.
|
||||
//! 2. Data persistence agnostic -- `bdk_chain` does not care where you cache on-chain data, what you
|
||||
//! cache or how you retrieve it from persistent storage.
|
||||
//!
|
||||
//! [Bitcoin Dev Kit]: https://bitcoindevkit.org/
|
||||
|
||||
#![no_std]
|
||||
#![warn(missing_docs)]
|
||||
|
||||
pub use bitcoin;
|
||||
mod spk_txout_index;
|
||||
pub use spk_txout_index::*;
|
||||
mod chain_data;
|
||||
pub use chain_data::*;
|
||||
pub mod indexed_tx_graph;
|
||||
pub use indexed_tx_graph::IndexedTxGraph;
|
||||
pub mod keychain;
|
||||
pub mod local_chain;
|
||||
mod tx_data_traits;
|
||||
pub mod tx_graph;
|
||||
pub use tx_data_traits::*;
|
||||
pub use tx_graph::TxGraph;
|
||||
mod chain_oracle;
|
||||
pub use chain_oracle::*;
|
||||
mod persist;
|
||||
pub use persist::*;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod example_utils;
|
||||
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use miniscript;
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod descriptor_ext;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use descriptor_ext::DescriptorExt;
|
||||
#[cfg(feature = "miniscript")]
|
||||
mod spk_iter;
|
||||
#[cfg(feature = "miniscript")]
|
||||
pub use spk_iter::*;
|
||||
|
||||
#[allow(unused_imports)]
|
||||
#[macro_use]
|
||||
extern crate alloc;
|
||||
|
||||
#[cfg(feature = "serde")]
|
||||
pub extern crate serde_crate as serde;
|
||||
|
||||
#[cfg(feature = "bincode")]
|
||||
extern crate bincode;
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
#[macro_use]
|
||||
extern crate std;
|
||||
|
||||
#[cfg(all(not(feature = "std"), feature = "hashbrown"))]
|
||||
extern crate hashbrown;
|
||||
|
||||
// When no-std use `alloc`'s Hash collections. This is activated by default
|
||||
#[cfg(all(not(feature = "std"), not(feature = "hashbrown")))]
|
||||
#[doc(hidden)]
|
||||
pub mod collections {
|
||||
#![allow(dead_code)]
|
||||
pub type HashSet<K> = alloc::collections::BTreeSet<K>;
|
||||
pub type HashMap<K, V> = alloc::collections::BTreeMap<K, V>;
|
||||
pub use alloc::collections::{btree_map as hash_map, *};
|
||||
}
|
||||
|
||||
// When we have std, use `std`'s all collections
|
||||
#[cfg(all(feature = "std", not(feature = "hashbrown")))]
|
||||
#[doc(hidden)]
|
||||
pub mod collections {
|
||||
pub use std::collections::{hash_map, *};
|
||||
}
|
||||
|
||||
// With this special feature `hashbrown`, use `hashbrown`'s hash collections, and else from `alloc`.
|
||||
#[cfg(feature = "hashbrown")]
|
||||
#[doc(hidden)]
|
||||
pub mod collections {
|
||||
#![allow(dead_code)]
|
||||
pub type HashSet<K> = hashbrown::HashSet<K>;
|
||||
pub type HashMap<K, V> = hashbrown::HashMap<K, V>;
|
||||
pub use alloc::collections::*;
|
||||
pub use hashbrown::hash_map;
|
||||
}
|
||||
|
||||
/// How many confirmations are needed f or a coinbase output to be spent.
|
||||
pub const COINBASE_MATURITY: u32 = 100;
|
||||
799
crates/chain/src/local_chain.rs
Normal file
799
crates/chain/src/local_chain.rs
Normal file
@@ -0,0 +1,799 @@
|
||||
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
||||
|
||||
use core::convert::Infallible;
|
||||
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::{BlockId, ChainOracle};
|
||||
use alloc::sync::Arc;
|
||||
use bitcoin::block::Header;
|
||||
use bitcoin::BlockHash;
|
||||
|
||||
/// The [`ChangeSet`] represents changes to [`LocalChain`].
|
||||
///
|
||||
/// The key represents the block height, and the value either represents added a new [`CheckPoint`]
|
||||
/// (if [`Some`]), or removing a [`CheckPoint`] (if [`None`]).
|
||||
pub type ChangeSet = BTreeMap<u32, Option<BlockHash>>;
|
||||
|
||||
/// A [`LocalChain`] checkpoint is used to find the agreement point between two chains and as a
|
||||
/// transaction anchor.
|
||||
///
|
||||
/// Each checkpoint contains the height and hash of a block ([`BlockId`]).
|
||||
///
|
||||
/// Internally, checkpoints are nodes of a reference-counted linked-list. This allows the caller to
|
||||
/// cheaply clone a [`CheckPoint`] without copying the whole list and to view the entire chain
|
||||
/// without holding a lock on [`LocalChain`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct CheckPoint(Arc<CPInner>);
|
||||
|
||||
/// The internal contents of [`CheckPoint`].
|
||||
#[derive(Debug, Clone)]
|
||||
struct CPInner {
|
||||
/// Block id (hash and height).
|
||||
block: BlockId,
|
||||
/// Previous checkpoint (if any).
|
||||
prev: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl CheckPoint {
|
||||
/// Construct a new base block at the front of a linked list.
|
||||
pub fn new(block: BlockId) -> Self {
|
||||
Self(Arc::new(CPInner { block, prev: None }))
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from a list of [`BlockId`]s in ascending height order.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This method will error if any of the follow occurs:
|
||||
///
|
||||
/// - The `blocks` iterator is empty, in which case, the error will be `None`.
|
||||
/// - The `blocks` iterator is not in ascending height order.
|
||||
/// - The `blocks` iterator contains multiple [`BlockId`]s of the same height.
|
||||
///
|
||||
/// The error type is the last successful checkpoint constructed (if any).
|
||||
pub fn from_block_ids(
|
||||
block_ids: impl IntoIterator<Item = BlockId>,
|
||||
) -> Result<Self, Option<Self>> {
|
||||
let mut blocks = block_ids.into_iter();
|
||||
let mut acc = CheckPoint::new(blocks.next().ok_or(None)?);
|
||||
for id in blocks {
|
||||
acc = acc.push(id).map_err(Some)?;
|
||||
}
|
||||
Ok(acc)
|
||||
}
|
||||
|
||||
/// Construct a checkpoint from the given `header` and block `height`.
|
||||
///
|
||||
/// If `header` is of the genesis block, the checkpoint won't have a [`prev`] node. Otherwise,
|
||||
/// we return a checkpoint linked with the previous block.
|
||||
///
|
||||
/// [`prev`]: CheckPoint::prev
|
||||
pub fn from_header(header: &bitcoin::block::Header, height: u32) -> Self {
|
||||
let hash = header.block_hash();
|
||||
let this_block_id = BlockId { height, hash };
|
||||
|
||||
let prev_height = match height.checked_sub(1) {
|
||||
Some(h) => h,
|
||||
None => return Self::new(this_block_id),
|
||||
};
|
||||
|
||||
let prev_block_id = BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
};
|
||||
|
||||
CheckPoint::new(prev_block_id)
|
||||
.push(this_block_id)
|
||||
.expect("must construct checkpoint")
|
||||
}
|
||||
|
||||
/// Convenience method to convert the [`CheckPoint`] into an [`Update`].
|
||||
///
|
||||
/// For more information, refer to [`Update`].
|
||||
pub fn into_update(self, introduce_older_blocks: bool) -> Update {
|
||||
Update {
|
||||
tip: self,
|
||||
introduce_older_blocks,
|
||||
}
|
||||
}
|
||||
|
||||
/// Puts another checkpoint onto the linked list representing the blockchain.
|
||||
///
|
||||
/// Returns an `Err(self)` if the block you are pushing on is not at a greater height that the one you
|
||||
/// are pushing on to.
|
||||
pub fn push(self, block: BlockId) -> Result<Self, Self> {
|
||||
if self.height() < block.height {
|
||||
Ok(Self(Arc::new(CPInner {
|
||||
block,
|
||||
prev: Some(self.0),
|
||||
})))
|
||||
} else {
|
||||
Err(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// Extends the checkpoint linked list by a iterator of block ids.
|
||||
///
|
||||
/// Returns an `Err(self)` if there is block which does not have a greater height than the
|
||||
/// previous one.
|
||||
pub fn extend(self, blocks: impl IntoIterator<Item = BlockId>) -> Result<Self, Self> {
|
||||
let mut curr = self.clone();
|
||||
for block in blocks {
|
||||
curr = curr.push(block).map_err(|_| self.clone())?;
|
||||
}
|
||||
Ok(curr)
|
||||
}
|
||||
|
||||
/// Get the [`BlockId`] of the checkpoint.
|
||||
pub fn block_id(&self) -> BlockId {
|
||||
self.0.block
|
||||
}
|
||||
|
||||
/// Get the height of the checkpoint.
|
||||
pub fn height(&self) -> u32 {
|
||||
self.0.block.height
|
||||
}
|
||||
|
||||
/// Get the block hash of the checkpoint.
|
||||
pub fn hash(&self) -> BlockHash {
|
||||
self.0.block.hash
|
||||
}
|
||||
|
||||
/// Get the previous checkpoint in the chain
|
||||
pub fn prev(&self) -> Option<CheckPoint> {
|
||||
self.0.prev.clone().map(CheckPoint)
|
||||
}
|
||||
|
||||
/// Iterate from this checkpoint in descending height.
|
||||
pub fn iter(&self) -> CheckPointIter {
|
||||
self.clone().into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over checkpoints backwards.
|
||||
pub struct CheckPointIter {
|
||||
current: Option<Arc<CPInner>>,
|
||||
}
|
||||
|
||||
impl Iterator for CheckPointIter {
|
||||
type Item = CheckPoint;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
let current = self.current.clone()?;
|
||||
self.current = current.prev.clone();
|
||||
Some(CheckPoint(current))
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoIterator for CheckPoint {
|
||||
type Item = CheckPoint;
|
||||
type IntoIter = CheckPointIter;
|
||||
|
||||
fn into_iter(self) -> Self::IntoIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Used to update [`LocalChain`].
|
||||
///
|
||||
/// This is used as input for [`LocalChain::apply_update`]. It contains the update's chain `tip` and
|
||||
/// a flag `introduce_older_blocks` which signals whether this update intends to introduce missing
|
||||
/// blocks to the original chain.
|
||||
///
|
||||
/// Block-by-block syncing mechanisms would typically create updates that builds upon the previous
|
||||
/// tip. In this case, `introduce_older_blocks` would be `false`.
|
||||
///
|
||||
/// Script-pubkey based syncing mechanisms may not introduce transactions in a chronological order
|
||||
/// so some updates require introducing older blocks (to anchor older transactions). For
|
||||
/// script-pubkey based syncing, `introduce_older_blocks` would typically be `true`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Update {
|
||||
/// The update chain's new tip.
|
||||
pub tip: CheckPoint,
|
||||
|
||||
/// Whether the update allows for introducing older blocks.
|
||||
///
|
||||
/// Refer to [struct-level documentation] for more.
|
||||
///
|
||||
/// [struct-level documentation]: Update
|
||||
pub introduce_older_blocks: bool,
|
||||
}
|
||||
|
||||
/// This is a local implementation of [`ChainOracle`].
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct LocalChain {
|
||||
tip: CheckPoint,
|
||||
index: BTreeMap<u32, BlockHash>,
|
||||
}
|
||||
|
||||
impl PartialEq for LocalChain {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
self.index == other.index
|
||||
}
|
||||
}
|
||||
|
||||
impl From<LocalChain> for BTreeMap<u32, BlockHash> {
|
||||
fn from(value: LocalChain) -> Self {
|
||||
value.index
|
||||
}
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
if block.height > chain_tip.height {
|
||||
return Ok(None);
|
||||
}
|
||||
Ok(
|
||||
match (
|
||||
self.index.get(&block.height),
|
||||
self.index.get(&chain_tip.height),
|
||||
) {
|
||||
(Some(cp), Some(tip_cp)) => Some(*cp == block.hash && *tip_cp == chain_tip.hash),
|
||||
_ => None,
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
fn get_chain_tip(&self) -> Result<BlockId, Self::Error> {
|
||||
Ok(self.tip.block_id())
|
||||
}
|
||||
}
|
||||
|
||||
impl LocalChain {
|
||||
/// Get the genesis hash.
|
||||
pub fn genesis_hash(&self) -> BlockHash {
|
||||
self.index.get(&0).copied().expect("must have genesis hash")
|
||||
}
|
||||
|
||||
/// Construct [`LocalChain`] from genesis `hash`.
|
||||
#[must_use]
|
||||
pub fn from_genesis_hash(hash: BlockHash) -> (Self, ChangeSet) {
|
||||
let height = 0;
|
||||
let chain = Self {
|
||||
tip: CheckPoint::new(BlockId { height, hash }),
|
||||
index: core::iter::once((height, hash)).collect(),
|
||||
};
|
||||
let changeset = chain.initial_changeset();
|
||||
(chain, changeset)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from an initial `changeset`.
|
||||
pub fn from_changeset(changeset: ChangeSet) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_entry = changeset.get(&0).copied().flatten();
|
||||
let genesis_hash = match genesis_entry {
|
||||
Some(hash) => hash,
|
||||
None => return Err(MissingGenesisError),
|
||||
};
|
||||
|
||||
let (mut chain, _) = Self::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&changeset)?;
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
debug_assert!(chain._check_changeset_is_applied(&changeset));
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||
let mut chain = Self {
|
||||
tip,
|
||||
index: BTreeMap::new(),
|
||||
};
|
||||
chain.reindex(0);
|
||||
|
||||
if chain.index.get(&0).copied().is_none() {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Constructs a [`LocalChain`] from a [`BTreeMap`] of height to [`BlockHash`].
|
||||
///
|
||||
/// The [`BTreeMap`] enforces the height order. However, the caller must ensure the blocks are
|
||||
/// all of the same chain.
|
||||
pub fn from_blocks(blocks: BTreeMap<u32, BlockHash>) -> Result<Self, MissingGenesisError> {
|
||||
if !blocks.contains_key(&0) {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
|
||||
let mut tip: Option<CheckPoint> = None;
|
||||
|
||||
for block in &blocks {
|
||||
match tip {
|
||||
Some(curr) => {
|
||||
tip = Some(
|
||||
curr.push(BlockId::from(block))
|
||||
.expect("BTreeMap is ordered"),
|
||||
)
|
||||
}
|
||||
None => tip = Some(CheckPoint::new(BlockId::from(block))),
|
||||
}
|
||||
}
|
||||
|
||||
let chain = Self {
|
||||
index: blocks,
|
||||
tip: tip.expect("already checked to have genesis"),
|
||||
};
|
||||
|
||||
debug_assert!(chain._check_index_is_consistent_with_tip());
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Get the highest checkpoint.
|
||||
pub fn tip(&self) -> CheckPoint {
|
||||
self.tip.clone()
|
||||
}
|
||||
|
||||
/// Applies the given `update` to the chain.
|
||||
///
|
||||
/// The method returns [`ChangeSet`] on success. This represents the applied changes to `self`.
|
||||
///
|
||||
/// There must be no ambiguity about which of the existing chain's blocks are still valid and
|
||||
/// which are now invalid. That is, the new chain must implicitly connect to a definite block in
|
||||
/// the existing chain and invalidate the block after it (if it exists) by including a block at
|
||||
/// the same height but with a different hash to explicitly exclude it as a connection point.
|
||||
///
|
||||
/// Additionally, an empty chain can be updated with any chain, and a chain with a single block
|
||||
/// can have it's block invalidated by an update chain with a block at the same height but
|
||||
/// different hash.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// An error will occur if the update does not correctly connect with `self`.
|
||||
///
|
||||
/// Refer to [`Update`] for more about the update struct.
|
||||
///
|
||||
/// [module-level documentation]: crate::local_chain
|
||||
pub fn apply_update(&mut self, update: Update) -> Result<ChangeSet, CannotConnectError> {
|
||||
let changeset = merge_chains(
|
||||
self.tip.clone(),
|
||||
update.tip.clone(),
|
||||
update.introduce_older_blocks,
|
||||
)?;
|
||||
// `._check_index_is_consistent_with_tip` and `._check_changeset_is_applied` is called in
|
||||
// `.apply_changeset`
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| CannotConnectError {
|
||||
try_include_height: 0,
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] at `height` which you claim is connected to a existing block in the chain.
|
||||
///
|
||||
/// This is useful when you have a block header that you want to record as part of the chain but
|
||||
/// don't necessarily know that the `prev_blockhash` is in the chain.
|
||||
///
|
||||
/// This will usually insert two new [`BlockId`]s into the chain: the header's block and the
|
||||
/// header's `prev_blockhash` block. `connected_to` must already be in the chain but is allowed
|
||||
/// to be `prev_blockhash` (in which case only one new block id will be inserted).
|
||||
/// To be successful, `connected_to` must be chosen carefully so that `LocalChain`'s [update
|
||||
/// rules][`apply_update`] are satisfied.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// [`ApplyHeaderError::InconsistentBlocks`] occurs if the `connected_to` block and the
|
||||
/// [`Header`] is inconsistent. For example, if the `connected_to` block is the same height as
|
||||
/// `header` or `prev_blockhash`, but has a different block hash. Or if the `connected_to`
|
||||
/// height is greater than the header's `height`.
|
||||
///
|
||||
/// [`ApplyHeaderError::CannotConnect`] occurs if the internal call to [`apply_update`] fails.
|
||||
///
|
||||
/// [`apply_update`]: Self::apply_update
|
||||
pub fn apply_header_connected_to(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
) -> Result<ChangeSet, ApplyHeaderError> {
|
||||
let this = BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
};
|
||||
let prev = height.checked_sub(1).map(|prev_height| BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
});
|
||||
let conn = match connected_to {
|
||||
// `connected_to` can be ignored if same as `this` or `prev` (duplicate)
|
||||
conn if conn == this || Some(conn) == prev => None,
|
||||
// this occurs if:
|
||||
// - `connected_to` height is the same as `prev`, but different hash
|
||||
// - `connected_to` height is the same as `this`, but different hash
|
||||
// - `connected_to` height is greater than `this` (this is not allowed)
|
||||
conn if conn.height >= height.saturating_sub(1) => {
|
||||
return Err(ApplyHeaderError::InconsistentBlocks)
|
||||
}
|
||||
conn => Some(conn),
|
||||
};
|
||||
|
||||
let update = Update {
|
||||
tip: CheckPoint::from_block_ids([conn, prev, Some(this)].into_iter().flatten())
|
||||
.expect("block ids must be in order"),
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
|
||||
self.apply_update(update)
|
||||
.map_err(ApplyHeaderError::CannotConnect)
|
||||
}
|
||||
|
||||
/// Update the chain with a given [`Header`] connecting it with the previous block.
|
||||
///
|
||||
/// This is a convenience method to call [`apply_header_connected_to`] with the `connected_to`
|
||||
/// parameter being `height-1:prev_blockhash`. If there is no previous block (i.e. genesis), we
|
||||
/// use the current block as `connected_to`.
|
||||
///
|
||||
/// [`apply_header_connected_to`]: LocalChain::apply_header_connected_to
|
||||
pub fn apply_header(
|
||||
&mut self,
|
||||
header: &Header,
|
||||
height: u32,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let connected_to = match height.checked_sub(1) {
|
||||
Some(prev_height) => BlockId {
|
||||
height: prev_height,
|
||||
hash: header.prev_blockhash,
|
||||
},
|
||||
None => BlockId {
|
||||
height,
|
||||
hash: header.block_hash(),
|
||||
},
|
||||
};
|
||||
self.apply_header_connected_to(header, height, connected_to)
|
||||
.map_err(|err| match err {
|
||||
ApplyHeaderError::InconsistentBlocks => {
|
||||
unreachable!("connected_to is derived from the block so is always consistent")
|
||||
}
|
||||
ApplyHeaderError::CannotConnect(err) => err,
|
||||
})
|
||||
}
|
||||
|
||||
/// Apply the given `changeset`.
|
||||
pub fn apply_changeset(&mut self, changeset: &ChangeSet) -> Result<(), MissingGenesisError> {
|
||||
if let Some(start_height) = changeset.keys().next().cloned() {
|
||||
// changes after point of agreement
|
||||
let mut extension = BTreeMap::default();
|
||||
// point of agreement
|
||||
let mut base: Option<CheckPoint> = None;
|
||||
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() >= start_height {
|
||||
extension.insert(cp.height(), cp.hash());
|
||||
} else {
|
||||
base = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (&height, &hash) in changeset {
|
||||
match hash {
|
||||
Some(hash) => {
|
||||
extension.insert(height, hash);
|
||||
}
|
||||
None => {
|
||||
extension.remove(&height);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
let new_tip = match base {
|
||||
Some(base) => base
|
||||
.extend(extension.into_iter().map(BlockId::from))
|
||||
.expect("extension is strictly greater than base"),
|
||||
None => LocalChain::from_blocks(extension)?.tip(),
|
||||
};
|
||||
self.tip = new_tip;
|
||||
self.reindex(start_height);
|
||||
|
||||
debug_assert!(self._check_index_is_consistent_with_tip());
|
||||
debug_assert!(self._check_changeset_is_applied(changeset));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Insert a [`BlockId`].
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// Replacing the block hash of an existing checkpoint will result in an error.
|
||||
pub fn insert_block(&mut self, block_id: BlockId) -> Result<ChangeSet, AlterCheckPointError> {
|
||||
if let Some(&original_hash) = self.index.get(&block_id.height) {
|
||||
if original_hash != block_id.hash {
|
||||
return Err(AlterCheckPointError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: Some(block_id.hash),
|
||||
});
|
||||
} else {
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
}
|
||||
|
||||
let mut changeset = ChangeSet::default();
|
||||
changeset.insert(block_id.height, Some(block_id.hash));
|
||||
self.apply_changeset(&changeset)
|
||||
.map_err(|_| AlterCheckPointError {
|
||||
height: 0,
|
||||
original_hash: self.genesis_hash(),
|
||||
update_hash: changeset.get(&0).cloned().flatten(),
|
||||
})?;
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Removes blocks from (and inclusive of) the given `block_id`.
|
||||
///
|
||||
/// This will remove blocks with a height equal or greater than `block_id`, but only if
|
||||
/// `block_id` exists in the chain.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// This will fail with [`MissingGenesisError`] if the caller attempts to disconnect from the
|
||||
/// genesis block.
|
||||
pub fn disconnect_from(&mut self, block_id: BlockId) -> Result<ChangeSet, MissingGenesisError> {
|
||||
if self.index.get(&block_id.height) != Some(&block_id.hash) {
|
||||
return Ok(ChangeSet::default());
|
||||
}
|
||||
|
||||
let changeset = self
|
||||
.index
|
||||
.range(block_id.height..)
|
||||
.map(|(&height, _)| (height, None))
|
||||
.collect::<ChangeSet>();
|
||||
self.apply_changeset(&changeset).map(|_| changeset)
|
||||
}
|
||||
|
||||
/// Reindex the heights in the chain from (and including) `from` height
|
||||
fn reindex(&mut self, from: u32) {
|
||||
let _ = self.index.split_off(&from);
|
||||
for cp in self.iter_checkpoints() {
|
||||
if cp.height() < from {
|
||||
break;
|
||||
}
|
||||
self.index.insert(cp.height(), cp.hash());
|
||||
}
|
||||
}
|
||||
|
||||
/// Derives an initial [`ChangeSet`], meaning that it can be applied to an empty chain to
|
||||
/// recover the current chain.
|
||||
pub fn initial_changeset(&self) -> ChangeSet {
|
||||
self.index.iter().map(|(k, v)| (*k, Some(*v))).collect()
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
pub fn iter_checkpoints(&self) -> CheckPointIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.tip.0.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the internal index mapping the height to block hash.
|
||||
pub fn blocks(&self) -> &BTreeMap<u32, BlockHash> {
|
||||
&self.index
|
||||
}
|
||||
|
||||
fn _check_index_is_consistent_with_tip(&self) -> bool {
|
||||
let tip_history = self
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
self.index == tip_history
|
||||
}
|
||||
|
||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||
for (height, exp_hash) in changeset {
|
||||
if self.index.get(height) != exp_hash.as_ref() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// An error which occurs when a [`LocalChain`] is constructed without a genesis checkpoint.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct MissingGenesisError;
|
||||
|
||||
impl core::fmt::Display for MissingGenesisError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"cannot construct `LocalChain` without a genesis checkpoint"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for MissingGenesisError {}
|
||||
|
||||
/// Represents a failure when trying to insert/remove a checkpoint to/from [`LocalChain`].
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct AlterCheckPointError {
|
||||
/// The checkpoint's height.
|
||||
pub height: u32,
|
||||
/// The original checkpoint's block hash which cannot be replaced/removed.
|
||||
pub original_hash: BlockHash,
|
||||
/// The attempted update to the `original_block` hash.
|
||||
pub update_hash: Option<BlockHash>,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for AlterCheckPointError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self.update_hash {
|
||||
Some(update_hash) => write!(
|
||||
f,
|
||||
"failed to insert block at height {}: original={} update={}",
|
||||
self.height, self.original_hash, update_hash
|
||||
),
|
||||
None => write!(
|
||||
f,
|
||||
"failed to remove block at height {}: original={}",
|
||||
self.height, self.original_hash
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for AlterCheckPointError {}
|
||||
|
||||
/// Occurs when an update does not have a common checkpoint with the original chain.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct CannotConnectError {
|
||||
/// The suggested checkpoint to include to connect the two chains.
|
||||
pub try_include_height: u32,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CannotConnectError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"introduced chain cannot connect with the original chain, try include height {}",
|
||||
self.try_include_height,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for CannotConnectError {}
|
||||
|
||||
/// The error type for [`LocalChain::apply_header_connected_to`].
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum ApplyHeaderError {
|
||||
/// Occurs when `connected_to` block conflicts with either the current block or previous block.
|
||||
InconsistentBlocks,
|
||||
/// Occurs when the update cannot connect with the original chain.
|
||||
CannotConnect(CannotConnectError),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for ApplyHeaderError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
ApplyHeaderError::InconsistentBlocks => write!(
|
||||
f,
|
||||
"the `connected_to` block conflicts with either the current or previous block"
|
||||
),
|
||||
ApplyHeaderError::CannotConnect(err) => core::fmt::Display::fmt(err, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "std")]
|
||||
impl std::error::Error for ApplyHeaderError {}
|
||||
|
||||
fn merge_chains(
|
||||
original_tip: CheckPoint,
|
||||
update_tip: CheckPoint,
|
||||
introduce_older_blocks: bool,
|
||||
) -> Result<ChangeSet, CannotConnectError> {
|
||||
let mut changeset = ChangeSet::default();
|
||||
let mut orig = original_tip.into_iter();
|
||||
let mut update = update_tip.into_iter();
|
||||
let mut curr_orig = None;
|
||||
let mut curr_update = None;
|
||||
let mut prev_orig: Option<CheckPoint> = None;
|
||||
let mut prev_update: Option<CheckPoint> = None;
|
||||
let mut point_of_agreement_found = false;
|
||||
let mut prev_orig_was_invalidated = false;
|
||||
let mut potentially_invalidated_heights = vec![];
|
||||
|
||||
// To find the difference between the new chain and the original we iterate over both of them
|
||||
// from the tip backwards in tandem. We always dealing with the highest one from either chain
|
||||
// first and move to the next highest. The crucial logic is applied when they have blocks at the
|
||||
// same height.
|
||||
loop {
|
||||
if curr_orig.is_none() {
|
||||
curr_orig = orig.next();
|
||||
}
|
||||
if curr_update.is_none() {
|
||||
curr_update = update.next();
|
||||
}
|
||||
|
||||
match (curr_orig.as_ref(), curr_update.as_ref()) {
|
||||
// Update block that doesn't exist in the original chain
|
||||
(o, Some(u)) if Some(u.height()) > o.map(|o| o.height()) => {
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
prev_update = curr_update.take();
|
||||
}
|
||||
// Original block that isn't in the update
|
||||
(Some(o), u) if Some(o.height()) > u.map(|u| u.height()) => {
|
||||
// this block might be gone if an earlier block gets invalidated
|
||||
potentially_invalidated_heights.push(o.height());
|
||||
prev_orig_was_invalidated = false;
|
||||
prev_orig = curr_orig.take();
|
||||
|
||||
// OPTIMIZATION: we have run out of update blocks so we don't need to continue
|
||||
// iterating because there's no possibility of adding anything to changeset.
|
||||
if u.is_none() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
(Some(o), Some(u)) => {
|
||||
if o.hash() == u.hash() {
|
||||
// We have found our point of agreement 🎉 -- we require that the previous (i.e.
|
||||
// higher because we are iterating backwards) block in the original chain was
|
||||
// invalidated (if it exists). This ensures that there is an unambiguous point of
|
||||
// connection to the original chain from the update chain (i.e. we know the
|
||||
// precisely which original blocks are invalid).
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let (Some(prev_orig), Some(_prev_update)) = (&prev_orig, &prev_update) {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
point_of_agreement_found = true;
|
||||
prev_orig_was_invalidated = false;
|
||||
// OPTIMIZATION 1 -- If we know that older blocks cannot be introduced without
|
||||
// invalidation, we can break after finding the point of agreement.
|
||||
// OPTIMIZATION 2 -- if we have the same underlying pointer at this point, we
|
||||
// can guarantee that no older blocks are introduced.
|
||||
if !introduce_older_blocks || Arc::as_ptr(&o.0) == Arc::as_ptr(&u.0) {
|
||||
return Ok(changeset);
|
||||
}
|
||||
} else {
|
||||
// We have an invalidation height so we set the height to the updated hash and
|
||||
// also purge all the original chain block hashes above this block.
|
||||
changeset.insert(u.height(), Some(u.hash()));
|
||||
for invalidated_height in potentially_invalidated_heights.drain(..) {
|
||||
changeset.insert(invalidated_height, None);
|
||||
}
|
||||
prev_orig_was_invalidated = true;
|
||||
}
|
||||
prev_update = curr_update.take();
|
||||
prev_orig = curr_orig.take();
|
||||
}
|
||||
(None, None) => {
|
||||
break;
|
||||
}
|
||||
_ => {
|
||||
unreachable!("compiler cannot tell that everything has been covered")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// When we don't have a point of agreement you can imagine it is implicitly the
|
||||
// genesis block so we need to do the final connectivity check which in this case
|
||||
// just means making sure the entire original chain was invalidated.
|
||||
if !prev_orig_was_invalidated && !point_of_agreement_found {
|
||||
if let Some(prev_orig) = prev_orig {
|
||||
return Err(CannotConnectError {
|
||||
try_include_height: prev_orig.height(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(changeset)
|
||||
}
|
||||
109
crates/chain/src/persist.rs
Normal file
109
crates/chain/src/persist.rs
Normal file
@@ -0,0 +1,109 @@
|
||||
use core::convert::Infallible;
|
||||
|
||||
use crate::Append;
|
||||
|
||||
/// `Persist` wraps a [`PersistBackend`] (`B`) to create a convenient staging area for changes (`C`)
|
||||
/// before they are persisted.
|
||||
///
|
||||
/// Not all changes to the in-memory representation needs to be written to disk right away, so
|
||||
/// [`Persist::stage`] can be used to *stage* changes first and then [`Persist::commit`] can be used
|
||||
/// to write changes to disk.
|
||||
#[derive(Debug)]
|
||||
pub struct Persist<B, C> {
|
||||
backend: B,
|
||||
stage: C,
|
||||
}
|
||||
|
||||
impl<B, C> Persist<B, C>
|
||||
where
|
||||
B: PersistBackend<C>,
|
||||
C: Default + Append,
|
||||
{
|
||||
/// Create a new [`Persist`] from [`PersistBackend`].
|
||||
pub fn new(backend: B) -> Self {
|
||||
Self {
|
||||
backend,
|
||||
stage: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Stage a `changeset` to be committed later with [`commit`].
|
||||
///
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn stage(&mut self, changeset: C) {
|
||||
self.stage.append(changeset)
|
||||
}
|
||||
|
||||
/// Get the changes that have not been committed yet.
|
||||
pub fn staged(&self) -> &C {
|
||||
&self.stage
|
||||
}
|
||||
|
||||
/// Commit the staged changes to the underlying persistence backend.
|
||||
///
|
||||
/// Changes that are committed (if any) are returned.
|
||||
///
|
||||
/// # Error
|
||||
///
|
||||
/// Returns a backend-defined error if this fails.
|
||||
pub fn commit(&mut self) -> Result<Option<C>, B::WriteError> {
|
||||
if self.stage.is_empty() {
|
||||
return Ok(None);
|
||||
}
|
||||
self.backend
|
||||
.write_changes(&self.stage)
|
||||
// if written successfully, take and return `self.stage`
|
||||
.map(|_| Some(core::mem::take(&mut self.stage)))
|
||||
}
|
||||
|
||||
/// Stages a new changeset and commits it (along with any other previously staged changes) to
|
||||
/// the persistence backend
|
||||
///
|
||||
/// Convenience method for calling [`stage`] and then [`commit`].
|
||||
///
|
||||
/// [`stage`]: Self::stage
|
||||
/// [`commit`]: Self::commit
|
||||
pub fn stage_and_commit(&mut self, changeset: C) -> Result<Option<C>, B::WriteError> {
|
||||
self.stage(changeset);
|
||||
self.commit()
|
||||
}
|
||||
}
|
||||
|
||||
/// A persistence backend for [`Persist`].
|
||||
///
|
||||
/// `C` represents the changeset; a datatype that records changes made to in-memory data structures
|
||||
/// that are to be persisted, or retrieved from persistence.
|
||||
pub trait PersistBackend<C> {
|
||||
/// The error the backend returns when it fails to write.
|
||||
type WriteError: core::fmt::Debug;
|
||||
|
||||
/// The error the backend returns when it fails to load changesets `C`.
|
||||
type LoadError: core::fmt::Debug;
|
||||
|
||||
/// Writes a changeset to the persistence backend.
|
||||
///
|
||||
/// It is up to the backend what it does with this. It could store every changeset in a list or
|
||||
/// it inserts the actual changes into a more structured database. All it needs to guarantee is
|
||||
/// that [`load_from_persistence`] restores a keychain tracker to what it should be if all
|
||||
/// changesets had been applied sequentially.
|
||||
///
|
||||
/// [`load_from_persistence`]: Self::load_from_persistence
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError>;
|
||||
|
||||
/// Return the aggregate changeset `C` from persistence.
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError>;
|
||||
}
|
||||
|
||||
impl<C> PersistBackend<C> for () {
|
||||
type WriteError = Infallible;
|
||||
|
||||
type LoadError = Infallible;
|
||||
|
||||
fn write_changes(&mut self, _changeset: &C) -> Result<(), Self::WriteError> {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
274
crates/chain/src/spk_iter.rs
Normal file
274
crates/chain/src/spk_iter.rs
Normal file
@@ -0,0 +1,274 @@
|
||||
use crate::{
|
||||
bitcoin::{secp256k1::Secp256k1, ScriptBuf},
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
};
|
||||
use core::{borrow::Borrow, ops::Bound, ops::RangeBounds};
|
||||
|
||||
/// Maximum [BIP32](https://bips.xyz/32) derivation index.
|
||||
pub const BIP32_MAX_INDEX: u32 = (1 << 31) - 1;
|
||||
|
||||
/// An iterator for derived script pubkeys.
|
||||
///
|
||||
/// [`SpkIterator`] is an implementation of the [`Iterator`] trait which possesses its own `next()`
|
||||
/// and `nth()` functions, both of which circumvent the unnecessary intermediate derivations required
|
||||
/// when using their default implementations.
|
||||
///
|
||||
/// ## Examples
|
||||
///
|
||||
/// ```
|
||||
/// use bdk_chain::SpkIterator;
|
||||
/// # use miniscript::{Descriptor, DescriptorPublicKey};
|
||||
/// # use bitcoin::{secp256k1::Secp256k1};
|
||||
/// # use std::str::FromStr;
|
||||
/// # let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
/// # let (descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||
/// # let external_spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
/// # let external_spk_3 = descriptor.at_derivation_index(3).unwrap().script_pubkey();
|
||||
/// # let external_spk_4 = descriptor.at_derivation_index(4).unwrap().script_pubkey();
|
||||
///
|
||||
/// // Creates a new script pubkey iterator starting at 0 from a descriptor.
|
||||
/// let mut spk_iter = SpkIterator::new(&descriptor);
|
||||
/// assert_eq!(spk_iter.next(), Some((0, external_spk_0)));
|
||||
/// assert_eq!(spk_iter.next(), None);
|
||||
/// ```
|
||||
#[derive(Clone)]
|
||||
pub struct SpkIterator<D> {
|
||||
next_index: u32,
|
||||
end: u32,
|
||||
descriptor: D,
|
||||
secp: Secp256k1<bitcoin::secp256k1::VerifyOnly>,
|
||||
}
|
||||
|
||||
impl<D> SpkIterator<D>
|
||||
where
|
||||
D: Borrow<Descriptor<DescriptorPublicKey>>,
|
||||
{
|
||||
/// Create a new script pubkey iterator from `descriptor`.
|
||||
///
|
||||
/// This iterates from derivation index 0 and stops at index 0x7FFFFFFF (as specified in
|
||||
/// BIP-32). Non-wildcard descriptors will only return one script pubkey at derivation index 0.
|
||||
///
|
||||
/// Use [`new_with_range`](SpkIterator::new_with_range) to create an iterator with a specified
|
||||
/// derivation index range.
|
||||
pub fn new(descriptor: D) -> Self {
|
||||
SpkIterator::new_with_range(descriptor, 0..=BIP32_MAX_INDEX)
|
||||
}
|
||||
|
||||
/// Create a new script pubkey iterator from `descriptor` and a given `range`.
|
||||
///
|
||||
/// Non-wildcard descriptors will only emit a single script pubkey (at derivation index 0).
|
||||
/// Wildcard descriptors have an end-bound of 0x7FFFFFFF (inclusive).
|
||||
///
|
||||
/// Refer to [`new`](SpkIterator::new) for more.
|
||||
pub fn new_with_range<R>(descriptor: D, range: R) -> Self
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
let start = match range.start_bound() {
|
||||
Bound::Included(start) => *start,
|
||||
Bound::Excluded(start) => *start + 1,
|
||||
Bound::Unbounded => u32::MIN,
|
||||
};
|
||||
|
||||
let mut end = match range.end_bound() {
|
||||
Bound::Included(end) => *end + 1,
|
||||
Bound::Excluded(end) => *end,
|
||||
Bound::Unbounded => u32::MAX,
|
||||
};
|
||||
|
||||
// Because `end` is exclusive, we want the maximum value to be BIP32_MAX_INDEX + 1.
|
||||
end = end.min(BIP32_MAX_INDEX + 1);
|
||||
|
||||
Self {
|
||||
next_index: start,
|
||||
end,
|
||||
descriptor,
|
||||
secp: Secp256k1::verification_only(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a reference to the internal descriptor.
|
||||
pub fn descriptor(&self) -> &D {
|
||||
&self.descriptor
|
||||
}
|
||||
}
|
||||
|
||||
impl<D> Iterator for SpkIterator<D>
|
||||
where
|
||||
D: Borrow<Descriptor<DescriptorPublicKey>>,
|
||||
{
|
||||
type Item = (u32, ScriptBuf);
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
// For non-wildcard descriptors, we expect the first element to be Some((0, spk)), then None after.
|
||||
// For wildcard descriptors, we expect it to keep iterating until exhausted.
|
||||
if self.next_index >= self.end {
|
||||
return None;
|
||||
}
|
||||
|
||||
// If the descriptor is non-wildcard, only index 0 will return an spk.
|
||||
if !self.descriptor.borrow().has_wildcard() && self.next_index != 0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
let script = self
|
||||
.descriptor
|
||||
.borrow()
|
||||
.derived_descriptor(&self.secp, self.next_index)
|
||||
.expect("the descriptor cannot need hardened derivation")
|
||||
.script_pubkey();
|
||||
let output = (self.next_index, script);
|
||||
|
||||
self.next_index += 1;
|
||||
|
||||
Some(output)
|
||||
}
|
||||
|
||||
fn nth(&mut self, n: usize) -> Option<Self::Item> {
|
||||
self.next_index = self
|
||||
.next_index
|
||||
.saturating_add(u32::try_from(n).unwrap_or(u32::MAX));
|
||||
self.next()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::{
|
||||
bitcoin::secp256k1::Secp256k1,
|
||||
keychain::KeychainTxOutIndex,
|
||||
miniscript::{Descriptor, DescriptorPublicKey},
|
||||
spk_iter::{SpkIterator, BIP32_MAX_INDEX},
|
||||
};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
enum TestKeychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
fn init_txout_index() -> (
|
||||
KeychainTxOutIndex<TestKeychain>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
) {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
|
||||
let secp = Secp256k1::signing_only();
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::iter_nth_zero)]
|
||||
#[rustfmt::skip]
|
||||
fn test_spkiterator_wildcard() {
|
||||
let (_, external_desc, _) = init_txout_index();
|
||||
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_20 = external_desc.at_derivation_index(20).unwrap().script_pubkey();
|
||||
let external_spk_21 = external_desc.at_derivation_index(21).unwrap().script_pubkey();
|
||||
let external_spk_max = external_desc.at_derivation_index(BIP32_MAX_INDEX).unwrap().script_pubkey();
|
||||
|
||||
let mut external_spk = SpkIterator::new(&external_desc);
|
||||
let max_index = BIP32_MAX_INDEX - 22;
|
||||
|
||||
assert_eq!(external_spk.next(), Some((0, external_spk_0)));
|
||||
assert_eq!(external_spk.nth(15), Some((16, external_spk_16)));
|
||||
assert_eq!(external_spk.nth(3), Some((20, external_spk_20.clone())));
|
||||
assert_eq!(external_spk.next(), Some((21, external_spk_21)));
|
||||
assert_eq!(
|
||||
external_spk.nth(max_index as usize),
|
||||
Some((BIP32_MAX_INDEX, external_spk_max))
|
||||
);
|
||||
assert_eq!(external_spk.nth(0), None);
|
||||
|
||||
let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
|
||||
assert_eq!(external_spk.nth(20), Some((20, external_spk_20)));
|
||||
assert_eq!(external_spk.next(), None);
|
||||
|
||||
let mut external_spk = SpkIterator::new_with_range(&external_desc, 0..21);
|
||||
assert_eq!(external_spk.nth(21), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[allow(clippy::iter_nth_zero)]
|
||||
fn test_spkiterator_non_wildcard() {
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||
let external_spk_0 = no_wildcard_descriptor
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
|
||||
let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
|
||||
|
||||
assert_eq!(external_spk.next(), Some((0, external_spk_0.clone())));
|
||||
assert_eq!(external_spk.next(), None);
|
||||
|
||||
let mut external_spk = SpkIterator::new(&no_wildcard_descriptor);
|
||||
|
||||
assert_eq!(external_spk.nth(0), Some((0, external_spk_0.clone())));
|
||||
assert_eq!(external_spk.nth(0), None);
|
||||
|
||||
let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..0);
|
||||
|
||||
assert_eq!(external_spk.next(), None);
|
||||
|
||||
let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..1);
|
||||
|
||||
assert_eq!(external_spk.nth(0), Some((0, external_spk_0.clone())));
|
||||
assert_eq!(external_spk.next(), None);
|
||||
|
||||
// We test that using new_with_range with range_len > 1 gives back an iterator with
|
||||
// range_len = 1
|
||||
let mut external_spk = SpkIterator::new_with_range(&no_wildcard_descriptor, 0..10);
|
||||
|
||||
assert_eq!(external_spk.nth(0), Some((0, external_spk_0)));
|
||||
assert_eq!(external_spk.nth(0), None);
|
||||
|
||||
// non index-0 should NOT return an spk
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..1).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=1).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..2).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 1..=2).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..11).next(),
|
||||
None
|
||||
);
|
||||
assert_eq!(
|
||||
SpkIterator::new_with_range(&no_wildcard_descriptor, 10..=10).next(),
|
||||
None
|
||||
);
|
||||
}
|
||||
|
||||
// The following dummy traits were created to test if SpkIterator is working properly.
|
||||
trait TestSendStatic: Send + 'static {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
|
||||
impl TestSendStatic for SpkIterator<Descriptor<DescriptorPublicKey>> {
|
||||
fn test(&self) -> u32 {
|
||||
20
|
||||
}
|
||||
}
|
||||
}
|
||||
324
crates/chain/src/spk_txout_index.rs
Normal file
324
crates/chain/src/spk_txout_index.rs
Normal file
@@ -0,0 +1,324 @@
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
use crate::{
|
||||
collections::{hash_map::Entry, BTreeMap, BTreeSet, HashMap},
|
||||
indexed_tx_graph::Indexer,
|
||||
};
|
||||
use bitcoin::{self, OutPoint, Script, ScriptBuf, Transaction, TxOut, Txid};
|
||||
|
||||
/// An index storing [`TxOut`]s that have a script pubkey that matches those in a list.
|
||||
///
|
||||
/// The basic idea is that you insert script pubkeys you care about into the index with
|
||||
/// [`insert_spk`] and then when you call [`Indexer::index_tx`] or [`Indexer::index_txout`], the
|
||||
/// index will look at any txouts you pass in and store and index any txouts matching one of its
|
||||
/// script pubkeys.
|
||||
///
|
||||
/// Each script pubkey is associated with an application-defined index script index `I`, which must be
|
||||
/// [`Ord`]. Usually, this is used to associate the derivation index of the script pubkey or even a
|
||||
/// combination of `(keychain, derivation_index)`.
|
||||
///
|
||||
/// Note there is no harm in scanning transactions that disappear from the blockchain or were never
|
||||
/// in there in the first place. `SpkTxOutIndex` is intentionally *monotone* -- you cannot delete or
|
||||
/// modify txouts that have been indexed. To find out which txouts from the index are actually in the
|
||||
/// chain or unspent, you must use other sources of information like a [`TxGraph`].
|
||||
///
|
||||
/// [`TxOut`]: bitcoin::TxOut
|
||||
/// [`insert_spk`]: Self::insert_spk
|
||||
/// [`Ord`]: core::cmp::Ord
|
||||
/// [`TxGraph`]: crate::tx_graph::TxGraph
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SpkTxOutIndex<I> {
|
||||
/// script pubkeys ordered by index
|
||||
spks: BTreeMap<I, ScriptBuf>,
|
||||
/// A reverse lookup from spk to spk index
|
||||
spk_indices: HashMap<ScriptBuf, I>,
|
||||
/// The set of unused indexes.
|
||||
unused: BTreeSet<I>,
|
||||
/// Lookup index and txout by outpoint.
|
||||
txouts: BTreeMap<OutPoint, (I, TxOut)>,
|
||||
/// Lookup from spk index to outpoints that had that spk
|
||||
spk_txouts: BTreeSet<(I, OutPoint)>,
|
||||
}
|
||||
|
||||
impl<I> Default for SpkTxOutIndex<I> {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
txouts: Default::default(),
|
||||
spks: Default::default(),
|
||||
spk_indices: Default::default(),
|
||||
spk_txouts: Default::default(),
|
||||
unused: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord> Indexer for SpkTxOutIndex<I> {
|
||||
type ChangeSet = ();
|
||||
|
||||
fn index_txout(&mut self, outpoint: OutPoint, txout: &TxOut) -> Self::ChangeSet {
|
||||
self.scan_txout(outpoint, txout);
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn index_tx(&mut self, tx: &Transaction) -> Self::ChangeSet {
|
||||
self.scan(tx);
|
||||
Default::default()
|
||||
}
|
||||
|
||||
fn initial_changeset(&self) -> Self::ChangeSet {}
|
||||
|
||||
fn apply_changeset(&mut self, _changeset: Self::ChangeSet) {
|
||||
// This applies nothing.
|
||||
}
|
||||
|
||||
fn is_tx_relevant(&self, tx: &Transaction) -> bool {
|
||||
self.is_relevant(tx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<I: Clone + Ord> SpkTxOutIndex<I> {
|
||||
/// Scans a transaction's outputs for matching script pubkeys.
|
||||
///
|
||||
/// Typically, this is used in two situations:
|
||||
///
|
||||
/// 1. After loading transaction data from the disk, you may scan over all the txouts to restore all
|
||||
/// your txouts.
|
||||
/// 2. When getting new data from the chain, you usually scan it before incorporating it into your chain state.
|
||||
pub fn scan(&mut self, tx: &Transaction) -> BTreeSet<I> {
|
||||
let mut scanned_indices = BTreeSet::new();
|
||||
let txid = tx.txid();
|
||||
for (i, txout) in tx.output.iter().enumerate() {
|
||||
let op = OutPoint::new(txid, i as u32);
|
||||
if let Some(spk_i) = self.scan_txout(op, txout) {
|
||||
scanned_indices.insert(spk_i.clone());
|
||||
}
|
||||
}
|
||||
|
||||
scanned_indices
|
||||
}
|
||||
|
||||
/// Scan a single `TxOut` for a matching script pubkey and returns the index that matches the
|
||||
/// script pubkey (if any).
|
||||
pub fn scan_txout(&mut self, op: OutPoint, txout: &TxOut) -> Option<&I> {
|
||||
let spk_i = self.spk_indices.get(&txout.script_pubkey);
|
||||
if let Some(spk_i) = spk_i {
|
||||
self.txouts.insert(op, (spk_i.clone(), txout.clone()));
|
||||
self.spk_txouts.insert((spk_i.clone(), op));
|
||||
self.unused.remove(spk_i);
|
||||
}
|
||||
spk_i
|
||||
}
|
||||
|
||||
/// Get a reference to the set of indexed outpoints.
|
||||
pub fn outpoints(&self) -> &BTreeSet<(I, OutPoint)> {
|
||||
&self.spk_txouts
|
||||
}
|
||||
|
||||
/// Iterate over all known txouts that spend to tracked script pubkeys.
|
||||
pub fn txouts(
|
||||
&self,
|
||||
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> + ExactSizeIterator {
|
||||
self.txouts
|
||||
.iter()
|
||||
.map(|(op, (index, txout))| (index, *op, txout))
|
||||
}
|
||||
|
||||
/// Finds all txouts on a transaction that has previously been scanned and indexed.
|
||||
pub fn txouts_in_tx(
|
||||
&self,
|
||||
txid: Txid,
|
||||
) -> impl DoubleEndedIterator<Item = (&I, OutPoint, &TxOut)> {
|
||||
self.txouts
|
||||
.range(OutPoint::new(txid, u32::MIN)..=OutPoint::new(txid, u32::MAX))
|
||||
.map(|(op, (index, txout))| (index, *op, txout))
|
||||
}
|
||||
|
||||
/// Iterates over all the outputs with script pubkeys in an index range.
|
||||
pub fn outputs_in_range(
|
||||
&self,
|
||||
range: impl RangeBounds<I>,
|
||||
) -> impl DoubleEndedIterator<Item = (&I, OutPoint)> {
|
||||
use bitcoin::hashes::Hash;
|
||||
use core::ops::Bound::*;
|
||||
let min_op = OutPoint {
|
||||
txid: Txid::all_zeros(),
|
||||
vout: u32::MIN,
|
||||
};
|
||||
let max_op = OutPoint {
|
||||
txid: Txid::from_byte_array([0xff; Txid::LEN]),
|
||||
vout: u32::MAX,
|
||||
};
|
||||
|
||||
let start = match range.start_bound() {
|
||||
Included(index) => Included((index.clone(), min_op)),
|
||||
Excluded(index) => Excluded((index.clone(), max_op)),
|
||||
Unbounded => Unbounded,
|
||||
};
|
||||
|
||||
let end = match range.end_bound() {
|
||||
Included(index) => Included((index.clone(), max_op)),
|
||||
Excluded(index) => Excluded((index.clone(), min_op)),
|
||||
Unbounded => Unbounded,
|
||||
};
|
||||
|
||||
self.spk_txouts.range((start, end)).map(|(i, op)| (i, *op))
|
||||
}
|
||||
|
||||
/// Returns the txout and script pubkey index of the `TxOut` at `OutPoint`.
|
||||
///
|
||||
/// Returns `None` if the `TxOut` hasn't been scanned or if nothing matching was found there.
|
||||
pub fn txout(&self, outpoint: OutPoint) -> Option<(&I, &TxOut)> {
|
||||
self.txouts.get(&outpoint).map(|v| (&v.0, &v.1))
|
||||
}
|
||||
|
||||
/// Returns the script that has been inserted at the `index`.
|
||||
///
|
||||
/// If that index hasn't been inserted yet, it will return `None`.
|
||||
pub fn spk_at_index(&self, index: &I) -> Option<&Script> {
|
||||
self.spks.get(index).map(|s| s.as_script())
|
||||
}
|
||||
|
||||
/// The script pubkeys that are being tracked by the index.
|
||||
pub fn all_spks(&self) -> &BTreeMap<I, ScriptBuf> {
|
||||
&self.spks
|
||||
}
|
||||
|
||||
/// Adds a script pubkey to scan for. Returns `false` and does nothing if spk already exists in the map
|
||||
///
|
||||
/// the index will look for outputs spending to this spk whenever it scans new data.
|
||||
pub fn insert_spk(&mut self, index: I, spk: ScriptBuf) -> bool {
|
||||
match self.spk_indices.entry(spk.clone()) {
|
||||
Entry::Vacant(value) => {
|
||||
value.insert(index.clone());
|
||||
self.spks.insert(index.clone(), spk);
|
||||
self.unused.insert(index);
|
||||
true
|
||||
}
|
||||
Entry::Occupied(_) => false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over all unused script pubkeys in an index range.
|
||||
///
|
||||
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||
/// never scanned a transaction output with it.
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// # use bdk_chain::SpkTxOutIndex;
|
||||
///
|
||||
/// // imagine our spks are indexed like (keychain, derivation_index).
|
||||
/// let txout_index = SpkTxOutIndex::<(u32, u32)>::default();
|
||||
/// let all_unused_spks = txout_index.unused_spks(..);
|
||||
/// let change_index = 1;
|
||||
/// let unused_change_spks =
|
||||
/// txout_index.unused_spks((change_index, u32::MIN)..(change_index, u32::MAX));
|
||||
/// ```
|
||||
pub fn unused_spks<R>(&self, range: R) -> impl DoubleEndedIterator<Item = (&I, &Script)> + Clone
|
||||
where
|
||||
R: RangeBounds<I>,
|
||||
{
|
||||
self.unused
|
||||
.range(range)
|
||||
.map(move |index| (index, self.spk_at_index(index).expect("must exist")))
|
||||
}
|
||||
|
||||
/// Returns whether the script pubkey at `index` has been used or not.
|
||||
///
|
||||
/// Here, "unused" means that after the script pubkey was stored in the index, the index has
|
||||
/// never scanned a transaction output with it.
|
||||
pub fn is_used(&self, index: &I) -> bool {
|
||||
self.unused.get(index).is_none()
|
||||
}
|
||||
|
||||
/// Marks the script pubkey at `index` as used even though it hasn't seen an output spending to it.
|
||||
/// This only affects when the `index` had already been added to `self` and was unused.
|
||||
///
|
||||
/// Returns whether the `index` was initially present as `unused`.
|
||||
///
|
||||
/// This is useful when you want to reserve a script pubkey for something but don't want to add
|
||||
/// the transaction output using it to the index yet. Other callers will consider the `index` used
|
||||
/// until you call [`unmark_used`].
|
||||
///
|
||||
/// [`unmark_used`]: Self::unmark_used
|
||||
pub fn mark_used(&mut self, index: &I) -> bool {
|
||||
self.unused.remove(index)
|
||||
}
|
||||
|
||||
/// Undoes the effect of [`mark_used`]. Returns whether the `index` is inserted back into
|
||||
/// `unused`.
|
||||
///
|
||||
/// Note that if `self` has scanned an output with this script pubkey then this will have no
|
||||
/// effect.
|
||||
///
|
||||
/// [`mark_used`]: Self::mark_used
|
||||
pub fn unmark_used(&mut self, index: &I) -> bool {
|
||||
// we cannot set the index as unused when it does not exist
|
||||
if !self.spks.contains_key(index) {
|
||||
return false;
|
||||
}
|
||||
// we cannot set the index as unused when txouts are indexed under it
|
||||
if self.outputs_in_range(index..=index).next().is_some() {
|
||||
return false;
|
||||
}
|
||||
self.unused.insert(index.clone())
|
||||
}
|
||||
|
||||
/// Returns the index associated with the script pubkey.
|
||||
pub fn index_of_spk(&self, script: &Script) -> Option<&I> {
|
||||
self.spk_indices.get(script)
|
||||
}
|
||||
|
||||
/// Computes total input value going from script pubkeys in the index (sent) and the total output
|
||||
/// value going to script pubkeys in the index (received) in `tx`. For the `sent` to be computed
|
||||
/// correctly, the output being spent must have already been scanned by the index. Calculating
|
||||
/// received just uses the [`Transaction`] outputs directly, so it will be correct even if it has
|
||||
/// not been scanned.
|
||||
pub fn sent_and_received(&self, tx: &Transaction) -> (u64, u64) {
|
||||
let mut sent = 0;
|
||||
let mut received = 0;
|
||||
|
||||
for txin in &tx.input {
|
||||
if let Some((_, txout)) = self.txout(txin.previous_output) {
|
||||
sent += txout.value;
|
||||
}
|
||||
}
|
||||
for txout in &tx.output {
|
||||
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
||||
received += txout.value;
|
||||
}
|
||||
}
|
||||
|
||||
(sent, received)
|
||||
}
|
||||
|
||||
/// Computes the net value that this transaction gives to the script pubkeys in the index and
|
||||
/// *takes* from the transaction outputs in the index. Shorthand for calling
|
||||
/// [`sent_and_received`] and subtracting sent from received.
|
||||
///
|
||||
/// [`sent_and_received`]: Self::sent_and_received
|
||||
pub fn net_value(&self, tx: &Transaction) -> i64 {
|
||||
let (sent, received) = self.sent_and_received(tx);
|
||||
received as i64 - sent as i64
|
||||
}
|
||||
|
||||
/// Whether any of the inputs of this transaction spend a txout tracked or whether any output
|
||||
/// matches one of our script pubkeys.
|
||||
///
|
||||
/// It is easily possible to misuse this method and get false negatives by calling it before you
|
||||
/// have scanned the `TxOut`s the transaction is spending. For example, if you want to filter out
|
||||
/// all the transactions in a block that are irrelevant, you **must first scan all the
|
||||
/// transactions in the block** and only then use this method.
|
||||
pub fn is_relevant(&self, tx: &Transaction) -> bool {
|
||||
let input_matches = tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|input| self.txouts.contains_key(&input.previous_output));
|
||||
let output_matches = tx
|
||||
.output
|
||||
.iter()
|
||||
.any(|output| self.spk_indices.contains_key(&output.script_pubkey));
|
||||
input_matches || output_matches
|
||||
}
|
||||
}
|
||||
185
crates/chain/src/tx_data_traits.rs
Normal file
185
crates/chain/src/tx_data_traits.rs
Normal file
@@ -0,0 +1,185 @@
|
||||
use crate::collections::BTreeMap;
|
||||
use crate::collections::BTreeSet;
|
||||
use crate::BlockId;
|
||||
use alloc::vec::Vec;
|
||||
|
||||
/// Trait that "anchors" blockchain data to a specific block of height and hash.
|
||||
///
|
||||
/// If transaction A is anchored in block B, and block B is in the best chain, we can
|
||||
/// assume that transaction A is also confirmed in the best chain. This does not necessarily mean
|
||||
/// that transaction A is confirmed in block B. It could also mean transaction A is confirmed in a
|
||||
/// parent block of B.
|
||||
///
|
||||
/// Every [`Anchor`] implementation must contain a [`BlockId`] parameter, and must implement
|
||||
/// [`Ord`]. When implementing [`Ord`], the anchors' [`BlockId`]s should take precedence
|
||||
/// over other elements inside the [`Anchor`]s for comparison purposes, i.e., you should first
|
||||
/// compare the anchors' [`BlockId`]s and then care about the rest.
|
||||
///
|
||||
/// The example shows different types of anchors:
|
||||
/// ```
|
||||
/// # use bdk_chain::local_chain::LocalChain;
|
||||
/// # use bdk_chain::tx_graph::TxGraph;
|
||||
/// # use bdk_chain::BlockId;
|
||||
/// # use bdk_chain::ConfirmationHeightAnchor;
|
||||
/// # use bdk_chain::ConfirmationTimeHeightAnchor;
|
||||
/// # use bdk_chain::example_utils::*;
|
||||
/// # use bitcoin::hashes::Hash;
|
||||
/// // Initialize the local chain with two blocks.
|
||||
/// let chain = LocalChain::from_blocks(
|
||||
/// [
|
||||
/// (1, Hash::hash("first".as_bytes())),
|
||||
/// (2, Hash::hash("second".as_bytes())),
|
||||
/// ]
|
||||
/// .into_iter()
|
||||
/// .collect(),
|
||||
/// );
|
||||
///
|
||||
/// // Transaction to be inserted into `TxGraph`s with different anchor types.
|
||||
/// let tx = tx_from_hex(RAW_TX_1);
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `BlockId` as the anchor type.
|
||||
/// // When a transaction is anchored with `BlockId`, the anchor block and the confirmation block of
|
||||
/// // the transaction is the same block.
|
||||
/// let mut graph_a = TxGraph::<BlockId>::default();
|
||||
/// let _ = graph_a.insert_tx(tx.clone());
|
||||
/// graph_a.insert_anchor(
|
||||
/// tx.txid(),
|
||||
/// BlockId {
|
||||
/// height: 1,
|
||||
/// hash: Hash::hash("first".as_bytes()),
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block and the confirmation height of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_b = TxGraph::<ConfirmationHeightAnchor>::default();
|
||||
/// let _ = graph_b.insert_tx(tx.clone());
|
||||
/// graph_b.insert_anchor(
|
||||
/// tx.txid(),
|
||||
/// ConfirmationHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// height: 2,
|
||||
/// hash: Hash::hash("second".as_bytes()),
|
||||
/// },
|
||||
/// confirmation_height: 1,
|
||||
/// },
|
||||
/// );
|
||||
///
|
||||
/// // Insert `tx` into a `TxGraph` that uses `ConfirmationTimeHeightAnchor` as the anchor type.
|
||||
/// // This anchor records the anchor block, the confirmation height and time of the transaction.
|
||||
/// // When a transaction is anchored with `ConfirmationTimeHeightAnchor`, the anchor block and
|
||||
/// // confirmation block can be different. However, the confirmation block cannot be higher than
|
||||
/// // the anchor block and both blocks must be in the same chain for the anchor to be valid.
|
||||
/// let mut graph_c = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
/// let _ = graph_c.insert_tx(tx.clone());
|
||||
/// graph_c.insert_anchor(
|
||||
/// tx.txid(),
|
||||
/// ConfirmationTimeHeightAnchor {
|
||||
/// anchor_block: BlockId {
|
||||
/// height: 2,
|
||||
/// hash: Hash::hash("third".as_bytes()),
|
||||
/// },
|
||||
/// confirmation_height: 1,
|
||||
/// confirmation_time: 123,
|
||||
/// },
|
||||
/// );
|
||||
/// ```
|
||||
pub trait Anchor: core::fmt::Debug + Clone + Eq + PartialOrd + Ord + core::hash::Hash {
|
||||
/// Returns the [`BlockId`] that the associated blockchain data is "anchored" in.
|
||||
fn anchor_block(&self) -> BlockId;
|
||||
|
||||
/// Get the upper bound of the chain data's confirmation height.
|
||||
///
|
||||
/// The default definition gives a pessimistic answer. This can be overridden by the `Anchor`
|
||||
/// implementation for a more accurate value.
|
||||
fn confirmation_height_upper_bound(&self) -> u32 {
|
||||
self.anchor_block().height
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a, A: Anchor> Anchor for &'a A {
|
||||
fn anchor_block(&self) -> BlockId {
|
||||
<A as Anchor>::anchor_block(self)
|
||||
}
|
||||
}
|
||||
|
||||
/// An [`Anchor`] that can be constructed from a given block, block height and transaction position
|
||||
/// within the block.
|
||||
pub trait AnchorFromBlockPosition: Anchor {
|
||||
/// Construct the anchor from a given `block`, block height and `tx_pos` within the block.
|
||||
fn from_block_position(block: &bitcoin::Block, block_id: BlockId, tx_pos: usize) -> Self;
|
||||
}
|
||||
|
||||
/// Trait that makes an object appendable.
|
||||
pub trait Append {
|
||||
/// Append another object of the same type onto `self`.
|
||||
fn append(&mut self, other: Self);
|
||||
|
||||
/// Returns whether the structure is considered empty.
|
||||
fn is_empty(&self) -> bool;
|
||||
}
|
||||
|
||||
impl<K: Ord, V> Append for BTreeMap<K, V> {
|
||||
fn append(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeMap::extend(self, other)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
BTreeMap::is_empty(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Ord> Append for BTreeSet<T> {
|
||||
fn append(&mut self, other: Self) {
|
||||
// We use `extend` instead of `BTreeMap::append` due to performance issues with `append`.
|
||||
// Refer to https://github.com/rust-lang/rust/issues/34666#issuecomment-675658420
|
||||
BTreeSet::extend(self, other)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
BTreeSet::is_empty(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> Append for Vec<T> {
|
||||
fn append(&mut self, mut other: Self) {
|
||||
Vec::append(self, &mut other)
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
Vec::is_empty(self)
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! impl_append_for_tuple {
|
||||
($($a:ident $b:tt)*) => {
|
||||
impl<$($a),*> Append for ($($a,)*) where $($a: Append),* {
|
||||
|
||||
fn append(&mut self, _other: Self) {
|
||||
$(Append::append(&mut self.$b, _other.$b) );*
|
||||
}
|
||||
|
||||
fn is_empty(&self) -> bool {
|
||||
$(Append::is_empty(&self.$b) && )* true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl_append_for_tuple!();
|
||||
impl_append_for_tuple!(T0 0);
|
||||
impl_append_for_tuple!(T0 0 T1 1);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9);
|
||||
impl_append_for_tuple!(T0 0 T1 1 T2 2 T3 3 T4 4 T5 5 T6 6 T7 7 T8 8 T9 9 T10 10);
|
||||
1574
crates/chain/src/tx_graph.rs
Normal file
1574
crates/chain/src/tx_graph.rs
Normal file
File diff suppressed because it is too large
Load Diff
78
crates/chain/tests/common/mod.rs
Normal file
78
crates/chain/tests/common/mod.rs
Normal file
@@ -0,0 +1,78 @@
|
||||
mod tx_template;
|
||||
#[allow(unused_imports)]
|
||||
pub use tx_template::*;
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! block_id {
|
||||
($height:expr, $hash:literal) => {{
|
||||
bdk_chain::BlockId {
|
||||
height: $height,
|
||||
hash: bitcoin::hashes::Hash::hash($hash.as_bytes()),
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! chain_update {
|
||||
[ $(($height:expr, $hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::Update {
|
||||
tip: bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
.tip(),
|
||||
introduce_older_blocks: true,
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused_macros)]
|
||||
macro_rules! changeset {
|
||||
(checkpoints: $($tail:tt)*) => { changeset!(index: TxHeight, checkpoints: $($tail)*) };
|
||||
(
|
||||
index: $ind:ty,
|
||||
checkpoints: [ $(( $height:expr, $cp_to:expr )),* ]
|
||||
$(,txids: [ $(( $txid:expr, $tx_to:expr )),* ])?
|
||||
) => {{
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::sparse_chain::ChangeSet::<$ind> {
|
||||
checkpoints: {
|
||||
let mut changes = BTreeMap::default();
|
||||
$(changes.insert($height, $cp_to);)*
|
||||
changes
|
||||
},
|
||||
txids: {
|
||||
let mut changes = BTreeMap::default();
|
||||
$($(changes.insert($txid, $tx_to.map(|h: TxHeight| h.into()));)*)?
|
||||
changes
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
pub fn new_tx(lt: u32) -> bitcoin::Transaction {
|
||||
bitcoin::Transaction {
|
||||
version: 0x00,
|
||||
lock_time: bitcoin::absolute::LockTime::from_consensus(lt),
|
||||
input: vec![],
|
||||
output: vec![],
|
||||
}
|
||||
}
|
||||
136
crates/chain/tests/common/tx_template.rs
Normal file
136
crates/chain/tests/common/tx_template.rs
Normal file
@@ -0,0 +1,136 @@
|
||||
use rand::distributions::{Alphanumeric, DistString};
|
||||
use std::collections::HashMap;
|
||||
|
||||
use bdk_chain::{tx_graph::TxGraph, Anchor, SpkTxOutIndex};
|
||||
use bitcoin::{
|
||||
locktime::absolute::LockTime, secp256k1::Secp256k1, OutPoint, ScriptBuf, Sequence, Transaction,
|
||||
TxIn, TxOut, Txid, Witness,
|
||||
};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Template for creating a transaction in `TxGraph`.
|
||||
///
|
||||
/// The incentive for transaction templates is to create a transaction history in a simple manner to
|
||||
/// avoid having to explicitly hash previous transactions to form previous outpoints of later
|
||||
/// transactions.
|
||||
#[derive(Clone, Copy, Default)]
|
||||
pub struct TxTemplate<'a, A> {
|
||||
/// Uniquely identifies the transaction, before it can have a txid.
|
||||
pub tx_name: &'a str,
|
||||
pub inputs: &'a [TxInTemplate<'a>],
|
||||
pub outputs: &'a [TxOutTemplate],
|
||||
pub anchors: &'a [A],
|
||||
pub last_seen: Option<u64>,
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub enum TxInTemplate<'a> {
|
||||
/// This will give a random txid and vout.
|
||||
Bogus,
|
||||
|
||||
/// This is used for coinbase transactions because they do not have previous outputs.
|
||||
Coinbase,
|
||||
|
||||
/// Contains the `tx_name` and `vout` that we are spending. The rule is that we must only spend
|
||||
/// from tx of a previous `TxTemplate`.
|
||||
PrevTx(&'a str, usize),
|
||||
}
|
||||
|
||||
pub struct TxOutTemplate {
|
||||
pub value: u64,
|
||||
pub spk_index: Option<u32>, // some = get spk from SpkTxOutIndex, none = random spk
|
||||
}
|
||||
|
||||
#[allow(unused)]
|
||||
impl TxOutTemplate {
|
||||
pub fn new(value: u64, spk_index: Option<u32>) -> Self {
|
||||
TxOutTemplate { value, spk_index }
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub fn init_graph<'a, A: Anchor + Clone + 'a>(
|
||||
tx_templates: impl IntoIterator<Item = &'a TxTemplate<'a, A>>,
|
||||
) -> (TxGraph<A>, SpkTxOutIndex<u32>, HashMap<&'a str, Txid>) {
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let mut graph = TxGraph::<A>::default();
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
(0..10).for_each(|index| {
|
||||
spk_index.insert_spk(
|
||||
index,
|
||||
descriptor
|
||||
.at_derivation_index(index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
);
|
||||
});
|
||||
let mut tx_ids = HashMap::<&'a str, Txid>::new();
|
||||
|
||||
for (bogus_txin_vout, tx_tmp) in tx_templates.into_iter().enumerate() {
|
||||
let tx = Transaction {
|
||||
version: 0,
|
||||
lock_time: LockTime::ZERO,
|
||||
input: tx_tmp
|
||||
.inputs
|
||||
.iter()
|
||||
.map(|input| match input {
|
||||
TxInTemplate::Bogus => TxIn {
|
||||
previous_output: OutPoint::new(
|
||||
bitcoin::hashes::Hash::hash(
|
||||
Alphanumeric
|
||||
.sample_string(&mut rand::thread_rng(), 20)
|
||||
.as_bytes(),
|
||||
),
|
||||
bogus_txin_vout as u32,
|
||||
),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::default(),
|
||||
witness: Witness::new(),
|
||||
},
|
||||
TxInTemplate::Coinbase => TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::MAX,
|
||||
witness: Witness::new(),
|
||||
},
|
||||
TxInTemplate::PrevTx(prev_name, prev_vout) => {
|
||||
let prev_txid = tx_ids.get(prev_name).expect(
|
||||
"txin template must spend from tx of template that comes before",
|
||||
);
|
||||
TxIn {
|
||||
previous_output: OutPoint::new(*prev_txid, *prev_vout as _),
|
||||
script_sig: ScriptBuf::new(),
|
||||
sequence: Sequence::default(),
|
||||
witness: Witness::new(),
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect(),
|
||||
output: tx_tmp
|
||||
.outputs
|
||||
.iter()
|
||||
.map(|output| match &output.spk_index {
|
||||
None => TxOut {
|
||||
value: output.value,
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
Some(index) => TxOut {
|
||||
value: output.value,
|
||||
script_pubkey: spk_index.spk_at_index(index).unwrap().to_owned(),
|
||||
},
|
||||
})
|
||||
.collect(),
|
||||
};
|
||||
|
||||
tx_ids.insert(tx_tmp.tx_name, tx.txid());
|
||||
spk_index.scan(&tx);
|
||||
let _ = graph.insert_tx(tx.clone());
|
||||
for anchor in tx_tmp.anchors.iter() {
|
||||
let _ = graph.insert_anchor(tx.txid(), anchor.clone());
|
||||
}
|
||||
if let Some(seen_at) = tx_tmp.last_seen {
|
||||
let _ = graph.insert_seen_at(tx.txid(), seen_at);
|
||||
}
|
||||
}
|
||||
(graph, spk_index, tx_ids)
|
||||
}
|
||||
467
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
467
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
@@ -0,0 +1,467 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::BTreeSet;
|
||||
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, Balance, KeychainTxOutIndex},
|
||||
local_chain::LocalChain,
|
||||
tx_graph, BlockId, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, Script, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
use miniscript::Descriptor;
|
||||
|
||||
/// Ensure [`IndexedTxGraph::insert_relevant_txs`] can successfully index transactions NOT presented
|
||||
/// in topological order.
|
||||
///
|
||||
/// Given 3 transactions (A, B, C), where A has 2 owned outputs. B and C spends an output each of A.
|
||||
/// Typically, we would only know whether B and C are relevant if we have indexed A (A's outpoints
|
||||
/// are associated with owned spks in the index). Ensure insertion and indexing is topological-
|
||||
/// agnostic.
|
||||
#[test]
|
||||
fn insert_relevant_txs() {
|
||||
const DESCRIPTOR: &str = "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)";
|
||||
let (descriptor, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), DESCRIPTOR)
|
||||
.expect("must be valid");
|
||||
let spk_0 = descriptor.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let spk_1 = descriptor.at_derivation_index(9).unwrap().script_pubkey();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<()>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
graph.index.add_keychain((), descriptor);
|
||||
|
||||
let tx_a = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 10_000,
|
||||
script_pubkey: spk_0,
|
||||
},
|
||||
TxOut {
|
||||
value: 20_000,
|
||||
script_pubkey: spk_1,
|
||||
},
|
||||
],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
let tx_b = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(1)
|
||||
};
|
||||
|
||||
let tx_c = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx_a.txid(), 1),
|
||||
..Default::default()
|
||||
}],
|
||||
..common::new_tx(2)
|
||||
};
|
||||
|
||||
let txs = [tx_c, tx_b, tx_a];
|
||||
|
||||
let changeset = indexed_tx_graph::ChangeSet {
|
||||
graph: tx_graph::ChangeSet {
|
||||
txs: txs.clone().into(),
|
||||
..Default::default()
|
||||
},
|
||||
indexer: keychain::ChangeSet([((), 9_u32)].into()),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
graph.batch_insert_relevant(txs.iter().map(|tx| (tx, None))),
|
||||
changeset,
|
||||
);
|
||||
|
||||
assert_eq!(graph.initial_changeset(), changeset,);
|
||||
}
|
||||
|
||||
#[test]
|
||||
/// Ensure consistency IndexedTxGraph list_* and balance methods. These methods lists
|
||||
/// relevant txouts and utxos from the information fetched from a ChainOracle (here a LocalChain).
|
||||
///
|
||||
/// Test Setup:
|
||||
///
|
||||
/// Local Chain => <0> ----- <1> ----- <2> ----- <3> ---- ... ---- <150>
|
||||
///
|
||||
/// Keychains:
|
||||
///
|
||||
/// keychain_1: Trusted
|
||||
/// keychain_2: Untrusted
|
||||
///
|
||||
/// Transactions:
|
||||
///
|
||||
/// tx1: A Coinbase, sending 70000 sats to "trusted" address. [Block 0]
|
||||
/// tx2: A external Receive, sending 30000 sats to "untrusted" address. [Block 1]
|
||||
/// tx3: Internal Spend. Spends tx2 and returns change of 10000 to "trusted" address. [Block 2]
|
||||
/// tx4: Mempool tx, sending 20000 sats to "trusted" address.
|
||||
/// tx5: Mempool tx, sending 15000 sats to "untested" address.
|
||||
/// tx6: Complete unrelated tx. [Block 3]
|
||||
///
|
||||
/// Different transactions are added via `insert_relevant_txs`.
|
||||
/// `list_owned_txout`, `list_owned_utxos` and `balance` method is asserted
|
||||
/// with expected values at Block height 0, 1, and 2.
|
||||
///
|
||||
/// Finally Add more blocks to local chain until tx1 coinbase maturity hits.
|
||||
/// Assert maturity at coinbase maturity inflection height. Block height 98 and 99.
|
||||
|
||||
fn test_list_owned_txouts() {
|
||||
// Create Local chains
|
||||
let local_chain = LocalChain::from_blocks((0..150).map(|i| (i as u32, h!("random"))).collect())
|
||||
.expect("must have genesis hash");
|
||||
|
||||
// Initiate IndexedTxGraph
|
||||
|
||||
let (desc_1, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/0/*)").unwrap();
|
||||
let (desc_2, _) = Descriptor::parse_descriptor(&Secp256k1::signing_only(), "tr(tprv8ZgxMBicQKsPd3krDUsBAmtnRsK3rb8u5yi1zhQgMhF1tR8MW7xfE4rnrbbsrbPR52e7rKapu6ztw1jXveJSCGHEriUGZV7mCe88duLp5pj/86'/1'/0'/1/*)").unwrap();
|
||||
|
||||
let mut graph = IndexedTxGraph::<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>::new(
|
||||
KeychainTxOutIndex::new(10),
|
||||
);
|
||||
|
||||
graph.index.add_keychain("keychain_1".into(), desc_1);
|
||||
graph.index.add_keychain("keychain_2".into(), desc_2);
|
||||
|
||||
// Get trusted and untrusted addresses
|
||||
|
||||
let mut trusted_spks: Vec<ScriptBuf> = Vec::new();
|
||||
let mut untrusted_spks: Vec<ScriptBuf> = Vec::new();
|
||||
|
||||
{
|
||||
// we need to scope here to take immutanble reference of the graph
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_1".to_string());
|
||||
// TODO Assert indexes
|
||||
trusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
{
|
||||
for _ in 0..10 {
|
||||
let ((_, script), _) = graph.index.reveal_next_spk(&"keychain_2".to_string());
|
||||
untrusted_spks.push(script.to_owned());
|
||||
}
|
||||
}
|
||||
|
||||
// Create test transactions
|
||||
|
||||
// tx1 is the genesis coinbase
|
||||
let tx1 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::null(),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 70000,
|
||||
script_pubkey: trusted_spks[0].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx2 is an incoming transaction received at untrusted keychain at block 1.
|
||||
let tx2 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 30000,
|
||||
script_pubkey: untrusted_spks[0].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx3 spends tx2 and gives a change back in trusted keychain. Confirmed at Block 2.
|
||||
let tx3 = Transaction {
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint::new(tx2.txid(), 0),
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: 10000,
|
||||
script_pubkey: trusted_spks[1].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx4 is an external transaction receiving at untrusted keychain, unconfirmed.
|
||||
let tx4 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 20000,
|
||||
script_pubkey: untrusted_spks[1].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx5 is spending tx3 and receiving change at trusted keychain, unconfirmed.
|
||||
let tx5 = Transaction {
|
||||
output: vec![TxOut {
|
||||
value: 15000,
|
||||
script_pubkey: trusted_spks[2].to_owned(),
|
||||
}],
|
||||
..common::new_tx(0)
|
||||
};
|
||||
|
||||
// tx6 is an unrelated transaction confirmed at 3.
|
||||
let tx6 = common::new_tx(0);
|
||||
|
||||
// Insert transactions into graph with respective anchors
|
||||
// For unconfirmed txs we pass in `None`.
|
||||
|
||||
let _ =
|
||||
graph.batch_insert_relevant([&tx1, &tx2, &tx3, &tx6].iter().enumerate().map(|(i, tx)| {
|
||||
let height = i as u32;
|
||||
(
|
||||
*tx,
|
||||
local_chain
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.cloned()
|
||||
.map(|hash| BlockId { height, hash })
|
||||
.map(|anchor_block| ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: anchor_block.height,
|
||||
}),
|
||||
)
|
||||
}));
|
||||
|
||||
let _ = graph.batch_insert_relevant_unconfirmed([&tx4, &tx5].iter().map(|tx| (*tx, 100)));
|
||||
|
||||
// A helper lambda to extract and filter data from the graph.
|
||||
let fetch =
|
||||
|height: u32,
|
||||
graph: &IndexedTxGraph<ConfirmationHeightAnchor, KeychainTxOutIndex<String>>| {
|
||||
let chain_tip = local_chain
|
||||
.blocks()
|
||||
.get(&height)
|
||||
.map(|&hash| BlockId { height, hash })
|
||||
.unwrap_or_else(|| panic!("block must exist at {}", height));
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.filter_chain_txouts(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let balance = graph.graph().balance(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|_, spk: &Script| trusted_spks.contains(&spk.to_owned()),
|
||||
);
|
||||
|
||||
assert_eq!(txouts.len(), 5);
|
||||
assert_eq!(utxos.len(), 4);
|
||||
|
||||
let confirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|(_, full_txout)| {
|
||||
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_txouts_txid = txouts
|
||||
.iter()
|
||||
.filter_map(|(_, full_txout)| {
|
||||
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let confirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|(_, full_txout)| {
|
||||
if matches!(full_txout.chain_position, ChainPosition::Confirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let unconfirmed_utxos_txid = utxos
|
||||
.iter()
|
||||
.filter_map(|(_, full_txout)| {
|
||||
if matches!(full_txout.chain_position, ChainPosition::Unconfirmed(_)) {
|
||||
Some(full_txout.outpoint.txid)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
(
|
||||
confirmed_txouts_txid,
|
||||
unconfirmed_txouts_txid,
|
||||
confirmed_utxos_txid,
|
||||
unconfirmed_utxos_txid,
|
||||
balance,
|
||||
)
|
||||
};
|
||||
|
||||
// ----- TEST BLOCK -----
|
||||
|
||||
// AT Block 0
|
||||
{
|
||||
let (
|
||||
confirmed_txouts_txid,
|
||||
unconfirmed_txouts_txid,
|
||||
confirmed_utxos_txid,
|
||||
unconfirmed_utxos_txid,
|
||||
balance,
|
||||
) = fetch(0, &graph);
|
||||
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx2.txid(), tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 1
|
||||
{
|
||||
let (
|
||||
confirmed_txouts_txid,
|
||||
unconfirmed_txouts_txid,
|
||||
confirmed_utxos_txid,
|
||||
unconfirmed_utxos_txid,
|
||||
balance,
|
||||
) = fetch(1, &graph);
|
||||
|
||||
// tx2 gets into confirmed txout set
|
||||
assert_eq!(confirmed_txouts_txid, [tx1.txid(), tx2.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_txouts_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
// tx2 doesn't get into confirmed utxos set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid()].into());
|
||||
assert_eq!(
|
||||
unconfirmed_utxos_txid,
|
||||
[tx3.txid(), tx4.txid(), tx5.txid()].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 25000, // tx3 + tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 0 // Nothing is confirmed yet
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 2
|
||||
{
|
||||
let (
|
||||
confirmed_txouts_txid,
|
||||
unconfirmed_txouts_txid,
|
||||
confirmed_utxos_txid,
|
||||
unconfirmed_utxos_txid,
|
||||
balance,
|
||||
) = fetch(2, &graph);
|
||||
|
||||
// tx3 now gets into the confirmed txout set
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
// tx3 also gets into confirmed utxo set
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx3 got confirmed
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 98
|
||||
{
|
||||
let (
|
||||
confirmed_txouts_txid,
|
||||
unconfirmed_txouts_txid,
|
||||
confirmed_utxos_txid,
|
||||
unconfirmed_utxos_txid,
|
||||
balance,
|
||||
) = fetch(98, &graph);
|
||||
|
||||
assert_eq!(
|
||||
confirmed_txouts_txid,
|
||||
[tx1.txid(), tx2.txid(), tx3.txid()].into()
|
||||
);
|
||||
assert_eq!(unconfirmed_txouts_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
assert_eq!(confirmed_utxos_txid, [tx1.txid(), tx3.txid()].into());
|
||||
assert_eq!(unconfirmed_utxos_txid, [tx4.txid(), tx5.txid()].into());
|
||||
|
||||
// Coinbase is still immature
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 70000, // immature coinbase
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 10000 // tx1 got matured
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// AT Block 99
|
||||
{
|
||||
let (_, _, _, _, balance) = fetch(100, &graph);
|
||||
|
||||
// Coinbase maturity hits
|
||||
assert_eq!(
|
||||
balance,
|
||||
Balance {
|
||||
immature: 0, // coinbase matured
|
||||
trusted_pending: 15000, // tx5
|
||||
untrusted_pending: 20000, // tx4
|
||||
confirmed: 80000 // tx1 + tx3
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
488
crates/chain/tests/test_keychain_txout_index.rs
Normal file
488
crates/chain/tests/test_keychain_txout_index.rs
Normal file
@@ -0,0 +1,488 @@
|
||||
#![cfg(feature = "miniscript")]
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
use bdk_chain::{
|
||||
collections::BTreeMap,
|
||||
indexed_tx_graph::Indexer,
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
Append,
|
||||
};
|
||||
|
||||
use bitcoin::{secp256k1::Secp256k1, OutPoint, ScriptBuf, Transaction, TxOut};
|
||||
use miniscript::{Descriptor, DescriptorPublicKey};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, Ord, PartialOrd)]
|
||||
enum TestKeychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
fn init_txout_index(
|
||||
lookahead: u32,
|
||||
) -> (
|
||||
bdk_chain::keychain::KeychainTxOutIndex<TestKeychain>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
Descriptor<DescriptorPublicKey>,
|
||||
) {
|
||||
let mut txout_index = bdk_chain::keychain::KeychainTxOutIndex::<TestKeychain>::new(lookahead);
|
||||
|
||||
let secp = bdk_chain::bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (external_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/0/*)").unwrap();
|
||||
let (internal_descriptor,_) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "tr([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/*)").unwrap();
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, external_descriptor.clone());
|
||||
txout_index.add_keychain(TestKeychain::Internal, internal_descriptor.clone());
|
||||
|
||||
(txout_index, external_descriptor, internal_descriptor)
|
||||
}
|
||||
|
||||
fn spk_at_index(descriptor: &Descriptor<DescriptorPublicKey>, index: u32) -> ScriptBuf {
|
||||
descriptor
|
||||
.derived_descriptor(&Secp256k1::verification_only(), index)
|
||||
.expect("must derive")
|
||||
.script_pubkey()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_set_all_derivation_indices() {
|
||||
use bdk_chain::indexed_tx_graph::Indexer;
|
||||
|
||||
let (mut txout_index, _, _) = init_txout_index(0);
|
||||
let derive_to: BTreeMap<_, _> =
|
||||
[(TestKeychain::External, 12), (TestKeychain::Internal, 24)].into();
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to).1.as_inner(),
|
||||
&derive_to
|
||||
);
|
||||
assert_eq!(txout_index.last_revealed_indices(), &derive_to);
|
||||
assert_eq!(
|
||||
txout_index.reveal_to_target_multi(&derive_to).1,
|
||||
keychain::ChangeSet::default(),
|
||||
"no changes if we set to the same thing"
|
||||
);
|
||||
assert_eq!(txout_index.initial_changeset().as_inner(), &derive_to);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_lookahead() {
|
||||
let (mut txout_index, external_desc, internal_desc) = init_txout_index(10);
|
||||
|
||||
// given:
|
||||
// - external lookahead set to 10
|
||||
// when:
|
||||
// - set external derivation index to value higher than last, but within the lookahead value
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly
|
||||
// - stored scripts of external keychain should be of expected counts
|
||||
for index in (0..20).skip_while(|i| i % 2 == 1) {
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, index);
|
||||
assert_eq!(
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
vec![(index, spk_at_index(&external_desc, index))],
|
||||
);
|
||||
assert_eq!(
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::External, index)].into()
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
txout_index.inner().all_spks().len(),
|
||||
10 /* external lookahead */ +
|
||||
10 /* internal lookahead */ +
|
||||
index as usize + 1 /* `derived` count */
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::External)
|
||||
.count(),
|
||||
index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.unused_keychain_spks(&TestKeychain::Internal)
|
||||
.count(),
|
||||
0,
|
||||
);
|
||||
}
|
||||
|
||||
// given:
|
||||
// - internal lookahead is 10
|
||||
// - internal derivation index is `None`
|
||||
// when:
|
||||
// - derivation index is set ahead of current derivation index + lookahead
|
||||
// expect:
|
||||
// - scripts cached in spk_txout_index should increase correctly, a.k.a. no scripts are skipped
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::Internal, 24);
|
||||
assert_eq!(
|
||||
revealed_spks.collect::<Vec<_>>(),
|
||||
(0..=24)
|
||||
.map(|index| (index, spk_at_index(&internal_desc, index)))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
assert_eq!(
|
||||
revealed_changeset.as_inner(),
|
||||
&[(TestKeychain::Internal, 24)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.inner().all_spks().len(),
|
||||
10 /* external lookahead */ +
|
||||
10 /* internal lookahead */ +
|
||||
20 /* external stored index count */ +
|
||||
25 /* internal stored index count */
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.count(),
|
||||
25,
|
||||
);
|
||||
|
||||
// ensure derivation indices are expected for each keychain
|
||||
let last_external_index = txout_index
|
||||
.last_revealed_index(&TestKeychain::External)
|
||||
.expect("already derived");
|
||||
let last_internal_index = txout_index
|
||||
.last_revealed_index(&TestKeychain::Internal)
|
||||
.expect("already derived");
|
||||
assert_eq!(last_external_index, 19);
|
||||
assert_eq!(last_internal_index, 24);
|
||||
|
||||
// when:
|
||||
// - scanning txouts with spks within stored indexes
|
||||
// expect:
|
||||
// - no changes to stored index counts
|
||||
let external_iter = 0..=last_external_index;
|
||||
let internal_iter = last_internal_index - last_external_index..=last_internal_index;
|
||||
for (external_index, internal_index) in external_iter.zip(internal_iter) {
|
||||
let tx = Transaction {
|
||||
output: vec![
|
||||
TxOut {
|
||||
script_pubkey: external_desc
|
||||
.at_derivation_index(external_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: 10_000,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: internal_desc
|
||||
.at_derivation_index(internal_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: 10_000,
|
||||
},
|
||||
],
|
||||
..common::new_tx(external_index)
|
||||
};
|
||||
assert_eq!(txout_index.index_tx(&tx), keychain::ChangeSet::default());
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
Some(last_external_index)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::Internal),
|
||||
Some(last_internal_index)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.count(),
|
||||
last_external_index as usize + 1,
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::Internal)
|
||||
.count(),
|
||||
last_internal_index as usize + 1,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// when:
|
||||
// - scanning txouts with spks above last stored index
|
||||
// expect:
|
||||
// - last revealed index should increase as expected
|
||||
// - last used index should change as expected
|
||||
#[test]
|
||||
fn test_scan_with_lookahead() {
|
||||
let (mut txout_index, external_desc, _) = init_txout_index(10);
|
||||
|
||||
let spks: BTreeMap<u32, ScriptBuf> = [0, 10, 20, 30]
|
||||
.into_iter()
|
||||
.map(|i| {
|
||||
(
|
||||
i,
|
||||
external_desc
|
||||
.at_derivation_index(i)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
for (&spk_i, spk) in &spks {
|
||||
let op = OutPoint::new(h!("fake tx"), spk_i);
|
||||
let txout = TxOut {
|
||||
script_pubkey: spk.clone(),
|
||||
value: 0,
|
||||
};
|
||||
|
||||
let changeset = txout_index.index_txout(op, &txout);
|
||||
assert_eq!(
|
||||
changeset.as_inner(),
|
||||
&[(TestKeychain::External, spk_i)].into()
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_revealed_index(&TestKeychain::External),
|
||||
Some(spk_i)
|
||||
);
|
||||
assert_eq!(
|
||||
txout_index.last_used_index(&TestKeychain::External),
|
||||
Some(spk_i)
|
||||
);
|
||||
}
|
||||
|
||||
// now try with index 41 (lookahead surpassed), we expect that the txout to not be indexed
|
||||
let spk_41 = external_desc
|
||||
.at_derivation_index(41)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
let op = OutPoint::new(h!("fake tx"), 41);
|
||||
let txout = TxOut {
|
||||
script_pubkey: spk_41,
|
||||
value: 0,
|
||||
};
|
||||
let changeset = txout_index.index_txout(op, &txout);
|
||||
assert!(changeset.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[rustfmt::skip]
|
||||
fn test_wildcard_derivations() {
|
||||
let (mut txout_index, external_desc, _) = init_txout_index(0);
|
||||
let external_spk_0 = external_desc.at_derivation_index(0).unwrap().script_pubkey();
|
||||
let external_spk_16 = external_desc.at_derivation_index(16).unwrap().script_pubkey();
|
||||
let external_spk_26 = external_desc.at_derivation_index(26).unwrap().script_pubkey();
|
||||
let external_spk_27 = external_desc.at_derivation_index(27).unwrap().script_pubkey();
|
||||
|
||||
// - nothing is derived
|
||||
// - unused list is also empty
|
||||
//
|
||||
// - next_derivation_index() == (0, true)
|
||||
// - derive_new() == ((0, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((0, <spk>), keychain::ChangeSet:is_empty())
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0_u32, external_spk_0.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// - derived till 25
|
||||
// - used all spks till 15.
|
||||
// - used list : [0..=15, 17, 20, 23]
|
||||
// - unused list: [16, 18, 19, 21, 22, 24, 25]
|
||||
|
||||
// - next_derivation_index() = (26, true)
|
||||
// - derive_new() = ((26, <spk>), keychain::ChangeSet)
|
||||
// - next_unused() == ((16, <spk>), keychain::ChangeSet::is_empty())
|
||||
let _ = txout_index.reveal_to_target(&TestKeychain::External, 25);
|
||||
|
||||
(0..=15)
|
||||
.chain([17, 20, 23])
|
||||
.for_each(|index| assert!(txout_index.mark_used(TestKeychain::External, index)));
|
||||
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (26, true));
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (26, external_spk_26.as_script()));
|
||||
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 26)].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (16, external_spk_16.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// - Use all the derived till 26.
|
||||
// - next_unused() = ((27, <spk>), keychain::ChangeSet)
|
||||
(0..=26).for_each(|index| {
|
||||
txout_index.mark_used(TestKeychain::External, index);
|
||||
});
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (27, external_spk_27.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 27)].into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_non_wildcard_derivations() {
|
||||
let mut txout_index = KeychainTxOutIndex::<TestKeychain>::new(0);
|
||||
|
||||
let secp = bitcoin::secp256k1::Secp256k1::signing_only();
|
||||
let (no_wildcard_descriptor, _) = Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, "wpkh([73c5da0a/86'/0'/0']xprv9xgqHN7yz9MwCkxsBPN5qetuNdQSUttZNKw1dcYTV4mkaAFiBVGQziHs3NRSWMkCzvgjEe3n9xV8oYywvM8at9yRqyaZVz6TYYhX98VjsUk/1/0)").unwrap();
|
||||
let external_spk = no_wildcard_descriptor
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.script_pubkey();
|
||||
|
||||
txout_index.add_keychain(TestKeychain::External, no_wildcard_descriptor);
|
||||
|
||||
// given:
|
||||
// - `txout_index` with no stored scripts
|
||||
// expect:
|
||||
// - next derivation index should be new
|
||||
// - when we derive a new script, script @ index 0
|
||||
// - when we get the next unused script, script @ index 0
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, true));
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[(TestKeychain::External, 0)].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
// given:
|
||||
// - the non-wildcard descriptor already has a stored and used script
|
||||
// expect:
|
||||
// - next derivation index should not be new
|
||||
// - derive new and next unused should return the old script
|
||||
// - store_up_to should not panic and return empty changeset
|
||||
assert_eq!(txout_index.next_index(&TestKeychain::External), (0, false));
|
||||
txout_index.mark_used(TestKeychain::External, 0);
|
||||
|
||||
let (spk, changeset) = txout_index.reveal_next_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
|
||||
let (spk, changeset) = txout_index.next_unused_spk(&TestKeychain::External);
|
||||
assert_eq!(spk, (0, external_spk.as_script()));
|
||||
assert_eq!(changeset.as_inner(), &[].into());
|
||||
let (revealed_spks, revealed_changeset) =
|
||||
txout_index.reveal_to_target(&TestKeychain::External, 200);
|
||||
assert_eq!(revealed_spks.count(), 0);
|
||||
assert!(revealed_changeset.is_empty());
|
||||
|
||||
// we check that spks_of_keychain returns a SpkIterator with just one element
|
||||
assert_eq!(
|
||||
txout_index
|
||||
.revealed_keychain_spks(&TestKeychain::External)
|
||||
.count(),
|
||||
1,
|
||||
);
|
||||
}
|
||||
|
||||
/// Check that calling `lookahead_to_target` stores the expected spks.
|
||||
#[test]
|
||||
fn lookahead_to_target() {
|
||||
#[derive(Default)]
|
||||
struct TestCase {
|
||||
/// Global lookahead value.
|
||||
lookahead: u32,
|
||||
/// Last revealed index for external keychain.
|
||||
external_last_revealed: Option<u32>,
|
||||
/// Last revealed index for internal keychain.
|
||||
internal_last_revealed: Option<u32>,
|
||||
/// Call `lookahead_to_target(External, u32)`.
|
||||
external_target: Option<u32>,
|
||||
/// Call `lookahead_to_target(Internal, u32)`.
|
||||
internal_target: Option<u32>,
|
||||
}
|
||||
|
||||
let test_cases = &[
|
||||
TestCase {
|
||||
lookahead: 0,
|
||||
external_target: Some(100),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 10,
|
||||
internal_target: Some(99),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 100,
|
||||
internal_target: Some(9),
|
||||
external_target: Some(10),
|
||||
..Default::default()
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 12,
|
||||
external_last_revealed: Some(2),
|
||||
internal_last_revealed: Some(2),
|
||||
internal_target: Some(15),
|
||||
external_target: Some(13),
|
||||
},
|
||||
TestCase {
|
||||
lookahead: 13,
|
||||
external_last_revealed: Some(100),
|
||||
internal_last_revealed: Some(21),
|
||||
internal_target: Some(120),
|
||||
external_target: Some(130),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases {
|
||||
let (mut index, _, _) = init_txout_index(t.lookahead);
|
||||
|
||||
if let Some(last_revealed) = t.external_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::External, last_revealed);
|
||||
}
|
||||
if let Some(last_revealed) = t.internal_last_revealed {
|
||||
let _ = index.reveal_to_target(&TestKeychain::Internal, last_revealed);
|
||||
}
|
||||
|
||||
let keychain_test_cases = [
|
||||
(
|
||||
TestKeychain::External,
|
||||
t.external_last_revealed,
|
||||
t.external_target,
|
||||
),
|
||||
(
|
||||
TestKeychain::Internal,
|
||||
t.internal_last_revealed,
|
||||
t.internal_target,
|
||||
),
|
||||
];
|
||||
for (keychain, last_revealed, target) in keychain_test_cases {
|
||||
if let Some(target) = target {
|
||||
let original_last_stored_index = match last_revealed {
|
||||
Some(last_revealed) => Some(last_revealed + t.lookahead),
|
||||
None => t.lookahead.checked_sub(1),
|
||||
};
|
||||
let exp_last_stored_index = match original_last_stored_index {
|
||||
Some(original_last_stored_index) => {
|
||||
Ord::max(target, original_last_stored_index)
|
||||
}
|
||||
None => target,
|
||||
};
|
||||
index.lookahead_to_target(&keychain, target);
|
||||
let keys = index
|
||||
.inner()
|
||||
.all_spks()
|
||||
.range((keychain.clone(), 0)..=(keychain.clone(), u32::MAX))
|
||||
.map(|(k, _)| k.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let exp_keys = core::iter::repeat(keychain)
|
||||
.zip(0_u32..=exp_last_stored_index)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(keys, exp_keys);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
681
crates/chain/tests/test_local_chain.rs
Normal file
681
crates/chain/tests/test_local_chain.rs
Normal file
@@ -0,0 +1,681 @@
|
||||
use bdk_chain::{
|
||||
local_chain::{
|
||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||
LocalChain, MissingGenesisError, Update,
|
||||
},
|
||||
BlockId,
|
||||
};
|
||||
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestLocalChain<'a> {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
update: Update,
|
||||
exp: ExpectedResult<'a>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum ExpectedResult<'a> {
|
||||
Ok {
|
||||
changeset: &'a [(u32, Option<BlockHash>)],
|
||||
init_changeset: &'a [(u32, Option<BlockHash>)],
|
||||
},
|
||||
Err(CannotConnectError),
|
||||
}
|
||||
|
||||
impl<'a> TestLocalChain<'a> {
|
||||
fn run(mut self) {
|
||||
println!("[TestLocalChain] test: {}", self.name);
|
||||
let got_changeset = match self.chain.apply_update(self.update) {
|
||||
Ok(changeset) => changeset,
|
||||
Err(got_err) => {
|
||||
assert_eq!(
|
||||
ExpectedResult::Err(got_err),
|
||||
self.exp,
|
||||
"{}: unexpected error",
|
||||
self.name
|
||||
);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
match self.exp {
|
||||
ExpectedResult::Ok {
|
||||
changeset,
|
||||
init_changeset,
|
||||
} => {
|
||||
assert_eq!(
|
||||
got_changeset,
|
||||
changeset.iter().cloned().collect(),
|
||||
"{}: unexpected changeset",
|
||||
self.name
|
||||
);
|
||||
assert_eq!(
|
||||
self.chain.initial_changeset(),
|
||||
init_changeset.iter().cloned().collect(),
|
||||
"{}: unexpected initial changeset",
|
||||
self.name
|
||||
);
|
||||
}
|
||||
ExpectedResult::Err(err) => panic!(
|
||||
"{}: expected error ({}), got non-error result: {:?}",
|
||||
self.name, err, got_changeset
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_local_chain() {
|
||||
[
|
||||
TestLocalChain {
|
||||
name: "add first tip",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "add second tip",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A")), (1, h!("B"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("A"))), (1, Some(h!("B")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge",
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 1,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "two disjoint chains cannot merge (existing chain longer)",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("A"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 2,
|
||||
}),
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "duplicate chains should merge",
|
||||
chain: local_chain![(0, h!("A"))],
|
||||
update: chain_update![(0, h!("A"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[],
|
||||
init_changeset: &[(0, Some(h!("A")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (B)
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | _ C D
|
||||
// update | _ B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B")), (2, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("B"))), (2, Some(h!("C"))), (3, Some(h!("D")))],
|
||||
},
|
||||
},
|
||||
// Introduce an older checkpoint (A) that is not directly behind PoA
|
||||
// | 0 | 2 | 3 | 4
|
||||
// chain | _ B C
|
||||
// update | _ A C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 2",
|
||||
chain: local_chain![(0, h!("_")), (3, h!("B")), (4, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("A")), (4, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("A")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (2, Some(h!("A"))), (3, Some(h!("B"))), (4, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce an older checkpoint (B) that is not the oldest checkpoint
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | _ A C
|
||||
// update | _ B C
|
||||
TestLocalChain {
|
||||
name: "can introduce older checkpoint 3",
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(2, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
}
|
||||
},
|
||||
// Introduce two older checkpoints below the PoA
|
||||
// | 0 | 1 | 2 | 3
|
||||
// chain | _ C
|
||||
// update | _ A B C
|
||||
TestLocalChain {
|
||||
name: "introduce two older checkpoints below PoA",
|
||||
chain: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("A"))), (2, Some(h!("B")))],
|
||||
init_changeset: &[(0, Some(h!("_"))), (1, Some(h!("A"))), (2, Some(h!("B"))), (3, Some(h!("C")))],
|
||||
},
|
||||
},
|
||||
TestLocalChain {
|
||||
name: "fix blockhash before agreement point",
|
||||
chain: local_chain![(0, h!("im-wrong")), (1, h!("we-agree"))],
|
||||
update: chain_update![(0, h!("fix")), (1, h!("we-agree"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(0, Some(h!("fix")))],
|
||||
init_changeset: &[(0, Some(h!("fix"))), (1, Some(h!("we-agree")))],
|
||||
},
|
||||
},
|
||||
// B and C are in both chain and update
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | _ B C
|
||||
// update | _ A B C D
|
||||
// This should succeed with the point of agreement being C and A should be added in addition.
|
||||
TestLocalChain {
|
||||
name: "two points of agreement",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[(1, Some(h!("A"))), (4, Some(h!("D")))],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(1, Some(h!("A"))),
|
||||
(2, Some(h!("B"))),
|
||||
(3, Some(h!("C"))),
|
||||
(4, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Update and chain does not connect:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | _ B C
|
||||
// update | _ A B D
|
||||
// This should fail as we cannot figure out whether C & D are on the same chain
|
||||
TestLocalChain {
|
||||
name: "update and chain does not connect",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("A")), (2, h!("B")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError {
|
||||
try_include_height: 3,
|
||||
}),
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | _ B C E
|
||||
// update | _ B' C' D
|
||||
// This should succeed and invalidate B,C and E with point of agreement being A.
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(2, Some(h!("B'"))),
|
||||
(3, Some(h!("C'"))),
|
||||
(4, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4
|
||||
// chain | _ B C E
|
||||
// update | _ B' C' D
|
||||
// This should succeed and invalidate B, C and E with no point of agreement
|
||||
TestLocalChain {
|
||||
name: "transitive invalidation applies to checkpoints higher than invalidation no point of agreement",
|
||||
chain: local_chain![(0, h!("_")), (1, h!("B")), (2, h!("C")), (4, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (1, h!("B'")), (2, h!("C'")), (3, h!("D"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, None)
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(1, Some(h!("B'"))),
|
||||
(2, Some(h!("C'"))),
|
||||
(3, Some(h!("D"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Transient invalidation:
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | _ A B C E
|
||||
// update | _ B' C' D
|
||||
// This should fail since although it tells us that B and C are invalid it doesn't tell us whether
|
||||
// A was invalid.
|
||||
TestLocalChain {
|
||||
name: "invalidation but no connection",
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A")), (2, h!("B")), (3, h!("C")), (5, h!("E"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("B'")), (3, h!("C'")), (4, h!("D"))],
|
||||
exp: ExpectedResult::Err(CannotConnectError { try_include_height: 1 }),
|
||||
},
|
||||
// Introduce blocks between two points of agreement
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A B D E
|
||||
// update | A C E F
|
||||
TestLocalChain {
|
||||
name: "introduce blocks between two points of agreement",
|
||||
chain: local_chain![(0, h!("A")), (1, h!("B")), (3, h!("D")), (4, h!("E"))],
|
||||
update: chain_update![(0, h!("A")), (2, h!("C")), (4, h!("E")), (5, h!("F"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(2, Some(h!("C"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("A"))),
|
||||
(1, Some(h!("B"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D"))),
|
||||
(4, Some(h!("E"))),
|
||||
(5, Some(h!("F"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
// Allow update that is shorter than original chain
|
||||
// | 0 | 1 | 2 | 3 | 4 | 5
|
||||
// chain | A C D E F
|
||||
// update | A C D'
|
||||
TestLocalChain {
|
||||
name: "allow update that is shorter than original chain",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("C")), (3, h!("D")), (4, h!("E")), (5, h!("F"))],
|
||||
update: chain_update![(0, h!("_")), (2, h!("C")), (3, h!("D'"))],
|
||||
exp: ExpectedResult::Ok {
|
||||
changeset: &[
|
||||
(3, Some(h!("D'"))),
|
||||
(4, None),
|
||||
(5, None),
|
||||
],
|
||||
init_changeset: &[
|
||||
(0, Some(h!("_"))),
|
||||
(2, Some(h!("C"))),
|
||||
(3, Some(h!("D'"))),
|
||||
],
|
||||
},
|
||||
},
|
||||
]
|
||||
.into_iter()
|
||||
.for_each(TestLocalChain::run);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_insert_block() {
|
||||
struct TestCase {
|
||||
original: LocalChain,
|
||||
insert: (u32, BlockHash),
|
||||
expected_result: Result<ChangeSet, AlterCheckPointError>,
|
||||
expected_final: LocalChain,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
original: local_chain![(0, h!("_"))],
|
||||
insert: (5, h!("block5")),
|
||||
expected_result: Ok([(5, Some(h!("block5")))].into()),
|
||||
expected_final: local_chain![(0, h!("_")), (5, h!("block5"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(0, h!("_")), (3, h!("A"))],
|
||||
insert: (4, h!("B")),
|
||||
expected_result: Ok([(4, Some(h!("B")))].into()),
|
||||
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(0, h!("_")), (4, h!("B"))],
|
||||
insert: (3, h!("A")),
|
||||
expected_result: Ok([(3, Some(h!("A")))].into()),
|
||||
expected_final: local_chain![(0, h!("_")), (3, h!("A")), (4, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
insert: (2, h!("K")),
|
||||
expected_result: Ok([].into()),
|
||||
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
},
|
||||
TestCase {
|
||||
original: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
insert: (2, h!("J")),
|
||||
expected_result: Err(AlterCheckPointError {
|
||||
height: 2,
|
||||
original_hash: h!("K"),
|
||||
update_hash: Some(h!("J")),
|
||||
}),
|
||||
expected_final: local_chain![(0, h!("_")), (2, h!("K"))],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
let mut chain = t.original;
|
||||
assert_eq!(
|
||||
chain.insert_block(t.insert.into()),
|
||||
t.expected_result,
|
||||
"[{}] unexpected result when inserting block",
|
||||
i,
|
||||
);
|
||||
assert_eq!(chain, t.expected_final, "[{}] unexpected final chain", i,);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_disconnect_from() {
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
original: LocalChain,
|
||||
disconnect_from: (u32, BlockHash),
|
||||
exp_result: Result<ChangeSet, MissingGenesisError>,
|
||||
exp_final: LocalChain,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "try_replace_genesis_should_fail",
|
||||
original: local_chain![(0, h!("_"))],
|
||||
disconnect_from: (0, h!("_")),
|
||||
exp_result: Err(MissingGenesisError),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "try_replace_genesis_should_fail_2",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
disconnect_from: (0, h!("_")),
|
||||
exp_result: Err(MissingGenesisError),
|
||||
exp_final: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "from_does_not_exist",
|
||||
original: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::default()),
|
||||
exp_final: local_chain![(0, h!("_")), (3, h!("C"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "from_has_different_blockhash",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
disconnect_from: (2, h!("not_B")),
|
||||
exp_result: Ok(ChangeSet::default()),
|
||||
exp_final: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "disconnect_one",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::from_iter([(2, None)])),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
TestCase {
|
||||
name: "disconnect_three",
|
||||
original: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C")), (4, h!("D"))],
|
||||
disconnect_from: (2, h!("B")),
|
||||
exp_result: Ok(ChangeSet::from_iter([(2, None), (3, None), (4, None)])),
|
||||
exp_final: local_chain![(0, h!("_"))],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
|
||||
let mut chain = t.original;
|
||||
let result = chain.disconnect_from(t.disconnect_from.into());
|
||||
assert_eq!(
|
||||
result, t.exp_result,
|
||||
"[{}:{}] unexpected changeset result",
|
||||
i, t.name
|
||||
);
|
||||
assert_eq!(
|
||||
chain, t.exp_final,
|
||||
"[{}:{}] unexpected final chain",
|
||||
i, t.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checkpoint_from_block_ids() {
|
||||
struct TestCase<'a> {
|
||||
name: &'a str,
|
||||
blocks: &'a [(u32, BlockHash)],
|
||||
exp_result: Result<(), Option<(u32, BlockHash)>>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "in_order",
|
||||
blocks: &[(0, h!("A")), (1, h!("B")), (3, h!("D"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
TestCase {
|
||||
name: "with_duplicates",
|
||||
blocks: &[(1, h!("B")), (2, h!("C")), (2, h!("C'"))],
|
||||
exp_result: Err(Some((2, h!("C")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "not_in_order",
|
||||
blocks: &[(1, h!("B")), (3, h!("D")), (2, h!("C"))],
|
||||
exp_result: Err(Some((3, h!("D")))),
|
||||
},
|
||||
TestCase {
|
||||
name: "empty",
|
||||
blocks: &[],
|
||||
exp_result: Err(None),
|
||||
},
|
||||
TestCase {
|
||||
name: "single",
|
||||
blocks: &[(21, h!("million"))],
|
||||
exp_result: Ok(()),
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let result = CheckPoint::from_block_ids(
|
||||
t.blocks
|
||||
.iter()
|
||||
.map(|&(height, hash)| BlockId { height, hash }),
|
||||
);
|
||||
match t.exp_result {
|
||||
Ok(_) => {
|
||||
assert!(result.is_ok(), "[{}:{}] should be Ok", i, t.name);
|
||||
let result_vec = {
|
||||
let mut v = result
|
||||
.unwrap()
|
||||
.into_iter()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<Vec<_>>();
|
||||
v.reverse();
|
||||
v
|
||||
};
|
||||
assert_eq!(
|
||||
&result_vec, t.blocks,
|
||||
"[{}:{}] not equal to original block ids",
|
||||
i, t.name
|
||||
);
|
||||
}
|
||||
Err(exp_last) => {
|
||||
assert!(result.is_err(), "[{}:{}] should be Err", i, t.name);
|
||||
let err = result.unwrap_err();
|
||||
assert_eq!(
|
||||
err.as_ref()
|
||||
.map(|last_cp| (last_cp.height(), last_cp.hash())),
|
||||
exp_last,
|
||||
"[{}:{}] error's last cp height should be {:?}, got {:?}",
|
||||
i,
|
||||
t.name,
|
||||
exp_last,
|
||||
err
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_chain_apply_header_connected_to() {
|
||||
fn header_from_prev_blockhash(prev_blockhash: BlockHash) -> Header {
|
||||
Header {
|
||||
version: bitcoin::block::Version::default(),
|
||||
prev_blockhash,
|
||||
merkle_root: bitcoin::hash_types::TxMerkleNode::all_zeros(),
|
||||
time: 0,
|
||||
bits: bitcoin::CompactTarget::default(),
|
||||
nonce: 0,
|
||||
}
|
||||
}
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
header: Header,
|
||||
height: u32,
|
||||
connected_to: BlockId,
|
||||
exp_result: Result<Vec<(u32, Option<BlockHash>)>, ApplyHeaderError>,
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("A"));
|
||||
let hash = header.block_hash();
|
||||
let height = 2;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "connected_to_self_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (height, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let hash = header.block_hash();
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: prev_hash,
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_prev_header_applied_to_self",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, prev_hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![(height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(BlockHash::all_zeros());
|
||||
let hash = header.block_hash();
|
||||
let height = 0;
|
||||
let connected_to = BlockId { height, hash };
|
||||
TestCase {
|
||||
name: "genesis_applied_to_self",
|
||||
chain: local_chain![(0, hash)],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Ok(vec![]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("Z"));
|
||||
let height = 10;
|
||||
let hash = header.block_hash();
|
||||
let prev_height = height - 1;
|
||||
let prev_hash = header.prev_blockhash;
|
||||
TestCase {
|
||||
name: "connect_at_connected_to",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
header,
|
||||
height: 10,
|
||||
connected_to: BlockId {
|
||||
height: 3,
|
||||
hash: h!("C"),
|
||||
},
|
||||
exp_result: Ok(vec![(prev_height, Some(prev_hash)), (height, Some(hash))]),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let connected_to = BlockId {
|
||||
height: prev_height,
|
||||
hash: h!("not_prev_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_prev_hash",
|
||||
chain: local_chain![(0, h!("_")), (prev_height, h!("not_prev_hash"))],
|
||||
header,
|
||||
height: prev_height + 1,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let prev_hash = h!("A");
|
||||
let prev_height = 1;
|
||||
let header = header_from_prev_blockhash(prev_hash);
|
||||
let height = prev_height + 1;
|
||||
let connected_to = BlockId {
|
||||
height,
|
||||
hash: h!("not_current_hash"),
|
||||
};
|
||||
TestCase {
|
||||
name: "inconsistent_current_block",
|
||||
chain: local_chain![(0, h!("_")), (height, h!("not_current_hash"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
{
|
||||
let header = header_from_prev_blockhash(h!("B"));
|
||||
let height = 3;
|
||||
let connected_to = BlockId {
|
||||
height: 4,
|
||||
hash: h!("D"),
|
||||
};
|
||||
TestCase {
|
||||
name: "connected_to_is_greater",
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B"))],
|
||||
header,
|
||||
height,
|
||||
connected_to,
|
||||
exp_result: Err(ApplyHeaderError::InconsistentBlocks),
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("running test case {}: '{}'", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
let result = chain.apply_header_connected_to(&t.header, t.height, t.connected_to);
|
||||
let exp_result = t
|
||||
.exp_result
|
||||
.map(|cs| cs.iter().cloned().collect::<ChangeSet>());
|
||||
assert_eq!(result, exp_result, "[{}:{}] unexpected result", i, t.name);
|
||||
}
|
||||
}
|
||||
100
crates/chain/tests/test_spk_txout_index.rs
Normal file
100
crates/chain/tests/test_spk_txout_index.rs
Normal file
@@ -0,0 +1,100 @@
|
||||
use bdk_chain::{indexed_tx_graph::Indexer, SpkTxOutIndex};
|
||||
use bitcoin::{absolute, OutPoint, ScriptBuf, Transaction, TxIn, TxOut};
|
||||
|
||||
#[test]
|
||||
fn spk_txout_sent_and_received() {
|
||||
let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
|
||||
let spk2 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
|
||||
|
||||
let mut index = SpkTxOutIndex::default();
|
||||
index.insert_spk(0, spk1.clone());
|
||||
index.insert_spk(1, spk2.clone());
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: 0x02,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: 42_000,
|
||||
script_pubkey: spk1.clone(),
|
||||
}],
|
||||
};
|
||||
|
||||
assert_eq!(index.sent_and_received(&tx1), (0, 42_000));
|
||||
assert_eq!(index.net_value(&tx1), 42_000);
|
||||
index.index_tx(&tx1);
|
||||
assert_eq!(
|
||||
index.sent_and_received(&tx1),
|
||||
(0, 42_000),
|
||||
"shouldn't change after scanning"
|
||||
);
|
||||
|
||||
let tx2 = Transaction {
|
||||
version: 0x1,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx1.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: 20_000,
|
||||
script_pubkey: spk2,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: spk1,
|
||||
value: 30_000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
assert_eq!(index.sent_and_received(&tx2), (42_000, 50_000));
|
||||
assert_eq!(index.net_value(&tx2), 8_000);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mark_used() {
|
||||
let spk1 = ScriptBuf::from_hex("001404f1e52ce2bab3423c6a8c63b7cd730d8f12542c").unwrap();
|
||||
let spk2 = ScriptBuf::from_hex("00142b57404ae14f08c3a0c903feb2af7830605eb00f").unwrap();
|
||||
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
spk_index.insert_spk(1, spk1.clone());
|
||||
spk_index.insert_spk(2, spk2);
|
||||
|
||||
assert!(!spk_index.is_used(&1));
|
||||
spk_index.mark_used(&1);
|
||||
assert!(spk_index.is_used(&1));
|
||||
spk_index.unmark_used(&1);
|
||||
assert!(!spk_index.is_used(&1));
|
||||
spk_index.mark_used(&1);
|
||||
assert!(spk_index.is_used(&1));
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: 0x02,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: 42_000,
|
||||
script_pubkey: spk1,
|
||||
}],
|
||||
};
|
||||
|
||||
spk_index.index_tx(&tx1);
|
||||
spk_index.unmark_used(&1);
|
||||
assert!(
|
||||
spk_index.is_used(&1),
|
||||
"even though we unmark_used it doesn't matter because there was a tx scanned that used it"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmark_used_does_not_result_in_invalid_representation() {
|
||||
let mut spk_index = SpkTxOutIndex::default();
|
||||
assert!(!spk_index.unmark_used(&0));
|
||||
assert!(!spk_index.unmark_used(&1));
|
||||
assert!(!spk_index.unmark_used(&2));
|
||||
assert!(spk_index.unused_spks(..).collect::<Vec<_>>().is_empty());
|
||||
}
|
||||
1265
crates/chain/tests/test_tx_graph.rs
Normal file
1265
crates/chain/tests/test_tx_graph.rs
Normal file
File diff suppressed because it is too large
Load Diff
668
crates/chain/tests/test_tx_graph_conflicts.rs
Normal file
668
crates/chain/tests/test_tx_graph_conflicts.rs
Normal file
@@ -0,0 +1,668 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::collections::{BTreeSet, HashSet};
|
||||
|
||||
use bdk_chain::{keychain::Balance, BlockId};
|
||||
use bitcoin::{OutPoint, Script};
|
||||
use common::*;
|
||||
|
||||
#[allow(dead_code)]
|
||||
struct Scenario<'a> {
|
||||
/// Name of the test scenario
|
||||
name: &'a str,
|
||||
/// Transaction templates
|
||||
tx_templates: &'a [TxTemplate<'a, BlockId>],
|
||||
/// Names of txs that must exist in the output of `list_chain_txs`
|
||||
exp_chain_txs: HashSet<&'a str>,
|
||||
/// Outpoints that must exist in the output of `filter_chain_txouts`
|
||||
exp_chain_txouts: HashSet<(&'a str, u32)>,
|
||||
/// Outpoints of UTXOs that must exist in the output of `filter_chain_unspents`
|
||||
exp_unspents: HashSet<(&'a str, u32)>,
|
||||
/// Expected balances
|
||||
exp_balance: Balance,
|
||||
}
|
||||
|
||||
/// This test ensures that [`TxGraph`] will reliably filter out irrelevant transactions when
|
||||
/// presented with multiple conflicting transaction scenarios using the [`TxTemplate`] structure.
|
||||
/// This test also checks that [`TxGraph::list_chain_txs`], [`TxGraph::filter_chain_txouts`],
|
||||
/// [`TxGraph::filter_chain_unspents`], and [`TxGraph::balance`] return correct data.
|
||||
#[test]
|
||||
fn test_tx_conflict_handling() {
|
||||
// Create Local chains
|
||||
let local_chain = local_chain!(
|
||||
(0, h!("A")),
|
||||
(1, h!("B")),
|
||||
(2, h!("C")),
|
||||
(3, h!("D")),
|
||||
(4, h!("E")),
|
||||
(5, h!("F")),
|
||||
(6, h!("G"))
|
||||
);
|
||||
let chain_tip = local_chain.tip().block_id();
|
||||
|
||||
let scenarios = [
|
||||
Scenario {
|
||||
name: "coinbase tx cannot be in mempool and be unconfirmed",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "unconfirmed_coinbase",
|
||||
inputs: &[TxInTemplate::Coinbase],
|
||||
outputs: &[TxOutTemplate::new(5000, Some(0))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "confirmed_genesis",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(1))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "unconfirmed_conflict",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("confirmed_genesis", 0),
|
||||
TxInTemplate::PrevTx("unconfirmed_coinbase", 0)
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "confirmed_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("confirmed_genesis", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(3))],
|
||||
anchors: &[block_id!(4, "E")],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["confirmed_genesis", "confirmed_conflict"]),
|
||||
exp_chain_txouts: HashSet::from([("confirmed_genesis", 0), ("confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "2 unconfirmed txs with same last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
outputs: &[TxOutTemplate::new(40000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// the txgraph is going to pick tx_conflict_2 because of higher lexicographical txid
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "2 unconfirmed txs with different last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0)), TxOutTemplate::new(10000, Some(1))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::PrevTx("tx1", 1)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_2"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx1", 1), ("tx_conflict_2", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_2", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "3 unconfirmed txs with different last_seens conflict",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(40000, Some(3))],
|
||||
last_seen: Some(400),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_3"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_3", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_3", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 40000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "unconfirmed tx conflicts with tx in orphaned block, orphaned higher last_seen",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_orphaned_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
anchors: &[block_id!(4, "Orphaned Block")],
|
||||
last_seen: Some(300),
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_orphaned_conflict"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_orphaned_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_orphaned_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "unconfirmed tx conflicts with tx in orphaned block, orphaned lower last_seen",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_orphaned_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
anchors: &[block_id!(4, "Orphaned Block")],
|
||||
last_seen: Some(100),
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_conflict_1"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_conflict_1", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_conflict_1", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 20000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "multiple unconfirmed txs conflict with a confirmed tx",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "tx1",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_1",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_2",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_conflict_3",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(40000, Some(3))],
|
||||
last_seen: Some(400),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "tx_confirmed_conflict",
|
||||
inputs: &[TxInTemplate::PrevTx("tx1", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
exp_chain_txs: HashSet::from(["tx1", "tx_confirmed_conflict"]),
|
||||
exp_chain_txouts: HashSet::from([("tx1", 0), ("tx_confirmed_conflict", 0)]),
|
||||
exp_unspents: HashSet::from([("tx_confirmed_conflict", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B, all the transactions are unconfirmed, B' has higher last_seen than B",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
last_seen: Some(22),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(23),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
last_seen: Some(24),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
last_seen: Some(25),
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// A, B, C will appear in the list methods
|
||||
// This is because B' has a higher last seen than B, but C has a higher
|
||||
// last seen than B', so B and C are considered canonical
|
||||
exp_chain_txs: HashSet::from(["A", "B", "C"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B", 0), ("C", 0)]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B, A and B' are in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(4, "E")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// B and C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 20000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends B', A and B' are in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(2))],
|
||||
anchors: &[block_id!(4, "E")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[TxInTemplate::PrevTx("B'", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// B should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'", "C"]),
|
||||
exp_chain_txouts: HashSet::from([
|
||||
("A", 0),
|
||||
("B'", 0),
|
||||
("C", 0),
|
||||
]),
|
||||
exp_unspents: HashSet::from([("C", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, C spends both B and B', A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(30000, Some(2))],
|
||||
last_seen: Some(300),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(3))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 30000,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 0,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(5))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// C should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
Scenario {
|
||||
name: "B and B' spend A and conflict, B' is confirmed, C spends both B and B', D spends C, A is in best chain",
|
||||
tx_templates: &[
|
||||
TxTemplate {
|
||||
tx_name: "A",
|
||||
inputs: &[TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(10000, Some(0))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
last_seen: None,
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0), TxInTemplate::Bogus],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(1))],
|
||||
last_seen: Some(200),
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "B'",
|
||||
inputs: &[TxInTemplate::PrevTx("A", 0)],
|
||||
outputs: &[TxOutTemplate::new(50000, Some(4))],
|
||||
anchors: &[block_id!(1, "B")],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "C",
|
||||
inputs: &[
|
||||
TxInTemplate::PrevTx("B", 0),
|
||||
TxInTemplate::PrevTx("B'", 0),
|
||||
],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(5))],
|
||||
..Default::default()
|
||||
},
|
||||
TxTemplate {
|
||||
tx_name: "D",
|
||||
inputs: &[TxInTemplate::PrevTx("C", 0)],
|
||||
outputs: &[TxOutTemplate::new(20000, Some(6))],
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
// D should not appear in the list methods
|
||||
exp_chain_txs: HashSet::from(["A", "B'"]),
|
||||
exp_chain_txouts: HashSet::from([("A", 0), ("B'", 0)]),
|
||||
exp_unspents: HashSet::from([("B'", 0)]),
|
||||
exp_balance: Balance {
|
||||
immature: 0,
|
||||
trusted_pending: 0,
|
||||
untrusted_pending: 0,
|
||||
confirmed: 50000,
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
for scenario in scenarios {
|
||||
let (tx_graph, spk_index, exp_tx_ids) = init_graph(scenario.tx_templates.iter());
|
||||
|
||||
let txs = tx_graph
|
||||
.list_chain_txs(&local_chain, chain_tip)
|
||||
.map(|tx| tx.tx_node.txid)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_txs = scenario
|
||||
.exp_chain_txs
|
||||
.iter()
|
||||
.map(|txid| *exp_tx_ids.get(txid).expect("txid must exist"))
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
txs, exp_txs,
|
||||
"\n[{}] 'list_chain_txs' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let txouts = tx_graph
|
||||
.filter_chain_txouts(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|(_, full_txout)| full_txout.outpoint)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_txouts = scenario
|
||||
.exp_chain_txouts
|
||||
.iter()
|
||||
.map(|(txid, vout)| OutPoint {
|
||||
txid: *exp_tx_ids.get(txid).expect("txid must exist"),
|
||||
vout: *vout,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
txouts, exp_txouts,
|
||||
"\n[{}] 'filter_chain_txouts' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let utxos = tx_graph
|
||||
.filter_chain_unspents(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
)
|
||||
.map(|(_, full_txout)| full_txout.outpoint)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let exp_utxos = scenario
|
||||
.exp_unspents
|
||||
.iter()
|
||||
.map(|(txid, vout)| OutPoint {
|
||||
txid: *exp_tx_ids.get(txid).expect("txid must exist"),
|
||||
vout: *vout,
|
||||
})
|
||||
.collect::<BTreeSet<_>>();
|
||||
assert_eq!(
|
||||
utxos, exp_utxos,
|
||||
"\n[{}] 'filter_chain_unspents' failed",
|
||||
scenario.name
|
||||
);
|
||||
|
||||
let balance = tx_graph.balance(
|
||||
&local_chain,
|
||||
chain_tip,
|
||||
spk_index.outpoints().iter().cloned(),
|
||||
|_, spk: &Script| spk_index.index_of_spk(spk).is_some(),
|
||||
);
|
||||
assert_eq!(
|
||||
balance, scenario.exp_balance,
|
||||
"\n[{}] 'balance' failed",
|
||||
scenario.name
|
||||
);
|
||||
}
|
||||
}
|
||||
17
crates/electrum/Cargo.toml
Normal file
17
crates/electrum/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_electrum"
|
||||
description = "Fetch data from electrum in the form BDK accepts"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
electrum-client = { version = "0.18" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
7
crates/electrum/README.md
Normal file
7
crates/electrum/README.md
Normal file
@@ -0,0 +1,7 @@
|
||||
# BDK Electrum
|
||||
|
||||
BDK Electrum extends [`electrum-client`] to update [`bdk_chain`] structures
|
||||
from an Electrum server.
|
||||
|
||||
[`electrum-client`]: https://docs.rs/electrum-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
559
crates/electrum/src/electrum_ext.rs
Normal file
559
crates/electrum/src/electrum_ext.rs
Normal file
@@ -0,0 +1,559 @@
|
||||
use bdk_chain::{
|
||||
bitcoin::{OutPoint, ScriptBuf, Transaction, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
tx_graph::{self, TxGraph},
|
||||
Anchor, BlockId, ConfirmationHeightAnchor, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
use electrum_client::{Client, ElectrumApi, Error, HeaderNotification};
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet, HashMap, HashSet},
|
||||
fmt::Debug,
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
/// We include a chain suffix of a certain length for the purpose of robustness.
|
||||
const CHAIN_SUFFIX_LENGTH: u32 = 8;
|
||||
|
||||
/// Represents updates fetched from an Electrum server, but excludes full transactions.
|
||||
///
|
||||
/// To provide a complete update to [`TxGraph`], you'll need to call [`Self::missing_full_txs`] to
|
||||
/// determine the full transactions missing from [`TxGraph`]. Then call [`Self::into_tx_graph`] to
|
||||
/// fetch the full transactions from Electrum and finalize the update.
|
||||
#[derive(Debug, Default, Clone)]
|
||||
pub struct RelevantTxids(HashMap<Txid, BTreeSet<ConfirmationHeightAnchor>>);
|
||||
|
||||
impl RelevantTxids {
|
||||
/// Determine the full transactions that are missing from `graph`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn missing_full_txs<A: Anchor>(&self, graph: &TxGraph<A>) -> Vec<Txid> {
|
||||
self.0
|
||||
.keys()
|
||||
.filter(move |&&txid| graph.as_ref().get_tx(txid).is_none())
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Finalizes the [`TxGraph`] update by fetching `missing` txids from the `client`.
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
pub fn into_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
seen_at: Option<u64>,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationHeightAnchor>, Error> {
|
||||
let new_txs = client.batch_transaction_get(&missing)?;
|
||||
let mut graph = TxGraph::<ConfirmationHeightAnchor>::new(new_txs);
|
||||
for (txid, anchors) in self.0 {
|
||||
if let Some(seen_at) = seen_at {
|
||||
let _ = graph.insert_seen_at(txid, seen_at);
|
||||
}
|
||||
for anchor in anchors {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
|
||||
/// Finalizes the update by fetching `missing` txids from the `client`, where the
|
||||
/// resulting [`TxGraph`] has anchors of type [`ConfirmationTimeHeightAnchor`].
|
||||
///
|
||||
/// Refer to [`RelevantTxids`] for more details.
|
||||
///
|
||||
/// **Note:** The confirmation time might not be precisely correct if there has been a reorg.
|
||||
// Electrum's API intends that we use the merkle proof API, we should change `bdk_electrum` to
|
||||
// use it.
|
||||
pub fn into_confirmation_time_tx_graph(
|
||||
self,
|
||||
client: &Client,
|
||||
seen_at: Option<u64>,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let graph = self.into_tx_graph(client, seen_at, missing)?;
|
||||
|
||||
let relevant_heights = {
|
||||
let mut visited_heights = HashSet::new();
|
||||
graph
|
||||
.all_anchors()
|
||||
.iter()
|
||||
.map(|(a, _)| a.confirmation_height_upper_bound())
|
||||
.filter(move |&h| visited_heights.insert(h))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
let height_to_time = relevant_heights
|
||||
.clone()
|
||||
.into_iter()
|
||||
.zip(
|
||||
client
|
||||
.batch_block_header(relevant_heights)?
|
||||
.into_iter()
|
||||
.map(|bh| bh.time as u64),
|
||||
)
|
||||
.collect::<HashMap<u32, u64>>();
|
||||
|
||||
let graph_changeset = {
|
||||
let old_changeset = TxGraph::default().apply_update(graph);
|
||||
tx_graph::ChangeSet {
|
||||
txs: old_changeset.txs,
|
||||
txouts: old_changeset.txouts,
|
||||
last_seen: old_changeset.last_seen,
|
||||
anchors: old_changeset
|
||||
.anchors
|
||||
.into_iter()
|
||||
.map(|(height_anchor, txid)| {
|
||||
let confirmation_height = height_anchor.confirmation_height;
|
||||
let confirmation_time = height_to_time[&confirmation_height];
|
||||
let time_anchor = ConfirmationTimeHeightAnchor {
|
||||
anchor_block: height_anchor.anchor_block,
|
||||
confirmation_height,
|
||||
confirmation_time,
|
||||
};
|
||||
(time_anchor, txid)
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
};
|
||||
|
||||
let mut new_graph = TxGraph::default();
|
||||
new_graph.apply_changeset(graph_changeset);
|
||||
Ok(new_graph)
|
||||
}
|
||||
}
|
||||
|
||||
/// Combination of chain and transactions updates from electrum
|
||||
///
|
||||
/// We have to update the chain and the txids at the same time since we anchor the txids to
|
||||
/// the same chain tip that we check before and after we gather the txids.
|
||||
#[derive(Debug)]
|
||||
pub struct ElectrumUpdate {
|
||||
/// Chain update
|
||||
pub chain_update: local_chain::Update,
|
||||
/// Transaction updates from electrum
|
||||
pub relevant_txids: RelevantTxids,
|
||||
}
|
||||
|
||||
/// Trait to extend [`Client`] functionality.
|
||||
pub trait ElectrumExt {
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Electrum client) and
|
||||
/// returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||
/// - `keychain_spks`: keychains that we want to scan transactions for
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `batch_size` specifies the max number of script pubkeys to request for in a
|
||||
/// single batch request.
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Electrum client) for the data specified
|
||||
/// and returns updates for [`bdk_chain`] data structures.
|
||||
///
|
||||
/// - `prev_tip`: the most recent blockchain tip present locally
|
||||
/// - `misc_spks`: an iterator of scripts we want to sync transactions for
|
||||
/// - `txids`: transactions for which we want updated [`Anchor`]s
|
||||
/// - `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
///
|
||||
/// `batch_size` specifies the max number of script pubkeys to request for in a single batch
|
||||
/// request.
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: ElectrumExt::full_scan
|
||||
fn sync(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
batch_size: usize,
|
||||
) -> Result<ElectrumUpdate, Error>;
|
||||
}
|
||||
|
||||
impl<A: ElectrumApi> ElectrumExt for A {
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<(ElectrumUpdate, BTreeMap<K, u32>), Error> {
|
||||
let mut request_spks = keychain_spks
|
||||
.into_iter()
|
||||
.map(|(k, s)| (k.clone(), s.into_iter()))
|
||||
.collect::<BTreeMap<K, _>>();
|
||||
let mut scanned_spks = BTreeMap::<(K, u32), (ScriptBuf, bool)>::new();
|
||||
|
||||
let (electrum_update, keychain_update) = loop {
|
||||
let (tip, _) = construct_update_tip(self, prev_tip.clone())?;
|
||||
let mut relevant_txids = RelevantTxids::default();
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
if !request_spks.is_empty() {
|
||||
if !scanned_spks.is_empty() {
|
||||
scanned_spks.append(&mut populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
&mut scanned_spks
|
||||
.iter()
|
||||
.map(|(i, (spk, _))| (i.clone(), spk.clone())),
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?);
|
||||
}
|
||||
for (keychain, keychain_spks) in &mut request_spks {
|
||||
scanned_spks.extend(
|
||||
populate_with_spks(
|
||||
self,
|
||||
&cps,
|
||||
&mut relevant_txids,
|
||||
keychain_spks,
|
||||
stop_gap,
|
||||
batch_size,
|
||||
)?
|
||||
.into_iter()
|
||||
.map(|(spk_i, spk)| ((keychain.clone(), spk_i), spk)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// check for reorgs during scan process
|
||||
let server_blockhash = self.block_header(tip.height() as usize)?.block_hash();
|
||||
if tip.hash() != server_blockhash {
|
||||
continue; // reorg
|
||||
}
|
||||
|
||||
let chain_update = local_chain::Update {
|
||||
tip,
|
||||
introduce_older_blocks: true,
|
||||
};
|
||||
|
||||
let keychain_update = request_spks
|
||||
.into_keys()
|
||||
.filter_map(|k| {
|
||||
scanned_spks
|
||||
.range((k.clone(), u32::MIN)..=(k.clone(), u32::MAX))
|
||||
.rev()
|
||||
.find(|(_, (_, active))| *active)
|
||||
.map(|((_, i), _)| (k, *i))
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
break (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
);
|
||||
};
|
||||
|
||||
Ok((electrum_update, keychain_update))
|
||||
}
|
||||
|
||||
fn sync(
|
||||
&self,
|
||||
prev_tip: CheckPoint,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
batch_size: usize,
|
||||
) -> Result<ElectrumUpdate, Error> {
|
||||
let spk_iter = misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk));
|
||||
|
||||
let (mut electrum_update, _) = self.full_scan(
|
||||
prev_tip.clone(),
|
||||
[((), spk_iter)].into(),
|
||||
usize::MAX,
|
||||
batch_size,
|
||||
)?;
|
||||
|
||||
let (tip, _) = construct_update_tip(self, prev_tip)?;
|
||||
let cps = tip
|
||||
.iter()
|
||||
.take(10)
|
||||
.map(|cp| (cp.height(), cp))
|
||||
.collect::<BTreeMap<u32, CheckPoint>>();
|
||||
|
||||
populate_with_txids(self, &cps, &mut electrum_update.relevant_txids, txids)?;
|
||||
|
||||
let _txs =
|
||||
populate_with_outpoints(self, &cps, &mut electrum_update.relevant_txids, outpoints)?;
|
||||
|
||||
Ok(electrum_update)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return a [`CheckPoint`] of the latest tip, that connects with `prev_tip`.
|
||||
fn construct_update_tip(
|
||||
client: &impl ElectrumApi,
|
||||
prev_tip: CheckPoint,
|
||||
) -> Result<(CheckPoint, Option<u32>), Error> {
|
||||
let HeaderNotification { height, .. } = client.block_headers_subscribe()?;
|
||||
let new_tip_height = height as u32;
|
||||
|
||||
// If electrum returns a tip height that is lower than our previous tip, then checkpoints do
|
||||
// not need updating. We just return the previous tip and use that as the point of agreement.
|
||||
if new_tip_height < prev_tip.height() {
|
||||
return Ok((prev_tip.clone(), Some(prev_tip.height())));
|
||||
}
|
||||
|
||||
// Atomically fetch the latest `CHAIN_SUFFIX_LENGTH` count of blocks from Electrum. We use this
|
||||
// to construct our checkpoint update.
|
||||
let mut new_blocks = {
|
||||
let start_height = new_tip_height.saturating_sub(CHAIN_SUFFIX_LENGTH - 1);
|
||||
let hashes = client
|
||||
.block_headers(start_height as _, CHAIN_SUFFIX_LENGTH as _)?
|
||||
.headers
|
||||
.into_iter()
|
||||
.map(|h| h.block_hash());
|
||||
(start_height..).zip(hashes).collect::<BTreeMap<u32, _>>()
|
||||
};
|
||||
|
||||
// Find the "point of agreement" (if any).
|
||||
let agreement_cp = {
|
||||
let mut agreement_cp = Option::<CheckPoint>::None;
|
||||
for cp in prev_tip.iter() {
|
||||
let cp_block = cp.block_id();
|
||||
let hash = match new_blocks.get(&cp_block.height) {
|
||||
Some(&hash) => hash,
|
||||
None => {
|
||||
assert!(
|
||||
new_tip_height >= cp_block.height,
|
||||
"already checked that electrum's tip cannot be smaller"
|
||||
);
|
||||
let hash = client.block_header(cp_block.height as _)?.block_hash();
|
||||
new_blocks.insert(cp_block.height, hash);
|
||||
hash
|
||||
}
|
||||
};
|
||||
if hash == cp_block.hash {
|
||||
agreement_cp = Some(cp);
|
||||
break;
|
||||
}
|
||||
}
|
||||
agreement_cp
|
||||
};
|
||||
|
||||
let agreement_height = agreement_cp.as_ref().map(CheckPoint::height);
|
||||
|
||||
let new_tip = new_blocks
|
||||
.into_iter()
|
||||
// Prune `new_blocks` to only include blocks that are actually new.
|
||||
.filter(|(height, _)| Some(*height) > agreement_height)
|
||||
.map(|(height, hash)| BlockId { height, hash })
|
||||
.fold(agreement_cp, |prev_cp, block| {
|
||||
Some(match prev_cp {
|
||||
Some(cp) => cp.push(block).expect("must extend checkpoint"),
|
||||
None => CheckPoint::new(block),
|
||||
})
|
||||
})
|
||||
.expect("must have at least one checkpoint");
|
||||
|
||||
Ok((new_tip, agreement_height))
|
||||
}
|
||||
|
||||
/// A [tx status] comprises of a concatenation of `tx_hash:height:`s. We transform a single one of
|
||||
/// these concatenations into a [`ConfirmationHeightAnchor`] if possible.
|
||||
///
|
||||
/// We use the lowest possible checkpoint as the anchor block (from `cps`). If an anchor block
|
||||
/// cannot be found, or the transaction is unconfirmed, [`None`] is returned.
|
||||
///
|
||||
/// [tx status](https://electrumx-spesmilo.readthedocs.io/en/latest/protocol-basics.html#status)
|
||||
fn determine_tx_anchor(
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
raw_height: i32,
|
||||
txid: Txid,
|
||||
) -> Option<ConfirmationHeightAnchor> {
|
||||
// The electrum API has a weird quirk where an unconfirmed transaction is presented with a
|
||||
// height of 0. To avoid invalid representation in our data structures, we manually set
|
||||
// transactions residing in the genesis block to have height 0, then interpret a height of 0 as
|
||||
// unconfirmed for all other transactions.
|
||||
if txid
|
||||
== Txid::from_str("4a5e1e4baab89f3a32518a88c31bc87f618f76673e2cc77ab2127b7afdeda33b")
|
||||
.expect("must deserialize genesis coinbase txid")
|
||||
{
|
||||
let anchor_block = cps.values().next()?.block_id();
|
||||
return Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: 0,
|
||||
});
|
||||
}
|
||||
match raw_height {
|
||||
h if h <= 0 => {
|
||||
debug_assert!(h == 0 || h == -1, "unexpected height ({}) from electrum", h);
|
||||
None
|
||||
}
|
||||
h => {
|
||||
let h = h as u32;
|
||||
let anchor_block = cps.range(h..).next().map(|(_, cp)| cp.block_id())?;
|
||||
if h > anchor_block.height {
|
||||
None
|
||||
} else {
|
||||
Some(ConfirmationHeightAnchor {
|
||||
anchor_block,
|
||||
confirmation_height: h,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn populate_with_outpoints(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
) -> Result<HashMap<Txid, Transaction>, Error> {
|
||||
let mut full_txs = HashMap::new();
|
||||
for outpoint in outpoints {
|
||||
let txid = outpoint.txid;
|
||||
let tx = client.transaction_get(&txid)?;
|
||||
debug_assert_eq!(tx.txid(), txid);
|
||||
let txout = match tx.output.get(outpoint.vout as usize) {
|
||||
Some(txout) => txout,
|
||||
None => continue,
|
||||
};
|
||||
// attempt to find the following transactions (alongside their chain positions), and
|
||||
// add to our sparsechain `update`:
|
||||
let mut has_residing = false; // tx in which the outpoint resides
|
||||
let mut has_spending = false; // tx that spends the outpoint
|
||||
for res in client.script_get_history(&txout.script_pubkey)? {
|
||||
if has_residing && has_spending {
|
||||
break;
|
||||
}
|
||||
|
||||
if res.tx_hash == txid {
|
||||
if has_residing {
|
||||
continue;
|
||||
}
|
||||
has_residing = true;
|
||||
full_txs.insert(res.tx_hash, tx.clone());
|
||||
} else {
|
||||
if has_spending {
|
||||
continue;
|
||||
}
|
||||
let res_tx = match full_txs.get(&res.tx_hash) {
|
||||
Some(tx) => tx,
|
||||
None => {
|
||||
let res_tx = client.transaction_get(&res.tx_hash)?;
|
||||
full_txs.insert(res.tx_hash, res_tx);
|
||||
full_txs.get(&res.tx_hash).expect("just inserted")
|
||||
}
|
||||
};
|
||||
has_spending = res_tx
|
||||
.input
|
||||
.iter()
|
||||
.any(|txin| txin.previous_output == outpoint);
|
||||
if !has_spending {
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
let anchor = determine_tx_anchor(cps, res.height, res.tx_hash);
|
||||
let tx_entry = relevant_txids.0.entry(res.tx_hash).or_default();
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(full_txs)
|
||||
}
|
||||
|
||||
fn populate_with_txids(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
) -> Result<(), Error> {
|
||||
for txid in txids {
|
||||
let tx = match client.transaction_get(&txid) {
|
||||
Ok(tx) => tx,
|
||||
Err(electrum_client::Error::Protocol(_)) => continue,
|
||||
Err(other_err) => return Err(other_err),
|
||||
};
|
||||
|
||||
let spk = tx
|
||||
.output
|
||||
.first()
|
||||
.map(|txo| &txo.script_pubkey)
|
||||
.expect("tx must have an output");
|
||||
|
||||
let anchor = match client
|
||||
.script_get_history(spk)?
|
||||
.into_iter()
|
||||
.find(|r| r.tx_hash == txid)
|
||||
{
|
||||
Some(r) => determine_tx_anchor(cps, r.height, txid),
|
||||
None => continue,
|
||||
};
|
||||
|
||||
let tx_entry = relevant_txids.0.entry(txid).or_default();
|
||||
if let Some(anchor) = anchor {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn populate_with_spks<I: Ord + Clone>(
|
||||
client: &impl ElectrumApi,
|
||||
cps: &BTreeMap<u32, CheckPoint>,
|
||||
relevant_txids: &mut RelevantTxids,
|
||||
spks: &mut impl Iterator<Item = (I, ScriptBuf)>,
|
||||
stop_gap: usize,
|
||||
batch_size: usize,
|
||||
) -> Result<BTreeMap<I, (ScriptBuf, bool)>, Error> {
|
||||
let mut unused_spk_count = 0_usize;
|
||||
let mut scanned_spks = BTreeMap::new();
|
||||
|
||||
loop {
|
||||
let spks = (0..batch_size)
|
||||
.map_while(|_| spks.next())
|
||||
.collect::<Vec<_>>();
|
||||
if spks.is_empty() {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
|
||||
let spk_histories =
|
||||
client.batch_script_get_history(spks.iter().map(|(_, s)| s.as_script()))?;
|
||||
|
||||
for ((spk_index, spk), spk_history) in spks.into_iter().zip(spk_histories) {
|
||||
if spk_history.is_empty() {
|
||||
scanned_spks.insert(spk_index, (spk, false));
|
||||
unused_spk_count += 1;
|
||||
if unused_spk_count > stop_gap {
|
||||
return Ok(scanned_spks);
|
||||
}
|
||||
continue;
|
||||
} else {
|
||||
scanned_spks.insert(spk_index, (spk, true));
|
||||
unused_spk_count = 0;
|
||||
}
|
||||
|
||||
for tx in spk_history {
|
||||
let tx_entry = relevant_txids.0.entry(tx.tx_hash).or_default();
|
||||
if let Some(anchor) = determine_tx_anchor(cps, tx.height, tx.tx_hash) {
|
||||
tx_entry.insert(anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
30
crates/electrum/src/lib.rs
Normal file
30
crates/electrum/src/lib.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
//! This crate is used for updating structures of [`bdk_chain`] with data from an Electrum server.
|
||||
//!
|
||||
//! The two primary methods are [`ElectrumExt::sync`] and [`ElectrumExt::full_scan`]. In most cases
|
||||
//! [`ElectrumExt::sync`] is used to sync the transaction histories of scripts that the application
|
||||
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
|
||||
//! has shown a user. [`ElectrumExt::full_scan`] is meant to be used when importing or restoring a
|
||||
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
|
||||
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
|
||||
//! sync or full scan the user receives relevant blockchain data and output updates for
|
||||
//! [`bdk_chain`] including [`RelevantTxids`].
|
||||
//!
|
||||
//! The [`RelevantTxids`] only includes `txid`s and not full transactions. The caller is responsible
|
||||
//! for obtaining full transactions before applying new data to their [`bdk_chain`]. This can be
|
||||
//! done with these steps:
|
||||
//!
|
||||
//! 1. Determine which full transactions are missing. Use [`RelevantTxids::missing_full_txs`].
|
||||
//!
|
||||
//! 2. Obtaining the full transactions. To do this via electrum use [`ElectrumApi::batch_transaction_get`].
|
||||
//!
|
||||
//! Refer to [`example_electrum`] for a complete example.
|
||||
//!
|
||||
//! [`ElectrumApi::batch_transaction_get`]: electrum_client::ElectrumApi::batch_transaction_get
|
||||
//! [`example_electrum`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_electrum
|
||||
|
||||
#![warn(missing_docs)]
|
||||
|
||||
mod electrum_ext;
|
||||
pub use bdk_chain;
|
||||
pub use electrum_client;
|
||||
pub use electrum_ext::*;
|
||||
34
crates/esplora/Cargo.toml
Normal file
34
crates/esplora/Cargo.toml
Normal file
@@ -0,0 +1,34 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.9.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_esplora"
|
||||
description = "Fetch data from esplora in the form that accepts"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", default-features = false }
|
||||
esplora-client = { version = "0.6.0", default-features = false }
|
||||
async-trait = { version = "0.1.66", optional = true }
|
||||
futures = { version = "0.3.26", optional = true }
|
||||
|
||||
# use these dependencies if you need to enable their /no-std features
|
||||
bitcoin = { version = "0.30.0", optional = true, default-features = false }
|
||||
miniscript = { version = "10.0.0", optional = true, default-features = false }
|
||||
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
|
||||
electrsd = { version= "0.25.0", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "macros"] }
|
||||
|
||||
[features]
|
||||
default = ["std", "async-https", "blocking"]
|
||||
std = ["bdk_chain/std"]
|
||||
async = ["async-trait", "futures", "esplora-client/async"]
|
||||
async-https = ["async", "esplora-client/async-https"]
|
||||
async-https-rustls = ["async", "esplora-client/async-https-rustls"]
|
||||
blocking = ["esplora-client/blocking"]
|
||||
36
crates/esplora/README.md
Normal file
36
crates/esplora/README.md
Normal file
@@ -0,0 +1,36 @@
|
||||
# BDK Esplora
|
||||
|
||||
BDK Esplora extends [`esplora-client`] to update [`bdk_chain`] structures
|
||||
from an Esplora server.
|
||||
|
||||
## Usage
|
||||
|
||||
There are two versions of the extension trait (blocking and async).
|
||||
|
||||
For blocking-only:
|
||||
```toml
|
||||
bdk_esplora = { version = "0.3", features = ["blocking"] }
|
||||
```
|
||||
|
||||
For async-only:
|
||||
```toml
|
||||
bdk_esplora = { version = "0.3", features = ["async"] }
|
||||
```
|
||||
|
||||
For async-only (with https):
|
||||
```toml
|
||||
bdk_esplora = { version = "0.3", features = ["async-https"] }
|
||||
```
|
||||
|
||||
To use the extension traits:
|
||||
```rust
|
||||
// for blocking
|
||||
use bdk_esplora::EsploraExt;
|
||||
// for async
|
||||
// use bdk_esplora::EsploraAsyncExt;
|
||||
```
|
||||
|
||||
For full examples, refer to [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking) and [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async).
|
||||
|
||||
[`esplora-client`]: https://docs.rs/esplora-client/
|
||||
[`bdk_chain`]: https://docs.rs/bdk-chain/
|
||||
320
crates/esplora/src/async_ext.rs
Normal file
320
crates/esplora/src/async_ext.rs
Normal file
@@ -0,0 +1,320 @@
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
collections::BTreeMap,
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::TxStatus;
|
||||
use futures::{stream::FuturesOrdered, TryStreamExt};
|
||||
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::AsyncClient`].
|
||||
///
|
||||
/// Refer to [crate-level documentation] for more.
|
||||
///
|
||||
/// [crate-level documentation]: crate
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
pub trait EsploraAsyncExt {
|
||||
/// Prepare a [`LocalChain`] update with blocks fetched from Esplora.
|
||||
///
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
///
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## Consistency
|
||||
///
|
||||
/// The chain update returned is guaranteed to be consistent as long as there is not a *large* re-org
|
||||
/// during the call. The size of re-org we can tollerate is server dependent but will be at
|
||||
/// least 10.
|
||||
///
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
|
||||
/// returns a [`TxGraph`] and a map of last active indices.
|
||||
///
|
||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||
/// specified and return a [`TxGraph`].
|
||||
///
|
||||
/// * `misc_spks`: scripts that we want to sync transactions for
|
||||
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
|
||||
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraAsyncExt::full_scan
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
|
||||
}
|
||||
|
||||
#[cfg_attr(target_arch = "wasm32", async_trait(?Send))]
|
||||
#[cfg_attr(not(target_arch = "wasm32"), async_trait)]
|
||||
impl EsploraAsyncExt for esplora_client::AsyncClient {
|
||||
async fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<IntoIter = impl Iterator<Item = u32> + Send> + Send,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
// Fetch latest N (server dependent) blocks from Esplora. The server guarantees these are
|
||||
// consistent.
|
||||
let mut fetched_blocks = self
|
||||
.get_blocks(None)
|
||||
.await?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let new_tip_height = fetched_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.copied()
|
||||
.expect("must have atleast one block");
|
||||
|
||||
// Fetch blocks of heights that the caller is interested in, skipping blocks that are
|
||||
// already fetched when constructing `fetched_blocks`.
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
// ❗The return value of `get_block_hash` is not strictly guaranteed to be consistent
|
||||
// with the chain at the time of `get_blocks` above (there could have been a deep
|
||||
// re-org). Since `get_blocks` returns 10 (or so) blocks we are assuming that it's
|
||||
// not possible to have a re-org deeper than that.
|
||||
entry.insert(self.get_block_hash(height).await?);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `fetched_blocks` can create an update that connects with the original chain by
|
||||
// finding a "Point of Agreement".
|
||||
for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) {
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fetched_hash = match fetched_blocks.entry(height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => {
|
||||
*entry.insert(self.get_block_hash(height).await?)
|
||||
}
|
||||
};
|
||||
|
||||
// We have found point of agreement so the update will connect!
|
||||
if fetched_hash == local_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from))
|
||||
.expect("must be in height order"),
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
|
||||
async fn full_scan<K: Ord + Clone + Send>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<
|
||||
K,
|
||||
impl IntoIterator<IntoIter = impl Iterator<Item = (u32, ScriptBuf)> + Send> + Send,
|
||||
>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
loop {
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.map(|(spk_index, spk)| {
|
||||
let client = self.clone();
|
||||
async move {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen).await?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Result::<_, Error>::Ok((spk_index, spk_txs));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (index, txs) in handles.try_collect::<Vec<TxsOfSpkIndex>>().await? {
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let past_gap_limit = if let Some(i) = last_active_index {
|
||||
last_index > i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index >= stop_gap as u32
|
||||
};
|
||||
if past_gap_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((graph, last_active_indexes))
|
||||
}
|
||||
|
||||
async fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<IntoIter = impl Iterator<Item = ScriptBuf> + Send> + Send,
|
||||
txids: impl IntoIterator<IntoIter = impl Iterator<Item = Txid> + Send> + Send,
|
||||
outpoints: impl IntoIterator<IntoIter = impl Iterator<Item = OutPoint> + Send> + Send,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let mut graph = self
|
||||
.full_scan(
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.await
|
||||
.map(|(g, _)| g)?;
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
let client = self.clone();
|
||||
async move { client.get_tx_status(&txid).await.map(|s| (txid, s)) }
|
||||
})
|
||||
.collect::<FuturesOrdered<_>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for (txid, status) in handles.try_collect::<Vec<(Txid, TxStatus)>>().await? {
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints.into_iter() {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&op.txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&op.txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _).await? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&txid).await? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&txid).await?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
}
|
||||
317
crates/esplora/src/blocking_ext.rs
Normal file
317
crates/esplora/src/blocking_ext.rs
Normal file
@@ -0,0 +1,317 @@
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::{
|
||||
bitcoin::{BlockHash, OutPoint, ScriptBuf, TxOut, Txid},
|
||||
local_chain::{self, CheckPoint},
|
||||
BlockId, ConfirmationTimeHeightAnchor, TxGraph,
|
||||
};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
use crate::anchor_from_status;
|
||||
|
||||
/// [`esplora_client::Error`]
|
||||
type Error = Box<esplora_client::Error>;
|
||||
|
||||
/// Trait to extend the functionality of [`esplora_client::BlockingClient`].
|
||||
///
|
||||
/// Refer to [crate-level documentation] for more.
|
||||
///
|
||||
/// [crate-level documentation]: crate
|
||||
pub trait EsploraExt {
|
||||
/// Prepare a [`LocalChain`] update with blocks fetched from Esplora.
|
||||
///
|
||||
/// * `local_tip` is the previous tip of [`LocalChain::tip`].
|
||||
/// * `request_heights` is the block heights that we are interested in fetching from Esplora.
|
||||
///
|
||||
/// The result of this method can be applied to [`LocalChain::apply_update`].
|
||||
///
|
||||
/// ## Consistency
|
||||
///
|
||||
/// The chain update returned is guaranteed to be consistent as long as there is not a *large* re-org
|
||||
/// during the call. The size of re-org we can tollerate is server dependent but will be at
|
||||
/// least 10.
|
||||
///
|
||||
/// [`LocalChain`]: bdk_chain::local_chain::LocalChain
|
||||
/// [`LocalChain::tip`]: bdk_chain::local_chain::LocalChain::tip
|
||||
/// [`LocalChain::apply_update`]: bdk_chain::local_chain::LocalChain::apply_update
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<Item = u32>,
|
||||
) -> Result<local_chain::Update, Error>;
|
||||
|
||||
/// Full scan the keychain scripts specified with the blockchain (via an Esplora client) and
|
||||
/// returns a [`TxGraph`] and a map of last active indices.
|
||||
///
|
||||
/// * `keychain_spks`: keychains that we want to scan transactions for
|
||||
///
|
||||
/// The full scan for each keychain stops after a gap of `stop_gap` script pubkeys with no associated
|
||||
/// transactions. `parallel_requests` specifies the max number of HTTP requests to make in
|
||||
/// parallel.
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error>;
|
||||
|
||||
/// Sync a set of scripts with the blockchain (via an Esplora client) for the data
|
||||
/// specified and return a [`TxGraph`].
|
||||
///
|
||||
/// * `misc_spks`: scripts that we want to sync transactions for
|
||||
/// * `txids`: transactions for which we want updated [`ConfirmationTimeHeightAnchor`]s
|
||||
/// * `outpoints`: transactions associated with these outpoints (residing, spending) that we
|
||||
/// want to include in the update
|
||||
///
|
||||
/// If the scripts to sync are unknown, such as when restoring or importing a keychain that
|
||||
/// may include scripts that have been used, use [`full_scan`] with the keychain.
|
||||
///
|
||||
/// [`full_scan`]: EsploraExt::full_scan
|
||||
fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error>;
|
||||
}
|
||||
|
||||
impl EsploraExt for esplora_client::BlockingClient {
|
||||
fn update_local_chain(
|
||||
&self,
|
||||
local_tip: CheckPoint,
|
||||
request_heights: impl IntoIterator<Item = u32>,
|
||||
) -> Result<local_chain::Update, Error> {
|
||||
// Fetch latest N (server dependent) blocks from Esplora. The server guarantees these are
|
||||
// consistent.
|
||||
let mut fetched_blocks = self
|
||||
.get_blocks(None)?
|
||||
.into_iter()
|
||||
.map(|b| (b.time.height, b.id))
|
||||
.collect::<BTreeMap<u32, BlockHash>>();
|
||||
let new_tip_height = fetched_blocks
|
||||
.keys()
|
||||
.last()
|
||||
.copied()
|
||||
.expect("must atleast have one block");
|
||||
|
||||
// Fetch blocks of heights that the caller is interested in, skipping blocks that are
|
||||
// already fetched when constructing `fetched_blocks`.
|
||||
for height in request_heights {
|
||||
// do not fetch blocks higher than remote tip
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
// only fetch what is missing
|
||||
if let btree_map::Entry::Vacant(entry) = fetched_blocks.entry(height) {
|
||||
// ❗The return value of `get_block_hash` is not strictly guaranteed to be consistent
|
||||
// with the chain at the time of `get_blocks` above (there could have been a deep
|
||||
// re-org). Since `get_blocks` returns 10 (or so) blocks we are assuming that it's
|
||||
// not possible to have a re-org deeper than that.
|
||||
entry.insert(self.get_block_hash(height)?);
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure `fetched_blocks` can create an update that connects with the original chain by
|
||||
// finding a "Point of Agreement".
|
||||
for (height, local_hash) in local_tip.iter().map(|cp| (cp.height(), cp.hash())) {
|
||||
if height > new_tip_height {
|
||||
continue;
|
||||
}
|
||||
|
||||
let fetched_hash = match fetched_blocks.entry(height) {
|
||||
btree_map::Entry::Occupied(entry) => *entry.get(),
|
||||
btree_map::Entry::Vacant(entry) => *entry.insert(self.get_block_hash(height)?),
|
||||
};
|
||||
|
||||
// We have found point of agreement so the update will connect!
|
||||
if fetched_hash == local_hash {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(local_chain::Update {
|
||||
tip: CheckPoint::from_block_ids(fetched_blocks.into_iter().map(BlockId::from))
|
||||
.expect("must be in height order"),
|
||||
introduce_older_blocks: true,
|
||||
})
|
||||
}
|
||||
|
||||
fn full_scan<K: Ord + Clone>(
|
||||
&self,
|
||||
keychain_spks: BTreeMap<K, impl IntoIterator<Item = (u32, ScriptBuf)>>,
|
||||
stop_gap: usize,
|
||||
parallel_requests: usize,
|
||||
) -> Result<(TxGraph<ConfirmationTimeHeightAnchor>, BTreeMap<K, u32>), Error> {
|
||||
type TxsOfSpkIndex = (u32, Vec<esplora_client::Tx>);
|
||||
let parallel_requests = Ord::max(parallel_requests, 1);
|
||||
let mut graph = TxGraph::<ConfirmationTimeHeightAnchor>::default();
|
||||
let mut last_active_indexes = BTreeMap::<K, u32>::new();
|
||||
|
||||
for (keychain, spks) in keychain_spks {
|
||||
let mut spks = spks.into_iter();
|
||||
let mut last_index = Option::<u32>::None;
|
||||
let mut last_active_index = Option::<u32>::None;
|
||||
|
||||
loop {
|
||||
let handles = spks
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.map(|(spk_index, spk)| {
|
||||
std::thread::spawn({
|
||||
let client = self.clone();
|
||||
move || -> Result<TxsOfSpkIndex, Error> {
|
||||
let mut last_seen = None;
|
||||
let mut spk_txs = Vec::new();
|
||||
loop {
|
||||
let txs = client.scripthash_txs(&spk, last_seen)?;
|
||||
let tx_count = txs.len();
|
||||
last_seen = txs.last().map(|tx| tx.txid);
|
||||
spk_txs.extend(txs);
|
||||
if tx_count < 25 {
|
||||
break Ok((spk_index, spk_txs));
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<JoinHandle<Result<TxsOfSpkIndex, Error>>>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let (index, txs) = handle.join().expect("thread must not panic")?;
|
||||
last_index = Some(index);
|
||||
if !txs.is_empty() {
|
||||
last_active_index = Some(index);
|
||||
}
|
||||
for tx in txs {
|
||||
let _ = graph.insert_tx(tx.to_tx());
|
||||
if let Some(anchor) = anchor_from_status(&tx.status) {
|
||||
let _ = graph.insert_anchor(tx.txid, anchor);
|
||||
}
|
||||
|
||||
let previous_outputs = tx.vin.iter().filter_map(|vin| {
|
||||
let prevout = vin.prevout.as_ref()?;
|
||||
Some((
|
||||
OutPoint {
|
||||
txid: vin.txid,
|
||||
vout: vin.vout,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: prevout.scriptpubkey.clone(),
|
||||
value: prevout.value,
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
for (outpoint, txout) in previous_outputs {
|
||||
let _ = graph.insert_txout(outpoint, txout);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let last_index = last_index.expect("Must be set since handles wasn't empty.");
|
||||
let past_gap_limit = if let Some(i) = last_active_index {
|
||||
last_index > i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index >= stop_gap as u32
|
||||
};
|
||||
if past_gap_limit {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(last_active_index) = last_active_index {
|
||||
last_active_indexes.insert(keychain, last_active_index);
|
||||
}
|
||||
}
|
||||
|
||||
Ok((graph, last_active_indexes))
|
||||
}
|
||||
|
||||
fn sync(
|
||||
&self,
|
||||
misc_spks: impl IntoIterator<Item = ScriptBuf>,
|
||||
txids: impl IntoIterator<Item = Txid>,
|
||||
outpoints: impl IntoIterator<Item = OutPoint>,
|
||||
parallel_requests: usize,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let mut graph = self
|
||||
.full_scan(
|
||||
[(
|
||||
(),
|
||||
misc_spks
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(i, spk)| (i as u32, spk)),
|
||||
)]
|
||||
.into(),
|
||||
usize::MAX,
|
||||
parallel_requests,
|
||||
)
|
||||
.map(|(g, _)| g)?;
|
||||
|
||||
let mut txids = txids.into_iter();
|
||||
loop {
|
||||
let handles = txids
|
||||
.by_ref()
|
||||
.take(parallel_requests)
|
||||
.filter(|&txid| graph.get_tx(txid).is_none())
|
||||
.map(|txid| {
|
||||
std::thread::spawn({
|
||||
let client = self.clone();
|
||||
move || {
|
||||
client
|
||||
.get_tx_status(&txid)
|
||||
.map_err(Box::new)
|
||||
.map(|s| (txid, s))
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect::<Vec<JoinHandle<Result<(Txid, TxStatus), Error>>>>();
|
||||
|
||||
if handles.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
for handle in handles {
|
||||
let (txid, status) = handle.join().expect("thread must not panic")?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for op in outpoints {
|
||||
if graph.get_tx(op.txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&op.txid)? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&op.txid)?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(op.txid, anchor);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(op_status) = self.get_output_status(&op.txid, op.vout as _)? {
|
||||
if let Some(txid) = op_status.txid {
|
||||
if graph.get_tx(txid).is_none() {
|
||||
if let Some(tx) = self.get_tx(&txid)? {
|
||||
let _ = graph.insert_tx(tx);
|
||||
}
|
||||
let status = self.get_tx_status(&txid)?;
|
||||
if let Some(anchor) = anchor_from_status(&status) {
|
||||
let _ = graph.insert_anchor(txid, anchor);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(graph)
|
||||
}
|
||||
}
|
||||
50
crates/esplora/src/lib.rs
Normal file
50
crates/esplora/src/lib.rs
Normal file
@@ -0,0 +1,50 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
|
||||
//! This crate is used for updating structures of [`bdk_chain`] with data from an Esplora server.
|
||||
//!
|
||||
//! The two primary methods are [`EsploraExt::sync`] and [`EsploraExt::full_scan`]. In most cases
|
||||
//! [`EsploraExt::sync`] is used to sync the transaction histories of scripts that the application
|
||||
//! cares about, for example the scripts for all the receive addresses of a Wallet's keychain that it
|
||||
//! has shown a user. [`EsploraExt::full_scan`] is meant to be used when importing or restoring a
|
||||
//! keychain where the range of possibly used scripts is not known. In this case it is necessary to
|
||||
//! scan all keychain scripts until a number (the "stop gap") of unused scripts is discovered. For a
|
||||
//! sync or full scan the user receives relevant blockchain data and output updates for [`bdk_chain`]
|
||||
//! via a new [`TxGraph`] to be appended to any existing [`TxGraph`] data.
|
||||
//!
|
||||
//! Refer to [`example_esplora`] for a complete example.
|
||||
//!
|
||||
//! [`TxGraph`]: bdk_chain::tx_graph::TxGraph
|
||||
//! [`example_esplora`]: https://github.com/bitcoindevkit/bdk/tree/master/example-crates/example_esplora
|
||||
|
||||
use bdk_chain::{BlockId, ConfirmationTimeHeightAnchor};
|
||||
use esplora_client::TxStatus;
|
||||
|
||||
pub use esplora_client;
|
||||
|
||||
#[cfg(feature = "blocking")]
|
||||
mod blocking_ext;
|
||||
#[cfg(feature = "blocking")]
|
||||
pub use blocking_ext::*;
|
||||
|
||||
#[cfg(feature = "async")]
|
||||
mod async_ext;
|
||||
#[cfg(feature = "async")]
|
||||
pub use async_ext::*;
|
||||
|
||||
fn anchor_from_status(status: &TxStatus) -> Option<ConfirmationTimeHeightAnchor> {
|
||||
if let TxStatus {
|
||||
block_height: Some(height),
|
||||
block_hash: Some(hash),
|
||||
block_time: Some(time),
|
||||
..
|
||||
} = status.clone()
|
||||
{
|
||||
Some(ConfirmationTimeHeightAnchor {
|
||||
anchor_block: BlockId { height, hash },
|
||||
confirmation_height: height,
|
||||
confirmation_time: time,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
228
crates/esplora/tests/async_ext.rs
Normal file
228
crates/esplora/tests/async_ext.rs
Normal file
@@ -0,0 +1,228 @@
|
||||
use bdk_esplora::EsploraAsyncExt;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, AsyncClient, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: AsyncClient,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Result<Self, anyhow::Error> {
|
||||
let bitcoind_exe =
|
||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
Ok(Self {
|
||||
bitcoind,
|
||||
electrsd,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
|
||||
|
||||
let misc_spks = [
|
||||
receive_address0.script_pubkey(),
|
||||
receive_address1.script_pubkey(),
|
||||
];
|
||||
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
let txid1 = env.bitcoind.client.send_to_address(
|
||||
&receive_address1,
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let txid2 = env.bitcoind.client.send_to_address(
|
||||
&receive_address0,
|
||||
Amount::from_sat(20000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = env
|
||||
.client
|
||||
.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
expected_txids.sort();
|
||||
assert_eq!(graph_update_txids, expected_txids);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the bounds of the address scan depending on the gap limit.
|
||||
#[tokio::test]
|
||||
pub async fn test_async_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
let addresses = [
|
||||
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
|
||||
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
|
||||
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
|
||||
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
|
||||
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
|
||||
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
|
||||
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
|
||||
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
|
||||
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
|
||||
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
|
||||
];
|
||||
let addresses: Vec<_> = addresses
|
||||
.into_iter()
|
||||
.map(|s| Address::from_str(s).unwrap().assume_checked())
|
||||
.collect();
|
||||
let spks: Vec<_> = addresses
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[3],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||
// will.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1).await?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1).await?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[addresses.len() - 1],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().await.unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1).await?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
433
crates/esplora/tests/blocking_ext.rs
Normal file
433
crates/esplora/tests/blocking_ext.rs
Normal file
@@ -0,0 +1,433 @@
|
||||
use bdk_chain::local_chain::LocalChain;
|
||||
use bdk_chain::BlockId;
|
||||
use bdk_esplora::EsploraExt;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use electrsd::bitcoind::{self, anyhow, BitcoinD};
|
||||
use electrsd::{Conf, ElectrsD};
|
||||
use esplora_client::{self, BlockingClient, Builder};
|
||||
use std::collections::{BTreeMap, BTreeSet, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, BlockHash, Txid};
|
||||
|
||||
macro_rules! h {
|
||||
($index:literal) => {{
|
||||
bdk_chain::bitcoin::hashes::Hash::hash($index.as_bytes())
|
||||
}};
|
||||
}
|
||||
|
||||
macro_rules! local_chain {
|
||||
[ $(($height:expr, $block_hash:expr)), * ] => {{
|
||||
#[allow(unused_mut)]
|
||||
bdk_chain::local_chain::LocalChain::from_blocks([$(($height, $block_hash).into()),*].into_iter().collect())
|
||||
.expect("chain must have genesis block")
|
||||
}};
|
||||
}
|
||||
|
||||
struct TestEnv {
|
||||
bitcoind: BitcoinD,
|
||||
#[allow(dead_code)]
|
||||
electrsd: ElectrsD,
|
||||
client: BlockingClient,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
fn new() -> Result<Self, anyhow::Error> {
|
||||
let bitcoind_exe =
|
||||
bitcoind::downloaded_exe_path().expect("bitcoind version feature must be enabled");
|
||||
let bitcoind = BitcoinD::new(bitcoind_exe).unwrap();
|
||||
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
|
||||
Ok(Self {
|
||||
bitcoind,
|
||||
electrsd,
|
||||
client,
|
||||
})
|
||||
}
|
||||
|
||||
fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||
let mut electrs_conf = Conf::default();
|
||||
electrs_conf.http_enabled = true;
|
||||
let electrs_exe =
|
||||
electrsd::downloaded_exe_path().expect("electrs version feature must be enabled");
|
||||
let electrsd = ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrs_conf)?;
|
||||
|
||||
let base_url = format!("http://{}", &electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking()?;
|
||||
self.electrsd = electrsd;
|
||||
self.client = client;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
fn mine_blocks(
|
||||
&self,
|
||||
count: usize,
|
||||
address: Option<Address>,
|
||||
) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let coinbase_address = match address {
|
||||
Some(address) => address,
|
||||
None => self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked(),
|
||||
};
|
||||
let block_hashes = self
|
||||
.bitcoind
|
||||
.client
|
||||
.generate_to_address(count as _, &coinbase_address)?;
|
||||
Ok(block_hashes)
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let receive_address0 =
|
||||
Address::from_str("bcrt1qc6fweuf4xjvz4x3gx3t9e0fh4hvqyu2qw4wvxm")?.assume_checked();
|
||||
let receive_address1 =
|
||||
Address::from_str("bcrt1qfjg5lv3dvc9az8patec8fjddrs4aqtauadnagr")?.assume_checked();
|
||||
|
||||
let misc_spks = [
|
||||
receive_address0.script_pubkey(),
|
||||
receive_address1.script_pubkey(),
|
||||
];
|
||||
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
let txid1 = env.bitcoind.client.send_to_address(
|
||||
&receive_address1,
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let txid2 = env.bitcoind.client.send_to_address(
|
||||
&receive_address0,
|
||||
Amount::from_sat(20000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = env.client.sync(
|
||||
misc_spks.into_iter(),
|
||||
vec![].into_iter(),
|
||||
vec![].into_iter(),
|
||||
1,
|
||||
)?;
|
||||
|
||||
// Check to see if we have the floating txouts available from our two created transactions'
|
||||
// previous outputs in order to calculate transaction fees.
|
||||
for tx in graph_update.full_txs() {
|
||||
// Retrieve the calculated fee from `TxGraph`, which will panic if we do not have the
|
||||
// floating txouts available from the transactions' previous outputs.
|
||||
let fee = graph_update.calculate_fee(tx.tx).expect("Fee must exist");
|
||||
|
||||
// Retrieve the fee in the transaction data from `bitcoind`.
|
||||
let tx_fee = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_transaction(&tx.txid, None)
|
||||
.expect("Tx must exist")
|
||||
.fee
|
||||
.expect("Fee must exist")
|
||||
.abs()
|
||||
.to_sat() as u64;
|
||||
|
||||
// Check that the calculated fee matches the fee from the transaction data.
|
||||
assert_eq!(fee, tx_fee);
|
||||
}
|
||||
|
||||
let mut graph_update_txids: Vec<Txid> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
graph_update_txids.sort();
|
||||
let mut expected_txids = vec![txid1, txid2];
|
||||
expected_txids.sort();
|
||||
assert_eq!(graph_update_txids, expected_txids);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Test the bounds of the address scan depending on the gap limit.
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_gap_limit() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let _block_hashes = env.mine_blocks(101, None)?;
|
||||
|
||||
// Now let's test the gap limit. First of all get a chain of 10 addresses.
|
||||
let addresses = [
|
||||
"bcrt1qj9f7r8r3p2y0sqf4r3r62qysmkuh0fzep473d2ar7rcz64wqvhssjgf0z4",
|
||||
"bcrt1qmm5t0ch7vh2hryx9ctq3mswexcugqe4atkpkl2tetm8merqkthas3w7q30",
|
||||
"bcrt1qut9p7ej7l7lhyvekj28xknn8gnugtym4d5qvnp5shrsr4nksmfqsmyn87g",
|
||||
"bcrt1qqz0xtn3m235p2k96f5wa2dqukg6shxn9n3txe8arlrhjh5p744hsd957ww",
|
||||
"bcrt1q9c0t62a8l6wfytmf2t9lfj35avadk3mm8g4p3l84tp6rl66m48sqrme7wu",
|
||||
"bcrt1qkmh8yrk2v47cklt8dytk8f3ammcwa4q7dzattedzfhqzvfwwgyzsg59zrh",
|
||||
"bcrt1qvgrsrzy07gjkkfr5luplt0azxtfwmwq5t62gum5jr7zwcvep2acs8hhnp2",
|
||||
"bcrt1qw57edarcg50ansq8mk3guyrk78rk0fwvrds5xvqeupteu848zayq549av8",
|
||||
"bcrt1qvtve5ekf6e5kzs68knvnt2phfw6a0yjqrlgat392m6zt9jsvyxhqfx67ef",
|
||||
"bcrt1qw03ddumfs9z0kcu76ln7jrjfdwam20qtffmkcral3qtza90sp9kqm787uk",
|
||||
];
|
||||
let addresses: Vec<_> = addresses
|
||||
.into_iter()
|
||||
.map(|s| Address::from_str(s).unwrap().assume_checked())
|
||||
.collect();
|
||||
let spks: Vec<_> = addresses
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, addr)| (i as u32, addr.script_pubkey()))
|
||||
.collect();
|
||||
let mut keychains = BTreeMap::new();
|
||||
keychains.insert(0, spks);
|
||||
|
||||
// Then receive coins on the 4th address.
|
||||
let txid_4th_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[3],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a gap limit of 2 won't find the transaction, but a scan with a gap limit of 3
|
||||
// will.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 2, 1)?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 3, 1)?;
|
||||
assert_eq!(graph_update.full_txs().next().unwrap().txid, txid_4th_addr);
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
|
||||
// Now receive a coin on the last address.
|
||||
let txid_last_addr = env.bitcoind.client.send_to_address(
|
||||
&addresses[addresses.len() - 1],
|
||||
Amount::from_sat(10000),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
Some(1),
|
||||
None,
|
||||
)?;
|
||||
let _block_hashes = env.mine_blocks(1, None)?;
|
||||
while env.client.get_height().unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 4 won't find the second transaction, but a scan with gap limit 5 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains.clone(), 4, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 1);
|
||||
assert!(txs.contains(&txid_4th_addr));
|
||||
assert_eq!(active_indices[&0], 3);
|
||||
let (graph_update, active_indices) = env.client.full_scan(keychains, 5, 1)?;
|
||||
let txs: HashSet<_> = graph_update.full_txs().map(|tx| tx.txid).collect();
|
||||
assert_eq!(txs.len(), 2);
|
||||
assert!(txs.contains(&txid_4th_addr) && txs.contains(&txid_last_addr));
|
||||
assert_eq!(active_indices[&0], 9);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn update_local_chain() -> anyhow::Result<()> {
|
||||
const TIP_HEIGHT: u32 = 50;
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let blocks = {
|
||||
let bitcoind_client = &env.bitcoind.client;
|
||||
assert_eq!(bitcoind_client.get_block_count()?, 1);
|
||||
[
|
||||
(0, bitcoind_client.get_block_hash(0)?),
|
||||
(1, bitcoind_client.get_block_hash(1)?),
|
||||
]
|
||||
.into_iter()
|
||||
.chain((2..).zip(env.mine_blocks((TIP_HEIGHT - 1) as usize, None)?))
|
||||
.collect::<BTreeMap<_, _>>()
|
||||
};
|
||||
// so new blocks can be seen by Electrs
|
||||
let env = env.reset_electrsd()?;
|
||||
|
||||
struct TestCase {
|
||||
name: &'static str,
|
||||
chain: LocalChain,
|
||||
request_heights: &'static [u32],
|
||||
exp_update_heights: &'static [u32],
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
name: "request_later_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (21, blocks[&21])],
|
||||
request_heights: &[22, 25, 28],
|
||||
exp_update_heights: &[21, 22, 25, 28],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (5, blocks[&5])],
|
||||
request_heights: &[4],
|
||||
exp_update_heights: &[4, 5],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_2",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (10, blocks[&10])],
|
||||
request_heights: &[4, 6],
|
||||
exp_update_heights: &[4, 6, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks",
|
||||
chain: local_chain![(0, blocks[&0]), (7, blocks[&7]), (11, blocks[&11])],
|
||||
request_heights: &[8, 9, 15],
|
||||
exp_update_heights: &[8, 9, 11, 15],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_tip_only",
|
||||
chain: local_chain![(0, blocks[&0]), (5, blocks[&5]), (49, blocks[&49])],
|
||||
request_heights: &[TIP_HEIGHT],
|
||||
exp_update_heights: &[49],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, blocks[&23])],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (13, blocks[&13]), (23, h!("23"))],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[13, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_nothing_during_reorg_2",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[],
|
||||
exp_update_heights: &[21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_prev_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(21, blocks[&21]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[17, 20],
|
||||
exp_update_heights: &[17, 20, 21, 22, 23],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg",
|
||||
chain: local_chain![
|
||||
(0, blocks[&0]),
|
||||
(9, blocks[&9]),
|
||||
(22, h!("22")),
|
||||
(23, h!("23"))
|
||||
],
|
||||
request_heights: &[25, 27],
|
||||
exp_update_heights: &[9, 22, 23, 25, 27],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_blocks_during_reorg_2",
|
||||
chain: local_chain![(0, blocks[&0]), (9, h!("9"))],
|
||||
request_heights: &[10],
|
||||
exp_update_heights: &[0, 9, 10],
|
||||
},
|
||||
TestCase {
|
||||
name: "request_later_and_prev_blocks_during_reorg",
|
||||
chain: local_chain![(0, blocks[&0]), (1, blocks[&1]), (9, h!("9"))],
|
||||
request_heights: &[8, 11],
|
||||
exp_update_heights: &[1, 8, 9, 11],
|
||||
},
|
||||
];
|
||||
|
||||
for (i, t) in test_cases.into_iter().enumerate() {
|
||||
println!("Case {}: {}", i, t.name);
|
||||
let mut chain = t.chain;
|
||||
|
||||
let update = env
|
||||
.client
|
||||
.update_local_chain(chain.tip(), t.request_heights.iter().copied())
|
||||
.map_err(|err| {
|
||||
anyhow::format_err!("[{}:{}] `update_local_chain` failed: {}", i, t.name, err)
|
||||
})?;
|
||||
|
||||
let update_blocks = update
|
||||
.tip
|
||||
.iter()
|
||||
.map(|cp| cp.block_id())
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
let exp_update_blocks = t
|
||||
.exp_update_heights
|
||||
.iter()
|
||||
.map(|&height| {
|
||||
let hash = blocks[&height];
|
||||
BlockId { height, hash }
|
||||
})
|
||||
.chain(
|
||||
// Electrs Esplora `get_block` call fetches 10 blocks which is included in the
|
||||
// update
|
||||
blocks
|
||||
.range(TIP_HEIGHT - 9..)
|
||||
.map(|(&height, &hash)| BlockId { height, hash }),
|
||||
)
|
||||
.collect::<BTreeSet<_>>();
|
||||
|
||||
assert_eq!(
|
||||
update_blocks, exp_update_blocks,
|
||||
"[{}:{}] unexpected update",
|
||||
i, t.name
|
||||
);
|
||||
|
||||
let _ = chain
|
||||
.apply_update(update)
|
||||
.unwrap_or_else(|err| panic!("[{}:{}] update failed to apply: {}", i, t.name, err));
|
||||
|
||||
// all requested heights must exist in the final chain
|
||||
for height in t.request_heights {
|
||||
let exp_blockhash = blocks.get(height).expect("block must exist in bitcoind");
|
||||
assert_eq!(
|
||||
chain.blocks().get(height),
|
||||
Some(exp_blockhash),
|
||||
"[{}:{}] block {}:{} must exist in final chain",
|
||||
i,
|
||||
t.name,
|
||||
height,
|
||||
exp_blockhash
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
19
crates/file_store/Cargo.toml
Normal file
19
crates/file_store/Cargo.toml
Normal file
@@ -0,0 +1,19 @@
|
||||
[package]
|
||||
name = "bdk_file_store"
|
||||
version = "0.7.0"
|
||||
edition = "2021"
|
||||
license = "MIT OR Apache-2.0"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_file_store"
|
||||
description = "A simple append-only flat file implementation of Persist for Bitcoin Dev Kit."
|
||||
keywords = ["bitcoin", "persist", "persistence", "bdk", "file"]
|
||||
authors = ["Bitcoin Dev Kit Developers"]
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../chain", version = "0.11.0", features = [ "serde", "miniscript" ] }
|
||||
bincode = { version = "1" }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[dev-dependencies]
|
||||
tempfile = "3"
|
||||
10
crates/file_store/README.md
Normal file
10
crates/file_store/README.md
Normal file
@@ -0,0 +1,10 @@
|
||||
# BDK File Store
|
||||
|
||||
This is a simple append-only flat file implementation of
|
||||
[`Persist`](`bdk_chain::Persist`).
|
||||
|
||||
The main structure is [`Store`](`crate::Store`), which can be used with [`bdk`]'s
|
||||
`Wallet` to persist wallet data into a flat file.
|
||||
|
||||
[`bdk`]: https://docs.rs/bdk/latest
|
||||
[`bdk_chain`]: https://docs.rs/bdk_chain/latest
|
||||
108
crates/file_store/src/entry_iter.rs
Normal file
108
crates/file_store/src/entry_iter.rs
Normal file
@@ -0,0 +1,108 @@
|
||||
use bincode::Options;
|
||||
use std::{
|
||||
fs::File,
|
||||
io::{self, BufReader, Seek},
|
||||
marker::PhantomData,
|
||||
};
|
||||
|
||||
use crate::bincode_options;
|
||||
|
||||
/// Iterator over entries in a file store.
|
||||
///
|
||||
/// Reads and returns an entry each time [`next`] is called. If an error occurs while reading the
|
||||
/// iterator will yield a `Result::Err(_)` instead and then `None` for the next call to `next`.
|
||||
///
|
||||
/// [`next`]: Self::next
|
||||
pub struct EntryIter<'t, T> {
|
||||
/// Buffered reader around the file
|
||||
db_file: BufReader<&'t mut File>,
|
||||
finished: bool,
|
||||
/// The file position for the first read of `db_file`.
|
||||
start_pos: Option<u64>,
|
||||
types: PhantomData<T>,
|
||||
}
|
||||
|
||||
impl<'t, T> EntryIter<'t, T> {
|
||||
pub fn new(start_pos: u64, db_file: &'t mut File) -> Self {
|
||||
Self {
|
||||
db_file: BufReader::new(db_file),
|
||||
start_pos: Some(start_pos),
|
||||
finished: false,
|
||||
types: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T> Iterator for EntryIter<'t, T>
|
||||
where
|
||||
T: serde::de::DeserializeOwned,
|
||||
{
|
||||
type Item = Result<T, IterError>;
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
if self.finished {
|
||||
return None;
|
||||
}
|
||||
(|| {
|
||||
if let Some(start) = self.start_pos.take() {
|
||||
self.db_file.seek(io::SeekFrom::Start(start))?;
|
||||
}
|
||||
|
||||
let pos_before_read = self.db_file.stream_position()?;
|
||||
match bincode_options().deserialize_from(&mut self.db_file) {
|
||||
Ok(changeset) => Ok(Some(changeset)),
|
||||
Err(e) => {
|
||||
self.finished = true;
|
||||
let pos_after_read = self.db_file.stream_position()?;
|
||||
// allow unexpected EOF if 0 bytes were read
|
||||
if let bincode::ErrorKind::Io(inner) = &*e {
|
||||
if inner.kind() == io::ErrorKind::UnexpectedEof
|
||||
&& pos_after_read == pos_before_read
|
||||
{
|
||||
return Ok(None);
|
||||
}
|
||||
}
|
||||
self.db_file.seek(io::SeekFrom::Start(pos_before_read))?;
|
||||
Err(IterError::Bincode(*e))
|
||||
}
|
||||
}
|
||||
})()
|
||||
.transpose()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'t, T> Drop for EntryIter<'t, T> {
|
||||
fn drop(&mut self) {
|
||||
// This syncs the underlying file's offset with the buffer's position. This way, we
|
||||
// maintain the correct position to start the next read/write.
|
||||
if let Ok(pos) = self.db_file.stream_position() {
|
||||
let _ = self.db_file.get_mut().seek(io::SeekFrom::Start(pos));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for [`EntryIter`].
|
||||
#[derive(Debug)]
|
||||
pub enum IterError {
|
||||
/// Failure to read from the file.
|
||||
Io(io::Error),
|
||||
/// Failure to decode data from the file.
|
||||
Bincode(bincode::ErrorKind),
|
||||
}
|
||||
|
||||
impl core::fmt::Display for IterError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
IterError::Io(e) => write!(f, "io error trying to read entry {}", e),
|
||||
IterError::Bincode(e) => write!(f, "bincode error while reading entry {}", e),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for IterError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
IterError::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for IterError {}
|
||||
42
crates/file_store/src/lib.rs
Normal file
42
crates/file_store/src/lib.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
#![doc = include_str!("../README.md")]
|
||||
mod entry_iter;
|
||||
mod store;
|
||||
use std::io;
|
||||
|
||||
use bincode::{DefaultOptions, Options};
|
||||
pub use entry_iter::*;
|
||||
pub use store::*;
|
||||
|
||||
pub(crate) fn bincode_options() -> impl bincode::Options {
|
||||
DefaultOptions::new().with_varint_encoding()
|
||||
}
|
||||
|
||||
/// Error that occurs due to problems encountered with the file.
|
||||
#[derive(Debug)]
|
||||
pub enum FileError {
|
||||
/// IO error, this may mean that the file is too short.
|
||||
Io(io::Error),
|
||||
/// Magic bytes do not match what is expected.
|
||||
InvalidMagicBytes { got: Vec<u8>, expected: Vec<u8> },
|
||||
}
|
||||
|
||||
impl core::fmt::Display for FileError {
|
||||
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
|
||||
match self {
|
||||
Self::Io(e) => write!(f, "io error trying to read file: {}", e),
|
||||
Self::InvalidMagicBytes { got, expected } => write!(
|
||||
f,
|
||||
"file has invalid magic bytes: expected={:?} got={:?}",
|
||||
expected, got,
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<io::Error> for FileError {
|
||||
fn from(value: io::Error) -> Self {
|
||||
Self::Io(value)
|
||||
}
|
||||
}
|
||||
|
||||
impl std::error::Error for FileError {}
|
||||
459
crates/file_store/src/store.rs
Normal file
459
crates/file_store/src/store.rs
Normal file
@@ -0,0 +1,459 @@
|
||||
use std::{
|
||||
fmt::Debug,
|
||||
fs::{File, OpenOptions},
|
||||
io::{self, Read, Seek, Write},
|
||||
marker::PhantomData,
|
||||
path::Path,
|
||||
};
|
||||
|
||||
use bdk_chain::{Append, PersistBackend};
|
||||
use bincode::Options;
|
||||
|
||||
use crate::{bincode_options, EntryIter, FileError, IterError};
|
||||
|
||||
/// Persists an append-only list of changesets (`C`) to a single file.
|
||||
///
|
||||
/// The changesets are the results of altering a tracker implementation (`T`).
|
||||
#[derive(Debug)]
|
||||
pub struct Store<C> {
|
||||
magic_len: usize,
|
||||
db_file: File,
|
||||
marker: PhantomData<C>,
|
||||
}
|
||||
|
||||
impl<C> PersistBackend<C> for Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
type WriteError = std::io::Error;
|
||||
|
||||
type LoadError = IterError;
|
||||
|
||||
fn write_changes(&mut self, changeset: &C) -> Result<(), Self::WriteError> {
|
||||
self.append_changeset(changeset)
|
||||
}
|
||||
|
||||
fn load_from_persistence(&mut self) -> Result<Option<C>, Self::LoadError> {
|
||||
self.aggregate_changesets().map_err(|e| e.iter_error)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C> Store<C>
|
||||
where
|
||||
C: Append + serde::Serialize + serde::de::DeserializeOwned,
|
||||
{
|
||||
/// Create a new [`Store`] file in write-only mode; error if the file exists.
|
||||
///
|
||||
/// `magic` is the prefixed bytes to write to the new file. This will be checked when opening
|
||||
/// the `Store` in the future with [`open`].
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
pub fn create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if file_path.as_ref().exists() {
|
||||
// `io::Error` is used instead of a variant on `FileError` because there is already a
|
||||
// nightly-only `File::create_new` method
|
||||
return Err(FileError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"file already exists",
|
||||
)));
|
||||
}
|
||||
let mut f = OpenOptions::new()
|
||||
.create(true)
|
||||
.read(true)
|
||||
.write(true)
|
||||
.open(file_path)?;
|
||||
f.write_all(magic)?;
|
||||
Ok(Self {
|
||||
magic_len: magic.len(),
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Open an existing [`Store`].
|
||||
///
|
||||
/// Use [`create_new`] to create a new `Store`.
|
||||
///
|
||||
/// # Errors
|
||||
///
|
||||
/// If the prefixed bytes of the opened file does not match the provided `magic`, the
|
||||
/// [`FileError::InvalidMagicBytes`] error variant will be returned.
|
||||
///
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
let mut f = OpenOptions::new().read(true).write(true).open(file_path)?;
|
||||
|
||||
let mut magic_buf = vec![0_u8; magic.len()];
|
||||
f.read_exact(&mut magic_buf)?;
|
||||
if magic_buf != magic {
|
||||
return Err(FileError::InvalidMagicBytes {
|
||||
got: magic_buf,
|
||||
expected: magic.to_vec(),
|
||||
});
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
magic_len: magic.len(),
|
||||
db_file: f,
|
||||
marker: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
/// Attempt to open existing [`Store`] file; create it if the file is non-existent.
|
||||
///
|
||||
/// Internally, this calls either [`open`] or [`create_new`].
|
||||
///
|
||||
/// [`open`]: Store::open
|
||||
/// [`create_new`]: Store::create_new
|
||||
pub fn open_or_create_new<P>(magic: &[u8], file_path: P) -> Result<Self, FileError>
|
||||
where
|
||||
P: AsRef<Path>,
|
||||
{
|
||||
if file_path.as_ref().exists() {
|
||||
Self::open(magic, file_path)
|
||||
} else {
|
||||
Self::create_new(magic, file_path)
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterates over the stored changeset from first to last, changing the seek position at each
|
||||
/// iteration.
|
||||
///
|
||||
/// The iterator may fail to read an entry and therefore return an error. However, the first time
|
||||
/// it returns an error will be the last. After doing so, the iterator will always yield `None`.
|
||||
///
|
||||
/// **WARNING**: This method changes the write position in the underlying file. You should
|
||||
/// always iterate over all entries until `None` is returned if you want your next write to go
|
||||
/// at the end; otherwise, you will write over existing entries.
|
||||
pub fn iter_changesets(&mut self) -> EntryIter<C> {
|
||||
EntryIter::new(self.magic_len as u64, &mut self.db_file)
|
||||
}
|
||||
|
||||
/// Loads all the changesets that have been stored as one giant changeset.
|
||||
///
|
||||
/// This function returns the aggregate changeset, or `None` if nothing was persisted.
|
||||
/// If reading or deserializing any of the entries fails, an error is returned that
|
||||
/// consists of all those it was able to read.
|
||||
///
|
||||
/// You should usually check the error. In many applications, it may make sense to do a full
|
||||
/// wallet scan with a stop-gap after getting an error, since it is likely that one of the
|
||||
/// changesets it was unable to read changed the derivation indices of the tracker.
|
||||
///
|
||||
/// **WARNING**: This method changes the write position of the underlying file. The next
|
||||
/// changeset will be written over the erroring entry (or the end of the file if none existed).
|
||||
pub fn aggregate_changesets(&mut self) -> Result<Option<C>, AggregateChangesetsError<C>> {
|
||||
let mut changeset = Option::<C>::None;
|
||||
for next_changeset in self.iter_changesets() {
|
||||
let next_changeset = match next_changeset {
|
||||
Ok(next_changeset) => next_changeset,
|
||||
Err(iter_error) => {
|
||||
return Err(AggregateChangesetsError {
|
||||
changeset,
|
||||
iter_error,
|
||||
})
|
||||
}
|
||||
};
|
||||
match &mut changeset {
|
||||
Some(changeset) => changeset.append(next_changeset),
|
||||
changeset => *changeset = Some(next_changeset),
|
||||
}
|
||||
}
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// Append a new changeset to the file and truncate the file to the end of the appended
|
||||
/// changeset.
|
||||
///
|
||||
/// The truncation is to avoid the possibility of having a valid but inconsistent changeset
|
||||
/// directly after the appended changeset.
|
||||
pub fn append_changeset(&mut self, changeset: &C) -> Result<(), io::Error> {
|
||||
// no need to write anything if changeset is empty
|
||||
if changeset.is_empty() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
bincode_options()
|
||||
.serialize_into(&mut self.db_file, changeset)
|
||||
.map_err(|e| match *e {
|
||||
bincode::ErrorKind::Io(inner) => inner,
|
||||
unexpected_err => panic!("unexpected bincode error: {}", unexpected_err),
|
||||
})?;
|
||||
|
||||
// truncate file after this changeset addition
|
||||
// if this is not done, data after this changeset may represent valid changesets, however
|
||||
// applying those changesets on top of this one may result in an inconsistent state
|
||||
let pos = self.db_file.stream_position()?;
|
||||
self.db_file.set_len(pos)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Error type for [`Store::aggregate_changesets`].
|
||||
#[derive(Debug)]
|
||||
pub struct AggregateChangesetsError<C> {
|
||||
/// The partially-aggregated changeset.
|
||||
pub changeset: Option<C>,
|
||||
|
||||
/// The error returned by [`EntryIter`].
|
||||
pub iter_error: IterError,
|
||||
}
|
||||
|
||||
impl<C> std::fmt::Display for AggregateChangesetsError<C> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
std::fmt::Display::fmt(&self.iter_error, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl<C: std::fmt::Debug> std::error::Error for AggregateChangesetsError<C> {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
|
||||
use bincode::DefaultOptions;
|
||||
use std::{
|
||||
collections::BTreeSet,
|
||||
io::{Read, Write},
|
||||
vec::Vec,
|
||||
};
|
||||
use tempfile::NamedTempFile;
|
||||
|
||||
const TEST_MAGIC_BYTES_LEN: usize = 12;
|
||||
const TEST_MAGIC_BYTES: [u8; TEST_MAGIC_BYTES_LEN] =
|
||||
[98, 100, 107, 102, 115, 49, 49, 49, 49, 49, 49, 49];
|
||||
|
||||
type TestChangeSet = BTreeSet<String>;
|
||||
|
||||
#[derive(Debug)]
|
||||
struct TestTracker;
|
||||
|
||||
/// Check behavior of [`Store::create_new`] and [`Store::open`].
|
||||
#[test]
|
||||
fn construct_store() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect_err("must not open as file does not exist yet");
|
||||
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must create file");
|
||||
// cannot create new as file already exists
|
||||
let _ = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect_err("must fail as file already exists now");
|
||||
let _ = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must open as file exists now");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn open_or_create_new() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
let file_path = temp_dir.path().join("db_file");
|
||||
let changeset = BTreeSet::from(["hello".to_string(), "world".to_string()]);
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must create");
|
||||
assert!(file_path.exists());
|
||||
db.append_changeset(&changeset).expect("must succeed");
|
||||
}
|
||||
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open_or_create_new(&TEST_MAGIC_BYTES, &file_path)
|
||||
.expect("must recover");
|
||||
let recovered_changeset = db.aggregate_changesets().expect("must succeed");
|
||||
assert_eq!(recovered_changeset, Some(changeset));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_fails_if_file_is_too_short() {
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&TEST_MAGIC_BYTES[..TEST_MAGIC_BYTES_LEN - 1])
|
||||
.expect("should write");
|
||||
|
||||
match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
|
||||
Err(FileError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::UnexpectedEof),
|
||||
unexpected => panic!("unexpected result: {:?}", unexpected),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_fails_if_magic_bytes_are_invalid() {
|
||||
let invalid_magic_bytes = "ldkfs0000000";
|
||||
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(invalid_magic_bytes.as_bytes())
|
||||
.expect("should write");
|
||||
|
||||
match Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()) {
|
||||
Err(FileError::InvalidMagicBytes { got, .. }) => {
|
||||
assert_eq!(got, invalid_magic_bytes.as_bytes())
|
||||
}
|
||||
unexpected => panic!("unexpected result: {:?}", unexpected),
|
||||
};
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn append_changeset_truncates_invalid_bytes() {
|
||||
// initial data to write to file (magic bytes + invalid data)
|
||||
let mut data = [255_u8; 2000];
|
||||
data[..TEST_MAGIC_BYTES_LEN].copy_from_slice(&TEST_MAGIC_BYTES);
|
||||
|
||||
let changeset = TestChangeSet::from(["one".into(), "two".into(), "three!".into()]);
|
||||
|
||||
let mut file = NamedTempFile::new().unwrap();
|
||||
file.write_all(&data).expect("should write");
|
||||
|
||||
let mut store =
|
||||
Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, file.path()).expect("should open");
|
||||
match store.iter_changesets().next() {
|
||||
Some(Err(IterError::Bincode(_))) => {}
|
||||
unexpected_res => panic!("unexpected result: {:?}", unexpected_res),
|
||||
}
|
||||
|
||||
store.append_changeset(&changeset).expect("should append");
|
||||
|
||||
drop(store);
|
||||
|
||||
let got_bytes = {
|
||||
let mut buf = Vec::new();
|
||||
file.reopen()
|
||||
.unwrap()
|
||||
.read_to_end(&mut buf)
|
||||
.expect("should read");
|
||||
buf
|
||||
};
|
||||
|
||||
let expected_bytes = {
|
||||
let mut buf = TEST_MAGIC_BYTES.to_vec();
|
||||
DefaultOptions::new()
|
||||
.with_varint_encoding()
|
||||
.serialize_into(&mut buf, &changeset)
|
||||
.expect("should encode");
|
||||
buf
|
||||
};
|
||||
|
||||
assert_eq!(got_bytes, expected_bytes);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn last_write_is_short() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = [
|
||||
TestChangeSet::from(["1".into()]),
|
||||
TestChangeSet::from(["2".into(), "3".into()]),
|
||||
TestChangeSet::from(["4".into(), "5".into(), "6".into()]),
|
||||
];
|
||||
let last_changeset = TestChangeSet::from(["7".into(), "8".into(), "9".into()]);
|
||||
let last_changeset_bytes = bincode_options().serialize(&last_changeset).unwrap();
|
||||
|
||||
for short_write_len in 1..last_changeset_bytes.len() - 1 {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", short_write_len));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// simulate creating a file, writing data where the last write is incomplete
|
||||
{
|
||||
let mut db =
|
||||
Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
// this is the incomplete write
|
||||
db.db_file
|
||||
.write_all(&last_changeset_bytes[..short_write_len])
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
// load file again and aggregate changesets
|
||||
// write the last changeset again (this time it succeeds)
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let err = db
|
||||
.aggregate_changesets()
|
||||
.expect_err("should return error as last read is short");
|
||||
assert_eq!(
|
||||
err.changeset,
|
||||
changesets.iter().cloned().reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets that are written in full",
|
||||
);
|
||||
db.db_file.write_all(&last_changeset_bytes).unwrap();
|
||||
}
|
||||
|
||||
// load file again - this time we should successfully aggregate all changesets
|
||||
{
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let aggregated_changesets = db
|
||||
.aggregate_changesets()
|
||||
.expect("aggregating all changesets should succeed");
|
||||
assert_eq!(
|
||||
aggregated_changesets,
|
||||
changesets
|
||||
.iter()
|
||||
.cloned()
|
||||
.chain(core::iter::once(last_changeset.clone()))
|
||||
.reduce(|mut acc, cs| {
|
||||
Append::append(&mut acc, cs);
|
||||
acc
|
||||
}),
|
||||
"should recover all changesets",
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn write_after_short_read() {
|
||||
let temp_dir = tempfile::tempdir().unwrap();
|
||||
|
||||
let changesets = (0..20)
|
||||
.map(|n| TestChangeSet::from([format!("{}", n)]))
|
||||
.collect::<Vec<_>>();
|
||||
let last_changeset = TestChangeSet::from(["last".into()]);
|
||||
|
||||
for read_count in 0..changesets.len() {
|
||||
let file_path = temp_dir.path().join(format!("{}.dat", read_count));
|
||||
println!("Test file: {:?}", file_path);
|
||||
|
||||
// First, we create the file with all the changesets!
|
||||
let mut db = Store::<TestChangeSet>::create_new(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
for changeset in &changesets {
|
||||
db.append_changeset(changeset).unwrap();
|
||||
}
|
||||
drop(db);
|
||||
|
||||
// We re-open the file and read `read_count` number of changesets.
|
||||
let mut db = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path).unwrap();
|
||||
let mut exp_aggregation = db
|
||||
.iter_changesets()
|
||||
.take(read_count)
|
||||
.map(|r| r.expect("must read valid changeset"))
|
||||
.fold(TestChangeSet::default(), |mut acc, v| {
|
||||
Append::append(&mut acc, v);
|
||||
acc
|
||||
});
|
||||
// We write after a short read.
|
||||
db.write_changes(&last_changeset)
|
||||
.expect("last write must succeed");
|
||||
Append::append(&mut exp_aggregation, last_changeset.clone());
|
||||
drop(db);
|
||||
|
||||
// We open the file again and check whether aggregate changeset is expected.
|
||||
let aggregation = Store::<TestChangeSet>::open(&TEST_MAGIC_BYTES, &file_path)
|
||||
.unwrap()
|
||||
.aggregate_changesets()
|
||||
.expect("must aggregate changesets")
|
||||
.unwrap_or_default();
|
||||
assert_eq!(aggregation, exp_aggregation);
|
||||
}
|
||||
}
|
||||
}
|
||||
13
crates/hwi/Cargo.toml
Normal file
13
crates/hwi/Cargo.toml
Normal file
@@ -0,0 +1,13 @@
|
||||
[package]
|
||||
name = "bdk_hwi"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
description = "Utilities to use bdk with hardware wallets"
|
||||
license = "MIT OR Apache-2.0"
|
||||
readme = "README.md"
|
||||
|
||||
[dependencies]
|
||||
bdk = { path = "../bdk" }
|
||||
hwi = { version = "0.7.0", features = [ "miniscript"] }
|
||||
42
crates/hwi/src/lib.rs
Normal file
42
crates/hwi/src/lib.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
//! HWI Signer
|
||||
//!
|
||||
//! This crate contains HWISigner, an implementation of a [`TransactionSigner`] to be
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk_hwi::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut devices = HWIClient::enumerate()?;
|
||||
//! if devices.is_empty() {
|
||||
//! panic!("No devices found!");
|
||||
//! }
|
||||
//! let first_device = devices.remove(0)?;
|
||||
//! let custom_signer = HWISigner::from_device(&first_device, Network::Testnet.into())?;
|
||||
//!
|
||||
//! # let mut wallet = Wallet::new_no_persist(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
//! # )?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
//! wallet.add_signer(
|
||||
//! KeychainKind::External,
|
||||
//! SignerOrdering(200),
|
||||
//! Arc::new(custom_signer),
|
||||
//! );
|
||||
//!
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! [`TransactionSigner`]: bdk::wallet::signer::TransactionSigner
|
||||
|
||||
mod signer;
|
||||
pub use signer::*;
|
||||
94
crates/hwi/src/signer.rs
Normal file
94
crates/hwi/src/signer.rs
Normal file
@@ -0,0 +1,94 @@
|
||||
use bdk::bitcoin::bip32::Fingerprint;
|
||||
use bdk::bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bdk::bitcoin::secp256k1::{All, Secp256k1};
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
use hwi::HWIClient;
|
||||
|
||||
use bdk::signer::{SignerCommon, SignerError, SignerId, TransactionSigner};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Custom signer for Hardware Wallets
|
||||
///
|
||||
/// This ignores `sign_options` and leaves the decisions up to the hardware wallet.
|
||||
pub struct HWISigner {
|
||||
fingerprint: Fingerprint,
|
||||
client: HWIClient,
|
||||
}
|
||||
|
||||
impl HWISigner {
|
||||
/// Create a instance from the specified device and chain
|
||||
pub fn from_device(device: &HWIDevice, chain: HWIChain) -> Result<HWISigner, Error> {
|
||||
let client = HWIClient::get_client(device, false, chain)?;
|
||||
Ok(HWISigner {
|
||||
fingerprint: device.fingerprint,
|
||||
client,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for HWISigner {
|
||||
fn id(&self, _secp: &Secp256k1<All>) -> SignerId {
|
||||
SignerId::Fingerprint(self.fingerprint)
|
||||
}
|
||||
}
|
||||
|
||||
impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut PartiallySignedTransaction,
|
||||
_sign_options: &bdk::SignOptions,
|
||||
_secp: &Secp256k1<All>,
|
||||
) -> Result<(), SignerError> {
|
||||
psbt.combine(
|
||||
self.client
|
||||
.sign_tx(psbt)
|
||||
.map_err(|e| {
|
||||
SignerError::External(format!("While signing with hardware wallet: {}", e))
|
||||
})?
|
||||
.psbt,
|
||||
)
|
||||
.expect("Failed to combine HW signed psbt with passed PSBT");
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: re-enable this once we have the `get_funded_wallet` test util
|
||||
// #[cfg(test)]
|
||||
// mod tests {
|
||||
// #[test]
|
||||
// fn test_hardware_signer() {
|
||||
// use std::sync::Arc;
|
||||
//
|
||||
// use bdk::tests::get_funded_wallet;
|
||||
// use bdk::signer::SignerOrdering;
|
||||
// use bdk::bitcoin::Network;
|
||||
// use crate::HWISigner;
|
||||
// use hwi::HWIClient;
|
||||
//
|
||||
// let mut devices = HWIClient::enumerate().unwrap();
|
||||
// if devices.is_empty() {
|
||||
// panic!("No devices found!");
|
||||
// }
|
||||
// let device = devices.remove(0).unwrap();
|
||||
// let client = HWIClient::get_client(&device, true, Network::Regtest.into()).unwrap();
|
||||
// let descriptors = client.get_descriptors::<String>(None).unwrap();
|
||||
// let custom_signer = HWISigner::from_device(&device, Network::Regtest.into()).unwrap();
|
||||
//
|
||||
// let (mut wallet, _) = get_funded_wallet(&descriptors.internal[0]);
|
||||
// wallet.add_signer(
|
||||
// bdk::KeychainKind::External,
|
||||
// SignerOrdering(200),
|
||||
// Arc::new(custom_signer),
|
||||
// );
|
||||
//
|
||||
// let addr = wallet.get_address(bdk::wallet::AddressIndex::LastUnused);
|
||||
// let mut builder = wallet.build_tx();
|
||||
// builder.drain_to(addr.script_pubkey()).drain_wallet();
|
||||
// let (mut psbt, _) = builder.finish().unwrap();
|
||||
//
|
||||
// let finalized = wallet.sign(&mut psbt, Default::default()).unwrap();
|
||||
// assert!(finalized);
|
||||
// }
|
||||
// }
|
||||
12
example-crates/example_bitcoind_rpc_polling/Cargo.toml
Normal file
12
example-crates/example_bitcoind_rpc_polling/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "example_bitcoind_rpc_polling"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
|
||||
bdk_bitcoind_rpc = { path = "../../crates/bitcoind_rpc" }
|
||||
example_cli = { path = "../example_cli" }
|
||||
ctrlc = { version = "^2" }
|
||||
68
example-crates/example_bitcoind_rpc_polling/README.md
Normal file
68
example-crates/example_bitcoind_rpc_polling/README.md
Normal file
@@ -0,0 +1,68 @@
|
||||
# Example RPC CLI
|
||||
|
||||
### Simple Regtest Test
|
||||
|
||||
1. Start local regtest bitcoind.
|
||||
```
|
||||
mkdir -p /tmp/regtest/bitcoind
|
||||
bitcoind -regtest -server -fallbackfee=0.0002 -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -daemon
|
||||
```
|
||||
2. Create a test bitcoind wallet and set bitcoind env.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -named createwallet wallet_name="test"
|
||||
export RPC_URL=127.0.0.1:18443
|
||||
export RPC_USER=<your-rpc-username>
|
||||
export RPC_PASS=<your-rpc-password>
|
||||
```
|
||||
3. Get test bitcoind wallet info.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -datadir=/tmp/regtest/bitcoind -regtest getwalletinfo
|
||||
```
|
||||
4. Get new test bitcoind wallet address.
|
||||
```
|
||||
BITCOIND_ADDRESS=$(bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getnewaddress)
|
||||
echo $BITCOIND_ADDRESS
|
||||
```
|
||||
5. Generate 101 blocks with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> generatetoaddress 101 $BITCOIND_ADDRESS
|
||||
```
|
||||
6. Verify test bitcoind wallet balance.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> getbalances
|
||||
```
|
||||
7. Set descriptor env and get address from RPC CLI wallet.
|
||||
```
|
||||
export DESCRIPTOR="wpkh(tprv8ZgxMBicQKsPfK9BTf82oQkHhawtZv19CorqQKPFeaHDMA4dXYX6eWsJGNJ7VTQXWmoHdrfjCYuDijcRmNFwSKcVhswzqs4fugE8turndGc/1/*)"
|
||||
cargo run -- --network regtest address next
|
||||
```
|
||||
8. Send 5 test bitcoin to RPC CLI wallet.
|
||||
```
|
||||
bitcoin-cli -rpcwallet="test" -datadir=/tmp/regtest/bitcoind -regtest -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> sendtoaddress <address> 5
|
||||
```
|
||||
9. Sync blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
10. Get RPC CLI wallet unconfirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
11. Generate 1 block with reward to test bitcoind wallet address.
|
||||
```
|
||||
bitcoin-cli -datadir=/tmp/regtest/bitcoind -rpcuser=<your-rpc-username> -rpcpassword=<your-rpc-password> -regtest generatetoaddress 10 $BITCOIND_ADDRESS
|
||||
```
|
||||
12. Sync the blockchain with RPC CLI wallet.
|
||||
```
|
||||
cargo run -- --network regtest sync
|
||||
<CNTRL-C to stop syncing>
|
||||
```
|
||||
13. Get RPC CLI wallet confirmed balances.
|
||||
```
|
||||
cargo run -- --network regtest balance
|
||||
```
|
||||
14. Get RPC CLI wallet transactions.
|
||||
```
|
||||
cargo run -- --network regtest txout list
|
||||
```
|
||||
390
example-crates/example_bitcoind_rpc_polling/src/main.rs
Normal file
390
example-crates/example_bitcoind_rpc_polling/src/main.rs
Normal file
@@ -0,0 +1,390 @@
|
||||
use std::{
|
||||
path::PathBuf,
|
||||
sync::{
|
||||
atomic::{AtomicBool, Ordering},
|
||||
Arc, Mutex,
|
||||
},
|
||||
time::{Duration, Instant},
|
||||
};
|
||||
|
||||
use bdk_bitcoind_rpc::{
|
||||
bitcoincore_rpc::{Auth, Client, RpcApi},
|
||||
Emitter,
|
||||
};
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Block, Transaction},
|
||||
indexed_tx_graph, keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow,
|
||||
clap::{self, Args, Subcommand},
|
||||
Keychain,
|
||||
};
|
||||
|
||||
const DB_MAGIC: &[u8] = b"bdk_example_rpc";
|
||||
const DB_PATH: &str = ".bdk_example_rpc.db";
|
||||
|
||||
/// The mpsc channel bound for emissions from [`Emitter`].
|
||||
const CHANNEL_BOUND: usize = 10;
|
||||
/// Delay for printing status to stdout.
|
||||
const STDOUT_PRINT_DELAY: Duration = Duration::from_secs(6);
|
||||
/// Delay between mempool emissions.
|
||||
const MEMPOOL_EMIT_DELAY: Duration = Duration::from_secs(30);
|
||||
/// Delay for committing to persistence.
|
||||
const DB_COMMIT_DELAY: Duration = Duration::from_secs(60);
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
|
||||
#[derive(Debug)]
|
||||
enum Emission {
|
||||
Block(bdk_bitcoind_rpc::BlockEvent<Block>),
|
||||
Mempool(Vec<(Transaction, u64)>),
|
||||
Tip(u32),
|
||||
}
|
||||
|
||||
#[derive(Args, Debug, Clone)]
|
||||
struct RpcArgs {
|
||||
/// RPC URL
|
||||
#[clap(env = "RPC_URL", long, default_value = "127.0.0.1:8332")]
|
||||
url: String,
|
||||
/// RPC auth cookie file
|
||||
#[clap(env = "RPC_COOKIE", long)]
|
||||
rpc_cookie: Option<PathBuf>,
|
||||
/// RPC auth username
|
||||
#[clap(env = "RPC_USER", long)]
|
||||
rpc_user: Option<String>,
|
||||
/// RPC auth password
|
||||
#[clap(env = "RPC_PASS", long)]
|
||||
rpc_password: Option<String>,
|
||||
/// Starting block height to fallback to if no point of agreement if found
|
||||
#[clap(env = "FALLBACK_HEIGHT", long, default_value = "0")]
|
||||
fallback_height: u32,
|
||||
}
|
||||
|
||||
impl From<RpcArgs> for Auth {
|
||||
fn from(args: RpcArgs) -> Self {
|
||||
match (args.rpc_cookie, args.rpc_user, args.rpc_password) {
|
||||
(None, None, None) => Self::None,
|
||||
(Some(path), _, _) => Self::CookieFile(path),
|
||||
(_, Some(user), Some(pass)) => Self::UserPass(user, pass),
|
||||
(_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
|
||||
(_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl RpcArgs {
|
||||
fn new_client(&self) -> anyhow::Result<Client> {
|
||||
Ok(Client::new(
|
||||
&self.url,
|
||||
match (&self.rpc_cookie, &self.rpc_user, &self.rpc_password) {
|
||||
(None, None, None) => Auth::None,
|
||||
(Some(path), _, _) => Auth::CookieFile(path.clone()),
|
||||
(_, Some(user), Some(pass)) => Auth::UserPass(user.clone(), pass.clone()),
|
||||
(_, Some(_), None) => panic!("rpc auth: missing rpc_pass"),
|
||||
(_, None, Some(_)) => panic!("rpc auth: missing rpc_user"),
|
||||
},
|
||||
)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum RpcCommands {
|
||||
/// Syncs local state with remote state via RPC (starting from last point of agreement) and
|
||||
/// stores/indexes relevant transactions
|
||||
Sync {
|
||||
#[clap(flatten)]
|
||||
rpc_args: RpcArgs,
|
||||
},
|
||||
/// Sync by having the emitter logic in a separate thread
|
||||
Live {
|
||||
#[clap(flatten)]
|
||||
rpc_args: RpcArgs,
|
||||
},
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let start = Instant::now();
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<RpcCommands, RpcArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
println!(
|
||||
"[{:>10}s] loaded initial changeset from db",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
let (init_chain_changeset, init_graph_changeset) = init_changeset;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
graph.apply_changeset(init_graph_changeset);
|
||||
graph
|
||||
});
|
||||
println!(
|
||||
"[{:>10}s] loaded indexed tx graph from changeset",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
let chain = Mutex::new(if init_chain_changeset.is_empty() {
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
let (chain, chain_changeset) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage((chain_changeset, Default::default()));
|
||||
db.commit()?;
|
||||
chain
|
||||
} else {
|
||||
LocalChain::from_changeset(init_chain_changeset)?
|
||||
});
|
||||
println!(
|
||||
"[{:>10}s] loaded local chain from changeset",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
|
||||
let rpc_cmd = match args.command {
|
||||
example_cli::Commands::ChainSpecific(rpc_cmd) => rpc_cmd,
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
&keymap,
|
||||
args.network,
|
||||
|rpc_args, tx| {
|
||||
let client = rpc_args.new_client()?;
|
||||
client.send_raw_transaction(tx)?;
|
||||
Ok(())
|
||||
},
|
||||
general_cmd,
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
match rpc_cmd {
|
||||
RpcCommands::Sync { rpc_args } => {
|
||||
let RpcArgs {
|
||||
fallback_height, ..
|
||||
} = rpc_args;
|
||||
|
||||
let chain_tip = chain.lock().unwrap().tip();
|
||||
let rpc_client = rpc_args.new_client()?;
|
||||
let mut emitter = Emitter::new(&rpc_client, chain_tip, fallback_height);
|
||||
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Instant::now();
|
||||
|
||||
while let Some(emission) = emitter.next_block()? {
|
||||
let height = emission.block_height();
|
||||
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
let mut db = db.lock().unwrap();
|
||||
|
||||
let chain_changeset = chain
|
||||
.apply_update(local_chain::Update {
|
||||
tip: emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
})
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset = graph.apply_block_relevant(&emission.block, height);
|
||||
db.stage((chain_changeset, graph_changeset));
|
||||
|
||||
// commit staged db changes in intervals
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
last_db_commit = Instant::now();
|
||||
db.commit()?;
|
||||
println!(
|
||||
"[{:>10}s] committed to db (took {}s)",
|
||||
start.elapsed().as_secs_f32(),
|
||||
last_db_commit.elapsed().as_secs_f32()
|
||||
);
|
||||
}
|
||||
|
||||
// print synced-to height and current balance in intervals
|
||||
if last_print.elapsed() >= STDOUT_PRINT_DELAY {
|
||||
last_print = Instant::now();
|
||||
let synced_to = chain.tip();
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
let mempool_txs = emitter.mempool()?;
|
||||
let graph_changeset = graph.lock().unwrap().batch_insert_relevant_unconfirmed(
|
||||
mempool_txs.iter().map(|(tx, time)| (tx, *time)),
|
||||
);
|
||||
{
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage((local_chain::ChangeSet::default(), graph_changeset));
|
||||
db.commit()?; // commit one last time
|
||||
}
|
||||
}
|
||||
RpcCommands::Live { rpc_args } => {
|
||||
let RpcArgs {
|
||||
fallback_height, ..
|
||||
} = rpc_args;
|
||||
let sigterm_flag = start_ctrlc_handler();
|
||||
|
||||
let last_cp = chain.lock().unwrap().tip();
|
||||
|
||||
println!(
|
||||
"[{:>10}s] starting emitter thread...",
|
||||
start.elapsed().as_secs_f32()
|
||||
);
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel::<Emission>(CHANNEL_BOUND);
|
||||
let emission_jh = std::thread::spawn(move || -> anyhow::Result<()> {
|
||||
let rpc_client = rpc_args.new_client()?;
|
||||
let mut emitter = Emitter::new(&rpc_client, last_cp, fallback_height);
|
||||
|
||||
let mut block_count = rpc_client.get_block_count()? as u32;
|
||||
tx.send(Emission::Tip(block_count))?;
|
||||
|
||||
loop {
|
||||
match emitter.next_block()? {
|
||||
Some(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
if sigterm_flag.load(Ordering::Acquire) {
|
||||
break;
|
||||
}
|
||||
if height > block_count {
|
||||
block_count = rpc_client.get_block_count()? as u32;
|
||||
tx.send(Emission::Tip(block_count))?;
|
||||
}
|
||||
tx.send(Emission::Block(block_emission))?;
|
||||
}
|
||||
None => {
|
||||
if await_flag(&sigterm_flag, MEMPOOL_EMIT_DELAY) {
|
||||
break;
|
||||
}
|
||||
println!("preparing mempool emission...");
|
||||
let now = Instant::now();
|
||||
tx.send(Emission::Mempool(emitter.mempool()?))?;
|
||||
println!("mempool emission prepared in {}s", now.elapsed().as_secs());
|
||||
continue;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
println!("emitter thread shutting down...");
|
||||
Ok(())
|
||||
});
|
||||
|
||||
let mut tip_height = 0_u32;
|
||||
let mut last_db_commit = Instant::now();
|
||||
let mut last_print = Option::<Instant>::None;
|
||||
|
||||
for emission in rx {
|
||||
let mut db = db.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
let mut chain = chain.lock().unwrap();
|
||||
|
||||
let changeset = match emission {
|
||||
Emission::Block(block_emission) => {
|
||||
let height = block_emission.block_height();
|
||||
let chain_update = local_chain::Update {
|
||||
tip: block_emission.checkpoint,
|
||||
introduce_older_blocks: false,
|
||||
};
|
||||
let chain_changeset = chain
|
||||
.apply_update(chain_update)
|
||||
.expect("must always apply as we receive blocks in order from emitter");
|
||||
let graph_changeset =
|
||||
graph.apply_block_relevant(&block_emission.block, height);
|
||||
(chain_changeset, graph_changeset)
|
||||
}
|
||||
Emission::Mempool(mempool_txs) => {
|
||||
let graph_changeset = graph.batch_insert_relevant_unconfirmed(
|
||||
mempool_txs.iter().map(|(tx, time)| (tx, *time)),
|
||||
);
|
||||
(local_chain::ChangeSet::default(), graph_changeset)
|
||||
}
|
||||
Emission::Tip(h) => {
|
||||
tip_height = h;
|
||||
continue;
|
||||
}
|
||||
};
|
||||
|
||||
db.stage(changeset);
|
||||
|
||||
if last_db_commit.elapsed() >= DB_COMMIT_DELAY {
|
||||
last_db_commit = Instant::now();
|
||||
db.commit()?;
|
||||
println!(
|
||||
"[{:>10}s] committed to db (took {}s)",
|
||||
start.elapsed().as_secs_f32(),
|
||||
last_db_commit.elapsed().as_secs_f32()
|
||||
);
|
||||
}
|
||||
|
||||
if last_print.map_or(Duration::MAX, |i| i.elapsed()) >= STDOUT_PRINT_DELAY {
|
||||
last_print = Some(Instant::now());
|
||||
let synced_to = chain.tip();
|
||||
let balance = {
|
||||
graph.graph().balance(
|
||||
&*chain,
|
||||
synced_to.block_id(),
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)
|
||||
};
|
||||
println!(
|
||||
"[{:>10}s] synced to {} @ {} / {} | total: {} sats",
|
||||
start.elapsed().as_secs_f32(),
|
||||
synced_to.hash(),
|
||||
synced_to.height(),
|
||||
tip_height,
|
||||
balance.total()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
emission_jh.join().expect("must join emitter thread")?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn start_ctrlc_handler() -> Arc<AtomicBool> {
|
||||
let flag = Arc::new(AtomicBool::new(false));
|
||||
let cloned_flag = flag.clone();
|
||||
|
||||
ctrlc::set_handler(move || cloned_flag.store(true, Ordering::Release));
|
||||
|
||||
flag
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn await_flag(flag: &AtomicBool, duration: Duration) -> bool {
|
||||
let start = Instant::now();
|
||||
loop {
|
||||
if flag.load(Ordering::Acquire) {
|
||||
return true;
|
||||
}
|
||||
if start.elapsed() >= duration {
|
||||
return false;
|
||||
}
|
||||
std::thread::sleep(Duration::from_secs(1));
|
||||
}
|
||||
}
|
||||
17
example-crates/example_cli/Cargo.toml
Normal file
17
example-crates/example_cli/Cargo.toml
Normal file
@@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "example_cli"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde", "miniscript"]}
|
||||
bdk_file_store = { path = "../../crates/file_store" }
|
||||
bdk_tmp_plan = { path = "../../nursery/tmp_plan" }
|
||||
bdk_coin_select = { path = "../../nursery/coin_select" }
|
||||
|
||||
clap = { version = "3.2.23", features = ["derive", "env"] }
|
||||
anyhow = "1"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
719
example-crates/example_cli/src/lib.rs
Normal file
719
example-crates/example_cli/src/lib.rs
Normal file
@@ -0,0 +1,719 @@
|
||||
pub use anyhow;
|
||||
use anyhow::Context;
|
||||
use bdk_coin_select::{coin_select_bnb, CoinSelector, CoinSelectorOpt, WeightedValue};
|
||||
use bdk_file_store::Store;
|
||||
use serde::{de::DeserializeOwned, Serialize};
|
||||
use std::{cmp::Reverse, collections::HashMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
absolute, address, psbt::Prevouts, secp256k1::Secp256k1, sighash::SighashCache, Address,
|
||||
Network, Sequence, Transaction, TxIn, TxOut,
|
||||
},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, KeychainTxOutIndex},
|
||||
local_chain,
|
||||
miniscript::{
|
||||
descriptor::{DescriptorSecretKey, KeyMap},
|
||||
Descriptor, DescriptorPublicKey,
|
||||
},
|
||||
Anchor, Append, ChainOracle, DescriptorExt, FullTxOut, Persist, PersistBackend,
|
||||
};
|
||||
pub use bdk_file_store;
|
||||
pub use clap;
|
||||
|
||||
use clap::{Parser, Subcommand};
|
||||
|
||||
pub type KeychainTxGraph<A> = IndexedTxGraph<A, KeychainTxOutIndex<Keychain>>;
|
||||
pub type KeychainChangeSet<A> = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<A, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
pub type Database<C> = Persist<Store<C>, C>;
|
||||
|
||||
#[derive(Parser)]
|
||||
#[clap(author, version, about, long_about = None)]
|
||||
#[clap(propagate_version = true)]
|
||||
pub struct Args<CS: clap::Subcommand, S: clap::Args> {
|
||||
#[clap(env = "DESCRIPTOR")]
|
||||
pub descriptor: String,
|
||||
#[clap(env = "CHANGE_DESCRIPTOR")]
|
||||
pub change_descriptor: Option<String>,
|
||||
|
||||
#[clap(env = "BITCOIN_NETWORK", long, default_value = "signet")]
|
||||
pub network: Network,
|
||||
|
||||
#[clap(env = "BDK_DB_PATH", long, default_value = ".bdk_example_db")]
|
||||
pub db_path: PathBuf,
|
||||
|
||||
#[clap(env = "BDK_CP_LIMIT", long, default_value = "20")]
|
||||
pub cp_limit: usize,
|
||||
|
||||
#[clap(subcommand)]
|
||||
pub command: Commands<CS, S>,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum Commands<CS: clap::Subcommand, S: clap::Args> {
|
||||
#[clap(flatten)]
|
||||
ChainSpecific(CS),
|
||||
/// Address generation and inspection.
|
||||
Address {
|
||||
#[clap(subcommand)]
|
||||
addr_cmd: AddressCmd,
|
||||
},
|
||||
/// Get the wallet balance.
|
||||
Balance,
|
||||
/// TxOut related commands.
|
||||
#[clap(name = "txout")]
|
||||
TxOut {
|
||||
#[clap(subcommand)]
|
||||
txout_cmd: TxOutCmd,
|
||||
},
|
||||
/// Send coins to an address.
|
||||
Send {
|
||||
/// Amount to send in satoshis
|
||||
value: u64,
|
||||
/// Destination address
|
||||
address: Address<address::NetworkUnchecked>,
|
||||
#[clap(short, default_value = "bnb")]
|
||||
coin_select: CoinSelectionAlgo,
|
||||
#[clap(flatten)]
|
||||
chain_specific: S,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub enum CoinSelectionAlgo {
|
||||
LargestFirst,
|
||||
SmallestFirst,
|
||||
OldestFirst,
|
||||
NewestFirst,
|
||||
BranchAndBound,
|
||||
}
|
||||
|
||||
impl Default for CoinSelectionAlgo {
|
||||
fn default() -> Self {
|
||||
Self::LargestFirst
|
||||
}
|
||||
}
|
||||
|
||||
impl core::str::FromStr for CoinSelectionAlgo {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
||||
use CoinSelectionAlgo::*;
|
||||
Ok(match s {
|
||||
"largest-first" => LargestFirst,
|
||||
"smallest-first" => SmallestFirst,
|
||||
"oldest-first" => OldestFirst,
|
||||
"newest-first" => NewestFirst,
|
||||
"bnb" => BranchAndBound,
|
||||
unknown => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"unknown coin selection algorithm '{}'",
|
||||
unknown
|
||||
))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl core::fmt::Display for CoinSelectionAlgo {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
use CoinSelectionAlgo::*;
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
LargestFirst => "largest-first",
|
||||
SmallestFirst => "smallest-first",
|
||||
OldestFirst => "oldest-first",
|
||||
NewestFirst => "newest-first",
|
||||
BranchAndBound => "bnb",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum AddressCmd {
|
||||
/// Get the next unused address.
|
||||
Next,
|
||||
/// Get a new address regardless of the existing unused addresses.
|
||||
New,
|
||||
/// List all addresses
|
||||
List {
|
||||
/// List change addresses
|
||||
#[clap(long)]
|
||||
change: bool,
|
||||
},
|
||||
/// Get last revealed address index for each keychain.
|
||||
Index,
|
||||
}
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
pub enum TxOutCmd {
|
||||
/// List transaction outputs.
|
||||
List {
|
||||
/// Return only spent outputs.
|
||||
#[clap(short, long)]
|
||||
spent: bool,
|
||||
/// Return only unspent outputs.
|
||||
#[clap(short, long)]
|
||||
unspent: bool,
|
||||
/// Return only confirmed outputs.
|
||||
#[clap(long)]
|
||||
confirmed: bool,
|
||||
/// Return only unconfirmed outputs.
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq, serde::Deserialize, serde::Serialize,
|
||||
)]
|
||||
pub enum Keychain {
|
||||
External,
|
||||
Internal,
|
||||
}
|
||||
|
||||
impl core::fmt::Display for Keychain {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Keychain::External => write!(f, "external"),
|
||||
Keychain::Internal => write!(f, "internal"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CreateTxChange {
|
||||
pub index_changeset: keychain::ChangeSet<Keychain>,
|
||||
pub change_keychain: Keychain,
|
||||
pub index: u32,
|
||||
}
|
||||
|
||||
pub fn create_tx<A: Anchor, O: ChainOracle>(
|
||||
graph: &mut KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
cs_algorithm: CoinSelectionAlgo,
|
||||
address: Address,
|
||||
value: u64,
|
||||
) -> anyhow::Result<(Transaction, Option<CreateTxChange>)>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
{
|
||||
let mut changeset = keychain::ChangeSet::default();
|
||||
|
||||
let assets = bdk_tmp_plan::Assets {
|
||||
keys: keymap.iter().map(|(pk, _)| pk.clone()).collect(),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
// TODO use planning module
|
||||
let mut candidates = planned_utxos(graph, chain, &assets)?;
|
||||
|
||||
// apply coin selection algorithm
|
||||
match cs_algorithm {
|
||||
CoinSelectionAlgo::LargestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.txout.value))
|
||||
}
|
||||
CoinSelectionAlgo::SmallestFirst => candidates.sort_by_key(|(_, utxo)| utxo.txout.value),
|
||||
CoinSelectionAlgo::OldestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| utxo.chain_position.clone())
|
||||
}
|
||||
CoinSelectionAlgo::NewestFirst => {
|
||||
candidates.sort_by_key(|(_, utxo)| Reverse(utxo.chain_position.clone()))
|
||||
}
|
||||
CoinSelectionAlgo::BranchAndBound => {}
|
||||
}
|
||||
|
||||
// turn the txos we chose into weight and value
|
||||
let wv_candidates = candidates
|
||||
.iter()
|
||||
.map(|(plan, utxo)| {
|
||||
WeightedValue::new(
|
||||
utxo.txout.value,
|
||||
plan.expected_weight() as _,
|
||||
plan.witness_version().is_some(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut outputs = vec![TxOut {
|
||||
value,
|
||||
script_pubkey: address.script_pubkey(),
|
||||
}];
|
||||
|
||||
let internal_keychain = if graph.index.keychains().get(&Keychain::Internal).is_some() {
|
||||
Keychain::Internal
|
||||
} else {
|
||||
Keychain::External
|
||||
};
|
||||
|
||||
let ((change_index, change_script), change_changeset) =
|
||||
graph.index.next_unused_spk(&internal_keychain);
|
||||
changeset.append(change_changeset);
|
||||
|
||||
// Clone to drop the immutable reference.
|
||||
let change_script = change_script.into();
|
||||
|
||||
let change_plan = bdk_tmp_plan::plan_satisfaction(
|
||||
&graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.at_derivation_index(change_index)
|
||||
.expect("change_index can't be hardened"),
|
||||
&assets,
|
||||
)
|
||||
.expect("failed to obtain change plan");
|
||||
|
||||
let mut change_output = TxOut {
|
||||
value: 0,
|
||||
script_pubkey: change_script,
|
||||
};
|
||||
|
||||
let cs_opts = CoinSelectorOpt {
|
||||
target_feerate: 0.5,
|
||||
min_drain_value: graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&internal_keychain)
|
||||
.expect("must exist")
|
||||
.dust_value(),
|
||||
..CoinSelectorOpt::fund_outputs(
|
||||
&outputs,
|
||||
&change_output,
|
||||
change_plan.expected_weight() as u32,
|
||||
)
|
||||
};
|
||||
|
||||
// TODO: How can we make it easy to shuffle in order of inputs and outputs here?
|
||||
// apply coin selection by saying we need to fund these outputs
|
||||
let mut coin_selector = CoinSelector::new(&wv_candidates, &cs_opts);
|
||||
|
||||
// just select coins in the order provided until we have enough
|
||||
// only use the first result (least waste)
|
||||
let selection = match cs_algorithm {
|
||||
CoinSelectionAlgo::BranchAndBound => {
|
||||
coin_select_bnb(Duration::from_secs(10), coin_selector.clone())
|
||||
.map_or_else(|| coin_selector.select_until_finished(), |cs| cs.finish())?
|
||||
}
|
||||
_ => coin_selector.select_until_finished()?,
|
||||
};
|
||||
let (_, selection_meta) = selection.best_strategy();
|
||||
|
||||
// get the selected utxos
|
||||
let selected_txos = selection.apply_selection(&candidates).collect::<Vec<_>>();
|
||||
|
||||
if let Some(drain_value) = selection_meta.drain_value {
|
||||
change_output.value = drain_value;
|
||||
// if the selection tells us to use change and the change value is sufficient, we add it as an output
|
||||
outputs.push(change_output)
|
||||
}
|
||||
|
||||
let mut transaction = Transaction {
|
||||
version: 0x02,
|
||||
// because the temporary planning module does not support timelocks, we can use the chain
|
||||
// tip as the `lock_time` for anti-fee-sniping purposes
|
||||
lock_time: absolute::LockTime::from_height(chain.get_chain_tip()?.height)
|
||||
.expect("invalid height"),
|
||||
input: selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| TxIn {
|
||||
previous_output: utxo.outpoint,
|
||||
sequence: Sequence::ENABLE_RBF_NO_LOCKTIME,
|
||||
..Default::default()
|
||||
})
|
||||
.collect(),
|
||||
output: outputs,
|
||||
};
|
||||
|
||||
let prevouts = selected_txos
|
||||
.iter()
|
||||
.map(|(_, utxo)| utxo.txout.clone())
|
||||
.collect::<Vec<_>>();
|
||||
let sighash_prevouts = Prevouts::All(&prevouts);
|
||||
|
||||
// first, set tx values for the plan so that we don't change them while signing
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
if let Some(sequence) = plan.required_sequence() {
|
||||
transaction.input[i].sequence = sequence
|
||||
}
|
||||
}
|
||||
|
||||
// create a short lived transaction
|
||||
let _sighash_tx = transaction.clone();
|
||||
let mut sighash_cache = SighashCache::new(&_sighash_tx);
|
||||
|
||||
for (i, (plan, _)) in selected_txos.iter().enumerate() {
|
||||
let requirements = plan.requirements();
|
||||
let mut auth_data = bdk_tmp_plan::SatisfactionMaterial::default();
|
||||
assert!(
|
||||
!requirements.requires_hash_preimages(),
|
||||
"can't have hash pre-images since we didn't provide any."
|
||||
);
|
||||
assert!(
|
||||
requirements.signatures.sign_with_keymap(
|
||||
i,
|
||||
keymap,
|
||||
&sighash_prevouts,
|
||||
None,
|
||||
None,
|
||||
&mut sighash_cache,
|
||||
&mut auth_data,
|
||||
&Secp256k1::default(),
|
||||
)?,
|
||||
"we should have signed with this input."
|
||||
);
|
||||
|
||||
match plan.try_complete(&auth_data) {
|
||||
bdk_tmp_plan::PlanState::Complete {
|
||||
final_script_sig,
|
||||
final_script_witness,
|
||||
} => {
|
||||
if let Some(witness) = final_script_witness {
|
||||
transaction.input[i].witness = witness;
|
||||
}
|
||||
|
||||
if let Some(script_sig) = final_script_sig {
|
||||
transaction.input[i].script_sig = script_sig;
|
||||
}
|
||||
}
|
||||
bdk_tmp_plan::PlanState::Incomplete(_) => {
|
||||
return Err(anyhow::anyhow!(
|
||||
"we weren't able to complete the plan with our keys."
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let change_info = if selection_meta.drain_value.is_some() {
|
||||
Some(CreateTxChange {
|
||||
index_changeset: changeset,
|
||||
change_keychain: internal_keychain,
|
||||
index: change_index,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok((transaction, change_info))
|
||||
}
|
||||
|
||||
// Alias the elements of `Result` of `planned_utxos`
|
||||
pub type PlannedUtxo<K, A> = (bdk_tmp_plan::Plan<K>, FullTxOut<A>);
|
||||
|
||||
pub fn planned_utxos<A: Anchor, O: ChainOracle, K: Clone + bdk_tmp_plan::CanDerive>(
|
||||
graph: &KeychainTxGraph<A>,
|
||||
chain: &O,
|
||||
assets: &bdk_tmp_plan::Assets<K>,
|
||||
) -> Result<Vec<PlannedUtxo<K, A>>, O::Error> {
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
graph
|
||||
.graph()
|
||||
.try_filter_chain_unspents(chain, chain_tip, outpoints)
|
||||
.filter_map(|r| -> Option<Result<PlannedUtxo<K, A>, _>> {
|
||||
let (k, i, full_txo) = match r {
|
||||
Err(err) => return Some(Err(err)),
|
||||
Ok(((k, i), full_txo)) => (k, i, full_txo),
|
||||
};
|
||||
let desc = graph
|
||||
.index
|
||||
.keychains()
|
||||
.get(&k)
|
||||
.expect("keychain must exist")
|
||||
.at_derivation_index(i)
|
||||
.expect("i can't be hardened");
|
||||
let plan = bdk_tmp_plan::plan_satisfaction(&desc, assets)?;
|
||||
Some(Ok((plan, full_txo)))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn handle_commands<CS: clap::Subcommand, S: clap::Args, A: Anchor, O: ChainOracle, C>(
|
||||
graph: &Mutex<KeychainTxGraph<A>>,
|
||||
db: &Mutex<Database<C>>,
|
||||
chain: &Mutex<O>,
|
||||
keymap: &HashMap<DescriptorPublicKey, DescriptorSecretKey>,
|
||||
network: Network,
|
||||
broadcast: impl FnOnce(S, &Transaction) -> anyhow::Result<()>,
|
||||
cmd: Commands<CS, S>,
|
||||
) -> anyhow::Result<()>
|
||||
where
|
||||
O::Error: std::error::Error + Send + Sync + 'static,
|
||||
C: Default + Append + DeserializeOwned + Serialize + From<KeychainChangeSet<A>>,
|
||||
{
|
||||
match cmd {
|
||||
Commands::ChainSpecific(_) => unreachable!("example code should handle this!"),
|
||||
Commands::Address { addr_cmd } => {
|
||||
let graph = &mut *graph.lock().unwrap();
|
||||
let index = &mut graph.index;
|
||||
|
||||
match addr_cmd {
|
||||
AddressCmd::Next | AddressCmd::New => {
|
||||
let spk_chooser = match addr_cmd {
|
||||
AddressCmd::Next => KeychainTxOutIndex::next_unused_spk,
|
||||
AddressCmd::New => KeychainTxOutIndex::reveal_next_spk,
|
||||
_ => unreachable!("only these two variants exist in match arm"),
|
||||
};
|
||||
|
||||
let ((spk_i, spk), index_changeset) = spk_chooser(index, &Keychain::External);
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)))?;
|
||||
let addr =
|
||||
Address::from_script(spk, network).context("failed to derive address")?;
|
||||
println!("[address @ {}] {}", spk_i, addr);
|
||||
Ok(())
|
||||
}
|
||||
AddressCmd::Index => {
|
||||
for (keychain, derivation_index) in index.last_revealed_indices() {
|
||||
println!("{:?}: {}", keychain, derivation_index);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
AddressCmd::List { change } => {
|
||||
let target_keychain = match change {
|
||||
true => Keychain::Internal,
|
||||
false => Keychain::External,
|
||||
};
|
||||
for (spk_i, spk) in index.revealed_keychain_spks(&target_keychain) {
|
||||
let address = Address::from_script(spk, network)
|
||||
.expect("should always be able to derive address");
|
||||
println!(
|
||||
"{:?} {} used:{}",
|
||||
spk_i,
|
||||
address,
|
||||
index.is_used(target_keychain, spk_i)
|
||||
);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Balance => {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
fn print_balances<'a>(
|
||||
title_str: &'a str,
|
||||
items: impl IntoIterator<Item = (&'a str, u64)>,
|
||||
) {
|
||||
println!("{}:", title_str);
|
||||
for (name, amount) in items.into_iter() {
|
||||
println!(" {:<10} {:>12} sats", name, amount)
|
||||
}
|
||||
}
|
||||
|
||||
let balance = graph.graph().try_balance(
|
||||
chain,
|
||||
chain.get_chain_tip()?,
|
||||
graph.index.outpoints().iter().cloned(),
|
||||
|(k, _), _| k == &Keychain::Internal,
|
||||
)?;
|
||||
|
||||
let confirmed_total = balance.confirmed + balance.immature;
|
||||
let unconfirmed_total = balance.untrusted_pending + balance.trusted_pending;
|
||||
|
||||
print_balances(
|
||||
"confirmed",
|
||||
[
|
||||
("total", confirmed_total),
|
||||
("spendable", balance.confirmed),
|
||||
("immature", balance.immature),
|
||||
],
|
||||
);
|
||||
print_balances(
|
||||
"unconfirmed",
|
||||
[
|
||||
("total", unconfirmed_total),
|
||||
("trusted", balance.trusted_pending),
|
||||
("untrusted", balance.untrusted_pending),
|
||||
],
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
Commands::TxOut { txout_cmd } => {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let chain_tip = chain.get_chain_tip()?;
|
||||
let outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
match txout_cmd {
|
||||
TxOutCmd::List {
|
||||
spent,
|
||||
unspent,
|
||||
confirmed,
|
||||
unconfirmed,
|
||||
} => {
|
||||
let txouts = graph
|
||||
.graph()
|
||||
.try_filter_chain_txouts(chain, chain_tip, outpoints)
|
||||
.filter(|r| match r {
|
||||
Ok((_, full_txo)) => match (spent, unspent) {
|
||||
(true, false) => full_txo.spent_by.is_some(),
|
||||
(false, true) => full_txo.spent_by.is_none(),
|
||||
_ => true,
|
||||
},
|
||||
// always keep errored items
|
||||
Err(_) => true,
|
||||
})
|
||||
.filter(|r| match r {
|
||||
Ok((_, full_txo)) => match (confirmed, unconfirmed) {
|
||||
(true, false) => full_txo.chain_position.is_confirmed(),
|
||||
(false, true) => !full_txo.chain_position.is_confirmed(),
|
||||
_ => true,
|
||||
},
|
||||
// always keep errored items
|
||||
Err(_) => true,
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
for (spk_i, full_txo) in txouts {
|
||||
let addr = Address::from_script(&full_txo.txout.script_pubkey, network)?;
|
||||
println!(
|
||||
"{:?} {} {} {} spent:{:?}",
|
||||
spk_i, full_txo.txout.value, full_txo.outpoint, addr, full_txo.spent_by
|
||||
)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Commands::Send {
|
||||
value,
|
||||
address,
|
||||
coin_select,
|
||||
chain_specific,
|
||||
} => {
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let address = address.require_network(network)?;
|
||||
let (transaction, change_index) = {
|
||||
let graph = &mut *graph.lock().unwrap();
|
||||
// take mutable ref to construct tx -- it is only open for a short time while building it.
|
||||
let (tx, change_info) =
|
||||
create_tx(graph, chain, keymap, coin_select, address, value)?;
|
||||
|
||||
if let Some(CreateTxChange {
|
||||
index_changeset,
|
||||
change_keychain,
|
||||
index,
|
||||
}) = change_info
|
||||
{
|
||||
// We must first persist to disk the fact that we've got a new address from the
|
||||
// change keychain so future scans will find the tx we're about to broadcast.
|
||||
// If we're unable to persist this, then we don't want to broadcast.
|
||||
{
|
||||
let db = &mut *db.lock().unwrap();
|
||||
db.stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
indexed_tx_graph::ChangeSet::from(index_changeset),
|
||||
)))?;
|
||||
}
|
||||
|
||||
// We don't want other callers/threads to use this address while we're using it
|
||||
// but we also don't want to scan the tx we just created because it's not
|
||||
// technically in the blockchain yet.
|
||||
graph.index.mark_used(change_keychain, index);
|
||||
(tx, Some((change_keychain, index)))
|
||||
} else {
|
||||
(tx, None)
|
||||
}
|
||||
};
|
||||
|
||||
match (broadcast)(chain_specific, &transaction) {
|
||||
Ok(_) => {
|
||||
println!("Broadcasted Tx : {}", transaction.txid());
|
||||
|
||||
let keychain_changeset = graph.lock().unwrap().insert_tx(transaction);
|
||||
|
||||
// We know the tx is at least unconfirmed now. Note if persisting here fails,
|
||||
// it's not a big deal since we can always find it again form
|
||||
// blockchain.
|
||||
db.lock().unwrap().stage_and_commit(C::from((
|
||||
local_chain::ChangeSet::default(),
|
||||
keychain_changeset,
|
||||
)))?;
|
||||
Ok(())
|
||||
}
|
||||
Err(e) => {
|
||||
if let Some((keychain, index)) = change_index {
|
||||
// We failed to broadcast, so allow our change address to be used in the future
|
||||
graph.lock().unwrap().index.unmark_used(keychain, index);
|
||||
}
|
||||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The initial state returned by [`init`].
|
||||
pub struct Init<CS: clap::Subcommand, S: clap::Args, C> {
|
||||
/// Arguments parsed by the cli.
|
||||
pub args: Args<CS, S>,
|
||||
/// Descriptor keymap.
|
||||
pub keymap: KeyMap,
|
||||
/// Keychain-txout index.
|
||||
pub index: KeychainTxOutIndex<Keychain>,
|
||||
/// Persistence backend.
|
||||
pub db: Mutex<Database<C>>,
|
||||
/// Initial changeset.
|
||||
pub init_changeset: C,
|
||||
}
|
||||
|
||||
/// Parses command line arguments and initializes all components, creating
|
||||
/// a file store with the given parameters, or loading one if it exists.
|
||||
pub fn init<CS: clap::Subcommand, S: clap::Args, C>(
|
||||
db_magic: &[u8],
|
||||
db_default_path: &str,
|
||||
) -> anyhow::Result<Init<CS, S, C>>
|
||||
where
|
||||
C: Default + Append + Serialize + DeserializeOwned,
|
||||
{
|
||||
if std::env::var("BDK_DB_PATH").is_err() {
|
||||
std::env::set_var("BDK_DB_PATH", db_default_path);
|
||||
}
|
||||
let args = Args::<CS, S>::parse();
|
||||
let secp = Secp256k1::default();
|
||||
|
||||
let mut index = KeychainTxOutIndex::<Keychain>::default();
|
||||
|
||||
let (descriptor, mut keymap) =
|
||||
Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, &args.descriptor)?;
|
||||
index.add_keychain(Keychain::External, descriptor);
|
||||
|
||||
if let Some((internal_descriptor, internal_keymap)) = args
|
||||
.change_descriptor
|
||||
.as_ref()
|
||||
.map(|desc_str| Descriptor::<DescriptorPublicKey>::parse_descriptor(&secp, desc_str))
|
||||
.transpose()?
|
||||
{
|
||||
keymap.extend(internal_keymap);
|
||||
index.add_keychain(Keychain::Internal, internal_descriptor);
|
||||
}
|
||||
|
||||
let mut db_backend = match Store::<C>::open_or_create_new(db_magic, &args.db_path) {
|
||||
Ok(db_backend) => db_backend,
|
||||
// we cannot return `err` directly as it has lifetime `'m`
|
||||
Err(err) => return Err(anyhow::anyhow!("failed to init db backend: {:?}", err)),
|
||||
};
|
||||
|
||||
let init_changeset = db_backend.load_from_persistence()?.unwrap_or_default();
|
||||
|
||||
Ok(Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db: Mutex::new(Database::new(db_backend)),
|
||||
init_changeset,
|
||||
})
|
||||
}
|
||||
11
example-crates/example_electrum/Cargo.toml
Normal file
11
example-crates/example_electrum/Cargo.toml
Normal file
@@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "example_electrum"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
|
||||
bdk_electrum = { path = "../../crates/electrum" }
|
||||
example_cli = { path = "../example_cli" }
|
||||
334
example-crates/example_electrum/src/main.rs
Normal file
334
example-crates/example_electrum/src/main.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use std::{
|
||||
collections::BTreeMap,
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
Append, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bdk_electrum::{
|
||||
electrum_client::{self, Client, ElectrumApi},
|
||||
ElectrumExt, ElectrumUpdate,
|
||||
};
|
||||
use example_cli::{
|
||||
anyhow::{self, Context},
|
||||
clap::{self, Parser, Subcommand},
|
||||
Keychain,
|
||||
};
|
||||
|
||||
const DB_MAGIC: &[u8] = b"bdk_example_electrum";
|
||||
const DB_PATH: &str = ".bdk_example_electrum.db";
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum ElectrumCommands {
|
||||
/// Scans the addresses in the wallet using the electrum API.
|
||||
Scan {
|
||||
/// When a gap this large has been found for a keychain, it will stop.
|
||||
#[clap(long, default_value = "5")]
|
||||
stop_gap: usize,
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
#[clap(flatten)]
|
||||
electrum_args: ElectrumArgs,
|
||||
},
|
||||
/// Scans particular addresses using the electrum API.
|
||||
Sync {
|
||||
/// Scan all the unused addresses.
|
||||
#[clap(long)]
|
||||
unused_spks: bool,
|
||||
/// Scan every address that you have derived.
|
||||
#[clap(long)]
|
||||
all_spks: bool,
|
||||
/// Scan unspent outpoints for spends or changes to confirmation status of residing tx.
|
||||
#[clap(long)]
|
||||
utxos: bool,
|
||||
/// Scan unconfirmed transactions for updates.
|
||||
#[clap(long)]
|
||||
unconfirmed: bool,
|
||||
#[clap(flatten)]
|
||||
scan_options: ScanOptions,
|
||||
#[clap(flatten)]
|
||||
electrum_args: ElectrumArgs,
|
||||
},
|
||||
}
|
||||
|
||||
impl ElectrumCommands {
|
||||
fn electrum_args(&self) -> ElectrumArgs {
|
||||
match self {
|
||||
ElectrumCommands::Scan { electrum_args, .. } => electrum_args.clone(),
|
||||
ElectrumCommands::Sync { electrum_args, .. } => electrum_args.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Debug, Clone)]
|
||||
pub struct ElectrumArgs {
|
||||
/// The electrum url to use to connect to. If not provided it will use a default electrum server
|
||||
/// for your chosen network.
|
||||
electrum_url: Option<String>,
|
||||
}
|
||||
|
||||
impl ElectrumArgs {
|
||||
pub fn client(&self, network: Network) -> anyhow::Result<Client> {
|
||||
let electrum_url = self.electrum_url.as_deref().unwrap_or(match network {
|
||||
Network::Bitcoin => "ssl://electrum.blockstream.info:50002",
|
||||
Network::Testnet => "ssl://electrum.blockstream.info:60002",
|
||||
Network::Regtest => "tcp://localhost:60401",
|
||||
Network::Signet => "tcp://signet-electrumx.wakiyamap.dev:50001",
|
||||
_ => panic!("Unknown network"),
|
||||
});
|
||||
let config = electrum_client::Config::builder()
|
||||
.validate_domain(matches!(network, Network::Bitcoin))
|
||||
.build();
|
||||
|
||||
Ok(electrum_client::Client::from_config(electrum_url, config)?)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||
pub struct ScanOptions {
|
||||
/// Set batch size for each script_history call to electrum client.
|
||||
#[clap(long, default_value = "25")]
|
||||
pub batch_size: usize,
|
||||
}
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<ElectrumCommands, ElectrumArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let (disk_local_chain, disk_tx_graph) = init_changeset;
|
||||
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
graph.apply_changeset(disk_tx_graph);
|
||||
graph
|
||||
});
|
||||
|
||||
let chain = Mutex::new({
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&disk_local_chain)?;
|
||||
chain
|
||||
});
|
||||
|
||||
let electrum_cmd = match &args.command {
|
||||
example_cli::Commands::ChainSpecific(electrum_cmd) => electrum_cmd,
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
&keymap,
|
||||
args.network,
|
||||
|electrum_args, tx| {
|
||||
let client = electrum_args.client(args.network)?;
|
||||
client.transaction_broadcast(tx)?;
|
||||
Ok(())
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let client = electrum_cmd.electrum_args().client(args.network)?;
|
||||
|
||||
let response = match electrum_cmd.clone() {
|
||||
ElectrumCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let (keychain_spks, tip) = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
let chain = &*chain.lock().unwrap();
|
||||
|
||||
let keychain_spks = graph
|
||||
.index
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
|
||||
eprint!("{} ", i);
|
||||
let _ = io::stdout().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
let tip = chain.tip();
|
||||
(keychain_spks, tip)
|
||||
};
|
||||
|
||||
client
|
||||
.full_scan(tip, keychain_spks, stop_gap, scan_options.batch_size)
|
||||
.context("scanning the blockchain")?
|
||||
}
|
||||
ElectrumCommands::Sync {
|
||||
mut unused_spks,
|
||||
all_spks,
|
||||
mut utxos,
|
||||
mut unconfirmed,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
// Get a short lock on the tracker to get the spks we're interested in
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
if !(all_spks || unused_spks || utxos || unconfirmed) {
|
||||
unused_spks = true;
|
||||
unconfirmed = true;
|
||||
utxos = true;
|
||||
} else if all_spks {
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
let mut spks: Box<dyn Iterator<Item = bdk_chain::bitcoin::ScriptBuf>> =
|
||||
Box::new(core::iter::empty());
|
||||
if all_spks {
|
||||
let all_spks = graph
|
||||
.index
|
||||
.revealed_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(all_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!("scanning {}:{}", k, i);
|
||||
spk
|
||||
})));
|
||||
}
|
||||
if unused_spks {
|
||||
let unused_spks = graph
|
||||
.index
|
||||
.unused_spks()
|
||||
.map(|(k, i, spk)| (k, i, spk.to_owned()))
|
||||
.collect::<Vec<_>>();
|
||||
spks = Box::new(spks.chain(unused_spks.into_iter().map(|(k, i, spk)| {
|
||||
eprintln!(
|
||||
"Checking if address {} {}:{} has been used",
|
||||
Address::from_script(&spk, args.network).unwrap(),
|
||||
k,
|
||||
i,
|
||||
);
|
||||
spk
|
||||
})));
|
||||
}
|
||||
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
|
||||
if utxos {
|
||||
let init_outpoints = graph.index.outpoints().iter().cloned();
|
||||
|
||||
let utxos = graph
|
||||
.graph()
|
||||
.filter_chain_unspents(&*chain, chain_tip, init_outpoints)
|
||||
.map(|(_, utxo)| utxo)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
outpoints = Box::new(
|
||||
utxos
|
||||
.into_iter()
|
||||
.inspect(|utxo| {
|
||||
eprintln!(
|
||||
"Checking if outpoint {} (value: {}) has been spent",
|
||||
utxo.outpoint, utxo.txout.value
|
||||
);
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
};
|
||||
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
if unconfirmed {
|
||||
let unconfirmed_txids = graph
|
||||
.graph()
|
||||
.list_chain_txs(&*chain, chain_tip)
|
||||
.filter(|canonical_tx| !canonical_tx.chain_position.is_confirmed())
|
||||
.map(|canonical_tx| canonical_tx.tx_node.txid)
|
||||
.collect::<Vec<Txid>>();
|
||||
|
||||
txids = Box::new(unconfirmed_txids.into_iter().inspect(|txid| {
|
||||
eprintln!("Checking if {} is confirmed yet", txid);
|
||||
}));
|
||||
}
|
||||
|
||||
let tip = chain.tip();
|
||||
|
||||
// drop lock on graph and chain
|
||||
drop((graph, chain));
|
||||
|
||||
let electrum_update = client
|
||||
.sync(tip, spks, txids, outpoints, scan_options.batch_size)
|
||||
.context("scanning the blockchain")?;
|
||||
(electrum_update, BTreeMap::new())
|
||||
}
|
||||
};
|
||||
|
||||
let (
|
||||
ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
},
|
||||
keychain_update,
|
||||
) = response;
|
||||
|
||||
let missing_txids = {
|
||||
let graph = &*graph.lock().unwrap();
|
||||
relevant_txids.missing_full_txs(graph.graph())
|
||||
};
|
||||
|
||||
let now = std::time::UNIX_EPOCH
|
||||
.elapsed()
|
||||
.expect("must get time")
|
||||
.as_secs();
|
||||
|
||||
let graph_update = relevant_txids.into_tx_graph(&client, Some(now), missing_txids)?;
|
||||
|
||||
let db_changeset = {
|
||||
let mut chain = chain.lock().unwrap();
|
||||
let mut graph = graph.lock().unwrap();
|
||||
|
||||
let chain = chain.apply_update(chain_update)?;
|
||||
|
||||
let indexed_tx_graph = {
|
||||
let mut changeset =
|
||||
indexed_tx_graph::ChangeSet::<ConfirmationHeightAnchor, _>::default();
|
||||
let (_, indexer) = graph.index.reveal_to_target_multi(&keychain_update);
|
||||
changeset.append(indexed_tx_graph::ChangeSet {
|
||||
indexer,
|
||||
..Default::default()
|
||||
});
|
||||
changeset.append(graph.apply_update(graph_update));
|
||||
changeset
|
||||
};
|
||||
|
||||
(chain, indexed_tx_graph)
|
||||
};
|
||||
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage(db_changeset);
|
||||
db.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
12
example-crates/example_esplora/Cargo.toml
Normal file
12
example-crates/example_esplora/Cargo.toml
Normal file
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "example_esplora"
|
||||
version = "0.1.0"
|
||||
edition = "2021"
|
||||
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
bdk_chain = { path = "../../crates/chain", features = ["serde"] }
|
||||
bdk_esplora = { path = "../../crates/esplora", features = ["blocking"] }
|
||||
example_cli = { path = "../example_cli" }
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user