From 1447b842536774fbd5377057a9b9e122098b6a78 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 1 Apr 2021 08:24:12 +0200 Subject: [PATCH 1/4] Remove Electrum support Electrum support was provided for mobile wallets, server nodes should always run a bitcoind node as this provides more control (especially for utxo management for anchor outputs channels). Since wallets will use https://github.com/acinq/eclair-kmp instead of eclair, we can now remove Electrum and API fee providers from eclair. --- .../electrum/checkpoints_mainnet.json | 1230 ------ .../electrum/checkpoints_testnet.json | 3306 ----------------- .../resources/electrum/servers_mainnet.json | 108 - .../resources/electrum/servers_regtest.json | 10 - .../resources/electrum/servers_testnet.json | 38 - eclair-core/src/main/resources/reference.conf | 3 +- .../scala/fr/acinq/eclair/NodeParams.scala | 13 +- .../main/scala/fr/acinq/eclair/Setup.scala | 164 +- .../eclair/blockchain/WatcherTypes.scala | 6 +- .../blockchain/electrum/Blockchain.scala | 352 -- .../blockchain/electrum/CheckPoint.scala | 79 - .../blockchain/electrum/ElectrumClient.scala | 665 ---- .../electrum/ElectrumClientPool.scala | 241 -- .../electrum/ElectrumEclairWallet.scala | 100 - .../blockchain/electrum/ElectrumWallet.scala | 1090 ------ .../blockchain/electrum/ElectrumWatcher.scala | 246 -- .../blockchain/electrum/db/WalletDb.scala | 41 - .../electrum/db/sqlite/SqliteWalletDb.scala | 218 -- .../blockchain/fee/BitgoFeeProvider.scala | 82 - .../fee/EarnDotComFeeProvider.scala | 87 - .../blockchain/electrum/CheckPointSpec.scala | 27 - .../electrum/ElectrumClientPoolSpec.scala | 119 - .../electrum/ElectrumClientSpec.scala | 132 - .../electrum/ElectrumWalletBasicSpec.scala | 233 -- .../ElectrumWalletSimulatedClientSpec.scala | 412 -- .../electrum/ElectrumWalletSpec.scala | 388 -- .../electrum/ElectrumWatcherSpec.scala | 270 -- .../electrum/ElectrumxService.scala | 60 - .../db/sqlite/SqliteWalletDbSpec.scala | 129 - .../blockchain/fee/BitgoFeeProviderSpec.scala | 98 - .../fee/EarnDotComFeeProviderSpec.scala | 95 - .../scala/fr/acinq/eclair/gui/FxApp.scala | 2 - .../fr/acinq/eclair/gui/GUIUpdater.scala | 8 - .../gui/controllers/MainController.scala | 3 +- 34 files changed, 66 insertions(+), 9989 deletions(-) delete mode 100644 eclair-core/src/main/resources/electrum/checkpoints_mainnet.json delete mode 100644 eclair-core/src/main/resources/electrum/checkpoints_testnet.json delete mode 100644 eclair-core/src/main/resources/electrum/servers_mainnet.json delete mode 100644 eclair-core/src/main/resources/electrum/servers_regtest.json delete mode 100644 eclair-core/src/main/resources/electrum/servers_testnet.json delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/Blockchain.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/CheckPoint.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala delete mode 100644 eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/CheckPointSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala delete mode 100644 eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala diff --git a/eclair-core/src/main/resources/electrum/checkpoints_mainnet.json b/eclair-core/src/main/resources/electrum/checkpoints_mainnet.json deleted file mode 100644 index 960e071cfb..0000000000 --- a/eclair-core/src/main/resources/electrum/checkpoints_mainnet.json +++ /dev/null @@ -1,1230 +0,0 @@ -[ - [ - "00000000693067b0e6b440bc51450b9f3850561b07f6d3c021c54fbd6abb9763", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000f037ad09d0b05ee66b8c1da83030abaf909d2b1bf519c3c7d2cd3fdf", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000006ce8b5f16fcedde13acbc9641baa1c67734f177d770a4069c06c9de8", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000563298de120522b5ae17da21aaae02eee2d7fcb5be65d9224dbd601c", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000009b0a4b2833b4a0aa61171ee75b8eb301ac45a18713795a72e461a946", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000fa8a7363e8f6fdc88ec55edf264c9c7b31268c26e497a4587c750584", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000008ac55b5cd76a5c176f2457f0e9df5ff1c719d939f1022712b1ba2092", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000007f0c796631f00f542c0b402d638d3518bc208f8c9e5d29d2f169c084", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000ffb062296c9d4eb5f87bbf905d30669d26eab6bced341bd3f1dba5fd", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "0000000074c108842c3ec2252bba62db4050bf0dddfee3ddaa5f847076b8822f", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "0000000067dc2f84a73fbf5d3c70678ce4a1496ef3a62c557bc79cbdd1d49f22", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000dbf06f47c0624262ecb197bccf6bdaaabc2d973708ac401ac8955acc", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000009260fe30ec89ef367122f429dcc59f61735760f2b2288f2e854f04ac", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000f9f1a700898c4e0671af6efd441eaf339ba075a5c5c7b0949473c80b", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "000000005107662c86452e7365f32f8ffdc70d8d87aa6f78630a79f7d77fbfe6", - 26959535291011309493156476344723991336010898738574164086137773096960 - ], - [ - "00000000984f962134a7291e3693075ae03e521f0ee33378ec30a334d860034b", - 22791060871177364286867400663010583169263383106957897897309909286912 - ], - [ - "000000005e36047e39452a7beaaa6721048ac408a3e75bb60a8b0008713653ce", - 20657664212610420653213483117824978239553266057163961604478437687296 - ], - [ - "00000000128d789579ffbec00203a371cbb39cee27df35d951fd66e62ed59258", - 20055820920770189543295303139304627292355830414308479769458683936768 - ], - [ - "000000008dde642fb80481bb5e1671cb04c6716de5b7f783aa3388456d5c8a85", - 14823939180767414932263578623363531361763221729526512593941781544960 - ], - [ - "000000008135b689ad1557d4e148a8b9e58e2c4a67240fc87962abb69710231a", - 10665477591887247494381404907447500979192021944764506987270680608768 - ], - [ - "00000000308496ef3e4f9fa542a772df637b4aaf1dcce404424611feacfc09e7", - 7129927859545590787920041835044506526699926406309469412482969763840 - ], - [ - "000000001a2e0c63d7d012003c9173acfd04ccd6372027718979228c461b5ed5", - 5949911473257063494842414979623989957440207170696926280907451531264 - ], - [ - "000000002e0c0ac26ccde91b51ab018576b3a126b413e9f6f787b36637f1b174", - 5905492491837656485645884063467495540781288435542782321354050895872 - ], - [ - "00000000103226f85fe2b68795f087dcec345e523363f18017e60b5c94175355", - 4430143390146946405787502162943966061454423600514874825749833973760 - ], - [ - "000000001ae6f66fd4de47f8d6f357e798943bbfc4f39ebf14b0975fab059173", - 3447600406241317909690675945127070282093452846402311540118831235072 - ], - [ - "000000000a3f22690162744d3bc0b674c92e661a25afb3d2ac8b39b27ac14373", - 2351604382534916182160036119666703740669209516522695514729880748032 - ], - [ - "0000000006dc436c3c515a97446af858c1203a501c85d26c4a30afa380aba4a1", - 2098151686442211199940455690614286210348997571531298297574806519808 - ], - [ - "000000000943fe1680ffcc498ce50790ff8e842a8af2c157664e4fbc1cb7cb46", - 2275790652544821279950241890112140030244814501479017131553197129728 - ], - [ - "000000000847b2144376c1fb057ea1d5a027d5a6004277ed4c72422e93df04e9", - 1622203955679450683159610732218403647246163922223729367236739072000 - ], - [ - "00000000094505954deb1d31382b86d0510fd280a34143400b1856a4d52b4c93", - 1551048739079662593758612650769536967206480773659027300489594142720 - ], - [ - "000000000109272cecb3f7e98ac12cf149fa8a1b2aaab248e1b006b0dc595a3a", - 1389323280429349294447518501872137680563441219958739463959193059328 - ], - [ - "0000000009e6aa0fe39b790625ffeb18a2d6ff5060a5bd14e699e83c54109977", - 1147152896345386682952518188670047452875537662186691235300769792000 - ], - [ - "0000000000d14af55c4eae0121184919baba2deb8bf89c3af6b8e4c4f35c8e4e", - 594007861936424273334637371358095438347537381057796937154824241152 - ], - [ - "0000000003dfbfa2b33707e691ab2ab7cda7503be2c2cce43d1b21cd1cc757fb", - 148501965484106068333659342839523859586884345264449234288706060288 - ], - [ - "0000000000c169d181d66d242901f70d006f3e088c1ae9cacb88b94b8266e9c3", - 110393429764504113949181711819653188468070301266890302199533928448 - ], - [ - "000000000009f7d1439d6a2fc1a456db8e843674275bf0133fc7b43b5f45b96e", - 76554528428498296726819074079132986384157750623812250673757552640 - ], - [ - "000000000011b8a8fad7973548b50f6d4b2ba1690f7487c374e43248c576354f", - 52678642966898219212816601311127992435882858542187514726849708032 - ], - [ - "000000000077e856b6cc475d9cf784119811214c9cac8d7b674ec24faa7c2c0c", - 43246870766561725070861386869077695524372774526710079316876591104 - ], - [ - "00000000004cbb474f2cbf3a65f690efa09804512af3351ba3a0888c806c6625", - 37817516728945957090904676150631917288430706594442690521085247488 - ], - [ - "0000000000235b1ec6656d8e91f3dde3b6ab9ad7e75b332e4da9355ce60d860e", - 29373101246077110899697012205905070265841442578602225419818106880 - ], - [ - "00000000002a153a2c95a8e5493db93086b0e3fe590b636a5871ace57523ef93", - 20444488966645742314409346972440253478913291170842138088329707520 - ], - [ - "00000000000e9550e084908cf91a4e8b74f9f1315d1bc4020709f9e7f261bb18", - 19563849255781403323327768731100757126732627316116500830377476096 - ], - [ - "00000000002c2cfef3bb85b463d3fcd39b73a6d3d5ae11c1e2a8113e3794f28d", - 12545026348036226200394850922278603223904369245268262607334146048 - ], - [ - "00000000000fa92b757ee29674aa97e98a49ba3ad340d2baa94155d71648dfe1", - 8719867261221084516486306056196045840260667577454435863762042880 - ], - [ - "0000000000030571601dbc8e13d00d45004eee6ea8b6ab3cdfb38d2546fee21c", - 5942996718418989293499865695368015163438891473576991811912597504 - ], - [ - "00000000000bb6adef42e63082b20fd2b1dc1b324c51973512a4c31f29a9986e", - 3926013280397599483741094494745234959951218212740030386090803200 - ], - [ - "000000000000765094788a98dbb8adac30d248b7129b59b1441ee2b7ef9e332f", - 3337321571246095014985518819479127172783474909736415373333364736 - ], - [ - "00000000000431a0aa9625f82975709f3c6f4f64d04c559512af051599872084", - 2200419182034594781720344474937177839165432393990533906392154112 - ], - [ - "00000000000292b850b8f8578e6b4d03cbb4a78ada44afbb4d2f80a16490e8f9", - 1861311314983800126815643622927230076368334845814253369901973504 - ], - [ - "0000000000025afe84e27423011af25f777e5a94545dbd00fd04bebe9050f7dd", - 1653206561150525499452195696179626311675293455763937233695932416 - ], - [ - "0000000000000e389cccae2a40437be574fd806909e24136711e7f8bce671d65", - 1462200632444444190489436459820840230299714881944341127503020032 - ], - [ - "0000000000030510bf6bc1649726cf2e6e4010c64a2c8fd3fde5dc92535ca40e", - 1224744150896501443874292381730317417444978877835711165914677248 - ], - [ - "00000000000082648057f14fc835779c6ce46a407bafb2e5c2ac1d20d9f4e822", - 1036989760889350435547200084292752907272941324136347429599444992 - ], - [ - "000000000000f38accd6b22959010471a6d9f159d43bf2a9d4c53c220201254e", - 739430030225080220618328322475016688484025266646974337550123008 - ], - [ - "0000000000004ed7a73133678b5eb883cd8882bf14dfb26c104ae0c3f94cf4ee", - 484975157177710342494716926626447514974484083994735770500857856 - ], - [ - "00000000000037bb3ff4cf649a1757d4028ecc10f893529b4a2214792c981f96", - 353833947722011807976659613996792948209273674048993161457434624 - ], - [ - "0000000000008008f46559fe7f181e9dc0648f213472a1e576e8bf506b88f22f", - 390843739553851677760235428436025349398613161749553108945469440 - ], - [ - "000000000000691d0c2444db713bf6c088844cc95a37cdc55cc269bb0a31d8c8", - 327394795212563108599383268946242257264650552916910648089116672 - ], - [ - "00000000000071153b0afcc64a425f8442c29749610797119e732dd4b723f675", - 291935447509363748964474894494542149680088347011133317125767168 - ], - [ - "000000000000a384acb522e4e5935ad2bc31366ecf1f16f1f11023e967ef033d", - 245823858161213192073337185391658632187400443916100519594033152 - ], - [ - "0000000000002e532093d43e901292121fb7c6583caf2d13b666fe7e194b4a97", - 171262555713783851185422181139260521316022447660158187451973632 - ], - [ - "00000000000033e435c4bbddc7eb255146aa7f18e61a832983af3a9ee5dd144d", - 110438984653392107399822606842181601255647711092336854093004800 - ], - [ - "00000000000028ff4b0bd45f0e3e713f91fa1821d28a276a1a1f32f786662f13", - 61993465896324436412959469550829248888675813063783317791309824 - ], - [ - "0000000000001ef9c75318e116a607af4de68fb4f67c788677ee6779fb5fa0d5", - 47525089675259291211422247200069659468817014361857087365971968 - ], - [ - "0000000000000e6e98694ccb8247aad63aaa1e2bec5a7be14329407e4cea6223", - 30742228348699538311994447367921718297595975288392383715082240 - ], - [ - "000000000000000a2153574b2523a6d1844c3cb82d085e2575846dd8c5d4ebb4", - 19547336162709893274575855467812492508787617050928192350584832 - ], - [ - "00000000000002a92c1b1ffb2a8388979cf30798e312335ae2a1b922927ee83d", - 17248274092338559882155796390905381469049315669915374897332224 - ], - [ - "00000000000004d54b1422ce733922e7672a4e2ecc86dcf96c0de06565cddaa6", - 15943936487596784557029840069157210316687734428242467413295104 - ], - [ - "00000000000009dd91ae96cbbf67af42340b0bc715b3606aa725f630b470262d", - 14273467308195657992975774342458504496649432985410431166185472 - ], - [ - "00000000000007d33d78522fa95bdcd4a25072aeac844cbe9b6bc5d0cc885d0a", - 14930233597189143322113827544414041000381079823613435714732032 - ], - [ - "00000000000003dd57f5dd1228f68390b586700063225d26bac972bd120546d2", - 15164766714763258952996988973449124317842091658872414191747072 - ], - [ - "000000000000076bdeca878b47c392f51fbda543b1e69612cf7d305deb537604", - 15357836632983707094928406965317628870031114888441593128288256 - ], - [ - "00000000000008eb1bb7e18d9dfe62210d761cbf114d59ca08e4f638b8563e30", - 15958672964717750944291813934170287689797412223641384931819520 - ], - [ - "00000000000001b0d8d885e4d77d7c51e8f1fdaba68f229ac04d191915845f09", - 18362361570655080300849714079315004638119732162003921272832000 - ], - [ - "000000000000081baa3a716d5f9ab072c9fc3b798900234c9be23ab02a287c30", - 22401652017447755518156310839596703571934659990690572544245760 - ], - [ - "00000000000005b88d0224b9b0d4b65d3de9a61d93609bb91c9297440f1c4657", - 22607619418140130980719672680045705126213018528712048676700160 - ], - [ - "000000000000027d6a6870403fa43a650b7d9a6e61243f375a79ea935ad9ef1f", - 24717289559589094364468373797949472355802981654048927838633984 - ], - [ - "0000000000000810a3490b86e4f302f6557f9621c5c8620c2b09ec8f0cf72794", - 23340814324747679919001773364939281849550099124416593832968192 - ], - [ - "000000000000073833bca8d0ea909fde717e251576b7b3ccaaa58ad5d39eed60", - 23242391331131109072962566885467580392541369223033474166816768 - ], - [ - "000000000000031b7fd2ed1f28ff74e969aa891297706c38bd2e1d3bc48183c4", - 21554562042243053719921017803645315870071034703425342074257408 - ], - [ - "0000000000000b0738bcba382983811d40b531f2e68cd57126092755f1be4ba6", - 20615546854515052444405957679617344022137222968655050411343872 - ], - [ - "000000000000000664cbfd5e3fa497c07614c33a0934b83e01fbe980634a9aa4", - 19540887421473929614259883543522244007742949396702043752628224 - ], - [ - "000000000000021eb520df39289a70e40c59822a8c47924dc4940e7d0c1455c4", - 19588382523276445241758125434587686389961661359576757951266816 - ], - [ - "0000000000000275e0c41b11bc250fe887c5e60c8ebaaa449f5c28c67133d496", - 18009299117968233362105684657812007807160912568078774269116416 - ], - [ - "000000000000097fb0fdbeee0cee7e8f4e1a4ef8fad49f3d549624b0d47abed0", - 17993483763986497389087426516491816616385967180337839494660096 - ], - [ - "000000000000053f199ae19d34365277e534f978ea2f6c69cd4757a4fc099af5", - 16574638092431222848464934504874974361824393751455373256032256 - ], - [ - "0000000000000217b2e7b4f61682d24b9357d62ad29f27ed45ea2a32dc1f32f6", - 17085559845791583266730740536950670241169412424878408752693248 - ], - [ - "000000000000039c1d77acd4702393f48ca61983c64fc0209ade141c694b2359", - 17870687961287995446644888885900316642120964851955511819501568 - ], - [ - "0000000000000ae53f0c78330f6c2fbece2752909bc3742823e4fab29c5fd2b0", - 15554707140145502641228553657813466188995512591033787398225920 - ], - [ - "00000000000004b4d72b8631a85ec7d226dc696f1913ba1bf735b7c8dec207b8", - 16944226977030767532657500340718760127019357828074148225613824 - ], - [ - "00000000000006e06735bffb7d2f215dcadd8311fc33f4a46661fdca3dc0560e", - 17028747171100603034973679895960153979114298528140818252824576 - ], - [ - "000000000000055fc0110d4a38ffb338eabc30c8b0aef355d4643d21b5b6a860", - 15614535766060906942258863525753414259523988166363835227176960 - ], - [ - "000000000000081b69cb4de006c14084c4861f0e4a140c37200117a738733fe8", - 15392654931672180089790308609774483894682932641297604569726976 - ], - [ - "00000000000009920770f2d40b5b6a8aba33d969b855c91b0f56e3db9c27e41a", - 14444739009842829731785903206212823051010663269705670545375232 - ], - [ - "0000000000000791dd1cb7a684a54c72ccde51f459fff0fc3e6e051641b1e941", - 13237058963854547748734324548161076199478283141947127217782784 - ], - [ - "000000000000019da474a1a598b5cf28534b7fd9b214eed0f36c67c203a9b449", - 12305424274651356593961118223415860240572779254789271782948864 - ], - [ - "000000000000074333e888bac730f9772b65e4cc9d07edb122c6e3c6606bc8ab", - 11046080738989403765716562970384822165842244193743674858799104 - ], - [ - "000000000000067080669115c445f378f3dec19787558d0e03263b9dec5d7720", - 10007073282210984973971337419529346944295676968729147521105920 - ], - [ - "0000000000000304760bf583f4ac241c5ffe77312fa213634eba252c720530f1", - 9412783771427520201810837309176674245361798887059324066070528 - ], - [ - "000000000000041fb61665c8a31b8b5c3ae8fe81903ea81530c979d5094e6f9d", - 8825801199382903987726989797449454220615414953524072026210304 - ], - [ - "000000000000022fc7f2a5c87b2bab742d71c4eb662d572df33f18193d6abf0e", - 8774971387283464186072960143252932765613148614319486309236736 - ], - [ - "000000000000013c6d43ba38bc5f24e699515b9d78602694112fefdc64606640", - 8158785580212107593904235970576336449063725988071903546310656 - ], - [ - "00000000000001665176b9a810fddf27cca60dfcfd80bf113289fcc8ffed0284", - 8002789794116287035234223109988652176644807295346590313611264 - ], - [ - "00000000000002dc6ef80f56a00f1091471d942ce9bfb656ebdab4ea0b77eb0b", - 7839560629067579481152758851432818444879208153964570478641152 - ], - [ - "00000000000002a1fa5546ec48ca88b9e5710e2c6d895bb3675004fdacd6ab13", - 7999430563890709006856701613305138698914315019190763857641472 - ], - [ - "00000000000000f517517c11e649b98feca7da84ae44fb643de5a86798fe3c31", - 9047927233058169382412882048952728634925849476849852060008448 - ], - [ - "0000000000000299cab92a923348acf9251f656bcbacdb641fd0a66d895a6e8f", - 8296391419817537486273948666838217011279219811331013552898048 - ], - [ - "000000000000027508b977f72c3a0f06f1f36e311ad079536630661880934501", - 9081029136740872581753422344739175313292014241889017867010048 - ], - [ - "00000000000001925959229452cc6fbfef0104ebed7ccd6f584f2439c5dd1f1b", - 8230751570811169734692743946971314968326461977249645504495616 - ], - [ - "00000000000003b34ca89509da5f558af468c194afaa8d458bbeb07c50cc7c74", - 7384127474250891166670391848516180960454656786677558849568768 - ], - [ - "0000000000000076559e314ab0c86cc552e34fd79488415d3d17f6ea3c01adb3", - 6172230000534146257480611019445716458048957888854766248787968 - ], - [ - "000000000000003a58043252cdc30ed2f37fb17e6ef1658324b1478f16c1463b", - 5561365017980676031428107027647386014985059524839404952616960 - ], - [ - "000000000000011babf767e60240658195b693711c217d7da0d9215ccab45333", - 4026319404534786334009451711043898716884778820756489262596096 - ], - [ - "000000000000027579d28fb480ccad8e2516d1219d4c1919e3fd4fc0c882955d", - 3513558656525386849113615662535622466519417660386833443323904 - ], - [ - "0000000000000074546fe07f80ba15fc81897ec56a5535de727df9fda9dab500", - 3004083578955603829930099910053556479043735076695139267117056 - ], - [ - "00000000000000b6c55833b80c07894f4c4d3bb686e5ddbc1b1d162e22752ca3", - 2675541054922611112919804040984964595022815308724929898217472 - ], - [ - "00000000000001326f2f970753122e35bfdf3358d046ddf5ea22e57f5d82b00d", - 2409843108029446766213067266805752590003732794677225687351296 - ], - [ - "00000000000000641084745613912464ff73c974bafd0bf6dd306295f019d306", - 2218268905456883731807407021635746739577921454491297946533888 - ], - [ - "000000000000011ae105ddb1a5bbac6931a6578d95c201525f3a945276a64559", - 1727551573307299192250197436766000536509732237655131060961280 - ], - [ - "00000000000000d9b66fee19af89eaaf3f3933d1acd2617924c107f0abbe0a41", - 1394031503757574068227953656553224448260418805016069352194048 - ], - [ - "0000000000000011956d42670c2f75eeb344ac0657a806775998e2c58fa4b157", - 1263610003247723462826224891154624535497729630761756072607744 - ], - [ - "00000000000000959b1ea990368fd16d494e68ee13bd7245ddd9cdfba3330100", - 1030450001678223668360152541055867895065240185756254103142400 - ], - [ - "0000000000000091f86b1e423e24fe358c72db181cfcc2738c85f2f51871a960", - 862513010327976103705811440432628413487564277790886242287616 - ], - [ - "0000000000000055e146e473b49fe656a1f2f4b8c33e72b80acc18f84d9fcc26", - 720982641204331278205950312227594303241470815982254303477760 - ], - [ - "000000000000004f6a191a3261274735292bc30a1f79f23a143e4ee7dd2f64c1", - 530591525189316709998942710962548491505413142398652303540224 - ], - [ - "000000000000005327c8e714272803c60277333362e74ec88b9ffab5410c2358", - 410030579894253754102159787320079652501746816512444002729984 - ], - [ - "0000000000000002e2a62b8705564c38d6a746fc8e971a450a69989152b5ee97", - 310118479516817784682897231521434079438159381558537557639168 - ], - [ - "00000000000000202bf3ff30109538bfd9b5075c6438ab5ef64ebe2cf9b61404", - 239366800071949252578530950352093786414793290792735831228416 - ], - [ - "000000000000001c997105893f5991cb45765ff856b6e503f8466cb22cdd330a", - 181156297885756721946540202079438048595571151633323613224960 - ], - [ - "0000000000000010c13ce182a3d8fc6748b75640447eb360d7739a5fe984ffc1", - 142431093377788751676361246670241704468765375727695350988800 - ], - [ - "000000000000000bbb49db68b79ecc8393376d78272d237bb612288af64c1de8", - 100696259189502783924473792493100546893980348528488767029248 - ], - [ - "0000000000000001bbfd0973c367d30eef2416d9e94bdddea53bccf541a4858f", - 68962778243821519216393853205209897734463141354237780295680 - ], - [ - "0000000000000004ee5b6ace996ab746f1e6dd952cdbc74c0b4f8b9ac51c7335", - 52765641310467331636297188681879886184148735229489015947264 - ], - [ - "0000000000000002f2f23b515085d0c9f37a2824304ccb7ca1546a48548d0dac", - 44233472386696495417387091608220539804351405166731810832384 - ], - [ - "00000000000000045590c3fdeca1753d148a87614a70fa0897a17f90bb321654", - 38110290672195532365762668664552282566878756832852091863040 - ], - [ - "0000000000000002b704edc0bf1435fe2116040b547adb1bc2d196eb81779834", - 29679649578007061283718812081441644170496168236939550392320 - ], - [ - "00000000000000038cc59dc6dd68ae0fbe2ded8a3de65dbd9a2f9a36d26772df", - 22829202948393929850749706076701368331072452018388575715328 - ], - [ - "0000000000000000a979bc50075e7cdf0da5274f7314910b2d798b1aeaf6543f", - 19005913916847449503306572434028937600915626422125897711616 - ], - [ - "0000000000000001dd8e548c8cf5b77cde6e5631cd542e39f42c41952e5e7085", - 15065005852539512185984435657022720640916062598235628240896 - ], - [ - "0000000000000002513542a461de351a5a94f96b4bcd3e324a48d2d71b403fe0", - 12288698618318346282960995223961541766142764336009759948800 - ], - [ - "000000000000000150cc07163e78d599a7e56c0d1040641bffb382705ac17df0", - 10284386012808371892335572105827331142617405906583881252864 - ], - [ - "00000000000000009051d83d276dad5c547612f67c2907acf6a143039bddb1bb", - 8614444778121073626993210829679478604092861119379437256704 - ], - [ - "00000000000000000b83d3947d2790ab0bcbbb61eba1eb8d8f0f0eb3e9d461e0", - 7065379129219572345353864175298106702426244380437224882176 - ], - [ - "00000000000000005a4fbbaeffee6d52fa329dd8c559f90c9b30264c46ad33fd", - 6343094824615218102798845742064326605321937397913065881600 - ], - [ - "00000000000000006b6834bae83e895a78c5026a8c8141388040d90506cf3148", - 5384518863803604621895699676581808210968416076987222720512 - ], - [ - "0000000000000000bf3c066c9acdb008e7fff3672f1391b35c8877b76b9e295e", - 4405349994161605759458363322921957536960017949107037405184 - ], - [ - "00000000000000006bcf448b771c8f4db4e2ca653474e3b29504ec08422b3fba", - 3863038134637689339706803268689141874606936642244315185152 - ], - [ - "000000000000000098686ab04cc22fec77e4fa2d76d5a3cc0eb8cbf4ed800cdc", - 3369574570478873127315415525946742317481702644901195284480 - ], - [ - "000000000000000036cc637d80982595b1fa30f877efe8904965e6fd70aeae1a", - 3045099693687311168583241534842989903432036285033490677760 - ], - [ - "00000000000000000ee9b585e0a707347d7c80f3a905f48fa32d448917335366", - 2578448441038522347123624842639328775756428679710156783616 - ], - [ - "00000000000000000401800189014bad6a3ca1af029e19b362d6ef3c5425a8dc", - 2293149852232440455888971398133692017055281498246925516800 - ], - [ - "00000000000000001b44d4645ac00773be676f3de8a8bff1a5fdd1fb04d2b3b2", - 2002553378451099534811946324256852041059202347552707969024 - ], - [ - "00000000000000003ff2a53152ee98910d7383c0177459ad258c4b2d2c4d4610", - 1602972750958019380418919163663316163747908621623690788864 - ], - [ - "00000000000000001bb242c9463b511b9e6a99a6d48bd783acb070ca27861c2b", - 1555090122338762644529309082074529684497336694348804259840 - ], - [ - "000000000000000019d43247356b848a7ef8b1c786d8c833b76e382608cb59e9", - 1438882362326364789097016808333128944459434864174551793664 - ], - [ - "00000000000000003711b624fbde8c77d4c7e25334cfa8bc176b7248ca67b24b", - 1366448002777625511026173062127977611952455397852592472064 - ], - [ - "0000000000000000092c1f996e0b6d07fd0e73dfe6409a5c2adc1206e997c3a2", - 1130631509982695295834811811892052032638591596239280668672 - ], - [ - "000000000000000020ce180d66df9d3c28aee9fcec7896071ec67091a9753283", - 982897592923314645728937741958820396011314229953349812224 - ], - [ - "000000000000000018d37d53ae02e13634eefb8d9246253e99c1bdf65ac293ea", - 903780639904017349860452775965599807564731663176966340608 - ], - [ - "00000000000000001607d1a21507dea1c0e5f398daf94d35fb7e0a3238f96a0f", - 777796486219054632155478957346406689849105796561635377152 - ], - [ - "00000000000000001acae244523061f650ddab9c3271d13c0cd86071ae6e8a5f", - 770217816864616291160628694313702426464491250746461782016 - ], - [ - "0000000000000000104430189dba1219b0e3dd90824e8c2271609aca5b71250f", - 749174812297985386116525053725808178560617045558724395008 - ], - [ - "00000000000000001aa260733b6d8f8faa2092af35e55973278bb17f8eaeca6b", - 680733321990486529407107157001552378184394215934016880640 - ], - [ - "000000000000000009925ad5866a9cb3a1d83d9399137bccc7b5470b38b1db2b", - 668970595596618687654683311252875969389523722950049529856 - ], - [ - "00000000000000001133acacb92e43e24af63a487923361a4a98c87a5550dffe", - 673862533877092685902494685124943911912916060357898797056 - ], - [ - "000000000000000018c66b4a76ca69204e24ee069da9368c7a9883adb36c24af", - 683252062220249508849116041812776958610205092831121375232 - ], - [ - "000000000000000010b13aed220b96c35ccd5f07125b51308db976eefcd718f9", - 663358803453687177159928221638562617962497973903752691712 - ], - [ - "0000000000000000031b14ece1cfda0e23774e473cd2676834f73155e4f46a2b", - 613111582105360026820898034285227810088764320248934432768 - ], - [ - "000000000000000010bfa427c8d305d861ab5ee4776d87d6d911f5fb3045c754", - 653202279051259096361833571150520065936493508031976308736 - ], - [ - "000000000000000005d1e9e192a43a19e2fbd933ffb27df2623187ad5ce10adc", - 606439838822957553646521558653356639834299145437709336576 - ], - [ - "00000000000000000f9e30784bd647e91f6923263a674c9c5c18084fe79a41f8", - 577485176368838834686684127480472050622611986764206702592 - ], - [ - "00000000000000000036d3e1c36e4b959a3e4ad6376ce9ae65961e60350c86e8", - 568436119447114618883887501211268589217582000336195813376 - ], - [ - "00000000000000000b3ec9df7aebc319bb12491ba651337f9b3541e78446eca8", - 577075114085443079269506210404847846798089003835028668416 - ], - [ - "000000000000000012d24ce222e3c81d4c148f2bce88f752c0dba184c3bc6844", - 545227566982404669720599751103563308707559049533419683840 - ], - [ - "000000000000000000c4ccbdd98c267bd16bda12b63b648c47af3ac51c1cc574", - 566251116039239425785056264238964437451875594947144974336 - ], - [ - "00000000000000000056bfec1dca8e82710f411af64b1d3b04a2d2364a81993f", - 565860883410058976058672534759150528155363303710710038528 - ], - [ - "00000000000000001275d1cadce690546f74f77f6d4a6190e2137a8a819946f6", - 552364745922238091561919045022000637317595931246011088896 - ], - [ - "000000000000000003816ae80c6413b84cbee2f639ba497ab5872ec9711eb256", - 566500670366816952120145379831520408210047884740723212288 - ], - [ - "00000000000000000d92953224570f521b09553194da1ca3c4b31a09a238f4f6", - 542528489142608155505707877213460200687386787807972294656 - ], - [ - "000000000000000006721943f23cfacf20c17c2ad6ea4e902af36b01f92e3c06", - 545717322027080804612101478705745866012577831152301113344 - ], - [ - "0000000000000000031d9af2fe38cc02410361fb213181fdb667c74e210d54c4", - 527827980769521817826567786138322798799309668948178370560 - ], - [ - "0000000000000000142e8a13ef6994961655c8e86aece3f0abebd2ee05473e75", - 515692606534173891771672037645739723025219384908133171200 - ], - [ - "00000000000000000c7a8db37a746d6637ef6a6eab28735608fd715ee2f394e7", - 511567664312971151375333957573881285830542480898837708800 - ], - [ - "000000000000000007854877c66c71a49af40d20f2d6f817becfe4d66d5e5a81", - 496889230460615059653870414954457230681194245244172894208 - ], - [ - "000000000000000005ce1d2d10aeb9def4d38233e859d98a4a168ea3fa36687a", - 473325989086544548323169648982069700877697035484407005184 - ], - [ - "000000000000000007c71decfe74855ad99dc2aa4a2e713165db5a8d6da5f32a", - 454358737757395076722955683517864397151243915416267915264 - ], - [ - "000000000000000008ce4f34161be6760569877c685e37ebebce3546ea42a767", - 443316987659242217350916733941384923365365929826941140992 - ], - [ - "0000000000000000086233f4843682eb47bacb58930a5577fbfd5c9ebd57ddf9", - 442802913227320896234856097023585967110900073490544590848 - ], - [ - "000000000000000010a904eee4fc763c6b88d378884f368fd652f63c1af71580", - 433057199397126884276233483897801969646324654385408245760 - ], - [ - "00000000000000000c114754749d622d4fa2f78c84d7147c345b2b99a8e83d2e", - 409419129139225030716120689261979366152221060879441985536 - ], - [ - "000000000000000000a5039e32cc9a89aeffbde1391e8bc9ae9724127904f01d", - 370716507988397359530778284103407727265240291588416995328 - ], - [ - "000000000000000003b0b73d9b3259c318cca48a6335b5d64545583f7f3773fa", - 340818253309165415058055171484606858815006633875327680512 - ], - [ - "00000000000000000198bcc5bd65fd0ccd1c7e3b49e0170ea80296cbfee05042", - 288495652867775987986282369150900282132304927019642126336 - ], - [ - "00000000000000000a60f379d3dc1413491f360809a97cbb02c81442c613dce7", - 259524902203633530447121351815377152077137395840706412544 - ], - [ - "0000000000000000038973a5f8ba8cdc7e371dcc8f4b24337ef695f24b962907", - 237834253647442358407456603145452341381064939329604812800 - ], - [ - "000000000000000004b8ec471974913d052a3af7dc2a8c6f01c2ac2f3d1f7b19", - 224600391397450328424792273873642383828872941895338164224 - ], - [ - "0000000000000000075d572eef1c4210adc7abf4e40986d7f0a80003853bfec4", - 187067719845325692996306936867878122094522982476155977728 - ], - [ - "0000000000000000074f9edbfc07648dc74392ba8248f0983ffea63431b3bc20", - 164898540577033087399552264895286015147022701908103004160 - ], - [ - "000000000000000003c4a4d9c62b3a7f4893afe14eef8a6a377229d23ad4b1ea", - 170169861298531990750482624090969781281789404909188153344 - ], - [ - "00000000000000000404b6939e6c35a5448386e5d58f318c82ce2fefb7d73e47", - 162900609378736249874251099581569547607832255884553093120 - ], - [ - "0000000000000000034656c96781091b5fbc799c881ea85b41cba0b88128eff7", - 161578008857017275969393492955354620126364423170461532160 - ], - [ - "0000000000000000045645e2acd740a88d2b3a09369e9f0f80d5376e4b6c5189", - 150883090635422687830679296233896712896447026244773478400 - ], - [ - "00000000000000000381e6a138308c6547d6fe3eb3437250ffefdebbf71eefd1", - 150899178845446426410002882396535253739927398750206558208 - ], - [ - "0000000000000000012100ddbb2102e65fb1ebbf104ead754a4110abffc4b8bc", - 138784382553152119468195441786396823230753870240366460928 - ], - [ - "0000000000000000046f56e59b9b1293b5e7c1587aa6d29c4f3f79b98cf22ee6", - 135262935280049154152065372885142255350817451144176992256 - ], - [ - "000000000000000001bd1c291e91f4476f93454d4542d2ed7e44fc86902c93bb", - 137505556928474480767543871928291413858290772017802117120 - ], - [ - "000000000000000001c37a483375ff6fd6ed7c5b79d80167b027a8fdb0721dcd", - 128713911367130082233924624261304605948946745676720504832 - ], - [ - "0000000000000000051804b4c2da5298c4573386bf1d4242bf0e26a49ec32e42", - 126333978716874242627475052620752087219210710628817698816 - ], - [ - "0000000000000000034bff7888f1f7294311f0199322f77c1457018c875bd9e1", - 126278605342839049377710151409810132688161986656629424128 - ], - [ - "00000000000000000506b43c9283ccbc40f583e0c734e4a8af2ce6a4262c6221", - 133533639774706835230353390473157702360903922769486413824 - ], - [ - "000000000000000003937068e19a0750a33978050f019d2b60f430e3da707db9", - 124022888639743237872084547350559836284832548627419234304 - ], - [ - "000000000000000002e2f6ec3c9eb965aa706c788da7dede201b6b4b8fae3971", - 122123731568103772089607259872577666017242529148853813248 - ], - [ - "000000000000000000b3076636b13562bb4315f895bcb324e0c962763c2196b1", - 119378259820331825692479928211144812308894309500762193920 - ], - [ - "00000000000000000025b8961d1d0cfba33b0205ec10b3ce541618e352b0bbd5", - 111759931157462873316041289986819959868258380300102402048 - ], - [ - "00000000000000000421d58b78b9f063a4b20e181d55c9c79082f9e4b8b30925", - 104283029085035157753191385936387396702868516379761311744 - ], - [ - "0000000000000000027fd968d41741f31c73c4a3b304472da0165245278e2ea3", - 106299667504289830835845558415962632664710558339861315584 - ], - [ - "00000000000000000364a23184b8a2c009d13172094421c22e4d9bc85dcf90a5", - 105881374043672627773432318187360570734220873198601240576 - ], - [ - "0000000000000000042a2ed4a504424060407825d774a54f2e148fa769ee72ff", - 95668727978371040303278646201741713440261619517174579200 - ], - [ - "0000000000000000025f769f13f2806fed19d9948b1a7ef19048177789afc5d3", - 94012390634764280055243391736606357298689315295029362688 - ], - [ - "000000000000000000b3ff31d54e9e83515ee18360c7dc59e30697d083c745ff", - 86923102180582917240747796162767475850640519180006195200 - ], - [ - "0000000000000000021ecdcb2368ce66c23efd8bd8ab6a88a8bb70571c6e67f0", - 84861566431029438820446406485131195674434646972185968640 - ], - [ - "000000000000000001972cb33b862b27c1dc3f3a723f7d1cfd69aebe0409126c", - 80022382513656536844370512820784980102919810105407963136 - ], - [ - "000000000000000000cb26d2b1018d80670ccc41d89c7da92175bd6b00f27a3e", - 68605739707508652902977299640495787127103841947617329152 - ], - [ - "00000000000000000276deb4022f66cacd929c690cd6b4f7e740836b614b21f4", - 63859343606086615291372321518809062931940920926127783936 - ], - [ - "000000000000000000587912ced677698c86eec8b1d70144dccb1c6b0bad0f17", - 61163258921643354765656928775243357859392914550528409600 - ], - [ - "0000000000000000009f989a246ac4221ebdced8ccebae9b8d5c83b69bb5e7c8", - 58509826700983959310706392369835644790490546910263246848 - ], - [ - "000000000000000000038bed8b89c4e82c13076dd64dc5f7a349c39d3921d607", - 56672777602924507578641088682504585686103825941044133888 - ], - [ - "00000000000000000122f47d580700a3a5b4b6cb46669a36e4fa974c720ab6cd", - 53958359841942568206719748916397287559357255547625668608 - ], - [ - "00000000000000000172ad9ea56a90bdfed0f364a902500e9ff4d74f000ced99", - 51764751112426770751506128647798102319231116027761786880 - ], - [ - "00000000000000000201d7429db233c7055e9699c5bfb57b167ca8d0c710dc71", - 51649140486907347007064544362790913467244253139882213376 - ], - [ - "000000000000000000c0549b2a8adbefbf6c909f61fdc4d6087c44a549cf8201", - 48144529712666433692552181910809237167694270386587828224 - ], - [ - "0000000000000000015b6789cdc5dc13766f58b38f16d5b35bf79ce4b040f7fd", - 45240046586752885057924289339576851866807485277820420096 - ], - [ - "0000000000000000013a31b29f845d97465bff53f901027f8ab4b1a2f59118a8", - 39718797393257298660757754408019939605415460564426031104 - ], - [ - "00000000000000000088cdeaa7389a7de9f09e3a28b3647630fea3bd1b107134", - 37880625861940376795251270290737354395669643839013912576 - ], - [ - "000000000000000001389446206ebcd378c32cd00b4920a8a1ba7b540ca7d699", - 38043004539854389433075372490391464304285496568268718080 - ], - [ - "000000000000000000f41e2b7f056b6edef47477d0d0f5833d5d4a047151f2dc", - 33509870757351677175294676059494700127350769223450230784 - ], - [ - "0000000000000000010e0373719b7538e713e47d8d7189826dce4264d85a79b8", - 31340207270661909233492904963194738468218672502370467840 - ], - [ - "00000000000000000053e2d10bd703ad5b7787614965711d6170b69b133aa366", - 29201223626342991605750065618903157022235193117232857088 - ], - [ - "000000000000000000cbeff0b533f8e1189cf09dfbebf57a8ebe349362811b80", - 30353962581764818649842367179120467226026534727449575424 - ], - [ - "000000000000000000d0ad638ad61e7c4c3113618b8b26b2044347c00c042278", - 29217311836366730185073651781541697865715565622665936896 - ], - [ - "000000000000000000a7bda943639876a2d7a8caf4cac45678fb237d59c28ba1", - 24433127148609864747615599184820261456796420809345204224 - ], - [ - "000000000000000000fb6c6a307c8363e923873499ba6299597769c10a438e61", - 23988269434232535193761088780698748366141469438183997440 - ], - [ - "0000000000000000006f408147ffbcaa0fb1dcf1f199c527ffdaf159d86e5cd9", - 22526487188587264742197108840494583820145762956159746048 - ], - [ - "000000000000000000e3be3cf7343d7792c0d47d3c39ddb9ceaf19961e9eeab4", - 18556440756915402760741928101946749165024073301499052032 - ], - [ - "000000000000000000b3fb09d6def197657e20f9c1d5e9680cfcac1e1f9aa269", - 19758940920085072387393228723348383373068660102939017216 - ], - [ - "000000000000000000bfe71f044145e1b42fdfb3a523ee2a215e80fa6afc2a98", - 20014481558369106100835306608979160026489460596213284864 - ], - [ - "000000000000000000cee3bff56ee49c0f96d1cbd17fa17dc6f84b3f48aed765", - 16946123176864917983795071264823963343174695083267063808 - ], - [ - "00000000000000000089ef13654974b8896b0b0909dd9ae8e350b8a8a7807ce3", - 14392961660539521116256653268419249019684881662910398464 - ], - [ - "0000000000000000003105a067417c318dab31e25ae1583fa2b27be226945fdd", - 13960450711994363030255127593764523087979983609872252928 - ], - [ - "000000000000000000720da39f66f29337b9a29223e1ce05fd5ee57bb72a9223", - 12101157559014734955774763823279522156034099347349045248 - ], - [ - "0000000000000000006a8957cbd52c2038861514f106f7f9f76392d5cb83fd4c", - 10356793971791534424976101420669664288187918308140384256 - ], - [ - "0000000000000000006b68e55432541794388c94fe9e805652038e7b3cac0681", - 9378292318569022964986206758839123913433917663832178688 - ], - [ - "00000000000000000001c9deea9f0302eadb1250df1ad53da802dfb40d47face", - 8964447668935855171055978546867850348456065181232922624 - ], - [ - "00000000000000000013aaa8778111530a626a3fe57e4e6f4a878c92669b04d1", - 8192878571041388924351625416816775770172128369752145920 - ], - [ - "0000000000000000002f67aa98789b98304a32e54bffbb34c8693eb0acac4c30", - 7786052052270684126234611299412205796254663675224260608 - ], - [ - "0000000000000000002e5f072398ee27b25b6cdcf69051bcdbbece417093c979", - 7678459224733657715202292429397298472913633233275453440 - ], - [ - "00000000000000000028d7447c20ade2053bbaf49e8a16eb5fb1bc74335d0d18", - 7021961458254440109762706424650140438182306270565892096 - ], - [ - "00000000000000000042d89446b9043387be2d4c09aa9e9524176c5754616510", - 6702918573828378664524678433037841287557455508299317248 - ], - [ - "00000000000000000018ec4d369bab2c13174834a02138decea7c85685d46bd6", - 6505870154073602347674948421782035713149324747260035072 - ], - [ - "0000000000000000000d4a6c2237c6c46b963b17f60d9c850c4915518deb6678", - 6259542822111302646229226565336702507884435252736688128 - ], - [ - "00000000000000000031adb986da21237ce06b57ae5390b7f0f890ab8e21b66a", - 5456617206587901877414813377199700077413780408546361344 - ], - [ - "000000000000000000031df41201cd3789559333cd9529f99834a805014c9b13", - 5309609141393698345581459330931267317315649121846034432 - ], - [ - "00000000000000000020c68bfc8de14bc9dd2d6cf45161a67e0c6455cf28cfd8", - 5026314587016750785722693470327208449351582469580652544 - ], - [ - "00000000000000000009dce52e227d46a6bdf38a8c1f2e88c6044893289c2bf0", - 5205879062684137510961952799929229129995569309608312832 - ], - [ - "0000000000000000002eca92f4e44dcf144115851689ace0ff4ce271792f16fe", - 4531442825108320403104334767545311437480985430866264064 - ], - [ - "00000000000000000000943de85f4495f053ff55f27d135edc61c27990c2eec5", - 4219470685603665866184576203153693664105230070242607104 - ], - [ - "0000000000000000001d9d48d93793aaa85b5f6d17c176d4ef905c7e7112b1cf", - 4007526641161212986792514236082843733160766044725313536 - ], - [ - "0000000000000000001877e616b546d1ba5cf9e8b8edd9eba480a4fbb9f02bce", - 3840827764407250199942201944063224491938810378873470976 - ], - [ - "00000000000000000025eb2c783f2f29d68ab4260f4b0248450c0038debc7ba4", - 3769176185135465353474348091454476000617158630021529600 - ], - [ - "0000000000000000000c61b8a7779dcc46e88ca343b9a3fcc6763917fe3b87e2", - 3616317728887026217259424694800679959591344645351669760 - ], - [ - "00000000000000000003dba9fedba6a0b92b640167eeda0d41485a3c85ac4ac6", - 3753318892370425056811838111019504329853891761930240000 - ], - [ - "0000000000000000001ac75bed7eb6169255893f99de28f24e3e0e57b6f7db7b", - 3752507758961706405692235065937346792777982719368888320 - ], - [ - "0000000000000000000e5796e9c5cdc8a8a2de84fd17287d7dfe89074de31766", - 4052052750044136275098507698196378011637603685579620352 - ], - [ - "00000000000000000015fe695e8d2e5ed3a7de81d3818ef43a444e1ee7b3ace2", - 4774638159061819979596346127394133648234752261950013440 - ], - [ - "00000000000000000015a08d0a60237487070fe0d956d5fb5fd9d21ad6d7b2d3", - 5279534360700703025330663904443631645337169341976674304 - ], - [ - "00000000000000000008f4f64baaa9b28d4476f2a000c459df492d5664320b12", - 4798269179035823348880781507454323228379569035237392384 - ], - [ - "00000000000000000028a69d9498c46b2b073752133e3e9e585965e7dab55065", - 4581847093576588582947343450056030606262879232408420352 - ], - [ - "00000000000000000014dbca1d9ea7256a3993253c033a50d8b3064a2cbd056b", - 4636475101776743072223960781733299832971578678999777280 - ], - [ - "00000000000000000019046cf62aa17f6e526636c71c09161c8e730b64d755ae", - 4447653474738502407900799312400854215681091162244907008 - ], - [ - "00000000000000000017e5c36734296b27065045f181e028c0d91cebb336d50c", - 4440088742263677654396177039706714734771352055402463232 - ], - [ - "0000000000000000002296c06935b34f3ed946d98781ff471a99101796e8611b", - 4442250303185290059812200289574302117357423179633524736 - ], - [ - "0000000000000000001ccf7aa37a7f07e4d709eef9c6c4abd0b808686b14c314", - 4226119056551884143559484765457720035561644907380604928 - ], - [ - "0000000000000000000de3e7a7711130dbac9fb0a14e5ad6ab72d080182f3321", - 4217024131862773934699503234743726606330326039165665280 - ], - [ - "0000000000000000000e6829c1245de98ce5a35c177a75f67e9c1678cb6e24aa", - 4243570847603252455305754966045185171099356397876281344 - ], - [ - "00000000000000000001b2505c11119fcf29be733ec379f686518bf1090a522a", - 4022508494445492072607020209303018350395259009223360512 - ], - [ - "0000000000000000000a4adf6c5192128535d4dcb56cfb5753755f8d392b26bf", - 4021030916290150529756716283937142188262386861422411776 - ], - [ - "0000000000000000000485ab94f5ea60203aacfc9740b3e42700d7e7012f76d7", - 3614033401827878015998272335407144409231622422786998272 - ], - [ - "0000000000000000000cbc6dfb3f2afbd6ed1427e30ed1f3167898ac4aa4c673", - 3638558860803927897868648370584956354584468626790678528 - ], - [ - "0000000000000000001d9865df58f5f300552699fefc09aa840ba25ac044a534", - 3397669776434136486181562425402160438435718857259745280 - ], - [ - "000000000000000000115eb6c10b7a98bf23a46002baec8fbbbb2cf0583439a6", - 2974300520630483197933400799376074857018768662277914624 - ], - [ - "000000000000000000113978c5b95531173923ba81ed4d1df3b09db37ae0f0cf", - 2990922178751847556822131306978557143801315583089180672 - ], - [ - "000000000000000000096b8d24db6471fb5871e9ae8bd1d7384fbee9c80a6052", - 2699909434228155498652331786772923585210445951064342528 - ], - [ - "00000000000000000016e0dd8fe86bf34feaa611b4c52180b6822b5ad31b68ff", - 2647377219375933524160418539145769508351933111739613184 - ], - [ - "00000000000000000011e20e47a868d12a2bf3de814ebd067e83514aa2725745", - 2502742632840755378666227277045667991877723059489079296 - ], - [ - "0000000000000000000c48f6bed594da7bb5e75731b4e78501670e834d426e87", - 2267299103571658911252368261549572946260211294613274624 - ], - [ - "0000000000000000000f7871dc40f51b1ecd6343a6d9fd614d0e2235a7d9e3fd", - 2112846149036891759953684644743283440459952687539027968 - ], - [ - "0000000000000000001558c0f33a360d105b52a749103eb2abd4a66a68d52664", - 2072520395859657486634608572838975759381606196813234176 - ], - [ - "0000000000000000000676463abf3771ea01e0f8c948d1c93658a1d82d95df5a", - 1969073848467738847181233556694484530967339635488849920 - ], - [ - "0000000000000000000e24396612da4ec125ee6c0b4507e854c5cfed1884cd30", - 2119459443945814095658556318611324621123895782295994368 - ], - [ - "00000000000000000002fb021eeb13e47021920faf6e5daa3c40bc552c4d248e", - 2078088717097888226752964612051624797686495299801972736 - ], - [ - "000000000000000000067b904af747b653ba448a79779f7846bf1ea5537b8a4d", - 2093644940525638357414324633411056914147713045789409280 - ], - [ - "000000000000000000080ae07ccf2f1b6d1d089f5dcbc1fac50a6b93d005f1e0", - 2082043540528505650049623783208955059537684253263265792 - ], - [ - "00000000000000000008f9ddf24dbec1459689fc399329e9738b2795860e4361", - 1953761695813422977307213550702116033770404430236090368 - ], - [ - "0000000000000000000aacba541ebb7b56b0831e4ae33faf20ff1e528bb9a657", - 1824503568004603261415443256727022530945994444270206976 - ], - [ - "00000000000000000010fe23dd08a4b6465c4850984bb538e9dfcb93995a23cc", - 1743137387349479903250289511035208906392689711805104128 - ], - [ - "0000000000000000001166c174a9d34b0743953e724162fe44388e38d078204c", - 1734095076719313606895363312975193263350078457161711616 - ], - [ - "00000000000000000006da92c61b6b63ea910be27cab5fd951137105314f2969", - 1740794600224838465872409004248364704712181251938713600 - ] -] \ No newline at end of file diff --git a/eclair-core/src/main/resources/electrum/checkpoints_testnet.json b/eclair-core/src/main/resources/electrum/checkpoints_testnet.json deleted file mode 100644 index 50f8148b24..0000000000 --- a/eclair-core/src/main/resources/electrum/checkpoints_testnet.json +++ /dev/null @@ -1,3306 +0,0 @@ -[ - [ - "00000000864b744c5025331036aa4a16e9ed1cbb362908c625272150fa059b29", - 0 - ], - [ - "000000002e9ccffc999166ccf8d72129e1b2e9c754f6c90ad2f77cab0d9fb4c7", - 0 - ], - [ - "0000000009b9f0436a9c733e2c9a9d9c8fe3475d383bdc1beb7bfa995f90be70", - 0 - ], - [ - "000000000a9c9c79f246042b9e2819822287f2be7cd6487aecf7afab6a88bed5", - 0 - ], - [ - "000000003a7002e1247b0008cba36cd46f57cd7ce56ac9d9dc5644265064df09", - 0 - ], - [ - "00000000061e01e82afff6e7aaea4eb841b78cc0eed3af11f6706b14471fa9c8", - 0 - ], - [ - "000000003911e011ae2459e44d4581ac69ba703fb26e1421529bd326c538f12d", - 0 - ], - [ - "000000000a5984d6c73396fe40de392935f5fc2a8e48eedf38034ce0a3178a60", - 0 - ], - [ - "000000000786bdc642fa54c0a791d58b732ed5676516fffaeca04492be97c243", - 0 - ], - [ - "000000001359c49f9618f3ee69afbd1b3196f1832acc47557d42256fcc6b7f48", - 0 - ], - [ - "00000000270dde98d582af35dff5aed02087dad8529dc5c808c67573d6dabaf4", - 0 - ], - [ - "00000000425c160908c215c4adf998771a2d1c472051bc58320696f3a5eb0644", - 0 - ], - [ - "0000000006a5976471986377805d4a148d8822bb7f458138c83f167d197817c9", - 0 - ], - [ - "000000000318394ea17038ef369f3cccc79b3d7dfda957af6c8cd4a471ffa814", - 0 - ], - [ - "000000000ad4f9d0b8e86871478cc849f7bc42fb108ebec50e4a795afc284926", - 0 - ], - [ - "000000000207e63e68f2a7a4c067135883d726fd65e3620142fb9bdf50cce1f6", - 0 - ], - [ - "00000000003b426d2c12ee66b2eedb4dcc05d5e158685b222240d31e43687762", - 0 - ], - [ - "00000000017cf6ee86e3d483f9a978ded72be1fa5af37d287a71c5dfb87cdd83", - 0 - ], - [ - "00000000004b1d9fe16fc0c72cfa0395c98a3e460cd2affb8640e28bca295a4a", - 0 - ], - [ - "0000000046d191b09f7726e4f8bfaffed6c30734afbf1f95e6bddbe0b07d9e88", - 0 - ], - [ - "0000000082cec8200e9ea055c2991bf74560eb7e7140691ea53e7828dbdc9553", - 0 - ], - [ - "000000003775b96d6b362d4804afe2d9c3cf3cbb46a45c3ccc377c94e83edd23", - 0 - ], - [ - "00000000037835a92404acb2f18768a49d4f93685ead30aad6bb3b073f411e02", - 0 - ], - [ - "0000000006cf75d17706d1f62e6b08e6ba5facfde38a8920b7d808a6b6781ff2", - 0 - ], - [ - "0000000003dff257cdae43703fcd0ca91fda0970f5fc04258b4608fb1942a6f6", - 0 - ], - [ - "0000000000532d97d18867658e08c789f627535652382147e33bf8626d4131bc", - 0 - ], - [ - "000000000266dfb79bb11dedd0ae748505863ab3ab731269cd71a2c2fbd159b3", - 0 - ], - [ - "00000000349ff0119d5c0dd8ffad8bf41cd6126a88416148b81fa4dcaebc42e1", - 0 - ], - [ - "000000003c61939b4799eeea4335218d30de9b1071605126d719dce0f0d14810", - 0 - ], - [ - "000000003d9284570ed648d2b12ad24046ac8b9abcf05c4e9813ea110490cf73", - 0 - ], - [ - "0000000001360b66e6dc0ccfbd75356034e721ae55c3d5c71a58be5d281c252b", - 0 - ], - [ - "000000000c114f42504916bfb2ee26ed8307b3f7f74226c1cfe1f5302ec23d26", - 0 - ], - [ - "0000000007acac3fcf97b4ca81821263b704364adaa2736fce0a0722bfed4f8d", - 0 - ], - [ - "00000000059768ef7731d27f9c2be48c6e16d7cb56680625f08ff25ead504280", - 0 - ], - [ - "000000000351c8908f1f52518ce4bd251b896ca3fbccb69a2607db6624bafcfc", - 0 - ], - [ - "0000000068d7ccae048e212e9e2ecb4d944f583b4490df4fbf654b4915597052", - 0 - ], - [ - "000000000e2aaa36417187233ff55325473bd5b7a164b358da60c96d1920fd77", - 0 - ], - [ - "000000001eb11ef6dbe0647bc87a8d218f6e59c2b9690f17edcf0dbd39cd0308", - 0 - ], - [ - "00000000022e7855e24cc3fff67ce093242434a8ffa45882333a0f08a40aad9c", - 0 - ], - [ - "000000000210130ff4e3186258c09a8463c1e196f5c5432b4c7b6954e907bf63", - 0 - ], - [ - "0000000000e01372ede322bf88ee5ed8a46dd4fd8df832eca16180263fc8b1ef", - 0 - ], - [ - "00000000a0701896e26d5d884834b267512e0af52c92edc4bccf1c5c803d3c4f", - 0 - ], - [ - "00000000869fc8d9ac1588f3e5bdfd60253e9824083800b7794010e0e9c6b6fe", - 0 - ], - [ - "000000001d43b3165ec30736f28f0761600b092686f861db23ec38f2d92b0ec6", - 0 - ], - [ - "000000000ef4092da8c2056e5933de0e1530194c3ad941a9b393fbb26f98862e", - 0 - ], - [ - "0000000001e3fed39f70023909f962bea146b03bc8e94e5d19d7da93123f4f64", - 0 - ], - [ - "0000000000b4b8c877bbe3cde97649845290bb78999ecff4621b9bf2ab16aa2e", - 0 - ], - [ - "00000000006095ba3b4742883a0ec427a3fd685ffb65b987ea77ebfedea7da82", - 0 - ], - [ - "000000000168f0a76a6068a34fc042553aff4aa63b906028f28c2a4c327328e1", - 0 - ], - [ - "0000000000af10f3079b4989ac4ff0baaecab38220510cdae9672d6922e93919", - 0 - ], - [ - "0000000000312791ada0f6a4c5eaf2a1cd57cd06f5970a8ab49923817b862c35", - 0 - ], - [ - "000000000055f3d4f45c4d199d9c230cb2cfeb68c8e934cfd061bd616358655a", - 0 - ], - [ - "000000000036b6129bb5a786bfdd75cb4b932f7dcae9da469d3ba35096f1e821", - 0 - ], - [ - "00000000002fbccf271c13e486673251ecd7951ecc12ee73c4390e0ff09e9b59", - 0 - ], - [ - "0000000000314e297a81bf002fc40eb391d8883ea45ee4e782385aa0fdba6452", - 0 - ], - [ - "00000000d3c473819ec3b3c268f7b555df22772e407bc8f246a47cfc579ec61f", - 0 - ], - [ - "0000000075a438fda6bdb391263d0a2a6e8e68edd9dd8f70fe5734eab9351eb8", - 0 - ], - [ - "0000000017ebae0a2bec50008b4a4ea8839798cbd9ff228e76aba087d0ff1736", - 0 - ], - [ - "000000000800466ba31c0bbc12b125f16d05ed27788de045e25d6f093817d29c", - 0 - ], - [ - "00000000002163c41f2264f202e611aeb9ba6c0a3ee95cd8e5e7e571edc64edf", - 0 - ], - [ - "0000000000de9882d417786fce8c755cfaad17f40cda744d4badedfe5e414e31", - 0 - ], - [ - "00000000002af352cf41f60a5ebf033bf7e4967c0597cee706ba877b795aefb4", - 0 - ], - [ - "0000000000009ca0030f1dd0b09cc628f2d4d278c87b20781a1b136dc395debf", - 0 - ], - [ - "00000000ffd27370a76d06a0da0e3805f47e35e2cf584d73d2c5ecaa2e525642", - 0 - ], - [ - "00000000720da6910aa75099baa020cb8db37e1dc19cdff66152225b7609c23a", - 0 - ], - [ - "000000000a5c2cc704bce5e8527ce91bac7430c659624ecd86e6a1bb9b697962", - 0 - ], - [ - "00000000084273545134e9a06483c8fab00c2b0628056bb1967f310c74a971bc", - 0 - ], - [ - "0000000002f66f4da52804647b1c3e1f89d17bdb05e9cd4ebbd922007c773f21", - 0 - ], - [ - "00000000c46146c9d0a67a354b3f82947e52670a3bded6d8513ab34a68ae18bd", - 0 - ], - [ - "000000002f61c429d7dbe7bde75796086efe574998766806138710a2d6001eba", - 0 - ], - [ - "0000000001daf3e3e78a57df2c2d2ddd14093d10515925e75c818bec3bbd30c2", - 0 - ], - [ - "0000000002e133a7427a9aac6ceca969b27507c14111a45512cdf8f52a436de0", - 0 - ], - [ - "0000000000f7c4374d458666740de1d0e8c55229a209ced7c38e38708781487c", - 0 - ], - [ - "000000000035bb9ea329ba30b83eeb4ea6f57c2fe703b97f9b879f21e22643e0", - 0 - ], - [ - "00000000001220503e0aaee266bca85de09ce97b0091f24972d1ad1c8afe8609", - 0 - ], - [ - "000000000010a614c60457f8d2ae2bb826d037f52113252888fadda8ed773c9c", - 0 - ], - [ - "00000000585a8b882ecff8aa8434feeac4ef199ca669bd81ed473e37f0bb4528", - 0 - ], - [ - "000000009504ffdb5fe82ad88218fb5e75a8bc185247e30e22d23b9fd9b7f282", - 0 - ], - [ - "000000000ddec7d73bcd653168d82e34cf5746e006bccda8a9c031c3289b9568", - 0 - ], - [ - "000000000cb6620ee4e8cb8b6b4d51251e5961f7ae2e83538ab3a4fef3bcc773", - 0 - ], - [ - "000000000239224a0841738513c1eda712b73266ea958aa75f44a3985ebfab82", - 0 - ], - [ - "00000000002630c7c3586fcc19079300403c54dc293bcfdf8a9981f85a5c31bc", - 0 - ], - [ - "000000000028d8c34f44e51fd71f5401094a983f6566e6d08ce86ec5d1bd639c", - 0 - ], - [ - "00000000000dca95f1828adc3c37b4625f60aeb35a6614a4358322b7a6bc2f7d", - 0 - ], - [ - "00000000d72ec84fda18959ddc474d1a31a3a13b1d94695136c4810af8c01a0b", - 0 - ], - [ - "00000000327c29604996eb7f0a208160969ee4408a1cad277a956334f94e0f35", - 0 - ], - [ - "000000000e1bd41d009c1910fcfee7bf1cc1adb04b0b7a632ac36c1092f01bb7", - 0 - ], - [ - "000000000201a5afed48b9d095b949229e9882ef8bc96767be3097c87264dfb6", - 0 - ], - [ - "00000000003f28e8f3f9c80b1269bb0aa3b57501c12458550ef04fd43aca6a33", - 0 - ], - [ - "000000000029e09fc14e38a6a0103c8c67383f41af7d76998055682525f4ca89", - 0 - ], - [ - "00000000285ce297602995582ba5d32d583d618a6a92643566e25dd36cf2b7ab", - 0 - ], - [ - "00000000657045fa54fac52b8480dc84bd4c418940ba63679f4bd6add6a39962", - 0 - ], - [ - "0000000017b7bb58be05a47ff7c4ead27db750813d6bcf3f99cbcc35324cf445", - 0 - ], - [ - "00000000003a310e39b6df17f17450496b4f5c1593399bfa1ab8b4d39bac9b25", - 0 - ], - [ - "00000000000bfbc5294f003548a9636ebbcea3ba42577821266317676fbc363c", - 0 - ], - [ - "000000002329351dd70c24da2eea5ac19f65b6053c4611aa4eb93bcc2783c57e", - 0 - ], - [ - "000000004ce02f1005aa6fa4d158c6e4fce95ab053d88ae74881dd080c24e057", - 0 - ], - [ - "0000000000fdaaa54cdaade8cfb75245de0747c60c0307ad11be9fe154535565", - 0 - ], - [ - "0000000003dc49f7472f960eedb4fb2d1ccc8b0530ca6c75ed2bba9718b6f297", - 0 - ], - [ - "00000000014ca604d769d4b99fff03ae3ac84d1e8eb991c5dac7c3cd4d9e68ee", - 0 - ], - [ - "0000000000190ab8ecef3a3d5583563851672d81a4d4d952b8cf3bd503c655e5", - 0 - ], - [ - "00000000001204d263b607987fab11e1c19c94b7e3e674cc73cc2fb7b05fbf07", - 0 - ], - [ - "0000000000141e8d7f7ac359a8ae58e35ce6010c25ddd6f1881f41c0b939332e", - 0 - ], - [ - "00000000946344dd06ef5ddd13fb74f20c475daf911ff4e3f1dcdf64c330e274", - 0 - ], - [ - "00000000ec77a7892e48b85bcbaf404d16d7fc93747d7e9e3ba6195a9b6f1525", - 0 - ], - [ - "0000000018a305c04dea8e93e423ce9569872e0ec5af49d23a0e3872b0ad6297", - 0 - ], - [ - "00000000055e32c5f8a86c9a712eeb6440bbf9810ae6da12d0cea2493138a885", - 0 - ], - [ - "0000000001913fcbe67badbce4234e86e35a1ea867ecd69814b5f5ab039b7d4b", - 0 - ], - [ - "00000000002c71fe4403aee704720ceafd21f9f8c9c97a8bfbd25bb46223aa40", - 0 - ], - [ - "0000000000343a42da0c811836d0785c272591facd816f0e7fdcfb1109d8f9a8", - 0 - ], - [ - "00000000000309b182608b3eea7fafd0d72e3c79a0a3a9cda03cde3947e332e1", - 0 - ], - [ - "00000000000204cc04e421c3958a64d7bc024a474ce792d42ab5b48a5a6f3927", - 0 - ], - [ - "000000005eaa010e7255bd37e0b00780575074a74d889e17c4dbc578f917348d", - 0 - ], - [ - "00000000a0d425f62d9196c069286dc6635ded9d027de40070d397e45bd63e0e", - 0 - ], - [ - "000000003355fd37068ce2d5d2a94ef964eeb9b687f21f4a00850a3e6cc4a71f", - 0 - ], - [ - "000000000ca9148dabe9424cd8c96860c90d836ab25970a3e91856764e2e640c", - 0 - ], - [ - "0000000000bde23f829dde8edef35436be4b8978da21fd2c3a8100ef5334e3cc", - 0 - ], - [ - "000000000028bb26f1427fbfabeae65d55a9e59e18230713e40f0f7c9c2dee12", - 0 - ], - [ - "00000000002ac05422d254e597ee6b5e0f8be9b3e2f887486442d720c7766919", - 0 - ], - [ - "00000000000e36d0b6f187dd9601b1d1dcd987c3e0f6a081ffd039c7c5e32462", - 0 - ], - [ - "0000000000048d7b1f2a2a11fda34a5cfeea067ab03e482931e5a0f463f438ba", - 0 - ], - [ - "00000000f780ab88c8a4f4247573a749fbb087a4e3fb6a7d29926de8a9ab3462", - 0 - ], - [ - "000000000313bbe6a940e6a8c40ba091aa1ebbaad135bbbff3ed8ae07cf574d2", - 0 - ], - [ - "000000001d4ab29721aa2722482562670a0d71dc1eb73231c5dafb64756b04e8", - 0 - ], - [ - "0000000006588bcbdec38d19962b96cf0352cbf1b90f3379cc6787d018cdb96d", - 0 - ], - [ - "000000000022e79539a21ac24f9daa2cbddf2bb4a3125f88a5efc20d13ea856b", - 0 - ], - [ - "0000000000dd284b7fee584cc578a10fbe57e8efe6bf6ebacb23c0ac5d46cdf7", - 0 - ], - [ - "00000000001451143787f411c93d5506065c3fb597966f2fd7a4a5c078ee6aa2", - 0 - ], - [ - "00000000000ca977394af1e414dc1f9d83efa007f7226e11d3a00f59a1fdfad1", - 0 - ], - [ - "0000000000011f8caa80580e7a796bbce5b84e60731bf48e03c6ff5c6bba868e", - 0 - ], - [ - "000000000001705beb1376af1af08b437acef6befbe7d3b60c5fbaf6bb7f38c9", - 0 - ], - [ - "000000000000c838f1f45422d93ca9b5838368a37423efa8439ee24b2bf247a2", - 0 - ], - [ - "00000000000111ad857d31d07fdc8b32d17af2522c18bdaccfef449b29d17362", - 0 - ], - [ - "000000000000312a7718fc616b0ecfdbf6066f71ec1a4a8c43f50f02f61cc398", - 0 - ], - [ - "0000000000007d232b217a59b804ef67091c5720a5460c2c16bf97b97a24801e", - 0 - ], - [ - "000000000000177235c33695aced585685b4c500eb76e72caad02e17503900eb", - 0 - ], - [ - "00000000000037f5c5890da7a8e2acd2b0669ad7db648ac43140c637a1c81637", - 0 - ], - [ - "0000000000002123904063f223bc35135c426a4f9a0b74c1907e837b810f0321", - 0 - ], - [ - "0000000000000961db809da357d91a9341170fafef9f24896d8730bd05cf3f96", - 0 - ], - [ - "000000000d2e8fcd05eb874e98cfc3a6e239f6974950e6f50b0487513ecab760", - 0 - ], - [ - "00000000017e362508c8db23fae0431eaed708d9db13e48fd5d318066bf6733f", - 0 - ], - [ - "000000000011b2bc4fe36f90b7ba5a62f974db250bfdc285b70c71148023c7e3", - 0 - ], - [ - "000000000001be28570b378dd5dd2eb3aa495c229913b6757fe8900dfa3cce99", - 0 - ], - [ - "0000000000242bd0bb16d0a5324e0b4b5a83697dabb3b4a059084557478e50b9", - 0 - ], - [ - "0000000000d8ce69d18da32ed52e503d6b5ad48d970b90545f956b2d2af2edf6", - 0 - ], - [ - "0000000000366655bf0cb3dd0cd7801e0adbd26b5b441b77a9e3642597effb00", - 0 - ], - [ - "00000000000dc7aa00d4607ca8374d40d1187f1c084b620edb45fc39bc8d2db8", - 0 - ], - [ - "000000000003baf60d9c6e70a765cf517f66a124509191188e9547ad09edf68b", - 0 - ], - [ - "000000000000e0f476893b8fb4d37e855353075fde73dbc1fe181cc956349f19", - 0 - ], - [ - "00000000000032ed16b7de758abadf4a4fb2df7a101ff275c51f29e1555a89a5", - 0 - ], - [ - "0000000000000a564d03f0f2fe20f6fb5f038d931f732d817641cd7fff3b0acd", - 0 - ], - [ - "000000000000011aa4d0fdcea8d4ca85cd5d548e322e2b6abd17f8444be855c5", - 0 - ], - [ - "0000000000000610588540267a0eb544531047d4c8af0f21fca7cd3d96205cfc", - 0 - ], - [ - "00000000000002770dab5e14843149df8f76b8dc8458ed3ed2ed8a14a6e2e564", - 0 - ], - [ - "00000000000006b70ebc9f75bd32f466602cbd4b86c3c2d2379059542bb8bec6", - 0 - ], - [ - "00000000000000ef579af389fa7674f98a2371063fa8b218c5ca0ad94e21b896", - 0 - ], - [ - "000000000000021b6108dc988f9153383f9501ab9001109aa87902ddd4c8a4d1", - 0 - ], - [ - "000000000000022c02ff22bc0af5201f0e1a14a75879c494731e4fbf999218c8", - 0 - ], - [ - "000000000000032651c988edc1ccd08e82b888cbb8135e24a958ac0c0b640d5d", - 0 - ], - [ - "000000000000015aefdfa0790bed326c38c358c07aac0674f5b2e771258b8df3", - 0 - ], - [ - "00000000000000822e1534c86afef911b67d3fa20cf2b12d93d20d64005f54d7", - 0 - ], - [ - "00000000000000338b871276768c923b1c603fd6150bd054c2287e532e61de7f", - 0 - ], - [ - "00000000000002d0af52c0cae894bf836b61137ace2bd7500abd13a584c02741", - 0 - ], - [ - "000000006f8443a458f38d8731821c07a2fda0ecdbb1cf797f541844d468ce0c", - 0 - ], - [ - "0000000000b6fbd8b4e227f5514979a61d8b0b918d2adc154e585ca926386704", - 0 - ], - [ - "000000000f4f5e49b10278e27d9dee15b92f9d4a257138a206831e0c00188767", - 0 - ], - [ - "0000000002c7e9769bd8ae9906fc5682e937b5c31ab5b5b86e4d70af2c15a95c", - 0 - ], - [ - "0000000000f68a1db8cd387e0a2f93f45149fe1ee4a230bb386313bdd42058e8", - 0 - ], - [ - "0000000000f0f65c360c8f0f9853ad1142f16675dc1175d61afdbef977776b25", - 0 - ], - [ - "000000000004f734e634156511cbef7dfefebdf317e7488aa6c2562572d7ecb7", - 0 - ], - [ - "0000000000002a46a7a16787e8317dc567ae26816324c2035be0186ba54d5cb8", - 0 - ], - [ - "000000000001a593e6f01875b77e270163538d88452779bb557df7c2607c28e0", - 0 - ], - [ - "0000000000004f24cfafa10bd50a452535f64be577a6161e51c7c71542f654c4", - 0 - ], - [ - "00000000597cce73e84b63f08cfcb9b01f5e7621752d8c8e08fabbd6ab5c0dd5", - 0 - ], - [ - "000000007cad379df01247771fff471bc99faea1b86218602f45ab13efc5e9f6", - 0 - ], - [ - "000000000d6085aab25892be49c49d6c0a3949befdc3ddce2faa46b104e1e804", - 0 - ], - [ - "0000000002be5996786b42d6a229093896aea9966b1854ea261e01e84da1f420", - 0 - ], - [ - "00000000002684b72056e270b115d80b12b2f68eac7412355287226aecd9b5e0", - 0 - ], - [ - "0000000079ea27efb24366c87856a9e371c56fcbd59d09d3164a5c2fc15fcbca", - 0 - ], - [ - "000000001694120525dba4548ca54087544da1fbefa51c38f0208d683418825d", - 0 - ], - [ - "000000000693e80d372938f3553151ab9d0a9a6922182591c701df739dc9a502", - 0 - ], - [ - "0000000002950d9cb23c8511937811910b712f73d448e6fdc2e39e029b86848b", - 0 - ], - [ - "000000000091c40056c6a48f33db17764af89c01f62ae653aa5e494146164cee", - 0 - ], - [ - "00000000001f373c47e1a39af4e1ebcd8c88411ec49d6bd520c2781564070971", - 0 - ], - [ - "00000000000809ca4b2170c57958709b867095b1972d80a2ee55359fbd0940fe", - 0 - ], - [ - "0000000000038e7bd66fc3308447b1370dbdd0661c427c512bdbc641ff360fb2", - 0 - ], - [ - "000000009a3325df76e2de1fc1970cc2f241fa8a41da9ad745a0d9666d9ff51d", - 0 - ], - [ - "000000003176e92ff837bf43a48a995c1a321b166475f586ffb4b962e0254a4a", - 0 - ], - [ - "0000000001ae3292e81ca3859b75bccd5bff825cd9f496efd085160c716ed05e", - 0 - ], - [ - "00000000033bdac4f0d36bb912fba28bb5caa54d1b611759a10f79ff3c969cf2", - 0 - ], - [ - "00000000004c6db7fa0e2c9f08693abfeb128c5827b511a5c46c623a103b416b", - 0 - ], - [ - "00000000003d87f48bb95e9431760d0c5f4f93c77d02fce9dd1673e9f5b01029", - 0 - ], - [ - "00000000000e214fc3d8b97571eb75d248ca29f8e25a584c33de8488ceee72b0", - 0 - ], - [ - "00000000000133269b7159b828700d02de770a8cbd91f3d166e6bbc95d8e0dfc", - 0 - ], - [ - "000000000000cc92e2dd933a08f7fd87f84451627982fb66583587858217c059", - 0 - ], - [ - "00000000000030708136c20c4c8216314005b3cb5c551ded33b26cf64d2ff47d", - 0 - ], - [ - "00000000c472a1341d479ed02f31b699e448c035049a7092670b38f4ec6121f0", - 0 - ], - [ - "000000000a358834d6eed41b9b7161a338aba53828111414cdea7552ed15548a", - 0 - ], - [ - "000000000e13e77372daea775c8358916e57ed11835899c14e5140ed9be11089", - 0 - ], - [ - "00000000008252cd0931f94b2465bd4f93e4bfeec6697962c5b034cf3d12cf7c", - 0 - ], - [ - "00000000019812cd6cde3a43831234be71e68118be24a80161349b8b327acb5b", - 0 - ], - [ - "00000000005865499f301adfb59f8380743e4c3b3ab220ca4eb97dc6628df626", - 0 - ], - [ - "000000000015f77e1e61329560a4378eb401fa5bf0ef90b0a014a4d7857ca7a8", - 0 - ], - [ - "00000000e9cbcbb625e8a463ba8e7f14be46ba9538ffe93338784ccad3d992e8", - 0 - ], - [ - "000000000fb27169efcc2873cfaac223ebb91cc5e1e5ad7e9a312d42bedf7c42", - 0 - ], - [ - "000000000c9c96d62ebfbf3fa4003f1d46d175140ab084dee17e8125fa40f24a", - 0 - ], - [ - "000000000311e3a766b1ab2064b68a344a561eb496d595126808ffb166c71cc1", - 0 - ], - [ - "00000000677568c82262ac3a4ca3f909bdfb0b35145ad490fa3fbdc719d06b91", - 0 - ], - [ - "000000000ee77ba9ab657e51fd9140f5c9b46731d9341e98188f929c97d04746", - 0 - ], - [ - "0000000008a67eb9c91a6d74168f3f385270fa942ea00bdd31924d1b6ea11148", - 0 - ], - [ - "00000000017f93c9e0026e90d579e18c83b4a8557f0c00e9b85ab164cf4466c5", - 0 - ], - [ - "0000000000994efa379235c03711a8e6b29895d928b5fde96cb01c02374c0602", - 0 - ], - [ - "00000000b3be9f23c943d71d7c7dbdf6dd672d77a712f6c83e9796a85e4379f2", - 0 - ], - [ - "000000000713e1089b0b2bdcba462b740c9396f822f1c73e090713978a7f1314", - 0 - ], - [ - "0000000002fc44d358401a7ac9ce4ddcb17f3cbac08e40242e755e60ab2292ed", - 0 - ], - [ - "00000000021ef2c04fd30be7049f73b9a8353ac96a467dd5f0b9c1457be1bc5e", - 0 - ], - [ - "000000000023b95b440ccbbdcb914172cf675cd15d6111bd7f5a436a4925d36e", - 0 - ], - [ - "00000000001983521dbffd1b742a6d4b5dfda3f46579fbbdd83a2ebf9a039bec", - 0 - ], - [ - "0000000000044d53dbea312432e68fa90dc2148946f613216dbdeec86f6a67c1", - 0 - ], - [ - "00000000000107667692f12d21a55a72ff1dce828f96872e36c35bfbae475a8d", - 0 - ], - [ - "000000000000252d1d0c01744ec25af801ef7c57e2581c95295070b6a8a85bd5", - 0 - ], - [ - "000000001c1da54e16dc06158677024d9e74bff39bfaec83434ac33673fcc251", - 0 - ], - [ - "00000000b4d0c6ae86bfdf7ba4c205fc3e6b3b6d63836b85e30e9d8bac922301", - 0 - ], - [ - "000000002b16179cb022bf678bd847dd6fc1908d0df04abf0c7874981eb33ee7", - 0 - ], - [ - "000000000e6783554aae41856424d184dc4fa061f40676efd107e6f933a25641", - 0 - ], - [ - "00000000005ae4acbab519895b4b523d97a09e381c9e4b044e642f73b8c0f1b0", - 0 - ], - [ - "000000000010372b59c9595d947064804b75ab21868dd075a3842ab7d2df6181", - 0 - ], - [ - "00000000002f9f587ea304093be049d3142ac0c92f9c68928a4f82d12b929b69", - 0 - ], - [ - "000000000005d4cae51b3c76dc3c61bed0c265c4f228c0c4d1d3d147146c34eb", - 0 - ], - [ - "000000000001a5b6c0e0a0b485a490cb52ccdf9b22596656039b51545bb07be5", - 0 - ], - [ - "000000000000d723d0976338edf55d08edab995dd6283cbb688855f0dca6e8f5", - 0 - ], - [ - "00000000bfebfae90208a82c7fa06c0f61674dbf1e4f9162e370656c38d611bb", - 0 - ], - [ - "000000000c91cd144b2a92ab5024c87f70cc1d76a4a7f26a82a98c5aaad62850", - 0 - ], - [ - "00000000077c8114eb5cfb69c3924c699d0c70334360dd1daa95db0db4816953", - 0 - ], - [ - "000000000348a6443e091db8f68e88a10afad7c6e3e5392247902c4b4feade43", - 0 - ], - [ - "0000000000d63b70351e05829ad8a56336521b361b0d50eb7ea1f5b46c25b00a", - 0 - ], - [ - "00000000004658603163f0ede572120a1bbfce8d313aa282ae54d2ffd9fe9079", - 0 - ], - [ - "0000000000048063b410c793db34856f23acfb19a0ce72f5997fa572773378c8", - 0 - ], - [ - "00000000000228fb6e587fa593ff8b4764064bba8bfc2f43ba5b1f12af33d04a", - 0 - ], - [ - "00000000000082e3ddb75c0ea2a98922b1556ce10346f9bb0cedd97ccb3fdf62", - 0 - ], - [ - "00000000000005571b54d4886b44b81c21dfbefa554cd7c23430e5aeff6b5ae2", - 0 - ], - [ - "00000000306a603ca1a0d961e08e103a9f13f3615163c3373d1bd2a67cadc2a7", - 0 - ], - [ - "00000000195d93ba7ae19832b622de86ebdadf3c78f1751ef2b2e9b0e3a530d8", - 0 - ], - [ - "0000000000476d0d00cbc68bb20b4893f0e608b02a1e029b8c6c73e169c49e69", - 0 - ], - [ - "000000000051348044bc10fc05960c244c3ccd3b3b6c145ffd9958a1c8bc0215", - 0 - ], - [ - "0000000001e4df369203badca9aedc28c240d592b12d284ce0b0463fc7537c09", - 0 - ], - [ - "000000000091cc1ccd448b0ec9185618a84dea96f52477cfb9b9ca2b60cebe83", - 0 - ], - [ - "000000000024a50299c0ef0c6dec9c64336b6cf5c1a1b0013e22fd4fcee1d7d1", - 0 - ], - [ - "00000000000349248c1df06c3783d1270cd97ce7f605b9036fca0fdc2f0fbb96", - 0 - ], - [ - "000000000001afe6793e7427a3d780876d26eb7f2ded92563f991bf7302aea69", - 0 - ], - [ - "0000000000007148006e139e24d9fccc307661c9a0cbcd1af983487c2f0780c9", - 0 - ], - [ - "0000000000002734722a341984738177a3f6f264291424e4984f2128d921bf29", - 0 - ], - [ - "000000000109b02caaa95e49a477757a41a42daed40e92f54fa09e63f5538cd2", - 0 - ], - [ - "000000009a11c7ff8b8fa7fbff5a04c25906f701ab5bd67195736f9ccc839ab9", - 0 - ], - [ - "000000002b1d77f8e0cd60af1c62ef6d381e8905665b15a7fbc546d0c1a45e18", - 0 - ], - [ - "0000000002588cb017de9e2f23cea7edc5082f1b3faec890f9252d556efeac40", - 0 - ], - [ - "00000000008b07f177adc24a4b1a64d2dbcfbcc903ba861d493e11d6b33af7dc", - 0 - ], - [ - "0000000000bab8db5020aa8e052165275e8eb3e7c843533246bf6e4c8374757e", - 0 - ], - [ - "0000000000138488fdca8bfc327e6dbd6c72c5f1dc5868d9c0ea886665b9b56b", - 0 - ], - [ - "0000000000094021fc954efbf08be667fef1b817e8715d4093a561fc30264aa7", - 0 - ], - [ - "000000000000e8183e64072db79adfc6c09b650c4178001be3fade4050b06005", - 0 - ], - [ - "0000000000004c93e8661c75974cd191c68dd66999da4f70d039c0ba4a12b970", - 0 - ], - [ - "00000000000021c675b3ec404bb996f5e68f9eeceeac6946e5a6822987824d33", - 0 - ], - [ - "0000000000000ad85684d30f25d1ec34638f099df2f33b418a07307c68fe3c2d", - 0 - ], - [ - "000000000009c6add76ac42a1942c4ce74d25d1b8975d4e3ac8932185e785a44", - 0 - ], - [ - "000000001e7d828d354716881683eb6fb5caec5d91afce298e4e3bcee9574924", - 0 - ], - [ - "000000000a0e438ab203d8fd3e56100f2f14759f704bff6c699df0bb4e9aad64", - 0 - ], - [ - "000000000b7d5c2895df8bc1fdf5d31e0f663564cb5cff3b18642c44a71b6248", - 0 - ], - [ - "000000000193209ecd92fce00a75975446423d94a325ed525c15d5ab921da273", - 0 - ], - [ - "000000000020835bdc30ac67efdbc785d15186914bc14e86387f97450df46418", - 0 - ], - [ - "00000000000c9078321f0030214c75e170b01ec664d39bab1b1e48460a54eb63", - 0 - ], - [ - "00000000000ac68b63d486ade190dc9108eb3730d25e7537649fe21c30e0121f", - 0 - ], - [ - "000000000002a94dfc5f4b677b251a7a7647dbb99c0803df8658222227fe3e3f", - 0 - ], - [ - "000000000000b076bbef0e50593b1595ffb3d571e7ad95dbdf06dca8824ef7f3", - 0 - ], - [ - "000000000000167075c8bcd24233d25cd268271c0e8fcb6f301ee1b6f6ff0341", - 0 - ], - [ - "00000000013107aa587bcf12ac445330ff0325d73c5253f7e6a49ed8c50257bb", - 0 - ], - [ - "00000000090ff53d49c9ffd51511af8d5cba2038a8e25e3b17186b1bc941f43d", - 0 - ], - [ - "000000000d9e704d5607f77f8983cc56069571a3761d5bd5da55f05ec5d8e844", - 0 - ], - [ - "0000000002b2b4c0950fb6390f0ae860840e84eb0a82e5e8a9bc37c14bbf43b0", - 0 - ], - [ - "0000000000be10137a2434dce1d97850b768ce878c1c80ec905f6e9f21e65fa7", - 0 - ], - [ - "00000000005cd966f80183d4c048e63a5c14f649298dfd261d989d9e3c026bf4", - 0 - ], - [ - "00000000000e8f30e55006a4082380c4b1a372b7ad919d3a9b0a52fe5ee881d3", - 0 - ], - [ - "0000000000018c70a4c27bdba237ad19ebae5d3ca23f1394ccc746d73669a1c4", - 0 - ], - [ - "0000000000022acc8432c883953227786f7a6560aeaf0176d232c8affa5b25b4", - 0 - ], - [ - "0000000000001854e95b28b4efcb2cfeb08c76d8cf1fb03f2055b3fb758f3a1c", - 0 - ], - [ - "000000000000187080c2c39f5a3ea8be72ac4d3ec0d16b21cd34f1541bef23be", - 0 - ], - [ - "0000000000001593766a3c63b524f658ec7690df467cc7bbcebbdb56385500d4", - 0 - ], - [ - "00000000000012d6966dc51a41f2c617192169ec8418405e164ba83b9f7ecdfe", - 0 - ], - [ - "0000000000001d0c7d0a2605e127b00448b71e756ad96625116ab8ca18f74900", - 0 - ], - [ - "000000000009cb439ea49282d257595ad1f7602856c16cc26fff423f7783c792", - 0 - ], - [ - "0000000000889282b98336c994d7420a639221e0484b511227fd616d78dbd028", - 0 - ], - [ - "000000000071a4a2ad6767864bd21239c74c9912a40ca9fd3b209e21b66460d9", - 0 - ], - [ - "0000000000f3ed2c3c9a7c3a7291e859cecba8cf9243d23a4892e6be8ea9b70f", - 0 - ], - [ - "00000000006a4258ffdff8b7f6f4f685ce18c6eb1d7a1cf501ca9e02fcb7620a", - 0 - ], - [ - "00000000004af78f1a109d1267a9c24d69c6a4b30fea49f0efa6c8834cf394f9", - 0 - ], - [ - "0000000000193bf3efbb145747198470a81b2cd33c991057676742d5c22a64b2", - 0 - ], - [ - "000000000006b436798c7e4a8c3bdbf054a66707feee5a18ce9ca57eb95bb48a", - 0 - ], - [ - "0000000000001db50c7caa3a02ea4f173343f958f334a8bf3f8638add9e69b34", - 0 - ], - [ - "0000000000003c621629cc0bcec5968d61d2e42c6673de4d46555118ad5001d8", - 0 - ], - [ - "0000000000001262bef2918265f6dd4534013a4650444054fb4f5e490c5ed57b", - 0 - ], - [ - "0000000000000120ceee972d70cc84430006645997c7337976c673bd75cbef2b", - 0 - ], - [ - "00000000ba16134dc0c418a116b97ad5deccd6bf6e3daa028a8a6a80d7823faf", - 0 - ], - [ - "00000000a1a00d6d6fe0660e63402a5a7c7248589211594d37fd800456ce84b6", - 0 - ], - [ - "00000000394766cec78f962c29aaa715b66e3ad34e1f2323dba45e087cb3b395", - 0 - ], - [ - "0000000008b15a3020676f5e084210ecc05f646885eca1cf6a10e9ae9e3995cc", - 0 - ], - [ - "0000000002cf7eb98abe784f6e516670a88b9028a6faabfd099a364c2dc5c42b", - 0 - ], - [ - "000000000054015fec337a9ee43eea501d2292f031f5bc1f09758d20f5cd3135", - 0 - ], - [ - "0000000000068d24d31a9f1192d848155a2f90939627bc456c9a337135a923fa", - 0 - ], - [ - "000000000006262bd09358258edcc455f9ba46b7f9d6e69d0f6b9da89488a4a5", - 0 - ], - [ - "000000000002327bf77ae67961463ea98a78dab06c24ac7d58b1727c5f856626", - 0 - ], - [ - "0000000000006672235c1606fbacd7861b16b267d203b4d687708eeb1fc25e6d", - 0 - ], - [ - "000000000000ac0c9a39a47313a8715f125c46d6ea8be8741b99b1db4a8aae47", - 0 - ], - [ - "0000000000007e93f6578e7856aae0ecf6341e1312664d9e1d812ff254c37ae6", - 0 - ], - [ - "0000000000002a980acdb1443926875e7d4a57859b2b45ce3fa92c7716319f62", - 0 - ], - [ - "0000000000683bfd82c63514bc58a80daf699a6bcd040bb2a499540baf52463d", - 0 - ], - [ - "00000000373e6262928d7a6cac965b294aef35f90b72c85100ef91501775e06a", - 0 - ], - [ - "0000000000f7bc44061b65c62d4d7747138df127dd2a30f583c3ebb66a25c7a4", - 0 - ], - [ - "000000000212a71c38d0e13ab7c5646c949d4b7ca23afedbe351a43b7607043b", - 0 - ], - [ - "0000000000a836e88f76ee5dcca1e884572f32f4460a3b024280738d76e98ced", - 0 - ], - [ - "0000000000413f6c1b1c9841961636bb3290f2410ba0731f3522c4ff3faa2e0e", - 0 - ], - [ - "0000000000082336107412226110ab2a53016d4faad4deec048828507a300248", - 0 - ], - [ - "000000000000a91e7a3f35a23f01621dd051e314da617714991467131808d3bf", - 0 - ], - [ - "000000000000cd6576950f6f238227c3ba7f62405ed1bf3af4878c6dc1b04635", - 0 - ], - [ - "0000000000674099e9741e44da03e9531402a2607a19a65660b57470340828db", - 0 - ], - [ - "0000000030c4744001ae85f9e6b46ed0664449927b86b8fbf25b22b851d23671", - 0 - ], - [ - "00000000002f5095ad1a12eb9eedf88ce1e7268368461b6b4e10051148f436cb", - 0 - ], - [ - "000000000057d3e2a77eadb8b9613cb839ab02a96094dd5d0a6d1f09026c3936", - 0 - ], - [ - "00000000004e0a28be887d6ed037cd9102cbbda7d6c9e584ba51f2c2dce96232", - 0 - ], - [ - "0000000000211346d8099f7ecea72481c4cd45591f5e0d7e347725ac2162f142", - 0 - ], - [ - "0000000000199ae9fc06c5acee766db6033b86f76c266cadefe1461c611c2198", - 0 - ], - [ - "00000000004c9e5748558d4f5a75bc824171e3b958152dfd6844330f1e907f8c", - 0 - ], - [ - "0000000000137addf1521361dad1ee007eb9e6dd4eb8441492ebfaa3c240d556", - 0 - ], - [ - "000000000054d4c77bb7964e5327c35760d87b890ea336aec5ecdeb783350738", - 0 - ], - [ - "00000000006b7b06d04818e97a4df66164b471912f88d9cd02de4af6c8bbe74f", - 0 - ], - [ - "0000000000380fa9858e3e90335c061a3776a26bee1e8b6851de33ec63670782", - 0 - ], - [ - "00000000000842598b03fb79ce7386e9f9181a02dcf1effc8f70d3ff7368ccd5", - 0 - ], - [ - "000000000003d3475edecd733fc7b82432882d9c9f1350a98ef8921b87db4dec", - 0 - ], - [ - "00000000000000e330a8d57a38dbcc0b0a5dc7a4210f231b8082b9be5f9e4bce", - 0 - ], - [ - "000000000000218ff87fd50cfba2fd04203a78d2600cb2c4dcb039d803426e19", - 0 - ], - [ - "00000000007c96e6e3ed3146260348ac79ea7dc2ec2ae6bf8dc203400a37721d", - 0 - ], - [ - "000000005abaa10bf7260470c28ba32f1755b4cfd3734aad580681e39a9605a5", - 0 - ], - [ - "00000000005e77c226e6fffccafa56055e68f0ea0a30101e6a243ab9b3e07db0", - 0 - ], - [ - "0000000000e989fe27f85b89c1e852d7bc94b09033cc6c8b32fbbbd9383a9ae1", - 0 - ], - [ - "000000000091a1e962438583146293ef34156962445ffc5e81e4d0fe327d37ac", - 0 - ], - [ - "0000000000477978a6903217e2817d10e99bdfedb4f8bc396b96fd5b0b93b522", - 0 - ], - [ - "00000000000bfd9e5f13a9c03c48e8b58a937cf1ae2849160f1ca11f8fcced3c", - 0 - ], - [ - "00000000000158dd3c31b6379887b4353ef2898c03b7ce55458fcd57cb6f0639", - 0 - ], - [ - "00000000000029d7009eb56b9d38366005576b82a9b59fc845522a34ad36a38a", - 0 - ], - [ - "0000000000e6e207a82b8ad7136352204bb8e9ccfcd25885a715d3c65cbee997", - 0 - ], - [ - "0000000000fadc4429f50fc534ccac4db5e51a313df25034d6c5c25f7e83448c", - 0 - ], - [ - "000000000019c58defcfdab6c6ab9497685e61118effda4c2613bf44be19fcbd", - 0 - ], - [ - "000000000006cf444d846093c5045d42ddc0986ca805f261476d0fd2eb474c39", - 0 - ], - [ - "0000000000d0856a3d6a1e5b1ac7e388cc029bd8410b3b1489598974fe470568", - 0 - ], - [ - "00000000003d9aae63ed532b78082ca5386211e22410fd24ebd5318d1a4cd1da", - 0 - ], - [ - "00000000000345003879f86021a6d5e3fe93813246818c145947b7e225691177", - 0 - ], - [ - "00000000000175393730cde3e49de7af2b81ae736eee005a9f9c4a1e878c52ec", - 0 - ], - [ - "00000000000087a8c621c879aec2a897258632d6aa631b9a38ba4d564e08682a", - 0 - ], - [ - "0000000000002ea641b2975935bd9caf337b51ac9f9bb90a54f6ea6ee5d3112b", - 0 - ], - [ - "0000000000000c544f9b6a8cbab6d25caf949875622bf75139234850b10affe1", - 0 - ], - [ - "0000000000000f66fc4e37232a29f3389c493863a980d58a1d570eddd5268999", - 0 - ], - [ - "00000000001213fe2bbb8aacb1fc14983586e09db964151cb507956a81b35f25", - 0 - ], - [ - "0000000000ba82c2160602ddc1913bc4c133ad0af8848e014367c84110d00e05", - 0 - ], - [ - "0000000000b7a98b364b1cf9521275a915c7a1b3a0f0c052c7d8efb620ec0870", - 0 - ], - [ - "000000000047dc62db23540ab4aee43e54812aedb623a2a158aa3244fc784722", - 0 - ], - [ - "00000000005291002da10e53c3855882251a6e5a425b5e639ef9be3bd05767ca", - 0 - ], - [ - "00000000005ffbcbc0d9b380584bdc78050a6f0c3582b4c9c5103a150cbc71f5", - 0 - ], - [ - "00000000000a7a69cc06b0a68b27a8fa5d29727ec3b6db8d32d61cf7489b5ff3", - 0 - ], - [ - "000000000007212eb8c49758d98cefaa6098da2b877a6055be341f5f7c0ad301", - 0 - ], - [ - "000000000068d1099d8cf3f43f6d164f2925b1d52ede75640cc65ca020e1de1c", - 0 - ], - [ - "0000000008d5ddef4468a4414bd08184c2eba0ec536b85a743b1091828a6a884", - 0 - ], - [ - "000000000acae40db93b589783b0cde70b98552955cb3c12f08de1b417d9008d", - 0 - ], - [ - "000000000066a51eaa3a54036f338719da3d5779180c0bc3787b533410de90e5", - 0 - ], - [ - "00000000008b521677a6e897950aac69640e52efb01b7af10bba3820ecd09a89", - 0 - ], - [ - "00000000001823f0e399311cab0fcf57403e094feebf99b22030bafd2004da87", - 0 - ], - [ - "00000000000bf821c2abf5bcd00ca96439ddf5b0b593be5601145fda5338efdc", - 0 - ], - [ - "000000000003f4fd19b2af0141289177014ecc6dce6ea8fb50bab93d4a291095", - 0 - ], - [ - "00000000000011842d892a02e55ca594caddc9f3cea1979ddffefc070eda8498", - 0 - ], - [ - "000000000000208aa0259d20f51c0e7b8895e18a93aea79af9b3832e710ef134", - 0 - ], - [ - "00000000000007218f849e72dee1f7fb6fcf36f3b6745c6468187ed2ed13287f", - 0 - ], - [ - "00000000000f79f656cae641c2b74554c6ecd673c0c7550671c4c2af940661b3", - 0 - ], - [ - "0000000000199b4d178c05fd1c3154c9a4632eadc7bfc734c4522176c977ce8a", - 0 - ], - [ - "00000000085d0682d481635cb2e6de2e4d9884589455a86194f0b222f9acb3c6", - 0 - ], - [ - "00000000015972a5a6786a14b009bf582c4bbf7b9854591dd8d26f82b43ddaef", - 0 - ], - [ - "000000000064bf72b7bdbfcbe96dbbd0efcaf7aa94c0f92cb4e6662819468fe4", - 0 - ], - [ - "00000000003df36b7962bb4ad62266c462382eddc93f4bfeac464b95f7a89ee9", - 0 - ], - [ - "000000000006516d3a9f424eb61db5dfb85aeee29708b78c65d24827bd926263", - 0 - ], - [ - "000000000001c1709fe1b294712638db356e89155650f6fbecde79ec47a92af7", - 0 - ], - [ - "000000000000dfc23251344b593c16c28cd195abcb337519d7bc82175721a033", - 0 - ], - [ - "0000000000000aae2dd2bf0b8581d137fcfa3d9c4cadbe3ef3834d7cae4268c0", - 0 - ], - [ - "000000000000092a5baff3d9a5ae87689b2afe668e71bac3b342c7d383f0060f", - 0 - ], - [ - "00000000000fa906eeff7d2e126698d88b8cda01d32ea2c039c26984daaa17a3", - 0 - ], - [ - "00000000002d4315e5bdc2bcfdb245b914130764a50943a2b2e02ea3acf5c47b", - 0 - ], - [ - "0000000000fc2bc9bb83e04cbe922d64719295bfef6320027725402306bcf1a0", - 0 - ], - [ - "000000000142690e7c334b97612746d6db208e6153bdfa8479d86d1b575feacd", - 0 - ], - [ - "0000000000629a7820e8cdbbed18dcfe16c992152badc745ca73b9b34e53fb0d", - 0 - ], - [ - "000000000023c2e9dbf3fe03248e40f4ec3fb2dc81ac573d5a6a4f490c701877", - 0 - ], - [ - "000000000013658a43b6d1c4be95fa36e32d3edf80716de3a8f7e98858016adb", - 0 - ], - [ - "000000000007c847295d8c4b6da9d8a64b57c3a2307e64387bf8882b9d35d6de", - 0 - ], - [ - "0000000000032bf90b823332af80bd2ea18f411f081c7dca8f2fe79d9215526b", - 0 - ], - [ - "000000000000001bc0655da6f24c6952e811006897a0c6dd8b6bd94f178636c8", - 0 - ], - [ - "0000000000001e1d09b15393190cf686e25488db7fcbc2f1ebacc8165fe6e3a0", - 0 - ], - [ - "00000000000cc79ae066badb4157def4067057cefd705bf87f1d832845a7ab36", - 0 - ], - [ - "000000000014408398244b94b4eff6b54875802ede6df2d1d21915333a195719", - 0 - ], - [ - "0000000000114135a1bc757110c05162fa649b694db9569be117e34832c87257", - 0 - ], - [ - "00000000009b15fb2bcee1af904989ba0761e4cddc6b3ee214c0bb07dac6211f", - 0 - ], - [ - "000000000012be506dde2c54adf355bdb41a457b0abec436202a3be73f0b052c", - 0 - ], - [ - "00000000000963760ceb5fc65570650d494805e05c9d753f3ea6d44247ad3d08", - 0 - ], - [ - "00000000000bfec54977673f68b6fe5f088398e697d778fa7987f8bab6a70825", - 0 - ], - [ - "000000000000e7f428bb413c17032c0031af0d26133ba93f744a5a0c16cf7e1a", - 0 - ], - [ - "00000000000036bc80378323c6eaff8ab350b6d89955f602960cb7c93d2feb4c", - 0 - ], - [ - "00000000000f0d5edcaeba823db17f366be49a80d91d15b77747c2e017b8c20a", - 0 - ], - [ - "00000000001ff8fd57798082ab5a7452ada211e1c3be38745155505601498829", - 0 - ], - [ - "000000000020f960b535eac585e5810ad64f158c1142f0eecd925c8058172933", - 0 - ], - [ - "0000000000067bd89409368d221507a160e5c45972eeb01efe210054fe8e7d85", - 0 - ], - [ - "00000000003521f2d5ea3232d4835ca6c6bae083ba90458f67d4cd765ce93b09", - 0 - ], - [ - "000000000005ab3ff3a0c484eff7b571fb78ce27d93f77a480074232e5ce0c1d", - 0 - ], - [ - "00000000001048c9eca7cc1cbb86946c04498052071f7e7c775bba565ada337c", - 0 - ], - [ - "00000000000154caacde41be616f924d7d478812148242fba85605eefec9ac61", - 0 - ], - [ - "000000000000c34f75bd6f338c0206a31a8d5021cc2ded51e88a6ef4fe686d10", - 0 - ], - [ - "0000000000001e0581d86c49a6ca14ba88639ef908abb09210b57989e06b1a1f", - 0 - ], - [ - "0000000000d0e6dc0bf830b50bde3e400e16ec4f772f92a55390e62d4aa73af3", - 0 - ], - [ - "00000000069c2501a2f32cc69af72a602ff674438ae04dd05516f72a71b9ab26", - 0 - ], - [ - "0000000000c926b38954550c9b8d363ff058c2eb135eebdb3e640cfa67df803d", - 0 - ], - [ - "000000000011e9ad9c18e9e2095c3662af5be1e918dff653758583aa45dc8197", - 0 - ], - [ - "0000000000f311624ff4dcdf07400d0d2fec8b16b14c1c16babc377a2d85ad21", - 0 - ], - [ - "00000000002e455cabfdc2a8955e8ddfe717b12efe5b80937b0c0ad6ac977fc5", - 0 - ], - [ - "00000000000fed8889a22339b340f599ac7908e790bfc3cfca9b78078a52d228", - 0 - ], - [ - "0000000000012ca4492956b3f859b00e5db14b54d422cd95c68c7150743db365", - 0 - ], - [ - "0000000000004c58e8f7bac59eb4a036764a4d8e0da51c0290858ab14fb72481", - 0 - ], - [ - "0000000000002f60bc99563ff5b4b800c176fe8bde95e8f968fd6b53d74c9cef", - 0 - ], - [ - "0000000000000bffd10a3fb0b5b86d8b2561f39d07f8a4c41dfa08e3e49b7db5", - 0 - ], - [ - "00000000000006a296be9cd8fd4e3145c146863adbe08b71831abb8a869d032c", - 0 - ], - [ - "0000000000000c557f496e82891039ff22e277bd604be6e2e8b95e519bee91f9", - 0 - ], - [ - "0000000000399b30d2111c4bf3051c1f7f2f35bba7ff290d92393341ae47df55", - 0 - ], - [ - "000000001f88733439e4e8d3c474504aed62037faa16f3845b4c671f69732e26", - 0 - ], - [ - "0000000018aa2f93d2ab76a7e2f1bf5b565b4a1b0ececb6ee46490984f6c0d4b", - 0 - ], - [ - "0000000005e22674fcf65ce7be896a0557205ab26d1f76d73a717f5f14a6d6ad", - 0 - ], - [ - "0000000000223d866b324c097973210f8fc715c9535908359d61d8e1ab2f0100", - 0 - ], - [ - "00000000002b321fd6452ab43849bd7a781953ec4485554e0fdc579f2a52c90a", - 0 - ], - [ - "0000000000173132748c51b5754b0341232325bd118455bf3c8d25164d3eb92a", - 0 - ], - [ - "00000000000143158cdea5fbb9453bbe1a7a900e6feba1e2193e4f5c106d9fba", - 0 - ], - [ - "0000000000014677751456af5630025b3d9921a4eafb4d36a06498f0c6a84c56", - 0 - ], - [ - "000000000000243976cf2d30ecd3cb1fd0b805fba4da92d2758f78e1c6f8ae92", - 0 - ], - [ - "0000000000001323db1ab3f247bcb1e92592004b43e4bed0966ed09f675cf269", - 0 - ], - [ - "000000000000017a410c22c4b6caf710f5ccf005d644caf276ea8626a538798d", - 0 - ], - [ - "0000000000170b2b1374e3a0dfdce2fbc5e302e1e0e9fb419dc057c9959902d1", - 0 - ], - [ - "000000000015b4fad4d929630487680cda2d3aada138c58cc08241ef6dd4ab09", - 0 - ], - [ - "00000000000abebab869f1620843d413a3d9e06dc7d9f5201a414d547ace1f99", - 0 - ], - [ - "00000000000b0bdaf05c2fe8b12ebd2372f49d8eabcfbccdadd68b5e5b7c9565", - 0 - ], - [ - "00000000000ca1af42ee1be2c8895d94f39dab5fcdbe0b4b4065f4be534e7294", - 0 - ], - [ - "000000000069d0cc8c0452bf86cff87db05232f801a162acab2d080d6e4e9ea9", - 0 - ], - [ - "000000000019c7f7685f5bdc3afbb5e978cb3f4f70fea7b2b410139741303b53", - 0 - ], - [ - "00000000000d3874ce21db78f4d1883ad9ae8b26c1d7c13f3d723ff85629d595", - 0 - ], - [ - "0000000000033f87c25275ff72b58630d8da90221f2c84bcbd77c8e615709f8b", - 0 - ], - [ - "000000000000dc72adaaae6483eb6737de7d21b3a24b2426330e80b078ceaed1", - 0 - ], - [ - "00000000000002fb1337228db02ac464565271f22f045c1b6ee5e449f057a829", - 0 - ], - [ - "00000000000001902376ff640d3088899af0819dbd15f602156a13ac2fc8e94e", - 0 - ], - [ - "000000000000007ee49761a1c8284a3b8acefa39e37e455be4773d648e2db794", - 0 - ], - [ - "00000000000005b4d495a77f57018dbc72bf47993d494349329a3c653f04ab93", - 0 - ], - [ - "000000000000009dcb3ae6d68828e2f5ccfd58780abb260354e74484106f81ce", - 0 - ], - [ - "00000000a3ceb118021fb42d39be52db951c6f852bb9a241046e972706f7329a", - 0 - ], - [ - "00000000574e8e1c27fa54c77b4e7cd1b79de070f0d3ad5b383206ab9777d983", - 0 - ], - [ - "0000000039d562f640c1743421d53e7e04c3e8ba222c339fff6f3d25b1d4a7fe", - 0 - ], - [ - "000000000001cb1559d55c697871e18d5c26800f77fb11587241bfbec3b15e26", - 0 - ], - [ - "000000000006e01a93090319756c7ca826ef655feb0cc2ef9abcc59d67de5e5b", - 0 - ], - [ - "000000000000a81aaf5a4c013032638a077af6aad8bc449d74daef8ad3a74419", - 0 - ], - [ - "00000000000087d0574963c1582f2161298e2de5e48f74566291ef9afc2be24a", - 0 - ], - [ - "0000000000033251e71c347cd663945fb68efe82a8c6666c0b41e93f1c46658d", - 0 - ], - [ - "000000000000f592857e6f0e4711b5b93fdf95f2b21a5963bde15be750a07908", - 0 - ], - [ - "0000000000004353c8426e18b942a5012934ddac8322b86d6ab98ed7c0ee86ed", - 0 - ], - [ - "00000000004f027845b699f42e7d0d30c530e99524c5f97186ce6a250a5fac42", - 0 - ], - [ - "000000002fc6407edc060df90785082834867331e6746a43ed34a26fbdc5df64", - 0 - ], - [ - "0000000000048733007c91ea3665bd4e1653b10799e3f43abee0fe830ffbb3ad", - 0 - ], - [ - "0000000000025a9b1c5afceba0c78c4b0320797acdc1ad50b4e040f148fbff7f", - 0 - ], - [ - "00000000007ca6d026d27387edc1c5570de41c61bacbcb1dad2c0f300b49e637", - 0 - ], - [ - "00000000000258f683a77ad509da82a4fab24188fdb4b4690e212c50794a9abb", - 0 - ], - [ - "0000000000015111bce7b6ac13c930484e14e31e13e43355cb4d63c8f1782440", - 0 - ], - [ - "000000000001ca074fdecac7749d95f28f10c83a7e13787fd865bfbe505382bc", - 0 - ], - [ - "0000000000001c11a6505dd44ab405fdc07ddfc015f3c1166a5d9352ab58b52c", - 0 - ], - [ - "0000000000000c83f7f8e1cab4efa08d6c68c4555fb6ab542e01b87edd8f56ac", - 0 - ], - [ - "00000000000009561d0ceba15388573d2a994aff24512ec3ed7d7881aa0997dd", - 0 - ], - [ - "00000000007dc7cfbbb94db1fbc076a70a1252fd595686b4d75b2ea77ed6ee9e", - 0 - ], - [ - "00000000000251feb68a8c90852f73aeb29ebda191038737b7edd37c9475f4ac", - 0 - ], - [ - "0000000000013f9a97045ea9047654e514951288911b2c3986787c27bab49106", - 0 - ], - [ - "0000000006e8c37735c61f22bec69f4cb7eba03172349e7012b7704652f3e83a", - 0 - ], - [ - "0000000001f341add5657043d8e50e53ba079fe24966a2668f904be5579c84b9", - 0 - ], - [ - "000000000029a6275cd477d77939424bd183c2f1308a9912f45aa7cc9ed13b56", - 0 - ], - [ - "00000000000a0336239e5e1faedf5bd2eedf38c9a5ba34a832356aea70aeb102", - 0 - ], - [ - "000000000003c1a2b25093a64eb624055f6a3a26e18b8e7ea2d9382ec7a3609a", - 0 - ], - [ - "000000000001bd89bf7e8740ce22adfa6e8793bd1716a647e558ed1742ee8329", - 0 - ], - [ - "0000000000001320421f1bb2c94000e11a621f581fc277c0e2911c3b89f680bd", - 0 - ], - [ - "000000000054ce90a949f5ae2d43c4ace599668c6ccbc50620f6d5705922ea7c", - 0 - ], - [ - "00000000200d16fea4857e6b73169cc593421a57971acdbcaf87a31d7d8d72c8", - 0 - ], - [ - "0000000000e75602181c88f713b91c49de291ed878be305d25b75c0ec5fbe942", - 0 - ], - [ - "000000000081f8169c3c3665f20351dc0fe499612ae232ec0b55858a8e5dc6e9", - 0 - ], - [ - "0000000000d7ad232e7593fb435d125343b8113bbdb3705ab58ac0e18c26cc79", - 0 - ], - [ - "0000000000076df615d887e33193ca2dc0f2fc0e70744512c95da6242e9b1a81", - 0 - ], - [ - "0000000000084a62093d1929843e74456686429b698a7ea9b1901c1565779f58", - 0 - ], - [ - "00000000000251d1da01e9de9fcaf3ca3a64bff78a5faf51a8e697dfab6b5e4b", - 0 - ], - [ - "000000000000609a8798996b1f1fe0b66060a628eadc380d0d369a2318c2d0ec", - 0 - ], - [ - "00000000000014770aeab044a022e86d888a6ede75b6474022c71aead3a1db74", - 0 - ], - [ - "00000000000004101d04ebc90ade5d4b911aa13c038ecf25e9887d877203ddb8", - 0 - ], - [ - "000000007c700410b61eb7ff1aaccbfc3a79e4e4484ad7a2b0eda4d91dc4b613", - 0 - ], - [ - "00000000055ff438a031413ee042fd3c0a2b69be98690542806ff123b7988024", - 0 - ], - [ - "000000002eca5f9f2c3b656d2550662fdee4c95da133eade51a5cae653bc69fe", - 0 - ], - [ - "000000000c679b76ccf0c5b943095fdee8fa466311edbea2c4a05f9430ffef3f", - 0 - ], - [ - "00000000007c6f494e32d5d9de58fa008a770fdc0a7b4a141be5b7c2de3ab970", - 0 - ], - [ - "0000000000d5dcd5a26c8ad29c1293e70401e2f90d8288469df3816b8cc6d4aa", - 0 - ], - [ - "00000000000d754d94f36cacbfb620710672afb1558499cabe17ca62c54a7d3a", - 0 - ], - [ - "000000000004096bb78fba714b130f7f1f929e2803c75a7a85619f7a2b86567f", - 0 - ], - [ - "0000000000020e686c38d44c35896df35f9f1b7723a82a826a5e2393c25ef68c", - 0 - ], - [ - "000000000000504f9af6885c0cb6484109ea205a956c8efae9557a1f5b9233da", - 0 - ], - [ - "0000000000000e8746e52e4320ec17e66434a3936a3825f7046fe874e92275fb", - 0 - ], - [ - "0000000000000f48d818a9a026270c9f733f629959bea25192596d59874b1ce2", - 0 - ], - [ - "00000000eaa9214cb05b241828a1cfb0c4209fb7ea64429815d61f7c1d98939e", - 0 - ], - [ - "000000001f7f915a6002cce4edd5cba392307f3a199a520ee8937327a9135162", - 0 - ], - [ - "0000000009674ee0c606d687bdcddf8e023462927e2902b3381bc4bb862a7397", - 0 - ], - [ - "0000000001f3f3528c083a4b11eb2f04d8bbeca92b57f05d8282909bde78bc77", - 0 - ], - [ - "000000000131917ac459aefb91774dbb42caeca497afc0cfd1766e0338cc7f88", - 0 - ], - [ - "000000000027634444081e1289354cb50034a506bb306a2ac1d8280683771c5c", - 0 - ], - [ - "000000000017a852acff78fbee573329d45bb8b121e9f6fc1e4f687bb3778ada", - 0 - ], - [ - "000000000006789e1a00eca982fb2827f680b254c4a0ecb005af4464f3585a02", - 0 - ], - [ - "0000000000015d2e9f54b1e9419d6b32ce68ae626cdd7f2a1954f22ca39ae0fa", - 0 - ], - [ - "0000000000002f7893bc169165ed9fefb434b6201103f23cc84a747a68ff8797", - 0 - ], - [ - "00000000000008471ccf356a18dd48aa12506ef0b6162cb8f98a8d8bb0465902", - 0 - ], - [ - "0000000000000596f00b9db53c4111bcde16f3781471c5307af1a996e34ec20a", - 0 - ], - [ - "000000000000007b5d2406f64f5f5833c063a6906552e815e603140c00bca951", - 0 - ], - [ - "0000000093ca5d935740a1b25f10ce092fd777c2bb521f3156619389ae78931e", - 0 - ], - [ - "00000000292f3a48559527341f72400a0f8a783aebcaae5bfa0e390dfaa5286b", - 0 - ], - [ - "000000001e852ed7ddf0108d1fce0f4f686f43c8c1b85bcb12c43e564dc7630e", - 0 - ], - [ - "000000000c4bea8fb1e7f3a1f3e6c6b3f71388c0ec7eef3de381853767e89f87", - 0 - ], - [ - "00000000029ef31a21711b55c4300efa38ace0b706091e373f48285286f2c578", - 0 - ], - [ - "0000000000979060786bb008f193d3917e28667bb1b28329f3adadc172e4cce7", - 0 - ], - [ - "000000000019030ceb98013b1627517b45b04ee055ef445813bbebaa25fa1ed3", - 0 - ], - [ - "00000000000adf202247bb794fc9a3c82cd8767143f1e6ed5f60940ee18b09a8", - 0 - ], - [ - "000000000000b19061e2481d8be6183b3d881b0d58601072d2a32729435f6af3", - 0 - ], - [ - "0000000000007a6d34f59b29e8d4da53e51e3414acd18527466d064945fe19fc", - 0 - ], - [ - "0000000000002e66ca213a2c3e9eb5fa62de29feb83880a0bd29f90fca8ad199", - 0 - ], - [ - "0000000000000b4ca10aa100728d0928f37db5296303db1b74ffe29e4a17b6cd", - 0 - ], - [ - "0000000000000143309f6b19567955743775f61f8dc6932c0b46cf5fb11c6c72", - 0 - ], - [ - "00000000000000b04d5409b3ac60cc18c0b9a3d58b303594635a8f75a9d2abd5", - 0 - ], - [ - "000000000000040a2699f62a552703a278608248c2ce823f4cd8845376e9a371", - 0 - ], - [ - "00000000000005cfcb850db7e83d4963994f958bae9b1de1483f5aeb3d449925", - 0 - ], - [ - "00000000000190f80220e70c1481153671a7c90fd856988c183ab0e3d9313df8", - 0 - ], - [ - "000000009374563a06178641d06776f66554c2a094b5319f0801fe35cef72ccf", - 0 - ], - [ - "00000000003e4e6e5e8e4a89e7de50eed104d4a49d2992ff101b6740beec7cb5", - 0 - ], - [ - "0000000000618cd377d14aaa441cbdb92527894f98da316eca81664f8ab5488d", - 0 - ], - [ - "00000000000d977ab2897885fee712f58612fce8c10ffbe9400326fe3429b77b", - 0 - ], - [ - "00000000000c3575b487dd0f938c5bc744fa65ca4ca3a9c981b8bda903ec110b", - 0 - ], - [ - "0000000000247ac689595ed8d62678bfe53e5af13c0f5455e558f5e6bb375c16", - 0 - ], - [ - "0000000000093d175376aa621176511f335a48f824b66d998e8082f85134a48b", - 0 - ], - [ - "000000000000c0c0448fe922f2c737946297d35f2c25ad7cc223e11bbe58e1f8", - 0 - ], - [ - "00000000000016abe4e7c10ddb658bb089b2ef3b1de3f3329097cf679eedf2b5", - 0 - ], - [ - "000000000000242757cea5b68c52b83dd8c2eb9257492074fc69dfa30bd4cbf4", - 0 - ], - [ - "00000000000006813f3dd7726a509fbe3101835db155dfd35d44aeae6aedb316", - 0 - ], - [ - "000000000000053cc4f39cff1c8cee1aff7e289a85dee84164d2d981afc8f17a", - 0 - ], - [ - "00000000000000789724805cf1d37ef689acf52c47a460507f540d5e5ca79bfa", - 0 - ], - [ - "00000000000003d71618bb8952887f65540270a5e54d6246b9419e08831b5e4e", - 0 - ], - [ - "0000000000000251a513a33eadfad67c015f6e3b291dfd0ae1cc4bb3a43006dc", - 0 - ], - [ - "00000000968009e3f8d6e6071e7def68298307717a9af6c2d44986deaae297d5", - 0 - ], - [ - "0000000062bcacb734df83bbfa3e1b9a8dfa570ecffb6c29eaaf8e9498cccd30", - 0 - ], - [ - "000000001d4618c0931bd3c25ee592c35341f30ff3b549a671f637b3c26ef414", - 0 - ], - [ - "000000000418b329df96a004f1b652ad06a7ca295f9f2e711c412d00493f5a86", - 0 - ], - [ - "000000000302bfb88e9027237d023c4b969e106c9a7a23a84103776de7880836", - 0 - ], - [ - "000000000069b9f7d9134fd93c8b7e3af8b26bbcbb5553af02fb6ed644d7fca5", - 0 - ], - [ - "00000000000411ec444240ee91e2777ad18b80dee854e3e838e32209e84774fa", - 0 - ], - [ - "0000000000007c73f322eba4dee5463305227c7e1a8139f1b7b296444f265052", - 0 - ], - [ - "00000000000129adf0f9c0242aedbb9d87935d67ee4ddea758c00344d4b6a29e", - 0 - ], - [ - "000000000000343594e671158b6e1b4b6499f6ad66e2aeabf1f6d295d3dba850", - 0 - ], - [ - "000000000000320f0d5c22ba22b588b97a0e02273034bcd53669b1c8c4eeda1b", - 0 - ], - [ - "0000000000001e8cdb2d98471a5c60bdbddbe644b9ad08e17a97b3a7dce1e332", - 0 - ], - [ - "0000000000000026c9994ccdd027e86f51a2e36812f754bd855a7f9b1ca56511", - 0 - ], - [ - "00000000000002746a820a2c08b35b8d0493c4b5d468fcc971b9c88003e84849", - 0 - ], - [ - "000000000002949f844e92645df73ce9c093e5aac0d962a0fa13eb076eec835c", - 0 - ], - [ - "00000000000156fbda67468ae2863993b98a41227c420246e4bc4e68c84df0e8", - 0 - ], - [ - "000000000003b43c6c807122c8dd10e2a0cffbf72946f41c97c1ab82d416f74d", - 0 - ], - [ - "000000000004e0635c2438b1b649007e5d424b3de846299a8db53049ebf4da0c", - 0 - ], - [ - "00000000000258e4b79e3cca2ab7d12b35ba77fc491572f2e794f0a10f5236d9", - 0 - ], - [ - "0000000000f5816875d9fece105e499b0467b8fb23ea973c48d828a235acdebd", - 0 - ], - [ - "000000000001353bbaec810af7a4c74b4964ae072361c0889ed6d59cf16db6fd", - 0 - ], - [ - "00000000000b354d8c389473670ca6bed7e3dffa069f270d35ec9dad810af141", - 0 - ], - [ - "000000000002fa1f39e7cd8730fa08085ba2b532146ad1ef3b400a13e835ca36", - 0 - ], - [ - "000000000000d2c7943eee59652a9783bff27e474a92ec206c5c6e3cdd58d0d7", - 0 - ], - [ - "00000000000036034181b4d9a84a97490b49fbee4262b9cfb25a7bfc9c0eec9f", - 0 - ], - [ - "00000000000007deb59381cce692f152fc902732d96a7e7d463bc83915b37c0a", - 0 - ], - [ - "00000000ea7d32833462c0f72ade0cae4766e6065caa4e510331929c56d16632", - 0 - ], - [ - "000000000068fce0ddd370d4c8f9129a7bc7843e75fc57666202d3b90239e269", - 0 - ], - [ - "0000000026b4a2212c9c9493f8bd9d5331cab6d8eda8ee017410e58a783ca069", - 0 - ], - [ - "0000000009535ea2dc7e83c31cd17f1db1bb78b0a678fc0610844273de143bf5", - 0 - ], - [ - "00000000008607cbd5baca91d5b8b82ee965aace335744a3e21578af22bee8ba", - 0 - ], - [ - "000000000030dcedae0f5e98c4e176f9569ce76c4d4135bb028fc3144ef381d9", - 0 - ], - [ - "0000000000297c3f0e3fa85731222ba934a955bf513247a72a33c74c498cadbe", - 0 - ], - [ - "0000000000020a0d4a1e8120cbdb486e758b58919c9df12e0edc8ca1f2795e94", - 0 - ], - [ - "000000000000078773afc9023182bfb6534a60158672e6bc6e8aa5052854da80", - 0 - ], - [ - "00000000000102ecdd67800807d9e137357805b9bbf8a439ed86bde5b19fbeb7", - 0 - ], - [ - "0000000000005c3d2e3c7ee737c67ab465533acb233e0df902c1525fc11c3a55", - 0 - ], - [ - "0000000000001a77771650cdbbceff87caa4461391ba6a4ddc9815b5b0ab47b0", - 0 - ], - [ - "000000000000071ec390bbd28fa2a84e52ab5b32901d0723d22646b04ae01dc3", - 0 - ], - [ - "00000000000005c3ec3194f710c6f26ee736d59cc935ddfa574440f39846433a", - 0 - ], - [ - "00000000000001cc3df6924591939269d61ead563b9eb68402a2ca01d7ff99e2", - 0 - ], - [ - "000000008c778b3554ceaf3a13a856acbfe46b5750fb86fd92ba30651c2852f4", - 0 - ], - [ - "00000000107ca31f75f8ea76073dda3c33330b2706c1ec20c3ec240e853b65c5", - 0 - ], - [ - "0000000006ba99b08e7f2869ce113e2ad7464891de7b4cfa96f330d706a2da46", - 0 - ], - [ - "000000000f31036bd51b2818f6dfb90ada9be5019abf55fb15694b181e269865", - 0 - ], - [ - "00000000004fcc101bc47eb7a379b9f608d5c00ac04d2d0ea165ae2937070796", - 0 - ], - [ - "000000000044d5ca3eda838edef0df7e69e1934047f8482822ce58ff7a18466d", - 0 - ], - [ - "000000000029bdfb157be6d400c4dd3370d98afdd8cd3db6f1ada8c19bbf4650", - 0 - ], - [ - "000000000005e9699ad8035caa4f73af781ac2040c87b8aa77459b3607209aa8", - 0 - ], - [ - "000000000001c0ba033f7d85beeaa167c9bde0e192240653a7ff6d9b81679842", - 0 - ], - [ - "0000000000000e0176111f29e800b49c7b8c7226dbbf4df715f1a4f06bcaaa49", - 0 - ], - [ - "00000000ac3bb2cf42192e9053f5384355228a2b3d70b4ece4d45773a5d5ddd2", - 0 - ], - [ - "000000000f29f7b60842b1044b2db7998e9bcbd92f8ec6fe8d159c6d582f1f1a", - 0 - ], - [ - "00000000352f86bc5f9760961a25de009940508bb2cd0b37f378fbc87dc97eef", - 0 - ], - [ - "000000000e9b3086008679ed57f59857f64c3954368ba1088117dbf88d5839cd", - 0 - ], - [ - "000000000015324bd8fed0e61b62bd1d6c663b862cb98ea03c494a92e4a8d0af", - 0 - ], - [ - "000000000020475a181b7a084b341860a72fc0c1fdfcc13a85adeb0471444b0f", - 0 - ], - [ - "0000000000031905c508a975707b74f24e733880382775ee0e6250666473e1d8", - 0 - ], - [ - "000000000000ca38b15d2ea33a6eef505a9c661540a18882f79ba9a3c575a9bd", - 0 - ], - [ - "000000000002739979a7a89fa279303b6606885e750b19e91ed637d7f222b392", - 0 - ], - [ - "00000000000091e935fc266facc2c92759d5468a39aee5be6b76b519a9bc7567", - 0 - ], - [ - "00000000000006e339938254208203b67c3c400f703fc29535fc646699e36e58", - 0 - ], - [ - "00000000000008f6f1d1150d77f93a7f1baa24b65ceb471b1825b2e92ca6997c", - 0 - ], - [ - "000000000000004894e1edcc5421dbcec77d47c5c50bf27b2cff3f1c242c9eb3", - 0 - ], - [ - "000000000000054e97fb5e1a8bd7900f7c329385895761aaa40d11b3c75b0c8e", - 0 - ], - [ - "0000000000000600f4bcc5a89527eede43d1d3342dc12eee1371ab534b0102dc", - 0 - ], - [ - "00000000d1ad5c3ef8c3bb4610b34c264e4ca1ea51c4c8bac18b215e7dc96948", - 0 - ], - [ - "0000000062f6a07ae11f9724b8ba9dc2b7348ffd02b59edd3cd2bf387fab9723", - 0 - ], - [ - "000000000014e4c97c9b09ff20203213f3336b0927fd19d214cef1f544756e39", - 0 - ], - [ - "0000000000d004681880e127aed3fa73255a2e75c2e5c8580cd555526614b294", - 0 - ], - [ - "000000000008093189bba28d40662d6964afc1c0fc9b5c1681bbe32e8bee6c0b", - 0 - ], - [ - "00000000002df10cf8165b2204ef4db6721c8c2119d60463b040fbc81c266bbf", - 0 - ], - [ - "00000000000c28c789e7cd9800b98c1dd32e2dda54048116ff47ed856a14acfb", - 0 - ], - [ - "000000000003e8e7755d9b8299b28c71d9f0e18909f25bc9f3eeec3464ece1dd", - 0 - ], - [ - "0000000000004b95a0103abe2cb97806caca76f6922d9c5df003cf4a467df822", - 0 - ], - [ - "0000000000005f12d2ab72bfa715860444c281265ef77e09dc2d041ce89506c0", - 0 - ], - [ - "00000000000016eeedb3f367daaee93334188db877fb01cd0282b990f60812b3", - 0 - ], - [ - "00000000000001daf3bd8306b6f6899af8aa656d87ac2aa37d493fdcb0cb3000", - 0 - ], - [ - "0000000000000390b86892ad0bed9b520783056961cad7362ace8049aa00471c", - 0 - ], - [ - "00000000000002105d01b4de7d3e3ada9c757a239151d50b5dd193e3951a23cc", - 0 - ], - [ - "00000000000002362fa802df308201a4b1fff2fd8a91892915a46f5d54098ff4", - 0 - ], - [ - "00000000000004fb8aa6c6aecb64b9d8d7e691a6cd56fad69fc5278b9e8d98cb", - 0 - ], - [ - "00000000000000ce3bd9752b2508ddae1ee71332e905163a3c0d7e10b8c472f7", - 0 - ], - [ - "00000000000002d0d8520982f15a45d4a405334c61886b6d13d95843386af647", - 0 - ], - [ - "00000000cafd25502ad67d5d409edfc98f5bbd3173e86e085c69658d58da5f70", - 0 - ], - [ - "00000000b01e0675317a29a07731ea092fa029016a40ed8bb4fc17cde50eda05", - 0 - ], - [ - "000000002676805396ed2883ccc8ad401aa0a974627559fbae2416ba5c54999c", - 0 - ], - [ - "0000000000030ab759158f3d425824228dc5c91f32db91d404bee29ee3a41878", - 0 - ], - [ - "00000000000da1c8040ec08e7490fb201ca1fb3571f29c0efd3351ae197d3017", - 0 - ], - [ - "000000000004e3cba890c16ffc7d1c019d4ab88afa39315164e1b08b8e6a9330", - 0 - ], - [ - "00000000000bdcfb630b43977be44529e54daa02d199014a0967deac669bd060", - 0 - ], - [ - "000000000007254038f9c621d6df0d9fbd90b5697e4170cd6090daaf579f3790", - 0 - ], - [ - "000000000002263e27ea1cec943632bf469a28b067f0bfde3b9a6b48540981b4", - 0 - ], - [ - "000000000000f194a8d17e683d17f222d23a9032f034d4dc4497263fd785dfa0", - 0 - ], - [ - "00000000000036e359b7b07044e3cd5b132a3c72501a0f3f9ccde167f5316bba", - 0 - ], - [ - "0000000000000b10e98a90e0fd1ffbf7d5fc5a76e8e6e960c6fb158711af6f48", - 0 - ], - [ - "0000000000000104e1e4303b8dae78389bb4e6c38f3eb3fe42aec6464bd5c397", - 0 - ], - [ - "00000000000000bde368a635921f5ad25aeb4b784651de24d624cf20c27691c7", - 0 - ], - [ - "0000000081a626a33cff134e7e56dc0f0a67b1735c96256774885d5d095807c0", - 0 - ], - [ - "0000000055d357cdf39130eb767f416101e79025515906bea528f43cb6446920", - 0 - ], - [ - "0000000012558b30f9c1a156fd80b02451e8dfcc7fe0350fb4adeeb84951a0a6", - 0 - ], - [ - "000000000001a4868924fc7cca0334ffc4dd49c07fb841c1da059a7c219bdf95", - 0 - ], - [ - "00000000000010086bd2bba88c71b08cfc7e24183d610a2803e6d382049d52c0", - 0 - ], - [ - "0000000000018c83992fe05d820b097228de93787e3f59e65cb89ad4c385e364", - 0 - ], - [ - "00000000000023ab80324770ff4c6802d09e5e1e7de78d2a8e64783904d47f19", - 0 - ], - [ - "000000000000287fa294ea557835d8c98bfe94c4d8b18d5b10f1b62d68957113", - 0 - ], - [ - "000000000001d842f5a0dff13820ba1e151fd8c886e28e648a0be41f3a3f1cb3", - 0 - ], - [ - "000000000000906854973b2ec51409f0b78b25b074eef3f0dbb31e1060c07c3d", - 0 - ], - [ - "00000000000009e694e22b97a4757bffef74f0ccd832398b3e815171636e3a85", - 0 - ], - [ - "0000000000000594b95678610bd47671b1142eb575d1c1d4a0073f69a71a3c65", - 0 - ], - [ - "00000000000002ac6d5c058c9932f350aeef84f6e334f4e01b40be4db537f8c2", - 0 - ], - [ - "00000000000000c9a91d8277c58eab3bfda59d3068142dd54216129e5597ccbd", - 0 - ], - [ - "0000000000000051bff2f64c9078fb346d6a2a209ba5c3ffa0048c6b7027e47f", - 0 - ], - [ - "000000000000df3c366a105ce9ed82a4917c9e19f0736493894feaba2542c7cd", - 0 - ], - [ - "0000000000007c8006959f91675b2dbf6264a1172279c826ae7f561b70e88b12", - 0 - ], - [ - "0000000000015ab3720de7669e8731c84c392aae3509d937b8d883c304e0ca86", - 0 - ], - [ - "0000000000016d7156ee43da389020fb5d30f05e11498c54f7e324561d6a6039", - 0 - ], - [ - "0000000000009c9592f83d63fe39839080ced253e1d71c52bce576f823b7722a", - 0 - ], - [ - "00000000003dee6b438ddf51b831fbedb9d2ee91644aaf5866e3a85c740b3a99", - 0 - ], - [ - "00000000000155f5594d8a3ade605d1504ee9a6f6389f1c4516e974698ebb9e4", - 0 - ], - [ - "000000000001e21adfc306bf4aa2ad90e3c2aa4a43263d1bbdc70bf9f1593416", - 0 - ], - [ - "0000000000008218e84ba7d9850a5c12b77ec5d1348e7cbdfdcb86f8fe929682", - 0 - ], - [ - "00000000000054fb41b42b30fff1738104c3edca6dab47c75e4d3565bc4b9e34", - 0 - ], - [ - "0000000000002763b825c315ba35959dcc1bd8114627949ede769ac2eece8248", - 0 - ], - [ - "00000000000007437044da0baed38a28e2991c6a527f495e91739a8d9c35acbb", - 0 - ], - [ - "000000000000032d74ad8eb0a0be6b39b8e095bd9ca8537da93aae15087aafaf", - 0 - ], - [ - "000000000000006d4025181f5b54cca6d730cc26313817c6529ba9ed62cc83b3", - 0 - ], - [ - "000000001c3ad81ffea0b74d356b6886fd3381506b7c568f96c88a78815ede09", - 0 - ], - [ - "000000000140739d224af1254712d8c4e9fb9082b381baf22c628e459157ce49", - 0 - ], - [ - "000000000306491c835f1a03c8d1e17645435296d3593dacba8ab1a7d9341d38", - 0 - ], - [ - "000000000002b383618b228eb8e4cfcf269ba647b91ac6d60ddd070295709ad1", - 0 - ], - [ - "000000000000c90fc724a76407b4405032474fc8d1649817f7ad238b96856c6a", - 0 - ], - [ - "0000000000002d5a62b323a5f213152dd84e2f415a3c6c28043c0ccaaddb3229", - 0 - ], - [ - "0000000000008c086a21457ba523b682356c760538000a480650cd667a29647a", - 0 - ], - [ - "00000000000007c586d36266aa83d8cc702aa29f31e3cc01c6eeac5a0f5f9887", - 0 - ], - [ - "0000000000013bf175e35603f24758bf8d40b1f5c266e707e3ba4de6fae43a7f", - 0 - ], - [ - "00000000000096841c486983a4333afb2525549abe57e7263723b9782e9cfef1", - 0 - ], - [ - "00000000000012dfd7c4e1f40a1dd4833da2d010a33fc65c053871884146c941", - 0 - ], - [ - "0000000000000b47eb6bc8c6562b5a30cefcf81623a37f6f61cc7497a530eb33", - 0 - ], - [ - "0000000000000021ca4558aeb796f900e581c029d751f89e1a69ae9ba9f6ebb3", - 0 - ], - [ - "00000000000000a5bf9029aebb1956200304ffee31bc09f1323ae412d81fa2b2", - 0 - ], - [ - "0000000000000046f38ada53de3346d8191f69c8f3c0ba9e1950f5bf291989c4", - 0 - ], - [ - "00000000658b5a572ea407ac49a1dccf85d67d0adfc5f613b17fa3fff1d99d51", - 0 - ], - [ - "000000005d6be9ae758c520b0061feee99cd0a231f982cc074e4d0ced1f96952", - 0 - ], - [ - "0000000001aa4671747707d329a94c398c04aaf2268e551ac5d6a7f29ffd4acd", - 0 - ], - [ - "0000000004b441b97963463faca7a933469fabfa3e7b243621159e445e5c192a", - 0 - ], - [ - "0000000002ce8842113bc875330fa77f3b984a90806a5ec0bb73321fef3c76c6", - 0 - ], - [ - "0000000000019761bf9a1c6f679b880e9fb45b3f6dc1accdbdcfce01368c9377", - 0 - ], - [ - "0000000000008a069efd1a7923557be3d9584d307b2555dc0a56d66e74e083e1", - 0 - ], - [ - "000000000001c14cec52030659ef7d45318ca574f1633ef69e9c8c9bd7e45289", - 0 - ], - [ - "0000000000009cfccb8a27f66f1d9ff40c9d47449f78d82fee2465daca582ab7", - 0 - ], - [ - "0000000000007f30cfae7fbb8ff965f70d500b98be202b1dd57ea418500c922d", - 0 - ], - [ - "0000000000002cbd2dbab4352fe4979e0d5afc47f21ef575ae0e3bb620a5478a", - 0 - ], - [ - "000000000000017a872a5c7a15b3cb6e1ecf9e009759848b85c19ca6e7bd16d2", - 0 - ], - [ - "00000000000001ade79216032b49854c966a1061fd3f8c6c56a0d38d0024629e", - 0 - ], - [ - "0000000000000090b8dfe4dde9f9f8d675642db97b3649bd147f60d1fc64cd76", - 0 - ], - [ - "0000000000000109ed5f0d6fc387ad1bc45db1e522f76adce131067fc64440ec", - 0 - ], - [ - "000000000000003105650f0b8e7b4cb466cd32ff5608f59906879aff5cad64a7", - 0 - ], - [ - "0000000000000113d4262419a8aa3a4fe928c0ea81893a2d2ffee5258b2085d8", - 0 - ], - [ - "00000000000000f15b8a196b1c3568d14b5a7856da2fef7a7f5548266582ff28", - 0 - ], - [ - "0000000000000034fb9e91c8b5f7147bd1a4f089d19a266d183df6f8497d1dff", - 0 - ], - [ - "000000000000005e51ad800c9e8ab11abb4b945f5ea86b120fa140c8af6301e0", - 0 - ], - [ - "00000000000000e903f2002fd08a732fd5380ea1f2dac26bb84d57e247af8ac2", - 0 - ], - [ - "000000000015115dac432884296259f508dae6b6f5f15cef17939840f5a295c3", - 0 - ], - [ - "000000000029913c80e5f49d413603d91f5fd67b76a7e187f76c077973be6f8a", - 0 - ], - [ - "00000000002e864e470ccec1fec0ca5f2053c9a9b8978a40f3482b4d30f683a9", - 0 - ], - [ - "00000000001ccf523df85df9abdb7c5bbad5c5fcbd12a4a8eb4700de7291f03b", - 0 - ], - [ - "00000000002aa81027df021e3ccde48dff6e7f01a4aba27727308f2ce17f2f1a", - 0 - ], - [ - "000000000015a577d71d65bde7e8f5359458336218dc024584f7510b38dc1259", - 0 - ], - [ - "00000000003aef1877bcc6817cac497aeb95af3336ba2908e8194f96a2c9fc29", - 0 - ], - [ - "00000000000ccd42d542ddca68300ec2a9db2564327108234641535fd51aa7f3", - 0 - ], - [ - "000000000000a2652b2e523866f3c4d5c07dc1c204d439b627f2ab2848bfa139", - 0 - ], - [ - "0000000000002c065179a394d8da754c2e2db5fed21def076c16c24a902b448d", - 0 - ], - [ - "000000000000175a878558186e53b559e494ce7e9f687bf0462d63169bfcce03", - 0 - ], - [ - "00000000000007524a71cc81cadbd1ddf9d38848fa8081ad2a72eade4b70d1c1", - 0 - ], - [ - "0000000000000159321405d24d99131df6bf69ffeca30c4a949926807c4175ad", - 0 - ], - [ - "000000000000016c271ae44c8dca3567b332ec178a243be2a7dfa7a0aef270c3", - 0 - ], - [ - "00000000000000a7d62de601cdf73e25c49c1c99717c94ffd574fc657fd42fa8", - 0 - ], - [ - "0000000000000052d492170de491c1355d640bae48f4d954009e963f6f9a18c3", - 0 - ], - [ - "000000006f5707f2f707b9ddcce2739723e911210b131da4ca1efdff581212ad", - 0 - ], - [ - "00000000021be68dc9c33db0c2222e97cd2c06fc43834e8f5292133c45c2abb4", - 0 - ], - [ - "00000000019ca3eaf7c39f70a7a1a736f74021abf885bebc5d91aa946496bac5", - 0 - ], - [ - "00000000006e4752fbe2627ebb2d0118f7437908a8219f973324727195335209", - 0 - ], - [ - "00000000038471612a0955307f367071888985707ec0e42c82f9145caed8fea1", - 0 - ], - [ - "000000000004604d2d7d921b21d86f2ade82ded3af33877ec59d47072023d763", - 0 - ], - [ - "000000000034a3e45665a8dcbb94e7a218375a5199b3f3ca2cc7b5fe151bb198", - 0 - ], - [ - "0000000000043fb2c2ff5db60c6d2d35a633746e8585e04a096a9b55a4787fe6", - 0 - ], - [ - "0000000000020d4d8735b66134c1fcdd1d3f3d135b9ff3f70968ef96c227fb75", - 0 - ], - [ - "0000000000004f3f4dc1fa11a6ad9bd320413b042eb599c4599a14d341f6825f", - 0 - ], - [ - "0000000000001e0a495d23acf46a44f8b569ada39ac70730da5e9109871b77e9", - 0 - ], - [ - "00000000000002257a08acca858f239fabb258a7cc1665fc464f6e18e9372d32", - 0 - ], - [ - "00000000000002845d416fbfa05a5d40ba5ba5418a64f06443042a53cf1fd608", - 0 - ], - [ - "00000000000000fee91a2ae8b8d1bb9a687c9b28b0185723c8ff6ffdac2e9ce4", - 0 - ], - [ - "00000000000001d6874b4d88e387098c0b7100ff674d99781fc7045a78216a15", - 0 - ], - [ - "00000000000144a03e701c199673d72fc63766bcf0cdaf565f4c941c7ef72971", - 0 - ], - [ - "000000009b6cc4d8aee22cca6880e4d7bb30bff2851034ad437d63d3a7278de7", - 0 - ], - [ - "0000000023e998d64618475e31b4aee9d83d2bc32cb6d062aa97c0b4651fed08", - 0 - ], - [ - "0000000000036f4bf6b42a7776a97872fa24362064c5bc4bc946acb70ab6fbf4", - 0 - ], - [ - "0000000001e2252455ffd0cf0b4109ace996a0d2a03999f5cc5c5e08fb6130ac", - 0 - ], - [ - "0000000000002713db42d53f0c2d86c904f4e0338652acc1cbda953c530a15bb", - 0 - ], - [ - "000000000001b075f9ccc604a50326732f5d42373c4a831978be0e2d830cac75", - 0 - ], - [ - "0000000000000bfa7d93c6b36298b933b1a652c95ee9f0de4151e007f3180391", - 0 - ], - [ - "000000000002c60a0af1cfeb9c26c60970b354897fd0a94c8e5c414d0767b06b", - 0 - ], - [ - "0000000000001f2d9462507a9408859fb0b5f97013d6b4577337b0382340c5aa", - 0 - ], - [ - "0000000000000b7428e0d3c6c7fd2df623a74125db4989b1c61c78eeed1bcde5", - 0 - ], - [ - "00000000000002e8b4f1fa041a37515c1b76d59994792f1c772c9a4993c194dc", - 0 - ], - [ - "0000000000094e70c0cf5185b480542a1faa8392a3f2f7f583d91e033856d7ce", - 0 - ], - [ - "000000005b036d8c18ed5d1219e4137bd71438c9b1ba7ff4d10a626e9a7bcc98", - 0 - ], - [ - "0000000008745d4a943e958f5cb5084646c0fe1cae57eeab666c3ad0d4ff1dec", - 0 - ], - [ - "00000000000f8c5b3455e540d074b5c71709e37f8950975953798d27bdc701fa", - 0 - ], - [ - "0000000000050885884f7ac233bb174cf7b33c037f81907f7766afe9d0ad9091", - 0 - ], - [ - "000000000002d7cd1043ccd0581a47d6fdf82a7cf1646b61495f917a48ebeb5c", - 0 - ], - [ - "000000000003a2b3e3d7ef47829db1672bfd79e49f32ef3a04ec7c4df355392b", - 0 - ], - [ - "0000000000032a6c7e5bc3878c1815bc6759594a4736638fdacaa5642be3e649", - 0 - ], - [ - "000000000001386a3904f0ba4f25dc7ace09b67a6fe8977e7aecc55813fa9ac5", - 0 - ], - [ - "0000000000003fe030a2231da87076679c1d38d323bf56b45ceb49a5128fb4b1", - 0 - ], - [ - "000000000000147cd3b6195c6a727cd4fe6b3a879d7934e52bf29020ed9c6fcc", - 0 - ], - [ - "00000000000003ed5a0a7176f3f1b3ed26510045af2860e5b6313b358774fbad", - 0 - ], - [ - "00000000000000c2952ac8a580895ac13799a9c29badb6599bc4a86c1fc83b6e", - 0 - ], - [ - "0000000000000056f49d6f7b8243eecf6597946158efe044b07fd091398e380d", - 0 - ], - [ - "000000000000006b039683c36b18ec712346521edce4dc5b81cdaf6475d89bd7", - 0 - ], - [ - "00000000000000525de83fba2439549ef0ed78d6d08516a0513abb972b0fca95", - 0 - ], - [ - "000000000000006c5403ae9c42acf37362885c75c1a71a6b7fe20f9cfc5304a7", - 0 - ], - [ - "000000000000006f881a62bc5ec9d4c4da83ddc6619a7eee82617e26e2c7ef3c", - 0 - ], - [ - "000000000000012941300197c5b6627a66f9cf48ae9c6791b36c63c0218a1be9", - 0 - ], - [ - "00000000000002cd7ec2e00992a4dc6c5e0a56cfbc19b5afa9730bd94f174b5b", - 0 - ], - [ - "000000000022e09ee2ee7b3fd223cb9ccfe11058cca5ad0c705fe5a0c26b28dc", - 0 - ], - [ - "0000000007d35ebaf81412d40d1224bdc5792bfbc70827c09f05dc5fb168e67f", - 0 - ], - [ - "00000000328e1b1aecf68947ad53fb11c58a383704ddbb8b29704669e22225bd", - 0 - ], - [ - "000000000003d3b3f171fd10fda1be9d4464b1438bb9443081c2c224a047cc4e", - 0 - ], - [ - "000000000001e3c5dcea0586d3c8f69c0f35658fae283d29f64df9b5301bc721", - 0 - ], - [ - "00000000000ce5f3757a0cab09a8cb131b3f2c63303375ad1c84fe423866d33f", - 0 - ], - [ - "00000000000ca01b96070fb643bcebbc862cff4da78dcd52de1418c940d4f466", - 0 - ], - [ - "0000000000006eb74e5036cf42888759c4ebf91a5eb128463e60ae9ab02876a3", - 0 - ], - [ - "000000000003aae0765dfee956b322477d786a2cde617ff073e0bc4eeaf7c252", - 0 - ], - [ - "00000000000033421d804b4bc0f7dc61715d2fc0cc2a98904ff5e1f9ef909010", - 0 - ], - [ - "0000000000002a24b916b5f03bd47250276ad32f08a1684334c7f181b0b7a055", - 0 - ], - [ - "00000000000002a7399ec806255c4ae63d7583001bbde70e2038e9b90fb824f4", - 0 - ], - [ - "00000000000000ec89aaa13c7222b3ec787a487cdc7a17c1ee87ce313e6ed4d3", - 0 - ], - [ - "00000000000001564cf9db3397bd0983a68f450d5b7e59824339fe1d46ba1c75", - 0 - ], - [ - "00000000000e932953388774b6b3492d8756f936d74fda1d33eace33538fb0bb", - 0 - ], - [ - "0000000084c2d56f703e72f6ad637105409552792ee482bbc14376cfb29c30d9", - 0 - ], - [ - "00000000392f30ba333fac2e4937e162105ba2b20fe953848b1a4c004f460223", - 0 - ], - [ - "00000000000842b42c56e4dc573efd9b6b6864dba81730c4f95b837d52078ad5", - 0 - ], - [ - "0000000003e4cca12f6109687fcccfc5c3827bf3bca2487096fec0293b4b351e", - 0 - ], - [ - "00000000007b7eece3ebbf77ed583a711c8427284ea9b556ec67efd14e7f5d90", - 0 - ], - [ - "000000000002c0e026657401be7998fce1618869ec073a49ac935a15d16c5741", - 0 - ], - [ - "00000000000cf19ef67151f6d06b426371dfa63d9d2bbd6024cca520cf4d96b4", - 0 - ], - [ - "0000000000019a6ef183423833a4347d77e8687b4fc83a85f4c98c579631acbe", - 0 - ], - [ - "000000000000a292b9ff43becd4770243d2750e2b3c4e81a6ed79b8abd2f5052", - 0 - ], - [ - "000000000000280db4a9a31097024bc81f0358ba624f1f8dd83a2362a156a817", - 0 - ], - [ - "00000000000009b17b295d898cda8899ce547183fd63fa901b9f502aed00c45d", - 0 - ], - [ - "0000000000000013f5c40f6b0e7e8fe854045135564a4df6ff4ca736861d7ea8", - 0 - ], - [ - "000000000000c39ffca7d1daad0d4f8af9ee108443bb1b4352cd740fd8297aef", - 0 - ], - [ - "000000000002f42ee90d7d459393eb90e2ea5a3ed292394ce1dc5f7a42d66ce0", - 0 - ], - [ - "0000000000010d6bd31805e0a9b8629192c0ad704641d2b08c28865052bbf469", - 0 - ], - [ - "0000000001015f5067612dc0d681d71b33d278c50ca88d7756322ab90f753290", - 0 - ], - [ - "000000000003dadd324301ee6157c29e7aa9f120edefaf05369d849510e6d60c", - 0 - ], - [ - "000000000000a62107ea11c5db9929d819181d8903624e9088b8700d1dc66ea7", - 0 - ], - [ - "00000000000022b91e1b652f626cd3a81bfb2ff70717ace53c488dd45c75fcbb", - 0 - ], - [ - "0000000000002845027a6a08c436c6e99aa8af0f7c744a722fd598ba0f66f4cb", - 0 - ], - [ - "000000000000ae5347baecbcb3cd01265f0e52c8819f830dcfc6dafa1ec4327a", - 0 - ], - [ - "0000000000008dd3169522647ae90ca0a3acc405f0e8c2b53dab013433708921", - 0 - ], - [ - "00000000000023abea5dd709951fb1fa5c34a75670ddc7eea46d2d23c6033669", - 0 - ], - [ - "00000000000006fe20edd4be3beabc4432fbe410ab53466660105ced53056190", - 0 - ], - [ - "000000000000003f6d6889d2917ba88f6e286c156028baebf05be409e1b97ef8", - 0 - ], - [ - "000000000000005d871f102aaa25e60855c96c1aa8404f004db1c8bbfab341e9", - 0 - ] -] \ No newline at end of file diff --git a/eclair-core/src/main/resources/electrum/servers_mainnet.json b/eclair-core/src/main/resources/electrum/servers_mainnet.json deleted file mode 100644 index cccf839ed0..0000000000 --- a/eclair-core/src/main/resources/electrum/servers_mainnet.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "electrum.acinq.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "helicarrier.bauerj.eu": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "e.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "e2.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "e3.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "e8.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum-server.ninja": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum-unlimited.criptolayer.net": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum.qtornado.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "fortress.qtornado.com": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "enode.duckdns.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "bitcoin.dragon.zone": { - "pruning": "-", - "s": "50004", - "t": "50003", - "version": "1.4" - }, - "ecdsa.net" : { - "pruning": "-", - "s": "110", - "t": "50001", - "version": "1.4" - }, - "e2.keff.org": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "electrum.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum3.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum4.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum5.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - }, - "electrum6.hodlister.co": { - "pruning": "-", - "s": "50002", - "version": "1.4" - } -} diff --git a/eclair-core/src/main/resources/electrum/servers_regtest.json b/eclair-core/src/main/resources/electrum/servers_regtest.json deleted file mode 100644 index 265bf5abf7..0000000000 --- a/eclair-core/src/main/resources/electrum/servers_regtest.json +++ /dev/null @@ -1,10 +0,0 @@ -{ - "127.0.0.1": { - "t": "51001", - "s": "51002" - }, - "10.0.2.2": { - "t": "51001", - "s": "51002" - } -} diff --git a/eclair-core/src/main/resources/electrum/servers_testnet.json b/eclair-core/src/main/resources/electrum/servers_testnet.json deleted file mode 100644 index 3fffeaa388..0000000000 --- a/eclair-core/src/main/resources/electrum/servers_testnet.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "hsmithsxurybd7uh.onion": { - "pruning": "-", - "s": "53012", - "t": "53011", - "version": "1.4" - }, - "testnet.hsmiths.com": { - "pruning": "-", - "s": "53012", - "t": "53011", - "version": "1.4" - }, - "testnet.qtornado.com": { - "pruning": "-", - "s": "51002", - "t": "51001", - "version": "1.4" - }, - "testnet1.bauerj.eu": { - "pruning": "-", - "s": "50002", - "t": "50001", - "version": "1.4" - }, - "tn.not.fyi": { - "pruning": "-", - "s": "55002", - "t": "55001", - "version": "1.4" - }, - "bitcoin.cluelessperson.com": { - "pruning": "-", - "s": "51002", - "t": "51001", - "version": "1.4" - } -} diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index b5caa95f38..1c7ada7ad2 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -20,7 +20,7 @@ eclair { // override this with a script/exe that will be called everytime a new database backup has been created # backup-notify-script = "/absolute/path/to/script.sh" - watcher-type = "bitcoind" // other *experimental* values include "electrum" + watcher-type = "bitcoind" watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s bitcoind { @@ -91,7 +91,6 @@ eclair { on-chain-fees { min-feerate = 1 // minimum feerate in satoshis per byte smoothing-window = 6 // 1 = no smoothing - provider-timeout = 5 seconds // max time we'll wait for answers from a fee provider before we fallback to the next one default-feerates { // those are per target block, in satoshis per kilobyte 1 = 210000 diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 97b491a983..8fb112e5d1 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -110,11 +110,10 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, object NodeParams extends Logging { + // @formatter:off sealed trait WatcherType - object BITCOIND extends WatcherType - - object ELECTRUM extends WatcherType + // @formatter:on /** * Order of precedence for the configuration parameters: @@ -123,7 +122,7 @@ object NodeParams extends Logging { * 3) Optionally provided config * 4) Default values in reference.conf */ - def loadConfiguration(datadir: File) = + def loadConfiguration(datadir: File): Config = ConfigFactory.parseProperties(System.getProperties) .withFallback(ConfigFactory.parseFile(new File(datadir, "eclair.conf"))) .withFallback(ConfigFactory.load()) @@ -214,10 +213,8 @@ object NodeParams extends Logging { val color = ByteVector.fromValidHex(config.getString("node-color")) require(color.size == 3, "color should be a 3-bytes hex buffer") - val watcherType = config.getString("watcher-type") match { - case "electrum" => ELECTRUM - case _ => BITCOIND - } + require(config.getString("watcher-type") == "bitcoind", s"watcher-type `${config.getString("watcher-type")}` is not supported: `bitcoind` should be used") + val watcherType = BITCOIND val watchSpentWindow = FiniteDuration(config.getDuration("watch-spent-window").getSeconds, TimeUnit.SECONDS) require(watchSpentWindow > 0.seconds, "watch-spent-window must be strictly greater than 0") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala index c22304c24e..192964c13b 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/Setup.scala @@ -22,17 +22,12 @@ import akka.pattern.after import akka.util.Timeout import com.softwaremill.sttp.okhttp.OkHttpFutureBackend import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} -import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} import fr.acinq.eclair.Setup.Seeds +import fr.acinq.eclair.blockchain._ import fr.acinq.eclair.blockchain.bitcoind.rpc.{BasicBitcoinJsonRPCClient, BatchingBitcoinJsonRPCClient, ExtendedBitcoinClient} import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, ZmqWatcher} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL -import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress -import fr.acinq.eclair.blockchain.electrum._ -import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb -import fr.acinq.eclair.blockchain.fee.{ConstantFeeProvider, _} -import fr.acinq.eclair.blockchain.{EclairWallet, _} +import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Register import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} import fr.acinq.eclair.db.Databases.FileBackup @@ -51,7 +46,6 @@ import scodec.bits.ByteVector import java.io.File import java.net.InetSocketAddress -import java.sql.DriverManager import java.util.UUID import java.util.concurrent.TimeUnit import java.util.concurrent.atomic.{AtomicLong, AtomicReference} @@ -129,9 +123,7 @@ class Setup(datadir: File, val nodeParams = NodeParams.makeNodeParams(config, instanceId, nodeKeyManager, channelKeyManager, initTor(), databases, blockCount, feeEstimator, pluginParams) pluginParams.foreach(param => logger.info(s"using plugin=${param.name}")) - val serverBindingAddress = new InetSocketAddress( - config.getString("server.binding-ip"), - config.getInt("server.port")) + val serverBindingAddress = new InetSocketAddress(config.getString("server.binding-ip"), config.getInt("server.port")) // early checks DBCompatChecker.checkDBCompatibility(nodeParams) @@ -141,76 +133,51 @@ class Setup(datadir: File, logger.info(s"nodeid=${nodeParams.nodeId} alias=${nodeParams.alias}") logger.info(s"using chain=$chain chainHash=${nodeParams.chainHash}") - val bitcoin = nodeParams.watcherType match { - case BITCOIND => - val wallet = { - val name = config.getString("bitcoind.wallet") - if (!name.isBlank) Some(name) else None - } - val bitcoinClient = new BasicBitcoinJsonRPCClient( - user = config.getString("bitcoind.rpcuser"), - password = config.getString("bitcoind.rpcpassword"), - host = config.getString("bitcoind.host"), - port = config.getInt("bitcoind.rpcport"), - wallet = wallet) - val future = for { - json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) } - // Make sure wallet support is enabled in bitcoind. - _ <- bitcoinClient.invoke("getbalance").recover { case e => throw BitcoinWalletDisabledException(e) } - progress = (json \ "verificationprogress").extract[Double] - ibd = (json \ "initialblockdownload").extract[Boolean] - blocks = (json \ "blocks").extract[Long] - headers = (json \ "headers").extract[Long] - chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse) - bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int]) - unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) => - values - .filter(value => (value \ "spendable").extract[Boolean]) - .map(value => (value \ "address").extract[String]) - } - _ <- chain match { - case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 - case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 - case "regtest" => Future.successful(()) - } - } yield (progress, ibd, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) - // blocking sanity checks - val (progress, initialBlockDownload, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") - assert(bitcoinVersion >= 180000, "Eclair requires Bitcoin Core 0.18.0 or higher") - assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") - if (chainHash != Block.RegtestGenesisBlock.hash) { - assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") + val bitcoin = { + val wallet = { + val name = config.getString("bitcoind.wallet") + if (!name.isBlank) Some(name) else None + } + val bitcoinClient = new BasicBitcoinJsonRPCClient( + user = config.getString("bitcoind.rpcuser"), + password = config.getString("bitcoind.rpcpassword"), + host = config.getString("bitcoind.host"), + port = config.getInt("bitcoind.rpcport"), + wallet = wallet) + val future = for { + json <- bitcoinClient.invoke("getblockchaininfo").recover { case e => throw BitcoinRPCConnectionException(e) } + // Make sure wallet support is enabled in bitcoind. + _ <- bitcoinClient.invoke("getbalance").recover { case e => throw BitcoinWalletDisabledException(e) } + progress = (json \ "verificationprogress").extract[Double] + ibd = (json \ "initialblockdownload").extract[Boolean] + blocks = (json \ "blocks").extract[Long] + headers = (json \ "headers").extract[Long] + chainHash <- bitcoinClient.invoke("getblockhash", 0).map(_.extract[String]).map(s => ByteVector32.fromValidHex(s)).map(_.reverse) + bitcoinVersion <- bitcoinClient.invoke("getnetworkinfo").map(json => json \ "version").map(_.extract[Int]) + unspentAddresses <- bitcoinClient.invoke("listunspent").collect { case JArray(values) => + values + .filter(value => (value \ "spendable").extract[Boolean]) + .map(value => (value \ "address").extract[String]) } - assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") - assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") - assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") - logger.info(s"current blockchain height=$blocks") - blockCount.set(blocks) - Bitcoind(bitcoinClient) - case ELECTRUM => - val addresses = config.hasPath("electrum") match { - case true => - val host = config.getString("electrum.host") - val port = config.getInt("electrum.port") - val address = InetSocketAddress.createUnresolved(host, port) - val ssl = config.getString("electrum.ssl") match { - case "off" => SSL.OFF - case "loose" => SSL.LOOSE - case _ => SSL.STRICT // strict mode is the default when we specify a custom electrum server, we don't want to be MITMed - } - logger.info(s"override electrum default with server=$address ssl=$ssl") - Set(ElectrumServerAddress(address, ssl)) - case false => - val (addressesFile, sslEnabled) = (nodeParams.chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash => ("/electrum/servers_regtest.json", false) // in regtest we connect in plaintext - case Block.TestnetGenesisBlock.hash => ("/electrum/servers_testnet.json", true) - case Block.LivenetGenesisBlock.hash => ("/electrum/servers_mainnet.json", true) - } - val stream = classOf[Setup].getResourceAsStream(addressesFile) - ElectrumClientPool.readServerAddresses(stream, sslEnabled) + _ <- chain match { + case "mainnet" => bitcoinClient.invoke("getrawtransaction", "2157b554dcfda405233906e461ee593875ae4b1b97615872db6a25130ecc1dd6") // coinbase of #500000 + case "testnet" => bitcoinClient.invoke("getrawtransaction", "8f38a0dd41dc0ae7509081e262d791f8d53ed6f884323796d5ec7b0966dd3825") // coinbase of #1500000 + case "regtest" => Future.successful(()) } - val electrumClient = system.actorOf(SimpleSupervisor.props(Props(new ElectrumClientPool(blockCount, addresses, nodeParams.socksProxy_opt)), "electrum-client", SupervisorStrategy.Resume)) - Electrum(electrumClient) + } yield (progress, ibd, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) + // blocking sanity checks + val (progress, initialBlockDownload, chainHash, bitcoinVersion, unspentAddresses, blocks, headers) = await(future, 30 seconds, "bicoind did not respond after 30 seconds") + assert(bitcoinVersion >= 180000, "Eclair requires Bitcoin Core 0.18.0 or higher") + assert(chainHash == nodeParams.chainHash, s"chainHash mismatch (conf=${nodeParams.chainHash} != bitcoind=$chainHash)") + if (chainHash != Block.RegtestGenesisBlock.hash) { + assert(unspentAddresses.forall(address => !isPay2PubkeyHash(address)), "Your wallet contains non-segwit UTXOs. You must send those UTXOs to a bech32 address to use Eclair (check out our README for more details).") + } + assert(!initialBlockDownload, s"bitcoind should be synchronized (initialblockdownload=$initialBlockDownload)") + assert(progress > 0.999, s"bitcoind should be synchronized (progress=$progress)") + assert(headers - blocks <= 1, s"bitcoind should be synchronized (headers=$headers blocks=$blocks)") + logger.info(s"current blockchain height=$blocks") + blockCount.set(blocks) + bitcoinClient } def bootstrap: Future[Kit] = { @@ -241,14 +208,11 @@ class Setup(datadir: File, } minFeeratePerByte = FeeratePerByte(Satoshi(config.getLong("on-chain-fees.min-feerate"))) smoothFeerateWindow = config.getInt("on-chain-fees.smoothing-window") - readTimeout = FiniteDuration(config.getDuration("on-chain-fees.provider-timeout", TimeUnit.MILLISECONDS), TimeUnit.MILLISECONDS) - feeProvider = (nodeParams.chainHash, bitcoin) match { - case (Block.RegtestGenesisBlock.hash, _) => + feeProvider = nodeParams.chainHash match { + case Block.RegtestGenesisBlock.hash => new FallbackFeeProvider(new ConstantFeeProvider(defaultFeerates) :: Nil, minFeeratePerByte) - case (_, Bitcoind(bitcoinClient)) => - new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoinClient, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte) case _ => - new FallbackFeeProvider(new SmoothFeeProvider(new BitgoFeeProvider(nodeParams.chainHash, readTimeout), smoothFeerateWindow) :: new SmoothFeeProvider(new EarnDotComFeeProvider(readTimeout), smoothFeerateWindow) :: Nil, minFeeratePerByte) // order matters! + new FallbackFeeProvider(new SmoothFeeProvider(new BitcoinCoreFeeProvider(bitcoin, defaultFeerates), smoothFeerateWindow) :: Nil, minFeeratePerByte) } _ = system.scheduler.schedule(0 seconds, 10 minutes)(feeProvider.getFeerates.onComplete { case Success(feerates) => @@ -266,29 +230,17 @@ class Setup(datadir: File, }) _ <- feeratesRetrieved.future - watcher = bitcoin match { - case Bitcoind(bitcoinClient) => - system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) - system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) - system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(nodeParams.chainHash, blockCount, new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoinClient))), "watcher", SupervisorStrategy.Resume)) - case Electrum(electrumClient) => - zmqBlockConnected.success(Done) - zmqTxConnected.success(Done) - system.actorOf(SimpleSupervisor.props(Props(new ElectrumWatcher(blockCount, electrumClient)), "watcher", SupervisorStrategy.Resume)) + watcher = { + system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqblock"), Some(zmqBlockConnected))), "zmqblock", SupervisorStrategy.Restart)) + system.actorOf(SimpleSupervisor.props(Props(new ZMQActor(config.getString("bitcoind.zmqtx"), Some(zmqTxConnected))), "zmqtx", SupervisorStrategy.Restart)) + system.actorOf(SimpleSupervisor.props(ZmqWatcher.props(nodeParams.chainHash, blockCount, new ExtendedBitcoinClient(new BatchingBitcoinJsonRPCClient(bitcoin))), "watcher", SupervisorStrategy.Resume)) } router = system.actorOf(SimpleSupervisor.props(Router.props(nodeParams, watcher, Some(routerInitialized)), "router", SupervisorStrategy.Resume)) routerTimeout = after(FiniteDuration(config.getDuration("router.init-timeout").getSeconds, TimeUnit.SECONDS), using = system.scheduler)(Future.failed(new RuntimeException("Router initialization timed out"))) _ <- Future.firstCompletedOf(routerInitialized.future :: routerTimeout :: Nil) - wallet = bitcoin match { - case Bitcoind(bitcoinClient) => new BitcoinCoreWallet(bitcoinClient) - case Electrum(electrumClient) => - val sqlite = DriverManager.getConnection(s"jdbc:sqlite:${new File(chaindir, "wallet.sqlite")}") - val walletDb = new SqliteWalletDb(sqlite) - val electrumWallet = system.actorOf(ElectrumWallet.props(channelSeed, electrumClient, ElectrumWallet.WalletParameters(nodeParams.chainHash, walletDb)), "electrum-wallet") - new ElectrumEclairWallet(electrumWallet, nodeParams.chainHash) - } + wallet = new BitcoinCoreWallet(bitcoin) _ = wallet.getReceiveAddress.map(address => logger.info(s"initial wallet address=$address")) // do not change the name of this actor. it is used in the configuration to specify a custom bounded mailbox @@ -385,15 +337,11 @@ class Setup(datadir: File, } -// @formatter:off object Setup { + final case class Seeds(nodeSeed: ByteVector, channelSeed: ByteVector) -} -sealed trait Bitcoin -case class Bitcoind(bitcoinClient: BasicBitcoinJsonRPCClient) extends Bitcoin -case class Electrum(electrumClient: ActorRef) extends Bitcoin -// @formatter:on +} case class Kit(nodeParams: NodeParams, system: ActorSystem, diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala index c442532485..5516184578 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala @@ -43,7 +43,7 @@ sealed trait Watch { * * @param replyTo actor to notify once the transaction is confirmed. * @param txId txid of the transaction to watch. - * @param publicKeyScript when using electrum, we need to specify a public key script; any of the output scripts should work. + * @param publicKeyScript output script of any of the transaction's outputs. * @param minDepth number of confirmations. * @param event channel event related to the transaction. */ @@ -72,7 +72,7 @@ object WatchConfirmed { * @param replyTo actor to notify when the outpoint is spent. * @param txId txid of the outpoint to watch. * @param outputIndex index of the outpoint to watch. - * @param publicKeyScript electrum requires us to specify a public key script; the script of the outpoint must be provided. + * @param publicKeyScript output script of the outpoint. * @param event channel event related to the outpoint. * @param hints txids of potential spending transactions; most of the time we know the txs, and it allows for optimizations. * This argument can safely be ignored by watcher implementations. @@ -93,7 +93,7 @@ object WatchSpent { * @param replyTo actor to notify when the outpoint is spent. * @param txId txid of the outpoint to watch. * @param outputIndex index of the outpoint to watch. - * @param publicKeyScript electrum requires us to specify a public key script; the script of the outpoint must be provided. + * @param publicKeyScript output script of the outpoint. * @param event channel event related to the outpoint. */ final case class WatchSpentBasic(replyTo: ActorRef, txId: ByteVector32, outputIndex: Int, publicKeyScript: ByteVector, event: BitcoinEvent) extends Watch diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/Blockchain.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/Blockchain.scala deleted file mode 100644 index d026e5b2c1..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/Blockchain.scala +++ /dev/null @@ -1,352 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.math.BigInteger - -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, decodeCompact} -import fr.acinq.eclair.blockchain.electrum.db.HeaderDb -import grizzled.slf4j.Logging - -import scala.annotation.tailrec - -case class Blockchain(chainHash: ByteVector32, - checkpoints: Vector[CheckPoint], - headersMap: Map[ByteVector32, Blockchain.BlockIndex], - bestchain: Vector[Blockchain.BlockIndex], - orphans: Map[ByteVector32, BlockHeader] = Map()) { - - import Blockchain._ - - require(chainHash == Block.LivenetGenesisBlock.hash || chainHash == Block.TestnetGenesisBlock.hash || chainHash == Block.RegtestGenesisBlock.hash, s"invalid chain hash $chainHash") - - def tip = bestchain.last - - def height = if (bestchain.isEmpty) 0 else bestchain.last.height - - /** - * Build a chain of block indexes - * - * This is used in case of reorg to rebuilt the new best chain - * - * @param index last index of the chain - * @param acc accumulator - * @return the chain that starts at the genesis block and ends at index - */ - @tailrec - private def buildChain(index: BlockIndex, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]): Vector[BlockIndex] = { - index.parent match { - case None => index +: acc - case Some(parent) => buildChain(parent, index +: acc) - } - } - - /** - * - * @param height block height - * @return the encoded difficulty that a block at this height should have - */ - def getDifficulty(height: Int): Option[Long] = height match { - case value if value < RETARGETING_PERIOD * (checkpoints.length + 1) => - // we're within our checkpoints - val checkpoint = checkpoints(height / RETARGETING_PERIOD - 1) - Some(checkpoint.nextBits) - case value if value % RETARGETING_PERIOD != 0 => - // we're not at a retargeting height, difficulty is the same as for the previous block - getHeader(height - 1).map(_.bits) - case _ => - // difficulty retargeting - for { - previous <- getHeader(height - 1) - firstBlock <- getHeader(height - RETARGETING_PERIOD) - } yield BlockHeader.calculateNextWorkRequired(previous, firstBlock.time) - } - - def getHeader(height: Int): Option[BlockHeader] = if (!bestchain.isEmpty && height >= bestchain.head.height && height - bestchain.head.height < bestchain.size) - Some(bestchain(height - bestchain.head.height).header) - else None -} - -object Blockchain extends Logging { - - val RETARGETING_PERIOD = 2016 // on bitcoin, the difficulty re-targeting period is 2016 blocks - val MAX_REORG = 500 // we assume that there won't be a reorg of more than 500 blocks - - /** - * - * @param header block header - * @param height block height - * @param parent parent block - * @param chainwork cumulative chain work up to and including this block - */ - case class BlockIndex(header: BlockHeader, height: Int, parent: Option[BlockIndex], chainwork: BigInt) { - lazy val hash = header.hash - - lazy val blockId = header.blockId - - lazy val logwork = if (chainwork == 0) 0.0 else Math.log(chainwork.doubleValue) / Math.log(2.0) - - override def toString = s"BlockIndex($blockId, $height, ${parent.map(_.blockId)}, $logwork)" - } - - /** - * Build an empty blockchain from a series of checkpoints - * - * @param chainhash chain we're on - * @param checkpoints list of checkpoints - * @return a blockchain instance - */ - def fromCheckpoints(chainhash: ByteVector32, checkpoints: Vector[CheckPoint]): Blockchain = { - Blockchain(chainhash, checkpoints, Map(), Vector()) - } - - /** - * Used in tests - */ - def fromGenesisBlock(chainhash: ByteVector32, genesis: BlockHeader): Blockchain = { - require(chainhash == Block.RegtestGenesisBlock.hash) - // the height of the genesis block is 0 - val blockIndex = BlockIndex(genesis, 0, None, decodeCompact(genesis.bits)._1) - Blockchain(chainhash, Vector(), Map(blockIndex.hash -> blockIndex), Vector(blockIndex)) - } - - /** - * load an em - * - * @param chainHash - * @param headerDb - * @return - */ - def load(chainHash: ByteVector32, headerDb: HeaderDb): Blockchain = { - val checkpoints = CheckPoint.load(chainHash) - val checkpoints1 = headerDb.getTip match { - case Some((height, header)) => - val newcheckpoints = for {h <- checkpoints.size * RETARGETING_PERIOD - 1 + RETARGETING_PERIOD to height - RETARGETING_PERIOD by RETARGETING_PERIOD} yield { - val cpheader = headerDb.getHeader(h).get - val nextDiff = headerDb.getHeader(h + 1).get.bits - CheckPoint(cpheader.hash, nextDiff) - } - checkpoints ++ newcheckpoints - case None => checkpoints - } - Blockchain.fromCheckpoints(chainHash, checkpoints1) - } - - /** - * Validate a chunk of 2016 headers - * - * Used during initial sync to batch validate - * - * @param height height of the first header; must be a multiple of 2016 - * @param headers headers. - * @throws Exception if this chunk is not valid and consistent with our checkpoints - */ - def validateHeadersChunk(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Unit = { - if (headers.isEmpty) return - - require(height % RETARGETING_PERIOD == 0, s"header chunk height $height not a multiple of 2016") - require(BlockHeader.checkProofOfWork(headers.head)) - headers.tail.foldLeft(headers.head) { - case (previous, current) => - require(BlockHeader.checkProofOfWork(current)) - require(current.hashPreviousBlock == previous.hash) - // on mainnet all blocks with a re-targeting window have the same difficulty target - // on testnet it doesn't hold, there can be a drop in difficulty if there are no blocks for 20 minutes - blockchain.chainHash match { - case Block.LivenetGenesisBlock | Block.RegtestGenesisBlock.hash => require(current.bits == previous.bits) - case _ => () - } - current - } - - val cpindex = (height / RETARGETING_PERIOD) - 1 - if (cpindex < blockchain.checkpoints.length) { - // check that the first header in the chunk matches our checkpoint - val checkpoint = blockchain.checkpoints(cpindex) - require(headers(0).hashPreviousBlock == checkpoint.hash) - blockchain.chainHash match { - case Block.LivenetGenesisBlock.hash => require(headers(0).bits == checkpoint.nextBits) - case _ => () - } - } - - // if we have a checkpoint after this chunk, check that it is also satisfied - if (cpindex < blockchain.checkpoints.length - 1) { - require(headers.length == RETARGETING_PERIOD) - val nextCheckpoint = blockchain.checkpoints(cpindex + 1) - require(headers.last.hash == nextCheckpoint.hash) - blockchain.chainHash match { - case Block.LivenetGenesisBlock.hash => - val diff = BlockHeader.calculateNextWorkRequired(headers.last, headers.head.time) - require(diff == nextCheckpoint.nextBits) - case _ => () - } - } - } - - def addHeadersChunk(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Blockchain = { - if (headers.length > RETARGETING_PERIOD) { - val blockchain1 = Blockchain.addHeadersChunk(blockchain, height, headers.take(RETARGETING_PERIOD)) - return Blockchain.addHeadersChunk(blockchain1, height + RETARGETING_PERIOD, headers.drop(RETARGETING_PERIOD)) - } - if (headers.isEmpty) return blockchain - validateHeadersChunk(blockchain, height, headers) - - height match { - case _ if height == blockchain.checkpoints.length * RETARGETING_PERIOD => - // append after our last checkpoint - - // checkpoints are (block hash, * next * difficulty target), this is why: - // - we duplicate the first checkpoints because all headers in the first chunks on mainnet had the same difficulty target - // - we drop the last checkpoint - val chainwork = (blockchain.checkpoints(0) +: blockchain.checkpoints.dropRight(1)).map(t => BigInt(RETARGETING_PERIOD) * Blockchain.chainWork(t.nextBits)).sum - val blockIndex = BlockIndex(headers.head, height, None, chainwork + Blockchain.chainWork(headers.head)) - val bestchain1 = headers.tail.foldLeft(Vector(blockIndex)) { - case (indexes, header) => indexes :+ BlockIndex(header, indexes.last.height + 1, Some(indexes.last), indexes.last.chainwork + Blockchain.chainWork(header)) - } - val headersMap1 = blockchain.headersMap ++ bestchain1.map(bi => bi.hash -> bi) - blockchain.copy(bestchain = bestchain1, headersMap = headersMap1) - case _ if height < blockchain.checkpoints.length * RETARGETING_PERIOD => - blockchain - case _ if height == blockchain.height + 1 => - // attach at our best chain - require(headers.head.hashPreviousBlock == blockchain.bestchain.last.hash) - val blockIndex = BlockIndex(headers.head, height, None, blockchain.bestchain.last.chainwork + Blockchain.chainWork(headers.head)) - val indexes = headers.tail.foldLeft(Vector(blockIndex)) { - case (indexes, header) => indexes :+ BlockIndex(header, indexes.last.height + 1, Some(indexes.last), indexes.last.chainwork + Blockchain.chainWork(header)) - } - val bestchain1 = blockchain.bestchain ++ indexes - val headersMap1 = blockchain.headersMap ++ indexes.map(bi => bi.hash -> bi) - blockchain.copy(bestchain = bestchain1, headersMap = headersMap1) - // do nothing; headers have been validated - case _ => throw new IllegalArgumentException(s"cannot add headers chunk to an empty blockchain: not within our checkpoint") - } - } - - def addHeader(blockchain: Blockchain, height: Int, header: BlockHeader): Blockchain = { - require(BlockHeader.checkProofOfWork(header), s"invalid proof of work for $header") - blockchain.headersMap.get(header.hashPreviousBlock) match { - case Some(parent) if parent.height == height - 1 => - if (height % RETARGETING_PERIOD != 0 && (blockchain.chainHash == Block.LivenetGenesisBlock.hash || blockchain.chainHash == Block.RegtestGenesisBlock.hash)) { - // check difficulty target, which should be the same as for the parent block - // we only check this on mainnet, on testnet rules are much more lax - require(header.bits == parent.header.bits, s"header invalid difficulty target for ${header}, it should be ${parent.header.bits}") - } - val blockIndex = BlockIndex(header, height, Some(parent), parent.chainwork + Blockchain.chainWork(header)) - val headersMap1 = blockchain.headersMap + (blockIndex.hash -> blockIndex) - val bestChain1 = if (parent == blockchain.bestchain.last) { - // simplest case: we add to our current best chain - logger.info(s"new tip at $blockIndex") - blockchain.bestchain :+ blockIndex - } else if (blockIndex.chainwork > blockchain.bestchain.last.chainwork) { - logger.info(s"new best chain at $blockIndex") - // we have a new best chain - buildChain(blockIndex) - } else { - logger.info(s"received header $blockIndex which is not on the best chain") - blockchain.bestchain - } - blockchain.copy(headersMap = headersMap1, bestchain = bestChain1) - case Some(parent) => throw new IllegalArgumentException(s"parent for $header at $height is not valid: $parent ") - case None if height < blockchain.height - 1000 => blockchain - case None => throw new IllegalArgumentException(s"cannot find parent for $header at $height") - } - } - - def addHeaders(blockchain: Blockchain, height: Int, headers: Seq[BlockHeader]): Blockchain = { - if (headers.isEmpty) blockchain - else if (height % RETARGETING_PERIOD == 0) addHeadersChunk(blockchain, height, headers) - else { - @tailrec - def loop(bc: Blockchain, h: Int, hs: Seq[BlockHeader]): Blockchain = if (hs.isEmpty) bc else { - loop(Blockchain.addHeader(bc, h, hs.head), h + 1, hs.tail) - } - - loop(blockchain, height, headers) - } - } - - - /** - * build a chain of block indexes - * - * @param index last index of the chain - * @param acc accumulator - * @return the chain that starts at the genesis block and ends at index - */ - @tailrec - def buildChain(index: BlockIndex, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]): Vector[BlockIndex] = { - index.parent match { - case None => index +: acc - case Some(parent) => buildChain(parent, index +: acc) - } - } - - def chainWork(target: BigInt): BigInt = BigInt(2).pow(256) / (target + BigInt(1)) - - def chainWork(bits: Long): BigInt = { - val (target, negative, overflow) = decodeCompact(bits) - if (target == BigInteger.ZERO || negative || overflow) BigInt(0) else chainWork(target) - } - - def chainWork(header: BlockHeader): BigInt = chainWork(header.bits) - - /** - * Optimize blockchain - * - * @param blockchain - * @param acc internal accumulator - * @return a (blockchain, indexes) tuple where headers that are old enough have been removed and new checkpoints added, - * and indexes is the list of header indexes that have been optimized out and must be persisted - */ - @tailrec - def optimize(blockchain: Blockchain, acc: Vector[BlockIndex] = Vector.empty[BlockIndex]) : (Blockchain, Vector[BlockIndex]) = { - if (blockchain.bestchain.size >= RETARGETING_PERIOD + MAX_REORG) { - val saveme = blockchain.bestchain.take(RETARGETING_PERIOD) - val headersMap1 = blockchain.headersMap -- saveme.map(_.hash) - val bestchain1 = blockchain.bestchain.drop(RETARGETING_PERIOD) - val checkpoints1 = blockchain.checkpoints :+ CheckPoint(saveme.last.hash, bestchain1.head.header.bits) - optimize(blockchain.copy(headersMap = headersMap1, bestchain = bestchain1, checkpoints = checkpoints1), acc ++ saveme) - } else { - (blockchain, acc) - } - } - - /** - * Computes the difficulty target at a given height. - * - * @param blockchain blockchain - * @param height height for which we want the difficulty target - * @param headerDb header database - * @return the difficulty target for this height - */ - def getDifficulty(blockchain: Blockchain, height: Int, headerDb: HeaderDb): Option[Long] = { - blockchain.chainHash match { - case Block.LivenetGenesisBlock.hash => - (height % RETARGETING_PERIOD) match { - case 0 => - for { - parent <- blockchain.getHeader(height - 1) orElse headerDb.getHeader(height - 1) - previous <- blockchain.getHeader(height - 2016) orElse headerDb.getHeader(height - 2016) - target = BlockHeader.calculateNextWorkRequired(parent, previous.time) - } yield target - case _ => blockchain.getHeader(height - 1) orElse headerDb.getHeader(height - 1) map (_.bits) - } - case _ => None // no difficulty check on testnet - } - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/CheckPoint.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/CheckPoint.scala deleted file mode 100644 index 8f1047c9ff..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/CheckPoint.scala +++ /dev/null @@ -1,79 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.io.InputStream - -import fr.acinq.bitcoin.{Block, ByteVector32, encodeCompact} -import fr.acinq.eclair.blockchain.electrum.db.HeaderDb -import org.json4s.JsonAST.{JArray, JInt, JString} -import org.json4s.jackson.JsonMethods - -/** - * - * @param hash block hash - * @param nextBits difficulty target for the next block - */ -case class CheckPoint(hash: ByteVector32, nextBits: Long) - -object CheckPoint { - - import Blockchain.RETARGETING_PERIOD - - /** - * Load checkpoints. - * There is one checkpoint every 2016 blocks (which is the difficulty adjustment period). They are used to check that - * we're on the right chain and to validate proof-of-work by checking the difficulty target - * @return an ordered list of checkpoints, with one checkpoint every 2016 blocks - */ - def load(chainHash: ByteVector32): Vector[CheckPoint] = (chainHash: @unchecked) match { - case Block.LivenetGenesisBlock.hash => load(classOf[CheckPoint].getResourceAsStream("/electrum/checkpoints_mainnet.json")) - case Block.TestnetGenesisBlock.hash => load(classOf[CheckPoint].getResourceAsStream("/electrum/checkpoints_testnet.json")) - case Block.RegtestGenesisBlock.hash => Vector.empty[CheckPoint] // no checkpoints on regtest - } - - def load(stream: InputStream): Vector[CheckPoint] = { - val JArray(values) = JsonMethods.parse(stream) - val checkpoints = values.collect { - case JArray(JString(a) :: JInt(b) :: Nil) => CheckPoint(ByteVector32.fromValidHex(a).reverse, encodeCompact(b.bigInteger)) - } - checkpoints.toVector - } - - /** - * load checkpoints from our resources and header database - * - * @param chainHash chaim hash - * @param headerDb header db - * @return a series of checkpoints - */ - def load(chainHash: ByteVector32, headerDb: HeaderDb): Vector[CheckPoint] = { - val checkpoints = CheckPoint.load(chainHash) - val checkpoints1 = headerDb.getTip match { - case Some((height, header)) => - val newcheckpoints = for {h <- checkpoints.size * RETARGETING_PERIOD - 1 + RETARGETING_PERIOD to height - RETARGETING_PERIOD by RETARGETING_PERIOD} yield { - // we * should * have these headers in our db - val cpheader = headerDb.getHeader(h).get - val nextDiff = headerDb.getHeader(h + 1).get.bits - CheckPoint(cpheader.hash, nextDiff) - } - checkpoints ++ newcheckpoints - case None => checkpoints - } - checkpoints1 - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala deleted file mode 100644 index 88ece3fab2..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClient.scala +++ /dev/null @@ -1,665 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.net.{InetSocketAddress, SocketAddress} -import java.util - -import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated} -import fr.acinq.bitcoin._ -import fr.acinq.eclair.blockchain.bitcoind.rpc.{Error, JsonRPCRequest, JsonRPCResponse} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.tor.Socks5ProxyParams -import io.netty.bootstrap.Bootstrap -import io.netty.buffer.PooledByteBufAllocator -import io.netty.channel._ -import io.netty.channel.nio.NioEventLoopGroup -import io.netty.channel.socket.SocketChannel -import io.netty.channel.socket.nio.NioSocketChannel -import io.netty.handler.codec.string.{LineEncoder, StringDecoder} -import io.netty.handler.codec.{LineBasedFrameDecoder, MessageToMessageDecoder, MessageToMessageEncoder} -import io.netty.handler.proxy.Socks5ProxyHandler -import io.netty.handler.ssl.SslContextBuilder -import io.netty.handler.ssl.util.InsecureTrustManagerFactory -import io.netty.resolver.{NoopAddressResolver, NoopAddressResolverGroup} -import io.netty.util.CharsetUtil -import org.json4s.JsonAST._ -import org.json4s.jackson.JsonMethods -import org.json4s.{DefaultFormats, Formats, JInt, JLong, JString} -import scodec.bits.ByteVector - -import scala.annotation.tailrec -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.util.{Failure, Success, Try} - -/** - * For later optimizations, see http://normanmaurer.me/presentations/2014-facebook-eng-netty/slides.html - */ -class ElectrumClient(serverAddress: InetSocketAddress, ssl: SSL, socksProxy_opt: Option[Socks5ProxyParams] = None)(implicit val ec: ExecutionContext) extends Actor with Stash with ActorLogging { - - import ElectrumClient._ - - implicit val formats = DefaultFormats - - val b = new Bootstrap - b.group(workerGroup) - b.channel(classOf[NioSocketChannel]) - b.option[java.lang.Boolean](ChannelOption.SO_KEEPALIVE, true) - b.option[java.lang.Boolean](ChannelOption.TCP_NODELAY, true) - b.option[java.lang.Integer](ChannelOption.CONNECT_TIMEOUT_MILLIS, 5000) - b.option(ChannelOption.ALLOCATOR, PooledByteBufAllocator.DEFAULT) - b.handler(new ChannelInitializer[SocketChannel]() { - override def initChannel(ch: SocketChannel): Unit = { - ssl match { - case SSL.OFF => () - case SSL.STRICT => - val sslCtx = SslContextBuilder.forClient.build - val handler = sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort) - val sslParameters = handler.engine().getSSLParameters - sslParameters.setEndpointIdentificationAlgorithm("HTTPS") - handler.engine().setSSLParameters(sslParameters) - val enabledProtocols = if (handler.engine().getSupportedProtocols.contains("TLSv1.3")) { - "TLSv1.2" :: "TLSv1.3" :: Nil - } else { - "TLSv1.2" :: Nil - } - handler.engine().setEnabledProtocols(enabledProtocols.toArray) - ch.pipeline.addLast(handler) - case SSL.LOOSE => - // INSECURE VERSION THAT DOESN'T CHECK CERTIFICATE - val sslCtx = SslContextBuilder.forClient().trustManager(InsecureTrustManagerFactory.INSTANCE).build() - ch.pipeline.addLast(sslCtx.newHandler(ch.alloc(), serverAddress.getHostName, serverAddress.getPort)) - } - // inbound handlers - ch.pipeline.addLast(new LineBasedFrameDecoder(Int.MaxValue, true, true)) // JSON messages are separated by a new line - ch.pipeline.addLast(new StringDecoder(CharsetUtil.UTF_8)) - ch.pipeline.addLast(new ElectrumResponseDecoder) - ch.pipeline.addLast(new ActorHandler(self)) - // outbound handlers - ch.pipeline.addLast(new LineEncoder) - ch.pipeline.addLast(new JsonRPCRequestEncoder) - // error handler - ch.pipeline.addLast(new ExceptionHandler) - // optional proxy (must be the first handler) - socksProxy_opt.foreach(params => ch.pipeline().addFirst(new Socks5ProxyHandler(params.address))) - } - }) - - // don't try to resolve addresses if we're using a proxy - socksProxy_opt.foreach(params => b.resolver(NoopAddressResolverGroup.INSTANCE)) - - // Start the client. - log.debug("connecting to server={}", serverAddress) - - val channelOpenFuture = b.connect(serverAddress.getHostName, serverAddress.getPort) - - def errorHandler(t: Throwable): Unit = { - // generic errors don't need to be logged in most cases, what we actually want are errors that happened once we were - // properly connected and had exchanged version messages - log.debug("server={} connection error (reason={})", serverAddress, t.getMessage) - self ! Close - } - - channelOpenFuture.addListeners(new ChannelFutureListener { - override def operationComplete(future: ChannelFuture): Unit = { - if (!future.isSuccess) { - errorHandler(future.cause()) - } else { - future.channel().closeFuture().addListener(new ChannelFutureListener { - override def operationComplete(future: ChannelFuture): Unit = { - if (!future.isSuccess) { - errorHandler(future.cause()) - } else { - log.debug("server={} channel closed: {}", serverAddress, future.channel()) - self ! Close - } - } - }) - } - } - }) - - /** - * This error handler catches all exceptions and kill the actor - * See https://stackoverflow.com/questions/30994095/how-to-catch-all-exception-in-netty - */ - class ExceptionHandler extends ChannelDuplexHandler { - override def connect(ctx: ChannelHandlerContext, remoteAddress: SocketAddress, localAddress: SocketAddress, promise: ChannelPromise): Unit = { - ctx.connect(remoteAddress, localAddress, promise.addListener(new ChannelFutureListener() { - override def operationComplete(future: ChannelFuture): Unit = { - if (!future.isSuccess) { - errorHandler(future.cause()) - } - } - })) - } - - override def write(ctx: ChannelHandlerContext, msg: scala.Any, promise: ChannelPromise): Unit = { - ctx.write(msg, promise.addListener(new ChannelFutureListener() { - override def operationComplete(future: ChannelFuture): Unit = { - if (!future.isSuccess) { - errorHandler(future.cause()) - } - } - })) - } - - override def exceptionCaught(ctx: ChannelHandlerContext, cause: Throwable): Unit = { - errorHandler(cause) - } - } - - /** - * A decoder ByteBuf -> Either[Response, JsonRPCResponse] - */ - class ElectrumResponseDecoder extends MessageToMessageDecoder[String] { - override def decode(ctx: ChannelHandlerContext, msg: String, out: util.List[AnyRef]): Unit = { - val s = msg.asInstanceOf[String] - val r = parseResponse(s) - out.add(r) - } - } - - /** - * An encoder JsonRPCRequest -> ByteBuf - */ - class JsonRPCRequestEncoder extends MessageToMessageEncoder[JsonRPCRequest] { - override def encode(ctx: ChannelHandlerContext, request: JsonRPCRequest, out: util.List[AnyRef]): Unit = { - import org.json4s.JsonDSL._ - import org.json4s._ - import org.json4s.jackson.JsonMethods._ - - log.debug("sending {} to {}", request, serverAddress) - val json = ("method" -> request.method) ~ ("params" -> request.params.map { - case s: String => new JString(s) - case b: ByteVector32 => new JString(b.toHex) - case f: FeeratePerKw => new JLong(f.toLong) - case b: Boolean => new JBool(b) - case t: Int => new JInt(t) - case t: Long => new JLong(t) - case t: Double => new JDouble(t) - }) ~ ("id" -> request.id) ~ ("jsonrpc" -> request.jsonrpc) - val serialized = compact(render(json)) - out.add(serialized) - } - } - - /** - * Forwards incoming messages to the underlying actor - */ - class ActorHandler(actor: ActorRef) extends ChannelInboundHandlerAdapter { - - override def channelActive(ctx: ChannelHandlerContext): Unit = { - actor ! ctx - } - - override def channelRead(ctx: ChannelHandlerContext, msg: Any): Unit = { - actor ! msg - } - } - - var addressSubscriptions = Map.empty[String, Set[ActorRef]] - var scriptHashSubscriptions = Map.empty[ByteVector32, Set[ActorRef]] - val headerSubscriptions = collection.mutable.HashSet.empty[ActorRef] - val version = ServerVersion(CLIENT_NAME, PROTOCOL_VERSION) - val statusListeners = collection.mutable.HashSet.empty[ActorRef] - - var reqId = 0 - - // we need to regularly send a ping in order not to get disconnected - val pingTrigger = context.system.scheduler.schedule(30 seconds, 30 seconds, self, Ping) - - override def unhandled(message: Any): Unit = { - message match { - case Terminated(deadActor) => - addressSubscriptions = addressSubscriptions.mapValues(subscribers => subscribers - deadActor).toMap - scriptHashSubscriptions = scriptHashSubscriptions.mapValues(subscribers => subscribers - deadActor).toMap - statusListeners -= deadActor - headerSubscriptions -= deadActor - - case RemoveStatusListener(actor) => statusListeners -= actor - - case PingResponse => () - - case Close => - statusListeners.foreach(_ ! ElectrumDisconnected) - context.stop(self) - - case _ => log.warning("server={} unhandled message {}", serverAddress, message) - } - } - - override def postStop(): Unit = { - pingTrigger.cancel() - super.postStop() - } - - /** - * send an electrum request to the server - * - * @param ctx connection to the electrumx server - * @param request electrum request - * @return the request id used to send the request - */ - def send(ctx: ChannelHandlerContext, request: Request): String = { - val electrumRequestId = "" + reqId - if (ctx.channel().isWritable) { - ctx.channel().writeAndFlush(makeRequest(request, electrumRequestId)) - } else { - errorHandler(new RuntimeException(s"channel not writable")) - } - reqId = reqId + 1 - electrumRequestId - } - - def receive: Receive = disconnected - - def disconnected: Receive = { - case ctx: ChannelHandlerContext => - log.debug("connected to server={}", serverAddress) - send(ctx, version) - context become waitingForVersion(ctx) - - case AddStatusListener(actor) => statusListeners += actor - } - - def waitingForVersion(ctx: ChannelHandlerContext): Receive = { - case Right(json: JsonRPCResponse) => - (parseJsonResponse(version, json): @unchecked) match { - case ServerVersionResponse(clientName, protocolVersion) => - log.info("server={} clientName={} protocolVersion={}", serverAddress, clientName, protocolVersion) - send(ctx, HeaderSubscription(self)) - headerSubscriptions += self - log.debug("waiting for tip from server={}", serverAddress) - context become waitingForTip(ctx) - case ServerError(request, error) => - log.error("server={} sent error={} while processing request={}, disconnecting", serverAddress, error, request) - self ! Close - } - - case AddStatusListener(actor) => statusListeners += actor - } - - def waitingForTip(ctx: ChannelHandlerContext): Receive = { - case Right(json: JsonRPCResponse) => - val (height, header) = parseBlockHeader(json.result) - log.debug("connected to server={}, tip={} height={}", serverAddress, header.hash, height) - statusListeners.foreach(_ ! ElectrumReady(height, header, serverAddress)) - context become connected(ctx, height, header, Map()) - - case AddStatusListener(actor) => statusListeners += actor - } - - def connected(ctx: ChannelHandlerContext, height: Int, tip: BlockHeader, requests: Map[String, (Request, ActorRef)]): Receive = { - case AddStatusListener(actor) => - statusListeners += actor - actor ! ElectrumReady(height, tip, serverAddress) - - case HeaderSubscription(actor) => - headerSubscriptions += actor - actor ! HeaderSubscriptionResponse(height, tip) - context watch actor - - case request: Request => - val curReqId = send(ctx, request) - request match { - case AddressSubscription(address, actor) => - addressSubscriptions = addressSubscriptions.updated(address, addressSubscriptions.getOrElse(address, Set()) + actor) - context watch actor - case ScriptHashSubscription(scriptHash, actor) => - scriptHashSubscriptions = scriptHashSubscriptions.updated(scriptHash, scriptHashSubscriptions.getOrElse(scriptHash, Set()) + actor) - context watch actor - case _ => () - } - context become connected(ctx, height, tip, requests + (curReqId -> (request, sender()))) - - case Right(json: JsonRPCResponse) => - requests.get(json.id) match { - case Some((request, requestor)) => - val response = parseJsonResponse(request, json) - log.debug("server={} sent response for reqId={} request={} response={}", serverAddress, json.id, request, response) - requestor ! response - case None => - log.warning("server={} could not find requestor for reqId=${} response={}", serverAddress, json.id, json) - } - context become connected(ctx, height, tip, requests - json.id) - - case Left(response: HeaderSubscriptionResponse) => headerSubscriptions.foreach(_ ! response) - - case Left(response: AddressSubscriptionResponse) => addressSubscriptions.get(response.address).foreach(listeners => listeners.foreach(_ ! response)) - - case Left(response: ScriptHashSubscriptionResponse) => scriptHashSubscriptions.get(response.scriptHash).foreach(listeners => listeners.foreach(_ ! response)) - - case HeaderSubscriptionResponse(height, newtip) => - log.info("server={} new tip={}", serverAddress, newtip) - context become connected(ctx, height, newtip, requests) - } -} - -/** - * See the documentation at https://electrumx-spesmilo.readthedocs.io/en/latest/ - */ -object ElectrumClient { - val CLIENT_NAME = "3.3.6" // client name that we will include in our "version" message - val PROTOCOL_VERSION = "1.4" // version of the protocol that we require - - // this is expensive and shared with all clients - val workerGroup = new NioEventLoopGroup() - - /** - * Utility function to converts a publicKeyScript to electrum's scripthash - * - * @param publicKeyScript public key script - * @return the hash of the public key script, as used by ElectrumX's hash-based methods - */ - def computeScriptHash(publicKeyScript: ByteVector): ByteVector32 = Crypto.sha256(publicKeyScript).reverse - - // @formatter:off - case class AddStatusListener(actor: ActorRef) - case class RemoveStatusListener(actor: ActorRef) - - sealed trait Request { def context_opt: Option[Any] = None } - sealed trait Response { def context_opt: Option[Any] = None } - - case class ServerVersion(clientName: String, protocolVersion: String) extends Request - case class ServerVersionResponse(clientName: String, protocolVersion: String) extends Response - - case object Ping extends Request - case object PingResponse extends Response - - case class GetAddressHistory(address: String) extends Request - case class TransactionHistoryItem(height: Int, tx_hash: ByteVector32) - case class GetAddressHistoryResponse(address: String, history: Seq[TransactionHistoryItem]) extends Response - - case class GetScriptHashHistory(scriptHash: ByteVector32) extends Request - case class GetScriptHashHistoryResponse(scriptHash: ByteVector32, history: List[TransactionHistoryItem]) extends Response - - case class AddressListUnspent(address: String) extends Request - case class UnspentItem(tx_hash: ByteVector32, tx_pos: Int, value: Long, height: Long) { - lazy val outPoint = OutPoint(tx_hash.reverse, tx_pos) - } - case class AddressListUnspentResponse(address: String, unspents: Seq[UnspentItem]) extends Response - - case class ScriptHashListUnspent(scriptHash: ByteVector32) extends Request - case class ScriptHashListUnspentResponse(scriptHash: ByteVector32, unspents: Seq[UnspentItem]) extends Response - - case class BroadcastTransaction(tx: Transaction) extends Request - case class BroadcastTransactionResponse(tx: Transaction, error: Option[Error]) extends Response - - case class GetTransactionIdFromPosition(height: Int, tx_pos: Int, merkle: Boolean = false) extends Request - case class GetTransactionIdFromPositionResponse(txid: ByteVector32, height: Int, tx_pos: Int, merkle: Seq[ByteVector32]) extends Response - - case class GetTransaction(txid: ByteVector32, override val context_opt: Option[Any] = None) extends Request - case class GetTransactionResponse(tx: Transaction, override val context_opt: Option[Any]) extends Response - - case class GetHeader(height: Int) extends Request - case class GetHeaderResponse(height: Int, header: BlockHeader) extends Response - object GetHeaderResponse { - def apply(t: (Int, BlockHeader)) = new GetHeaderResponse(t._1, t._2) - } - - case class GetHeaders(start_height: Int, count: Int, cp_height: Int = 0) extends Request - case class GetHeadersResponse(start_height: Int, headers: Seq[BlockHeader], max: Int) extends Response { - override def toString = s"GetHeadersResponse($start_height, ${headers.length}, ${headers.headOption}, ${headers.lastOption}, $max)" - } - - case class GetMerkle(txid: ByteVector32, height: Int, override val context_opt: Option[Any] = None) extends Request - case class GetMerkleResponse(txid: ByteVector32, merkle: List[ByteVector32], block_height: Int, pos: Int, override val context_opt: Option[Any]) extends Response { - lazy val root: ByteVector32 = { - @tailrec - def loop(pos: Int, hashes: Seq[ByteVector32]): ByteVector32 = { - if (hashes.length == 1) hashes(0) - else { - val h = if (pos % 2 == 1) Crypto.hash256(hashes(1) ++ hashes(0)) else Crypto.hash256(hashes(0) ++ hashes(1)) - loop(pos / 2, h +: hashes.drop(2)) - } - } - loop(pos, txid.reverse +: merkle.map(b => b.reverse)) - } - } - - case class AddressSubscription(address: String, actor: ActorRef) extends Request - case class AddressSubscriptionResponse(address: String, status: String) extends Response - - case class ScriptHashSubscription(scriptHash: ByteVector32, actor: ActorRef) extends Request - case class ScriptHashSubscriptionResponse(scriptHash: ByteVector32, status: String) extends Response - - case class HeaderSubscription(actor: ActorRef) extends Request - case class HeaderSubscriptionResponse(height: Int, header: BlockHeader) extends Response - object HeaderSubscriptionResponse { - def apply(t: (Int, BlockHeader)) = new HeaderSubscriptionResponse(t._1, t._2) - } - - case class Header(block_height: Long, version: Long, prev_block_hash: ByteVector32, merkle_root: ByteVector32, timestamp: Long, bits: Long, nonce: Long) { - def blockHeader = BlockHeader(version, prev_block_hash.reverse, merkle_root.reverse, timestamp, bits, nonce) - - lazy val block_hash: ByteVector32 = blockHeader.hash - lazy val block_id: ByteVector32 = block_hash.reverse - } - - object Header { - def makeHeader(height: Long, header: BlockHeader) = ElectrumClient.Header(height, header.version, header.hashPreviousBlock.reverse, header.hashMerkleRoot.reverse, header.time, header.bits, header.nonce) - - val RegtestGenesisHeader = makeHeader(0, Block.RegtestGenesisBlock.header) - val TestnetGenesisHeader = makeHeader(0, Block.TestnetGenesisBlock.header) - val LivenetGenesisHeader = makeHeader(0, Block.LivenetGenesisBlock.header) - } - - case class TransactionHistory(history: Seq[TransactionHistoryItem]) extends Response - - case class AddressStatus(address: String, status: String) extends Response - - case class ServerError(request: Request, error: Error) extends Response - - sealed trait ElectrumEvent - - case class ElectrumReady(height: Int, tip: BlockHeader, serverAddress: InetSocketAddress) extends ElectrumEvent - object ElectrumReady { - def apply(t: (Int, BlockHeader), serverAddress: InetSocketAddress) = new ElectrumReady(t._1 , t._2, serverAddress) - } - case object ElectrumDisconnected extends ElectrumEvent - - sealed trait SSL - object SSL { - case object OFF extends SSL - case object STRICT extends SSL - case object LOOSE extends SSL - } - - case object Close - - // @formatter:on - - def parseResponse(input: String): Either[Response, JsonRPCResponse] = { - implicit val formats = DefaultFormats - val json = JsonMethods.parse(new String(input)) - json \ "method" match { - case JString(method) => - // this is a jsonrpc request, i.e. a subscription response - val JArray(params) = json \ "params" - Left(((method, params): @unchecked) match { - case ("blockchain.headers.subscribe", header :: Nil) => HeaderSubscriptionResponse(parseBlockHeader(header)) - case ("blockchain.address.subscribe", JString(address) :: JNull :: Nil) => AddressSubscriptionResponse(address, "") - case ("blockchain.address.subscribe", JString(address) :: JString(status) :: Nil) => AddressSubscriptionResponse(address, status) - case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JNull :: Nil) => ScriptHashSubscriptionResponse(ByteVector32.fromValidHex(scriptHashHex), "") - case ("blockchain.scripthash.subscribe", JString(scriptHashHex) :: JString(status) :: Nil) => ScriptHashSubscriptionResponse(ByteVector32.fromValidHex(scriptHashHex), status) - }) - case _ => Right(parseJsonRpcResponse(json)) - } - } - - def parseJsonRpcResponse(json: JValue): JsonRPCResponse = { - implicit val formats = DefaultFormats - val result = json \ "result" - val error = json \ "error" match { - case JNull => None - case JNothing => None - case other => - val message = other \ "message" match { - case JString(value) => value - case _ => "" - } - val code = other \ " code" match { - case JInt(value) => value.intValue - case JLong(value) => value.intValue - case _ => 0 - } - Some(Error(code, message)) - } - val id = json \ "id" match { - case JString(value) => value - case JInt(value) => value.toString() - case JLong(value) => value.toString - case _ => "" - } - JsonRPCResponse(result, error, id) - } - - def longField(jvalue: JValue, field: String): Long = (jvalue \ field: @unchecked) match { - case JLong(value) => value.longValue - case JInt(value) => value.longValue - } - - def intField(jvalue: JValue, field: String): Int = (jvalue \ field: @unchecked) match { - case JLong(value) => value.intValue - case JInt(value) => value.intValue - } - - def parseBlockHeader(json: JValue): (Int, BlockHeader) = { - val height = intField(json, "height") - val JString(hex) = json \ "hex" - (height, BlockHeader.read(hex)) - } - - def makeRequest(request: Request, reqId: String): JsonRPCRequest = request match { - case ServerVersion(clientName, protocolVersion) => JsonRPCRequest(id = reqId, method = "server.version", params = clientName :: protocolVersion :: Nil) - case Ping => JsonRPCRequest(id = reqId, method = "server.ping", params = Nil) - case GetAddressHistory(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.get_history", params = address :: Nil) - case GetScriptHashHistory(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.get_history", params = scripthash.toHex :: Nil) - case AddressListUnspent(address) => JsonRPCRequest(id = reqId, method = "blockchain.address.listunspent", params = address :: Nil) - case ScriptHashListUnspent(scripthash) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.listunspent", params = scripthash.toHex :: Nil) - case AddressSubscription(address, _) => JsonRPCRequest(id = reqId, method = "blockchain.address.subscribe", params = address :: Nil) - case ScriptHashSubscription(scriptHash, _) => JsonRPCRequest(id = reqId, method = "blockchain.scripthash.subscribe", params = scriptHash.toString() :: Nil) - case BroadcastTransaction(tx) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.broadcast", params = Transaction.write(tx).toHex :: Nil) - case GetTransactionIdFromPosition(height, tx_pos, merkle) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.id_from_pos", params = height :: tx_pos :: merkle :: Nil) - case GetTransaction(txid, _) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get", params = txid :: Nil) - case HeaderSubscription(_) => JsonRPCRequest(id = reqId, method = "blockchain.headers.subscribe", params = Nil) - case GetHeader(height) => JsonRPCRequest(id = reqId, method = "blockchain.block.header", params = height :: Nil) - case GetHeaders(start_height, count, _) => JsonRPCRequest(id = reqId, method = "blockchain.block.headers", params = start_height :: count :: Nil) - case GetMerkle(txid, height, _) => JsonRPCRequest(id = reqId, method = "blockchain.transaction.get_merkle", params = txid :: height :: Nil) - } - - def parseJsonResponse(request: Request, json: JsonRPCResponse): Response = { - implicit val formats: Formats = DefaultFormats - json.error match { - case Some(error) => (request: @unchecked) match { - case BroadcastTransaction(tx) => BroadcastTransactionResponse(tx, Some(error)) // for this request type, error are considered a "normal" response - case _ => ServerError(request, error) - } - case None => (request: @unchecked) match { - case _: ServerVersion => - val JArray(jitems) = json.result - val JString(clientName) = jitems(0) - val JString(protocolVersion) = jitems(1) - ServerVersionResponse(clientName, protocolVersion) - case Ping => PingResponse - case GetAddressHistory(address) => - val JArray(jitems) = json.result - val items = jitems.map(jvalue => { - val JString(tx_hash) = jvalue \ "tx_hash" - val height = intField(jvalue, "height") - TransactionHistoryItem(height, ByteVector32.fromValidHex(tx_hash)) - }) - GetAddressHistoryResponse(address, items) - case GetScriptHashHistory(scripthash) => - val JArray(jitems) = json.result - val items = jitems.map(jvalue => { - val JString(tx_hash) = jvalue \ "tx_hash" - val height = intField(jvalue, "height") - TransactionHistoryItem(height, ByteVector32.fromValidHex(tx_hash)) - }) - GetScriptHashHistoryResponse(scripthash, items) - case AddressListUnspent(address) => - val JArray(jitems) = json.result - val items = jitems.map(jvalue => { - val JString(tx_hash) = jvalue \ "tx_hash" - val tx_pos = intField(jvalue, "tx_pos") - val height = intField(jvalue, "height") - val value = longField(jvalue, "value") - UnspentItem(ByteVector32.fromValidHex(tx_hash), tx_pos, value, height) - }) - AddressListUnspentResponse(address, items) - case ScriptHashListUnspent(scripthash) => - val JArray(jitems) = json.result - val items = jitems.map(jvalue => { - val JString(tx_hash) = jvalue \ "tx_hash" - val tx_pos = intField(jvalue, "tx_pos") - val height = longField(jvalue, "height") - val value = longField(jvalue, "value") - UnspentItem(ByteVector32.fromValidHex(tx_hash), tx_pos, value, height) - }) - ScriptHashListUnspentResponse(scripthash, items) - case GetTransactionIdFromPosition(height, tx_pos, false) => - val JString(tx_hash) = json.result - GetTransactionIdFromPositionResponse(ByteVector32.fromValidHex(tx_hash), height, tx_pos, Nil) - case GetTransactionIdFromPosition(height, tx_pos, true) => - val JString(tx_hash) = json.result \ "tx_hash" - val JArray(hashes) = json.result \ "merkle" - val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex(value) } - GetTransactionIdFromPositionResponse(ByteVector32.fromValidHex(tx_hash), height, tx_pos, leaves) - case GetTransaction(_, context_opt) => - val JString(hex) = json.result - GetTransactionResponse(Transaction.read(hex), context_opt) - case AddressSubscription(address, _) => json.result match { - case JString(status) => AddressSubscriptionResponse(address, status) - case _ => AddressSubscriptionResponse(address, "") - } - case ScriptHashSubscription(scriptHash, _) => json.result match { - case JString(status) => ScriptHashSubscriptionResponse(scriptHash, status) - case _ => ScriptHashSubscriptionResponse(scriptHash, "") - } - case BroadcastTransaction(tx) => - val JString(message) = json.result - // if we got here, it means that the server's response does not contain an error and message should be our - // transaction id. However, it seems that at least on testnet some servers still use an older version of the - // Electrum protocol and return an error message in the result field - Try(ByteVector32.fromValidHex(message)) match { - case Success(txid) if txid == tx.txid => BroadcastTransactionResponse(tx, None) - case Success(txid) => BroadcastTransactionResponse(tx, Some(Error(1, s"response txid $txid does not match request txid ${tx.txid}"))) - case Failure(_) => BroadcastTransactionResponse(tx, Some(Error(1, message))) - } - case GetHeader(height) => - val JString(hex) = json.result - GetHeaderResponse(height, BlockHeader.read(hex)) - case GetHeaders(start_height, _, _) => - val max = intField(json.result, "max") - val JString(hex) = json.result \ "hex" - val bin = ByteVector.fromValidHex(hex).toArray - val blockHeaders = bin.grouped(80).map(BlockHeader.read).toList - GetHeadersResponse(start_height, blockHeaders, max) - case GetMerkle(txid, _, context_opt) => - val JArray(hashes) = json.result \ "merkle" - val leaves = hashes collect { case JString(value) => ByteVector32.fromValidHex(value) } - val blockHeight = intField(json.result, "block_height") - val JInt(pos) = json.result \ "pos" - GetMerkleResponse(txid, leaves, blockHeight, pos.toInt, context_opt) - } - } - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala deleted file mode 100644 index d2d26cfa05..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPool.scala +++ /dev/null @@ -1,241 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.io.InputStream -import java.net.InetSocketAddress -import java.util.concurrent.atomic.AtomicLong - -import akka.actor.{Actor, ActorRef, FSM, OneForOneStrategy, Props, SupervisorStrategy, Terminated} -import fr.acinq.bitcoin.BlockHeader -import fr.acinq.eclair.blockchain.CurrentBlockCount -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL -import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress -import fr.acinq.eclair.tor.Socks5ProxyParams -import org.json4s.JsonAST.{JObject, JString} -import org.json4s.jackson.JsonMethods - -import scala.concurrent.ExecutionContext -import scala.concurrent.duration._ -import scala.util.Random - -class ElectrumClientPool(blockCount: AtomicLong, serverAddresses: Set[ElectrumServerAddress], socksProxy_opt: Option[Socks5ProxyParams] = None)(implicit val ec: ExecutionContext) extends Actor with FSM[ElectrumClientPool.State, ElectrumClientPool.Data] { - import ElectrumClientPool._ - - val statusListeners = collection.mutable.HashSet.empty[ActorRef] - val addresses = collection.mutable.Map.empty[ActorRef, InetSocketAddress] - - - // on startup, we attempt to connect to a number of electrum clients - // they will send us an `ElectrumReady` message when they're connected, or - // terminate if they cannot connect - (0 until Math.min(MAX_CONNECTION_COUNT, serverAddresses.size)) foreach (_ => self ! Connect) - - log.debug("starting electrum pool with serverAddresses={}", serverAddresses) - - // custom supervision strategy: always stop Electrum clients when there's a problem, we will automatically reconnect - // to another client - override def supervisorStrategy: SupervisorStrategy = OneForOneStrategy(loggingEnabled = true) { - case _ => SupervisorStrategy.stop - } - - startWith(Disconnected, DisconnectedData) - - when(Disconnected) { - case Event(ElectrumClient.ElectrumReady(height, tip, _), _) if addresses.contains(sender) => - sender ! ElectrumClient.HeaderSubscription(self) - handleHeader(sender, height, tip, None) - - case Event(ElectrumClient.AddStatusListener(listener), _) => - statusListeners += listener - stay - - case Event(Terminated(actor), _) => - log.debug("lost connection to {}", addresses(actor)) - addresses -= actor - context.system.scheduler.scheduleOnce(5 seconds, self, Connect) - stay - } - - when(Connected) { - case Event(ElectrumClient.ElectrumReady(height, tip, _), d: ConnectedData) if addresses.contains(sender) => - sender ! ElectrumClient.HeaderSubscription(self) - handleHeader(sender, height, tip, Some(d)) - - case Event(ElectrumClient.HeaderSubscriptionResponse(height, tip), d: ConnectedData) if addresses.contains(sender) => - handleHeader(sender, height, tip, Some(d)) - - case Event(request: ElectrumClient.Request, ConnectedData(master, _)) => - master forward request - stay - - case Event(ElectrumClient.AddStatusListener(listener), d: ConnectedData) if addresses.contains(d.master) => - statusListeners += listener - listener ! ElectrumClient.ElectrumReady(d.tips(d.master), addresses(d.master)) - stay - - case Event(Terminated(actor), d: ConnectedData) => - val address = addresses(actor) - addresses -= actor - context.system.scheduler.scheduleOnce(5 seconds, self, Connect) - val tips1 = d.tips - actor - if (tips1.isEmpty) { - log.info("lost connection to {}, no active connections left", address) - goto(Disconnected) using DisconnectedData // no more connections - } else if (d.master != actor) { - log.debug("lost connection to {}, we still have our master server", address) - stay using d.copy(tips = tips1) // we don't care, this wasn't our master - } else { - log.info("lost connection to our master server {}", address) - // we choose next best candidate as master - val tips1 = d.tips - actor - val (bestClient, bestTip) = tips1.toSeq.maxBy(_._2._1) - handleHeader(bestClient, bestTip._1, bestTip._2, Some(d.copy(tips = tips1))) - } - } - - whenUnhandled { - case Event(Connect, _) => - pickAddress(serverAddresses, addresses.values.toSet) match { - case Some(ElectrumServerAddress(address, ssl)) => - val resolved = new InetSocketAddress(address.getHostName, address.getPort) - val client = context.actorOf(Props(new ElectrumClient(resolved, ssl, socksProxy_opt))) - client ! ElectrumClient.AddStatusListener(self) - // we watch each electrum client, they will stop on disconnection - context watch client - addresses += (client -> address) - case None => () // no more servers available - } - stay - - case Event(ElectrumClient.ElectrumDisconnected, _) => - stay // ignored, we rely on Terminated messages to detect disconnections - } - - onTransition { - case Connected -> Disconnected => - statusListeners.foreach(_ ! ElectrumClient.ElectrumDisconnected) - context.system.eventStream.publish(ElectrumClient.ElectrumDisconnected) - } - - initialize() - - private def handleHeader(connection: ActorRef, height: Int, tip: BlockHeader, data: Option[ConnectedData]) = { - val remoteAddress = addresses(connection) - // we update our block count even if it doesn't come from our current master - updateBlockCount(height) - data match { - case None => - // as soon as we have a connection to an electrum server, we select it as master - log.info("selecting master {} at {}", remoteAddress, tip) - statusListeners.foreach(_ ! ElectrumClient.ElectrumReady(height, tip, remoteAddress)) - context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress)) - goto(Connected) using ConnectedData(connection, Map(connection -> (height, tip))) - case Some(d) if connection != d.master && height >= d.blockHeight + 2L => - // we only switch to a new master if there is a significant difference with our current master, because - // we don't want to switch to a new master every time a new block arrives (some servers will be notified before others) - // we check that the current connection is not our master because on regtest when you generate several blocks at once - // (and maybe on testnet in some pathological cases where there's a block every second) it may seen like our master - // skipped a block and is suddenly at height + 2 - log.info("switching to master {} at {}", remoteAddress, tip) - // we've switched to a new master, treat this as a disconnection/reconnection - // so users (wallet, watcher, ...) will reset their subscriptions - statusListeners.foreach(_ ! ElectrumClient.ElectrumDisconnected) - context.system.eventStream.publish(ElectrumClient.ElectrumDisconnected) - statusListeners.foreach(_ ! ElectrumClient.ElectrumReady(height, tip, remoteAddress)) - context.system.eventStream.publish(ElectrumClient.ElectrumReady(height, tip, remoteAddress)) - goto(Connected) using d.copy(master = connection, tips = d.tips + (connection -> (height, tip))) - case Some(d) => - log.debug("received tip {} from {} at {}", tip, remoteAddress, height) - stay using d.copy(tips = d.tips + (connection -> (height, tip))) - } - } - - private def updateBlockCount(blockCount: Long): Unit = { - // when synchronizing we don't want to advertise previous blocks - if (this.blockCount.get() < blockCount) { - log.debug("current blockchain height={}", blockCount) - context.system.eventStream.publish(CurrentBlockCount(blockCount)) - this.blockCount.set(blockCount) - } - } -} - -object ElectrumClientPool { - - val MAX_CONNECTION_COUNT = 3 - - case class ElectrumServerAddress(address: InetSocketAddress, ssl: SSL) - - /** - * Parses default electrum server list and extract addresses - * - * @param stream - * @param sslEnabled select plaintext/ssl ports - * @return - */ - def readServerAddresses(stream: InputStream, sslEnabled: Boolean): Set[ElectrumServerAddress] = try { - val JObject(values) = JsonMethods.parse(stream) - val addresses = values - .toMap - .filterKeys(!_.endsWith(".onion")) - .flatMap { - case (name, fields) => - if (sslEnabled) { - // We don't authenticate seed servers (SSL.LOOSE), because: - // - we don't know them so authentication doesn't really bring anything - // - most of them have self-signed SSL certificates so it would always fail - fields \ "s" match { - case JString(port) => Some(ElectrumServerAddress(InetSocketAddress.createUnresolved(name, port.toInt), SSL.LOOSE)) - case _ => None - } - } else { - fields \ "t" match { - case JString(port) => Some(ElectrumServerAddress(InetSocketAddress.createUnresolved(name, port.toInt), SSL.OFF)) - case _ => None - } - } - } - addresses.toSet - } finally { - stream.close() - } - - /** - * - * @param serverAddresses all addresses to choose from - * @param usedAddresses current connections - * @return a random address that we're not connected to yet - */ - def pickAddress(serverAddresses: Set[ElectrumServerAddress], usedAddresses: Set[InetSocketAddress]): Option[ElectrumServerAddress] = { - Random.shuffle(serverAddresses.filterNot(a => usedAddresses.contains(a.address)).toSeq).headOption - } - - // @formatter:off - sealed trait State - case object Disconnected extends State - case object Connected extends State - - sealed trait Data - case object DisconnectedData extends Data - case class ConnectedData(master: ActorRef, tips: Map[ActorRef, (Int, BlockHeader)]) extends Data { - def blockHeight = tips.get(master).map(_._1).getOrElse(0) - } - - case object Connect - // @formatter:on -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala deleted file mode 100644 index 818cb7ef11..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumEclairWallet.scala +++ /dev/null @@ -1,100 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.{ActorRef, ActorSystem} -import akka.pattern.ask -import fr.acinq.bitcoin.{ByteVector32, Crypto, Satoshi, Script, Transaction, TxOut} -import fr.acinq.eclair.addressToPublicKeyScript -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.BroadcastTransaction -import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.blockchain.{EclairWallet, MakeFundingTxResponse, OnChainBalance} -import grizzled.slf4j.Logging -import scodec.bits.ByteVector - -import scala.concurrent.{ExecutionContext, Future} - -class ElectrumEclairWallet(val wallet: ActorRef, chainHash: ByteVector32)(implicit system: ActorSystem, ec: ExecutionContext, timeout: akka.util.Timeout) extends EclairWallet with Logging { - - override def getBalance: Future[OnChainBalance] = (wallet ? GetBalance).mapTo[GetBalanceResponse].map(balance => OnChainBalance(balance.confirmed, balance.unconfirmed)) - - override def getReceiveAddress: Future[String] = (wallet ? GetCurrentReceiveAddress).mapTo[GetCurrentReceiveAddressResponse].map(_.address) - - override def getReceivePubkey(receiveAddress: Option[String] = None): Future[Crypto.PublicKey] = Future.failed(new RuntimeException("Not implemented")) - - def getXpub: Future[GetXpubResponse] = (wallet ? GetXpub).mapTo[GetXpubResponse] - - override def makeFundingTx(pubkeyScript: ByteVector, amount: Satoshi, feeRatePerKw: FeeratePerKw): Future[MakeFundingTxResponse] = { - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, pubkeyScript) :: Nil, lockTime = 0) - (wallet ? CompleteTransaction(tx, feeRatePerKw)).mapTo[CompleteTransactionResponse].map { - case CompleteTransactionResponse(tx1, fee1, None) => MakeFundingTxResponse(tx1, 0, fee1) - case CompleteTransactionResponse(_, _, Some(error)) => throw error - } - } - - override def commit(tx: Transaction): Future[Boolean] = - (wallet ? BroadcastTransaction(tx)) flatMap { - case ElectrumClient.BroadcastTransactionResponse(tx, None) => - //tx broadcast successfully: commit tx - wallet ? CommitTransaction(tx) - case ElectrumClient.BroadcastTransactionResponse(tx, Some(error)) if error.message.contains("transaction already in block chain") => - // tx was already in the blockchain, that's weird but it is OK - wallet ? CommitTransaction(tx) - case ElectrumClient.BroadcastTransactionResponse(_, Some(error)) => - //tx broadcast failed: cancel tx - logger.error(s"cannot broadcast tx ${tx.txid}: $error") - wallet ? CancelTransaction(tx) - case ElectrumClient.ServerError(ElectrumClient.BroadcastTransaction(tx), error) => - //tx broadcast failed: cancel tx - logger.error(s"cannot broadcast tx ${tx.txid}: $error") - wallet ? CancelTransaction(tx) - } map { - case CommitTransactionResponse(_) => true - case CancelTransactionResponse(_) => false - } - - def sendPayment(amount: Satoshi, address: String, feeRatePerKw: FeeratePerKw): Future[String] = { - val publicKeyScript = Script.write(addressToPublicKeyScript(address, chainHash)) - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0) - (wallet ? CompleteTransaction(tx, feeRatePerKw)) - .mapTo[CompleteTransactionResponse] - .flatMap { - case CompleteTransactionResponse(tx, _, None) => commit(tx).map { - case true => tx.txid.toString() - case false => throw new RuntimeException(s"could not commit tx=$tx") - } - case CompleteTransactionResponse(_, _, Some(error)) => throw error - } - } - - def sendAll(address: String, feeRatePerKw: FeeratePerKw): Future[(Transaction, Satoshi)] = { - val publicKeyScript = Script.write(addressToPublicKeyScript(address, chainHash)) - (wallet ? SendAll(publicKeyScript, feeRatePerKw)) - .mapTo[SendAllResponse] - .map { - case SendAllResponse(tx, fee) => (tx, fee) - } - } - - override def rollback(tx: Transaction): Future[Boolean] = (wallet ? CancelTransaction(tx)).map(_ => true) - - override def doubleSpent(tx: Transaction): Future[Boolean] = { - (wallet ? IsDoubleSpent(tx)).mapTo[IsDoubleSpentResponse].map(_.isDoubleSpent) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala deleted file mode 100644 index d4a13be1b4..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWallet.scala +++ /dev/null @@ -1,1090 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.{ActorRef, FSM, PoisonPill, Props} -import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey, hardened} -import fr.acinq.bitcoin.{Base58, Base58Check, Block, ByteVector32, Crypto, DeterministicWallet, OP_PUSHDATA, OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptElt, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.bitcoind.rpc.Error -import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ -import fr.acinq.eclair.blockchain.electrum.db.{HeaderDb, WalletDb} -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.Transactions -import grizzled.slf4j.Logging -import scodec.bits.ByteVector - -import scala.annotation.tailrec -import scala.util.{Failure, Success, Try} - -/** - * Simple electrum wallet. - * See the documentation at https://electrumx-spesmilo.readthedocs.io/en/latest/ - * - * Typical workflow: - * - * client ---- header update ----> wallet - * client ---- status update ----> wallet - * client <--- ask history ----- wallet - * client ---- history ----> wallet - * client <--- ask tx ----- wallet - * client ---- tx ----> wallet - */ -class ElectrumWallet(seed: ByteVector, client: ActorRef, params: ElectrumWallet.WalletParameters) extends FSM[ElectrumWallet.State, ElectrumWallet.Data] { - - import Blockchain.RETARGETING_PERIOD - import ElectrumWallet._ - import params._ - - val master = DeterministicWallet.generate(seed) - - val accountMaster = accountKey(master, chainHash) - val changeMaster = changeKey(master, chainHash) - - client ! ElectrumClient.AddStatusListener(self) - - // disconnected --> waitingForTip --> running --+ - // ^ | - // | | - // +--------------------------------------------+ - - /** - * If the wallet is ready and its state changed since the last time it was ready: - * - publish a `WalletReady` notification - * - persist state data - * - * @param data wallet data - * @return the input data with an updated 'last ready message' if needed - */ - def persistAndNotify(data: ElectrumWallet.Data): ElectrumWallet.Data = { - if (data.isReady(swipeRange)) { - data.lastReadyMessage match { - case Some(value) if value == data.readyMessage => - log.debug("ready message {} has already been sent", value) - data - case _ => - log.info(s"checking wallet") - val ready = data.readyMessage - log.info(s"wallet is ready with $ready") - context.system.eventStream.publish(ready) - context.system.eventStream.publish(NewWalletReceiveAddress(data.currentReceiveAddress)) - params.walletDb.persist(PersistentData(data)) - data.copy(lastReadyMessage = Some(ready)) - } - } else data - } - - // sent notifications for all wallet transactions - def advertiseTransactions(data: ElectrumWallet.Data): Unit = { - data.transactions.values.foreach(tx => data.computeTransactionDelta(tx).foreach { - case (received, sent, fee_opt) => - context.system.eventStream.publish(TransactionReceived(tx, data.computeTransactionDepth(tx.txid), received, sent, fee_opt, data.computeTimestamp(tx.txid, params.walletDb))) - }) - } - - startWith(DISCONNECTED, { - val blockchain = params.chainHash match { - // regtest is a special case, there are no checkpoints and we start with a single header - case Block.RegtestGenesisBlock.hash => Blockchain.fromGenesisBlock(Block.RegtestGenesisBlock.hash, Block.RegtestGenesisBlock.header) - case _ => - val checkpoints = CheckPoint.load(params.chainHash, params.walletDb) - Blockchain.fromCheckpoints(params.chainHash, checkpoints) - } - val headers = params.walletDb.getHeaders(blockchain.checkpoints.size * RETARGETING_PERIOD, None) - log.info(s"loading ${headers.size} headers from db") - val blockchain1 = Blockchain.addHeadersChunk(blockchain, blockchain.checkpoints.size * RETARGETING_PERIOD, headers) - val data = Try(params.walletDb.readPersistentData()) match { - case Success(Some(persisted)) => - val firstAccountKeys = (0 until persisted.accountKeysCount).map(i => derivePrivateKey(accountMaster, i)).toVector - val firstChangeKeys = (0 until persisted.changeKeysCount).map(i => derivePrivateKey(changeMaster, i)).toVector - - Data(blockchain1, - firstAccountKeys, - firstChangeKeys, - status = persisted.status, - transactions = persisted.transactions, - heights = persisted.heights, - history = persisted.history, - proofs = persisted.proofs, - locks = persisted.locks, - pendingHistoryRequests = Set(), - pendingHeadersRequests = Set(), - pendingTransactionRequests = Set(), - pendingTransactions = persisted.pendingTransactions, - lastReadyMessage = None) - case Success(None) => - log.info(s"wallet db is empty, starting with a default wallet") - val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector - val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector - Data(params, blockchain1, firstAccountKeys, firstChangeKeys) - case Failure(exception) => - log.info(s"cannot read wallet db ($exception), starting with a default wallet") - val firstAccountKeys = (0 until params.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector - val firstChangeKeys = (0 until params.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector - Data(params, blockchain1, firstAccountKeys, firstChangeKeys) - } - context.system.eventStream.publish(NewWalletReceiveAddress(data.currentReceiveAddress)) - log.info(s"restored wallet balance=${data.balance}") - data - }) - - when(DISCONNECTED) { - case Event(ElectrumClient.ElectrumReady(_, _, _), data) => - // subscribe to headers stream, server will reply with its current tip - client ! ElectrumClient.HeaderSubscription(self) - goto(WAITING_FOR_TIP) using data - } - - when(WAITING_FOR_TIP) { - case Event(ElectrumClient.HeaderSubscriptionResponse(height, header), data) => - if (height < data.blockchain.height) { - log.info(s"electrum server is behind at ${height} we're at ${data.blockchain.height}, disconnecting") - sender ! PoisonPill - goto(DISCONNECTED) using data - } else if (data.blockchain.bestchain.isEmpty) { - log.info("performing full sync") - // now ask for the first header after our latest checkpoint - client ! ElectrumClient.GetHeaders(data.blockchain.checkpoints.size * RETARGETING_PERIOD, RETARGETING_PERIOD) - goto(SYNCING) using data - } else if (header == data.blockchain.tip.header) { - // nothing to sync - data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self)) - data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self)) - advertiseTransactions(data) - goto(RUNNING) using persistAndNotify(data) - } else { - client ! ElectrumClient.GetHeaders(data.blockchain.tip.height + 1, RETARGETING_PERIOD) - log.info(s"syncing headers from ${data.blockchain.height} to ${height}, ready=${data.isReady(params.swipeRange)}") - goto(SYNCING) using data - } - } - - when(SYNCING) { - case Event(ElectrumClient.GetHeadersResponse(start, headers, _), data) => - if (headers.isEmpty) { - // ok, we're all synced now - log.info(s"headers sync complete, tip=${data.blockchain.tip}") - data.accountKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self)) - data.changeKeys.foreach(key => client ! ElectrumClient.ScriptHashSubscription(computeScriptHashFromPublicKey(key.publicKey), self)) - advertiseTransactions(data) - goto(RUNNING) using persistAndNotify(data) - } else { - Try(Blockchain.addHeaders(data.blockchain, start, headers)) match { - case Success(blockchain1) => - val (blockchain2, saveme) = Blockchain.optimize(blockchain1) - saveme.grouped(RETARGETING_PERIOD).foreach(chunk => params.walletDb.addHeaders(chunk.head.height, chunk.map(_.header))) - log.info(s"requesting new headers chunk at ${blockchain2.tip.height}") - client ! ElectrumClient.GetHeaders(blockchain2.tip.height + 1, RETARGETING_PERIOD) - goto(SYNCING) using data.copy(blockchain = blockchain2) - case Failure(error) => - log.error("electrum server sent bad headers, disconnecting", error) - sender ! PoisonPill - goto(DISCONNECTED) using data - } - } - - case Event(ElectrumClient.HeaderSubscriptionResponse(height, header), data) => - // we can ignore this, we will request header chunks until the server has nothing left to send us - log.debug("ignoring header {} at {} while syncing", header, height) - stay() - } - - when(RUNNING) { - case Event(ElectrumClient.HeaderSubscriptionResponse(_, header), data) if data.blockchain.tip.header == header => stay - - case Event(ElectrumClient.HeaderSubscriptionResponse(height, header), data) => - log.info(s"got new tip ${header.blockId} at ${height}") - - val difficulty = Blockchain.getDifficulty(data.blockchain, height, params.walletDb) - - if (!difficulty.forall(target => header.bits == target)) { - log.error(s"electrum server send bad header (difficulty is not valid), disconnecting") - sender ! PoisonPill - stay() - } else { - Try(Blockchain.addHeader(data.blockchain, height, header)) match { - case Success(blockchain1) => - data.heights.collect { - case (txid, txheight) if txheight > 0 => - val confirmations = computeDepth(height, txheight) - context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations, data.computeTimestamp(txid, params.walletDb))) - } - val (blockchain2, saveme) = Blockchain.optimize(blockchain1) - saveme.grouped(RETARGETING_PERIOD).foreach(chunk => params.walletDb.addHeaders(chunk.head.height, chunk.map(_.header))) - stay using persistAndNotify(data.copy(blockchain = blockchain2)) - case Failure(error) => - log.error(error, s"electrum server sent bad header, disconnecting") - sender ! PoisonPill - stay() using data - } - } - - case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if data.status.get(scriptHash) == Some(status) => - val missing = data.missingTransactions(scriptHash) - missing.foreach(txid => client ! GetTransaction(txid)) - stay using persistAndNotify(data.copy(pendingHistoryRequests = data.pendingTransactionRequests ++ missing)) - - case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if !data.accountKeyMap.contains(scriptHash) && !data.changeKeyMap.contains(scriptHash) => - log.warning(s"received status=$status for scriptHash=$scriptHash which does not match any of our keys") - stay - - case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) if status == "" => - val data1 = data.copy(status = data.status + (scriptHash -> status)) // empty status, nothing to do - stay using persistAndNotify(data1) - - case Event(ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status), data) => - val key = data.accountKeyMap.getOrElse(scriptHash, data.changeKeyMap(scriptHash)) - val isChange = data.changeKeyMap.contains(scriptHash) - log.info(s"received status=$status for scriptHash=$scriptHash key=${segwitAddress(key, chainHash)} isChange=$isChange") - - // let's retrieve the tx history for this key - client ! ElectrumClient.GetScriptHashHistory(scriptHash) - - val (newAccountKeys, newChangeKeys) = data.status.get(Try(ByteVector32.fromValidHex(status)).getOrElse(ByteVector32.Zeroes)) match { - case None => - // first time this script hash is used, need to generate a new key - val newKey = if (isChange) derivePrivateKey(changeMaster, data.changeKeys.last.path.lastChildNumber + 1) else derivePrivateKey(accountMaster, data.accountKeys.last.path.lastChildNumber + 1) - val newScriptHash = computeScriptHashFromPublicKey(newKey.publicKey) - log.info(s"generated key with index=${newKey.path.lastChildNumber} scriptHash=$newScriptHash key=${segwitAddress(newKey, chainHash)} isChange=$isChange") - // listens to changes for the newly generated key - client ! ElectrumClient.ScriptHashSubscription(newScriptHash, self) - if (isChange) (data.accountKeys, data.changeKeys :+ newKey) else (data.accountKeys :+ newKey, data.changeKeys) - case Some(_) => (data.accountKeys, data.changeKeys) - } - - val data1 = data.copy( - accountKeys = newAccountKeys, - changeKeys = newChangeKeys, - status = data.status + (scriptHash -> status), - pendingHistoryRequests = data.pendingHistoryRequests + scriptHash) - - stay using persistAndNotify(data1) - - case Event(ElectrumClient.GetScriptHashHistoryResponse(scriptHash, items), data) => - log.debug("scriptHash={} has history={}", scriptHash, items) - val shadow_items = data.history.get(scriptHash) match { - case Some(existing_items) => existing_items.filterNot(item => items.exists(_.tx_hash == item.tx_hash)) - case None => Nil - } - shadow_items.foreach(item => log.warning(s"keeping shadow item for txid=${item.tx_hash}")) - val items0 = items ++ shadow_items - - val pendingHeadersRequests1 = collection.mutable.HashSet.empty[GetHeaders] - pendingHeadersRequests1 ++= data.pendingHeadersRequests - - /** - * If we don't already have a header at this height, or a pending request to download the header chunk it's in, - * download this header chunk. - * We don't have this header because it's most likely older than our current checkpoint, downloading the whole header - * chunk (2016 headers) is quick and they're easy to verify. - */ - def downloadHeadersIfMissing(height: Int): Unit = { - if (data.blockchain.getHeader(height).orElse(params.walletDb.getHeader(height)).isEmpty) { - // we don't have this header, probably because it is older than our checkpoints - // request the entire chunk, we will be able to check it efficiently and then store it - val start = (height / RETARGETING_PERIOD) * RETARGETING_PERIOD - val request = GetHeaders(start, RETARGETING_PERIOD) - // there may be already a pending request for this chunk of headers - if (!pendingHeadersRequests1.contains(request)) { - client ! request - pendingHeadersRequests1.add(request) - } - } - } - - val (heights1, pendingTransactionRequests1) = items0.foldLeft((data.heights, data.pendingTransactionRequests)) { - case ((heights, hashes), item) if !data.transactions.contains(item.tx_hash) && !data.pendingTransactionRequests.contains(item.tx_hash) => - // we retrieve the tx if we don't have it and haven't yet requested it - client ! GetTransaction(item.tx_hash) - if (item.height > 0) { // don't ask for merkle proof for unconfirmed transactions - downloadHeadersIfMissing(item.height) - client ! GetMerkle(item.tx_hash, item.height) - } - (heights + (item.tx_hash -> item.height), hashes + item.tx_hash) - case ((heights, hashes), item) => - // otherwise we just update the height - (heights + (item.tx_hash -> item.height), hashes) - } - - // we now have updated height for all our transactions, - heights1.collect { - case (txid, height) => - val confirmations = if (height <= 0) 0 else computeDepth(data.blockchain.tip.height, height) - (data.heights.get(txid), height) match { - case (None, height) if height <= 0 => - // height=0 => unconfirmed, height=-1 => unconfirmed and one input is unconfirmed - case (None, height) if height > 0 => - // first time we get a height for this tx: either it was just confirmed, or we restarted the wallet - context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations, data.computeTimestamp(txid, params.walletDb))) - downloadHeadersIfMissing(height.toInt) - client ! GetMerkle(txid, height.toInt) - case (Some(previousHeight), height) if previousHeight != height => - // there was a reorg - context.system.eventStream.publish(TransactionConfidenceChanged(txid, confirmations, data.computeTimestamp(txid, params.walletDb))) - if (height > 0) { - downloadHeadersIfMissing(height.toInt) - client ! GetMerkle(txid, height.toInt) - } - case (Some(previousHeight), height) if previousHeight == height && height > 0 && data.proofs.get(txid).isEmpty => - downloadHeadersIfMissing(height.toInt) - client ! GetMerkle(txid, height.toInt) - case (Some(previousHeight), height) if previousHeight == height => - // no reorg, nothing to do - } - } - val data1 = data.copy( - heights = heights1, - history = data.history + (scriptHash -> items0), - pendingHistoryRequests = data.pendingHistoryRequests - scriptHash, - pendingTransactionRequests = pendingTransactionRequests1, - pendingHeadersRequests = pendingHeadersRequests1.toSet) - stay using persistAndNotify(data1) - - case Event(ElectrumClient.GetHeadersResponse(start, headers, _), data) => - Try(Blockchain.addHeadersChunk(data.blockchain, start, headers)) match { - case Success(blockchain1) => - params.walletDb.addHeaders(start, headers) - stay() using data.copy(blockchain = blockchain1) - case Failure(error) => - log.error("electrum server sent bad headers, disconnecting", error) - sender ! PoisonPill - goto(DISCONNECTED) using data - } - - case Event(GetTransactionResponse(tx, context_opt), data) => - log.debug(s"received transaction ${tx.txid}") - data.computeTransactionDelta(tx) match { - case Some((received, sent, fee_opt)) => - log.info(s"successfully connected txid=${tx.txid}") - context.system.eventStream.publish(TransactionReceived(tx, data.computeTransactionDepth(tx.txid), received, sent, fee_opt, data.computeTimestamp(tx.txid, params.walletDb))) - // when we have successfully processed a new tx, we retry all pending txes to see if they can be added now - data.pendingTransactions.foreach(self ! GetTransactionResponse(_, context_opt)) - val data1 = data.copy(transactions = data.transactions + (tx.txid -> tx), pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = Nil) - stay using persistAndNotify(data1) - case None => - // missing parents - log.info(s"couldn't connect txid=${tx.txid}") - val data1 = data.copy(pendingTransactionRequests = data.pendingTransactionRequests - tx.txid, pendingTransactions = data.pendingTransactions :+ tx) - stay using persistAndNotify(data1) - } - - case Event(ServerError(GetTransaction(txid, _), error), data) if data.pendingTransactionRequests.contains(txid) => - // server tells us that txid belongs to our wallet history, but cannot provide tx ? - log.error(s"server cannot find history tx $txid: $error") - sender ! PoisonPill - goto(DISCONNECTED) using data - - - case Event(response@GetMerkleResponse(txid, _, height, _, _), data) => - data.blockchain.getHeader(height).orElse(params.walletDb.getHeader(height)) match { - case Some(header) if header.hashMerkleRoot == response.root => - log.info(s"transaction $txid has been verified") - val data1 = if (data.transactions.get(txid).isEmpty && !data.pendingTransactionRequests.contains(txid) && !data.pendingTransactions.exists(_.txid == txid)) { - log.warning(s"we received a Merkle proof for transaction $txid that we don't have") - data - } else { - data.copy(proofs = data.proofs + (txid -> response)) - } - stay using data1 - case Some(_) => - log.error(s"server sent an invalid proof for $txid, disconnecting") - sender ! PoisonPill - stay() using data.copy(transactions = data.transactions - txid) - case None => - // this is probably because the tx is old and within our checkpoints => request the whole header chunk - val start = (height / RETARGETING_PERIOD) * RETARGETING_PERIOD - val request = GetHeaders(start, RETARGETING_PERIOD) - val pendingHeadersRequest1 = if (data.pendingHeadersRequests.contains(request)) { - data.pendingHeadersRequests - } else { - client ! request - self ! response - data.pendingHeadersRequests + request - } - stay() using data.copy(pendingHeadersRequests = pendingHeadersRequest1) - } - - case Event(CompleteTransaction(tx, feeRatePerKw), data) => - Try(data.completeTransaction(tx, feeRatePerKw, minimumFee, dustLimit, allowSpendUnconfirmed)) match { - case Success((data1, tx1, fee1)) => stay using data1 replying CompleteTransactionResponse(tx1, fee1, None) - case Failure(t) => stay replying CompleteTransactionResponse(tx, 0 sat, Some(t)) - } - - case Event(SendAll(publicKeyScript, feeRatePerKw), data) => - val (tx, fee) = data.spendAll(publicKeyScript, feeRatePerKw) - stay replying SendAllResponse(tx, fee) - - case Event(CommitTransaction(tx), data) => - log.info(s"committing txid=${tx.txid}") - val data1 = data.commitTransaction(tx) - // we use the initial state to compute the effect of the tx - // note: we know that computeTransactionDelta and the fee will be defined, because we built the tx ourselves so - // we know all the parents - val (received, sent, Some(fee)) = data.computeTransactionDelta(tx).get - // we notify here because the tx won't be downloaded again (it has been added to the state at commit) - context.system.eventStream.publish(TransactionReceived(tx, data1.computeTransactionDepth(tx.txid), received, sent, Some(fee), None)) - stay using persistAndNotify(data1) replying CommitTransactionResponse(tx) // goto instead of stay because we want to fire transitions - - case Event(CancelTransaction(tx), data) => - log.info(s"cancelling txid=${tx.txid}") - stay using persistAndNotify(data.cancelTransaction(tx)) replying CancelTransactionResponse(tx) - - case Event(bc@ElectrumClient.BroadcastTransaction(tx), _) => - log.info(s"broadcasting txid=${tx.txid}") - client forward bc - stay - } - - whenUnhandled { - - case Event(IsDoubleSpent(tx), data) => - // detect if one of our transaction (i.e a transaction that spends from our wallet) has been double-spent - val isDoubleSpent = data.heights - .filter { case (_, height) => computeDepth(data.blockchain.height, height) >= 2 } // we only consider tx that have been confirmed - .flatMap { case (txid, _) => data.transactions.get(txid) } // we get the full tx - .exists(spendingTx => spendingTx.txIn.map(_.outPoint).toSet.intersect(tx.txIn.map(_.outPoint).toSet).nonEmpty && spendingTx.txid != tx.txid) // look for a tx that spend the same utxos and has a different txid - stay() replying IsDoubleSpentResponse(tx, isDoubleSpent) - - case Event(ElectrumClient.ElectrumDisconnected, data) => - log.info(s"wallet got disconnected") - // remove status for each script hash for which we have pending requests - // this will make us query script hash history for these script hashes again when we reconnect - goto(DISCONNECTED) using data.copy( - status = data.status -- data.pendingHistoryRequests, - pendingHistoryRequests = Set(), - pendingTransactionRequests = Set(), - pendingHeadersRequests = Set(), - lastReadyMessage = None - ) - - case Event(GetCurrentReceiveAddress, data) => stay replying GetCurrentReceiveAddressResponse(data.currentReceiveAddress) - - case Event(GetBalance, data) => - val (confirmed, unconfirmed) = data.balance - stay replying GetBalanceResponse(confirmed, unconfirmed) - - case Event(GetData, data) => stay replying GetDataResponse(data) - - case Event(GetXpub, _) => - val (xpub, path) = computeXpub(master, chainHash) - stay replying GetXpubResponse(xpub, path) - - case Event(ElectrumClient.BroadcastTransaction(tx), _) => stay replying ElectrumClient.BroadcastTransactionResponse(tx, Some(Error(-1, "wallet is not connected"))) - } - - initialize() - -} - -object ElectrumWallet { - def props(seed: ByteVector, client: ActorRef, params: WalletParameters): Props = Props(new ElectrumWallet(seed, client, params)) - - case class WalletParameters(chainHash: ByteVector32, walletDb: WalletDb, minimumFee: Satoshi = 2000 sat, dustLimit: Satoshi = 546 sat, swipeRange: Int = 10, allowSpendUnconfirmed: Boolean = true) - - // @formatter:off - sealed trait State - case object DISCONNECTED extends State - case object WAITING_FOR_TIP extends State - case object SYNCING extends State - case object RUNNING extends State - - sealed trait Request - sealed trait Response - - case object GetBalance extends Request - case class GetBalanceResponse(confirmed: Satoshi, unconfirmed: Satoshi) extends Response - - case object GetXpub extends Request - case class GetXpubResponse(xpub: String, path: String) extends Response - - case object GetCurrentReceiveAddress extends Request - case class GetCurrentReceiveAddressResponse(address: String) extends Response - - case object GetData extends Request - case class GetDataResponse(state: Data) extends Response - - case class CompleteTransaction(tx: Transaction, feeRatePerKw: FeeratePerKw) extends Request - case class CompleteTransactionResponse(tx: Transaction, fee: Satoshi, error: Option[Throwable]) extends Response - - case class SendAll(publicKeyScript: ByteVector, feeRatePerKw: FeeratePerKw) extends Request - case class SendAllResponse(tx: Transaction, fee: Satoshi) extends Response - - case class CommitTransaction(tx: Transaction) extends Request - case class CommitTransactionResponse(tx: Transaction) extends Response - - case class SendTransaction(tx: Transaction) extends Request - case class SendTransactionReponse(tx: Transaction) extends Response - - case class CancelTransaction(tx: Transaction) extends Request - case class CancelTransactionResponse(tx: Transaction) extends Response - - case object InsufficientFunds extends Response - case class AmountBelowDustLimit(dustLimit: Satoshi) extends Response - - case class GetPrivateKey(address: String) extends Request - case class GetPrivateKeyResponse(address: String, key: Option[ExtendedPrivateKey]) extends Response - - case class IsDoubleSpent(tx: Transaction) extends Request - case class IsDoubleSpentResponse(tx: Transaction, isDoubleSpent: Boolean) extends Response - - sealed trait WalletEvent - /** - * - * @param tx - * @param depth - * @param received - * @param sent - * @param feeOpt is set only when we know it (i.e. for outgoing transactions) - */ - case class TransactionReceived(tx: Transaction, depth: Long, received: Satoshi, sent: Satoshi, feeOpt: Option[Satoshi], timestamp: Option[Long]) extends WalletEvent - case class TransactionConfidenceChanged(txid: ByteVector32, depth: Long, timestamp: Option[Long]) extends WalletEvent - case class NewWalletReceiveAddress(address: String) extends WalletEvent - case class WalletReady(confirmedBalance: Satoshi, unconfirmedBalance: Satoshi, height: Long, timestamp: Long) extends WalletEvent - // @formatter:on - - /** - * - * @param key public key - * @return the address of the p2sh-of-p2wpkh script for this key - */ - def segwitAddress(key: PublicKey, chainHash: ByteVector32): String = { - val script = Script.pay2wpkh(key) - val hash = Crypto.hash160(Script.write(script)) - (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => Base58Check.encode(Base58.Prefix.ScriptAddressTestnet, hash) - case Block.LivenetGenesisBlock.hash => Base58Check.encode(Base58.Prefix.ScriptAddress, hash) - } - } - - def segwitAddress(key: ExtendedPrivateKey, chainHash: ByteVector32): String = segwitAddress(key.publicKey, chainHash) - - def segwitAddress(key: PrivateKey, chainHash: ByteVector32): String = segwitAddress(key.publicKey, chainHash) - - /** - * - * @param key public key - * @return a p2sh-of-p2wpkh script for this key - */ - def computePublicKeyScript(key: PublicKey) = Script.pay2sh(Script.pay2wpkh(key)) - - /** - * - * @param key public key - * @return the hash of the public key script for this key, as used by Electrum's hash-based methods - */ - def computeScriptHashFromPublicKey(key: PublicKey): ByteVector32 = Crypto.sha256(Script.write(computePublicKeyScript(key))).reverse - - def accountPath(chainHash: ByteVector32): List[Long] = (chainHash: @unchecked) match { - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => hardened(49) :: hardened(1) :: hardened(0) :: Nil - case Block.LivenetGenesisBlock.hash => hardened(49) :: hardened(0) :: hardened(0) :: Nil - } - - /** - * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh - * - * @param master master key - * @return the BIP49 account key for this master key: m/49'/1'/0'/0 on testnet/regtest, m/49'/0'/0'/0 on mainnet - */ - def accountKey(master: ExtendedPrivateKey, chainHash: ByteVector32) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 0L :: Nil) - - - /** - * Compute the wallet's xpub - * - * @param master master key - * @param chainHash chain hash - * @return a (xpub, path) tuple where xpub is the encoded account public key, and path is the derivation path for the account key - */ - def computeXpub(master: ExtendedPrivateKey, chainHash: ByteVector32): (String, String) = { - val xpub = DeterministicWallet.publicKey(DeterministicWallet.derivePrivateKey(master, accountPath(chainHash))) - val prefix = (chainHash: @unchecked) match { - case Block.LivenetGenesisBlock.hash => DeterministicWallet.ypub - case Block.RegtestGenesisBlock.hash | Block.TestnetGenesisBlock.hash => DeterministicWallet.upub - } - (DeterministicWallet.encode(xpub, prefix), xpub.path.toString()) - } - - /** - * use BIP49 (and not BIP44) since we use p2sh-of-p2wpkh - * - * @param master master key - * @return the BIP49 change key for this master key: m/49'/1'/0'/1 on testnet/regtest, m/49'/0'/0'/1 on mainnet - */ - def changeKey(master: ExtendedPrivateKey, chainHash: ByteVector32) = DeterministicWallet.derivePrivateKey(master, accountPath(chainHash) ::: 1L :: Nil) - - def totalAmount(utxos: Seq[Utxo]): Satoshi = Satoshi(utxos.map(_.item.value).sum) - - def totalAmount(utxos: Set[Utxo]): Satoshi = totalAmount(utxos.toSeq) - - /** - * - * @param weight transaction weight - * @param feeRatePerKw fee rate - * @return the fee for this tx weight - */ - def computeFee(weight: Int, feeRatePerKw: Long): Satoshi = Satoshi((weight * feeRatePerKw) / 1000) - - /** - * - * @param txIn transaction input - * @return Some(pubkey) if this tx input spends a p2sh-of-p2wpkh(pub), None otherwise - */ - def extractPubKeySpentFrom(txIn: TxIn): Option[PublicKey] = { - Try { - // we're looking for tx that spend a pay2sh-of-p2wkph output - require(txIn.witness.stack.size == 2) - val sig = txIn.witness.stack(0) - val pub = txIn.witness.stack(1) - val OP_PUSHDATA(script, _) :: Nil = Script.parse(txIn.signatureScript) - val publicKey = PublicKey(pub) - if (Script.write(Script.pay2wpkh(publicKey)) == script) { - Some(publicKey) - } else None - } getOrElse None - } - - def computeDepth(currentHeight: Long, txHeight: Long): Long = - if (txHeight <= 0) { - // txHeight is 0 if tx in unconfirmed, and -1 if one of its inputs is unconfirmed - 0 - } else { - currentHeight - txHeight + 1 - } - - case class Utxo(key: ExtendedPrivateKey, item: ElectrumClient.UnspentItem) { - def outPoint: OutPoint = item.outPoint - } - - /** - * Wallet state, which stores data returned by Electrum servers. - * Most items are indexed by script hash (i.e. by pubkey script sha256 hash). - * Height follows Electrum's conventions: - * - h > 0 means that the tx was confirmed at block #h - * - 0 means unconfirmed, but all input are confirmed - * < 0 means unconfirmed, and some inputs are unconfirmed as well - * - * @param blockchain blockchain - * @param accountKeys account keys - * @param changeKeys change keys - * @param status script hash -> status; "" means that the script hash has not been used yet - * @param transactions wallet transactions - * @param heights transactions heights - * @param history script hash -> history - * @param locks transactions which lock some of our utxos. - * @param pendingHistoryRequests requests pending a response from the electrum server - * @param pendingTransactionRequests requests pending a response from the electrum server - * @param pendingTransactions transactions received but not yet connected to their parents - */ - case class Data(blockchain: Blockchain, - accountKeys: Vector[ExtendedPrivateKey], - changeKeys: Vector[ExtendedPrivateKey], - status: Map[ByteVector32, String], - transactions: Map[ByteVector32, Transaction], - heights: Map[ByteVector32, Int], - history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], - proofs: Map[ByteVector32, GetMerkleResponse], - locks: Set[Transaction], - pendingHistoryRequests: Set[ByteVector32], - pendingTransactionRequests: Set[ByteVector32], - pendingHeadersRequests: Set[GetHeaders], - pendingTransactions: List[Transaction], - lastReadyMessage: Option[WalletReady]) extends Logging { - val chainHash = blockchain.chainHash - - lazy val accountKeyMap = accountKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap - - lazy val changeKeyMap = changeKeys.map(key => computeScriptHashFromPublicKey(key.publicKey) -> key).toMap - - lazy val firstUnusedAccountKeys = accountKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some("")) - - lazy val firstUnusedChangeKeys = changeKeys.find(key => status.get(computeScriptHashFromPublicKey(key.publicKey)) == Some("")) - - lazy val publicScriptMap = (accountKeys ++ changeKeys).map(key => Script.write(computePublicKeyScript(key.publicKey)) -> key).toMap - - lazy val utxos = history.keys.toSeq.map(scriptHash => getUtxos(scriptHash)).flatten - - /** - * The wallet is ready if all current keys have an empty status, and we don't have - * any history/tx request pending - * NB: swipeRange * 2 because we have account keys and change keys - */ - def isReady(swipeRange: Int) = status.filter(_._2 == "").size >= swipeRange * 2 && pendingHistoryRequests.isEmpty && pendingTransactionRequests.isEmpty - - def readyMessage: WalletReady = { - val (confirmed, unconfirmed) = balance - WalletReady(confirmed, unconfirmed, blockchain.tip.height, blockchain.tip.header.time) - } - - /** - * - * @return the ids of transactions that belong to our wallet history for this script hash but that we don't have - * and have no pending requests for. - */ - def missingTransactions(scriptHash: ByteVector32): Set[ByteVector32] = { - val txids = history.getOrElse(scriptHash, List()).map(_.tx_hash).filterNot(txhash => transactions.contains(txhash)).toSet - txids -- pendingTransactionRequests - } - - /** - * - * @return the current receive key. In most cases it will be a key that has not - * been used yet but it may be possible that we are still looking for - * unused keys and none is available yet. In this case we will return - * the latest account key. - */ - def currentReceiveKey = firstUnusedAccountKeys.headOption.getOrElse { - // bad luck we are still looking for unused keys - // use the first account key - accountKeys.head - } - - def currentReceiveAddress = segwitAddress(currentReceiveKey, chainHash) - - /** - * - * @return the current change key. In most cases it will be a key that has not - * been used yet but it may be possible that we are still looking for - * unused keys and none is available yet. In this case we will return - * the latest change key. - */ - def currentChangeKey = firstUnusedChangeKeys.headOption.getOrElse { - // bad luck we are still looking for unused keys - // use the first account key - changeKeys.head - } - - def currentChangeAddress = segwitAddress(currentChangeKey, chainHash) - - def isMine(txIn: TxIn): Boolean = extractPubKeySpentFrom(txIn).exists(pub => publicScriptMap.contains(Script.write(computePublicKeyScript(pub)))) - - def isSpend(txIn: TxIn, publicKey: PublicKey): Boolean = extractPubKeySpentFrom(txIn).contains(publicKey) - - /** - * - * @param txIn - * @param scriptHash - * @return true if txIn spends from an address that matches scriptHash - */ - def isSpend(txIn: TxIn, scriptHash: ByteVector32): Boolean = extractPubKeySpentFrom(txIn).exists(pub => computeScriptHashFromPublicKey(pub) == scriptHash) - - def isReceive(txOut: TxOut, scriptHash: ByteVector32): Boolean = publicScriptMap.get(txOut.publicKeyScript).exists(key => computeScriptHashFromPublicKey(key.publicKey) == scriptHash) - - def isMine(txOut: TxOut): Boolean = publicScriptMap.contains(txOut.publicKeyScript) - - def computeTransactionDepth(txid: ByteVector32): Long = heights.get(txid).map(height => if (height > 0) computeDepth(blockchain.tip.height, height) else 0).getOrElse(0) - - /** - * - * @param txid transaction id - * @param headerDb header db - * @return the timestamp of the block this tx was included in - */ - def computeTimestamp(txid: ByteVector32, headerDb: HeaderDb): Option[Long] = { - for { - height <- heights.get(txid) - header <- blockchain.getHeader(height).orElse(headerDb.getHeader(height)) - } yield header.time - } - - /** - * - * @param scriptHash script hash - * @return the list of UTXOs for this script hash (including unconfirmed UTXOs) - */ - def getUtxos(scriptHash: ByteVector32) = { - history.get(scriptHash) match { - case None => Seq() - case Some(items) if items.isEmpty => Seq() - case Some(items) => - // this is the private key for this script hash - val key = accountKeyMap.getOrElse(scriptHash, changeKeyMap(scriptHash)) - - // find all transactions that send to or receive from this script hash - // we use collect because we may not yet have received all transactions in the history - val txs = items collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) } - - // find all tx outputs that send to our script hash - val unspents = items collect { case item if transactions.contains(item.tx_hash) => - val tx = transactions(item.tx_hash) - val outputs = tx.txOut.zipWithIndex.filter { case (txOut, index) => isReceive(txOut, scriptHash) } - outputs.map { case (txOut, index) => Utxo(key, ElectrumClient.UnspentItem(item.tx_hash, index, txOut.amount.toLong, item.height)) } - } flatten - - // and remove the outputs that are being spent. this is needed because we may have unconfirmed UTXOs - // that are spend by unconfirmed transactions - unspents.filterNot(utxo => txs.exists(tx => tx.txIn.exists(_.outPoint == utxo.outPoint))) - } - } - - - /** - * - * @param scriptHash script hash - * @return the (confirmed, unconfirmed) balance for this script hash. This balance may not - * be up-to-date if we have not received all data we've asked for yet. - */ - def balance(scriptHash: ByteVector32): (Satoshi, Satoshi) = { - history.get(scriptHash) match { - case None => (0 sat, 0 sat) - - case Some(items) if items.isEmpty => (0 sat, 0 sat) - - case Some(items) => - val (confirmedItems, unconfirmedItems) = items.partition(_.height > 0) - val confirmedTxs = confirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) } - val unconfirmedTxs = unconfirmedItems.collect { case item if transactions.contains(item.tx_hash) => transactions(item.tx_hash) } - if (confirmedTxs.size + unconfirmedTxs.size < confirmedItems.size + unconfirmedItems.size) logger.warn(s"we have not received all transactions yet, balance will not be up to date") - - def findOurSpentOutputs(txs: Seq[Transaction]): Seq[TxOut] = { - val inputs = txs.flatMap(_.txIn).filter(txIn => isSpend(txIn, scriptHash)) - val spentOutputs = inputs.map(_.outPoint).flatMap(outPoint => transactions.get(outPoint.txid).map(_.txOut(outPoint.index.toInt))) - spentOutputs - } - - val confirmedSpents = findOurSpentOutputs(confirmedTxs) - val confirmedReceived = confirmedTxs.flatMap(_.txOut).filter(txOut => isReceive(txOut, scriptHash)) - - val unconfirmedSpents = findOurSpentOutputs(unconfirmedTxs) - val unconfirmedReceived = unconfirmedTxs.flatMap(_.txOut).filter(txOut => isReceive(txOut, scriptHash)) - - val confirmedBalance = confirmedReceived.map(_.amount).sum - confirmedSpents.map(_.amount).sum - val unconfirmedBalance = unconfirmedReceived.map(_.amount).sum - unconfirmedSpents.map(_.amount).sum - - logger.debug(s"scriptHash=$scriptHash confirmedBalance=$confirmedBalance unconfirmedBalance=$unconfirmedBalance)") - (confirmedBalance, unconfirmedBalance) - } - } - - /** - * - * @return the (confirmed, unconfirmed) balance for this wallet. This balance may not - * be up-to-date if we have not received all data we've asked for yet. - */ - lazy val balance: (Satoshi, Satoshi) = { - // `.toList` is very important here: keys are returned in a Set-like structure, without the .toList we map - // to another set-like structure that will remove duplicates, so if we have several script hashes with exactly the - // same balance we don't return the correct aggregated balance - val balances = (accountKeyMap.keys ++ changeKeyMap.keys).toList.map(scriptHash => balance(scriptHash)) - balances.foldLeft((0 sat, 0 sat)) { - case ((confirmed, unconfirmed), (confirmed1, unconfirmed1)) => (confirmed + confirmed1, unconfirmed + unconfirmed1) - } - } - - /** - * Computes the effect of this transaction on the wallet - * - * @param tx input transaction - * @return an option: - * - Some(received, sent, fee) where sent if what the tx spends from us, received is what the tx sends to us, - * and fee is the fee for the tx) tuple where sent if what the tx spends from us, and received is what the tx sends to us - * - None if we are missing one or more parent txs - */ - def computeTransactionDelta(tx: Transaction): Option[(Satoshi, Satoshi, Option[Satoshi])] = { - val ourInputs = tx.txIn.filter(isMine) - // we need to make sure that for all inputs spending an output we control, we already have the parent tx - // (otherwise we can't estimate our balance) - val missingParent = ourInputs.exists(txIn => !transactions.contains(txIn.outPoint.txid)) - if (missingParent) { - None - } else { - val sent = ourInputs.map(txIn => transactions(txIn.outPoint.txid).txOut(txIn.outPoint.index.toInt)).map(_.amount).sum - val received = tx.txOut.filter(isMine).map(_.amount).sum - // if all the inputs were ours, we can compute the fee, otherwise we can't - val fee_opt = if (ourInputs.size == tx.txIn.size) Some(sent - tx.txOut.map(_.amount).sum) else None - Some((received, sent, fee_opt)) - } - } - - /** - * - * @param tx input transaction - * @param utxos input uxtos - * @return a tx where all utxos have been added as inputs, signed with dummy invalid signatures. This - * is used to estimate the weight of the signed transaction - */ - def addUtxosWithDummySig(tx: Transaction, utxos: Seq[Utxo]): Transaction = - tx.copy(txIn = utxos.map { case utxo => - // we use dummy signature here, because the result is only used to estimate fees - val sig = ByteVector.fill(71)(1) - val sigScript = Script.write(OP_PUSHDATA(Script.write(Script.pay2wpkh(utxo.key.publicKey))) :: Nil) - val witness = ScriptWitness(sig :: utxo.key.publicKey.value :: Nil) - TxIn(utxo.outPoint, signatureScript = sigScript, sequence = TxIn.SEQUENCE_FINAL, witness = witness) - }) - - /** - * - * @param tx input tx that has no inputs - * @param feeRatePerKw fee rate per kiloweight - * @param minimumFee minimum fee - * @param dustLimit dust limit - * @return a (state, tx, fee) tuple where state has been updated and tx is a complete, - * fully signed transaction that can be broadcast. - * our utxos spent by this tx are locked and won't be available for spending - * until the tx has been cancelled. If the tx is committed, they will be removed - */ - def completeTransaction(tx: Transaction, feeRatePerKw: FeeratePerKw, minimumFee: Satoshi, dustLimit: Satoshi, allowSpendUnconfirmed: Boolean): (Data, Transaction, Satoshi) = { - require(tx.txIn.isEmpty, "cannot complete a tx that already has inputs") - val amount = tx.txOut.map(_.amount).sum - require(amount > dustLimit, "amount to send is below dust limit") - - val unlocked = { - // select utxos that are not locked by pending txs - val lockedOutputs = locks.flatMap(_.txIn.map(_.outPoint)) - val unlocked1 = utxos.filterNot(utxo => lockedOutputs.contains(utxo.outPoint)) - val unlocked2 = if (allowSpendUnconfirmed) unlocked1 else unlocked1.filter(_.item.height > 0) - // sort utxos by amount, in increasing order - // this way we minimize the number of utxos in the wallet, and so we minimize the fees we'll pay for them - unlocked2.sortBy(_.item.value) - } - - // computes the fee what we would have to pay for our tx with our candidate utxos and an optional change output - def computeFee(candidates: Seq[Utxo], change: Option[TxOut]): Satoshi = { - val tx1 = addUtxosWithDummySig(tx, candidates) - val tx2 = change.map(o => tx1.addOutput(o)).getOrElse(tx1) - Transactions.weight2fee(feeRatePerKw, tx2.weight()) - } - - val dummyChange = TxOut(Satoshi(0), computePublicKeyScript(currentChangeKey.publicKey)) - - @tailrec - def loop(current: Seq[Utxo], remaining: Seq[Utxo]): (Seq[Utxo], Option[TxOut]) = { - totalAmount(current) match { - case total if total - computeFee(current, None) < amount && remaining.isEmpty => - // not enough funds to send amount and pay fees even without a change output - throw new IllegalArgumentException("insufficient funds") - case total if total - computeFee(current, None) < amount => - // not enough funds, try with an additional input - loop(remaining.head +: current, remaining.tail) - case total if total - computeFee(current, None) <= amount + dustLimit => - // change output would be below dust, we don't add one and just overpay fees - (current, None) - case total if total - computeFee(current, Some(dummyChange)) <= amount + dustLimit && remaining.isEmpty => - // change output is above dust limit but cannot pay for it's own fee, and we have no more utxos => we overpay a bit - (current, None) - case total if total - computeFee(current, Some(dummyChange)) <= amount + dustLimit => - // try with an additional input - loop(remaining.head +: current, remaining.tail) - case total => - val fee = computeFee(current, Some(dummyChange)) - val change = dummyChange.copy(amount = total - amount - fee) - (current, Some(change)) - } - } - - val (selected, change_opt) = loop(Seq.empty[Utxo], unlocked) - - // sign our tx - val tx1 = addUtxosWithDummySig(tx, selected) - val tx2 = change_opt.map(out => tx1.addOutput(out)).getOrElse(tx1) - val tx3 = signTransaction(tx2) - - // and add the completed tx to the locks - val data1 = this.copy(locks = this.locks + tx3) - val fee = selected.map(s => Satoshi(s.item.value)).sum - tx3.txOut.map(_.amount).sum - - (data1, tx3, fee) - } - - def signTransaction(tx: Transaction): Transaction = { - tx.copy(txIn = tx.txIn.zipWithIndex.map { case (txIn, i) => - val utxo = utxos.find(_.outPoint == txIn.outPoint).getOrElse(throw new RuntimeException(s"cannot sign input that spends from ${txIn.outPoint}")) - val key = utxo.key - val sig = Transaction.signInput(tx, i, Script.pay2pkh(key.publicKey), SIGHASH_ALL, Satoshi(utxo.item.value), SigVersion.SIGVERSION_WITNESS_V0, key.privateKey) - val sigScript = Script.write(OP_PUSHDATA(Script.write(Script.pay2wpkh(key.publicKey))) :: Nil) - val witness = ScriptWitness(sig :: key.publicKey.value :: Nil) - txIn.copy(signatureScript = sigScript, witness = witness) - }) - } - - /** - * unlocks input locked by a pending tx. call this method if the tx will not be used after all - * - * @param tx pending transaction - * @return an updated state - */ - def cancelTransaction(tx: Transaction): Data = this.copy(locks = this.locks - tx) - - /** - * remove all our utxos spent by this tx. call this method if the tx was broadcast successfully - * - * @param tx pending transaction - * @return an updated state - */ - def commitTransaction(tx: Transaction): Data = { - // HACK! since we base our utxos computation on the history as seen by the electrum server (so that it is - // reorg-proof out of the box), we need to update the history right away if we want to be able to build chained - // unconfirmed transactions. A few seconds later electrum will notify us and the entry will be overwritten. - // Note that we need to take into account both inputs and outputs, because there may be change. - val history1 = (tx.txIn.filter(isMine).map(extractPubKeySpentFrom).flatten.map(computeScriptHashFromPublicKey) ++ tx.txOut.filter(isMine).map(_.publicKeyScript).map(computeScriptHash)) - .foldLeft(this.history) { - case (history, scriptHash) => - val entry = history.get(scriptHash) match { - case None => List(TransactionHistoryItem(0, tx.txid)) - case Some(items) if items.map(_.tx_hash).contains(tx.txid) => items - case Some(items) => TransactionHistoryItem(0, tx.txid) :: items - } - history + (scriptHash -> entry) - } - this.copy(locks = this.locks - tx, transactions = this.transactions + (tx.txid -> tx), heights = this.heights + (tx.txid -> 0), history = history1) - } - - /** - * spend all our balance, including unconfirmed utxos and locked utxos (i.e utxos - * that are used in funding transactions that have not been published yet - * - * @param publicKeyScript script to send all our funds to - * @param feeRatePerKw fee rate in satoshi per kiloweight - * @return a (tx, fee) tuple, tx is a signed transaction that spends all our balance and - * fee is the associated bitcoin network fee - */ - def spendAll(publicKeyScript: ByteVector, feeRatePerKw: FeeratePerKw): (Transaction, Satoshi) = { - // use confirmed and unconfirmed balance - val amount = balance._1 + balance._2 - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, publicKeyScript) :: Nil, lockTime = 0) - // use all uxtos, including locked ones - val tx1 = addUtxosWithDummySig(tx, utxos) - val fee = Transactions.weight2fee(feeRatePerKw, tx1.weight()) - val tx2 = tx1.copy(txOut = TxOut(amount - fee, publicKeyScript) :: Nil) - val tx3 = signTransaction(tx2) - (tx3, fee) - } - - def spendAll(publicKeyScript: Seq[ScriptElt], feeRatePerKw: FeeratePerKw): (Transaction, Satoshi) = spendAll(Script.write(publicKeyScript), feeRatePerKw) - } - - object Data { - def apply(params: ElectrumWallet.WalletParameters, blockchain: Blockchain, accountKeys: Vector[ExtendedPrivateKey], changeKeys: Vector[ExtendedPrivateKey]): Data - = Data(blockchain, accountKeys, changeKeys, Map(), Map(), Map(), Map(), Map(), Set(), Set(), Set(), Set(), List(), None) - } - - case class InfiniteLoopException(data: Data, tx: Transaction) extends Exception - - case class PersistentData(accountKeysCount: Int, - changeKeysCount: Int, - status: Map[ByteVector32, String], - transactions: Map[ByteVector32, Transaction], - heights: Map[ByteVector32, Int], - history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], - proofs: Map[ByteVector32, GetMerkleResponse], - pendingTransactions: List[Transaction], - locks: Set[Transaction]) - - object PersistentData { - def apply(data: Data) = new PersistentData(data.accountKeys.length, data.changeKeys.length, data.status, data.transactions, data.heights, data.history, data.proofs, data.pendingTransactions, data.locks) - } - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala deleted file mode 100644 index 22e5d1205c..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcher.scala +++ /dev/null @@ -1,246 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.{Actor, ActorLogging, ActorRef, Stash, Terminated} -import fr.acinq.bitcoin.{BlockHeader, ByteVector32, SatoshiLong, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.computeScriptHash -import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_PARENT_TX_CONFIRMED} -import fr.acinq.eclair.transactions.Scripts -import fr.acinq.eclair.{ShortChannelId, TxCoordinates} - -import java.util.concurrent.atomic.AtomicLong -import scala.collection.immutable.{Queue, SortedMap} - - -class ElectrumWatcher(blockCount: AtomicLong, client: ActorRef) extends Actor with Stash with ActorLogging { - - client ! ElectrumClient.AddStatusListener(self) - - override def unhandled(message: Any): Unit = message match { - case ValidateRequest(c) => - log.info("blindly validating channel={}", c) - val pubkeyScript = Script.write(Script.pay2wsh(Scripts.multiSig2of2(c.bitcoinKey1, c.bitcoinKey2))) - val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(c.shortChannelId) - val fakeFundingTx = Transaction( - version = 2, - txIn = Seq.empty[TxIn], - txOut = List.fill(outputIndex + 1)(TxOut(0 sat, pubkeyScript)), // quick and dirty way to be sure that the outputIndex'th output is of the expected format - lockTime = 0) - sender ! ValidateResult(c, Right((fakeFundingTx, UtxoStatus.Unspent))) - - case _ => log.warning("unhandled message {}", message) - } - - def receive: Receive = disconnected(Set.empty, Queue.empty, SortedMap.empty, Queue.empty) - - def disconnected(watches: Set[Watch], publishQueue: Queue[PublishAsap], block2tx: SortedMap[Long, Seq[Transaction]], getTxQueue: Queue[(GetTxWithMeta, ActorRef)]): Receive = { - case ElectrumClient.ElectrumReady(_, _, _) => - client ! ElectrumClient.HeaderSubscription(self) - case ElectrumClient.HeaderSubscriptionResponse(height, header) => - watches.foreach(self ! _) - publishQueue.foreach(self ! _) - getTxQueue.foreach { case (msg, origin) => self.tell(msg, origin) } - context become running(height, header, Set(), Map(), block2tx, Queue.empty) - case watch: Watch => context become disconnected(watches + watch, publishQueue, block2tx, getTxQueue) - case publish: PublishAsap => context become disconnected(watches, publishQueue :+ publish, block2tx, getTxQueue) - case getTx: GetTxWithMeta => context become disconnected(watches, publishQueue, block2tx, getTxQueue :+ (getTx, sender)) - } - - def running(height: Int, tip: BlockHeader, watches: Set[Watch], scriptHashStatus: Map[ByteVector32, String], block2tx: SortedMap[Long, Seq[Transaction]], sent: Queue[Transaction]): Receive = { - case ElectrumClient.HeaderSubscriptionResponse(_, newtip) if tip == newtip => () - - case ElectrumClient.HeaderSubscriptionResponse(newheight, newtip) => - log.info(s"new tip: ${newtip.blockId} $height") - watches collect { - case watch: WatchConfirmed => - val scriptHash = computeScriptHash(watch.publicKeyScript) - client ! ElectrumClient.GetScriptHashHistory(scriptHash) - } - val toPublish = block2tx.filterKeys(_ <= newheight) - toPublish.values.flatten.foreach(publish) - context become running(newheight, newtip, watches, scriptHashStatus, block2tx -- toPublish.keys, sent ++ toPublish.values.flatten) - - case watch: Watch if watches.contains(watch) => () - - case watch@WatchSpent(_, txid, outputIndex, publicKeyScript, _, _) => - val scriptHash = computeScriptHash(publicKeyScript) - log.info(s"added watch-spent on output=$txid:$outputIndex scriptHash=$scriptHash") - client ! ElectrumClient.ScriptHashSubscription(scriptHash, self) - context.watch(watch.replyTo) - context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent) - - case watch@WatchSpentBasic(_, txid, outputIndex, publicKeyScript, _) => - val scriptHash = computeScriptHash(publicKeyScript) - log.info(s"added watch-spent-basic on output=$txid:$outputIndex scriptHash=$scriptHash") - client ! ElectrumClient.ScriptHashSubscription(scriptHash, self) - context.watch(watch.replyTo) - context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent) - - case watch@WatchConfirmed(_, txid, publicKeyScript, _, _) => - val scriptHash = computeScriptHash(publicKeyScript) - log.info(s"added watch-confirmed on txid=$txid scriptHash=$scriptHash") - client ! ElectrumClient.ScriptHashSubscription(scriptHash, self) - context.watch(watch.replyTo) - context become running(height, tip, watches + watch, scriptHashStatus, block2tx, sent) - - case Terminated(actor) => - val watches1 = watches.filterNot(_.replyTo == actor) - context become running(height, tip, watches1, scriptHashStatus, block2tx, sent) - - case ElectrumClient.ScriptHashSubscriptionResponse(scriptHash, status) => - scriptHashStatus.get(scriptHash) match { - case Some(s) if s == status => log.debug(s"already have status=$status for scriptHash=$scriptHash") - case _ if status.isEmpty => log.info(s"empty status for scriptHash=$scriptHash") - case _ => - log.info(s"new status=$status for scriptHash=$scriptHash") - client ! ElectrumClient.GetScriptHashHistory(scriptHash) - } - context become running(height, tip, watches, scriptHashStatus + (scriptHash -> status), block2tx, sent) - - case ElectrumClient.GetScriptHashHistoryResponse(_, history) => - // we retrieve the transaction before checking watches - // NB: height=-1 means that the tx is unconfirmed and at least one of its inputs is also unconfirmed. we need to take them into consideration if we want to handle unconfirmed txes (which is the case for turbo channels) - history.filter(_.height >= -1).foreach { item => client ! ElectrumClient.GetTransaction(item.tx_hash, Some(item)) } - - case ElectrumClient.GetTransactionResponse(tx, Some(item: ElectrumClient.TransactionHistoryItem)) => - // this is for WatchSpent/WatchSpentBasic - val watchSpentTriggered = tx.txIn.map(_.outPoint).flatMap(outPoint => watches.collect { - case WatchSpent(channel, txid, pos, _, event, _) if txid == outPoint.txid && pos == outPoint.index.toInt => - log.info(s"output $txid:$pos spent by transaction ${tx.txid}") - channel ! WatchEventSpent(event, tx) - // NB: WatchSpent are permanent because we need to detect multiple spending of the funding tx - // They are never cleaned up but it is not a big deal for now (1 channel == 1 watch) - None - case w@WatchSpentBasic(channel, txid, pos, _, event) if txid == outPoint.txid && pos == outPoint.index.toInt => - log.info(s"output $txid:$pos spent by transaction ${tx.txid}") - channel ! WatchEventSpentBasic(event) - Some(w) - }).flatten - - // this is for WatchConfirmed - val watchConfirmedTriggered = watches.collect { - case w@WatchConfirmed(channel, txid, _, minDepth, BITCOIN_FUNDING_DEPTHOK) if txid == tx.txid && minDepth == 0 => - // special case for mempool watches (min depth = 0) - val (dummyHeight, dummyTxIndex) = ElectrumWatcher.makeDummyShortChannelId(txid) - channel ! WatchEventConfirmed(BITCOIN_FUNDING_DEPTHOK, dummyHeight, dummyTxIndex, tx) - Some(w) - case WatchConfirmed(_, txid, _, minDepth, _) if txid == tx.txid && minDepth > 0 && item.height > 0 => - // min depth > 0 here - val txheight = item.height - val confirmations = height - txheight + 1 - log.info(s"txid=$txid was confirmed at height=$txheight and now has confirmations=$confirmations (currentHeight=$height)") - if (confirmations >= minDepth) { - // we need to get the tx position in the block - client ! ElectrumClient.GetMerkle(txid, txheight, Some(tx)) - } - None - }.flatten - - context become running(height, tip, watches -- watchSpentTriggered -- watchConfirmedTriggered, scriptHashStatus, block2tx, sent) - - case ElectrumClient.GetMerkleResponse(tx_hash, _, txheight, pos, Some(tx: Transaction)) => - val confirmations = height - txheight + 1 - val triggered = watches.collect { - case w@WatchConfirmed(channel, txid, _, minDepth, event) if txid == tx_hash && confirmations >= minDepth => - log.info(s"txid=$txid had confirmations=$confirmations in block=$txheight pos=$pos") - channel ! WatchEventConfirmed(event, txheight.toInt, pos, tx) - w - } - context become running(height, tip, watches -- triggered, scriptHashStatus, block2tx, sent) - - case GetTxWithMeta(txid) => client ! ElectrumClient.GetTransaction(txid, Some(sender)) - - case ElectrumClient.GetTransactionResponse(tx, Some(origin: ActorRef)) => origin ! GetTxWithMetaResponse(tx.txid, Some(tx), tip.time) - - case ElectrumClient.ServerError(ElectrumClient.GetTransaction(txid, Some(origin: ActorRef)), _) => origin ! GetTxWithMetaResponse(txid, None, tip.time) - - case PublishAsap(tx, _) => - val blockCount = this.blockCount.get() - val cltvTimeout = Scripts.cltvTimeout(tx) - val csvTimeouts = Scripts.csvTimeouts(tx) - if (csvTimeouts.nonEmpty) { - // watcher supports txs with multiple csv-delayed inputs: we watch all delayed parents and try to publish every - // time a parent's relative delays are satisfied, so we will eventually succeed. - csvTimeouts.foreach { case (parentTxId, csvTimeout) => - log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parentTxId=$parentTxId tx={}", tx) - val parentPublicKeyScript = WatchConfirmed.extractPublicKeyScript(tx.txIn.find(_.outPoint.txid == parentTxId).get.witness) - self ! WatchConfirmed(self, parentTxId, parentPublicKeyScript, minDepth = csvTimeout, BITCOIN_PARENT_TX_CONFIRMED(PublishAsap(tx, PublishStrategy.JustPublish))) - } - } else if (cltvTimeout > blockCount) { - log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)") - val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx) - context become running(height, tip, watches, scriptHashStatus, block2tx1, sent) - } else { - publish(tx) - context become running(height, tip, watches, scriptHashStatus, block2tx, sent :+ tx) - } - - case WatchEventConfirmed(BITCOIN_PARENT_TX_CONFIRMED(PublishAsap(tx, _)), _, _, _) => - log.info(s"parent tx of txid=${tx.txid} has been confirmed") - val blockCount = this.blockCount.get() - val cltvTimeout = Scripts.cltvTimeout(tx) - if (cltvTimeout > blockCount) { - log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)") - val block2tx1 = block2tx.updated(cltvTimeout, block2tx.getOrElse(cltvTimeout, Seq.empty[Transaction]) :+ tx) - context become running(height, tip, watches, scriptHashStatus, block2tx1, sent) - } else { - publish(tx) - context become running(height, tip, watches, scriptHashStatus, block2tx, sent :+ tx) - } - - case ElectrumClient.BroadcastTransactionResponse(tx, error_opt) => - error_opt match { - case None => log.info(s"broadcast succeeded for txid=${tx.txid} tx={}", tx) - case Some(error) if error.message.contains("transaction already in block chain") => log.info(s"broadcast ignored for txid=${tx.txid} tx={} (tx was already in blockchain)", tx) - case Some(error) => log.error(s"broadcast failed for txid=${tx.txid} tx=$tx with error=$error") - } - context become running(height, tip, watches, scriptHashStatus, block2tx, sent diff Seq(tx)) - - case ElectrumClient.ElectrumDisconnected => - // we remember watches and keep track of tx that have not yet been published - // we also re-send the txs that we previously sent but hadn't yet received the confirmation - context become disconnected(watches, sent.map(tx => PublishAsap(tx, PublishStrategy.JustPublish)), block2tx, Queue.empty) - } - - def publish(tx: Transaction): Unit = { - log.info("publishing tx={}", tx) - client ! ElectrumClient.BroadcastTransaction(tx) - } - -} - -object ElectrumWatcher { - /** - * @param txid funding transaction id - * @return a (blockHeight, txIndex) tuple that is extracted from the input source - * This is used to create unique "dummy" short channel ids for zero-conf channels - */ - def makeDummyShortChannelId(txid: ByteVector32): (Int, Int) = { - // we use a height of 0 - // - to make sure that the tx will be marked as "confirmed" - // - to easily identify scids linked to 0-conf channels - // - // this gives us a probability of collisions of 0.1% for 5 0-conf channels and 1% for 20 - // collisions mean that users may temporarily see incorrect numbers for their 0-conf channels (until they've been confirmed) - // if this ever becomes a problem we could just extract some bits for our dummy height instead of just returning 0 - val height = 0 - val txIndex = txid.bits.sliceToInt(0, 16, signed = false) - (height, txIndex) - } -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala deleted file mode 100644 index d50ccc9ffd..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/WalletDb.scala +++ /dev/null @@ -1,41 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum.db - -import fr.acinq.bitcoin.{BlockHeader, ByteVector32} -import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData - -trait HeaderDb { - def addHeader(height: Int, header: BlockHeader): Unit - - def addHeaders(startHeight: Int, headers: Seq[BlockHeader]): Unit - - def getHeader(height: Int): Option[BlockHeader] - - // used only in unit tests - def getHeader(blockHash: ByteVector32): Option[(Int, BlockHeader)] - - def getHeaders(startHeight: Int, maxCount: Option[Int]): Seq[BlockHeader] - - def getTip: Option[(Int, BlockHeader)] -} - -trait WalletDb extends HeaderDb { - def persist(data: PersistentData): Unit - - def readPersistentData(): Option[PersistentData] -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala deleted file mode 100644 index f265ec33a9..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDb.scala +++ /dev/null @@ -1,218 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum.db.sqlite - -import fr.acinq.bitcoin.{BlockHeader, ByteVector32, Transaction} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{GetMerkleResponse, TransactionHistoryItem} -import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData -import fr.acinq.eclair.blockchain.electrum.db.WalletDb -import fr.acinq.eclair.blockchain.electrum.{ElectrumClient, ElectrumWallet} -import fr.acinq.eclair.db.sqlite.SqliteUtils - -import java.sql.Connection -import scala.collection.immutable.Queue - -class SqliteWalletDb(sqlite: Connection) extends WalletDb { - - import SqliteUtils._ - - using(sqlite.createStatement()) { statement => - statement.executeUpdate("CREATE TABLE IF NOT EXISTS headers (height INTEGER NOT NULL PRIMARY KEY, block_hash BLOB NOT NULL, header BLOB NOT NULL)") - statement.executeUpdate("CREATE TABLE IF NOT EXISTS wallet (data BLOB)") - } - - override def addHeader(height: Int, header: BlockHeader): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)")) { statement => - statement.setInt(1, height) - statement.setBytes(2, header.hash.toArray) - statement.setBytes(3, BlockHeader.write(header).toArray) - statement.executeUpdate() - } - } - - override def addHeaders(startHeight: Int, headers: Seq[BlockHeader]): Unit = { - using(sqlite.prepareStatement("INSERT OR IGNORE INTO headers VALUES (?, ?, ?)"), inTransaction = true) { statement => - var height = startHeight - headers.foreach(header => { - statement.setInt(1, height) - statement.setBytes(2, header.hash.toArray) - statement.setBytes(3, BlockHeader.write(header).toArray) - statement.addBatch() - height = height + 1 - }) - val result = statement.executeBatch() - } - } - - override def getHeader(height: Int): Option[BlockHeader] = { - using(sqlite.prepareStatement("SELECT header FROM headers WHERE height = ?")) { statement => - statement.setInt(1, height) - val rs = statement.executeQuery() - if (rs.next()) { - Some(BlockHeader.read(rs.getBytes("header"))) - } else { - None - } - } - } - - override def getHeader(blockHash: ByteVector32): Option[(Int, BlockHeader)] = { - using(sqlite.prepareStatement("SELECT height, header FROM headers WHERE block_hash = ?")) { statement => - statement.setBytes(1, blockHash.toArray) - val rs = statement.executeQuery() - if (rs.next()) { - Some((rs.getInt("height"), BlockHeader.read(rs.getBytes("header")))) - } else { - None - } - } - } - - override def getHeaders(startHeight: Int, maxCount: Option[Int]): Seq[BlockHeader] = { - val query = "SELECT height, header FROM headers WHERE height >= ? ORDER BY height " + maxCount.map(m => s" LIMIT $m").getOrElse("") - using(sqlite.prepareStatement(query)) { statement => - statement.setInt(1, startHeight) - val rs = statement.executeQuery() - var q: Queue[BlockHeader] = Queue() - while (rs.next()) { - q = q :+ BlockHeader.read(rs.getBytes("header")) - } - q - } - } - - - override def getTip: Option[(Int, BlockHeader)] = { - using(sqlite.prepareStatement("SELECT t.height, t.header FROM headers t INNER JOIN (SELECT MAX(height) AS maxHeight FROM headers) q ON t.height = q.maxHeight")) { statement => - val rs = statement.executeQuery() - if (rs.next()) { - Some((rs.getInt("height"), BlockHeader.read(rs.getBytes("header")))) - } else { - None - } - } - } - - override def persist(data: ElectrumWallet.PersistentData): Unit = { - val bin = SqliteWalletDb.serialize(data) - using(sqlite.prepareStatement("UPDATE wallet SET data=(?)")) { update => - update.setBytes(1, bin) - if (update.executeUpdate() == 0) { - using(sqlite.prepareStatement("INSERT INTO wallet VALUES (?)")) { statement => - statement.setBytes(1, bin) - statement.executeUpdate() - } - } - } - } - - override def readPersistentData(): Option[ElectrumWallet.PersistentData] = { - using(sqlite.prepareStatement("SELECT data FROM wallet")) { statement => - val rs = statement.executeQuery() - if (rs.next()) { - Option(rs.getBytes(1)).map(bin => SqliteWalletDb.deserializePersistentData(bin)) - } else { - None - } - } - } -} - -object SqliteWalletDb { - - import fr.acinq.eclair.wire.protocol.CommonCodecs._ - import scodec.Codec - import scodec.bits.BitVector - import scodec.codecs._ - - val proofCodec: Codec[GetMerkleResponse] = ( - ("txid" | bytes32) :: - ("merkle" | listOfN(uint16, bytes32)) :: - ("block_height" | uint24) :: - ("pos" | uint24) :: - ("context_opt" | provide(Option.empty[Any]))).as[GetMerkleResponse] - - def serializeMerkleProof(proof: GetMerkleResponse): Array[Byte] = proofCodec.encode(proof).require.toByteArray - - def deserializeMerkleProof(bin: Array[Byte]): GetMerkleResponse = proofCodec.decode(BitVector(bin)).require.value - - val statusListCodec: Codec[List[(ByteVector32, String)]] = listOfN(uint16, bytes32 ~ cstring) - - val statusCodec: Codec[Map[ByteVector32, String]] = Codec[Map[ByteVector32, String]]( - (map: Map[ByteVector32, String]) => statusListCodec.encode(map.toList), - (wire: BitVector) => statusListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val heightsListCodec: Codec[List[(ByteVector32, Int)]] = listOfN(uint16, bytes32 ~ int32) - - val heightsCodec: Codec[Map[ByteVector32, Int]] = Codec[Map[ByteVector32, Int]]( - (map: Map[ByteVector32, Int]) => heightsListCodec.encode(map.toList), - (wire: BitVector) => heightsListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val txCodec: Codec[Transaction] = lengthDelimited(bytes.xmap(d => Transaction.read(d.toArray), d => Transaction.write(d))) - - val transactionListCodec: Codec[List[(ByteVector32, Transaction)]] = listOfN(uint16, bytes32 ~ txCodec) - - val transactionsCodec: Codec[Map[ByteVector32, Transaction]] = Codec[Map[ByteVector32, Transaction]]( - (map: Map[ByteVector32, Transaction]) => transactionListCodec.encode(map.toList), - (wire: BitVector) => transactionListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val transactionHistoryItemCodec: Codec[ElectrumClient.TransactionHistoryItem] = ( - ("height" | int32) :: ("tx_hash" | bytes32)).as[ElectrumClient.TransactionHistoryItem] - - val seqOfTransactionHistoryItemCodec: Codec[List[TransactionHistoryItem]] = listOfN[TransactionHistoryItem](uint16, transactionHistoryItemCodec) - - val historyListCodec: Codec[List[(ByteVector32, List[ElectrumClient.TransactionHistoryItem])]] = - listOfN[(ByteVector32, List[ElectrumClient.TransactionHistoryItem])](uint16, bytes32 ~ seqOfTransactionHistoryItemCodec) - - val historyCodec: Codec[Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]] = Codec[Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]]( - (map: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]]) => historyListCodec.encode(map.toList), - (wire: BitVector) => historyListCodec.decode(wire).map(_.map(_.toMap)) - ) - - val proofsListCodec: Codec[List[(ByteVector32, GetMerkleResponse)]] = listOfN(uint16, bytes32 ~ proofCodec) - - val proofsCodec: Codec[Map[ByteVector32, GetMerkleResponse]] = Codec[Map[ByteVector32, GetMerkleResponse]]( - (map: Map[ByteVector32, GetMerkleResponse]) => proofsListCodec.encode(map.toList), - (wire: BitVector) => proofsListCodec.decode(wire).map(_.map(_.toMap)) - ) - - /** - * change this value - * -if the new codec is incompatible with the old one - * - OR if you want to force a full sync from Electrum servers - */ - val version = 0x0000 - - val persistentDataCodec: Codec[PersistentData] = ( - ("version" | constant(BitVector.fromInt(version))) :: - ("accountKeysCount" | int32) :: - ("changeKeysCount" | int32) :: - ("status" | statusCodec) :: - ("transactions" | transactionsCodec) :: - ("heights" | heightsCodec) :: - ("history" | historyCodec) :: - ("proofs" | proofsCodec) :: - ("pendingTransactions" | listOfN(uint16, txCodec)) :: - ("locks" | provide(Set.empty[Transaction]))).as[PersistentData] - - def serialize(data: PersistentData): Array[Byte] = persistentDataCodec.encode(data).require.toByteArray - - def deserializePersistentData(bin: Array[Byte]): PersistentData = persistentDataCodec.decode(BitVector(bin)).require.value -} \ No newline at end of file diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala deleted file mode 100644 index 53bdae980a..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProvider.scala +++ /dev/null @@ -1,82 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import com.softwaremill.sttp._ -import com.softwaremill.sttp.json4s._ -import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi} -import org.json4s.DefaultFormats -import org.json4s.JsonAST.{JInt, JValue} -import org.json4s.jackson.Serialization - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} - -class BitgoFeeProvider(chainHash: ByteVector32, readTimeOut: Duration)(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider { - - import BitgoFeeProvider._ - - implicit val formats = DefaultFormats - implicit val serialization = Serialization - - val uri = chainHash match { - case Block.LivenetGenesisBlock.hash => uri"https://www.bitgo.com/api/v2/btc/tx/fee" - case _ => uri"https://test.bitgo.com/api/v2/tbtc/tx/fee" - } - - override def getFeerates: Future[FeeratesPerKB] = - for { - res <- sttp.readTimeout(readTimeOut).get(uri) - .response(asJson[JValue]) - .send() - feeRanges = parseFeeRanges(res.unsafeBody) - } yield extractFeerates(feeRanges) - -} - -object BitgoFeeProvider { - - case class BlockTarget(block: Int, fee: Long) - - def parseFeeRanges(json: JValue): Seq[BlockTarget] = { - val blockTargets = json \ "feeByBlockTarget" - blockTargets.foldField(Seq.empty[BlockTarget]) { - // BitGo returns estimates in Satoshi/KB, which is what we want - case (list, (strBlockTarget, JInt(feePerKB))) => list :+ BlockTarget(strBlockTarget.toInt, feePerKB.longValue) - } - } - - def extractFeerate(feeRanges: Seq[BlockTarget], maxBlockDelay: Int): FeeratePerKB = { - // first we keep only fee ranges with a max block delay below the limit - val belowLimit = feeRanges.filter(_.block <= maxBlockDelay) - // out of all the remaining fee ranges, we select the one with the minimum higher bound - FeeratePerKB(Satoshi(belowLimit.map(_.fee).min)) - } - - def extractFeerates(feeRanges: Seq[BlockTarget]): FeeratesPerKB = - FeeratesPerKB( - mempoolMinFee = extractFeerate(feeRanges, 1008), - block_1 = extractFeerate(feeRanges, 1), - blocks_2 = extractFeerate(feeRanges, 2), - blocks_6 = extractFeerate(feeRanges, 6), - blocks_12 = extractFeerate(feeRanges, 12), - blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72), - blocks_144 = extractFeerate(feeRanges, 144), - blocks_1008 = extractFeerate(feeRanges, 1008)) - -} diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala deleted file mode 100644 index 7338d6dd5f..0000000000 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProvider.scala +++ /dev/null @@ -1,87 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import com.softwaremill.sttp._ -import com.softwaremill.sttp.json4s._ -import fr.acinq.bitcoin.Satoshi -import org.json4s.DefaultFormats -import org.json4s.JsonAST.{JArray, JInt, JValue} -import org.json4s.jackson.Serialization - -import scala.concurrent.duration._ -import scala.concurrent.{ExecutionContext, Future} - -/** - * Created by PM on 16/11/2017. - */ -class EarnDotComFeeProvider(readTimeOut: Duration)(implicit http: SttpBackend[Future, Nothing], ec: ExecutionContext) extends FeeProvider { - - import EarnDotComFeeProvider._ - - implicit val formats = DefaultFormats - implicit val serialization = Serialization - - val uri = uri"https://bitcoinfees.earn.com/api/v1/fees/list" - - override def getFeerates: Future[FeeratesPerKB] = - for { - json <- sttp.readTimeout(readTimeOut).get(uri) - .response(asJson[JValue]) - .send() - feeRanges = parseFeeRanges(json.unsafeBody) - } yield extractFeerates(feeRanges) - -} - -object EarnDotComFeeProvider { - - case class FeeRange(minFee: Long, maxFee: Long, memCount: Long, minDelay: Long, maxDelay: Long) - - def parseFeeRanges(json: JValue): Seq[FeeRange] = { - val JArray(items) = json \ "fees" - items.map(item => { - val JInt(minFee) = item \ "minFee" - val JInt(maxFee) = item \ "maxFee" - val JInt(memCount) = item \ "memCount" - val JInt(minDelay) = item \ "minDelay" - val JInt(maxDelay) = item \ "maxDelay" - // earn.com returns fees in Satoshi/byte and we want Satoshi/KiloByte - FeeRange(minFee = 1000 * minFee.toLong, maxFee = 1000 * maxFee.toLong, memCount = memCount.toLong, minDelay = minDelay.toLong, maxDelay = maxDelay.toLong) - }) - } - - def extractFeerate(feeRanges: Seq[FeeRange], maxBlockDelay: Int): FeeratePerKB = { - // first we keep only fee ranges with a max block delay below the limit - val belowLimit = feeRanges.filter(_.maxDelay <= maxBlockDelay) - // out of all the remaining fee ranges, we select the one with the minimum higher bound and make sure it is > 0 - FeeratePerKB(Satoshi(Math.max(belowLimit.minBy(_.maxFee).maxFee, 1))) - } - - def extractFeerates(feeRanges: Seq[FeeRange]): FeeratesPerKB = - FeeratesPerKB( - mempoolMinFee = extractFeerate(feeRanges, 1008), - block_1 = extractFeerate(feeRanges, 1), - blocks_2 = extractFeerate(feeRanges, 2), - blocks_6 = extractFeerate(feeRanges, 6), - blocks_12 = extractFeerate(feeRanges, 12), - blocks_36 = extractFeerate(feeRanges, 36), - blocks_72 = extractFeerate(feeRanges, 72), - blocks_144 = extractFeerate(feeRanges, 144), - blocks_1008 = extractFeerate(feeRanges, 1008)) - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/CheckPointSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/CheckPointSpec.scala deleted file mode 100644 index d1cf11b590..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/CheckPointSpec.scala +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import fr.acinq.bitcoin.Block -import org.scalatest.funsuite.AnyFunSuite - -class CheckPointSpec extends AnyFunSuite { - test("load checkpoint") { - val checkpoints = CheckPoint.load(Block.LivenetGenesisBlock.hash) - assert(!checkpoints.isEmpty) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala deleted file mode 100644 index 6e9c85a697..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientPoolSpec.scala +++ /dev/null @@ -1,119 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.net.InetSocketAddress -import java.util.concurrent.atomic.AtomicLong - -import akka.actor.{ActorRef, Props} -import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, Crypto, Transaction} -import fr.acinq.eclair.TestKitBaseClass -import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ -import grizzled.slf4j.Logging -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits._ - -import scala.concurrent.duration._ -import scala.util.Random - - -class ElectrumClientPoolSpec extends TestKitBaseClass with AnyFunSuiteLike with Logging { - var pool: ActorRef = _ - val probe = TestProbe() - // this is tx #2690 of block #500000 - val referenceTx = Transaction.read("0200000001983c5b32ced1de5ae97d3ce9b7436f8bb0487d15bf81e5cae97b1e238dc395c6000000006a47304402205957c75766e391350eba2c7b752f0056cb34b353648ecd0992a8a81fc9bcfe980220629c286592842d152cdde71177cd83086619744a533f262473298cacf60193500121021b8b51f74dbf0ac1e766d162c8707b5e8d89fc59da0796f3b4505e7c0fb4cf31feffffff0276bd0101000000001976a914219de672ba773aa0bc2e15cdd9d2e69b734138fa88ac3e692001000000001976a914301706dede031e9fb4b60836e073a4761855f6b188ac09a10700") - val scriptHash = Crypto.sha256(referenceTx.txOut(0).publicKeyScript).reverse - val serverAddresses = { - val stream = classOf[ElectrumClientSpec].getResourceAsStream("/electrum/servers_mainnet.json") - val addresses = ElectrumClientPool.readServerAddresses(stream, sslEnabled = false) - stream.close() - addresses - } - - implicit val timeout = 20 seconds - - import concurrent.ExecutionContext.Implicits.global - - test("pick a random, unused server address") { - val usedAddresses = Random.shuffle(serverAddresses.toSeq).take(serverAddresses.size / 2).map(_.address).toSet - for (_ <- 1 to 10) { - val Some(pick) = ElectrumClientPool.pickAddress(serverAddresses, usedAddresses) - assert(!usedAddresses.contains(pick.address)) - } - } - - test("init an electrumx connection pool") { - val random = new Random() - val stream = classOf[ElectrumClientSpec].getResourceAsStream("/electrum/servers_mainnet.json") - val addresses = random.shuffle(serverAddresses.toSeq).take(2).toSet + ElectrumClientPool.ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT) - stream.close() - assert(addresses.nonEmpty) - pool = system.actorOf(Props(new ElectrumClientPool(new AtomicLong(), addresses)), "electrum-client") - } - - test("connect to an electrumx mainnet server") { - probe.send(pool, AddStatusListener(probe.ref)) - // make sure our master is stable, if the first master that we select is behind the other servers we will switch - // during the first few seconds - awaitCond({ - probe.expectMsgType[ElectrumReady](30 seconds) - probe.receiveOne(5 seconds) == null - }, max = 60 seconds, interval = 1000 millis) - } - - test("get transaction") { - probe.send(pool, GetTransaction(referenceTx.txid)) - val GetTransactionResponse(tx, _) = probe.expectMsgType[GetTransactionResponse](timeout) - assert(tx == referenceTx) - } - - test("get merkle tree") { - probe.send(pool, GetMerkle(referenceTx.txid, 500000)) - val response = probe.expectMsgType[GetMerkleResponse](timeout) - assert(response.txid == referenceTx.txid) - assert(response.block_height == 500000) - assert(response.pos == 2690) - assert(response.root == ByteVector32(hex"1f6231ed3de07345b607ec2a39b2d01bec2fe10dfb7f516ba4958a42691c9531")) - } - - test("header subscription") { - val probe1 = TestProbe() - probe1.send(pool, HeaderSubscription(probe1.ref)) - val HeaderSubscriptionResponse(_, header) = probe1.expectMsgType[HeaderSubscriptionResponse](timeout) - logger.info(s"received header for block ${header.blockId}") - } - - test("scripthash subscription") { - val probe1 = TestProbe() - probe1.send(pool, ScriptHashSubscription(scriptHash, probe1.ref)) - val ScriptHashSubscriptionResponse(scriptHash1, status) = probe1.expectMsgType[ScriptHashSubscriptionResponse](timeout) - assert(status != "") - } - - test("get scripthash history") { - probe.send(pool, GetScriptHashHistory(scriptHash)) - val GetScriptHashHistoryResponse(scriptHash1, history) = probe.expectMsgType[GetScriptHashHistoryResponse](timeout) - assert(history.contains((TransactionHistoryItem(500000, referenceTx.txid)))) - } - - test("list script unspents") { - probe.send(pool, ScriptHashListUnspent(scriptHash)) - val ScriptHashListUnspentResponse(scriptHash1, unspents) = probe.expectMsgType[ScriptHashListUnspentResponse](timeout) - assert(unspents.isEmpty) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala deleted file mode 100644 index 96867ed89c..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumClientSpec.scala +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import java.net.InetSocketAddress - -import akka.actor.{ActorRef, Props} -import akka.testkit.TestProbe -import fr.acinq.bitcoin.{ByteVector32, Crypto, Transaction} -import fr.acinq.eclair.TestKitBaseClass -import grizzled.slf4j.Logging -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits._ - -import scala.concurrent.ExecutionContext.Implicits.global -import scala.concurrent.duration._ - -class ElectrumClientSpec extends TestKitBaseClass with AnyFunSuiteLike with Logging { - - import ElectrumClient._ - - var client: ActorRef = _ - val probe = TestProbe() - // this is tx #2690 of block #500000 - val referenceTx = Transaction.read("0200000001983c5b32ced1de5ae97d3ce9b7436f8bb0487d15bf81e5cae97b1e238dc395c6000000006a47304402205957c75766e391350eba2c7b752f0056cb34b353648ecd0992a8a81fc9bcfe980220629c286592842d152cdde71177cd83086619744a533f262473298cacf60193500121021b8b51f74dbf0ac1e766d162c8707b5e8d89fc59da0796f3b4505e7c0fb4cf31feffffff0276bd0101000000001976a914219de672ba773aa0bc2e15cdd9d2e69b734138fa88ac3e692001000000001976a914301706dede031e9fb4b60836e073a4761855f6b188ac09a10700") - val scriptHash = Crypto.sha256(referenceTx.txOut(0).publicKeyScript).reverse - val height = 500000 - val position = 2690 - val merkleProof = List( - hex"b500cd85cd6c7e0e570b82728dd516646536a477b61cc82056505d84a5820dc3", - hex"c98798c2e576566a92b23d2405f59d95c506966a6e26fecfb356d6447a199546", - hex"930d95c428546812fd11f8242904a9a1ba05d2140cd3a83be0e2ed794821c9ec", - hex"90c97965b12f4262fe9bf95bc37ff7d6362902745eaa822ecf0cf85801fa8b48", - hex"23792d51fddd6e439ed4c92ad9f19a9b73fc9d5c52bdd69039be70ad6619a1aa", - hex"4b73075f29a0abdcec2c83c2cfafc5f304d2c19dcacb50a88a023df725468760", - hex"f80225a32a5ce4ef0703822c6aa29692431a816dec77d9b1baa5b09c3ba29bfb", - hex"4858ac33f2022383d3b4dd674666a0880557d02a155073be93231a02ecbb81f4", - hex"eb5b142030ed4e0b55a8ba5a7b5b783a0a24e0c2fd67c1cfa2f7b308db00c38a", - hex"86858812c3837d209110f7ea79de485abdfd22039467a8aa15a8d85856ee7d30", - hex"de20eb85f2e9ad525a6fb5c618682b6bdce2fa83df836a698f31575c4e5b3d38", - hex"98bd1048e04ff1b0af5856d9890cd708d8d67ad6f3a01f777130fbc16810eeb3") - .map(ByteVector32(_)) - - override protected def beforeAll(): Unit = { - client = system.actorOf(Props(new ElectrumClient(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT)), "electrum-client") - } - - test("connect to an electrumx mainnet server") { - probe.send(client, AddStatusListener(probe.ref)) - probe.expectMsgType[ElectrumReady](15 seconds) - } - - test("get transaction id from position") { - probe.send(client, GetTransactionIdFromPosition(height, position)) - probe.expectMsg(GetTransactionIdFromPositionResponse(referenceTx.txid, height, position, Nil)) - } - - test("get transaction id from position with merkle proof") { - probe.send(client, GetTransactionIdFromPosition(height, position, merkle = true)) - probe.expectMsg(GetTransactionIdFromPositionResponse(referenceTx.txid, height, position, merkleProof)) - } - - test("get transaction") { - probe.send(client, GetTransaction(referenceTx.txid)) - val GetTransactionResponse(tx, _) = probe.expectMsgType[GetTransactionResponse] - assert(tx == referenceTx) - } - - test("get header") { - probe.send(client, GetHeader(100000)) - val GetHeaderResponse(height, header) = probe.expectMsgType[GetHeaderResponse] - assert(header.blockId == ByteVector32(hex"000000000003ba27aa200b1cecaad478d2b00432346c3f1f3986da1afd33e506")) - } - - test("get headers") { - val start = (500000 / 2016) * 2016 - probe.send(client, GetHeaders(start, 2016)) - val GetHeadersResponse(start1, headers, _) = probe.expectMsgType[GetHeadersResponse] - assert(start1 == start) - assert(headers.size == 2016) - } - - test("get merkle tree") { - probe.send(client, GetMerkle(referenceTx.txid, 500000)) - val response = probe.expectMsgType[GetMerkleResponse] - assert(response.txid == referenceTx.txid) - assert(response.block_height == 500000) - assert(response.pos == 2690) - assert(response.root == ByteVector32(hex"1f6231ed3de07345b607ec2a39b2d01bec2fe10dfb7f516ba4958a42691c9531")) - } - - test("header subscription") { - val probe1 = TestProbe() - probe1.send(client, HeaderSubscription(probe1.ref)) - val HeaderSubscriptionResponse(_, header) = probe1.expectMsgType[HeaderSubscriptionResponse] - logger.info(s"received header for block ${header.blockId}") - } - - test("scripthash subscription") { - val probe1 = TestProbe() - probe1.send(client, ScriptHashSubscription(scriptHash, probe1.ref)) - val ScriptHashSubscriptionResponse(scriptHash1, status) = probe1.expectMsgType[ScriptHashSubscriptionResponse] - assert(status != "") - } - - test("get scripthash history") { - probe.send(client, GetScriptHashHistory(scriptHash)) - val GetScriptHashHistoryResponse(scriptHash1, history) = probe.expectMsgType[GetScriptHashHistoryResponse] - assert(history.contains(TransactionHistoryItem(500000, referenceTx.txid))) - } - - test("list script unspents") { - probe.send(client, ScriptHashListUnspent(scriptHash)) - val ScriptHashListUnspentResponse(scriptHash1, unspents) = probe.expectMsgType[ScriptHashListUnspentResponse] - assert(unspents.isEmpty) - } - -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala deleted file mode 100644 index 85082e3c3d..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletBasicSpec.scala +++ /dev/null @@ -1,233 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import fr.acinq.bitcoin.Crypto.PrivateKey -import fr.acinq.bitcoin.DeterministicWallet.{ExtendedPrivateKey, derivePrivateKey} -import fr.acinq.bitcoin._ -import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.{Scripts, Transactions} -import grizzled.slf4j.Logging -import org.scalatest.funsuite.AnyFunSuite -import scodec.bits.ByteVector - -import java.sql.DriverManager -import scala.util.{Failure, Random, Success, Try} - -class ElectrumWalletBasicSpec extends AnyFunSuite with Logging { - - import ElectrumWallet._ - import ElectrumWalletBasicSpec._ - - val swipeRange = 10 - val dustLimit = 546 sat - val feerate = FeeratePerKw(20000 sat) - val minimumFee = 2000 sat - - val master = DeterministicWallet.generate(ByteVector32(ByteVector.fill(32)(1))) - val accountMaster = accountKey(master, Block.RegtestGenesisBlock.hash) - val accountIndex = 0 - - val changeMaster = changeKey(master, Block.RegtestGenesisBlock.hash) - val changeIndex = 0 - - val firstAccountKeys = (0 until 10).map(i => derivePrivateKey(accountMaster, i)).toVector - val firstChangeKeys = (0 until 10).map(i => derivePrivateKey(changeMaster, i)).toVector - - val params = ElectrumWallet.WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:"))) - - val state = Data(params, Blockchain.fromCheckpoints(Block.RegtestGenesisBlock.hash, CheckPoint.load(Block.RegtestGenesisBlock.hash)), firstAccountKeys, firstChangeKeys) - .copy(status = (firstAccountKeys ++ firstChangeKeys).map(key => computeScriptHashFromPublicKey(key.publicKey) -> "").toMap) - - def addFunds(data: Data, key: ExtendedPrivateKey, amount: Satoshi): Data = { - val tx = Transaction(version = 1, txIn = Nil, txOut = TxOut(amount, ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) - val scriptHash = ElectrumWallet.computeScriptHashFromPublicKey(key.publicKey) - val scriptHashHistory = data.history.getOrElse(scriptHash, List.empty[ElectrumClient.TransactionHistoryItem]) - data.copy( - history = data.history.updated(scriptHash, ElectrumClient.TransactionHistoryItem(100, tx.txid) :: scriptHashHistory), - transactions = data.transactions + (tx.txid -> tx) - ) - } - - def addFunds(data: Data, keyamount: (ExtendedPrivateKey, Satoshi)): Data = { - val tx = Transaction(version = 1, txIn = Nil, txOut = TxOut(keyamount._2, ElectrumWallet.computePublicKeyScript(keyamount._1.publicKey)) :: Nil, lockTime = 0) - val scriptHash = ElectrumWallet.computeScriptHashFromPublicKey(keyamount._1.publicKey) - val scriptHashHistory = data.history.getOrElse(scriptHash, List.empty[ElectrumClient.TransactionHistoryItem]) - data.copy( - history = data.history.updated(scriptHash, ElectrumClient.TransactionHistoryItem(100, tx.txid) :: scriptHashHistory), - transactions = data.transactions + (tx.txid -> tx) - ) - } - - def addFunds(data: Data, keyamounts: Seq[(ExtendedPrivateKey, Satoshi)]): Data = keyamounts.foldLeft(data)(addFunds) - - test("compute addresses") { - val priv = PrivateKey.fromBase58("cRumXueoZHjhGXrZWeFoEBkeDHu2m8dW5qtFBCqSAt4LDR2Hnd8Q", Base58.Prefix.SecretKeyTestnet)._1 - assert(Base58Check.encode(Base58.Prefix.PubkeyAddressTestnet, priv.publicKey.hash160) == "ms93boMGZZjvjciujPJgDAqeR86EKBf9MC") - assert(segwitAddress(priv, Block.RegtestGenesisBlock.hash) == "2MscvqgGXMTYJNAY3owdUtgWJaxPUjH38Cx") - } - - test("implement BIP49") { - val mnemonics = "pizza afraid guess romance pair steel record jazz rubber prison angle hen heart engage kiss visual helmet twelve lady found between wave rapid twist".split(" ") - val seed = MnemonicCode.toSeed(mnemonics, "") - val master = DeterministicWallet.generate(seed) - - val accountMaster = accountKey(master, Block.RegtestGenesisBlock.hash) - val firstKey = derivePrivateKey(accountMaster, 0) - assert(segwitAddress(firstKey, Block.RegtestGenesisBlock.hash) === "2MxJejujQJRRJdbfTKNQQ94YCnxJwRaE7yo") - } - - test("complete transactions (enough funds)") { - val state1 = addFunds(state, state.accountKeys.head, 1 btc) - val (confirmed1, unconfirmed1) = state1.balance - - val pub = PrivateKey(ByteVector32(ByteVector.fill(32)(1))).publicKey - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(pub)) :: Nil, lockTime = 0) - val (state2, tx1, fee1) = state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = false) - val Some((_, _, Some(fee))) = state2.computeTransactionDelta(tx1) - assert(fee == fee1) - - val state3 = state2.cancelTransaction(tx1) - assert(state3 == state1) - - val state4 = state2.commitTransaction(tx1) - val (confirmed4, unconfirmed4) = state4.balance - assert(confirmed4 == confirmed1) - assert(unconfirmed1 - unconfirmed4 >= btc2satoshi(0.5 btc)) - } - - test("complete transactions (insufficient funds)") { - val state1 = addFunds(state, state.accountKeys.head, 5 btc) - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(6 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - intercept[IllegalArgumentException] { - state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = false) - } - } - - test("compute the effect of tx") { - val state1 = addFunds(state, state.accountKeys.head, 1 btc) - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(0.5 btc, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - val (_, tx1, fee1) = state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = false) - - val Some((received, sent, Some(fee))) = state1.computeTransactionDelta(tx1) - assert(fee == fee1) - assert(sent - received - fee == btc2satoshi(0.5 btc)) - } - - test("use actual transaction weight to compute fees") { - val state1 = addFunds(state, (state.accountKeys(0), 5000000 sat) :: (state.accountKeys(1), 6000000 sat) :: (state.accountKeys(2), 4000000 sat) :: Nil) - - { - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000000 sat, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - val (state3, tx1, fee1) = state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = true) - val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) - assert(fee == fee1) - val actualFeeRate = Transactions.fee2rate(fee, tx1.weight()) - assert(isFeerateOk(actualFeeRate, feerate)) - } - { - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(5000000.sat - dustLimit, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - val (state3, tx1, fee1) = state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = true) - val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) - assert(fee == fee1) - val actualFeeRate = Transactions.fee2rate(fee, tx1.weight()) - assert(isFeerateOk(actualFeeRate, feerate)) - } - { - // with a huge fee rate that will force us to use an additional input when we complete our tx - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(3000000 sat, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - val (state3, tx1, fee1) = state1.completeTransaction(tx, feerate * 100, minimumFee, dustLimit, allowSpendUnconfirmed = true) - val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) - assert(fee == fee1) - val actualFeeRate = Transactions.fee2rate(fee, tx1.weight()) - assert(isFeerateOk(actualFeeRate, feerate * 100)) - } - { - // with a tiny fee rate that will force us to use an additional input when we complete our tx - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(0.09), Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - val (state3, tx1, fee1) = state1.completeTransaction(tx, feerate / 10, minimumFee / 10, dustLimit, allowSpendUnconfirmed = true) - val Some((_, _, Some(fee))) = state3.computeTransactionDelta(tx1) - assert(fee == fee1) - val actualFeeRate = Transactions.fee2rate(fee, tx1.weight()) - assert(isFeerateOk(actualFeeRate, feerate / 10)) - } - } - - test("spend all our balance") { - val state1 = addFunds(state, state.accountKeys(0), 1 btc) - val state2 = addFunds(state1, state1.accountKeys(1), 2 btc) - val state3 = addFunds(state2, state2.changeKeys(0), 0.5 btc) - assert(state3.utxos.length == 3) - assert(state3.balance == (350000000 sat, 0 sat)) - - val (tx, fee) = state3.spendAll(Script.pay2wpkh(ByteVector.fill(20)(1)), feerate) - val Some((received, _, Some(fee1))) = state3.computeTransactionDelta(tx) - assert(received === 0.sat) - assert(fee == fee1) - assert(tx.txOut.map(_.amount).sum + fee == state3.balance._1 + state3.balance._2) - } - - test("check that issue #1146 is fixed") { - val state3 = addFunds(state, state.changeKeys(0), 0.5 btc) - - val pub1 = state.accountKeys(0).publicKey - val pub2 = state.accountKeys(1).publicKey - val redeemScript = Scripts.multiSig2of2(pub1, pub2) - val pubkeyScript = Script.pay2wsh(redeemScript) - val (tx, fee) = state3.spendAll(pubkeyScript, FeeratePerKw(750 sat)) - val Some((received, _, Some(fee1))) = state3.computeTransactionDelta(tx) - assert(received === 0.sat) - assert(fee == fee1) - assert(tx.txOut.map(_.amount).sum + fee == state3.balance._1 + state3.balance._2) - - val tx1 = Transaction(version = 2, txIn = Nil, txOut = TxOut(tx.txOut.map(_.amount).sum, pubkeyScript) :: Nil, lockTime = 0) - assert(Try(state3.completeTransaction(tx1, FeeratePerKw(750 sat), 0 sat, dustLimit, allowSpendUnconfirmed = true)).isSuccess) - } - - test("fuzzy test") { - val random = new Random() - (0 to 10) foreach { _ => - val funds = for (_ <- 0 until random.nextInt(10)) yield { - val index = random.nextInt(state.accountKeys.length) - val amount = dustLimit + random.nextInt(10000000).sat - (state.accountKeys(index), amount) - } - val state1 = addFunds(state, funds) - (0 until 30) foreach { _ => - val amount = dustLimit + random.nextInt(10000000).sat - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(amount, Script.pay2pkh(state1.accountKeys(0).publicKey)) :: Nil, lockTime = 0) - Try(state1.completeTransaction(tx, feerate, minimumFee, dustLimit, allowSpendUnconfirmed = true)) match { - case Success((_, tx1, _)) => - tx1.txOut.foreach(o => require(o.amount >= dustLimit, "output is below dust limit")) - case Failure(cause) if cause.getMessage != null && cause.getMessage.contains("insufficient funds") => () - case Failure(cause) => logger.error(s"unexpected $cause") - } - } - } - } -} - -object ElectrumWalletBasicSpec { - /** - * @param actualFeeRate actual fee rate - * @param targetFeeRate target fee rate - * @return true if actual fee rate is within 10% of target - */ - def isFeerateOk(actualFeeRate: FeeratePerKw, targetFeeRate: FeeratePerKw): Boolean = - Math.abs(actualFeeRate.toLong - targetFeeRate.toLong) < 0.1 * (actualFeeRate + targetFeeRate).toLong -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala deleted file mode 100644 index 732238379e..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSimulatedClientSpec.scala +++ /dev/null @@ -1,412 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.{ActorRef, Terminated} -import akka.testkit -import akka.testkit.{TestActor, TestFSMRef, TestProbe} -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.DeterministicWallet.derivePrivateKey -import fr.acinq.bitcoin.{Block, BlockHeader, ByteVector32, Crypto, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, SatoshiLong, Script, Transaction, TxIn, TxOut} -import fr.acinq.eclair.TestKitBaseClass -import fr.acinq.eclair.blockchain.bitcoind.rpc.Error -import fr.acinq.eclair.blockchain.electrum.ElectrumClient._ -import fr.acinq.eclair.blockchain.electrum.ElectrumWallet._ -import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector - -import java.net.InetSocketAddress -import java.sql.DriverManager -import scala.annotation.tailrec -import scala.concurrent.duration._ - -class ElectrumWalletSimulatedClientSpec extends TestKitBaseClass with AnyFunSuiteLike { - - import ElectrumWalletSimulatedClientSpec._ - - val sender = TestProbe() - - val entropy = ByteVector32(ByteVector.fill(32)(1)) - val mnemonics = MnemonicCode.toMnemonics(entropy) - val seed = MnemonicCode.toSeed(mnemonics, "") - - val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[WalletEvent]) - - val genesis = Block.RegtestGenesisBlock.header - // initial headers that we will sync when we connect to our mock server - var headers = makeHeaders(genesis, 2016 + 2000) - - val client = TestProbe() - client.ignoreMsg { - case ElectrumClient.Ping => true - case _: AddStatusListener => true - case _: HeaderSubscription => true - } - client.setAutoPilot(new testkit.TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { - case ScriptHashSubscription(scriptHash, replyTo) => - replyTo ! ScriptHashSubscriptionResponse(scriptHash, "") - TestActor.KeepRunning - case GetHeaders(start, count, _) => - sender ! GetHeadersResponse(start, headers.drop(start - 1).take(count), 2016) - TestActor.KeepRunning - case _ => TestActor.KeepRunning - } - }) - - val walletParameters = WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = 5000 sat) - val wallet = TestFSMRef(new ElectrumWallet(seed, client.ref, walletParameters)) - - // wallet sends a receive address notification as soon as it is created - listener.expectMsgType[NewWalletReceiveAddress] - - def reconnect: WalletReady = { - sender.send(wallet, ElectrumClient.ElectrumReady(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header, InetSocketAddress.createUnresolved("0.0.0.0", 9735))) - awaitCond(wallet.stateName == ElectrumWallet.WAITING_FOR_TIP) - while (listener.msgAvailable) { - listener.receiveOne(100 milliseconds) - } - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(wallet.stateData.blockchain.bestchain.last.height, wallet.stateData.blockchain.bestchain.last.header)) - awaitCond(wallet.stateName == ElectrumWallet.RUNNING) - val ready = listener.expectMsgType[WalletReady] - ready - } - - test("wait until wallet is ready") { - sender.send(wallet, ElectrumClient.ElectrumReady(2016, headers(2015), InetSocketAddress.createUnresolved("0.0.0.0", 9735))) - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(2016, headers(2015))) - val ready = listener.expectMsgType[WalletReady] - assert(ready.timestamp == headers.last.time) - listener.expectMsgType[NewWalletReceiveAddress] - listener.send(wallet, GetXpub) - val GetXpubResponse(xpub, path) = listener.expectMsgType[GetXpubResponse] - assert(xpub == "upub5DffbMENbUsLcJbhufWvy1jourQfXfC6SoYyxhy2gPKeTSGzYHB3wKTnKH2LYCDemSzZwqzNcHNjnQZJCDn7Jy2LvvQeysQ6hrcK5ogp11B") - assert(path == "m/49'/1'/0'") - } - - test("tell wallet is ready when a new block comes in, even if nothing else has changed") { - val last = wallet.stateData.blockchain.tip - assert(last.header == headers.last) - val header = makeHeader(last.header) - headers = headers :+ header - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header)) - assert(listener.expectMsgType[WalletReady].timestamp == header.time) - val NewWalletReceiveAddress(address) = listener.expectMsgType[NewWalletReceiveAddress] - assert(address == "2NDjBqJugL3gCtjWTToDgaWWogq9nYuYw31") - } - - test("tell wallet is ready when it is reconnected, even if nothing has changed") { - // disconnect wallet - sender.send(wallet, ElectrumClient.ElectrumDisconnected) - awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED) - - // reconnect wallet - val last = wallet.stateData.blockchain.tip - assert(last.header == headers.last) - sender.send(wallet, ElectrumClient.ElectrumReady(2016, headers(2015), InetSocketAddress.createUnresolved("0.0.0.0", 9735))) - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height, last.header)) - awaitCond(wallet.stateName == ElectrumWallet.RUNNING) - - // listener should be notified - assert(listener.expectMsgType[WalletReady].timestamp == last.header.time) - listener.expectMsgType[NewWalletReceiveAddress] - } - - test("don't send the same ready message more then once") { - // listener should be notified - val last = wallet.stateData.blockchain.tip - assert(last.header == headers.last) - val header = makeHeader(last.header) - headers = headers :+ header - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header)) - assert(listener.expectMsgType[WalletReady].timestamp == header.time) - listener.expectMsgType[NewWalletReceiveAddress] - - sender.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, header)) - listener.expectNoMsg(500 milliseconds) - } - - test("disconnect if server sends a bad header") { - val last = wallet.stateData.blockchain.bestchain.last - val bad = makeHeader(last.header, 42L).copy(bits = Long.MaxValue) - - // here we simulate a bad client - val probe = TestProbe() - val watcher = TestProbe() - watcher.watch(probe.ref) - watcher.setAutoPilot(new TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { - case Terminated(actor) if actor == probe.ref => - // if the client dies, we tell the wallet that it's been disconnected - wallet ! ElectrumClient.ElectrumDisconnected - TestActor.KeepRunning - } - }) - - probe.send(wallet, ElectrumClient.HeaderSubscriptionResponse(last.height + 1, bad)) - watcher.expectTerminated(probe.ref) - awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED) - - reconnect - } - - - test("disconnect if server sends an invalid transaction") { - while (client.msgAvailable) { - client.receiveOne(100 milliseconds) - } - val key = wallet.stateData.accountKeys(0) - val scriptHash = computeScriptHashFromPublicKey(key.publicKey) - wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(1)).toHex) - client.expectMsg(GetScriptHashHistory(scriptHash)) - - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(100000 sat, ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) - wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil) - - // wallet will generate a new address and the corresponding subscription - client.expectMsgType[ScriptHashSubscription] - - while (listener.msgAvailable) { - listener.receiveOne(100 milliseconds) - } - - client.expectMsg(GetTransaction(tx.txid)) - wallet ! GetTransactionResponse(tx, None) - val TransactionReceived(_, _, Satoshi(100000), _, _, _) = listener.expectMsgType[TransactionReceived] - // we think we have some unconfirmed funds - val WalletReady(Satoshi(100000), _, _, _) = listener.expectMsgType[WalletReady] - - client.expectMsg(GetMerkle(tx.txid, 2)) - - val probe = TestProbe() - val watcher = TestProbe() - watcher.watch(probe.ref) - watcher.setAutoPilot(new TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = msg match { - case Terminated(actor) if actor == probe.ref => - wallet ! ElectrumClient.ElectrumDisconnected - TestActor.KeepRunning - } - }) - probe.send(wallet, GetMerkleResponse(tx.txid, ByteVector32(ByteVector.fill(32)(1)) :: Nil, 2, 0, None)) - watcher.expectTerminated(probe.ref) - awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED) - - val ready = reconnect - assert(ready.unconfirmedBalance === 0.sat) - } - - test("clear status when we have pending history requests") { - while (client.msgAvailable) { - client.receiveOne(100 milliseconds) - } - // tell wallet that there is something for our first account key - val scriptHash = ElectrumWallet.computeScriptHashFromPublicKey(wallet.stateData.accountKeys(0).publicKey) - wallet ! ScriptHashSubscriptionResponse(scriptHash, "010101") - client.expectMsg(GetScriptHashHistory(scriptHash)) - assert(wallet.stateData.status(scriptHash) == "010101") - - // disconnect wallet - wallet ! ElectrumDisconnected - awaitCond(wallet.stateName == ElectrumWallet.DISCONNECTED) - assert(wallet.stateData.status.get(scriptHash).isEmpty) - - reconnect - } - - test("handle pending transaction requests") { - while (client.msgAvailable) { - client.receiveOne(100 milliseconds) - } - val key = wallet.stateData.accountKeys(1) - val scriptHash = computeScriptHashFromPublicKey(key.publicKey) - wallet ! ScriptHashSubscriptionResponse(scriptHash, ByteVector32(ByteVector.fill(32)(2)).toHex) - client.expectMsg(GetScriptHashHistory(scriptHash)) - - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(100000 sat, ElectrumWallet.computePublicKeyScript(key.publicKey)) :: Nil, lockTime = 0) - wallet ! GetScriptHashHistoryResponse(scriptHash, TransactionHistoryItem(2, tx.txid) :: Nil) - - // wallet will generate a new address and the corresponding subscription - client.expectMsgType[ScriptHashSubscription] - - while (listener.msgAvailable) { - listener.receiveOne(100 milliseconds) - } - - client.expectMsg(GetTransaction(tx.txid)) - assert(wallet.stateData.pendingTransactionRequests == Set(tx.txid)) - } - - test("handle disconnect/reconnect events") { - val data = { - val master = DeterministicWallet.generate(seed) - val accountMaster = ElectrumWallet.accountKey(master, walletParameters.chainHash) - val changeMaster = ElectrumWallet.changeKey(master, walletParameters.chainHash) - val firstAccountKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(accountMaster, i)).toVector - val firstChangeKeys = (0 until walletParameters.swipeRange).map(i => derivePrivateKey(changeMaster, i)).toVector - val data1 = Data(walletParameters, Blockchain.fromGenesisBlock(Block.RegtestGenesisBlock.hash, Block.RegtestGenesisBlock.header), firstAccountKeys, firstChangeKeys) - - val amount1 = 1000000 sat - val amount2 = 1500000 sat - - // transactions that send funds to our wallet - val wallettxs = Seq( - addOutputs(emptyTx, amount1, data1.accountKeys(0).publicKey), - addOutputs(emptyTx, amount2, data1.accountKeys(1).publicKey), - addOutputs(emptyTx, amount2, data1.accountKeys(2).publicKey), - addOutputs(emptyTx, amount2, data1.accountKeys(3).publicKey) - ) - val data2 = wallettxs.foldLeft(data1)(addTransaction) - - // a tx that spend from our wallet to our wallet, plus change to our wallet - val tx1 = { - val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(wallettxs(0), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = walletOutput(wallettxs(0).txOut(0).amount - 50000.sat, data2.accountKeys(2).publicKey) :: walletOutput(50000 sat, data2.changeKeys(0).publicKey) :: Nil, - lockTime = 0) - data2.signTransaction(tx) - } - - // a tx that spend from our wallet to a random address, plus change to our wallet - val tx2 = { - val tx = Transaction(version = 2, - txIn = TxIn(OutPoint(wallettxs(1), 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(wallettxs(1).txOut(0).amount - 50000.sat, Script.pay2wpkh(fr.acinq.eclair.randomKey.publicKey)) :: walletOutput(50000 sat, data2.changeKeys(1).publicKey) :: Nil, - lockTime = 0) - data2.signTransaction(tx) - } - val data3 = Seq(tx1, tx2).foldLeft(data2)(addTransaction) - data3 - } - - // simulated electrum server that disconnects after a given number of messages - - var counter = 0 - var disconnectAfter = 10 // disconnect when counter % disconnectAfter == 0 - - client.setAutoPilot(new testkit.TestActor.AutoPilot { - override def run(sender: ActorRef, msg: Any): TestActor.AutoPilot = { - counter = msg match { - case _: ScriptHashSubscription => counter - case _ => counter + 1 - } - msg match { - case ScriptHashSubscription(scriptHash, replyTo) => - // we skip over these otherwise we would never converge (there are at least 20 such messages sent when we're - // reconnected, one for each account/change key) - replyTo ! ScriptHashSubscriptionResponse(scriptHash, data.status.getOrElse(scriptHash, "")) - TestActor.KeepRunning - case msg if counter % disconnectAfter == 0 => - // disconnect - sender ! ElectrumClient.ElectrumDisconnected - // and reconnect - sender ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735)) - sender ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last) - TestActor.KeepRunning - case request@GetTransaction(txid, context_opt) => - data.transactions.get(txid) match { - case Some(tx) => sender ! GetTransactionResponse(tx, context_opt) - case None => - sender ! ServerError(request, Error(0, s"unknwown tx $txid")) - } - TestActor.KeepRunning - case GetScriptHashHistory(scriptHash) => - sender ! GetScriptHashHistoryResponse(scriptHash, data.history.getOrElse(scriptHash, List())) - TestActor.KeepRunning - case GetHeaders(start, count, _) => - sender ! GetHeadersResponse(start, headers.drop(start - 1).take(count), 2016) - TestActor.KeepRunning - case HeaderSubscription(actor) => actor ! HeaderSubscriptionResponse(headers.length, headers.last) - TestActor.KeepRunning - case _ => - TestActor.KeepRunning - } - } - }) - - val sender = TestProbe() - wallet ! ElectrumClient.ElectrumDisconnected - wallet ! ElectrumClient.ElectrumReady(headers.length, headers.last, InetSocketAddress.createUnresolved("0.0.0.0", 9735)) - wallet ! ElectrumClient.HeaderSubscriptionResponse(headers.length, headers.last) - - data.status.foreach { case (scriptHash, status) => sender.send(wallet, ScriptHashSubscriptionResponse(scriptHash, status)) } - - val expected = data.transactions.keySet - awaitCond { - wallet.stateData.transactions.keySet == expected - } - } -} - -object ElectrumWalletSimulatedClientSpec { - def makeHeader(previousHeader: BlockHeader, timestamp: Long): BlockHeader = { - var template = previousHeader.copy(hashPreviousBlock = previousHeader.hash, time = timestamp, nonce = 0) - while (!BlockHeader.checkProofOfWork(template)) { - template = template.copy(nonce = template.nonce + 1) - } - template - } - - def makeHeader(previousHeader: BlockHeader): BlockHeader = makeHeader(previousHeader, previousHeader.time + 1) - - def makeHeaders(previousHeader: BlockHeader, count: Int): Vector[BlockHeader] = { - @tailrec - def loop(acc: Vector[BlockHeader]): Vector[BlockHeader] = if (acc.length == count) acc else loop(acc :+ makeHeader(acc.last)) - - loop(Vector(makeHeader(previousHeader))) - } - - val emptyTx = Transaction(version = 2, txIn = Nil, txOut = Nil, lockTime = 0) - - def walletOutput(amount: Satoshi, key: PublicKey) = TxOut(amount, ElectrumWallet.computePublicKeyScript(key)) - - def addOutputs(tx: Transaction, amount: Satoshi, keys: PublicKey*): Transaction = keys.foldLeft(tx) { case (t, k) => t.copy(txOut = t.txOut :+ walletOutput(amount, k)) } - - def addToHistory(history: Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]], scriptHash: ByteVector32, item: TransactionHistoryItem): Map[ByteVector32, List[ElectrumClient.TransactionHistoryItem]] = { - history.get(scriptHash) match { - case None => history + (scriptHash -> List(item)) - case Some(items) if items.contains(item) => history - case _ => history.updated(scriptHash, history(scriptHash) :+ item) - } - } - - def updateStatus(data: ElectrumWallet.Data): ElectrumWallet.Data = { - val status1 = data.history.mapValues(items => { - val status = items.map(i => s"${i.tx_hash}:${i.height}:").mkString("") - Crypto.sha256(ByteVector.view(status.getBytes())).toString() - }).toMap - data.copy(status = status1) - } - - def addTransaction(data: ElectrumWallet.Data, tx: Transaction): ElectrumWallet.Data = { - data.transactions.get(tx.txid) match { - case Some(_) => data - case None => - val history1 = tx.txOut.filter(o => data.isMine(o)).foldLeft(data.history) { case (a, b) => - addToHistory(a, Crypto.sha256(b.publicKeyScript).reverse, TransactionHistoryItem(100000, tx.txid)) - } - val data1 = data.copy(history = history1, transactions = data.transactions + (tx.txid -> tx)) - val history2 = tx.txIn.filter(i => data1.isMine(i)).foldLeft(data1.history) { case (a, b) => - addToHistory(a, ElectrumWallet.computeScriptHashFromPublicKey(extractPubKeySpentFrom(b).get), TransactionHistoryItem(100000, tx.txid)) - } - val data2 = data1.copy(history = history2) - updateStatus(data2) - } - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala deleted file mode 100644 index 088a98dc19..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWalletSpec.scala +++ /dev/null @@ -1,388 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.{ActorRef, Props} -import akka.testkit.TestProbe -import com.whisk.docker.DockerReadyChecker -import fr.acinq.bitcoin.{Block, Btc, BtcDouble, ByteVector32, DeterministicWallet, MnemonicCode, OutPoint, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} -import fr.acinq.eclair.TestKitBaseClass -import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq -import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient -import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient.{FundTransactionResponse, SignTransactionResponse} -import fr.acinq.eclair.blockchain.bitcoind.{BitcoinCoreWallet, BitcoindService} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{BroadcastTransaction, BroadcastTransactionResponse, SSL} -import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress -import fr.acinq.eclair.blockchain.electrum.db.sqlite.SqliteWalletDb -import fr.acinq.eclair.blockchain.fee.FeeratePerKw -import fr.acinq.eclair.transactions.{Scripts, Transactions} -import fr.acinq.{bitcoin, eclair} -import grizzled.slf4j.Logging -import org.json4s.JsonAST.{JDecimal, JString, JValue} -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits.ByteVector - -import java.net.InetSocketAddress -import java.sql.DriverManager -import java.util.concurrent.atomic.AtomicLong -import scala.concurrent.Await -import scala.concurrent.duration._ - -class ElectrumWalletSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { - - import ElectrumWallet._ - - val entropy = ByteVector32(ByteVector.fill(32)(1)) - val mnemonics = MnemonicCode.toMnemonics(entropy) - val seed = MnemonicCode.toSeed(mnemonics, "") - logger.info(s"mnemonic codes for our wallet: $mnemonics") - val master = DeterministicWallet.generate(seed) - var wallet: ActorRef = _ - var electrumClient: ActorRef = _ - - override def beforeAll(): Unit = { - logger.info("starting bitcoind") - startBitcoind() - super.beforeAll() - } - - override def afterAll(): Unit = { - logger.info("stopping bitcoind") - stopBitcoind() - super.afterAll() - } - - def getCurrentAddress(probe: TestProbe) = { - probe.send(wallet, GetCurrentReceiveAddress) - probe.expectMsgType[GetCurrentReceiveAddressResponse] - } - - def getBalance(probe: TestProbe) = { - probe.send(wallet, GetBalance) - probe.expectMsgType[GetBalanceResponse] - } - - test("generate 150 blocks") { - waitForBitcoindReady() - DockerReadyChecker.LogLineContains("INFO:BlockProcessor:height: 151").looped(attempts = 15, delay = 1 second) - } - - test("wait until wallet is ready") { - electrumClient = system.actorOf(Props(new ElectrumClientPool(new AtomicLong(), Set(ElectrumServerAddress(new InetSocketAddress("localhost", electrumPort), SSL.OFF))))) - wallet = system.actorOf(Props(new ElectrumWallet(seed, electrumClient, WalletParameters(Block.RegtestGenesisBlock.hash, new SqliteWalletDb(DriverManager.getConnection("jdbc:sqlite::memory:")), minimumFee = 5000 sat))), "wallet") - val probe = TestProbe() - awaitCond({ - probe.send(wallet, GetData) - val GetDataResponse(state) = probe.expectMsgType[GetDataResponse] - state.status.size == state.accountKeys.size + state.changeKeys.size - }, max = 30 seconds, interval = 1 second) - logger.info(s"wallet is ready") - } - - test("receive funds") { - val probe = TestProbe() - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) - logger.info(s"initial balance: $confirmed $unconfirmed") - - // send money to our wallet - val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) - - logger.info(s"sending 1 btc to $address") - probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - probe.expectMsgType[JValue] - - awaitCond({ - val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) - unconfirmed1 == unconfirmed + 100000000.sat - }, max = 30 seconds, interval = 1 second) - - // confirm our tx - generateBlocks(1) - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - confirmed1 == confirmed + 100000000.sat - }, max = 30 seconds, interval = 1 second) - - val GetCurrentReceiveAddressResponse(address1) = getCurrentAddress(probe) - - logger.info(s"sending 1 btc to $address1") - probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1, 1.0)) - probe.expectMsgType[JValue] - logger.info(s"sending 0.5 btc to $address1") - probe.send(bitcoincli, BitcoinReq("sendtoaddress", address1, 0.5)) - probe.expectMsgType[JValue] - generateBlocks(1) - - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - confirmed1 == confirmed + 250000000.sat - }, max = 30 seconds, interval = 1 second) - } - - test("handle transactions with identical outputs to us") { - val probe = TestProbe() - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) - logger.info(s"initial balance: $confirmed $unconfirmed") - - // send money to our wallet - val amount = 750000 sat - val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) - val tx = Transaction(version = 2, - txIn = Nil, - txOut = Seq( - TxOut(amount, fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)), - TxOut(amount, fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) - ), lockTime = 0L) - val btcWallet = new BitcoinCoreWallet(bitcoinrpcclient) - val btcClient = new ExtendedBitcoinClient(bitcoinrpcclient) - val future = for { - FundTransactionResponse(tx1, _, _) <- btcWallet.fundTransaction(tx, lockUtxos = false, FeeratePerKw(10000 sat)) - SignTransactionResponse(tx2, true) <- btcWallet.signTransaction(tx1) - txid <- btcClient.publishTransaction(tx2) - } yield txid - Await.result(future, 10 seconds) - - awaitCond({ - val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) - unconfirmed1 == unconfirmed + amount + amount - }, max = 30 seconds, interval = 1 second) - - generateBlocks(1) - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - confirmed1 == confirmed + amount + amount - }, max = 30 seconds, interval = 1 second) - } - - test("receive 'confidence changed' notification") { - val probe = TestProbe() - val listener = TestProbe() - system.eventStream.subscribe(listener.ref, classOf[WalletEvent]) - - val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) - logger.info(s"initial balance $confirmed $unconfirmed") - - logger.info(s"sending 1 btc to $address") - probe.send(bitcoincli, BitcoinReq("sendtoaddress", address, 1.0)) - val JString(txid) = probe.expectMsgType[JValue] - logger.info(s"$txid sent 1 btc to us at $address") - awaitCond({ - val GetBalanceResponse(_, unconfirmed1) = getBalance(probe) - unconfirmed1 - unconfirmed === 100000000L.sat - }, max = 30 seconds, interval = 1 second) - - val TransactionReceived(tx, 0, received, _, _, _) = listener.receiveOne(5 seconds) - assert(tx.txid === ByteVector32.fromValidHex(txid)) - assert(received === 100000000.sat) - - logger.info("generating a new block") - generateBlocks(1) - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - confirmed1 - confirmed === 100000000.sat - }, max = 30 seconds, interval = 1 second) - - awaitCond({ - val msg = listener.receiveOne(5 seconds) - msg match { - case TransactionConfidenceChanged(_, 1, _) => true - case _ => false - } - }, max = 30 seconds, interval = 1 second) - } - - test("send money to someone else (we broadcast)") { - val probe = TestProbe() - val GetBalanceResponse(confirmed, _) = getBalance(probe) - - // create a tx that sends money to Bitcoin Core's address - probe.send(bitcoincli, BitcoinReq("getnewaddress")) - val JString(address) = probe.expectMsgType[JValue] - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L) - probe.send(wallet, CompleteTransaction(tx, FeeratePerKw(20000 sat))) - val CompleteTransactionResponse(tx1, _, None) = probe.expectMsgType[CompleteTransactionResponse] - - // send it ourselves - logger.info(s"sending 1 btc to $address with tx ${tx1.txid}") - probe.send(wallet, BroadcastTransaction(tx1)) - val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] - - generateBlocks(1) - - awaitCond({ - probe.send(bitcoincli, BitcoinReq("getreceivedbyaddress", address)) - val JDecimal(value) = probe.expectMsgType[JValue] - value == BigDecimal(1.0) - }, max = 30 seconds, interval = 1 second) - - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - logger.debug(s"current balance is $confirmed1") - confirmed1 < confirmed - 1.btc && confirmed1 > confirmed - 1.btc - 50000.sat - }, max = 30 seconds, interval = 1 second) - } - - test("send money to ourselves (we broadcast)") { - val probe = TestProbe() - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) - logger.info(s"current balance is $confirmed $unconfirmed") - - // create a tx that sends money to Bitcoin Core's address - probe.send(bitcoincli, BitcoinReq("getnewaddress")) - val JString(address) = probe.expectMsgType[JValue] - val tx = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L) - probe.send(wallet, CompleteTransaction(tx, FeeratePerKw(20000 sat))) - val CompleteTransactionResponse(tx1, _, None) = probe.expectMsgType[CompleteTransactionResponse] - - // send it ourselves - logger.info(s"sending 1 btc to $address with tx ${tx1.txid}") - probe.send(wallet, BroadcastTransaction(tx1)) - val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] - - generateBlocks(1) - - awaitCond({ - val GetBalanceResponse(confirmed1, _) = getBalance(probe) - logger.info(s"current balance is $confirmed $unconfirmed") - confirmed1 < confirmed - 1.btc && confirmed1 > confirmed - 1.btc - 50000.sat - }, max = 30 seconds, interval = 1 second) - } - - test("detect is a tx has been double-spent") { - val probe = TestProbe() - val GetBalanceResponse(confirmed, unconfirmed) = getBalance(probe) - logger.info(s"current balance is $confirmed $unconfirmed") - - // create 2 transactions that spend the same wallet UTXO - val tx1 = { - probe.send(bitcoincli, BitcoinReq("getnewaddress")) - val JString(address) = probe.expectMsgType[JValue] - val tmp = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L) - probe.send(wallet, CompleteTransaction(tmp, FeeratePerKw(20000 sat))) - val CompleteTransactionResponse(tx, _, None) = probe.expectMsgType[CompleteTransactionResponse] - probe.send(wallet, CancelTransaction(tx)) - probe.expectMsg(CancelTransactionResponse(tx)) - tx - } - val tx2 = { - probe.send(bitcoincli, BitcoinReq("getnewaddress")) - val JString(address) = probe.expectMsgType[JValue] - val tmp = Transaction(version = 2, txIn = Nil, txOut = TxOut(Btc(1), fr.acinq.eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, lockTime = 0L) - probe.send(wallet, CompleteTransaction(tmp, FeeratePerKw(20000 sat))) - val CompleteTransactionResponse(tx, _, None) = probe.expectMsgType[CompleteTransactionResponse] - probe.send(wallet, CancelTransaction(tx)) - probe.expectMsg(CancelTransactionResponse(tx)) - tx - } - - probe.send(wallet, IsDoubleSpent(tx1)) - probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false)) - probe.send(wallet, IsDoubleSpent(tx2)) - probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = false)) - - // publish tx1 - probe.send(wallet, BroadcastTransaction(tx1)) - probe.expectMsg(BroadcastTransactionResponse(tx1, None)) - - awaitCond({ - probe.send(wallet, GetData) - val data = probe.expectMsgType[GetDataResponse].state - data.heights.contains(tx1.txid) && data.transactions.contains(tx1.txid) - }, max = 30 seconds, interval = 1 second) - - // as long as tx1 is unconfirmed tx2 won't be considered double-spent - probe.send(wallet, IsDoubleSpent(tx1)) - probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false)) - probe.send(wallet, IsDoubleSpent(tx2)) - probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = false)) - - generateBlocks(2) - - awaitCond({ - probe.send(wallet, GetData) - val data = probe.expectMsgType[GetDataResponse].state - data.heights.exists { case (txid, height) => txid == tx1.txid && data.transactions.contains(txid) && ElectrumWallet.computeDepth(data.blockchain.height, height) > 1 } - }, max = 30 seconds, interval = 1 second) - - // tx2 is double-spent - probe.send(wallet, IsDoubleSpent(tx1)) - probe.expectMsg(IsDoubleSpentResponse(tx1, isDoubleSpent = false)) - probe.send(wallet, IsDoubleSpent(tx2)) - probe.expectMsg(IsDoubleSpentResponse(tx2, isDoubleSpent = true)) - } - - test("use all available balance") { - val probe = TestProbe() - - // send all our funds to ourself, so we have only one utxo which is the worse case here - val GetCurrentReceiveAddressResponse(address) = getCurrentAddress(probe) - probe.send(wallet, SendAll(Script.write(eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)), FeeratePerKw(750 sat))) - val SendAllResponse(tx, _) = probe.expectMsgType[SendAllResponse] - probe.send(wallet, BroadcastTransaction(tx)) - val BroadcastTransactionResponse(`tx`, None) = probe.expectMsgType[BroadcastTransactionResponse] - - generateBlocks(1) - - awaitCond({ - probe.send(wallet, GetData) - val data = probe.expectMsgType[GetDataResponse].state - data.utxos.length == 1 && data.utxos(0).outPoint.txid == tx.txid - }, max = 30 seconds, interval = 1 second) - - - // send everything to a multisig 2-of-2, with the smallest possible fee rate - val priv = eclair.randomKey - val script = Script.pay2wsh(Scripts.multiSig2of2(priv.publicKey, priv.publicKey)) - probe.send(wallet, SendAll(Script.write(script), FeeratePerKw.MinimumFeeratePerKw)) - val SendAllResponse(tx1, _) = probe.expectMsgType[SendAllResponse] - probe.send(wallet, BroadcastTransaction(tx1)) - val BroadcastTransactionResponse(`tx1`, None) = probe.expectMsgType[BroadcastTransactionResponse] - - generateBlocks(1) - - awaitCond({ - probe.send(wallet, GetData) - val data = probe.expectMsgType[GetDataResponse].state - data.utxos.isEmpty - }, max = 30 seconds, interval = 1 second) - - // send everything back to ourselves again - val tx2 = Transaction(version = 2, - txIn = TxIn(OutPoint(tx1, 0), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(Satoshi(0), eclair.addressToPublicKeyScript(address, Block.RegtestGenesisBlock.hash)) :: Nil, - lockTime = 0) - - val sig = Transaction.signInput(tx2, 0, Scripts.multiSig2of2(priv.publicKey, priv.publicKey), bitcoin.SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv) - val tx3 = tx2.updateWitness(0, ScriptWitness(Seq(ByteVector.empty, sig, sig, Script.write(Scripts.multiSig2of2(priv.publicKey, priv.publicKey))))) - Transaction.correctlySpends(tx3, Seq(tx1), ScriptFlags.STANDARD_SCRIPT_VERIFY_FLAGS) - val fee = Transactions.weight2fee(FeeratePerKw.MinimumFeeratePerKw, tx3.weight()) - val tx4 = tx3.copy(txOut = tx3.txOut(0).copy(amount = tx1.txOut(0).amount - fee) :: Nil) - val sig1 = Transaction.signInput(tx4, 0, Scripts.multiSig2of2(priv.publicKey, priv.publicKey), bitcoin.SIGHASH_ALL, tx1.txOut(0).amount, SigVersion.SIGVERSION_WITNESS_V0, priv) - val tx5 = tx4.updateWitness(0, ScriptWitness(Seq(ByteVector.empty, sig1, sig1, Script.write(Scripts.multiSig2of2(priv.publicKey, priv.publicKey))))) - - probe.send(wallet, BroadcastTransaction(tx5)) - val BroadcastTransactionResponse(_, None) = probe.expectMsgType[BroadcastTransactionResponse] - - awaitCond({ - probe.send(wallet, GetData) - val data = probe.expectMsgType[GetDataResponse].state - data.utxos.length == 1 && data.utxos(0).outPoint.txid == tx5.txid - }, max = 30 seconds, interval = 1 second) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala deleted file mode 100644 index 154c25399d..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumWatcherSpec.scala +++ /dev/null @@ -1,270 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import akka.actor.Props -import akka.testkit.TestProbe -import fr.acinq.bitcoin.{Btc, ByteVector32, SatoshiLong, Transaction, TxIn} -import fr.acinq.eclair.blockchain.WatcherSpec._ -import fr.acinq.eclair.blockchain._ -import fr.acinq.eclair.blockchain.bitcoind.BitcoindService -import fr.acinq.eclair.blockchain.bitcoind.BitcoindService.BitcoinReq -import fr.acinq.eclair.blockchain.bitcoind.rpc.ExtendedBitcoinClient -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.SSL -import fr.acinq.eclair.blockchain.electrum.ElectrumClientPool.ElectrumServerAddress -import fr.acinq.eclair.channel.{BITCOIN_FUNDING_DEPTHOK, BITCOIN_FUNDING_SPENT} -import fr.acinq.eclair.{TestKitBaseClass, randomBytes32} -import grizzled.slf4j.Logging -import org.json4s.JsonAST.JValue -import org.scalatest.BeforeAndAfterAll -import org.scalatest.funsuite.AnyFunSuiteLike -import scodec.bits._ - -import java.net.InetSocketAddress -import java.util.concurrent.atomic.AtomicLong -import scala.concurrent.duration._ - -class ElectrumWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with BitcoindService with ElectrumxService with BeforeAndAfterAll with Logging { - - override def beforeAll(): Unit = { - logger.info("starting bitcoind") - startBitcoind() - waitForBitcoindReady() - super.beforeAll() - } - - override def afterAll(): Unit = { - logger.info("stopping bitcoind") - stopBitcoind() - super.afterAll() - } - - val electrumAddress = ElectrumServerAddress(new InetSocketAddress("localhost", electrumPort), SSL.OFF) - - test("watch for confirmed transactions") { - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - - val address = getNewAddress(probe) - val tx = sendToAddress(address, Btc(1), probe) - - probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 2, BITCOIN_FUNDING_DEPTHOK)) - generateBlocks(2) - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx) - probe.send(watcher, WatchConfirmed(listener.ref, tx, 4, BITCOIN_FUNDING_DEPTHOK)) - generateBlocks(2) - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx) - system.stop(watcher) - } - - test("watch for spent transactions") { - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - - val address = getNewAddress(probe) - val priv = dumpPrivateKey(address, probe) - val tx = sendToAddress(address, Btc(1)) - - // create a tx that spends the previous output - val spendingTx = createSpendP2WPKH(tx, priv, priv.publicKey, 1000 sat, TxIn.SEQUENCE_FINAL, 0) - val outpointIndex = spendingTx.txIn.head.outPoint.index.toInt - probe.send(watcher, WatchSpent(listener.ref, tx.txid, outpointIndex, tx.txOut(outpointIndex).publicKeyScript, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - listener.expectNoMsg(1 second) - probe.send(bitcoincli, BitcoinReq("sendrawtransaction", spendingTx.toString)) - probe.expectMsgType[JValue] - generateBlocks(2) - assert(listener.expectMsgType[WatchEventSpent].tx === spendingTx) - system.stop(watcher) - } - - test("watch for mempool transactions (txs in mempool before we set the watch)") { - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref)) - probe.expectMsgType[ElectrumClient.ElectrumReady] - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - - val address = getNewAddress(probe) - val priv1 = dumpPrivateKey(address, probe) - val tx = sendToAddress(address, Btc(1)) - val priv2 = dumpPrivateKey(getNewAddress(probe), probe) - val priv3 = dumpPrivateKey(getNewAddress(probe), probe) - val tx1 = createSpendP2WPKH(tx, priv1, priv2.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) - val tx2 = createSpendP2WPKH(tx1, priv2, priv3.publicKey, 10000 sat, TxIn.SEQUENCE_FINAL, 0) - probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString())) - probe.expectMsgType[JValue] - probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString())) - probe.expectMsgType[JValue] - - // wait until tx1 and tx2 are in the mempool (as seen by our ElectrumX server) - awaitCond({ - probe.send(electrumClient, ElectrumClient.GetScriptHashHistory(ElectrumClient.computeScriptHash(tx2.txOut.head.publicKeyScript))) - val ElectrumClient.GetScriptHashHistoryResponse(_, history) = probe.expectMsgType[ElectrumClient.GetScriptHashHistoryResponse] - history.map(_.tx_hash).toSet == Set(tx2.txid) - }, max = 30 seconds, interval = 5 seconds) - - // then set watches - probe.send(watcher, WatchConfirmed(listener.ref, tx2, 0, BITCOIN_FUNDING_DEPTHOK)) - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx2) - probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2)) - system.stop(watcher) - } - - test("watch for mempool transactions (txs not yet in the mempool when we set the watch)") { - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref)) - probe.expectMsgType[ElectrumClient.ElectrumReady] - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - - val address = getNewAddress(probe) - val priv = dumpPrivateKey(address, probe) - val tx = sendToAddress(address, Btc(1)) - val (tx1, tx2) = createUnspentTxChain(tx, priv) - - // here we set watches * before * we publish our transactions - probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - probe.send(watcher, WatchConfirmed(listener.ref, tx1, 0, BITCOIN_FUNDING_DEPTHOK)) - probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx1.toString())) - probe.expectMsgType[JValue] - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx1) - probe.send(bitcoincli, BitcoinReq("sendrawtransaction", tx2.toString())) - probe.expectMsgType[JValue] - listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2)) - system.stop(watcher) - } - - test("publish transactions with relative or absolute delays") { - import akka.pattern.pipe - - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val bitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - bitcoinClient.getBlockCount.pipeTo(probe.ref) - val initialBlockCount = probe.expectMsgType[Long] - probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref)) - awaitCond(probe.expectMsgType[ElectrumClient.ElectrumReady].height >= initialBlockCount, message = s"waiting for tip at $initialBlockCount") - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - val recipient = dumpPrivateKey(getNewAddress(probe), probe).publicKey - - val address1 = getNewAddress(probe) - val priv1 = dumpPrivateKey(address1, probe) - val tx1 = sendToAddress(address1, Btc(0.2)) - val address2 = getNewAddress(probe) - val priv2 = dumpPrivateKey(address2, probe) - val tx2 = sendToAddress(address2, Btc(0.2)) - generateBlocks(1) - for (tx <- Seq(tx1, tx2)) { - probe.send(watcher, WatchConfirmed(listener.ref, tx, 1, BITCOIN_FUNDING_DEPTHOK)) - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx) - } - - // spend tx1 with an absolute delay but no relative delay - val spend1 = createSpendP2WPKH(tx1, priv1, recipient, 5000 sat, sequence = 0, lockTime = blockCount.get + 1) - probe.send(watcher, WatchSpent(listener.ref, tx1, spend1.txIn.head.outPoint.index.toInt, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - probe.send(watcher, PublishAsap(spend1, PublishStrategy.JustPublish)) - // spend tx2 with a relative delay but no absolute delay - val spend2 = createSpendP2WPKH(tx2, priv2, recipient, 3000 sat, sequence = 1, lockTime = 0) - probe.send(watcher, WatchSpent(listener.ref, tx2, spend2.txIn.head.outPoint.index.toInt, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - probe.send(watcher, PublishAsap(spend2, PublishStrategy.JustPublish)) - - generateBlocks(1) - listener.expectMsgAllOf(WatchEventSpent(BITCOIN_FUNDING_SPENT, spend1), WatchEventSpent(BITCOIN_FUNDING_SPENT, spend2)) - - system.stop(watcher) - } - - test("publish transactions with relative and absolute delays") { - import akka.pattern.pipe - - val (probe, listener) = (TestProbe(), TestProbe()) - val blockCount = new AtomicLong() - val bitcoinClient = new ExtendedBitcoinClient(bitcoinrpcclient) - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(electrumAddress)))) - bitcoinClient.getBlockCount.pipeTo(probe.ref) - val initialBlockCount = probe.expectMsgType[Long] - probe.send(electrumClient, ElectrumClient.AddStatusListener(probe.ref)) - awaitCond(probe.expectMsgType[ElectrumClient.ElectrumReady].height >= initialBlockCount, message = s"waiting for tip at $initialBlockCount") - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - val recipient = dumpPrivateKey(getNewAddress(probe), probe).publicKey - - val address = getNewAddress(probe) - val priv = dumpPrivateKey(address, probe) - val tx = sendToAddress(address, Btc(0.2)) - generateBlocks(1) - probe.send(watcher, WatchConfirmed(listener.ref, tx, 1, BITCOIN_FUNDING_DEPTHOK)) - assert(listener.expectMsgType[WatchEventConfirmed].tx === tx) - - // spend tx with both relative and absolute delays - val spend = createSpendP2WPKH(tx, priv, recipient, 6000 sat, sequence = 1, lockTime = blockCount.get + 2) - probe.send(watcher, WatchSpent(listener.ref, tx, spend.txIn.head.outPoint.index.toInt, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - probe.send(watcher, PublishAsap(spend, PublishStrategy.JustPublish)) - generateBlocks(2) - listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, spend)) - - system.stop(watcher) - } - - test("generate unique dummy scids") { - // generate 1000 dummy ids - val dummies = (0 until 20).map { _ => - ElectrumWatcher.makeDummyShortChannelId(randomBytes32) - } toSet - - // make sure that they are unique (we allow for 1 collision here, actual probability of a collision with the current impl. is 1% - // but that could change and we don't want to make this test impl. dependent) - // if this test fails it's very likely that the code that generates dummy scids is broken - assert(dummies.size >= 19) - } - - test("get transaction") { - val blockCount = new AtomicLong() - val mainnetAddress = ElectrumServerAddress(new InetSocketAddress("electrum.acinq.co", 50002), SSL.STRICT) - val electrumClient = system.actorOf(Props(new ElectrumClientPool(blockCount, Set(mainnetAddress)))) - val watcher = system.actorOf(Props(new ElectrumWatcher(blockCount, electrumClient))) - val probe = TestProbe() - - { - // tx is in the blockchain - val txid = ByteVector32(hex"c0b18008713360d7c30dae0940d88152a4bbb10faef5a69fefca5f7a7e1a06cc") - probe.send(watcher, GetTxWithMeta(txid)) - val res = probe.expectMsgType[GetTxWithMetaResponse] - assert(res.txid === txid) - assert(res.tx_opt === Some(Transaction.read("0100000001b5cbd7615a7494f60304695c180eb255113bd5effcf54aec6c7dfbca67f533a1010000006a473044022042115a5d1a489bbc9bd4348521b098025625c9b6c6474f84b96b11301da17a0602203ccb684b1d133ff87265a6017ef0fdd2d22dd6eef0725c57826f8aaadcc16d9d012103629aa3df53cad290078bbad26491f1e11f9c01697c65db0967561f6f142c993cffffffff02801015000000000017a914b8984d6344eed24689cdbc77adaf73c66c4fdd688734e9e818000000001976a91404607585722760691867b42d43701905736be47d88ac00000000"))) - assert(res.lastBlockTimestamp > System.currentTimeMillis().millis.toSeconds - 7200) // this server should be in sync - } - - { - // tx doesn't exist - val txid = ByteVector32(hex"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") - probe.send(watcher, GetTxWithMeta(txid)) - val res = probe.expectMsgType[GetTxWithMetaResponse] - assert(res.txid === txid) - assert(res.tx_opt === None) - assert(res.lastBlockTimestamp > System.currentTimeMillis().millis.toSeconds - 7200) // this server should be in sync - } - system.stop(watcher) - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala deleted file mode 100644 index fc54580826..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/ElectrumxService.scala +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum - -import com.spotify.docker.client.{DefaultDockerClient, DockerClient} -import com.whisk.docker.impl.spotify.SpotifyDockerFactory -import com.whisk.docker.scalatest.DockerTestKit -import com.whisk.docker.{DockerContainer, DockerFactory} -import fr.acinq.eclair.TestUtils -import fr.acinq.eclair.blockchain.bitcoind.BitcoindService -import org.scalatest.Suite - -import scala.concurrent.duration.DurationInt - -trait ElectrumxService extends DockerTestKit { - self: Suite with BitcoindService => - - val electrumPort = TestUtils.availablePort - - val electrumxContainer = if (System.getProperty("os.name").startsWith("Linux")) { - // "host" mode will let the container access the host network on linux - // we use our own docker image because other images on Docker lag behind and don't yet support 1.4 - DockerContainer("acinq/electrumx") - .withNetworkMode("host") - .withEnv(s"DAEMON_URL=http://foo:bar@localhost:$bitcoindRpcPort", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") - //.withLogLineReceiver(LogLineReceiver(true, println)) - } else { - // on windows or oxs, host mode is not available, but from docker 18.03 on host.docker.internal can be used instead - // host.docker.internal is not (yet ?) available on linux though - DockerContainer("acinq/electrumx") - .withPorts(electrumPort -> Some(electrumPort)) - .withEnv(s"DAEMON_URL=http://foo:bar@host.docker.internal:$bitcoindRpcPort", "COIN=BitcoinSegwit", "NET=regtest", s"TCP_PORT=$electrumPort") - //.withLogLineReceiver(LogLineReceiver(true, println)) - } - - //override DockerKit timeouts - override val StartContainersTimeout = 60 seconds - - override val StopContainersTimeout = 60 seconds - - override def dockerContainers: List[DockerContainer] = electrumxContainer :: super.dockerContainers - - private val client: DockerClient = DefaultDockerClient.fromEnv().build() - - override implicit val dockerFactory: DockerFactory = new SpotifyDockerFactory(client) -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala deleted file mode 100644 index 21cca52fd8..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/electrum/db/sqlite/SqliteWalletDbSpec.scala +++ /dev/null @@ -1,129 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.electrum.db.sqlite - -import fr.acinq.bitcoin.{Block, BlockHeader, OutPoint, Satoshi, Transaction, TxIn, TxOut} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.GetMerkleResponse -import fr.acinq.eclair.blockchain.electrum.ElectrumWallet.PersistentData -import fr.acinq.eclair.{TestDatabases, randomBytes, randomBytes32} -import org.scalatest.funsuite.AnyFunSuite -import scodec.Codec -import scodec.bits.BitVector - -import scala.util.Random - -class SqliteWalletDbSpec extends AnyFunSuite { - val random = new Random() - - def makeChildHeader(header: BlockHeader): BlockHeader = header.copy(hashPreviousBlock = header.hash, nonce = random.nextLong() & 0xffffffffL) - - def makeHeaders(n: Int, acc: Seq[BlockHeader] = Seq(Block.RegtestGenesisBlock.header)): Seq[BlockHeader] = { - if (acc.size == n) acc else makeHeaders(n, acc :+ makeChildHeader(acc.last)) - } - - def randomTransaction = Transaction(version = 2, - txIn = TxIn(OutPoint(randomBytes32, random.nextInt(100)), signatureScript = Nil, sequence = TxIn.SEQUENCE_FINAL) :: Nil, - txOut = TxOut(Satoshi(random.nextInt(10000000)), randomBytes(20)) :: Nil, - 0L - ) - - def randomHeight = if (random.nextBoolean()) random.nextInt(500000) else -1 - - def randomHistoryItem = ElectrumClient.TransactionHistoryItem(randomHeight, randomBytes32) - - def randomHistoryItems = (0 to random.nextInt(100)).map(_ => randomHistoryItem).toList - - def randomProof = GetMerkleResponse(randomBytes32, ((0 until 10).map(_ => randomBytes32)).toList, random.nextInt(100000), 0, None) - - def randomPersistentData = { - val transactions = for (i <- 0 until random.nextInt(100)) yield randomTransaction - - PersistentData( - accountKeysCount = 10, - changeKeysCount = 10, - status = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> random.nextInt(100000).toHexString).toMap, - transactions = transactions.map(tx => tx.hash -> tx).toMap, - heights = transactions.map(tx => tx.hash -> randomHeight).toMap, - history = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomHistoryItems).toMap, - proofs = (for (i <- 0 until random.nextInt(100)) yield randomBytes32 -> randomProof).toMap, - pendingTransactions = transactions.toList, - locks = (for (i <- 0 until random.nextInt(10)) yield randomTransaction).toSet - ) - } - - test("add/get/list headers") { - val db = new SqliteWalletDb(TestDatabases.sqliteInMemory()) - val headers = makeHeaders(100) - db.addHeaders(2016, headers) - - val headers1 = db.getHeaders(2016, None) - assert(headers1 === headers) - - val headers2 = db.getHeaders(2016, Some(50)) - assert(headers2 === headers.take(50)) - - var height = 2016 - headers.foreach(header => { - val Some((height1, header1)) = db.getHeader(header.hash) - assert(height1 == height) - assert(header1 == header) - - val Some(header2) = db.getHeader(height1) - assert(header2 == header) - height = height + 1 - }) - } - - test("serialize persistent data") { - val db = new SqliteWalletDb(TestDatabases.sqliteInMemory()) - assert(db.readPersistentData() == None) - - for (i <- 0 until 50) { - val data = randomPersistentData - db.persist(data) - val Some(check) = db.readPersistentData() - assert(check === data.copy(locks = Set.empty[Transaction])) - } - } - - test("read old persistent data") { - import SqliteWalletDb._ - import scodec.codecs._ - - def setCodec[T](codec: Codec[T]): Codec[Set[T]] = listOfN(uint16, codec).xmap(_.toSet, _.toList) - - val oldPersistentDataCodec: Codec[PersistentData] = ( - ("version" | constant(BitVector.fromInt(version))) :: - ("accountKeysCount" | int32) :: - ("changeKeysCount" | int32) :: - ("status" | statusCodec) :: - ("transactions" | transactionsCodec) :: - ("heights" | heightsCodec) :: - ("history" | historyCodec) :: - ("proofs" | proofsCodec) :: - ("pendingTransactions" | listOfN(uint16, txCodec)) :: - ("locks" | setCodec(txCodec))).as[PersistentData] - - for (_ <- 0 until 50) { - val data = randomPersistentData - val encoded = oldPersistentDataCodec.encode(data).require - val decoded = persistentDataCodec.decode(encoded).require.value - assert(decoded === data.copy(locks = Set.empty[Transaction])) - } - } -} diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala deleted file mode 100644 index f1a43aead9..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/BitgoFeeProviderSpec.scala +++ /dev/null @@ -1,98 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import akka.actor.ActorSystem -import akka.util.Timeout -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.{Block, SatoshiLong} -import fr.acinq.eclair.TestTags -import org.json4s.DefaultFormats -import org.scalatest.funsuite.AnyFunSuite - -import scala.concurrent.Await - -/** - * Created by PM on 27/01/2017. - */ - -class BitgoFeeProviderSpec extends AnyFunSuite { - - import BitgoFeeProvider._ - import org.json4s.jackson.JsonMethods.parse - - implicit val formats = DefaultFormats - - val sample_response = - """ - {"feePerKb":136797,"cpfpFeePerKb":136797,"numBlocks":2,"confidence":80,"multiplier":1,"feeByBlockTarget":{"1":149453,"2":136797,"5":122390,"6":105566,"8":100149,"9":96254,"10":122151,"13":116855,"15":110860,"17":87402,"27":82635,"33":71098,"42":105782,"49":68182,"73":59207,"97":17336,"121":16577,"193":13545,"313":12268,"529":11122,"553":9139,"577":5395,"793":5070}} - """ - - test("parse test") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - assert(feeRanges.size === 23) - } - - test("extract fee for a particular block delay") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - val fee = extractFeerate(feeRanges, 6) - assert(fee === FeeratePerKB(105566 sat)) - } - - test("extract all fees") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - val feerates = extractFeerates(feeRanges) - val ref = FeeratesPerKB( - mempoolMinFee = FeeratePerKB(5070 sat), - block_1 = FeeratePerKB(149453 sat), - blocks_2 = FeeratePerKB(136797 sat), - blocks_6 = FeeratePerKB(105566 sat), - blocks_12 = FeeratePerKB(96254 sat), - blocks_36 = FeeratePerKB(71098 sat), - blocks_72 = FeeratePerKB(68182 sat), - blocks_144 = FeeratePerKB(16577 sat), - blocks_1008 = FeeratePerKB(5070 sat)) - assert(feerates === ref) - } - - test("make sure API hasn't changed", TestTags.ExternalApi) { - import scala.concurrent.duration._ - implicit val system = ActorSystem("test") - implicit val ec = system.dispatcher - implicit val sttp = OkHttpFutureBackend() - implicit val timeout = Timeout(30 seconds) - val bitgo = new BitgoFeeProvider(Block.LivenetGenesisBlock.hash, 5 seconds) - assert(Await.result(bitgo.getFeerates, timeout.duration).block_1.toLong > 0) - } - - test("check that read timeout is enforced", TestTags.ExternalApi) { - import scala.concurrent.duration._ - implicit val system = ActorSystem("test") - implicit val ec = system.dispatcher - implicit val sttp = OkHttpFutureBackend() - implicit val timeout = Timeout(30 second) - val bitgo = new BitgoFeeProvider(Block.LivenetGenesisBlock.hash, 1 millisecond) - val e = intercept[Exception] { - Await.result(bitgo.getFeerates, timeout.duration) - } - assert(e.getMessage.contains("timed out") || e.getMessage.contains("timeout")) - } - -} \ No newline at end of file diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala deleted file mode 100644 index d72d380fe2..0000000000 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/fee/EarnDotComFeeProviderSpec.scala +++ /dev/null @@ -1,95 +0,0 @@ -/* - * Copyright 2019 ACINQ SAS - * - * Licensed under the Apache License, Version 2.0 (the "License"); - * you may not use this file except in compliance with the License. - * You may obtain a copy of the License at - * - * http://www.apache.org/licenses/LICENSE-2.0 - * - * Unless required by applicable law or agreed to in writing, software - * distributed under the License is distributed on an "AS IS" BASIS, - * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - * See the License for the specific language governing permissions and - * limitations under the License. - */ - -package fr.acinq.eclair.blockchain.fee - -import akka.util.Timeout -import com.softwaremill.sttp.okhttp.OkHttpFutureBackend -import fr.acinq.bitcoin.SatoshiLong -import fr.acinq.eclair.TestTags -import grizzled.slf4j.Logging -import org.json4s.DefaultFormats -import org.scalatest.funsuite.AnyFunSuite - -import scala.concurrent.Await - -/** - * Created by PM on 27/01/2017. - */ - -class EarnDotComFeeProviderSpec extends AnyFunSuite with Logging { - - import EarnDotComFeeProvider._ - import org.json4s.jackson.JsonMethods.parse - - implicit val formats = DefaultFormats - - val sample_response = - """ - {"fees":[{"minFee":0,"maxFee":0,"dayCount":10,"memCount":0,"minDelay":83,"maxDelay":10000,"minMinutes":600,"maxMinutes":10000},{"minFee":1,"maxFee":10,"dayCount":31871,"memCount":29690,"minDelay":3,"maxDelay":118,"minMinutes":30,"maxMinutes":1320},{"minFee":11,"maxFee":20,"dayCount":8272,"memCount":7261,"minDelay":3,"maxDelay":118,"minMinutes":30,"maxMinutes":1320},{"minFee":21,"maxFee":30,"dayCount":13817,"memCount":12781,"minDelay":3,"maxDelay":89,"minMinutes":30,"maxMinutes":1080},{"minFee":31,"maxFee":40,"dayCount":8651,"memCount":4484,"minDelay":3,"maxDelay":54,"minMinutes":25,"maxMinutes":660},{"minFee":41,"maxFee":50,"dayCount":18306,"memCount":2152,"minDelay":3,"maxDelay":38,"minMinutes":15,"maxMinutes":420},{"minFee":51,"maxFee":60,"dayCount":16592,"memCount":1699,"minDelay":3,"maxDelay":21,"minMinutes":10,"maxMinutes":300},{"minFee":61,"maxFee":70,"dayCount":5351,"memCount":711,"minDelay":3,"maxDelay":19,"minMinutes":5,"maxMinutes":300},{"minFee":71,"maxFee":80,"dayCount":4138,"memCount":350,"minDelay":3,"maxDelay":18,"minMinutes":5,"maxMinutes":300},{"minFee":81,"maxFee":90,"dayCount":3190,"memCount":584,"minDelay":2,"maxDelay":18,"minMinutes":5,"maxMinutes":300},{"minFee":91,"maxFee":100,"dayCount":3738,"memCount":596,"minDelay":2,"maxDelay":16,"minMinutes":5,"maxMinutes":240},{"minFee":101,"maxFee":110,"dayCount":1649,"memCount":348,"minDelay":1,"maxDelay":14,"minMinutes":0,"maxMinutes":240},{"minFee":111,"maxFee":120,"dayCount":1622,"memCount":252,"minDelay":1,"maxDelay":13,"minMinutes":0,"maxMinutes":180},{"minFee":121,"maxFee":130,"dayCount":1562,"memCount":106,"minDelay":1,"maxDelay":13,"minMinutes":0,"maxMinutes":180},{"minFee":131,"maxFee":140,"dayCount":2386,"memCount":165,"minDelay":1,"maxDelay":12,"minMinutes":0,"maxMinutes":180},{"minFee":141,"maxFee":150,"dayCount":2008,"memCount":217,"minDelay":1,"maxDelay":11,"minMinutes":0,"maxMinutes":180},{"minFee":151,"maxFee":160,"dayCount":1594,"memCount":136,"minDelay":1,"maxDelay":10,"minMinutes":0,"maxMinutes":180},{"minFee":161,"maxFee":170,"dayCount":1415,"memCount":65,"minDelay":1,"maxDelay":10,"minMinutes":0,"maxMinutes":180},{"minFee":171,"maxFee":180,"dayCount":1533,"memCount":169,"minDelay":1,"maxDelay":10,"minMinutes":0,"maxMinutes":180},{"minFee":181,"maxFee":190,"dayCount":1569,"memCount":121,"minDelay":1,"maxDelay":9,"minMinutes":0,"maxMinutes":180},{"minFee":191,"maxFee":200,"dayCount":5824,"memCount":205,"minDelay":1,"maxDelay":8,"minMinutes":0,"maxMinutes":120},{"minFee":201,"maxFee":210,"dayCount":3028,"memCount":142,"minDelay":0,"maxDelay":7,"minMinutes":0,"maxMinutes":110},{"minFee":211,"maxFee":220,"dayCount":1521,"memCount":104,"minDelay":0,"maxDelay":7,"minMinutes":0,"maxMinutes":110},{"minFee":221,"maxFee":230,"dayCount":2057,"memCount":249,"minDelay":0,"maxDelay":6,"minMinutes":0,"maxMinutes":100},{"minFee":231,"maxFee":240,"dayCount":1429,"memCount":86,"minDelay":0,"maxDelay":6,"minMinutes":0,"maxMinutes":100},{"minFee":241,"maxFee":250,"dayCount":2351,"memCount":87,"minDelay":0,"maxDelay":6,"minMinutes":0,"maxMinutes":100},{"minFee":251,"maxFee":260,"dayCount":3748,"memCount":90,"minDelay":0,"maxDelay":5,"minMinutes":0,"maxMinutes":90},{"minFee":261,"maxFee":270,"dayCount":3518,"memCount":154,"minDelay":0,"maxDelay":5,"minMinutes":0,"maxMinutes":80},{"minFee":271,"maxFee":280,"dayCount":1731,"memCount":86,"minDelay":0,"maxDelay":4,"minMinutes":0,"maxMinutes":80},{"minFee":281,"maxFee":290,"dayCount":1467,"memCount":101,"minDelay":0,"maxDelay":4,"minMinutes":0,"maxMinutes":80},{"minFee":291,"maxFee":300,"dayCount":5799,"memCount":161,"minDelay":0,"maxDelay":3,"minMinutes":0,"maxMinutes":70},{"minFee":301,"maxFee":310,"dayCount":4390,"memCount":147,"minDelay":0,"maxDelay":3,"minMinutes":0,"maxMinutes":70},{"minFee":311,"maxFee":320,"dayCount":2655,"memCount":78,"minDelay":0,"maxDelay":3,"minMinutes":0,"maxMinutes":60},{"minFee":321,"maxFee":330,"dayCount":1422,"memCount":44,"minDelay":0,"maxDelay":3,"minMinutes":0,"maxMinutes":60},{"minFee":331,"maxFee":340,"dayCount":1328,"memCount":27,"minDelay":0,"maxDelay":3,"minMinutes":0,"maxMinutes":60},{"minFee":341,"maxFee":350,"dayCount":1725,"memCount":43,"minDelay":0,"maxDelay":2,"minMinutes":0,"maxMinutes":60},{"minFee":351,"maxFee":360,"dayCount":2527,"memCount":77,"minDelay":0,"maxDelay":2,"minMinutes":0,"maxMinutes":55},{"minFee":361,"maxFee":370,"dayCount":2130,"memCount":37,"minDelay":0,"maxDelay":2,"minMinutes":0,"maxMinutes":55},{"minFee":371,"maxFee":380,"dayCount":2038,"memCount":46,"minDelay":0,"maxDelay":2,"minMinutes":0,"maxMinutes":50},{"minFee":381,"maxFee":390,"dayCount":3248,"memCount":58,"minDelay":0,"maxDelay":2,"minMinutes":0,"maxMinutes":45},{"minFee":391,"maxFee":400,"dayCount":7721,"memCount":37,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":401,"maxFee":410,"dayCount":6337,"memCount":19,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":411,"maxFee":420,"dayCount":7327,"memCount":96,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":421,"maxFee":430,"dayCount":4298,"memCount":21,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":431,"maxFee":440,"dayCount":10088,"memCount":63,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":441,"maxFee":450,"dayCount":6246,"memCount":33,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":40},{"minFee":451,"maxFee":460,"dayCount":8231,"memCount":19,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":35},{"minFee":461,"maxFee":470,"dayCount":3725,"memCount":19,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":35},{"minFee":471,"maxFee":480,"dayCount":2903,"memCount":21,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":35},{"minFee":481,"maxFee":490,"dayCount":3086,"memCount":11,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":30},{"minFee":491,"maxFee":500,"dayCount":8870,"memCount":14,"minDelay":0,"maxDelay":1,"minMinutes":0,"maxMinutes":30},{"minFee":501,"maxFee":510,"dayCount":33705,"memCount":105,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":511,"maxFee":520,"dayCount":2373,"memCount":17,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":521,"maxFee":530,"dayCount":2457,"memCount":8,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":531,"maxFee":540,"dayCount":2275,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":541,"maxFee":550,"dayCount":1412,"memCount":5,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":551,"maxFee":560,"dayCount":11752,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":561,"maxFee":570,"dayCount":1370,"memCount":2,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":571,"maxFee":580,"dayCount":2743,"memCount":3,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":581,"maxFee":590,"dayCount":1741,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":591,"maxFee":600,"dayCount":1398,"memCount":6,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":601,"maxFee":610,"dayCount":1602,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":611,"maxFee":620,"dayCount":891,"memCount":9,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":621,"maxFee":630,"dayCount":1284,"memCount":7,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":631,"maxFee":640,"dayCount":1543,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":641,"maxFee":650,"dayCount":683,"memCount":6,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":651,"maxFee":660,"dayCount":717,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":661,"maxFee":670,"dayCount":656,"memCount":5,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":671,"maxFee":680,"dayCount":564,"memCount":4,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":681,"maxFee":690,"dayCount":1126,"memCount":3,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":691,"maxFee":700,"dayCount":9357,"memCount":25,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":701,"maxFee":710,"dayCount":10161,"memCount":34,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":711,"maxFee":720,"dayCount":2206,"memCount":20,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30},{"minFee":721,"maxFee":50710,"dayCount":17883,"memCount":142,"minDelay":0,"maxDelay":0,"minMinutes":0,"maxMinutes":30}]} - """ - - test("parse test") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - assert(feeRanges.size === 74) - } - - test("extract fee for a particular block delay") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - val fee = extractFeerate(feeRanges, 6) - assert(fee === FeeratePerKB(230 * 1000 sat)) - } - - test("extract all fees") { - val json = parse(sample_response) - val feeRanges = parseFeeRanges(json) - val feerates = extractFeerates(feeRanges) - val ref = FeeratesPerKB( - mempoolMinFee = FeeratePerKB(10 * 1000 sat), - block_1 = FeeratePerKB(400 * 1000 sat), - blocks_2 = FeeratePerKB(350 * 1000 sat), - blocks_6 = FeeratePerKB(230 * 1000 sat), - blocks_12 = FeeratePerKB(140 * 1000 sat), - blocks_36 = FeeratePerKB(60 * 1000 sat), - blocks_72 = FeeratePerKB(40 * 1000 sat), - blocks_144 = FeeratePerKB(10 * 1000 sat), - blocks_1008 = FeeratePerKB(10 * 1000 sat)) - assert(feerates === ref) - } - - test("make sure API hasn't changed", TestTags.ExternalApi) { - import scala.concurrent.ExecutionContext.Implicits.global - import scala.concurrent.duration._ - implicit val sttpBackend = OkHttpFutureBackend() - implicit val timeout = Timeout(30 seconds) - val provider = new EarnDotComFeeProvider(5 seconds) - logger.info("earn.com livenet fees: " + Await.result(provider.getFeerates, 10 seconds)) - } - - test("check that read timeout is enforced", TestTags.ExternalApi) { - import scala.concurrent.ExecutionContext.Implicits.global - import scala.concurrent.duration._ - implicit val sttpBackend = OkHttpFutureBackend() - implicit val timeout = Timeout(5 second) - val provider = new EarnDotComFeeProvider(1 millisecond) - intercept[Exception] { - Await.result(provider.getFeerates, timeout.duration) - } - } - -} diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala index c8e62ebe80..8ccd1ecdf7 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/FxApp.scala @@ -21,7 +21,6 @@ import java.io.File import akka.actor.{ActorSystem, Props, SupervisorStrategy} import fr.acinq.eclair._ import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor._ -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.ElectrumEvent import fr.acinq.eclair.channel.ChannelEvent import fr.acinq.eclair.gui.controllers.{MainController, NotificationsController} import fr.acinq.eclair.payment.PaymentEvent @@ -100,7 +99,6 @@ class FxApp extends Application with Logging { system.eventStream.subscribe(guiUpdater, classOf[NetworkEvent]) system.eventStream.subscribe(guiUpdater, classOf[PaymentEvent]) system.eventStream.subscribe(guiUpdater, classOf[ZMQEvent]) - system.eventStream.subscribe(guiUpdater, classOf[ElectrumEvent]) pKit.completeWith(setup.bootstrap) pKit.future.onComplete { case Success(kit) => diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala index da8653111d..512c4bf081 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/GUIUpdater.scala @@ -23,7 +23,6 @@ import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin._ import fr.acinq.eclair.CoinUtils import fr.acinq.eclair.blockchain.bitcoind.zmq.ZMQActor.{ZMQConnected, ZMQDisconnected} -import fr.acinq.eclair.blockchain.electrum.ElectrumClient.{ElectrumDisconnected, ElectrumReady} import fr.acinq.eclair.channel._ import fr.acinq.eclair.gui.controllers._ import fr.acinq.eclair.payment._ @@ -230,12 +229,5 @@ class GUIUpdater(mainController: MainController) extends Actor with ActorLogging log.debug("ZMQ connection DOWN") runInGuiThread(() => mainController.showBlockerModal("Bitcoin Core")) - case _: ElectrumReady => - log.debug("Electrum connection UP") - runInGuiThread(() => mainController.hideBlockerModal) - - case ElectrumDisconnected => - log.debug("Electrum connection DOWN") - runInGuiThread(() => mainController.showBlockerModal("Electrum")) } } diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index 1664a2995c..ef32b2f179 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -24,7 +24,7 @@ import java.util.Locale import com.google.common.net.HostAndPort import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.Satoshi -import fr.acinq.eclair.NodeParams.{BITCOIND, ELECTRUM} +import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.gui.stages._ import fr.acinq.eclair.gui.utils.{ContextMenuUtils, CopyAction, IndexedObservableList} import fr.acinq.eclair.gui.{FxApp, Handlers} @@ -361,7 +361,6 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext val wallet = setup.nodeParams.watcherType match { case BITCOIND => "Bitcoin-core" - case ELECTRUM => "Electrum" } bitcoinWallet.setText(wallet) bitcoinChain.setText(s"${setup.chain.toUpperCase()}") From 447cba4187a56c7d974c73feb3e533d8f7369708 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 1 Apr 2021 14:53:08 +0200 Subject: [PATCH 2/4] Remove pubKeyScript from watches --- .../eclair/blockchain/WatcherTypes.scala | 72 ++++++------------- .../blockchain/bitcoind/ZmqWatcher.scala | 7 +- .../fr/acinq/eclair/channel/Channel.scala | 24 +++---- .../scala/fr/acinq/eclair/router/Router.scala | 8 +-- .../fr/acinq/eclair/router/Validation.scala | 2 +- .../acinq/eclair/blockchain/WatcherSpec.scala | 17 ----- .../blockchain/bitcoind/ZmqWatcherSpec.scala | 38 +++++----- 7 files changed, 60 insertions(+), 108 deletions(-) diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala index 5516184578..a8537eb584 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/WatcherTypes.scala @@ -17,50 +17,32 @@ package fr.acinq.eclair.blockchain import akka.actor.ActorRef -import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.{ByteVector32, Satoshi, Script, ScriptWitness, Transaction} +import fr.acinq.bitcoin.{ByteVector32, Satoshi, Transaction} import fr.acinq.eclair.blockchain.fee.FeeratePerKw import fr.acinq.eclair.channel.BitcoinEvent import fr.acinq.eclair.transactions.Transactions.TransactionSigningKit import fr.acinq.eclair.wire.protocol.ChannelAnnouncement -import scodec.bits.ByteVector - -import scala.util.{Failure, Success, Try} /** * Created by PM on 19/01/2016. */ -// @formatter:off - sealed trait Watch { + // @formatter:off def replyTo: ActorRef def event: BitcoinEvent + // @formatter:on } /** * Watch for confirmation of a given transaction. * - * @param replyTo actor to notify once the transaction is confirmed. - * @param txId txid of the transaction to watch. - * @param publicKeyScript output script of any of the transaction's outputs. - * @param minDepth number of confirmations. - * @param event channel event related to the transaction. + * @param replyTo actor to notify once the transaction is confirmed. + * @param txId txid of the transaction to watch. + * @param minDepth number of confirmations. + * @param event channel event related to the transaction. */ -final case class WatchConfirmed(replyTo: ActorRef, txId: ByteVector32, publicKeyScript: ByteVector, minDepth: Long, event: BitcoinEvent) extends Watch -object WatchConfirmed { - // if we have the entire transaction, we can get the publicKeyScript from any of the outputs - def apply(replyTo: ActorRef, tx: Transaction, minDepth: Long, event: BitcoinEvent): WatchConfirmed = WatchConfirmed(replyTo, tx.txid, tx.txOut.map(_.publicKeyScript).headOption.getOrElse(ByteVector.empty), minDepth, event) - - def extractPublicKeyScript(witness: ScriptWitness): ByteVector = Try(PublicKey(witness.stack.last)) match { - case Success(pubKey) => - // if last element of the witness is a public key, then this is a p2wpkh - Script.write(Script.pay2wpkh(pubKey)) - case Failure(_) => - // otherwise this is a p2wsh - Script.write(Script.pay2wsh(witness.stack.last)) - } -} +final case class WatchConfirmed(replyTo: ActorRef, txId: ByteVector32, minDepth: Long, event: BitcoinEvent) extends Watch /** * Watch for transactions spending the given outpoint. @@ -69,19 +51,14 @@ object WatchConfirmed { * - we see a spending transaction in the mempool, but it is then replaced (RBF) * - we see a spending transaction in the mempool, but a conflicting transaction "wins" and gets confirmed in a block * - * @param replyTo actor to notify when the outpoint is spent. - * @param txId txid of the outpoint to watch. - * @param outputIndex index of the outpoint to watch. - * @param publicKeyScript output script of the outpoint. - * @param event channel event related to the outpoint. - * @param hints txids of potential spending transactions; most of the time we know the txs, and it allows for optimizations. - * This argument can safely be ignored by watcher implementations. + * @param replyTo actor to notify when the outpoint is spent. + * @param txId txid of the outpoint to watch. + * @param outputIndex index of the outpoint to watch. + * @param event channel event related to the outpoint. + * @param hints txids of potential spending transactions; most of the time we know the txs, and it allows for optimizations. + * This argument can safely be ignored by watcher implementations. */ -final case class WatchSpent(replyTo: ActorRef, txId: ByteVector32, outputIndex: Int, publicKeyScript: ByteVector, event: BitcoinEvent, hints: Set[ByteVector32]) extends Watch -object WatchSpent { - // if we have the entire transaction, we can get the publicKeyScript from the relevant output - def apply(replyTo: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent, hints: Set[ByteVector32]): WatchSpent = WatchSpent(replyTo, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event, hints) -} +final case class WatchSpent(replyTo: ActorRef, txId: ByteVector32, outputIndex: Int, event: BitcoinEvent, hints: Set[ByteVector32]) extends Watch /** * Watch for the first transaction spending the given outpoint. We assume that txid is already confirmed or in the @@ -90,17 +67,12 @@ object WatchSpent { * NB: an event will be triggered only once when we see a transaction that spends the given outpoint. If you want to * react to the transaction spending the outpoint, you should use [[WatchSpent]] instead. * - * @param replyTo actor to notify when the outpoint is spent. - * @param txId txid of the outpoint to watch. - * @param outputIndex index of the outpoint to watch. - * @param publicKeyScript output script of the outpoint. - * @param event channel event related to the outpoint. + * @param replyTo actor to notify when the outpoint is spent. + * @param txId txid of the outpoint to watch. + * @param outputIndex index of the outpoint to watch. + * @param event channel event related to the outpoint. */ -final case class WatchSpentBasic(replyTo: ActorRef, txId: ByteVector32, outputIndex: Int, publicKeyScript: ByteVector, event: BitcoinEvent) extends Watch -object WatchSpentBasic { - // if we have the entire transaction, we can get the publicKeyScript from the relevant output - def apply(replyTo: ActorRef, tx: Transaction, outputIndex: Int, event: BitcoinEvent): WatchSpentBasic = WatchSpentBasic(replyTo, tx.txid, outputIndex, tx.txOut(outputIndex).publicKeyScript, event) -} +final case class WatchSpentBasic(replyTo: ActorRef, txId: ByteVector32, outputIndex: Int, event: BitcoinEvent) extends Watch // TODO: not implemented yet: notify me if confirmation number gets below minDepth? final case class WatchLost(replyTo: ActorRef, txId: ByteVector32, minDepth: Long, event: BitcoinEvent) extends Watch @@ -138,6 +110,7 @@ final case class WatchEventSpentBasic(event: BitcoinEvent) extends WatchEvent // TODO: not implemented yet. final case class WatchEventLost(event: BitcoinEvent) extends WatchEvent +// @formatter:off sealed trait PublishStrategy object PublishStrategy { case object JustPublish extends PublishStrategy @@ -145,10 +118,12 @@ object PublishStrategy { override def toString = s"SetFeerate(target=$targetFeerate)" } } +// @formatter:on /** Publish the provided tx as soon as possible depending on lock time, csv and publishing strategy. */ final case class PublishAsap(tx: Transaction, strategy: PublishStrategy) +// @formatter:off sealed trait UtxoStatus object UtxoStatus { case object Unspent extends UtxoStatus @@ -160,5 +135,4 @@ final case class ValidateResult(c: ChannelAnnouncement, fundingTx: Either[Throwa final case class GetTxWithMeta(txid: ByteVector32) final case class GetTxWithMetaResponse(txid: ByteVector32, tx_opt: Option[Transaction], lastBlockTimestamp: Long) - // @formatter:on diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala index a70ab9e2c9..8a1385ecd8 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcher.scala @@ -138,7 +138,7 @@ class ZmqWatcher(chainHash: ByteVector32, blockCount: AtomicLong, client: Extend val result = w match { case _ if watches.contains(w) => Ignore // we ignore duplicates - case WatchSpentBasic(_, txid, outputIndex, _, _) => + case WatchSpentBasic(_, txid, outputIndex, _) => // NB: we assume parent tx was published, we just need to make sure this particular output has not been spent client.isTransactionOutputSpendable(txid, outputIndex, includeMempool = true).collect { case false => @@ -147,7 +147,7 @@ class ZmqWatcher(chainHash: ByteVector32, blockCount: AtomicLong, client: Extend } Keep - case WatchSpent(_, txid, outputIndex, _, _, hints) => + case WatchSpent(_, txid, outputIndex, _, hints) => // first let's see if the parent tx was published or not client.getTxConfirmations(txid).collect { case Some(_) => @@ -209,8 +209,7 @@ class ZmqWatcher(chainHash: ByteVector32, blockCount: AtomicLong, client: Extend // time a parent's relative delays are satisfied, so we will eventually succeed. csvTimeouts.foreach { case (parentTxId, csvTimeout) => log.info(s"txid=${tx.txid} has a relative timeout of $csvTimeout blocks, watching parentTxId=$parentTxId tx={}", tx) - val parentPublicKeyScript = Script.write(Script.pay2wsh(tx.txIn.find(_.outPoint.txid == parentTxId).get.witness.stack.last)) - self ! WatchConfirmed(self, parentTxId, parentPublicKeyScript, minDepth = csvTimeout, BITCOIN_PARENT_TX_CONFIRMED(p)) + self ! WatchConfirmed(self, parentTxId, minDepth = csvTimeout, BITCOIN_PARENT_TX_CONFIRMED(p)) } } else if (cltvTimeout > blockCount) { log.info(s"delaying publication of txid=${tx.txid} until block=$cltvTimeout (curblock=$blockCount)") diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala index 8ae8070c90..2f27826214 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/channel/Channel.scala @@ -475,7 +475,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId log.info(s"waiting for them to publish the funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") val fundingMinDepth = Helpers.minDepthForFunding(nodeParams, fundingAmount) watchFundingTx(commitments) - blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, fundingMinDepth, BITCOIN_FUNDING_DEPTHOK) + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, fundingMinDepth, BITCOIN_FUNDING_DEPTHOK) goto(WAIT_FOR_FUNDING_CONFIRMED) using DATA_WAIT_FOR_FUNDING_CONFIRMED(commitments, None, initialRelayFees_opt, nodeParams.currentBlockHeight, None, Right(fundingSigned)) storing() sending fundingSigned } } @@ -514,7 +514,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId context.system.eventStream.publish(ChannelSignatureReceived(self, commitments)) log.info(s"publishing funding tx for channelId=$channelId fundingTxid=${commitInput.outPoint.txid}") watchFundingTx(commitments) - blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, commitInput.txOut.publicKeyScript, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) + blockchain ! WatchConfirmed(self, commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_DEPTHOK) log.info(s"committing txid=${fundingTx.txid}") // we will publish the funding tx only after the channel state has been written to disk because we want to @@ -618,7 +618,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId when(WAIT_FOR_FUNDING_LOCKED)(handleExceptions { case Event(FundingLocked(_, nextPerCommitmentPoint), d@DATA_WAIT_FOR_FUNDING_LOCKED(commitments, shortChannelId, _, initialRelayFees_opt)) => // used to get the final shortChannelId, used in announcements (if minDepth >= ANNOUNCEMENTS_MINCONF this event will fire instantly) - blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) + blockchain ! WatchConfirmed(self, commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) context.system.eventStream.publish(ShortChannelIdAssigned(self, commitments.channelId, shortChannelId, None)) // we create a channel_update early so that we can use it to send payments through this channel, but it won't be propagated to other nodes since the channel is not yet announced val (feeBase, feeProportionalMillionths) = initialRelayFees_opt.getOrElse((nodeParams.feeBase, nodeParams.feeProportionalMillionth)) @@ -1322,7 +1322,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId case Event(WatchEventSpent(BITCOIN_OUTPUT_SPENT, tx), d: DATA_CLOSING) => // one of the outputs of the local/remote/revoked commit was spent // we just put a watch to be notified when it is confirmed - blockchain ! WatchConfirmed(self, tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx)) + blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx)) // when a remote or local commitment tx containing outgoing htlcs is published on the network, // we watch it in order to extract payment preimage if funds are pulled by the counterparty // we can then use these preimages to fulfill origin htlcs @@ -1342,7 +1342,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val revokedCommitPublished1 = d.revokedCommitPublished.map { rev => val (rev1, penaltyTxs) = Closing.claimRevokedHtlcTxOutputs(keyManager, d.commitments, rev, tx, nodeParams.onChainFeeConf.feeEstimator) penaltyTxs.foreach(claimTx => blockchain ! PublishAsap(claimTx.tx, PublishStrategy.JustPublish)) - penaltyTxs.foreach(claimTx => blockchain ! WatchSpent(self, tx, claimTx.input.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set(claimTx.tx.txid))) + penaltyTxs.foreach(claimTx => blockchain ! WatchSpent(self, tx.txid, claimTx.input.outPoint.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set(claimTx.tx.txid))) rev1 } stay using d.copy(revokedCommitPublished = revokedCommitPublished1) storing() @@ -1356,7 +1356,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId val (localCommitPublished1, claimHtlcTx_opt) = Closing.claimLocalCommitHtlcTxOutput(localCommitPublished, keyManager, d.commitments, tx, nodeParams.onChainFeeConf.feeEstimator, nodeParams.onChainFeeConf.feeTargets) claimHtlcTx_opt.foreach(claimHtlcTx => { blockchain ! PublishAsap(claimHtlcTx.tx, PublishStrategy.JustPublish) - blockchain ! WatchConfirmed(self, claimHtlcTx.tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(claimHtlcTx.tx)) + blockchain ! WatchConfirmed(self, claimHtlcTx.tx.txid, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(claimHtlcTx.tx)) }) Closing.updateLocalCommitPublished(localCommitPublished1, tx) }), @@ -1529,7 +1529,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId Helpers.minDepthForFunding(nodeParams, d.commitments.commitInput.txOut.amount) } // we put back the watch (operation is idempotent) because the event may have been fired while we were in OFFLINE - blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, minDepth, BITCOIN_FUNDING_DEPTHOK) + blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, minDepth, BITCOIN_FUNDING_DEPTHOK) goto(WAIT_FOR_FUNDING_CONFIRMED) case Event(_: ChannelReestablish, d: DATA_WAIT_FOR_FUNDING_LOCKED) => @@ -1589,7 +1589,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId if (!d.buried) { // even if we were just disconnected/reconnected, we need to put back the watch because the event may have been // fired while we were in OFFLINE (if not, the operation is idempotent anyway) - blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, d.commitments.commitInput.txOut.publicKeyScript, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) + blockchain ! WatchConfirmed(self, d.commitments.commitInput.outPoint.txid, ANNOUNCEMENTS_MINCONF, BITCOIN_FUNDING_DEEPLYBURIED) } else { // channel has been buried enough, should we (re)send our announcement sigs? d.channelAnnouncement match { @@ -1960,7 +1960,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId private def watchFundingTx(commitments: Commitments, additionalKnownSpendingTxs: Set[ByteVector32] = Set.empty): Unit = { // TODO: should we wait for an acknowledgment from the watcher? val knownSpendingTxs = Set(commitments.localCommit.publishableTxs.commitTx.tx.txid, commitments.remoteCommit.txid) ++ commitments.remoteNextCommitInfo.left.toSeq.map(_.nextRemoteCommit.txid).toSet ++ additionalKnownSpendingTxs - blockchain ! WatchSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, commitments.commitInput.txOut.publicKeyScript, BITCOIN_FUNDING_SPENT, knownSpendingTxs) + blockchain ! WatchSpent(self, commitments.commitInput.outPoint.txid, commitments.commitInput.outPoint.index.toInt, BITCOIN_FUNDING_SPENT, knownSpendingTxs) // TODO: implement this? (not needed if we use a reasonable min_depth) //blockchain ! WatchLost(self, commitments.commitInput.outPoint.txid, nodeParams.minDepthBlocks, BITCOIN_FUNDING_LOST) } @@ -2143,7 +2143,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId private def doPublish(closingTx: ClosingTx): Unit = { blockchain ! PublishAsap(closingTx.tx, PublishStrategy.JustPublish) - blockchain ! WatchConfirmed(self, closingTx.tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(closingTx.tx)) + blockchain ! WatchConfirmed(self, closingTx.tx.txid, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(closingTx.tx)) } private def spendLocalCurrent(d: HasCommitments) = { @@ -2185,7 +2185,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId */ private def watchConfirmedIfNeeded(txs: Iterable[Transaction], irrevocablySpent: Map[OutPoint, Transaction]): Unit = { val (skip, process) = txs.partition(Closing.inputsAlreadySpent(_, irrevocablySpent)) - process.foreach(tx => blockchain ! WatchConfirmed(self, tx, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx))) + process.foreach(tx => blockchain ! WatchConfirmed(self, tx.txid, nodeParams.minDepthBlocks, BITCOIN_TX_CONFIRMED(tx))) skip.foreach(tx => log.info(s"no need to watch txid=${tx.txid}, it has already been confirmed")) } @@ -2200,7 +2200,7 @@ class Channel(val nodeParams: NodeParams, val wallet: EclairWallet, remoteNodeId require(output.txid == parentTx.txid && output.index < parentTx.txOut.size, s"output doesn't belong to the given parentTx: output=${output.txid}:${output.index} (expected txid=${parentTx.txid} index < ${parentTx.txOut.size})") } val (skip, process) = outputs.partition(irrevocablySpent.contains) - process.foreach(output => blockchain ! WatchSpent(self, parentTx, output.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set.empty)) + process.foreach(output => blockchain ! WatchSpent(self, parentTx.txid, output.index.toInt, BITCOIN_OUTPUT_SPENT, hints = Set.empty)) skip.foreach(output => log.info(s"no need to watch output=${output.txid}:${output.index}, it has already been spent by txid=${irrevocablySpent.get(output).map(_.txid)}")) } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala index 6c275d6fb0..9c443500ac 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Router.scala @@ -16,14 +16,11 @@ package fr.acinq.eclair.router -import java.util.UUID - import akka.Done import akka.actor.{Actor, ActorLogging, ActorRef, Props, Terminated} import akka.event.DiagnosticLoggingAdapter import akka.event.Logging.MDC import fr.acinq.bitcoin.Crypto.PublicKey -import fr.acinq.bitcoin.Script.{pay2wsh, write} import fr.acinq.bitcoin.{ByteVector32, Satoshi} import fr.acinq.eclair.Logs.LogCategory import fr.acinq.eclair._ @@ -37,10 +34,10 @@ import fr.acinq.eclair.remote.EclairInternalsSerializer.RemoteTypes import fr.acinq.eclair.router.Graph.GraphStructure.DirectedGraph import fr.acinq.eclair.router.Graph.WeightRatios import fr.acinq.eclair.router.Monitoring.{Metrics, Tags} -import fr.acinq.eclair.transactions.Scripts import fr.acinq.eclair.wire.protocol._ import kamon.context.Context +import java.util.UUID import scala.collection.immutable.SortedMap import scala.concurrent.duration._ import scala.concurrent.{ExecutionContext, Promise} @@ -89,10 +86,9 @@ class Router(val nodeParams: NodeParams, watcher: ActorRef, initialized: Option[ initChannels.values.foreach { pc => val txid = pc.fundingTxid val TxCoordinates(_, _, outputIndex) = ShortChannelId.coordinates(pc.ann.shortChannelId) - val fundingOutputScript = write(pay2wsh(Scripts.multiSig2of2(pc.ann.bitcoinKey1, pc.ann.bitcoinKey2))) // avoid herd effect at startup because watch-spent are intensive in terms of rpc calls to bitcoind context.system.scheduler.scheduleOnce(Random.nextLong(nodeParams.watchSpentWindow.toSeconds).seconds) { - watcher ! WatchSpentBasic(self, txid, outputIndex, fundingOutputScript, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(pc.ann.shortChannelId)) + watcher ! WatchSpentBasic(self, txid, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(pc.ann.shortChannelId)) } } diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala index 3b30025fa3..e4da951f10 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/router/Validation.scala @@ -103,7 +103,7 @@ object Validation { remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.InvalidAnnouncement(c)))) None } else { - watcher ! WatchSpentBasic(ctx.self, tx, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) + watcher ! WatchSpentBasic(ctx.self, tx.txid, outputIndex, BITCOIN_FUNDING_EXTERNAL_CHANNEL_SPENT(c.shortChannelId)) log.debug("added channel channelId={}", c.shortChannelId) remoteOrigins_opt.foreach(_.foreach(o => sendDecision(o.peerConnection, GossipDecision.Accepted(c)))) val capacity = tx.txOut(outputIndex).amount diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala index a6567117ab..2508191290 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/WatcherSpec.scala @@ -18,28 +18,11 @@ package fr.acinq.eclair.blockchain import fr.acinq.bitcoin.Crypto.{PrivateKey, PublicKey} import fr.acinq.bitcoin.{OutPoint, SIGHASH_ALL, Satoshi, SatoshiLong, Script, ScriptFlags, ScriptWitness, SigVersion, Transaction, TxIn, TxOut} -import org.scalatest.funsuite.AnyFunSuiteLike /** * Created by PM on 27/01/2017. */ -class WatcherSpec extends AnyFunSuiteLike { - - test("extract pay2wpkh pubkey script") { - val commitTx = Transaction.read("020000000001010ba75314a116c1e585d1454d079598c5f00edc8a21ebd9e4f3b64e5c318ff2a30100000000e832a680012e850100000000001600147d2a3fc37dba8e946e0238d7eeb6fb602be658200400473044022010d4f249861bb9828ddfd2cda91dc10b8f8ffd0f15c8a4a85a2d373d52f5e0ff02205356242878121676e3e823ceb3dc075d18fed015053badc8f8d754b8959a9178014730440220521002cf241311facf541b689e7229977bfceffa0e4ded785b4e6197af80bfa202204a168d1f7ee59c73ae09c3e0a854b20262b9969fe4ed69b15796dca3ea286582014752210365375134360808be0b4756ba8a2995488310ac4c69571f2b600aaba3ec6cc2d32103a0d9c18794f16dfe01d6d6716bcd1e97ecff2f39451ec48e1899af40f20a18bc52aec3dd9520") - val claimMainTx = Transaction.read("020000000001012537488e9d066a8f3550cc9adc141a11668425e046e69e07f53bb831f3296cbf00000000000000000001bf8401000000000017a9143f398d81d3c42367b779ea869c7dd3b6826fbb7487024730440220477b961f6360ef6cb62a76898dcecbb130627c7e6a452646e3be601f04627c1f02202572313d0c0afecbfb0c7d0e47ba689427a54f3debaded6d406daa1f5da4918c01210291ed78158810ad867465377f5920036ea865a29b3a39a1b1808d0c3c351a4b4100000000") - assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainTx.txIn.head.witness)) - } - - test("extract pay2wsh pubkey script") { - val commitTx = Transaction.read("02000000000101fb98507ff5f47bcc5b4497a145e631f68b2b5fcf2752598bc54c8f33696e1c73000000000017f15b80015b3f0f0000000000220020345fc26988f6252d9d93ee95f2198e820db1a4d7c7ec557e4cc5d7e60750cc21040047304402202fd9cbc8446a10193f378269bf12d321aa972743c0a011089aff522de2a1414d02204dd65bf43e41fe911c7180e5e036d609646a798fa5c3f288ede73679978df36b01483045022100fced8966c2527cb175521c4eb41aaaee96838420fa5fce3d4730c0da37f6253502202dc9667530a9f79bc6444b54335467d2043c4b996da5fbca7496e0fa64ccc1bd0147522103a16c06d8626bad5d6d8ea8fee980c287590b9dedeb5857a3d0cd6c4b4e95631c2103d872e26e43f723523d2d8eff5f93a1b344fe51eb76bcfd4906315ae2fe35389a52ae620acc20") - val claimMainDelayedTx = Transaction.read("02000000000101b285ffeb84c366f621fe33b6ff77a9b7578075b65e69c363d12c35aa422d98fd00000000009000000001e03e0f000000000017a9147407522166f1ed3030788b1b6a48803867d1797f8703483045022100fe9eefd010a80411ccae87590db3f54c1c04605170bdcd83c1e04222d474ef41022036db7fd3c07c0523c2cf72d80c7fe3bdc2d5028a8bc2864b478a707e8af627dc01004d63210298f7dada89d882c4ab971e7e914f4953249bad70333b29aa504bb67e5ce9239c67029000b275210328170f7e781c70ea679efc30383d3e03451ca350e2a8690f8ed3db9dabb3866768ac00000000") - assert(commitTx.txOut.head.publicKeyScript === WatchConfirmed.extractPublicKeyScript(claimMainDelayedTx.txIn.head.witness)) - } - -} - object WatcherSpec { /** diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index f804643ffd..39177426ab 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -121,11 +121,11 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val outputIndex = 42 val utxo = OutPoint(txid.reverse, outputIndex) - val w1 = WatchSpent(null, txid, outputIndex, randomBytes32, BITCOIN_FUNDING_SPENT, hints = Set.empty) - val w2 = WatchSpent(null, txid, outputIndex, randomBytes32, BITCOIN_FUNDING_SPENT, hints = Set.empty) - val w3 = WatchSpentBasic(null, txid, outputIndex, randomBytes32, BITCOIN_FUNDING_SPENT) - val w4 = WatchSpentBasic(null, randomBytes32, 5, randomBytes32, BITCOIN_FUNDING_SPENT) - val w5 = WatchConfirmed(null, txid, randomBytes32, 3, BITCOIN_FUNDING_SPENT) + val w1 = WatchSpent(null, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) + val w2 = WatchSpent(null, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) + val w3 = WatchSpentBasic(null, txid, outputIndex, BITCOIN_FUNDING_SPENT) + val w4 = WatchSpentBasic(null, randomBytes32, 5, BITCOIN_FUNDING_SPENT) + val w5 = WatchConfirmed(null, txid, 3, BITCOIN_FUNDING_SPENT) // we test as if the collection was immutable val m1 = addWatchedUtxos(m0, w1) @@ -158,14 +158,14 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val tx = sendToAddress(address, Btc(1), probe) val listener = TestProbe() - probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) - probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) // setting the watch multiple times should be a no-op + probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, 4, BITCOIN_FUNDING_DEPTHOK)) + probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, 4, BITCOIN_FUNDING_DEPTHOK)) // setting the watch multiple times should be a no-op generateBlocks(5) assert(listener.expectMsgType[WatchEventConfirmed].tx.txid === tx.txid) listener.expectNoMsg(1 second) // If we try to watch a transaction that has already been confirmed, we should immediately receive a WatchEventConfirmed. - probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, tx.txOut.head.publicKeyScript, 4, BITCOIN_FUNDING_DEPTHOK)) + probe.send(watcher, WatchConfirmed(listener.ref, tx.txid, 4, BITCOIN_FUNDING_DEPTHOK)) assert(listener.expectMsgType[WatchEventConfirmed].tx.txid === tx.txid) listener.expectNoMsg(1 second) }) @@ -182,8 +182,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val (tx1, tx2) = createUnspentTxChain(tx, priv) val listener = TestProbe() - probe.send(watcher, WatchSpentBasic(listener.ref, tx, outputIndex, BITCOIN_FUNDING_SPENT)) - probe.send(watcher, WatchSpent(listener.ref, tx, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty)) + probe.send(watcher, WatchSpentBasic(listener.ref, tx.txid, outputIndex, BITCOIN_FUNDING_SPENT)) + probe.send(watcher, WatchSpent(listener.ref, tx.txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty)) listener.expectNoMsg(1 second) bitcoinClient.publishTransaction(tx1).pipeTo(probe.ref) probe.expectMsg(tx1.txid) @@ -202,19 +202,19 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind probe.expectMsg(tx2.txid) listener.expectNoMsg(1 second) generateBlocks(1) - probe.send(watcher, WatchSpentBasic(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT)) - probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) + probe.send(watcher, WatchSpentBasic(listener.ref, tx1.txid, 0, BITCOIN_FUNDING_SPENT)) + probe.send(watcher, WatchSpent(listener.ref, tx1.txid, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) listener.expectMsgAllOf( WatchEventSpentBasic(BITCOIN_FUNDING_SPENT), WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2) ) // We use hints and see if we can find tx2 - probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT, hints = Set(tx2.txid))) + probe.send(watcher, WatchSpent(listener.ref, tx1.txid, 0, BITCOIN_FUNDING_SPENT, hints = Set(tx2.txid))) listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2)) // We should still find tx2 if the provided hint is wrong - probe.send(watcher, WatchSpent(listener.ref, tx1, 0, BITCOIN_FUNDING_SPENT, hints = Set(randomBytes32))) + probe.send(watcher, WatchSpent(listener.ref, tx1.txid, 0, BITCOIN_FUNDING_SPENT, hints = Set(randomBytes32))) listener.expectMsg(WatchEventSpent(BITCOIN_FUNDING_SPENT, tx2)) }) } @@ -235,8 +235,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, 1, 0) // setup watches before we publish transactions - probe.send(watcher, WatchSpent(probe.ref, tx1, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty)) - probe.send(watcher, WatchConfirmed(probe.ref, tx1, 3, BITCOIN_FUNDING_SPENT)) + probe.send(watcher, WatchSpent(probe.ref, tx1.txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty)) + probe.send(watcher, WatchConfirmed(probe.ref, tx1.txid, 3, BITCOIN_FUNDING_SPENT)) bitcoinClient.publishTransaction(tx1).pipeTo(probe.ref) probe.expectMsg(tx1.txid) generateBlocks(1) @@ -281,7 +281,7 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // tx2 has a relative delay but no absolute delay val tx2 = createSpendP2WPKH(tx1, priv, priv.publicKey, 10000 sat, sequence = 2, lockTime = 0) - probe.send(watcher, WatchConfirmed(probe.ref, tx1, 1, BITCOIN_FUNDING_DEPTHOK)) + probe.send(watcher, WatchConfirmed(probe.ref, tx1.txid, 1, BITCOIN_FUNDING_DEPTHOK)) probe.send(watcher, PublishAsap(tx2, PublishStrategy.JustPublish)) generateBlocks(1) assert(probe.expectMsgType[WatchEventConfirmed].tx === tx1) @@ -293,8 +293,8 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind // tx3 has both relative and absolute delays val tx3 = createSpendP2WPKH(tx2, priv, priv.publicKey, 10000 sat, sequence = 1, lockTime = blockCount.get + 5) - probe.send(watcher, WatchConfirmed(probe.ref, tx2, 1, BITCOIN_FUNDING_DEPTHOK)) - probe.send(watcher, WatchSpent(probe.ref, tx2, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) + probe.send(watcher, WatchConfirmed(probe.ref, tx2.txid, 1, BITCOIN_FUNDING_DEPTHOK)) + probe.send(watcher, WatchSpent(probe.ref, tx2.txid, 0, BITCOIN_FUNDING_SPENT, hints = Set.empty)) probe.send(watcher, PublishAsap(tx3, PublishStrategy.JustPublish)) generateBlocks(1) assert(probe.expectMsgType[WatchEventConfirmed].tx === tx2) From 9771ce6d4868ac7955f6d338dc9f70779d523f88 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 1 Apr 2021 17:52:32 +0200 Subject: [PATCH 3/4] Remove watcher type and fix test --- eclair-core/src/main/resources/reference.conf | 1 - .../src/main/scala/fr/acinq/eclair/NodeParams.scala | 6 ------ .../src/test/scala/fr/acinq/eclair/TestConstants.scala | 3 --- .../eclair/blockchain/bitcoind/ZmqWatcherSpec.scala | 10 +++++----- 4 files changed, 5 insertions(+), 15 deletions(-) diff --git a/eclair-core/src/main/resources/reference.conf b/eclair-core/src/main/resources/reference.conf index 1c7ada7ad2..5b33da5b92 100644 --- a/eclair-core/src/main/resources/reference.conf +++ b/eclair-core/src/main/resources/reference.conf @@ -20,7 +20,6 @@ eclair { // override this with a script/exe that will be called everytime a new database backup has been created # backup-notify-script = "/absolute/path/to/script.sh" - watcher-type = "bitcoind" watch-spent-window = 1 minute // at startup watches will be put back within that window to reduce herd effect; must be > 0s bitcoind { diff --git a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala index 8fb112e5d1..017fda8969 100644 --- a/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala +++ b/eclair-core/src/main/scala/fr/acinq/eclair/NodeParams.scala @@ -19,7 +19,6 @@ package fr.acinq.eclair import com.typesafe.config.{Config, ConfigFactory, ConfigValueType} import fr.acinq.bitcoin.Crypto.PublicKey import fr.acinq.bitcoin.{Block, ByteVector32, Crypto, Satoshi} -import fr.acinq.eclair.NodeParams.WatcherType import fr.acinq.eclair.Setup.Seeds import fr.acinq.eclair.blockchain.fee._ import fr.acinq.eclair.channel.Channel @@ -79,7 +78,6 @@ case class NodeParams(nodeKeyManager: NodeKeyManager, maxReconnectInterval: FiniteDuration, chainHash: ByteVector32, channelFlags: Byte, - watcherType: WatcherType, watchSpentWindow: FiniteDuration, paymentRequestExpiry: FiniteDuration, multiPartPaymentExpiry: FiniteDuration, @@ -213,9 +211,6 @@ object NodeParams extends Logging { val color = ByteVector.fromValidHex(config.getString("node-color")) require(color.size == 3, "color should be a 3-bytes hex buffer") - require(config.getString("watcher-type") == "bitcoind", s"watcher-type `${config.getString("watcher-type")}` is not supported: `bitcoind` should be used") - val watcherType = BITCOIND - val watchSpentWindow = FiniteDuration(config.getDuration("watch-spent-window").getSeconds, TimeUnit.SECONDS) require(watchSpentWindow > 0.seconds, "watch-spent-window must be strictly greater than 0") @@ -361,7 +356,6 @@ object NodeParams extends Logging { maxReconnectInterval = FiniteDuration(config.getDuration("max-reconnect-interval").getSeconds, TimeUnit.SECONDS), chainHash = chainHash, channelFlags = config.getInt("channel-flags").toByte, - watcherType = watcherType, watchSpentWindow = watchSpentWindow, paymentRequestExpiry = FiniteDuration(config.getDuration("payment-request-expiry").getSeconds, TimeUnit.SECONDS), multiPartPaymentExpiry = FiniteDuration(config.getDuration("multi-part-payment-expiry").getSeconds, TimeUnit.SECONDS), diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala index a22856a89c..6332ae165d 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/TestConstants.scala @@ -20,7 +20,6 @@ import fr.acinq.bitcoin.Crypto.PrivateKey import fr.acinq.bitcoin.{Block, ByteVector32, Satoshi, SatoshiLong, Script} import fr.acinq.eclair.FeatureSupport.Optional import fr.acinq.eclair.Features._ -import fr.acinq.eclair.NodeParams.BITCOIND import fr.acinq.eclair.blockchain.fee.{FeeEstimator, FeeTargets, FeeratesPerKw, OnChainFeeConf, _} import fr.acinq.eclair.channel.LocalParams import fr.acinq.eclair.crypto.keymanager.{LocalChannelKeyManager, LocalNodeKeyManager} @@ -128,7 +127,6 @@ object TestConstants { maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, channelFlags = 1, - watcherType = BITCOIND, watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, @@ -233,7 +231,6 @@ object TestConstants { maxReconnectInterval = 1 hour, chainHash = Block.RegtestGenesisBlock.hash, channelFlags = 1, - watcherType = BITCOIND, watchSpentWindow = 1 second, paymentRequestExpiry = 1 hour, multiPartPaymentExpiry = 30 seconds, diff --git a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala index 39177426ab..1962c913be 100644 --- a/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala +++ b/eclair-core/src/test/scala/fr/acinq/eclair/blockchain/bitcoind/ZmqWatcherSpec.scala @@ -121,11 +121,11 @@ class ZmqWatcherSpec extends TestKitBaseClass with AnyFunSuiteLike with Bitcoind val outputIndex = 42 val utxo = OutPoint(txid.reverse, outputIndex) - val w1 = WatchSpent(null, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) - val w2 = WatchSpent(null, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) - val w3 = WatchSpentBasic(null, txid, outputIndex, BITCOIN_FUNDING_SPENT) - val w4 = WatchSpentBasic(null, randomBytes32, 5, BITCOIN_FUNDING_SPENT) - val w5 = WatchConfirmed(null, txid, 3, BITCOIN_FUNDING_SPENT) + val w1 = WatchSpent(TestProbe().ref, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) + val w2 = WatchSpent(TestProbe().ref, txid, outputIndex, BITCOIN_FUNDING_SPENT, hints = Set.empty) + val w3 = WatchSpentBasic(TestProbe().ref, txid, outputIndex, BITCOIN_FUNDING_SPENT) + val w4 = WatchSpentBasic(TestProbe().ref, randomBytes32, 5, BITCOIN_FUNDING_SPENT) + val w5 = WatchConfirmed(TestProbe().ref, txid, 3, BITCOIN_FUNDING_SPENT) // we test as if the collection was immutable val m1 = addWatchedUtxos(m0, w1) From 6d38952f199e6bbec9c3be2718c4667f08248119 Mon Sep 17 00:00:00 2001 From: t-bast Date: Thu, 1 Apr 2021 17:58:11 +0200 Subject: [PATCH 4/4] Fix GUI --- .../fr/acinq/eclair/gui/controllers/MainController.scala | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala index ef32b2f179..ecc37a1581 100644 --- a/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala +++ b/eclair-node-gui/src/main/scala/fr/acinq/eclair/gui/controllers/MainController.scala @@ -359,10 +359,7 @@ class MainController(val handlers: Handlers, val hostServices: HostServices) ext labelApi.setText(s"${setup.config.getInt("api.port")}") labelServer.setText(s"${setup.config.getInt("server.port")}") - val wallet = setup.nodeParams.watcherType match { - case BITCOIND => "Bitcoin-core" - } - bitcoinWallet.setText(wallet) + bitcoinWallet.setText("Bitcoin-core") bitcoinChain.setText(s"${setup.chain.toUpperCase()}") bitcoinChain.getStyleClass.add(setup.chain)