Compare commits
1336 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
db58257e24 | ||
|
|
d97fa8b923 | ||
|
|
655d470280 | ||
|
|
ce16689d73 | ||
|
|
3da675df42 | ||
|
|
e2a063f0e3 | ||
|
|
ddeb835168 | ||
|
|
74d15d1fbc | ||
|
|
88cbec291c | ||
|
|
d30cba1089 | ||
|
|
3c0810963f | ||
|
|
3447484a66 | ||
|
|
71eb1ae57d | ||
|
|
4c61411802 | ||
|
|
a1e5dfc266 | ||
|
|
c579a6bf2f | ||
|
|
eb93eb7ee9 | ||
|
|
0b90d3c3c2 | ||
|
|
f312064bfb | ||
|
|
b63ac70113 | ||
|
|
72b4a1cb34 | ||
|
|
662049681d | ||
|
|
699e2b74f6 | ||
|
|
f5ad1ee438 | ||
|
|
ae227ce2c6 | ||
|
|
7e228f22a8 | ||
|
|
c1129bb086 | ||
|
|
9246d5c51c | ||
|
|
117a145b97 | ||
|
|
e074a8fab3 | ||
|
|
f9d876e8a3 | ||
|
|
a4211251bd | ||
|
|
d695383386 | ||
|
|
79d0655a96 | ||
|
|
78c8990772 | ||
|
|
86de1ebdc2 | ||
|
|
4c9d25cbe7 | ||
|
|
04b053e405 | ||
|
|
e1a25f6c30 | ||
|
|
6db7d42654 | ||
|
|
b8035d9db7 | ||
|
|
83f1c883ba | ||
|
|
f1a1c033d1 | ||
|
|
6914e6a4a5 | ||
|
|
413fdaf3fb | ||
|
|
e3eef0adff | ||
|
|
84aacc6c39 | ||
|
|
57617dc4f7 | ||
|
|
a5beb340e4 | ||
|
|
31ace742a5 | ||
|
|
2c25f38a5a | ||
|
|
c8033e1725 | ||
|
|
9bf48a7e9a | ||
|
|
a4414fbbb9 | ||
|
|
af08c42d58 | ||
|
|
b2b5ec052b | ||
|
|
941590d384 | ||
|
|
906fa0b7b8 | ||
|
|
593afee12d | ||
|
|
552d182594 | ||
|
|
615f3d633e | ||
|
|
6f401b4265 | ||
|
|
2999b37607 | ||
|
|
b69fc15cc1 | ||
|
|
d650829f9b | ||
|
|
455cdffede | ||
|
|
8703153c44 | ||
|
|
6feb74137d | ||
|
|
753301cf38 | ||
|
|
afb7532f17 | ||
|
|
ed5d748a9f | ||
|
|
b6363a8da4 | ||
|
|
afdd53194b | ||
|
|
6f0d7f9a87 | ||
|
|
f09b638343 | ||
|
|
56e05a3e1e | ||
|
|
1aae94f17d | ||
|
|
3b8b738be9 | ||
|
|
55e6460707 | ||
|
|
34ea769501 | ||
|
|
57e7b41510 | ||
|
|
6aec5b00ad | ||
|
|
5966150db5 | ||
|
|
fe8b89dc2c | ||
|
|
9f0402c685 | ||
|
|
4f332436cc | ||
|
|
e888c2a317 | ||
|
|
eb64ae0260 | ||
|
|
48120906c9 | ||
|
|
ae4bb33c6a | ||
|
|
8742a9a506 | ||
|
|
9ca21ecf2d | ||
|
|
58bf721f2e | ||
|
|
560b7286b5 | ||
|
|
33508380e7 | ||
|
|
ca3bde901e | ||
|
|
89c84f1da6 | ||
|
|
a3db2145dc | ||
|
|
a5eca91602 | ||
|
|
5c7be35ca5 | ||
|
|
16edbf3026 | ||
|
|
00ed751ee5 | ||
|
|
9f048b0ea3 | ||
|
|
6619646088 | ||
|
|
3031f21182 | ||
|
|
4eaba9b3db | ||
|
|
72d437d5c8 | ||
|
|
4efa39ddb1 | ||
|
|
745f3a6181 | ||
|
|
1a3c94a501 | ||
|
|
3d7d9d2612 | ||
|
|
4c16e55aca | ||
|
|
ded6959fd5 | ||
|
|
d0501ab840 | ||
|
|
678028cee9 | ||
|
|
a21fb1b69c | ||
|
|
23e99ddd4d | ||
|
|
3726cfa319 | ||
|
|
fef368bb38 | ||
|
|
080771db62 | ||
|
|
fc02cd7b0c | ||
|
|
07ee114116 | ||
|
|
74d27f2c7c | ||
|
|
f4710ca639 | ||
|
|
900a0a114a | ||
|
|
035126b280 | ||
|
|
9d50e273a3 | ||
|
|
053b6bcfad | ||
|
|
663f4c251e | ||
|
|
6df4f27929 | ||
|
|
575d699917 | ||
|
|
d2c745e610 | ||
|
|
7857f38f45 | ||
|
|
9ce799578c | ||
|
|
39e4e8d8a4 | ||
|
|
fbbcecc635 | ||
|
|
8b97d4f833 | ||
|
|
e536cfad0f | ||
|
|
e2dae56698 | ||
|
|
2d8c4fda2f | ||
|
|
0cb211e14c | ||
|
|
7e4f6f5b4a | ||
|
|
6feb189a21 | ||
|
|
35ce62c021 | ||
|
|
0cb599dbf6 | ||
|
|
c1aa44c476 | ||
|
|
109e27cfb2 | ||
|
|
ce7c053de0 | ||
|
|
e417250f6d | ||
|
|
7285dfa66a | ||
|
|
f903e16c47 | ||
|
|
4d9422a064 | ||
|
|
acf0d0ebe8 | ||
|
|
153a133ea6 | ||
|
|
c3627545be | ||
|
|
b35dae72a5 | ||
|
|
f9debb148b | ||
|
|
23ef68a2f7 | ||
|
|
9c80088abd | ||
|
|
69155bc60b | ||
|
|
4db0d76ca4 | ||
|
|
b83783cc3d | ||
|
|
6d3f8af007 | ||
|
|
266aba8a6c | ||
|
|
6dda51d026 | ||
|
|
dbb87b4ef9 | ||
|
|
18dca6f4c8 | ||
|
|
1402f087ab | ||
|
|
f2dff16c37 | ||
|
|
39b1080166 | ||
|
|
0a7139c9ca | ||
|
|
723621bd2b | ||
|
|
7f3aff9e10 | ||
|
|
5f5bbf9ae2 | ||
|
|
f73f5f5582 | ||
|
|
a7534597dd | ||
|
|
56866448dc | ||
|
|
f8979c0614 | ||
|
|
7e5f6b5802 | ||
|
|
addf9e8dbe | ||
|
|
870800b81a | ||
|
|
a3e127d1d6 | ||
|
|
875d2e4664 | ||
|
|
dd9568b648 | ||
|
|
e7d545fb0a | ||
|
|
160747cf5e | ||
|
|
22b2568f22 | ||
|
|
7f2fe2e5ff | ||
|
|
d364b9b9f8 | ||
|
|
9904af5d96 | ||
|
|
e4497b4f45 | ||
|
|
fb841b6436 | ||
|
|
391370de20 | ||
|
|
a40d363936 | ||
|
|
8cb8f589ee | ||
|
|
b0be0adcef | ||
|
|
1822b2f79a | ||
|
|
444ef199a6 | ||
|
|
5489b06628 | ||
|
|
6ac750955f | ||
|
|
ec2b2c6e1e | ||
|
|
f927563049 | ||
|
|
69430ce042 | ||
|
|
c70832a823 | ||
|
|
0403b97a87 | ||
|
|
ff1a45549e | ||
|
|
857a16d838 | ||
|
|
ef2c9801fb | ||
|
|
b5efc0ef3e | ||
|
|
1b28f47c1d | ||
|
|
533b9f2d45 | ||
|
|
49db662b82 | ||
|
|
cb15297853 | ||
|
|
2cfa89c719 | ||
|
|
ed69bb8757 | ||
|
|
b07e857aef | ||
|
|
08ca28773c | ||
|
|
d6ef2a122f | ||
|
|
2b1f347f98 | ||
|
|
148ba49530 | ||
|
|
4757e891a2 | ||
|
|
4ef68512a9 | ||
|
|
0c95dc2118 | ||
|
|
2da56bd776 | ||
|
|
d4a4233e96 | ||
|
|
b337213fb2 | ||
|
|
214dac0c45 | ||
|
|
61383f1014 | ||
|
|
89a94e9ac8 | ||
|
|
767a6f99a5 | ||
|
|
b0728647c9 | ||
|
|
860cacb70a | ||
|
|
b1c8a836e3 | ||
|
|
180648072c | ||
|
|
eb600a9447 | ||
|
|
1f785e897e | ||
|
|
314a24689c | ||
|
|
12ab2c3b51 | ||
|
|
ef56d8654e | ||
|
|
9db810d67a | ||
|
|
ae1b121cdd | ||
|
|
da9ceb131b | ||
|
|
cd155d7d17 | ||
|
|
41bd293f33 | ||
|
|
ca5dcbbf5c | ||
|
|
cae4f21897 | ||
|
|
62713c1270 | ||
|
|
b7e153fd82 | ||
|
|
9fd8287c7b | ||
|
|
1e71b1e544 | ||
|
|
74cb1f48ad | ||
|
|
e71627f893 | ||
|
|
264dcd0641 | ||
|
|
830dd49fa8 | ||
|
|
1447493081 | ||
|
|
2d6ba8c056 | ||
|
|
4fd1abfe52 | ||
|
|
89f75e7d1b | ||
|
|
29f139225b | ||
|
|
90470065ba | ||
|
|
41ca01a1b7 | ||
|
|
cd5fa5976a | ||
|
|
b59ecc9270 | ||
|
|
cc2b7b6fda | ||
|
|
58ed8e751d | ||
|
|
1893104392 | ||
|
|
213618adcc | ||
|
|
7e656257d1 | ||
|
|
c849f91590 | ||
|
|
03493d491b | ||
|
|
7502bf3ad9 | ||
|
|
e735f77d43 | ||
|
|
3b5e1fa764 | ||
|
|
8eae1eec4a | ||
|
|
35e48838c3 | ||
|
|
e257188017 | ||
|
|
a888573fcc | ||
|
|
0719c808dd | ||
|
|
8344699a0d | ||
|
|
d72a3aaf26 | ||
|
|
6eea5f3b13 | ||
|
|
4f40e43b26 | ||
|
|
d85ce8415b | ||
|
|
e1351eaa0a | ||
|
|
ce442a5083 | ||
|
|
d4fbc815c6 | ||
|
|
2341019016 | ||
|
|
b62db3733d | ||
|
|
9298133614 | ||
|
|
b3dc3ed5c8 | ||
|
|
cadce23b47 | ||
|
|
8c6e24346a | ||
|
|
e152a52625 | ||
|
|
d13908881c | ||
|
|
c7300f452c | ||
|
|
3bb4d6d370 | ||
|
|
d111d8d53c | ||
|
|
e69fdf5b83 | ||
|
|
6111ac3468 | ||
|
|
53a8d37df7 | ||
|
|
ae33b760f8 | ||
|
|
de58e3e98e | ||
|
|
3e318b4187 | ||
|
|
b7c4a3cd18 | ||
|
|
4558eb04d0 | ||
|
|
e431e5b71f | ||
|
|
72929079c9 | ||
|
|
59355b17a5 | ||
|
|
89ef3737b1 | ||
|
|
38c81516ec | ||
|
|
dbe4ca3efe | ||
|
|
4102e33670 | ||
|
|
d906010508 | ||
|
|
44413868c7 | ||
|
|
b23e69c0b7 | ||
|
|
53799e9f91 | ||
|
|
57221bd760 | ||
|
|
8f25de054d | ||
|
|
e99b09d38a | ||
|
|
fcbd1e14d3 | ||
|
|
58fa379cc7 | ||
|
|
8d5f66b5fc | ||
|
|
7c2cf49d66 | ||
|
|
d946972ac8 | ||
|
|
c28190c73b | ||
|
|
a309bbbbf0 | ||
|
|
fc7b2c1d59 | ||
|
|
6495ab7066 | ||
|
|
9f94d045ae | ||
|
|
42b856dbb9 | ||
|
|
f695451da0 | ||
|
|
362182bd88 | ||
|
|
e34702ab5b | ||
|
|
c841954c51 | ||
|
|
b3aecc7ac5 | ||
|
|
0faa1f4de2 | ||
|
|
f6f5e312f3 | ||
|
|
8fca329810 | ||
|
|
9b43b0aa1a | ||
|
|
6de26dc8ad | ||
|
|
3da3d28064 | ||
|
|
8bba79222f | ||
|
|
ab8f319750 | ||
|
|
504f0d0024 | ||
|
|
18d94a22a3 | ||
|
|
b119ac87f9 | ||
|
|
b1506eca95 | ||
|
|
1ae89d67ad | ||
|
|
ed6f03ea3e | ||
|
|
b5cfc2fa05 | ||
|
|
8f1460464e | ||
|
|
6cbe5172d6 | ||
|
|
b02bbd5e95 | ||
|
|
f9a2818d95 | ||
|
|
08cb77c415 | ||
|
|
2859a5a16d | ||
|
|
a6fc5a901a | ||
|
|
6d566068df | ||
|
|
033292f2c2 | ||
|
|
68c24831df | ||
|
|
599ff50fd4 | ||
|
|
a258ff74e9 | ||
|
|
8a825a4687 | ||
|
|
192c35b2c7 | ||
|
|
39206a96fb | ||
|
|
0d5deef407 | ||
|
|
c2a597747d | ||
|
|
ba80ce46e4 | ||
|
|
13aa7f9924 | ||
|
|
d5b05d6930 | ||
|
|
937fe6fda6 | ||
|
|
f550d04c7d | ||
|
|
01eb7e539a | ||
|
|
b4425f6eea | ||
|
|
41e251b0d7 | ||
|
|
dd2ead1022 | ||
|
|
5565dffbd1 | ||
|
|
87b4749d60 | ||
|
|
bb27135477 | ||
|
|
c6d52147e8 | ||
|
|
8ac3480942 | ||
|
|
97975098b9 | ||
|
|
c6836d777e | ||
|
|
dd8be9fc87 | ||
|
|
23e177514f | ||
|
|
b8c6baa94a | ||
|
|
6744c64e62 | ||
|
|
ceb8e8a109 | ||
|
|
0aaf144b76 | ||
|
|
01761ab99b | ||
|
|
04e0c9bc39 | ||
|
|
a331308a02 | ||
|
|
a283fc62fa | ||
|
|
0f5e4077ba | ||
|
|
2988c85479 | ||
|
|
a41833c466 | ||
|
|
c8bd66e881 | ||
|
|
744feb04e2 | ||
|
|
eea15f0952 | ||
|
|
18a5dda6a9 | ||
|
|
31791aadc8 | ||
|
|
8e8923c251 | ||
|
|
f7e2db9687 | ||
|
|
0116d9bd0b | ||
|
|
8ffd64b554 | ||
|
|
7dffad8e13 | ||
|
|
30fc8e96be | ||
|
|
cbccf61c7f | ||
|
|
710545b829 | ||
|
|
155f66b16f | ||
|
|
464ddb4206 | ||
|
|
c8409575bb | ||
|
|
2c2ba34537 | ||
|
|
76862ed10b | ||
|
|
948f8732d6 | ||
|
|
6c7bca2a17 | ||
|
|
9fa2057cd4 | ||
|
|
b2a5368846 | ||
|
|
280a97cbaa | ||
|
|
db6acec141 | ||
|
|
452af8ffa1 | ||
|
|
6ea7d92c2e | ||
|
|
6a4dabc684 | ||
|
|
b04927f688 | ||
|
|
35bfd27467 | ||
|
|
c0b14c7c39 | ||
|
|
52fdf7086c | ||
|
|
6806a13f68 | ||
|
|
42dd0fa912 | ||
|
|
58193ef18c | ||
|
|
a0448eb14a | ||
|
|
5c4f2c129f | ||
|
|
7be54096b6 | ||
|
|
b8464ddd7e | ||
|
|
0cb9ccd066 | ||
|
|
2cb208e97c | ||
|
|
91719b0f49 | ||
|
|
c2148ad757 | ||
|
|
dde605d375 | ||
|
|
7ebc538c10 | ||
|
|
738e0c64ec | ||
|
|
28c260e4ea | ||
|
|
75406d177b | ||
|
|
3c605dbff5 | ||
|
|
6521576f42 | ||
|
|
c54373522e | ||
|
|
11bae66d31 | ||
|
|
9115aa385f | ||
|
|
e234af10a2 | ||
|
|
dd83fa5286 | ||
|
|
bc951d7c24 | ||
|
|
39ecf64086 | ||
|
|
5dd6d0a588 | ||
|
|
38726b19ad | ||
|
|
97629cbae2 | ||
|
|
9786a1fa2c | ||
|
|
f1c2739118 | ||
|
|
b8570fcd9d | ||
|
|
6bd30047c1 | ||
|
|
3c60fc0857 | ||
|
|
3bf179a888 | ||
|
|
8dcd8529a2 | ||
|
|
dea815dac2 | ||
|
|
47483f1c66 | ||
|
|
f76e52e60a | ||
|
|
29f9fe5c1a | ||
|
|
96435425e9 | ||
|
|
96a24e035c | ||
|
|
1ba6467c6b | ||
|
|
9b4fce1468 | ||
|
|
ddf551f16d | ||
|
|
5d20d088e9 | ||
|
|
fcbe6844c0 | ||
|
|
bd43defefd | ||
|
|
9b98352caf | ||
|
|
143fc4ced4 | ||
|
|
51f0082c53 | ||
|
|
ee01956978 | ||
|
|
b26be81c7a | ||
|
|
4d0136af8c | ||
|
|
caabfb7eb4 | ||
|
|
43b774d1e6 | ||
|
|
1ce7559110 | ||
|
|
5e1664dc82 | ||
|
|
15d920456a | ||
|
|
cd70936a77 | ||
|
|
fdbeb1e702 | ||
|
|
6cf1b086a9 | ||
|
|
12f3cd5bb1 | ||
|
|
63f01c1ebd | ||
|
|
5b00c4fc93 | ||
|
|
a6345797ae | ||
|
|
f7129c1d62 | ||
|
|
0d1c5f85fc | ||
|
|
2f4b515afc | ||
|
|
fb765fdb1d | ||
|
|
a82192efb7 | ||
|
|
508b0a4426 | ||
|
|
c2851c2342 | ||
|
|
fc3b8bff69 | ||
|
|
31b27fdd3d | ||
|
|
4c10599ae0 | ||
|
|
8c1f9a8e5a | ||
|
|
c96fe0ec58 | ||
|
|
e808e913ec | ||
|
|
a4e815c05c | ||
|
|
334f005ba1 | ||
|
|
11f1735e85 | ||
|
|
b90b66ea80 | ||
|
|
e207424da8 | ||
|
|
7149b12cd9 | ||
|
|
946b4f0184 | ||
|
|
c15a38407f | ||
|
|
f9b1250ecf | ||
|
|
1fa269425e | ||
|
|
2fac0af553 | ||
|
|
b765c44539 | ||
|
|
0e1aaad6d4 | ||
|
|
bf839461b4 | ||
|
|
76ba68091f | ||
|
|
35d1bff321 | ||
|
|
318efa4c33 | ||
|
|
208628d7f2 | ||
|
|
28ab3d5ccb | ||
|
|
a53b0a483c | ||
|
|
24e1a0ec7d | ||
|
|
3c4ba857cc | ||
|
|
4cf9d7c982 | ||
|
|
a74793ecbc | ||
|
|
16f428f580 | ||
|
|
a49671d076 | ||
|
|
261461deeb | ||
|
|
7ab6eb1140 | ||
|
|
70bad093a9 | ||
|
|
4e402082b3 | ||
|
|
02be618935 | ||
|
|
239fda1894 | ||
|
|
32b117b9df | ||
|
|
43660c2c31 | ||
|
|
aca89c38aa | ||
|
|
44dcfec947 | ||
|
|
cf88d105f3 | ||
|
|
91448274fa | ||
|
|
2509000878 | ||
|
|
7401de3348 | ||
|
|
020d79f18e | ||
|
|
3a4ab10806 | ||
|
|
6c8932a4e1 | ||
|
|
a7815b29f5 | ||
|
|
b129e10da3 | ||
|
|
d5be710b95 | ||
|
|
350f4293c7 | ||
|
|
c95205a94f | ||
|
|
2ded805858 | ||
|
|
0b8ac4870d | ||
|
|
3d167b2687 | ||
|
|
86fb0d7b51 | ||
|
|
2115e338ca | ||
|
|
eba78e6786 | ||
|
|
39cc227d0d | ||
|
|
36b7dc6c17 | ||
|
|
326c4f2c55 | ||
|
|
a1cd4eafbd | ||
|
|
c5f48e6079 | ||
|
|
ac7a391732 | ||
|
|
5c3b1724ab | ||
|
|
c1a9d15a82 | ||
|
|
238ef0d2eb | ||
|
|
26b23e2af0 | ||
|
|
444bbfa9f7 | ||
|
|
d65789950c | ||
|
|
c855d83a4b | ||
|
|
90231dd41c | ||
|
|
1b4bbca8ad | ||
|
|
21703127a4 | ||
|
|
4c434438e3 | ||
|
|
3f80670831 | ||
|
|
1c4bd230e1 | ||
|
|
f52aff64f7 | ||
|
|
3f25db755c | ||
|
|
8d7079d997 | ||
|
|
9c2807eba4 | ||
|
|
96fe51e2e5 | ||
|
|
c8a3fbe265 | ||
|
|
64f20b6b5b | ||
|
|
77cd2bf5e4 | ||
|
|
d65833fca4 | ||
|
|
b4e7131bdb | ||
|
|
f674960e4e | ||
|
|
34cd2f5d7c | ||
|
|
4c6f9eca5a | ||
|
|
69b7d9666d | ||
|
|
77dbf5d22e | ||
|
|
0a6f48ae0b | ||
|
|
cd045c0c65 | ||
|
|
700c66ea86 | ||
|
|
004e2a1066 | ||
|
|
110d29438a | ||
|
|
8c6bc4d9b2 | ||
|
|
1b95bfe171 | ||
|
|
2fe1da8109 | ||
|
|
a18a559a4b | ||
|
|
27b60143cb | ||
|
|
cfe30ed674 | ||
|
|
2ec4e5bdbd | ||
|
|
714210492e | ||
|
|
4d0cff73ea | ||
|
|
102d4f8d48 | ||
|
|
9b40f5dbfd | ||
|
|
55b7bc6a42 | ||
|
|
dce4f282ac | ||
|
|
48c167b860 | ||
|
|
f10eff4495 | ||
|
|
33ecc9d177 | ||
|
|
3897f595a5 | ||
|
|
80a7ff8963 | ||
|
|
7a859109d2 | ||
|
|
b344d9fd69 | ||
|
|
dd7566dab5 | ||
|
|
d71639220c | ||
|
|
a5d3f08cca | ||
|
|
276ce611e6 | ||
|
|
09386f1433 | ||
|
|
08a957ff45 | ||
|
|
aff2ed3a6a | ||
|
|
d65d3dd0cd | ||
|
|
a502c386cd | ||
|
|
fd1a95bf32 | ||
|
|
e5e20a71be | ||
|
|
29ac0e171c | ||
|
|
7324256f42 | ||
|
|
cabeef2128 | ||
|
|
b9ae910d39 | ||
|
|
b4177b5b9d | ||
|
|
5e3c70600b | ||
|
|
63012b84da | ||
|
|
92c3769c2d | ||
|
|
1a72fa7be6 | ||
|
|
e682abcf8f | ||
|
|
9ac8bd4097 | ||
|
|
037636a702 | ||
|
|
92a657e7f0 | ||
|
|
43b08b75e7 | ||
|
|
414edeb5bd | ||
|
|
0d8bae8004 | ||
|
|
259f104a48 | ||
|
|
1294c3d03f | ||
|
|
197e3b7c26 | ||
|
|
bf7c417216 | ||
|
|
5003af9c59 | ||
|
|
10d4953b31 | ||
|
|
51fd540c36 | ||
|
|
973265483d | ||
|
|
2e2357449f | ||
|
|
7bf83b13a8 | ||
|
|
62a52cf56c | ||
|
|
090f30966e | ||
|
|
13f82ba388 | ||
|
|
98c69a3988 | ||
|
|
bc18282e1f | ||
|
|
f44464e592 | ||
|
|
21668be1f6 | ||
|
|
637dd0f902 | ||
|
|
a210475d11 | ||
|
|
a3951d4774 | ||
|
|
41db3ea19b | ||
|
|
436bf51411 | ||
|
|
dc87d6016f | ||
|
|
6a00952dcc | ||
|
|
31b7baa4f3 | ||
|
|
05f05e6e79 | ||
|
|
44ad0165e5 | ||
|
|
1bee0bd1fc | ||
|
|
9c4209bb4b | ||
|
|
3ada219762 | ||
|
|
a91770553f | ||
|
|
72754ba6b1 | ||
|
|
f54dfe30aa | ||
|
|
ed1fdd264c | ||
|
|
7357df9202 | ||
|
|
f9ef5c6ffe | ||
|
|
6201b2d7f0 | ||
|
|
ab14e29757 | ||
|
|
b6ea634ba9 | ||
|
|
2fb304b6f4 | ||
|
|
e0d8f461a1 | ||
|
|
9ae013ca46 | ||
|
|
170c9fd8c3 | ||
|
|
deeaa0c9c7 | ||
|
|
345e66153b | ||
|
|
8ad95972af | ||
|
|
f96866915b | ||
|
|
d9d0e470ad | ||
|
|
5c93c2c677 | ||
|
|
91329e7970 | ||
|
|
f2c767e79a | ||
|
|
c09f1875aa | ||
|
|
2a6ac8a117 | ||
|
|
a5755d4568 | ||
|
|
626a8b3504 | ||
|
|
65a2f3ac81 | ||
|
|
e84fa208ff | ||
|
|
45903c2f5e | ||
|
|
02879f08c0 | ||
|
|
120dc4ae77 | ||
|
|
955ef2e10e | ||
|
|
7b46d1dc1d | ||
|
|
658515c9ce | ||
|
|
c8935e61bc | ||
|
|
c4dde72aa3 | ||
|
|
d6fd8d1ba1 | ||
|
|
8a17009afa | ||
|
|
95a6425189 | ||
|
|
c327a35e4e | ||
|
|
510ca08e64 | ||
|
|
7881f0a510 | ||
|
|
2c4cf9379f | ||
|
|
2275d75f07 | ||
|
|
ac7d8e5d13 | ||
|
|
6e59bd811a | ||
|
|
74a8cb09e5 | ||
|
|
62dcb3ca34 | ||
|
|
c9461d3925 | ||
|
|
31ba12d553 | ||
|
|
ec8506e923 | ||
|
|
b4df816530 | ||
|
|
350ddfb1c7 | ||
|
|
1ad08d1b44 | ||
|
|
eb95fc9579 | ||
|
|
07a0a22531 | ||
|
|
23f7923d2b | ||
|
|
cacf83e0ec | ||
|
|
d32427eb06 | ||
|
|
bb5199936c | ||
|
|
a70fe7e41e | ||
|
|
01be76c631 | ||
|
|
b9d15055a6 | ||
|
|
94204f529a | ||
|
|
0412784cb4 | ||
|
|
f0b762cbf8 | ||
|
|
bbb1781e1d | ||
|
|
179d1b8db5 | ||
|
|
45c499a022 | ||
|
|
83defb449a | ||
|
|
b08d1b2a4f | ||
|
|
cc8ccabec4 | ||
|
|
5e8a073203 | ||
|
|
5a038a9de1 | ||
|
|
d7dcbbf9d8 | ||
|
|
db32955f54 | ||
|
|
7778d987f4 | ||
|
|
a936eeee9a | ||
|
|
bedebc5caa | ||
|
|
907e1084fc | ||
|
|
3968f2a50f | ||
|
|
45751da711 | ||
|
|
c8395475c8 | ||
|
|
78d7545e4d | ||
|
|
a226894cda | ||
|
|
3b66147164 | ||
|
|
ffb7f1d017 | ||
|
|
e0221e593f | ||
|
|
fff6f6b07c | ||
|
|
ea4be6069b | ||
|
|
adea487638 | ||
|
|
7ec5d09ef1 | ||
|
|
37c68c1f19 | ||
|
|
d242b86232 | ||
|
|
550f8f0d5a | ||
|
|
d7d84a1f7e | ||
|
|
888fbc6be3 | ||
|
|
5d2b686f0b | ||
|
|
23b6460201 | ||
|
|
c815fb8de7 | ||
|
|
113de58d77 | ||
|
|
e5870319be | ||
|
|
270971c475 | ||
|
|
5c8e8ae7be | ||
|
|
c25641b624 | ||
|
|
d4cf0f1bdc | ||
|
|
43e6a20d37 | ||
|
|
60017f87f4 | ||
|
|
f14a02c356 | ||
|
|
fc2c90275c | ||
|
|
064e0c6273 | ||
|
|
0a1289d606 | ||
|
|
0d98121f12 | ||
|
|
b8e5b9b5e5 | ||
|
|
4d2c9928dd | ||
|
|
d1b33e0fbf | ||
|
|
d4cdc01073 | ||
|
|
d707b239f7 | ||
|
|
3171d17edf | ||
|
|
e28de57075 | ||
|
|
7279732c92 | ||
|
|
f273d11a3b | ||
|
|
d88e58c34f | ||
|
|
492ed23d70 | ||
|
|
4a0ae8cd61 | ||
|
|
51a2fb5c33 | ||
|
|
de09d56369 | ||
|
|
8f4dd85773 | ||
|
|
212832e8de | ||
|
|
e07c6c2681 | ||
|
|
3e889a5069 | ||
|
|
e1fb7dd4ca | ||
|
|
2c5ac5554e | ||
|
|
e45a2b9f12 | ||
|
|
d1ea22dde3 | ||
|
|
90a0df6d90 | ||
|
|
e59c410282 | ||
|
|
ebe0fb155d | ||
|
|
aaf2a0238e | ||
|
|
acf35c4e6c | ||
|
|
6f07a7e9e9 | ||
|
|
80cd2b1b7e | ||
|
|
d588c04997 | ||
|
|
1502ce73ea | ||
|
|
71d754890e | ||
|
|
231c4b86a1 | ||
|
|
4383f20477 | ||
|
|
49465b8e32 | ||
|
|
b393e37e19 | ||
|
|
8cbb3ab598 | ||
|
|
f53522001b | ||
|
|
d532c60d99 | ||
|
|
c7387534ea | ||
|
|
3b05e5607f | ||
|
|
ddc54387ba | ||
|
|
bd32b648f4 | ||
|
|
12afc2f447 | ||
|
|
47ba871d82 | ||
|
|
3f3bc404c2 | ||
|
|
ffd9226c82 | ||
|
|
bdad44761e | ||
|
|
8dd4571442 | ||
|
|
ca4d531435 | ||
|
|
acd72a7fe5 | ||
|
|
b0473a9034 | ||
|
|
713b79680f | ||
|
|
48e3fa8eed | ||
|
|
6a5c491ee6 | ||
|
|
722219aaf8 | ||
|
|
1e511be523 | ||
|
|
8c0d61c7e2 | ||
|
|
5446478ec0 | ||
|
|
d4b621b0be | ||
|
|
11c57040aa | ||
|
|
4754e87ce6 | ||
|
|
2295ae34ee | ||
|
|
25081770b6 | ||
|
|
824c2a70fc | ||
|
|
9448ca93ea | ||
|
|
09b1ae99e6 | ||
|
|
8f12fe3daa | ||
|
|
4be12bcd2c | ||
|
|
ed3defc412 | ||
|
|
4b9a23b251 | ||
|
|
5dbff49d96 | ||
|
|
de4332a6a9 | ||
|
|
324c578f77 | ||
|
|
9b4ab45c5a | ||
|
|
3e301785a4 | ||
|
|
4f85ff8b69 | ||
|
|
99b1347789 | ||
|
|
a619a9bb04 | ||
|
|
a7676e83f4 | ||
|
|
c6439b907b | ||
|
|
07a9130734 | ||
|
|
543eff9d24 | ||
|
|
8a7afd17fb | ||
|
|
a95acb393b | ||
|
|
ab79c7bcaa | ||
|
|
d6c1b7129d | ||
|
|
2d2bb75d85 | ||
|
|
592c5363c4 | ||
|
|
695baa1b45 | ||
|
|
5dca6dce00 | ||
|
|
5915f546a3 | ||
|
|
7d5841e95e | ||
|
|
6476f1601c | ||
|
|
1d16c44db9 | ||
|
|
0a9153593c | ||
|
|
8c284fd150 | ||
|
|
785b4118f8 | ||
|
|
f206992cfc | ||
|
|
b59470e351 | ||
|
|
50bddcab38 | ||
|
|
981464846a | ||
|
|
ea595cbee1 | ||
|
|
ade8514afe | ||
|
|
384962c272 | ||
|
|
6b36326259 | ||
|
|
1052aabfeb | ||
|
|
b597690741 | ||
|
|
52f313593e | ||
|
|
cc7d5e57eb | ||
|
|
c538d3d307 | ||
|
|
cd2012719b | ||
|
|
a34ac85c50 | ||
|
|
0a2e56245f | ||
|
|
248c6fc723 | ||
|
|
d868ca3e34 | ||
|
|
286e3742f6 | ||
|
|
6602efde63 | ||
|
|
9a54f273c2 | ||
|
|
5b331cfe27 | ||
|
|
5176a2c8d0 | ||
|
|
e3e21745d3 | ||
|
|
a6e931ed5b | ||
|
|
d3714376d2 | ||
|
|
c505c5f946 | ||
|
|
e5f62a0349 | ||
|
|
5f5f8573e1 | ||
|
|
3e99fea7bc | ||
|
|
777d2f2f22 | ||
|
|
170684a170 | ||
|
|
beb1f06d9f | ||
|
|
bc1d70a184 | ||
|
|
5a17ec3b65 | ||
|
|
880d977346 | ||
|
|
5b69e2c626 | ||
|
|
e6df1522a7 | ||
|
|
0ae6c5dcf6 | ||
|
|
0db1b7ec11 | ||
|
|
ea1fec84be | ||
|
|
837569875e | ||
|
|
1fe306e21c | ||
|
|
360638879b | ||
|
|
3000a691e5 | ||
|
|
16526e40a8 | ||
|
|
4275e74cd3 | ||
|
|
fd92c738e9 | ||
|
|
c2fa9ff229 | ||
|
|
8c92e029eb | ||
|
|
137fe297a6 | ||
|
|
e07ab7aea6 | ||
|
|
4f1905cea1 | ||
|
|
bbc16ae30f | ||
|
|
c808d27fef | ||
|
|
c48db1ebeb | ||
|
|
9b51237607 | ||
|
|
bad0db11b8 | ||
|
|
3d0bd75947 | ||
|
|
92cda1a358 | ||
|
|
eb5f6d5d92 | ||
|
|
7845fe0bba | ||
|
|
e39fbcef11 | ||
|
|
fc7b5f8b5f | ||
|
|
a4e87fd2de | ||
|
|
c8ba5e2ca3 | ||
|
|
dc0b0e9b38 | ||
|
|
94def4508e | ||
|
|
9f53786bdb | ||
|
|
ba46b98c3c | ||
|
|
9d399d412a | ||
|
|
3e3de70a7a | ||
|
|
e8002dd536 | ||
|
|
c8173b60b3 | ||
|
|
f06aecc5da | ||
|
|
a5471576ea | ||
|
|
6678f25ca0 | ||
|
|
1b9a126809 | ||
|
|
ad8fc3ceb7 | ||
|
|
2bdb8e3b62 | ||
|
|
acbc11fd40 | ||
|
|
b227d72067 | ||
|
|
9c0d2cf208 | ||
|
|
36d98a9b3d | ||
|
|
24a033900a | ||
|
|
9c81108de2 | ||
|
|
361b8a9691 | ||
|
|
a9bb51a788 | ||
|
|
3fb9cbc032 | ||
|
|
e179d3f0e4 | ||
|
|
f02f230ba9 | ||
|
|
508a3714aa | ||
|
|
2a4b3fe354 | ||
|
|
4062003c91 | ||
|
|
f99cf4f7cb | ||
|
|
f12f40dfcb | ||
|
|
e9e580ee1a | ||
|
|
43507e969c | ||
|
|
b767e1c7ae | ||
|
|
e2ddfde44e | ||
|
|
a08ce6fdb7 | ||
|
|
177d04ecaa | ||
|
|
bfda9ea312 | ||
|
|
94693c31b7 | ||
|
|
b9f43ac43e | ||
|
|
67907c415e | ||
|
|
820c830ae1 | ||
|
|
83b7c64b76 | ||
|
|
25c56c4974 | ||
|
|
22937c97f1 | ||
|
|
9f60321b89 | ||
|
|
787ab54072 | ||
|
|
9a8f64c512 | ||
|
|
2fa461271b | ||
|
|
e32d15659b | ||
|
|
4f567577db | ||
|
|
186b6cce2b | ||
|
|
ab8d342cad | ||
|
|
6e519ca910 | ||
|
|
4569ed424d | ||
|
|
c97040b77e | ||
|
|
53899b3fd4 | ||
|
|
50b7748f2c | ||
|
|
6d1fa7d84a | ||
|
|
fca4058f2e | ||
|
|
13bc2eee66 | ||
|
|
cda414c84d | ||
|
|
a8bb570a19 | ||
|
|
fb8a841d3e | ||
|
|
29a6b101fd | ||
|
|
0342c8b33c | ||
|
|
2f68e95013 | ||
|
|
0e3d06a562 | ||
|
|
072bde093c | ||
|
|
16d59544ea | ||
|
|
4558381ffa | ||
|
|
1fc413e2f6 | ||
|
|
c6fe623061 | ||
|
|
655778d80d | ||
|
|
ddbd0b672a | ||
|
|
86d3dec7b9 | ||
|
|
ef70a92e49 | ||
|
|
fed9c1cd8d | ||
|
|
eac3f8ea1f | ||
|
|
6057200c72 | ||
|
|
683f6f3b2e | ||
|
|
dec49aa15b | ||
|
|
824a998274 | ||
|
|
570bc0824f | ||
|
|
6baa0ee593 | ||
|
|
025f953f6f | ||
|
|
f0714bf36e | ||
|
|
ae1b824e4b | ||
|
|
c49af2288c | ||
|
|
596fd90ef3 | ||
|
|
db577b23b4 | ||
|
|
2b49c4d344 | ||
|
|
17b0461583 | ||
|
|
7d676848cb | ||
|
|
a1c04622d3 | ||
|
|
8187303ada | ||
|
|
78d5af59ee | ||
|
|
3b3c52f1e7 | ||
|
|
b4ac312d2e | ||
|
|
7e82d9a451 | ||
|
|
e0089475f7 | ||
|
|
ae99043041 | ||
|
|
c7f4e0c306 | ||
|
|
5b63dc1f67 | ||
|
|
d233f61383 | ||
|
|
51aaf2bb13 | ||
|
|
923134e52c | ||
|
|
a38c82d8c7 | ||
|
|
3ae60cd63f | ||
|
|
953e3cc50d | ||
|
|
7bfec3f8c2 | ||
|
|
5fd74659bc | ||
|
|
f1786f9454 | ||
|
|
5cddeb7449 | ||
|
|
1f012c461e | ||
|
|
64660a2574 | ||
|
|
5643fe8dbe | ||
|
|
1ed3c340e8 | ||
|
|
877fd96e8c | ||
|
|
d6c697d509 | ||
|
|
d51f61af25 | ||
|
|
2f4de5dc5f | ||
|
|
be54ac78a6 | ||
|
|
3a4b44a480 | ||
|
|
59764d7b04 | ||
|
|
d29528bb69 | ||
|
|
87a3aad001 | ||
|
|
6236c5b620 | ||
|
|
5fe7d19e6a | ||
|
|
48caa962cb | ||
|
|
87dfb04f1e | ||
|
|
7027e940ef | ||
|
|
34c2d77ec5 | ||
|
|
cdea598a30 | ||
|
|
00dc248baa | ||
|
|
547181d36f | ||
|
|
7627c619a1 | ||
|
|
de8dbe2c74 | ||
|
|
4ca83c3bc1 | ||
|
|
cb6e677400 | ||
|
|
ea2b0150c6 | ||
|
|
1e2190e86d | ||
|
|
b57ea5c1cd | ||
|
|
6bca1fbc30 | ||
|
|
71ab07763d | ||
|
|
b38d8d84d9 | ||
|
|
4d0a8ed435 | ||
|
|
083a62a3b2 | ||
|
|
423082a4a4 | ||
|
|
673cbc6d91 | ||
|
|
e35c97c214 | ||
|
|
6dde42eee6 | ||
|
|
3a1e34b100 | ||
|
|
7b98483ebe | ||
|
|
d75756b1d1 | ||
|
|
1378b83f7a | ||
|
|
1c5ff35260 | ||
|
|
3de4af2348 | ||
|
|
b67c840b4c | ||
|
|
d61ec5c2f7 | ||
|
|
a3b83494ee | ||
|
|
ed9e64e810 | ||
|
|
d73673492f | ||
|
|
bb5edcd3c6 | ||
|
|
de4509adc1 | ||
|
|
79c8ecc9d7 | ||
|
|
964f2e7781 | ||
|
|
ad052ad7f0 | ||
|
|
1d3985d3af | ||
|
|
e06f817b47 | ||
|
|
732eabc8df | ||
|
|
bb1f738d1d | ||
|
|
2cc8b16339 | ||
|
|
d6d6f5b1a3 | ||
|
|
905e1cb009 | ||
|
|
aeaf5c0ed2 | ||
|
|
775b6e5f68 | ||
|
|
bbe090a538 | ||
|
|
e6af248ed8 | ||
|
|
f04074835d | ||
|
|
275055d4a0 | ||
|
|
c8ce80a3f4 | ||
|
|
505133310d | ||
|
|
4db594425b | ||
|
|
631e6b99f9 | ||
|
|
816c3dd2a8 | ||
|
|
8259dac5fb | ||
|
|
d67d02bde5 | ||
|
|
14653865b2 | ||
|
|
ab6ad25a92 | ||
|
|
f92ec41e74 | ||
|
|
a4168b2e07 | ||
|
|
f15f0f8126 | ||
|
|
3ad5a90800 | ||
|
|
0fb4e7c60c | ||
|
|
9048a92016 | ||
|
|
b23dce1bc8 | ||
|
|
1108c63218 | ||
|
|
cfb2343121 | ||
|
|
dbddc0e841 | ||
|
|
22f35f40be | ||
|
|
6c1abd4de9 | ||
|
|
be692522d7 | ||
|
|
6c661b65b6 | ||
|
|
49ac6a15be | ||
|
|
236c839ee7 | ||
|
|
dfc8b00332 | ||
|
|
f40c6ec9dd | ||
|
|
29b40b7c2e | ||
|
|
38376fce72 | ||
|
|
6ab9e9a778 | ||
|
|
7b6c31d223 | ||
|
|
a185998bcf | ||
|
|
0795375787 | ||
|
|
72f916e5d7 | ||
|
|
59b57ebdc0 | ||
|
|
4a4374005b | ||
|
|
160aaf40d0 | ||
|
|
df5e4eed8d | ||
|
|
67fabc30bb | ||
|
|
477983cf64 | ||
|
|
903a5ae4c5 | ||
|
|
6574ea15ef | ||
|
|
4a7514fba7 | ||
|
|
9c0e948662 | ||
|
|
ee7b9cefc1 | ||
|
|
30efca1a0e | ||
|
|
43f266655a | ||
|
|
0a7bedc21e | ||
|
|
adc4b8e425 | ||
|
|
da572ca2af | ||
|
|
109da44f48 | ||
|
|
8ebc7f4c06 | ||
|
|
0cf1a3f6b6 | ||
|
|
dcd6e32a05 | ||
|
|
98ec34c523 | ||
|
|
52bb3f3370 | ||
|
|
4f2221d56c | ||
|
|
df2c832aa8 | ||
|
|
bca3bb4d33 | ||
|
|
018b55881e | ||
|
|
564217213c | ||
|
|
745bf0a6c9 | ||
|
|
cb36cd9357 | ||
|
|
6cd4cc47c1 | ||
|
|
ac7bb1dec2 | ||
|
|
5cb7ca6bf4 | ||
|
|
544e606e2c | ||
|
|
2edda573a5 | ||
|
|
ae6e84ddfd | ||
|
|
d764cc8fbf | ||
|
|
c45b2387c2 | ||
|
|
e883304279 | ||
|
|
2c789d92d3 | ||
|
|
adbbde6f31 | ||
|
|
580d478cd3 | ||
|
|
e52982fd0b | ||
|
|
470537ec3a | ||
|
|
d63198a792 | ||
|
|
d914f55e2c | ||
|
|
35c23a9d85 | ||
|
|
0c13b29a0e | ||
|
|
3afa4178ba | ||
|
|
badbc5a491 | ||
|
|
1cbb574333 | ||
|
|
2de2f28d23 | ||
|
|
4a91472469 | ||
|
|
7daead6d85 | ||
|
|
5e83ed9d89 | ||
|
|
389b715f15 | ||
|
|
eb445d5789 | ||
|
|
412d7f5c38 | ||
|
|
4b7e8669ac | ||
|
|
935c2f2c10 | ||
|
|
3c0b438f16 | ||
|
|
b38c529ba8 | ||
|
|
3289b31b09 | ||
|
|
a80c69982d | ||
|
|
926892c906 | ||
|
|
d70f77efaf | ||
|
|
08da737316 | ||
|
|
913c787082 | ||
|
|
f8b85eaf12 | ||
|
|
4384978728 | ||
|
|
97e5763601 | ||
|
|
08f25bb5bf | ||
|
|
5cfd1603da | ||
|
|
b4c54919e9 | ||
|
|
62e49ec081 | ||
|
|
840abbbdd2 | ||
|
|
967b0662ce | ||
|
|
afb350e553 | ||
|
|
83994bef9c | ||
|
|
7324f343b9 | ||
|
|
7c275b2b25 | ||
|
|
f05de358c7 | ||
|
|
ab87dc5fe2 | ||
|
|
9f47bbc552 | ||
|
|
8aead58288 | ||
|
|
a7cf1b4ff5 | ||
|
|
e78d2a061b | ||
|
|
1fa21e6bcd | ||
|
|
d7f6a39704 | ||
|
|
2c8a18fe6f | ||
|
|
51aba35512 | ||
|
|
362d53e5bb | ||
|
|
15099045fc | ||
|
|
8db2465638 | ||
|
|
c30831dd0d | ||
|
|
e3da653f33 | ||
|
|
7628eb2b76 | ||
|
|
2f0c01c992 | ||
|
|
60d843f32d | ||
|
|
9373f92940 | ||
|
|
e5c39a6569 | ||
|
|
74eeff5370 | ||
|
|
28aece1220 | ||
|
|
46c5293ff4 | ||
|
|
22b8402322 | ||
|
|
ab2324c652 | ||
|
|
2908984b16 | ||
|
|
857d26eb71 | ||
|
|
e3e93ab0e6 | ||
|
|
6152625c53 | ||
|
|
f24db730e4 | ||
|
|
66210aa722 | ||
|
|
8fb33dc5bf | ||
|
|
fbcf388a56 | ||
|
|
b0c6c1c39c | ||
|
|
44495fdefb | ||
|
|
eea67b00cb | ||
|
|
b269185cf0 | ||
|
|
8437f127f0 | ||
|
|
f102a00a04 | ||
|
|
1d8da3123e | ||
|
|
4ac37c9f34 | ||
|
|
156f683e87 | ||
|
|
ba822bb7cc | ||
|
|
666da0d10c | ||
|
|
38445efce7 | ||
|
|
9da8ad6aa7 | ||
|
|
a1b6edef87 | ||
|
|
716c87fc4e | ||
|
|
f8b1c8162c | ||
|
|
1f477f443b | ||
|
|
25b3d78cb5 | ||
|
|
d0d542c477 | ||
|
|
84c25f83bd | ||
|
|
2fbbcd2e1d | ||
|
|
2e98e45a6e | ||
|
|
84200f9299 | ||
|
|
dc2ef53b82 | ||
|
|
4ba6cd647d | ||
|
|
9cd40c5950 | ||
|
|
0bc44ec5aa | ||
|
|
7ab4667114 | ||
|
|
dbb1f3b873 | ||
|
|
87851c2cdf | ||
|
|
da737764d0 | ||
|
|
8d54778c16 | ||
|
|
ab9ac096ea | ||
|
|
0bcc2bb52f | ||
|
|
afeb6eea65 | ||
|
|
594c832180 | ||
|
|
d7fd142d1f | ||
|
|
0df8cf866d | ||
|
|
1fab42a7e2 | ||
|
|
6a6ba4efd7 | ||
|
|
bb6e3d92e9 | ||
|
|
6893931fbf | ||
|
|
6530b499fc | ||
|
|
9b65a90f3f | ||
|
|
b3599b0354 | ||
|
|
f345297009 | ||
|
|
8ebf765924 | ||
|
|
b8bda0d934 | ||
|
|
f5aae3c06c | ||
|
|
dae5f614fc | ||
|
|
052358f9c8 | ||
|
|
08ad05bca0 | ||
|
|
a6b5798acd | ||
|
|
8aedb15382 | ||
|
|
127dabcc02 | ||
|
|
f0d13cbb8e | ||
|
|
0bff2ed554 | ||
|
|
2b1effdf99 |
107
.circleci/config.yml
Normal file
107
.circleci/config.yml
Normal file
@ -0,0 +1,107 @@
|
|||||||
|
version: 2
|
||||||
|
jobs:
|
||||||
|
# publish jobs require $DOCKERHUB_REPO, $DOCKERHUB_USER, $DOCKERHUB_PASS defined
|
||||||
|
amd64:
|
||||||
|
machine:
|
||||||
|
enabled: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
LATEST_TAG=${CIRCLE_TAG:8} #trim "basedon-" from tag
|
||||||
|
#
|
||||||
|
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-amd64 -f linuxamd64.Dockerfile .
|
||||||
|
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||||
|
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-amd64
|
||||||
|
|
||||||
|
arm32:
|
||||||
|
machine:
|
||||||
|
enabled: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
LATEST_TAG=${CIRCLE_TAG:8} #trim "basedon-" from tag
|
||||||
|
#
|
||||||
|
# Make sure the builder is copy the arm emulator
|
||||||
|
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y qemu qemu-user-static qemu-user binfmt-support
|
||||||
|
|
||||||
|
sudo cp /usr/bin/qemu-arm-static "qemu-arm-static"
|
||||||
|
sed -i -e 's/#EnableQEMU //g' "linuxarm32v7.Dockerfile"
|
||||||
|
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 -f linuxarm32v7.Dockerfile .
|
||||||
|
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||||
|
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm32v7
|
||||||
|
|
||||||
|
arm64:
|
||||||
|
machine:
|
||||||
|
enabled: true
|
||||||
|
steps:
|
||||||
|
- checkout
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
LATEST_TAG=${CIRCLE_TAG:8} #trim "basedon-" from tag
|
||||||
|
#
|
||||||
|
# Make sure the builder is copy the arm emulator
|
||||||
|
sudo docker run --rm --privileged multiarch/qemu-user-static:register --reset
|
||||||
|
sudo apt update
|
||||||
|
sudo apt install -y qemu qemu-user-static qemu-user binfmt-support
|
||||||
|
|
||||||
|
sudo cp /usr/bin/qemu-aarch64-static "qemu-aarch64-static"
|
||||||
|
sed -i -e 's/#EnableQEMU //g' "linuxarm64v8.Dockerfile"
|
||||||
|
sudo docker build --pull -t $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 -f linuxarm64v8.Dockerfile .
|
||||||
|
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||||
|
sudo docker push $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
|
||||||
|
|
||||||
|
multiarch:
|
||||||
|
machine:
|
||||||
|
enabled: true
|
||||||
|
image: default
|
||||||
|
steps:
|
||||||
|
- run:
|
||||||
|
command: |
|
||||||
|
#
|
||||||
|
sudo docker login --username=$DOCKERHUB_USER --password=$DOCKERHUB_PASS
|
||||||
|
#
|
||||||
|
LATEST_TAG=${CIRCLE_TAG:8} #trim "basedon-" from tag
|
||||||
|
sudo docker manifest create --amend $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 $DOCKERHUB_REPO:$LATEST_TAG-arm64v8
|
||||||
|
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-amd64 --os linux --arch amd64
|
||||||
|
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm32v7 --os linux --arch arm --variant v7
|
||||||
|
sudo docker manifest annotate $DOCKERHUB_REPO:$LATEST_TAG $DOCKERHUB_REPO:$LATEST_TAG-arm64v8 --os linux --arch arm64 --variant v8
|
||||||
|
sudo docker manifest push $DOCKERHUB_REPO:$LATEST_TAG -p
|
||||||
|
|
||||||
|
workflows:
|
||||||
|
version: 2
|
||||||
|
publish:
|
||||||
|
jobs:
|
||||||
|
- amd64:
|
||||||
|
filters:
|
||||||
|
# ignore any commit on any branch by default
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
# only act on version tags
|
||||||
|
tags:
|
||||||
|
only: /basedon-.+/
|
||||||
|
- arm32:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /basedon-.+/
|
||||||
|
- arm64:
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /basedon-.+/
|
||||||
|
- multiarch:
|
||||||
|
requires:
|
||||||
|
- amd64
|
||||||
|
- arm32
|
||||||
|
- arm64
|
||||||
|
filters:
|
||||||
|
branches:
|
||||||
|
ignore: /.*/
|
||||||
|
tags:
|
||||||
|
only: /basedon-.+/
|
||||||
4
.dockerignore
Normal file
4
.dockerignore
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
Dockerfile
|
||||||
|
linuxamd64.Dockerfile
|
||||||
|
linuxarm32v7.Dockerfile
|
||||||
|
.circleci/
|
||||||
4
.gitattributes
vendored
Normal file
4
.gitattributes
vendored
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
# Declare files that will always have CRLF line endings on checkout.
|
||||||
|
*.sh text eol=lf
|
||||||
|
*.go text eol=lf
|
||||||
|
Makefile text eol=lf
|
||||||
4
.github/workflows/main.yml
vendored
4
.github/workflows/main.yml
vendored
@ -21,7 +21,7 @@ defaults:
|
|||||||
shell: bash
|
shell: bash
|
||||||
|
|
||||||
env:
|
env:
|
||||||
BITCOIN_VERSION: "27"
|
BITCOIN_VERSION: "28"
|
||||||
|
|
||||||
TRANCHES: 8
|
TRANCHES: 8
|
||||||
|
|
||||||
@ -31,7 +31,7 @@ env:
|
|||||||
# /dev.Dockerfile
|
# /dev.Dockerfile
|
||||||
# /make/builder.Dockerfile
|
# /make/builder.Dockerfile
|
||||||
# /.github/workflows/release.yml
|
# /.github/workflows/release.yml
|
||||||
GO_VERSION: 1.22.5
|
GO_VERSION: 1.22.6
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
########################
|
########################
|
||||||
|
|||||||
3
.github/workflows/release.yaml
vendored
3
.github/workflows/release.yaml
vendored
@ -11,12 +11,11 @@ defaults:
|
|||||||
|
|
||||||
env:
|
env:
|
||||||
# If you change this value, please change it in the following files as well:
|
# If you change this value, please change it in the following files as well:
|
||||||
# /.travis.yml
|
|
||||||
# /Dockerfile
|
# /Dockerfile
|
||||||
# /dev.Dockerfile
|
# /dev.Dockerfile
|
||||||
# /make/builder.Dockerfile
|
# /make/builder.Dockerfile
|
||||||
# /.github/workflows/main.yml
|
# /.github/workflows/main.yml
|
||||||
GO_VERSION: 1.22.5
|
GO_VERSION: 1.22.6
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
main:
|
main:
|
||||||
|
|||||||
1
.gitignore
vendored
1
.gitignore
vendored
@ -66,6 +66,7 @@ profile.tmp
|
|||||||
.DS_Store
|
.DS_Store
|
||||||
|
|
||||||
.vscode
|
.vscode
|
||||||
|
*.code-workspace
|
||||||
|
|
||||||
# Coverage test
|
# Coverage test
|
||||||
coverage.txt
|
coverage.txt
|
||||||
|
|||||||
@ -1,18 +1,8 @@
|
|||||||
run:
|
run:
|
||||||
# timeout for analysis
|
go: "1.22.6"
|
||||||
deadline: 10m
|
|
||||||
|
|
||||||
# Skip autogenerated files for mobile and gRPC as well as copied code for
|
# Abort after 10 minutes.
|
||||||
# internal use.
|
timeout: 10m
|
||||||
skip-files:
|
|
||||||
- "mobile\\/.*generated\\.go"
|
|
||||||
- "\\.pb\\.go$"
|
|
||||||
- "\\.pb\\.gw\\.go$"
|
|
||||||
- "internal\\/musig2v040"
|
|
||||||
|
|
||||||
skip-dirs:
|
|
||||||
- channeldb/migration_01_to_11
|
|
||||||
- channeldb/migration/lnwire21
|
|
||||||
|
|
||||||
build-tags:
|
build-tags:
|
||||||
- autopilotrpc
|
- autopilotrpc
|
||||||
@ -57,7 +47,6 @@ linters-settings:
|
|||||||
- G306 # Poor file permissions used when writing to a new file.
|
- G306 # Poor file permissions used when writing to a new file.
|
||||||
|
|
||||||
staticcheck:
|
staticcheck:
|
||||||
go: "1.22.5"
|
|
||||||
checks: ["-SA1019"]
|
checks: ["-SA1019"]
|
||||||
|
|
||||||
lll:
|
lll:
|
||||||
@ -104,11 +93,13 @@ linters-settings:
|
|||||||
- 'errors.Wrap'
|
- 'errors.Wrap'
|
||||||
|
|
||||||
gomoddirectives:
|
gomoddirectives:
|
||||||
|
replace-local: true
|
||||||
replace-allow-list:
|
replace-allow-list:
|
||||||
# See go.mod for the explanation why these are needed.
|
# See go.mod for the explanation why these are needed.
|
||||||
- github.com/ulikunitz/xz
|
- github.com/ulikunitz/xz
|
||||||
- github.com/gogo/protobuf
|
- github.com/gogo/protobuf
|
||||||
- google.golang.org/protobuf
|
- google.golang.org/protobuf
|
||||||
|
- github.com/lightningnetwork/lnd/sqldb
|
||||||
|
|
||||||
|
|
||||||
linters:
|
linters:
|
||||||
@ -131,25 +122,15 @@ linters:
|
|||||||
- gochecknoinits
|
- gochecknoinits
|
||||||
|
|
||||||
# Deprecated linters. See https://golangci-lint.run/usage/linters/.
|
# Deprecated linters. See https://golangci-lint.run/usage/linters/.
|
||||||
- interfacer
|
|
||||||
- golint
|
|
||||||
- maligned
|
|
||||||
- scopelint
|
|
||||||
- exhaustivestruct
|
|
||||||
- bodyclose
|
- bodyclose
|
||||||
- contextcheck
|
- contextcheck
|
||||||
- nilerr
|
- nilerr
|
||||||
- noctx
|
- noctx
|
||||||
- rowserrcheck
|
- rowserrcheck
|
||||||
- sqlclosecheck
|
- sqlclosecheck
|
||||||
- structcheck
|
|
||||||
- tparallel
|
- tparallel
|
||||||
- unparam
|
- unparam
|
||||||
- wastedassign
|
- wastedassign
|
||||||
- ifshort
|
|
||||||
- varcheck
|
|
||||||
- deadcode
|
|
||||||
- nosnakecase
|
|
||||||
|
|
||||||
|
|
||||||
# Disable gofumpt as it has weird behavior regarding formatting multiple
|
# Disable gofumpt as it has weird behavior regarding formatting multiple
|
||||||
@ -189,7 +170,7 @@ linters:
|
|||||||
- wrapcheck
|
- wrapcheck
|
||||||
|
|
||||||
# Allow dynamic errors.
|
# Allow dynamic errors.
|
||||||
- goerr113
|
- err113
|
||||||
|
|
||||||
# We use ErrXXX instead.
|
# We use ErrXXX instead.
|
||||||
- errname
|
- errname
|
||||||
@ -205,15 +186,41 @@ linters:
|
|||||||
# The linter is too aggressive and doesn't add much value since reviewers
|
# The linter is too aggressive and doesn't add much value since reviewers
|
||||||
# will also catch magic numbers that make sense to extract.
|
# will also catch magic numbers that make sense to extract.
|
||||||
- gomnd
|
- gomnd
|
||||||
|
- mnd
|
||||||
|
|
||||||
# Some of the tests cannot be parallelized. On the other hand, we don't
|
# Some of the tests cannot be parallelized. On the other hand, we don't
|
||||||
# gain much performance with this check so we disable it for now until
|
# gain much performance with this check so we disable it for now until
|
||||||
# unit tests become our CI bottleneck.
|
# unit tests become our CI bottleneck.
|
||||||
- paralleltest
|
- paralleltest
|
||||||
|
|
||||||
|
# New linters that we haven't had time to address yet.
|
||||||
|
- testifylint
|
||||||
|
- perfsprint
|
||||||
|
- inamedparam
|
||||||
|
- copyloopvar
|
||||||
|
- tagalign
|
||||||
|
- protogetter
|
||||||
|
- revive
|
||||||
|
- depguard
|
||||||
|
- gosmopolitan
|
||||||
|
- intrange
|
||||||
|
|
||||||
|
|
||||||
issues:
|
issues:
|
||||||
# Only show newly introduced problems.
|
# Only show newly introduced problems.
|
||||||
new-from-rev: 8c66353e4c02329abdacb5a8df29998035ec2e24
|
new-from-rev: 77c7f776d5cbf9e147edc81d65ae5ba177a684e5
|
||||||
|
|
||||||
|
# Skip autogenerated files for mobile and gRPC as well as copied code for
|
||||||
|
# internal use.
|
||||||
|
skip-files:
|
||||||
|
- "mobile\\/.*generated\\.go"
|
||||||
|
- "\\.pb\\.go$"
|
||||||
|
- "\\.pb\\.gw\\.go$"
|
||||||
|
- "internal\\/musig2v040"
|
||||||
|
|
||||||
|
skip-dirs:
|
||||||
|
- channeldb/migration_01_to_11
|
||||||
|
- channeldb/migration/lnwire21
|
||||||
|
|
||||||
exclude-rules:
|
exclude-rules:
|
||||||
# Exclude gosec from running for tests so that tests with weak randomness
|
# Exclude gosec from running for tests so that tests with weak randomness
|
||||||
@ -254,8 +261,8 @@ issues:
|
|||||||
- forbidigo
|
- forbidigo
|
||||||
- godot
|
- godot
|
||||||
|
|
||||||
# Allow fmt.Printf() in lncli.
|
# Allow fmt.Printf() in commands.
|
||||||
- path: cmd/lncli/*
|
- path: cmd/commands/*
|
||||||
linters:
|
linters:
|
||||||
- forbidigo
|
- forbidigo
|
||||||
|
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
# /make/builder.Dockerfile
|
# /make/builder.Dockerfile
|
||||||
# /.github/workflows/main.yml
|
# /.github/workflows/main.yml
|
||||||
# /.github/workflows/release.yml
|
# /.github/workflows/release.yml
|
||||||
FROM golang:1.22.5-alpine as builder
|
FROM golang:1.22.6-alpine as builder
|
||||||
|
|
||||||
# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
|
# Force Go to use the cgo based DNS resolver. This is required to ensure DNS
|
||||||
# queries required to connect to linked containers succeed.
|
# queries required to connect to linked containers succeed.
|
||||||
|
|||||||
10
Makefile
10
Makefile
@ -23,6 +23,9 @@ ANDROID_BUILD := $(ANDROID_BUILD_DIR)/Lndmobile.aar
|
|||||||
|
|
||||||
COMMIT := $(shell git describe --tags --dirty)
|
COMMIT := $(shell git describe --tags --dirty)
|
||||||
|
|
||||||
|
COMMIT := $(subst -dirty,-fresh-btcpay,$(COMMIT))
|
||||||
|
LDFLAGS := -ldflags "-X $(PKG)/build.Commit=$(COMMIT)"
|
||||||
|
|
||||||
# Determine the minor version of the active Go installation.
|
# Determine the minor version of the active Go installation.
|
||||||
ACTIVE_GO_VERSION := $(shell go version | sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p')
|
ACTIVE_GO_VERSION := $(shell go version | sed -nre 's/^[^0-9]*(([0-9]+\.)*[0-9]+).*/\1/p')
|
||||||
ACTIVE_GO_VERSION_MINOR := $(shell echo $(ACTIVE_GO_VERSION) | cut -d. -f2)
|
ACTIVE_GO_VERSION_MINOR := $(shell echo $(ACTIVE_GO_VERSION) | cut -d. -f2)
|
||||||
@ -32,10 +35,15 @@ ifeq ($(shell expr $(ACTIVE_GO_VERSION_MINOR) \>= 21), 1)
|
|||||||
LOOPVARFIX := GOEXPERIMENT=loopvar
|
LOOPVARFIX := GOEXPERIMENT=loopvar
|
||||||
endif
|
endif
|
||||||
|
|
||||||
|
LOOPVARFIX :=
|
||||||
|
ifeq ($(shell expr $(GO_VERSION_MINOR) \>= 21), 1)
|
||||||
|
LOOPVARFIX := GOEXPERIMENT=loopvar
|
||||||
|
endif
|
||||||
|
|
||||||
# GO_VERSION is the Go version used for the release build, docker files, and
|
# GO_VERSION is the Go version used for the release build, docker files, and
|
||||||
# GitHub Actions. This is the reference version for the project. All other Go
|
# GitHub Actions. This is the reference version for the project. All other Go
|
||||||
# versions are checked against this version.
|
# versions are checked against this version.
|
||||||
GO_VERSION = 1.22.5
|
GO_VERSION = 1.22.6
|
||||||
|
|
||||||
GOBUILD := $(LOOPVARFIX) go build -v
|
GOBUILD := $(LOOPVARFIX) go build -v
|
||||||
GOINSTALL := $(LOOPVARFIX) go install -v
|
GOINSTALL := $(LOOPVARFIX) go install -v
|
||||||
|
|||||||
153
README.md
153
README.md
@ -1,98 +1,89 @@
|
|||||||
## Lightning Network Daemon
|
# BTCPayServer LND
|
||||||
|
|
||||||
[](https://github.com/lightningnetwork/lnd/actions/workflows/release.yaml)
|
This repository is used to build LND Docker container images that are distributed with BTCPayServer by default.
|
||||||
[](https://github.com/lightningnetwork/lnd/blob/master/LICENSE)
|
|
||||||
[](https://web.libera.chat/#lnd)
|
|
||||||
[](https://godoc.org/github.com/lightningnetwork/lnd)
|
|
||||||
[](https://goreportcard.com/report/github.com/lightningnetwork/lnd)
|
|
||||||
|
|
||||||
<img src="logo.png">
|
Docker images are published to https://hub.docker.com/r/btcpayserver/lnd/
|
||||||
|
|
||||||
The Lightning Network Daemon (`lnd`) - is a complete implementation of a
|
Versions:
|
||||||
[Lightning Network](https://lightning.network) node. `lnd` has several pluggable back-end
|
- [0.18.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.3-beta/images/sha256-513ddd55a5af44a14e27110ee14cb28f1c7a69205bcaa2fba4e66275c1f725e5?context=repo)
|
||||||
chain services including [`btcd`](https://github.com/btcsuite/btcd) (a
|
- Includes 0.28.7-beta Loop
|
||||||
full-node), [`bitcoind`](https://github.com/bitcoin/bitcoin), and
|
- [Fix for lnd unlock password \n problem](https://github.com/btcpayserver/lnd/pull/7)
|
||||||
[`neutrino`](https://github.com/lightninglabs/neutrino) (a new experimental light client). The project's codebase uses the
|
- [0.18.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.1-beta/images/sha256-5fbfa76a218ab59bf9206485f4c0c071a525f9f0906255a5672054741d043b79?context=repo)
|
||||||
[btcsuite](https://github.com/btcsuite/) set of Bitcoin libraries, and also
|
- Includes 0.28.5-beta Loop
|
||||||
exports a large set of isolated re-usable Lightning Network related libraries
|
- [0.18.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.0-beta/images/sha256-e6043dddf0bdbd5c740e882447c441b37f87f2c736ebb08747a4aff5e100d9bf?context=repo)
|
||||||
within it. In the current state `lnd` is capable of:
|
- Includes 0.28.2-beta Loop
|
||||||
* Creating channels.
|
- [0.17.4-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.4-beta/images/sha256-b62ecff5ca71d37f9b4846f35b4d86ddc4faa3fc1dd0618ae9221d99f47708bd?context=explore)
|
||||||
* Closing channels.
|
- Includes 0.26.6-beta Loop
|
||||||
* Completely managing all channel states (including the exceptional ones!).
|
- [0.17.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.3-beta/images/sha256-141611de6c062835e9513dd1ec4155c779d7a7b55258eb1fe06e228b0835fa56?context=repo)
|
||||||
* Maintaining a fully authenticated+validated channel graph.
|
- Includes 0.26.6-beta Loop
|
||||||
* Performing path finding within the network, passively forwarding incoming payments.
|
- [0.17.2-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.2-beta/images/sha256-936767369b703a67daf6db6a008a3b53c15f407d29a7ad2327a0de28f5951b30?context=explore)
|
||||||
* Sending outgoing [onion-encrypted payments](https://github.com/lightningnetwork/lightning-onion)
|
- Includes 0.26.5-beta Loop
|
||||||
through the network.
|
- [0.17.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.1-beta/images/sha256-b5c106136bd33a422463c736a1db8bd3541f95ac6f277dae86ab2a01b0c3445a?context=explore)
|
||||||
* Updating advertised fee schedules.
|
- Includes 0.26.5-beta Loop
|
||||||
* Automatic channel management ([`autopilot`](https://github.com/lightningnetwork/lnd/tree/master/autopilot)).
|
- [0.17.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.0-beta/images/sha256-58b98f983cd786bcb4d48ea8586144cafd44d58dc3018e26bfbfcf875f495368?context=explore)
|
||||||
|
- Includes 0.26.4-beta Loop
|
||||||
|
- [0.17.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.17.0-beta/images/sha256-58b98f983cd786bcb4d48ea8586144cafd44d58dc3018e26bfbfcf875f495368?context=explore)
|
||||||
|
- Includes 0.26.4-beta Loop
|
||||||
|
- [0.16.4-beta-1](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.4-beta-1/images/sha256-9dd204b62d6c892485b3dd8a76e8f48545ceda5702c9d47329ba4bcbc535a8b4?context=explore)
|
||||||
|
- [0.16.3-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.3-beta/images/sha256-9ff34769378cfca18664c7d1da3747e7ad7fb7f38a9a7b82a3d4f85e5bfef7bf?context=explore)
|
||||||
|
- [0.16.2-beta-1](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.2-beta-1/images/sha256-bfff9de84a0a4af9d643ff555125358861b70374976b970cc00d1e7fc44ed520?context=explore)
|
||||||
|
- [0.16.1-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.0-beta/images/sha256-f0eb70c20691aaa2ffc34fd5bd6c284299c84e96152cda5e46882a3aa4a3c6a2?context=explore)
|
||||||
|
- [0.16.0-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.16.0-beta/images/sha256-f0eb70c20691aaa2ffc34fd5bd6c284299c84e96152cda5e46882a3aa4a3c6a2?context=explore)
|
||||||
|
- [0.15.4-beta](https://hub.docker.com/layers/btcpayserver/lnd/v0.15.4-beta-1/images/sha256-cadbbff93cf36146e24fa4f32170b4b9d278a2e1acfdc50470790a94506ee9c3?context=explore)
|
||||||
|
- [Other versions are tagged](https://github.com/btcpayserver/lnd/tags), but obsoleted and not supported.
|
||||||
|
- All LND versions prior to 0.15.4 contain a consensus bug that prevents them from properly parsing transactions with more than 500,000 witness items per input (https://github.com/btcsuite/btcd/issues/1906)
|
||||||
|
- All LND versions prior to 0.15.2 contain a bug that prevents them from properly parsing Taproot transactions with script size over 11000 bytes (https://github.com/lightningnetwork/lnd/issues/7002)
|
||||||
|
- LND version 0.14.0-beta shipped with check that made it incompatable with c-lightning and eclair (https://github.com/lightningnetwork/lnd/issues/5890)
|
||||||
|
- All LND versions prior to 0.13.3 contain specification-level vulnerability (https://lists.linuxfoundation.org/pipermail/lightning-dev/2021-October/003257.html)
|
||||||
|
- All LND versions prior to 0.7 contain critical vulnerability (https://lists.linuxfoundation.org/pipermail/lightning-dev/2019-September/002174.html)
|
||||||
|
|
||||||
## Lightning Network Specification Compliance
|
Each version is marked with appropriate `basedon-vX.X.X-beta` tags. We are using `basedon` prefix in order not to conflict with LND tags from source repository.
|
||||||
`lnd` _fully_ conforms to the [Lightning Network specification
|
|
||||||
(BOLTs)](https://github.com/lightningnetwork/lightning-rfc). BOLT stands for:
|
|
||||||
Basis of Lightning Technology. The specifications are currently being drafted
|
|
||||||
by several groups of implementers based around the world including the
|
|
||||||
developers of `lnd`. The set of specification documents as well as our
|
|
||||||
implementation of the specification are still a work-in-progress. With that
|
|
||||||
said, the current status of `lnd`'s BOLT compliance is:
|
|
||||||
|
|
||||||
- [X] BOLT 1: Base Protocol
|
## Updating LND version in BTCPay Server
|
||||||
- [X] BOLT 2: Peer Protocol for Channel Management
|
|
||||||
- [X] BOLT 3: Bitcoin Transaction and Script Formats
|
|
||||||
- [X] BOLT 4: Onion Routing Protocol
|
|
||||||
- [X] BOLT 5: Recommendations for On-chain Transaction Handling
|
|
||||||
- [X] BOLT 7: P2P Node and Channel Discovery
|
|
||||||
- [X] BOLT 8: Encrypted and Authenticated Transport
|
|
||||||
- [X] BOLT 9: Assigned Feature Flags
|
|
||||||
- [X] BOLT 10: DNS Bootstrap and Assisted Node Location
|
|
||||||
- [X] BOLT 11: Invoice Protocol for Lightning Payments
|
|
||||||
|
|
||||||
## Developer Resources
|
1. **Update https://github.com/btcpayserver/lnd**
|
||||||
|
|
||||||
The daemon has been designed to be as developer friendly as possible in order
|
a) Go to https://github.com/lightningnetwork/lnd/releases and find the commit on which we should add our resources.
|
||||||
to facilitate application development on top of `lnd`. Two primary RPC
|
b) Checkout a new branch for that commit, usually in the format of `lnd/v0.18.3-beta`.
|
||||||
interfaces are exported: an HTTP REST API, and a [gRPC](https://grpc.io/)
|
c) Cherry-pick the `Adding BtcPayServer related files and resources` commit. [Example commit](https://github.com/btcpayserver/lnd/commit/ae4bb33c6a3db8b7cc01d18fdf46e600ead9bed4).
|
||||||
service. The exported APIs are not yet stable, so be warned: they may change
|
d) Tag it with the `basedon-v*` prefix name and push it. For v0.18.1, the tag name was `basedon-v0.18.3-beta`.
|
||||||
drastically in the near future.
|
i. Before you push the tag to CircleCI to build and publish image to Docker Hub, you can test if building the image works locally.
|
||||||
|
ii. You can do this for linuxamd64 for example by using command `docker build --pull -t local-lnd:test_version -f linuxamd64.Dockerfile .`
|
||||||
|
e) The build process will start (it [matches on tag format](.circleci/config.yml#L11)). Here is [an example CircleCI build](https://app.circleci.com/pipelines/github/btcpayserver/lnd/202/workflows/b90b5888-c0b8-4207-860e-a63ce21077af).
|
||||||
|
f) The resulting image will be published to Docker Hub. Example [Docker Hub image for v0.18.3](https://hub.docker.com/layers/btcpayserver/lnd/v0.18.3-beta/images/sha256-513ddd55a5af44a14e27110ee14cb28f1c7a69205bcaa2fba4e66275c1f725e5?context=repo).
|
||||||
|
|
||||||
An automatically generated set of documentation for the RPC APIs can be found
|
Occasionally, there are problems with:
|
||||||
at [api.lightning.community](https://api.lightning.community). A set of developer
|
- Versioning of base Docker images used for building Go binaries. You may need to bump that base image - [example commit](https://github.com/btcpayserver/lnd/commit/c841954c515a9d067c24987291316b093b91c2f2).
|
||||||
resources including guides, articles, example applications and community resources can be found at:
|
- [Updating Loop](https://github.com/lightninglabs/loop) as part of the package, which needs to happen occasionally. [Example bump of Loop version](https://github.com/btcpayserver/lnd/commit/b3aecc7ac58280ef662e39ba99461573a30fe79a
|
||||||
[docs.lightning.engineering](https://docs.lightning.engineering).
|
|
||||||
|
|
||||||
Finally, we also have an active
|
3. **Update https://github.com/btcpayserver/BTCPayServer.Lightning**
|
||||||
[Slack](https://lightning.engineering/slack.html) where protocol developers, application developers, testers and users gather to
|
|
||||||
discuss various aspects of `lnd` and also Lightning in general.
|
|
||||||
|
|
||||||
## Installation
|
Now we need to update the dependency in our Lightning library project. This library has tests, so we will know if something is broken.
|
||||||
In order to build from source, please see [the installation
|
|
||||||
instructions](docs/INSTALL.md).
|
|
||||||
|
|
||||||
## Docker
|
a) Modify the `docker-compose.yml` file to reference the new LND version. [Example](https://github.com/btcpayserver/BTCPayServer.Lightning/pull/162/commits/413784ef9b2a8e7aa0496eb91f792ff0086c0ef7).
|
||||||
To run lnd from Docker, please see the main [Docker instructions](docs/DOCKER.md)
|
b) Checkout a new branch for that commit, usually in the format of `feat/lnd-0.18.1`.
|
||||||
|
c) Title the commit `Bumping LND to 0.18.1-beta`.
|
||||||
|
d) Open a pull request and reference Docker Hub and Tag. [Example PR](https://github.com/btcpayserver/BTCPayServer.Lightning/pull/162).
|
||||||
|
e) Once tests pass, you can merge it.
|
||||||
|
|
||||||
## IRC
|
4. **Update https://github.com/btcpayserver/btcpayserver**
|
||||||
* irc.libera.chat
|
|
||||||
* channel #lnd
|
|
||||||
* [webchat](https://web.libera.chat/#lnd)
|
|
||||||
|
|
||||||
## Safety
|
This will give access to LND to the whole dev team and allow for further testing on their dev machines if everything works as expected.
|
||||||
|
|
||||||
When operating a mainnet `lnd` node, please refer to our [operational safety
|
a) Modify 4 `docker-compose.yml` files in `BTCPayServer.Tests`. [Example pull request to emulate](https://github.com/btcpayserver/btcpayserver/pull/6795).
|
||||||
guidelines](docs/safety.md). It is important to note that `lnd` is still
|
b) When you open the PR, include the version and link to the BTCPayServer.Lightning PR.
|
||||||
**beta** software and that ignoring these operational guidelines can lead to
|
c) Once tests pass, you can merge it.
|
||||||
loss of funds.
|
|
||||||
|
|
||||||
## Security
|
5. **Update https://github.com/btcpayserver/btcpayserver-docker**
|
||||||
|
|
||||||
The developers of `lnd` take security _very_ seriously. The disclosure of
|
a) Now that everything is prepared, open a PR in the btcpayserver-docker repository to allow these changes to propagate to everyone. [Example pull request](https://github.com/btcpayserver/btcpayserver-docker/pull/911).
|
||||||
security vulnerabilities helps us secure the health of `lnd`, privacy of our
|
b) Open the PR in DRAFT mode and tag @NicolasDorier and @Pavlenex as reviewers. They typically handle releases, and once they test that the LND version update works on their server, they can ACK the update and merge it as part of the release process.
|
||||||
users, and also the health of the Lightning Network as a whole. If you find
|
|
||||||
any issues regarding security or privacy, please disclose the information
|
|
||||||
responsibly by sending an email to security at lightning dot engineering,
|
|
||||||
preferably encrypted using our designated PGP key
|
|
||||||
(`91FE464CD75101DA6B6BAB60555C6465E5BCB3AF`) which can be found
|
|
||||||
[here](https://gist.githubusercontent.com/Roasbeef/6fb5b52886183239e4aa558f83d085d3/raw/5fa96010af201628bcfa61e9309d9b13d23d220f/security@lightning.engineering).
|
|
||||||
|
|
||||||
## Further reading
|
## Source repository
|
||||||
* [Step-by-step send payment guide with docker](https://github.com/lightningnetwork/lnd/tree/master/docker)
|
|
||||||
* [Contribution guide](https://github.com/lightningnetwork/lnd/blob/master/docs/code_contribution_guidelines.md)
|
https://github.com/lightningnetwork/lnd
|
||||||
|
|
||||||
|
## Links
|
||||||
|
* [BTCPayServer main repo](https://github.com/btcpayserver/btcpayserver)
|
||||||
|
* [BTCPayServer-Docker repo](https://github.com/btcpayserver/btcpayserver-docker)
|
||||||
|
* [BTCPayServer.Lightning](https://github.com/btcpayserver/BTCPayServer.Lightning)
|
||||||
|
|||||||
@ -5,11 +5,22 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
"sync"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
"github.com/lightningnetwork/lnd/htlcswitch/hop"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
"golang.org/x/exp/maps"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// UpdateLinkAliases is a function type for a function that locates the active
|
||||||
|
// link that matches the given shortID and triggers an update based on the
|
||||||
|
// latest values of the alias manager.
|
||||||
|
type UpdateLinkAliases func(shortID lnwire.ShortChannelID) error
|
||||||
|
|
||||||
|
// ScidAliasMap is a map from a base short channel ID to a set of alias short
|
||||||
|
// channel IDs.
|
||||||
|
type ScidAliasMap map[lnwire.ShortChannelID][]lnwire.ShortChannelID
|
||||||
|
|
||||||
var (
|
var (
|
||||||
// aliasBucket stores aliases as keys and their base SCIDs as values.
|
// aliasBucket stores aliases as keys and their base SCIDs as values.
|
||||||
// This is used to populate the maps that the Manager uses. The keys
|
// This is used to populate the maps that the Manager uses. The keys
|
||||||
@ -47,17 +58,18 @@ var (
|
|||||||
// operations.
|
// operations.
|
||||||
byteOrder = binary.BigEndian
|
byteOrder = binary.BigEndian
|
||||||
|
|
||||||
// startBlockHeight is the starting block height of the alias range.
|
// AliasStartBlockHeight is the starting block height of the alias
|
||||||
startingBlockHeight = 16_000_000
|
// range.
|
||||||
|
AliasStartBlockHeight uint32 = 16_000_000
|
||||||
|
|
||||||
// endBlockHeight is the ending block height of the alias range.
|
// AliasEndBlockHeight is the ending block height of the alias range.
|
||||||
endBlockHeight = 16_250_000
|
AliasEndBlockHeight uint32 = 16_250_000
|
||||||
|
|
||||||
// StartingAlias is the first alias ShortChannelID that will get
|
// StartingAlias is the first alias ShortChannelID that will get
|
||||||
// assigned by RequestAlias. The starting BlockHeight is chosen so that
|
// assigned by RequestAlias. The starting BlockHeight is chosen so that
|
||||||
// legitimate SCIDs in integration tests aren't mistaken for an alias.
|
// legitimate SCIDs in integration tests aren't mistaken for an alias.
|
||||||
StartingAlias = lnwire.ShortChannelID{
|
StartingAlias = lnwire.ShortChannelID{
|
||||||
BlockHeight: uint32(startingBlockHeight),
|
BlockHeight: AliasStartBlockHeight,
|
||||||
TxIndex: 0,
|
TxIndex: 0,
|
||||||
TxPosition: 0,
|
TxPosition: 0,
|
||||||
}
|
}
|
||||||
@ -68,6 +80,10 @@ var (
|
|||||||
// errNoPeerAlias is returned when the peer's alias for a given
|
// errNoPeerAlias is returned when the peer's alias for a given
|
||||||
// channel is not found.
|
// channel is not found.
|
||||||
errNoPeerAlias = fmt.Errorf("no peer alias found")
|
errNoPeerAlias = fmt.Errorf("no peer alias found")
|
||||||
|
|
||||||
|
// ErrAliasNotFound is returned when the alias is not found and can't
|
||||||
|
// be mapped to a base SCID.
|
||||||
|
ErrAliasNotFound = fmt.Errorf("alias not found")
|
||||||
)
|
)
|
||||||
|
|
||||||
// Manager is a struct that handles aliases for LND. It has an underlying
|
// Manager is a struct that handles aliases for LND. It has an underlying
|
||||||
@ -77,10 +93,14 @@ var (
|
|||||||
type Manager struct {
|
type Manager struct {
|
||||||
backend kvdb.Backend
|
backend kvdb.Backend
|
||||||
|
|
||||||
|
// linkAliasUpdater is a function used by the alias manager to
|
||||||
|
// facilitate live update of aliases in other subsystems.
|
||||||
|
linkAliasUpdater UpdateLinkAliases
|
||||||
|
|
||||||
// baseToSet is a mapping from the "base" SCID to the set of aliases
|
// baseToSet is a mapping from the "base" SCID to the set of aliases
|
||||||
// for this channel. This mapping includes all channels that
|
// for this channel. This mapping includes all channels that
|
||||||
// negotiated the option-scid-alias feature bit.
|
// negotiated the option-scid-alias feature bit.
|
||||||
baseToSet map[lnwire.ShortChannelID][]lnwire.ShortChannelID
|
baseToSet ScidAliasMap
|
||||||
|
|
||||||
// aliasToBase is a mapping that maps all aliases for a given channel
|
// aliasToBase is a mapping that maps all aliases for a given channel
|
||||||
// to its base SCID. This is only used for channels that have
|
// to its base SCID. This is only used for channels that have
|
||||||
@ -98,9 +118,15 @@ type Manager struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewManager initializes an alias Manager from the passed database backend.
|
// NewManager initializes an alias Manager from the passed database backend.
|
||||||
func NewManager(db kvdb.Backend) (*Manager, error) {
|
func NewManager(db kvdb.Backend, linkAliasUpdater UpdateLinkAliases) (*Manager,
|
||||||
m := &Manager{backend: db}
|
error) {
|
||||||
m.baseToSet = make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
|
|
||||||
|
m := &Manager{
|
||||||
|
backend: db,
|
||||||
|
baseToSet: make(ScidAliasMap),
|
||||||
|
linkAliasUpdater: linkAliasUpdater,
|
||||||
|
}
|
||||||
|
|
||||||
m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
|
m.aliasToBase = make(map[lnwire.ShortChannelID]lnwire.ShortChannelID)
|
||||||
m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
|
m.peerAlias = make(map[lnwire.ChannelID]lnwire.ShortChannelID)
|
||||||
|
|
||||||
@ -215,12 +241,22 @@ func (m *Manager) populateMaps() error {
|
|||||||
// AddLocalAlias adds a database mapping from the passed alias to the passed
|
// AddLocalAlias adds a database mapping from the passed alias to the passed
|
||||||
// base SCID. The gossip boolean marks whether or not to create a mapping
|
// base SCID. The gossip boolean marks whether or not to create a mapping
|
||||||
// that the gossiper will use. It is set to false for the upgrade path where
|
// that the gossiper will use. It is set to false for the upgrade path where
|
||||||
// the feature-bit is toggled on and there are existing channels.
|
// the feature-bit is toggled on and there are existing channels. The linkUpdate
|
||||||
|
// flag is used to signal whether this function should also trigger an update
|
||||||
|
// on the htlcswitch scid alias maps.
|
||||||
func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
|
func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
|
||||||
gossip bool) error {
|
gossip, linkUpdate bool) error {
|
||||||
|
|
||||||
|
// We need to lock the manager for the whole duration of this method,
|
||||||
|
// except for the very last part where we call the link updater. In
|
||||||
|
// order for us to safely use a defer _and_ still be able to manually
|
||||||
|
// unlock, we use a sync.Once.
|
||||||
m.Lock()
|
m.Lock()
|
||||||
defer m.Unlock()
|
unlockOnce := sync.Once{}
|
||||||
|
unlock := func() {
|
||||||
|
unlockOnce.Do(m.Unlock)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||||
// If the caller does not want to allow the alias to be used
|
// If the caller does not want to allow the alias to be used
|
||||||
@ -270,6 +306,18 @@ func (m *Manager) AddLocalAlias(alias, baseScid lnwire.ShortChannelID,
|
|||||||
m.aliasToBase[alias] = baseScid
|
m.aliasToBase[alias] = baseScid
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We definitely need to unlock the Manager before calling the link
|
||||||
|
// updater. If we don't, we'll deadlock. We use a sync.Once to ensure
|
||||||
|
// that we only unlock once.
|
||||||
|
unlock()
|
||||||
|
|
||||||
|
// Finally, we trigger a htlcswitch update if the flag is set, in order
|
||||||
|
// for any future htlc that references the added alias to be properly
|
||||||
|
// routed.
|
||||||
|
if linkUpdate {
|
||||||
|
return m.linkAliasUpdater(baseScid)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -340,6 +388,74 @@ func (m *Manager) DeleteSixConfs(baseScid lnwire.ShortChannelID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// DeleteLocalAlias removes a mapping from the database and the Manager's maps.
|
||||||
|
func (m *Manager) DeleteLocalAlias(alias,
|
||||||
|
baseScid lnwire.ShortChannelID) error {
|
||||||
|
|
||||||
|
// We need to lock the manager for the whole duration of this method,
|
||||||
|
// except for the very last part where we call the link updater. In
|
||||||
|
// order for us to safely use a defer _and_ still be able to manually
|
||||||
|
// unlock, we use a sync.Once.
|
||||||
|
m.Lock()
|
||||||
|
unlockOnce := sync.Once{}
|
||||||
|
unlock := func() {
|
||||||
|
unlockOnce.Do(m.Unlock)
|
||||||
|
}
|
||||||
|
defer unlock()
|
||||||
|
|
||||||
|
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||||
|
aliasToBaseBucket, err := tx.CreateTopLevelBucket(aliasBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var aliasBytes [8]byte
|
||||||
|
byteOrder.PutUint64(aliasBytes[:], alias.ToUint64())
|
||||||
|
|
||||||
|
// If the user attempts to delete an alias that doesn't exist,
|
||||||
|
// we'll want to inform them about it and not just do nothing.
|
||||||
|
if aliasToBaseBucket.Get(aliasBytes[:]) == nil {
|
||||||
|
return ErrAliasNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
return aliasToBaseBucket.Delete(aliasBytes[:])
|
||||||
|
}, func() {})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now that the database state has been updated, we'll delete the
|
||||||
|
// mapping from the Manager's maps.
|
||||||
|
aliasSet, ok := m.baseToSet[baseScid]
|
||||||
|
if !ok {
|
||||||
|
return ErrAliasNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll filter the alias set and remove the alias from it.
|
||||||
|
aliasSet = fn.Filter(func(a lnwire.ShortChannelID) bool {
|
||||||
|
return a.ToUint64() != alias.ToUint64()
|
||||||
|
}, aliasSet)
|
||||||
|
|
||||||
|
// If the alias set is empty, we'll delete the base SCID from the
|
||||||
|
// baseToSet map.
|
||||||
|
if len(aliasSet) == 0 {
|
||||||
|
delete(m.baseToSet, baseScid)
|
||||||
|
} else {
|
||||||
|
m.baseToSet[baseScid] = aliasSet
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we'll delete the aliasToBase mapping from the Manager's
|
||||||
|
// cache (but this is only set if we gossip the alias).
|
||||||
|
delete(m.aliasToBase, alias)
|
||||||
|
|
||||||
|
// We definitely need to unlock the Manager before calling the link
|
||||||
|
// updater. If we don't, we'll deadlock. We use a sync.Once to ensure
|
||||||
|
// that we only unlock once.
|
||||||
|
unlock()
|
||||||
|
|
||||||
|
return m.linkAliasUpdater(baseScid)
|
||||||
|
}
|
||||||
|
|
||||||
// PutPeerAlias stores the peer's alias SCID once we learn of it in the
|
// PutPeerAlias stores the peer's alias SCID once we learn of it in the
|
||||||
// channel_ready message.
|
// channel_ready message.
|
||||||
func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID,
|
func (m *Manager) PutPeerAlias(chanID lnwire.ChannelID,
|
||||||
@ -392,6 +508,19 @@ func (m *Manager) GetPeerAlias(chanID lnwire.ChannelID) (lnwire.ShortChannelID,
|
|||||||
func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
||||||
var nextAlias lnwire.ShortChannelID
|
var nextAlias lnwire.ShortChannelID
|
||||||
|
|
||||||
|
m.RLock()
|
||||||
|
defer m.RUnlock()
|
||||||
|
|
||||||
|
// haveAlias returns true if the passed alias is already assigned to a
|
||||||
|
// channel in the baseToSet map.
|
||||||
|
haveAlias := func(maybeNextAlias lnwire.ShortChannelID) bool {
|
||||||
|
return fn.Any(func(aliasList []lnwire.ShortChannelID) bool {
|
||||||
|
return fn.Any(func(alias lnwire.ShortChannelID) bool {
|
||||||
|
return alias == maybeNextAlias
|
||||||
|
}, aliasList)
|
||||||
|
}, maps.Values(m.baseToSet))
|
||||||
|
}
|
||||||
|
|
||||||
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
err := kvdb.Update(m.backend, func(tx kvdb.RwTx) error {
|
||||||
bucket, err := tx.CreateTopLevelBucket(aliasAllocBucket)
|
bucket, err := tx.CreateTopLevelBucket(aliasAllocBucket)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -404,6 +533,29 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
|||||||
// StartingAlias to it.
|
// StartingAlias to it.
|
||||||
nextAlias = StartingAlias
|
nextAlias = StartingAlias
|
||||||
|
|
||||||
|
// If the very first alias is already assigned, we'll
|
||||||
|
// keep incrementing until we find an unassigned alias.
|
||||||
|
// This is to avoid collision with custom added SCID
|
||||||
|
// aliases that fall into the same range as the ones we
|
||||||
|
// generate here monotonically. Those custom SCIDs are
|
||||||
|
// stored in a different bucket, but we can just check
|
||||||
|
// the in-memory map for simplicity.
|
||||||
|
for {
|
||||||
|
if !haveAlias(nextAlias) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAlias = getNextScid(nextAlias)
|
||||||
|
|
||||||
|
// Abort if we've reached the end of the range.
|
||||||
|
if nextAlias.BlockHeight >=
|
||||||
|
AliasEndBlockHeight {
|
||||||
|
|
||||||
|
return fmt.Errorf("range for custom " +
|
||||||
|
"aliases exhausted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var scratch [8]byte
|
var scratch [8]byte
|
||||||
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
||||||
return bucket.Put(lastAliasKey, scratch[:])
|
return bucket.Put(lastAliasKey, scratch[:])
|
||||||
@ -418,6 +570,26 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
|||||||
)
|
)
|
||||||
nextAlias = getNextScid(lastScid)
|
nextAlias = getNextScid(lastScid)
|
||||||
|
|
||||||
|
// If the next alias is already assigned, we'll keep
|
||||||
|
// incrementing until we find an unassigned alias. This is to
|
||||||
|
// avoid collision with custom added SCID aliases that fall into
|
||||||
|
// the same range as the ones we generate here monotonically.
|
||||||
|
// Those custom SCIDs are stored in a different bucket, but we
|
||||||
|
// can just check the in-memory map for simplicity.
|
||||||
|
for {
|
||||||
|
if !haveAlias(nextAlias) {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
|
||||||
|
nextAlias = getNextScid(nextAlias)
|
||||||
|
|
||||||
|
// Abort if we've reached the end of the range.
|
||||||
|
if nextAlias.BlockHeight >= AliasEndBlockHeight {
|
||||||
|
return fmt.Errorf("range for custom " +
|
||||||
|
"aliases exhausted")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var scratch [8]byte
|
var scratch [8]byte
|
||||||
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
byteOrder.PutUint64(scratch[:], nextAlias.ToUint64())
|
||||||
return bucket.Put(lastAliasKey, scratch[:])
|
return bucket.Put(lastAliasKey, scratch[:])
|
||||||
@ -433,11 +605,11 @@ func (m *Manager) RequestAlias() (lnwire.ShortChannelID, error) {
|
|||||||
|
|
||||||
// ListAliases returns a carbon copy of baseToSet. This is used by the rpc
|
// ListAliases returns a carbon copy of baseToSet. This is used by the rpc
|
||||||
// layer.
|
// layer.
|
||||||
func (m *Manager) ListAliases() map[lnwire.ShortChannelID][]lnwire.ShortChannelID {
|
func (m *Manager) ListAliases() ScidAliasMap {
|
||||||
m.RLock()
|
m.RLock()
|
||||||
defer m.RUnlock()
|
defer m.RUnlock()
|
||||||
|
|
||||||
baseCopy := make(map[lnwire.ShortChannelID][]lnwire.ShortChannelID)
|
baseCopy := make(ScidAliasMap)
|
||||||
|
|
||||||
for k, v := range m.baseToSet {
|
for k, v := range m.baseToSet {
|
||||||
setCopy := make([]lnwire.ShortChannelID, len(v))
|
setCopy := make([]lnwire.ShortChannelID, len(v))
|
||||||
@ -496,10 +668,10 @@ func getNextScid(last lnwire.ShortChannelID) lnwire.ShortChannelID {
|
|||||||
|
|
||||||
// IsAlias returns true if the passed SCID is an alias. The function determines
|
// IsAlias returns true if the passed SCID is an alias. The function determines
|
||||||
// this by looking at the BlockHeight. If the BlockHeight is greater than
|
// this by looking at the BlockHeight. If the BlockHeight is greater than
|
||||||
// startingBlockHeight and less than endBlockHeight, then it is an alias
|
// AliasStartBlockHeight and less than AliasEndBlockHeight, then it is an alias
|
||||||
// assigned by RequestAlias. These bounds only apply to aliases we generate.
|
// assigned by RequestAlias. These bounds only apply to aliases we generate.
|
||||||
// Our peers are free to use any range they choose.
|
// Our peers are free to use any range they choose.
|
||||||
func IsAlias(scid lnwire.ShortChannelID) bool {
|
func IsAlias(scid lnwire.ShortChannelID) bool {
|
||||||
return scid.BlockHeight >= uint32(startingBlockHeight) &&
|
return scid.BlockHeight >= AliasStartBlockHeight &&
|
||||||
scid.BlockHeight < uint32(endBlockHeight)
|
scid.BlockHeight < AliasEndBlockHeight
|
||||||
}
|
}
|
||||||
|
|||||||
@ -23,7 +23,11 @@ func TestAliasStorePeerAlias(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
aliasStore, err := NewManager(db)
|
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasStore, err := NewManager(db, linkUpdater)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
var chanID1 [32]byte
|
var chanID1 [32]byte
|
||||||
@ -52,7 +56,11 @@ func TestAliasStoreRequest(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
defer db.Close()
|
defer db.Close()
|
||||||
|
|
||||||
aliasStore, err := NewManager(db)
|
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasStore, err := NewManager(db, linkUpdater)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// We'll assert that the very first alias we receive is StartingAlias.
|
// We'll assert that the very first alias we receive is StartingAlias.
|
||||||
@ -68,6 +76,118 @@ func TestAliasStoreRequest(t *testing.T) {
|
|||||||
require.Equal(t, nextAlias, alias2)
|
require.Equal(t, nextAlias, alias2)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestAliasLifecycle tests that the aliases can be created and deleted.
|
||||||
|
func TestAliasLifecycle(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Create the backend database and use this to create the aliasStore.
|
||||||
|
dbPath := filepath.Join(t.TempDir(), "testdb")
|
||||||
|
db, err := kvdb.Create(
|
||||||
|
kvdb.BoltBackendName, dbPath, true, kvdb.DefaultDBTimeout,
|
||||||
|
)
|
||||||
|
require.NoError(t, err)
|
||||||
|
defer db.Close()
|
||||||
|
|
||||||
|
updateChan := make(chan struct{}, 1)
|
||||||
|
|
||||||
|
linkUpdater := func(shortID lnwire.ShortChannelID) error {
|
||||||
|
updateChan <- struct{}{}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
aliasStore, err := NewManager(db, linkUpdater)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
const (
|
||||||
|
base = uint64(123123123)
|
||||||
|
alias = uint64(456456456)
|
||||||
|
)
|
||||||
|
|
||||||
|
// Parse the aliases and base to short channel ID format.
|
||||||
|
baseScid := lnwire.NewShortChanIDFromInt(base)
|
||||||
|
aliasScid := lnwire.NewShortChanIDFromInt(alias)
|
||||||
|
aliasScid2 := lnwire.NewShortChanIDFromInt(alias + 1)
|
||||||
|
|
||||||
|
// Add the first alias.
|
||||||
|
err = aliasStore.AddLocalAlias(aliasScid, baseScid, false, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The link updater should be called.
|
||||||
|
<-updateChan
|
||||||
|
|
||||||
|
// Query the aliases and verify the results.
|
||||||
|
aliasList := aliasStore.GetAliases(baseScid)
|
||||||
|
require.Len(t, aliasList, 1)
|
||||||
|
require.Contains(t, aliasList, aliasScid)
|
||||||
|
|
||||||
|
// Add the second alias.
|
||||||
|
err = aliasStore.AddLocalAlias(aliasScid2, baseScid, false, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The link updater should be called.
|
||||||
|
<-updateChan
|
||||||
|
|
||||||
|
// Query the aliases and verify the results.
|
||||||
|
aliasList = aliasStore.GetAliases(baseScid)
|
||||||
|
require.Len(t, aliasList, 2)
|
||||||
|
require.Contains(t, aliasList, aliasScid)
|
||||||
|
require.Contains(t, aliasList, aliasScid2)
|
||||||
|
|
||||||
|
// Delete the first alias.
|
||||||
|
err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The link updater should be called.
|
||||||
|
<-updateChan
|
||||||
|
|
||||||
|
// We expect to get an error if we attempt to delete the same alias
|
||||||
|
// again.
|
||||||
|
err = aliasStore.DeleteLocalAlias(aliasScid, baseScid)
|
||||||
|
require.ErrorIs(t, err, ErrAliasNotFound)
|
||||||
|
|
||||||
|
// The link updater should _not_ be called.
|
||||||
|
select {
|
||||||
|
case <-updateChan:
|
||||||
|
t.Fatal("link alias updater should not have been called")
|
||||||
|
default:
|
||||||
|
}
|
||||||
|
|
||||||
|
// Query the aliases and verify that first one doesn't exist anymore.
|
||||||
|
aliasList = aliasStore.GetAliases(baseScid)
|
||||||
|
require.Len(t, aliasList, 1)
|
||||||
|
require.Contains(t, aliasList, aliasScid2)
|
||||||
|
require.NotContains(t, aliasList, aliasScid)
|
||||||
|
|
||||||
|
// Delete the second alias.
|
||||||
|
err = aliasStore.DeleteLocalAlias(aliasScid2, baseScid)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// The link updater should be called.
|
||||||
|
<-updateChan
|
||||||
|
|
||||||
|
// Query the aliases and verify that none exists.
|
||||||
|
aliasList = aliasStore.GetAliases(baseScid)
|
||||||
|
require.Len(t, aliasList, 0)
|
||||||
|
|
||||||
|
// We now request an alias generated by the aliasStore. This should give
|
||||||
|
// the first from the pre-defined list of allocated aliases.
|
||||||
|
firstRequested, err := aliasStore.RequestAlias()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, StartingAlias, firstRequested)
|
||||||
|
|
||||||
|
// We now manually add the next alias from the range as a custom alias.
|
||||||
|
secondAlias := getNextScid(firstRequested)
|
||||||
|
err = aliasStore.AddLocalAlias(secondAlias, baseScid, false, true)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// When we now request another alias from the allocation list, we expect
|
||||||
|
// the third one (tx position 2) to be returned.
|
||||||
|
thirdRequested, err := aliasStore.RequestAlias()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, getNextScid(secondAlias), thirdRequested)
|
||||||
|
require.EqualValues(t, 2, thirdRequested.TxPosition)
|
||||||
|
}
|
||||||
|
|
||||||
// TestGetNextScid tests that given a current lnwire.ShortChannelID,
|
// TestGetNextScid tests that given a current lnwire.ShortChannelID,
|
||||||
// getNextScid returns the expected alias to use next.
|
// getNextScid returns the expected alias to use next.
|
||||||
func TestGetNextScid(t *testing.T) {
|
func TestGetNextScid(t *testing.T) {
|
||||||
@ -80,7 +200,7 @@ func TestGetNextScid(t *testing.T) {
|
|||||||
name: "starting alias",
|
name: "starting alias",
|
||||||
current: StartingAlias,
|
current: StartingAlias,
|
||||||
expected: lnwire.ShortChannelID{
|
expected: lnwire.ShortChannelID{
|
||||||
BlockHeight: uint32(startingBlockHeight),
|
BlockHeight: AliasStartBlockHeight,
|
||||||
TxIndex: 0,
|
TxIndex: 0,
|
||||||
TxPosition: 1,
|
TxPosition: 1,
|
||||||
},
|
},
|
||||||
|
|||||||
@ -43,7 +43,7 @@ const (
|
|||||||
AppMinor uint = 18
|
AppMinor uint = 18
|
||||||
|
|
||||||
// AppPatch defines the application patch for this binary.
|
// AppPatch defines the application patch for this binary.
|
||||||
AppPatch uint = 00
|
AppPatch uint = 4
|
||||||
|
|
||||||
// AppPreRelease MUST only contain characters from semanticAlphabet per
|
// AppPreRelease MUST only contain characters from semanticAlphabet per
|
||||||
// the semantic versioning spec.
|
// the semantic versioning spec.
|
||||||
|
|||||||
@ -24,6 +24,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
|
"github.com/lightningnetwork/lnd/chainntnfs/neutrinonotify"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
@ -63,6 +64,14 @@ type Config struct {
|
|||||||
// state.
|
// state.
|
||||||
ChanStateDB *channeldb.ChannelStateDB
|
ChanStateDB *channeldb.ChannelStateDB
|
||||||
|
|
||||||
|
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||||
|
// leaves for certain custom channel types.
|
||||||
|
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||||
|
|
||||||
|
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||||
|
// leaves for certain custom channel types.
|
||||||
|
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||||
|
|
||||||
// BlockCache is the main cache for storing block information.
|
// BlockCache is the main cache for storing block information.
|
||||||
BlockCache *blockcache.BlockCache
|
BlockCache *blockcache.BlockCache
|
||||||
|
|
||||||
|
|||||||
@ -356,6 +356,30 @@ func (r *RPCAcceptor) sendAcceptRequests(errChan chan error,
|
|||||||
):
|
):
|
||||||
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
|
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT
|
||||||
|
|
||||||
|
case channelFeatures.OnlyContains(
|
||||||
|
lnwire.SimpleTaprootOverlayChansRequired,
|
||||||
|
lnwire.ZeroConfRequired,
|
||||||
|
lnwire.ScidAliasRequired,
|
||||||
|
):
|
||||||
|
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||||
|
|
||||||
|
case channelFeatures.OnlyContains(
|
||||||
|
lnwire.SimpleTaprootOverlayChansRequired,
|
||||||
|
lnwire.ZeroConfRequired,
|
||||||
|
):
|
||||||
|
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||||
|
|
||||||
|
case channelFeatures.OnlyContains(
|
||||||
|
lnwire.SimpleTaprootOverlayChansRequired,
|
||||||
|
lnwire.ScidAliasRequired,
|
||||||
|
):
|
||||||
|
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||||
|
|
||||||
|
case channelFeatures.OnlyContains(
|
||||||
|
lnwire.SimpleTaprootOverlayChansRequired,
|
||||||
|
):
|
||||||
|
commitmentType = lnrpc.CommitmentType_SIMPLE_TAPROOT_OVERLAY
|
||||||
|
|
||||||
case channelFeatures.OnlyContains(
|
case channelFeatures.OnlyContains(
|
||||||
lnwire.StaticRemoteKeyRequired,
|
lnwire.StaticRemoteKeyRequired,
|
||||||
):
|
):
|
||||||
|
|||||||
@ -226,28 +226,109 @@ const (
|
|||||||
// A tlv type definition used to serialize an outpoint's indexStatus
|
// A tlv type definition used to serialize an outpoint's indexStatus
|
||||||
// for use in the outpoint index.
|
// for use in the outpoint index.
|
||||||
indexStatusType tlv.Type = 0
|
indexStatusType tlv.Type = 0
|
||||||
|
|
||||||
// A tlv type definition used to serialize and deserialize a KeyLocator
|
|
||||||
// from the database.
|
|
||||||
keyLocType tlv.Type = 1
|
|
||||||
|
|
||||||
// A tlv type used to serialize and deserialize the
|
|
||||||
// `InitialLocalBalance` field.
|
|
||||||
initialLocalBalanceType tlv.Type = 2
|
|
||||||
|
|
||||||
// A tlv type used to serialize and deserialize the
|
|
||||||
// `InitialRemoteBalance` field.
|
|
||||||
initialRemoteBalanceType tlv.Type = 3
|
|
||||||
|
|
||||||
// A tlv type definition used to serialize and deserialize the
|
|
||||||
// confirmed ShortChannelID for a zero-conf channel.
|
|
||||||
realScidType tlv.Type = 4
|
|
||||||
|
|
||||||
// A tlv type definition used to serialize and deserialize the
|
|
||||||
// Memo for the channel channel.
|
|
||||||
channelMemoType tlv.Type = 5
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// openChannelTlvData houses the new data fields that are stored for each
|
||||||
|
// channel in a TLV stream within the root bucket. This is stored as a TLV
|
||||||
|
// stream appended to the existing hard-coded fields in the channel's root
|
||||||
|
// bucket. New fields being added to the channel state should be added here.
|
||||||
|
//
|
||||||
|
// NOTE: This struct is used for serialization purposes only and its fields
|
||||||
|
// should be accessed via the OpenChannel struct while in memory.
|
||||||
|
type openChannelTlvData struct {
|
||||||
|
// revokeKeyLoc is the key locator for the revocation key.
|
||||||
|
revokeKeyLoc tlv.RecordT[tlv.TlvType1, keyLocRecord]
|
||||||
|
|
||||||
|
// initialLocalBalance is the initial local balance of the channel.
|
||||||
|
initialLocalBalance tlv.RecordT[tlv.TlvType2, uint64]
|
||||||
|
|
||||||
|
// initialRemoteBalance is the initial remote balance of the channel.
|
||||||
|
initialRemoteBalance tlv.RecordT[tlv.TlvType3, uint64]
|
||||||
|
|
||||||
|
// realScid is the real short channel ID of the channel corresponding to
|
||||||
|
// the on-chain outpoint.
|
||||||
|
realScid tlv.RecordT[tlv.TlvType4, lnwire.ShortChannelID]
|
||||||
|
|
||||||
|
// memo is an optional text field that gives context to the user about
|
||||||
|
// the channel.
|
||||||
|
memo tlv.OptionalRecordT[tlv.TlvType5, []byte]
|
||||||
|
|
||||||
|
// tapscriptRoot is the optional Tapscript root the channel funding
|
||||||
|
// output commits to.
|
||||||
|
tapscriptRoot tlv.OptionalRecordT[tlv.TlvType6, [32]byte]
|
||||||
|
|
||||||
|
// customBlob is an optional TLV encoded blob of data representing
|
||||||
|
// custom channel funding information.
|
||||||
|
customBlob tlv.OptionalRecordT[tlv.TlvType7, tlv.Blob]
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode serializes the openChannelTlvData to the given io.Writer.
|
||||||
|
func (c *openChannelTlvData) encode(w io.Writer) error {
|
||||||
|
tlvRecords := []tlv.Record{
|
||||||
|
c.revokeKeyLoc.Record(),
|
||||||
|
c.initialLocalBalance.Record(),
|
||||||
|
c.initialRemoteBalance.Record(),
|
||||||
|
c.realScid.Record(),
|
||||||
|
}
|
||||||
|
c.memo.WhenSome(func(memo tlv.RecordT[tlv.TlvType5, []byte]) {
|
||||||
|
tlvRecords = append(tlvRecords, memo.Record())
|
||||||
|
})
|
||||||
|
c.tapscriptRoot.WhenSome(
|
||||||
|
func(root tlv.RecordT[tlv.TlvType6, [32]byte]) {
|
||||||
|
tlvRecords = append(tlvRecords, root.Record())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType7, tlv.Blob]) {
|
||||||
|
tlvRecords = append(tlvRecords, blob.Record())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the tlv stream.
|
||||||
|
tlvStream, err := tlv.NewStream(tlvRecords...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlvStream.Encode(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode deserializes the openChannelTlvData from the given io.Reader.
|
||||||
|
func (c *openChannelTlvData) decode(r io.Reader) error {
|
||||||
|
memo := c.memo.Zero()
|
||||||
|
tapscriptRoot := c.tapscriptRoot.Zero()
|
||||||
|
blob := c.customBlob.Zero()
|
||||||
|
|
||||||
|
// Create the tlv stream.
|
||||||
|
tlvStream, err := tlv.NewStream(
|
||||||
|
c.revokeKeyLoc.Record(),
|
||||||
|
c.initialLocalBalance.Record(),
|
||||||
|
c.initialRemoteBalance.Record(),
|
||||||
|
c.realScid.Record(),
|
||||||
|
memo.Record(),
|
||||||
|
tapscriptRoot.Record(),
|
||||||
|
blob.Record(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := tlvs[memo.TlvType()]; ok {
|
||||||
|
c.memo = tlv.SomeRecordT(memo)
|
||||||
|
}
|
||||||
|
if _, ok := tlvs[tapscriptRoot.TlvType()]; ok {
|
||||||
|
c.tapscriptRoot = tlv.SomeRecordT(tapscriptRoot)
|
||||||
|
}
|
||||||
|
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
|
||||||
|
c.customBlob = tlv.SomeRecordT(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// indexStatus is an enum-like type that describes what state the
|
// indexStatus is an enum-like type that describes what state the
|
||||||
// outpoint is in. Currently only two possible values.
|
// outpoint is in. Currently only two possible values.
|
||||||
type indexStatus uint8
|
type indexStatus uint8
|
||||||
@ -325,6 +406,11 @@ const (
|
|||||||
// SimpleTaprootFeatureBit indicates that the simple-taproot-chans
|
// SimpleTaprootFeatureBit indicates that the simple-taproot-chans
|
||||||
// feature bit was negotiated during the lifetime of the channel.
|
// feature bit was negotiated during the lifetime of the channel.
|
||||||
SimpleTaprootFeatureBit ChannelType = 1 << 10
|
SimpleTaprootFeatureBit ChannelType = 1 << 10
|
||||||
|
|
||||||
|
// TapscriptRootBit indicates that this is a MuSig2 channel with a top
|
||||||
|
// level tapscript commitment. This MUST be set along with the
|
||||||
|
// SimpleTaprootFeatureBit.
|
||||||
|
TapscriptRootBit ChannelType = 1 << 11
|
||||||
)
|
)
|
||||||
|
|
||||||
// IsSingleFunder returns true if the channel type if one of the known single
|
// IsSingleFunder returns true if the channel type if one of the known single
|
||||||
@ -395,6 +481,12 @@ func (c ChannelType) IsTaproot() bool {
|
|||||||
return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit
|
return c&SimpleTaprootFeatureBit == SimpleTaprootFeatureBit
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// HasTapscriptRoot returns true if the channel is using a top level tapscript
|
||||||
|
// root commitment.
|
||||||
|
func (c ChannelType) HasTapscriptRoot() bool {
|
||||||
|
return c&TapscriptRootBit == TapscriptRootBit
|
||||||
|
}
|
||||||
|
|
||||||
// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
|
// ChannelStateBounds are the parameters from OpenChannel and AcceptChannel
|
||||||
// that are responsible for providing bounds on the state space of the abstract
|
// that are responsible for providing bounds on the state space of the abstract
|
||||||
// channel state. These values must be remembered for normal channel operation
|
// channel state. These values must be remembered for normal channel operation
|
||||||
@ -496,6 +588,53 @@ type ChannelConfig struct {
|
|||||||
HtlcBasePoint keychain.KeyDescriptor
|
HtlcBasePoint keychain.KeyDescriptor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// commitTlvData stores all the optional data that may be stored as a TLV stream
|
||||||
|
// at the _end_ of the normal serialized commit on disk.
|
||||||
|
type commitTlvData struct {
|
||||||
|
// customBlob is a custom blob that may store extra data for custom
|
||||||
|
// channels.
|
||||||
|
customBlob tlv.OptionalRecordT[tlv.TlvType1, tlv.Blob]
|
||||||
|
}
|
||||||
|
|
||||||
|
// encode encodes the aux data into the passed io.Writer.
|
||||||
|
func (c *commitTlvData) encode(w io.Writer) error {
|
||||||
|
var tlvRecords []tlv.Record
|
||||||
|
c.customBlob.WhenSome(func(blob tlv.RecordT[tlv.TlvType1, tlv.Blob]) {
|
||||||
|
tlvRecords = append(tlvRecords, blob.Record())
|
||||||
|
})
|
||||||
|
|
||||||
|
// Create the tlv stream.
|
||||||
|
tlvStream, err := tlv.NewStream(tlvRecords...)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlvStream.Encode(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
// decode attempts to decode the aux data from the passed io.Reader.
|
||||||
|
func (c *commitTlvData) decode(r io.Reader) error {
|
||||||
|
blob := c.customBlob.Zero()
|
||||||
|
|
||||||
|
tlvStream, err := tlv.NewStream(
|
||||||
|
blob.Record(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
tlvs, err := tlvStream.DecodeWithParsedTypes(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, ok := tlvs[c.customBlob.TlvType()]; ok {
|
||||||
|
c.customBlob = tlv.SomeRecordT(blob)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// ChannelCommitment is a snapshot of the commitment state at a particular
|
// ChannelCommitment is a snapshot of the commitment state at a particular
|
||||||
// point in the commitment chain. With each state transition, a snapshot of the
|
// point in the commitment chain. With each state transition, a snapshot of the
|
||||||
// current state along with all non-settled HTLCs are recorded. These snapshots
|
// current state along with all non-settled HTLCs are recorded. These snapshots
|
||||||
@ -562,6 +701,11 @@ type ChannelCommitment struct {
|
|||||||
// able by us.
|
// able by us.
|
||||||
CommitTx *wire.MsgTx
|
CommitTx *wire.MsgTx
|
||||||
|
|
||||||
|
// CustomBlob is an optional blob that can be used to store information
|
||||||
|
// specific to a custom channel type. This may track some custom
|
||||||
|
// specific state for this given commitment.
|
||||||
|
CustomBlob fn.Option[tlv.Blob]
|
||||||
|
|
||||||
// CommitSig is one half of the signature required to fully complete
|
// CommitSig is one half of the signature required to fully complete
|
||||||
// the script for the commitment transaction above. This is the
|
// the script for the commitment transaction above. This is the
|
||||||
// signature signed by the remote party for our version of the
|
// signature signed by the remote party for our version of the
|
||||||
@ -571,9 +715,26 @@ type ChannelCommitment struct {
|
|||||||
// Htlcs is the set of HTLC's that are pending at this particular
|
// Htlcs is the set of HTLC's that are pending at this particular
|
||||||
// commitment height.
|
// commitment height.
|
||||||
Htlcs []HTLC
|
Htlcs []HTLC
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): pending commit pointer?
|
// amendTlvData updates the channel with the given auxiliary TLV data.
|
||||||
// * lets just walk through
|
func (c *ChannelCommitment) amendTlvData(auxData commitTlvData) {
|
||||||
|
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
|
||||||
|
c.CustomBlob = fn.Some(blob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTlvData creates a new commitTlvData from the given commitment.
|
||||||
|
func (c *ChannelCommitment) extractTlvData() commitTlvData {
|
||||||
|
var auxData commitTlvData
|
||||||
|
|
||||||
|
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
auxData.customBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType1](blob),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return auxData
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
|
// ChannelStatus is a bit vector used to indicate whether an OpenChannel is in
|
||||||
@ -867,6 +1028,16 @@ type OpenChannel struct {
|
|||||||
// channel that will be useful to our future selves.
|
// channel that will be useful to our future selves.
|
||||||
Memo []byte
|
Memo []byte
|
||||||
|
|
||||||
|
// TapscriptRoot is an optional tapscript root used to derive the MuSig2
|
||||||
|
// funding output.
|
||||||
|
TapscriptRoot fn.Option[chainhash.Hash]
|
||||||
|
|
||||||
|
// CustomBlob is an optional blob that can be used to store information
|
||||||
|
// specific to a custom channel type. This information is only created
|
||||||
|
// at channel funding time, and after wards is to be considered
|
||||||
|
// immutable.
|
||||||
|
CustomBlob fn.Option[tlv.Blob]
|
||||||
|
|
||||||
// TODO(roasbeef): eww
|
// TODO(roasbeef): eww
|
||||||
Db *ChannelStateDB
|
Db *ChannelStateDB
|
||||||
|
|
||||||
@ -1025,6 +1196,64 @@ func (c *OpenChannel) SetBroadcastHeight(height uint32) {
|
|||||||
c.FundingBroadcastHeight = height
|
c.FundingBroadcastHeight = height
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// amendTlvData updates the channel with the given auxiliary TLV data.
|
||||||
|
func (c *OpenChannel) amendTlvData(auxData openChannelTlvData) {
|
||||||
|
c.RevocationKeyLocator = auxData.revokeKeyLoc.Val.KeyLocator
|
||||||
|
c.InitialLocalBalance = lnwire.MilliSatoshi(
|
||||||
|
auxData.initialLocalBalance.Val,
|
||||||
|
)
|
||||||
|
c.InitialRemoteBalance = lnwire.MilliSatoshi(
|
||||||
|
auxData.initialRemoteBalance.Val,
|
||||||
|
)
|
||||||
|
c.confirmedScid = auxData.realScid.Val
|
||||||
|
|
||||||
|
auxData.memo.WhenSomeV(func(memo []byte) {
|
||||||
|
c.Memo = memo
|
||||||
|
})
|
||||||
|
auxData.tapscriptRoot.WhenSomeV(func(h [32]byte) {
|
||||||
|
c.TapscriptRoot = fn.Some[chainhash.Hash](h)
|
||||||
|
})
|
||||||
|
auxData.customBlob.WhenSomeV(func(blob tlv.Blob) {
|
||||||
|
c.CustomBlob = fn.Some(blob)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractTlvData creates a new openChannelTlvData from the given channel.
|
||||||
|
func (c *OpenChannel) extractTlvData() openChannelTlvData {
|
||||||
|
auxData := openChannelTlvData{
|
||||||
|
revokeKeyLoc: tlv.NewRecordT[tlv.TlvType1](
|
||||||
|
keyLocRecord{c.RevocationKeyLocator},
|
||||||
|
),
|
||||||
|
initialLocalBalance: tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||||
|
uint64(c.InitialLocalBalance),
|
||||||
|
),
|
||||||
|
initialRemoteBalance: tlv.NewPrimitiveRecord[tlv.TlvType3](
|
||||||
|
uint64(c.InitialRemoteBalance),
|
||||||
|
),
|
||||||
|
realScid: tlv.NewRecordT[tlv.TlvType4](
|
||||||
|
c.confirmedScid,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(c.Memo) != 0 {
|
||||||
|
auxData.memo = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType5](c.Memo),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
c.TapscriptRoot.WhenSome(func(h chainhash.Hash) {
|
||||||
|
auxData.tapscriptRoot = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType6, [32]byte](h),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
c.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
auxData.customBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType7](blob),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return auxData
|
||||||
|
}
|
||||||
|
|
||||||
// Refresh updates the in-memory channel state using the latest state observed
|
// Refresh updates the in-memory channel state using the latest state observed
|
||||||
// on disk.
|
// on disk.
|
||||||
func (c *OpenChannel) Refresh() error {
|
func (c *OpenChannel) Refresh() error {
|
||||||
@ -2351,6 +2580,12 @@ type HTLC struct {
|
|||||||
// HTLC. It is stored in the ExtraData field, which is used to store
|
// HTLC. It is stored in the ExtraData field, which is used to store
|
||||||
// a TLV stream of additional information associated with the HTLC.
|
// a TLV stream of additional information associated with the HTLC.
|
||||||
BlindingPoint lnwire.BlindingPointRecord
|
BlindingPoint lnwire.BlindingPointRecord
|
||||||
|
|
||||||
|
// CustomRecords is a set of custom TLV records that are associated with
|
||||||
|
// this HTLC. These records are used to store additional information
|
||||||
|
// about the HTLC that is not part of the standard HTLC fields. This
|
||||||
|
// field is encoded within the ExtraData field.
|
||||||
|
CustomRecords lnwire.CustomRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
// serializeExtraData encodes a TLV stream of extra data to be stored with a
|
// serializeExtraData encodes a TLV stream of extra data to be stored with a
|
||||||
@ -2369,6 +2604,11 @@ func (h *HTLC) serializeExtraData() error {
|
|||||||
records = append(records, &b)
|
records = append(records, &b)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
records, err := h.CustomRecords.ExtendRecordProducers(records)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return h.ExtraData.PackRecords(records...)
|
return h.ExtraData.PackRecords(records...)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2390,8 +2630,19 @@ func (h *HTLC) deserializeExtraData() error {
|
|||||||
|
|
||||||
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
|
if val, ok := tlvMap[h.BlindingPoint.TlvType()]; ok && val == nil {
|
||||||
h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
|
h.BlindingPoint = tlv.SomeRecordT(blindingPoint)
|
||||||
|
|
||||||
|
// Remove the entry from the TLV map. Anything left in the map
|
||||||
|
// will be included in the custom records field.
|
||||||
|
delete(tlvMap, h.BlindingPoint.TlvType())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Set the custom records field to the remaining TLV records.
|
||||||
|
customRecords, err := lnwire.NewCustomRecords(tlvMap)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
h.CustomRecords = customRecords
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2529,6 +2780,8 @@ func (h *HTLC) Copy() HTLC {
|
|||||||
copy(clone.Signature[:], h.Signature)
|
copy(clone.Signature[:], h.Signature)
|
||||||
copy(clone.RHash[:], h.RHash[:])
|
copy(clone.RHash[:], h.RHash[:])
|
||||||
copy(clone.ExtraData, h.ExtraData)
|
copy(clone.ExtraData, h.ExtraData)
|
||||||
|
clone.BlindingPoint = h.BlindingPoint
|
||||||
|
clone.CustomRecords = h.CustomRecords.Copy()
|
||||||
|
|
||||||
return clone
|
return clone
|
||||||
}
|
}
|
||||||
@ -2690,6 +2943,14 @@ func serializeCommitDiff(w io.Writer, diff *CommitDiff) error { // nolint: dupl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// We'll also encode the commit aux data stream here. We do this here
|
||||||
|
// rather than above (at the call to serializeChanCommit), to ensure
|
||||||
|
// backwards compat for reads to existing non-custom channels.
|
||||||
|
auxData := diff.Commitment.extractTlvData()
|
||||||
|
if err := auxData.encode(w); err != nil {
|
||||||
|
return fmt.Errorf("unable to write aux data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -2750,6 +3011,17 @@ func deserializeCommitDiff(r io.Reader) (*CommitDiff, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// As a final step, we'll read out any aux commit data that we have at
|
||||||
|
// the end of this byte stream. We do this here to ensure backward
|
||||||
|
// compatibility, as otherwise we risk erroneously reading into the
|
||||||
|
// wrong field.
|
||||||
|
var auxData commitTlvData
|
||||||
|
if err := auxData.decode(r); err != nil {
|
||||||
|
return nil, fmt.Errorf("unable to decode aux data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
d.Commitment.amendTlvData(auxData)
|
||||||
|
|
||||||
return &d, nil
|
return &d, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -3728,6 +4000,13 @@ func (c *OpenChannel) Snapshot() *ChannelSnapshot {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
localCommit.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
blobCopy := make([]byte, len(blob))
|
||||||
|
copy(blobCopy, blob)
|
||||||
|
|
||||||
|
snapshot.ChannelCommitment.CustomBlob = fn.Some(blobCopy)
|
||||||
|
})
|
||||||
|
|
||||||
// Copy over the current set of HTLCs to ensure the caller can't mutate
|
// Copy over the current set of HTLCs to ensure the caller can't mutate
|
||||||
// our internal state.
|
// our internal state.
|
||||||
snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs))
|
snapshot.Htlcs = make([]HTLC, len(localCommit.Htlcs))
|
||||||
@ -4030,32 +4309,9 @@ func putChanInfo(chanBucket kvdb.RwBucket, channel *OpenChannel) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert balance fields into uint64.
|
auxData := channel.extractTlvData()
|
||||||
localBalance := uint64(channel.InitialLocalBalance)
|
if err := auxData.encode(&w); err != nil {
|
||||||
remoteBalance := uint64(channel.InitialRemoteBalance)
|
return fmt.Errorf("unable to encode aux data: %w", err)
|
||||||
|
|
||||||
// Create the tlv stream.
|
|
||||||
tlvStream, err := tlv.NewStream(
|
|
||||||
// Write the RevocationKeyLocator as the first entry in a tlv
|
|
||||||
// stream.
|
|
||||||
MakeKeyLocRecord(
|
|
||||||
keyLocType, &channel.RevocationKeyLocator,
|
|
||||||
),
|
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
initialLocalBalanceType, &localBalance,
|
|
||||||
),
|
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
initialRemoteBalanceType, &remoteBalance,
|
|
||||||
),
|
|
||||||
MakeScidRecord(realScidType, &channel.confirmedScid),
|
|
||||||
tlv.MakePrimitiveRecord(channelMemoType, &channel.Memo),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := tlvStream.Encode(&w); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
|
if err := chanBucket.Put(chanInfoKey, w.Bytes()); err != nil {
|
||||||
@ -4142,6 +4398,12 @@ func putChanCommitment(chanBucket kvdb.RwBucket, c *ChannelCommitment,
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Before we write to disk, we'll also write our aux data as well.
|
||||||
|
auxData := c.extractTlvData()
|
||||||
|
if err := auxData.encode(&b); err != nil {
|
||||||
|
return fmt.Errorf("unable to write aux data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
return chanBucket.Put(commitKey, b.Bytes())
|
return chanBucket.Put(commitKey, b.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -4244,45 +4506,14 @@ func fetchChanInfo(chanBucket kvdb.RBucket, channel *OpenChannel) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Create balance fields in uint64, and Memo field as byte slice.
|
var auxData openChannelTlvData
|
||||||
var (
|
if err := auxData.decode(r); err != nil {
|
||||||
localBalance uint64
|
return fmt.Errorf("unable to decode aux data: %w", err)
|
||||||
remoteBalance uint64
|
|
||||||
memo []byte
|
|
||||||
)
|
|
||||||
|
|
||||||
// Create the tlv stream.
|
|
||||||
tlvStream, err := tlv.NewStream(
|
|
||||||
// Write the RevocationKeyLocator as the first entry in a tlv
|
|
||||||
// stream.
|
|
||||||
MakeKeyLocRecord(
|
|
||||||
keyLocType, &channel.RevocationKeyLocator,
|
|
||||||
),
|
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
initialLocalBalanceType, &localBalance,
|
|
||||||
),
|
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
initialRemoteBalanceType, &remoteBalance,
|
|
||||||
),
|
|
||||||
MakeScidRecord(realScidType, &channel.confirmedScid),
|
|
||||||
tlv.MakePrimitiveRecord(channelMemoType, &memo),
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := tlvStream.Decode(r); err != nil {
|
// Assign all the relevant fields from the aux data into the actual
|
||||||
return err
|
// open channel.
|
||||||
}
|
channel.amendTlvData(auxData)
|
||||||
|
|
||||||
// Attach the balance fields.
|
|
||||||
channel.InitialLocalBalance = lnwire.MilliSatoshi(localBalance)
|
|
||||||
channel.InitialRemoteBalance = lnwire.MilliSatoshi(remoteBalance)
|
|
||||||
|
|
||||||
// Attach the memo field if non-empty.
|
|
||||||
if len(memo) > 0 {
|
|
||||||
channel.Memo = memo
|
|
||||||
}
|
|
||||||
|
|
||||||
channel.Packager = NewChannelPackager(channel.ShortChannelID)
|
channel.Packager = NewChannelPackager(channel.ShortChannelID)
|
||||||
|
|
||||||
@ -4318,7 +4549,9 @@ func deserializeChanCommit(r io.Reader) (ChannelCommitment, error) {
|
|||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment, error) {
|
func fetchChanCommitment(chanBucket kvdb.RBucket,
|
||||||
|
local bool) (ChannelCommitment, error) {
|
||||||
|
|
||||||
var commitKey []byte
|
var commitKey []byte
|
||||||
if local {
|
if local {
|
||||||
commitKey = append(chanCommitmentKey, byte(0x00))
|
commitKey = append(chanCommitmentKey, byte(0x00))
|
||||||
@ -4332,7 +4565,23 @@ func fetchChanCommitment(chanBucket kvdb.RBucket, local bool) (ChannelCommitment
|
|||||||
}
|
}
|
||||||
|
|
||||||
r := bytes.NewReader(commitBytes)
|
r := bytes.NewReader(commitBytes)
|
||||||
return deserializeChanCommit(r)
|
chanCommit, err := deserializeChanCommit(r)
|
||||||
|
if err != nil {
|
||||||
|
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
|
||||||
|
"chan commit: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll also check to see if we have any aux data stored as the end of
|
||||||
|
// the stream.
|
||||||
|
var auxData commitTlvData
|
||||||
|
if err := auxData.decode(r); err != nil {
|
||||||
|
return ChannelCommitment{}, fmt.Errorf("unable to decode "+
|
||||||
|
"chan aux data: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
chanCommit.amendTlvData(auxData)
|
||||||
|
|
||||||
|
return chanCommit, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error {
|
func fetchChanCommitments(chanBucket kvdb.RBucket, channel *OpenChannel) error {
|
||||||
@ -4440,6 +4689,25 @@ func deleteThawHeight(chanBucket kvdb.RwBucket) error {
|
|||||||
return chanBucket.Delete(frozenChanKey)
|
return chanBucket.Delete(frozenChanKey)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// keyLocRecord is a wrapper struct around keychain.KeyLocator to implement the
|
||||||
|
// tlv.RecordProducer interface.
|
||||||
|
type keyLocRecord struct {
|
||||||
|
keychain.KeyLocator
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record creates a Record out of a KeyLocator using the passed Type and the
|
||||||
|
// EKeyLocator and DKeyLocator functions. The size will always be 8 as
|
||||||
|
// KeyFamily is uint32 and the Index is uint32.
|
||||||
|
//
|
||||||
|
// NOTE: This is part of the tlv.RecordProducer interface.
|
||||||
|
func (k *keyLocRecord) Record() tlv.Record {
|
||||||
|
// Note that we set the type here as zero, as when used with a
|
||||||
|
// tlv.RecordT, the type param will be used as the type.
|
||||||
|
return tlv.MakeStaticRecord(
|
||||||
|
0, &k.KeyLocator, 8, EKeyLocator, DKeyLocator,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// EKeyLocator is an encoder for keychain.KeyLocator.
|
// EKeyLocator is an encoder for keychain.KeyLocator.
|
||||||
func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error {
|
func EKeyLocator(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||||
if v, ok := val.(*keychain.KeyLocator); ok {
|
if v, ok := val.(*keychain.KeyLocator); ok {
|
||||||
@ -4468,22 +4736,6 @@ func DKeyLocator(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
|
|||||||
return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8)
|
return tlv.NewTypeForDecodingErr(val, "keychain.KeyLocator", l, 8)
|
||||||
}
|
}
|
||||||
|
|
||||||
// MakeKeyLocRecord creates a Record out of a KeyLocator using the passed
|
|
||||||
// Type and the EKeyLocator and DKeyLocator functions. The size will always be
|
|
||||||
// 8 as KeyFamily is uint32 and the Index is uint32.
|
|
||||||
func MakeKeyLocRecord(typ tlv.Type, keyLoc *keychain.KeyLocator) tlv.Record {
|
|
||||||
return tlv.MakeStaticRecord(typ, keyLoc, 8, EKeyLocator, DKeyLocator)
|
|
||||||
}
|
|
||||||
|
|
||||||
// MakeScidRecord creates a Record out of a ShortChannelID using the passed
|
|
||||||
// Type and the EShortChannelID and DShortChannelID functions. The size will
|
|
||||||
// always be 8 for the ShortChannelID.
|
|
||||||
func MakeScidRecord(typ tlv.Type, scid *lnwire.ShortChannelID) tlv.Record {
|
|
||||||
return tlv.MakeStaticRecord(
|
|
||||||
typ, scid, 8, lnwire.EShortChannelID, lnwire.DShortChannelID,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ShutdownInfo contains various info about the shutdown initiation of a
|
// ShutdownInfo contains various info about the shutdown initiation of a
|
||||||
// channel.
|
// channel.
|
||||||
type ShutdownInfo struct {
|
type ShutdownInfo struct {
|
||||||
|
|||||||
@ -17,6 +17,7 @@ import (
|
|||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
"github.com/lightningnetwork/lnd/clock"
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lnmock"
|
"github.com/lightningnetwork/lnd/lnmock"
|
||||||
@ -173,7 +174,7 @@ func fundingPointOption(chanPoint wire.OutPoint) testChannelOption {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// channelIDOption is an option which sets the short channel ID of the channel.
|
// channelIDOption is an option which sets the short channel ID of the channel.
|
||||||
var channelIDOption = func(chanID lnwire.ShortChannelID) testChannelOption {
|
func channelIDOption(chanID lnwire.ShortChannelID) testChannelOption {
|
||||||
return func(params *testChannelParams) {
|
return func(params *testChannelParams) {
|
||||||
params.channel.ShortChannelID = chanID
|
params.channel.ShortChannelID = chanID
|
||||||
}
|
}
|
||||||
@ -326,6 +327,9 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
|||||||
uniqueOutputIndex.Add(1)
|
uniqueOutputIndex.Add(1)
|
||||||
op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()}
|
op := wire.OutPoint{Hash: key, Index: uniqueOutputIndex.Load()}
|
||||||
|
|
||||||
|
var tapscriptRoot chainhash.Hash
|
||||||
|
copy(tapscriptRoot[:], bytes.Repeat([]byte{1}, 32))
|
||||||
|
|
||||||
return &OpenChannel{
|
return &OpenChannel{
|
||||||
ChanType: SingleFunderBit | FrozenBit,
|
ChanType: SingleFunderBit | FrozenBit,
|
||||||
ChainHash: key,
|
ChainHash: key,
|
||||||
@ -347,6 +351,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
|||||||
FeePerKw: btcutil.Amount(5000),
|
FeePerKw: btcutil.Amount(5000),
|
||||||
CommitTx: channels.TestFundingTx,
|
CommitTx: channels.TestFundingTx,
|
||||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||||
|
CustomBlob: fn.Some([]byte{1, 2, 3}),
|
||||||
},
|
},
|
||||||
RemoteCommitment: ChannelCommitment{
|
RemoteCommitment: ChannelCommitment{
|
||||||
CommitHeight: 0,
|
CommitHeight: 0,
|
||||||
@ -356,6 +361,7 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
|||||||
FeePerKw: btcutil.Amount(5000),
|
FeePerKw: btcutil.Amount(5000),
|
||||||
CommitTx: channels.TestFundingTx,
|
CommitTx: channels.TestFundingTx,
|
||||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||||
|
CustomBlob: fn.Some([]byte{4, 5, 6}),
|
||||||
},
|
},
|
||||||
NumConfsRequired: 4,
|
NumConfsRequired: 4,
|
||||||
RemoteCurrentRevocation: privKey.PubKey(),
|
RemoteCurrentRevocation: privKey.PubKey(),
|
||||||
@ -368,6 +374,9 @@ func createTestChannelState(t *testing.T, cdb *ChannelStateDB) *OpenChannel {
|
|||||||
ThawHeight: uint32(defaultPendingHeight),
|
ThawHeight: uint32(defaultPendingHeight),
|
||||||
InitialLocalBalance: lnwire.MilliSatoshi(9000),
|
InitialLocalBalance: lnwire.MilliSatoshi(9000),
|
||||||
InitialRemoteBalance: lnwire.MilliSatoshi(3000),
|
InitialRemoteBalance: lnwire.MilliSatoshi(3000),
|
||||||
|
Memo: []byte("test"),
|
||||||
|
TapscriptRoot: fn.Some(tapscriptRoot),
|
||||||
|
CustomBlob: fn.Some([]byte{1, 2, 3}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -575,24 +584,32 @@ func assertCommitmentEqual(t *testing.T, a, b *ChannelCommitment) {
|
|||||||
func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment,
|
func assertRevocationLogEntryEqual(t *testing.T, c *ChannelCommitment,
|
||||||
r *RevocationLog) {
|
r *RevocationLog) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
// Check the common fields.
|
// Check the common fields.
|
||||||
require.EqualValues(
|
require.EqualValues(
|
||||||
t, r.CommitTxHash, c.CommitTx.TxHash(), "CommitTx mismatch",
|
t, r.CommitTxHash.Val, c.CommitTx.TxHash(), "CommitTx mismatch",
|
||||||
)
|
)
|
||||||
|
|
||||||
// Now check the common fields from the HTLCs.
|
// Now check the common fields from the HTLCs.
|
||||||
require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch")
|
require.Equal(t, len(r.HTLCEntries), len(c.Htlcs), "HTLCs len mismatch")
|
||||||
for i, rHtlc := range r.HTLCEntries {
|
for i, rHtlc := range r.HTLCEntries {
|
||||||
cHtlc := c.Htlcs[i]
|
cHtlc := c.Htlcs[i]
|
||||||
require.Equal(t, rHtlc.RHash, cHtlc.RHash, "RHash mismatch")
|
require.Equal(t, rHtlc.RHash.Val[:], cHtlc.RHash[:], "RHash")
|
||||||
require.Equal(t, rHtlc.Amt, cHtlc.Amt.ToSatoshis(),
|
require.Equal(
|
||||||
"Amt mismatch")
|
t, rHtlc.Amt.Val.Int(), cHtlc.Amt.ToSatoshis(), "Amt",
|
||||||
require.Equal(t, rHtlc.RefundTimeout, cHtlc.RefundTimeout,
|
)
|
||||||
"RefundTimeout mismatch")
|
require.Equal(
|
||||||
require.EqualValues(t, rHtlc.OutputIndex, cHtlc.OutputIndex,
|
t, rHtlc.RefundTimeout.Val, cHtlc.RefundTimeout,
|
||||||
"OutputIndex mismatch")
|
"RefundTimeout",
|
||||||
require.Equal(t, rHtlc.Incoming, cHtlc.Incoming,
|
)
|
||||||
"Incoming mismatch")
|
require.EqualValues(
|
||||||
|
t, rHtlc.OutputIndex.Val, cHtlc.OutputIndex,
|
||||||
|
"OutputIndex",
|
||||||
|
)
|
||||||
|
require.Equal(
|
||||||
|
t, rHtlc.Incoming.Val, cHtlc.Incoming, "Incoming",
|
||||||
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,6 +674,7 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
CommitTx: newTx,
|
CommitTx: newTx,
|
||||||
CommitSig: newSig,
|
CommitSig: newSig,
|
||||||
Htlcs: htlcs,
|
Htlcs: htlcs,
|
||||||
|
CustomBlob: fn.Some([]byte{4, 5, 6}),
|
||||||
}
|
}
|
||||||
|
|
||||||
// First update the local node's broadcastable state and also add a
|
// First update the local node's broadcastable state and also add a
|
||||||
@ -694,9 +712,14 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
// have been updated.
|
// have been updated.
|
||||||
updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub)
|
updatedChannel, err := cdb.FetchOpenChannels(channel.IdentityPub)
|
||||||
require.NoError(t, err, "unable to fetch updated channel")
|
require.NoError(t, err, "unable to fetch updated channel")
|
||||||
assertCommitmentEqual(t, &commitment, &updatedChannel[0].LocalCommitment)
|
|
||||||
|
assertCommitmentEqual(
|
||||||
|
t, &commitment, &updatedChannel[0].LocalCommitment,
|
||||||
|
)
|
||||||
|
|
||||||
numDiskUpdates, err := updatedChannel[0].CommitmentHeight()
|
numDiskUpdates, err := updatedChannel[0].CommitmentHeight()
|
||||||
require.NoError(t, err, "unable to read commitment height from disk")
|
require.NoError(t, err, "unable to read commitment height from disk")
|
||||||
|
|
||||||
if numDiskUpdates != uint64(commitment.CommitHeight) {
|
if numDiskUpdates != uint64(commitment.CommitHeight) {
|
||||||
t.Fatalf("num disk updates doesn't match: %v vs %v",
|
t.Fatalf("num disk updates doesn't match: %v vs %v",
|
||||||
numDiskUpdates, commitment.CommitHeight)
|
numDiskUpdates, commitment.CommitHeight)
|
||||||
@ -799,10 +822,10 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
|
|
||||||
// Check the output indexes are saved as expected.
|
// Check the output indexes are saved as expected.
|
||||||
require.EqualValues(
|
require.EqualValues(
|
||||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
|
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
|
||||||
)
|
)
|
||||||
require.EqualValues(
|
require.EqualValues(
|
||||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
|
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
|
||||||
)
|
)
|
||||||
|
|
||||||
// The two deltas (the original vs the on-disk version) should
|
// The two deltas (the original vs the on-disk version) should
|
||||||
@ -844,10 +867,10 @@ func TestChannelStateTransition(t *testing.T) {
|
|||||||
|
|
||||||
// Check the output indexes are saved as expected.
|
// Check the output indexes are saved as expected.
|
||||||
require.EqualValues(
|
require.EqualValues(
|
||||||
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex,
|
t, dummyLocalOutputIndex, diskPrevCommit.OurOutputIndex.Val,
|
||||||
)
|
)
|
||||||
require.EqualValues(
|
require.EqualValues(
|
||||||
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex,
|
t, dummyRemoteOutIndex, diskPrevCommit.TheirOutputIndex.Val,
|
||||||
)
|
)
|
||||||
|
|
||||||
assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit)
|
assertRevocationLogEntryEqual(t, &oldRemoteCommit, prevCommit)
|
||||||
@ -1642,6 +1665,24 @@ func TestHTLCsExtraData(t *testing.T) {
|
|||||||
),
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Custom channel data htlc with a blinding point.
|
||||||
|
customDataHTLC := HTLC{
|
||||||
|
Signature: testSig.Serialize(),
|
||||||
|
Incoming: false,
|
||||||
|
Amt: 10,
|
||||||
|
RHash: key,
|
||||||
|
RefundTimeout: 1,
|
||||||
|
OnionBlob: lnmock.MockOnion(),
|
||||||
|
BlindingPoint: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[lnwire.BlindingPointTlvType](
|
||||||
|
pubKey,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
CustomRecords: map[uint64][]byte{
|
||||||
|
uint64(lnwire.MinCustomRecordsTlvType + 3): {1, 2, 3},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
testCases := []struct {
|
||||||
name string
|
name string
|
||||||
htlcs []HTLC
|
htlcs []HTLC
|
||||||
@ -1663,6 +1704,7 @@ func TestHTLCsExtraData(t *testing.T) {
|
|||||||
mockHtlc,
|
mockHtlc,
|
||||||
blindingPointHTLC,
|
blindingPointHTLC,
|
||||||
mockHtlc,
|
mockHtlc,
|
||||||
|
customDataHTLC,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|||||||
@ -43,6 +43,10 @@ var (
|
|||||||
// created.
|
// created.
|
||||||
ErrMetaNotFound = fmt.Errorf("unable to locate meta information")
|
ErrMetaNotFound = fmt.Errorf("unable to locate meta information")
|
||||||
|
|
||||||
|
// ErrClosedScidsNotFound is returned when the closed scid bucket
|
||||||
|
// hasn't been created.
|
||||||
|
ErrClosedScidsNotFound = fmt.Errorf("closed scid bucket doesn't exist")
|
||||||
|
|
||||||
// ErrGraphNotFound is returned when at least one of the components of
|
// ErrGraphNotFound is returned when at least one of the components of
|
||||||
// graph doesn't exist.
|
// graph doesn't exist.
|
||||||
ErrGraphNotFound = fmt.Errorf("graph bucket not initialized")
|
ErrGraphNotFound = fmt.Errorf("graph bucket not initialized")
|
||||||
|
|||||||
@ -286,6 +286,27 @@ func NewFwdPkg(source lnwire.ShortChannelID, height uint64,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// SourceRef is a convenience method that returns an AddRef to this forwarding
|
||||||
|
// package for the index in the argument. It is the caller's responsibility
|
||||||
|
// to ensure that the index is in bounds.
|
||||||
|
func (f *FwdPkg) SourceRef(i uint16) AddRef {
|
||||||
|
return AddRef{
|
||||||
|
Height: f.Height,
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DestRef is a convenience method that returns a SettleFailRef to this
|
||||||
|
// forwarding package for the index in the argument. It is the caller's
|
||||||
|
// responsibility to ensure that the index is in bounds.
|
||||||
|
func (f *FwdPkg) DestRef(i uint16) SettleFailRef {
|
||||||
|
return SettleFailRef{
|
||||||
|
Source: f.Source,
|
||||||
|
Height: f.Height,
|
||||||
|
Index: i,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ID returns an unique identifier for this package, used to ensure that sphinx
|
// ID returns an unique identifier for this package, used to ensure that sphinx
|
||||||
// replay processing of this batch is idempotent.
|
// replay processing of this batch is idempotent.
|
||||||
func (f *FwdPkg) ID() []byte {
|
func (f *FwdPkg) ID() []byte {
|
||||||
|
|||||||
@ -153,6 +153,14 @@ var (
|
|||||||
// case we'll remove all entries from the prune log with a block height
|
// case we'll remove all entries from the prune log with a block height
|
||||||
// that no longer exists.
|
// that no longer exists.
|
||||||
pruneLogBucket = []byte("prune-log")
|
pruneLogBucket = []byte("prune-log")
|
||||||
|
|
||||||
|
// closedScidBucket is a top-level bucket that stores scids for
|
||||||
|
// channels that we know to be closed. This is used so that we don't
|
||||||
|
// need to perform expensive validation checks if we receive a channel
|
||||||
|
// announcement for the channel again.
|
||||||
|
//
|
||||||
|
// maps: scid -> []byte{}
|
||||||
|
closedScidBucket = []byte("closed-scid")
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -318,6 +326,7 @@ var graphTopLevelBuckets = [][]byte{
|
|||||||
nodeBucket,
|
nodeBucket,
|
||||||
edgeBucket,
|
edgeBucket,
|
||||||
graphMetaBucket,
|
graphMetaBucket,
|
||||||
|
closedScidBucket,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Wipe completely deletes all saved state within all used buckets within the
|
// Wipe completely deletes all saved state within all used buckets within the
|
||||||
@ -2143,6 +2152,21 @@ func (c *ChannelGraph) FilterKnownChanIDs(chansInfo []ChannelUpdateInfo,
|
|||||||
zombieIndex, scid,
|
zombieIndex, scid,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(ziggie): Make sure that for the strict
|
||||||
|
// pruning case we compare the pubkeys and
|
||||||
|
// whether the right timestamp is not older than
|
||||||
|
// the `ChannelPruneExpiry`.
|
||||||
|
//
|
||||||
|
// NOTE: The timestamp data has no verification
|
||||||
|
// attached to it in the `ReplyChannelRange` msg
|
||||||
|
// so we are trusting this data at this point.
|
||||||
|
// However it is not critical because we are
|
||||||
|
// just removing the channel from the db when
|
||||||
|
// the timestamps are more recent. During the
|
||||||
|
// querying of the gossip msg verification
|
||||||
|
// happens as usual.
|
||||||
|
// However we should start punishing peers when
|
||||||
|
// they don't provide us honest data ?
|
||||||
isStillZombie := isZombieChan(
|
isStillZombie := isZombieChan(
|
||||||
info.Node1UpdateTimestamp,
|
info.Node1UpdateTimestamp,
|
||||||
info.Node2UpdateTimestamp,
|
info.Node2UpdateTimestamp,
|
||||||
@ -2211,6 +2235,29 @@ type ChannelUpdateInfo struct {
|
|||||||
Node2UpdateTimestamp time.Time
|
Node2UpdateTimestamp time.Time
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// NewChannelUpdateInfo is a constructor which makes sure we initialize the
|
||||||
|
// timestamps with zero seconds unix timestamp which equals
|
||||||
|
// `January 1, 1970, 00:00:00 UTC` in case the value is `time.Time{}`.
|
||||||
|
func NewChannelUpdateInfo(scid lnwire.ShortChannelID, node1Timestamp,
|
||||||
|
node2Timestamp time.Time) ChannelUpdateInfo {
|
||||||
|
|
||||||
|
chanInfo := ChannelUpdateInfo{
|
||||||
|
ShortChannelID: scid,
|
||||||
|
Node1UpdateTimestamp: node1Timestamp,
|
||||||
|
Node2UpdateTimestamp: node2Timestamp,
|
||||||
|
}
|
||||||
|
|
||||||
|
if node1Timestamp.IsZero() {
|
||||||
|
chanInfo.Node1UpdateTimestamp = time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
if node2Timestamp.IsZero() {
|
||||||
|
chanInfo.Node2UpdateTimestamp = time.Unix(0, 0)
|
||||||
|
}
|
||||||
|
|
||||||
|
return chanInfo
|
||||||
|
}
|
||||||
|
|
||||||
// BlockChannelRange represents a range of channels for a given block height.
|
// BlockChannelRange represents a range of channels for a given block height.
|
||||||
type BlockChannelRange struct {
|
type BlockChannelRange struct {
|
||||||
// Height is the height of the block all of the channels below were
|
// Height is the height of the block all of the channels below were
|
||||||
@ -2284,9 +2331,9 @@ func (c *ChannelGraph) FilterChannelRange(startHeight,
|
|||||||
rawCid := byteOrder.Uint64(k)
|
rawCid := byteOrder.Uint64(k)
|
||||||
cid := lnwire.NewShortChanIDFromInt(rawCid)
|
cid := lnwire.NewShortChanIDFromInt(rawCid)
|
||||||
|
|
||||||
chanInfo := ChannelUpdateInfo{
|
chanInfo := NewChannelUpdateInfo(
|
||||||
ShortChannelID: cid,
|
cid, time.Time{}, time.Time{},
|
||||||
}
|
)
|
||||||
|
|
||||||
if !withTimestamps {
|
if !withTimestamps {
|
||||||
channelsPerBlock[cid.BlockHeight] = append(
|
channelsPerBlock[cid.BlockHeight] = append(
|
||||||
@ -3846,6 +3893,53 @@ func (c *ChannelGraph) NumZombies() (uint64, error) {
|
|||||||
return numZombies, nil
|
return numZombies, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// PutClosedScid stores a SCID for a closed channel in the database. This is so
|
||||||
|
// that we can ignore channel announcements that we know to be closed without
|
||||||
|
// having to validate them and fetch a block.
|
||||||
|
func (c *ChannelGraph) PutClosedScid(scid lnwire.ShortChannelID) error {
|
||||||
|
return kvdb.Update(c.db, func(tx kvdb.RwTx) error {
|
||||||
|
closedScids, err := tx.CreateTopLevelBucket(closedScidBucket)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var k [8]byte
|
||||||
|
byteOrder.PutUint64(k[:], scid.ToUint64())
|
||||||
|
|
||||||
|
return closedScids.Put(k[:], []byte{})
|
||||||
|
}, func() {})
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsClosedScid checks whether a channel identified by the passed in scid is
|
||||||
|
// closed. This helps avoid having to perform expensive validation checks.
|
||||||
|
// TODO: Add an LRU cache to cut down on disc reads.
|
||||||
|
func (c *ChannelGraph) IsClosedScid(scid lnwire.ShortChannelID) (bool, error) {
|
||||||
|
var isClosed bool
|
||||||
|
err := kvdb.View(c.db, func(tx kvdb.RTx) error {
|
||||||
|
closedScids := tx.ReadBucket(closedScidBucket)
|
||||||
|
if closedScids == nil {
|
||||||
|
return ErrClosedScidsNotFound
|
||||||
|
}
|
||||||
|
|
||||||
|
var k [8]byte
|
||||||
|
byteOrder.PutUint64(k[:], scid.ToUint64())
|
||||||
|
|
||||||
|
if closedScids.Get(k[:]) != nil {
|
||||||
|
isClosed = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, func() {
|
||||||
|
isClosed = false
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return isClosed, nil
|
||||||
|
}
|
||||||
|
|
||||||
func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // nolint:dupl
|
func putLightningNode(nodeBucket kvdb.RwBucket, aliasBucket kvdb.RwBucket, // nolint:dupl
|
||||||
updateIndex kvdb.RwBucket, node *LightningNode) error {
|
updateIndex kvdb.RwBucket, node *LightningNode) error {
|
||||||
|
|
||||||
|
|||||||
@ -1980,9 +1980,9 @@ func TestFilterKnownChanIDs(t *testing.T) {
|
|||||||
t.Fatalf("unable to create channel edge: %v", err)
|
t.Fatalf("unable to create channel edge: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
chanIDs = append(chanIDs, ChannelUpdateInfo{
|
chanIDs = append(chanIDs, NewChannelUpdateInfo(
|
||||||
ShortChannelID: chanID,
|
chanID, time.Time{}, time.Time{},
|
||||||
})
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
const numZombies = 5
|
const numZombies = 5
|
||||||
@ -2024,20 +2024,28 @@ func TestFilterKnownChanIDs(t *testing.T) {
|
|||||||
// should get the same set back.
|
// should get the same set back.
|
||||||
{
|
{
|
||||||
queryIDs: []ChannelUpdateInfo{
|
queryIDs: []ChannelUpdateInfo{
|
||||||
{ShortChannelID: lnwire.ShortChannelID{
|
{
|
||||||
BlockHeight: 99,
|
ShortChannelID: lnwire.ShortChannelID{
|
||||||
}},
|
BlockHeight: 99,
|
||||||
{ShortChannelID: lnwire.ShortChannelID{
|
},
|
||||||
BlockHeight: 100,
|
},
|
||||||
}},
|
{
|
||||||
|
ShortChannelID: lnwire.ShortChannelID{
|
||||||
|
BlockHeight: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
resp: []ChannelUpdateInfo{
|
resp: []ChannelUpdateInfo{
|
||||||
{ShortChannelID: lnwire.ShortChannelID{
|
{
|
||||||
BlockHeight: 99,
|
ShortChannelID: lnwire.ShortChannelID{
|
||||||
}},
|
BlockHeight: 99,
|
||||||
{ShortChannelID: lnwire.ShortChannelID{
|
},
|
||||||
BlockHeight: 100,
|
},
|
||||||
}},
|
{
|
||||||
|
ShortChannelID: lnwire.ShortChannelID{
|
||||||
|
BlockHeight: 100,
|
||||||
|
},
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
||||||
@ -2374,7 +2382,7 @@ func TestStressTestChannelGraphAPI(t *testing.T) {
|
|||||||
methodsMu.Unlock()
|
methodsMu.Unlock()
|
||||||
|
|
||||||
err := fn()
|
err := fn()
|
||||||
require.NoErrorf(t, err, fmt.Sprintf(name))
|
require.NoErrorf(t, err, name)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -2419,7 +2427,7 @@ func TestFilterChannelRange(t *testing.T) {
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
updateTimeSeed := int64(1)
|
updateTimeSeed := time.Now().Unix()
|
||||||
maybeAddPolicy := func(chanID uint64, node *LightningNode,
|
maybeAddPolicy := func(chanID uint64, node *LightningNode,
|
||||||
node2 bool) time.Time {
|
node2 bool) time.Time {
|
||||||
|
|
||||||
@ -2428,7 +2436,7 @@ func TestFilterChannelRange(t *testing.T) {
|
|||||||
chanFlags = lnwire.ChanUpdateDirection
|
chanFlags = lnwire.ChanUpdateDirection
|
||||||
}
|
}
|
||||||
|
|
||||||
var updateTime time.Time
|
var updateTime = time.Unix(0, 0)
|
||||||
if rand.Int31n(2) == 0 {
|
if rand.Int31n(2) == 0 {
|
||||||
updateTime = time.Unix(updateTimeSeed, 0)
|
updateTime = time.Unix(updateTimeSeed, 0)
|
||||||
err = graph.UpdateEdgePolicy(&models.ChannelEdgePolicy{
|
err = graph.UpdateEdgePolicy(&models.ChannelEdgePolicy{
|
||||||
@ -2456,11 +2464,16 @@ func TestFilterChannelRange(t *testing.T) {
|
|||||||
)
|
)
|
||||||
require.NoError(t, graph.AddChannelEdge(&channel2))
|
require.NoError(t, graph.AddChannelEdge(&channel2))
|
||||||
|
|
||||||
|
chanInfo1 := NewChannelUpdateInfo(
|
||||||
|
chanID1, time.Time{}, time.Time{},
|
||||||
|
)
|
||||||
|
chanInfo2 := NewChannelUpdateInfo(
|
||||||
|
chanID2, time.Time{}, time.Time{},
|
||||||
|
)
|
||||||
channelRanges = append(channelRanges, BlockChannelRange{
|
channelRanges = append(channelRanges, BlockChannelRange{
|
||||||
Height: chanHeight,
|
Height: chanHeight,
|
||||||
Channels: []ChannelUpdateInfo{
|
Channels: []ChannelUpdateInfo{
|
||||||
{ShortChannelID: chanID1},
|
chanInfo1, chanInfo2,
|
||||||
{ShortChannelID: chanID2},
|
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -2471,20 +2484,17 @@ func TestFilterChannelRange(t *testing.T) {
|
|||||||
time4 = maybeAddPolicy(channel2.ChannelID, node2, true)
|
time4 = maybeAddPolicy(channel2.ChannelID, node2, true)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
chanInfo1 = NewChannelUpdateInfo(
|
||||||
|
chanID1, time1, time2,
|
||||||
|
)
|
||||||
|
chanInfo2 = NewChannelUpdateInfo(
|
||||||
|
chanID2, time3, time4,
|
||||||
|
)
|
||||||
channelRangesWithTimestamps = append(
|
channelRangesWithTimestamps = append(
|
||||||
channelRangesWithTimestamps, BlockChannelRange{
|
channelRangesWithTimestamps, BlockChannelRange{
|
||||||
Height: chanHeight,
|
Height: chanHeight,
|
||||||
Channels: []ChannelUpdateInfo{
|
Channels: []ChannelUpdateInfo{
|
||||||
{
|
chanInfo1, chanInfo2,
|
||||||
ShortChannelID: chanID1,
|
|
||||||
Node1UpdateTimestamp: time1,
|
|
||||||
Node2UpdateTimestamp: time2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
ShortChannelID: chanID2,
|
|
||||||
Node1UpdateTimestamp: time3,
|
|
||||||
Node2UpdateTimestamp: time4,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
@ -4027,3 +4037,28 @@ func TestGraphLoading(t *testing.T) {
|
|||||||
graphReloaded.graphCache.nodeFeatures,
|
graphReloaded.graphCache.nodeFeatures,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestClosedScid tests that we can correctly insert a SCID into the index of
|
||||||
|
// closed short channel ids.
|
||||||
|
func TestClosedScid(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
graph, err := MakeTestGraph(t)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
scid := lnwire.ShortChannelID{}
|
||||||
|
|
||||||
|
// The scid should not exist in the closedScidBucket.
|
||||||
|
exists, err := graph.IsClosedScid(scid)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.False(t, exists)
|
||||||
|
|
||||||
|
// After we call PutClosedScid, the call to IsClosedScid should return
|
||||||
|
// true.
|
||||||
|
err = graph.PutClosedScid(scid)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
exists, err = graph.IsClosedScid(scid)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.True(t, exists)
|
||||||
|
}
|
||||||
|
|||||||
@ -269,7 +269,9 @@ func (d *DB) InvoicesAddedSince(_ context.Context, sinceAddIndex uint64) (
|
|||||||
|
|
||||||
// For each key found, we'll look up the actual
|
// For each key found, we'll look up the actual
|
||||||
// invoice, then accumulate it into our return value.
|
// invoice, then accumulate it into our return value.
|
||||||
invoice, err := fetchInvoice(invoiceKey, invoices)
|
invoice, err := fetchInvoice(
|
||||||
|
invoiceKey, invoices, nil, false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -341,7 +343,9 @@ func (d *DB) LookupInvoice(_ context.Context, ref invpkg.InvoiceRef) (
|
|||||||
|
|
||||||
// An invoice was found, retrieve the remainder of the invoice
|
// An invoice was found, retrieve the remainder of the invoice
|
||||||
// body.
|
// body.
|
||||||
i, err := fetchInvoice(invoiceNum, invoices, setID)
|
i, err := fetchInvoice(
|
||||||
|
invoiceNum, invoices, []*invpkg.SetID{setID}, true,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -468,7 +472,7 @@ func (d *DB) FetchPendingInvoices(_ context.Context) (
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice, err := fetchInvoice(v, invoices)
|
invoice, err := fetchInvoice(v, invoices, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -526,7 +530,9 @@ func (d *DB) QueryInvoices(_ context.Context, q invpkg.InvoiceQuery) (
|
|||||||
// characteristics for our query and returns the number of items
|
// characteristics for our query and returns the number of items
|
||||||
// we have added to our set of invoices.
|
// we have added to our set of invoices.
|
||||||
accumulateInvoices := func(_, indexValue []byte) (bool, error) {
|
accumulateInvoices := func(_, indexValue []byte) (bool, error) {
|
||||||
invoice, err := fetchInvoice(indexValue, invoices)
|
invoice, err := fetchInvoice(
|
||||||
|
indexValue, invoices, nil, false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -654,7 +660,9 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
|
|||||||
if setIDHint != nil {
|
if setIDHint != nil {
|
||||||
invSetID = *setIDHint
|
invSetID = *setIDHint
|
||||||
}
|
}
|
||||||
invoice, err := fetchInvoice(invoiceNum, invoices, &invSetID)
|
invoice, err := fetchInvoice(
|
||||||
|
invoiceNum, invoices, []*invpkg.SetID{&invSetID}, false,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -676,8 +684,17 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
|
|||||||
updatedInvoice, err = invpkg.UpdateInvoice(
|
updatedInvoice, err = invpkg.UpdateInvoice(
|
||||||
payHash, updater.invoice, now, callback, updater,
|
payHash, updater.invoice, now, callback, updater,
|
||||||
)
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return err
|
// If this is an AMP update, then limit the returned AMP state
|
||||||
|
// to only the requested set ID.
|
||||||
|
if setIDHint != nil {
|
||||||
|
filterInvoiceAMPState(updatedInvoice, &invSetID)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}, func() {
|
}, func() {
|
||||||
updatedInvoice = nil
|
updatedInvoice = nil
|
||||||
})
|
})
|
||||||
@ -685,6 +702,25 @@ func (d *DB) UpdateInvoice(_ context.Context, ref invpkg.InvoiceRef,
|
|||||||
return updatedInvoice, err
|
return updatedInvoice, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterInvoiceAMPState filters the AMP state of the invoice to only include
|
||||||
|
// state for the specified set IDs.
|
||||||
|
func filterInvoiceAMPState(invoice *invpkg.Invoice, setIDs ...*invpkg.SetID) {
|
||||||
|
filteredAMPState := make(invpkg.AMPInvoiceState)
|
||||||
|
|
||||||
|
for _, setID := range setIDs {
|
||||||
|
if setID == nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ampState, ok := invoice.AMPState[*setID]
|
||||||
|
if ok {
|
||||||
|
filteredAMPState[*setID] = ampState
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
invoice.AMPState = filteredAMPState
|
||||||
|
}
|
||||||
|
|
||||||
// ampHTLCsMap is a map of AMP HTLCs affected by an invoice update.
|
// ampHTLCsMap is a map of AMP HTLCs affected by an invoice update.
|
||||||
type ampHTLCsMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC
|
type ampHTLCsMap map[invpkg.SetID]map[models.CircuitKey]*invpkg.InvoiceHTLC
|
||||||
|
|
||||||
@ -1056,7 +1092,8 @@ func (d *DB) InvoicesSettledSince(_ context.Context, sinceSettleIndex uint64) (
|
|||||||
// For each key found, we'll look up the actual
|
// For each key found, we'll look up the actual
|
||||||
// invoice, then accumulate it into our return value.
|
// invoice, then accumulate it into our return value.
|
||||||
invoice, err := fetchInvoice(
|
invoice, err := fetchInvoice(
|
||||||
invoiceKey[:], invoices, setID,
|
invoiceKey[:], invoices, []*invpkg.SetID{setID},
|
||||||
|
true,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1485,7 +1522,7 @@ func fetchAmpSubInvoices(invoiceBucket kvdb.RBucket, invoiceNum []byte,
|
|||||||
// specified by the invoice number. If the setID fields are set, then only the
|
// specified by the invoice number. If the setID fields are set, then only the
|
||||||
// HTLC information pertaining to those set IDs is returned.
|
// HTLC information pertaining to those set IDs is returned.
|
||||||
func fetchInvoice(invoiceNum []byte, invoices kvdb.RBucket,
|
func fetchInvoice(invoiceNum []byte, invoices kvdb.RBucket,
|
||||||
setIDs ...*invpkg.SetID) (invpkg.Invoice, error) {
|
setIDs []*invpkg.SetID, filterAMPState bool) (invpkg.Invoice, error) {
|
||||||
|
|
||||||
invoiceBytes := invoices.Get(invoiceNum)
|
invoiceBytes := invoices.Get(invoiceNum)
|
||||||
if invoiceBytes == nil {
|
if invoiceBytes == nil {
|
||||||
@ -1518,6 +1555,10 @@ func fetchInvoice(invoiceNum []byte, invoices kvdb.RBucket,
|
|||||||
log.Errorf("unable to fetch amp htlcs for inv "+
|
log.Errorf("unable to fetch amp htlcs for inv "+
|
||||||
"%v and setIDs %v: %w", invoiceNum, setIDs, err)
|
"%v and setIDs %v: %w", invoiceNum, setIDs, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if filterAMPState {
|
||||||
|
filterInvoiceAMPState(&invoice, setIDs...)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return invoice, nil
|
return invoice, nil
|
||||||
@ -2163,7 +2204,7 @@ func (d *DB) DeleteCanceledInvoices(_ context.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
invoice, err := fetchInvoice(v, invoices)
|
invoice, err := fetchInvoice(v, invoices, nil, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|||||||
@ -2,7 +2,6 @@ package migration_01_to_11
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"fmt"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -154,12 +153,7 @@ func signDigestCompact(hash []byte) ([]byte, error) {
|
|||||||
privKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
privKey, _ := btcec.PrivKeyFromBytes(testPrivKeyBytes)
|
||||||
|
|
||||||
// ecdsa.SignCompact returns a pubkey-recoverable signature
|
// ecdsa.SignCompact returns a pubkey-recoverable signature
|
||||||
sig, err := ecdsa.SignCompact(privKey, hash, isCompressedKey)
|
return ecdsa.SignCompact(privKey, hash, isCompressedKey), nil
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("can't sign the hash: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return sig, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// getPayReq creates a payment request for the given net.
|
// getPayReq creates a payment request for the given net.
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
"github.com/btcsuite/btcd/chaincfg/chainhash"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ChannelEdgeInfo represents a fully authenticated channel along with all its
|
// ChannelEdgeInfo represents a fully authenticated channel along with all its
|
||||||
@ -62,6 +63,11 @@ type ChannelEdgeInfo struct {
|
|||||||
// the value output in the outpoint that created this channel.
|
// the value output in the outpoint that created this channel.
|
||||||
Capacity btcutil.Amount
|
Capacity btcutil.Amount
|
||||||
|
|
||||||
|
// TapscriptRoot is the optional Merkle root of the tapscript tree if
|
||||||
|
// this channel is a taproot channel that also commits to a tapscript
|
||||||
|
// tree (custom channel).
|
||||||
|
TapscriptRoot fn.Option[chainhash.Hash]
|
||||||
|
|
||||||
// ExtraOpaqueData is the set of data that was appended to this
|
// ExtraOpaqueData is the set of data that was appended to this
|
||||||
// message, some of which we may not actually know how to iterate or
|
// message, some of which we may not actually know how to iterate or
|
||||||
// parse. By holding onto this data, we ensure that we're able to
|
// parse. By holding onto this data, we ensure that we're able to
|
||||||
|
|||||||
@ -195,6 +195,11 @@ type PaymentCreationInfo struct {
|
|||||||
|
|
||||||
// PaymentRequest is the full payment request, if any.
|
// PaymentRequest is the full payment request, if any.
|
||||||
PaymentRequest []byte
|
PaymentRequest []byte
|
||||||
|
|
||||||
|
// FirstHopCustomRecords are the TLV records that are to be sent to the
|
||||||
|
// first hop of this payment. These records will be transmitted via the
|
||||||
|
// wire message only and therefore do not affect the onion payload size.
|
||||||
|
FirstHopCustomRecords lnwire.CustomRecords
|
||||||
}
|
}
|
||||||
|
|
||||||
// htlcBucketKey creates a composite key from prefix and id where the result is
|
// htlcBucketKey creates a composite key from prefix and id where the result is
|
||||||
@ -1010,10 +1015,21 @@ func serializePaymentCreationInfo(w io.Writer, c *PaymentCreationInfo) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any remaining bytes are TLV encoded records. Currently, these are
|
||||||
|
// only the custom records provided by the user to be sent to the first
|
||||||
|
// hop. But this can easily be extended with further records by merging
|
||||||
|
// the records into a single TLV stream.
|
||||||
|
err := c.FirstHopCustomRecords.SerializeTo(w)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo, error) {
|
func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo,
|
||||||
|
error) {
|
||||||
|
|
||||||
var scratch [8]byte
|
var scratch [8]byte
|
||||||
|
|
||||||
c := &PaymentCreationInfo{}
|
c := &PaymentCreationInfo{}
|
||||||
@ -1046,6 +1062,15 @@ func deserializePaymentCreationInfo(r io.Reader) (*PaymentCreationInfo, error) {
|
|||||||
}
|
}
|
||||||
c.PaymentRequest = payReq
|
c.PaymentRequest = payReq
|
||||||
|
|
||||||
|
// Any remaining bytes are TLV encoded records. Currently, these are
|
||||||
|
// only the custom records provided by the user to be sent to the first
|
||||||
|
// hop. But this can easily be extended with further records by merging
|
||||||
|
// the records into a single TLV stream.
|
||||||
|
c.FirstHopCustomRecords, err = lnwire.ParseCustomRecordsFrom(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
return c, nil
|
return c, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1071,6 +1096,25 @@ func serializeHTLCAttemptInfo(w io.Writer, a *HTLCAttemptInfo) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Merge the fixed/known records together with the custom records to
|
||||||
|
// serialize them as a single blob. We can't do this in SerializeRoute
|
||||||
|
// because we're in the middle of the byte stream there. We can only do
|
||||||
|
// TLV serialization at the end of the stream, since EOF is allowed for
|
||||||
|
// a stream if no more data is expected.
|
||||||
|
producers := []tlv.RecordProducer{
|
||||||
|
&a.Route.FirstHopAmount,
|
||||||
|
}
|
||||||
|
tlvData, err := lnwire.MergeAndEncode(
|
||||||
|
producers, nil, a.Route.FirstHopWireCustomRecords,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err := w.Write(tlvData); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1108,6 +1152,22 @@ func deserializeHTLCAttemptInfo(r io.Reader) (*HTLCAttemptInfo, error) {
|
|||||||
|
|
||||||
a.Hash = &hash
|
a.Hash = &hash
|
||||||
|
|
||||||
|
// Read any remaining data (if any) and parse it into the known records
|
||||||
|
// and custom records.
|
||||||
|
extraData, err := io.ReadAll(r)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
customRecords, _, _, err := lnwire.ParseAndExtractCustomRecords(
|
||||||
|
extraData, &a.Route.FirstHopAmount,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
a.Route.FirstHopWireCustomRecords = customRecords
|
||||||
|
|
||||||
return a, nil
|
return a, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1373,6 +1433,8 @@ func SerializeRoute(w io.Writer, r route.Route) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Any new/extra TLV data is encoded in serializeHTLCAttemptInfo!
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1406,5 +1468,7 @@ func DeserializeRoute(r io.Reader) (route.Route, error) {
|
|||||||
}
|
}
|
||||||
rt.Hops = hops
|
rt.Hops = hops
|
||||||
|
|
||||||
|
// Any new/extra TLV data is decoded in deserializeHTLCAttemptInfo!
|
||||||
|
|
||||||
return rt, nil
|
return rt, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@ -13,8 +13,10 @@ import (
|
|||||||
"github.com/davecgh/go-spew/spew"
|
"github.com/davecgh/go-spew/spew"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -108,7 +110,7 @@ func makeFakeInfo() (*PaymentCreationInfo, *HTLCAttemptInfo) {
|
|||||||
// Use single second precision to avoid false positive test
|
// Use single second precision to avoid false positive test
|
||||||
// failures due to the monotonic time component.
|
// failures due to the monotonic time component.
|
||||||
CreationTime: time.Unix(time.Now().Unix(), 0),
|
CreationTime: time.Unix(time.Now().Unix(), 0),
|
||||||
PaymentRequest: []byte(""),
|
PaymentRequest: []byte("test"),
|
||||||
}
|
}
|
||||||
|
|
||||||
a := NewHtlcAttempt(
|
a := NewHtlcAttempt(
|
||||||
@ -124,51 +126,64 @@ func TestSentPaymentSerialization(t *testing.T) {
|
|||||||
c, s := makeFakeInfo()
|
c, s := makeFakeInfo()
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
if err := serializePaymentCreationInfo(&b, c); err != nil {
|
require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
|
||||||
t.Fatalf("unable to serialize creation info: %v", err)
|
|
||||||
}
|
// Assert the length of the serialized creation info is as expected,
|
||||||
|
// without any custom records.
|
||||||
|
baseLength := 32 + 8 + 8 + 4 + len(c.PaymentRequest)
|
||||||
|
require.Len(t, b.Bytes(), baseLength)
|
||||||
|
|
||||||
newCreationInfo, err := deserializePaymentCreationInfo(&b)
|
newCreationInfo, err := deserializePaymentCreationInfo(&b)
|
||||||
require.NoError(t, err, "unable to deserialize creation info")
|
require.NoError(t, err, "deserialize")
|
||||||
|
require.Equal(t, c, newCreationInfo)
|
||||||
if !reflect.DeepEqual(c, newCreationInfo) {
|
|
||||||
t.Fatalf("Payments do not match after "+
|
|
||||||
"serialization/deserialization %v vs %v",
|
|
||||||
spew.Sdump(c), spew.Sdump(newCreationInfo),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
b.Reset()
|
b.Reset()
|
||||||
if err := serializeHTLCAttemptInfo(&b, s); err != nil {
|
|
||||||
t.Fatalf("unable to serialize info: %v", err)
|
// Now we add some custom records to the creation info and serialize it
|
||||||
|
// again.
|
||||||
|
c.FirstHopCustomRecords = lnwire.CustomRecords{
|
||||||
|
lnwire.MinCustomRecordsTlvType: []byte{1, 2, 3},
|
||||||
}
|
}
|
||||||
|
require.NoError(t, serializePaymentCreationInfo(&b, c), "serialize")
|
||||||
|
|
||||||
|
newCreationInfo, err = deserializePaymentCreationInfo(&b)
|
||||||
|
require.NoError(t, err, "deserialize")
|
||||||
|
require.Equal(t, c, newCreationInfo)
|
||||||
|
|
||||||
|
b.Reset()
|
||||||
|
require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
|
||||||
|
|
||||||
newWireInfo, err := deserializeHTLCAttemptInfo(&b)
|
newWireInfo, err := deserializeHTLCAttemptInfo(&b)
|
||||||
require.NoError(t, err, "unable to deserialize info")
|
require.NoError(t, err, "deserialize")
|
||||||
newWireInfo.AttemptID = s.AttemptID
|
|
||||||
|
|
||||||
// First we verify all the records match up porperly, as they aren't
|
// First we verify all the records match up properly.
|
||||||
// able to be properly compared using reflect.DeepEqual.
|
require.Equal(t, s.Route, newWireInfo.Route)
|
||||||
err = assertRouteEqual(&s.Route, &newWireInfo.Route)
|
|
||||||
if err != nil {
|
// We now add the new fields and custom records to the route and
|
||||||
t.Fatalf("Routes do not match after "+
|
// serialize it again.
|
||||||
"serialization/deserialization: %v", err)
|
b.Reset()
|
||||||
|
s.Route.FirstHopAmount = tlv.NewRecordT[tlv.TlvType0](
|
||||||
|
tlv.NewBigSizeT(lnwire.MilliSatoshi(1234)),
|
||||||
|
)
|
||||||
|
s.Route.FirstHopWireCustomRecords = lnwire.CustomRecords{
|
||||||
|
lnwire.MinCustomRecordsTlvType + 3: []byte{4, 5, 6},
|
||||||
}
|
}
|
||||||
|
require.NoError(t, serializeHTLCAttemptInfo(&b, s), "serialize")
|
||||||
|
|
||||||
|
newWireInfo, err = deserializeHTLCAttemptInfo(&b)
|
||||||
|
require.NoError(t, err, "deserialize")
|
||||||
|
require.Equal(t, s.Route, newWireInfo.Route)
|
||||||
|
|
||||||
// Clear routes to allow DeepEqual to compare the remaining fields.
|
// Clear routes to allow DeepEqual to compare the remaining fields.
|
||||||
newWireInfo.Route = route.Route{}
|
newWireInfo.Route = route.Route{}
|
||||||
s.Route = route.Route{}
|
s.Route = route.Route{}
|
||||||
|
newWireInfo.AttemptID = s.AttemptID
|
||||||
|
|
||||||
// Call session key method to set our cached session key so we can use
|
// Call session key method to set our cached session key so we can use
|
||||||
// DeepEqual, and assert that our key equals the original key.
|
// DeepEqual, and assert that our key equals the original key.
|
||||||
require.Equal(t, s.cachedSessionKey, newWireInfo.SessionKey())
|
require.Equal(t, s.cachedSessionKey, newWireInfo.SessionKey())
|
||||||
|
|
||||||
if !reflect.DeepEqual(s, newWireInfo) {
|
require.Equal(t, s, newWireInfo)
|
||||||
t.Fatalf("Payments do not match after "+
|
|
||||||
"serialization/deserialization %v vs %v",
|
|
||||||
spew.Sdump(s), spew.Sdump(newWireInfo),
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// assertRouteEquals compares to routes for equality and returns an error if
|
// assertRouteEquals compares to routes for equality and returns an error if
|
||||||
|
|||||||
@ -7,6 +7,7 @@ import (
|
|||||||
"math"
|
"math"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -16,16 +17,15 @@ import (
|
|||||||
const (
|
const (
|
||||||
// OutputIndexEmpty is used when the output index doesn't exist.
|
// OutputIndexEmpty is used when the output index doesn't exist.
|
||||||
OutputIndexEmpty = math.MaxUint16
|
OutputIndexEmpty = math.MaxUint16
|
||||||
|
)
|
||||||
|
|
||||||
// A set of tlv type definitions used to serialize the body of
|
type (
|
||||||
// revocation logs to the database.
|
// BigSizeAmount is a type alias for a TLV record of a btcutil.Amount.
|
||||||
//
|
BigSizeAmount = tlv.BigSizeT[btcutil.Amount]
|
||||||
// NOTE: A migration should be added whenever this list changes.
|
|
||||||
revLogOurOutputIndexType tlv.Type = 0
|
// BigSizeMilliSatoshi is a type alias for a TLV record of a
|
||||||
revLogTheirOutputIndexType tlv.Type = 1
|
// lnwire.MilliSatoshi.
|
||||||
revLogCommitTxHashType tlv.Type = 2
|
BigSizeMilliSatoshi = tlv.BigSizeT[lnwire.MilliSatoshi]
|
||||||
revLogOurBalanceType tlv.Type = 3
|
|
||||||
revLogTheirBalanceType tlv.Type = 4
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -54,6 +54,74 @@ var (
|
|||||||
ErrOutputIndexTooBig = errors.New("output index is over uint16")
|
ErrOutputIndexTooBig = errors.New("output index is over uint16")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// SparsePayHash is a type alias for a 32 byte array, which when serialized is
|
||||||
|
// able to save some space by not including an empty payment hash on disk.
|
||||||
|
type SparsePayHash [32]byte
|
||||||
|
|
||||||
|
// NewSparsePayHash creates a new SparsePayHash from a 32 byte array.
|
||||||
|
func NewSparsePayHash(rHash [32]byte) SparsePayHash {
|
||||||
|
return SparsePayHash(rHash)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record returns a tlv record for the SparsePayHash.
|
||||||
|
func (s *SparsePayHash) Record() tlv.Record {
|
||||||
|
// We use a zero for the type here, as this'll be used along with the
|
||||||
|
// RecordT type.
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
0, s, s.hashLen,
|
||||||
|
sparseHashEncoder, sparseHashDecoder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// hashLen is used by MakeDynamicRecord to return the size of the RHash.
|
||||||
|
//
|
||||||
|
// NOTE: for zero hash, we return a length 0.
|
||||||
|
func (s *SparsePayHash) hashLen() uint64 {
|
||||||
|
if bytes.Equal(s[:], lntypes.ZeroHash[:]) {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return 32
|
||||||
|
}
|
||||||
|
|
||||||
|
// sparseHashEncoder is the customized encoder which skips encoding the empty
|
||||||
|
// hash.
|
||||||
|
func sparseHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
||||||
|
v, ok := val.(*SparsePayHash)
|
||||||
|
if !ok {
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the value is an empty hash, we will skip encoding it.
|
||||||
|
if bytes.Equal(v[:], lntypes.ZeroHash[:]) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vArray := (*[32]byte)(v)
|
||||||
|
|
||||||
|
return tlv.EBytes32(w, vArray, buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// sparseHashDecoder is the customized decoder which skips decoding the empty
|
||||||
|
// hash.
|
||||||
|
func sparseHashDecoder(r io.Reader, val interface{}, buf *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
v, ok := val.(*SparsePayHash)
|
||||||
|
if !ok {
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "SparsePayHash")
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the length is zero, we will skip encoding the empty hash.
|
||||||
|
if l == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
vArray := (*[32]byte)(v)
|
||||||
|
|
||||||
|
return tlv.DBytes32(r, vArray, buf, 32)
|
||||||
|
}
|
||||||
|
|
||||||
// HTLCEntry specifies the minimal info needed to be stored on disk for ALL the
|
// HTLCEntry specifies the minimal info needed to be stored on disk for ALL the
|
||||||
// historical HTLCs, which is useful for constructing RevocationLog when a
|
// historical HTLCs, which is useful for constructing RevocationLog when a
|
||||||
// breach is detected.
|
// breach is detected.
|
||||||
@ -72,116 +140,90 @@ var (
|
|||||||
// made into tlv records without further conversion.
|
// made into tlv records without further conversion.
|
||||||
type HTLCEntry struct {
|
type HTLCEntry struct {
|
||||||
// RHash is the payment hash of the HTLC.
|
// RHash is the payment hash of the HTLC.
|
||||||
RHash [32]byte
|
RHash tlv.RecordT[tlv.TlvType0, SparsePayHash]
|
||||||
|
|
||||||
// RefundTimeout is the absolute timeout on the HTLC that the sender
|
// RefundTimeout is the absolute timeout on the HTLC that the sender
|
||||||
// must wait before reclaiming the funds in limbo.
|
// must wait before reclaiming the funds in limbo.
|
||||||
RefundTimeout uint32
|
RefundTimeout tlv.RecordT[tlv.TlvType1, uint32]
|
||||||
|
|
||||||
// OutputIndex is the output index for this particular HTLC output
|
// OutputIndex is the output index for this particular HTLC output
|
||||||
// within the commitment transaction.
|
// within the commitment transaction.
|
||||||
//
|
//
|
||||||
// NOTE: we use uint16 instead of int32 here to save us 2 bytes, which
|
// NOTE: we use uint16 instead of int32 here to save us 2 bytes, which
|
||||||
// gives us a max number of HTLCs of 65K.
|
// gives us a max number of HTLCs of 65K.
|
||||||
OutputIndex uint16
|
OutputIndex tlv.RecordT[tlv.TlvType2, uint16]
|
||||||
|
|
||||||
// Incoming denotes whether we're the receiver or the sender of this
|
// Incoming denotes whether we're the receiver or the sender of this
|
||||||
// HTLC.
|
// HTLC.
|
||||||
//
|
Incoming tlv.RecordT[tlv.TlvType3, bool]
|
||||||
// NOTE: this field is the memory representation of the field
|
|
||||||
// incomingUint.
|
|
||||||
Incoming bool
|
|
||||||
|
|
||||||
// Amt is the amount of satoshis this HTLC escrows.
|
// Amt is the amount of satoshis this HTLC escrows.
|
||||||
//
|
Amt tlv.RecordT[tlv.TlvType4, tlv.BigSizeT[btcutil.Amount]]
|
||||||
// NOTE: this field is the memory representation of the field amtUint.
|
|
||||||
Amt btcutil.Amount
|
|
||||||
|
|
||||||
// amtTlv is the uint64 format of Amt. This field is created so we can
|
// CustomBlob is an optional blob that can be used to store information
|
||||||
// easily make it into a tlv record and save it to disk.
|
// specific to revocation handling for a custom channel type.
|
||||||
//
|
CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
|
||||||
// NOTE: we keep this field for accounting purpose only. If the disk
|
|
||||||
// space becomes an issue, we could delete this field to save us extra
|
|
||||||
// 8 bytes.
|
|
||||||
amtTlv uint64
|
|
||||||
|
|
||||||
// incomingTlv is the uint8 format of Incoming. This field is created
|
// HtlcIndex is the index of the HTLC in the channel.
|
||||||
// so we can easily make it into a tlv record and save it to disk.
|
HtlcIndex tlv.OptionalRecordT[tlv.TlvType6, uint16]
|
||||||
incomingTlv uint8
|
|
||||||
}
|
|
||||||
|
|
||||||
// RHashLen is used by MakeDynamicRecord to return the size of the RHash.
|
|
||||||
//
|
|
||||||
// NOTE: for zero hash, we return a length 0.
|
|
||||||
func (h *HTLCEntry) RHashLen() uint64 {
|
|
||||||
if h.RHash == lntypes.ZeroHash {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return 32
|
|
||||||
}
|
|
||||||
|
|
||||||
// RHashEncoder is the customized encoder which skips encoding the empty hash.
|
|
||||||
func RHashEncoder(w io.Writer, val interface{}, buf *[8]byte) error {
|
|
||||||
v, ok := val.(*[32]byte)
|
|
||||||
if !ok {
|
|
||||||
return tlv.NewTypeForEncodingErr(val, "RHash")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the value is an empty hash, we will skip encoding it.
|
|
||||||
if *v == lntypes.ZeroHash {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlv.EBytes32(w, v, buf)
|
|
||||||
}
|
|
||||||
|
|
||||||
// RHashDecoder is the customized decoder which skips decoding the empty hash.
|
|
||||||
func RHashDecoder(r io.Reader, val interface{}, buf *[8]byte, l uint64) error {
|
|
||||||
v, ok := val.(*[32]byte)
|
|
||||||
if !ok {
|
|
||||||
return tlv.NewTypeForEncodingErr(val, "RHash")
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the length is zero, we will skip encoding the empty hash.
|
|
||||||
if l == 0 {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlv.DBytes32(r, v, buf, 32)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// toTlvStream converts an HTLCEntry record into a tlv representation.
|
// toTlvStream converts an HTLCEntry record into a tlv representation.
|
||||||
func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
|
func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
|
||||||
const (
|
records := []tlv.Record{
|
||||||
// A set of tlv type definitions used to serialize htlc entries
|
h.RHash.Record(),
|
||||||
// to the database. We define it here instead of the head of
|
h.RefundTimeout.Record(),
|
||||||
// the file to avoid naming conflicts.
|
h.OutputIndex.Record(),
|
||||||
//
|
h.Incoming.Record(),
|
||||||
// NOTE: A migration should be added whenever this list
|
h.Amt.Record(),
|
||||||
// changes.
|
}
|
||||||
rHashType tlv.Type = 0
|
|
||||||
refundTimeoutType tlv.Type = 1
|
|
||||||
outputIndexType tlv.Type = 2
|
|
||||||
incomingType tlv.Type = 3
|
|
||||||
amtType tlv.Type = 4
|
|
||||||
)
|
|
||||||
|
|
||||||
return tlv.NewStream(
|
h.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
|
||||||
tlv.MakeDynamicRecord(
|
records = append(records, r.Record())
|
||||||
rHashType, &h.RHash, h.RHashLen,
|
})
|
||||||
RHashEncoder, RHashDecoder,
|
|
||||||
|
h.HtlcIndex.WhenSome(func(r tlv.RecordT[tlv.TlvType6, uint16]) {
|
||||||
|
records = append(records, r.Record())
|
||||||
|
})
|
||||||
|
|
||||||
|
tlv.SortRecords(records)
|
||||||
|
|
||||||
|
return tlv.NewStream(records...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewHTLCEntryFromHTLC creates a new HTLCEntry from an HTLC.
|
||||||
|
func NewHTLCEntryFromHTLC(htlc HTLC) (*HTLCEntry, error) {
|
||||||
|
h := &HTLCEntry{
|
||||||
|
RHash: tlv.NewRecordT[tlv.TlvType0](
|
||||||
|
NewSparsePayHash(htlc.RHash),
|
||||||
),
|
),
|
||||||
tlv.MakePrimitiveRecord(
|
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||||
refundTimeoutType, &h.RefundTimeout,
|
htlc.RefundTimeout,
|
||||||
),
|
),
|
||||||
tlv.MakePrimitiveRecord(
|
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||||
outputIndexType, &h.OutputIndex,
|
uint16(htlc.OutputIndex),
|
||||||
),
|
),
|
||||||
tlv.MakePrimitiveRecord(incomingType, &h.incomingTlv),
|
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](htlc.Incoming),
|
||||||
// We will save 3 bytes if the amount is less or equal to
|
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||||
// 4,294,967,295 msat, or roughly 0.043 bitcoin.
|
tlv.NewBigSizeT(htlc.Amt.ToSatoshis()),
|
||||||
tlv.MakeBigSizeRecord(amtType, &h.amtTlv),
|
),
|
||||||
)
|
HtlcIndex: tlv.SomeRecordT(tlv.NewPrimitiveRecord[tlv.TlvType6](
|
||||||
|
uint16(htlc.HtlcIndex),
|
||||||
|
)),
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(htlc.CustomRecords) != 0 {
|
||||||
|
blob, err := htlc.CustomRecords.Serialize()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
h.CustomBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
return h, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RevocationLog stores the info needed to construct a breach retribution. Its
|
// RevocationLog stores the info needed to construct a breach retribution. Its
|
||||||
@ -191,15 +233,15 @@ func (h *HTLCEntry) toTlvStream() (*tlv.Stream, error) {
|
|||||||
type RevocationLog struct {
|
type RevocationLog struct {
|
||||||
// OurOutputIndex specifies our output index in this commitment. In a
|
// OurOutputIndex specifies our output index in this commitment. In a
|
||||||
// remote commitment transaction, this is the to remote output index.
|
// remote commitment transaction, this is the to remote output index.
|
||||||
OurOutputIndex uint16
|
OurOutputIndex tlv.RecordT[tlv.TlvType0, uint16]
|
||||||
|
|
||||||
// TheirOutputIndex specifies their output index in this commitment. In
|
// TheirOutputIndex specifies their output index in this commitment. In
|
||||||
// a remote commitment transaction, this is the to local output index.
|
// a remote commitment transaction, this is the to local output index.
|
||||||
TheirOutputIndex uint16
|
TheirOutputIndex tlv.RecordT[tlv.TlvType1, uint16]
|
||||||
|
|
||||||
// CommitTxHash is the hash of the latest version of the commitment
|
// CommitTxHash is the hash of the latest version of the commitment
|
||||||
// state, broadcast able by us.
|
// state, broadcast able by us.
|
||||||
CommitTxHash [32]byte
|
CommitTxHash tlv.RecordT[tlv.TlvType2, [32]byte]
|
||||||
|
|
||||||
// HTLCEntries is the set of HTLCEntry's that are pending at this
|
// HTLCEntries is the set of HTLCEntry's that are pending at this
|
||||||
// particular commitment height.
|
// particular commitment height.
|
||||||
@ -209,21 +251,65 @@ type RevocationLog struct {
|
|||||||
// directly spendable by us. In other words, it is the value of the
|
// directly spendable by us. In other words, it is the value of the
|
||||||
// to_remote output on the remote parties' commitment transaction.
|
// to_remote output on the remote parties' commitment transaction.
|
||||||
//
|
//
|
||||||
// NOTE: this is a pointer so that it is clear if the value is zero or
|
// NOTE: this is an option so that it is clear if the value is zero or
|
||||||
// nil. Since migration 30 of the channeldb initially did not include
|
// nil. Since migration 30 of the channeldb initially did not include
|
||||||
// this field, it could be the case that the field is not present for
|
// this field, it could be the case that the field is not present for
|
||||||
// all revocation logs.
|
// all revocation logs.
|
||||||
OurBalance *lnwire.MilliSatoshi
|
OurBalance tlv.OptionalRecordT[tlv.TlvType3, BigSizeMilliSatoshi]
|
||||||
|
|
||||||
// TheirBalance is the current available balance within the channel
|
// TheirBalance is the current available balance within the channel
|
||||||
// directly spendable by the remote node. In other words, it is the
|
// directly spendable by the remote node. In other words, it is the
|
||||||
// value of the to_local output on the remote parties' commitment.
|
// value of the to_local output on the remote parties' commitment.
|
||||||
//
|
//
|
||||||
// NOTE: this is a pointer so that it is clear if the value is zero or
|
// NOTE: this is an option so that it is clear if the value is zero or
|
||||||
// nil. Since migration 30 of the channeldb initially did not include
|
// nil. Since migration 30 of the channeldb initially did not include
|
||||||
// this field, it could be the case that the field is not present for
|
// this field, it could be the case that the field is not present for
|
||||||
// all revocation logs.
|
// all revocation logs.
|
||||||
TheirBalance *lnwire.MilliSatoshi
|
TheirBalance tlv.OptionalRecordT[tlv.TlvType4, BigSizeMilliSatoshi]
|
||||||
|
|
||||||
|
// CustomBlob is an optional blob that can be used to store information
|
||||||
|
// specific to a custom channel type. This information is only created
|
||||||
|
// at channel funding time, and after wards is to be considered
|
||||||
|
// immutable.
|
||||||
|
CustomBlob tlv.OptionalRecordT[tlv.TlvType5, tlv.Blob]
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewRevocationLog creates a new RevocationLog from the given parameters.
|
||||||
|
func NewRevocationLog(ourOutputIndex uint16, theirOutputIndex uint16,
|
||||||
|
commitHash [32]byte, ourBalance,
|
||||||
|
theirBalance fn.Option[lnwire.MilliSatoshi], htlcs []*HTLCEntry,
|
||||||
|
customBlob fn.Option[tlv.Blob]) RevocationLog {
|
||||||
|
|
||||||
|
rl := RevocationLog{
|
||||||
|
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||||
|
ourOutputIndex,
|
||||||
|
),
|
||||||
|
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||||
|
theirOutputIndex,
|
||||||
|
),
|
||||||
|
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2](commitHash),
|
||||||
|
HTLCEntries: htlcs,
|
||||||
|
}
|
||||||
|
|
||||||
|
ourBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
|
||||||
|
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
|
||||||
|
tlv.NewBigSizeT(balance),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
theirBalance.WhenSome(func(balance lnwire.MilliSatoshi) {
|
||||||
|
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
|
||||||
|
tlv.NewBigSizeT(balance),
|
||||||
|
))
|
||||||
|
})
|
||||||
|
|
||||||
|
customBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
rl.CustomBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
return rl
|
||||||
}
|
}
|
||||||
|
|
||||||
// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a
|
// putRevocationLog uses the fields `CommitTx` and `Htlcs` from a
|
||||||
@ -242,15 +328,32 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
|
|||||||
}
|
}
|
||||||
|
|
||||||
rl := &RevocationLog{
|
rl := &RevocationLog{
|
||||||
OurOutputIndex: uint16(ourOutputIndex),
|
OurOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType0](
|
||||||
TheirOutputIndex: uint16(theirOutputIndex),
|
uint16(ourOutputIndex),
|
||||||
CommitTxHash: commit.CommitTx.TxHash(),
|
),
|
||||||
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)),
|
TheirOutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType1](
|
||||||
|
uint16(theirOutputIndex),
|
||||||
|
),
|
||||||
|
CommitTxHash: tlv.NewPrimitiveRecord[tlv.TlvType2, [32]byte](
|
||||||
|
commit.CommitTx.TxHash(),
|
||||||
|
),
|
||||||
|
HTLCEntries: make([]*HTLCEntry, 0, len(commit.Htlcs)),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
commit.CustomBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
rl.CustomBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType5, tlv.Blob](blob),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
if !noAmtData {
|
if !noAmtData {
|
||||||
rl.OurBalance = &commit.LocalBalance
|
rl.OurBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType3](
|
||||||
rl.TheirBalance = &commit.RemoteBalance
|
tlv.NewBigSizeT(commit.LocalBalance),
|
||||||
|
))
|
||||||
|
|
||||||
|
rl.TheirBalance = tlv.SomeRecordT(tlv.NewRecordT[tlv.TlvType4](
|
||||||
|
tlv.NewBigSizeT(commit.RemoteBalance),
|
||||||
|
))
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, htlc := range commit.Htlcs {
|
for _, htlc := range commit.Htlcs {
|
||||||
@ -265,12 +368,9 @@ func putRevocationLog(bucket kvdb.RwBucket, commit *ChannelCommitment,
|
|||||||
return ErrOutputIndexTooBig
|
return ErrOutputIndexTooBig
|
||||||
}
|
}
|
||||||
|
|
||||||
entry := &HTLCEntry{
|
entry, err := NewHTLCEntryFromHTLC(htlc)
|
||||||
RHash: htlc.RHash,
|
if err != nil {
|
||||||
RefundTimeout: htlc.RefundTimeout,
|
return err
|
||||||
Incoming: htlc.Incoming,
|
|
||||||
OutputIndex: uint16(htlc.OutputIndex),
|
|
||||||
Amt: htlc.Amt.ToSatoshis(),
|
|
||||||
}
|
}
|
||||||
rl.HTLCEntries = append(rl.HTLCEntries, entry)
|
rl.HTLCEntries = append(rl.HTLCEntries, entry)
|
||||||
}
|
}
|
||||||
@ -306,31 +406,27 @@ func fetchRevocationLog(log kvdb.RBucket,
|
|||||||
func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
|
func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
|
||||||
// Add the tlv records for all non-optional fields.
|
// Add the tlv records for all non-optional fields.
|
||||||
records := []tlv.Record{
|
records := []tlv.Record{
|
||||||
tlv.MakePrimitiveRecord(
|
rl.OurOutputIndex.Record(),
|
||||||
revLogOurOutputIndexType, &rl.OurOutputIndex,
|
rl.TheirOutputIndex.Record(),
|
||||||
),
|
rl.CommitTxHash.Record(),
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
|
|
||||||
),
|
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
revLogCommitTxHashType, &rl.CommitTxHash,
|
|
||||||
),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Now we add any optional fields that are non-nil.
|
// Now we add any optional fields that are non-nil.
|
||||||
if rl.OurBalance != nil {
|
rl.OurBalance.WhenSome(
|
||||||
lb := uint64(*rl.OurBalance)
|
func(r tlv.RecordT[tlv.TlvType3, BigSizeMilliSatoshi]) {
|
||||||
records = append(records, tlv.MakeBigSizeRecord(
|
records = append(records, r.Record())
|
||||||
revLogOurBalanceType, &lb,
|
},
|
||||||
))
|
)
|
||||||
}
|
|
||||||
|
|
||||||
if rl.TheirBalance != nil {
|
rl.TheirBalance.WhenSome(
|
||||||
rb := uint64(*rl.TheirBalance)
|
func(r tlv.RecordT[tlv.TlvType4, BigSizeMilliSatoshi]) {
|
||||||
records = append(records, tlv.MakeBigSizeRecord(
|
records = append(records, r.Record())
|
||||||
revLogTheirBalanceType, &rb,
|
},
|
||||||
))
|
)
|
||||||
}
|
|
||||||
|
rl.CustomBlob.WhenSome(func(r tlv.RecordT[tlv.TlvType5, tlv.Blob]) {
|
||||||
|
records = append(records, r.Record())
|
||||||
|
})
|
||||||
|
|
||||||
// Create the tlv stream.
|
// Create the tlv stream.
|
||||||
tlvStream, err := tlv.NewStream(records...)
|
tlvStream, err := tlv.NewStream(records...)
|
||||||
@ -351,14 +447,6 @@ func serializeRevocationLog(w io.Writer, rl *RevocationLog) error {
|
|||||||
// format.
|
// format.
|
||||||
func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
|
func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
|
||||||
for _, htlc := range htlcs {
|
for _, htlc := range htlcs {
|
||||||
// Patch the incomingTlv field.
|
|
||||||
if htlc.Incoming {
|
|
||||||
htlc.incomingTlv = 1
|
|
||||||
}
|
|
||||||
|
|
||||||
// Patch the amtTlv field.
|
|
||||||
htlc.amtTlv = uint64(htlc.Amt)
|
|
||||||
|
|
||||||
// Create the tlv stream.
|
// Create the tlv stream.
|
||||||
tlvStream, err := htlc.toTlvStream()
|
tlvStream, err := htlc.toTlvStream()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -376,27 +464,20 @@ func serializeHTLCEntries(w io.Writer, htlcs []*HTLCEntry) error {
|
|||||||
|
|
||||||
// deserializeRevocationLog deserializes a RevocationLog based on tlv format.
|
// deserializeRevocationLog deserializes a RevocationLog based on tlv format.
|
||||||
func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
|
func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
|
||||||
var (
|
var rl RevocationLog
|
||||||
rl RevocationLog
|
|
||||||
ourBalance uint64
|
ourBalance := rl.OurBalance.Zero()
|
||||||
theirBalance uint64
|
theirBalance := rl.TheirBalance.Zero()
|
||||||
)
|
customBlob := rl.CustomBlob.Zero()
|
||||||
|
|
||||||
// Create the tlv stream.
|
// Create the tlv stream.
|
||||||
tlvStream, err := tlv.NewStream(
|
tlvStream, err := tlv.NewStream(
|
||||||
tlv.MakePrimitiveRecord(
|
rl.OurOutputIndex.Record(),
|
||||||
revLogOurOutputIndexType, &rl.OurOutputIndex,
|
rl.TheirOutputIndex.Record(),
|
||||||
),
|
rl.CommitTxHash.Record(),
|
||||||
tlv.MakePrimitiveRecord(
|
ourBalance.Record(),
|
||||||
revLogTheirOutputIndexType, &rl.TheirOutputIndex,
|
theirBalance.Record(),
|
||||||
),
|
customBlob.Record(),
|
||||||
tlv.MakePrimitiveRecord(
|
|
||||||
revLogCommitTxHashType, &rl.CommitTxHash,
|
|
||||||
),
|
|
||||||
tlv.MakeBigSizeRecord(revLogOurBalanceType, &ourBalance),
|
|
||||||
tlv.MakeBigSizeRecord(
|
|
||||||
revLogTheirBalanceType, &theirBalance,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return rl, err
|
return rl, err
|
||||||
@ -408,14 +489,16 @@ func deserializeRevocationLog(r io.Reader) (RevocationLog, error) {
|
|||||||
return rl, err
|
return rl, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if t, ok := parsedTypes[revLogOurBalanceType]; ok && t == nil {
|
if t, ok := parsedTypes[ourBalance.TlvType()]; ok && t == nil {
|
||||||
lb := lnwire.MilliSatoshi(ourBalance)
|
rl.OurBalance = tlv.SomeRecordT(ourBalance)
|
||||||
rl.OurBalance = &lb
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if t, ok := parsedTypes[revLogTheirBalanceType]; ok && t == nil {
|
if t, ok := parsedTypes[theirBalance.TlvType()]; ok && t == nil {
|
||||||
rb := lnwire.MilliSatoshi(theirBalance)
|
rl.TheirBalance = tlv.SomeRecordT(theirBalance)
|
||||||
rl.TheirBalance = &rb
|
}
|
||||||
|
|
||||||
|
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
|
||||||
|
rl.CustomBlob = tlv.SomeRecordT(customBlob)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the HTLC entries.
|
// Read the HTLC entries.
|
||||||
@ -432,14 +515,28 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
|
|||||||
for {
|
for {
|
||||||
var htlc HTLCEntry
|
var htlc HTLCEntry
|
||||||
|
|
||||||
|
customBlob := htlc.CustomBlob.Zero()
|
||||||
|
htlcIndex := htlc.HtlcIndex.Zero()
|
||||||
|
|
||||||
// Create the tlv stream.
|
// Create the tlv stream.
|
||||||
tlvStream, err := htlc.toTlvStream()
|
records := []tlv.Record{
|
||||||
|
htlc.RHash.Record(),
|
||||||
|
htlc.RefundTimeout.Record(),
|
||||||
|
htlc.OutputIndex.Record(),
|
||||||
|
htlc.Incoming.Record(),
|
||||||
|
htlc.Amt.Record(),
|
||||||
|
customBlob.Record(),
|
||||||
|
htlcIndex.Record(),
|
||||||
|
}
|
||||||
|
|
||||||
|
tlvStream, err := tlv.NewStream(records...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Read the HTLC entry.
|
// Read the HTLC entry.
|
||||||
if _, err := readTlvStream(r, tlvStream); err != nil {
|
parsedTypes, err := readTlvStream(r, tlvStream)
|
||||||
|
if err != nil {
|
||||||
// We've reached the end when hitting an EOF.
|
// We've reached the end when hitting an EOF.
|
||||||
if err == io.ErrUnexpectedEOF {
|
if err == io.ErrUnexpectedEOF {
|
||||||
break
|
break
|
||||||
@ -447,13 +544,13 @@ func deserializeHTLCEntries(r io.Reader) ([]*HTLCEntry, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch the Incoming field.
|
if t, ok := parsedTypes[customBlob.TlvType()]; ok && t == nil {
|
||||||
if htlc.incomingTlv == 1 {
|
htlc.CustomBlob = tlv.SomeRecordT(customBlob)
|
||||||
htlc.Incoming = true
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Patch the Amt field.
|
if t, ok := parsedTypes[htlcIndex.TlvType()]; ok && t == nil {
|
||||||
htlc.Amt = btcutil.Amount(htlc.amtTlv)
|
htlc.HtlcIndex = tlv.SomeRecordT(htlcIndex)
|
||||||
|
}
|
||||||
|
|
||||||
// Append the entry.
|
// Append the entry.
|
||||||
htlcs = append(htlcs, &htlc)
|
htlcs = append(htlcs, &htlc)
|
||||||
@ -469,6 +566,7 @@ func writeTlvStream(w io.Writer, s *tlv.Stream) error {
|
|||||||
if err := s.Encode(&b); err != nil {
|
if err := s.Encode(&b); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Write the stream's length as a varint.
|
// Write the stream's length as a varint.
|
||||||
err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{})
|
err := tlv.WriteVarInt(w, uint64(b.Len()), &[8]byte{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -8,6 +8,7 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lntest/channels"
|
"github.com/lightningnetwork/lnd/lntest/channels"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -33,17 +34,38 @@ var (
|
|||||||
0xff, // value = 255
|
0xff, // value = 255
|
||||||
}
|
}
|
||||||
|
|
||||||
|
customRecords = lnwire.CustomRecords{
|
||||||
|
lnwire.MinCustomRecordsTlvType + 1: []byte("custom data"),
|
||||||
|
}
|
||||||
|
|
||||||
|
blobBytes = []byte{
|
||||||
|
// Corresponds to the encoded version of the above custom
|
||||||
|
// records.
|
||||||
|
0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73, 0x74,
|
||||||
|
0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||||
|
}
|
||||||
|
|
||||||
testHTLCEntry = HTLCEntry{
|
testHTLCEntry = HTLCEntry{
|
||||||
RefundTimeout: 740_000,
|
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
|
||||||
OutputIndex: 10,
|
740_000,
|
||||||
Incoming: true,
|
),
|
||||||
Amt: 1000_000,
|
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
|
||||||
amtTlv: 1000_000,
|
10,
|
||||||
incomingTlv: 1,
|
),
|
||||||
|
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
|
||||||
|
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||||
|
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
|
||||||
|
),
|
||||||
|
CustomBlob: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType5](blobBytes),
|
||||||
|
),
|
||||||
|
HtlcIndex: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType6, uint16](0x33),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
testHTLCEntryBytes = []byte{
|
testHTLCEntryBytes = []byte{
|
||||||
// Body length 23.
|
// Body length 45.
|
||||||
0x16,
|
0x2d,
|
||||||
// Rhash tlv.
|
// Rhash tlv.
|
||||||
0x0, 0x0,
|
0x0, 0x0,
|
||||||
// RefundTimeout tlv.
|
// RefundTimeout tlv.
|
||||||
@ -54,6 +76,45 @@ var (
|
|||||||
0x3, 0x1, 0x1,
|
0x3, 0x1, 0x1,
|
||||||
// Amt tlv.
|
// Amt tlv.
|
||||||
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
|
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
|
||||||
|
// Custom blob tlv.
|
||||||
|
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||||
|
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||||
|
// HLTC index tlv.
|
||||||
|
0x6, 0x2, 0x0, 0x33,
|
||||||
|
}
|
||||||
|
|
||||||
|
testHTLCEntryHash = HTLCEntry{
|
||||||
|
RHash: tlv.NewPrimitiveRecord[tlv.TlvType0](NewSparsePayHash(
|
||||||
|
[32]byte{0x33, 0x44, 0x55},
|
||||||
|
)),
|
||||||
|
RefundTimeout: tlv.NewPrimitiveRecord[tlv.TlvType1, uint32](
|
||||||
|
740_000,
|
||||||
|
),
|
||||||
|
OutputIndex: tlv.NewPrimitiveRecord[tlv.TlvType2, uint16](
|
||||||
|
10,
|
||||||
|
),
|
||||||
|
Incoming: tlv.NewPrimitiveRecord[tlv.TlvType3](true),
|
||||||
|
Amt: tlv.NewRecordT[tlv.TlvType4](
|
||||||
|
tlv.NewBigSizeT(btcutil.Amount(1_000_000)),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
testHTLCEntryHashBytes = []byte{
|
||||||
|
// Body length 54.
|
||||||
|
0x36,
|
||||||
|
// Rhash tlv.
|
||||||
|
0x0, 0x20,
|
||||||
|
0x33, 0x44, 0x55, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
|
||||||
|
// RefundTimeout tlv.
|
||||||
|
0x1, 0x4, 0x0, 0xb, 0x4a, 0xa0,
|
||||||
|
// OutputIndex tlv.
|
||||||
|
0x2, 0x2, 0x0, 0xa,
|
||||||
|
// Incoming tlv.
|
||||||
|
0x3, 0x1, 0x1,
|
||||||
|
// Amt tlv.
|
||||||
|
0x4, 0x5, 0xfe, 0x0, 0xf, 0x42, 0x40,
|
||||||
}
|
}
|
||||||
|
|
||||||
localBalance = lnwire.MilliSatoshi(9000)
|
localBalance = lnwire.MilliSatoshi(9000)
|
||||||
@ -68,24 +129,29 @@ var (
|
|||||||
CommitTx: channels.TestFundingTx,
|
CommitTx: channels.TestFundingTx,
|
||||||
CommitSig: bytes.Repeat([]byte{1}, 71),
|
CommitSig: bytes.Repeat([]byte{1}, 71),
|
||||||
Htlcs: []HTLC{{
|
Htlcs: []HTLC{{
|
||||||
RefundTimeout: testHTLCEntry.RefundTimeout,
|
RefundTimeout: testHTLCEntry.RefundTimeout.Val,
|
||||||
OutputIndex: int32(testHTLCEntry.OutputIndex),
|
OutputIndex: int32(testHTLCEntry.OutputIndex.Val),
|
||||||
Incoming: testHTLCEntry.Incoming,
|
HtlcIndex: uint64(
|
||||||
Amt: lnwire.NewMSatFromSatoshis(
|
testHTLCEntry.HtlcIndex.ValOpt().
|
||||||
testHTLCEntry.Amt,
|
UnsafeFromSome(),
|
||||||
),
|
),
|
||||||
|
Incoming: testHTLCEntry.Incoming.Val,
|
||||||
|
Amt: lnwire.NewMSatFromSatoshis(
|
||||||
|
testHTLCEntry.Amt.Val.Int(),
|
||||||
|
),
|
||||||
|
CustomRecords: customRecords,
|
||||||
}},
|
}},
|
||||||
|
CustomBlob: fn.Some(blobBytes),
|
||||||
}
|
}
|
||||||
|
|
||||||
testRevocationLogNoAmts = RevocationLog{
|
testRevocationLogNoAmts = NewRevocationLog(
|
||||||
OurOutputIndex: 0,
|
0, 1, testChannelCommit.CommitTx.TxHash(),
|
||||||
TheirOutputIndex: 1,
|
fn.None[lnwire.MilliSatoshi](), fn.None[lnwire.MilliSatoshi](),
|
||||||
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
|
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
|
||||||
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
|
)
|
||||||
}
|
|
||||||
testRevocationLogNoAmtsBytes = []byte{
|
testRevocationLogNoAmtsBytes = []byte{
|
||||||
// Body length 42.
|
// Body length 61.
|
||||||
0x2a,
|
0x3d,
|
||||||
// OurOutputIndex tlv.
|
// OurOutputIndex tlv.
|
||||||
0x0, 0x2, 0x0, 0x0,
|
0x0, 0x2, 0x0, 0x0,
|
||||||
// TheirOutputIndex tlv.
|
// TheirOutputIndex tlv.
|
||||||
@ -96,19 +162,19 @@ var (
|
|||||||
0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6,
|
0x6e, 0x60, 0x29, 0x23, 0x1d, 0x5e, 0xc5, 0xe6,
|
||||||
0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff,
|
0xbd, 0xf7, 0xd3, 0x9b, 0x16, 0x7d, 0x0, 0xff,
|
||||||
0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd,
|
0xc8, 0x22, 0x51, 0xb1, 0x5b, 0xa0, 0xbf, 0xd,
|
||||||
|
// Custom blob tlv.
|
||||||
|
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||||
|
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||||
}
|
}
|
||||||
|
|
||||||
testRevocationLogWithAmts = RevocationLog{
|
testRevocationLogWithAmts = NewRevocationLog(
|
||||||
OurOutputIndex: 0,
|
0, 1, testChannelCommit.CommitTx.TxHash(),
|
||||||
TheirOutputIndex: 1,
|
fn.Some(localBalance), fn.Some(remoteBalance),
|
||||||
CommitTxHash: testChannelCommit.CommitTx.TxHash(),
|
[]*HTLCEntry{&testHTLCEntry}, fn.Some(blobBytes),
|
||||||
HTLCEntries: []*HTLCEntry{&testHTLCEntry},
|
)
|
||||||
OurBalance: &localBalance,
|
|
||||||
TheirBalance: &remoteBalance,
|
|
||||||
}
|
|
||||||
testRevocationLogWithAmtsBytes = []byte{
|
testRevocationLogWithAmtsBytes = []byte{
|
||||||
// Body length 52.
|
// Body length 71.
|
||||||
0x34,
|
0x47,
|
||||||
// OurOutputIndex tlv.
|
// OurOutputIndex tlv.
|
||||||
0x0, 0x2, 0x0, 0x0,
|
0x0, 0x2, 0x0, 0x0,
|
||||||
// TheirOutputIndex tlv.
|
// TheirOutputIndex tlv.
|
||||||
@ -123,6 +189,9 @@ var (
|
|||||||
0x3, 0x3, 0xfd, 0x23, 0x28,
|
0x3, 0x3, 0xfd, 0x23, 0x28,
|
||||||
// Remote Balance.
|
// Remote Balance.
|
||||||
0x4, 0x3, 0xfd, 0x0b, 0xb8,
|
0x4, 0x3, 0xfd, 0x0b, 0xb8,
|
||||||
|
// Custom blob tlv.
|
||||||
|
0x5, 0x11, 0xfe, 0x00, 0x01, 0x00, 0x01, 0x0b, 0x63, 0x75, 0x73,
|
||||||
|
0x74, 0x6f, 0x6d, 0x20, 0x64, 0x61, 0x74, 0x61,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -193,11 +262,6 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
|||||||
// Copy the testHTLCEntry.
|
// Copy the testHTLCEntry.
|
||||||
entry := testHTLCEntry
|
entry := testHTLCEntry
|
||||||
|
|
||||||
// Set the internal fields to empty values so we can test the bytes are
|
|
||||||
// padded.
|
|
||||||
entry.incomingTlv = 0
|
|
||||||
entry.amtTlv = 0
|
|
||||||
|
|
||||||
// Write the tlv stream.
|
// Write the tlv stream.
|
||||||
buf := bytes.NewBuffer([]byte{})
|
buf := bytes.NewBuffer([]byte{})
|
||||||
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
|
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
|
||||||
@ -207,6 +271,21 @@ func TestSerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
|||||||
require.Equal(t, testHTLCEntryBytes, buf.Bytes())
|
require.Equal(t, testHTLCEntryBytes, buf.Bytes())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestSerializeHTLCEntriesWithRHash(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
// Copy the testHTLCEntry.
|
||||||
|
entry := testHTLCEntryHash
|
||||||
|
|
||||||
|
// Write the tlv stream.
|
||||||
|
buf := bytes.NewBuffer([]byte{})
|
||||||
|
err := serializeHTLCEntries(buf, []*HTLCEntry{&entry})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Check the bytes are read as expected.
|
||||||
|
require.Equal(t, testHTLCEntryHashBytes, buf.Bytes())
|
||||||
|
}
|
||||||
|
|
||||||
func TestSerializeHTLCEntries(t *testing.T) {
|
func TestSerializeHTLCEntries(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@ -215,7 +294,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
|
|||||||
|
|
||||||
// Create a fake rHash.
|
// Create a fake rHash.
|
||||||
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
||||||
copy(entry.RHash[:], rHashBytes)
|
copy(entry.RHash.Val[:], rHashBytes)
|
||||||
|
|
||||||
// Construct the serialized bytes.
|
// Construct the serialized bytes.
|
||||||
//
|
//
|
||||||
@ -224,7 +303,7 @@ func TestSerializeHTLCEntries(t *testing.T) {
|
|||||||
partialBytes := testHTLCEntryBytes[3:]
|
partialBytes := testHTLCEntryBytes[3:]
|
||||||
|
|
||||||
// Write the total length and RHash tlv.
|
// Write the total length and RHash tlv.
|
||||||
expectedBytes := []byte{0x36, 0x0, 0x20}
|
expectedBytes := []byte{0x4d, 0x0, 0x20}
|
||||||
expectedBytes = append(expectedBytes, rHashBytes...)
|
expectedBytes = append(expectedBytes, rHashBytes...)
|
||||||
|
|
||||||
// Append the rest.
|
// Append the rest.
|
||||||
@ -269,7 +348,7 @@ func TestSerializeAndDeserializeRevLog(t *testing.T) {
|
|||||||
t, &test.revLog, test.revLogBytes,
|
t, &test.revLog, test.revLogBytes,
|
||||||
)
|
)
|
||||||
|
|
||||||
testDerializeRevocationLog(
|
testDeserializeRevocationLog(
|
||||||
t, &test.revLog, test.revLogBytes,
|
t, &test.revLog, test.revLogBytes,
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
@ -293,7 +372,7 @@ func testSerializeRevocationLog(t *testing.T, rl *RevocationLog,
|
|||||||
require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex])
|
require.Equal(t, revLogBytes, buf.Bytes()[:bodyIndex])
|
||||||
}
|
}
|
||||||
|
|
||||||
func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
func testDeserializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
||||||
revLogBytes []byte) {
|
revLogBytes []byte) {
|
||||||
|
|
||||||
// Construct the full bytes.
|
// Construct the full bytes.
|
||||||
@ -309,7 +388,7 @@ func testDerializeRevocationLog(t *testing.T, revLog *RevocationLog,
|
|||||||
require.Equal(t, *revLog, rl)
|
require.Equal(t, *revLog, rl)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
func TestDeserializeHTLCEntriesEmptyRHash(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Read the tlv stream.
|
// Read the tlv stream.
|
||||||
@ -322,7 +401,7 @@ func TestDerializeHTLCEntriesEmptyRHash(t *testing.T) {
|
|||||||
require.Equal(t, &testHTLCEntry, htlcs[0])
|
require.Equal(t, &testHTLCEntry, htlcs[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDerializeHTLCEntries(t *testing.T) {
|
func TestDeserializeHTLCEntries(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
// Copy the testHTLCEntry.
|
// Copy the testHTLCEntry.
|
||||||
@ -330,7 +409,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
|
|||||||
|
|
||||||
// Create a fake rHash.
|
// Create a fake rHash.
|
||||||
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
rHashBytes := bytes.Repeat([]byte{10}, 32)
|
||||||
copy(entry.RHash[:], rHashBytes)
|
copy(entry.RHash.Val[:], rHashBytes)
|
||||||
|
|
||||||
// Construct the serialized bytes.
|
// Construct the serialized bytes.
|
||||||
//
|
//
|
||||||
@ -339,7 +418,7 @@ func TestDerializeHTLCEntries(t *testing.T) {
|
|||||||
partialBytes := testHTLCEntryBytes[3:]
|
partialBytes := testHTLCEntryBytes[3:]
|
||||||
|
|
||||||
// Write the total length and RHash tlv.
|
// Write the total length and RHash tlv.
|
||||||
testBytes := append([]byte{0x36, 0x0, 0x20}, rHashBytes...)
|
testBytes := append([]byte{0x4d, 0x0, 0x20}, rHashBytes...)
|
||||||
|
|
||||||
// Append the rest.
|
// Append the rest.
|
||||||
testBytes = append(testBytes, partialBytes...)
|
testBytes = append(testBytes, partialBytes...)
|
||||||
@ -398,11 +477,11 @@ func TestDeleteLogBucket(t *testing.T) {
|
|||||||
|
|
||||||
err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
|
err = kvdb.Update(backend, func(tx kvdb.RwTx) error {
|
||||||
// Create the buckets.
|
// Create the buckets.
|
||||||
chanBucket, _, err := createTestRevocatoinLogBuckets(tx)
|
chanBucket, _, err := createTestRevocationLogBuckets(tx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create the buckets again should give us an error.
|
// Create the buckets again should give us an error.
|
||||||
_, _, err = createTestRevocatoinLogBuckets(tx)
|
_, _, err = createTestRevocationLogBuckets(tx)
|
||||||
require.ErrorIs(t, err, kvdb.ErrBucketExists)
|
require.ErrorIs(t, err, kvdb.ErrBucketExists)
|
||||||
|
|
||||||
// Delete both buckets.
|
// Delete both buckets.
|
||||||
@ -410,7 +489,7 @@ func TestDeleteLogBucket(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Create the buckets again should give us NO error.
|
// Create the buckets again should give us NO error.
|
||||||
_, _, err = createTestRevocatoinLogBuckets(tx)
|
_, _, err = createTestRevocationLogBuckets(tx)
|
||||||
return err
|
return err
|
||||||
}, func() {})
|
}, func() {})
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
@ -516,7 +595,7 @@ func TestPutRevocationLog(t *testing.T) {
|
|||||||
// Construct the testing db transaction.
|
// Construct the testing db transaction.
|
||||||
dbTx := func(tx kvdb.RwTx) (RevocationLog, error) {
|
dbTx := func(tx kvdb.RwTx) (RevocationLog, error) {
|
||||||
// Create the buckets.
|
// Create the buckets.
|
||||||
_, bucket, err := createTestRevocatoinLogBuckets(tx)
|
_, bucket, err := createTestRevocationLogBuckets(tx)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
// Save the log.
|
// Save the log.
|
||||||
@ -686,7 +765,7 @@ func TestFetchRevocationLogCompatible(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestRevocatoinLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
|
func createTestRevocationLogBuckets(tx kvdb.RwTx) (kvdb.RwBucket,
|
||||||
kvdb.RwBucket, error) {
|
kvdb.RwBucket, error) {
|
||||||
|
|
||||||
chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)
|
chanBucket, err := tx.CreateTopLevelBucket(openChannelBucket)
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -42,7 +42,7 @@ func parseTime(s string, base time.Time) (uint64, error) {
|
|||||||
|
|
||||||
var lightningPrefix = "lightning:"
|
var lightningPrefix = "lightning:"
|
||||||
|
|
||||||
// stripPrefix removes accidentally copied 'lightning:' prefix.
|
// StripPrefix removes accidentally copied 'lightning:' prefix.
|
||||||
func stripPrefix(s string) string {
|
func StripPrefix(s string) string {
|
||||||
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
return strings.TrimSpace(strings.TrimPrefix(s, lightningPrefix))
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"testing"
|
"testing"
|
||||||
@ -111,7 +111,7 @@ func TestStripPrefix(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
for _, test := range stripPrefixTests {
|
for _, test := range stripPrefixTests {
|
||||||
actual := stripPrefix(test.in)
|
actual := StripPrefix(test.in)
|
||||||
require.Equal(t, test.expected, actual)
|
require.Equal(t, test.expected, actual)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build autopilotrpc
|
//go:build autopilotrpc
|
||||||
// +build autopilotrpc
|
// +build autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/autopilotrpc"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !autopilotrpc
|
//go:build !autopilotrpc
|
||||||
// +build !autopilotrpc
|
// +build !autopilotrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build chainrpc
|
//go:build chainrpc
|
||||||
// +build chainrpc
|
// +build chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !chainrpc
|
//go:build !chainrpc
|
||||||
// +build !chainrpc
|
// +build !chainrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -9,7 +9,7 @@ import (
|
|||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
var addInvoiceCommand = cli.Command{
|
var AddInvoiceCommand = cli.Command{
|
||||||
Name: "addinvoice",
|
Name: "addinvoice",
|
||||||
Category: "Invoices",
|
Category: "Invoices",
|
||||||
Usage: "Add a new invoice.",
|
Usage: "Add a new invoice.",
|
||||||
@ -408,7 +408,7 @@ func decodePayReq(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
resp, err := client.DecodePayReq(ctxc, &lnrpc.PayReqString{
|
||||||
PayReq: stripPrefix(payreq),
|
PayReq: StripPrefix(payreq),
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -265,6 +265,7 @@ func setCfg(ctx *cli.Context) error {
|
|||||||
Config: mcCfg.Config,
|
Config: mcCfg.Config,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -366,5 +367,6 @@ func resetMissionControl(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req := &routerrpc.ResetMissionControlRequest{}
|
req := &routerrpc.ResetMissionControlRequest{}
|
||||||
_, err := client.ResetMissionControl(ctxc, req)
|
_, err := client.ResetMissionControl(ctxc, req)
|
||||||
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/record"
|
"github.com/lightningnetwork/lnd/record"
|
||||||
"github.com/lightningnetwork/lnd/routing/route"
|
"github.com/lightningnetwork/lnd/routing/route"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
"google.golang.org/grpc"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -152,8 +153,8 @@ var (
|
|||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
// paymentFlags returns common flags for sendpayment and payinvoice.
|
// PaymentFlags returns common flags for sendpayment and payinvoice.
|
||||||
func paymentFlags() []cli.Flag {
|
func PaymentFlags() []cli.Flag {
|
||||||
return []cli.Flag{
|
return []cli.Flag{
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "pay_req",
|
Name: "pay_req",
|
||||||
@ -202,7 +203,7 @@ func paymentFlags() []cli.Flag {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendPaymentCommand = cli.Command{
|
var SendPaymentCommand = cli.Command{
|
||||||
Name: "sendpayment",
|
Name: "sendpayment",
|
||||||
Category: "Payments",
|
Category: "Payments",
|
||||||
Usage: "Send a payment over lightning.",
|
Usage: "Send a payment over lightning.",
|
||||||
@ -226,7 +227,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
`,
|
`,
|
||||||
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
ArgsUsage: "dest amt payment_hash final_cltv_delta pay_addr | " +
|
||||||
"--pay_req=R [--pay_addr=H]",
|
"--pay_req=R [--pay_addr=H]",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "dest, d",
|
Name: "dest, d",
|
||||||
Usage: "the compressed identity pubkey of the " +
|
Usage: "the compressed identity pubkey of the " +
|
||||||
@ -253,7 +254,7 @@ var sendPaymentCommand = cli.Command{
|
|||||||
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
Usage: "will generate a pre-image and encode it in the sphinx packet, a dest must be set [experimental]",
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
Action: sendPayment,
|
Action: SendPayment,
|
||||||
}
|
}
|
||||||
|
|
||||||
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
// retrieveFeeLimit retrieves the fee limit based on the different fee limit
|
||||||
@ -324,20 +325,23 @@ func parsePayAddr(ctx *cli.Context, args cli.Args) ([]byte, error) {
|
|||||||
return payAddr, nil
|
return payAddr, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPayment(ctx *cli.Context) error {
|
func SendPayment(ctx *cli.Context) error {
|
||||||
// Show command help if no arguments provided
|
// Show command help if no arguments provided
|
||||||
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
if ctx.NArg() == 0 && ctx.NumFlags() == 0 {
|
||||||
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
_ = cli.ShowCommandHelp(ctx, "sendpayment")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
args := ctx.Args()
|
args := ctx.Args()
|
||||||
|
|
||||||
// If a payment request was provided, we can exit early since all of the
|
// If a payment request was provided, we can exit early since all of the
|
||||||
// details of the payment are encoded within the request.
|
// details of the payment are encoded within the request.
|
||||||
if ctx.IsSet("pay_req") {
|
if ctx.IsSet("pay_req") {
|
||||||
req := &routerrpc.SendPaymentRequest{
|
req := &routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: stripPrefix(ctx.String("pay_req")),
|
PaymentRequest: StripPrefix(ctx.String("pay_req")),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
Amp: ctx.Bool(ampFlag.Name),
|
Amp: ctx.Bool(ampFlag.Name),
|
||||||
@ -357,7 +361,9 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(
|
||||||
|
ctx, req, conn, conn, routerRPCSendPayment,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -466,19 +472,29 @@ func sendPayment(ctx *cli.Context) error {
|
|||||||
|
|
||||||
req.PaymentAddr = payAddr
|
req.PaymentAddr = payAddr
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
func sendPaymentRequest(ctx *cli.Context,
|
// SendPaymentFn is a function type that abstracts the SendPaymentV2 call of the
|
||||||
req *routerrpc.SendPaymentRequest) error {
|
// router client.
|
||||||
|
type SendPaymentFn func(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||||
|
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error)
|
||||||
|
|
||||||
|
// routerRPCSendPayment is the default implementation of the SendPaymentFn type
|
||||||
|
// that uses the lnd routerrpc.SendPaymentV2 call.
|
||||||
|
func routerRPCSendPayment(ctx context.Context, payConn grpc.ClientConnInterface,
|
||||||
|
req *routerrpc.SendPaymentRequest) (PaymentResultStream, error) {
|
||||||
|
|
||||||
|
return routerrpc.NewRouterClient(payConn).SendPaymentV2(ctx, req)
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendPaymentRequest(ctx *cli.Context, req *routerrpc.SendPaymentRequest,
|
||||||
|
lnConn, paymentConn grpc.ClientConnInterface,
|
||||||
|
callSendPayment SendPaymentFn) error {
|
||||||
|
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
|
|
||||||
conn := getClientConn(ctx, false)
|
lnClient := lnrpc.NewLightningClient(lnConn)
|
||||||
defer conn.Close()
|
|
||||||
|
|
||||||
client := lnrpc.NewLightningClient(conn)
|
|
||||||
routerClient := routerrpc.NewRouterClient(conn)
|
|
||||||
|
|
||||||
outChan := ctx.Int64Slice("outgoing_chan_id")
|
outChan := ctx.Int64Slice("outgoing_chan_id")
|
||||||
if len(outChan) != 0 {
|
if len(outChan) != 0 {
|
||||||
@ -558,7 +574,7 @@ func sendPaymentRequest(ctx *cli.Context,
|
|||||||
if req.PaymentRequest != "" {
|
if req.PaymentRequest != "" {
|
||||||
// Decode payment request to find out the amount.
|
// Decode payment request to find out the amount.
|
||||||
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
decodeReq := &lnrpc.PayReqString{PayReq: req.PaymentRequest}
|
||||||
decodeResp, err := client.DecodePayReq(ctxc, decodeReq)
|
decodeResp, err := lnClient.DecodePayReq(ctxc, decodeReq)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -602,14 +618,12 @@ func sendPaymentRequest(ctx *cli.Context,
|
|||||||
printJSON := ctx.Bool(jsonFlag.Name)
|
printJSON := ctx.Bool(jsonFlag.Name)
|
||||||
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
req.NoInflightUpdates = !ctx.Bool(inflightUpdatesFlag.Name) && printJSON
|
||||||
|
|
||||||
stream, err := routerClient.SendPaymentV2(ctxc, req)
|
stream, err := callSendPayment(ctxc, paymentConn, req)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
finalState, err := printLivePayment(
|
finalState, err := PrintLivePayment(ctxc, stream, lnClient, printJSON)
|
||||||
ctxc, stream, client, printJSON,
|
|
||||||
)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -667,24 +681,29 @@ func trackPayment(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
client := lnrpc.NewLightningClient(conn)
|
client := lnrpc.NewLightningClient(conn)
|
||||||
_, err = printLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
_, err = PrintLivePayment(ctxc, stream, client, ctx.Bool(jsonFlag.Name))
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// printLivePayment receives payment updates from the given stream and either
|
// PaymentResultStream is an interface that abstracts the Recv method of the
|
||||||
|
// SendPaymentV2 or TrackPaymentV2 client stream.
|
||||||
|
type PaymentResultStream interface {
|
||||||
|
Recv() (*lnrpc.Payment, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PrintLivePayment receives payment updates from the given stream and either
|
||||||
// outputs them as json or as a more user-friendly formatted table. The table
|
// outputs them as json or as a more user-friendly formatted table. The table
|
||||||
// option uses terminal control codes to rewrite the output. This call
|
// option uses terminal control codes to rewrite the output. This call
|
||||||
// terminates when the payment reaches a final state.
|
// terminates when the payment reaches a final state.
|
||||||
func printLivePayment(ctxc context.Context,
|
func PrintLivePayment(ctxc context.Context, stream PaymentResultStream,
|
||||||
stream routerrpc.Router_TrackPaymentV2Client,
|
lnClient lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
||||||
client lnrpc.LightningClient, json bool) (*lnrpc.Payment, error) {
|
|
||||||
|
|
||||||
// Terminal escape codes aren't supported on Windows, fall back to json.
|
// Terminal escape codes aren't supported on Windows, fall back to json.
|
||||||
if !json && runtime.GOOS == "windows" {
|
if !json && runtime.GOOS == "windows" {
|
||||||
json = true
|
json = true
|
||||||
}
|
}
|
||||||
|
|
||||||
aliases := newAliasCache(client)
|
aliases := newAliasCache(lnClient)
|
||||||
|
|
||||||
first := true
|
first := true
|
||||||
var lastLineCount int
|
var lastLineCount int
|
||||||
@ -706,17 +725,17 @@ func printLivePayment(ctxc context.Context,
|
|||||||
// Write raw json to stdout.
|
// Write raw json to stdout.
|
||||||
printRespJSON(payment)
|
printRespJSON(payment)
|
||||||
} else {
|
} else {
|
||||||
table := formatPayment(ctxc, payment, aliases)
|
resultTable := formatPayment(ctxc, payment, aliases)
|
||||||
|
|
||||||
// Clear all previously written lines and print the
|
// Clear all previously written lines and print the
|
||||||
// updated table.
|
// updated table.
|
||||||
clearLines(lastLineCount)
|
clearLines(lastLineCount)
|
||||||
fmt.Print(table)
|
fmt.Print(resultTable)
|
||||||
|
|
||||||
// Store the number of lines written for the next update
|
// Store the number of lines written for the next update
|
||||||
// pass.
|
// pass.
|
||||||
lastLineCount = 0
|
lastLineCount = 0
|
||||||
for _, b := range table {
|
for _, b := range resultTable {
|
||||||
if b == '\n' {
|
if b == '\n' {
|
||||||
lastLineCount++
|
lastLineCount++
|
||||||
}
|
}
|
||||||
@ -874,7 +893,7 @@ var payInvoiceCommand = cli.Command{
|
|||||||
This command is a shortcut for 'sendpayment --pay_req='.
|
This command is a shortcut for 'sendpayment --pay_req='.
|
||||||
`,
|
`,
|
||||||
ArgsUsage: "pay_req",
|
ArgsUsage: "pay_req",
|
||||||
Flags: append(paymentFlags(),
|
Flags: append(PaymentFlags(),
|
||||||
cli.Int64Flag{
|
cli.Int64Flag{
|
||||||
Name: "amt",
|
Name: "amt",
|
||||||
Usage: "(optional) number of satoshis to fulfill the " +
|
Usage: "(optional) number of satoshis to fulfill the " +
|
||||||
@ -885,6 +904,9 @@ var payInvoiceCommand = cli.Command{
|
|||||||
}
|
}
|
||||||
|
|
||||||
func payInvoice(ctx *cli.Context) error {
|
func payInvoice(ctx *cli.Context) error {
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
defer conn.Close()
|
||||||
|
|
||||||
args := ctx.Args()
|
args := ctx.Args()
|
||||||
|
|
||||||
var payReq string
|
var payReq string
|
||||||
@ -898,14 +920,14 @@ func payInvoice(ctx *cli.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
req := &routerrpc.SendPaymentRequest{
|
req := &routerrpc.SendPaymentRequest{
|
||||||
PaymentRequest: stripPrefix(payReq),
|
PaymentRequest: StripPrefix(payReq),
|
||||||
Amt: ctx.Int64("amt"),
|
Amt: ctx.Int64("amt"),
|
||||||
DestCustomRecords: make(map[uint64][]byte),
|
DestCustomRecords: make(map[uint64][]byte),
|
||||||
Amp: ctx.Bool(ampFlag.Name),
|
Amp: ctx.Bool(ampFlag.Name),
|
||||||
Cancelable: ctx.Bool(cancelableFlag.Name),
|
Cancelable: ctx.Bool(cancelableFlag.Name),
|
||||||
}
|
}
|
||||||
|
|
||||||
return sendPaymentRequest(ctx, req)
|
return SendPaymentRequest(ctx, req, conn, conn, routerRPCSendPayment)
|
||||||
}
|
}
|
||||||
|
|
||||||
var sendToRouteCommand = cli.Command{
|
var sendToRouteCommand = cli.Command{
|
||||||
@ -1900,7 +1922,7 @@ func estimateRouteFee(ctx *cli.Context) error {
|
|||||||
req.AmtSat = amtSat
|
req.AmtSat = amtSat
|
||||||
|
|
||||||
case ctx.IsSet("pay_req"):
|
case ctx.IsSet("pay_req"):
|
||||||
req.PaymentRequest = stripPrefix(ctx.String("pay_req"))
|
req.PaymentRequest = StripPrefix(ctx.String("pay_req"))
|
||||||
if ctx.IsSet("timeout") {
|
if ctx.IsSet("timeout") {
|
||||||
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
req.Timeout = uint32(ctx.Duration("timeout").Seconds())
|
||||||
}
|
}
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bufio"
|
"bufio"
|
||||||
@ -11,6 +11,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"math"
|
"math"
|
||||||
"os"
|
"os"
|
||||||
|
"regexp"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -41,8 +42,49 @@ const (
|
|||||||
defaultUtxoMinConf = 1
|
defaultUtxoMinConf = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
var errBadChanPoint = errors.New("expecting chan_point to be in format of: " +
|
var (
|
||||||
"txid:index")
|
errBadChanPoint = errors.New(
|
||||||
|
"expecting chan_point to be in format of: txid:index",
|
||||||
|
)
|
||||||
|
|
||||||
|
customDataPattern = regexp.MustCompile(
|
||||||
|
`"custom_channel_data":\s*"([0-9a-f]+)"`,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
// replaceCustomData replaces the custom channel data hex string with the
|
||||||
|
// decoded custom channel data in the JSON response.
|
||||||
|
func replaceCustomData(jsonBytes []byte) []byte {
|
||||||
|
// If there's nothing to replace, return the original JSON.
|
||||||
|
if !customDataPattern.Match(jsonBytes) {
|
||||||
|
return jsonBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
replacedBytes := customDataPattern.ReplaceAllFunc(
|
||||||
|
jsonBytes, func(match []byte) []byte {
|
||||||
|
encoded := customDataPattern.FindStringSubmatch(
|
||||||
|
string(match),
|
||||||
|
)[1]
|
||||||
|
decoded, err := hex.DecodeString(encoded)
|
||||||
|
if err != nil {
|
||||||
|
return match
|
||||||
|
}
|
||||||
|
|
||||||
|
return []byte("\"custom_channel_data\":" +
|
||||||
|
string(decoded))
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
var buf bytes.Buffer
|
||||||
|
err := json.Indent(&buf, replacedBytes, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
// If we can't indent the JSON, it likely means the replacement
|
||||||
|
// data wasn't correct, so we return the original JSON.
|
||||||
|
return jsonBytes
|
||||||
|
}
|
||||||
|
|
||||||
|
return buf.Bytes()
|
||||||
|
}
|
||||||
|
|
||||||
func getContext() context.Context {
|
func getContext() context.Context {
|
||||||
shutdownInterceptor, err := signal.Intercept()
|
shutdownInterceptor, err := signal.Intercept()
|
||||||
@ -66,9 +108,9 @@ func printJSON(resp interface{}) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var out bytes.Buffer
|
var out bytes.Buffer
|
||||||
json.Indent(&out, b, "", "\t")
|
_ = json.Indent(&out, b, "", " ")
|
||||||
out.WriteString("\n")
|
_, _ = out.WriteString("\n")
|
||||||
out.WriteTo(os.Stdout)
|
_, _ = out.WriteTo(os.Stdout)
|
||||||
}
|
}
|
||||||
|
|
||||||
func printRespJSON(resp proto.Message) {
|
func printRespJSON(resp proto.Message) {
|
||||||
@ -78,7 +120,9 @@ func printRespJSON(resp proto.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s\n", jsonBytes)
|
jsonBytesReplaced := replaceCustomData(jsonBytes)
|
||||||
|
|
||||||
|
fmt.Printf("%s\n", jsonBytesReplaced)
|
||||||
}
|
}
|
||||||
|
|
||||||
// actionDecorator is used to add additional information and error handling
|
// actionDecorator is used to add additional information and error handling
|
||||||
@ -1442,15 +1486,15 @@ func walletBalance(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var channelBalanceCommand = cli.Command{
|
var ChannelBalanceCommand = cli.Command{
|
||||||
Name: "channelbalance",
|
Name: "channelbalance",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "Returns the sum of the total available channel balance across " +
|
Usage: "Returns the sum of the total available channel balance across " +
|
||||||
"all open channels.",
|
"all open channels.",
|
||||||
Action: actionDecorator(channelBalance),
|
Action: actionDecorator(ChannelBalance),
|
||||||
}
|
}
|
||||||
|
|
||||||
func channelBalance(ctx *cli.Context) error {
|
func ChannelBalance(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@ -1575,7 +1619,7 @@ func pendingChannels(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var listChannelsCommand = cli.Command{
|
var ListChannelsCommand = cli.Command{
|
||||||
Name: "listchannels",
|
Name: "listchannels",
|
||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all open channels.",
|
Usage: "List all open channels.",
|
||||||
@ -1608,7 +1652,7 @@ var listChannelsCommand = cli.Command{
|
|||||||
"order to improve performance",
|
"order to improve performance",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
Action: actionDecorator(listChannels),
|
Action: actionDecorator(ListChannels),
|
||||||
}
|
}
|
||||||
|
|
||||||
var listAliasesCommand = cli.Command{
|
var listAliasesCommand = cli.Command{
|
||||||
@ -1616,10 +1660,10 @@ var listAliasesCommand = cli.Command{
|
|||||||
Category: "Channels",
|
Category: "Channels",
|
||||||
Usage: "List all aliases.",
|
Usage: "List all aliases.",
|
||||||
Flags: []cli.Flag{},
|
Flags: []cli.Flag{},
|
||||||
Action: actionDecorator(listaliases),
|
Action: actionDecorator(listAliases),
|
||||||
}
|
}
|
||||||
|
|
||||||
func listaliases(ctx *cli.Context) error {
|
func listAliases(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@ -1636,7 +1680,7 @@ func listaliases(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func listChannels(ctx *cli.Context) error {
|
func ListChannels(ctx *cli.Context) error {
|
||||||
ctxc := getContext()
|
ctxc := getContext()
|
||||||
client, cleanUp := getClient(ctx)
|
client, cleanUp := getClient(ctx)
|
||||||
defer cleanUp()
|
defer cleanUp()
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -120,3 +120,74 @@ func TestParseTimeLockDelta(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestReplaceCustomData tests that hex encoded custom data can be formatted as
|
||||||
|
// JSON in the console output.
|
||||||
|
func TestReplaceCustomData(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := []struct {
|
||||||
|
name string
|
||||||
|
data string
|
||||||
|
replaceData string
|
||||||
|
expected string
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "no replacement necessary",
|
||||||
|
data: "foo",
|
||||||
|
expected: "foo",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json with replacement",
|
||||||
|
data: "{\"foo\":\"bar\",\"custom_channel_data\":\"" +
|
||||||
|
hex.EncodeToString([]byte(
|
||||||
|
"{\"bar\":\"baz\"}",
|
||||||
|
)) + "\"}",
|
||||||
|
expected: `{
|
||||||
|
"foo": "bar",
|
||||||
|
"custom_channel_data": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json with replacement and space",
|
||||||
|
data: "{\"foo\":\"bar\",\"custom_channel_data\": \"" +
|
||||||
|
hex.EncodeToString([]byte(
|
||||||
|
"{\"bar\":\"baz\"}",
|
||||||
|
)) + "\"}",
|
||||||
|
expected: `{
|
||||||
|
"foo": "bar",
|
||||||
|
"custom_channel_data": {
|
||||||
|
"bar": "baz"
|
||||||
|
}
|
||||||
|
}`,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "doesn't match pattern, returned identical",
|
||||||
|
data: "this ain't even json, and no custom data " +
|
||||||
|
"either",
|
||||||
|
expected: "this ain't even json, and no custom data " +
|
||||||
|
"either",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid json",
|
||||||
|
data: "this ain't json, " +
|
||||||
|
"\"custom_channel_data\":\"a\"",
|
||||||
|
expected: "this ain't json, " +
|
||||||
|
"\"custom_channel_data\":\"a\"",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "valid json, invalid hex, just formatted",
|
||||||
|
data: "{\"custom_channel_data\":\"f\"}",
|
||||||
|
expected: "{\n \"custom_channel_data\": \"f\"\n}",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tc := range testCases {
|
||||||
|
t.Run(tc.name, func(t *testing.T) {
|
||||||
|
result := replaceCustomData([]byte(tc.data))
|
||||||
|
require.Equal(t, tc.expected, string(result))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build dev
|
//go:build dev
|
||||||
// +build dev
|
// +build dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !dev
|
//go:build !dev
|
||||||
// +build !dev
|
// +build !dev
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build invoicesrpc
|
//go:build invoicesrpc
|
||||||
// +build invoicesrpc
|
// +build invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !invoicesrpc
|
//go:build !invoicesrpc
|
||||||
// +build !invoicesrpc
|
// +build !invoicesrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
601
cmd/commands/main.go
Normal file
601
cmd/commands/main.go
Normal file
@ -0,0 +1,601 @@
|
|||||||
|
// Copyright (c) 2013-2017 The btcsuite developers
|
||||||
|
// Copyright (c) 2015-2016 The Decred developers
|
||||||
|
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||||
|
|
||||||
|
package commands
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcutil"
|
||||||
|
"github.com/btcsuite/btcd/chaincfg"
|
||||||
|
"github.com/lightningnetwork/lnd"
|
||||||
|
"github.com/lightningnetwork/lnd/build"
|
||||||
|
"github.com/lightningnetwork/lnd/lncfg"
|
||||||
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
|
"github.com/lightningnetwork/lnd/tor"
|
||||||
|
"github.com/urfave/cli"
|
||||||
|
"golang.org/x/term"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
"google.golang.org/grpc/credentials"
|
||||||
|
"google.golang.org/grpc/metadata"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
defaultDataDir = "data"
|
||||||
|
defaultChainSubDir = "chain"
|
||||||
|
defaultTLSCertFilename = "tls.cert"
|
||||||
|
defaultMacaroonFilename = "admin.macaroon"
|
||||||
|
defaultRPCPort = "10009"
|
||||||
|
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
||||||
|
|
||||||
|
envVarRPCServer = "LNCLI_RPCSERVER"
|
||||||
|
envVarLNDDir = "LNCLI_LNDDIR"
|
||||||
|
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
||||||
|
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
||||||
|
envVarChain = "LNCLI_CHAIN"
|
||||||
|
envVarNetwork = "LNCLI_NETWORK"
|
||||||
|
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
||||||
|
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
||||||
|
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
||||||
|
envVarProfile = "LNCLI_PROFILE"
|
||||||
|
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
DefaultLndDir = btcutil.AppDataDir("lnd", false)
|
||||||
|
defaultTLSCertPath = filepath.Join(
|
||||||
|
DefaultLndDir, defaultTLSCertFilename,
|
||||||
|
)
|
||||||
|
|
||||||
|
// maxMsgRecvSize is the largest message our client will receive. We
|
||||||
|
// set this to 200MiB atm.
|
||||||
|
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
||||||
|
)
|
||||||
|
|
||||||
|
func fatal(err error) {
|
||||||
|
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
|
||||||
|
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient,
|
||||||
|
func()) {
|
||||||
|
|
||||||
|
conn := getClientConn(ctx, true)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
||||||
|
conn := getClientConn(ctx, true)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewStateClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
||||||
|
conn := getClientConn(ctx, false)
|
||||||
|
|
||||||
|
cleanUp := func() {
|
||||||
|
conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
return lnrpc.NewLightningClient(conn), cleanUp
|
||||||
|
}
|
||||||
|
|
||||||
|
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
||||||
|
// First, we'll get the selected stored profile or an ephemeral one
|
||||||
|
// created from the global options in the CLI context.
|
||||||
|
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not load global options: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create a dial options array.
|
||||||
|
opts := []grpc.DialOption{
|
||||||
|
grpc.WithUnaryInterceptor(
|
||||||
|
addMetadataUnaryInterceptor(profile.Metadata),
|
||||||
|
),
|
||||||
|
grpc.WithStreamInterceptor(
|
||||||
|
addMetaDataStreamInterceptor(profile.Metadata),
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
if profile.Insecure {
|
||||||
|
opts = append(opts, grpc.WithInsecure())
|
||||||
|
} else {
|
||||||
|
// Load the specified TLS certificate.
|
||||||
|
certPool, err := profile.cert()
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build transport credentials from the certificate pool. If
|
||||||
|
// there is no certificate pool, we expect the server to use a
|
||||||
|
// non-self-signed certificate such as a certificate obtained
|
||||||
|
// from Let's Encrypt.
|
||||||
|
var creds credentials.TransportCredentials
|
||||||
|
if certPool != nil {
|
||||||
|
creds = credentials.NewClientTLSFromCert(certPool, "")
|
||||||
|
} else {
|
||||||
|
// Fallback to the system pool. Using an empty tls
|
||||||
|
// config is an alternative to x509.SystemCertPool().
|
||||||
|
// That call is not supported on Windows.
|
||||||
|
creds = credentials.NewTLS(&tls.Config{})
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, grpc.WithTransportCredentials(creds))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Only process macaroon credentials if --no-macaroons isn't set and
|
||||||
|
// if we're not skipping macaroon processing.
|
||||||
|
if !profile.NoMacaroons && !skipMacaroons {
|
||||||
|
// Find out which macaroon to load.
|
||||||
|
macName := profile.Macaroons.Default
|
||||||
|
if ctx.GlobalIsSet("macfromjar") {
|
||||||
|
macName = ctx.GlobalString("macfromjar")
|
||||||
|
}
|
||||||
|
var macEntry *macaroonEntry
|
||||||
|
for _, entry := range profile.Macaroons.Jar {
|
||||||
|
if entry.Name == macName {
|
||||||
|
macEntry = entry
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if macEntry == nil {
|
||||||
|
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
||||||
|
"in profile", macName))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get and possibly decrypt the specified macaroon.
|
||||||
|
//
|
||||||
|
// TODO(guggero): Make it possible to cache the password so we
|
||||||
|
// don't need to ask for it every time.
|
||||||
|
mac, err := macEntry.loadMacaroon(readPassword)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
macConstraints := []macaroons.Constraint{
|
||||||
|
// We add a time-based constraint to prevent replay of
|
||||||
|
// the macaroon. It's good for 60 seconds by default to
|
||||||
|
// make up for any discrepancy between client and server
|
||||||
|
// clocks, but leaking the macaroon before it becomes
|
||||||
|
// invalid makes it possible for an attacker to reuse
|
||||||
|
// the macaroon. In addition, the validity time of the
|
||||||
|
// macaroon is extended by the time the server clock is
|
||||||
|
// behind the client clock, or shortened by the time the
|
||||||
|
// server clock is ahead of the client clock (or invalid
|
||||||
|
// altogether if, in the latter case, this time is more
|
||||||
|
// than 60 seconds).
|
||||||
|
// TODO(aakselrod): add better anti-replay protection.
|
||||||
|
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
||||||
|
|
||||||
|
// Lock macaroon down to a specific IP address.
|
||||||
|
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
||||||
|
|
||||||
|
// ... Add more constraints if needed.
|
||||||
|
}
|
||||||
|
|
||||||
|
// Apply constraints to the macaroon.
|
||||||
|
constrainedMac, err := macaroons.AddConstraints(
|
||||||
|
mac, macConstraints...,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Now we append the macaroon credentials to the dial options.
|
||||||
|
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("error cloning mac: %w", err))
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
||||||
|
}
|
||||||
|
|
||||||
|
// If a socksproxy server is specified we use a tor dialer
|
||||||
|
// to connect to the grpc server.
|
||||||
|
if ctx.GlobalIsSet("socksproxy") {
|
||||||
|
socksProxy := ctx.GlobalString("socksproxy")
|
||||||
|
torDialer := func(_ context.Context, addr string) (net.Conn,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
return tor.Dial(
|
||||||
|
addr, socksProxy, false, false,
|
||||||
|
tor.DefaultConnTimeout,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
opts = append(opts, grpc.WithContextDialer(torDialer))
|
||||||
|
} else {
|
||||||
|
// We need to use a custom dialer so we can also connect to
|
||||||
|
// unix sockets and not just TCP addresses.
|
||||||
|
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
||||||
|
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
||||||
|
}
|
||||||
|
|
||||||
|
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
||||||
|
|
||||||
|
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
||||||
|
if err != nil {
|
||||||
|
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
||||||
|
}
|
||||||
|
|
||||||
|
return conn
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
||||||
|
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||||
|
// unary call.
|
||||||
|
func addMetadataUnaryInterceptor(
|
||||||
|
md map[string]string) grpc.UnaryClientInterceptor {
|
||||||
|
|
||||||
|
return func(ctx context.Context, method string, req, reply interface{},
|
||||||
|
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
||||||
|
opts ...grpc.CallOption) error {
|
||||||
|
|
||||||
|
outCtx := contextWithMetadata(ctx, md)
|
||||||
|
return invoker(outCtx, method, req, reply, cc, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
||||||
|
// appends any key-value metadata strings to the outgoing context of a grpc
|
||||||
|
// stream call.
|
||||||
|
func addMetaDataStreamInterceptor(
|
||||||
|
md map[string]string) grpc.StreamClientInterceptor {
|
||||||
|
|
||||||
|
return func(ctx context.Context, desc *grpc.StreamDesc,
|
||||||
|
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
||||||
|
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
||||||
|
|
||||||
|
outCtx := contextWithMetadata(ctx, md)
|
||||||
|
return streamer(outCtx, desc, cc, method, opts...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// contextWithMetaData appends the given metadata key-value pairs to the given
|
||||||
|
// context.
|
||||||
|
func contextWithMetadata(ctx context.Context,
|
||||||
|
md map[string]string) context.Context {
|
||||||
|
|
||||||
|
kvPairs := make([]string, 0, 2*len(md))
|
||||||
|
for k, v := range md {
|
||||||
|
kvPairs = append(kvPairs, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
||||||
|
// command.
|
||||||
|
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
||||||
|
network := strings.ToLower(ctx.GlobalString("network"))
|
||||||
|
switch network {
|
||||||
|
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
||||||
|
default:
|
||||||
|
return "", "", fmt.Errorf("unknown network: %v", network)
|
||||||
|
}
|
||||||
|
|
||||||
|
// We'll now fetch the lnddir so we can make a decision on how to
|
||||||
|
// properly read the macaroons (if needed) and also the cert. This will
|
||||||
|
// either be the default, or will have been overwritten by the end
|
||||||
|
// user.
|
||||||
|
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
||||||
|
|
||||||
|
// If the macaroon path as been manually provided, then we'll only
|
||||||
|
// target the specified file.
|
||||||
|
var macPath string
|
||||||
|
if ctx.GlobalString("macaroonpath") != "" {
|
||||||
|
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString(
|
||||||
|
"macaroonpath",
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
// Otherwise, we'll go into the path:
|
||||||
|
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
||||||
|
// macaroon that we need.
|
||||||
|
macPath = filepath.Join(
|
||||||
|
lndDir, defaultDataDir, defaultChainSubDir,
|
||||||
|
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
||||||
|
|
||||||
|
// If a custom lnd directory was set, we'll also check if custom paths
|
||||||
|
// for the TLS cert and macaroon file were set as well. If not, we'll
|
||||||
|
// override their paths so they can be found within the custom lnd
|
||||||
|
// directory set. This allows us to set a custom lnd directory, along
|
||||||
|
// with custom paths to the TLS cert and macaroon file.
|
||||||
|
if lndDir != DefaultLndDir {
|
||||||
|
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsCertPath, macPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
||||||
|
// or flag b can be set, but not both. It returns the name of the flag or an
|
||||||
|
// error.
|
||||||
|
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
||||||
|
if ctx.IsSet(a) && ctx.IsSet(b) {
|
||||||
|
return "", fmt.Errorf(
|
||||||
|
"either %s or %s should be set, but not both", a, b,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
if ctx.IsSet(a) {
|
||||||
|
return a, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return b, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func Main() {
|
||||||
|
app := cli.NewApp()
|
||||||
|
app.Name = "lncli"
|
||||||
|
app.Version = build.Version() + " commit=" + build.Commit
|
||||||
|
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
||||||
|
app.Flags = []cli.Flag{
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "rpcserver",
|
||||||
|
Value: defaultRPCHostPort,
|
||||||
|
Usage: "The host:port of LN daemon.",
|
||||||
|
EnvVar: envVarRPCServer,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "lnddir",
|
||||||
|
Value: DefaultLndDir,
|
||||||
|
Usage: "The path to lnd's base directory.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarLNDDir,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "socksproxy",
|
||||||
|
Usage: "The host:port of a SOCKS proxy through " +
|
||||||
|
"which all connections to the LN " +
|
||||||
|
"daemon will be established over.",
|
||||||
|
EnvVar: envVarSOCKSProxy,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "tlscertpath",
|
||||||
|
Value: defaultTLSCertPath,
|
||||||
|
Usage: "The path to lnd's TLS certificate.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarTLSCertPath,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "chain, c",
|
||||||
|
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
||||||
|
Value: "bitcoin",
|
||||||
|
EnvVar: envVarChain,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "network, n",
|
||||||
|
Usage: "The network lnd is running on, e.g. mainnet, " +
|
||||||
|
"testnet, etc.",
|
||||||
|
Value: "mainnet",
|
||||||
|
EnvVar: envVarNetwork,
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "no-macaroons",
|
||||||
|
Usage: "Disable macaroon authentication.",
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macaroonpath",
|
||||||
|
Usage: "The path to macaroon file.",
|
||||||
|
TakesFile: true,
|
||||||
|
EnvVar: envVarMacaroonPath,
|
||||||
|
},
|
||||||
|
cli.Int64Flag{
|
||||||
|
Name: "macaroontimeout",
|
||||||
|
Value: 60,
|
||||||
|
Usage: "Anti-replay macaroon validity time in " +
|
||||||
|
"seconds.",
|
||||||
|
EnvVar: envVarMacaroonTimeout,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macaroonip",
|
||||||
|
Usage: "If set, lock macaroon to specific IP address.",
|
||||||
|
EnvVar: envVarMacaroonIP,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "profile, p",
|
||||||
|
Usage: "Instead of reading settings from command " +
|
||||||
|
"line parameters or using the default " +
|
||||||
|
"profile, use a specific profile. If " +
|
||||||
|
"a default profile is set, this flag can be " +
|
||||||
|
"set to an empty string to disable reading " +
|
||||||
|
"values from the profiles file.",
|
||||||
|
EnvVar: envVarProfile,
|
||||||
|
},
|
||||||
|
cli.StringFlag{
|
||||||
|
Name: "macfromjar",
|
||||||
|
Usage: "Use this macaroon from the profile's " +
|
||||||
|
"macaroon jar instead of the default one. " +
|
||||||
|
"Can only be used if profiles are defined.",
|
||||||
|
EnvVar: envVarMacFromJar,
|
||||||
|
},
|
||||||
|
cli.StringSliceFlag{
|
||||||
|
Name: "metadata",
|
||||||
|
Usage: "This flag can be used to specify a key-value " +
|
||||||
|
"pair that should be appended to the " +
|
||||||
|
"outgoing context before the request is sent " +
|
||||||
|
"to lnd. This flag may be specified multiple " +
|
||||||
|
"times. The format is: \"key:value\".",
|
||||||
|
},
|
||||||
|
cli.BoolFlag{
|
||||||
|
Name: "insecure",
|
||||||
|
Usage: "Connect to the rpc server without TLS " +
|
||||||
|
"authentication",
|
||||||
|
Hidden: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
app.Commands = []cli.Command{
|
||||||
|
createCommand,
|
||||||
|
createWatchOnlyCommand,
|
||||||
|
unlockCommand,
|
||||||
|
changePasswordCommand,
|
||||||
|
newAddressCommand,
|
||||||
|
estimateFeeCommand,
|
||||||
|
sendManyCommand,
|
||||||
|
sendCoinsCommand,
|
||||||
|
listUnspentCommand,
|
||||||
|
connectCommand,
|
||||||
|
disconnectCommand,
|
||||||
|
openChannelCommand,
|
||||||
|
batchOpenChannelCommand,
|
||||||
|
closeChannelCommand,
|
||||||
|
closeAllChannelsCommand,
|
||||||
|
abandonChannelCommand,
|
||||||
|
listPeersCommand,
|
||||||
|
walletBalanceCommand,
|
||||||
|
ChannelBalanceCommand,
|
||||||
|
getInfoCommand,
|
||||||
|
getDebugInfoCommand,
|
||||||
|
encryptDebugPackageCommand,
|
||||||
|
decryptDebugPackageCommand,
|
||||||
|
getRecoveryInfoCommand,
|
||||||
|
pendingChannelsCommand,
|
||||||
|
SendPaymentCommand,
|
||||||
|
payInvoiceCommand,
|
||||||
|
sendToRouteCommand,
|
||||||
|
AddInvoiceCommand,
|
||||||
|
lookupInvoiceCommand,
|
||||||
|
listInvoicesCommand,
|
||||||
|
ListChannelsCommand,
|
||||||
|
closedChannelsCommand,
|
||||||
|
listPaymentsCommand,
|
||||||
|
describeGraphCommand,
|
||||||
|
getNodeMetricsCommand,
|
||||||
|
getChanInfoCommand,
|
||||||
|
getNodeInfoCommand,
|
||||||
|
queryRoutesCommand,
|
||||||
|
getNetworkInfoCommand,
|
||||||
|
debugLevelCommand,
|
||||||
|
decodePayReqCommand,
|
||||||
|
listChainTxnsCommand,
|
||||||
|
stopCommand,
|
||||||
|
signMessageCommand,
|
||||||
|
verifyMessageCommand,
|
||||||
|
feeReportCommand,
|
||||||
|
updateChannelPolicyCommand,
|
||||||
|
forwardingHistoryCommand,
|
||||||
|
exportChanBackupCommand,
|
||||||
|
verifyChanBackupCommand,
|
||||||
|
restoreChanBackupCommand,
|
||||||
|
bakeMacaroonCommand,
|
||||||
|
listMacaroonIDsCommand,
|
||||||
|
deleteMacaroonIDCommand,
|
||||||
|
listPermissionsCommand,
|
||||||
|
printMacaroonCommand,
|
||||||
|
constrainMacaroonCommand,
|
||||||
|
trackPaymentCommand,
|
||||||
|
versionCommand,
|
||||||
|
profileSubCommand,
|
||||||
|
getStateCommand,
|
||||||
|
deletePaymentsCommand,
|
||||||
|
sendCustomCommand,
|
||||||
|
subscribeCustomCommand,
|
||||||
|
fishCompletionCommand,
|
||||||
|
listAliasesCommand,
|
||||||
|
estimateRouteFeeCommand,
|
||||||
|
generateManPageCommand,
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add any extra commands determined by build flags.
|
||||||
|
app.Commands = append(app.Commands, autopilotCommands()...)
|
||||||
|
app.Commands = append(app.Commands, invoicesCommands()...)
|
||||||
|
app.Commands = append(app.Commands, neutrinoCommands()...)
|
||||||
|
app.Commands = append(app.Commands, routerCommands()...)
|
||||||
|
app.Commands = append(app.Commands, walletCommands()...)
|
||||||
|
app.Commands = append(app.Commands, watchtowerCommands()...)
|
||||||
|
app.Commands = append(app.Commands, wtclientCommands()...)
|
||||||
|
app.Commands = append(app.Commands, devCommands()...)
|
||||||
|
app.Commands = append(app.Commands, peersCommands()...)
|
||||||
|
app.Commands = append(app.Commands, chainCommands()...)
|
||||||
|
|
||||||
|
if err := app.Run(os.Args); err != nil {
|
||||||
|
fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// readPassword reads a password from the terminal. This requires there to be an
|
||||||
|
// actual TTY so passing in a password from stdin won't work.
|
||||||
|
func readPassword(text string) ([]byte, error) {
|
||||||
|
fmt.Print(text)
|
||||||
|
|
||||||
|
// The variable syscall.Stdin is of a different type in the Windows API
|
||||||
|
// that's why we need the explicit cast. And of course the linter
|
||||||
|
// doesn't like it either.
|
||||||
|
pw, err := term.ReadPassword(int(syscall.Stdin)) //nolint:unconvert
|
||||||
|
fmt.Println()
|
||||||
|
|
||||||
|
return pw, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// networkParams parses the global network flag into a chaincfg.Params.
|
||||||
|
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
||||||
|
network := strings.ToLower(ctx.GlobalString("network"))
|
||||||
|
switch network {
|
||||||
|
case "mainnet":
|
||||||
|
return &chaincfg.MainNetParams, nil
|
||||||
|
|
||||||
|
case "testnet":
|
||||||
|
return &chaincfg.TestNet3Params, nil
|
||||||
|
|
||||||
|
case "regtest":
|
||||||
|
return &chaincfg.RegressionNetParams, nil
|
||||||
|
|
||||||
|
case "simnet":
|
||||||
|
return &chaincfg.SimNetParams, nil
|
||||||
|
|
||||||
|
case "signet":
|
||||||
|
return &chaincfg.SigNetParams, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("unknown network: %v", network)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// parseCoinSelectionStrategy parses a coin selection strategy string
|
||||||
|
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
||||||
|
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
||||||
|
lnrpc.CoinSelectionStrategy, error) {
|
||||||
|
|
||||||
|
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
||||||
|
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||||
|
nil
|
||||||
|
}
|
||||||
|
|
||||||
|
switch strategy {
|
||||||
|
case "global-config":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
||||||
|
nil
|
||||||
|
|
||||||
|
case "largest":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
||||||
|
|
||||||
|
case "random":
|
||||||
|
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
||||||
|
|
||||||
|
default:
|
||||||
|
return 0, fmt.Errorf("unknown coin selection strategy "+
|
||||||
|
"%v", strategy)
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,11 +1,13 @@
|
|||||||
//go:build neutrinorpc
|
//go:build neutrinorpc
|
||||||
// +build neutrinorpc
|
// +build neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
"github.com/lightningnetwork/lnd/lnrpc/neutrinorpc"
|
||||||
"github.com/urfave/cli"
|
"github.com/urfave/cli"
|
||||||
|
|
||||||
|
"strconv"
|
||||||
)
|
)
|
||||||
|
|
||||||
func getNeutrinoKitClient(ctx *cli.Context) (neutrinorpc.NeutrinoKitClient, func()) {
|
func getNeutrinoKitClient(ctx *cli.Context) (neutrinorpc.NeutrinoKitClient, func()) {
|
||||||
@ -225,6 +227,47 @@ func getCFilter(ctx *cli.Context) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var getBlockHashCommand = cli.Command{
|
||||||
|
Name: "getblockhash",
|
||||||
|
Usage: "Get a block hash.",
|
||||||
|
Category: "Neutrino",
|
||||||
|
Description: "Returns the header hash of a block at a given height.",
|
||||||
|
ArgsUsage: "height",
|
||||||
|
Action: actionDecorator(getBlockHash),
|
||||||
|
}
|
||||||
|
|
||||||
|
func getBlockHash(ctx *cli.Context) error {
|
||||||
|
ctxc := getContext()
|
||||||
|
args := ctx.Args()
|
||||||
|
|
||||||
|
// Display the command's help message if we do not have the expected
|
||||||
|
// number of arguments/flags.
|
||||||
|
if !args.Present() {
|
||||||
|
return cli.ShowCommandHelp(ctx, "getblockhash")
|
||||||
|
}
|
||||||
|
|
||||||
|
client, cleanUp := getNeutrinoKitClient(ctx)
|
||||||
|
defer cleanUp()
|
||||||
|
|
||||||
|
height, err := strconv.ParseInt(args.First(), 10, 32)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req := &neutrinorpc.GetBlockHashRequest{
|
||||||
|
Height: int32(height),
|
||||||
|
}
|
||||||
|
|
||||||
|
resp, err := client.GetBlockHash(ctxc, req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
printRespJSON(resp)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// neutrinoCommands will return the set of commands to enable for neutrinorpc
|
// neutrinoCommands will return the set of commands to enable for neutrinorpc
|
||||||
// builds.
|
// builds.
|
||||||
func neutrinoCommands() []cli.Command {
|
func neutrinoCommands() []cli.Command {
|
||||||
@ -241,6 +284,7 @@ func neutrinoCommands() []cli.Command {
|
|||||||
isBannedCommand,
|
isBannedCommand,
|
||||||
getBlockHeaderNeutrinoCommand,
|
getBlockHeaderNeutrinoCommand,
|
||||||
getCFilterCommand,
|
getCFilterCommand,
|
||||||
|
getBlockHashCommand,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !neutrinorpc
|
//go:build !neutrinorpc
|
||||||
// +build !neutrinorpc
|
// +build !neutrinorpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build peersrpc
|
//go:build peersrpc
|
||||||
// +build peersrpc
|
// +build peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !peersrpc
|
//go:build !peersrpc
|
||||||
// +build !peersrpc
|
// +build !peersrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build walletrpc
|
//go:build walletrpc
|
||||||
// +build walletrpc
|
// +build walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !walletrpc
|
//go:build !walletrpc
|
||||||
// +build !walletrpc
|
// +build !walletrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
import "github.com/lightningnetwork/lnd/lnrpc/walletrpc"
|
||||||
|
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build watchtowerrpc
|
//go:build watchtowerrpc
|
||||||
// +build watchtowerrpc
|
// +build watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
"github.com/lightningnetwork/lnd/lnrpc/watchtowerrpc"
|
||||||
@ -1,7 +1,7 @@
|
|||||||
//go:build !watchtowerrpc
|
//go:build !watchtowerrpc
|
||||||
// +build !watchtowerrpc
|
// +build !watchtowerrpc
|
||||||
|
|
||||||
package main
|
package commands
|
||||||
|
|
||||||
import "github.com/urfave/cli"
|
import "github.com/urfave/cli"
|
||||||
|
|
||||||
@ -1,4 +1,4 @@
|
|||||||
package main
|
package commands
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
@ -1,594 +1,11 @@
|
|||||||
// Copyright (c) 2013-2017 The btcsuite developers
|
// Copyright (c) 2013-2017 The btcsuite developers
|
||||||
// Copyright (c) 2015-2016 The Decred developers
|
// Copyright (c) 2015-2016 The Decred developers
|
||||||
// Copyright (C) 2015-2022 The Lightning Network Developers
|
// Copyright (C) 2015-2024 The Lightning Network Developers
|
||||||
|
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import "github.com/lightningnetwork/lnd/cmd/commands"
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcutil"
|
|
||||||
"github.com/btcsuite/btcd/chaincfg"
|
|
||||||
"github.com/lightningnetwork/lnd"
|
|
||||||
"github.com/lightningnetwork/lnd/build"
|
|
||||||
"github.com/lightningnetwork/lnd/lncfg"
|
|
||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
|
||||||
"github.com/lightningnetwork/lnd/tor"
|
|
||||||
"github.com/urfave/cli"
|
|
||||||
"golang.org/x/term"
|
|
||||||
"google.golang.org/grpc"
|
|
||||||
"google.golang.org/grpc/credentials"
|
|
||||||
"google.golang.org/grpc/metadata"
|
|
||||||
)
|
|
||||||
|
|
||||||
const (
|
|
||||||
defaultDataDir = "data"
|
|
||||||
defaultChainSubDir = "chain"
|
|
||||||
defaultTLSCertFilename = "tls.cert"
|
|
||||||
defaultMacaroonFilename = "admin.macaroon"
|
|
||||||
defaultRPCPort = "10009"
|
|
||||||
defaultRPCHostPort = "localhost:" + defaultRPCPort
|
|
||||||
|
|
||||||
envVarRPCServer = "LNCLI_RPCSERVER"
|
|
||||||
envVarLNDDir = "LNCLI_LNDDIR"
|
|
||||||
envVarSOCKSProxy = "LNCLI_SOCKSPROXY"
|
|
||||||
envVarTLSCertPath = "LNCLI_TLSCERTPATH"
|
|
||||||
envVarChain = "LNCLI_CHAIN"
|
|
||||||
envVarNetwork = "LNCLI_NETWORK"
|
|
||||||
envVarMacaroonPath = "LNCLI_MACAROONPATH"
|
|
||||||
envVarMacaroonTimeout = "LNCLI_MACAROONTIMEOUT"
|
|
||||||
envVarMacaroonIP = "LNCLI_MACAROONIP"
|
|
||||||
envVarProfile = "LNCLI_PROFILE"
|
|
||||||
envVarMacFromJar = "LNCLI_MACFROMJAR"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
defaultLndDir = btcutil.AppDataDir("lnd", false)
|
|
||||||
defaultTLSCertPath = filepath.Join(defaultLndDir, defaultTLSCertFilename)
|
|
||||||
|
|
||||||
// maxMsgRecvSize is the largest message our client will receive. We
|
|
||||||
// set this to 200MiB atm.
|
|
||||||
maxMsgRecvSize = grpc.MaxCallRecvMsgSize(lnrpc.MaxGrpcMsgSize)
|
|
||||||
)
|
|
||||||
|
|
||||||
func fatal(err error) {
|
|
||||||
fmt.Fprintf(os.Stderr, "[lncli] %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getWalletUnlockerClient(ctx *cli.Context) (lnrpc.WalletUnlockerClient, func()) {
|
|
||||||
conn := getClientConn(ctx, true)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewWalletUnlockerClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getStateServiceClient(ctx *cli.Context) (lnrpc.StateClient, func()) {
|
|
||||||
conn := getClientConn(ctx, true)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewStateClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClient(ctx *cli.Context) (lnrpc.LightningClient, func()) {
|
|
||||||
conn := getClientConn(ctx, false)
|
|
||||||
|
|
||||||
cleanUp := func() {
|
|
||||||
conn.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
return lnrpc.NewLightningClient(conn), cleanUp
|
|
||||||
}
|
|
||||||
|
|
||||||
func getClientConn(ctx *cli.Context, skipMacaroons bool) *grpc.ClientConn {
|
|
||||||
// First, we'll get the selected stored profile or an ephemeral one
|
|
||||||
// created from the global options in the CLI context.
|
|
||||||
profile, err := getGlobalOptions(ctx, skipMacaroons)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not load global options: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a dial options array.
|
|
||||||
opts := []grpc.DialOption{
|
|
||||||
grpc.WithUnaryInterceptor(
|
|
||||||
addMetadataUnaryInterceptor(profile.Metadata),
|
|
||||||
),
|
|
||||||
grpc.WithStreamInterceptor(
|
|
||||||
addMetaDataStreamInterceptor(profile.Metadata),
|
|
||||||
),
|
|
||||||
}
|
|
||||||
|
|
||||||
if profile.Insecure {
|
|
||||||
opts = append(opts, grpc.WithInsecure())
|
|
||||||
} else {
|
|
||||||
// Load the specified TLS certificate.
|
|
||||||
certPool, err := profile.cert()
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not create cert pool: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build transport credentials from the certificate pool. If
|
|
||||||
// there is no certificate pool, we expect the server to use a
|
|
||||||
// non-self-signed certificate such as a certificate obtained
|
|
||||||
// from Let's Encrypt.
|
|
||||||
var creds credentials.TransportCredentials
|
|
||||||
if certPool != nil {
|
|
||||||
creds = credentials.NewClientTLSFromCert(certPool, "")
|
|
||||||
} else {
|
|
||||||
// Fallback to the system pool. Using an empty tls
|
|
||||||
// config is an alternative to x509.SystemCertPool().
|
|
||||||
// That call is not supported on Windows.
|
|
||||||
creds = credentials.NewTLS(&tls.Config{})
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, grpc.WithTransportCredentials(creds))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Only process macaroon credentials if --no-macaroons isn't set and
|
|
||||||
// if we're not skipping macaroon processing.
|
|
||||||
if !profile.NoMacaroons && !skipMacaroons {
|
|
||||||
// Find out which macaroon to load.
|
|
||||||
macName := profile.Macaroons.Default
|
|
||||||
if ctx.GlobalIsSet("macfromjar") {
|
|
||||||
macName = ctx.GlobalString("macfromjar")
|
|
||||||
}
|
|
||||||
var macEntry *macaroonEntry
|
|
||||||
for _, entry := range profile.Macaroons.Jar {
|
|
||||||
if entry.Name == macName {
|
|
||||||
macEntry = entry
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if macEntry == nil {
|
|
||||||
fatal(fmt.Errorf("macaroon with name '%s' not found "+
|
|
||||||
"in profile", macName))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get and possibly decrypt the specified macaroon.
|
|
||||||
//
|
|
||||||
// TODO(guggero): Make it possible to cache the password so we
|
|
||||||
// don't need to ask for it every time.
|
|
||||||
mac, err := macEntry.loadMacaroon(readPassword)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("could not load macaroon: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
macConstraints := []macaroons.Constraint{
|
|
||||||
// We add a time-based constraint to prevent replay of
|
|
||||||
// the macaroon. It's good for 60 seconds by default to
|
|
||||||
// make up for any discrepancy between client and server
|
|
||||||
// clocks, but leaking the macaroon before it becomes
|
|
||||||
// invalid makes it possible for an attacker to reuse
|
|
||||||
// the macaroon. In addition, the validity time of the
|
|
||||||
// macaroon is extended by the time the server clock is
|
|
||||||
// behind the client clock, or shortened by the time the
|
|
||||||
// server clock is ahead of the client clock (or invalid
|
|
||||||
// altogether if, in the latter case, this time is more
|
|
||||||
// than 60 seconds).
|
|
||||||
// TODO(aakselrod): add better anti-replay protection.
|
|
||||||
macaroons.TimeoutConstraint(profile.Macaroons.Timeout),
|
|
||||||
|
|
||||||
// Lock macaroon down to a specific IP address.
|
|
||||||
macaroons.IPLockConstraint(profile.Macaroons.IP),
|
|
||||||
|
|
||||||
// ... Add more constraints if needed.
|
|
||||||
}
|
|
||||||
|
|
||||||
// Apply constraints to the macaroon.
|
|
||||||
constrainedMac, err := macaroons.AddConstraints(
|
|
||||||
mac, macConstraints...,
|
|
||||||
)
|
|
||||||
if err != nil {
|
|
||||||
fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now we append the macaroon credentials to the dial options.
|
|
||||||
cred, err := macaroons.NewMacaroonCredential(constrainedMac)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("error cloning mac: %w", err))
|
|
||||||
}
|
|
||||||
opts = append(opts, grpc.WithPerRPCCredentials(cred))
|
|
||||||
}
|
|
||||||
|
|
||||||
// If a socksproxy server is specified we use a tor dialer
|
|
||||||
// to connect to the grpc server.
|
|
||||||
if ctx.GlobalIsSet("socksproxy") {
|
|
||||||
socksProxy := ctx.GlobalString("socksproxy")
|
|
||||||
torDialer := func(_ context.Context, addr string) (net.Conn,
|
|
||||||
error) {
|
|
||||||
|
|
||||||
return tor.Dial(
|
|
||||||
addr, socksProxy, false, false,
|
|
||||||
tor.DefaultConnTimeout,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
opts = append(opts, grpc.WithContextDialer(torDialer))
|
|
||||||
} else {
|
|
||||||
// We need to use a custom dialer so we can also connect to
|
|
||||||
// unix sockets and not just TCP addresses.
|
|
||||||
genericDialer := lncfg.ClientAddressDialer(defaultRPCPort)
|
|
||||||
opts = append(opts, grpc.WithContextDialer(genericDialer))
|
|
||||||
}
|
|
||||||
|
|
||||||
opts = append(opts, grpc.WithDefaultCallOptions(maxMsgRecvSize))
|
|
||||||
|
|
||||||
conn, err := grpc.Dial(profile.RPCServer, opts...)
|
|
||||||
if err != nil {
|
|
||||||
fatal(fmt.Errorf("unable to connect to RPC server: %w", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
return conn
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMetadataUnaryInterceptor returns a grpc client side interceptor that
|
|
||||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
|
||||||
// unary call.
|
|
||||||
func addMetadataUnaryInterceptor(
|
|
||||||
md map[string]string) grpc.UnaryClientInterceptor {
|
|
||||||
|
|
||||||
return func(ctx context.Context, method string, req, reply interface{},
|
|
||||||
cc *grpc.ClientConn, invoker grpc.UnaryInvoker,
|
|
||||||
opts ...grpc.CallOption) error {
|
|
||||||
|
|
||||||
outCtx := contextWithMetadata(ctx, md)
|
|
||||||
return invoker(outCtx, method, req, reply, cc, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// addMetaDataStreamInterceptor returns a grpc client side interceptor that
|
|
||||||
// appends any key-value metadata strings to the outgoing context of a grpc
|
|
||||||
// stream call.
|
|
||||||
func addMetaDataStreamInterceptor(
|
|
||||||
md map[string]string) grpc.StreamClientInterceptor {
|
|
||||||
|
|
||||||
return func(ctx context.Context, desc *grpc.StreamDesc,
|
|
||||||
cc *grpc.ClientConn, method string, streamer grpc.Streamer,
|
|
||||||
opts ...grpc.CallOption) (grpc.ClientStream, error) {
|
|
||||||
|
|
||||||
outCtx := contextWithMetadata(ctx, md)
|
|
||||||
return streamer(outCtx, desc, cc, method, opts...)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// contextWithMetaData appends the given metadata key-value pairs to the given
|
|
||||||
// context.
|
|
||||||
func contextWithMetadata(ctx context.Context,
|
|
||||||
md map[string]string) context.Context {
|
|
||||||
|
|
||||||
kvPairs := make([]string, 0, 2*len(md))
|
|
||||||
for k, v := range md {
|
|
||||||
kvPairs = append(kvPairs, k, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return metadata.AppendToOutgoingContext(ctx, kvPairs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// extractPathArgs parses the TLS certificate and macaroon paths from the
|
|
||||||
// command.
|
|
||||||
func extractPathArgs(ctx *cli.Context) (string, string, error) {
|
|
||||||
network := strings.ToLower(ctx.GlobalString("network"))
|
|
||||||
switch network {
|
|
||||||
case "mainnet", "testnet", "regtest", "simnet", "signet":
|
|
||||||
default:
|
|
||||||
return "", "", fmt.Errorf("unknown network: %v", network)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll now fetch the lnddir so we can make a decision on how to
|
|
||||||
// properly read the macaroons (if needed) and also the cert. This will
|
|
||||||
// either be the default, or will have been overwritten by the end
|
|
||||||
// user.
|
|
||||||
lndDir := lncfg.CleanAndExpandPath(ctx.GlobalString("lnddir"))
|
|
||||||
|
|
||||||
// If the macaroon path as been manually provided, then we'll only
|
|
||||||
// target the specified file.
|
|
||||||
var macPath string
|
|
||||||
if ctx.GlobalString("macaroonpath") != "" {
|
|
||||||
macPath = lncfg.CleanAndExpandPath(ctx.GlobalString("macaroonpath"))
|
|
||||||
} else {
|
|
||||||
// Otherwise, we'll go into the path:
|
|
||||||
// lnddir/data/chain/<chain>/<network> in order to fetch the
|
|
||||||
// macaroon that we need.
|
|
||||||
macPath = filepath.Join(
|
|
||||||
lndDir, defaultDataDir, defaultChainSubDir,
|
|
||||||
lnd.BitcoinChainName, network, defaultMacaroonFilename,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
tlsCertPath := lncfg.CleanAndExpandPath(ctx.GlobalString("tlscertpath"))
|
|
||||||
|
|
||||||
// If a custom lnd directory was set, we'll also check if custom paths
|
|
||||||
// for the TLS cert and macaroon file were set as well. If not, we'll
|
|
||||||
// override their paths so they can be found within the custom lnd
|
|
||||||
// directory set. This allows us to set a custom lnd directory, along
|
|
||||||
// with custom paths to the TLS cert and macaroon file.
|
|
||||||
if lndDir != defaultLndDir {
|
|
||||||
tlsCertPath = filepath.Join(lndDir, defaultTLSCertFilename)
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlsCertPath, macPath, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// checkNotBothSet accepts two flag names, a and b, and checks that only flag a
|
|
||||||
// or flag b can be set, but not both. It returns the name of the flag or an
|
|
||||||
// error.
|
|
||||||
func checkNotBothSet(ctx *cli.Context, a, b string) (string, error) {
|
|
||||||
if ctx.IsSet(a) && ctx.IsSet(b) {
|
|
||||||
return "", fmt.Errorf(
|
|
||||||
"either %s or %s should be set, but not both", a, b,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if ctx.IsSet(a) {
|
|
||||||
return a, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return b, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
app := cli.NewApp()
|
commands.Main()
|
||||||
app.Name = "lncli"
|
|
||||||
app.Version = build.Version() + " commit=" + build.Commit
|
|
||||||
app.Usage = "control plane for your Lightning Network Daemon (lnd)"
|
|
||||||
app.Flags = []cli.Flag{
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "rpcserver",
|
|
||||||
Value: defaultRPCHostPort,
|
|
||||||
Usage: "The host:port of LN daemon.",
|
|
||||||
EnvVar: envVarRPCServer,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "lnddir",
|
|
||||||
Value: defaultLndDir,
|
|
||||||
Usage: "The path to lnd's base directory.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarLNDDir,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "socksproxy",
|
|
||||||
Usage: "The host:port of a SOCKS proxy through " +
|
|
||||||
"which all connections to the LN " +
|
|
||||||
"daemon will be established over.",
|
|
||||||
EnvVar: envVarSOCKSProxy,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "tlscertpath",
|
|
||||||
Value: defaultTLSCertPath,
|
|
||||||
Usage: "The path to lnd's TLS certificate.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarTLSCertPath,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "chain, c",
|
|
||||||
Usage: "The chain lnd is running on, e.g. bitcoin.",
|
|
||||||
Value: "bitcoin",
|
|
||||||
EnvVar: envVarChain,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "network, n",
|
|
||||||
Usage: "The network lnd is running on, e.g. mainnet, " +
|
|
||||||
"testnet, etc.",
|
|
||||||
Value: "mainnet",
|
|
||||||
EnvVar: envVarNetwork,
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "no-macaroons",
|
|
||||||
Usage: "Disable macaroon authentication.",
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macaroonpath",
|
|
||||||
Usage: "The path to macaroon file.",
|
|
||||||
TakesFile: true,
|
|
||||||
EnvVar: envVarMacaroonPath,
|
|
||||||
},
|
|
||||||
cli.Int64Flag{
|
|
||||||
Name: "macaroontimeout",
|
|
||||||
Value: 60,
|
|
||||||
Usage: "Anti-replay macaroon validity time in " +
|
|
||||||
"seconds.",
|
|
||||||
EnvVar: envVarMacaroonTimeout,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macaroonip",
|
|
||||||
Usage: "If set, lock macaroon to specific IP address.",
|
|
||||||
EnvVar: envVarMacaroonIP,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "profile, p",
|
|
||||||
Usage: "Instead of reading settings from command " +
|
|
||||||
"line parameters or using the default " +
|
|
||||||
"profile, use a specific profile. If " +
|
|
||||||
"a default profile is set, this flag can be " +
|
|
||||||
"set to an empty string to disable reading " +
|
|
||||||
"values from the profiles file.",
|
|
||||||
EnvVar: envVarProfile,
|
|
||||||
},
|
|
||||||
cli.StringFlag{
|
|
||||||
Name: "macfromjar",
|
|
||||||
Usage: "Use this macaroon from the profile's " +
|
|
||||||
"macaroon jar instead of the default one. " +
|
|
||||||
"Can only be used if profiles are defined.",
|
|
||||||
EnvVar: envVarMacFromJar,
|
|
||||||
},
|
|
||||||
cli.StringSliceFlag{
|
|
||||||
Name: "metadata",
|
|
||||||
Usage: "This flag can be used to specify a key-value " +
|
|
||||||
"pair that should be appended to the " +
|
|
||||||
"outgoing context before the request is sent " +
|
|
||||||
"to lnd. This flag may be specified multiple " +
|
|
||||||
"times. The format is: \"key:value\".",
|
|
||||||
},
|
|
||||||
cli.BoolFlag{
|
|
||||||
Name: "insecure",
|
|
||||||
Usage: "Connect to the rpc server without TLS " +
|
|
||||||
"authentication",
|
|
||||||
Hidden: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
app.Commands = []cli.Command{
|
|
||||||
createCommand,
|
|
||||||
createWatchOnlyCommand,
|
|
||||||
unlockCommand,
|
|
||||||
changePasswordCommand,
|
|
||||||
newAddressCommand,
|
|
||||||
estimateFeeCommand,
|
|
||||||
sendManyCommand,
|
|
||||||
sendCoinsCommand,
|
|
||||||
listUnspentCommand,
|
|
||||||
connectCommand,
|
|
||||||
disconnectCommand,
|
|
||||||
openChannelCommand,
|
|
||||||
batchOpenChannelCommand,
|
|
||||||
closeChannelCommand,
|
|
||||||
closeAllChannelsCommand,
|
|
||||||
abandonChannelCommand,
|
|
||||||
listPeersCommand,
|
|
||||||
walletBalanceCommand,
|
|
||||||
channelBalanceCommand,
|
|
||||||
getInfoCommand,
|
|
||||||
getDebugInfoCommand,
|
|
||||||
encryptDebugPackageCommand,
|
|
||||||
decryptDebugPackageCommand,
|
|
||||||
getRecoveryInfoCommand,
|
|
||||||
pendingChannelsCommand,
|
|
||||||
sendPaymentCommand,
|
|
||||||
payInvoiceCommand,
|
|
||||||
sendToRouteCommand,
|
|
||||||
addInvoiceCommand,
|
|
||||||
lookupInvoiceCommand,
|
|
||||||
listInvoicesCommand,
|
|
||||||
listChannelsCommand,
|
|
||||||
closedChannelsCommand,
|
|
||||||
listPaymentsCommand,
|
|
||||||
describeGraphCommand,
|
|
||||||
getNodeMetricsCommand,
|
|
||||||
getChanInfoCommand,
|
|
||||||
getNodeInfoCommand,
|
|
||||||
queryRoutesCommand,
|
|
||||||
getNetworkInfoCommand,
|
|
||||||
debugLevelCommand,
|
|
||||||
decodePayReqCommand,
|
|
||||||
listChainTxnsCommand,
|
|
||||||
stopCommand,
|
|
||||||
signMessageCommand,
|
|
||||||
verifyMessageCommand,
|
|
||||||
feeReportCommand,
|
|
||||||
updateChannelPolicyCommand,
|
|
||||||
forwardingHistoryCommand,
|
|
||||||
exportChanBackupCommand,
|
|
||||||
verifyChanBackupCommand,
|
|
||||||
restoreChanBackupCommand,
|
|
||||||
bakeMacaroonCommand,
|
|
||||||
listMacaroonIDsCommand,
|
|
||||||
deleteMacaroonIDCommand,
|
|
||||||
listPermissionsCommand,
|
|
||||||
printMacaroonCommand,
|
|
||||||
constrainMacaroonCommand,
|
|
||||||
trackPaymentCommand,
|
|
||||||
versionCommand,
|
|
||||||
profileSubCommand,
|
|
||||||
getStateCommand,
|
|
||||||
deletePaymentsCommand,
|
|
||||||
sendCustomCommand,
|
|
||||||
subscribeCustomCommand,
|
|
||||||
fishCompletionCommand,
|
|
||||||
listAliasesCommand,
|
|
||||||
estimateRouteFeeCommand,
|
|
||||||
generateManPageCommand,
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add any extra commands determined by build flags.
|
|
||||||
app.Commands = append(app.Commands, autopilotCommands()...)
|
|
||||||
app.Commands = append(app.Commands, invoicesCommands()...)
|
|
||||||
app.Commands = append(app.Commands, neutrinoCommands()...)
|
|
||||||
app.Commands = append(app.Commands, routerCommands()...)
|
|
||||||
app.Commands = append(app.Commands, walletCommands()...)
|
|
||||||
app.Commands = append(app.Commands, watchtowerCommands()...)
|
|
||||||
app.Commands = append(app.Commands, wtclientCommands()...)
|
|
||||||
app.Commands = append(app.Commands, devCommands()...)
|
|
||||||
app.Commands = append(app.Commands, peersCommands()...)
|
|
||||||
app.Commands = append(app.Commands, chainCommands()...)
|
|
||||||
|
|
||||||
if err := app.Run(os.Args); err != nil {
|
|
||||||
fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// readPassword reads a password from the terminal. This requires there to be an
|
|
||||||
// actual TTY so passing in a password from stdin won't work.
|
|
||||||
func readPassword(text string) ([]byte, error) {
|
|
||||||
fmt.Print(text)
|
|
||||||
|
|
||||||
// The variable syscall.Stdin is of a different type in the Windows API
|
|
||||||
// that's why we need the explicit cast. And of course the linter
|
|
||||||
// doesn't like it either.
|
|
||||||
pw, err := term.ReadPassword(int(syscall.Stdin)) // nolint:unconvert
|
|
||||||
fmt.Println()
|
|
||||||
return pw, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// networkParams parses the global network flag into a chaincfg.Params.
|
|
||||||
func networkParams(ctx *cli.Context) (*chaincfg.Params, error) {
|
|
||||||
network := strings.ToLower(ctx.GlobalString("network"))
|
|
||||||
switch network {
|
|
||||||
case "mainnet":
|
|
||||||
return &chaincfg.MainNetParams, nil
|
|
||||||
|
|
||||||
case "testnet":
|
|
||||||
return &chaincfg.TestNet3Params, nil
|
|
||||||
|
|
||||||
case "regtest":
|
|
||||||
return &chaincfg.RegressionNetParams, nil
|
|
||||||
|
|
||||||
case "simnet":
|
|
||||||
return &chaincfg.SimNetParams, nil
|
|
||||||
|
|
||||||
case "signet":
|
|
||||||
return &chaincfg.SigNetParams, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return nil, fmt.Errorf("unknown network: %v", network)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// parseCoinSelectionStrategy parses a coin selection strategy string
|
|
||||||
// from the CLI to its lnrpc.CoinSelectionStrategy counterpart proto type.
|
|
||||||
func parseCoinSelectionStrategy(ctx *cli.Context) (
|
|
||||||
lnrpc.CoinSelectionStrategy, error) {
|
|
||||||
|
|
||||||
strategy := ctx.String(coinSelectionStrategyFlag.Name)
|
|
||||||
if !ctx.IsSet(coinSelectionStrategyFlag.Name) {
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
|
||||||
nil
|
|
||||||
}
|
|
||||||
|
|
||||||
switch strategy {
|
|
||||||
case "global-config":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_USE_GLOBAL_CONFIG,
|
|
||||||
nil
|
|
||||||
|
|
||||||
case "largest":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_LARGEST, nil
|
|
||||||
|
|
||||||
case "random":
|
|
||||||
return lnrpc.CoinSelectionStrategy_STRATEGY_RANDOM, nil
|
|
||||||
|
|
||||||
default:
|
|
||||||
return 0, fmt.Errorf("unknown coin selection strategy "+
|
|
||||||
"%v", strategy)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,6 +33,9 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainreg"
|
"github.com/lightningnetwork/lnd/chainreg"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/clock"
|
"github.com/lightningnetwork/lnd/clock"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
|
"github.com/lightningnetwork/lnd/funding"
|
||||||
|
"github.com/lightningnetwork/lnd/htlcswitch"
|
||||||
"github.com/lightningnetwork/lnd/invoices"
|
"github.com/lightningnetwork/lnd/invoices"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
@ -40,11 +43,14 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnrpc"
|
"github.com/lightningnetwork/lnd/lnrpc"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/btcwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwallet/chancloser"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
"github.com/lightningnetwork/lnd/lnwallet/rpcwallet"
|
||||||
"github.com/lightningnetwork/lnd/macaroons"
|
"github.com/lightningnetwork/lnd/macaroons"
|
||||||
|
"github.com/lightningnetwork/lnd/msgmux"
|
||||||
"github.com/lightningnetwork/lnd/rpcperms"
|
"github.com/lightningnetwork/lnd/rpcperms"
|
||||||
"github.com/lightningnetwork/lnd/signal"
|
"github.com/lightningnetwork/lnd/signal"
|
||||||
"github.com/lightningnetwork/lnd/sqldb"
|
"github.com/lightningnetwork/lnd/sqldb"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
"github.com/lightningnetwork/lnd/walletunlocker"
|
"github.com/lightningnetwork/lnd/walletunlocker"
|
||||||
"github.com/lightningnetwork/lnd/watchtower"
|
"github.com/lightningnetwork/lnd/watchtower"
|
||||||
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
"github.com/lightningnetwork/lnd/watchtower/wtclient"
|
||||||
@ -103,7 +109,7 @@ type DatabaseBuilder interface {
|
|||||||
type WalletConfigBuilder interface {
|
type WalletConfigBuilder interface {
|
||||||
// BuildWalletConfig is responsible for creating or unlocking and then
|
// BuildWalletConfig is responsible for creating or unlocking and then
|
||||||
// fully initializing a wallet.
|
// fully initializing a wallet.
|
||||||
BuildWalletConfig(context.Context, *DatabaseInstances,
|
BuildWalletConfig(context.Context, *DatabaseInstances, *AuxComponents,
|
||||||
*rpcperms.InterceptorChain,
|
*rpcperms.InterceptorChain,
|
||||||
[]*ListenerWithSignal) (*chainreg.PartialChainControl,
|
[]*ListenerWithSignal) (*chainreg.PartialChainControl,
|
||||||
*btcwallet.Config, func(), error)
|
*btcwallet.Config, func(), error)
|
||||||
@ -144,6 +150,52 @@ type ImplementationCfg struct {
|
|||||||
// ChainControlBuilder is a type that can provide a custom wallet
|
// ChainControlBuilder is a type that can provide a custom wallet
|
||||||
// implementation.
|
// implementation.
|
||||||
ChainControlBuilder
|
ChainControlBuilder
|
||||||
|
|
||||||
|
// AuxComponents is a set of auxiliary components that can be used by
|
||||||
|
// lnd for certain custom channel types.
|
||||||
|
AuxComponents
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuxComponents is a set of auxiliary components that can be used by lnd for
|
||||||
|
// certain custom channel types.
|
||||||
|
type AuxComponents struct {
|
||||||
|
// AuxLeafStore is an optional data source that can be used by custom
|
||||||
|
// channels to fetch+store various data.
|
||||||
|
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||||
|
|
||||||
|
// TrafficShaper is an optional traffic shaper that can be used to
|
||||||
|
// control the outgoing channel of a payment.
|
||||||
|
TrafficShaper fn.Option[htlcswitch.AuxTrafficShaper]
|
||||||
|
|
||||||
|
// MsgRouter is an optional message router that if set will be used in
|
||||||
|
// place of a new blank default message router.
|
||||||
|
MsgRouter fn.Option[msgmux.Router]
|
||||||
|
|
||||||
|
// AuxFundingController is an optional controller that can be used to
|
||||||
|
// modify the way we handle certain custom channel types. It's also
|
||||||
|
// able to automatically handle new custom protocol messages related to
|
||||||
|
// the funding process.
|
||||||
|
AuxFundingController fn.Option[funding.AuxFundingController]
|
||||||
|
|
||||||
|
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||||
|
// leaves for certain custom channel types.
|
||||||
|
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||||
|
|
||||||
|
// AuxDataParser is an optional data parser that can be used to parse
|
||||||
|
// auxiliary data for certain custom channel types.
|
||||||
|
AuxDataParser fn.Option[AuxDataParser]
|
||||||
|
|
||||||
|
// AuxChanCloser is an optional channel closer that can be used to
|
||||||
|
// modify the way a coop-close transaction is constructed.
|
||||||
|
AuxChanCloser fn.Option[chancloser.AuxChanCloser]
|
||||||
|
|
||||||
|
// AuxSweeper is an optional interface that can be used to modify the
|
||||||
|
// way sweep transaction are generated.
|
||||||
|
AuxSweeper fn.Option[sweep.AuxSweeper]
|
||||||
|
|
||||||
|
// AuxContractResolver is an optional interface that can be used to
|
||||||
|
// modify the way contracts are resolved.
|
||||||
|
AuxContractResolver fn.Option[lnwallet.AuxContractResolver]
|
||||||
}
|
}
|
||||||
|
|
||||||
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
// DefaultWalletImpl is the default implementation of our normal, btcwallet
|
||||||
@ -228,7 +280,8 @@ func (d *DefaultWalletImpl) Permissions() map[string][]bakery.Op {
|
|||||||
//
|
//
|
||||||
// NOTE: This is part of the WalletConfigBuilder interface.
|
// NOTE: This is part of the WalletConfigBuilder interface.
|
||||||
func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
||||||
dbs *DatabaseInstances, interceptorChain *rpcperms.InterceptorChain,
|
dbs *DatabaseInstances, aux *AuxComponents,
|
||||||
|
interceptorChain *rpcperms.InterceptorChain,
|
||||||
grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl,
|
grpcListeners []*ListenerWithSignal) (*chainreg.PartialChainControl,
|
||||||
*btcwallet.Config, func(), error) {
|
*btcwallet.Config, func(), error) {
|
||||||
|
|
||||||
@ -548,6 +601,8 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
|||||||
HeightHintDB: dbs.HeightHintDB,
|
HeightHintDB: dbs.HeightHintDB,
|
||||||
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
ChanStateDB: dbs.ChanStateDB.ChannelStateDB(),
|
||||||
NeutrinoCS: neutrinoCS,
|
NeutrinoCS: neutrinoCS,
|
||||||
|
AuxLeafStore: aux.AuxLeafStore,
|
||||||
|
AuxSigner: aux.AuxSigner,
|
||||||
ActiveNetParams: d.cfg.ActiveNetParams,
|
ActiveNetParams: d.cfg.ActiveNetParams,
|
||||||
FeeURL: d.cfg.FeeURL,
|
FeeURL: d.cfg.FeeURL,
|
||||||
Fee: &lncfg.Fee{
|
Fee: &lncfg.Fee{
|
||||||
@ -611,8 +666,9 @@ func (d *DefaultWalletImpl) BuildWalletConfig(ctx context.Context,
|
|||||||
|
|
||||||
// proxyBlockEpoch proxies a block epoch subsections to the underlying neutrino
|
// proxyBlockEpoch proxies a block epoch subsections to the underlying neutrino
|
||||||
// rebroadcaster client.
|
// rebroadcaster client.
|
||||||
func proxyBlockEpoch(notifier chainntnfs.ChainNotifier,
|
func proxyBlockEpoch(
|
||||||
) func() (*blockntfns.Subscription, error) {
|
notifier chainntnfs.ChainNotifier) func() (*blockntfns.Subscription,
|
||||||
|
error) {
|
||||||
|
|
||||||
return func() (*blockntfns.Subscription, error) {
|
return func() (*blockntfns.Subscription, error) {
|
||||||
blockEpoch, err := notifier.RegisterBlockEpochNtfn(
|
blockEpoch, err := notifier.RegisterBlockEpochNtfn(
|
||||||
@ -703,6 +759,8 @@ func (d *DefaultWalletImpl) BuildChainControl(
|
|||||||
ChainIO: walletController,
|
ChainIO: walletController,
|
||||||
NetParams: *walletConfig.NetParams,
|
NetParams: *walletConfig.NetParams,
|
||||||
CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
|
CoinSelectionStrategy: walletConfig.CoinSelectionStrategy,
|
||||||
|
AuxLeafStore: partialChainControl.Cfg.AuxLeafStore,
|
||||||
|
AuxSigner: partialChainControl.Cfg.AuxSigner,
|
||||||
}
|
}
|
||||||
|
|
||||||
// The broadcast is already always active for neutrino nodes, so we
|
// The broadcast is already always active for neutrino nodes, so we
|
||||||
|
|||||||
@ -15,6 +15,7 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/labels"
|
"github.com/lightningnetwork/lnd/labels"
|
||||||
@ -22,6 +23,8 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnutils"
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
"github.com/lightningnetwork/lnd/lnwallet/chainfee"
|
||||||
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@ -147,7 +150,7 @@ type BreachConfig struct {
|
|||||||
Estimator chainfee.Estimator
|
Estimator chainfee.Estimator
|
||||||
|
|
||||||
// GenSweepScript generates the receiving scripts for swept outputs.
|
// GenSweepScript generates the receiving scripts for swept outputs.
|
||||||
GenSweepScript func() ([]byte, error)
|
GenSweepScript func() fn.Result[lnwallet.AddrWithKey]
|
||||||
|
|
||||||
// Notifier provides a publish/subscribe interface for event driven
|
// Notifier provides a publish/subscribe interface for event driven
|
||||||
// notifications regarding the confirmation of txids.
|
// notifications regarding the confirmation of txids.
|
||||||
@ -172,6 +175,10 @@ type BreachConfig struct {
|
|||||||
// breached channels. This is used in conjunction with DB to recover
|
// breached channels. This is used in conjunction with DB to recover
|
||||||
// from crashes, restarts, or other failures.
|
// from crashes, restarts, or other failures.
|
||||||
Store RetributionStorer
|
Store RetributionStorer
|
||||||
|
|
||||||
|
// AuxSweeper is an optional interface that can be used to modify the
|
||||||
|
// way sweep transaction are generated.
|
||||||
|
AuxSweeper fn.Option[sweep.AuxSweeper]
|
||||||
}
|
}
|
||||||
|
|
||||||
// BreachArbitrator is a special subsystem which is responsible for watching and
|
// BreachArbitrator is a special subsystem which is responsible for watching and
|
||||||
@ -735,10 +742,28 @@ justiceTxBroadcast:
|
|||||||
brarLog.Debugf("Broadcasting justice tx: %v", lnutils.SpewLogClosure(
|
brarLog.Debugf("Broadcasting justice tx: %v", lnutils.SpewLogClosure(
|
||||||
finalTx))
|
finalTx))
|
||||||
|
|
||||||
|
// As we're about to broadcast our breach transaction, we'll notify the
|
||||||
|
// aux sweeper of our broadcast attempt first.
|
||||||
|
err = fn.MapOptionZ(b.cfg.AuxSweeper, func(aux sweep.AuxSweeper) error {
|
||||||
|
bumpReq := sweep.BumpRequest{
|
||||||
|
Inputs: finalTx.inputs,
|
||||||
|
DeliveryAddress: finalTx.sweepAddr,
|
||||||
|
ExtraTxOut: finalTx.extraTxOut,
|
||||||
|
}
|
||||||
|
|
||||||
|
return aux.NotifyBroadcast(
|
||||||
|
&bumpReq, finalTx.justiceTx, finalTx.fee, nil,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
brarLog.Errorf("unable to notify broadcast: %w", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
// We'll now attempt to broadcast the transaction which finalized the
|
// We'll now attempt to broadcast the transaction which finalized the
|
||||||
// channel's retribution against the cheating counter party.
|
// channel's retribution against the cheating counter party.
|
||||||
label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil)
|
label := labels.MakeLabel(labels.LabelTypeJusticeTransaction, nil)
|
||||||
err = b.cfg.PublishTransaction(finalTx, label)
|
err = b.cfg.PublishTransaction(finalTx.justiceTx, label)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
brarLog.Errorf("Unable to broadcast justice tx: %v", err)
|
brarLog.Errorf("Unable to broadcast justice tx: %v", err)
|
||||||
}
|
}
|
||||||
@ -858,7 +883,9 @@ Loop:
|
|||||||
"spending commitment outs: %v",
|
"spending commitment outs: %v",
|
||||||
lnutils.SpewLogClosure(tx))
|
lnutils.SpewLogClosure(tx))
|
||||||
|
|
||||||
err = b.cfg.PublishTransaction(tx, label)
|
err = b.cfg.PublishTransaction(
|
||||||
|
tx.justiceTx, label,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
brarLog.Warnf("Unable to broadcast "+
|
brarLog.Warnf("Unable to broadcast "+
|
||||||
"commit out spending justice "+
|
"commit out spending justice "+
|
||||||
@ -873,7 +900,9 @@ Loop:
|
|||||||
"spending HTLC outs: %v",
|
"spending HTLC outs: %v",
|
||||||
lnutils.SpewLogClosure(tx))
|
lnutils.SpewLogClosure(tx))
|
||||||
|
|
||||||
err = b.cfg.PublishTransaction(tx, label)
|
err = b.cfg.PublishTransaction(
|
||||||
|
tx.justiceTx, label,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
brarLog.Warnf("Unable to broadcast "+
|
brarLog.Warnf("Unable to broadcast "+
|
||||||
"HTLC out spending justice "+
|
"HTLC out spending justice "+
|
||||||
@ -888,7 +917,9 @@ Loop:
|
|||||||
"spending second-level HTLC output: %v",
|
"spending second-level HTLC output: %v",
|
||||||
lnutils.SpewLogClosure(tx))
|
lnutils.SpewLogClosure(tx))
|
||||||
|
|
||||||
err = b.cfg.PublishTransaction(tx, label)
|
err = b.cfg.PublishTransaction(
|
||||||
|
tx.justiceTx, label,
|
||||||
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
brarLog.Warnf("Unable to broadcast "+
|
brarLog.Warnf("Unable to broadcast "+
|
||||||
"second-level HTLC out "+
|
"second-level HTLC out "+
|
||||||
@ -1067,15 +1098,18 @@ type breachedOutput struct {
|
|||||||
secondLevelTapTweak [32]byte
|
secondLevelTapTweak [32]byte
|
||||||
|
|
||||||
witnessFunc input.WitnessGenerator
|
witnessFunc input.WitnessGenerator
|
||||||
|
|
||||||
|
resolutionBlob fn.Option[tlv.Blob]
|
||||||
|
|
||||||
|
// TODO(roasbeef): function opt and hook into brar
|
||||||
}
|
}
|
||||||
|
|
||||||
// makeBreachedOutput assembles a new breachedOutput that can be used by the
|
// makeBreachedOutput assembles a new breachedOutput that can be used by the
|
||||||
// breach arbiter to construct a justice or sweep transaction.
|
// breach arbiter to construct a justice or sweep transaction.
|
||||||
func makeBreachedOutput(outpoint *wire.OutPoint,
|
func makeBreachedOutput(outpoint *wire.OutPoint,
|
||||||
witnessType input.StandardWitnessType,
|
witnessType input.StandardWitnessType, secondLevelScript []byte,
|
||||||
secondLevelScript []byte,
|
signDescriptor *input.SignDescriptor, confHeight uint32,
|
||||||
signDescriptor *input.SignDescriptor,
|
resolutionBlob fn.Option[tlv.Blob]) breachedOutput {
|
||||||
confHeight uint32) breachedOutput {
|
|
||||||
|
|
||||||
amount := signDescriptor.Output.Value
|
amount := signDescriptor.Output.Value
|
||||||
|
|
||||||
@ -1086,6 +1120,7 @@ func makeBreachedOutput(outpoint *wire.OutPoint,
|
|||||||
witnessType: witnessType,
|
witnessType: witnessType,
|
||||||
signDesc: *signDescriptor,
|
signDesc: *signDescriptor,
|
||||||
confHeight: confHeight,
|
confHeight: confHeight,
|
||||||
|
resolutionBlob: resolutionBlob,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1125,6 +1160,11 @@ func (bo *breachedOutput) SignDesc() *input.SignDescriptor {
|
|||||||
return &bo.signDesc
|
return &bo.signDesc
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Preimage returns the preimage that was used to create the breached output.
|
||||||
|
func (bo *breachedOutput) Preimage() fn.Option[lntypes.Preimage] {
|
||||||
|
return fn.None[lntypes.Preimage]()
|
||||||
|
}
|
||||||
|
|
||||||
// CraftInputScript computes a valid witness that allows us to spend from the
|
// CraftInputScript computes a valid witness that allows us to spend from the
|
||||||
// breached output. It does so by first generating and memoizing the witness
|
// breached output. It does so by first generating and memoizing the witness
|
||||||
// generation function, which parameterized primarily by the witness type and
|
// generation function, which parameterized primarily by the witness type and
|
||||||
@ -1174,6 +1214,12 @@ func (bo *breachedOutput) UnconfParent() *input.TxInfo {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ResolutionBlob returns a special opaque blob to be used to sweep/resolve this
|
||||||
|
// input.
|
||||||
|
func (bo *breachedOutput) ResolutionBlob() fn.Option[tlv.Blob] {
|
||||||
|
return bo.resolutionBlob
|
||||||
|
}
|
||||||
|
|
||||||
// Add compile-time constraint ensuring breachedOutput implements the Input
|
// Add compile-time constraint ensuring breachedOutput implements the Input
|
||||||
// interface.
|
// interface.
|
||||||
var _ input.Input = (*breachedOutput)(nil)
|
var _ input.Input = (*breachedOutput)(nil)
|
||||||
@ -1258,6 +1304,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
nil,
|
nil,
|
||||||
breachInfo.LocalOutputSignDesc,
|
breachInfo.LocalOutputSignDesc,
|
||||||
breachInfo.BreachHeight,
|
breachInfo.BreachHeight,
|
||||||
|
breachInfo.LocalResolutionBlob,
|
||||||
)
|
)
|
||||||
|
|
||||||
breachedOutputs = append(breachedOutputs, localOutput)
|
breachedOutputs = append(breachedOutputs, localOutput)
|
||||||
@ -1284,6 +1331,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
nil,
|
nil,
|
||||||
breachInfo.RemoteOutputSignDesc,
|
breachInfo.RemoteOutputSignDesc,
|
||||||
breachInfo.BreachHeight,
|
breachInfo.BreachHeight,
|
||||||
|
breachInfo.RemoteResolutionBlob,
|
||||||
)
|
)
|
||||||
|
|
||||||
breachedOutputs = append(breachedOutputs, remoteOutput)
|
breachedOutputs = append(breachedOutputs, remoteOutput)
|
||||||
@ -1318,6 +1366,7 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
|
breachInfo.HtlcRetributions[i].SecondLevelWitnessScript,
|
||||||
&breachInfo.HtlcRetributions[i].SignDesc,
|
&breachInfo.HtlcRetributions[i].SignDesc,
|
||||||
breachInfo.BreachHeight,
|
breachInfo.BreachHeight,
|
||||||
|
breachInfo.HtlcRetributions[i].ResolutionBlob,
|
||||||
)
|
)
|
||||||
|
|
||||||
// For taproot outputs, we also need to hold onto the second
|
// For taproot outputs, we also need to hold onto the second
|
||||||
@ -1357,10 +1406,10 @@ func newRetributionInfo(chanPoint *wire.OutPoint,
|
|||||||
// spend the to_local output and commitment level HTLC outputs separately,
|
// spend the to_local output and commitment level HTLC outputs separately,
|
||||||
// before the CSV locks expire.
|
// before the CSV locks expire.
|
||||||
type justiceTxVariants struct {
|
type justiceTxVariants struct {
|
||||||
spendAll *wire.MsgTx
|
spendAll *justiceTxCtx
|
||||||
spendCommitOuts *wire.MsgTx
|
spendCommitOuts *justiceTxCtx
|
||||||
spendHTLCs *wire.MsgTx
|
spendHTLCs *justiceTxCtx
|
||||||
spendSecondLevelHTLCs []*wire.MsgTx
|
spendSecondLevelHTLCs []*justiceTxCtx
|
||||||
}
|
}
|
||||||
|
|
||||||
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
|
// createJusticeTx creates transactions which exacts "justice" by sweeping ALL
|
||||||
@ -1424,7 +1473,9 @@ func (b *BreachArbitrator) createJusticeTx(
|
|||||||
err)
|
err)
|
||||||
}
|
}
|
||||||
|
|
||||||
secondLevelSweeps := make([]*wire.MsgTx, 0, len(secondLevelInputs))
|
// TODO(roasbeef): only register one of them?
|
||||||
|
|
||||||
|
secondLevelSweeps := make([]*justiceTxCtx, 0, len(secondLevelInputs))
|
||||||
for _, input := range secondLevelInputs {
|
for _, input := range secondLevelInputs {
|
||||||
sweepTx, err := b.createSweepTx(input)
|
sweepTx, err := b.createSweepTx(input)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1441,9 +1492,23 @@ func (b *BreachArbitrator) createJusticeTx(
|
|||||||
return txs, nil
|
return txs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// justiceTxCtx contains the justice transaction along with other related meta
|
||||||
|
// data.
|
||||||
|
type justiceTxCtx struct {
|
||||||
|
justiceTx *wire.MsgTx
|
||||||
|
|
||||||
|
sweepAddr lnwallet.AddrWithKey
|
||||||
|
|
||||||
|
extraTxOut fn.Option[sweep.SweepOutput]
|
||||||
|
|
||||||
|
fee btcutil.Amount
|
||||||
|
|
||||||
|
inputs []input.Input
|
||||||
|
}
|
||||||
|
|
||||||
// createSweepTx creates a tx that sweeps the passed inputs back to our wallet.
|
// createSweepTx creates a tx that sweeps the passed inputs back to our wallet.
|
||||||
func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
|
func (b *BreachArbitrator) createSweepTx(
|
||||||
error) {
|
inputs ...input.Input) (*justiceTxCtx, error) {
|
||||||
|
|
||||||
if len(inputs) == 0 {
|
if len(inputs) == 0 {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
@ -1466,6 +1531,18 @@ func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
|
|||||||
// nLockTime, and output are already included in the TxWeightEstimator.
|
// nLockTime, and output are already included in the TxWeightEstimator.
|
||||||
weightEstimate.AddP2TROutput()
|
weightEstimate.AddP2TROutput()
|
||||||
|
|
||||||
|
// If any of our inputs has a resolution blob, then we'll add another
|
||||||
|
// P2TR _output_, since we'll want to separate the custom channel
|
||||||
|
// outputs from the regular, BTC only outputs. So we only need one such
|
||||||
|
// output, which'll carry the custom channel "valuables" from both the
|
||||||
|
// breached commitment and HTLC outputs.
|
||||||
|
hasBlobs := fn.Any(func(i input.Input) bool {
|
||||||
|
return i.ResolutionBlob().IsSome()
|
||||||
|
}, inputs)
|
||||||
|
if hasBlobs {
|
||||||
|
weightEstimate.AddP2TROutput()
|
||||||
|
}
|
||||||
|
|
||||||
// Next, we iterate over the breached outputs contained in the
|
// Next, we iterate over the breached outputs contained in the
|
||||||
// retribution info. For each, we switch over the witness type such
|
// retribution info. For each, we switch over the witness type such
|
||||||
// that we contribute the appropriate weight for each input and
|
// that we contribute the appropriate weight for each input and
|
||||||
@ -1499,13 +1576,13 @@ func (b *BreachArbitrator) createSweepTx(inputs ...input.Input) (*wire.MsgTx,
|
|||||||
// sweepSpendableOutputsTxn creates a signed transaction from a sequence of
|
// sweepSpendableOutputsTxn creates a signed transaction from a sequence of
|
||||||
// spendable outputs by sweeping the funds into a single p2wkh output.
|
// spendable outputs by sweeping the funds into a single p2wkh output.
|
||||||
func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
||||||
inputs ...input.Input) (*wire.MsgTx, error) {
|
inputs ...input.Input) (*justiceTxCtx, error) {
|
||||||
|
|
||||||
// First, we obtain a new public key script from the wallet which we'll
|
// First, we obtain a new public key script from the wallet which we'll
|
||||||
// sweep the funds to.
|
// sweep the funds to.
|
||||||
// TODO(roasbeef): possibly create many outputs to minimize change in
|
// TODO(roasbeef): possibly create many outputs to minimize change in
|
||||||
// the future?
|
// the future?
|
||||||
pkScript, err := b.cfg.GenSweepScript()
|
pkScript, err := b.cfg.GenSweepScript().Unpack()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -1524,6 +1601,18 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
|||||||
}
|
}
|
||||||
txFee := feePerKw.FeeForWeight(txWeight)
|
txFee := feePerKw.FeeForWeight(txWeight)
|
||||||
|
|
||||||
|
// At this point, we'll check to see if we have any extra outputs to
|
||||||
|
// add from the aux sweeper.
|
||||||
|
extraChangeOut := fn.MapOptionZ(
|
||||||
|
b.cfg.AuxSweeper,
|
||||||
|
func(aux sweep.AuxSweeper) fn.Result[sweep.SweepOutput] {
|
||||||
|
return aux.DeriveSweepAddr(inputs, pkScript)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err := extraChangeOut.Err(); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
// TODO(roasbeef): already start to siphon their funds into fees
|
// TODO(roasbeef): already start to siphon their funds into fees
|
||||||
sweepAmt := int64(totalAmt - txFee)
|
sweepAmt := int64(totalAmt - txFee)
|
||||||
|
|
||||||
@ -1531,12 +1620,24 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
|||||||
// information gathered above and the provided retribution information.
|
// information gathered above and the provided retribution information.
|
||||||
txn := wire.NewMsgTx(2)
|
txn := wire.NewMsgTx(2)
|
||||||
|
|
||||||
// We begin by adding the output to which our funds will be deposited.
|
// First, we'll add the extra sweep output if it exists, subtracting the
|
||||||
|
// amount from the sweep amt.
|
||||||
|
if b.cfg.AuxSweeper.IsSome() {
|
||||||
|
extraChangeOut.WhenResult(func(o sweep.SweepOutput) {
|
||||||
|
sweepAmt -= o.Value
|
||||||
|
|
||||||
|
txn.AddTxOut(&o.TxOut)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, we'll add the output to which our funds will be deposited.
|
||||||
txn.AddTxOut(&wire.TxOut{
|
txn.AddTxOut(&wire.TxOut{
|
||||||
PkScript: pkScript,
|
PkScript: pkScript.DeliveryAddress,
|
||||||
Value: sweepAmt,
|
Value: sweepAmt,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// TODO(roasbeef): add other output change modify sweep amt
|
||||||
|
|
||||||
// Next, we add all of the spendable outputs as inputs to the
|
// Next, we add all of the spendable outputs as inputs to the
|
||||||
// transaction.
|
// transaction.
|
||||||
for _, inp := range inputs {
|
for _, inp := range inputs {
|
||||||
@ -1592,7 +1693,13 @@ func (b *BreachArbitrator) sweepSpendableOutputsTxn(txWeight lntypes.WeightUnit,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return txn, nil
|
return &justiceTxCtx{
|
||||||
|
justiceTx: txn,
|
||||||
|
sweepAddr: pkScript,
|
||||||
|
extraTxOut: extraChangeOut.Option(),
|
||||||
|
fee: txFee,
|
||||||
|
inputs: inputs,
|
||||||
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// RetributionStore handles persistence of retribution states to disk and is
|
// RetributionStore handles persistence of retribution states to disk and is
|
||||||
@ -1622,13 +1729,29 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
|
|||||||
// commitment, we'll need to stash the control block.
|
// commitment, we'll need to stash the control block.
|
||||||
case input.TaprootRemoteCommitSpend:
|
case input.TaprootRemoteCommitSpend:
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
|
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||||
|
|
||||||
|
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
tapCase.SettledCommitBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType2](
|
||||||
|
blob,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// To spend the revoked output again, we'll store the same
|
// To spend the revoked output again, we'll store the same
|
||||||
// control block value as above, but in a different place.
|
// control block value as above, but in a different place.
|
||||||
case input.TaprootCommitmentRevoke:
|
case input.TaprootCommitmentRevoke:
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
|
tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock = bo.signDesc.ControlBlock
|
||||||
|
|
||||||
|
bo.resolutionBlob.WhenSome(func(blob tlv.Blob) {
|
||||||
|
tapCase.BreachedCommitBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType3](
|
||||||
|
blob,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
// For spending the HTLC outputs, we'll store the first and
|
// For spending the HTLC outputs, we'll store the first and
|
||||||
// second level tweak values.
|
// second level tweak values.
|
||||||
@ -1642,10 +1765,10 @@ func taprootBriefcaseFromRetInfo(retInfo *retributionInfo) *taprootBriefcase {
|
|||||||
secondLevelTweak := bo.secondLevelTapTweak
|
secondLevelTweak := bo.secondLevelTapTweak
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.TapTweaks.BreachedHtlcTweaks[resID] = firstLevelTweak
|
tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID] = firstLevelTweak
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
|
tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID] = secondLevelTweak
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1665,13 +1788,25 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
|||||||
// commitment, we'll apply the control block.
|
// commitment, we'll apply the control block.
|
||||||
case input.TaprootRemoteCommitSpend:
|
case input.TaprootRemoteCommitSpend:
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.CommitSweepCtrlBlock
|
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
|
||||||
|
|
||||||
|
tapCase.SettledCommitBlob.WhenSomeV(
|
||||||
|
func(blob tlv.Blob) {
|
||||||
|
bo.resolutionBlob = fn.Some(blob)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// To spend the revoked output again, we'll apply the same
|
// To spend the revoked output again, we'll apply the same
|
||||||
// control block value as above, but to a different place.
|
// control block value as above, but to a different place.
|
||||||
case input.TaprootCommitmentRevoke:
|
case input.TaprootCommitmentRevoke:
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.RevokeSweepCtrlBlock
|
bo.signDesc.ControlBlock = tapCase.CtrlBlocks.Val.RevokeSweepCtrlBlock
|
||||||
|
|
||||||
|
tapCase.BreachedCommitBlob.WhenSomeV(
|
||||||
|
func(blob tlv.Blob) {
|
||||||
|
bo.resolutionBlob = fn.Some(blob)
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
// For spending the HTLC outputs, we'll apply the first and
|
// For spending the HTLC outputs, we'll apply the first and
|
||||||
// second level tweak values.
|
// second level tweak values.
|
||||||
@ -1680,7 +1815,8 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
|||||||
case input.TaprootHtlcOfferedRevoke:
|
case input.TaprootHtlcOfferedRevoke:
|
||||||
resID := newResolverID(bo.OutPoint())
|
resID := newResolverID(bo.OutPoint())
|
||||||
|
|
||||||
tap1, ok := tapCase.TapTweaks.BreachedHtlcTweaks[resID]
|
//nolint:lll
|
||||||
|
tap1, ok := tapCase.TapTweaks.Val.BreachedHtlcTweaks[resID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unable to find taproot "+
|
return fmt.Errorf("unable to find taproot "+
|
||||||
"tweak for: %v", bo.OutPoint())
|
"tweak for: %v", bo.OutPoint())
|
||||||
@ -1688,7 +1824,7 @@ func applyTaprootRetInfo(tapCase *taprootBriefcase,
|
|||||||
bo.signDesc.TapTweak = tap1[:]
|
bo.signDesc.TapTweak = tap1[:]
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tap2, ok := tapCase.TapTweaks.BreachedSecondLevelHltcTweaks[resID]
|
tap2, ok := tapCase.TapTweaks.Val.BreachedSecondLevelHltcTweaks[resID]
|
||||||
if !ok {
|
if !ok {
|
||||||
return fmt.Errorf("unable to find taproot "+
|
return fmt.Errorf("unable to find taproot "+
|
||||||
"tweak for: %v", bo.OutPoint())
|
"tweak for: %v", bo.OutPoint())
|
||||||
|
|||||||
@ -22,6 +22,7 @@ import (
|
|||||||
"github.com/go-errors/errors"
|
"github.com/go-errors/errors"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/lntest/channels"
|
"github.com/lightningnetwork/lnd/lntest/channels"
|
||||||
@ -1198,6 +1199,8 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
|||||||
input.HtlcSecondLevelRevoke,
|
input.HtlcSecondLevelRevoke,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rBlob := fn.Some([]byte{0x01})
|
||||||
|
|
||||||
breachedOutputs := make([]breachedOutput, len(outputTypes))
|
breachedOutputs := make([]breachedOutput, len(outputTypes))
|
||||||
for i, wt := range outputTypes {
|
for i, wt := range outputTypes {
|
||||||
// Create a fake breached output for each type, ensuring they
|
// Create a fake breached output for each type, ensuring they
|
||||||
@ -1216,6 +1219,7 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
|||||||
nil,
|
nil,
|
||||||
signDesc,
|
signDesc,
|
||||||
1,
|
1,
|
||||||
|
rBlob,
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1226,16 +1230,16 @@ func TestBreachCreateJusticeTx(t *testing.T) {
|
|||||||
|
|
||||||
// The spendAll tx should be spending all the outputs. This is the
|
// The spendAll tx should be spending all the outputs. This is the
|
||||||
// "regular" justice transaction type.
|
// "regular" justice transaction type.
|
||||||
require.Len(t, justiceTxs.spendAll.TxIn, len(breachedOutputs))
|
require.Len(t, justiceTxs.spendAll.justiceTx.TxIn, len(breachedOutputs))
|
||||||
|
|
||||||
// The spendCommitOuts tx should be spending the 4 types of commit outs
|
// The spendCommitOuts tx should be spending the 4 types of commit outs
|
||||||
// (note that in practice there will be at most two commit outputs per
|
// (note that in practice there will be at most two commit outputs per
|
||||||
// commit, but we test all 4 types here).
|
// commit, but we test all 4 types here).
|
||||||
require.Len(t, justiceTxs.spendCommitOuts.TxIn, 4)
|
require.Len(t, justiceTxs.spendCommitOuts.justiceTx.TxIn, 4)
|
||||||
|
|
||||||
// Check that the spendHTLCs tx is spending the two revoked commitment
|
// Check that the spendHTLCs tx is spending the two revoked commitment
|
||||||
// level HTLC output types.
|
// level HTLC output types.
|
||||||
require.Len(t, justiceTxs.spendHTLCs.TxIn, 2)
|
require.Len(t, justiceTxs.spendHTLCs.justiceTx.TxIn, 2)
|
||||||
|
|
||||||
// Finally, check that the spendSecondLevelHTLCs txs are spending the
|
// Finally, check that the spendSecondLevelHTLCs txs are spending the
|
||||||
// second level type.
|
// second level type.
|
||||||
@ -1590,6 +1594,10 @@ func testBreachSpends(t *testing.T, test breachTest) {
|
|||||||
// Notify the breach arbiter about the breach.
|
// Notify the breach arbiter about the breach.
|
||||||
retribution, err := lnwallet.NewBreachRetribution(
|
retribution, err := lnwallet.NewBreachRetribution(
|
||||||
alice.State(), height, 1, forceCloseTx,
|
alice.State(), height, 1, forceCloseTx,
|
||||||
|
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
|
||||||
|
fn.Some[lnwallet.AuxContractResolver](
|
||||||
|
&lnwallet.MockAuxContractResolver{},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create breach retribution")
|
require.NoError(t, err, "unable to create breach retribution")
|
||||||
|
|
||||||
@ -1799,6 +1807,10 @@ func TestBreachDelayedJusticeConfirmation(t *testing.T) {
|
|||||||
// Notify the breach arbiter about the breach.
|
// Notify the breach arbiter about the breach.
|
||||||
retribution, err := lnwallet.NewBreachRetribution(
|
retribution, err := lnwallet.NewBreachRetribution(
|
||||||
alice.State(), height, uint32(blockHeight), forceCloseTx,
|
alice.State(), height, uint32(blockHeight), forceCloseTx,
|
||||||
|
fn.Some[lnwallet.AuxLeafStore](&lnwallet.MockAuxLeafStore{}),
|
||||||
|
fn.Some[lnwallet.AuxContractResolver](
|
||||||
|
&lnwallet.MockAuxContractResolver{},
|
||||||
|
),
|
||||||
)
|
)
|
||||||
require.NoError(t, err, "unable to create breach retribution")
|
require.NoError(t, err, "unable to create breach retribution")
|
||||||
|
|
||||||
@ -2126,15 +2138,19 @@ func createTestArbiter(t *testing.T, contractBreaches chan *ContractBreachEvent,
|
|||||||
// Assemble our test arbiter.
|
// Assemble our test arbiter.
|
||||||
notifier := mock.MakeMockSpendNotifier()
|
notifier := mock.MakeMockSpendNotifier()
|
||||||
ba := NewBreachArbitrator(&BreachConfig{
|
ba := NewBreachArbitrator(&BreachConfig{
|
||||||
CloseLink: func(_ *wire.OutPoint, _ ChannelCloseType) {},
|
CloseLink: func(_ *wire.OutPoint, _ ChannelCloseType) {},
|
||||||
DB: db.ChannelStateDB(),
|
DB: db.ChannelStateDB(),
|
||||||
Estimator: chainfee.NewStaticEstimator(12500, 0),
|
Estimator: chainfee.NewStaticEstimator(12500, 0),
|
||||||
GenSweepScript: func() ([]byte, error) { return nil, nil },
|
GenSweepScript: func() fn.Result[lnwallet.AddrWithKey] {
|
||||||
ContractBreaches: contractBreaches,
|
return fn.Ok(lnwallet.AddrWithKey{})
|
||||||
Signer: signer,
|
},
|
||||||
Notifier: notifier,
|
ContractBreaches: contractBreaches,
|
||||||
PublishTransaction: func(_ *wire.MsgTx, _ string) error { return nil },
|
Signer: signer,
|
||||||
Store: store,
|
Notifier: notifier,
|
||||||
|
PublishTransaction: func(_ *wire.MsgTx, _ string) error {
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Store: store,
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := ba.Start(); err != nil {
|
if err := ba.Start(); err != nil {
|
||||||
@ -2357,9 +2373,12 @@ func createInitChannels(t *testing.T) (
|
|||||||
)
|
)
|
||||||
bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
|
bobSigner := input.NewMockSigner([]*btcec.PrivateKey{bobKeyPriv}, nil)
|
||||||
|
|
||||||
|
signerMock := lnwallet.NewDefaultAuxSignerMock(t)
|
||||||
alicePool := lnwallet.NewSigPool(1, aliceSigner)
|
alicePool := lnwallet.NewSigPool(1, aliceSigner)
|
||||||
channelAlice, err := lnwallet.NewLightningChannel(
|
channelAlice, err := lnwallet.NewLightningChannel(
|
||||||
aliceSigner, aliceChannelState, alicePool,
|
aliceSigner, aliceChannelState, alicePool,
|
||||||
|
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||||
|
lnwallet.WithAuxSigner(signerMock),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
@ -2372,6 +2391,8 @@ func createInitChannels(t *testing.T) (
|
|||||||
bobPool := lnwallet.NewSigPool(1, bobSigner)
|
bobPool := lnwallet.NewSigPool(1, bobSigner)
|
||||||
channelBob, err := lnwallet.NewLightningChannel(
|
channelBob, err := lnwallet.NewLightningChannel(
|
||||||
bobSigner, bobChannelState, bobPool,
|
bobSigner, bobChannelState, bobPool,
|
||||||
|
lnwallet.WithLeafStore(&lnwallet.MockAuxLeafStore{}),
|
||||||
|
lnwallet.WithAuxSigner(signerMock),
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, nil, err
|
return nil, nil, err
|
||||||
|
|||||||
@ -10,9 +10,11 @@ import (
|
|||||||
"github.com/btcsuite/btcd/txscript"
|
"github.com/btcsuite/btcd/txscript"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// ContractResolutions is a wrapper struct around the two forms of resolutions
|
// ContractResolutions is a wrapper struct around the two forms of resolutions
|
||||||
@ -1553,9 +1555,16 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
|||||||
commitResolution := c.CommitResolution
|
commitResolution := c.CommitResolution
|
||||||
commitSignDesc := commitResolution.SelfOutputSignDesc
|
commitSignDesc := commitResolution.SelfOutputSignDesc
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
|
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock = commitSignDesc.ControlBlock
|
||||||
|
|
||||||
|
c.CommitResolution.ResolutionBlob.WhenSome(func(b []byte) {
|
||||||
|
tapCase.SettledCommitBlob = tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType2](b),
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htlcBlobs := newAuxHtlcBlobs()
|
||||||
for _, htlc := range c.HtlcResolutions.IncomingHTLCs {
|
for _, htlc := range c.HtlcResolutions.IncomingHTLCs {
|
||||||
htlc := htlc
|
htlc := htlc
|
||||||
|
|
||||||
@ -1566,12 +1575,13 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resID resolverID
|
||||||
if htlc.SignedSuccessTx != nil {
|
if htlc.SignedSuccessTx != nil {
|
||||||
resID := newResolverID(
|
resID = newResolverID(
|
||||||
htlc.SignedSuccessTx.TxIn[0].PreviousOutPoint,
|
htlc.SignedSuccessTx.TxIn[0].PreviousOutPoint,
|
||||||
)
|
)
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||||
|
|
||||||
// For HTLCs we need to go to the second level for, we
|
// For HTLCs we need to go to the second level for, we
|
||||||
// also need to store the control block needed to
|
// also need to store the control block needed to
|
||||||
@ -1580,13 +1590,17 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
|||||||
//nolint:lll
|
//nolint:lll
|
||||||
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resID := newResolverID(htlc.ClaimOutpoint)
|
resID = newResolverID(htlc.ClaimOutpoint)
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
|
tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htlc.ResolutionBlob.WhenSome(func(b []byte) {
|
||||||
|
htlcBlobs[resID] = b
|
||||||
|
})
|
||||||
}
|
}
|
||||||
for _, htlc := range c.HtlcResolutions.OutgoingHTLCs {
|
for _, htlc := range c.HtlcResolutions.OutgoingHTLCs {
|
||||||
htlc := htlc
|
htlc := htlc
|
||||||
@ -1598,12 +1612,13 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resID resolverID
|
||||||
if htlc.SignedTimeoutTx != nil {
|
if htlc.SignedTimeoutTx != nil {
|
||||||
resID := newResolverID(
|
resID = newResolverID(
|
||||||
htlc.SignedTimeoutTx.TxIn[0].PreviousOutPoint,
|
htlc.SignedTimeoutTx.TxIn[0].PreviousOutPoint,
|
||||||
)
|
)
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID] = ctrlBlock
|
||||||
|
|
||||||
// For HTLCs we need to go to the second level for, we
|
// For HTLCs we need to go to the second level for, we
|
||||||
// also need to store the control block needed to
|
// also need to store the control block needed to
|
||||||
@ -1614,18 +1629,28 @@ func encodeTaprootAuxData(w io.Writer, c *ContractResolutions) error {
|
|||||||
//nolint:lll
|
//nolint:lll
|
||||||
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
bridgeCtrlBlock := htlc.SignDetails.SignDesc.ControlBlock
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = bridgeCtrlBlock
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resID := newResolverID(htlc.ClaimOutpoint)
|
resID = newResolverID(htlc.ClaimOutpoint)
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
|
tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID] = ctrlBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htlc.ResolutionBlob.WhenSome(func(b []byte) {
|
||||||
|
htlcBlobs[resID] = b
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.AnchorResolution != nil {
|
if c.AnchorResolution != nil {
|
||||||
anchorSignDesc := c.AnchorResolution.AnchorSignDescriptor
|
anchorSignDesc := c.AnchorResolution.AnchorSignDescriptor
|
||||||
tapCase.TapTweaks.AnchorTweak = anchorSignDesc.TapTweak
|
tapCase.TapTweaks.Val.AnchorTweak = anchorSignDesc.TapTweak
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(htlcBlobs) != 0 {
|
||||||
|
tapCase.HtlcBlobs = tlv.SomeRecordT(
|
||||||
|
tlv.NewRecordT[tlv.TlvType4](htlcBlobs),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
return tapCase.Encode(w)
|
return tapCase.Encode(w)
|
||||||
@ -1639,9 +1664,15 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
|||||||
|
|
||||||
if c.CommitResolution != nil {
|
if c.CommitResolution != nil {
|
||||||
c.CommitResolution.SelfOutputSignDesc.ControlBlock =
|
c.CommitResolution.SelfOutputSignDesc.ControlBlock =
|
||||||
tapCase.CtrlBlocks.CommitSweepCtrlBlock
|
tapCase.CtrlBlocks.Val.CommitSweepCtrlBlock
|
||||||
|
|
||||||
|
tapCase.SettledCommitBlob.WhenSomeV(func(b []byte) {
|
||||||
|
c.CommitResolution.ResolutionBlob = fn.Some(b)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
htlcBlobs := tapCase.HtlcBlobs.ValOpt().UnwrapOr(newAuxHtlcBlobs())
|
||||||
|
|
||||||
for i := range c.HtlcResolutions.IncomingHTLCs {
|
for i := range c.HtlcResolutions.IncomingHTLCs {
|
||||||
htlc := c.HtlcResolutions.IncomingHTLCs[i]
|
htlc := c.HtlcResolutions.IncomingHTLCs[i]
|
||||||
|
|
||||||
@ -1652,23 +1683,28 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
|
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
|
||||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
if htlc.SignDetails != nil {
|
if htlc.SignDetails != nil {
|
||||||
bridgeCtrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
|
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID]
|
||||||
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resID = newResolverID(htlc.ClaimOutpoint)
|
resID = newResolverID(htlc.ClaimOutpoint)
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
ctrlBlock := tapCase.CtrlBlocks.IncomingHtlcCtrlBlocks[resID]
|
ctrlBlock := tapCase.CtrlBlocks.Val.IncomingHtlcCtrlBlocks[resID]
|
||||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if htlcBlob, ok := htlcBlobs[resID]; ok {
|
||||||
|
htlc.ResolutionBlob = fn.Some(htlcBlob)
|
||||||
|
}
|
||||||
|
|
||||||
c.HtlcResolutions.IncomingHTLCs[i] = htlc
|
c.HtlcResolutions.IncomingHTLCs[i] = htlc
|
||||||
|
|
||||||
}
|
}
|
||||||
for i := range c.HtlcResolutions.OutgoingHTLCs {
|
for i := range c.HtlcResolutions.OutgoingHTLCs {
|
||||||
htlc := c.HtlcResolutions.OutgoingHTLCs[i]
|
htlc := c.HtlcResolutions.OutgoingHTLCs[i]
|
||||||
@ -1680,28 +1716,32 @@ func decodeTapRootAuxData(r io.Reader, c *ContractResolutions) error {
|
|||||||
)
|
)
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
ctrlBlock := tapCase.CtrlBlocks.SecondLevelCtrlBlocks[resID]
|
ctrlBlock := tapCase.CtrlBlocks.Val.SecondLevelCtrlBlocks[resID]
|
||||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
if htlc.SignDetails != nil {
|
if htlc.SignDetails != nil {
|
||||||
bridgeCtrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
|
bridgeCtrlBlock := tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID]
|
||||||
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
htlc.SignDetails.SignDesc.ControlBlock = bridgeCtrlBlock
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
resID = newResolverID(htlc.ClaimOutpoint)
|
resID = newResolverID(htlc.ClaimOutpoint)
|
||||||
|
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
ctrlBlock := tapCase.CtrlBlocks.OutgoingHtlcCtrlBlocks[resID]
|
ctrlBlock := tapCase.CtrlBlocks.Val.OutgoingHtlcCtrlBlocks[resID]
|
||||||
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
htlc.SweepSignDesc.ControlBlock = ctrlBlock
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if htlcBlob, ok := htlcBlobs[resID]; ok {
|
||||||
|
htlc.ResolutionBlob = fn.Some(htlcBlob)
|
||||||
|
}
|
||||||
|
|
||||||
c.HtlcResolutions.OutgoingHTLCs[i] = htlc
|
c.HtlcResolutions.OutgoingHTLCs[i] = htlc
|
||||||
}
|
}
|
||||||
|
|
||||||
if c.AnchorResolution != nil {
|
if c.AnchorResolution != nil {
|
||||||
c.AnchorResolution.AnchorSignDescriptor.TapTweak =
|
c.AnchorResolution.AnchorSignDescriptor.TapTweak =
|
||||||
tapCase.TapTweaks.AnchorTweak
|
tapCase.TapTweaks.Val.AnchorTweak
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@ -217,6 +217,18 @@ type ChainArbitratorConfig struct {
|
|||||||
// meanwhile, turn `PaymentCircuit` into an interface or bring it to a
|
// meanwhile, turn `PaymentCircuit` into an interface or bring it to a
|
||||||
// lower package.
|
// lower package.
|
||||||
QueryIncomingCircuit func(circuit models.CircuitKey) *models.CircuitKey
|
QueryIncomingCircuit func(circuit models.CircuitKey) *models.CircuitKey
|
||||||
|
|
||||||
|
// AuxLeafStore is an optional store that can be used to store auxiliary
|
||||||
|
// leaves for certain custom channel types.
|
||||||
|
AuxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||||
|
|
||||||
|
// AuxSigner is an optional signer that can be used to sign auxiliary
|
||||||
|
// leaves for certain custom channel types.
|
||||||
|
AuxSigner fn.Option[lnwallet.AuxSigner]
|
||||||
|
|
||||||
|
// AuxResolver is an optional interface that can be used to modify the
|
||||||
|
// way contracts are resolved.
|
||||||
|
AuxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||||
}
|
}
|
||||||
|
|
||||||
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all
|
// ChainArbitrator is a sub-system that oversees the on-chain resolution of all
|
||||||
@ -299,8 +311,19 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var chanOpts []lnwallet.ChannelOpt
|
||||||
|
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||||
|
})
|
||||||
|
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||||
|
})
|
||||||
|
a.c.cfg.AuxResolver.WhenSome(func(s lnwallet.AuxContractResolver) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
|
||||||
|
})
|
||||||
|
|
||||||
chanMachine, err := lnwallet.NewLightningChannel(
|
chanMachine, err := lnwallet.NewLightningChannel(
|
||||||
a.c.cfg.Signer, channel, nil,
|
a.c.cfg.Signer, channel, nil, chanOpts...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -312,11 +335,10 @@ func (a *arbChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
|||||||
// ForceCloseChan should force close the contract that this attendant is
|
// ForceCloseChan should force close the contract that this attendant is
|
||||||
// watching over. We'll use this when we decide that we need to go to chain. It
|
// watching over. We'll use this when we decide that we need to go to chain. It
|
||||||
// should in addition tell the switch to remove the corresponding link, such
|
// should in addition tell the switch to remove the corresponding link, such
|
||||||
// that we won't accept any new updates. The returned summary contains all items
|
// that we won't accept any new updates.
|
||||||
// needed to eventually resolve all outputs on chain.
|
|
||||||
//
|
//
|
||||||
// NOTE: Part of the ArbChannel interface.
|
// NOTE: Part of the ArbChannel interface.
|
||||||
func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
|
func (a *arbChannel) ForceCloseChan() (*wire.MsgTx, error) {
|
||||||
// First, we mark the channel as borked, this ensure
|
// First, we mark the channel as borked, this ensure
|
||||||
// that no new state transitions can happen, and also
|
// that no new state transitions can happen, and also
|
||||||
// that the link won't be loaded into the switch.
|
// that the link won't be loaded into the switch.
|
||||||
@ -344,15 +366,34 @@ func (a *arbChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var chanOpts []lnwallet.ChannelOpt
|
||||||
|
a.c.cfg.AuxLeafStore.WhenSome(func(s lnwallet.AuxLeafStore) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithLeafStore(s))
|
||||||
|
})
|
||||||
|
a.c.cfg.AuxSigner.WhenSome(func(s lnwallet.AuxSigner) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithAuxSigner(s))
|
||||||
|
})
|
||||||
|
a.c.cfg.AuxResolver.WhenSome(func(s lnwallet.AuxContractResolver) {
|
||||||
|
chanOpts = append(chanOpts, lnwallet.WithAuxResolver(s))
|
||||||
|
})
|
||||||
|
|
||||||
// Finally, we'll force close the channel completing
|
// Finally, we'll force close the channel completing
|
||||||
// the force close workflow.
|
// the force close workflow.
|
||||||
chanMachine, err := lnwallet.NewLightningChannel(
|
chanMachine, err := lnwallet.NewLightningChannel(
|
||||||
a.c.cfg.Signer, channel, nil,
|
a.c.cfg.Signer, channel, nil, chanOpts...,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return chanMachine.ForceClose()
|
|
||||||
|
closeSummary, err := chanMachine.ForceClose(
|
||||||
|
lnwallet.WithSkipContractResolutions(),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return closeSummary.CloseTx, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// newActiveChannelArbitrator creates a new instance of an active channel
|
// newActiveChannelArbitrator creates a new instance of an active channel
|
||||||
@ -557,6 +598,8 @@ func (c *ChainArbitrator) Start() error {
|
|||||||
isOurAddr: c.cfg.IsOurAddress,
|
isOurAddr: c.cfg.IsOurAddress,
|
||||||
contractBreach: breachClosure,
|
contractBreach: breachClosure,
|
||||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||||
|
auxLeafStore: c.cfg.AuxLeafStore,
|
||||||
|
auxResolver: c.cfg.AuxResolver,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -1186,6 +1229,8 @@ func (c *ChainArbitrator) WatchNewChannel(newChan *channeldb.OpenChannel) error
|
|||||||
)
|
)
|
||||||
},
|
},
|
||||||
extractStateNumHint: lnwallet.GetStateNumHint,
|
extractStateNumHint: lnwallet.GetStateNumHint,
|
||||||
|
auxLeafStore: c.cfg.AuxLeafStore,
|
||||||
|
auxResolver: c.cfg.AuxResolver,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@ -193,6 +193,12 @@ type chainWatcherConfig struct {
|
|||||||
// obfuscater. This is used by the chain watcher to identify which
|
// obfuscater. This is used by the chain watcher to identify which
|
||||||
// state was broadcast and confirmed on-chain.
|
// state was broadcast and confirmed on-chain.
|
||||||
extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
|
extractStateNumHint func(*wire.MsgTx, [lnwallet.StateHintSize]byte) uint64
|
||||||
|
|
||||||
|
// auxLeafStore can be used to fetch information for custom channels.
|
||||||
|
auxLeafStore fn.Option[lnwallet.AuxLeafStore]
|
||||||
|
|
||||||
|
// auxResolver is used to supplement contract resolution.
|
||||||
|
auxResolver fn.Option[lnwallet.AuxContractResolver]
|
||||||
}
|
}
|
||||||
|
|
||||||
// chainWatcher is a system that's assigned to every active channel. The duty
|
// chainWatcher is a system that's assigned to every active channel. The duty
|
||||||
@ -308,7 +314,7 @@ func (c *chainWatcher) Start() error {
|
|||||||
)
|
)
|
||||||
if chanState.ChanType.IsTaproot() {
|
if chanState.ChanType.IsTaproot() {
|
||||||
c.fundingPkScript, _, err = input.GenTaprootFundingScript(
|
c.fundingPkScript, _, err = input.GenTaprootFundingScript(
|
||||||
localKey, remoteKey, 0,
|
localKey, remoteKey, 0, chanState.TapscriptRoot,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -423,15 +429,37 @@ func (c *chainWatcher) handleUnknownLocalState(
|
|||||||
&c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
|
&c.cfg.chanState.LocalChanCfg, &c.cfg.chanState.RemoteChanCfg,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
auxResult, err := fn.MapOptionZ(
|
||||||
|
c.cfg.auxLeafStore,
|
||||||
|
//nolint:lll
|
||||||
|
func(s lnwallet.AuxLeafStore) fn.Result[lnwallet.CommitDiffAuxResult] {
|
||||||
|
return s.FetchLeavesFromCommit(
|
||||||
|
lnwallet.NewAuxChanState(c.cfg.chanState),
|
||||||
|
c.cfg.chanState.LocalCommitment, *commitKeyRing,
|
||||||
|
lntypes.Local,
|
||||||
|
)
|
||||||
|
},
|
||||||
|
).Unpack()
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("unable to fetch aux leaves: %w", err)
|
||||||
|
}
|
||||||
|
|
||||||
// With the keys derived, we'll construct the remote script that'll be
|
// With the keys derived, we'll construct the remote script that'll be
|
||||||
// present if they have a non-dust balance on the commitment.
|
// present if they have a non-dust balance on the commitment.
|
||||||
var leaseExpiry uint32
|
var leaseExpiry uint32
|
||||||
if c.cfg.chanState.ChanType.HasLeaseExpiration() {
|
if c.cfg.chanState.ChanType.HasLeaseExpiration() {
|
||||||
leaseExpiry = c.cfg.chanState.ThawHeight
|
leaseExpiry = c.cfg.chanState.ThawHeight
|
||||||
}
|
}
|
||||||
|
|
||||||
|
remoteAuxLeaf := fn.ChainOption(
|
||||||
|
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
|
||||||
|
return l.RemoteAuxLeaf
|
||||||
|
},
|
||||||
|
)(auxResult.AuxLeaves)
|
||||||
remoteScript, _, err := lnwallet.CommitScriptToRemote(
|
remoteScript, _, err := lnwallet.CommitScriptToRemote(
|
||||||
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
||||||
commitKeyRing.ToRemoteKey, leaseExpiry,
|
commitKeyRing.ToRemoteKey, leaseExpiry,
|
||||||
|
remoteAuxLeaf,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -440,10 +468,16 @@ func (c *chainWatcher) handleUnknownLocalState(
|
|||||||
// Next, we'll derive our script that includes the revocation base for
|
// Next, we'll derive our script that includes the revocation base for
|
||||||
// the remote party allowing them to claim this output before the CSV
|
// the remote party allowing them to claim this output before the CSV
|
||||||
// delay if we breach.
|
// delay if we breach.
|
||||||
|
localAuxLeaf := fn.ChainOption(
|
||||||
|
func(l lnwallet.CommitAuxLeaves) input.AuxTapLeaf {
|
||||||
|
return l.LocalAuxLeaf
|
||||||
|
},
|
||||||
|
)(auxResult.AuxLeaves)
|
||||||
localScript, err := lnwallet.CommitScriptToSelf(
|
localScript, err := lnwallet.CommitScriptToSelf(
|
||||||
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
c.cfg.chanState.ChanType, c.cfg.chanState.IsInitiator,
|
||||||
commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
|
commitKeyRing.ToLocalKey, commitKeyRing.RevocationKey,
|
||||||
uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
|
uint32(c.cfg.chanState.LocalChanCfg.CsvDelay), leaseExpiry,
|
||||||
|
localAuxLeaf,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
@ -866,7 +900,7 @@ func (c *chainWatcher) handlePossibleBreach(commitSpend *chainntnfs.SpendDetail,
|
|||||||
spendHeight := uint32(commitSpend.SpendingHeight)
|
spendHeight := uint32(commitSpend.SpendingHeight)
|
||||||
retribution, err := lnwallet.NewBreachRetribution(
|
retribution, err := lnwallet.NewBreachRetribution(
|
||||||
c.cfg.chanState, broadcastStateNum, spendHeight,
|
c.cfg.chanState, broadcastStateNum, spendHeight,
|
||||||
commitSpend.SpendingTx,
|
commitSpend.SpendingTx, c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
@ -1116,8 +1150,8 @@ func (c *chainWatcher) dispatchLocalForceClose(
|
|||||||
"detected", c.cfg.chanState.FundingOutpoint)
|
"detected", c.cfg.chanState.FundingOutpoint)
|
||||||
|
|
||||||
forceClose, err := lnwallet.NewLocalForceCloseSummary(
|
forceClose, err := lnwallet.NewLocalForceCloseSummary(
|
||||||
c.cfg.chanState, c.cfg.signer,
|
c.cfg.chanState, c.cfg.signer, commitSpend.SpendingTx, stateNum,
|
||||||
commitSpend.SpendingTx, stateNum,
|
c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -1141,16 +1175,29 @@ func (c *chainWatcher) dispatchLocalForceClose(
|
|||||||
LocalChanConfig: c.cfg.chanState.LocalChanCfg,
|
LocalChanConfig: c.cfg.chanState.LocalChanCfg,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
resolutions, err := forceClose.ContractResolutions.UnwrapOrErr(
|
||||||
|
fmt.Errorf("resolutions not found"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
// If our commitment output isn't dust or we have active HTLC's on the
|
// If our commitment output isn't dust or we have active HTLC's on the
|
||||||
// commitment transaction, then we'll populate the balances on the
|
// commitment transaction, then we'll populate the balances on the
|
||||||
// close channel summary.
|
// close channel summary.
|
||||||
if forceClose.CommitResolution != nil {
|
if resolutions.CommitResolution != nil {
|
||||||
closeSummary.SettledBalance = chanSnapshot.LocalBalance.ToSatoshis()
|
localBalance := chanSnapshot.LocalBalance.ToSatoshis()
|
||||||
closeSummary.TimeLockedBalance = chanSnapshot.LocalBalance.ToSatoshis()
|
closeSummary.SettledBalance = localBalance
|
||||||
|
closeSummary.TimeLockedBalance = localBalance
|
||||||
}
|
}
|
||||||
for _, htlc := range forceClose.HtlcResolutions.OutgoingHTLCs {
|
|
||||||
htlcValue := btcutil.Amount(htlc.SweepSignDesc.Output.Value)
|
if resolutions.HtlcResolutions != nil {
|
||||||
closeSummary.TimeLockedBalance += htlcValue
|
for _, htlc := range resolutions.HtlcResolutions.OutgoingHTLCs {
|
||||||
|
htlcValue := btcutil.Amount(
|
||||||
|
htlc.SweepSignDesc.Output.Value,
|
||||||
|
)
|
||||||
|
closeSummary.TimeLockedBalance += htlcValue
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Attempt to add a channel sync message to the close summary.
|
// Attempt to add a channel sync message to the close summary.
|
||||||
@ -1209,8 +1256,8 @@ func (c *chainWatcher) dispatchRemoteForceClose(
|
|||||||
// materials required to let each subscriber sweep the funds in the
|
// materials required to let each subscriber sweep the funds in the
|
||||||
// channel on-chain.
|
// channel on-chain.
|
||||||
uniClose, err := lnwallet.NewUnilateralCloseSummary(
|
uniClose, err := lnwallet.NewUnilateralCloseSummary(
|
||||||
c.cfg.chanState, c.cfg.signer, commitSpend,
|
c.cfg.chanState, c.cfg.signer, commitSpend, remoteCommit,
|
||||||
remoteCommit, commitPoint,
|
commitPoint, c.cfg.auxLeafStore, c.cfg.auxResolver,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package contractcourt
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"fmt"
|
"fmt"
|
||||||
"testing"
|
"testing"
|
||||||
@ -145,17 +146,15 @@ func TestChainWatcherRemoteUnilateralClosePendingCommit(t *testing.T) {
|
|||||||
|
|
||||||
// With the HTLC added, we'll now manually initiate a state transition
|
// With the HTLC added, we'll now manually initiate a state transition
|
||||||
// from Alice to Bob.
|
// from Alice to Bob.
|
||||||
_, err = aliceChannel.SignNextCommitment()
|
testQuit, testQuitFunc := context.WithCancel(context.Background())
|
||||||
if err != nil {
|
t.Cleanup(testQuitFunc)
|
||||||
t.Fatal(err)
|
_, err = aliceChannel.SignNextCommitment(testQuit)
|
||||||
}
|
require.NoError(t, err)
|
||||||
|
|
||||||
// At this point, we'll now Bob broadcasting this new pending unrevoked
|
// At this point, we'll now Bob broadcasting this new pending unrevoked
|
||||||
// commitment.
|
// commitment.
|
||||||
bobPendingCommit, err := aliceChannel.State().RemoteCommitChainTip()
|
bobPendingCommit, err := aliceChannel.State().RemoteCommitChainTip()
|
||||||
if err != nil {
|
require.NoError(t, err)
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// We'll craft a fake spend notification with Bob's actual commitment.
|
// We'll craft a fake spend notification with Bob's actual commitment.
|
||||||
// The chain watcher should be able to detect that this is a pending
|
// The chain watcher should be able to detect that this is a pending
|
||||||
@ -505,14 +504,24 @@ func TestChainWatcherLocalForceCloseDetect(t *testing.T) {
|
|||||||
// outputs.
|
// outputs.
|
||||||
select {
|
select {
|
||||||
case summary := <-chanEvents.LocalUnilateralClosure:
|
case summary := <-chanEvents.LocalUnilateralClosure:
|
||||||
|
resOpt := summary.LocalForceCloseSummary.
|
||||||
|
ContractResolutions
|
||||||
|
|
||||||
|
resolutions, err := resOpt.UnwrapOrErr(
|
||||||
|
fmt.Errorf("resolutions not found"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to get resolutions: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
// Make sure we correctly extracted the commit
|
// Make sure we correctly extracted the commit
|
||||||
// resolution if we had a local output.
|
// resolution if we had a local output.
|
||||||
if remoteOutputOnly {
|
if remoteOutputOnly {
|
||||||
if summary.CommitResolution != nil {
|
if resolutions.CommitResolution != nil {
|
||||||
t.Fatalf("expected no commit resolution")
|
t.Fatalf("expected no commit resolution")
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if summary.CommitResolution == nil {
|
if resolutions.CommitResolution == nil {
|
||||||
t.Fatalf("expected commit resolution")
|
t.Fatalf("expected commit resolution")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -98,7 +98,7 @@ type ArbChannel interface {
|
|||||||
// corresponding link, such that we won't accept any new updates. The
|
// corresponding link, such that we won't accept any new updates. The
|
||||||
// returned summary contains all items needed to eventually resolve all
|
// returned summary contains all items needed to eventually resolve all
|
||||||
// outputs on chain.
|
// outputs on chain.
|
||||||
ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error)
|
ForceCloseChan() (*wire.MsgTx, error)
|
||||||
|
|
||||||
// NewAnchorResolutions returns the anchor resolutions for currently
|
// NewAnchorResolutions returns the anchor resolutions for currently
|
||||||
// valid commitment transactions.
|
// valid commitment transactions.
|
||||||
@ -482,6 +482,20 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.wg.Add(1)
|
||||||
|
go c.channelAttendant(bestHeight, state.commitSet)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// progressStateMachineAfterRestart attempts to progress the state machine
|
||||||
|
// after a restart. This makes sure that if the state transition failed, we
|
||||||
|
// will try to progress the state machine again. Moreover it will relaunch
|
||||||
|
// resolvers if the channel is still in the pending close state and has not
|
||||||
|
// been fully resolved yet.
|
||||||
|
func (c *ChannelArbitrator) progressStateMachineAfterRestart(bestHeight int32,
|
||||||
|
commitSet *CommitSet) error {
|
||||||
|
|
||||||
// If the channel has been marked pending close in the database, and we
|
// If the channel has been marked pending close in the database, and we
|
||||||
// haven't transitioned the state machine to StateContractClosed (or a
|
// haven't transitioned the state machine to StateContractClosed (or a
|
||||||
// succeeding state), then a state transition most likely failed. We'll
|
// succeeding state), then a state transition most likely failed. We'll
|
||||||
@ -527,7 +541,7 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState) error {
|
|||||||
// on-chain state, and our set of active contracts.
|
// on-chain state, and our set of active contracts.
|
||||||
startingState := c.state
|
startingState := c.state
|
||||||
nextState, _, err := c.advanceState(
|
nextState, _, err := c.advanceState(
|
||||||
triggerHeight, trigger, state.commitSet,
|
triggerHeight, trigger, commitSet,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
switch err {
|
switch err {
|
||||||
@ -564,14 +578,12 @@ func (c *ChannelArbitrator) Start(state *chanArbStartState) error {
|
|||||||
// receive a chain event from the chain watcher that the
|
// receive a chain event from the chain watcher that the
|
||||||
// commitment has been confirmed on chain, and before we
|
// commitment has been confirmed on chain, and before we
|
||||||
// advance our state step, we call InsertConfirmedCommitSet.
|
// advance our state step, we call InsertConfirmedCommitSet.
|
||||||
err := c.relaunchResolvers(state.commitSet, triggerHeight)
|
err := c.relaunchResolvers(commitSet, triggerHeight)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.wg.Add(1)
|
|
||||||
go c.channelAttendant(bestHeight)
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1058,7 +1070,7 @@ func (c *ChannelArbitrator) stateStep(
|
|||||||
// We'll tell the switch that it should remove the link for
|
// We'll tell the switch that it should remove the link for
|
||||||
// this channel, in addition to fetching the force close
|
// this channel, in addition to fetching the force close
|
||||||
// summary needed to close this channel on chain.
|
// summary needed to close this channel on chain.
|
||||||
closeSummary, err := c.cfg.Channel.ForceCloseChan()
|
forceCloseTx, err := c.cfg.Channel.ForceCloseChan()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("ChannelArbitrator(%v): unable to "+
|
log.Errorf("ChannelArbitrator(%v): unable to "+
|
||||||
"force close: %v", c.cfg.ChanPoint, err)
|
"force close: %v", c.cfg.ChanPoint, err)
|
||||||
@ -1078,7 +1090,7 @@ func (c *ChannelArbitrator) stateStep(
|
|||||||
|
|
||||||
return StateError, closeTx, err
|
return StateError, closeTx, err
|
||||||
}
|
}
|
||||||
closeTx = closeSummary.CloseTx
|
closeTx = forceCloseTx
|
||||||
|
|
||||||
// Before publishing the transaction, we store it to the
|
// Before publishing the transaction, we store it to the
|
||||||
// database, such that we can re-publish later in case it
|
// database, such that we can re-publish later in case it
|
||||||
@ -1982,9 +1994,11 @@ func (c *ChannelArbitrator) isPreimageAvailable(hash lntypes.Hash) (bool,
|
|||||||
// have the incoming contest resolver decide that we don't want to
|
// have the incoming contest resolver decide that we don't want to
|
||||||
// settle this invoice.
|
// settle this invoice.
|
||||||
invoice, err := c.cfg.Registry.LookupInvoice(context.Background(), hash)
|
invoice, err := c.cfg.Registry.LookupInvoice(context.Background(), hash)
|
||||||
switch err {
|
switch {
|
||||||
case nil:
|
case err == nil:
|
||||||
case invoices.ErrInvoiceNotFound, invoices.ErrNoInvoicesCreated:
|
case errors.Is(err, invoices.ErrInvoiceNotFound) ||
|
||||||
|
errors.Is(err, invoices.ErrNoInvoicesCreated):
|
||||||
|
|
||||||
return false, nil
|
return false, nil
|
||||||
default:
|
default:
|
||||||
return false, err
|
return false, err
|
||||||
@ -2773,13 +2787,28 @@ func (c *ChannelArbitrator) updateActiveHTLCs() {
|
|||||||
// Nursery for incubation, and ultimate sweeping.
|
// Nursery for incubation, and ultimate sweeping.
|
||||||
//
|
//
|
||||||
// NOTE: This MUST be run as a goroutine.
|
// NOTE: This MUST be run as a goroutine.
|
||||||
func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
//
|
||||||
|
//nolint:funlen
|
||||||
|
func (c *ChannelArbitrator) channelAttendant(bestHeight int32,
|
||||||
|
commitSet *CommitSet) {
|
||||||
|
|
||||||
// TODO(roasbeef): tell top chain arb we're done
|
// TODO(roasbeef): tell top chain arb we're done
|
||||||
defer func() {
|
defer func() {
|
||||||
c.wg.Done()
|
c.wg.Done()
|
||||||
}()
|
}()
|
||||||
|
|
||||||
|
err := c.progressStateMachineAfterRestart(bestHeight, commitSet)
|
||||||
|
if err != nil {
|
||||||
|
// In case of an error, we return early but we do not shutdown
|
||||||
|
// LND, because there might be other channels that still can be
|
||||||
|
// resolved and we don't want to interfere with that.
|
||||||
|
// We continue to run the channel attendant in case the channel
|
||||||
|
// closes via other means for example the remote pary force
|
||||||
|
// closes the channel. So we log the error and continue.
|
||||||
|
log.Errorf("Unable to progress state machine after "+
|
||||||
|
"restart: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
|
|
||||||
@ -2869,11 +2898,36 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
|||||||
}
|
}
|
||||||
closeTx := closeInfo.CloseTx
|
closeTx := closeInfo.CloseTx
|
||||||
|
|
||||||
|
resolutions, err := closeInfo.ContractResolutions.
|
||||||
|
UnwrapOrErr(
|
||||||
|
fmt.Errorf("resolutions not found"),
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("ChannelArbitrator(%v): unable to "+
|
||||||
|
"get resolutions: %v", c.cfg.ChanPoint,
|
||||||
|
err)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// We make sure that the htlc resolutions are present
|
||||||
|
// otherwise we would panic dereferencing the pointer.
|
||||||
|
//
|
||||||
|
// TODO(ziggie): Refactor ContractResolutions to use
|
||||||
|
// options.
|
||||||
|
if resolutions.HtlcResolutions == nil {
|
||||||
|
log.Errorf("ChannelArbitrator(%v): htlc "+
|
||||||
|
"resolutions not found",
|
||||||
|
c.cfg.ChanPoint)
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
contractRes := &ContractResolutions{
|
contractRes := &ContractResolutions{
|
||||||
CommitHash: closeTx.TxHash(),
|
CommitHash: closeTx.TxHash(),
|
||||||
CommitResolution: closeInfo.CommitResolution,
|
CommitResolution: resolutions.CommitResolution,
|
||||||
HtlcResolutions: *closeInfo.HtlcResolutions,
|
HtlcResolutions: *resolutions.HtlcResolutions,
|
||||||
AnchorResolution: closeInfo.AnchorResolution,
|
AnchorResolution: resolutions.AnchorResolution,
|
||||||
}
|
}
|
||||||
|
|
||||||
// When processing a unilateral close event, we'll
|
// When processing a unilateral close event, we'll
|
||||||
@ -2882,7 +2936,7 @@ func (c *ChannelArbitrator) channelAttendant(bestHeight int32) {
|
|||||||
// available to fetch in that state, we'll also write
|
// available to fetch in that state, we'll also write
|
||||||
// the commit set so we can reconstruct our chain
|
// the commit set so we can reconstruct our chain
|
||||||
// actions on restart.
|
// actions on restart.
|
||||||
err := c.log.LogContractResolutions(contractRes)
|
err = c.log.LogContractResolutions(contractRes)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Errorf("Unable to write resolutions: %v",
|
log.Errorf("Unable to write resolutions: %v",
|
||||||
err)
|
err)
|
||||||
|
|||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
"github.com/lightningnetwork/lnd/lntest/mock"
|
"github.com/lightningnetwork/lnd/lntest/mock"
|
||||||
|
"github.com/lightningnetwork/lnd/lntest/wait"
|
||||||
"github.com/lightningnetwork/lnd/lntypes"
|
"github.com/lightningnetwork/lnd/lntypes"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/lnwire"
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
@ -693,11 +694,15 @@ func TestChannelArbitratorLocalForceClose(t *testing.T) {
|
|||||||
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
chanArbCtx.AssertState(StateCommitmentBroadcasted)
|
||||||
|
|
||||||
// Now notify about the local force close getting confirmed.
|
// Now notify about the local force close getting confirmed.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||||
SpendDetail: &chainntnfs.SpendDetail{},
|
SpendDetail: &chainntnfs.SpendDetail{},
|
||||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||||
CloseTx: &wire.MsgTx{},
|
CloseTx: &wire.MsgTx{},
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||||
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||||
}
|
}
|
||||||
@ -969,15 +974,18 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||||
SpendDetail: &chainntnfs.SpendDetail{},
|
SpendDetail: &chainntnfs.SpendDetail{},
|
||||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||||
CloseTx: closeTx,
|
CloseTx: closeTx,
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{
|
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||||
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
HtlcResolutions: &lnwallet.HtlcResolutions{
|
||||||
outgoingRes,
|
OutgoingHTLCs: []lnwallet.OutgoingHtlcResolution{
|
||||||
|
outgoingRes,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
},
|
}),
|
||||||
},
|
},
|
||||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||||
CommitSet: CommitSet{
|
CommitSet: CommitSet{
|
||||||
@ -1036,10 +1044,19 @@ func TestChannelArbitratorLocalForceClosePendingHtlc(t *testing.T) {
|
|||||||
|
|
||||||
// Post restart, it should be the case that our resolver was properly
|
// Post restart, it should be the case that our resolver was properly
|
||||||
// supplemented, and we only have a single resolver in the final set.
|
// supplemented, and we only have a single resolver in the final set.
|
||||||
if len(chanArb.activeResolvers) != 1 {
|
// The resolvers are added concurrently so we need to wait here.
|
||||||
t.Fatalf("expected single resolver, instead got: %v",
|
err = wait.NoError(func() error {
|
||||||
len(chanArb.activeResolvers))
|
chanArb.activeResolversLock.Lock()
|
||||||
}
|
defer chanArb.activeResolversLock.Unlock()
|
||||||
|
|
||||||
|
if len(chanArb.activeResolvers) != 1 {
|
||||||
|
return fmt.Errorf("expected single resolver, instead "+
|
||||||
|
"got: %v", len(chanArb.activeResolvers))
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}, defaultTimeout)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
// We'll now examine the in-memory state of the active resolvers to
|
// We'll now examine the in-memory state of the active resolvers to
|
||||||
// ensure t hey were populated properly.
|
// ensure t hey were populated properly.
|
||||||
@ -1611,12 +1628,15 @@ func TestChannelArbitratorCommitFailure(t *testing.T) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
closeType: channeldb.LocalForceClose,
|
closeType: channeldb.LocalForceClose,
|
||||||
|
//nolint:lll
|
||||||
sendEvent: func(chanArb *ChannelArbitrator) {
|
sendEvent: func(chanArb *ChannelArbitrator) {
|
||||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||||
SpendDetail: &chainntnfs.SpendDetail{},
|
SpendDetail: &chainntnfs.SpendDetail{},
|
||||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||||
CloseTx: &wire.MsgTx{},
|
CloseTx: &wire.MsgTx{},
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||||
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||||
}
|
}
|
||||||
@ -1944,11 +1964,15 @@ func TestChannelArbitratorDanglingCommitForceClose(t *testing.T) {
|
|||||||
// being canalled back. Also note that there're no HTLC
|
// being canalled back. Also note that there're no HTLC
|
||||||
// resolutions sent since we have none on our
|
// resolutions sent since we have none on our
|
||||||
// commitment transaction.
|
// commitment transaction.
|
||||||
|
//
|
||||||
|
//nolint:lll
|
||||||
uniCloseInfo := &LocalUnilateralCloseInfo{
|
uniCloseInfo := &LocalUnilateralCloseInfo{
|
||||||
SpendDetail: &chainntnfs.SpendDetail{},
|
SpendDetail: &chainntnfs.SpendDetail{},
|
||||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||||
CloseTx: closeTx,
|
CloseTx: closeTx,
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||||
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||||
CommitSet: CommitSet{
|
CommitSet: CommitSet{
|
||||||
@ -2754,12 +2778,15 @@ func TestChannelArbitratorAnchors(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
chanArb.cfg.ChainEvents.LocalUnilateralClosure <- &LocalUnilateralCloseInfo{
|
||||||
SpendDetail: &chainntnfs.SpendDetail{},
|
SpendDetail: &chainntnfs.SpendDetail{},
|
||||||
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
LocalForceCloseSummary: &lnwallet.LocalForceCloseSummary{
|
||||||
CloseTx: closeTx,
|
CloseTx: closeTx,
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
ContractResolutions: fn.Some(lnwallet.ContractResolutions{
|
||||||
AnchorResolution: anchorResolution,
|
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
||||||
|
AnchorResolution: anchorResolution,
|
||||||
|
}),
|
||||||
},
|
},
|
||||||
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
ChannelCloseSummary: &channeldb.ChannelCloseSummary{},
|
||||||
CommitSet: CommitSet{
|
CommitSet: CommitSet{
|
||||||
@ -2867,9 +2894,12 @@ func TestChannelArbitratorStartForceCloseFail(t *testing.T) {
|
|||||||
{
|
{
|
||||||
name: "Commitment is rejected with an " +
|
name: "Commitment is rejected with an " +
|
||||||
"unmatched error",
|
"unmatched error",
|
||||||
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
|
broadcastErr: fmt.Errorf("Reject Commitment Tx"),
|
||||||
expectedState: StateBroadcastCommit,
|
expectedState: StateBroadcastCommit,
|
||||||
expectedStartup: false,
|
// We should still be able to start up since we other
|
||||||
|
// channels might be closing as well and we should
|
||||||
|
// resolve the contracts.
|
||||||
|
expectedStartup: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// We started after the DLP was triggered, and try to force
|
// We started after the DLP was triggered, and try to force
|
||||||
@ -2993,14 +3023,10 @@ func (m *mockChannel) NewAnchorResolutions() (*lnwallet.AnchorResolutions,
|
|||||||
return &lnwallet.AnchorResolutions{}, nil
|
return &lnwallet.AnchorResolutions{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockChannel) ForceCloseChan() (*lnwallet.LocalForceCloseSummary, error) {
|
func (m *mockChannel) ForceCloseChan() (*wire.MsgTx, error) {
|
||||||
if m.forceCloseErr != nil {
|
if m.forceCloseErr != nil {
|
||||||
return nil, m.forceCloseErr
|
return nil, m.forceCloseErr
|
||||||
}
|
}
|
||||||
|
|
||||||
summary := &lnwallet.LocalForceCloseSummary{
|
return &wire.MsgTx{}, nil
|
||||||
CloseTx: &wire.MsgTx{},
|
|
||||||
HtlcResolutions: &lnwallet.HtlcResolutions{},
|
|
||||||
}
|
|
||||||
return summary, nil
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -345,12 +345,18 @@ func (c *commitSweepResolver) Resolve(_ bool) (ContractResolver, error) {
|
|||||||
&c.commitResolution.SelfOutputSignDesc,
|
&c.commitResolution.SelfOutputSignDesc,
|
||||||
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
||||||
c.leaseExpiry,
|
c.leaseExpiry,
|
||||||
|
input.WithResolutionBlob(
|
||||||
|
c.commitResolution.ResolutionBlob,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
inp = input.NewCsvInput(
|
inp = input.NewCsvInput(
|
||||||
&c.commitResolution.SelfOutPoint, witnessType,
|
&c.commitResolution.SelfOutPoint, witnessType,
|
||||||
&c.commitResolution.SelfOutputSignDesc,
|
&c.commitResolution.SelfOutputSignDesc,
|
||||||
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
c.broadcastHeight, c.commitResolution.MaturityDelay,
|
||||||
|
input.WithResolutionBlob(
|
||||||
|
c.commitResolution.ResolutionBlob,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -308,7 +308,8 @@ func (h *htlcIncomingContestResolver) Resolve(
|
|||||||
|
|
||||||
resolution, err := h.Registry.NotifyExitHopHtlc(
|
resolution, err := h.Registry.NotifyExitHopHtlc(
|
||||||
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
h.htlc.RHash, h.htlc.Amt, h.htlcExpiry, currentHeight,
|
||||||
circuitKey, hodlQueue.ChanIn(), payload,
|
circuitKey, hodlQueue.ChanIn(), h.htlc.CustomRecords,
|
||||||
|
payload,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
|
|||||||
@ -6,7 +6,9 @@ import (
|
|||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/input"
|
"github.com/lightningnetwork/lnd/input"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// htlcLeaseResolver is a struct that houses the lease specific HTLC resolution
|
// htlcLeaseResolver is a struct that houses the lease specific HTLC resolution
|
||||||
@ -52,8 +54,8 @@ func (h *htlcLeaseResolver) deriveWaitHeight(csvDelay uint32,
|
|||||||
// send to the sweeper so the output can ultimately be swept.
|
// send to the sweeper so the output can ultimately be swept.
|
||||||
func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
|
func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
|
||||||
wType, cltvWtype input.StandardWitnessType,
|
wType, cltvWtype input.StandardWitnessType,
|
||||||
signDesc *input.SignDescriptor,
|
signDesc *input.SignDescriptor, csvDelay, broadcastHeight uint32,
|
||||||
csvDelay, broadcastHeight uint32, payHash [32]byte) *input.BaseInput {
|
payHash [32]byte, resBlob fn.Option[tlv.Blob]) *input.BaseInput {
|
||||||
|
|
||||||
if h.hasCLTV() {
|
if h.hasCLTV() {
|
||||||
log.Infof("%T(%x): CSV and CLTV locks expired, offering "+
|
log.Infof("%T(%x): CSV and CLTV locks expired, offering "+
|
||||||
@ -63,13 +65,17 @@ func (h *htlcLeaseResolver) makeSweepInput(op *wire.OutPoint,
|
|||||||
op, cltvWtype, signDesc,
|
op, cltvWtype, signDesc,
|
||||||
broadcastHeight, csvDelay,
|
broadcastHeight, csvDelay,
|
||||||
h.leaseExpiry,
|
h.leaseExpiry,
|
||||||
|
input.WithResolutionBlob(resBlob),
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Infof("%T(%x): CSV lock expired, offering second-layer output to "+
|
log.Infof("%T(%x): CSV lock expired, offering second-layer output to "+
|
||||||
"sweeper: %v", h, payHash, op)
|
"sweeper: %v", h, payHash, op)
|
||||||
|
|
||||||
return input.NewCsvInput(op, wType, signDesc, broadcastHeight, csvDelay)
|
return input.NewCsvInput(
|
||||||
|
op, wType, signDesc, broadcastHeight, csvDelay,
|
||||||
|
input.WithResolutionBlob(resBlob),
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// SupplementState allows the user of a ContractResolver to supplement it with
|
// SupplementState allows the user of a ContractResolver to supplement it with
|
||||||
|
|||||||
@ -247,6 +247,9 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
|
|||||||
h.htlcResolution.SignedSuccessTx,
|
h.htlcResolution.SignedSuccessTx,
|
||||||
h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
|
h.htlcResolution.SignDetails, h.htlcResolution.Preimage,
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
|
input.WithResolutionBlob(
|
||||||
|
h.htlcResolution.ResolutionBlob,
|
||||||
|
),
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
//nolint:lll
|
//nolint:lll
|
||||||
@ -403,7 +406,7 @@ func (h *htlcSuccessResolver) broadcastReSignedSuccessTx(immediate bool) (
|
|||||||
input.LeaseHtlcAcceptedSuccessSecondLevel,
|
input.LeaseHtlcAcceptedSuccessSecondLevel,
|
||||||
&h.htlcResolution.SweepSignDesc,
|
&h.htlcResolution.SweepSignDesc,
|
||||||
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
|
h.htlcResolution.CsvDelay, uint32(commitSpend.SpendingHeight),
|
||||||
h.htlc.RHash,
|
h.htlc.RHash, h.htlcResolution.ResolutionBlob,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate the budget for this sweep.
|
// Calculate the budget for this sweep.
|
||||||
@ -459,6 +462,9 @@ func (h *htlcSuccessResolver) resolveRemoteCommitOutput(immediate bool) (
|
|||||||
h.htlcResolution.Preimage[:],
|
h.htlcResolution.Preimage[:],
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
h.htlcResolution.CsvDelay,
|
h.htlcResolution.CsvDelay,
|
||||||
|
input.WithResolutionBlob(
|
||||||
|
h.htlcResolution.ResolutionBlob,
|
||||||
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
inp = lnutils.Ptr(input.MakeHtlcSucceedInput(
|
inp = lnutils.Ptr(input.MakeHtlcSucceedInput(
|
||||||
|
|||||||
@ -484,6 +484,9 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
|
|||||||
h.htlcResolution.SignedTimeoutTx,
|
h.htlcResolution.SignedTimeoutTx,
|
||||||
h.htlcResolution.SignDetails,
|
h.htlcResolution.SignDetails,
|
||||||
h.broadcastHeight,
|
h.broadcastHeight,
|
||||||
|
input.WithResolutionBlob(
|
||||||
|
h.htlcResolution.ResolutionBlob,
|
||||||
|
),
|
||||||
))
|
))
|
||||||
} else {
|
} else {
|
||||||
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
inp = lnutils.Ptr(input.MakeHtlcSecondLevelTimeoutAnchorInput(
|
||||||
@ -538,7 +541,6 @@ func (h *htlcTimeoutResolver) sweepSecondLevelTx(immediate bool) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO(yy): checkpoint here?
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -562,6 +564,60 @@ func (h *htlcTimeoutResolver) sendSecondLevelTxLegacy() error {
|
|||||||
return h.Checkpoint(h)
|
return h.Checkpoint(h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sweepDirectHtlcOutput sends the direct spend of the HTLC output to the
|
||||||
|
// sweeper. This is used when the remote party goes on chain, and we're able to
|
||||||
|
// sweep an HTLC we offered after a timeout. Only the CLTV encumbered outputs
|
||||||
|
// are resolved via this path.
|
||||||
|
func (h *htlcTimeoutResolver) sweepDirectHtlcOutput(immediate bool) error {
|
||||||
|
var htlcWitnessType input.StandardWitnessType
|
||||||
|
if h.isTaproot() {
|
||||||
|
htlcWitnessType = input.TaprootHtlcOfferedRemoteTimeout
|
||||||
|
} else {
|
||||||
|
htlcWitnessType = input.HtlcOfferedRemoteTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
sweepInput := input.NewCsvInputWithCltv(
|
||||||
|
&h.htlcResolution.ClaimOutpoint, htlcWitnessType,
|
||||||
|
&h.htlcResolution.SweepSignDesc, h.broadcastHeight,
|
||||||
|
h.htlcResolution.CsvDelay, h.htlcResolution.Expiry,
|
||||||
|
input.WithResolutionBlob(h.htlcResolution.ResolutionBlob),
|
||||||
|
)
|
||||||
|
|
||||||
|
// Calculate the budget.
|
||||||
|
//
|
||||||
|
// TODO(yy): the budget is twice the output's value, which is needed as
|
||||||
|
// we don't force sweep the output now. To prevent cascading force
|
||||||
|
// closes, we use all its output value plus a wallet input as the
|
||||||
|
// budget. This is a temporary solution until we can optionally cancel
|
||||||
|
// the incoming HTLC, more details in,
|
||||||
|
// - https://github.com/lightningnetwork/lnd/issues/7969
|
||||||
|
budget := calculateBudget(
|
||||||
|
btcutil.Amount(sweepInput.SignDesc().Output.Value), 2, 0,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Infof("%T(%x): offering offered remote timeout HTLC output to "+
|
||||||
|
"sweeper with deadline %v and budget=%v at height=%v",
|
||||||
|
h, h.htlc.RHash[:], h.incomingHTLCExpiryHeight, budget,
|
||||||
|
h.broadcastHeight)
|
||||||
|
|
||||||
|
_, err := h.Sweeper.SweepInput(
|
||||||
|
sweepInput,
|
||||||
|
sweep.Params{
|
||||||
|
Budget: budget,
|
||||||
|
|
||||||
|
// This is an outgoing HTLC, so we want to make sure
|
||||||
|
// that we sweep it before the incoming HTLC expires.
|
||||||
|
DeadlineHeight: h.incomingHTLCExpiryHeight,
|
||||||
|
Immediate: immediate,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
|
// spendHtlcOutput handles the initial spend of an HTLC output via the timeout
|
||||||
// clause. If this is our local commitment, the second-level timeout TX will be
|
// clause. If this is our local commitment, the second-level timeout TX will be
|
||||||
// used to spend the output into the next stage. If this is the remote
|
// used to spend the output into the next stage. If this is the remote
|
||||||
@ -582,8 +638,18 @@ func (h *htlcTimeoutResolver) spendHtlcOutput(
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// If we have no SignDetails, and we haven't already sent the output to
|
// If this is a remote commitment there's no second level timeout txn,
|
||||||
// the utxo nursery, then we'll do so now.
|
// and we can just send this directly to the sweeper.
|
||||||
|
case h.htlcResolution.SignedTimeoutTx == nil && !h.outputIncubating:
|
||||||
|
if err := h.sweepDirectHtlcOutput(immediate); err != nil {
|
||||||
|
log.Errorf("Sending direct spend to sweeper: %v", err)
|
||||||
|
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||||
|
// commitment for a non-anchor channel, so we'll send it to the utxo
|
||||||
|
// nursery.
|
||||||
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
case h.htlcResolution.SignDetails == nil && !h.outputIncubating:
|
||||||
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
if err := h.sendSecondLevelTxLegacy(); err != nil {
|
||||||
log.Errorf("Sending timeout tx to nursery: %v", err)
|
log.Errorf("Sending timeout tx to nursery: %v", err)
|
||||||
@ -690,6 +756,13 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
)
|
)
|
||||||
|
|
||||||
switch {
|
switch {
|
||||||
|
|
||||||
|
// If we swept an HTLC directly off the remote party's commitment
|
||||||
|
// transaction, then we can exit here as there's no second level sweep
|
||||||
|
// to do.
|
||||||
|
case h.htlcResolution.SignedTimeoutTx == nil:
|
||||||
|
break
|
||||||
|
|
||||||
// If the sweeper is handling the second level transaction, wait for
|
// If the sweeper is handling the second level transaction, wait for
|
||||||
// the CSV and possible CLTV lock to expire, before sweeping the output
|
// the CSV and possible CLTV lock to expire, before sweeping the output
|
||||||
// on the second-level.
|
// on the second-level.
|
||||||
@ -762,7 +835,9 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
&h.htlcResolution.SweepSignDesc,
|
&h.htlcResolution.SweepSignDesc,
|
||||||
h.htlcResolution.CsvDelay,
|
h.htlcResolution.CsvDelay,
|
||||||
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
|
uint32(commitSpend.SpendingHeight), h.htlc.RHash,
|
||||||
|
h.htlcResolution.ResolutionBlob,
|
||||||
)
|
)
|
||||||
|
|
||||||
// Calculate the budget for this sweep.
|
// Calculate the budget for this sweep.
|
||||||
budget := calculateBudget(
|
budget := calculateBudget(
|
||||||
btcutil.Amount(inp.SignDesc().Output.Value),
|
btcutil.Amount(inp.SignDesc().Output.Value),
|
||||||
@ -800,6 +875,7 @@ func (h *htlcTimeoutResolver) handleCommitSpend(
|
|||||||
case h.htlcResolution.SignedTimeoutTx != nil:
|
case h.htlcResolution.SignedTimeoutTx != nil:
|
||||||
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
log.Infof("%T(%v): waiting for nursery/sweeper to spend CSV "+
|
||||||
"delayed output", h, claimOutpoint)
|
"delayed output", h, claimOutpoint)
|
||||||
|
|
||||||
sweepTx, err := waitForSpend(
|
sweepTx, err := waitForSpend(
|
||||||
&claimOutpoint,
|
&claimOutpoint,
|
||||||
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
h.htlcResolution.SweepSignDesc.Output.PkScript,
|
||||||
@ -866,9 +942,11 @@ func (h *htlcTimeoutResolver) IsResolved() bool {
|
|||||||
|
|
||||||
// report returns a report on the resolution state of the contract.
|
// report returns a report on the resolution state of the contract.
|
||||||
func (h *htlcTimeoutResolver) report() *ContractReport {
|
func (h *htlcTimeoutResolver) report() *ContractReport {
|
||||||
// If the sign details are nil, the report will be created by handled
|
// If we have a SignedTimeoutTx but no SignDetails, this is a local
|
||||||
// by the nursery.
|
// commitment for a non-anchor channel, which was handled by the utxo
|
||||||
if h.htlcResolution.SignDetails == nil {
|
// nursery.
|
||||||
|
if h.htlcResolution.SignDetails == nil && h.
|
||||||
|
htlcResolution.SignedTimeoutTx != nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -888,13 +966,20 @@ func (h *htlcTimeoutResolver) initReport() {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If there's no timeout transaction, then we're already effectively in
|
||||||
|
// level two.
|
||||||
|
stage := uint32(1)
|
||||||
|
if h.htlcResolution.SignedTimeoutTx == nil {
|
||||||
|
stage = 2
|
||||||
|
}
|
||||||
|
|
||||||
h.currentReport = ContractReport{
|
h.currentReport = ContractReport{
|
||||||
Outpoint: h.htlcResolution.ClaimOutpoint,
|
Outpoint: h.htlcResolution.ClaimOutpoint,
|
||||||
Type: ReportOutputOutgoingHtlc,
|
Type: ReportOutputOutgoingHtlc,
|
||||||
Amount: finalAmt,
|
Amount: finalAmt,
|
||||||
MaturityHeight: h.htlcResolution.Expiry,
|
MaturityHeight: h.htlcResolution.Expiry,
|
||||||
LimboBalance: finalAmt,
|
LimboBalance: finalAmt,
|
||||||
Stage: 1,
|
Stage: stage,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -69,11 +69,31 @@ func (m *mockWitnessBeacon) AddPreimages(preimages ...lntypes.Preimage) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
|
type htlcTimeoutTestCase struct {
|
||||||
// variations of possible local+remote spends.
|
// name is a human readable description of the test case.
|
||||||
func TestHtlcTimeoutResolver(t *testing.T) {
|
name string
|
||||||
t.Parallel()
|
|
||||||
|
|
||||||
|
// remoteCommit denotes if the commitment broadcast was the remote
|
||||||
|
// commitment or not.
|
||||||
|
remoteCommit bool
|
||||||
|
|
||||||
|
// timeout denotes if the HTLC should be let timeout, or if the "remote"
|
||||||
|
// party should sweep it on-chain. This also affects what type of
|
||||||
|
// resolution message we expect.
|
||||||
|
timeout bool
|
||||||
|
|
||||||
|
// txToBroadcast is a function closure that should generate the
|
||||||
|
// transaction that should spend the HTLC output. Test authors can use
|
||||||
|
// this to customize the witness used when spending to trigger various
|
||||||
|
// redemption cases.
|
||||||
|
txToBroadcast func() (*wire.MsgTx, error)
|
||||||
|
|
||||||
|
// outcome is the resolver outcome that we expect to be reported once
|
||||||
|
// the contract is fully resolved.
|
||||||
|
outcome channeldb.ResolverOutcome
|
||||||
|
}
|
||||||
|
|
||||||
|
func genHtlcTimeoutTestCases() []htlcTimeoutTestCase {
|
||||||
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -105,29 +125,7 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
testCases := []struct {
|
return []htlcTimeoutTestCase{
|
||||||
// name is a human readable description of the test case.
|
|
||||||
name string
|
|
||||||
|
|
||||||
// remoteCommit denotes if the commitment broadcast was the
|
|
||||||
// remote commitment or not.
|
|
||||||
remoteCommit bool
|
|
||||||
|
|
||||||
// timeout denotes if the HTLC should be let timeout, or if the
|
|
||||||
// "remote" party should sweep it on-chain. This also affects
|
|
||||||
// what type of resolution message we expect.
|
|
||||||
timeout bool
|
|
||||||
|
|
||||||
// txToBroadcast is a function closure that should generate the
|
|
||||||
// transaction that should spend the HTLC output. Test authors
|
|
||||||
// can use this to customize the witness used when spending to
|
|
||||||
// trigger various redemption cases.
|
|
||||||
txToBroadcast func() (*wire.MsgTx, error)
|
|
||||||
|
|
||||||
// outcome is the resolver outcome that we expect to be reported
|
|
||||||
// once the contract is fully resolved.
|
|
||||||
outcome channeldb.ResolverOutcome
|
|
||||||
}{
|
|
||||||
// Remote commitment is broadcast, we time out the HTLC on
|
// Remote commitment is broadcast, we time out the HTLC on
|
||||||
// chain, and should expect a fail HTLC resolution.
|
// chain, and should expect a fail HTLC resolution.
|
||||||
{
|
{
|
||||||
@ -149,7 +147,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
// immediately if the witness is already set
|
// immediately if the witness is already set
|
||||||
// correctly.
|
// correctly.
|
||||||
if reflect.DeepEqual(
|
if reflect.DeepEqual(
|
||||||
templateTx.TxIn[0].Witness, witness,
|
templateTx.TxIn[0].Witness,
|
||||||
|
witness,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
@ -219,7 +218,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
// immediately if the witness is already set
|
// immediately if the witness is already set
|
||||||
// correctly.
|
// correctly.
|
||||||
if reflect.DeepEqual(
|
if reflect.DeepEqual(
|
||||||
templateTx.TxIn[0].Witness, witness,
|
templateTx.TxIn[0].Witness,
|
||||||
|
witness,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
@ -253,7 +253,8 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
// immediately if the witness is already set
|
// immediately if the witness is already set
|
||||||
// correctly.
|
// correctly.
|
||||||
if reflect.DeepEqual(
|
if reflect.DeepEqual(
|
||||||
templateTx.TxIn[0].Witness, witness,
|
templateTx.TxIn[0].Witness,
|
||||||
|
witness,
|
||||||
) {
|
) {
|
||||||
|
|
||||||
return templateTx, nil
|
return templateTx, nil
|
||||||
@ -265,243 +266,280 @@ func TestHtlcTimeoutResolver(t *testing.T) {
|
|||||||
outcome: channeldb.ResolverOutcomeClaimed,
|
outcome: channeldb.ResolverOutcomeClaimed,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func testHtlcTimeoutResolver(t *testing.T, testCase htlcTimeoutTestCase) {
|
||||||
|
fakePreimageBytes := bytes.Repeat([]byte{1}, lntypes.HashSize)
|
||||||
|
var fakePreimage lntypes.Preimage
|
||||||
|
|
||||||
|
fakeSignDesc := &input.SignDescriptor{
|
||||||
|
Output: &wire.TxOut{},
|
||||||
|
}
|
||||||
|
|
||||||
|
copy(fakePreimage[:], fakePreimageBytes)
|
||||||
|
|
||||||
notifier := &mock.ChainNotifier{
|
notifier := &mock.ChainNotifier{
|
||||||
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
EpochChan: make(chan *chainntnfs.BlockEpoch),
|
||||||
SpendChan: make(chan *chainntnfs.SpendDetail),
|
SpendChan: make(chan *chainntnfs.SpendDetail),
|
||||||
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
ConfChan: make(chan *chainntnfs.TxConfirmation),
|
||||||
}
|
}
|
||||||
|
|
||||||
witnessBeacon := newMockWitnessBeacon()
|
witnessBeacon := newMockWitnessBeacon()
|
||||||
|
checkPointChan := make(chan struct{}, 1)
|
||||||
|
incubateChan := make(chan struct{}, 1)
|
||||||
|
resolutionChan := make(chan ResolutionMsg, 1)
|
||||||
|
reportChan := make(chan *channeldb.ResolverReport)
|
||||||
|
|
||||||
|
//nolint:lll
|
||||||
|
chainCfg := ChannelArbitratorConfig{
|
||||||
|
ChainArbitratorConfig: ChainArbitratorConfig{
|
||||||
|
Notifier: notifier,
|
||||||
|
Sweeper: newMockSweeper(),
|
||||||
|
PreimageDB: witnessBeacon,
|
||||||
|
IncubateOutputs: func(wire.OutPoint,
|
||||||
|
fn.Option[lnwallet.OutgoingHtlcResolution],
|
||||||
|
fn.Option[lnwallet.IncomingHtlcResolution],
|
||||||
|
uint32, fn.Option[int32]) error {
|
||||||
|
|
||||||
|
incubateChan <- struct{}{}
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
||||||
|
if len(msgs) != 1 {
|
||||||
|
return fmt.Errorf("expected 1 "+
|
||||||
|
"resolution msg, instead got %v",
|
||||||
|
len(msgs))
|
||||||
|
}
|
||||||
|
|
||||||
|
resolutionChan <- msgs[0]
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
Budget: *DefaultBudgetConfig(),
|
||||||
|
QueryIncomingCircuit: func(circuit models.CircuitKey,
|
||||||
|
) *models.CircuitKey {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
},
|
||||||
|
PutResolverReport: func(_ kvdb.RwTx,
|
||||||
|
_ *channeldb.ResolverReport) error {
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg := ResolverConfig{
|
||||||
|
ChannelArbitratorConfig: chainCfg,
|
||||||
|
Checkpoint: func(_ ContractResolver,
|
||||||
|
reports ...*channeldb.ResolverReport) error {
|
||||||
|
|
||||||
|
checkPointChan <- struct{}{}
|
||||||
|
|
||||||
|
// Send all of our reports into the channel.
|
||||||
|
for _, report := range reports {
|
||||||
|
reportChan <- report
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
},
|
||||||
|
}
|
||||||
|
resolver := &htlcTimeoutResolver{
|
||||||
|
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
||||||
|
ClaimOutpoint: testChanPoint2,
|
||||||
|
SweepSignDesc: *fakeSignDesc,
|
||||||
|
},
|
||||||
|
contractResolverKit: *newContractResolverKit(
|
||||||
|
cfg,
|
||||||
|
),
|
||||||
|
htlc: channeldb.HTLC{
|
||||||
|
Amt: testHtlcAmt,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
var reports []*channeldb.ResolverReport
|
||||||
|
|
||||||
|
// If the test case needs the remote commitment to be
|
||||||
|
// broadcast, then we'll set the timeout commit to a fake
|
||||||
|
// transaction to force the code path.
|
||||||
|
if !testCase.remoteCommit {
|
||||||
|
timeoutTx, err := testCase.txToBroadcast()
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
||||||
|
|
||||||
|
if testCase.timeout {
|
||||||
|
timeoutTxID := timeoutTx.TxHash()
|
||||||
|
report := &channeldb.ResolverReport{
|
||||||
|
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint, //nolint:lll
|
||||||
|
Amount: testHtlcAmt.ToSatoshis(),
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc, //nolint:lll
|
||||||
|
ResolverOutcome: channeldb.ResolverOutcomeFirstStage, //nolint:lll
|
||||||
|
SpendTxID: &timeoutTxID,
|
||||||
|
}
|
||||||
|
|
||||||
|
reports = append(reports, report)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// With all the setup above complete, we can initiate the
|
||||||
|
// resolution process, and the bulk of our test.
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
resolveErr := make(chan error, 1)
|
||||||
|
wg.Add(1)
|
||||||
|
go func() {
|
||||||
|
defer wg.Done()
|
||||||
|
|
||||||
|
_, err := resolver.Resolve(false)
|
||||||
|
if err != nil {
|
||||||
|
resolveErr <- err
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// If this is a remote commit, then we expct the outputs should receive
|
||||||
|
// an incubation request to go through the sweeper, otherwise the
|
||||||
|
// nursery.
|
||||||
|
var sweepChan chan input.Input
|
||||||
|
if testCase.remoteCommit {
|
||||||
|
mockSweeper, ok := resolver.Sweeper.(*mockSweeper)
|
||||||
|
require.True(t, ok)
|
||||||
|
sweepChan = mockSweeper.sweptInputs
|
||||||
|
}
|
||||||
|
|
||||||
|
// The output should be offered to either the sweeper or
|
||||||
|
// the nursery.
|
||||||
|
select {
|
||||||
|
case <-incubateChan:
|
||||||
|
case <-sweepChan:
|
||||||
|
case err := <-resolveErr:
|
||||||
|
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("failed to receive incubation request")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Next, the resolver should request a spend notification for
|
||||||
|
// the direct HTLC output. We'll use the txToBroadcast closure
|
||||||
|
// for the test case to generate the transaction that we'll
|
||||||
|
// send to the resolver.
|
||||||
|
spendingTx, err := testCase.txToBroadcast()
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("unable to generate tx: %v", err)
|
||||||
|
}
|
||||||
|
spendTxHash := spendingTx.TxHash()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: spendingTx,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
}:
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("failed to request spend ntfn")
|
||||||
|
}
|
||||||
|
|
||||||
|
if !testCase.timeout {
|
||||||
|
// If the resolver should settle now, then we'll
|
||||||
|
// extract the pre-image to be extracted and the
|
||||||
|
// resolution message sent.
|
||||||
|
select {
|
||||||
|
case newPreimage := <-witnessBeacon.newPreimages:
|
||||||
|
if newPreimage[0] != fakePreimage {
|
||||||
|
t.Fatalf("wrong pre-image: "+
|
||||||
|
"expected %v, got %v",
|
||||||
|
fakePreimage, newPreimage)
|
||||||
|
}
|
||||||
|
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("pre-image not added")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Finally, we should get a resolution message with the
|
||||||
|
// pre-image set within the message.
|
||||||
|
select {
|
||||||
|
case resolutionMsg := <-resolutionChan:
|
||||||
|
// Once again, the pre-images should match up.
|
||||||
|
if *resolutionMsg.PreImage != fakePreimage {
|
||||||
|
t.Fatalf("wrong pre-image: "+
|
||||||
|
"expected %v, got %v",
|
||||||
|
fakePreimage, resolutionMsg.PreImage)
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("resolution not sent")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Otherwise, the HTLC should now timeout. First, we
|
||||||
|
// should get a resolution message with a populated
|
||||||
|
// failure message.
|
||||||
|
select {
|
||||||
|
case resolutionMsg := <-resolutionChan:
|
||||||
|
if resolutionMsg.Failure == nil {
|
||||||
|
t.Fatalf("expected failure resolution msg")
|
||||||
|
}
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("resolution not sent")
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should also get another request for the spend
|
||||||
|
// notification of the second-level transaction to
|
||||||
|
// indicate that it's been swept by the nursery, but
|
||||||
|
// only if this is a local commitment transaction.
|
||||||
|
if !testCase.remoteCommit {
|
||||||
|
select {
|
||||||
|
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
|
SpendingTx: spendingTx,
|
||||||
|
SpenderTxHash: &spendTxHash,
|
||||||
|
}:
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("failed to request spend ntfn")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// In any case, before the resolver exits, it should checkpoint
|
||||||
|
// its final state.
|
||||||
|
select {
|
||||||
|
case <-checkPointChan:
|
||||||
|
case err := <-resolveErr:
|
||||||
|
t.Fatalf("unable to resolve HTLC: %v", err)
|
||||||
|
case <-time.After(time.Second * 5):
|
||||||
|
t.Fatalf("check point not received")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add a report to our set of expected reports with the outcome
|
||||||
|
// that the test specifies (either success or timeout).
|
||||||
|
spendTxID := spendingTx.TxHash()
|
||||||
|
amt := btcutil.Amount(fakeSignDesc.Output.Value)
|
||||||
|
|
||||||
|
reports = append(reports, &channeldb.ResolverReport{
|
||||||
|
OutPoint: testChanPoint2,
|
||||||
|
Amount: amt,
|
||||||
|
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
||||||
|
ResolverOutcome: testCase.outcome,
|
||||||
|
SpendTxID: &spendTxID,
|
||||||
|
})
|
||||||
|
|
||||||
|
for _, report := range reports {
|
||||||
|
assertResolverReport(t, reportChan, report)
|
||||||
|
}
|
||||||
|
|
||||||
|
wg.Wait()
|
||||||
|
|
||||||
|
// Finally, the resolver should be marked as resolved.
|
||||||
|
if !resolver.resolved {
|
||||||
|
t.Fatalf("resolver should be marked as resolved")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestHtlcTimeoutResolver tests that the timeout resolver properly handles all
|
||||||
|
// variations of possible local+remote spends.
|
||||||
|
func TestHtlcTimeoutResolver(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
testCases := genHtlcTimeoutTestCases()
|
||||||
|
|
||||||
for _, testCase := range testCases {
|
for _, testCase := range testCases {
|
||||||
t.Logf("Running test case: %v", testCase.name)
|
t.Run(testCase.name, func(t *testing.T) {
|
||||||
|
testHtlcTimeoutResolver(t, testCase)
|
||||||
checkPointChan := make(chan struct{}, 1)
|
|
||||||
incubateChan := make(chan struct{}, 1)
|
|
||||||
resolutionChan := make(chan ResolutionMsg, 1)
|
|
||||||
reportChan := make(chan *channeldb.ResolverReport)
|
|
||||||
|
|
||||||
//nolint:lll
|
|
||||||
chainCfg := ChannelArbitratorConfig{
|
|
||||||
ChainArbitratorConfig: ChainArbitratorConfig{
|
|
||||||
Notifier: notifier,
|
|
||||||
PreimageDB: witnessBeacon,
|
|
||||||
IncubateOutputs: func(wire.OutPoint,
|
|
||||||
fn.Option[lnwallet.OutgoingHtlcResolution],
|
|
||||||
fn.Option[lnwallet.IncomingHtlcResolution],
|
|
||||||
uint32, fn.Option[int32]) error {
|
|
||||||
|
|
||||||
incubateChan <- struct{}{}
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
DeliverResolutionMsg: func(msgs ...ResolutionMsg) error {
|
|
||||||
if len(msgs) != 1 {
|
|
||||||
return fmt.Errorf("expected 1 "+
|
|
||||||
"resolution msg, instead got %v",
|
|
||||||
len(msgs))
|
|
||||||
}
|
|
||||||
|
|
||||||
resolutionChan <- msgs[0]
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
Budget: *DefaultBudgetConfig(),
|
|
||||||
QueryIncomingCircuit: func(circuit models.CircuitKey) *models.CircuitKey {
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
PutResolverReport: func(_ kvdb.RwTx,
|
|
||||||
_ *channeldb.ResolverReport) error {
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := ResolverConfig{
|
|
||||||
ChannelArbitratorConfig: chainCfg,
|
|
||||||
Checkpoint: func(_ ContractResolver,
|
|
||||||
reports ...*channeldb.ResolverReport) error {
|
|
||||||
|
|
||||||
checkPointChan <- struct{}{}
|
|
||||||
|
|
||||||
// Send all of our reports into the channel.
|
|
||||||
for _, report := range reports {
|
|
||||||
reportChan <- report
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
},
|
|
||||||
}
|
|
||||||
resolver := &htlcTimeoutResolver{
|
|
||||||
htlcResolution: lnwallet.OutgoingHtlcResolution{
|
|
||||||
ClaimOutpoint: testChanPoint2,
|
|
||||||
SweepSignDesc: *fakeSignDesc,
|
|
||||||
},
|
|
||||||
contractResolverKit: *newContractResolverKit(
|
|
||||||
cfg,
|
|
||||||
),
|
|
||||||
htlc: channeldb.HTLC{
|
|
||||||
Amt: testHtlcAmt,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
var reports []*channeldb.ResolverReport
|
|
||||||
|
|
||||||
// If the test case needs the remote commitment to be
|
|
||||||
// broadcast, then we'll set the timeout commit to a fake
|
|
||||||
// transaction to force the code path.
|
|
||||||
if !testCase.remoteCommit {
|
|
||||||
timeoutTx, err := testCase.txToBroadcast()
|
|
||||||
require.NoError(t, err)
|
|
||||||
|
|
||||||
resolver.htlcResolution.SignedTimeoutTx = timeoutTx
|
|
||||||
|
|
||||||
if testCase.timeout {
|
|
||||||
timeoutTxID := timeoutTx.TxHash()
|
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
|
||||||
OutPoint: timeoutTx.TxIn[0].PreviousOutPoint,
|
|
||||||
Amount: testHtlcAmt.ToSatoshis(),
|
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
|
||||||
ResolverOutcome: channeldb.ResolverOutcomeFirstStage,
|
|
||||||
SpendTxID: &timeoutTxID,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// With all the setup above complete, we can initiate the
|
|
||||||
// resolution process, and the bulk of our test.
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
resolveErr := make(chan error, 1)
|
|
||||||
wg.Add(1)
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
|
|
||||||
_, err := resolver.Resolve(false)
|
|
||||||
if err != nil {
|
|
||||||
resolveErr <- err
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// At the output isn't yet in the nursery, we expect that we
|
|
||||||
// should receive an incubation request.
|
|
||||||
select {
|
|
||||||
case <-incubateChan:
|
|
||||||
case err := <-resolveErr:
|
|
||||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("failed to receive incubation request")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Next, the resolver should request a spend notification for
|
|
||||||
// the direct HTLC output. We'll use the txToBroadcast closure
|
|
||||||
// for the test case to generate the transaction that we'll
|
|
||||||
// send to the resolver.
|
|
||||||
spendingTx, err := testCase.txToBroadcast()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("unable to generate tx: %v", err)
|
|
||||||
}
|
|
||||||
spendTxHash := spendingTx.TxHash()
|
|
||||||
|
|
||||||
select {
|
|
||||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
|
||||||
SpendingTx: spendingTx,
|
|
||||||
SpenderTxHash: &spendTxHash,
|
|
||||||
}:
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("failed to request spend ntfn")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !testCase.timeout {
|
|
||||||
// If the resolver should settle now, then we'll
|
|
||||||
// extract the pre-image to be extracted and the
|
|
||||||
// resolution message sent.
|
|
||||||
select {
|
|
||||||
case newPreimage := <-witnessBeacon.newPreimages:
|
|
||||||
if newPreimage[0] != fakePreimage {
|
|
||||||
t.Fatalf("wrong pre-image: "+
|
|
||||||
"expected %v, got %v",
|
|
||||||
fakePreimage, newPreimage)
|
|
||||||
}
|
|
||||||
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("pre-image not added")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Finally, we should get a resolution message with the
|
|
||||||
// pre-image set within the message.
|
|
||||||
select {
|
|
||||||
case resolutionMsg := <-resolutionChan:
|
|
||||||
// Once again, the pre-images should match up.
|
|
||||||
if *resolutionMsg.PreImage != fakePreimage {
|
|
||||||
t.Fatalf("wrong pre-image: "+
|
|
||||||
"expected %v, got %v",
|
|
||||||
fakePreimage, resolutionMsg.PreImage)
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("resolution not sent")
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
|
|
||||||
// Otherwise, the HTLC should now timeout. First, we
|
|
||||||
// should get a resolution message with a populated
|
|
||||||
// failure message.
|
|
||||||
select {
|
|
||||||
case resolutionMsg := <-resolutionChan:
|
|
||||||
if resolutionMsg.Failure == nil {
|
|
||||||
t.Fatalf("expected failure resolution msg")
|
|
||||||
}
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("resolution not sent")
|
|
||||||
}
|
|
||||||
|
|
||||||
// We should also get another request for the spend
|
|
||||||
// notification of the second-level transaction to
|
|
||||||
// indicate that it's been swept by the nursery, but
|
|
||||||
// only if this is a local commitment transaction.
|
|
||||||
if !testCase.remoteCommit {
|
|
||||||
select {
|
|
||||||
case notifier.SpendChan <- &chainntnfs.SpendDetail{
|
|
||||||
SpendingTx: spendingTx,
|
|
||||||
SpenderTxHash: &spendTxHash,
|
|
||||||
}:
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("failed to request spend ntfn")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// In any case, before the resolver exits, it should checkpoint
|
|
||||||
// its final state.
|
|
||||||
select {
|
|
||||||
case <-checkPointChan:
|
|
||||||
case err := <-resolveErr:
|
|
||||||
t.Fatalf("unable to resolve HTLC: %v", err)
|
|
||||||
case <-time.After(time.Second * 5):
|
|
||||||
t.Fatalf("check point not received")
|
|
||||||
}
|
|
||||||
|
|
||||||
// Add a report to our set of expected reports with the outcome
|
|
||||||
// that the test specifies (either success or timeout).
|
|
||||||
spendTxID := spendingTx.TxHash()
|
|
||||||
amt := btcutil.Amount(fakeSignDesc.Output.Value)
|
|
||||||
|
|
||||||
reports = append(reports, &channeldb.ResolverReport{
|
|
||||||
OutPoint: testChanPoint2,
|
|
||||||
Amount: amt,
|
|
||||||
ResolverType: channeldb.ResolverTypeOutgoingHtlc,
|
|
||||||
ResolverOutcome: testCase.outcome,
|
|
||||||
SpendTxID: &spendTxID,
|
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, report := range reports {
|
|
||||||
assertResolverReport(t, reportChan, report)
|
|
||||||
}
|
|
||||||
|
|
||||||
wg.Wait()
|
|
||||||
|
|
||||||
// Finally, the resolver should be marked as resolved.
|
|
||||||
if !resolver.resolved {
|
|
||||||
t.Fatalf("resolver should be marked as resolved")
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -536,15 +574,12 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkpoints := []checkpoint{
|
checkpoints := []checkpoint{
|
||||||
{
|
|
||||||
// Output should be handed off to the nursery.
|
|
||||||
incubating: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// We send a confirmation the sweep tx from published
|
// We send a confirmation the sweep tx from published
|
||||||
// by the nursery.
|
// by the nursery.
|
||||||
preCheckpoint: func(ctx *htlcResolverTestContext,
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
_ bool) error {
|
_ bool) error {
|
||||||
|
|
||||||
// The nursery will create and publish a sweep
|
// The nursery will create and publish a sweep
|
||||||
// tx.
|
// tx.
|
||||||
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
@ -570,7 +605,7 @@ func TestHtlcTimeoutSingleStage(t *testing.T) {
|
|||||||
// After the sweep has confirmed, we expect the
|
// After the sweep has confirmed, we expect the
|
||||||
// checkpoint to be resolved, and with the above
|
// checkpoint to be resolved, and with the above
|
||||||
// report.
|
// report.
|
||||||
incubating: true,
|
incubating: false,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
reports: []*channeldb.ResolverReport{
|
reports: []*channeldb.ResolverReport{
|
||||||
claim,
|
claim,
|
||||||
@ -653,6 +688,7 @@ func TestHtlcTimeoutSecondStage(t *testing.T) {
|
|||||||
// that our sweep succeeded.
|
// that our sweep succeeded.
|
||||||
preCheckpoint: func(ctx *htlcResolverTestContext,
|
preCheckpoint: func(ctx *htlcResolverTestContext,
|
||||||
_ bool) error {
|
_ bool) error {
|
||||||
|
|
||||||
// The nursery will publish the timeout tx.
|
// The nursery will publish the timeout tx.
|
||||||
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
ctx.notifier.SpendChan <- &chainntnfs.SpendDetail{
|
||||||
SpendingTx: timeoutTx,
|
SpendingTx: timeoutTx,
|
||||||
@ -824,9 +860,9 @@ func TestHtlcTimeoutSingleStageRemoteSpend(t *testing.T) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remite commitment
|
// TestHtlcTimeoutSecondStageRemoteSpend tests that when a remote commitment
|
||||||
// confirms, and the remote spends the output using the success tx, we
|
// confirms, and the remote spends the output using the success tx, we properly
|
||||||
// properly detect this and extract the preimage.
|
// detect this and extract the preimage.
|
||||||
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
||||||
commitOutpoint := wire.OutPoint{Index: 2}
|
commitOutpoint := wire.OutPoint{Index: 2}
|
||||||
|
|
||||||
@ -870,10 +906,6 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
checkpoints := []checkpoint{
|
checkpoints := []checkpoint{
|
||||||
{
|
|
||||||
// Output should be handed off to the nursery.
|
|
||||||
incubating: true,
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
// We send a confirmation for the remote's second layer
|
// We send a confirmation for the remote's second layer
|
||||||
// success transcation.
|
// success transcation.
|
||||||
@ -919,7 +951,7 @@ func TestHtlcTimeoutSecondStageRemoteSpend(t *testing.T) {
|
|||||||
// After the sweep has confirmed, we expect the
|
// After the sweep has confirmed, we expect the
|
||||||
// checkpoint to be resolved, and with the above
|
// checkpoint to be resolved, and with the above
|
||||||
// report.
|
// report.
|
||||||
incubating: true,
|
incubating: false,
|
||||||
resolved: true,
|
resolved: true,
|
||||||
reports: []*channeldb.ResolverReport{
|
reports: []*channeldb.ResolverReport{
|
||||||
claim,
|
claim,
|
||||||
@ -1298,6 +1330,8 @@ func TestHtlcTimeoutSecondStageSweeperRemoteSpend(t *testing.T) {
|
|||||||
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
func testHtlcTimeout(t *testing.T, resolution lnwallet.OutgoingHtlcResolution,
|
||||||
checkpoints []checkpoint) {
|
checkpoints []checkpoint) {
|
||||||
|
|
||||||
|
t.Helper()
|
||||||
|
|
||||||
defer timeout()()
|
defer timeout()()
|
||||||
|
|
||||||
// We first run the resolver from start to finish, ensuring it gets
|
// We first run the resolver from start to finish, ensuring it gets
|
||||||
|
|||||||
@ -30,6 +30,7 @@ type Registry interface {
|
|||||||
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
NotifyExitHopHtlc(payHash lntypes.Hash, paidAmount lnwire.MilliSatoshi,
|
||||||
expiry uint32, currentHeight int32,
|
expiry uint32, currentHeight int32,
|
||||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||||
|
wireCustomRecords lnwire.CustomRecords,
|
||||||
payload invoices.Payload) (invoices.HtlcResolution, error)
|
payload invoices.Payload) (invoices.HtlcResolution, error)
|
||||||
|
|
||||||
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
// HodlUnsubscribeAll unsubscribes from all htlc resolutions.
|
||||||
|
|||||||
@ -10,5 +10,6 @@ type mockHTLCNotifier struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (m *mockHTLCNotifier) NotifyFinalHtlcEvent(key models.CircuitKey,
|
func (m *mockHTLCNotifier) NotifyFinalHtlcEvent(key models.CircuitKey,
|
||||||
info channeldb.FinalHtlcInfo) { //nolint:whitespace
|
info channeldb.FinalHtlcInfo) {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -26,6 +26,7 @@ type mockRegistry struct {
|
|||||||
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
func (r *mockRegistry) NotifyExitHopHtlc(payHash lntypes.Hash,
|
||||||
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
paidAmount lnwire.MilliSatoshi, expiry uint32, currentHeight int32,
|
||||||
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
circuitKey models.CircuitKey, hodlChan chan<- interface{},
|
||||||
|
wireCustomRecords lnwire.CustomRecords,
|
||||||
payload invoices.Payload) (invoices.HtlcResolution, error) {
|
payload invoices.Payload) (invoices.HtlcResolution, error) {
|
||||||
|
|
||||||
r.notifyChan <- notifyExitHopData{
|
r.notifyChan <- notifyExitHopData{
|
||||||
|
|||||||
@ -8,9 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
taprootCtrlBlockType tlv.Type = 0
|
|
||||||
taprootTapTweakType tlv.Type = 1
|
|
||||||
|
|
||||||
commitCtrlBlockType tlv.Type = 0
|
commitCtrlBlockType tlv.Type = 0
|
||||||
revokeCtrlBlockType tlv.Type = 1
|
revokeCtrlBlockType tlv.Type = 1
|
||||||
outgoingHtlcCtrlBlockType tlv.Type = 2
|
outgoingHtlcCtrlBlockType tlv.Type = 2
|
||||||
@ -26,36 +23,67 @@ const (
|
|||||||
// information we need to sweep taproot outputs.
|
// information we need to sweep taproot outputs.
|
||||||
type taprootBriefcase struct {
|
type taprootBriefcase struct {
|
||||||
// CtrlBlock is the set of control block for the taproot outputs.
|
// CtrlBlock is the set of control block for the taproot outputs.
|
||||||
CtrlBlocks *ctrlBlocks
|
CtrlBlocks tlv.RecordT[tlv.TlvType0, ctrlBlocks]
|
||||||
|
|
||||||
// TapTweaks is the set of taproot tweaks for the taproot outputs that
|
// TapTweaks is the set of taproot tweaks for the taproot outputs that
|
||||||
// are to be spent via a keyspend path. This includes anchors, and any
|
// are to be spent via a keyspend path. This includes anchors, and any
|
||||||
// revocation paths.
|
// revocation paths.
|
||||||
TapTweaks *tapTweaks
|
TapTweaks tlv.RecordT[tlv.TlvType1, tapTweaks]
|
||||||
|
|
||||||
|
// SettledCommitBlob is an optional record that contains an opaque blob
|
||||||
|
// that may be used to properly sweep commitment outputs on a force
|
||||||
|
// close transaction.
|
||||||
|
SettledCommitBlob tlv.OptionalRecordT[tlv.TlvType2, tlv.Blob]
|
||||||
|
|
||||||
|
// BreachCommitBlob is an optional record that contains an opaque blob
|
||||||
|
// used to sweep a remote party's breached output.
|
||||||
|
BreachedCommitBlob tlv.OptionalRecordT[tlv.TlvType3, tlv.Blob]
|
||||||
|
|
||||||
|
// HtlcBlobs is an optikonal record that contains the opaque blobs for
|
||||||
|
// the set of active HTLCs on the commitment transaction.
|
||||||
|
HtlcBlobs tlv.OptionalRecordT[tlv.TlvType4, htlcAuxBlobs]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TODO(roasbeef): morph into new tlv record
|
||||||
|
|
||||||
// newTaprootBriefcase returns a new instance of the taproot specific briefcase
|
// newTaprootBriefcase returns a new instance of the taproot specific briefcase
|
||||||
// variant.
|
// variant.
|
||||||
func newTaprootBriefcase() *taprootBriefcase {
|
func newTaprootBriefcase() *taprootBriefcase {
|
||||||
return &taprootBriefcase{
|
return &taprootBriefcase{
|
||||||
CtrlBlocks: newCtrlBlocks(),
|
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](newCtrlBlocks()),
|
||||||
TapTweaks: newTapTweaks(),
|
TapTweaks: tlv.NewRecordT[tlv.TlvType1](newTapTweaks()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// EncodeRecords returns a slice of TLV records that should be encoded.
|
// EncodeRecords returns a slice of TLV records that should be encoded.
|
||||||
func (t *taprootBriefcase) EncodeRecords() []tlv.Record {
|
func (t *taprootBriefcase) EncodeRecords() []tlv.Record {
|
||||||
return []tlv.Record{
|
records := []tlv.Record{
|
||||||
newCtrlBlocksRecord(&t.CtrlBlocks),
|
t.CtrlBlocks.Record(),
|
||||||
newTapTweaksRecord(&t.TapTweaks),
|
t.TapTweaks.Record(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
t.SettledCommitBlob.WhenSome(
|
||||||
|
func(r tlv.RecordT[tlv.TlvType2, tlv.Blob]) {
|
||||||
|
records = append(records, r.Record())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
t.BreachedCommitBlob.WhenSome(
|
||||||
|
func(r tlv.RecordT[tlv.TlvType3, tlv.Blob]) {
|
||||||
|
records = append(records, r.Record())
|
||||||
|
},
|
||||||
|
)
|
||||||
|
t.HtlcBlobs.WhenSome(func(r tlv.RecordT[tlv.TlvType4, htlcAuxBlobs]) {
|
||||||
|
records = append(records, r.Record())
|
||||||
|
})
|
||||||
|
|
||||||
|
return records
|
||||||
}
|
}
|
||||||
|
|
||||||
// DecodeRecords returns a slice of TLV records that should be decoded.
|
// DecodeRecords returns a slice of TLV records that should be decoded.
|
||||||
func (t *taprootBriefcase) DecodeRecords() []tlv.Record {
|
func (t *taprootBriefcase) DecodeRecords() []tlv.Record {
|
||||||
return []tlv.Record{
|
return []tlv.Record{
|
||||||
newCtrlBlocksRecord(&t.CtrlBlocks),
|
t.CtrlBlocks.Record(),
|
||||||
newTapTweaksRecord(&t.TapTweaks),
|
t.TapTweaks.Record(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -71,12 +99,35 @@ func (t *taprootBriefcase) Encode(w io.Writer) error {
|
|||||||
|
|
||||||
// Decode decodes the given reader into the target struct.
|
// Decode decodes the given reader into the target struct.
|
||||||
func (t *taprootBriefcase) Decode(r io.Reader) error {
|
func (t *taprootBriefcase) Decode(r io.Reader) error {
|
||||||
stream, err := tlv.NewStream(t.DecodeRecords()...)
|
settledCommitBlob := t.SettledCommitBlob.Zero()
|
||||||
|
breachedCommitBlob := t.BreachedCommitBlob.Zero()
|
||||||
|
htlcBlobs := t.HtlcBlobs.Zero()
|
||||||
|
|
||||||
|
records := append(
|
||||||
|
t.DecodeRecords(), settledCommitBlob.Record(),
|
||||||
|
breachedCommitBlob.Record(), htlcBlobs.Record(),
|
||||||
|
)
|
||||||
|
stream, err := tlv.NewStream(records...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return stream.Decode(r)
|
typeMap, err := stream.DecodeWithParsedTypes(r)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if val, ok := typeMap[t.SettledCommitBlob.TlvType()]; ok && val == nil {
|
||||||
|
t.SettledCommitBlob = tlv.SomeRecordT(settledCommitBlob)
|
||||||
|
}
|
||||||
|
if v, ok := typeMap[t.BreachedCommitBlob.TlvType()]; ok && v == nil {
|
||||||
|
t.BreachedCommitBlob = tlv.SomeRecordT(breachedCommitBlob)
|
||||||
|
}
|
||||||
|
if v, ok := typeMap[t.HtlcBlobs.TlvType()]; ok && v == nil {
|
||||||
|
t.HtlcBlobs = tlv.SomeRecordT(htlcBlobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// resolverCtrlBlocks is a map of resolver IDs to their corresponding control
|
// resolverCtrlBlocks is a map of resolver IDs to their corresponding control
|
||||||
@ -216,8 +267,8 @@ type ctrlBlocks struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newCtrlBlocks returns a new instance of the ctrlBlocks struct.
|
// newCtrlBlocks returns a new instance of the ctrlBlocks struct.
|
||||||
func newCtrlBlocks() *ctrlBlocks {
|
func newCtrlBlocks() ctrlBlocks {
|
||||||
return &ctrlBlocks{
|
return ctrlBlocks{
|
||||||
OutgoingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
OutgoingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
||||||
IncomingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
IncomingHtlcCtrlBlocks: newResolverCtrlBlocks(),
|
||||||
SecondLevelCtrlBlocks: newResolverCtrlBlocks(),
|
SecondLevelCtrlBlocks: newResolverCtrlBlocks(),
|
||||||
@ -260,7 +311,7 @@ func varBytesDecoder(r io.Reader, val any, buf *[8]byte, l uint64) error {
|
|||||||
|
|
||||||
// ctrlBlockEncoder is a custom TLV encoder for the ctrlBlocks struct.
|
// ctrlBlockEncoder is a custom TLV encoder for the ctrlBlocks struct.
|
||||||
func ctrlBlockEncoder(w io.Writer, val any, _ *[8]byte) error {
|
func ctrlBlockEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||||
if t, ok := val.(**ctrlBlocks); ok {
|
if t, ok := val.(*ctrlBlocks); ok {
|
||||||
return (*t).Encode(w)
|
return (*t).Encode(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -269,7 +320,7 @@ func ctrlBlockEncoder(w io.Writer, val any, _ *[8]byte) error {
|
|||||||
|
|
||||||
// ctrlBlockDecoder is a custom TLV decoder for the ctrlBlocks struct.
|
// ctrlBlockDecoder is a custom TLV decoder for the ctrlBlocks struct.
|
||||||
func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||||
if typ, ok := val.(**ctrlBlocks); ok {
|
if typ, ok := val.(*ctrlBlocks); ok {
|
||||||
ctrlReader := io.LimitReader(r, int64(l))
|
ctrlReader := io.LimitReader(r, int64(l))
|
||||||
|
|
||||||
var ctrlBlocks ctrlBlocks
|
var ctrlBlocks ctrlBlocks
|
||||||
@ -278,7 +329,7 @@ func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*typ = &ctrlBlocks
|
*typ = ctrlBlocks
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -286,28 +337,6 @@ func ctrlBlockDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
|||||||
return tlv.NewTypeForDecodingErr(val, "ctrlBlocks", l, l)
|
return tlv.NewTypeForDecodingErr(val, "ctrlBlocks", l, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newCtrlBlocksRecord returns a new TLV record that can be used to
|
|
||||||
// encode/decode the set of cotrol blocks for the taproot outputs for a
|
|
||||||
// channel.
|
|
||||||
func newCtrlBlocksRecord(blks **ctrlBlocks) tlv.Record {
|
|
||||||
recordSize := func() uint64 {
|
|
||||||
var (
|
|
||||||
b bytes.Buffer
|
|
||||||
buf [8]byte
|
|
||||||
)
|
|
||||||
if err := ctrlBlockEncoder(&b, blks, &buf); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint64(len(b.Bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlv.MakeDynamicRecord(
|
|
||||||
taprootCtrlBlockType, blks, recordSize, ctrlBlockEncoder,
|
|
||||||
ctrlBlockDecoder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeRecords returns the set of TLV records that encode the control block
|
// EncodeRecords returns the set of TLV records that encode the control block
|
||||||
// for the commitment transaction.
|
// for the commitment transaction.
|
||||||
func (c *ctrlBlocks) EncodeRecords() []tlv.Record {
|
func (c *ctrlBlocks) EncodeRecords() []tlv.Record {
|
||||||
@ -382,7 +411,21 @@ func (c *ctrlBlocks) DecodeRecords() []tlv.Record {
|
|||||||
// Record returns a TLV record that can be used to encode/decode the control
|
// Record returns a TLV record that can be used to encode/decode the control
|
||||||
// blocks. type from a given TLV stream.
|
// blocks. type from a given TLV stream.
|
||||||
func (c *ctrlBlocks) Record() tlv.Record {
|
func (c *ctrlBlocks) Record() tlv.Record {
|
||||||
return tlv.MakePrimitiveRecord(commitCtrlBlockType, c)
|
recordSize := func() uint64 {
|
||||||
|
var (
|
||||||
|
b bytes.Buffer
|
||||||
|
buf [8]byte
|
||||||
|
)
|
||||||
|
if err := ctrlBlockEncoder(&b, c, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(len(b.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
0, c, recordSize, ctrlBlockEncoder, ctrlBlockDecoder,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode encodes the set of control blocks.
|
// Encode encodes the set of control blocks.
|
||||||
@ -530,8 +573,8 @@ type tapTweaks struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// newTapTweaks returns a new tapTweaks struct.
|
// newTapTweaks returns a new tapTweaks struct.
|
||||||
func newTapTweaks() *tapTweaks {
|
func newTapTweaks() tapTweaks {
|
||||||
return &tapTweaks{
|
return tapTweaks{
|
||||||
BreachedHtlcTweaks: make(htlcTapTweaks),
|
BreachedHtlcTweaks: make(htlcTapTweaks),
|
||||||
BreachedSecondLevelHltcTweaks: make(htlcTapTweaks),
|
BreachedSecondLevelHltcTweaks: make(htlcTapTweaks),
|
||||||
}
|
}
|
||||||
@ -539,7 +582,7 @@ func newTapTweaks() *tapTweaks {
|
|||||||
|
|
||||||
// tapTweaksEncoder is a custom TLV encoder for the tapTweaks struct.
|
// tapTweaksEncoder is a custom TLV encoder for the tapTweaks struct.
|
||||||
func tapTweaksEncoder(w io.Writer, val any, _ *[8]byte) error {
|
func tapTweaksEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||||
if t, ok := val.(**tapTweaks); ok {
|
if t, ok := val.(*tapTweaks); ok {
|
||||||
return (*t).Encode(w)
|
return (*t).Encode(w)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,7 +591,7 @@ func tapTweaksEncoder(w io.Writer, val any, _ *[8]byte) error {
|
|||||||
|
|
||||||
// tapTweaksDecoder is a custom TLV decoder for the tapTweaks struct.
|
// tapTweaksDecoder is a custom TLV decoder for the tapTweaks struct.
|
||||||
func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
||||||
if typ, ok := val.(**tapTweaks); ok {
|
if typ, ok := val.(*tapTweaks); ok {
|
||||||
tweakReader := io.LimitReader(r, int64(l))
|
tweakReader := io.LimitReader(r, int64(l))
|
||||||
|
|
||||||
var tapTweaks tapTweaks
|
var tapTweaks tapTweaks
|
||||||
@ -557,7 +600,7 @@ func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
*typ = &tapTweaks
|
*typ = tapTweaks
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@ -565,27 +608,6 @@ func tapTweaksDecoder(r io.Reader, val any, _ *[8]byte, l uint64) error {
|
|||||||
return tlv.NewTypeForDecodingErr(val, "tapTweaks", l, l)
|
return tlv.NewTypeForDecodingErr(val, "tapTweaks", l, l)
|
||||||
}
|
}
|
||||||
|
|
||||||
// newTapTweaksRecord returns a new TLV record that can be used to
|
|
||||||
// encode/decode the tap tweak structs.
|
|
||||||
func newTapTweaksRecord(tweaks **tapTweaks) tlv.Record {
|
|
||||||
recordSize := func() uint64 {
|
|
||||||
var (
|
|
||||||
b bytes.Buffer
|
|
||||||
buf [8]byte
|
|
||||||
)
|
|
||||||
if err := tapTweaksEncoder(&b, tweaks, &buf); err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return uint64(len(b.Bytes()))
|
|
||||||
}
|
|
||||||
|
|
||||||
return tlv.MakeDynamicRecord(
|
|
||||||
taprootTapTweakType, tweaks, recordSize, tapTweaksEncoder,
|
|
||||||
tapTweaksDecoder,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// EncodeRecords returns the set of TLV records that encode the tweaks.
|
// EncodeRecords returns the set of TLV records that encode the tweaks.
|
||||||
func (t *tapTweaks) EncodeRecords() []tlv.Record {
|
func (t *tapTweaks) EncodeRecords() []tlv.Record {
|
||||||
var records []tlv.Record
|
var records []tlv.Record
|
||||||
@ -637,7 +659,21 @@ func (t *tapTweaks) DecodeRecords() []tlv.Record {
|
|||||||
// Record returns a TLV record that can be used to encode/decode the tap
|
// Record returns a TLV record that can be used to encode/decode the tap
|
||||||
// tweaks.
|
// tweaks.
|
||||||
func (t *tapTweaks) Record() tlv.Record {
|
func (t *tapTweaks) Record() tlv.Record {
|
||||||
return tlv.MakePrimitiveRecord(taprootTapTweakType, t)
|
recordSize := func() uint64 {
|
||||||
|
var (
|
||||||
|
b bytes.Buffer
|
||||||
|
buf [8]byte
|
||||||
|
)
|
||||||
|
if err := tapTweaksEncoder(&b, t, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(len(b.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
0, t, recordSize, tapTweaksEncoder, tapTweaksDecoder,
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Encode encodes the set of tap tweaks.
|
// Encode encodes the set of tap tweaks.
|
||||||
@ -659,3 +695,110 @@ func (t *tapTweaks) Decode(r io.Reader) error {
|
|||||||
|
|
||||||
return stream.Decode(r)
|
return stream.Decode(r)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// htlcAuxBlobs is a map of resolver IDs to their corresponding HTLC blobs.
|
||||||
|
// This is used to store the resolution blobs for HTLCs that are not yet
|
||||||
|
// resolved.
|
||||||
|
type htlcAuxBlobs map[resolverID]tlv.Blob
|
||||||
|
|
||||||
|
// newAuxHtlcBlobs returns a new instance of the htlcAuxBlobs struct.
|
||||||
|
func newAuxHtlcBlobs() htlcAuxBlobs {
|
||||||
|
return make(htlcAuxBlobs)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Encode encodes the set of HTLC blobs into the target writer.
|
||||||
|
func (h *htlcAuxBlobs) Encode(w io.Writer) error {
|
||||||
|
var buf [8]byte
|
||||||
|
|
||||||
|
numBlobs := uint64(len(*h))
|
||||||
|
if err := tlv.WriteVarInt(w, numBlobs, &buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for id, blob := range *h {
|
||||||
|
if _, err := w.Write(id[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := varBytesEncoder(w, &blob, &buf); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Decode decodes the set of HTLC blobs from the target reader.
|
||||||
|
func (h *htlcAuxBlobs) Decode(r io.Reader) error {
|
||||||
|
var buf [8]byte
|
||||||
|
|
||||||
|
numBlobs, err := tlv.ReadVarInt(r, &buf)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := uint64(0); i < numBlobs; i++ {
|
||||||
|
var id resolverID
|
||||||
|
if _, err := io.ReadFull(r, id[:]); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob tlv.Blob
|
||||||
|
if err := varBytesDecoder(r, &blob, &buf, 0); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
(*h)[id] = blob
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// eHtlcAuxBlobsEncoder is a custom TLV encoder for the htlcAuxBlobs struct.
|
||||||
|
func htlcAuxBlobsEncoder(w io.Writer, val any, _ *[8]byte) error {
|
||||||
|
if t, ok := val.(*htlcAuxBlobs); ok {
|
||||||
|
return (*t).Encode(w)
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForEncodingErr(val, "htlcAuxBlobs")
|
||||||
|
}
|
||||||
|
|
||||||
|
// dHtlcAuxBlobsDecoder is a custom TLV decoder for the htlcAuxBlobs struct.
|
||||||
|
func htlcAuxBlobsDecoder(r io.Reader, val any, _ *[8]byte,
|
||||||
|
l uint64) error {
|
||||||
|
|
||||||
|
if typ, ok := val.(*htlcAuxBlobs); ok {
|
||||||
|
blobReader := io.LimitReader(r, int64(l))
|
||||||
|
|
||||||
|
htlcBlobs := newAuxHtlcBlobs()
|
||||||
|
err := htlcBlobs.Decode(blobReader)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*typ = htlcBlobs
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.NewTypeForDecodingErr(val, "htlcAuxBlobs", l, l)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Record returns a tlv.Record for the htlcAuxBlobs struct.
|
||||||
|
func (h *htlcAuxBlobs) Record() tlv.Record {
|
||||||
|
recordSize := func() uint64 {
|
||||||
|
var (
|
||||||
|
b bytes.Buffer
|
||||||
|
buf [8]byte
|
||||||
|
)
|
||||||
|
if err := htlcAuxBlobsEncoder(&b, h, &buf); err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return uint64(len(b.Bytes()))
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlv.MakeDynamicRecord(
|
||||||
|
0, h, recordSize, htlcAuxBlobsEncoder, htlcAuxBlobsDecoder,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|||||||
@ -5,7 +5,9 @@ import (
|
|||||||
"math/rand"
|
"math/rand"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
"github.com/stretchr/testify/require"
|
"github.com/stretchr/testify/require"
|
||||||
|
"pgregory.net/rapid"
|
||||||
)
|
)
|
||||||
|
|
||||||
func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks {
|
func randResolverCtrlBlocks(t *testing.T) resolverCtrlBlocks {
|
||||||
@ -52,6 +54,25 @@ func randHtlcTweaks(t *testing.T) htlcTapTweaks {
|
|||||||
return tweaks
|
return tweaks
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func randHtlcAuxBlobs(t *testing.T) htlcAuxBlobs {
|
||||||
|
numBlobs := rand.Int() % 256
|
||||||
|
blobs := make(htlcAuxBlobs, numBlobs)
|
||||||
|
|
||||||
|
for i := 0; i < numBlobs; i++ {
|
||||||
|
var id resolverID
|
||||||
|
_, err := rand.Read(id[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var blob [100]byte
|
||||||
|
_, err = rand.Read(blob[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
blobs[id] = blob[:]
|
||||||
|
}
|
||||||
|
|
||||||
|
return blobs
|
||||||
|
}
|
||||||
|
|
||||||
// TestTaprootBriefcase tests the encode/decode methods of the taproot
|
// TestTaprootBriefcase tests the encode/decode methods of the taproot
|
||||||
// briefcase extension.
|
// briefcase extension.
|
||||||
func TestTaprootBriefcase(t *testing.T) {
|
func TestTaprootBriefcase(t *testing.T) {
|
||||||
@ -69,19 +90,32 @@ func TestTaprootBriefcase(t *testing.T) {
|
|||||||
_, err = rand.Read(anchorTweak[:])
|
_, err = rand.Read(anchorTweak[:])
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
var commitBlob [100]byte
|
||||||
|
_, err = rand.Read(commitBlob[:])
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
testCase := &taprootBriefcase{
|
testCase := &taprootBriefcase{
|
||||||
CtrlBlocks: &ctrlBlocks{
|
CtrlBlocks: tlv.NewRecordT[tlv.TlvType0](ctrlBlocks{
|
||||||
CommitSweepCtrlBlock: sweepCtrlBlock[:],
|
CommitSweepCtrlBlock: sweepCtrlBlock[:],
|
||||||
RevokeSweepCtrlBlock: revokeCtrlBlock[:],
|
RevokeSweepCtrlBlock: revokeCtrlBlock[:],
|
||||||
OutgoingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
OutgoingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
||||||
IncomingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
IncomingHtlcCtrlBlocks: randResolverCtrlBlocks(t),
|
||||||
SecondLevelCtrlBlocks: randResolverCtrlBlocks(t),
|
SecondLevelCtrlBlocks: randResolverCtrlBlocks(t),
|
||||||
},
|
}),
|
||||||
TapTweaks: &tapTweaks{
|
TapTweaks: tlv.NewRecordT[tlv.TlvType1](tapTweaks{
|
||||||
AnchorTweak: anchorTweak[:],
|
AnchorTweak: anchorTweak[:],
|
||||||
BreachedHtlcTweaks: randHtlcTweaks(t),
|
BreachedHtlcTweaks: randHtlcTweaks(t),
|
||||||
BreachedSecondLevelHltcTweaks: randHtlcTweaks(t),
|
BreachedSecondLevelHltcTweaks: randHtlcTweaks(t),
|
||||||
},
|
}),
|
||||||
|
SettledCommitBlob: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType2](commitBlob[:]),
|
||||||
|
),
|
||||||
|
BreachedCommitBlob: tlv.SomeRecordT(
|
||||||
|
tlv.NewPrimitiveRecord[tlv.TlvType3](commitBlob[:]),
|
||||||
|
),
|
||||||
|
HtlcBlobs: tlv.SomeRecordT(
|
||||||
|
tlv.NewRecordT[tlv.TlvType4](randHtlcAuxBlobs(t)),
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
var b bytes.Buffer
|
var b bytes.Buffer
|
||||||
@ -92,3 +126,21 @@ func TestTaprootBriefcase(t *testing.T) {
|
|||||||
|
|
||||||
require.Equal(t, testCase, &decodedCase)
|
require.Equal(t, testCase, &decodedCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestHtlcAuxBlobEncodeDecode tests the encode/decode methods of the HTLC aux
|
||||||
|
// blobs.
|
||||||
|
func TestHtlcAuxBlobEncodeDecode(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
rapid.Check(t, func(t *rapid.T) {
|
||||||
|
htlcBlobs := rapid.Make[htlcAuxBlobs]().Draw(t, "htlcAuxBlobs")
|
||||||
|
|
||||||
|
var b bytes.Buffer
|
||||||
|
require.NoError(t, htlcBlobs.Encode(&b))
|
||||||
|
|
||||||
|
decodedBlobs := newAuxHtlcBlobs()
|
||||||
|
require.NoError(t, decodedBlobs.Decode(&b))
|
||||||
|
|
||||||
|
require.Equal(t, htlcBlobs, decodedBlobs)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,78 @@
|
|||||||
|
# 2024/09/02 14:02:53.354676 [TestHtlcAuxBlobEncodeDecode] [rapid] draw htlcAuxBlobs: contractcourt.htlcAuxBlobs{contractcourt.resolverID{0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0}:[]uint8{}}
|
||||||
|
#
|
||||||
|
v0.4.8#15807814492030881602
|
||||||
|
0x5555555555555
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
|
0x0
|
||||||
@ -21,6 +21,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/lnutils"
|
"github.com/lightningnetwork/lnd/lnutils"
|
||||||
"github.com/lightningnetwork/lnd/lnwallet"
|
"github.com/lightningnetwork/lnd/lnwallet"
|
||||||
"github.com/lightningnetwork/lnd/sweep"
|
"github.com/lightningnetwork/lnd/sweep"
|
||||||
|
"github.com/lightningnetwork/lnd/tlv"
|
||||||
)
|
)
|
||||||
|
|
||||||
// SUMMARY OF OUTPUT STATES
|
// SUMMARY OF OUTPUT STATES
|
||||||
@ -1423,6 +1424,7 @@ func makeKidOutput(outpoint, originChanPoint *wire.OutPoint,
|
|||||||
return kidOutput{
|
return kidOutput{
|
||||||
breachedOutput: makeBreachedOutput(
|
breachedOutput: makeBreachedOutput(
|
||||||
outpoint, witnessType, nil, signDescriptor, heightHint,
|
outpoint, witnessType, nil, signDescriptor, heightHint,
|
||||||
|
fn.None[tlv.Blob](),
|
||||||
),
|
),
|
||||||
isHtlc: isHtlc,
|
isHtlc: isHtlc,
|
||||||
originChanPoint: *originChanPoint,
|
originChanPoint: *originChanPoint,
|
||||||
|
|||||||
@ -3,7 +3,7 @@
|
|||||||
# /make/builder.Dockerfile
|
# /make/builder.Dockerfile
|
||||||
# /.github/workflows/main.yml
|
# /.github/workflows/main.yml
|
||||||
# /.github/workflows/release.yml
|
# /.github/workflows/release.yml
|
||||||
FROM golang:1.22.5-alpine as builder
|
FROM golang:1.22.6-alpine as builder
|
||||||
|
|
||||||
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"
|
LABEL maintainer="Olaoluwa Osuntokun <laolu@lightning.engineering>"
|
||||||
|
|
||||||
|
|||||||
252
discovery/ban.go
Normal file
252
discovery/ban.go
Normal file
@ -0,0 +1,252 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
|
"github.com/lightninglabs/neutrino/cache"
|
||||||
|
"github.com/lightninglabs/neutrino/cache/lru"
|
||||||
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
|
"github.com/lightningnetwork/lnd/lnwire"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
// maxBannedPeers limits the maximum number of banned pubkeys that
|
||||||
|
// we'll store.
|
||||||
|
// TODO(eugene): tune.
|
||||||
|
maxBannedPeers = 10_000
|
||||||
|
|
||||||
|
// banThreshold is the point at which non-channel peers will be banned.
|
||||||
|
// TODO(eugene): tune.
|
||||||
|
banThreshold = 100
|
||||||
|
|
||||||
|
// banTime is the amount of time that the non-channel peer will be
|
||||||
|
// banned for. Channel announcements from channel peers will be dropped
|
||||||
|
// if it's not one of our channels.
|
||||||
|
// TODO(eugene): tune.
|
||||||
|
banTime = time.Hour * 48
|
||||||
|
|
||||||
|
// resetDelta is the time after a peer's last ban update that we'll
|
||||||
|
// reset its ban score.
|
||||||
|
// TODO(eugene): tune.
|
||||||
|
resetDelta = time.Hour * 48
|
||||||
|
|
||||||
|
// purgeInterval is how often we'll remove entries from the
|
||||||
|
// peerBanIndex and allow peers to be un-banned. This interval is also
|
||||||
|
// used to reset ban scores of peers that aren't banned.
|
||||||
|
purgeInterval = time.Minute * 10
|
||||||
|
)
|
||||||
|
|
||||||
|
var ErrPeerBanned = errors.New("peer has bypassed ban threshold - banning")
|
||||||
|
|
||||||
|
// ClosedChannelTracker handles closed channels being gossiped to us.
|
||||||
|
type ClosedChannelTracker interface {
|
||||||
|
// GraphCloser is used to mark channels as closed and to check whether
|
||||||
|
// certain channels are closed.
|
||||||
|
GraphCloser
|
||||||
|
|
||||||
|
// IsChannelPeer checks whether we have a channel with a peer.
|
||||||
|
IsChannelPeer(*btcec.PublicKey) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GraphCloser handles tracking closed channels by their scid.
|
||||||
|
type GraphCloser interface {
|
||||||
|
// PutClosedScid marks a channel as closed so that we won't validate
|
||||||
|
// channel announcements for it again.
|
||||||
|
PutClosedScid(lnwire.ShortChannelID) error
|
||||||
|
|
||||||
|
// IsClosedScid checks if a short channel id is closed.
|
||||||
|
IsClosedScid(lnwire.ShortChannelID) (bool, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NodeInfoInquirier handles queries relating to specific nodes and channels
|
||||||
|
// they may have with us.
|
||||||
|
type NodeInfoInquirer interface {
|
||||||
|
// FetchOpenChannels returns the set of channels that we have with the
|
||||||
|
// peer identified by the passed-in public key.
|
||||||
|
FetchOpenChannels(*btcec.PublicKey) ([]*channeldb.OpenChannel, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ScidCloserMan helps the gossiper handle closed channels that are in the
|
||||||
|
// ChannelGraph.
|
||||||
|
type ScidCloserMan struct {
|
||||||
|
graph GraphCloser
|
||||||
|
channelDB NodeInfoInquirer
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewScidCloserMan creates a new ScidCloserMan.
|
||||||
|
func NewScidCloserMan(graph GraphCloser,
|
||||||
|
channelDB NodeInfoInquirer) *ScidCloserMan {
|
||||||
|
|
||||||
|
return &ScidCloserMan{
|
||||||
|
graph: graph,
|
||||||
|
channelDB: channelDB,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// PutClosedScid marks scid as closed so the gossiper can ignore this channel
|
||||||
|
// in the future.
|
||||||
|
func (s *ScidCloserMan) PutClosedScid(scid lnwire.ShortChannelID) error {
|
||||||
|
return s.graph.PutClosedScid(scid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsClosedScid checks whether scid is closed so that the gossiper can ignore
|
||||||
|
// it.
|
||||||
|
func (s *ScidCloserMan) IsClosedScid(scid lnwire.ShortChannelID) (bool,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
return s.graph.IsClosedScid(scid)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsChannelPeer checks whether we have a channel with the peer.
|
||||||
|
func (s *ScidCloserMan) IsChannelPeer(peerKey *btcec.PublicKey) (bool, error) {
|
||||||
|
chans, err := s.channelDB.FetchOpenChannels(peerKey)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return len(chans) > 0, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// A compile-time constraint to ensure ScidCloserMan implements
|
||||||
|
// ClosedChannelTracker.
|
||||||
|
var _ ClosedChannelTracker = (*ScidCloserMan)(nil)
|
||||||
|
|
||||||
|
// cachedBanInfo is used to track a peer's ban score and if it is banned.
|
||||||
|
type cachedBanInfo struct {
|
||||||
|
score uint64
|
||||||
|
lastUpdate time.Time
|
||||||
|
}
|
||||||
|
|
||||||
|
// Size returns the "size" of an entry.
|
||||||
|
func (c *cachedBanInfo) Size() (uint64, error) {
|
||||||
|
return 1, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBanned returns true if the ban score is greater than the ban threshold.
|
||||||
|
func (c *cachedBanInfo) isBanned() bool {
|
||||||
|
return c.score >= banThreshold
|
||||||
|
}
|
||||||
|
|
||||||
|
// banman is responsible for banning peers that are misbehaving. The banman is
|
||||||
|
// in-memory and will be reset upon restart of LND. If a node's pubkey is in
|
||||||
|
// the peerBanIndex, it has a ban score. Ban scores start at 1 and are
|
||||||
|
// incremented by 1 for each instance of misbehavior. It uses an LRU cache to
|
||||||
|
// cut down on memory usage in case there are many banned peers and to protect
|
||||||
|
// against DoS.
|
||||||
|
type banman struct {
|
||||||
|
// peerBanIndex tracks our peers' ban scores and if they are banned and
|
||||||
|
// for how long. The ban score is incremented when our peer gives us
|
||||||
|
// gossip messages that are invalid.
|
||||||
|
peerBanIndex *lru.Cache[[33]byte, *cachedBanInfo]
|
||||||
|
|
||||||
|
wg sync.WaitGroup
|
||||||
|
quit chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// newBanman creates a new banman with the default maxBannedPeers.
|
||||||
|
func newBanman() *banman {
|
||||||
|
return &banman{
|
||||||
|
peerBanIndex: lru.NewCache[[33]byte, *cachedBanInfo](
|
||||||
|
maxBannedPeers,
|
||||||
|
),
|
||||||
|
quit: make(chan struct{}),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// start kicks off the banman by calling purgeExpiredBans.
|
||||||
|
func (b *banman) start() {
|
||||||
|
b.wg.Add(1)
|
||||||
|
go b.purgeExpiredBans()
|
||||||
|
}
|
||||||
|
|
||||||
|
// stop halts the banman.
|
||||||
|
func (b *banman) stop() {
|
||||||
|
close(b.quit)
|
||||||
|
b.wg.Wait()
|
||||||
|
}
|
||||||
|
|
||||||
|
// purgeOldEntries removes ban entries if their ban has expired.
|
||||||
|
func (b *banman) purgeExpiredBans() {
|
||||||
|
defer b.wg.Done()
|
||||||
|
|
||||||
|
purgeTicker := time.NewTicker(purgeInterval)
|
||||||
|
defer purgeTicker.Stop()
|
||||||
|
|
||||||
|
for {
|
||||||
|
select {
|
||||||
|
case <-purgeTicker.C:
|
||||||
|
b.purgeBanEntries()
|
||||||
|
|
||||||
|
case <-b.quit:
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// purgeBanEntries does two things:
|
||||||
|
// - removes peers from our ban list whose ban timer is up
|
||||||
|
// - removes peers whose ban scores have expired.
|
||||||
|
func (b *banman) purgeBanEntries() {
|
||||||
|
keysToRemove := make([][33]byte, 0)
|
||||||
|
|
||||||
|
sweepEntries := func(pubkey [33]byte, banInfo *cachedBanInfo) bool {
|
||||||
|
if banInfo.isBanned() {
|
||||||
|
// If the peer is banned, check if the ban timer has
|
||||||
|
// expired.
|
||||||
|
if banInfo.lastUpdate.Add(banTime).Before(time.Now()) {
|
||||||
|
keysToRemove = append(keysToRemove, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
if banInfo.lastUpdate.Add(resetDelta).Before(time.Now()) {
|
||||||
|
// Remove non-banned peers whose ban scores have
|
||||||
|
// expired.
|
||||||
|
keysToRemove = append(keysToRemove, pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
b.peerBanIndex.Range(sweepEntries)
|
||||||
|
|
||||||
|
for _, key := range keysToRemove {
|
||||||
|
b.peerBanIndex.Delete(key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// isBanned checks whether the peer identified by the pubkey is banned.
|
||||||
|
func (b *banman) isBanned(pubkey [33]byte) bool {
|
||||||
|
banInfo, err := b.peerBanIndex.Get(pubkey)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, cache.ErrElementNotFound):
|
||||||
|
return false
|
||||||
|
|
||||||
|
default:
|
||||||
|
return banInfo.isBanned()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// incrementBanScore increments a peer's ban score.
|
||||||
|
func (b *banman) incrementBanScore(pubkey [33]byte) {
|
||||||
|
banInfo, err := b.peerBanIndex.Get(pubkey)
|
||||||
|
switch {
|
||||||
|
case errors.Is(err, cache.ErrElementNotFound):
|
||||||
|
cachedInfo := &cachedBanInfo{
|
||||||
|
score: 1,
|
||||||
|
lastUpdate: time.Now(),
|
||||||
|
}
|
||||||
|
_, _ = b.peerBanIndex.Put(pubkey, cachedInfo)
|
||||||
|
default:
|
||||||
|
cachedInfo := &cachedBanInfo{
|
||||||
|
score: banInfo.score + 1,
|
||||||
|
lastUpdate: time.Now(),
|
||||||
|
}
|
||||||
|
|
||||||
|
_, _ = b.peerBanIndex.Put(pubkey, cachedInfo)
|
||||||
|
}
|
||||||
|
}
|
||||||
60
discovery/ban_test.go
Normal file
60
discovery/ban_test.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package discovery
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/lightninglabs/neutrino/cache"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestPurgeBanEntries tests that we properly purge ban entries on a timer.
|
||||||
|
func TestPurgeBanEntries(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
b := newBanman()
|
||||||
|
|
||||||
|
// Ban a peer by repeatedly incrementing its ban score.
|
||||||
|
peer1 := [33]byte{0x00}
|
||||||
|
|
||||||
|
for i := 0; i < banThreshold; i++ {
|
||||||
|
b.incrementBanScore(peer1)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Assert that the peer is now banned.
|
||||||
|
require.True(t, b.isBanned(peer1))
|
||||||
|
|
||||||
|
// A call to purgeBanEntries should not remove the peer from the index.
|
||||||
|
b.purgeBanEntries()
|
||||||
|
require.True(t, b.isBanned(peer1))
|
||||||
|
|
||||||
|
// Now set the peer's last update time to two banTimes in the past so
|
||||||
|
// that we can assert that purgeBanEntries does remove it from the
|
||||||
|
// index.
|
||||||
|
banInfo, err := b.peerBanIndex.Get(peer1)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
banInfo.lastUpdate = time.Now().Add(-2 * banTime)
|
||||||
|
|
||||||
|
b.purgeBanEntries()
|
||||||
|
_, err = b.peerBanIndex.Get(peer1)
|
||||||
|
require.ErrorIs(t, err, cache.ErrElementNotFound)
|
||||||
|
|
||||||
|
// Increment the peer's ban score again but don't get it banned.
|
||||||
|
b.incrementBanScore(peer1)
|
||||||
|
require.False(t, b.isBanned(peer1))
|
||||||
|
|
||||||
|
// Assert that purgeBanEntries does nothing.
|
||||||
|
b.purgeBanEntries()
|
||||||
|
banInfo, err = b.peerBanIndex.Get(peer1)
|
||||||
|
require.Nil(t, err)
|
||||||
|
|
||||||
|
// Set its lastUpdate time to 2 resetDelta's in the past so that
|
||||||
|
// purgeBanEntries removes it.
|
||||||
|
banInfo.lastUpdate = time.Now().Add(-2 * resetDelta)
|
||||||
|
|
||||||
|
b.purgeBanEntries()
|
||||||
|
|
||||||
|
_, err = b.peerBanIndex.Get(peer1)
|
||||||
|
require.ErrorIs(t, err, cache.ErrElementNotFound)
|
||||||
|
}
|
||||||
@ -20,6 +20,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/graph"
|
"github.com/lightningnetwork/lnd/graph"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
@ -82,9 +83,10 @@ var (
|
|||||||
// can provide that serve useful when processing a specific network
|
// can provide that serve useful when processing a specific network
|
||||||
// announcement.
|
// announcement.
|
||||||
type optionalMsgFields struct {
|
type optionalMsgFields struct {
|
||||||
capacity *btcutil.Amount
|
capacity *btcutil.Amount
|
||||||
channelPoint *wire.OutPoint
|
channelPoint *wire.OutPoint
|
||||||
remoteAlias *lnwire.ShortChannelID
|
remoteAlias *lnwire.ShortChannelID
|
||||||
|
tapscriptRoot fn.Option[chainhash.Hash]
|
||||||
}
|
}
|
||||||
|
|
||||||
// apply applies the optional fields within the functional options.
|
// apply applies the optional fields within the functional options.
|
||||||
@ -115,6 +117,14 @@ func ChannelPoint(op wire.OutPoint) OptionalMsgField {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TapscriptRoot is an optional field that lets the gossiper know of the root of
|
||||||
|
// the tapscript tree for a custom channel.
|
||||||
|
func TapscriptRoot(root fn.Option[chainhash.Hash]) OptionalMsgField {
|
||||||
|
return func(f *optionalMsgFields) {
|
||||||
|
f.tapscriptRoot = root
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// RemoteAlias is an optional field that lets the gossiper know that a locally
|
// RemoteAlias is an optional field that lets the gossiper know that a locally
|
||||||
// sent channel update is actually an update for the peer that should replace
|
// sent channel update is actually an update for the peer that should replace
|
||||||
// the ShortChannelID field with the remote's alias. This is only used for
|
// the ShortChannelID field with the remote's alias. This is only used for
|
||||||
@ -256,6 +266,11 @@ type Config struct {
|
|||||||
// here?
|
// here?
|
||||||
AnnSigner lnwallet.MessageSigner
|
AnnSigner lnwallet.MessageSigner
|
||||||
|
|
||||||
|
// ScidCloser is an instance of ClosedChannelTracker that helps the
|
||||||
|
// gossiper cut down on spam channel announcements for already closed
|
||||||
|
// channels.
|
||||||
|
ScidCloser ClosedChannelTracker
|
||||||
|
|
||||||
// NumActiveSyncers is the number of peers for which we should have
|
// NumActiveSyncers is the number of peers for which we should have
|
||||||
// active syncers with. After reaching NumActiveSyncers, any future
|
// active syncers with. After reaching NumActiveSyncers, any future
|
||||||
// gossip syncers will be passive.
|
// gossip syncers will be passive.
|
||||||
@ -434,6 +449,9 @@ type AuthenticatedGossiper struct {
|
|||||||
// ChannelAnnouncement for the channel is received.
|
// ChannelAnnouncement for the channel is received.
|
||||||
prematureChannelUpdates *lru.Cache[uint64, *cachedNetworkMsg]
|
prematureChannelUpdates *lru.Cache[uint64, *cachedNetworkMsg]
|
||||||
|
|
||||||
|
// banman tracks our peer's ban status.
|
||||||
|
banman *banman
|
||||||
|
|
||||||
// networkMsgs is a channel that carries new network broadcasted
|
// networkMsgs is a channel that carries new network broadcasted
|
||||||
// message from outside the gossiper service to be processed by the
|
// message from outside the gossiper service to be processed by the
|
||||||
// networkHandler.
|
// networkHandler.
|
||||||
@ -512,6 +530,7 @@ func New(cfg Config, selfKeyDesc *keychain.KeyDescriptor) *AuthenticatedGossiper
|
|||||||
maxRejectedUpdates,
|
maxRejectedUpdates,
|
||||||
),
|
),
|
||||||
chanUpdateRateLimiter: make(map[uint64][2]*rate.Limiter),
|
chanUpdateRateLimiter: make(map[uint64][2]*rate.Limiter),
|
||||||
|
banman: newBanman(),
|
||||||
}
|
}
|
||||||
|
|
||||||
gossiper.syncMgr = newSyncManager(&SyncManagerCfg{
|
gossiper.syncMgr = newSyncManager(&SyncManagerCfg{
|
||||||
@ -606,6 +625,8 @@ func (d *AuthenticatedGossiper) start() error {
|
|||||||
|
|
||||||
d.syncMgr.Start()
|
d.syncMgr.Start()
|
||||||
|
|
||||||
|
d.banman.start()
|
||||||
|
|
||||||
// Start receiving blocks in its dedicated goroutine.
|
// Start receiving blocks in its dedicated goroutine.
|
||||||
d.wg.Add(2)
|
d.wg.Add(2)
|
||||||
go d.syncBlockHeight()
|
go d.syncBlockHeight()
|
||||||
@ -762,6 +783,8 @@ func (d *AuthenticatedGossiper) stop() {
|
|||||||
|
|
||||||
d.syncMgr.Stop()
|
d.syncMgr.Stop()
|
||||||
|
|
||||||
|
d.banman.stop()
|
||||||
|
|
||||||
close(d.quit)
|
close(d.quit)
|
||||||
d.wg.Wait()
|
d.wg.Wait()
|
||||||
|
|
||||||
@ -2232,7 +2255,7 @@ func (d *AuthenticatedGossiper) updateChannel(info *models.ChannelEdgeInfo,
|
|||||||
BitcoinKey1: info.BitcoinKey1Bytes,
|
BitcoinKey1: info.BitcoinKey1Bytes,
|
||||||
Features: lnwire.NewRawFeatureVector(),
|
Features: lnwire.NewRawFeatureVector(),
|
||||||
BitcoinKey2: info.BitcoinKey2Bytes,
|
BitcoinKey2: info.BitcoinKey2Bytes,
|
||||||
ExtraOpaqueData: edge.ExtraOpaqueData,
|
ExtraOpaqueData: info.ExtraOpaqueData,
|
||||||
}
|
}
|
||||||
chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature(
|
chanAnn.NodeSig1, err = lnwire.NewSigFromECDSARawSignature(
|
||||||
info.AuthProof.NodeSig1Bytes,
|
info.AuthProof.NodeSig1Bytes,
|
||||||
@ -2399,8 +2422,10 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
ann *lnwire.ChannelAnnouncement,
|
ann *lnwire.ChannelAnnouncement,
|
||||||
ops []batch.SchedulerOption) ([]networkMsg, bool) {
|
ops []batch.SchedulerOption) ([]networkMsg, bool) {
|
||||||
|
|
||||||
|
scid := ann.ShortChannelID
|
||||||
|
|
||||||
log.Debugf("Processing ChannelAnnouncement: peer=%v, short_chan_id=%v",
|
log.Debugf("Processing ChannelAnnouncement: peer=%v, short_chan_id=%v",
|
||||||
nMsg.peer, ann.ShortChannelID.ToUint64())
|
nMsg.peer, scid.ToUint64())
|
||||||
|
|
||||||
// We'll ignore any channel announcements that target any chain other
|
// We'll ignore any channel announcements that target any chain other
|
||||||
// than the set of chains we know of.
|
// than the set of chains we know of.
|
||||||
@ -2411,7 +2436,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
|
|
||||||
key := newRejectCacheKey(
|
key := newRejectCacheKey(
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(),
|
||||||
sourceToPub(nMsg.source),
|
sourceToPub(nMsg.source),
|
||||||
)
|
)
|
||||||
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
@ -2423,13 +2448,12 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
// If this is a remote ChannelAnnouncement with an alias SCID, we'll
|
// If this is a remote ChannelAnnouncement with an alias SCID, we'll
|
||||||
// reject the announcement. Since the router accepts alias SCIDs,
|
// reject the announcement. Since the router accepts alias SCIDs,
|
||||||
// not erroring out would be a DoS vector.
|
// not erroring out would be a DoS vector.
|
||||||
if nMsg.isRemote && d.cfg.IsAlias(ann.ShortChannelID) {
|
if nMsg.isRemote && d.cfg.IsAlias(scid) {
|
||||||
err := fmt.Errorf("ignoring remote alias channel=%v",
|
err := fmt.Errorf("ignoring remote alias channel=%v", scid)
|
||||||
ann.ShortChannelID)
|
|
||||||
log.Errorf(err.Error())
|
log.Errorf(err.Error())
|
||||||
|
|
||||||
key := newRejectCacheKey(
|
key := newRejectCacheKey(
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(),
|
||||||
sourceToPub(nMsg.source),
|
sourceToPub(nMsg.source),
|
||||||
)
|
)
|
||||||
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
@ -2441,11 +2465,10 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
// If the advertised inclusionary block is beyond our knowledge of the
|
// If the advertised inclusionary block is beyond our knowledge of the
|
||||||
// chain tip, then we'll ignore it for now.
|
// chain tip, then we'll ignore it for now.
|
||||||
d.Lock()
|
d.Lock()
|
||||||
if nMsg.isRemote && d.isPremature(ann.ShortChannelID, 0, nMsg) {
|
if nMsg.isRemote && d.isPremature(scid, 0, nMsg) {
|
||||||
log.Warnf("Announcement for chan_id=(%v), is premature: "+
|
log.Warnf("Announcement for chan_id=(%v), is premature: "+
|
||||||
"advertises height %v, only height %v is known",
|
"advertises height %v, only height %v is known",
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(), scid.BlockHeight, d.bestHeight)
|
||||||
ann.ShortChannelID.BlockHeight, d.bestHeight)
|
|
||||||
d.Unlock()
|
d.Unlock()
|
||||||
nMsg.err <- nil
|
nMsg.err <- nil
|
||||||
return nil, false
|
return nil, false
|
||||||
@ -2454,11 +2477,56 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
|
|
||||||
// At this point, we'll now ask the router if this is a zombie/known
|
// At this point, we'll now ask the router if this is a zombie/known
|
||||||
// edge. If so we can skip all the processing below.
|
// edge. If so we can skip all the processing below.
|
||||||
if d.cfg.Graph.IsKnownEdge(ann.ShortChannelID) {
|
if d.cfg.Graph.IsKnownEdge(scid) {
|
||||||
nMsg.err <- nil
|
nMsg.err <- nil
|
||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check if the channel is already closed in which case we can ignore
|
||||||
|
// it.
|
||||||
|
closed, err := d.cfg.ScidCloser.IsClosedScid(scid)
|
||||||
|
if err != nil {
|
||||||
|
log.Errorf("failed to check if scid %v is closed: %v", scid,
|
||||||
|
err)
|
||||||
|
nMsg.err <- err
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if closed {
|
||||||
|
err = fmt.Errorf("ignoring closed channel %v", scid)
|
||||||
|
log.Error(err)
|
||||||
|
|
||||||
|
// If this is an announcement from us, we'll just ignore it.
|
||||||
|
if !nMsg.isRemote {
|
||||||
|
nMsg.err <- err
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the peer's ban score if they are sending closed
|
||||||
|
// channel announcements.
|
||||||
|
d.banman.incrementBanScore(nMsg.peer.PubKey())
|
||||||
|
|
||||||
|
// If the peer is banned and not a channel peer, we'll
|
||||||
|
// disconnect them.
|
||||||
|
shouldDc, dcErr := d.ShouldDisconnect(nMsg.peer.IdentityKey())
|
||||||
|
if dcErr != nil {
|
||||||
|
log.Errorf("failed to check if we should disconnect "+
|
||||||
|
"peer: %v", dcErr)
|
||||||
|
nMsg.err <- dcErr
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldDc {
|
||||||
|
nMsg.peer.Disconnect(ErrPeerBanned)
|
||||||
|
}
|
||||||
|
|
||||||
|
nMsg.err <- err
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// If this is a remote channel announcement, then we'll validate all
|
// If this is a remote channel announcement, then we'll validate all
|
||||||
// the signatures within the proof as it should be well formed.
|
// the signatures within the proof as it should be well formed.
|
||||||
var proof *models.ChannelAuthProof
|
var proof *models.ChannelAuthProof
|
||||||
@ -2468,7 +2536,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
"%v", err)
|
"%v", err)
|
||||||
|
|
||||||
key := newRejectCacheKey(
|
key := newRejectCacheKey(
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(),
|
||||||
sourceToPub(nMsg.source),
|
sourceToPub(nMsg.source),
|
||||||
)
|
)
|
||||||
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
@ -2499,7 +2567,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
}
|
}
|
||||||
|
|
||||||
edge := &models.ChannelEdgeInfo{
|
edge := &models.ChannelEdgeInfo{
|
||||||
ChannelID: ann.ShortChannelID.ToUint64(),
|
ChannelID: scid.ToUint64(),
|
||||||
ChainHash: ann.ChainHash,
|
ChainHash: ann.ChainHash,
|
||||||
NodeKey1Bytes: ann.NodeID1,
|
NodeKey1Bytes: ann.NodeID1,
|
||||||
NodeKey2Bytes: ann.NodeID2,
|
NodeKey2Bytes: ann.NodeID2,
|
||||||
@ -2520,10 +2588,12 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
cp := *nMsg.optionalMsgFields.channelPoint
|
cp := *nMsg.optionalMsgFields.channelPoint
|
||||||
edge.ChannelPoint = cp
|
edge.ChannelPoint = cp
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Optional tapscript root for custom channels.
|
||||||
|
edge.TapscriptRoot = nMsg.optionalMsgFields.tapscriptRoot
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Adding edge for short_chan_id: %v",
|
log.Debugf("Adding edge for short_chan_id: %v", scid.ToUint64())
|
||||||
ann.ShortChannelID.ToUint64())
|
|
||||||
|
|
||||||
// We will add the edge to the channel router. If the nodes present in
|
// We will add the edge to the channel router. If the nodes present in
|
||||||
// this channel are not present in the database, a partial node will be
|
// this channel are not present in the database, a partial node will be
|
||||||
@ -2533,24 +2603,25 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
// channel ID. We do this to ensure no other goroutine has read the
|
// channel ID. We do this to ensure no other goroutine has read the
|
||||||
// database and is now making decisions based on this DB state, before
|
// database and is now making decisions based on this DB state, before
|
||||||
// it writes to the DB.
|
// it writes to the DB.
|
||||||
d.channelMtx.Lock(ann.ShortChannelID.ToUint64())
|
d.channelMtx.Lock(scid.ToUint64())
|
||||||
err := d.cfg.Graph.AddEdge(edge, ops...)
|
err = d.cfg.Graph.AddEdge(edge, ops...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("Graph rejected edge for short_chan_id(%v): %v",
|
log.Debugf("Graph rejected edge for short_chan_id(%v): %v",
|
||||||
ann.ShortChannelID.ToUint64(), err)
|
scid.ToUint64(), err)
|
||||||
|
|
||||||
defer d.channelMtx.Unlock(ann.ShortChannelID.ToUint64())
|
defer d.channelMtx.Unlock(scid.ToUint64())
|
||||||
|
|
||||||
// If the edge was rejected due to already being known, then it
|
// If the edge was rejected due to already being known, then it
|
||||||
// may be the case that this new message has a fresh channel
|
// may be the case that this new message has a fresh channel
|
||||||
// proof, so we'll check.
|
// proof, so we'll check.
|
||||||
if graph.IsError(err, graph.ErrIgnored) {
|
switch {
|
||||||
|
case graph.IsError(err, graph.ErrIgnored):
|
||||||
// Attempt to process the rejected message to see if we
|
// Attempt to process the rejected message to see if we
|
||||||
// get any new announcements.
|
// get any new announcements.
|
||||||
anns, rErr := d.processRejectedEdge(ann, proof)
|
anns, rErr := d.processRejectedEdge(ann, proof)
|
||||||
if rErr != nil {
|
if rErr != nil {
|
||||||
key := newRejectCacheKey(
|
key := newRejectCacheKey(
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(),
|
||||||
sourceToPub(nMsg.source),
|
sourceToPub(nMsg.source),
|
||||||
)
|
)
|
||||||
cr := &cachedReject{}
|
cr := &cachedReject{}
|
||||||
@ -2572,31 +2643,99 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
nMsg.err <- nil
|
nMsg.err <- nil
|
||||||
|
|
||||||
return anns, true
|
return anns, true
|
||||||
} else {
|
|
||||||
|
case graph.IsError(
|
||||||
|
err, graph.ErrNoFundingTransaction,
|
||||||
|
graph.ErrInvalidFundingOutput,
|
||||||
|
):
|
||||||
|
key := newRejectCacheKey(
|
||||||
|
scid.ToUint64(),
|
||||||
|
sourceToPub(nMsg.source),
|
||||||
|
)
|
||||||
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
|
|
||||||
|
// Increment the peer's ban score. We check isRemote
|
||||||
|
// so we don't actually ban the peer in case of a local
|
||||||
|
// bug.
|
||||||
|
if nMsg.isRemote {
|
||||||
|
d.banman.incrementBanScore(nMsg.peer.PubKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
case graph.IsError(err, graph.ErrChannelSpent):
|
||||||
|
key := newRejectCacheKey(
|
||||||
|
scid.ToUint64(),
|
||||||
|
sourceToPub(nMsg.source),
|
||||||
|
)
|
||||||
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
|
|
||||||
|
// Since this channel has already been closed, we'll
|
||||||
|
// add it to the graph's closed channel index such that
|
||||||
|
// we won't attempt to do expensive validation checks
|
||||||
|
// on it again.
|
||||||
|
// TODO: Populate the ScidCloser by using closed
|
||||||
|
// channel notifications.
|
||||||
|
dbErr := d.cfg.ScidCloser.PutClosedScid(scid)
|
||||||
|
if dbErr != nil {
|
||||||
|
log.Errorf("failed to mark scid(%v) as "+
|
||||||
|
"closed: %v", scid, dbErr)
|
||||||
|
|
||||||
|
nMsg.err <- dbErr
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increment the peer's ban score. We check isRemote
|
||||||
|
// so we don't accidentally ban ourselves in case of a
|
||||||
|
// bug.
|
||||||
|
if nMsg.isRemote {
|
||||||
|
d.banman.incrementBanScore(nMsg.peer.PubKey())
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
// Otherwise, this is just a regular rejected edge.
|
// Otherwise, this is just a regular rejected edge.
|
||||||
key := newRejectCacheKey(
|
key := newRejectCacheKey(
|
||||||
ann.ShortChannelID.ToUint64(),
|
scid.ToUint64(),
|
||||||
sourceToPub(nMsg.source),
|
sourceToPub(nMsg.source),
|
||||||
)
|
)
|
||||||
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
_, _ = d.recentRejects.Put(key, &cachedReject{})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if !nMsg.isRemote {
|
||||||
|
log.Errorf("failed to add edge for local channel: %v",
|
||||||
|
err)
|
||||||
|
nMsg.err <- err
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldDc, dcErr := d.ShouldDisconnect(nMsg.peer.IdentityKey())
|
||||||
|
if dcErr != nil {
|
||||||
|
log.Errorf("failed to check if we should disconnect "+
|
||||||
|
"peer: %v", dcErr)
|
||||||
|
nMsg.err <- dcErr
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
|
if shouldDc {
|
||||||
|
nMsg.peer.Disconnect(ErrPeerBanned)
|
||||||
|
}
|
||||||
|
|
||||||
nMsg.err <- err
|
nMsg.err <- err
|
||||||
|
|
||||||
return nil, false
|
return nil, false
|
||||||
}
|
}
|
||||||
|
|
||||||
// If err is nil, release the lock immediately.
|
// If err is nil, release the lock immediately.
|
||||||
d.channelMtx.Unlock(ann.ShortChannelID.ToUint64())
|
d.channelMtx.Unlock(scid.ToUint64())
|
||||||
|
|
||||||
log.Debugf("Finish adding edge for short_chan_id: %v",
|
log.Debugf("Finish adding edge for short_chan_id: %v", scid.ToUint64())
|
||||||
ann.ShortChannelID.ToUint64())
|
|
||||||
|
|
||||||
// If we earlier received any ChannelUpdates for this channel, we can
|
// If we earlier received any ChannelUpdates for this channel, we can
|
||||||
// now process them, as the channel is added to the graph.
|
// now process them, as the channel is added to the graph.
|
||||||
shortChanID := ann.ShortChannelID.ToUint64()
|
|
||||||
var channelUpdates []*processedNetworkMsg
|
var channelUpdates []*processedNetworkMsg
|
||||||
|
|
||||||
earlyChanUpdates, err := d.prematureChannelUpdates.Get(shortChanID)
|
earlyChanUpdates, err := d.prematureChannelUpdates.Get(scid.ToUint64())
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// There was actually an entry in the map, so we'll accumulate
|
// There was actually an entry in the map, so we'll accumulate
|
||||||
// it. We don't worry about deletion, since it'll eventually
|
// it. We don't worry about deletion, since it'll eventually
|
||||||
@ -2629,8 +2768,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
// shuts down.
|
// shuts down.
|
||||||
case *lnwire.ChannelUpdate:
|
case *lnwire.ChannelUpdate:
|
||||||
log.Debugf("Reprocessing ChannelUpdate for "+
|
log.Debugf("Reprocessing ChannelUpdate for "+
|
||||||
"shortChanID=%v",
|
"shortChanID=%v", scid.ToUint64())
|
||||||
msg.ShortChannelID.ToUint64())
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case d.networkMsgs <- updMsg:
|
case d.networkMsgs <- updMsg:
|
||||||
@ -2664,7 +2802,7 @@ func (d *AuthenticatedGossiper) handleChanAnnouncement(nMsg *networkMsg,
|
|||||||
nMsg.err <- nil
|
nMsg.err <- nil
|
||||||
|
|
||||||
log.Debugf("Processed ChannelAnnouncement: peer=%v, short_chan_id=%v",
|
log.Debugf("Processed ChannelAnnouncement: peer=%v, short_chan_id=%v",
|
||||||
nMsg.peer, ann.ShortChannelID.ToUint64())
|
nMsg.peer, scid.ToUint64())
|
||||||
|
|
||||||
return announcements, true
|
return announcements, true
|
||||||
}
|
}
|
||||||
@ -2745,6 +2883,22 @@ func (d *AuthenticatedGossiper) handleChanUpdate(nMsg *networkMsg,
|
|||||||
return nil, true
|
return nil, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Check that the ChanUpdate is not too far into the future, this could
|
||||||
|
// reveal some faulty implementation therefore we log an error.
|
||||||
|
if time.Until(timestamp) > graph.DefaultChannelPruneExpiry {
|
||||||
|
log.Errorf("Skewed timestamp (%v) for edge policy of "+
|
||||||
|
"short_chan_id(%v), timestamp too far in the future: "+
|
||||||
|
"peer=%v, msg=%s, is_remote=%v", timestamp.Unix(),
|
||||||
|
shortChanID, nMsg.peer, nMsg.msg.MsgType(),
|
||||||
|
nMsg.isRemote,
|
||||||
|
)
|
||||||
|
|
||||||
|
nMsg.err <- fmt.Errorf("skewed timestamp of edge policy, "+
|
||||||
|
"timestamp too far in the future: %v", timestamp.Unix())
|
||||||
|
|
||||||
|
return nil, false
|
||||||
|
}
|
||||||
|
|
||||||
// Get the node pub key as far since we don't have it in the channel
|
// Get the node pub key as far since we don't have it in the channel
|
||||||
// update announcement message. We'll need this to properly verify the
|
// update announcement message. We'll need this to properly verify the
|
||||||
// message's signature.
|
// message's signature.
|
||||||
@ -3373,3 +3527,36 @@ func (d *AuthenticatedGossiper) handleAnnSig(nMsg *networkMsg,
|
|||||||
nMsg.err <- nil
|
nMsg.err <- nil
|
||||||
return announcements, true
|
return announcements, true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// isBanned returns true if the peer identified by pubkey is banned for sending
|
||||||
|
// invalid channel announcements.
|
||||||
|
func (d *AuthenticatedGossiper) isBanned(pubkey [33]byte) bool {
|
||||||
|
return d.banman.isBanned(pubkey)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ShouldDisconnect returns true if we should disconnect the peer identified by
|
||||||
|
// pubkey.
|
||||||
|
func (d *AuthenticatedGossiper) ShouldDisconnect(pubkey *btcec.PublicKey) (
|
||||||
|
bool, error) {
|
||||||
|
|
||||||
|
pubkeySer := pubkey.SerializeCompressed()
|
||||||
|
|
||||||
|
var pubkeyBytes [33]byte
|
||||||
|
copy(pubkeyBytes[:], pubkeySer)
|
||||||
|
|
||||||
|
// If the public key is banned, check whether or not this is a channel
|
||||||
|
// peer.
|
||||||
|
if d.isBanned(pubkeyBytes) {
|
||||||
|
isChanPeer, err := d.cfg.ScidCloser.IsChannelPeer(pubkey)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// We should only disconnect non-channel peers.
|
||||||
|
if !isChanPeer {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -25,6 +25,7 @@ import (
|
|||||||
"github.com/lightningnetwork/lnd/chainntnfs"
|
"github.com/lightningnetwork/lnd/chainntnfs"
|
||||||
"github.com/lightningnetwork/lnd/channeldb"
|
"github.com/lightningnetwork/lnd/channeldb"
|
||||||
"github.com/lightningnetwork/lnd/channeldb/models"
|
"github.com/lightningnetwork/lnd/channeldb/models"
|
||||||
|
"github.com/lightningnetwork/lnd/fn"
|
||||||
"github.com/lightningnetwork/lnd/graph"
|
"github.com/lightningnetwork/lnd/graph"
|
||||||
"github.com/lightningnetwork/lnd/keychain"
|
"github.com/lightningnetwork/lnd/keychain"
|
||||||
"github.com/lightningnetwork/lnd/kvdb"
|
"github.com/lightningnetwork/lnd/kvdb"
|
||||||
@ -90,12 +91,13 @@ func makeTestDB(t *testing.T) (*channeldb.DB, error) {
|
|||||||
type mockGraphSource struct {
|
type mockGraphSource struct {
|
||||||
bestHeight uint32
|
bestHeight uint32
|
||||||
|
|
||||||
mu sync.Mutex
|
mu sync.Mutex
|
||||||
nodes []channeldb.LightningNode
|
nodes []channeldb.LightningNode
|
||||||
infos map[uint64]models.ChannelEdgeInfo
|
infos map[uint64]models.ChannelEdgeInfo
|
||||||
edges map[uint64][]models.ChannelEdgePolicy
|
edges map[uint64][]models.ChannelEdgePolicy
|
||||||
zombies map[uint64][][33]byte
|
zombies map[uint64][][33]byte
|
||||||
chansToReject map[uint64]struct{}
|
chansToReject map[uint64]struct{}
|
||||||
|
addEdgeErrCode fn.Option[graph.ErrorCode]
|
||||||
}
|
}
|
||||||
|
|
||||||
func newMockRouter(height uint32) *mockGraphSource {
|
func newMockRouter(height uint32) *mockGraphSource {
|
||||||
@ -126,6 +128,12 @@ func (r *mockGraphSource) AddEdge(info *models.ChannelEdgeInfo,
|
|||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
|
|
||||||
|
if r.addEdgeErrCode.IsSome() {
|
||||||
|
return graph.NewErrf(
|
||||||
|
r.addEdgeErrCode.UnsafeFromSome(), "received error",
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
if _, ok := r.infos[info.ChannelID]; ok {
|
if _, ok := r.infos[info.ChannelID]; ok {
|
||||||
return errors.New("info already exist")
|
return errors.New("info already exist")
|
||||||
}
|
}
|
||||||
@ -138,6 +146,14 @@ func (r *mockGraphSource) AddEdge(info *models.ChannelEdgeInfo,
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (r *mockGraphSource) resetAddEdgeErrCode() {
|
||||||
|
r.addEdgeErrCode = fn.None[graph.ErrorCode]()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *mockGraphSource) setAddEdgeErrCode(code graph.ErrorCode) {
|
||||||
|
r.addEdgeErrCode = fn.Some[graph.ErrorCode](code)
|
||||||
|
}
|
||||||
|
|
||||||
func (r *mockGraphSource) queueValidationFail(chanID uint64) {
|
func (r *mockGraphSource) queueValidationFail(chanID uint64) {
|
||||||
r.mu.Lock()
|
r.mu.Lock()
|
||||||
defer r.mu.Unlock()
|
defer r.mu.Unlock()
|
||||||
@ -707,7 +723,9 @@ type testCtx struct {
|
|||||||
broadcastedMessage chan msgWithSenders
|
broadcastedMessage chan msgWithSenders
|
||||||
}
|
}
|
||||||
|
|
||||||
func createTestCtx(t *testing.T, startHeight uint32) (*testCtx, error) {
|
func createTestCtx(t *testing.T, startHeight uint32, isChanPeer bool) (
|
||||||
|
*testCtx, error) {
|
||||||
|
|
||||||
// Next we'll initialize an instance of the channel router with mock
|
// Next we'll initialize an instance of the channel router with mock
|
||||||
// versions of the chain and channel notifier. As we don't need to test
|
// versions of the chain and channel notifier. As we don't need to test
|
||||||
// any p2p functionality, the peer send and switch send,
|
// any p2p functionality, the peer send and switch send,
|
||||||
@ -765,7 +783,7 @@ func createTestCtx(t *testing.T, startHeight uint32) (*testCtx, error) {
|
|||||||
peerChan chan<- lnpeer.Peer) {
|
peerChan chan<- lnpeer.Peer) {
|
||||||
|
|
||||||
pk, _ := btcec.ParsePubKey(target[:])
|
pk, _ := btcec.ParsePubKey(target[:])
|
||||||
peerChan <- &mockPeer{pk, nil, nil}
|
peerChan <- &mockPeer{pk, nil, nil, atomic.Bool{}}
|
||||||
},
|
},
|
||||||
NotifyWhenOffline: func(_ [33]byte) <-chan struct{} {
|
NotifyWhenOffline: func(_ [33]byte) <-chan struct{} {
|
||||||
c := make(chan struct{})
|
c := make(chan struct{})
|
||||||
@ -803,6 +821,7 @@ func createTestCtx(t *testing.T, startHeight uint32) (*testCtx, error) {
|
|||||||
FindBaseByAlias: findBaseByAlias,
|
FindBaseByAlias: findBaseByAlias,
|
||||||
GetAlias: getAlias,
|
GetAlias: getAlias,
|
||||||
FindChannel: mockFindChannel,
|
FindChannel: mockFindChannel,
|
||||||
|
ScidCloser: newMockScidCloser(isChanPeer),
|
||||||
}, selfKeyDesc)
|
}, selfKeyDesc)
|
||||||
|
|
||||||
if err := gossiper.Start(); err != nil {
|
if err := gossiper.Start(); err != nil {
|
||||||
@ -831,7 +850,7 @@ func TestProcessAnnouncement(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
timestamp := testTimestamp
|
timestamp := testTimestamp
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
assertSenderExistence := func(sender *btcec.PublicKey, msg msgWithSenders) {
|
assertSenderExistence := func(sender *btcec.PublicKey, msg msgWithSenders) {
|
||||||
@ -843,7 +862,7 @@ func TestProcessAnnouncement(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// First, we'll craft a valid remote channel announcement and send it to
|
// First, we'll craft a valid remote channel announcement and send it to
|
||||||
// the gossiper so that it can be processed.
|
// the gossiper so that it can be processed.
|
||||||
@ -947,13 +966,13 @@ func TestPrematureAnnouncement(t *testing.T) {
|
|||||||
|
|
||||||
timestamp := testTimestamp
|
timestamp := testTimestamp
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
_, err = createNodeAnnouncement(remoteKeyPriv1, timestamp)
|
_, err = createNodeAnnouncement(remoteKeyPriv1, timestamp)
|
||||||
require.NoError(t, err, "can't create node announcement")
|
require.NoError(t, err, "can't create node announcement")
|
||||||
|
|
||||||
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// Pretending that we receive the valid channel announcement from
|
// Pretending that we receive the valid channel announcement from
|
||||||
// remote side, but block height of this announcement is greater than
|
// remote side, but block height of this announcement is greater than
|
||||||
@ -978,7 +997,7 @@ func TestPrematureAnnouncement(t *testing.T) {
|
|||||||
func TestSignatureAnnouncementLocalFirst(t *testing.T) {
|
func TestSignatureAnnouncementLocalFirst(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
// Set up a channel that we can use to inspect the messages sent
|
// Set up a channel that we can use to inspect the messages sent
|
||||||
@ -990,7 +1009,9 @@ func TestSignatureAnnouncementLocalFirst(t *testing.T) {
|
|||||||
pk, _ := btcec.ParsePubKey(target[:])
|
pk, _ := btcec.ParsePubKey(target[:])
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case peerChan <- &mockPeer{pk, sentMsgs, ctx.gossiper.quit}:
|
case peerChan <- &mockPeer{
|
||||||
|
pk, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}:
|
||||||
case <-ctx.gossiper.quit:
|
case <-ctx.gossiper.quit:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1000,7 +1021,9 @@ func TestSignatureAnnouncementLocalFirst(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, sentMsgs, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Recreate lightning network topology. Initialize router with channel
|
// Recreate lightning network topology. Initialize router with channel
|
||||||
// between two nodes.
|
// between two nodes.
|
||||||
@ -1150,7 +1173,7 @@ func TestSignatureAnnouncementLocalFirst(t *testing.T) {
|
|||||||
func TestOrphanSignatureAnnouncement(t *testing.T) {
|
func TestOrphanSignatureAnnouncement(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
// Set up a channel that we can use to inspect the messages sent
|
// Set up a channel that we can use to inspect the messages sent
|
||||||
@ -1162,7 +1185,9 @@ func TestOrphanSignatureAnnouncement(t *testing.T) {
|
|||||||
pk, _ := btcec.ParsePubKey(target[:])
|
pk, _ := btcec.ParsePubKey(target[:])
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case peerChan <- &mockPeer{pk, sentMsgs, ctx.gossiper.quit}:
|
case peerChan <- &mockPeer{
|
||||||
|
pk, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}:
|
||||||
case <-ctx.gossiper.quit:
|
case <-ctx.gossiper.quit:
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1172,7 +1197,9 @@ func TestOrphanSignatureAnnouncement(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, sentMsgs, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Pretending that we receive local channel announcement from funding
|
// Pretending that we receive local channel announcement from funding
|
||||||
// manager, thereby kick off the announcement exchange process, in
|
// manager, thereby kick off the announcement exchange process, in
|
||||||
@ -1333,7 +1360,7 @@ func TestOrphanSignatureAnnouncement(t *testing.T) {
|
|||||||
func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {
|
func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -1344,7 +1371,9 @@ func TestSignatureAnnouncementRetryAtStartup(t *testing.T) {
|
|||||||
|
|
||||||
// Set up a channel to intercept the messages sent to the remote peer.
|
// Set up a channel to intercept the messages sent to the remote peer.
|
||||||
sentToPeer := make(chan lnwire.Message, 1)
|
sentToPeer := make(chan lnwire.Message, 1)
|
||||||
remotePeer := &mockPeer{remoteKey, sentToPeer, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentToPeer, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Since the reliable send to the remote peer of the local channel proof
|
// Since the reliable send to the remote peer of the local channel proof
|
||||||
// requires a notification when the peer comes online, we'll capture the
|
// requires a notification when the peer comes online, we'll capture the
|
||||||
@ -1566,7 +1595,7 @@ out:
|
|||||||
func TestSignatureAnnouncementFullProofWhenRemoteProof(t *testing.T) {
|
func TestSignatureAnnouncementFullProofWhenRemoteProof(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -1578,7 +1607,9 @@ func TestSignatureAnnouncementFullProofWhenRemoteProof(t *testing.T) {
|
|||||||
// Set up a channel we can use to inspect messages sent by the
|
// Set up a channel we can use to inspect messages sent by the
|
||||||
// gossiper to the remote peer.
|
// gossiper to the remote peer.
|
||||||
sentToPeer := make(chan lnwire.Message, 1)
|
sentToPeer := make(chan lnwire.Message, 1)
|
||||||
remotePeer := &mockPeer{remoteKey, sentToPeer, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentToPeer, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Override NotifyWhenOnline to return the remote peer which we expect
|
// Override NotifyWhenOnline to return the remote peer which we expect
|
||||||
// meesages to be sent to.
|
// meesages to be sent to.
|
||||||
@ -1772,7 +1803,7 @@ func TestDeDuplicatedAnnouncements(t *testing.T) {
|
|||||||
ca, err := createRemoteChannelAnnouncement(0)
|
ca, err := createRemoteChannelAnnouncement(0)
|
||||||
require.NoError(t, err, "can't create remote channel announcement")
|
require.NoError(t, err, "can't create remote channel announcement")
|
||||||
|
|
||||||
nodePeer := &mockPeer{bitcoinKeyPub2, nil, nil}
|
nodePeer := &mockPeer{bitcoinKeyPub2, nil, nil, atomic.Bool{}}
|
||||||
announcements.AddMsgs(networkMsg{
|
announcements.AddMsgs(networkMsg{
|
||||||
msg: ca,
|
msg: ca,
|
||||||
peer: nodePeer,
|
peer: nodePeer,
|
||||||
@ -2004,7 +2035,7 @@ func TestForwardPrivateNodeAnnouncement(t *testing.T) {
|
|||||||
timestamp = 123456
|
timestamp = 123456
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, startingHeight)
|
ctx, err := createTestCtx(t, startingHeight, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
// We'll start off by processing a channel announcement without a proof
|
// We'll start off by processing a channel announcement without a proof
|
||||||
@ -2058,7 +2089,7 @@ func TestForwardPrivateNodeAnnouncement(t *testing.T) {
|
|||||||
// process it.
|
// process it.
|
||||||
remoteChanAnn, err := createRemoteChannelAnnouncement(startingHeight - 1)
|
remoteChanAnn, err := createRemoteChannelAnnouncement(startingHeight - 1)
|
||||||
require.NoError(t, err, "unable to create remote channel announcement")
|
require.NoError(t, err, "unable to create remote channel announcement")
|
||||||
peer := &mockPeer{pubKey, nil, nil}
|
peer := &mockPeer{pubKey, nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(remoteChanAnn, peer):
|
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(remoteChanAnn, peer):
|
||||||
@ -2103,7 +2134,7 @@ func TestRejectZombieEdge(t *testing.T) {
|
|||||||
|
|
||||||
// We'll start by creating our test context with a batch of
|
// We'll start by creating our test context with a batch of
|
||||||
// announcements.
|
// announcements.
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "unable to create test context")
|
require.NoError(t, err, "unable to create test context")
|
||||||
|
|
||||||
batch, err := createRemoteAnnouncements(0)
|
batch, err := createRemoteAnnouncements(0)
|
||||||
@ -2204,7 +2235,7 @@ func TestProcessZombieEdgeNowLive(t *testing.T) {
|
|||||||
|
|
||||||
// We'll start by creating our test context with a batch of
|
// We'll start by creating our test context with a batch of
|
||||||
// announcements.
|
// announcements.
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "unable to create test context")
|
require.NoError(t, err, "unable to create test context")
|
||||||
|
|
||||||
batch, err := createRemoteAnnouncements(0)
|
batch, err := createRemoteAnnouncements(0)
|
||||||
@ -2361,7 +2392,7 @@ func TestProcessZombieEdgeNowLive(t *testing.T) {
|
|||||||
func TestReceiveRemoteChannelUpdateFirst(t *testing.T) {
|
func TestReceiveRemoteChannelUpdateFirst(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -2373,7 +2404,9 @@ func TestReceiveRemoteChannelUpdateFirst(t *testing.T) {
|
|||||||
// Set up a channel that we can use to inspect the messages sent
|
// Set up a channel that we can use to inspect the messages sent
|
||||||
// directly from the gossiper.
|
// directly from the gossiper.
|
||||||
sentMsgs := make(chan lnwire.Message, 10)
|
sentMsgs := make(chan lnwire.Message, 10)
|
||||||
remotePeer := &mockPeer{remoteKey, sentMsgs, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Override NotifyWhenOnline to return the remote peer which we expect
|
// Override NotifyWhenOnline to return the remote peer which we expect
|
||||||
// messages to be sent to.
|
// messages to be sent to.
|
||||||
@ -2558,10 +2591,12 @@ func TestReceiveRemoteChannelUpdateFirst(t *testing.T) {
|
|||||||
func TestExtraDataChannelAnnouncementValidation(t *testing.T) {
|
func TestExtraDataChannelAnnouncementValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
remotePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// We'll now create an announcement that contains an extra set of bytes
|
// We'll now create an announcement that contains an extra set of bytes
|
||||||
// that we don't know of ourselves, but should still include in the
|
// that we don't know of ourselves, but should still include in the
|
||||||
@ -2589,10 +2624,12 @@ func TestExtraDataChannelUpdateValidation(t *testing.T) {
|
|||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
timestamp := testTimestamp
|
timestamp := testTimestamp
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
remotePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// In this scenario, we'll create two announcements, one regular
|
// In this scenario, we'll create two announcements, one regular
|
||||||
// channel announcement, and another channel update announcement, that
|
// channel announcement, and another channel update announcement, that
|
||||||
@ -2640,10 +2677,12 @@ func TestExtraDataChannelUpdateValidation(t *testing.T) {
|
|||||||
func TestExtraDataNodeAnnouncementValidation(t *testing.T) {
|
func TestExtraDataNodeAnnouncementValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
remotePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
timestamp := testTimestamp
|
timestamp := testTimestamp
|
||||||
|
|
||||||
// We'll create a node announcement that includes a set of opaque data
|
// We'll create a node announcement that includes a set of opaque data
|
||||||
@ -2708,7 +2747,7 @@ func assertProcessAnnouncement(t *testing.T, result chan error) {
|
|||||||
func TestRetransmit(t *testing.T) {
|
func TestRetransmit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -2716,7 +2755,7 @@ func TestRetransmit(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, nil, nil}
|
remotePeer := &mockPeer{remoteKey, nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// Process a local channel announcement, channel update and node
|
// Process a local channel announcement, channel update and node
|
||||||
// announcement. No messages should be broadcasted yet, since no proof
|
// announcement. No messages should be broadcasted yet, since no proof
|
||||||
@ -2814,7 +2853,7 @@ func TestRetransmit(t *testing.T) {
|
|||||||
func TestNodeAnnouncementNoChannels(t *testing.T) {
|
func TestNodeAnnouncementNoChannels(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createRemoteAnnouncements(0)
|
batch, err := createRemoteAnnouncements(0)
|
||||||
@ -2822,7 +2861,7 @@ func TestNodeAnnouncementNoChannels(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, nil, nil}
|
remotePeer := &mockPeer{remoteKey, nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// Process the remote node announcement.
|
// Process the remote node announcement.
|
||||||
select {
|
select {
|
||||||
@ -2899,14 +2938,14 @@ func TestNodeAnnouncementNoChannels(t *testing.T) {
|
|||||||
func TestOptionalFieldsChannelUpdateValidation(t *testing.T) {
|
func TestOptionalFieldsChannelUpdateValidation(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
processRemoteAnnouncement := ctx.gossiper.ProcessRemoteAnnouncement
|
processRemoteAnnouncement := ctx.gossiper.ProcessRemoteAnnouncement
|
||||||
|
|
||||||
chanUpdateHeight := uint32(0)
|
chanUpdateHeight := uint32(0)
|
||||||
timestamp := uint32(123456)
|
timestamp := uint32(123456)
|
||||||
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// In this scenario, we'll test whether the message flags field in a
|
// In this scenario, we'll test whether the message flags field in a
|
||||||
// channel update is properly handled.
|
// channel update is properly handled.
|
||||||
@ -2998,7 +3037,7 @@ func TestSendChannelUpdateReliably(t *testing.T) {
|
|||||||
|
|
||||||
// We'll start by creating our test context and a batch of
|
// We'll start by creating our test context and a batch of
|
||||||
// announcements.
|
// announcements.
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "unable to create test context")
|
require.NoError(t, err, "unable to create test context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -3013,7 +3052,9 @@ func TestSendChannelUpdateReliably(t *testing.T) {
|
|||||||
// Set up a channel we can use to inspect messages sent by the
|
// Set up a channel we can use to inspect messages sent by the
|
||||||
// gossiper to the remote peer.
|
// gossiper to the remote peer.
|
||||||
sentToPeer := make(chan lnwire.Message, 1)
|
sentToPeer := make(chan lnwire.Message, 1)
|
||||||
remotePeer := &mockPeer{remoteKey, sentToPeer, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentToPeer, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// Since we first wait to be notified of the peer before attempting to
|
// Since we first wait to be notified of the peer before attempting to
|
||||||
// send the message, we'll overwrite NotifyWhenOnline and
|
// send the message, we'll overwrite NotifyWhenOnline and
|
||||||
@ -3350,7 +3391,7 @@ func TestPropagateChanPolicyUpdate(t *testing.T) {
|
|||||||
// First, we'll make out test context and add 3 random channels to the
|
// First, we'll make out test context and add 3 random channels to the
|
||||||
// graph.
|
// graph.
|
||||||
startingHeight := uint32(10)
|
startingHeight := uint32(10)
|
||||||
ctx, err := createTestCtx(t, startingHeight)
|
ctx, err := createTestCtx(t, startingHeight, false)
|
||||||
require.NoError(t, err, "unable to create test context")
|
require.NoError(t, err, "unable to create test context")
|
||||||
|
|
||||||
const numChannels = 3
|
const numChannels = 3
|
||||||
@ -3367,7 +3408,9 @@ func TestPropagateChanPolicyUpdate(t *testing.T) {
|
|||||||
remoteKey := remoteKeyPriv1.PubKey()
|
remoteKey := remoteKeyPriv1.PubKey()
|
||||||
|
|
||||||
sentMsgs := make(chan lnwire.Message, 10)
|
sentMsgs := make(chan lnwire.Message, 10)
|
||||||
remotePeer := &mockPeer{remoteKey, sentMsgs, ctx.gossiper.quit}
|
remotePeer := &mockPeer{
|
||||||
|
remoteKey, sentMsgs, ctx.gossiper.quit, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
// The forced code path for sending the private ChannelUpdate to the
|
// The forced code path for sending the private ChannelUpdate to the
|
||||||
// remote peer will be hit, forcing it to request a notification that
|
// remote peer will be hit, forcing it to request a notification that
|
||||||
@ -3529,7 +3572,7 @@ func TestProcessChannelAnnouncementOptionalMsgFields(t *testing.T) {
|
|||||||
|
|
||||||
// We'll start by creating our test context and a set of test channel
|
// We'll start by creating our test context and a set of test channel
|
||||||
// announcements.
|
// announcements.
|
||||||
ctx, err := createTestCtx(t, 0)
|
ctx, err := createTestCtx(t, 0, false)
|
||||||
require.NoError(t, err, "unable to create test context")
|
require.NoError(t, err, "unable to create test context")
|
||||||
|
|
||||||
chanAnn1 := createAnnouncementWithoutProof(
|
chanAnn1 := createAnnouncementWithoutProof(
|
||||||
@ -3590,7 +3633,7 @@ func assertMessage(t *testing.T, expected, got lnwire.Message) {
|
|||||||
func TestSplitAnnouncementsCorrectSubBatches(t *testing.T) {
|
func TestSplitAnnouncementsCorrectSubBatches(t *testing.T) {
|
||||||
// Create our test harness.
|
// Create our test harness.
|
||||||
const blockHeight = 100
|
const blockHeight = 100
|
||||||
ctx, err := createTestCtx(t, blockHeight)
|
ctx, err := createTestCtx(t, blockHeight, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
const subBatchSize = 10
|
const subBatchSize = 10
|
||||||
@ -3702,7 +3745,7 @@ func (m *SyncManager) markGraphSyncing() {
|
|||||||
func TestBroadcastAnnsAfterGraphSynced(t *testing.T) {
|
func TestBroadcastAnnsAfterGraphSynced(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, 10)
|
ctx, err := createTestCtx(t, 10, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
// We'll mark the graph as not synced. This should prevent us from
|
// We'll mark the graph as not synced. This should prevent us from
|
||||||
@ -3715,7 +3758,9 @@ func TestBroadcastAnnsAfterGraphSynced(t *testing.T) {
|
|||||||
|
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
nodePeer := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
var errChan chan error
|
var errChan chan error
|
||||||
if isRemote {
|
if isRemote {
|
||||||
errChan = ctx.gossiper.ProcessRemoteAnnouncement(
|
errChan = ctx.gossiper.ProcessRemoteAnnouncement(
|
||||||
@ -3775,7 +3820,7 @@ func TestRateLimitChannelUpdates(t *testing.T) {
|
|||||||
|
|
||||||
// Create our test harness.
|
// Create our test harness.
|
||||||
const blockHeight = 100
|
const blockHeight = 100
|
||||||
ctx, err := createTestCtx(t, blockHeight)
|
ctx, err := createTestCtx(t, blockHeight, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
ctx.gossiper.cfg.RebroadcastInterval = time.Hour
|
ctx.gossiper.cfg.RebroadcastInterval = time.Hour
|
||||||
ctx.gossiper.cfg.MaxChannelUpdateBurst = 5
|
ctx.gossiper.cfg.MaxChannelUpdateBurst = 5
|
||||||
@ -3791,7 +3836,9 @@ func TestRateLimitChannelUpdates(t *testing.T) {
|
|||||||
batch, err := createRemoteAnnouncements(blockHeight)
|
batch, err := createRemoteAnnouncements(blockHeight)
|
||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
|
|
||||||
nodePeer1 := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil}
|
nodePeer1 := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(
|
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(
|
||||||
batch.chanAnn, nodePeer1,
|
batch.chanAnn, nodePeer1,
|
||||||
@ -3810,7 +3857,9 @@ func TestRateLimitChannelUpdates(t *testing.T) {
|
|||||||
t.Fatal("remote announcement not processed")
|
t.Fatal("remote announcement not processed")
|
||||||
}
|
}
|
||||||
|
|
||||||
nodePeer2 := &mockPeer{remoteKeyPriv2.PubKey(), nil, nil}
|
nodePeer2 := &mockPeer{
|
||||||
|
remoteKeyPriv2.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
select {
|
select {
|
||||||
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(
|
case err := <-ctx.gossiper.ProcessRemoteAnnouncement(
|
||||||
batch.chanUpdAnn2, nodePeer2,
|
batch.chanUpdAnn2, nodePeer2,
|
||||||
@ -3921,7 +3970,7 @@ func TestRateLimitChannelUpdates(t *testing.T) {
|
|||||||
func TestIgnoreOwnAnnouncement(t *testing.T) {
|
func TestIgnoreOwnAnnouncement(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
batch, err := createLocalAnnouncements(0)
|
batch, err := createLocalAnnouncements(0)
|
||||||
@ -3929,7 +3978,7 @@ func TestIgnoreOwnAnnouncement(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, nil, nil}
|
remotePeer := &mockPeer{remoteKey, nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// Try to let the remote peer tell us about the channel we are part of.
|
// Try to let the remote peer tell us about the channel we are part of.
|
||||||
select {
|
select {
|
||||||
@ -4065,7 +4114,7 @@ func TestIgnoreOwnAnnouncement(t *testing.T) {
|
|||||||
func TestRejectCacheChannelAnn(t *testing.T) {
|
func TestRejectCacheChannelAnn(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
ctx, err := createTestCtx(t, proofMatureDelta)
|
ctx, err := createTestCtx(t, proofMatureDelta, false)
|
||||||
require.NoError(t, err, "can't create context")
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
// First, we create a channel announcement to send over to our test
|
// First, we create a channel announcement to send over to our test
|
||||||
@ -4075,7 +4124,7 @@ func TestRejectCacheChannelAnn(t *testing.T) {
|
|||||||
|
|
||||||
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
remoteKey, err := btcec.ParsePubKey(batch.nodeAnn2.NodeID[:])
|
||||||
require.NoError(t, err, "unable to parse pubkey")
|
require.NoError(t, err, "unable to parse pubkey")
|
||||||
remotePeer := &mockPeer{remoteKey, nil, nil}
|
remotePeer := &mockPeer{remoteKey, nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
// Before sending over the announcement, we'll modify it such that we
|
// Before sending over the announcement, we'll modify it such that we
|
||||||
// know it will always fail.
|
// know it will always fail.
|
||||||
@ -4139,3 +4188,134 @@ func TestFutureMsgCacheEviction(t *testing.T) {
|
|||||||
require.NoError(t, err)
|
require.NoError(t, err)
|
||||||
require.EqualValues(t, 2, item.height, "should be the second item")
|
require.EqualValues(t, 2, item.height, "should be the second item")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TestChanAnnBanningNonChanPeer asserts that non-channel peers who send bogus
|
||||||
|
// channel announcements are banned properly.
|
||||||
|
func TestChanAnnBanningNonChanPeer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, err := createTestCtx(t, 1000, false)
|
||||||
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
|
nodePeer1 := &mockPeer{
|
||||||
|
remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
|
nodePeer2 := &mockPeer{
|
||||||
|
remoteKeyPriv2.PubKey(), nil, nil, atomic.Bool{},
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.router.setAddEdgeErrCode(graph.ErrInvalidFundingOutput)
|
||||||
|
|
||||||
|
// Loop 100 times to get nodePeer banned.
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
// Craft a valid channel announcement for a channel we don't
|
||||||
|
// have. We will ensure that it fails validation by modifying
|
||||||
|
// the router.
|
||||||
|
ca, err := createRemoteChannelAnnouncement(uint32(i))
|
||||||
|
require.NoError(t, err, "can't create channel announcement")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-ctx.gossiper.ProcessRemoteAnnouncement(
|
||||||
|
ca, nodePeer1,
|
||||||
|
):
|
||||||
|
require.True(
|
||||||
|
t, graph.IsError(
|
||||||
|
err, graph.ErrInvalidFundingOutput,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("remote announcement not processed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer should be banned now.
|
||||||
|
require.True(t, ctx.gossiper.isBanned(nodePeer1.PubKey()))
|
||||||
|
|
||||||
|
// Assert that nodePeer has been disconnected.
|
||||||
|
require.True(t, nodePeer1.disconnected.Load())
|
||||||
|
|
||||||
|
ca, err := createRemoteChannelAnnouncement(101)
|
||||||
|
require.NoError(t, err, "can't create channel announcement")
|
||||||
|
|
||||||
|
// Set the error to ErrChannelSpent so that we can test that the
|
||||||
|
// gossiper ignores closed channels.
|
||||||
|
ctx.router.setAddEdgeErrCode(graph.ErrChannelSpent)
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-ctx.gossiper.ProcessRemoteAnnouncement(ca, nodePeer2):
|
||||||
|
require.True(t, graph.IsError(err, graph.ErrChannelSpent))
|
||||||
|
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("remote announcement not processed")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check that the announcement's scid is marked as closed.
|
||||||
|
isClosed, err := ctx.gossiper.cfg.ScidCloser.IsClosedScid(
|
||||||
|
ca.ShortChannelID,
|
||||||
|
)
|
||||||
|
require.Nil(t, err)
|
||||||
|
require.True(t, isClosed)
|
||||||
|
|
||||||
|
// Remove the scid from the reject cache.
|
||||||
|
key := newRejectCacheKey(
|
||||||
|
ca.ShortChannelID.ToUint64(),
|
||||||
|
sourceToPub(nodePeer2.IdentityKey()),
|
||||||
|
)
|
||||||
|
|
||||||
|
ctx.gossiper.recentRejects.Delete(key)
|
||||||
|
|
||||||
|
// Reset the AddEdge error and pass the same announcement again. An
|
||||||
|
// error should be returned even though AddEdge won't fail.
|
||||||
|
ctx.router.resetAddEdgeErrCode()
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-ctx.gossiper.ProcessRemoteAnnouncement(ca, nodePeer2):
|
||||||
|
require.NotNil(t, err)
|
||||||
|
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("remote announcement not processed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestChanAnnBanningChanPeer asserts that channel peers that are banned don't
|
||||||
|
// get disconnected.
|
||||||
|
func TestChanAnnBanningChanPeer(t *testing.T) {
|
||||||
|
t.Parallel()
|
||||||
|
|
||||||
|
ctx, err := createTestCtx(t, 1000, true)
|
||||||
|
require.NoError(t, err, "can't create context")
|
||||||
|
|
||||||
|
nodePeer := &mockPeer{remoteKeyPriv1.PubKey(), nil, nil, atomic.Bool{}}
|
||||||
|
|
||||||
|
ctx.router.setAddEdgeErrCode(graph.ErrInvalidFundingOutput)
|
||||||
|
|
||||||
|
// Loop 100 times to get nodePeer banned.
|
||||||
|
for i := 0; i < 100; i++ {
|
||||||
|
// Craft a valid channel announcement for a channel we don't
|
||||||
|
// have. We will ensure that it fails validation by modifying
|
||||||
|
// the router.
|
||||||
|
ca, err := createRemoteChannelAnnouncement(uint32(i))
|
||||||
|
require.NoError(t, err, "can't create channel announcement")
|
||||||
|
|
||||||
|
select {
|
||||||
|
case err = <-ctx.gossiper.ProcessRemoteAnnouncement(
|
||||||
|
ca, nodePeer,
|
||||||
|
):
|
||||||
|
require.True(
|
||||||
|
t, graph.IsError(
|
||||||
|
err, graph.ErrInvalidFundingOutput,
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
case <-time.After(2 * time.Second):
|
||||||
|
t.Fatalf("remote announcement not processed")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// The peer should be banned now.
|
||||||
|
require.True(t, ctx.gossiper.isBanned(nodePeer.PubKey()))
|
||||||
|
|
||||||
|
// Assert that the peer wasn't disconnected.
|
||||||
|
require.False(t, nodePeer.disconnected.Load())
|
||||||
|
}
|
||||||
|
|||||||
@ -4,6 +4,7 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
|
"sync/atomic"
|
||||||
|
|
||||||
"github.com/btcsuite/btcd/btcec/v2"
|
"github.com/btcsuite/btcd/btcec/v2"
|
||||||
"github.com/btcsuite/btcd/wire"
|
"github.com/btcsuite/btcd/wire"
|
||||||
@ -14,9 +15,10 @@ import (
|
|||||||
// mockPeer implements the lnpeer.Peer interface and is used to test the
|
// mockPeer implements the lnpeer.Peer interface and is used to test the
|
||||||
// gossiper's interaction with peers.
|
// gossiper's interaction with peers.
|
||||||
type mockPeer struct {
|
type mockPeer struct {
|
||||||
pk *btcec.PublicKey
|
pk *btcec.PublicKey
|
||||||
sentMsgs chan lnwire.Message
|
sentMsgs chan lnwire.Message
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
|
disconnected atomic.Bool
|
||||||
}
|
}
|
||||||
|
|
||||||
var _ lnpeer.Peer = (*mockPeer)(nil)
|
var _ lnpeer.Peer = (*mockPeer)(nil)
|
||||||
@ -74,6 +76,10 @@ func (p *mockPeer) RemovePendingChannel(_ lnwire.ChannelID) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (p *mockPeer) Disconnect(err error) {
|
||||||
|
p.disconnected.Store(true)
|
||||||
|
}
|
||||||
|
|
||||||
// mockMessageStore is an in-memory implementation of the MessageStore interface
|
// mockMessageStore is an in-memory implementation of the MessageStore interface
|
||||||
// used for the gossiper's unit tests.
|
// used for the gossiper's unit tests.
|
||||||
type mockMessageStore struct {
|
type mockMessageStore struct {
|
||||||
@ -155,3 +161,40 @@ func (s *mockMessageStore) MessagesForPeer(pubKey [33]byte) ([]lnwire.Message, e
|
|||||||
|
|
||||||
return msgs, nil
|
return msgs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type mockScidCloser struct {
|
||||||
|
m map[lnwire.ShortChannelID]struct{}
|
||||||
|
channelPeer bool
|
||||||
|
|
||||||
|
sync.Mutex
|
||||||
|
}
|
||||||
|
|
||||||
|
func newMockScidCloser(channelPeer bool) *mockScidCloser {
|
||||||
|
return &mockScidCloser{
|
||||||
|
m: make(map[lnwire.ShortChannelID]struct{}),
|
||||||
|
channelPeer: channelPeer,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockScidCloser) PutClosedScid(scid lnwire.ShortChannelID) error {
|
||||||
|
m.Lock()
|
||||||
|
m.m[scid] = struct{}{}
|
||||||
|
m.Unlock()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockScidCloser) IsClosedScid(scid lnwire.ShortChannelID) (bool,
|
||||||
|
error) {
|
||||||
|
|
||||||
|
m.Lock()
|
||||||
|
defer m.Unlock()
|
||||||
|
|
||||||
|
_, ok := m.m[scid]
|
||||||
|
|
||||||
|
return ok, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (m *mockScidCloser) IsChannelPeer(pubkey *btcec.PublicKey) (bool, error) {
|
||||||
|
return m.channelPeer, nil
|
||||||
|
}
|
||||||
|
|||||||
@ -2,6 +2,7 @@ package discovery
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"sync/atomic"
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -74,7 +75,7 @@ func TestReliableSenderFlow(t *testing.T) {
|
|||||||
// Create a mock peer to send the messages to.
|
// Create a mock peer to send the messages to.
|
||||||
pubKey := randPubKey(t)
|
pubKey := randPubKey(t)
|
||||||
msgsSent := make(chan lnwire.Message)
|
msgsSent := make(chan lnwire.Message)
|
||||||
peer := &mockPeer{pubKey, msgsSent, reliableSender.quit}
|
peer := &mockPeer{pubKey, msgsSent, reliableSender.quit, atomic.Bool{}}
|
||||||
|
|
||||||
// Override NotifyWhenOnline and NotifyWhenOffline to provide the
|
// Override NotifyWhenOnline and NotifyWhenOffline to provide the
|
||||||
// notification channels so that we can control when notifications get
|
// notification channels so that we can control when notifications get
|
||||||
@ -193,7 +194,7 @@ func TestReliableSenderStaleMessages(t *testing.T) {
|
|||||||
// Create a mock peer to send the messages to.
|
// Create a mock peer to send the messages to.
|
||||||
pubKey := randPubKey(t)
|
pubKey := randPubKey(t)
|
||||||
msgsSent := make(chan lnwire.Message)
|
msgsSent := make(chan lnwire.Message)
|
||||||
peer := &mockPeer{pubKey, msgsSent, reliableSender.quit}
|
peer := &mockPeer{pubKey, msgsSent, reliableSender.quit, atomic.Bool{}}
|
||||||
|
|
||||||
// Override NotifyWhenOnline to provide the notification channel so that
|
// Override NotifyWhenOnline to provide the notification channel so that
|
||||||
// we can control when notifications get dispatched.
|
// we can control when notifications get dispatched.
|
||||||
|
|||||||
@ -22,6 +22,9 @@ const (
|
|||||||
// force a historical sync to ensure we have as much of the public
|
// force a historical sync to ensure we have as much of the public
|
||||||
// network as possible.
|
// network as possible.
|
||||||
DefaultHistoricalSyncInterval = time.Hour
|
DefaultHistoricalSyncInterval = time.Hour
|
||||||
|
|
||||||
|
// filterSemaSize is the capacity of gossipFilterSema.
|
||||||
|
filterSemaSize = 5
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
@ -161,12 +164,22 @@ type SyncManager struct {
|
|||||||
// duration of the connection.
|
// duration of the connection.
|
||||||
pinnedActiveSyncers map[route.Vertex]*GossipSyncer
|
pinnedActiveSyncers map[route.Vertex]*GossipSyncer
|
||||||
|
|
||||||
|
// gossipFilterSema contains semaphores for the gossip timestamp
|
||||||
|
// queries.
|
||||||
|
gossipFilterSema chan struct{}
|
||||||
|
|
||||||
wg sync.WaitGroup
|
wg sync.WaitGroup
|
||||||
quit chan struct{}
|
quit chan struct{}
|
||||||
}
|
}
|
||||||
|
|
||||||
// newSyncManager constructs a new SyncManager backed by the given config.
|
// newSyncManager constructs a new SyncManager backed by the given config.
|
||||||
func newSyncManager(cfg *SyncManagerCfg) *SyncManager {
|
func newSyncManager(cfg *SyncManagerCfg) *SyncManager {
|
||||||
|
|
||||||
|
filterSema := make(chan struct{}, filterSemaSize)
|
||||||
|
for i := 0; i < filterSemaSize; i++ {
|
||||||
|
filterSema <- struct{}{}
|
||||||
|
}
|
||||||
|
|
||||||
return &SyncManager{
|
return &SyncManager{
|
||||||
cfg: *cfg,
|
cfg: *cfg,
|
||||||
newSyncers: make(chan *newSyncer),
|
newSyncers: make(chan *newSyncer),
|
||||||
@ -178,7 +191,8 @@ func newSyncManager(cfg *SyncManagerCfg) *SyncManager {
|
|||||||
pinnedActiveSyncers: make(
|
pinnedActiveSyncers: make(
|
||||||
map[route.Vertex]*GossipSyncer, len(cfg.PinnedSyncers),
|
map[route.Vertex]*GossipSyncer, len(cfg.PinnedSyncers),
|
||||||
),
|
),
|
||||||
quit: make(chan struct{}),
|
gossipFilterSema: filterSema,
|
||||||
|
quit: make(chan struct{}),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -507,7 +521,7 @@ func (m *SyncManager) createGossipSyncer(peer lnpeer.Peer) *GossipSyncer {
|
|||||||
maxQueryChanRangeReplies: maxQueryChanRangeReplies,
|
maxQueryChanRangeReplies: maxQueryChanRangeReplies,
|
||||||
noTimestampQueryOption: m.cfg.NoTimestampQueries,
|
noTimestampQueryOption: m.cfg.NoTimestampQueries,
|
||||||
isStillZombieChannel: m.cfg.IsStillZombieChannel,
|
isStillZombieChannel: m.cfg.IsStillZombieChannel,
|
||||||
})
|
}, m.gossipFilterSema)
|
||||||
|
|
||||||
// Gossip syncers are initialized by default in a PassiveSync type
|
// Gossip syncers are initialized by default in a PassiveSync type
|
||||||
// and chansSynced state so that they can reply to any peer queries or
|
// and chansSynced state so that they can reply to any peer queries or
|
||||||
@ -561,7 +575,7 @@ func (m *SyncManager) removeGossipSyncer(peer route.Vertex) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Debugf("Replaced active GossipSyncer(%x) with GossipSyncer(%x)",
|
log.Debugf("Replaced active GossipSyncer(%v) with GossipSyncer(%x)",
|
||||||
peer, newActiveSyncer.cfg.peerPub)
|
peer, newActiveSyncer.cfg.peerPub)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user