Compare commits
618 Commits
release/0.
...
v1.0.0-alp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ee21ffeee0 | ||
|
|
5f238d8e67 | ||
|
|
358e842dcd | ||
|
|
c7f87b50e4 | ||
|
|
446b045161 | ||
|
|
62619d3a4a | ||
|
|
984c758f96 | ||
|
|
a2a64ffb6e | ||
|
|
37fca35dde | ||
|
|
53791eb6c5 | ||
|
|
53942cced4 | ||
|
|
2d1d95a685 | ||
|
|
9a62d56900 | ||
|
|
2bb654077d | ||
|
|
19304c13ec | ||
|
|
798ed8ced2 | ||
|
|
b5557dce70 | ||
|
|
7b97c956c7 | ||
|
|
e5aa4fe9e6 | ||
|
|
2580013912 | ||
|
|
380bc4025a | ||
|
|
7c1861aab9 | ||
|
|
8ab58af093 | ||
|
|
80e190b3e7 | ||
|
|
7c9ba3cfc8 | ||
|
|
2462e90415 | ||
|
|
04d0ab5a97 | ||
|
|
4edf533b67 | ||
|
|
6e648fd5af | ||
|
|
a837cd349b | ||
|
|
0eb1ac2bcb | ||
|
|
6e8a4a8966 | ||
|
|
475a77219a | ||
|
|
0d64beb040 | ||
|
|
89608ddd0f | ||
|
|
09bd86e2d8 | ||
|
|
004957dc29 | ||
|
|
fc637a7bcc | ||
|
|
ec1c5f4cf8 | ||
|
|
06d7dc5c3a | ||
|
|
c01983d02a | ||
|
|
fef70d5e8f | ||
|
|
c3544c9b8c | ||
|
|
5840ce473e | ||
|
|
b290b29502 | ||
|
|
8c78a42163 | ||
|
|
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 | ||
|
|
e9bbb8724f |
1
.github/ISSUE_TEMPLATE/minor_release.md
vendored
1
.github/ISSUE_TEMPLATE/minor_release.md
vendored
@@ -34,6 +34,7 @@ Change the `master` branch to the next MINOR+1 version:
|
||||
- [ ] 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".
|
||||
|
||||
1
.github/ISSUE_TEMPLATE/patch_release.md
vendored
1
.github/ISSUE_TEMPLATE/patch_release.md
vendored
@@ -34,6 +34,7 @@ Change the `master` branch to the new PATCH+1 version:
|
||||
- [ ] 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".
|
||||
|
||||
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"
|
||||
54
.github/workflows/code_coverage.yml
vendored
54
.github/workflows/code_coverage.yml
vendored
@@ -9,47 +9,43 @@ jobs:
|
||||
env:
|
||||
RUSTFLAGS: "-Cinstrument-coverage"
|
||||
RUSTDOCFLAGS: "-Cinstrument-coverage"
|
||||
LLVM_PROFILE_FILE: "report-%p-%m.profraw"
|
||||
LLVM_PROFILE_FILE: "./target/coverage/%p-%m.profraw"
|
||||
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v2
|
||||
- name: Install lcov tools
|
||||
run: sudo apt-get install lcov -y
|
||||
- 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: Add llvm tools
|
||||
run: rustup component add llvm-tools-preview
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Cache cargo
|
||||
uses: actions/cache@v3
|
||||
- name: Install Rust toolchain
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
path: |
|
||||
~/.cargo/bin/
|
||||
~/.cargo/registry/index/
|
||||
~/.cargo/registry/cache/
|
||||
~/.cargo/git/db/
|
||||
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
|
||||
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
|
||||
# WARNING: this is not testing the following features: test-esplora, test-hardware-signer, async-interface
|
||||
# This is because some of our features are mutually exclusive, and generating various reports and
|
||||
# merging them doesn't seem to be working very well.
|
||||
# For more info, see:
|
||||
# - https://github.com/bitcoindevkit/bdk/issues/696
|
||||
# - https://github.com/bitcoindevkit/bdk/pull/748#issuecomment-1242721040
|
||||
run: cargo test --features all-keys,compact_filters,compiler,key-value-db,sqlite,sqlite-bundled,test-electrum,test-rpc,verify
|
||||
run: cargo test --all-features
|
||||
- name: Make coverage directory
|
||||
run: mkdir coverage
|
||||
- name: Run grcov
|
||||
run: mkdir coverage; grcov . --binary-path ./target/debug/ -s . -t lcov --branch --ignore-not-existing --ignore '/*' -o ./coverage/lcov.info
|
||||
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 ./coverage/lcov.info
|
||||
|
||||
run: genhtml -o coverage-report.html --ignore-errors source ./coverage/lcov.info
|
||||
- name: Coveralls upload
|
||||
uses: coverallsapp/github-action@master
|
||||
with:
|
||||
|
||||
222
.github/workflows/cont_integration.yml
vendored
222
.github/workflows/cont_integration.yml
vendored
@@ -10,118 +10,62 @@ 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-blocking
|
||||
- key-value-db
|
||||
- electrum
|
||||
- compact_filters
|
||||
- use-esplora-blocking,key-value-db,electrum
|
||||
- compiler
|
||||
- rpc
|
||||
- verify
|
||||
- async-interface
|
||||
- use-esplora-async
|
||||
- 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"
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
- 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-async,verify
|
||||
- name: esplora
|
||||
testprefix: esplora
|
||||
features: test-esplora,use-esplora-blocking,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 +76,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-async,dev-getrandom-wasm --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,42 +102,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
|
||||
|
||||
test_harware_wallet:
|
||||
clippy_check:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
matrix:
|
||||
rust:
|
||||
- version: 1.60.0 # STABLE
|
||||
- version: 1.56.1 # MSRV
|
||||
steps:
|
||||
- name: Checkout
|
||||
uses: actions/checkout@v3
|
||||
- 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: Set default toolchain
|
||||
run: rustup default ${{ matrix.rust.version }}
|
||||
- name: Set profile
|
||||
run: rustup set profile minimal
|
||||
- name: Update toolchain
|
||||
run: rustup update
|
||||
- name: Test
|
||||
run: cargo test --features test-hardware-signer
|
||||
- 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
|
||||
|
||||
18
.github/workflows/nightly_docs.yml
vendored
18
.github/workflows/nightly_docs.yml
vendored
@@ -9,22 +9,20 @@ 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: Pin dependencies for MSRV
|
||||
run: cargo update -p home --precise "0.5.5"
|
||||
- name: Build docs
|
||||
run: cargo rustdoc --verbose --features=compiler,electrum,esplora,use-esplora-blocking,compact_filters,rpc,key-value-db,sqlite,all-keys,verify,hardware-signer -- --cfg docsrs -Dwarnings
|
||||
run: cargo doc --no-deps
|
||||
env:
|
||||
RUSTDOCFLAGS: '--cfg docsrs -Dwarnings'
|
||||
- name: Upload artifact
|
||||
uses: actions/upload-artifact@v2
|
||||
with:
|
||||
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -4,3 +4,6 @@ Cargo.lock
|
||||
|
||||
*.swp
|
||||
.idea
|
||||
|
||||
# Example persisted files.
|
||||
*.db
|
||||
|
||||
212
CHANGELOG.md
212
CHANGELOG.md
@@ -1,13 +1,155 @@
|
||||
# Changelog
|
||||
All notable changes to this project prior to release **0.22.0** are documented in this file. Future
|
||||
changelog information can be found in each release's git tag and can be viewed with `git tag -ln100 "v*"`.
|
||||
Changelog info is also documented on the [GitHub releases](https://github.com/bitcoindevkit/bdk/releases)
|
||||
page. See [DEVELOPMENT_CYCLE.md](DEVELOPMENT_CYCLE.md) for more details.
|
||||
|
||||
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).
|
||||
|
||||
## [v0.21.0] - [v0.20.0]
|
||||
## [Unreleased]
|
||||
|
||||
## [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.
|
||||
@@ -16,11 +158,11 @@ 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)
|
||||
@@ -31,7 +173,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- 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`
|
||||
@@ -47,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.
|
||||
@@ -57,7 +199,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||
- Rename `WalletExport` to `FullyNodedExport`, deprecate the former.
|
||||
- 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`.
|
||||
@@ -78,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`.
|
||||
@@ -130,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
|
||||
@@ -159,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
|
||||
@@ -183,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
|
||||
@@ -199,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
|
||||
@@ -228,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
|
||||
@@ -265,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
|
||||
@@ -307,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
|
||||
@@ -375,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
|
||||
@@ -387,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
|
||||
@@ -408,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
|
||||
|
||||
@@ -493,3 +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
|
||||
[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
|
||||
|
||||
169
Cargo.toml
169
Cargo.toml
@@ -1,146 +1,25 @@
|
||||
[package]
|
||||
name = "bdk"
|
||||
version = "0.24.0"
|
||||
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 = "8.0", features = ["serde"] }
|
||||
bitcoin = { version = "0.29.1", features = ["serde", "base64", "rand"] }
|
||||
serde = { version = "^1.0", features = ["derive"] }
|
||||
serde_json = { version = "^1.0" }
|
||||
rand = "^0.8"
|
||||
|
||||
# Optional dependencies
|
||||
sled = { version = "0.34", optional = true }
|
||||
electrum-client = { version = "0.12", optional = true }
|
||||
esplora-client = { version = "0.2", default-features = false, optional = true }
|
||||
rusqlite = { version = "0.27.0", optional = true }
|
||||
ahash = { version = "0.7.6", 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 }
|
||||
hwi = { version = "0.3.0", 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.16", optional = true }
|
||||
|
||||
# Platform-specific dependencies
|
||||
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
|
||||
tokio = { version = "1", features = ["rt"] }
|
||||
|
||||
[target.'cfg(target_arch = "wasm32")'.dependencies]
|
||||
getrandom = "0.2"
|
||||
async-trait = "0.1"
|
||||
js-sys = "0.3"
|
||||
|
||||
[features]
|
||||
minimal = []
|
||||
compiler = ["miniscript/compiler"]
|
||||
verify = ["bitcoinconsensus"]
|
||||
default = ["key-value-db", "electrum"]
|
||||
sqlite = ["rusqlite", "ahash"]
|
||||
sqlite-bundled = ["sqlite", "rusqlite/bundled"]
|
||||
compact_filters = ["rocksdb", "socks", "cc"]
|
||||
key-value-db = ["sled"]
|
||||
all-keys = ["keys-bip39"]
|
||||
keys-bip39 = ["bip39"]
|
||||
rpc = ["bitcoincore-rpc"]
|
||||
hardware-signer = ["hwi"]
|
||||
|
||||
# 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 the `use-esplora-async` feature.
|
||||
# - 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 the `use-esplora-blocking` feature.
|
||||
#
|
||||
# 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-async = ["esplora", "esplora-client/async", "futures"]
|
||||
use-esplora-blocking = ["esplora", "esplora-client/blocking"]
|
||||
# Deprecated aliases
|
||||
use-esplora-reqwest = ["use-esplora-async"]
|
||||
use-esplora-ureq = ["use-esplora-blocking"]
|
||||
# Typical configurations will not need to use `esplora` feature directly.
|
||||
esplora = []
|
||||
|
||||
# Use below feature with `use-esplora-async` to enable reqwest default TLS support
|
||||
reqwest-default-tls = ["esplora-client/async-https"]
|
||||
|
||||
# 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"]
|
||||
test-hardware-signer = ["hardware-signer"]
|
||||
|
||||
# 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"
|
||||
env_logger = "0.7"
|
||||
electrsd = "0.21"
|
||||
# Move back to importing from rust-bitcoin once https://github.com/rust-bitcoin/rust-bitcoin/pull/1342 is released
|
||||
base64 = "^0.13"
|
||||
|
||||
[[example]]
|
||||
name = "compact_filters_balance"
|
||||
required-features = ["compact_filters"]
|
||||
|
||||
[[example]]
|
||||
name = "miniscriptc"
|
||||
path = "examples/compiler.rs"
|
||||
required-features = ["compiler"]
|
||||
|
||||
[[example]]
|
||||
name = "policy"
|
||||
path = "examples/policy.rs"
|
||||
|
||||
[[example]]
|
||||
name = "rpcwallet"
|
||||
path = "examples/rpcwallet.rs"
|
||||
required-features = ["keys-bip39", "key-value-db", "rpc", "electrsd/bitcoind_22_0"]
|
||||
|
||||
[[example]]
|
||||
name = "psbt_signer"
|
||||
path = "examples/psbt_signer.rs"
|
||||
required-features = ["electrum"]
|
||||
|
||||
[[example]]
|
||||
name = "hardware_signer"
|
||||
path = "examples/hardware_signer.rs"
|
||||
required-features = ["electrum", "hardware-signer"]
|
||||
|
||||
[workspace]
|
||||
members = ["macros"]
|
||||
[package.metadata.docs.rs]
|
||||
features = ["compiler", "electrum", "esplora", "use-esplora-blocking", "compact_filters", "rpc", "key-value-db", "sqlite", "all-keys", "verify", "hardware-signer"]
|
||||
# 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",
|
||||
"crates/testenv",
|
||||
"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"]
|
||||
|
||||
206
README.md
206
README.md
@@ -1,3 +1,5 @@
|
||||
# The Bitcoin Dev Kit
|
||||
|
||||
<div align="center">
|
||||
<h1>BDK</h1>
|
||||
|
||||
@@ -13,7 +15,7 @@
|
||||
<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/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,175 +28,69 @@
|
||||
|
||||
## 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"
|
||||
# proptest 1.4.0 has MSRV 1.65.0
|
||||
cargo update -p proptest --precise "1.2.0"
|
||||
```
|
||||
|
||||
### 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 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 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.
|
||||
|
||||
## Running under WASM
|
||||
|
||||
If you want to run this library under WASM you will probably have to add the following lines to you `Cargo.toml`:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
getrandom = { version = "0.2", features = ["js"] }
|
||||
```
|
||||
|
||||
This enables the `rand` crate to work in environments where JavaScript is available. See [this link](https://docs.rs/getrandom/0.2.8/getrandom/#webassembly-support) to learn more.
|
||||
|
||||
## 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.
|
||||
|
||||
@@ -6,4 +6,4 @@ 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", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
|
||||
ENTRYPOINT ["python", "./speculos.py", "--automation", "file:automation.json", "--model", "nanos", "--display", "headless", "--vnc-port", "41000", "btc.elf"]
|
||||
|
||||
1
clippy.toml
Normal file
1
clippy.toml
Normal file
@@ -0,0 +1 @@
|
||||
msrv="1.63.0"
|
||||
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.9"
|
||||
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 = "11.0.0", features = ["serde"], default-features = false }
|
||||
bitcoin = { version = "0.31.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.12.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"]
|
||||
228
crates/bdk/README.md
Normal file
228
crates/bdk/README.md
Normal file
@@ -0,0 +1,228 @@
|
||||
<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`] 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 details about the API of `Wallet` see the [module-level documentation][`Wallet`].
|
||||
|
||||
### Blockchain data
|
||||
|
||||
In order to get blockchain data for `Wallet` to consume, you should configure a client from
|
||||
an available chain source. Typically you make a request to the chain source and get a response
|
||||
that the `Wallet` can use to update its view of the chain.
|
||||
|
||||
**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.
|
||||
* [`bdk_bitcoind_rpc`]: Grabs blockchain data from Bitcoin Core for updating BDK structures.
|
||||
|
||||
**Examples**
|
||||
|
||||
* [`example-crates/wallet_esplora_async`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_async)
|
||||
* [`example-crates/wallet_esplora_blocking`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_esplora_blocking)
|
||||
* [`example-crates/wallet_electrum`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_electrum)
|
||||
* [`example-crates/wallet_rpc`](https://github.com/bitcoindevkit/bdk/tree/master/example-crates/wallet_rpc)
|
||||
|
||||
### Persistence
|
||||
|
||||
To persist the `Wallet` on disk, it must be constructed with a [`PersistBackend`] implementation.
|
||||
|
||||
**Implementations**
|
||||
|
||||
* [`bdk_file_store`]: A simple flat-file implementation of [`PersistBackend`].
|
||||
|
||||
**Example**
|
||||
|
||||
<!-- compile_fail because outpoint and txout are fake variables -->
|
||||
```rust,compile_fail
|
||||
use bdk::{bitcoin::Network, wallet::{ChangeSet, Wallet}};
|
||||
|
||||
fn main() {
|
||||
// Create a new file `Store`.
|
||||
let db = bdk_file_store::Store::<ChangeSet>::open_or_create_new(b"magic_bytes", "path/to/my_wallet.db").expect("create store");
|
||||
|
||||
let descriptor = "wpkh(tprv8ZgxMBicQKsPdcAqYBpzAFwU5yxBUo88ggoBqu1qPcHUfSbKK1sKMLmC7EAk438btHQrSdu3jGGQa6PA71nvH5nkDexhLteJqkM4dQmWF9g/84'/1'/0'/0/*)";
|
||||
let mut wallet = Wallet::new_or_load(descriptor, None, db, Network::Testnet).expect("create or load wallet");
|
||||
|
||||
// Insert a single `TxOut` at `OutPoint` into the wallet.
|
||||
let _ = wallet.insert_txout(outpoint, txout);
|
||||
wallet.commit().expect("must write to database");
|
||||
}
|
||||
```
|
||||
|
||||
<!-- ### 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.
|
||||
|
||||
[`Wallet`]: https://docs.rs/bdk/1.0.0-alpha.7/bdk/wallet/struct.Wallet.html
|
||||
[`PersistBackend`]: https://docs.rs/bdk_chain/latest/bdk_chain/trait.PersistBackend.html
|
||||
[`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
|
||||
[`bdk_bitcoind_rpc`]: https://docs.rs/bdk_bitcoind_rpc/latest
|
||||
[`rust-miniscript`]: https://docs.rs/miniscript/latest/miniscript/index.html
|
||||
@@ -11,25 +11,21 @@
|
||||
|
||||
extern crate bdk;
|
||||
extern crate bitcoin;
|
||||
extern crate log;
|
||||
extern crate miniscript;
|
||||
extern crate serde_json;
|
||||
|
||||
use std::error::Error;
|
||||
use std::str::FromStr;
|
||||
|
||||
use log::info;
|
||||
|
||||
use bitcoin::Network;
|
||||
use miniscript::policy::Concrete;
|
||||
use miniscript::Descriptor;
|
||||
|
||||
use bdk::database::memory::MemoryDatabase;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{KeychainKind, Wallet};
|
||||
|
||||
/// Miniscript policy is a high level abstraction of spending conditions. Defined in the
|
||||
/// rust-miscript library here https://docs.rs/miniscript/7.0.0/miniscript/policy/index.html
|
||||
/// 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.
|
||||
@@ -37,13 +33,9 @@ use bdk::{KeychainKind, Wallet};
|
||||
/// This example demonstrates the interaction between a bdk wallet and miniscript policy.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
||||
// 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))))";
|
||||
info!("Compiling policy: \n{}", policy_str);
|
||||
println!("Compiling policy: \n{}", policy_str);
|
||||
|
||||
// Parse the string as a [`Concrete`] type miniscript policy.
|
||||
let policy = Concrete::<String>::from_str(policy_str)?;
|
||||
@@ -52,22 +44,20 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
// `policy.compile()` returns the resulting miniscript from the policy.
|
||||
let descriptor = Descriptor::new_wsh(policy.compile()?)?;
|
||||
|
||||
info!("Compiled into following Descriptor: \n{}", descriptor);
|
||||
|
||||
let database = MemoryDatabase::new();
|
||||
println!("Compiled into following Descriptor: \n{}", descriptor);
|
||||
|
||||
// Create a new wallet from this descriptor
|
||||
let wallet = Wallet::new(&format!("{}", descriptor), None, Network::Regtest, database)?;
|
||||
let mut wallet = Wallet::new_no_persist(&format!("{}", descriptor), None, Network::Regtest)?;
|
||||
|
||||
info!(
|
||||
println!(
|
||||
"First derived address from the descriptor: \n{}",
|
||||
wallet.get_address(New)?
|
||||
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)?;
|
||||
info!(
|
||||
println!(
|
||||
"The BDK spending policy: \n{}",
|
||||
serde_json::to_string_pretty(&spending_policy)?
|
||||
);
|
||||
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(())
|
||||
}
|
||||
@@ -10,8 +10,6 @@
|
||||
// licenses.
|
||||
|
||||
extern crate bdk;
|
||||
extern crate env_logger;
|
||||
extern crate log;
|
||||
use std::error::Error;
|
||||
|
||||
use bdk::bitcoin::Network;
|
||||
@@ -29,10 +27,6 @@ use bdk::wallet::signer::SignersContainer;
|
||||
/// one of the Extend Private key.
|
||||
|
||||
fn main() -> Result<(), Box<dyn Error>> {
|
||||
env_logger::init_from_env(
|
||||
env_logger::Env::default().filter_or(env_logger::DEFAULT_FILTER_ENV, "info"),
|
||||
);
|
||||
|
||||
let secp = bitcoin::secp256k1::Secp256k1::new();
|
||||
|
||||
// The descriptor used in the example
|
||||
@@ -48,7 +42,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
// But they can be used as independent tools also.
|
||||
let (wallet_desc, keymap) = desc.into_wallet_descriptor(&secp, Network::Testnet)?;
|
||||
|
||||
log::info!("Example Descriptor for policy analysis : {}", wallet_desc);
|
||||
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);
|
||||
@@ -60,7 +54,7 @@ fn main() -> Result<(), Box<dyn Error>> {
|
||||
.extract_policy(&signers_container, BuildSatisfaction::None, &secp)?
|
||||
.expect("We expect a policy");
|
||||
|
||||
log::info!("Derived Policy for the descriptor {:#?}", policy);
|
||||
println!("Derived Policy for the descriptor {:#?}", policy);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
//! 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";
|
||||
@@ -41,22 +42,16 @@ fn poly_mod(mut c: u64, val: u64) -> u64 {
|
||||
c
|
||||
}
|
||||
|
||||
/// Computes the checksum bytes of a descriptor.
|
||||
/// `exclude_hash = true` ignores all data after the first '#' (inclusive).
|
||||
pub(crate) fn calc_checksum_bytes_internal(
|
||||
mut desc: &str,
|
||||
exclude_hash: bool,
|
||||
) -> Result<[u8; 8], DescriptorError> {
|
||||
/// 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 exclude_hash {
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
if let Some(split) = desc.split_once('#') {
|
||||
desc = split.0;
|
||||
original_checksum = Some(split.1);
|
||||
}
|
||||
|
||||
for ch in desc.as_bytes() {
|
||||
@@ -94,45 +89,17 @@ pub(crate) fn calc_checksum_bytes_internal(
|
||||
Ok(checksum)
|
||||
}
|
||||
|
||||
/// Compute the checksum bytes of a descriptor, excludes any existing checksum in the descriptor string from the calculation
|
||||
pub fn calc_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, true)
|
||||
}
|
||||
|
||||
/// 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_internal(desc, true)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
}
|
||||
|
||||
// TODO in release 0.25.0, remove get_checksum_bytes and get_checksum
|
||||
// TODO in release 0.25.0, consolidate calc_checksum_bytes_internal into calc_checksum_bytes
|
||||
|
||||
/// Compute the checksum bytes of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum_bytes` function which excludes any existing checksum in the descriptor string before calculating the checksum hash bytes. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum_bytes(desc: &str) -> Result<[u8; 8], DescriptorError> {
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
}
|
||||
|
||||
/// Compute the checksum of a descriptor
|
||||
#[deprecated(
|
||||
since = "0.24.0",
|
||||
note = "Use new `calc_checksum` function which excludes any existing checksum in the descriptor string before calculating the checksum hash. See https://github.com/bitcoindevkit/bdk/pull/765."
|
||||
)]
|
||||
pub fn get_checksum(desc: &str) -> Result<String, DescriptorError> {
|
||||
// unsafe is okay here as the checksum only uses bytes in `CHECKSUM_CHARSET`
|
||||
calc_checksum_bytes_internal(desc, false)
|
||||
.map(|b| unsafe { String::from_utf8_unchecked(b.to_vec()) })
|
||||
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]
|
||||
@@ -155,26 +122,26 @@ mod test {
|
||||
assert_eq!(calc_checksum(desc).unwrap(), "lasegmfs");
|
||||
|
||||
let desc = "wpkh(tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N/1/2/*)#tqz0nc26";
|
||||
assert!(matches!(
|
||||
calc_checksum(desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
|
||||
let desc = "pkh(tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK/44'/1'/0'/0/*)#lasegmsf";
|
||||
assert!(matches!(
|
||||
calc_checksum(desc).err(),
|
||||
Some(DescriptorError::InvalidDescriptorChecksum)
|
||||
));
|
||||
assert_matches!(
|
||||
calc_checksum(desc),
|
||||
Err(DescriptorError::InvalidDescriptorChecksum)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_calc_checksum_invalid_character() {
|
||||
let sparkle_heart = unsafe { std::str::from_utf8_unchecked(&[240, 159, 146, 150]) };
|
||||
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(),
|
||||
Some(DescriptorError::InvalidDescriptorCharacter(invalid_char)) if invalid_char == sparkle_heart.as_bytes()[0]
|
||||
));
|
||||
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,14 +274,13 @@ macro_rules! impl_sortedmulti {
|
||||
#[macro_export]
|
||||
macro_rules! parse_tap_tree {
|
||||
( @merge $tree_a:expr, $tree_b:expr) => {{
|
||||
use std::sync::Arc;
|
||||
use $crate::miniscript::descriptor::TapTree;
|
||||
|
||||
$tree_a
|
||||
.and_then(|tree_a| Ok((tree_a, $tree_b?)))
|
||||
.and_then(|((a_tree, mut a_keymap, a_networks), (b_tree, b_keymap, b_networks))| {
|
||||
a_keymap.extend(b_keymap.into_iter());
|
||||
Ok((TapTree::Tree(Arc::new(a_tree), Arc::new(b_tree)), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||
Ok((TapTree::combine(a_tree, b_tree), a_keymap, $crate::keys::merge_networks(&a_networks, &b_networks)))
|
||||
})
|
||||
|
||||
}};
|
||||
@@ -318,7 +317,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 +336,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 +373,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 +383,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 +393,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 +494,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 +515,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 +532,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 +601,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,7 +702,7 @@ macro_rules! fragment {
|
||||
$crate::keys::make_pkh($key, &secp)
|
||||
});
|
||||
( after ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::bitcoin::PackedLockTime($value)) // TODO!! https://github.com/rust-bitcoin/rust-bitcoin/issues/1302
|
||||
$crate::impl_leaf_opcode_value!(After, $crate::miniscript::AbsLockTime::from_consensus($value))
|
||||
});
|
||||
( older ( $value:expr ) ) => ({
|
||||
$crate::impl_leaf_opcode_value!(Older, $crate::bitcoin::Sequence($value)) // TODO!!
|
||||
@@ -744,8 +746,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 +762,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,17 +795,17 @@ macro_rules! fragment {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use bitcoin::hashes::hex::ToHex;
|
||||
use alloc::string::ToString;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
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::network::constants::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||
use bitcoin::util::bip32;
|
||||
use bitcoin::bip32;
|
||||
use bitcoin::Network::{Bitcoin, Regtest, Signet, Testnet};
|
||||
use bitcoin::PrivateKey;
|
||||
|
||||
// test the descriptor!() macro
|
||||
@@ -819,18 +821,15 @@ mod test {
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
assert_eq!(!desc.has_wildcard(), is_fixed);
|
||||
for i in 0..expected.len() {
|
||||
let index = i as u32;
|
||||
let child_desc = if !desc.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
} else {
|
||||
desc.at_derivation_index(index)
|
||||
};
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -936,7 +935,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_bip32_legacy_descriptors() {
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
@@ -981,7 +980,7 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_bip32_segwitv0_descriptors() {
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path.clone()).into_descriptor_key().unwrap();
|
||||
@@ -1038,10 +1037,10 @@ mod test {
|
||||
|
||||
#[test]
|
||||
fn test_dsl_sortedmulti() {
|
||||
let key_1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let key_1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path_1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
|
||||
let key_2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let key_2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let path_2 = bip32::DerivationPath::from_str("m/1").unwrap();
|
||||
|
||||
let desc_key1 = (key_1, path_1);
|
||||
@@ -1097,7 +1096,7 @@ mod test {
|
||||
// - verify the valid_networks returned is correctly computed based on the keys present in the descriptor
|
||||
#[test]
|
||||
fn test_valid_networks() {
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1107,7 +1106,7 @@ mod test {
|
||||
[Testnet, Regtest, Signet].iter().cloned().collect()
|
||||
);
|
||||
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||
let xprv = bip32::Xpriv::from_str("xprv9s21ZrQH143K3QTDL4LXw2F7HEK3wJUD2nW2nRk4stbPy6cq3jPPqjiChkVvvNKmPGJxWUtg6LnF5kejMRNNU3TGtRBeJgk33yuGBxrMPHi").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||
let desc_key = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1120,15 +1119,15 @@ mod test {
|
||||
fn test_key_maps_merged() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xprv1 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv1 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path1 = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key1 = (xprv1, path1.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
let xprv2 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let xprv2 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPegBHHnq7YEgM815dG24M2Jk5RVqipgDxF1HJ1tsnT815X5Fd5FRfMVUs8NZs9XCb6y9an8hRPThnhfwfXJ36intaekySHGF").unwrap();
|
||||
let path2 = bip32::DerivationPath::from_str("m/2147483647'/0").unwrap();
|
||||
let desc_key2 = (xprv2, path2.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
let xprv3 = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||
let xprv3 = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf").unwrap();
|
||||
let path3 = bip32::DerivationPath::from_str("m/10/20/30/40").unwrap();
|
||||
let desc_key3 = (xprv3, path3.clone()).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1152,7 +1151,7 @@ mod test {
|
||||
#[test]
|
||||
fn test_script_context_validation() {
|
||||
// this compiles
|
||||
let xprv = bip32::ExtendedPrivKey::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let xprv = bip32::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/0").unwrap();
|
||||
let desc_key: DescriptorKey<Legacy> = (xprv, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -1175,9 +1174,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::hex::HexToBytesError),
|
||||
}
|
||||
|
||||
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::hex::HexToBytesError> for Error {
|
||||
fn from(err: bitcoin::hex::HexToBytesError) -> Self {
|
||||
Error::Hex(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<crate::descriptor::policy::PolicyError> for Error {
|
||||
fn from(err: crate::descriptor::policy::PolicyError) -> Self {
|
||||
Error::Policy(err)
|
||||
}
|
||||
}
|
||||
@@ -14,17 +14,21 @@
|
||||
//! This module contains generic utilities to work with descriptors, plus some re-exported types
|
||||
//! from [`miniscript`].
|
||||
|
||||
use std::collections::BTreeMap;
|
||||
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::bip32::{ChildNumber, DerivationPath, Fingerprint, KeySource, Xpub};
|
||||
use bitcoin::{key::XOnlyPublicKey, secp256k1, PublicKey};
|
||||
use bitcoin::{psbt, taproot};
|
||||
use bitcoin::{Network, TxOut};
|
||||
|
||||
use miniscript::descriptor::{DefiniteDescriptorKey, DescriptorType, InnerXKey, SinglePubKey};
|
||||
use miniscript::descriptor::{
|
||||
DefiniteDescriptorKey, DescriptorMultiXKey, DescriptorSecretKey, DescriptorType,
|
||||
DescriptorXKey, InnerXKey, KeyMap, SinglePubKey, Wildcard,
|
||||
};
|
||||
pub use miniscript::{
|
||||
descriptor::DescriptorXKey, descriptor::KeyMap, descriptor::Wildcard, Descriptor,
|
||||
DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
Descriptor, DescriptorPublicKey, Legacy, Miniscript, ScriptContext, Segwitv0,
|
||||
};
|
||||
use miniscript::{ForEachKey, MiniscriptKey, TranslatePk};
|
||||
|
||||
@@ -55,16 +59,16 @@ 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 {
|
||||
@@ -132,14 +136,10 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
network: Network,
|
||||
}
|
||||
|
||||
impl<'s, 'd>
|
||||
miniscript::Translator<DescriptorPublicKey, miniscript::DummyKey, DescriptorError>
|
||||
impl<'s, 'd> miniscript::Translator<DescriptorPublicKey, String, DescriptorError>
|
||||
for Translator<'s, 'd>
|
||||
{
|
||||
fn pk(
|
||||
&mut self,
|
||||
pk: &DescriptorPublicKey,
|
||||
) -> Result<miniscript::DummyKey, DescriptorError> {
|
||||
fn pk(&mut self, pk: &DescriptorPublicKey) -> Result<String, DescriptorError> {
|
||||
let secp = &self.secp;
|
||||
|
||||
let (_, _, networks) = if self.descriptor.is_taproot() {
|
||||
@@ -157,7 +157,7 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
};
|
||||
|
||||
if networks.contains(&self.network) {
|
||||
Ok(miniscript::DummyKey)
|
||||
Ok(Default::default())
|
||||
} else {
|
||||
Err(DescriptorError::Key(KeyError::InvalidNetwork))
|
||||
}
|
||||
@@ -165,35 +165,40 @@ impl IntoWalletDescriptor for (ExtendedDescriptor, KeyMap) {
|
||||
fn sha256(
|
||||
&mut self,
|
||||
_sha256: &<DescriptorPublicKey as MiniscriptKey>::Sha256,
|
||||
) -> Result<miniscript::DummySha256Hash, DescriptorError> {
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash256(
|
||||
&mut self,
|
||||
_hash256: &<DescriptorPublicKey as MiniscriptKey>::Hash256,
|
||||
) -> Result<miniscript::DummyHash256Hash, DescriptorError> {
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn ripemd160(
|
||||
&mut self,
|
||||
_ripemd160: &<DescriptorPublicKey as MiniscriptKey>::Ripemd160,
|
||||
) -> Result<miniscript::DummyRipemd160Hash, DescriptorError> {
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
fn hash160(
|
||||
&mut self,
|
||||
_hash160: &<DescriptorPublicKey as MiniscriptKey>::Hash160,
|
||||
) -> Result<miniscript::DummyHash160Hash, DescriptorError> {
|
||||
) -> Result<String, DescriptorError> {
|
||||
Ok(Default::default())
|
||||
}
|
||||
}
|
||||
|
||||
// check the network for the keys
|
||||
self.0.translate_pk(&mut Translator {
|
||||
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(self)
|
||||
}
|
||||
@@ -240,14 +245,39 @@ impl IntoWalletDescriptor for DescriptorTemplateOut {
|
||||
);
|
||||
}
|
||||
|
||||
if !self.2.contains(&network) {
|
||||
let (desc, keymap, networks) = self;
|
||||
|
||||
if !networks.contains(&network) {
|
||||
return Err(DescriptorError::Key(KeyError::InvalidNetwork));
|
||||
}
|
||||
|
||||
// fixup the network for keys that need it
|
||||
let translated = self.0.translate_pk(&mut Translator { network })?;
|
||||
// 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;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
Ok((translated, self.1))
|
||||
(k, v)
|
||||
})
|
||||
.collect();
|
||||
|
||||
Ok((translated, fixed_keymap))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -278,6 +308,10 @@ pub(crate) fn into_wallet_descriptor_checked<T: IntoWalletDescriptor>(
|
||||
return Err(DescriptorError::HardenedDerivationXpub);
|
||||
}
|
||||
|
||||
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()?;
|
||||
@@ -316,6 +350,18 @@ pub(crate) trait XKeyUtils {
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint;
|
||||
}
|
||||
|
||||
impl<T> XKeyUtils for DescriptorMultiXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
{
|
||||
fn root_fingerprint(&self, secp: &SecpCtx) -> Fingerprint {
|
||||
match self.origin {
|
||||
Some((fingerprint, _)) => fingerprint,
|
||||
None => self.xkey.xkey_fingerprint(secp),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> XKeyUtils for DescriptorXKey<T>
|
||||
where
|
||||
T: InnerXKey,
|
||||
@@ -331,27 +377,27 @@ where
|
||||
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<Xpub>>;
|
||||
fn derive_from_hd_keypaths(
|
||||
&self,
|
||||
hd_keypaths: &HdKeyPaths,
|
||||
secp: &'s SecpCtx,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_tap_key_origins<'s>(
|
||||
fn derive_from_tap_key_origins(
|
||||
&self,
|
||||
tap_key_origins: &TapKeyOrigins,
|
||||
secp: &'s SecpCtx,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_key_origins<'s>(
|
||||
fn derive_from_psbt_key_origins(
|
||||
&self,
|
||||
key_origins: BTreeMap<Fingerprint, (&DerivationPath, SinglePubKey)>,
|
||||
secp: &'s SecpCtx,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
fn derive_from_psbt_input<'s>(
|
||||
fn derive_from_psbt_input(
|
||||
&self,
|
||||
psbt_input: &psbt::Input,
|
||||
utxo: Option<TxOut>,
|
||||
secp: &'s SecpCtx,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor>;
|
||||
}
|
||||
|
||||
@@ -372,7 +418,7 @@ 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<Xpub>> {
|
||||
let mut answer = Vec::new();
|
||||
|
||||
self.for_each_key(|pk| {
|
||||
@@ -383,30 +429,29 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
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,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// Ensure that deriving `xpub` with `path` yields `expected`
|
||||
let verify_key = |xpub: &DescriptorXKey<ExtendedPubKey>,
|
||||
path: &DerivationPath,
|
||||
expected: &SinglePubKey| {
|
||||
let derived = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, path)
|
||||
.expect("The path should never contain hardened derivation steps")
|
||||
.public_key;
|
||||
let verify_key =
|
||||
|xpub: &DescriptorXKey<Xpub>, path: &DerivationPath, expected: &SinglePubKey| {
|
||||
let derived = xpub
|
||||
.xkey
|
||||
.derive_pub(secp, path)
|
||||
.expect("The path should never contain hardened derivation steps")
|
||||
.public_key;
|
||||
|
||||
match expected {
|
||||
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
match expected {
|
||||
SinglePubKey::FullKey(pk) if &PublicKey::new(derived) == pk => true,
|
||||
SinglePubKey::XOnly(pk) if &XOnlyPublicKey::from(derived) == pk => true,
|
||||
_ => false,
|
||||
}
|
||||
};
|
||||
|
||||
let mut path_found = None;
|
||||
|
||||
@@ -442,11 +487,6 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
) {
|
||||
Some(derive_path)
|
||||
} else {
|
||||
log::debug!(
|
||||
"Key `{}` derived with {} yields an unexpected key",
|
||||
root_fingerprint,
|
||||
derive_path
|
||||
);
|
||||
None
|
||||
}
|
||||
});
|
||||
@@ -470,13 +510,16 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
false
|
||||
});
|
||||
|
||||
path_found.map(|path| self.at_derivation_index(path))
|
||||
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,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
// "Convert" an hd_keypaths map to the format required by `derive_from_psbt_key_origins`
|
||||
let key_origins = hd_keypaths
|
||||
@@ -491,10 +534,10 @@ 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,
|
||||
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
|
||||
@@ -504,11 +547,11 @@ 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,
|
||||
secp: &SecpCtx,
|
||||
) -> Option<DerivedDescriptor> {
|
||||
if let Some(derived) = self.derive_from_hd_keypaths(&psbt_input.bip32_derivation, secp) {
|
||||
return Some(derived);
|
||||
@@ -521,7 +564,7 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
return None;
|
||||
}
|
||||
|
||||
let descriptor = self.at_derivation_index(0);
|
||||
let descriptor = self.at_derivation_index(0).expect("0 is not hardened");
|
||||
match descriptor.desc_type() {
|
||||
// TODO: add pk() here
|
||||
DescriptorType::Pkh
|
||||
@@ -557,13 +600,14 @@ impl DescriptorMeta for ExtendedDescriptor {
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::str::FromStr;
|
||||
use alloc::string::ToString;
|
||||
use core::str::FromStr;
|
||||
|
||||
use bitcoin::consensus::encode::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use assert_matches::assert_matches;
|
||||
use bitcoin::hex::FromHex;
|
||||
use bitcoin::secp256k1::Secp256k1;
|
||||
use bitcoin::util::{bip32, psbt};
|
||||
use bitcoin::Script;
|
||||
use bitcoin::ScriptBuf;
|
||||
use bitcoin::{bip32, Psbt};
|
||||
|
||||
use super::*;
|
||||
use crate::psbt::PsbtUtils;
|
||||
@@ -574,7 +618,7 @@ mod test {
|
||||
"wpkh(02b4632d08485ff1df2db55b9dafd23347d1c47a457072a1e87be26896549a8737)",
|
||||
)
|
||||
.unwrap();
|
||||
let psbt: psbt::PartiallySignedTransaction = deserialize(
|
||||
let psbt = Psbt::deserialize(
|
||||
&Vec::<u8>::from_hex(
|
||||
"70736274ff010052010000000162307be8e431fbaff807cdf9cdc3fde44d7402\
|
||||
11bc8342c31ffd6ec11fe35bcc0100000000ffffffff01328601000000000016\
|
||||
@@ -597,7 +641,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\
|
||||
@@ -628,7 +672,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\
|
||||
@@ -652,7 +696,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\
|
||||
@@ -682,23 +726,40 @@ mod test {
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let xpub = bip32::ExtendedPubKey::from_str("xpub6ERApfZwUNrhLCkDtcHTcxd75RbzS1ed54G1LkBUHQVHQKqhMkhgbmJbZRkrgZw4koxb5JaHWkY4ALHY2grBGRjaDMzQLcgJvLJuZZvRcEL").unwrap();
|
||||
let xprv = bip32::Xpriv::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::Xpub::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
|
||||
@@ -724,17 +785,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
|
||||
@@ -768,17 +823,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
|
||||
@@ -786,7 +835,7 @@ mod test {
|
||||
fn test_descriptor_from_str_from_output_of_macro() {
|
||||
let secp = Secp256k1::new();
|
||||
|
||||
let tpub = bip32::ExtendedPubKey::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||
let tpub = bip32::Xpub::from_str("tpubD6NzVbkrYhZ4XHndKkuB8FifXm8r5FQHwrN6oZuWCz13qb93rtgKvD4PQsqC4HP4yhV3tA2fqr2RbY5mNXfM7RxXUoeABoDtsFUq2zJq6YK").unwrap();
|
||||
let path = bip32::DerivationPath::from_str("m/1/2").unwrap();
|
||||
let key = (tpub, path).into_descriptor_key().unwrap();
|
||||
|
||||
@@ -812,12 +861,14 @@ 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 = "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);
|
||||
|
||||
@@ -834,16 +885,16 @@ mod test {
|
||||
let (descriptor, _) =
|
||||
into_wallet_descriptor_checked(descriptor, &secp, Network::Testnet).unwrap();
|
||||
|
||||
let descriptor = descriptor.at_derivation_index(0);
|
||||
let descriptor = descriptor.at_derivation_index(0).unwrap();
|
||||
|
||||
let script = Script::from_str("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||
let script = ScriptBuf::from_hex("5321022f533b667e2ea3b36e21961c9fe9dca340fbe0af5210173a83ae0337ab20a57621026bb53a98e810bd0ee61a0ed1164ba6c024786d76554e793e202dc6ce9c78c4ea2102d5b8a7d66a41ffdb6f4c53d61994022e886b4f45001fb158b95c9164d45f8ca3210324b75eead2c1f9c60e8adeb5e7009fec7a29afcdb30d829d82d09562fe8bae8521032d34f8932200833487bd294aa219dcbe000b9f9b3d824799541430009f0fa55121037468f8ea99b6c64788398b5ad25480cad08f4b0d65be54ce3a55fd206b5ae4722103f72d3d96663b0ea99b0aeb0d7f273cab11a8de37885f1dddc8d9112adb87169357ae").unwrap();
|
||||
|
||||
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.redeem_script, Some(script.to_p2wsh()));
|
||||
assert_eq!(psbt_input.witness_script, Some(script));
|
||||
}
|
||||
}
|
||||
@@ -32,20 +32,23 @@
|
||||
//!
|
||||
//! 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::bip32::Fingerprint;
|
||||
use bitcoin::hashes::{hash160, ripemd160, sha256};
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::{LockTime, PublicKey, Sequence, XOnlyPublicKey};
|
||||
use bitcoin::{absolute, key::XOnlyPublicKey, PublicKey, Sequence};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
DescriptorPublicKey, ShInner, SinglePub, SinglePubKey, SortedMultiVec, WshInner,
|
||||
@@ -55,9 +58,6 @@ use miniscript::{
|
||||
Descriptor, Miniscript, Satisfier, ScriptContext, SigType, Terminal, ToPublicKey,
|
||||
};
|
||||
|
||||
#[allow(unused_imports)]
|
||||
use log::{debug, error, info, trace};
|
||||
|
||||
use crate::descriptor::ExtractPolicy;
|
||||
use crate::keys::ExtScriptContext;
|
||||
use crate::wallet::signer::{SignerId, SignersContainer};
|
||||
@@ -66,7 +66,7 @@ use crate::wallet::utils::{After, Older, SecpCtx};
|
||||
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
|
||||
@@ -93,6 +93,9 @@ impl PkOrF {
|
||||
..
|
||||
}) => PkOrF::XOnlyPubkey(*pk),
|
||||
DescriptorPublicKey::XPub(xpub) => PkOrF::Fingerprint(xpub.root_fingerprint(secp)),
|
||||
DescriptorPublicKey::MultiXPub(multi) => {
|
||||
PkOrF::Fingerprint(multi.root_fingerprint(secp))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -129,7 +132,7 @@ pub enum SatisfiableItem {
|
||||
/// Absolute timeclock timestamp
|
||||
AbsoluteTimelock {
|
||||
/// The timelock value
|
||||
value: LockTime,
|
||||
value: absolute::LockTime,
|
||||
},
|
||||
/// Relative timelock locktime
|
||||
RelativeTimelock {
|
||||
@@ -449,11 +452,14 @@ pub struct Condition {
|
||||
pub csv: Option<Sequence>,
|
||||
/// Optional timelock condition
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub timelock: Option<LockTime>,
|
||||
pub timelock: Option<absolute::LockTime>,
|
||||
}
|
||||
|
||||
impl Condition {
|
||||
fn merge_nlocktime(a: LockTime, b: LockTime) -> Result<LockTime, PolicyError> {
|
||||
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 {
|
||||
@@ -512,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 {
|
||||
@@ -652,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 {
|
||||
@@ -664,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
|
||||
@@ -680,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())
|
||||
@@ -734,6 +753,7 @@ fn signer_id(key: &DescriptorPublicKey, secp: &SecpCtx) -> SignerId {
|
||||
..
|
||||
}) => pk.to_pubkeyhash(SigType::Ecdsa).into(),
|
||||
DescriptorPublicKey::XPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||
DescriptorPublicKey::MultiXPub(xpub) => xpub.root_fingerprint(secp).into(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -771,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,
|
||||
@@ -791,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,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -896,12 +923,12 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
}
|
||||
Terminal::After(value) => {
|
||||
let mut policy: Policy = SatisfiableItem::AbsoluteTimelock {
|
||||
value: value.into(),
|
||||
value: (*value).into(),
|
||||
}
|
||||
.into();
|
||||
policy.contribution = Satisfaction::Complete {
|
||||
condition: Condition {
|
||||
timelock: Some(value.into()),
|
||||
timelock: Some((*value).into()),
|
||||
csv: None,
|
||||
},
|
||||
};
|
||||
@@ -913,9 +940,9 @@ impl<Ctx: ScriptContext + 'static> ExtractPolicy for Miniscript<DescriptorPublic
|
||||
{
|
||||
let after = After::new(Some(current_height), false);
|
||||
let after_sat =
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&after, value.into());
|
||||
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())
|
||||
Satisfier::<bitcoin::PublicKey>::check_after(&sat, (*value).into())
|
||||
});
|
||||
if after_sat && inputs_sat {
|
||||
policy.satisfaction = policy.contribution.clone();
|
||||
@@ -1110,7 +1137,7 @@ impl ExtractPolicy for Descriptor<DescriptorPublicKey> {
|
||||
let key_spend_sig =
|
||||
miniscript::Tap::make_signature(tr.internal_key(), signers, build_sat, secp);
|
||||
|
||||
if tr.taptree().is_none() {
|
||||
if tr.tap_tree().is_none() {
|
||||
Ok(Some(key_spend_sig))
|
||||
} else {
|
||||
let mut items = vec![key_spend_sig];
|
||||
@@ -1139,11 +1166,12 @@ mod test {
|
||||
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 std::str::FromStr;
|
||||
use std::sync::Arc;
|
||||
use core::str::FromStr;
|
||||
|
||||
const TPRV0_STR:&str = "tprv8ZgxMBicQKsPdZXrcHNLf5JAJWFAoJ2TrstMRdSKtEggz6PddbuSkvHKM9oKJyFgZV1B7rw8oChspxyYbtmEXYyg1AjfWbL3ho3XHDpHRZf";
|
||||
const TPRV1_STR:&str = "tprv8ZgxMBicQKsPdpkqS7Eair4YxjcuuvDPNYmKX3sCniCf16tHEVrjjiSXEkFRnUH77yXc6ZcwHHcLNfjdi5qUvw3VDfgYiH5mNsj5izuiu2N";
|
||||
@@ -1156,8 +1184,8 @@ mod test {
|
||||
secp: &SecpCtx,
|
||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||
let path = bip32::DerivationPath::from_str(path).unwrap();
|
||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_priv(secp, &tprv);
|
||||
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||
let tpub = bip32::Xpub::from_priv(secp, &tprv);
|
||||
let fingerprint = tprv.fingerprint(secp);
|
||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||
@@ -1182,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
|
||||
@@ -1195,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
|
||||
@@ -1217,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()
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1248,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)
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1281,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])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1313,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])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1345,8 +1359,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
|
||||
@@ -1358,10 +1372,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());
|
||||
}
|
||||
|
||||
// single key, 1 prv and 1 pub key descriptor, required 1 prv keys
|
||||
@@ -1382,18 +1394,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])
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1425,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(sequence))
|
||||
&& conditions.get(&vec![1,2]).unwrap().iter().next().unwrap().csv == Some(Sequence(sequence))
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1459,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
|
||||
@@ -1484,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;
|
||||
@@ -1502,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?
|
||||
}
|
||||
@@ -1582,6 +1587,7 @@ mod test {
|
||||
|
||||
let addr = wallet_desc
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1599,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();
|
||||
@@ -1613,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();
|
||||
@@ -1625,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]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1654,6 +1654,7 @@ mod test {
|
||||
|
||||
let addr = wallet_desc
|
||||
.at_derivation_index(0)
|
||||
.unwrap()
|
||||
.address(Network::Testnet)
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
@@ -1673,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());
|
||||
|
||||
@@ -1691,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());
|
||||
|
||||
@@ -1711,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());
|
||||
}
|
||||
@@ -1790,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));
|
||||
@@ -1887,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::Xpriv::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::Xpub::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::Xpriv::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::Xpub::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::Xpriv::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::Xpub::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::Xpriv::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::Xpub::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,51 +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::{DescriptorError, DescriptorMeta};
|
||||
use crate::keys::ValidNetworks;
|
||||
use bitcoin::network::constants::Network::Regtest;
|
||||
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::Xpriv::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::Xpriv::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 });
|
||||
}
|
||||
}
|
||||
|
||||
@@ -512,20 +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 (desc, _key_map, _networks) = desc.unwrap();
|
||||
assert_eq!(desc.is_witness(), is_witness);
|
||||
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.has_wildcard() {
|
||||
desc.at_derivation_index(0)
|
||||
desc.at_derivation_index(0).unwrap()
|
||||
} else {
|
||||
desc.at_derivation_index(index)
|
||||
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());
|
||||
}
|
||||
}
|
||||
@@ -539,7 +630,9 @@ mod test {
|
||||
check(
|
||||
P2Pkh(prvkey).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["mwJ8hxFYW19JLuc65RCTaP4v1rzVU8cVMT"],
|
||||
);
|
||||
|
||||
@@ -550,7 +643,9 @@ mod test {
|
||||
check(
|
||||
P2Pkh(pubkey).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["muZpTpBYhxmRFuCjLc7C6BBDF32C8XVJUi"],
|
||||
);
|
||||
}
|
||||
@@ -564,7 +659,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh_P2Sh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["2NB4ox5VDRw1ecUv6SnT3VQHPXveYztRqk5"],
|
||||
);
|
||||
|
||||
@@ -575,7 +672,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh_P2Sh(pubkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["2N5LiC3CqzxDamRTPG1kiNv1FpNJQ7x28sb"],
|
||||
);
|
||||
}
|
||||
@@ -589,7 +688,9 @@ mod test {
|
||||
check(
|
||||
P2Wpkh(prvkey).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
true,
|
||||
Network::Regtest,
|
||||
&["bcrt1q4525hmgw265tl3drrl8jjta7ayffu6jfcwxx9y"],
|
||||
);
|
||||
|
||||
@@ -600,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::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip44(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"n453VtnjDHPyDt2fDstKSu7A3YCJoHZ5g5",
|
||||
"mvfrrumXgTtwFPWDNUecBBgzuMXhYM7KRP",
|
||||
@@ -623,6 +757,8 @@ mod test {
|
||||
Bip44(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"muHF98X9KxEzdKrnFAX85KeHv96eXopaip",
|
||||
"n4hpyLJE5ub6B5Bymv4eqFxS5KjrewSmYR",
|
||||
@@ -634,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::Xpub::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",
|
||||
@@ -650,6 +788,8 @@ mod test {
|
||||
Bip44Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
false,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"moDr3vJ8wpt5nNxSK55MPq797nXJb2Ru9H",
|
||||
"ms7A1Yt4uTezT2XkefW12AvLoko8WfNJMG",
|
||||
@@ -661,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::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip49(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2N9bCAJXGm168MjVwpkBdNt6ucka3PKVoUV",
|
||||
"2NDckYkqrYyDMtttEav5hB3Bfw9EGAW5HtS",
|
||||
@@ -676,6 +818,8 @@ mod test {
|
||||
Bip49(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2NB3pA8PnzJLGV8YEKNDFpbViZv3Bm1K6CG",
|
||||
"2NBiX2Wzxngb5rPiWpUiJQ2uLVB4HBjFD4p",
|
||||
@@ -687,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::Xpub::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",
|
||||
@@ -703,6 +849,8 @@ mod test {
|
||||
Bip49Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"2NF2vttKibwyxigxtx95Zw8K7JhDbo5zPVJ",
|
||||
"2Mtmyd8taksxNVWCJ4wVvaiss7QPZGcAJuH",
|
||||
@@ -714,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::Xpriv::from_str("tprv8ZgxMBicQKsPcx5nBGsR63Pe8KnRUqmbJNENAfGftF3yuXoMMoVJJcYeUw5eVkm9WBPjWYt6HMWYJNesB5HaNVBaFc1M6dRjWSYnmewUMYy").unwrap();
|
||||
check(
|
||||
Bip84(prvkey, KeychainKind::External).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qkmvk2nadgplmd57ztld8nf8v2yxkzmdvwtjf8s",
|
||||
"bcrt1qx0v6zgfwe50m4kqc58cqzcyem7ay2sfl3gvqhp",
|
||||
@@ -729,6 +879,8 @@ mod test {
|
||||
Bip84(prvkey, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qtrwtz00wxl69e5xex7amy4xzlxkaefg3gfdkxa",
|
||||
"bcrt1qqqasfhxpkkf7zrxqnkr2sfhn74dgsrc3e3ky45",
|
||||
@@ -740,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::Xpub::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",
|
||||
@@ -756,6 +910,8 @@ mod test {
|
||||
Bip84Public(pubkey, fingerprint, KeychainKind::Internal).build(Network::Bitcoin),
|
||||
true,
|
||||
false,
|
||||
false,
|
||||
Network::Regtest,
|
||||
&[
|
||||
"bcrt1qm6wqukenh7guu792lj2njgw9n78cmwsy8xy3z2",
|
||||
"bcrt1q694twxtjn4nnrvnyvra769j0a23rllj5c6cgwp",
|
||||
@@ -763,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::Xpriv::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::Xpub::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;
|
||||
@@ -56,7 +57,7 @@ pub type MnemonicWithPassphrase = (Mnemonic, Option<String>);
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "keys-bip39")))]
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for Seed {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(bip32::ExtendedPrivKey::new_master(Network::Bitcoin, &self[..])?.into())
|
||||
Ok(bip32::Xpriv::new_master(Network::Bitcoin, &self[..])?.into())
|
||||
}
|
||||
|
||||
fn into_descriptor_key(
|
||||
@@ -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,16 +11,19 @@
|
||||
|
||||
//! 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::{
|
||||
@@ -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(
|
||||
@@ -107,7 +110,7 @@ impl<Ctx: ScriptContext> DescriptorKey<Ctx> {
|
||||
Ok((public, KeyMap::default(), valid_networks))
|
||||
}
|
||||
DescriptorKey::Secret(secret, valid_networks, _) => {
|
||||
let mut key_map = KeyMap::with_capacity(1);
|
||||
let mut key_map = KeyMap::new();
|
||||
|
||||
let public = secret
|
||||
.to_public(secp)
|
||||
@@ -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};
|
||||
///
|
||||
@@ -306,15 +309,15 @@ pub trait IntoDescriptorKey<Ctx: ScriptContext>: Sized {
|
||||
|
||||
/// Enum for extended keys that can be either `xprv` or `xpub`
|
||||
///
|
||||
/// An instance of [`ExtendedKey`] can be constructed from an [`ExtendedPrivKey`](bip32::ExtendedPrivKey)
|
||||
/// or an [`ExtendedPubKey`](bip32::ExtendedPubKey) by using the `From` trait.
|
||||
/// An instance of [`ExtendedKey`] can be constructed from an [`Xpriv`](bip32::Xpriv)
|
||||
/// or an [`Xpub`](bip32::Xpub) by using the `From` trait.
|
||||
///
|
||||
/// Defaults to the [`Legacy`](miniscript::Legacy) context.
|
||||
pub enum ExtendedKey<Ctx: ScriptContext = miniscript::Legacy> {
|
||||
/// A private extended key, aka an `xprv`
|
||||
Private((bip32::ExtendedPrivKey, PhantomData<Ctx>)),
|
||||
Private((bip32::Xpriv, PhantomData<Ctx>)),
|
||||
/// A public extended key, aka an `xpub`
|
||||
Public((bip32::ExtendedPubKey, PhantomData<Ctx>)),
|
||||
Public((bip32::Xpub, PhantomData<Ctx>)),
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
@@ -326,9 +329,9 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPrivKey`](bip32::ExtendedPrivKey) for the
|
||||
/// Transform the [`ExtendedKey`] into an [`Xpriv`](bip32::Xpriv) for the
|
||||
/// given [`Network`], if the key contains the private data
|
||||
pub fn into_xprv(self, network: Network) -> Option<bip32::ExtendedPrivKey> {
|
||||
pub fn into_xprv(self, network: Network) -> Option<bip32::Xpriv> {
|
||||
match self {
|
||||
ExtendedKey::Private((mut xprv, _)) => {
|
||||
xprv.network = network;
|
||||
@@ -338,15 +341,15 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Transform the [`ExtendedKey`] into an [`ExtendedPubKey`](bip32::ExtendedPubKey) for the
|
||||
/// Transform the [`ExtendedKey`] into an [`Xpub`](bip32::Xpub) for the
|
||||
/// given [`Network`]
|
||||
pub fn into_xpub<C: Signing>(
|
||||
self,
|
||||
network: bitcoin::Network,
|
||||
secp: &Secp256k1<C>,
|
||||
) -> bip32::ExtendedPubKey {
|
||||
) -> bip32::Xpub {
|
||||
let mut xpub = match self {
|
||||
ExtendedKey::Private((xprv, _)) => bip32::ExtendedPubKey::from_priv(secp, &xprv),
|
||||
ExtendedKey::Private((xprv, _)) => bip32::Xpub::from_priv(secp, &xprv),
|
||||
ExtendedKey::Public((xpub, _)) => xpub,
|
||||
};
|
||||
|
||||
@@ -355,14 +358,14 @@ impl<Ctx: ScriptContext> ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPubKey> for ExtendedKey<Ctx> {
|
||||
fn from(xpub: bip32::ExtendedPubKey) -> Self {
|
||||
impl<Ctx: ScriptContext> From<bip32::Xpub> for ExtendedKey<Ctx> {
|
||||
fn from(xpub: bip32::Xpub) -> Self {
|
||||
ExtendedKey::Public((xpub, PhantomData))
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
fn from(xprv: bip32::ExtendedPrivKey) -> Self {
|
||||
impl<Ctx: ScriptContext> From<bip32::Xpriv> for ExtendedKey<Ctx> {
|
||||
fn from(xprv: bip32::Xpriv) -> Self {
|
||||
ExtendedKey::Private((xprv, PhantomData))
|
||||
}
|
||||
}
|
||||
@@ -375,33 +378,33 @@ 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
|
||||
///
|
||||
/// Key types that can be directly converted into an [`ExtendedPrivKey`] or
|
||||
/// an [`ExtendedPubKey`] can implement only the required `into_extended_key()` method.
|
||||
/// Key types that can be directly converted into an [`Xpriv`] or
|
||||
/// an [`Xpub`] can implement only the required `into_extended_key()` method.
|
||||
///
|
||||
/// ```
|
||||
/// 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,
|
||||
/// }
|
||||
///
|
||||
/// impl<Ctx: ScriptContext> DerivableKey<Ctx> for MyCustomKeyType {
|
||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
/// let xprv = bip32::ExtendedPrivKey {
|
||||
/// let xprv = bip32::Xpriv {
|
||||
/// network: self.network,
|
||||
/// 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,30 +413,30 @@ 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.
|
||||
/// [`Xpriv`] or [`Xpub`] 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 {
|
||||
/// fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
/// let xprv = bip32::ExtendedPrivKey {
|
||||
/// let xprv = bip32::Xpriv {
|
||||
/// network: bitcoin::Network::Bitcoin, // pick an arbitrary network here
|
||||
/// 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 },
|
||||
/// };
|
||||
///
|
||||
@@ -456,16 +459,15 @@ impl<Ctx: ScriptContext> From<bip32::ExtendedPrivKey> for ExtendedKey<Ctx> {
|
||||
/// ```
|
||||
///
|
||||
/// [`DerivationPath`]: (bip32::DerivationPath)
|
||||
/// [`ExtendedPrivKey`]: (bip32::ExtendedPrivKey)
|
||||
/// [`ExtendedPubKey`]: (bip32::ExtendedPubKey)
|
||||
/// [`Xpriv`]: (bip32::Xpriv)
|
||||
/// [`Xpub`]: (bip32::Xpub)
|
||||
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};
|
||||
@@ -518,13 +520,13 @@ impl<Ctx: ScriptContext> DerivableKey<Ctx> for ExtendedKey<Ctx> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPubKey {
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpub {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||
impl<Ctx: ScriptContext> DerivableKey<Ctx> for bip32::Xpriv {
|
||||
fn into_extended_key(self) -> Result<ExtendedKey<Ctx>, KeyError> {
|
||||
Ok(self.into())
|
||||
}
|
||||
@@ -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(
|
||||
@@ -668,7 +670,7 @@ where
|
||||
{
|
||||
}
|
||||
|
||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||
impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::Xpriv {
|
||||
type Entropy = [u8; 32];
|
||||
|
||||
type Options = ();
|
||||
@@ -679,7 +681,7 @@ impl<Ctx: ScriptContext> GeneratableKey<Ctx> for bip32::ExtendedPrivKey {
|
||||
entropy: Self::Entropy,
|
||||
) -> Result<GeneratedKey<Self, Ctx>, Self::Error> {
|
||||
// pick a arbitrary network here, but say that we support all of them
|
||||
let xprv = bip32::ExtendedPrivKey::new_master(Network::Bitcoin, entropy.as_ref())?;
|
||||
let xprv = bip32::Xpriv::new_master(Network::Bitcoin, entropy.as_ref())?;
|
||||
Ok(GeneratedKey::new(xprv, any_network()))
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
@@ -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::*;
|
||||
|
||||
@@ -952,7 +971,7 @@ pub mod test {
|
||||
#[test]
|
||||
fn test_keys_generate_xprv() {
|
||||
let generated_xprv: GeneratedKey<_, miniscript::Segwitv0> =
|
||||
bip32::ExtendedPrivKey::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
||||
bip32::Xpriv::generate_with_entropy_default(TEST_ENTROPY).unwrap();
|
||||
|
||||
assert_eq!(generated_xprv.valid_networks, any_network());
|
||||
assert_eq!(generated_xprv.to_string(), "xprv9s21ZrQH143K4Xr1cJyqTvuL2FWR8eicgY9boWqMBv8MDVUZ65AXHnzBrK1nyomu6wdcabRgmGTaAKawvhAno1V5FowGpTLVx3jxzE5uk3Q");
|
||||
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;
|
||||
75
crates/bdk/src/psbt/mod.rs
Normal file
75
crates/bdk/src/psbt/mod.rs
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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` `Psbt` structure.
|
||||
|
||||
use alloc::vec::Vec;
|
||||
use bitcoin::Amount;
|
||||
use bitcoin::FeeRate;
|
||||
use bitcoin::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
|
||||
/// `Psbt` 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.to_sat()).sum();
|
||||
let output_amount: u64 = self
|
||||
.unsigned_tx
|
||||
.output
|
||||
.iter()
|
||||
.map(|o| o.value.to_sat())
|
||||
.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();
|
||||
let weight = self.clone().extract_tx().ok()?.weight();
|
||||
fee_amount.map(|fee| Amount::from_sat(fee) / weight)
|
||||
}
|
||||
}
|
||||
135
crates/bdk/src/types.rs
Normal file
135
crates/bdk/src/types.rs
Normal file
@@ -0,0 +1,135 @@
|
||||
// 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.
|
||||
|
||||
use alloc::boxed::Box;
|
||||
use core::convert::AsRef;
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::blockdata::transaction::{OutPoint, Sequence, TxOut};
|
||||
use bitcoin::psbt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Types of keychains
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq, Hash, Ord, PartialOrd)]
|
||||
pub enum KeychainKind {
|
||||
/// External keychain, used for deriving recipient addresses.
|
||||
External = 0,
|
||||
/// Internal keychain, used for deriving change addresses.
|
||||
Internal = 1,
|
||||
}
|
||||
|
||||
impl KeychainKind {
|
||||
/// Return [`KeychainKind`] as a byte
|
||||
pub fn as_byte(&self) -> u8 {
|
||||
match self {
|
||||
KeychainKind::External => b'e',
|
||||
KeychainKind::Internal => b'i',
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for KeychainKind {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match self {
|
||||
KeychainKind::External => b"e",
|
||||
KeychainKind::Internal => b"i",
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An unspent output owned by a [`Wallet`].
|
||||
///
|
||||
/// [`Wallet`]: crate::Wallet
|
||||
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct LocalOutput {
|
||||
/// Reference to a transaction output
|
||||
pub outpoint: OutPoint,
|
||||
/// Transaction output
|
||||
pub txout: TxOut,
|
||||
/// Type of keychain
|
||||
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, 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.
|
||||
///
|
||||
/// [weight units]: https://en.bitcoin.it/wiki/Weight_units
|
||||
pub satisfaction_weight: usize,
|
||||
/// The UTXO
|
||||
pub utxo: Utxo,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// An unspent transaction output (UTXO).
|
||||
pub enum Utxo {
|
||||
/// A UTXO owned by the local wallet.
|
||||
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>,
|
||||
},
|
||||
}
|
||||
|
||||
impl Utxo {
|
||||
/// Get the location of the UTXO
|
||||
pub fn outpoint(&self) -> OutPoint {
|
||||
match &self {
|
||||
Utxo::Local(local) => local.outpoint,
|
||||
Utxo::Foreign { outpoint, .. } => *outpoint,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the `TxOut` of the UTXO
|
||||
pub fn txout(&self) -> &TxOut {
|
||||
match &self {
|
||||
Utxo::Local(local) => &local.txout,
|
||||
Utxo::Foreign {
|
||||
outpoint,
|
||||
psbt_input,
|
||||
..
|
||||
} => {
|
||||
if let Some(prev_tx) = &psbt_input.non_witness_utxo {
|
||||
return &prev_tx.output[outpoint.vout as usize];
|
||||
}
|
||||
|
||||
if let Some(txout) = &psbt_input.witness_utxo {
|
||||
return txout;
|
||||
}
|
||||
|
||||
unreachable!("Foreign UTXOs will always have one of these set")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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,
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
294
crates/bdk/src/wallet/error.rs
Normal file
294
crates/bdk/src/wallet/error.rs
Normal file
@@ -0,0 +1,294 @@
|
||||
// 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, 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
|
||||
required: bitcoin::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,
|
||||
// Note: alternate fmt as sat/vb (ceil) available in bitcoin-0.31
|
||||
//"Fee rate too low: required {required:#}"
|
||||
"Fee rate too low: required {} sat/vb",
|
||||
crate::floating_rate!(required)
|
||||
)
|
||||
}
|
||||
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::{transaction, 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: transaction::Version::non_standard(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\"}");
|
||||
@@ -15,24 +15,25 @@
|
||||
//! used with hardware wallets.
|
||||
//! ```no_run
|
||||
//! # use bdk::bitcoin::Network;
|
||||
//! # use bdk::database::MemoryDatabase;
|
||||
//! # use bdk::signer::SignerOrdering;
|
||||
//! # use bdk::wallet::hardwaresigner::HWISigner;
|
||||
//! # use bdk::wallet::AddressIndex::New;
|
||||
//! # use bdk::{FeeRate, KeychainKind, SignOptions, SyncOptions, Wallet};
|
||||
//! # use hwi::{types::HWIChain, HWIClient};
|
||||
//! # use bdk::{KeychainKind, SignOptions, Wallet};
|
||||
//! # use hwi::HWIClient;
|
||||
//! # use std::sync::Arc;
|
||||
//! #
|
||||
//! # fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let devices = HWIClient::enumerate()?;
|
||||
//! let first_device = devices.first().expect("No devices found!");
|
||||
//! let custom_signer = HWISigner::from_device(first_device, HWIChain::Test)?;
|
||||
//! 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(
|
||||
//! # let mut wallet = Wallet::new_no_persist(
|
||||
//! # "",
|
||||
//! # None,
|
||||
//! # Network::Testnet,
|
||||
//! # MemoryDatabase::default(),
|
||||
//! # )?;
|
||||
//! #
|
||||
//! // Adding the hardware signer to the BDK wallet
|
||||
@@ -46,9 +47,9 @@
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use bitcoin::psbt::PartiallySignedTransaction;
|
||||
use bitcoin::bip32::Fingerprint;
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::util::bip32::Fingerprint;
|
||||
use bitcoin::Psbt;
|
||||
|
||||
use hwi::error::Error;
|
||||
use hwi::types::{HWIChain, HWIDevice};
|
||||
@@ -86,7 +87,7 @@ impl SignerCommon for HWISigner {
|
||||
impl TransactionSigner for HWISigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
_sign_options: &crate::SignOptions,
|
||||
_secp: &crate::wallet::utils::SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
2603
crates/bdk/src/wallet/mod.rs
Normal file
2603
crates/bdk/src/wallet/mod.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -15,18 +15,16 @@
|
||||
//! 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 bdk::signer::*;
|
||||
//! # use bdk::database::*;
|
||||
//! # use bdk::*;
|
||||
//! # #[derive(Debug)]
|
||||
//! # struct CustomHSM;
|
||||
//! # impl CustomHSM {
|
||||
//! # fn hsm_sign_input(&self, _psbt: &mut psbt::PartiallySignedTransaction, _input: usize) -> Result<(), SignerError> {
|
||||
//! # fn hsm_sign_input(&self, _psbt: &mut Psbt, _input: usize) -> Result<(), SignerError> {
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! # fn connect() -> Self {
|
||||
@@ -56,7 +54,7 @@
|
||||
//! impl InputSigner for CustomSigner {
|
||||
//! fn sign_input(
|
||||
//! &self,
|
||||
//! psbt: &mut psbt::PartiallySignedTransaction,
|
||||
//! psbt: &mut Psbt,
|
||||
//! input_index: usize,
|
||||
//! _sign_options: &SignOptions,
|
||||
//! _secp: &Secp256k1<All>,
|
||||
@@ -70,40 +68,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, Fingerprint, Xpriv};
|
||||
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, Psbt, PublicKey};
|
||||
|
||||
use miniscript::descriptor::{
|
||||
Descriptor, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey, KeyMap, SinglePriv,
|
||||
SinglePubKey,
|
||||
Descriptor, DescriptorMultiXKey, DescriptorPublicKey, DescriptorSecretKey, DescriptorXKey,
|
||||
InnerXKey, KeyMap, SinglePriv, SinglePubKey,
|
||||
};
|
||||
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
|
||||
@@ -130,7 +130,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,
|
||||
@@ -160,16 +160,12 @@ pub enum SignerError {
|
||||
InvalidSighash,
|
||||
/// Error while computing the hash to sign
|
||||
SighashError(sighash::Error),
|
||||
/// Error while signing using hardware wallets
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
HWIError(hwi::error::Error),
|
||||
}
|
||||
|
||||
#[cfg(feature = "hardware-signer")]
|
||||
impl From<hwi::error::Error> for SignerError {
|
||||
fn from(e: hwi::error::Error) -> Self {
|
||||
SignerError::HWIError(e)
|
||||
}
|
||||
/// 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 {
|
||||
@@ -180,10 +176,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
|
||||
@@ -202,7 +214,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,
|
||||
@@ -251,7 +263,7 @@ pub trait InputSigner: SignerCommon {
|
||||
/// Sign a single psbt input
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -266,7 +278,7 @@ pub trait TransactionSigner: SignerCommon {
|
||||
/// Sign all the inputs of the psbt
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError>;
|
||||
@@ -275,7 +287,7 @@ pub trait TransactionSigner: SignerCommon {
|
||||
impl<T: InputSigner> TransactionSigner for T {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
@@ -287,7 +299,7 @@ impl<T: InputSigner> TransactionSigner for T {
|
||||
}
|
||||
}
|
||||
|
||||
impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
impl SignerCommon for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
@@ -297,10 +309,10 @@ impl SignerCommon for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
impl InputSigner for SignerWrapper<DescriptorXKey<Xpriv>> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -367,6 +379,48 @@ impl InputSigner for SignerWrapper<DescriptorXKey<ExtendedPrivKey>> {
|
||||
}
|
||||
}
|
||||
|
||||
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<Xpriv>> {
|
||||
fn id(&self, secp: &SecpCtx) -> SignerId {
|
||||
SignerId::from(self.root_fingerprint(secp))
|
||||
}
|
||||
|
||||
fn descriptor_secret_key(&self) -> Option<DescriptorSecretKey> {
|
||||
Some(DescriptorSecretKey::MultiXPrv(self.signer.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl InputSigner for SignerWrapper<DescriptorMultiXKey<Xpriv>> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut Psbt,
|
||||
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))
|
||||
@@ -383,7 +437,7 @@ impl SignerCommon for SignerWrapper<PrivateKey> {
|
||||
impl InputSigner for SignerWrapper<PrivateKey> {
|
||||
fn sign_input(
|
||||
&self,
|
||||
psbt: &mut psbt::PartiallySignedTransaction,
|
||||
psbt: &mut Psbt,
|
||||
input_index: usize,
|
||||
sign_options: &SignOptions,
|
||||
secp: &SecpCtx,
|
||||
@@ -402,20 +456,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, _)) =
|
||||
@@ -461,8 +518,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(
|
||||
@@ -483,12 +548,12 @@ 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 msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||
let msg = &Message::from(hash);
|
||||
let sig = if allow_grinding {
|
||||
secp.sign_ecdsa_low_r(msg, secret_key)
|
||||
} else {
|
||||
@@ -497,7 +562,7 @@ fn sign_psbt_ecdsa(
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -507,13 +572,11 @@ 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 = 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)
|
||||
@@ -521,12 +584,12 @@ fn sign_psbt_schnorr(
|
||||
Some(_) => keypair, // no tweak for script spend
|
||||
};
|
||||
|
||||
let msg = &Message::from_slice(&hash.into_inner()[..]).unwrap();
|
||||
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
|
||||
@@ -545,7 +608,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)
|
||||
}
|
||||
@@ -616,6 +679,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)),
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -683,7 +751,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
|
||||
@@ -713,6 +781,16 @@ pub struct SignOptions {
|
||||
/// Defaults to `true` which will remove partial signatures during finalization.
|
||||
pub remove_partial_sigs: bool,
|
||||
|
||||
/// Whether to remove taproot specific fields from the PSBT on finalization.
|
||||
///
|
||||
/// For inputs this includes the taproot internal key, merkle root, and individual
|
||||
/// scripts and signatures. For both inputs and outputs it includes key origin info.
|
||||
///
|
||||
/// Defaults to `true` which will remove all of the above mentioned fields when finalizing.
|
||||
///
|
||||
/// See [`BIP371`](https://github.com/bitcoin/bips/blob/master/bip-0371.mediawiki) for details.
|
||||
pub remove_taproot_extras: bool,
|
||||
|
||||
/// Whether to try finalizing the PSBT after the inputs are signed.
|
||||
///
|
||||
/// Defaults to `true` which will try finalizing PSBT after inputs are signed.
|
||||
@@ -737,9 +815,10 @@ pub struct SignOptions {
|
||||
}
|
||||
|
||||
/// Customize which taproot script-path leaves the signer should sign.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[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.
|
||||
@@ -750,13 +829,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 {
|
||||
@@ -764,6 +836,7 @@ impl Default for SignOptions {
|
||||
assume_height: None,
|
||||
allow_all_sighashes: false,
|
||||
remove_partial_sigs: true,
|
||||
remove_taproot_extras: true,
|
||||
try_finalize: true,
|
||||
tap_leaves_options: TapLeavesOptions::default(),
|
||||
sign_with_tap_internal_key: true,
|
||||
@@ -778,7 +851,7 @@ pub(crate) trait ComputeSighash {
|
||||
type SighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
extra: Self::Extra,
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError>;
|
||||
@@ -786,11 +859,11 @@ pub(crate) trait ComputeSighash {
|
||||
|
||||
impl ComputeSighash for Legacy {
|
||||
type Extra = ();
|
||||
type Sighash = bitcoin::Sighash;
|
||||
type Sighash = sighash::LegacySighash;
|
||||
type SighashType = EcdsaSighashType;
|
||||
|
||||
fn sighash(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
@@ -833,23 +906,13 @@ 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(
|
||||
psbt: &psbt::PartiallySignedTransaction,
|
||||
psbt: &Psbt,
|
||||
input_index: usize,
|
||||
_extra: (),
|
||||
) -> Result<(Self::Sighash, Self::SighashType), SignerError> {
|
||||
@@ -860,7 +923,7 @@ impl ComputeSighash for Segwitv0 {
|
||||
let psbt_input = &psbt.inputs[input_index];
|
||||
let tx_input = &psbt.unsigned_tx.input[input_index];
|
||||
|
||||
let sighash = psbt_input
|
||||
let sighash_type = psbt_input
|
||||
.sighash_type
|
||||
.unwrap_or_else(|| EcdsaSighashType::All.into())
|
||||
.ecdsa_hash_ty()
|
||||
@@ -888,46 +951,52 @@ impl ComputeSighash for Segwitv0 {
|
||||
};
|
||||
let value = utxo.value;
|
||||
|
||||
let script = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => witness_script.clone(),
|
||||
let mut sighasher = sighash::SighashCache::new(&psbt.unsigned_tx);
|
||||
|
||||
let sighash = match psbt_input.witness_script {
|
||||
Some(ref witness_script) => {
|
||||
sighasher.p2wsh_signature_hash(input_index, witness_script, value, sighash_type)?
|
||||
}
|
||||
None => {
|
||||
if utxo.script_pubkey.is_v0_p2wpkh() {
|
||||
p2wpkh_script_code(&utxo.script_pubkey)
|
||||
if utxo.script_pubkey.is_p2wpkh() {
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
&utxo.script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
} else if psbt_input
|
||||
.redeem_script
|
||||
.as_ref()
|
||||
.map(Script::is_v0_p2wpkh)
|
||||
.map(|s| s.is_p2wpkh())
|
||||
.unwrap_or(false)
|
||||
{
|
||||
p2wpkh_script_code(psbt_input.redeem_script.as_ref().unwrap())
|
||||
let script_pubkey = psbt_input.redeem_script.as_ref().unwrap();
|
||||
sighasher.p2wpkh_signature_hash(
|
||||
input_index,
|
||||
script_pubkey,
|
||||
value,
|
||||
sighash_type,
|
||||
)?
|
||||
} else {
|
||||
return Err(SignerError::MissingWitnessScript);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
Ok((
|
||||
sighash::SighashCache::new(&psbt.unsigned_tx).segwit_signature_hash(
|
||||
input_index,
|
||||
&script,
|
||||
value,
|
||||
sighash,
|
||||
)?,
|
||||
sighash,
|
||||
))
|
||||
Ok((sighash, sighash_type))
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
psbt: &Psbt,
|
||||
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);
|
||||
}
|
||||
@@ -936,8 +1005,8 @@ 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 = (0..psbt.inputs.len())
|
||||
.map(|i| psbt.get_utxo_for(i))
|
||||
@@ -998,11 +1067,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();
|
||||
@@ -1067,17 +1137,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)]
|
||||
@@ -1094,7 +1164,7 @@ mod signers_container_tests {
|
||||
impl TransactionSigner for DummySigner {
|
||||
fn sign_transaction(
|
||||
&self,
|
||||
_psbt: &mut psbt::PartiallySignedTransaction,
|
||||
_psbt: &mut Psbt,
|
||||
_sign_options: &SignOptions,
|
||||
_secp: &SecpCtx,
|
||||
) -> Result<(), SignerError> {
|
||||
@@ -1112,8 +1182,8 @@ mod signers_container_tests {
|
||||
) -> (DescriptorKey<Ctx>, DescriptorKey<Ctx>, Fingerprint) {
|
||||
let secp: Secp256k1<All> = Secp256k1::new();
|
||||
let path = bip32::DerivationPath::from_str(PATH).unwrap();
|
||||
let tprv = bip32::ExtendedPrivKey::from_str(tprv).unwrap();
|
||||
let tpub = bip32::ExtendedPubKey::from_priv(&secp, &tprv);
|
||||
let tprv = bip32::Xpriv::from_str(tprv).unwrap();
|
||||
let tpub = bip32::Xpub::from_priv(&secp, &tprv);
|
||||
let fingerprint = tprv.fingerprint(&secp);
|
||||
let prvkey = (tprv, path.clone()).into_descriptor_key().unwrap();
|
||||
let pubkey = (tpub, path).into_descriptor_key().unwrap();
|
||||
@@ -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,31 +31,32 @@
|
||||
//! // 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(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
//! // 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 alloc::{boxed::Box, rc::Rc, string::String, vec::Vec};
|
||||
use core::cell::RefCell;
|
||||
use core::fmt;
|
||||
use core::marker::PhantomData;
|
||||
|
||||
use bitcoin::util::psbt::{self, PartiallySignedTransaction as Psbt};
|
||||
use bitcoin::{LockTime, OutPoint, Script, Sequence, Transaction};
|
||||
use bdk_chain::PersistBackend;
|
||||
use bitcoin::psbt::{self, Psbt};
|
||||
use bitcoin::script::PushBytes;
|
||||
use bitcoin::{absolute, FeeRate, 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, CreateTxError, Wallet};
|
||||
use crate::collections::{BTreeMap, HashSet};
|
||||
use crate::{KeychainKind, LocalOutput, Utxo, WeightedUtxo};
|
||||
|
||||
/// 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).
|
||||
@@ -78,11 +83,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)
|
||||
@@ -92,7 +101,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] {
|
||||
@@ -102,7 +111,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`.
|
||||
@@ -116,7 +125,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>,
|
||||
@@ -126,9 +135,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>>>,
|
||||
@@ -137,7 +146,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<LockTime>,
|
||||
pub(crate) locktime: Option<absolute::LockTime>,
|
||||
pub(crate) rbf: Option<RbfValue>,
|
||||
pub(crate) version: Option<Version>,
|
||||
pub(crate) change_policy: ChangeSpendPolicy,
|
||||
@@ -145,14 +154,14 @@ 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<LockTime>,
|
||||
pub(crate) current_height: Option<absolute::LockTime>,
|
||||
pub(crate) allow_dust: bool,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug)]
|
||||
pub(crate) struct PreviousFee {
|
||||
pub absolute: u64,
|
||||
pub rate: f32,
|
||||
pub rate: FeeRate,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
@@ -161,16 +170,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())
|
||||
FeePolicy::FeeRate(FeeRate::BROADCAST_MIN)
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
@@ -179,16 +188,31 @@ 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>
|
||||
{
|
||||
/// Set a custom fee rate
|
||||
impl<'a, D, Cs, Ctx> TxBuilder<'a, D, Cs, Ctx> {
|
||||
/// Set a custom fee rate.
|
||||
///
|
||||
/// This 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 `fee_rate` times the size
|
||||
/// of the transaction. Default is 1 sat/vB in accordance with Bitcoin Core's default
|
||||
/// relay policy.
|
||||
///
|
||||
/// 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
|
||||
@@ -241,8 +265,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]);
|
||||
///
|
||||
@@ -251,7 +278,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,
|
||||
@@ -273,19 +300,26 @@ 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);
|
||||
let satisfaction_weight = descriptor.max_weight_to_satisfy().unwrap();
|
||||
self.params.utxos.push(WeightedUtxo {
|
||||
satisfaction_weight,
|
||||
utxo: Utxo::Local(utxo),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(self)
|
||||
@@ -295,7 +329,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])
|
||||
}
|
||||
|
||||
@@ -321,12 +355,16 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
/// causing you to pay a fee that is too high. The party who is broadcasting the transaction can
|
||||
/// of course check the real input weight matches the expected weight prior to broadcasting.
|
||||
///
|
||||
/// To guarantee the `satisfaction_weight` is correct, you can require the party providing the
|
||||
/// To guarantee the `max_weight_to_satisfy` is correct, you can require the party providing the
|
||||
/// `psbt_input` provide a miniscript descriptor for the input so you can check it against the
|
||||
/// `script_pubkey` and then ask it for the [`max_satisfaction_weight`].
|
||||
/// `script_pubkey` and then ask it for the [`max_weight_to_satisfy`].
|
||||
///
|
||||
/// 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:
|
||||
@@ -340,29 +378,44 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
///
|
||||
/// [`only_witness_utxo`]: Self::only_witness_utxo
|
||||
/// [`finish`]: Self::finish
|
||||
/// [`max_satisfaction_weight`]: miniscript::Descriptor::max_satisfaction_weight
|
||||
/// [`max_weight_to_satisfy`]: miniscript::Descriptor::max_weight_to_satisfy
|
||||
pub fn add_foreign_utxo(
|
||||
&mut self,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -371,6 +424,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),
|
||||
},
|
||||
});
|
||||
@@ -424,7 +478,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: LockTime) -> &mut Self {
|
||||
pub fn nlocktime(&mut self, locktime: absolute::LockTime) -> &mut Self {
|
||||
self.params.locktime = Some(locktime);
|
||||
self
|
||||
}
|
||||
@@ -463,7 +517,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
|
||||
@@ -473,8 +527,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 {
|
||||
@@ -500,10 +554,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> {
|
||||
@@ -515,15 +569,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`.
|
||||
@@ -556,7 +601,8 @@ 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(LockTime::from_height(height).expect("Invalid height"));
|
||||
self.params.current_height =
|
||||
Some(absolute::LockTime::from_height(height).expect("Invalid height"));
|
||||
self
|
||||
}
|
||||
|
||||
@@ -569,22 +615,122 @@ impl<'a, D: BatchDatabase, Cs: CoinSelectionAlgorithm<D>, Ctx: TxBuilderContext>
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
}
|
||||
@@ -613,9 +759,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
|
||||
@@ -623,24 +776,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(FeeRate::from_sat_per_vb(5).expect("valid feerate"))
|
||||
/// .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.
|
||||
@@ -651,7 +804,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
|
||||
@@ -663,18 +819,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,
|
||||
@@ -682,12 +836,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) {
|
||||
@@ -695,14 +843,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(12345);
|
||||
|
||||
tx.input.shuffle(&mut rng);
|
||||
tx.output.shuffle(&mut rng);
|
||||
}
|
||||
TxOrdering::Bip69Lexicographic => {
|
||||
@@ -747,9 +889,10 @@ impl RbfValue {
|
||||
}
|
||||
|
||||
/// 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,
|
||||
@@ -757,14 +900,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,
|
||||
@@ -788,8 +925,10 @@ mod test {
|
||||
};
|
||||
}
|
||||
|
||||
use bdk_chain::ConfirmationTime;
|
||||
use bitcoin::consensus::deserialize;
|
||||
use bitcoin::hashes::hex::FromHex;
|
||||
use bitcoin::hex::FromHex;
|
||||
use bitcoin::TxOut;
|
||||
|
||||
use super::*;
|
||||
|
||||
@@ -813,15 +952,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;
|
||||
@@ -850,32 +999,42 @@ mod test {
|
||||
.unwrap()
|
||||
);
|
||||
|
||||
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[0].value.to_sat(), 800);
|
||||
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: bitcoin::Txid::from_inner([0; 32]),
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 0,
|
||||
},
|
||||
txout: Default::default(),
|
||||
txout: TxOut::NULL,
|
||||
keychain: KeychainKind::External,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Unconfirmed { last_seen: 0 },
|
||||
derivation_index: 0,
|
||||
},
|
||||
LocalUtxo {
|
||||
LocalOutput {
|
||||
outpoint: OutPoint {
|
||||
txid: bitcoin::Txid::from_inner([0; 32]),
|
||||
txid: bitcoin::Txid::from_slice(&[0; 32]).unwrap(),
|
||||
vout: 1,
|
||||
},
|
||||
txout: Default::default(),
|
||||
txout: TxOut::NULL,
|
||||
keychain: KeychainKind::Internal,
|
||||
is_spent: false,
|
||||
confirmation_time: ConfirmationTime::Confirmed {
|
||||
height: 32,
|
||||
time: 42,
|
||||
},
|
||||
derivation_index: 1,
|
||||
},
|
||||
]
|
||||
}
|
||||
@@ -10,7 +10,7 @@
|
||||
// licenses.
|
||||
|
||||
use bitcoin::secp256k1::{All, Secp256k1};
|
||||
use bitcoin::{LockTime, Script, Sequence};
|
||||
use bitcoin::{absolute, Script, Sequence};
|
||||
|
||||
use miniscript::{MiniscriptKey, Satisfier, ToPublicKey};
|
||||
|
||||
@@ -65,7 +65,7 @@ pub(crate) fn check_nsequence_rbf(rbf: Sequence, csv: Sequence) -> bool {
|
||||
}
|
||||
|
||||
impl<Pk: MiniscriptKey + ToPublicKey> Satisfier<Pk> for After {
|
||||
fn check_after(&self, n: LockTime) -> bool {
|
||||
fn check_after(&self, n: absolute::LockTime) -> bool {
|
||||
if let Some(current_height) = self.current_height {
|
||||
current_height >= n.to_consensus_u32()
|
||||
} else {
|
||||
@@ -119,12 +119,14 @@ mod test {
|
||||
pub(crate) const SEQUENCE_LOCKTIME_TYPE_FLAG: u32 = 1 << 22;
|
||||
|
||||
use super::{check_nsequence_rbf, IsDust};
|
||||
use crate::bitcoin::{Address, Sequence};
|
||||
use std::str::FromStr;
|
||||
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());
|
||||
@@ -132,9 +134,11 @@ 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());
|
||||
assert!(script_p2wpkh.is_p2wpkh());
|
||||
assert!(293.is_dust(&script_p2wpkh));
|
||||
assert!(!294.is_dust(&script_p2wpkh));
|
||||
}
|
||||
172
crates/bdk/tests/common.rs
Normal file
172
crates/bdk/tests/common.rs
Normal file
@@ -0,0 +1,172 @@
|
||||
#![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::{
|
||||
transaction, Address, Amount, BlockHash, FeeRate, 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: transaction::Version::ONE,
|
||||
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: Amount::from_sat(76_000),
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
}],
|
||||
};
|
||||
|
||||
let tx1 = Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
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: Amount::from_sat(50_000),
|
||||
script_pubkey: change_address.script_pubkey(),
|
||||
},
|
||||
TxOut {
|
||||
value: Amount::from_sat(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)})"
|
||||
}
|
||||
|
||||
/// Construct a new [`FeeRate`] from the given raw `sat_vb` feerate. This is
|
||||
/// useful in cases where we want to create a feerate from a `f64`, as the
|
||||
/// traditional [`FeeRate::from_sat_per_vb`] method will only accept an integer.
|
||||
///
|
||||
/// **Note** this 'quick and dirty' conversion should only be used when the input
|
||||
/// parameter has units of `satoshis/vbyte` **AND** is not expected to overflow,
|
||||
/// or else the resulting value will be inaccurate.
|
||||
pub fn feerate_unchecked(sat_vb: f64) -> FeeRate {
|
||||
// 1 sat_vb / 4wu_vb * 1000kwu_wu = 250 sat_kwu
|
||||
let sat_kwu = (sat_vb * 250.0).ceil() as u64;
|
||||
FeeRate::from_sat_per_kwu(sat_kwu)
|
||||
}
|
||||
223
crates/bdk/tests/psbt.rs
Normal file
223
crates/bdk/tests/psbt.rs
Normal file
@@ -0,0 +1,223 @@
|
||||
use bdk::bitcoin::{Amount, FeeRate, Psbt, TxIn};
|
||||
use bdk::wallet::AddressIndex;
|
||||
use bdk::wallet::AddressIndex::New;
|
||||
use bdk::{psbt, SignOptions};
|
||||
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 = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
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(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 >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_nonwitness_utxo() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
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(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 >= expected_fee_rate);
|
||||
assert!(finalized_fee_rate < unfinalized_fee_rate);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_psbt_fee_rate_with_missing_txout() {
|
||||
use psbt::PsbtUtils;
|
||||
|
||||
let expected_fee_rate = FeeRate::from_sat_per_kwu(310);
|
||||
|
||||
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(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(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::key::TapTweak;
|
||||
use bitcoin::secp256k1::{schnorr, Keypair, Message, Secp256k1, XOnlyPublicKey};
|
||||
use bitcoin::sighash::{Prevouts, SighashCache, TapSighashType};
|
||||
use bitcoin::{PrivateKey, TxOut};
|
||||
use std::sync::Arc;
|
||||
|
||||
let secp = Secp256k1::new();
|
||||
let wif = "cNJmN3fH9DDbDt131fQNkVakkpzawJBSeybCUNmP1BovpmGQ45xG";
|
||||
let desc = format!("tr({})", wif);
|
||||
let prv = PrivateKey::from_wif(wif).unwrap();
|
||||
let keypair = Keypair::from_secret_key(&secp, &prv.inner);
|
||||
|
||||
let (mut wallet, _) = get_funded_wallet(&desc);
|
||||
let to_spend = wallet.get_balance().total();
|
||||
let send_to = wallet.get_address(AddressIndex::New);
|
||||
let mut builder = wallet.build_tx();
|
||||
builder.drain_to(send_to.script_pubkey()).drain_wallet();
|
||||
let mut psbt = builder.finish().unwrap();
|
||||
let unsigned_tx = psbt.unsigned_tx.clone();
|
||||
|
||||
// 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 finalized = wallet.sign(&mut psbt, SignOptions::default()).unwrap();
|
||||
assert!(finalized);
|
||||
|
||||
// To verify, we need the signature, message, and pubkey
|
||||
let witness = psbt.inputs[0].final_script_witness.as_ref().unwrap();
|
||||
assert!(!witness.is_empty());
|
||||
let signature = schnorr::Signature::from_slice(witness.iter().next().unwrap()).unwrap();
|
||||
|
||||
// the prevout we're spending
|
||||
let prevouts = &[TxOut {
|
||||
script_pubkey: send_to.script_pubkey(),
|
||||
value: Amount::from_sat(to_spend),
|
||||
}];
|
||||
let prevouts = Prevouts::All(prevouts);
|
||||
let input_index = 0;
|
||||
let mut sighash_cache = SighashCache::new(unsigned_tx);
|
||||
let sighash = sighash_cache
|
||||
.taproot_key_spend_signature_hash(input_index, &prevouts, TapSighashType::Default)
|
||||
.unwrap();
|
||||
let message = Message::from(sighash);
|
||||
|
||||
// add tweak. this was taken from `signer::sign_psbt_schnorr`
|
||||
let keypair = keypair.tap_tweak(&secp, None).to_inner();
|
||||
let (xonlykey, _parity) = XOnlyPublicKey::from_keypair(&keypair);
|
||||
|
||||
// Must verify if we used the correct key to sign
|
||||
let verify_res = secp.verify_schnorr(&signature, &message, &xonlykey);
|
||||
assert!(verify_res.is_ok(), "The wrong internal key was used");
|
||||
}
|
||||
3765
crates/bdk/tests/wallet.rs
Normal file
3765
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.8.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.31", default-features = false }
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bdk_chain = { path = "../chain", version = "0.12", default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
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
|
||||
}
|
||||
}
|
||||
}
|
||||
750
crates/bitcoind_rpc/tests/test_emitter.rs
Normal file
750
crates/bitcoind_rpc/tests/test_emitter.rs
Normal file
@@ -0,0 +1,750 @@
|
||||
use std::collections::{BTreeMap, BTreeSet};
|
||||
|
||||
use bdk_bitcoind_rpc::Emitter;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Address, Amount, Txid},
|
||||
keychain::Balance,
|
||||
local_chain::{self, CheckPoint, LocalChain},
|
||||
Append, BlockId, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_testenv::TestEnv;
|
||||
use bitcoin::{hashes::Hash, Block, OutPoint, ScriptBuf, WScriptHash};
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
|
||||
/// 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 network_tip = env.rpc_client().get_block_count()?;
|
||||
let (mut local_chain, _) = LocalChain::from_genesis_hash(env.rpc_client().get_block_hash(0)?);
|
||||
let mut emitter = Emitter::new(env.rpc_client(), local_chain.tip(), 0);
|
||||
|
||||
// Mine some blocks and return the actual block hashes.
|
||||
// Because initializing `ElectrsD` already mines some blocks, we must include those too when
|
||||
// returning block hashes.
|
||||
let exp_hashes = {
|
||||
let mut hashes = (0..=network_tip)
|
||||
.map(|height| env.rpc_client().get_block_hash(height))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
hashes.extend(env.mine_blocks(101 - network_tip as usize, 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
|
||||
.iter_checkpoints()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
"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
|
||||
.iter_checkpoints()
|
||||
.map(|cp| (cp.height(), cp.hash()))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
exp_hashes
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, hash)| (i as u32, *hash))
|
||||
.collect::<BTreeSet<_>>(),
|
||||
"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
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_1 = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let addr_2 = env
|
||||
.rpc_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.rpc_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.rpc_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.rpc_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.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// setup addresses
|
||||
let addr_to_mine = env
|
||||
.rpc_client()
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_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.rpc_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
|
||||
.rpc_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.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks and sync up emitter
|
||||
let addr = env
|
||||
.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance, sync emitter up to tip
|
||||
let addr = env
|
||||
.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_client().get_block_hash(0)?,
|
||||
}),
|
||||
0,
|
||||
);
|
||||
|
||||
// mine blocks to get initial balance
|
||||
let addr = env
|
||||
.rpc_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.rpc_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
|
||||
.rpc_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.rpc_client(),
|
||||
CheckPoint::new(BlockId {
|
||||
height: 0,
|
||||
hash: env.rpc_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.rpc_client().get_block_hash(101)?;
|
||||
|
||||
// invalidate blocks 99a, 100a, 101a
|
||||
env.rpc_client().invalidate_block(&block_hash_99a)?;
|
||||
env.rpc_client().invalidate_block(&block_hash_100a)?;
|
||||
env.rpc_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(())
|
||||
}
|
||||
31
crates/chain/Cargo.toml
Normal file
31
crates/chain/Cargo.toml
Normal file
@@ -0,0 +1,31 @@
|
||||
[package]
|
||||
name = "bdk_chain"
|
||||
version = "0.12.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.31.0", default-features = false }
|
||||
serde_crate = { package = "serde", version = "1", optional = true, features = ["derive", "rc"] }
|
||||
|
||||
# Use hashbrown as a feature flag to have HashSet and HashMap from it.
|
||||
hashbrown = { version = "0.9.1", optional = true, features = ["serde"] }
|
||||
miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
rand = "0.8"
|
||||
proptest = "1.2.0"
|
||||
|
||||
[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;
|
||||
836
crates/chain/src/local_chain.rs
Normal file
836
crates/chain/src/local_chain.rs
Normal file
@@ -0,0 +1,836 @@
|
||||
//! The [`LocalChain`] is a local implementation of [`ChainOracle`].
|
||||
|
||||
use core::convert::Infallible;
|
||||
use core::ops::RangeBounds;
|
||||
|
||||
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 PartialEq for CheckPoint {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let self_cps = self.iter().map(|cp| cp.block_id());
|
||||
let other_cps = other.iter().map(|cp| cp.block_id());
|
||||
self_cps.eq(other_cps)
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
/// Get checkpoint at `height`.
|
||||
///
|
||||
/// Returns `None` if checkpoint at `height` does not exist`.
|
||||
pub fn get(&self, height: u32) -> Option<Self> {
|
||||
self.range(height..=height).next()
|
||||
}
|
||||
|
||||
/// Iterate checkpoints over a height range.
|
||||
///
|
||||
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||
/// height).
|
||||
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
let start_bound = range.start_bound().cloned();
|
||||
let end_bound = range.end_bound().cloned();
|
||||
self.iter()
|
||||
.skip_while(move |cp| match end_bound {
|
||||
core::ops::Bound::Included(inc_bound) => cp.height() > inc_bound,
|
||||
core::ops::Bound::Excluded(exc_bound) => cp.height() >= exc_bound,
|
||||
core::ops::Bound::Unbounded => false,
|
||||
})
|
||||
.take_while(move |cp| match start_bound {
|
||||
core::ops::Bound::Included(inc_bound) => cp.height() >= inc_bound,
|
||||
core::ops::Bound::Excluded(exc_bound) => cp.height() > exc_bound,
|
||||
core::ops::Bound::Unbounded => true,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
/// 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, PartialEq)]
|
||||
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, PartialEq)]
|
||||
pub struct LocalChain {
|
||||
tip: CheckPoint,
|
||||
}
|
||||
|
||||
impl ChainOracle for LocalChain {
|
||||
type Error = Infallible;
|
||||
|
||||
fn is_block_in_chain(
|
||||
&self,
|
||||
block: BlockId,
|
||||
chain_tip: BlockId,
|
||||
) -> Result<Option<bool>, Self::Error> {
|
||||
let chain_tip_cp = match self.tip.get(chain_tip.height) {
|
||||
// we can only determine whether `block` is in chain of `chain_tip` if `chain_tip` can
|
||||
// be identified in chain
|
||||
Some(cp) if cp.hash() == chain_tip.hash => cp,
|
||||
_ => return Ok(None),
|
||||
};
|
||||
match chain_tip_cp.get(block.height) {
|
||||
Some(cp) => Ok(Some(cp.hash() == block.hash)),
|
||||
None => Ok(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.tip.get(0).expect("genesis must exist").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 }),
|
||||
};
|
||||
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_changeset_is_applied(&changeset));
|
||||
|
||||
Ok(chain)
|
||||
}
|
||||
|
||||
/// Construct a [`LocalChain`] from a given `checkpoint` tip.
|
||||
pub fn from_tip(tip: CheckPoint) -> Result<Self, MissingGenesisError> {
|
||||
let genesis_cp = tip.iter().last().expect("must have at least one element");
|
||||
if genesis_cp.height() != 0 {
|
||||
return Err(MissingGenesisError);
|
||||
}
|
||||
Ok(Self { tip })
|
||||
}
|
||||
|
||||
/// 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.get(&0).is_none() {
|
||||
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))),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Self {
|
||||
tip: tip.expect("already checked to have genesis"),
|
||||
})
|
||||
}
|
||||
|
||||
/// 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;
|
||||
|
||||
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_cp) = self.tip.get(block_id.height) {
|
||||
let original_hash = original_cp.hash();
|
||||
if original_hash != block_id.hash {
|
||||
return Err(AlterCheckPointError {
|
||||
height: block_id.height,
|
||||
original_hash,
|
||||
update_hash: Some(block_id.hash),
|
||||
});
|
||||
}
|
||||
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> {
|
||||
let mut remove_from = Option::<CheckPoint>::None;
|
||||
let mut changeset = ChangeSet::default();
|
||||
for cp in self.tip().iter() {
|
||||
let cp_id = cp.block_id();
|
||||
if cp_id.height < block_id.height {
|
||||
break;
|
||||
}
|
||||
changeset.insert(cp_id.height, None);
|
||||
if cp_id == block_id {
|
||||
remove_from = Some(cp);
|
||||
}
|
||||
}
|
||||
self.tip = match remove_from.map(|cp| cp.prev()) {
|
||||
// The checkpoint below the earliest checkpoint to remove will be the new tip.
|
||||
Some(Some(new_tip)) => new_tip,
|
||||
// If there is no checkpoint below the earliest checkpoint to remove, it means the
|
||||
// "earliest checkpoint to remove" is the genesis block. We disallow removing the
|
||||
// genesis block.
|
||||
Some(None) => return Err(MissingGenesisError),
|
||||
// If there is nothing to remove, we return an empty changeset.
|
||||
None => return Ok(ChangeSet::default()),
|
||||
};
|
||||
Ok(changeset)
|
||||
}
|
||||
|
||||
/// 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.tip
|
||||
.iter()
|
||||
.map(|cp| {
|
||||
let block_id = cp.block_id();
|
||||
(block_id.height, Some(block_id.hash))
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Iterate over checkpoints in descending height order.
|
||||
pub fn iter_checkpoints(&self) -> CheckPointIter {
|
||||
CheckPointIter {
|
||||
current: Some(self.tip.0.clone()),
|
||||
}
|
||||
}
|
||||
|
||||
fn _check_changeset_is_applied(&self, changeset: &ChangeSet) -> bool {
|
||||
let mut curr_cp = self.tip.clone();
|
||||
for (height, exp_hash) in changeset.iter().rev() {
|
||||
match curr_cp.get(*height) {
|
||||
Some(query_cp) => {
|
||||
if query_cp.height() != *height || Some(query_cp.hash()) != *exp_hash {
|
||||
return false;
|
||||
}
|
||||
curr_cp = query_cp;
|
||||
}
|
||||
None => {
|
||||
if exp_hash.is_some() {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
/// Get checkpoint at given `height` (if it exists).
|
||||
///
|
||||
/// This is a shorthand for calling [`CheckPoint::get`] on the [`tip`].
|
||||
///
|
||||
/// [`tip`]: LocalChain::tip
|
||||
pub fn get(&self, height: u32) -> Option<CheckPoint> {
|
||||
self.tip.get(height)
|
||||
}
|
||||
|
||||
/// Iterate checkpoints over a height range.
|
||||
///
|
||||
/// Note that we always iterate checkpoints in reverse height order (iteration starts at tip
|
||||
/// height).
|
||||
///
|
||||
/// This is a shorthand for calling [`CheckPoint::range`] on the [`tip`].
|
||||
///
|
||||
/// [`tip`]: LocalChain::tip
|
||||
pub fn range<R>(&self, range: R) -> impl Iterator<Item = CheckPoint>
|
||||
where
|
||||
R: RangeBounds<u32>,
|
||||
{
|
||||
self.tip.range(range)
|
||||
}
|
||||
}
|
||||
|
||||
/// 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::{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.to_sat();
|
||||
}
|
||||
}
|
||||
for txout in &tx.output {
|
||||
if self.index_of_spk(&txout.script_pubkey).is_some() {
|
||||
received += txout.value.to_sat();
|
||||
}
|
||||
}
|
||||
|
||||
(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);
|
||||
1659
crates/chain/src/tx_graph.rs
Normal file
1659
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: bitcoin::transaction::Version::non_standard(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, transaction, Amount, 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: transaction::Version::non_standard(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: Amount::from_sat(output.value),
|
||||
script_pubkey: ScriptBuf::new(),
|
||||
},
|
||||
Some(index) => TxOut {
|
||||
value: Amount::from_sat(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)
|
||||
}
|
||||
465
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
465
crates/chain/tests/test_indexed_tx_graph.rs
Normal file
@@ -0,0 +1,465 @@
|
||||
#[macro_use]
|
||||
mod common;
|
||||
|
||||
use std::{collections::BTreeSet, sync::Arc};
|
||||
|
||||
use bdk_chain::{
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain::{self, Balance, KeychainTxOutIndex},
|
||||
local_chain::LocalChain,
|
||||
tx_graph, ChainPosition, ConfirmationHeightAnchor,
|
||||
};
|
||||
use bitcoin::{
|
||||
secp256k1::Secp256k1, Amount, 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: Amount::from_sat(10_000),
|
||||
script_pubkey: spk_0,
|
||||
},
|
||||
TxOut {
|
||||
value: Amount::from_sat(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.iter().cloned().map(Arc::new).collect(),
|
||||
..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,);
|
||||
}
|
||||
|
||||
/// 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.
|
||||
#[test]
|
||||
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: Amount::from_sat(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: Amount::from_sat(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: Amount::from_sat(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: Amount::from_sat(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: Amount::from_sat(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
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
.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
|
||||
.get(height)
|
||||
.map(|cp| cp.block_id())
|
||||
.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, Amount, 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: Amount::from_sat(10_000),
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: internal_desc
|
||||
.at_derivation_index(internal_index)
|
||||
.unwrap()
|
||||
.script_pubkey(),
|
||||
value: Amount::from_sat(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: Amount::ZERO,
|
||||
};
|
||||
|
||||
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: Amount::ZERO,
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
775
crates/chain/tests/test_local_chain.rs
Normal file
775
crates/chain/tests/test_local_chain.rs
Normal file
@@ -0,0 +1,775 @@
|
||||
use std::ops::{Bound, RangeBounds};
|
||||
|
||||
use bdk_chain::{
|
||||
local_chain::{
|
||||
AlterCheckPointError, ApplyHeaderError, CannotConnectError, ChangeSet, CheckPoint,
|
||||
LocalChain, MissingGenesisError, Update,
|
||||
},
|
||||
BlockId,
|
||||
};
|
||||
use bitcoin::{block::Header, hashes::Hash, BlockHash};
|
||||
use proptest::prelude::*;
|
||||
|
||||
#[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 checkpoint_query() {
|
||||
struct TestCase {
|
||||
chain: LocalChain,
|
||||
/// The heights we want to call [`CheckPoint::query`] with, represented as an inclusive
|
||||
/// range.
|
||||
///
|
||||
/// If a [`CheckPoint`] exists at that height, we expect [`CheckPoint::query`] to return
|
||||
/// it. If not, [`CheckPoint::query`] should return `None`.
|
||||
query_range: (u32, u32),
|
||||
}
|
||||
|
||||
let test_cases = [
|
||||
TestCase {
|
||||
chain: local_chain![(0, h!("_")), (1, h!("A"))],
|
||||
query_range: (0, 2),
|
||||
},
|
||||
TestCase {
|
||||
chain: local_chain![(0, h!("_")), (2, h!("B")), (3, h!("C"))],
|
||||
query_range: (0, 3),
|
||||
},
|
||||
];
|
||||
|
||||
for t in test_cases.into_iter() {
|
||||
let tip = t.chain.tip();
|
||||
for h in t.query_range.0..=t.query_range.1 {
|
||||
let query_result = tip.get(h);
|
||||
|
||||
// perform an exhausitive search for the checkpoint at height `h`
|
||||
let exp_hash = t
|
||||
.chain
|
||||
.iter_checkpoints()
|
||||
.find(|cp| cp.height() == h)
|
||||
.map(|cp| cp.hash());
|
||||
|
||||
match query_result {
|
||||
Some(cp) => {
|
||||
assert_eq!(Some(cp.hash()), exp_hash);
|
||||
assert_eq!(cp.height(), h);
|
||||
}
|
||||
None => assert!(exp_hash.is_none()),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[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);
|
||||
}
|
||||
}
|
||||
|
||||
fn generate_height_range_bounds(
|
||||
height_upper_bound: u32,
|
||||
) -> impl Strategy<Value = (Bound<u32>, Bound<u32>)> {
|
||||
fn generate_height_bound(height_upper_bound: u32) -> impl Strategy<Value = Bound<u32>> {
|
||||
prop_oneof![
|
||||
(0..height_upper_bound).prop_map(Bound::Included),
|
||||
(0..height_upper_bound).prop_map(Bound::Excluded),
|
||||
Just(Bound::Unbounded),
|
||||
]
|
||||
}
|
||||
(
|
||||
generate_height_bound(height_upper_bound),
|
||||
generate_height_bound(height_upper_bound),
|
||||
)
|
||||
}
|
||||
|
||||
fn generate_checkpoints(max_height: u32, max_count: usize) -> impl Strategy<Value = CheckPoint> {
|
||||
proptest::collection::btree_set(1..max_height, 0..max_count).prop_map(|mut heights| {
|
||||
heights.insert(0); // must have genesis
|
||||
CheckPoint::from_block_ids(heights.into_iter().map(|height| {
|
||||
let hash = bitcoin::hashes::Hash::hash(height.to_le_bytes().as_slice());
|
||||
BlockId { height, hash }
|
||||
}))
|
||||
.expect("blocks must be in order as it comes from btreeset")
|
||||
})
|
||||
}
|
||||
|
||||
proptest! {
|
||||
#![proptest_config(ProptestConfig {
|
||||
..Default::default()
|
||||
})]
|
||||
|
||||
/// Ensure that [`CheckPoint::range`] returns the expected checkpoint heights by comparing it
|
||||
/// against a more primitive approach.
|
||||
#[test]
|
||||
fn checkpoint_range(
|
||||
range in generate_height_range_bounds(21_000),
|
||||
cp in generate_checkpoints(21_000, 2100)
|
||||
) {
|
||||
let exp_heights = cp.iter().map(|cp| cp.height()).filter(|h| range.contains(h)).collect::<Vec<u32>>();
|
||||
let heights = cp.range(range).map(|cp| cp.height()).collect::<Vec<u32>>();
|
||||
prop_assert_eq!(heights, exp_heights);
|
||||
}
|
||||
}
|
||||
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, transaction, Amount, 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: transaction::Version::TWO,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(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: transaction::Version::ONE,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![TxIn {
|
||||
previous_output: OutPoint {
|
||||
txid: tx1.txid(),
|
||||
vout: 0,
|
||||
},
|
||||
..Default::default()
|
||||
}],
|
||||
output: vec![
|
||||
TxOut {
|
||||
value: Amount::from_sat(20_000),
|
||||
script_pubkey: spk2,
|
||||
},
|
||||
TxOut {
|
||||
script_pubkey: spk1,
|
||||
value: Amount::from_sat(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: transaction::Version::TWO,
|
||||
lock_time: absolute::LockTime::ZERO,
|
||||
input: vec![],
|
||||
output: vec![TxOut {
|
||||
value: Amount::from_sat(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());
|
||||
}
|
||||
1304
crates/chain/tests/test_tx_graph.rs
Normal file
1304
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
|
||||
);
|
||||
}
|
||||
}
|
||||
22
crates/electrum/Cargo.toml
Normal file
22
crates/electrum/Cargo.toml
Normal file
@@ -0,0 +1,22 @@
|
||||
[package]
|
||||
name = "bdk_electrum"
|
||||
version = "0.11.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.12.0", default-features = false }
|
||||
electrum-client = { version = "0.19" }
|
||||
#rustls = { version = "=0.21.1", optional = true, features = ["dangerous_configuration"] }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default-features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = "1"
|
||||
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/
|
||||
554
crates/electrum/src/electrum_ext.rs
Normal file
554
crates/electrum/src/electrum_ext.rs
Normal file
@@ -0,0 +1,554 @@
|
||||
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,
|
||||
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 {
|
||||
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,
|
||||
missing: Vec<Txid>,
|
||||
) -> Result<TxGraph<ConfirmationTimeHeightAnchor>, Error> {
|
||||
let graph = self.into_tx_graph(client, 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, 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::*;
|
||||
191
crates/electrum/tests/test_electrum.rs
Normal file
191
crates/electrum/tests/test_electrum.rs
Normal file
@@ -0,0 +1,191 @@
|
||||
use anyhow::Result;
|
||||
use bdk_chain::{
|
||||
bitcoin::{hashes::Hash, Address, Amount, ScriptBuf, WScriptHash},
|
||||
keychain::Balance,
|
||||
local_chain::LocalChain,
|
||||
ConfirmationTimeHeightAnchor, IndexedTxGraph, SpkTxOutIndex,
|
||||
};
|
||||
use bdk_electrum::{ElectrumExt, ElectrumUpdate};
|
||||
use bdk_testenv::TestEnv;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
|
||||
fn get_balance(
|
||||
recv_chain: &LocalChain,
|
||||
recv_graph: &IndexedTxGraph<ConfirmationTimeHeightAnchor, SpkTxOutIndex<()>>,
|
||||
) -> 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)
|
||||
}
|
||||
|
||||
/// Ensure that [`ElectrumExt`] can sync properly.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
/// 2. Send a tx.
|
||||
/// 3. Mine extra block to confirm sent tx.
|
||||
/// 4. Check [`Balance`] to ensure tx is confirmed.
|
||||
#[test]
|
||||
fn scan_detects_confirmed_tx() -> Result<()> {
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
});
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||
|
||||
// Create transaction that is tracked by our receiver.
|
||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
|
||||
// Mine a block to confirm sent tx.
|
||||
env.mine_blocks(1, None)?;
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
|
||||
// Check to see if tx is confirmed.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat(),
|
||||
..Balance::default()
|
||||
},
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure that confirmed txs that are reorged become unconfirmed.
|
||||
///
|
||||
/// 1. Mine 101 blocks.
|
||||
/// 2. Mine 8 blocks with a confirmed tx in each.
|
||||
/// 3. Perform 8 separate reorgs on each block with a confirmed tx.
|
||||
/// 4. Check [`Balance`] after each reorg to ensure unconfirmed amount is correct.
|
||||
#[test]
|
||||
fn tx_can_become_unconfirmed_after_reorg() -> Result<()> {
|
||||
const REORG_COUNT: usize = 8;
|
||||
const SEND_AMOUNT: Amount = Amount::from_sat(10_000);
|
||||
|
||||
let env = TestEnv::new()?;
|
||||
let client = electrum_client::Client::new(env.electrsd.electrum_url.as_str())?;
|
||||
|
||||
// Setup addresses.
|
||||
let addr_to_mine = env
|
||||
.bitcoind
|
||||
.client
|
||||
.get_new_address(None, None)?
|
||||
.assume_checked();
|
||||
let spk_to_track = ScriptBuf::new_p2wsh(&WScriptHash::all_zeros());
|
||||
let addr_to_track = Address::from_script(&spk_to_track, bdk_chain::bitcoin::Network::Regtest)?;
|
||||
|
||||
// Setup receiver.
|
||||
let (mut recv_chain, _) = LocalChain::from_genesis_hash(env.bitcoind.client.get_block_hash(0)?);
|
||||
let mut recv_graph = IndexedTxGraph::<ConfirmationTimeHeightAnchor, _>::new({
|
||||
let mut recv_index = SpkTxOutIndex::default();
|
||||
recv_index.insert_spk((), spk_to_track.clone());
|
||||
recv_index
|
||||
});
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, Some(addr_to_mine))?;
|
||||
|
||||
// Create transactions that are tracked by our receiver.
|
||||
for _ in 0..REORG_COUNT {
|
||||
env.send(&addr_to_track, SEND_AMOUNT)?;
|
||||
env.mine_blocks(1, None)?;
|
||||
}
|
||||
|
||||
// Sync up to tip.
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
let _ = recv_graph.apply_update(graph_update.clone());
|
||||
|
||||
// Retain a snapshot of all anchors before reorg process.
|
||||
let initial_anchors = graph_update.all_anchors();
|
||||
|
||||
// Check if initial balance is correct.
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * REORG_COUNT as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"initial balance must be correct",
|
||||
);
|
||||
|
||||
// Perform reorgs with different depths.
|
||||
for depth in 1..=REORG_COUNT {
|
||||
env.reorg_empty_blocks(depth)?;
|
||||
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let ElectrumUpdate {
|
||||
chain_update,
|
||||
relevant_txids,
|
||||
} = client.sync(recv_chain.tip(), [spk_to_track.clone()], None, None, 5)?;
|
||||
|
||||
let missing = relevant_txids.missing_full_txs(recv_graph.graph());
|
||||
let graph_update = relevant_txids.into_confirmation_time_tx_graph(&client, missing)?;
|
||||
let _ = recv_chain
|
||||
.apply_update(chain_update)
|
||||
.map_err(|err| anyhow::anyhow!("LocalChain update error: {:?}", err))?;
|
||||
|
||||
// Check to see if a new anchor is added during current reorg.
|
||||
if !initial_anchors.is_superset(graph_update.all_anchors()) {
|
||||
println!("New anchor added at reorg depth {}", depth);
|
||||
}
|
||||
let _ = recv_graph.apply_update(graph_update);
|
||||
|
||||
assert_eq!(
|
||||
get_balance(&recv_chain, &recv_graph)?,
|
||||
Balance {
|
||||
confirmed: SEND_AMOUNT.to_sat() * (REORG_COUNT - depth) as u64,
|
||||
trusted_pending: SEND_AMOUNT.to_sat() * depth as u64,
|
||||
..Balance::default()
|
||||
},
|
||||
"reorg_count: {}",
|
||||
depth,
|
||||
);
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
35
crates/esplora/Cargo.toml
Normal file
35
crates/esplora/Cargo.toml
Normal file
@@ -0,0 +1,35 @@
|
||||
[package]
|
||||
name = "bdk_esplora"
|
||||
version = "0.11.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.12.0", default-features = false }
|
||||
esplora-client = { version = "0.7.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.31.0", optional = true, default-features = false }
|
||||
miniscript = { version = "11.0.0", optional = true, default-features = false }
|
||||
|
||||
[dev-dependencies]
|
||||
bdk_testenv = { path = "../testenv", default_features = false }
|
||||
electrsd = { version= "0.27.1", 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/
|
||||
334
crates/esplora/src/async_ext.rs
Normal file
334
crates/esplora/src/async_ext.rs
Normal file
@@ -0,0 +1,334 @@
|
||||
use async_trait::async_trait;
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Amount, 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.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||
///
|
||||
/// This follows the same approach as other Bitcoin-related software,
|
||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||
///
|
||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||
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();
|
||||
let stop_gap = Ord::max(stop_gap, 1);
|
||||
|
||||
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: Amount::from_sat(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 gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if gap_limit_reached {
|
||||
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)
|
||||
}
|
||||
}
|
||||
331
crates/esplora/src/blocking_ext.rs
Normal file
331
crates/esplora/src/blocking_ext.rs
Normal file
@@ -0,0 +1,331 @@
|
||||
use std::thread::JoinHandle;
|
||||
|
||||
use bdk_chain::collections::btree_map;
|
||||
use bdk_chain::collections::BTreeMap;
|
||||
use bdk_chain::{
|
||||
bitcoin::{Amount, 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.
|
||||
///
|
||||
/// ## Note
|
||||
///
|
||||
/// `stop_gap` is defined as "the maximum number of consecutive unused addresses".
|
||||
/// For example, with a `stop_gap` of 3, `full_scan` will keep scanning
|
||||
/// until it encounters 3 consecutive script pubkeys with no associated transactions.
|
||||
///
|
||||
/// This follows the same approach as other Bitcoin-related software,
|
||||
/// such as [Electrum](https://electrum.readthedocs.io/en/latest/faq.html#what-is-the-gap-limit),
|
||||
/// [BTCPay Server](https://docs.btcpayserver.org/FAQ/Wallet/#the-gap-limit-problem),
|
||||
/// and [Sparrow](https://www.sparrowwallet.com/docs/faq.html#ive-restored-my-wallet-but-some-of-my-funds-are-missing).
|
||||
///
|
||||
/// A `stop_gap` of 0 will be treated as a `stop_gap` of 1.
|
||||
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();
|
||||
let stop_gap = Ord::max(stop_gap, 1);
|
||||
|
||||
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: Amount::from_sat(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 gap_limit_reached = if let Some(i) = last_active_index {
|
||||
last_index >= i.saturating_add(stop_gap as u32)
|
||||
} else {
|
||||
last_index + 1 >= stop_gap as u32
|
||||
};
|
||||
if gap_limit_reached {
|
||||
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
|
||||
}
|
||||
}
|
||||
182
crates/esplora/tests/async_ext.rs
Normal file
182
crates/esplora/tests/async_ext.rs
Normal file
@@ -0,0 +1,182 @@
|
||||
use bdk_esplora::EsploraAsyncExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use esplora_client::{self, Builder};
|
||||
use std::collections::{BTreeMap, HashSet};
|
||||
use std::str::FromStr;
|
||||
use std::thread::sleep;
|
||||
use std::time::Duration;
|
||||
|
||||
use bdk_chain::bitcoin::{Address, Amount, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
|
||||
#[tokio::test]
|
||||
pub async fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
|
||||
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 client.get_height().await.unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = 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 `stop_gap`.
|
||||
#[tokio::test]
|
||||
pub async fn test_async_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_async()?;
|
||||
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 client.get_height().await.unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a gap limit of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1).await?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 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 client.get_height().await.unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 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) = client.full_scan(keychains, 6, 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(())
|
||||
}
|
||||
375
crates/esplora/tests/blocking_ext.rs
Normal file
375
crates/esplora/tests/blocking_ext.rs
Normal file
@@ -0,0 +1,375 @@
|
||||
use bdk_chain::local_chain::LocalChain;
|
||||
use bdk_chain::BlockId;
|
||||
use bdk_esplora::EsploraExt;
|
||||
use electrsd::bitcoind::anyhow;
|
||||
use electrsd::bitcoind::bitcoincore_rpc::RpcApi;
|
||||
use esplora_client::{self, 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, Txid};
|
||||
use bdk_testenv::TestEnv;
|
||||
|
||||
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")
|
||||
}};
|
||||
}
|
||||
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_without_keychain() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
|
||||
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 client.get_height().unwrap() < 102 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
let graph_update = 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 `stop_gap`.
|
||||
#[test]
|
||||
pub fn test_update_tx_graph_stop_gap() -> anyhow::Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
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 client.get_height().unwrap() < 103 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with a stop_gap of 3 won't find the transaction, but a scan with a gap limit of 4
|
||||
// will.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 3, 1)?;
|
||||
assert!(graph_update.full_txs().next().is_none());
|
||||
assert!(active_indices.is_empty());
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 4, 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 client.get_height().unwrap() < 104 {
|
||||
sleep(Duration::from_millis(10))
|
||||
}
|
||||
|
||||
// A scan with gap limit 5 won't find the second transaction, but a scan with gap limit 6 will.
|
||||
// The last active indice won't be updated in the first case but will in the second one.
|
||||
let (graph_update, active_indices) = client.full_scan(keychains.clone(), 5, 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) = client.full_scan(keychains, 6, 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()?;
|
||||
let base_url = format!("http://{}", &env.electrsd.esplora_url.clone().unwrap());
|
||||
let client = Builder::new(base_url.as_str()).build_blocking();
|
||||
|
||||
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 = 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.get(*height).map(|cp| cp.hash()),
|
||||
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.9.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.12.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 {}
|
||||
460
crates/file_store/src/store.rs
Normal file
460
crates/file_store/src/store.rs
Normal file
@@ -0,0 +1,460 @@
|
||||
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)
|
||||
.truncate(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.2.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.8.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::{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::secp256k1::{All, Secp256k1};
|
||||
use bdk::bitcoin::Psbt;
|
||||
|
||||
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 Psbt,
|
||||
_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);
|
||||
// }
|
||||
// }
|
||||
24
crates/testenv/Cargo.toml
Normal file
24
crates/testenv/Cargo.toml
Normal file
@@ -0,0 +1,24 @@
|
||||
[package]
|
||||
name = "bdk_testenv"
|
||||
version = "0.2.0"
|
||||
edition = "2021"
|
||||
rust-version = "1.63"
|
||||
homepage = "https://bitcoindevkit.org"
|
||||
repository = "https://github.com/bitcoindevkit/bdk"
|
||||
documentation = "https://docs.rs/bdk_testenv"
|
||||
description = "Testing framework for BDK chain sources."
|
||||
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]
|
||||
bitcoincore-rpc = { version = "0.18" }
|
||||
bdk_chain = { path = "../chain", version = "0.12", default-features = false }
|
||||
electrsd = { version= "0.27.1", features = ["bitcoind_25_0", "esplora_a33e97e1", "legacy"] }
|
||||
anyhow = { version = "1" }
|
||||
|
||||
[features]
|
||||
default = ["std"]
|
||||
std = ["bdk_chain/std"]
|
||||
serde = ["bdk_chain/serde"]
|
||||
6
crates/testenv/README.md
Normal file
6
crates/testenv/README.md
Normal file
@@ -0,0 +1,6 @@
|
||||
# BDK TestEnv
|
||||
|
||||
This crate sets up a regtest environment with a single [`bitcoind`] node
|
||||
connected to an [`electrs`] instance. This framework provides the infrastructure
|
||||
for testing chain source crates, e.g., [`bdk_chain`], [`bdk_electrum`],
|
||||
[`bdk_esplora`], etc.
|
||||
278
crates/testenv/src/lib.rs
Normal file
278
crates/testenv/src/lib.rs
Normal file
@@ -0,0 +1,278 @@
|
||||
use bdk_chain::bitcoin::{
|
||||
address::NetworkChecked, block::Header, hash_types::TxMerkleNode, hashes::Hash,
|
||||
secp256k1::rand::random, transaction, Address, Amount, Block, BlockHash, CompactTarget,
|
||||
ScriptBuf, ScriptHash, Transaction, TxIn, TxOut, Txid,
|
||||
};
|
||||
use bitcoincore_rpc::{
|
||||
bitcoincore_rpc_json::{GetBlockTemplateModes, GetBlockTemplateRules},
|
||||
RpcApi,
|
||||
};
|
||||
use electrsd::electrum_client::ElectrumApi;
|
||||
use std::time::Duration;
|
||||
|
||||
/// Struct for running a regtest environment with a single `bitcoind` node with an `electrs`
|
||||
/// instance connected to it.
|
||||
pub struct TestEnv {
|
||||
pub bitcoind: electrsd::bitcoind::BitcoinD,
|
||||
pub electrsd: electrsd::ElectrsD,
|
||||
}
|
||||
|
||||
impl TestEnv {
|
||||
/// Construct a new [`TestEnv`] instance with default configurations.
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let bitcoind = match std::env::var_os("BITCOIND_EXE") {
|
||||
Some(bitcoind_path) => electrsd::bitcoind::BitcoinD::new(bitcoind_path),
|
||||
None => {
|
||||
let bitcoind_exe = electrsd::bitcoind::downloaded_exe_path()
|
||||
.expect(
|
||||
"you need to provide an env var BITCOIND_EXE or specify a bitcoind version feature",
|
||||
);
|
||||
electrsd::bitcoind::BitcoinD::with_conf(
|
||||
bitcoind_exe,
|
||||
&electrsd::bitcoind::Conf::default(),
|
||||
)
|
||||
}
|
||||
}?;
|
||||
|
||||
let mut electrsd_conf = electrsd::Conf::default();
|
||||
electrsd_conf.http_enabled = true;
|
||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||
Some(env_electrs_exe) => {
|
||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &bitcoind, &electrsd_conf)
|
||||
}
|
||||
None => {
|
||||
let electrs_exe = electrsd::downloaded_exe_path()
|
||||
.expect("electrs version feature must be enabled");
|
||||
electrsd::ElectrsD::with_conf(electrs_exe, &bitcoind, &electrsd_conf)
|
||||
}
|
||||
}?;
|
||||
|
||||
Ok(Self { bitcoind, electrsd })
|
||||
}
|
||||
|
||||
/// Exposes the [`ElectrumApi`] calls from the Electrum client.
|
||||
pub fn electrum_client(&self) -> &impl ElectrumApi {
|
||||
&self.electrsd.client
|
||||
}
|
||||
|
||||
/// Exposes the [`RpcApi`] calls from [`bitcoincore_rpc`].
|
||||
pub fn rpc_client(&self) -> &impl RpcApi {
|
||||
&self.bitcoind.client
|
||||
}
|
||||
|
||||
// Reset `electrsd` so that new blocks can be seen.
|
||||
pub fn reset_electrsd(mut self) -> anyhow::Result<Self> {
|
||||
let mut electrsd_conf = electrsd::Conf::default();
|
||||
electrsd_conf.http_enabled = true;
|
||||
let electrsd = match std::env::var_os("ELECTRS_EXE") {
|
||||
Some(env_electrs_exe) => {
|
||||
electrsd::ElectrsD::with_conf(env_electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||
}
|
||||
None => {
|
||||
let electrs_exe = electrsd::downloaded_exe_path()
|
||||
.expect("electrs version feature must be enabled");
|
||||
electrsd::ElectrsD::with_conf(electrs_exe, &self.bitcoind, &electrsd_conf)
|
||||
}
|
||||
}?;
|
||||
self.electrsd = electrsd;
|
||||
Ok(self)
|
||||
}
|
||||
|
||||
/// Mine a number of blocks of a given size `count`, which may be specified to a given coinbase
|
||||
/// `address`.
|
||||
pub 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)
|
||||
}
|
||||
|
||||
/// Mine a block that is guaranteed to be empty even with transactions in the mempool.
|
||||
pub fn mine_empty_block(&self) -> anyhow::Result<(usize, BlockHash)> {
|
||||
let bt = self.bitcoind.client.get_block_template(
|
||||
GetBlockTemplateModes::Template,
|
||||
&[GetBlockTemplateRules::SegWit],
|
||||
&[],
|
||||
)?;
|
||||
|
||||
let txdata = vec![Transaction {
|
||||
version: transaction::Version::ONE,
|
||||
lock_time: bdk_chain::bitcoin::absolute::LockTime::from_height(0)?,
|
||||
input: vec![TxIn {
|
||||
previous_output: bdk_chain::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: bdk_chain::bitcoin::Sequence::default(),
|
||||
witness: bdk_chain::bitcoin::Witness::new(),
|
||||
}],
|
||||
output: vec![TxOut {
|
||||
value: Amount::ZERO,
|
||||
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: bdk_chain::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.bitcoind.client.submit_block(&block)?;
|
||||
Ok((bt.height as usize, block.block_hash()))
|
||||
}
|
||||
|
||||
/// This method waits for the Electrum notification indicating that a new block has been mined.
|
||||
pub fn wait_until_electrum_sees_block(&self) -> anyhow::Result<()> {
|
||||
self.electrsd.client.block_headers_subscribe()?;
|
||||
let mut delay = Duration::from_millis(64);
|
||||
|
||||
loop {
|
||||
self.electrsd.trigger()?;
|
||||
self.electrsd.client.ping()?;
|
||||
if self.electrsd.client.block_headers_pop()?.is_some() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
if delay.as_millis() < 512 {
|
||||
delay = delay.mul_f32(2.0);
|
||||
}
|
||||
std::thread::sleep(delay);
|
||||
}
|
||||
}
|
||||
|
||||
/// Invalidate a number of blocks of a given size `count`.
|
||||
pub fn invalidate_blocks(&self, count: usize) -> anyhow::Result<()> {
|
||||
let mut hash = self.bitcoind.client.get_best_block_hash()?;
|
||||
for _ in 0..count {
|
||||
let prev_hash = self
|
||||
.bitcoind
|
||||
.client
|
||||
.get_block_info(&hash)?
|
||||
.previousblockhash;
|
||||
self.bitcoind.client.invalidate_block(&hash)?;
|
||||
match prev_hash {
|
||||
Some(prev_hash) => hash = prev_hash,
|
||||
None => break,
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Reorg a number of blocks of a given size `count`.
|
||||
/// Refer to [`TestEnv::mine_empty_block`] for more information.
|
||||
pub fn reorg(&self, count: usize) -> anyhow::Result<Vec<BlockHash>> {
|
||||
let start_height = self.bitcoind.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = self.mine_blocks(count, None);
|
||||
assert_eq!(
|
||||
self.bitcoind.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
res
|
||||
}
|
||||
|
||||
/// Reorg with a number of empty blocks of a given size `count`.
|
||||
pub fn reorg_empty_blocks(&self, count: usize) -> anyhow::Result<Vec<(usize, BlockHash)>> {
|
||||
let start_height = self.bitcoind.client.get_block_count()?;
|
||||
self.invalidate_blocks(count)?;
|
||||
|
||||
let res = (0..count)
|
||||
.map(|_| self.mine_empty_block())
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
assert_eq!(
|
||||
self.bitcoind.client.get_block_count()?,
|
||||
start_height,
|
||||
"reorg should not result in height change"
|
||||
);
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
/// Send a tx of a given `amount` to a given `address`.
|
||||
pub fn send(&self, address: &Address<NetworkChecked>, amount: Amount) -> anyhow::Result<Txid> {
|
||||
let txid = self
|
||||
.bitcoind
|
||||
.client
|
||||
.send_to_address(address, amount, None, None, None, None, None, None)?;
|
||||
Ok(txid)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use crate::TestEnv;
|
||||
use anyhow::Result;
|
||||
use bitcoincore_rpc::RpcApi;
|
||||
|
||||
/// This checks that reorgs initiated by `bitcoind` is detected by our `electrsd` instance.
|
||||
#[test]
|
||||
fn test_reorg_is_detected_in_electrsd() -> Result<()> {
|
||||
let env = TestEnv::new()?;
|
||||
|
||||
// Mine some blocks.
|
||||
env.mine_blocks(101, None)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let height = env.bitcoind.client.get_block_count()?;
|
||||
let blocks = (0..=height)
|
||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
// Perform reorg on six blocks.
|
||||
env.reorg(6)?;
|
||||
env.wait_until_electrum_sees_block()?;
|
||||
let reorged_height = env.bitcoind.client.get_block_count()?;
|
||||
let reorged_blocks = (0..=height)
|
||||
.map(|i| env.bitcoind.client.get_block_hash(i))
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
|
||||
assert_eq!(height, reorged_height);
|
||||
|
||||
// Block hashes should not be equal on the six reorged blocks.
|
||||
for (i, (block, reorged_block)) in blocks.iter().zip(reorged_blocks.iter()).enumerate() {
|
||||
match i <= height as usize - 6 {
|
||||
true => assert_eq!(block, reorged_block),
|
||||
false => assert_ne!(block, reorged_block),
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
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" }
|
||||
721
example-crates/example_cli/src/lib.rs
Normal file
721
example-crates/example_cli/src/lib.rs
Normal file
@@ -0,0 +1,721 @@
|
||||
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::BTreeMap, path::PathBuf, sync::Mutex, time::Duration};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{
|
||||
absolute, address,
|
||||
secp256k1::Secp256k1,
|
||||
sighash::{Prevouts, SighashCache},
|
||||
transaction, Address, Amount, 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: &BTreeMap<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.to_sat(),
|
||||
plan.expected_weight() as _,
|
||||
plan.witness_version().is_some(),
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
|
||||
let mut outputs = vec![TxOut {
|
||||
value: Amount::from_sat(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: Amount::ZERO,
|
||||
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 = Amount::from_sat(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: transaction::Version::TWO,
|
||||
// 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: &BTreeMap<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 mut graph_update = relevant_txids.into_tx_graph(&client, missing_txids)?;
|
||||
let now = std::time::UNIX_EPOCH
|
||||
.elapsed()
|
||||
.expect("must get time")
|
||||
.as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
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" }
|
||||
|
||||
358
example-crates/example_esplora/src/main.rs
Normal file
358
example-crates/example_esplora/src/main.rs
Normal file
@@ -0,0 +1,358 @@
|
||||
use std::{
|
||||
collections::{BTreeMap, BTreeSet},
|
||||
io::{self, Write},
|
||||
sync::Mutex,
|
||||
};
|
||||
|
||||
use bdk_chain::{
|
||||
bitcoin::{constants::genesis_block, Address, Network, OutPoint, ScriptBuf, Txid},
|
||||
indexed_tx_graph::{self, IndexedTxGraph},
|
||||
keychain,
|
||||
local_chain::{self, LocalChain},
|
||||
Append, ConfirmationTimeHeightAnchor,
|
||||
};
|
||||
|
||||
use bdk_esplora::{esplora_client, EsploraExt};
|
||||
|
||||
use example_cli::{
|
||||
anyhow::{self, Context},
|
||||
clap::{self, Parser, Subcommand},
|
||||
Keychain,
|
||||
};
|
||||
|
||||
const DB_MAGIC: &[u8] = b"bdk_example_esplora";
|
||||
const DB_PATH: &str = ".bdk_esplora_example.db";
|
||||
|
||||
type ChangeSet = (
|
||||
local_chain::ChangeSet,
|
||||
indexed_tx_graph::ChangeSet<ConfirmationTimeHeightAnchor, keychain::ChangeSet<Keychain>>,
|
||||
);
|
||||
|
||||
#[derive(Subcommand, Debug, Clone)]
|
||||
enum EsploraCommands {
|
||||
/// Scans the addresses in the wallet using the esplora 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)]
|
||||
esplora_args: EsploraArgs,
|
||||
},
|
||||
/// Scan for particular addresses and unconfirmed transactions using the esplora 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)]
|
||||
esplora_args: EsploraArgs,
|
||||
},
|
||||
}
|
||||
impl EsploraCommands {
|
||||
fn esplora_args(&self) -> EsploraArgs {
|
||||
match self {
|
||||
EsploraCommands::Scan { esplora_args, .. } => esplora_args.clone(),
|
||||
EsploraCommands::Sync { esplora_args, .. } => esplora_args.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(clap::Args, Debug, Clone)]
|
||||
pub struct EsploraArgs {
|
||||
/// The esplora url endpoint to connect to e.g. `<https://blockstream.info/api>`
|
||||
/// If not provided it'll be set to a default for the network provided
|
||||
esplora_url: Option<String>,
|
||||
}
|
||||
|
||||
impl EsploraArgs {
|
||||
pub fn client(&self, network: Network) -> anyhow::Result<esplora_client::BlockingClient> {
|
||||
let esplora_url = self.esplora_url.as_deref().unwrap_or(match network {
|
||||
Network::Bitcoin => "https://blockstream.info/api",
|
||||
Network::Testnet => "https://blockstream.info/testnet/api",
|
||||
Network::Regtest => "http://localhost:3002",
|
||||
Network::Signet => "https://mempool.space/signet/api",
|
||||
_ => panic!("unsupported network"),
|
||||
});
|
||||
|
||||
let client = esplora_client::Builder::new(esplora_url).build_blocking();
|
||||
Ok(client)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Parser, Debug, Clone, PartialEq)]
|
||||
pub struct ScanOptions {
|
||||
/// Max number of concurrent esplora server requests.
|
||||
#[clap(long, default_value = "1")]
|
||||
pub parallel_requests: usize,
|
||||
}
|
||||
|
||||
fn main() -> anyhow::Result<()> {
|
||||
let example_cli::Init {
|
||||
args,
|
||||
keymap,
|
||||
index,
|
||||
db,
|
||||
init_changeset,
|
||||
} = example_cli::init::<EsploraCommands, EsploraArgs, ChangeSet>(DB_MAGIC, DB_PATH)?;
|
||||
|
||||
let genesis_hash = genesis_block(args.network).block_hash();
|
||||
|
||||
let (init_chain_changeset, init_indexed_tx_graph_changeset) = init_changeset;
|
||||
|
||||
// Construct `IndexedTxGraph` and `LocalChain` with our initial changeset. They are wrapped in
|
||||
// `Mutex` to display how they can be used in a multithreaded context. Technically the mutexes
|
||||
// aren't strictly needed here.
|
||||
let graph = Mutex::new({
|
||||
let mut graph = IndexedTxGraph::new(index);
|
||||
graph.apply_changeset(init_indexed_tx_graph_changeset);
|
||||
graph
|
||||
});
|
||||
let chain = Mutex::new({
|
||||
let (mut chain, _) = LocalChain::from_genesis_hash(genesis_hash);
|
||||
chain.apply_changeset(&init_chain_changeset)?;
|
||||
chain
|
||||
});
|
||||
|
||||
let esplora_cmd = match &args.command {
|
||||
// These are commands that are handled by this example (sync, scan).
|
||||
example_cli::Commands::ChainSpecific(esplora_cmd) => esplora_cmd,
|
||||
// These are general commands handled by example_cli. Execute the cmd and return.
|
||||
general_cmd => {
|
||||
return example_cli::handle_commands(
|
||||
&graph,
|
||||
&db,
|
||||
&chain,
|
||||
&keymap,
|
||||
args.network,
|
||||
|esplora_args, tx| {
|
||||
let client = esplora_args.client(args.network)?;
|
||||
client
|
||||
.broadcast(tx)
|
||||
.map(|_| ())
|
||||
.map_err(anyhow::Error::from)
|
||||
},
|
||||
general_cmd.clone(),
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
let client = esplora_cmd.esplora_args().client(args.network)?;
|
||||
// Prepare the `IndexedTxGraph` update based on whether we are scanning or syncing.
|
||||
// Scanning: We are iterating through spks of all keychains and scanning for transactions for
|
||||
// each spk. We start with the lowest derivation index spk and stop scanning after `stop_gap`
|
||||
// number of consecutive spks have no transaction history. A Scan is done in situations of
|
||||
// wallet restoration. It is a special case. Applications should use "sync" style updates
|
||||
// after an initial scan.
|
||||
// Syncing: We only check for specified spks, utxos and txids to update their confirmation
|
||||
// status or fetch missing transactions.
|
||||
let indexed_tx_graph_changeset = match &esplora_cmd {
|
||||
EsploraCommands::Scan {
|
||||
stop_gap,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
let keychain_spks = graph
|
||||
.lock()
|
||||
.expect("mutex must not be poisoned")
|
||||
.index
|
||||
.all_unbounded_spk_iters()
|
||||
.into_iter()
|
||||
// This `map` is purely for logging.
|
||||
.map(|(keychain, iter)| {
|
||||
let mut first = true;
|
||||
let spk_iter = iter.inspect(move |(i, _)| {
|
||||
if first {
|
||||
eprint!("\nscanning {}: ", keychain);
|
||||
first = false;
|
||||
}
|
||||
eprint!("{} ", i);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
});
|
||||
(keychain, spk_iter)
|
||||
})
|
||||
.collect::<BTreeMap<_, _>>();
|
||||
|
||||
// The client scans keychain spks for transaction histories, stopping after `stop_gap`
|
||||
// is reached. It returns a `TxGraph` update (`graph_update`) and a structure that
|
||||
// represents the last active spk derivation indices of keychains
|
||||
// (`keychain_indices_update`).
|
||||
let (mut graph_update, last_active_indices) = client
|
||||
.full_scan(keychain_spks, *stop_gap, scan_options.parallel_requests)
|
||||
.context("scanning for transactions")?;
|
||||
|
||||
// We want to keep track of the latest time a transaction was seen unconfirmed.
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
let mut graph = graph.lock().expect("mutex must not be poisoned");
|
||||
// Because we did a stop gap based scan we are likely to have some updates to our
|
||||
// deriviation indices. Usually before a scan you are on a fresh wallet with no
|
||||
// addresses derived so we need to derive up to last active addresses the scan found
|
||||
// before adding the transactions.
|
||||
let (_, index_changeset) = graph.index.reveal_to_target_multi(&last_active_indices);
|
||||
let mut indexed_tx_graph_changeset = graph.apply_update(graph_update);
|
||||
indexed_tx_graph_changeset.append(index_changeset.into());
|
||||
indexed_tx_graph_changeset
|
||||
}
|
||||
EsploraCommands::Sync {
|
||||
mut unused_spks,
|
||||
all_spks,
|
||||
mut utxos,
|
||||
mut unconfirmed,
|
||||
scan_options,
|
||||
..
|
||||
} => {
|
||||
if !(*all_spks || unused_spks || utxos || unconfirmed) {
|
||||
// If nothing is specifically selected, we select everything (except all spks).
|
||||
unused_spks = true;
|
||||
unconfirmed = true;
|
||||
utxos = true;
|
||||
} else if *all_spks {
|
||||
// If all spks is selected, we don't need to also select unused spks (as unused spks
|
||||
// is a subset of all spks).
|
||||
unused_spks = false;
|
||||
}
|
||||
|
||||
// Spks, outpoints and txids we want updates on will be accumulated here.
|
||||
let mut spks: Box<dyn Iterator<Item = ScriptBuf>> = Box::new(core::iter::empty());
|
||||
let mut outpoints: Box<dyn Iterator<Item = OutPoint>> = Box::new(core::iter::empty());
|
||||
let mut txids: Box<dyn Iterator<Item = Txid>> = Box::new(core::iter::empty());
|
||||
|
||||
// Get a short lock on the structures to get spks, utxos, and txs that we are interested
|
||||
// in.
|
||||
{
|
||||
let graph = graph.lock().unwrap();
|
||||
let chain = chain.lock().unwrap();
|
||||
let chain_tip = chain.tip().block_id();
|
||||
|
||||
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);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
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,
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
spk
|
||||
})));
|
||||
}
|
||||
if utxos {
|
||||
// We want to search for whether the UTXO is spent, and spent by which
|
||||
// transaction. We provide the outpoint of the UTXO to
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
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
|
||||
);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
})
|
||||
.map(|utxo| utxo.outpoint),
|
||||
);
|
||||
};
|
||||
if unconfirmed {
|
||||
// We want to search for whether the unconfirmed transaction is now confirmed.
|
||||
// We provide the unconfirmed txids to
|
||||
// `EsploraExt::update_tx_graph_without_keychain`.
|
||||
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);
|
||||
// Flush early to ensure we print at every iteration.
|
||||
let _ = io::stderr().flush();
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
let mut graph_update =
|
||||
client.sync(spks, txids, outpoints, scan_options.parallel_requests)?;
|
||||
|
||||
// Update last seen unconfirmed
|
||||
let now = std::time::UNIX_EPOCH.elapsed().unwrap().as_secs();
|
||||
let _ = graph_update.update_last_seen_unconfirmed(now);
|
||||
|
||||
graph.lock().unwrap().apply_update(graph_update)
|
||||
}
|
||||
};
|
||||
|
||||
println!();
|
||||
|
||||
// Now that we're done updating the `IndexedTxGraph`, it's time to update the `LocalChain`! We
|
||||
// want the `LocalChain` to have data about all the anchors in the `TxGraph` - for this reason,
|
||||
// we want retrieve the blocks at the heights of the newly added anchors that are missing from
|
||||
// our view of the chain.
|
||||
let (missing_block_heights, tip) = {
|
||||
let chain = &*chain.lock().unwrap();
|
||||
let missing_block_heights = indexed_tx_graph_changeset
|
||||
.graph
|
||||
.missing_heights_from(chain)
|
||||
.collect::<BTreeSet<_>>();
|
||||
let tip = chain.tip();
|
||||
(missing_block_heights, tip)
|
||||
};
|
||||
|
||||
println!("prev tip: {}", tip.height());
|
||||
println!("missing block heights: {:?}", missing_block_heights);
|
||||
|
||||
// Here, we actually fetch the missing blocks and create a `local_chain::Update`.
|
||||
let chain_changeset = {
|
||||
let chain_update = client
|
||||
.update_local_chain(tip, missing_block_heights)
|
||||
.context("scanning for blocks")?;
|
||||
println!("new tip: {}", chain_update.tip.height());
|
||||
chain.lock().unwrap().apply_update(chain_update)?
|
||||
};
|
||||
|
||||
// We persist the changes
|
||||
let mut db = db.lock().unwrap();
|
||||
db.stage((chain_changeset, indexed_tx_graph_changeset));
|
||||
db.commit()?;
|
||||
Ok(())
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user