Compare commits

...

241 Commits

Author SHA1 Message Date
Janus
7664855e44 ln: add was_announced in test_lnhtlc 2018-06-20 15:46:41 +02:00
Janus
705b138f21 ln: close channels 2018-06-20 15:46:22 +02:00
Janus
1f46df9af5 ln: don't corrupt channels storage when multiple funding_locked are received 2018-06-20 10:48:31 +02:00
Janus
b668071310 ln: don't break channel when failing htlc 2018-06-19 15:13:48 +02:00
Janus
c096acddaa ln: announcement reliability fixes for qt, remove asserts forbidding unbalanced channels 2018-06-19 13:02:59 +02:00
Janus
3df8372956 ln: begin handling htlc failures 2018-06-18 19:46:44 +02:00
SomberNight
3f71a7e1f2
add minor comment for RouteEdge as clarification 2018-06-18 19:18:39 +02:00
SomberNight
1838d17713
LNPathFinder: cltv delta of first edge in a path should be ignored 2018-06-18 18:32:06 +02:00
Janus
84b9438670 ln: channel announcements 2018-06-18 15:34:18 +02:00
Janus
618b5a7988 ln: lnpay: revoke until we get a commitment tx without htlcs 2018-06-15 18:51:49 +02:00
Janus
7bfca9a951 ln: fix reestablishing channel with no mined funding tx 2018-06-15 18:36:41 +02:00
Janus
5794da2a2d ln: fix repeated payments 2018-06-15 17:04:34 +02:00
Janus
f761e0816c ln: avoid code duplication 2018-06-15 16:35:29 +02:00
Janus
31315c8a9b ln: save remote's secrets in RevocationStore, not our secrets. call lnhtlc.receive_revocation 2018-06-14 15:34:51 +02:00
Janus
6d808c755b lnbase/lnhtlc: use lnhtlc more instead of manually constructing tx'es 2018-06-14 14:45:36 +02:00
Janus
bf35aad40c lnbase: use sign_next_commitment for initial remote_ctx 2018-06-13 16:28:07 +02:00
Janus
4c24ed46c2 daemon: prevent json-rpc-pelix from suppressing stack traces of TypeErrors 2018-06-13 16:28:07 +02:00
Janus
1d7c332859 lnbase: use lnhtlc when verifying our initial commitment tx 2018-06-13 16:28:07 +02:00
Janus
65ec33bda3 lnbase: use broadcast_transaction instead of broadcast (follow up e57e55aad) 2018-06-13 16:28:07 +02:00
Janus
f2d651e909 test_lnbase: use new Peer API (with lnworker) 2018-06-13 16:28:07 +02:00
Janus
1b6a14875a ln: remove unneeded forwarding htlc features, check commitment sig using lnhtlc while receiving 2018-06-13 16:28:07 +02:00
Janus
d17ec4d7e3 ln: integrate lnhtlc in lnbase, fix multiple lnhtlc bugs 2018-06-13 16:28:07 +02:00
Janus
7cff6443a0 ln: remove lnbase global flag 2018-06-13 16:28:07 +02:00
Janus
06129d3dfd ln: request_initial_sync, increase our max_htlc_value, fix receiving payment 2018-06-13 16:28:07 +02:00
ThomasV
3dfb4cdbb6 do not block GUI with open_channel 2018-06-13 16:28:07 +02:00
ThomasV
91256c3e3c lightning: display remote balance in gui 2018-06-13 16:28:07 +02:00
ThomasV
c94f5a8563 lnbase: mark_open on startup 2018-06-13 16:28:07 +02:00
ThomasV
2edac7c6f4 revert the introduction of add_invoice_coroutine in a612c2b09 2018-06-13 16:28:07 +02:00
ThomasV
1d2a6c3092 do not pass channel list to update_rows signal, as it is sent to all windows 2018-06-13 16:28:07 +02:00
SomberNight
1433fdc842 wait for peer.initialized in channel_establishment_flow 2018-06-13 16:28:07 +02:00
ThomasV
25b9d065b2 follow up 0b3a882e7d57c8a42be48c491a46dc814eab6acb 2018-06-13 16:28:07 +02:00
ThomasV
4c7ca381a8 simplify funding_locked
expose lnworker in peer
update channel_db when channels are open
2018-06-13 16:28:07 +02:00
ThomasV
f3357787b9 Display channel status in the GUI.
Do not convert channel_id to integer; there is no reason to do that.
2018-06-13 16:28:07 +02:00
ThomasV
6a388a571e integrate channels_list with existing framework 2018-06-13 16:28:07 +02:00
ThomasV
dd0aded003 request list: remove Type column 2018-06-13 16:28:07 +02:00
ThomasV
209fd1faa6 qt: fix unit of lnaddr.amount 2018-06-13 16:28:07 +02:00
ThomasV
58cdabb031 follow-up a612c2b0983ab4c6798156aebf1cd550fb3e0447 2018-06-13 16:28:07 +02:00
Janus
0da069f631 ln: htlc state machine (not used yet) 2018-06-13 16:28:07 +02:00
Janus
f78f28b47c ln: save channels in dict, warn on invoice exceeding max_htlc amount 2018-06-13 16:28:07 +02:00
ThomasV
8f9397a18f lightning: connect send button 2018-06-13 16:28:07 +02:00
ThomasV
5979dcc293 lightning GUI: use existing receive and send tabs with lightning invoices 2018-06-13 16:28:07 +02:00
Janus
c9deed8694 ln: don't make invoice if peer can't possibly pay, append _sat to sat
parameters to avoid confusion
2018-06-13 16:28:07 +02:00
ThomasV
6eaf09fe94 lnworker: generate and save private key 2018-06-13 16:28:07 +02:00
ThomasV
c8401218be follow up previous commit 2018-06-13 16:28:07 +02:00
ThomasV
586ac743be lnworker: separate invoice creation from payment flow 2018-06-13 16:28:07 +02:00
Janus
044c253b5d ln: restore channels correctly after restart
* save funding_locked_received: if a node already sent us
funding_locked, save it to avoid superfluous messages

* use Queues instead of Futures: this ensure that we don't error if we
receive two messages of the same type, and in avoids having to delete
futures in finally blocks. A queue monitor could be added to detect
queue elements that are not popped.

* request initial routing sync: since we don't store the graph yet, it
is better to request the graph from the Peer so that we can route

* channel_state cleanup: now each channel should have a state, which is
initialized to OPENING and only marked OPEN once we have verified that
the funding_tx has been mined
2018-06-13 16:28:07 +02:00
ThomasV
39ec22d033 fix channel_reestablish 2018-06-13 16:28:07 +02:00
ThomasV
ccf2cf23b6 lnbase: fix read_message 2018-06-13 16:28:07 +02:00
Janus
da411bcb1e ln: restore functionality 2018-06-13 16:28:07 +02:00
Janus
f7b877f10d ln: save remote node_id in channel 2018-06-13 16:28:07 +02:00
SomberNight
1f06f4c439 split lnrouter from lnbase 2018-06-13 16:28:07 +02:00
SomberNight
47c9a0692c remove function H256 2018-06-13 16:28:07 +02:00
ThomasV
6f70cb02f3 fix amount in open_channel, add listchannels command 2018-06-13 16:28:07 +02:00
ThomasV
110b7b2933 move on_funding_locked to lnworker 2018-06-13 16:28:07 +02:00
ThomasV
f14bca737d lightning: add payment methods to lnworker 2018-06-13 16:28:07 +02:00
ThomasV
06c96b6f31 lightning: move lnworker code to its own module 2018-06-13 16:28:07 +02:00
ThomasV
98b93b75de fix lnaddr.py following rebase 2018-06-13 16:28:07 +02:00
ThomasV
8274f54539 update lnbase after crypto refactoring 2018-06-13 16:28:07 +02:00
Janus
71e1c95cc6 lnbase: remove lnbase stub 2018-06-13 16:28:06 +02:00
ThomasV
c6ea53542a lnbase: pass password to mktx 2018-06-13 16:28:06 +02:00
ThomasV
8860b8d9ac qt: fix password passed to open_channel, cleanup 2018-06-13 16:28:06 +02:00
Janus
3680658256 lnbase: mSAT hygiene, multiple multi-hop payments can be received 2018-06-13 16:28:06 +02:00
Janus
15e9a4c117 kivy: port lightning ui to lnbase 2018-06-13 16:28:06 +02:00
Janus
7b8735200f lightning: remove hub based approach, port qt gui to lnbase 2018-06-13 16:28:06 +02:00
Janus
de69ef7921 lnbase: use small buffer when reading, support new_channel without payment in online test, send channel_reserve_satoshis 2018-06-13 16:28:06 +02:00
Janus
15104e04a4 lnbase: use correct cltv_expiry calculation (use invoice) 2018-06-13 16:28:06 +02:00
Janus
84d019dfa3 lnbase: fix multi-hop payments 2018-06-13 16:28:06 +02:00
Janus
50461291a2 lnbase: fix onion-hop payload construction again (cltv currently broken) 2018-06-13 16:28:06 +02:00
Janus
3241b38007 lnbase: fix multi-hop fees, initial handling of received update_add_htlc during payment 2018-06-13 16:28:06 +02:00
Janus
f1fc357cf0 lnbase: calculate cltv_expiry for onion_packet correctly 2018-06-13 16:28:06 +02:00
Janus
164492b4bf lnbase: try multi-hop onion package, type safety 2018-06-13 16:28:06 +02:00
SomberNight
60a5dc0a6a PathFinder: change path element semantics from "from node, take edge" to "to get to node, use edge" 2018-06-13 16:28:06 +02:00
SomberNight
34540f43b1 create route from path, that includes extra info needed for routing 2018-06-13 16:28:06 +02:00
SomberNight
fd887ddd70 bolt-04: decryption of errors 2018-06-13 16:28:06 +02:00
Janus
17826daa8c lnbase online test: use random node key when making new channel, save node key, multiple actions per invocation 2018-06-13 16:28:06 +02:00
Janus
b7f24b816f lnbase: fix pay(), save htlc_id's, generate onion packet correctly 2018-06-13 16:28:06 +02:00
Janus
e26cdb266a lnbase/online_test: save short_channel_id to wallet and build onion packet with it 2018-06-13 16:28:06 +02:00
SomberNight
cfb59a62b0 calc short_channel_id after funding locked 2018-06-13 16:28:06 +02:00
Janus
1a69f909a8 lnbase: initial 'payment to remote' attempt 2018-06-13 16:28:06 +02:00
Janus
99dcf51ac3 lnbase: formatting, remove imports 2018-06-13 16:28:06 +02:00
Janus
5ef939ae4e lnbase: verify commitment tx'es again 2018-06-13 16:28:06 +02:00
Janus
ed60324555 lnbase: infinite amount of incoming payments 2018-06-13 16:28:06 +02:00
Janus
762689d2b3 lnbase: two payments working, temporarily disable sig check 2018-06-13 16:28:06 +02:00
Janus
81ad6282e0 lnbase: store remote revocation store, don't store all remote revocation points, verify ctn numbers in reestablish 2018-06-13 16:28:06 +02:00
Janus
7e179cfc01 lnbase: add RevocationStore test, remove unnecessary lnd helper functions 2018-06-13 16:28:06 +02:00
Janus
37e85111b0 lnbase: compact commitment secret storage 2018-06-13 16:28:06 +02:00
Janus
35bfe4f733 lnbase: no negative commitment number nonsense 2018-06-13 16:28:06 +02:00
Janus
27455687c2 lnbase: move channel commitment number increment to function 2018-06-13 16:28:06 +02:00
Janus
0bdce553f4 lnbase: receive repeated payments 2018-06-13 16:28:06 +02:00
Janus
338b4f3fe6 tests: don't use default lightning_peers in online test 2018-06-13 16:28:06 +02:00
Janus
8f97ea9840 lnbase: channel reestablishment working 2018-06-13 16:28:06 +02:00
SomberNight
7f99293e1e bolt-04: implement processing of onion packets 2018-06-13 16:28:06 +02:00
SomberNight
809d42fe60 minor clean-up of prev. util.xor_bytes 2018-06-13 16:28:06 +02:00
SomberNight
362fcb916f implement bolt-04 onion packet construction 2018-06-13 16:28:06 +02:00
Janus
4071b123cd lnbase: save channel details in wallet, enable running online test with reestablishment_mode 2018-06-13 16:28:06 +02:00
Janus
b26cc47756 lnbase: move waiting for funding_locked to new function, make function for signing and sig conversion 2018-06-13 16:28:06 +02:00
Janus
7cfd9f5e25 lnbase: make function for building htlc_tx depending on if it is for_us/we_receive 2018-06-13 16:28:06 +02:00
Janus
fef70d594e lnbase: verify their htlc signature 2018-06-13 16:28:06 +02:00
ThomasV
b922620d45 lnbase: standardize to_bytes calls 2018-06-13 16:28:06 +02:00
Janus
4bb20f4a02 lnbase: fix custom local to_self_delay, use node privkey derived from timestamp in online test 2018-06-13 16:28:06 +02:00
Janus
d9ca086290 test_lnbase_online: pass password=None to channel_establishment_flow 2018-06-13 16:28:06 +02:00
Janus
ecd5416c30 lnbase: avoid copying variables, insert newlines 2018-06-13 16:28:06 +02:00
ThomasV
e679f51012 lnbase: derive keys from wallet keystore 2018-06-13 16:28:06 +02:00
Janus
8af3166340 lnbase: avoid local variables, remote useless comments, name basepoints as such 2018-06-13 16:28:06 +02:00
Janus
5ce2ff0d9b lnbase: set new field in Transaction instead of returning a tuple in make_commitment 2018-06-13 16:28:06 +02:00
Janus
7f753fe29b lnbase: set to_self_delay back to 144, defer cltv_expiry problem 2018-06-13 16:28:06 +02:00
Janus
c03358f720 lnbase: use correct delay 2018-06-13 16:28:06 +02:00
Janus
fc4d1523c1 lnbase: avoid code duplication, return htlc outpoint dict in make_commitment 2018-06-13 16:28:06 +02:00
Janus
46eb66564a lnbase: simplify commitment transaction building with open channel 2018-06-13 16:28:06 +02:00
Janus
631282b547 lnbase: organize channel data 2018-06-13 16:28:06 +02:00
Janus
41840fd727 lnbase: allow passing KeypairGenerator to channel_establishment_flow, fix derive_privkey 2018-06-13 16:28:06 +02:00
Janus
2ae2771f9d lnbase: receiving invoice payment works 2018-06-13 16:28:06 +02:00
Janus
8a7c440203 lnbase: commitment_signed, revoke_and_ack now accepted without errors 2018-06-13 16:28:06 +02:00
SomberNight
1743280687 bitcoin.py: SCRIPT-related clean-up. transaction.py: construct_witness 2018-06-13 16:28:06 +02:00
Janus
34fb2162c7 lnbase: fix their new commitment transaction (htlc tx construction still incorrect) 2018-06-13 16:28:06 +02:00
ThomasV
455a0e8d36 lnbase: fix bug in message parsing 2018-06-13 16:28:06 +02:00
ThomasV
ad6c3f017b follow up b5eb7dd7683f24f03c80ab8f612658b5f3966eb1 2018-06-13 16:28:06 +02:00
Janus
b0dc461125 lnbase: attempt at making htlc_signature to send (currently remote fails due to wrong num_htlcs in commitment_signed) 2018-06-13 16:28:06 +02:00
ThomasV
9e81956f9b simplification 2018-06-13 16:28:06 +02:00
Janus
1b677c2809 lnbase: add TODO explaining how to verify htlc_signature given to us 2018-06-13 16:28:06 +02:00
Janus
6edfb94596 lnbase: verification of new local commitment working 2018-06-13 16:28:06 +02:00
Janus
8ac2e0cb9f lnbase: derive next keys when making updated local commitment transaction 2018-06-13 16:28:06 +02:00
Janus
3cfcc422ec lnbase: try to receive payment, work on commitment tx with htlcs 2018-06-13 16:28:06 +02:00
Janus
a03df2acef lnbase: handle commitment transaction update (receive funds, not working yet) 2018-06-13 16:28:06 +02:00
Janus
9cf341673c simnet/testnet support in bolt11, set max-htlc-value-in-flight 2018-06-13 16:28:06 +02:00
SomberNight
e47a4a715d transaction.py: sign_txin. allow override for get_preimage_script.
test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate now passes
2018-06-13 16:28:06 +02:00
ThomasV
12df2a498c redundant: you subscribed only to 'updated' 2018-06-13 16:28:06 +02:00
ThomasV
87fbc6b6b5 follow-up prev commit 2018-06-13 16:28:06 +02:00
ThomasV
22a1e6e6d6 lnbase: verify remote signature 2018-06-13 16:28:06 +02:00
SomberNight
4776aa0dc9 channel_establishment_flow: use get_per_commitment_secret_from_seed 2018-06-13 16:28:06 +02:00
Janus
b366d8e8b5 lnbase: improve htlc_tx generation (only localsig wrong) 2018-06-13 16:28:06 +02:00
SomberNight
b6863653cf get_per_commitment_secret_from_seed: small clean-up 2018-06-13 16:28:06 +02:00
Janus
ec3068cb33 fix derive_secret 2018-06-13 16:28:06 +02:00
SomberNight
6d034f6d62 get_per_commitment_secret_from_seed (not working yet) 2018-06-13 16:28:06 +02:00
Janus
e0a02f64ee lnbase: avoid race while waiting for funding_locked, wait for un-reversed hash 2018-06-13 16:28:06 +02:00
Janus
17e78fe8f1 complete bolt11 port to ecdsa instead of secp256k1 2018-06-13 16:28:06 +02:00
SomberNight
b7e63efc88 channel_establishment_flow: wait for confirmations of funding txn 2018-06-13 16:28:06 +02:00
ThomasV
a852966347 add processing flow for funding_locked 2018-06-13 16:28:06 +02:00
ThomasV
93fed7d51e lnbase: fix parameters to make_commitment in htlc test 2018-06-13 16:28:06 +02:00
SomberNight
092673901c transaction.py: shortcut witness/scriptSig serialisation 2018-06-13 16:28:06 +02:00
Janus
b25f4b8e13 lightning channels list: add mock server for testing 2018-06-13 16:28:06 +02:00
Janus
940e67a149 lnbase: add some comments 2018-06-13 16:28:06 +02:00
SomberNight
76ffc26c36 constants.py: Simnet inherits from Testnet 2018-06-13 16:28:06 +02:00
Janus
9ea5641f35 use same servers for simnet as for regtest 2018-06-13 16:28:06 +02:00
Janus
eeb11d36f7 lightning-hub: update rpc stubs, do not ignore them in gitignore 2018-06-13 16:28:06 +02:00
ThomasV
1a1d7e570f lnbase: fix initial commitment transaction 2018-06-13 16:28:06 +02:00
SomberNight
0916027dac refactor storage of channels, path finding 2018-06-13 16:28:06 +02:00
Janus
607cb83bb1 avoid duplicating bech32 module 2018-06-13 16:28:06 +02:00
Janus
45836eb3a1 lnbase: more work on make_htlc_tx 2018-06-13 16:28:06 +02:00
Janus
385a6932a4 lnbase: make_htlc_tx 2018-06-13 16:28:06 +02:00
ThomasV
cd9ff5c0e3 fix: use remote_per_commitment_point 2018-06-13 16:28:06 +02:00
ThomasV
79abb65008 lnbase: derive blinded pubkey 2018-06-13 16:28:06 +02:00
ThomasV
5bfbbe8679 lnbase: fix variable name 2018-06-13 16:28:06 +02:00
ThomasV
dca794147f lnbase: add privkey derivation 2018-06-13 16:28:06 +02:00
ThomasV
2664a9ec0c add test for key derivation 2018-06-13 16:28:06 +02:00
ThomasV
d50a3abaa3 lnbase: key derivation (WIP) 2018-06-13 16:28:06 +02:00
Janus
bbd62c2e6a lnbase: test signing of first htlc test case 2018-06-13 16:28:06 +02:00
Janus
ed1e5bd214 lnbase: make_received_htlc 2018-06-13 16:28:06 +02:00
ThomasV
a710bbf7cd fix hash in make_offered_htlc 2018-06-13 16:28:06 +02:00
ThomasV
d0a5c537e6 cleanup lnbase tests 2018-06-13 16:28:06 +02:00
Janus
cc7ec8042c lightning-hub: remove path hack, use relative imports 2018-06-13 16:28:06 +02:00
SomberNight
cce910b00c naive route finding 2018-06-13 16:28:06 +02:00
Janus
35907cdc27 lnbase: offered htlc script construction 2018-06-13 16:28:06 +02:00
ThomasV
a0082cf80e use acceptable variable names 2018-06-13 16:28:06 +02:00
ThomasV
8d3e2ec02b lightning: separate testing from main code 2018-06-13 16:28:06 +02:00
ThomasV
2d82e3453f lightning: store network view 2018-06-13 16:28:06 +02:00
ThomasV
c7efcac7ab lnbase: parse ipv6, fix transport bug 2018-06-13 16:28:06 +02:00
ThomasV
69bfc48cd8 lnbase: fix read_message, reduce verbosity 2018-06-13 16:28:06 +02:00
ThomasV
fe424a0153 lnbase: implement key rotation, request initial sync in localfeatures 2018-06-13 16:28:06 +02:00
ThomasV
25e7f515b7 lnbase: verify signature in node_announcement 2018-06-13 16:28:06 +02:00
SomberNight
82b88d7ad9 bitcoin.py: implement add_number_to_script. fix CSV arg in make_commitment. 2018-06-13 16:28:06 +02:00
ThomasV
de97c975af lnbase: fix test 2018-06-13 16:28:06 +02:00
SomberNight
256a9e1aaf implement script_num_to_hex. fix encoding of argument for CSV in make_commitment 2018-06-13 16:28:06 +02:00
Janus
50c262584e lightning_channels_list: use signals to avoid segfault 2018-06-13 16:28:06 +02:00
SomberNight
8d78a6ba73 fixes for make_commitment, but still incorrect destination address (csv arg?) 2018-06-13 16:28:06 +02:00
ThomasV
4d2984346c lnbase: fix tx amounts 2018-06-13 16:28:06 +02:00
Janus
bcbd942e35 lightning-hub: include ln relative to current directory 2018-06-13 16:28:06 +02:00
Janus
39cac24861 lnbase_test: add first commitment tx with 5 htlcs test 2018-06-13 16:28:06 +02:00
ThomasV
bfa4920c53 lnbase: fix locktime and nsequence 2018-06-13 16:28:06 +02:00
Janus
1cc89ba892 lnbase_test: insert remote_signature and compare fields independently 2018-06-13 16:28:06 +02:00
ThomasV
8502f100b9 lnbase: create unit test for commitment tx 2018-06-13 16:28:06 +02:00
Janus
1496a502ba network: stop loop on loop thread 2018-06-13 16:28:06 +02:00
ThomasV
c6adfabb72 lnbase: fix signature index 2018-06-13 16:28:06 +02:00
ThomasV
a068764410 lnbase: initial commitment transaction 2018-06-13 16:28:06 +02:00
ThomasV
9569ff0d47 lnbase: fix funding address, funding_output_index 2018-06-13 16:28:06 +02:00
ThomasV
e17f17107a follow up 1aac9e59ed957898fceef99b29b9cc17d7843569 2018-06-13 16:28:06 +02:00
ThomasV
9abeb27600 lnbase: communication privkey belongs to peer 2018-06-13 16:28:06 +02:00
Janus
6c588dc948 lightning: fix hub backend loop availability 2018-06-13 16:28:06 +02:00
Janus
18cbc6b90d lnbase: more parts of channel establishment 2018-06-13 16:28:06 +02:00
Janus
7b79c85f46 lnbase: remove unnecessary try/except 2018-06-13 16:28:06 +02:00
Janus
a245b5f5da lnbase: lnbase_test must use threadsafe task submission 2018-06-13 16:28:06 +02:00
ThomasV
2f15ce519e lnbase: decorator that handles exceptions 2018-06-13 16:28:06 +02:00
Janus
3c69aa48b4 lnbase: fix shutdown when lnbase has exception in main_loop 2018-06-13 16:28:06 +02:00
Janus
0e4091dcf8 lnbase: print exceptions from main_loop 2018-06-13 16:28:06 +02:00
Janus
a5fcb98305 lnbase: initialize loop variable in main 2018-06-13 16:28:06 +02:00
Janus
2b45e4eead network: do not acquire lightninglock for lnbase 2018-06-13 16:28:06 +02:00
Janus
77e144ef58 lnbase: add lnbase_test 2018-06-13 16:28:06 +02:00
ThomasV
829422c9fb lnbase: expose wallet object in LNWorker 2018-06-13 16:28:06 +02:00
Janus
7b8d3bf524 lnbase: merge initialize and main_loop 2018-06-13 16:28:06 +02:00
Janus
13353d3358 lnbase: handle error during channel establishment 2018-06-13 16:28:06 +02:00
Janus
46b42564bc lnbase: channel establishment flow, avoid using Wallet instance 2018-06-13 16:28:06 +02:00
ThomasV
6c3c1b5755 lnbase: use relative imports 2018-06-13 16:28:06 +02:00
Janus
009f778bf6 lnbase: support simnet/testnet, create accepted open_channel message 2018-06-13 16:28:06 +02:00
Janus
fbcb9eb85e lnbase: use valid pubkeys in open_channel 2018-06-13 16:28:06 +02:00
Janus
0a6570dd65 lnbase: try sending open_channel 2018-06-13 16:28:06 +02:00
ThomasV
2ae28eef48 lnbase: add draft handlers 2018-06-13 16:28:06 +02:00
Janus
89ce48ea94 lnbase: avoid reimplementing int.to_bytes 2018-06-13 16:28:06 +02:00
Janus
67a0b7f281 lnbase: avoid reimplementing int.from_bytes 2018-06-13 16:28:06 +02:00
ThomasV
11a85245e4 fix asyncio loop 2018-06-13 16:28:06 +02:00
Janus
b76eaad07e lnbase: do not catch all exceptions, tolerate calculations with variables from kwargs 2018-06-13 16:28:06 +02:00
ThomasV
ef56c5da53 integrate lnbase with network 2018-06-13 16:28:06 +02:00
ThomasV
1dbda7d4b7 lnbase: process ping messages 2018-06-13 16:28:06 +02:00
ThomasV
315888a06f lnbase: create main loop 2018-06-13 16:28:06 +02:00
ThomasV
54128099e3 lnbase: save buffer for next read 2018-06-13 16:28:06 +02:00
ThomasV
66c1f9d626 lnbase: Peer class 2018-06-13 16:28:06 +02:00
ThomasV
e262507c22 lnbase: fix json loading and indentation 2018-06-13 16:28:06 +02:00
Janus
5ea0a2d402 lightning: do not list python files as resources, use lightning spec generated serialization 2018-06-13 16:28:06 +02:00
ThomasV
68b136db35 lightning network base 2018-06-13 16:28:06 +02:00
Janus
fca996d1e9 lightning: qt channel dialog, fix for shutdown when lightning disabled 2018-06-13 16:28:06 +02:00
Janus
d232a3193d lightning: channel details popup 2018-06-13 16:28:06 +02:00
Janus
4b70653661 kivy: paste test seed using xclip, lightning: do not catch BaseException unnecessarily, fix clearSubscribers, detect passworded wallet correctly 2018-06-13 16:28:06 +02:00
ThomasV
e752fc5c70 simplify parameters, add lndhost to config 2018-06-13 16:28:06 +02:00
Janus
6224981f17 lightning: add --simnet and --lightning switches 2018-06-13 16:28:06 +02:00
Janus
a70ad53d06 lightning: paste sample using clipboard 2018-06-13 16:28:06 +02:00
Janus
524678e4dd kivy: fix channel list error handling, close functionality for inactive channels 2018-06-13 16:28:06 +02:00
Janus
d3c8e77e55 lightning: assert result type, add invoice qr dialog 2018-06-13 16:28:06 +02:00
Janus
185fc98403 lightning: kivy: open channel button in invoice 2018-06-13 16:28:06 +02:00
Janus
102f44fc53 lightning: fix kivy channel close 2018-06-13 16:28:06 +02:00
Janus
4933f5d4ed lightning: python3.5 compat 2018-06-13 16:28:06 +02:00
Janus
94e0420479 lightning: fix channels dialog 2018-06-13 16:28:06 +02:00
Janus
7f7b157a6a lightning: add missing import, set console to none initially 2018-06-13 16:28:06 +02:00
Janus
2fd8921971 lightning: do not require lock for broadcast tx, it is thread-safe 2018-06-13 16:28:06 +02:00
Janus
b97204f4a8 lightning: save key derivation point 2018-06-13 16:28:06 +02:00
Janus
d8cf7f84a1 lightning: separate thread for publish transaction 2018-06-13 16:28:06 +02:00
Janus
3ea0c9cfd0 lightning: use queueing lock 2018-06-13 16:28:06 +02:00
Janus
4967f5bae1 lightning: less junk on console, quicker shutdown 2018-06-13 16:28:06 +02:00
Janus
95f4fae8a0 lightning: don't receive too much data, workaround by reading byte by byte 2018-06-13 16:28:06 +02:00
Janus
932ad09ab2 lightning: fix syntax 2018-06-13 16:28:06 +02:00
Janus
3879a16a2c lightning: complete moving of lightning objects, acquire net/wallet lock while answering lightning requests 2018-06-13 16:28:06 +02:00
Janus
7df0e115b4 lightning: misc patches, launch asyncio loop on separate thread 2018-06-13 16:28:06 +02:00
Janus
d71c090a21 lightning: march 2018 rebase, without integration 2018-06-13 16:28:06 +02:00
39 changed files with 5454 additions and 120 deletions

4
.gitignore vendored
View File

@ -21,3 +21,7 @@ bin/
# tox files
.cache/
.coverage
# kivy
gui/kivy/theming/light-0.png
gui/kivy/theming/light.atlas

View File

@ -67,3 +67,5 @@ wheel==0.31.0 \
colorama==0.3.9 \
--hash=sha256:463f8483208e921368c9f306094eb6f725c6ca42b0f97e313cb5d5512459feda \
--hash=sha256:48eb22f4f8461b1df5734a074b57042430fb06e1d61bd1e11b078c0fe6d7a1f1
bitstring==3.1.5
cryptography

View File

@ -7,3 +7,5 @@ protobuf
dnspython
jsonrpclib-pelix
PySocks>=1.6.6
bitstring
cryptography

View File

@ -404,6 +404,9 @@ if __name__ == '__main__':
elif config.get('regtest'):
constants.set_regtest()
if config.get('simnet'):
constants.set_simnet()
# run non-RPC commands separately
if cmdname in ['create', 'restore']:
run_non_RPC(config)

View File

@ -448,6 +448,12 @@ BoxLayout:
ActionOvrButton:
name: 'network'
text: _('Network')
ActionOvrButton:
name: 'lightning_payer_dialog'
text: _('Pay Lightning Invoice')
ActionOvrButton:
name: 'lightning_channels_dialog'
text: _('Lightning Channels')
ActionOvrButton:
name: 'settings'
text: _('Settings')

View File

@ -70,6 +70,8 @@ Label.register('Roboto',
from electrum.util import base_units, NoDynamicFeeEstimates
from .uix.dialogs.lightning_payer import LightningPayerDialog
from .uix.dialogs.lightning_channels import LightningChannelsDialog
class ElectrumWindow(App):
@ -578,6 +580,14 @@ class ElectrumWindow(App):
self._settings_dialog.update()
self._settings_dialog.open()
def lightning_payer_dialog(self):
d = LightningPayerDialog(self)
d.open()
def lightning_channels_dialog(self):
d = LightningChannelsDialog(self)
d.open()
def popup_dialog(self, name):
if name == 'settings':
self.settings_dialog()
@ -595,6 +605,8 @@ class ElectrumWindow(App):
ref.data = xpub
master_public_keys_layout.add_widget(ref)
popup.open()
elif name.endswith("_dialog"):
getattr(self, name)()
else:
popup = Builder.load_file('gui/kivy/uix/ui_screens/'+name+'.kv')
popup.open()

View File

@ -762,7 +762,11 @@ class RestoreSeedDialog(WizardDialog):
from electrum.mnemonic import Mnemonic
from electrum.old_mnemonic import words as old_wordlist
self.words = set(Mnemonic('en').wordlist).union(set(old_wordlist))
self.ids.text_input_seed.text = test_seed if is_test else ''
import subprocess
proc = subprocess.run(["xclip","-sel","clipboard","-o"], stdout=subprocess.PIPE)
self.ids.text_input_seed.text = proc.stdout.decode("ascii") if is_test else ''
self.message = _('Please type your seed phrase using the virtual keyboard.')
self.title = _('Enter Seed')
self.ext = False

View File

@ -0,0 +1,123 @@
import binascii
from kivy.lang import Builder
from kivy.factory import Factory
from kivy.uix.popup import Popup
from kivy.clock import Clock
from electrum_gui.kivy.uix.context_menu import ContextMenu
Builder.load_string('''
<LightningChannelItem@CardItem>
details: {}
active: False
channelId: '<channelId not set>'
Label:
text: root.channelId
<LightningChannelsDialog@Popup>:
name: 'lightning_channels'
BoxLayout:
id: box
orientation: 'vertical'
spacing: '1dp'
ScrollView:
GridLayout:
cols: 1
id: lightning_channels_container
size_hint: 1, None
height: self.minimum_height
spacing: '2dp'
padding: '12dp'
<ChannelDetailsItem@BoxLayout>:
canvas.before:
Color:
rgba: 0.5, 0.5, 0.5, 1
Rectangle:
size: self.size
pos: self.pos
value: ''
Label:
text: root.value
text_size: self.size # this makes the text not overflow, but wrap
<ChannelDetailsRow@BoxLayout>:
keyName: ''
value: ''
ChannelDetailsItem:
value: root.keyName
size_hint_x: 0.5 # this makes the column narrower
# see https://blog.kivy.org/2014/07/wrapping-text-in-kivys-label/
ScrollView:
Label:
text: root.value
size_hint_y: None
text_size: self.width, None
height: self.texture_size[1]
<ChannelDetailsList@RecycleView>:
scroll_type: ['bars', 'content']
scroll_wheel_distance: dp(114)
bar_width: dp(10)
viewclass: 'ChannelDetailsRow'
RecycleBoxLayout:
default_size: None, dp(56)
default_size_hint: 1, None
size_hint_y: None
height: self.minimum_height
orientation: 'vertical'
spacing: dp(2)
<ChannelDetailsPopup@Popup>:
id: popuproot
data: []
ChannelDetailsList:
data: popuproot.data
''')
class ChannelDetailsPopup(Popup):
def __init__(self, data, **kwargs):
super(ChanenlDetailsPopup,self).__init__(**kwargs)
self.data = data
class LightningChannelsDialog(Factory.Popup):
def __init__(self, app):
super(LightningChannelsDialog, self).__init__()
self.clocks = []
self.app = app
self.context_menu = None
self.app.wallet.lnworker.subscribe_channel_list_updates_from_other_thread(self.rpc_result_handler)
def show_channel_details(self, obj):
p = Factory.ChannelDetailsPopup()
p.data = [{'keyName': key, 'value': str(obj.details[key])} for key in obj.details.keys()]
p.open()
def close_channel(self, obj):
print("UNIMPLEMENTED asked to close channel", obj.channelId) # TODO
def show_menu(self, obj):
self.hide_menu()
self.context_menu = ContextMenu(obj, [("Close", self.close_channel),
("Details", self.show_channel_details)])
self.ids.box.add_widget(self.context_menu)
def hide_menu(self):
if self.context_menu is not None:
self.ids.box.remove_widget(self.context_menu)
self.context_menu = None
def rpc_result_handler(self, res):
channel_cards = self.ids.lightning_channels_container
channel_cards.clear_widgets()
if "channels" in res:
for i in res["channels"]:
item = Factory.LightningChannelItem()
item.screen = self
print(i)
item.channelId = i["chan_id"]
item.active = i["active"]
item.details = i
channel_cards.add_widget(item)
else:
self.app.show_info(res)

View File

@ -0,0 +1,93 @@
import binascii
from kivy.lang import Builder
from kivy.factory import Factory
from electrum_gui.kivy.i18n import _
from kivy.clock import mainthread
from electrum.lightning_payencode.lnaddr import lndecode
Builder.load_string('''
<LightningPayerDialog@Popup>
id: s
name: 'lightning_payer'
invoice_data: ''
BoxLayout:
orientation: "vertical"
BlueButton:
text: s.invoice_data if s.invoice_data else _('Lightning invoice')
shorten: True
on_release: Clock.schedule_once(lambda dt: app.show_info(_('Copy and paste the lightning invoice using the Paste button, or use the camera to scan a QR code.')))
GridLayout:
cols: 4
size_hint: 1, None
height: '48dp'
IconButton:
id: qr
on_release: Clock.schedule_once(lambda dt: app.scan_qr(on_complete=s.on_lightning_qr))
icon: 'atlas://gui/kivy/theming/light/camera'
Button:
text: _('Paste')
on_release: s.do_paste()
Button:
text: _('Paste using xclip')
on_release: s.do_paste_xclip()
Button:
text: _('Clear')
on_release: s.do_clear()
Button:
size_hint: 1, None
height: '48dp'
text: _('Open channel to pubkey in invoice')
on_release: s.do_open_channel()
Button:
size_hint: 1, None
height: '48dp'
text: _('Pay pasted/scanned invoice')
on_release: s.do_pay()
''')
class LightningPayerDialog(Factory.Popup):
def __init__(self, app):
super(LightningPayerDialog, self).__init__()
self.app = app
#def open(self, *args, **kwargs):
# super(LightningPayerDialog, self).open(*args, **kwargs)
#def dismiss(self, *args, **kwargs):
# super(LightningPayerDialog, self).dismiss(*args, **kwargs)
def do_paste_xclip(self):
import subprocess
proc = subprocess.run(["xclip","-sel","clipboard","-o"], stdout=subprocess.PIPE)
self.invoice_data = proc.stdout.decode("ascii")
def do_paste(self):
contents = self.app._clipboard.paste()
if not contents:
self.app.show_info(_("Clipboard is empty"))
return
self.invoice_data = contents
def do_clear(self):
self.invoice_data = ""
def do_open_channel(self):
compressed_pubkey_bytes = lndecode(self.invoice_data).pubkey.serialize()
hexpubkey = binascii.hexlify(compressed_pubkey_bytes).decode("ascii")
local_amt = 200000
push_amt = 100000
def on_success(pw):
# node_id, local_amt, push_amt, emit_function, get_password
self.app.wallet.lnworker.open_channel_from_other_thread(hexpubkey, local_amt, push_amt, mainthread(lambda parent: self.app.show_info(_("Channel open, waiting for locking..."))), lambda: pw)
if self.app.wallet.has_keystore_encryption():
# wallet, msg, on_success (Tuple[str, str] -> ()), on_failure (() -> ())
self.app.password_dialog(self.app.wallet, _("Password needed for opening channel"), on_success, lambda: self.app.show_error(_("Failed getting password from you")))
else:
on_success("")
def do_pay(self):
self.app.wallet.lnworker.pay_invoice_from_other_thread(self.invoice_data)
def on_lightning_qr(self, data):
self.invoice_data = str(data)

View File

@ -51,7 +51,6 @@ from electrum.util import (UserCancelled, print_error,
from .installwizard import InstallWizard
try:
from . import icons_rc
except Exception as e:

84
gui/qt/channels_list.py Normal file
View File

@ -0,0 +1,84 @@
# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtWidgets
from electrum.util import inv_dict, bh2u
from electrum.i18n import _
from electrum.lnbase import OpenChannel
from .util import MyTreeWidget, SortableTreeWidgetItem
class ChannelsList(MyTreeWidget):
update_rows = QtCore.pyqtSignal()
update_single_row = QtCore.pyqtSignal(OpenChannel)
def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Node ID'), _('Balance'), _('Remote'), _('Status')], 0)
self.main_window = parent
self.update_rows.connect(self.do_update_rows)
self.update_single_row.connect(self.do_update_single_row)
def format_fields(self, chan):
status = self.parent.wallet.lnworker.channel_state[chan.channel_id]
return [
bh2u(chan.node_id),
self.parent.format_amount(chan.local_state.amount_msat//1000),
self.parent.format_amount(chan.remote_state.amount_msat//1000),
status
]
def create_menu(self, position):
menu = QtWidgets.QMenu()
channel_id = self.currentItem().data(0, QtCore.Qt.UserRole)
print('ID', bh2u(channel_id))
def close():
self.parent.wallet.lnworker.close_channel(channel_id)
menu.addAction(_("Close channel"), close)
menu.exec_(self.viewport().mapToGlobal(position))
@QtCore.pyqtSlot(OpenChannel)
def do_update_single_row(self, chan):
for i in range(self.topLevelItemCount()):
item = self.topLevelItem(i)
if item.data(0, QtCore.Qt.UserRole) == chan.channel_id:
for i, v in enumerate(self.format_fields(chan)):
item.setData(i, QtCore.Qt.DisplayRole, v)
@QtCore.pyqtSlot()
def do_update_rows(self):
self.clear()
for chan in self.parent.wallet.lnworker.channels.values():
item = SortableTreeWidgetItem(self.format_fields(chan))
item.setData(0, QtCore.Qt.UserRole, chan.channel_id)
self.insertTopLevelItem(0, item)
def get_toolbar(self):
nodeid_inp = QtWidgets.QLineEdit(self)
local_amt_inp = QtWidgets.QLineEdit(self, text='200000')
push_amt_inp = QtWidgets.QLineEdit(self, text='0')
button = QtWidgets.QPushButton(_('Open channel'), self)
button.clicked.connect(lambda: self.main_window.protect(self.open_channel, (nodeid_inp, local_amt_inp, push_amt_inp)))
h = QtWidgets.QGridLayout()
nodeid_label = QtWidgets.QLabel(self)
nodeid_label.setText(_("Node ID"))
local_amt_label = QtWidgets.QLabel(self)
local_amt_label.setText("Local amount (sat)")
push_amt_label = QtWidgets.QLabel(self)
push_amt_label.setText("Push amount (sat)")
h.addWidget(nodeid_label, 0, 0)
h.addWidget(local_amt_label, 0, 1)
h.addWidget(push_amt_label, 0, 2)
h.addWidget(nodeid_inp, 1, 0)
h.addWidget(local_amt_inp, 1, 1)
h.addWidget(push_amt_inp, 1, 2)
h.addWidget(button, 1, 3)
h.setColumnStretch(0, 3)
h.setColumnStretch(1, 1)
h.setColumnStretch(2, 1)
h.setColumnStretch(3, 1)
return h
def open_channel(self, nodeIdInput, local_amt_inp, push_amt_inp, password):
node_id = str(nodeIdInput.text())
local_amt = int(local_amt_inp.text())
push_amt = int(push_amt_inp.text())
assert local_amt >= 200000
assert local_amt >= push_amt
obj = self.parent.wallet.lnworker.open_channel(node_id, local_amt, push_amt, password)

View File

@ -61,6 +61,7 @@ from .qrtextedit import ShowQRTextEdit, ScanQRTextEdit
from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider
from .util import *
from .channels_list import ChannelsList
class StatusBarButton(QPushButton):
@ -138,6 +139,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.utxo_tab = self.create_utxo_tab()
self.console_tab = self.create_console_tab()
self.contacts_tab = self.create_contacts_tab()
self.channels_tab = self.create_channels_tab(wallet)
tabs.addTab(self.create_history_tab(), QIcon(":icons/tab_history.png"), _('History'))
tabs.addTab(self.send_tab, QIcon(":icons/tab_send.png"), _('Send'))
tabs.addTab(self.receive_tab, QIcon(":icons/tab_receive.png"), _('Receive'))
@ -151,6 +153,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
tabs.addTab(tab, icon, description.replace("&", ""))
add_optional_tab(tabs, self.addresses_tab, QIcon(":icons/tab_addresses.png"), _("&Addresses"), "addresses")
add_optional_tab(tabs, self.channels_tab, QIcon(":icons/lightning.png"), _("Channels"), "channels")
add_optional_tab(tabs, self.utxo_tab, QIcon(":icons/tab_coins.png"), _("Co&ins"), "utxo")
add_optional_tab(tabs, self.contacts_tab, QIcon(":icons/tab_contacts.png"), _("Con&tacts"), "contacts")
add_optional_tab(tabs, self.console_tab, QIcon(":icons/tab_console.png"), _("Con&sole"), "console")
@ -183,7 +186,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.network:
self.network_signal.connect(self.on_network_qt)
interests = ['updated', 'new_transaction', 'status',
'banner', 'verified', 'fee']
'banner', 'verified', 'fee', 'on_quotes',
'on_history', 'channel', 'channels']
# To avoid leaking references to "self" that prevent the
# window from being GC-ed when closed, callbacks should be
# methods of this class only, and specifically not be
@ -191,8 +195,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.network.register_callback(self.on_network, interests)
# set initial message
self.console.showMessage(self.network.banner)
self.network.register_callback(self.on_quotes, ['on_quotes'])
self.network.register_callback(self.on_history, ['on_history'])
self.new_fx_quotes_signal.connect(self.on_fx_quotes)
self.new_fx_history_signal.connect(self.on_fx_history)
@ -202,9 +204,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.connect_slots(gui_object.timer)
self.fetch_alias()
def on_history(self, b):
self.new_fx_history_signal.emit()
def setup_exception_hook(self):
Exception_Hook(self)
@ -213,9 +212,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.history_list.update()
self.address_list.update()
def on_quotes(self, b):
self.new_fx_quotes_signal.emit()
def on_fx_quotes(self):
self.update_status()
# Refresh edits with the new rate
@ -296,6 +292,14 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
elif event in ['status', 'banner', 'verified', 'fee']:
# Handle in GUI thread
self.network_signal.emit(event, args)
elif event == 'on_quotes':
self.new_fx_quotes_signal.emit()
elif event == 'on_history':
self.new_fx_history_signal.emit()
elif event == 'channels':
self.channels_list.update_rows.emit(*args)
elif event == 'channel':
self.channels_list.update_single_row.emit(*args)
else:
self.print_error("unexpected network message:", event, args)
@ -346,6 +350,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.history_list.update()
self.address_list.update()
self.utxo_list.update()
wallet.lnworker.on_channels_updated()
self.need_update.set()
# Once GUI has been initialized check if we want to announce something since the callback has been called before the GUI was initialized
self.notify_transactions()
@ -516,6 +521,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
view_menu = menubar.addMenu(_("&View"))
add_toggle_action(view_menu, self.addresses_tab)
add_toggle_action(view_menu, self.utxo_tab)
add_toggle_action(view_menu, self.channels_tab)
add_toggle_action(view_menu, self.contacts_tab)
add_toggle_action(view_menu, self.console_tab)
@ -758,6 +764,11 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.invoice_list.update()
self.update_completions()
def create_channels_tab(self, wallet):
self.channels_list = ChannelsList(self)
t = self.channels_list.get_toolbar()
return self.create_list_tab(self.channels_list, t)
def create_history_tab(self):
from .history_list import HistoryList
self.history_list = l = HistoryList(self)
@ -783,16 +794,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
grid.setSpacing(8)
grid.setColumnStretch(3, 1)
self.receive_address_e = ButtonsLineEdit()
self.receive_address_e.addCopyButton(self.app)
self.receive_address_e.setReadOnly(True)
msg = _('Bitcoin address where the payment should be received. Note that each payment request uses a different Bitcoin address.')
self.receive_address_label = HelpLabel(_('Receiving address'), msg)
self.receive_address_e.textChanged.connect(self.update_receive_qr)
self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
grid.addWidget(self.receive_address_label, 0, 0)
grid.addWidget(self.receive_address_e, 0, 1, 1, -1)
self.receive_message_e = QLineEdit()
grid.addWidget(QLabel(_('Description')), 1, 0)
grid.addWidget(self.receive_message_e, 1, 1, 1, -1)
@ -827,23 +828,30 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.expires_label.hide()
grid.addWidget(self.expires_label, 3, 1)
self.save_request_button = QPushButton(_('Save'))
self.save_request_button.clicked.connect(self.save_payment_request)
self.receive_type = QComboBox()
self.receive_type.addItems([_('Bitcoin address'), _('Lightning')])
grid.addWidget(QLabel(_('Type')), 4, 0)
grid.addWidget(self.receive_type, 4, 1)
self.new_request_button = QPushButton(_('New'))
self.new_request_button.clicked.connect(self.new_payment_request)
self.save_request_button = QPushButton(_('Create'))
self.save_request_button.clicked.connect(self.create_invoice)
self.receive_buttons = buttons = QHBoxLayout()
buttons.addWidget(self.save_request_button)
buttons.addStretch(1)
grid.addLayout(buttons, 4, 2, 1, 2)
self.receive_address_e = ButtonsTextEdit()
self.receive_address_e.addCopyButton(self.app)
self.receive_address_e.setReadOnly(True)
self.receive_address_e.textChanged.connect(self.update_receive_qr)
self.receive_address_e.setFocusPolicy(Qt.ClickFocus)
self.receive_qr = QRCodeWidget(fixedSize=200)
self.receive_qr.mouseReleaseEvent = lambda x: self.toggle_qr_window()
self.receive_qr.enterEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.PointingHandCursor))
self.receive_qr.leaveEvent = lambda x: self.app.setOverrideCursor(QCursor(Qt.ArrowCursor))
self.receive_buttons = buttons = QHBoxLayout()
buttons.addStretch(1)
buttons.addWidget(self.save_request_button)
buttons.addWidget(self.new_request_button)
grid.addLayout(buttons, 4, 1, 1, 2)
self.receive_requests_label = QLabel(_('Requests'))
from .request_list import RequestList
@ -854,14 +862,19 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
vbox_g.addLayout(grid)
vbox_g.addStretch()
hbox_r = QHBoxLayout()
hbox_r.addWidget(self.receive_qr)
hbox_r.addWidget(self.receive_address_e)
hbox = QHBoxLayout()
hbox.addLayout(vbox_g)
hbox.addWidget(self.receive_qr)
hbox.addLayout(hbox_r)
w = QWidget()
w.searchable_list = self.request_list
vbox = QVBoxLayout(w)
vbox.addLayout(hbox)
vbox.addStretch(1)
vbox.addWidget(self.receive_requests_label)
vbox.addWidget(self.request_list)
@ -880,8 +893,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
message = self.wallet.labels.get(addr, '')
amount = req['amount']
URI = util.create_URI(addr, amount, message)
if req.get('time'):
URI += "&time=%d"%req.get('time')
#if req.get('time'):
# URI += "&time=%d"%req.get('time')
if req.get('exp'):
URI += "&exp=%d"%req.get('exp')
if req.get('name') and req.get('sig'):
@ -912,15 +925,34 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
else:
return
def save_payment_request(self):
addr = str(self.receive_address_e.text())
def create_invoice(self):
amount = self.receive_amount_e.get_amount()
message = self.receive_message_e.text()
if not message and not amount:
self.show_error(_('No message or amount'))
return False
i = self.expires_combo.currentIndex()
expiration = list(map(lambda x: x[1], expiration_values))[i]
if self.receive_type.currentIndex() == 1:
self.create_lightning_request(amount, message, expiration)
else:
self.create_bitcoin_request(amount, message, expiration)
self.request_list.update()
def create_lightning_request(self, amount, message, expiration):
req = self.wallet.lnworker.add_invoice(amount, message)
def create_bitcoin_request(self, amount, message, expiration):
addr = self.wallet.get_unused_address()
if addr is None:
if not self.wallet.is_deterministic():
msg = [
_('No more addresses in your wallet.'),
_('You are using a non-deterministic wallet, which cannot create new addresses.'),
_('If you want to create new addresses, use a deterministic wallet instead.')
]
self.show_message(' '.join(msg))
return
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return
addr = self.wallet.create_new_address(False)
req = self.wallet.make_payment_request(addr, amount, message, expiration)
try:
self.wallet.add_payment_request(req, self.config)
@ -931,7 +963,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.sign_payment_request(addr)
self.save_request_button.setEnabled(False)
finally:
self.request_list.update()
self.address_list.update()
def view_and_paste(self, title, msg, data):
@ -957,26 +988,6 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.show_message(_("Request saved successfully"))
self.saved = True
def new_payment_request(self):
addr = self.wallet.get_unused_address()
if addr is None:
if not self.wallet.is_deterministic():
msg = [
_('No more addresses in your wallet.'),
_('You are using a non-deterministic wallet, which cannot create new addresses.'),
_('If you want to create new addresses, use a deterministic wallet instead.')
]
self.show_message(' '.join(msg))
return
if not self.question(_("Warning: The next address will not be recovered automatically if you restore your wallet from seed; you may need to add it manually.\n\nThis occurs because you have too many unused addresses in your wallet. To avoid this situation, use the existing addresses first.\n\nCreate anyway?")):
return
addr = self.wallet.create_new_address(False)
self.set_receive_address(addr)
self.expires_label.hide()
self.expires_combo.show()
self.new_request_button.setEnabled(False)
self.receive_message_e.setFocus(1)
def set_receive_address(self, addr):
self.receive_address_e.setText(addr)
self.receive_message_e.setText('')
@ -1019,14 +1030,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.new_request_button.setEnabled(True)
def update_receive_qr(self):
addr = str(self.receive_address_e.text())
amount = self.receive_amount_e.get_amount()
message = self.receive_message_e.text()
self.save_request_button.setEnabled((amount is not None) or (message != ""))
uri = util.create_URI(addr, amount, message)
uri = str(self.receive_address_e.text())
self.receive_qr.setData(uri)
if self.qr_window and self.qr_window.isVisible():
self.qr_window.set_content(addr, amount, message, uri)
self.qr_window.set_content(uri, '', '', uri)
def set_feerounding_text(self, num_satoshis_added):
self.feerounding_text = (_('Additional {} satoshis are going to be added.')
@ -1430,6 +1437,10 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
return func(self, *args, **kwargs)
return request_password
@protected
def protect(self, func, args, password):
return func(*args, password)
def is_send_fee_frozen(self):
return self.fee_e.isVisible() and self.fee_e.isModified() \
and (self.fee_e.text() or self.fee_e.hasFocus())
@ -1495,7 +1506,15 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
def do_preview(self):
self.do_send(preview = True)
def pay_lightning_invoice(self, invoice):
f = self.wallet.lnworker.pay(invoice)
self.do_clear()
def do_send(self, preview = False):
if self.payto_e.is_lightning:
self.pay_lightning_invoice(self.payto_e.lightning_invoice)
return
#
if run_hook('abort_send', self):
return
r = self.read_send_tab()
@ -1693,6 +1712,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
else:
self.payment_request_error_signal.emit()
def parse_lightning_invoice(self, invoice):
from electrum.lightning_payencode.lnaddr import lndecode
lnaddr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
pubkey = bh2u(lnaddr.pubkey.serialize())
for k,v in lnaddr.tags:
if k == 'd':
description = v
break
else:
description = ''
self.payto_e.setFrozen(True)
self.payto_e.setText(pubkey)
self.message_e.setText(description)
self.amount_e.setAmount(lnaddr.amount * COIN)
#self.amount_e.textEdited.emit("")
self.payto_e.is_lightning = True
def pay_to_URI(self, URI):
if not URI:
return
@ -1729,6 +1765,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.not_enough_funds = False
self.payment_request = None
self.payto_e.is_pr = False
self.payto_e.is_lightning = False
for e in [self.payto_e, self.message_e, self.amount_e, self.fiat_send_e,
self.fee_e, self.feerate_e]:
e.setText('')
@ -1753,8 +1790,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
w.searchable_list = l
vbox = QVBoxLayout()
w.setLayout(vbox)
vbox.setContentsMargins(0, 0, 0, 0)
vbox.setSpacing(0)
#vbox.setContentsMargins(0, 0, 0, 0)
#vbox.setSpacing(0)
if toolbar:
vbox.addLayout(toolbar)
vbox.addWidget(l)

View File

@ -56,10 +56,9 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
self.errors = []
self.is_pr = False
self.is_alias = False
self.scan_f = win.pay_to_URI
self.is_lightning = False
self.update_size()
self.payto_address = None
self.previous_payto = ''
def setFrozen(self, b):
@ -127,7 +126,11 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
if len(lines) == 1:
data = lines[0]
if data.startswith("bitcoin:"):
self.scan_f(data)
self.win.pay_to_URI(data)
return
if data.startswith("ln"):
self.win.parse_lightning_invoice(data)
self.lightning_invoice = data
return
try:
self.payto_address = self.parse_output(data)
@ -201,7 +204,7 @@ class PayToEdit(CompletionTextEdit, ScanQRTextEdit):
def qr_input(self):
data = super(PayToEdit,self).qr_input()
if data.startswith("bitcoin:"):
self.scan_f(data)
self.win.pay_to_URI(data)
# TODO: update fee
def resolve(self):

View File

@ -29,21 +29,30 @@ from electrum.plugins import run_hook
from electrum.paymentrequest import PR_UNKNOWN
from PyQt5.QtGui import *
from PyQt5.QtCore import *
from PyQt5.QtWidgets import QTreeWidgetItem, QMenu
from PyQt5.QtWidgets import QTreeWidgetItem, QMenu, QHeaderView
from .util import MyTreeWidget, pr_tooltips, pr_icons
class RequestList(MyTreeWidget):
filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount
class RequestList(MyTreeWidget):
filter_columns = [0, 1, 2, 3] # Date, Address, Description, Amount
def __init__(self, parent):
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), '', _('Description'), _('Amount'), _('Status')], 3)
MyTreeWidget.__init__(self, parent, self.create_menu, [_('Date'), _('Address'), _('Description'), _('Amount'), _('Status')], 2)
self.currentItemChanged.connect(self.item_changed)
self.itemClicked.connect(self.item_changed)
self.setSortingEnabled(True)
self.setColumnWidth(0, 180)
self.hideColumn(1)
self.setColumnWidth(1, 250)
def update_headers(self, headers):
self.setColumnCount(len(headers))
self.setHeaderLabels(headers)
self.header().setStretchLastSection(False)
for col in range(len(headers)):
if col in [1]: continue
sm = QHeaderView.Stretch if col == self.stretch_column else QHeaderView.ResizeToContents
self.header().setSectionResizeMode(col, sm)
def item_changed(self, item):
if item is None:
@ -51,20 +60,20 @@ class RequestList(MyTreeWidget):
if not item.isSelected():
return
addr = str(item.text(1))
req = self.wallet.receive_requests.get(addr)
if req is None:
self.update()
return
expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
amount = req['amount']
message = self.wallet.labels.get(addr, '')
self.parent.receive_address_e.setText(addr)
self.parent.receive_message_e.setText(message)
self.parent.receive_amount_e.setAmount(amount)
self.parent.expires_combo.hide()
self.parent.expires_label.show()
self.parent.expires_label.setText(expires)
self.parent.new_request_button.setEnabled(True)
#req = self.wallet.receive_requests.get(addr)
#if req is None:
# self.update()
# return
#expires = age(req['time'] + req['exp']) if req.get('exp') else _('Never')
#amount = req['amount']
#message = self.wallet.labels.get(addr, '')
#self.parent.receive_message_e.setText(message)
#self.parent.receive_amount_e.setAmount(amount)
#self.parent.expires_combo.hide()
#self.parent.expires_label.show()
#self.parent.expires_label.setText(expires)
#self.parent.new_request_button.setEnabled(True)
def on_update(self):
self.wallet = self.parent.wallet
@ -77,12 +86,12 @@ class RequestList(MyTreeWidget):
self.parent.expires_combo.show()
# update the receive address if necessary
current_address = self.parent.receive_address_e.text()
#current_address = self.parent.receive_address_e.text()
domain = self.wallet.get_receiving_addresses()
addr = self.wallet.get_unused_address()
if not current_address in domain and addr:
self.parent.set_receive_address(addr)
self.parent.new_request_button.setEnabled(addr != current_address)
#addr = self.wallet.get_unused_address()
#if not current_address in domain and addr:
# self.parent.set_receive_address(addr)
#self.parent.new_request_button.setEnabled(addr != current_address)
# clear the list and fill it again
self.clear()
@ -99,13 +108,30 @@ class RequestList(MyTreeWidget):
signature = req.get('sig')
requestor = req.get('name', '')
amount_str = self.parent.format_amount(amount) if amount else ""
item = QTreeWidgetItem([date, address, '', message, amount_str, pr_tooltips.get(status,'')])
URI = self.parent.get_request_URI(address)
item = QTreeWidgetItem([date, URI, message, amount_str, pr_tooltips.get(status,'')])
if signature is not None:
item.setIcon(2, self.icon_cache.get(":icons/seal.png"))
item.setToolTip(2, 'signed by '+ requestor)
item.setIcon(1, self.icon_cache.get(":icons/seal.png"))
item.setToolTip(1, 'signed by '+ requestor)
if status is not PR_UNKNOWN:
item.setIcon(6, self.icon_cache.get(pr_icons.get(status)))
self.addTopLevelItem(item)
# lightning
for k, r in self.wallet.lnworker.invoices.items():
from electrum.lightning_payencode.lnaddr import lndecode
import electrum.constants as constants
lnaddr = lndecode(r, expected_hrp=constants.net.SEGWIT_HRP)
amount_str = self.parent.format_amount(lnaddr.amount*100000000)
for k,v in lnaddr.tags:
if k == 'd':
description = v
break
else:
description = ''
date = format_time(lnaddr.date)
item = QTreeWidgetItem([date, r, description, amount_str, ''])
item.setIcon(1, QIcon(":icons/lightning.png"))
self.addTopLevelItem(item)
def create_menu(self, position):

View File

@ -22,6 +22,7 @@
<file>icons/key.png</file>
<file>icons/ledger.png</file>
<file>icons/ledger_unpaired.png</file>
<file>icons/lightning.png</file>
<file>icons/lock.png</file>
<file>icons/microphone.png</file>
<file>icons/network.png</file>

BIN
icons/lightning.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 446 B

View File

@ -23,6 +23,7 @@
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import queue
import sys
import datetime
import copy
@ -667,6 +668,31 @@ class Commands:
# for the python console
return sorted(known_commands.keys())
# lightning network commands
@command('wpn')
def open_channel(self, node_id, amount, channel_push=0, password=None):
f = self.wallet.lnworker.open_channel(node_id, satoshis(amount), satoshis(channel_push), password)
return f.result()
@command('wn')
def reestablish_channel(self):
self.wallet.lnworker.reestablish_channel()
@command('wn')
def lnpay(self, invoice):
f = self.wallet.lnworker.pay(invoice)
return f.result()
@command('wn')
def addinvoice(self, requested_amount, message):
# using requested_amount because it is documented in param_descriptions
return self.wallet.lnworker.add_invoice(satoshis(requested_amount), message)
@command('wn')
def listchannels(self):
return self.wallet.lnworker.list_channels()
param_descriptions = {
'privkey': 'Private key. Type \'?\' to get a prompt.',
'destination': 'Bitcoin address, contact or alias',
@ -714,6 +740,7 @@ command_options = {
'timeout': (None, "Timeout in seconds"),
'force': (None, "Create new address beyond gap limit, if no more addresses are available."),
'pending': (None, "Show only pending requests."),
'channel_push':(None, 'Push initial amount (in BTC)'),
'expired': (None, "Show only expired requests."),
'paid': (None, "Show only paid requests."),
'show_addresses': (None, "Show input and output addresses"),
@ -818,6 +845,7 @@ def add_global_options(parser):
group.add_argument("-w", "--wallet", dest="wallet_path", help="wallet path")
group.add_argument("--testnet", action="store_true", dest="testnet", default=False, help="Use Testnet")
group.add_argument("--regtest", action="store_true", dest="regtest", default=False, help="Use Regtest")
group.add_argument("--simnet", action="store_true", dest="simnet", default=False, help="Use Simnet")
def get_parser():
# create main parser

View File

@ -93,6 +93,16 @@ class BitcoinTestnet:
}
class BitcoinSimnet(BitcoinTestnet):
ADDRTYPE_P2PKH = 0x3f
ADDRTYPE_P2SH = 0x7b
SEGWIT_HRP = "sb"
GENESIS = "683e86bd5c6d110d91b94b97137ba6bfe02dbbdb8e3dff722a669b5d69d77af6"
WIF_PREFIX = 0x00
DEFAULT_SERVERS = read_json('servers_regtest.json', {}) # Note: regtest!
CHECKPOINTS = []
class BitcoinRegtest(BitcoinTestnet):
SEGWIT_HRP = "bcrt"
@ -109,6 +119,9 @@ def set_mainnet():
global net
net = BitcoinMainnet
def set_simnet():
global net
net = BitcoinSimnet
def set_testnet():
global net

View File

@ -132,11 +132,15 @@ def Hash(x: bytes) -> bytes:
def hash_160(x: bytes) -> bytes:
return ripemd(sha256(x))
def ripemd(x):
try:
md = hashlib.new('ripemd160')
md.update(sha256(x))
md.update(x)
return md.digest()
except BaseException:
from . import ripemd
md = ripemd.new(sha256(x))
md = ripemd.new(x)
return md.digest()

View File

@ -280,7 +280,11 @@ class Daemon(DaemonThread):
kwargs[x] = (config_options.get(x) if x in ['password', 'new_password'] else config.get(x))
cmd_runner = Commands(config, wallet, self.network)
func = getattr(cmd_runner, cmd.name)
result = func(*args, **kwargs)
try:
result = func(*args, **kwargs)
except TypeError as e:
# we are catching here because JSON-RPC-Pelix would throw away the trace of e
raise Exception("TypeError occured in Electrum") from e
return result
def run(self):

View File

@ -231,9 +231,9 @@ class ECPubkey(object):
def __ne__(self, other):
return not (self == other)
def verify_message_for_address(self, sig65: bytes, message: bytes) -> None:
def verify_message_for_address(self, sig65: bytes, message: bytes, algo=lambda x: Hash(msg_magic(x))) -> None:
assert_bytes(message)
h = Hash(msg_magic(message))
h = algo(message)
public_key, compressed = self.from_signature65(sig65, h)
# check public key
if public_key != self:
@ -279,6 +279,13 @@ def msg_magic(message: bytes) -> bytes:
return b"\x18Bitcoin Signed Message:\n" + length + message
def verify_signature(pubkey, sig, h):
try:
ECPubkey(pubkey).verify_message_hash(sig, h)
except:
return False
return True
def verify_message_with_address(address: str, sig65: bytes, message: bytes):
from .bitcoin import pubkey_to_address
assert_bytes(sig65, message)
@ -360,12 +367,12 @@ class ECPrivkey(ECPubkey):
sigencode=der_sig_from_r_and_s,
sigdecode=get_r_and_s_from_der_sig)
def sign_message(self, message: bytes, is_compressed: bool) -> bytes:
def sign_message(self, message: bytes, is_compressed: bool, algo=lambda x: Hash(msg_magic(x))) -> bytes:
def bruteforce_recid(sig_string):
for recid in range(4):
sig65 = construct_sig65(sig_string, recid, is_compressed)
try:
self.verify_message_for_address(sig65, message)
self.verify_message_for_address(sig65, message, algo)
return sig65, recid
except Exception as e:
continue
@ -373,7 +380,7 @@ class ECPrivkey(ECPubkey):
raise Exception("error: cannot sign message. no recid fits..")
message = to_bytes(message, 'utf8')
msg_hash = Hash(msg_magic(message))
msg_hash = algo(message)
sig_string = self.sign(msg_hash,
sigencode=sig_string_from_r_and_s,
sigdecode=get_r_and_s_from_sig_string)

View File

@ -334,7 +334,10 @@ class BIP32_KeyStore(Deterministic_KeyStore, Xpub):
pk = bip32_private_key(sequence, k, c)
return pk, True
def get_keypair(self, sequence, password):
k, _ = self.get_private_key(sequence, password)
cK = ecc.ECPrivkey(k).get_public_key_bytes()
return cK, k
class Old_KeyStore(Deterministic_KeyStore):

801
lib/lightning.json Normal file
View File

@ -0,0 +1,801 @@
{
"init": {
"type": "16",
"payload": {
"gflen": {
"position": "0",
"length": "2"
},
"globalfeatures": {
"position": "2",
"length": "gflen"
},
"lflen": {
"position": "2+gflen",
"length": "2"
},
"localfeatures": {
"position": "4+gflen",
"length": "lflen"
}
}
},
"error": {
"type": "17",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"len": {
"position": "32",
"length": "2"
},
"data": {
"position": "34",
"length": "len"
}
}
},
"ping": {
"type": "18",
"payload": {
"num_pong_bytes": {
"position": "0",
"length": "2"
},
"byteslen": {
"position": "2",
"length": "2"
},
"ignored": {
"position": "4",
"length": "byteslen"
}
}
},
"pong": {
"type": "19",
"payload": {
"byteslen": {
"position": "0",
"length": "2"
},
"ignored": {
"position": "2",
"length": "byteslen"
}
}
},
"open_channel": {
"type": "32",
"payload": {
"chain_hash": {
"position": "0",
"length": "32"
},
"temporary_channel_id": {
"position": "32",
"length": "32"
},
"funding_satoshis": {
"position": "64",
"length": "8"
},
"push_msat": {
"position": "72",
"length": "8"
},
"dust_limit_satoshis": {
"position": "80",
"length": "8"
},
"max_htlc_value_in_flight_msat": {
"position": "88",
"length": "8"
},
"channel_reserve_satoshis": {
"position": "96",
"length": "8"
},
"htlc_minimum_msat": {
"position": "104",
"length": "8"
},
"feerate_per_kw": {
"position": "112",
"length": "4"
},
"to_self_delay": {
"position": "116",
"length": "2"
},
"max_accepted_htlcs": {
"position": "118",
"length": "2"
},
"funding_pubkey": {
"position": "120",
"length": "33"
},
"revocation_basepoint": {
"position": "153",
"length": "33"
},
"payment_basepoint": {
"position": "186",
"length": "33"
},
"delayed_payment_basepoint": {
"position": "219",
"length": "33"
},
"htlc_basepoint": {
"position": "252",
"length": "33"
},
"first_per_commitment_point": {
"position": "285",
"length": "33"
},
"channel_flags": {
"position": "318",
"length": "1"
},
"shutdown_len": {
"position": "319",
"length": "2",
"feature": "option_upfront_shutdown_script"
},
"shutdown_scriptpubkey": {
"position": "321",
"length": "shutdown_len",
"feature": "option_upfront_shutdown_script"
}
}
},
"accept_channel": {
"type": "33",
"payload": {
"temporary_channel_id": {
"position": "0",
"length": "32"
},
"dust_limit_satoshis": {
"position": "32",
"length": "8"
},
"max_htlc_value_in_flight_msat": {
"position": "40",
"length": "8"
},
"channel_reserve_satoshis": {
"position": "48",
"length": "8"
},
"htlc_minimum_msat": {
"position": "56",
"length": "8"
},
"minimum_depth": {
"position": "64",
"length": "4"
},
"to_self_delay": {
"position": "68",
"length": "2"
},
"max_accepted_htlcs": {
"position": "70",
"length": "2"
},
"funding_pubkey": {
"position": "72",
"length": "33"
},
"revocation_basepoint": {
"position": "105",
"length": "33"
},
"payment_basepoint": {
"position": "138",
"length": "33"
},
"delayed_payment_basepoint": {
"position": "171",
"length": "33"
},
"htlc_basepoint": {
"position": "204",
"length": "33"
},
"first_per_commitment_point": {
"position": "237",
"length": "33"
},
"shutdown_len": {
"position": "270",
"length": "2",
"feature": "option_upfront_shutdown_script"
},
"shutdown_scriptpubkey": {
"position": "272",
"length": "shutdown_len",
"feature": "option_upfront_shutdown_script"
}
}
},
"funding_created": {
"type": "34",
"payload": {
"temporary_channel_id": {
"position": "0",
"length": "32"
},
"funding_txid": {
"position": "32",
"length": "32"
},
"funding_output_index": {
"position": "64",
"length": "2"
},
"signature": {
"position": "66",
"length": "64"
}
}
},
"funding_signed": {
"type": "35",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"signature": {
"position": "32",
"length": "64"
}
}
},
"funding_locked": {
"type": "36",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"next_per_commitment_point": {
"position": "32",
"length": "33"
}
}
},
"shutdown": {
"type": "38",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"len": {
"position": "32",
"length": "2"
},
"scriptpubkey": {
"position": "34",
"length": "len"
}
}
},
"closing_signed": {
"type": "39",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"fee_satoshis": {
"position": "32",
"length": "8"
},
"signature": {
"position": "40",
"length": "64"
}
}
},
"update_add_htlc": {
"type": "128",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"id": {
"position": "32",
"length": "8"
},
"amount_msat": {
"position": "40",
"length": "8"
},
"payment_hash": {
"position": "48",
"length": "32"
},
"cltv_expiry": {
"position": "80",
"length": "4"
},
"onion_routing_packet": {
"position": "84",
"length": "1366"
}
}
},
"update_fulfill_htlc": {
"type": "130",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"id": {
"position": "32",
"length": "8"
},
"payment_preimage": {
"position": "40",
"length": "32"
}
}
},
"update_fail_htlc": {
"type": "131",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"id": {
"position": "32",
"length": "8"
},
"len": {
"position": "40",
"length": "2"
},
"reason": {
"position": "42",
"length": "len"
}
}
},
"update_fail_malformed_htlc": {
"type": "135",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"id": {
"position": "32",
"length": "8"
},
"sha256_of_onion": {
"position": "40",
"length": "32"
},
"failure_code": {
"position": "72",
"length": "2"
}
}
},
"commitment_signed": {
"type": "132",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"signature": {
"position": "32",
"length": "64"
},
"num_htlcs": {
"position": "96",
"length": "2"
},
"htlc_signature": {
"position": "98",
"length": "num_htlcs*64"
}
}
},
"revoke_and_ack": {
"type": "133",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"per_commitment_secret": {
"position": "32",
"length": "32"
},
"next_per_commitment_point": {
"position": "64",
"length": "33"
}
}
},
"update_fee": {
"type": "134",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"feerate_per_kw": {
"position": "32",
"length": "4"
}
}
},
"channel_reestablish": {
"type": "136",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"next_local_commitment_number": {
"position": "32",
"length": "8"
},
"next_remote_revocation_number": {
"position": "40",
"length": "8"
},
"your_last_per_commitment_secret": {
"position": "48",
"length": "32",
"feature": "option_data_loss_protect"
},
"my_current_per_commitment_point": {
"position": "80",
"length": "33",
"feature": "option_data_loss_protect"
}
}
},
"invalid_realm": {
"type": "PERM|1",
"payload": {}
},
"temporary_node_failure": {
"type": "NODE|2",
"payload": {}
},
"permanent_node_failure": {
"type": "PERM|NODE|2",
"payload": {}
},
"required_node_feature_missing": {
"type": "PERM|NODE|3",
"payload": {}
},
"invalid_onion_version": {
"type": "BADONION|PERM|4",
"payload": {
"sha256_of_onion": {
"position": "0",
"length": "32"
}
}
},
"invalid_onion_hmac": {
"type": "BADONION|PERM|5",
"payload": {
"sha256_of_onion": {
"position": "0",
"length": "32"
}
}
},
"invalid_onion_key": {
"type": "BADONION|PERM|6",
"payload": {
"sha256_of_onion": {
"position": "0",
"length": "32"
}
}
},
"temporary_channel_failure": {
"type": "UPDATE|7",
"payload": {
"len": {
"position": "0",
"length": "2"
},
"channel_update": {
"position": "2",
"length": "len"
}
}
},
"permanent_channel_failure": {
"type": "PERM|8",
"payload": {}
},
"required_channel_feature_missing": {
"type": "PERM|9",
"payload": {}
},
"unknown_next_peer": {
"type": "PERM|10",
"payload": {}
},
"amount_below_minimum": {
"type": "UPDATE|11",
"payload": {
"htlc_msat": {
"position": "0",
"length": "8"
},
"len": {
"position": "8",
"length": "2"
},
"channel_update": {
"position": "10",
"length": "len"
}
}
},
"fee_insufficient": {
"type": "UPDATE|12",
"payload": {
"htlc_msat": {
"position": "0",
"length": "8"
},
"len": {
"position": "8",
"length": "2"
},
"channel_update": {
"position": "10",
"length": "len"
}
}
},
"incorrect_cltv_expiry": {
"type": "UPDATE|13",
"payload": {
"cltv_expiry": {
"position": "0",
"length": "4"
},
"len": {
"position": "4",
"length": "2"
},
"channel_update": {
"position": "6",
"length": "len"
}
}
},
"expiry_too_soon": {
"type": "UPDATE|14",
"payload": {
"len": {
"position": "0",
"length": "2"
},
"channel_update": {
"position": "2",
"length": "len"
}
}
},
"unknown_payment_hash": {
"type": "PERM|15",
"payload": {}
},
"incorrect_payment_amount": {
"type": "PERM|16",
"payload": {}
},
"final_expiry_too_soon": {
"type": "17",
"payload": {}
},
"final_incorrect_cltv_expiry": {
"type": "18",
"payload": {
"cltv_expiry": {
"position": "0",
"length": "4"
}
}
},
"final_incorrect_htlc_amount": {
"type": "19",
"payload": {
"incoming_htlc_amt": {
"position": "0",
"length": "4"
}
}
},
"channel_disabled": {
"type": "UPDATE|20",
"payload": {}
},
"expiry_too_far": {
"type": "21",
"payload": {}
},
"announcement_signatures": {
"type": "259",
"payload": {
"channel_id": {
"position": "0",
"length": "32"
},
"short_channel_id": {
"position": "32",
"length": "8"
},
"node_signature": {
"position": "40",
"length": "64"
},
"bitcoin_signature": {
"position": "104",
"length": "64"
}
}
},
"channel_announcement": {
"type": "256",
"payload": {
"node_signature_1": {
"position": "0",
"length": "64"
},
"node_signature_2": {
"position": "64",
"length": "64"
},
"bitcoin_signature_1": {
"position": "128",
"length": "64"
},
"bitcoin_signature_2": {
"position": "192",
"length": "64"
},
"len": {
"position": "256",
"length": "2"
},
"features": {
"position": "258",
"length": "len"
},
"chain_hash": {
"position": "258+len",
"length": "32"
},
"short_channel_id": {
"position": "290+len",
"length": "8"
},
"node_id_1": {
"position": "298+len",
"length": "33"
},
"node_id_2": {
"position": "331+len",
"length": "33"
},
"bitcoin_key_1": {
"position": "364+len",
"length": "33"
},
"bitcoin_key_2": {
"position": "397+len",
"length": "33"
}
}
},
"node_announcement": {
"type": "257",
"payload": {
"signature": {
"position": "0",
"length": "64"
},
"flen": {
"position": "64",
"length": "2"
},
"features": {
"position": "66",
"length": "flen"
},
"timestamp": {
"position": "66+flen",
"length": "4"
},
"node_id": {
"position": "70+flen",
"length": "33"
},
"rgb_color": {
"position": "103+flen",
"length": "3"
},
"alias": {
"position": "106+flen",
"length": "32"
},
"addrlen": {
"position": "138+flen",
"length": "2"
},
"addresses": {
"position": "140+flen",
"length": "addrlen"
}
}
},
"channel_update": {
"type": "258",
"payload": {
"signature": {
"position": "0",
"length": "64"
},
"chain_hash": {
"position": "64",
"length": "32"
},
"short_channel_id": {
"position": "96",
"length": "8"
},
"timestamp": {
"position": "104",
"length": "4"
},
"flags": {
"position": "108",
"length": "2"
},
"cltv_expiry_delta": {
"position": "110",
"length": "2"
},
"htlc_minimum_msat": {
"position": "112",
"length": "8"
},
"fee_base_msat": {
"position": "120",
"length": "4"
},
"fee_proportional_millionths": {
"position": "124",
"length": "4"
}
}
}
}

View File

@ -0,0 +1 @@
This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23

393
lib/lightning_payencode/lnaddr.py Executable file
View File

@ -0,0 +1,393 @@
#! /usr/bin/env python3
import ecdsa.curves
from ecdsa.ecdsa import generator_secp256k1
from ..bitcoin import hash160_to_b58_address, b58_address_to_hash160
from hashlib import sha256
from ..segwit_addr import bech32_encode, bech32_decode, CHARSET
from binascii import hexlify, unhexlify
from bitstring import BitArray
from decimal import Decimal
from .. import constants
from .. import ecc
import bitstring
import hashlib
import re
import time
# BOLT #11:
#
# A writer MUST encode `amount` as a positive decimal integer with no
# leading zeroes, SHOULD use the shortest representation possible.
def shorten_amount(amount):
""" Given an amount in bitcoin, shorten it
"""
# Convert to pico initially
amount = int(amount * 10**12)
units = ['p', 'n', 'u', 'm', '']
for unit in units:
if amount % 1000 == 0:
amount //= 1000
else:
break
return str(amount) + unit
def unshorten_amount(amount):
""" Given a shortened amount, convert it into a decimal
"""
# BOLT #11:
# The following `multiplier` letters are defined:
#
#* `m` (milli): multiply by 0.001
#* `u` (micro): multiply by 0.000001
#* `n` (nano): multiply by 0.000000001
#* `p` (pico): multiply by 0.000000000001
units = {
'p': 10**12,
'n': 10**9,
'u': 10**6,
'm': 10**3,
}
unit = str(amount)[-1]
# BOLT #11:
# A reader SHOULD fail if `amount` contains a non-digit, or is followed by
# anything except a `multiplier` in the table above.
if not re.fullmatch("\d+[pnum]?", str(amount)):
raise ValueError("Invalid amount '{}'".format(amount))
if unit in units.keys():
return Decimal(amount[:-1]) / units[unit]
else:
return Decimal(amount)
# Bech32 spits out array of 5-bit values. Shim here.
def u5_to_bitarray(arr):
ret = bitstring.BitArray()
for a in arr:
ret += bitstring.pack("uint:5", a)
return ret
def bitarray_to_u5(barr):
assert barr.len % 5 == 0
ret = []
s = bitstring.ConstBitStream(barr)
while s.pos != s.len:
ret.append(s.read(5).uint)
return ret
def encode_fallback(fallback, currency):
""" Encode all supported fallback addresses.
"""
if currency == 'bc' or currency == 'tb':
fbhrp, witness = bech32_decode(fallback, ignore_long_length=True)
if fbhrp:
if fbhrp != currency:
raise ValueError("Not a bech32 address for this currency")
wver = witness[0]
if wver > 16:
raise ValueError("Invalid witness version {}".format(witness[0]))
wprog = u5_to_bitarray(witness[1:])
else:
addrtype, addr = b58_address_to_hash160(fallback)
if is_p2pkh(currency, addrtype):
wver = 17
elif is_p2sh(currency, addrtype):
wver = 18
else:
raise ValueError("Unknown address type for {}".format(currency))
wprog = addr
return tagged('f', bitstring.pack("uint:5", wver) + wprog)
else:
raise NotImplementedError("Support for currency {} not implemented".format(currency))
def parse_fallback(fallback, currency):
if currency == 'bc' or currency == 'tb':
wver = fallback[0:5].uint
if wver == 17:
addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][0])
elif wver == 18:
addr=hash160_to_b58_address(fallback[5:].tobytes(), base58_prefix_map[currency][1])
elif wver <= 16:
addr=bech32_encode(currency, bitarray_to_u5(fallback))
else:
return None
else:
addr=fallback.tobytes()
return addr
# Map of classical and witness address prefixes
base58_prefix_map = {
'bc' : (0, 5),
'tb' : (111, 196)
}
def is_p2pkh(currency, prefix):
return prefix == base58_prefix_map[currency][0]
def is_p2sh(currency, prefix):
return prefix == base58_prefix_map[currency][1]
# Tagged field containing BitArray
def tagged(char, l):
# Tagged fields need to be zero-padded to 5 bits.
while l.len % 5 != 0:
l.append('0b0')
return bitstring.pack("uint:5, uint:5, uint:5",
CHARSET.find(char),
(l.len / 5) / 32, (l.len / 5) % 32) + l
# Tagged field containing bytes
def tagged_bytes(char, l):
return tagged(char, bitstring.BitArray(l))
# Discard trailing bits, convert to bytes.
def trim_to_bytes(barr):
# Adds a byte if necessary.
b = barr.tobytes()
if barr.len % 8 != 0:
return b[:-1]
return b
# Try to pull out tagged data: returns tag, tagged data and remainder.
def pull_tagged(stream):
tag = stream.read(5).uint
length = stream.read(5).uint * 32 + stream.read(5).uint
return (CHARSET[tag], stream.read(length * 5), stream)
def lnencode(addr, privkey):
if addr.amount:
amount = Decimal(str(addr.amount))
# We can only send down to millisatoshi.
if amount * 10**12 % 10:
raise ValueError("Cannot encode {}: too many decimal places".format(
addr.amount))
amount = addr.currency + shorten_amount(amount)
else:
amount = addr.currency if addr.currency else ''
hrp = 'ln' + amount
# Start with the timestamp
data = bitstring.pack('uint:35', addr.date)
# Payment hash
data += tagged_bytes('p', addr.paymenthash)
tags_set = set()
for k, v in addr.tags:
# BOLT #11:
#
# A writer MUST NOT include more than one `d`, `h`, `n` or `x` fields,
if k in ('d', 'h', 'n', 'x'):
if k in tags_set:
raise ValueError("Duplicate '{}' tag".format(k))
if k == 'r':
route = bitstring.BitArray()
for step in v:
pubkey, channel, feebase, feerate, cltv = step
route.append(bitstring.BitArray(pubkey) + bitstring.BitArray(channel) + bitstring.pack('intbe:32', feebase) + bitstring.pack('intbe:32', feerate) + bitstring.pack('intbe:16', cltv))
data += tagged('r', route)
elif k == 'f':
data += encode_fallback(v, addr.currency)
elif k == 'd':
data += tagged_bytes('d', v.encode())
elif k == 'x':
# Get minimal length by trimming leading 5 bits at a time.
expirybits = bitstring.pack('intbe:64', v)[4:64]
while expirybits.startswith('0b00000'):
expirybits = expirybits[5:]
data += tagged('x', expirybits)
elif k == 'h':
data += tagged_bytes('h', sha256(v.encode('utf-8')).digest())
elif k == 'n':
data += tagged_bytes('n', v)
else:
# FIXME: Support unknown tags?
raise ValueError("Unknown tag {}".format(k))
tags_set.add(k)
# BOLT #11:
#
# A writer MUST include either a `d` or `h` field, and MUST NOT include
# both.
if 'd' in tags_set and 'h' in tags_set:
raise ValueError("Cannot include both 'd' and 'h'")
if not 'd' in tags_set and not 'h' in tags_set:
raise ValueError("Must include either 'd' or 'h'")
# We actually sign the hrp, then data (padded to 8 bits with zeroes).
msg = hrp.encode("ascii") + data.tobytes()
privkey = ecc.ECPrivkey(privkey)
sig = privkey.sign_message(msg, is_compressed=False, algo=lambda x:sha256(x).digest())
recovery_flag = bytes([sig[0] - 27])
sig = bytes(sig[1:]) + recovery_flag
data += sig
return bech32_encode(hrp, bitarray_to_u5(data))
class LnAddr(object):
def __init__(self, paymenthash=None, amount=None, currency=None, tags=None, date=None):
self.date = int(time.time()) if not date else int(date)
self.tags = [] if not tags else tags
self.unknown_tags = []
self.paymenthash=paymenthash
self.signature = None
self.pubkey = None
self.currency = constants.net.SEGWIT_HRP if currency is None else currency
self.amount = amount
self.min_final_cltv_expiry = 9
def __str__(self):
return "LnAddr[{}, amount={}{} tags=[{}]]".format(
hexlify(self.pubkey.serialize()).decode('utf-8'),
self.amount, self.currency,
", ".join([k + '=' + str(v) for k, v in self.tags])
)
def lndecode(a, verbose=False, expected_hrp=constants.net.SEGWIT_HRP):
hrp, data = bech32_decode(a, ignore_long_length=True)
if not hrp:
raise ValueError("Bad bech32 checksum")
# BOLT #11:
#
# A reader MUST fail if it does not understand the `prefix`.
if not hrp.startswith('ln'):
raise ValueError("Does not start with ln")
if not hrp[2:].startswith(expected_hrp):
raise ValueError("Wrong Lightning invoice HRP " + hrp[2:] + ", should be " + expected_hrp)
data = u5_to_bitarray(data);
# Final signature 65 bytes, split it off.
if len(data) < 65*8:
raise ValueError("Too short to contain signature")
sigdecoded = data[-65*8:].tobytes()
data = bitstring.ConstBitStream(data[:-65*8])
addr = LnAddr()
addr.pubkey = None
m = re.search("[^\d]+", hrp[2:])
if m:
addr.currency = m.group(0)
amountstr = hrp[2+m.end():]
# BOLT #11:
#
# A reader SHOULD indicate if amount is unspecified, otherwise it MUST
# multiply `amount` by the `multiplier` value (if any) to derive the
# amount required for payment.
if amountstr != '':
addr.amount = unshorten_amount(amountstr)
addr.date = data.read(35).uint
while data.pos != data.len:
tag, tagdata, data = pull_tagged(data)
# BOLT #11:
#
# A reader MUST skip over unknown fields, an `f` field with unknown
# `version`, or a `p`, `h`, or `n` field which does not have
# `data_length` 52, 52, or 53 respectively.
data_length = len(tagdata) / 5
if tag == 'r':
# BOLT #11:
#
# * `r` (3): `data_length` variable. One or more entries
# containing extra routing information for a private route;
# there may be more than one `r` field, too.
# * `pubkey` (264 bits)
# * `short_channel_id` (64 bits)
# * `feebase` (32 bits, big-endian)
# * `feerate` (32 bits, big-endian)
# * `cltv_expiry_delta` (16 bits, big-endian)
route=[]
s = bitstring.ConstBitStream(tagdata)
while s.pos + 264 + 64 + 32 + 32 + 16 < s.len:
route.append((s.read(264).tobytes(),
s.read(64).tobytes(),
s.read(32).intbe,
s.read(32).intbe,
s.read(16).intbe))
addr.tags.append(('r',route))
elif tag == 'f':
fallback = parse_fallback(tagdata, addr.currency)
if fallback:
addr.tags.append(('f', fallback))
else:
# Incorrect version.
addr.unknown_tags.append((tag, tagdata))
continue
elif tag == 'd':
addr.tags.append(('d', trim_to_bytes(tagdata).decode('utf-8')))
elif tag == 'h':
if data_length != 52:
addr.unknown_tags.append((tag, tagdata))
continue
addr.tags.append(('h', trim_to_bytes(tagdata)))
elif tag == 'x':
addr.tags.append(('x', tagdata.uint))
elif tag == 'p':
if data_length != 52:
addr.unknown_tags.append((tag, tagdata))
continue
addr.paymenthash = trim_to_bytes(tagdata)
elif tag == 'n':
if data_length != 53:
addr.unknown_tags.append((tag, tagdata))
continue
pubkeybytes = trim_to_bytes(tagdata)
addr.pubkey = pubkeybytes
elif tag == 'c':
addr.min_final_cltv_expiry = tagdata.int
else:
addr.unknown_tags.append((tag, tagdata))
if verbose:
print('hex of signature data (32 byte r, 32 byte s): {}'
.format(hexlify(sigdecoded[0:64])))
print('recovery flag: {}'.format(sigdecoded[64]))
print('hex of data for signing: {}'
.format(hexlify(hrp.encode("ascii") + data.tobytes())))
print('SHA256 of above: {}'.format(sha256(hrp.encode("ascii") + data.tobytes()).hexdigest()))
# BOLT #11:
#
# A reader MUST check that the `signature` is valid (see the `n` tagged
# field specified below).
addr.signature = sigdecoded[:65]
hrp_hash = sha256(hrp.encode("ascii") + data.tobytes()).digest()
if addr.pubkey: # Specified by `n`
# BOLT #11:
#
# A reader MUST use the `n` field to validate the signature instead of
# performing signature recovery if a valid `n` field is provided.
ecc.ECPubkey(addr.pubkey).verify_message_hash(sigdecoded[:64], hrp_hash)
pubkey_copy = addr.pubkey
class WrappedBytesKey:
serialize = lambda: pubkey_copy
addr.pubkey = WrappedBytesKey
else: # Recover pubkey from signature.
addr.pubkey = SerializableKey(ecc.ECPubkey.from_sig_string(sigdecoded[:64], sigdecoded[64], hrp_hash))
return addr
class SerializableKey:
def __init__(self, pubkey):
self.pubkey = pubkey
def serialize(self):
return self.pubkey.get_public_key_bytes(True)

1336
lib/lnbase.py Normal file

File diff suppressed because it is too large Load Diff

397
lib/lnhtlc.py Normal file
View File

@ -0,0 +1,397 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
from ecdsa.util import sigencode_string_canonize, sigdecode_der
from .util import bfh, PrintError
from .bitcoin import Hash
from collections import namedtuple
from ecdsa.curves import SECP256k1
from .crypto import sha256
from . import ecc
SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
class UpdateAddHtlc:
def __init__(self, amount_msat, payment_hash, cltv_expiry, total_fee):
self.amount_msat = amount_msat
self.payment_hash = payment_hash
self.cltv_expiry = cltv_expiry
self.total_fee = total_fee
# the height the htlc was locked in at, or None
self.r_locked_in = None
self.l_locked_in = None
self.htlc_id = None
def as_tuple(self):
return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.r_locked_in, self.l_locked_in, self.total_fee)
def __hash__(self):
return hash(self.as_tuple())
def __eq__(self, o):
return type(o) is UpdateAddHtlc and self.as_tuple() == o.as_tuple()
def __repr__(self):
return "UpdateAddHtlc" + str(self.as_tuple())
class HTLCStateMachine(PrintError):
def lookup_htlc(self, log, htlc_id):
assert type(htlc_id) is int
for htlc in log:
if type(htlc) is not UpdateAddHtlc: continue
if htlc.htlc_id == htlc_id:
return htlc
assert False, self.diagnostic_name() + ": htlc_id {} not found in {}".format(htlc_id, log)
def diagnostic_name(self):
return str(self.name)
def __init__(self, state, name = None):
self.state = state
self.local_update_log = []
self.remote_update_log = []
self.name = name
self.total_msat_sent = 0
self.total_msat_received = 0
def add_htlc(self, htlc):
"""
AddHTLC adds an HTLC to the state machine's local update log. This method
should be called when preparing to send an outgoing HTLC.
"""
assert type(htlc) is UpdateAddHtlc
self.local_update_log.append(htlc)
self.print_error("add_htlc")
htlc_id = self.state.local_state.next_htlc_id
self.state = self.state._replace(local_state=self.state.local_state._replace(next_htlc_id=htlc_id + 1))
htlc.htlc_id = htlc_id
return htlc_id
def receive_htlc(self, htlc):
"""
ReceiveHTLC adds an HTLC to the state machine's remote update log. This
method should be called in response to receiving a new HTLC from the remote
party.
"""
assert type(htlc) is UpdateAddHtlc
self.remote_update_log.append(htlc)
self.print_error("receive_htlc")
htlc_id = self.state.remote_state.next_htlc_id
self.state = self.state._replace(remote_state=self.state.remote_state._replace(next_htlc_id=htlc_id + 1))
htlc.htlc_id = htlc_id
return htlc_id
def sign_next_commitment(self):
"""
SignNextCommitment signs a new commitment which includes any previous
unsettled HTLCs, any new HTLCs, and any modifications to prior HTLCs
committed in previous commitment updates. Signing a new commitment
decrements the available revocation window by 1. After a successful method
call, the remote party's commitment chain is extended by a new commitment
which includes all updates to the HTLC log prior to this method invocation.
The first return parameter is the signature for the commitment transaction
itself, while the second parameter is a slice of all HTLC signatures (if
any). The HTLC signatures are sorted according to the BIP 69 order of the
HTLC's on the commitment transaction.
"""
from .lnbase import sign_and_get_sig_string, derive_privkey, make_htlc_tx_with_open_channel
for htlc in self.local_update_log:
if not type(htlc) is UpdateAddHtlc: continue
if htlc.l_locked_in is None: htlc.l_locked_in = self.state.local_state.ctn
self.print_error("sign_next_commitment")
sig_64 = sign_and_get_sig_string(self.remote_commitment, self.state.local_config, self.state.remote_config)
their_remote_htlc_privkey_number = derive_privkey(
int.from_bytes(self.state.local_config.htlc_basepoint.privkey, 'big'),
self.state.remote_state.next_per_commitment_point)
their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
for_us = False
htlcsigs = []
for we_receive, htlcs in zip([True, False], [self.htlcs_in_remote, self.htlcs_in_local]):
assert len(htlcs) <= 1
for htlc in htlcs:
original_htlc_output_index = 0
args = [self.state.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat + htlc.total_fee, htlc.cltv_expiry, htlc.payment_hash, self.remote_commitment, original_htlc_output_index]
htlc_tx = make_htlc_tx_with_open_channel(self.state, *args)
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
r, s = sigdecode_der(sig[:-1], SECP256k1.generator.order())
htlc_sig = sigencode_string_canonize(r, s, SECP256k1.generator.order())
htlcsigs.append(htlc_sig)
return sig_64, htlcsigs
def receive_new_commitment(self, sig, htlc_sigs):
"""
ReceiveNewCommitment process a signature for a new commitment state sent by
the remote party. This method should be called in response to the
remote party initiating a new change, or when the remote party sends a
signature fully accepting a new state we've initiated. If we are able to
successfully validate the signature, then the generated commitment is added
to our local commitment chain. Once we send a revocation for our prior
state, then this newly added commitment becomes our current accepted channel
state.
"""
from .lnbase import make_htlc_tx_with_open_channel , derive_pubkey
self.print_error("receive_new_commitment")
for htlc in self.remote_update_log:
if not type(htlc) is UpdateAddHtlc: continue
if htlc.r_locked_in is None: htlc.r_locked_in = self.state.remote_state.ctn
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
preimage_hex = self.local_commitment.serialize_preimage(0)
pre_hash = Hash(bfh(preimage_hex))
if not ecc.verify_signature(self.state.remote_config.multisig_key.pubkey, sig, pre_hash):
raise Exception('failed verifying signature of our updated commitment transaction: ' + str(sig))
_, this_point, _ = self.points
if len(self.htlcs_in_remote) > 0:
print("CHECKING HTLC SIGS")
assert len(self.local_commitment.outputs()) == 3 # TODO
we_receive = True
payment_hash = self.htlcs_in_remote[0].payment_hash
amount_msat = self.htlcs_in_remote[0].amount_msat
cltv_expiry = self.htlcs_in_remote[0].cltv_expiry
htlc_tx = make_htlc_tx_with_open_channel(self.state, this_point, True, we_receive, amount_msat, cltv_expiry, payment_hash, self.local_commitment, 0)
pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.state.remote_config.htlc_basepoint.pubkey, this_point)
if not ecc.verify_signature(remote_htlc_pubkey, htlc_sigs[0], pre_hash):
raise Exception("failed verifying signature an HTLC tx spending from one of our commit tx'es HTLC outputs")
# TODO check htlc in htlcs_in_local
def revoke_current_commitment(self):
"""
RevokeCurrentCommitment revokes the next lowest unrevoked commitment
transaction in the local commitment chain. As a result the edge of our
revocation window is extended by one, and the tail of our local commitment
chain is advanced by a single commitment. This now lowest unrevoked
commitment becomes our currently accepted state within the channel. This
method also returns the set of HTLC's currently active within the commitment
transaction. This return value allows callers to act once an HTLC has been
locked into our commitment transaction.
"""
self.print_error("revoke_current_commitment")
last_secret, this_point, next_point = self.points
self.state = self.state._replace(
local_state=self.state.local_state._replace(
ctn=self.state.local_state.ctn + 1
)
)
return RevokeAndAck(last_secret, next_point), "current htlcs"
@property
def points(self):
from .lnbase import get_per_commitment_secret_from_seed, secret_to_pubkey
chan = self.state
last_small_num = chan.local_state.ctn
next_small_num = last_small_num + 2
this_small_num = last_small_num + 1
last_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-last_small_num-1)
this_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-this_small_num-1)
this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big'))
next_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-next_small_num-1)
next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big'))
return last_secret, this_point, next_point
def receive_revocation(self, revocation):
"""
ReceiveRevocation processes a revocation sent by the remote party for the
lowest unrevoked commitment within their commitment chain. We receive a
revocation either during the initial session negotiation wherein revocation
windows are extended, or in response to a state update that we initiate. If
successful, then the remote commitment chain is advanced by a single
commitment, and a log compaction is attempted.
Returns the forwarding package corresponding to the remote commitment height
that was revoked.
"""
self.print_error("receive_revocation")
settle_fails2 = []
for x in self.remote_update_log:
if type(x) is not SettleHtlc:
continue
settle_fails2.append(x)
sent_this_batch = 0
received_this_batch = 0
for x in settle_fails2:
htlc = self.lookup_htlc(self.local_update_log, x.htlc_id)
sent_this_batch += htlc.amount_msat
self.total_msat_sent += sent_this_batch
# increase received_msat counter for htlc's that have been settled
adds2 = self.gen_htlc_indices("remote")
for htlc in adds2:
htlc_id = htlc.htlc_id
if SettleHtlc(htlc_id) in self.local_update_log:
htlc = self.lookup_htlc(self.remote_update_log, htlc_id)
received_this_batch += htlc.amount_msat
self.total_msat_received += received_this_batch
# log compaction (remove entries relating to htlc's that have been settled)
to_remove = []
for x in filter(lambda x: type(x) is SettleHtlc, self.remote_update_log):
to_remove += [y for y in self.local_update_log if y.htlc_id == x.htlc_id]
if to_remove != []:
print("REMOVING")
# assert that we should have compacted the log earlier
assert len(to_remove) <= 1, to_remove
if len(to_remove) == 1:
self.remote_update_log = [x for x in self.remote_update_log if x.htlc_id != to_remove[0].htlc_id]
self.local_update_log = [x for x in self.local_update_log if x.htlc_id != to_remove[0].htlc_id]
to_remove = []
for x in filter(lambda x: type(x) is SettleHtlc, self.local_update_log):
to_remove += [y for y in self.remote_update_log if y.htlc_id == x.htlc_id]
assert len(to_remove) <= 1, to_remove
if len(to_remove) == 1:
self.local_update_log = [x for x in self.local_update_log if x.htlc_id != to_remove[0].htlc_id]
self.remote_update_log = [x for x in self.remote_update_log if x.htlc_id != to_remove[0].htlc_id]
self.state.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
next_point = self.state.remote_state.next_per_commitment_point
self.state = self.state._replace(
remote_state=self.state.remote_state._replace(
ctn=self.state.remote_state.ctn + 1,
last_per_commitment_point=next_point,
next_per_commitment_point=revocation.next_per_commitment_point,
amount_msat=self.state.remote_state.amount_msat + (sent_this_batch - received_this_batch)
),
local_state=self.state.local_state._replace(
amount_msat = self.state.local_state.amount_msat + (received_this_batch - sent_this_batch)
)
)
@staticmethod
def htlcsum(htlcs):
return sum(x.amount_msat for x in htlcs), sum(x.total_fee for x in htlcs)
@property
def remote_commitment(self):
from .lnbase import make_commitment_using_open_channel, make_received_htlc, make_offered_htlc, derive_pubkey, derive_blinded_pubkey
htlc_value_local, total_fee_local = self.htlcsum(self.htlcs_in_local)
htlc_value_remote, total_fee_remote = self.htlcsum(self.htlcs_in_remote)
local_msat = self.state.local_state.amount_msat -\
htlc_value_local
remote_msat = self.state.remote_state.amount_msat -\
htlc_value_remote
assert local_msat >= 0
assert remote_msat >= 0
this_point = self.state.remote_state.next_per_commitment_point
remote_htlc_pubkey = derive_pubkey(self.state.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
local_revocation_pubkey = derive_blinded_pubkey(self.state.local_config.revocation_basepoint.pubkey, this_point)
htlcs_in_local = []
for htlc in self.htlcs_in_local:
htlcs_in_local.append(
( make_received_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + total_fee_local))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
htlcs_in_remote.append(
( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + total_fee_remote))
commit = make_commitment_using_open_channel(self.state, self.state.remote_state.ctn + 1,
False, this_point,
remote_msat - total_fee_remote, local_msat - total_fee_local, htlcs_in_local + htlcs_in_remote)
return commit
@property
def local_commitment(self):
from .lnbase import make_commitment_using_open_channel, make_received_htlc, make_offered_htlc, derive_pubkey, derive_blinded_pubkey, get_per_commitment_secret_from_seed, secret_to_pubkey
htlc_value_local, total_fee_local = self.htlcsum(self.htlcs_in_local)
htlc_value_remote, total_fee_remote = self.htlcsum(self.htlcs_in_remote)
local_msat = self.state.local_state.amount_msat -\
htlc_value_local
remote_msat = self.state.remote_state.amount_msat -\
htlc_value_remote
assert local_msat >= 0
assert remote_msat >= 0
_, this_point, _ = self.points
remote_htlc_pubkey = derive_pubkey(self.state.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
remote_revocation_pubkey = derive_blinded_pubkey(self.state.remote_config.revocation_basepoint.pubkey, this_point)
htlcs_in_local = []
for htlc in self.htlcs_in_local:
htlcs_in_local.append(
( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + total_fee_local))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
htlcs_in_remote.append(
( make_received_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash, htlc.cltv_expiry), htlc.amount_msat + total_fee_remote))
commit = make_commitment_using_open_channel(self.state, self.state.local_state.ctn + 1,
True, this_point,
local_msat - total_fee_local, remote_msat - total_fee_remote, htlcs_in_local + htlcs_in_remote)
return commit
def gen_htlc_indices(self, subject):
assert subject in ["local", "remote"]
update_log = (self.remote_update_log if subject == "remote" else self.local_update_log)
res = []
for htlc in update_log:
if type(htlc) is not UpdateAddHtlc:
continue
height = (self.state.local_state.ctn if subject == "remote" else self.state.remote_state.ctn)
locked_in = (htlc.r_locked_in if subject == "remote" else htlc.l_locked_in)
if locked_in is None:
print("skipping", locked_in, height)
continue
res.append(htlc)
return res
@property
def htlcs_in_local(self):
return self.gen_htlc_indices("local")
@property
def htlcs_in_remote(self):
return self.gen_htlc_indices("remote")
def settle_htlc(self, preimage, htlc_id):
"""
SettleHTLC attempts to settle an existing outstanding received HTLC.
"""
htlc = self.lookup_htlc(self.remote_update_log, htlc_id)
assert htlc.payment_hash == sha256(preimage)
self.local_update_log.append(SettleHtlc(htlc_id))
def receive_htlc_settle(self, preimage, htlc_index):
htlc = self.lookup_htlc(self.local_update_log, htlc_index)
assert htlc.payment_hash == sha256(preimage)
assert len([x.htlc_id == htlc_index for x in self.local_update_log]) == 1
self.remote_update_log.append(SettleHtlc(htlc_index))
def fail_htlc(self, htlc):
# TODO
self.local_update_log = []
self.remote_update_log = []

523
lib/lnrouter.py Normal file
View File

@ -0,0 +1,523 @@
# -*- coding: utf-8 -*-
#
# Electrum - lightweight Bitcoin client
# Copyright (C) 2018 The Electrum developers
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation files
# (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge,
# publish, distribute, sublicense, and/or sell copies of the Software,
# and to permit persons to whom the Software is furnished to do so,
# subject to the following conditions:
#
# The above copyright notice and this permission notice shall be
# included in all copies or substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
# EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
# MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
# NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS
# BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import queue
import traceback
import sys
import binascii
import hashlib
import hmac
from collections import namedtuple, defaultdict
from typing import Sequence, Union, Tuple
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms
from cryptography.hazmat.backends import default_backend
from . import bitcoin
from . import ecc
from . import crypto
from .crypto import sha256
from .util import PrintError, bh2u, print_error, bfh, profiler, xor_bytes
from . import lnbase
class ChannelInfo(PrintError):
def __init__(self, channel_announcement_payload):
self.channel_id = channel_announcement_payload['short_channel_id']
self.node_id_1 = channel_announcement_payload['node_id_1']
self.node_id_2 = channel_announcement_payload['node_id_2']
assert type(self.node_id_1) is bytes
assert type(self.node_id_2) is bytes
assert list(sorted([self.node_id_1, self.node_id_2])) == [self.node_id_1, self.node_id_2]
self.capacity_sat = None
self.policy_node1 = None
self.policy_node2 = None
def set_capacity(self, capacity):
# TODO call this after looking up UTXO for funding txn on chain
self.capacity_sat = capacity
def on_channel_update(self, msg_payload):
assert self.channel_id == msg_payload['short_channel_id']
flags = int.from_bytes(msg_payload['flags'], 'big')
direction = flags & 1
if direction == 0:
self.policy_node1 = ChannelInfoDirectedPolicy(msg_payload)
else:
self.policy_node2 = ChannelInfoDirectedPolicy(msg_payload)
self.print_error('channel update', binascii.hexlify(self.channel_id).decode("ascii"), flags)
def get_policy_for_node(self, node_id):
if node_id == self.node_id_1:
return self.policy_node1
elif node_id == self.node_id_2:
return self.policy_node2
else:
raise Exception('node_id {} not in channel {}'.format(node_id, self.channel_id))
class ChannelInfoDirectedPolicy:
def __init__(self, channel_update_payload):
self.cltv_expiry_delta = channel_update_payload['cltv_expiry_delta']
self.htlc_minimum_msat = channel_update_payload['htlc_minimum_msat']
self.fee_base_msat = channel_update_payload['fee_base_msat']
self.fee_proportional_millionths = channel_update_payload['fee_proportional_millionths']
self.cltv_expiry_delta = int.from_bytes(self.cltv_expiry_delta, "big")
self.htlc_minimum_msat = int.from_bytes(self.htlc_minimum_msat, "big")
self.fee_base_msat = int.from_bytes(self.fee_base_msat, "big")
self.fee_proportional_millionths = int.from_bytes(self.fee_proportional_millionths, "big")
class ChannelDB(PrintError):
def __init__(self):
self._id_to_channel_info = {}
self._channels_for_node = defaultdict(set) # node -> set(short_channel_id)
def get_channel_info(self, channel_id):
return self._id_to_channel_info.get(channel_id, None)
def get_channels_for_node(self, node_id):
"""Returns the set of channels that have node_id as one of the endpoints."""
return self._channels_for_node[node_id]
def on_channel_announcement(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
self.print_error('channel announcement', binascii.hexlify(short_channel_id).decode("ascii"))
channel_info = ChannelInfo(msg_payload)
self._id_to_channel_info[short_channel_id] = channel_info
self._channels_for_node[channel_info.node_id_1].add(short_channel_id)
self._channels_for_node[channel_info.node_id_2].add(short_channel_id)
def on_channel_update(self, msg_payload):
short_channel_id = msg_payload['short_channel_id']
try:
channel_info = self._id_to_channel_info[short_channel_id]
except KeyError:
self.print_error("could not find", short_channel_id)
else:
channel_info.on_channel_update(msg_payload)
def remove_channel(self, short_channel_id):
try:
channel_info = self._id_to_channel_info[short_channel_id]
except KeyError:
self.print_error('cannot find channel {}'.format(short_channel_id))
return
self._id_to_channel_info.pop(short_channel_id, None)
for node in (channel_info.node_id_1, channel_info.node_id_2):
try:
self._channels_for_node[node].remove(short_channel_id)
except KeyError:
pass
class RouteEdge:
def __init__(self, node_id: bytes, short_channel_id: bytes,
channel_policy: ChannelInfoDirectedPolicy):
# "if you travel through short_channel_id, you will reach node_id"
self.node_id = node_id
self.short_channel_id = short_channel_id
self.channel_policy = channel_policy
class LNPathFinder(PrintError):
def __init__(self, channel_db):
self.channel_db = channel_db
self.blacklist = set()
def _edge_cost(self, short_channel_id: bytes, start_node: bytes, payment_amt_msat: int,
ignore_cltv=False) -> float:
"""Heuristic cost of going through a channel.
direction: 0 or 1. --- 0 means node_id_1 -> node_id_2
"""
channel_info = self.channel_db.get_channel_info(short_channel_id)
if channel_info is None:
return float('inf')
channel_policy = channel_info.get_policy_for_node(start_node)
if channel_policy is None: return float('inf')
cltv_expiry_delta = channel_policy.cltv_expiry_delta
htlc_minimum_msat = channel_policy.htlc_minimum_msat
fee_base_msat = channel_policy.fee_base_msat
fee_proportional_millionths = channel_policy.fee_proportional_millionths
if payment_amt_msat is not None:
if payment_amt_msat < htlc_minimum_msat:
return float('inf') # payment amount too little
if channel_info.capacity_sat is not None and \
payment_amt_msat // 1000 > channel_info.capacity_sat:
return float('inf') # payment amount too large
amt = payment_amt_msat or 50000 * 1000 # guess for typical payment amount
fee_msat = fee_base_msat + amt * fee_proportional_millionths / 1000000
# TODO revise
# paying 10 more satoshis ~ waiting one more block
fee_cost = fee_msat / 1000 / 10
cltv_cost = cltv_expiry_delta if not ignore_cltv else 0
return cltv_cost + fee_cost + 1
@profiler
def find_path_for_payment(self, from_node_id: bytes, to_node_id: bytes,
amount_msat: int=None) -> Sequence[Tuple[bytes, bytes]]:
"""Return a path between from_node_id and to_node_id.
Returns a list of (node_id, short_channel_id) representing a path.
To get from node ret[n][0] to ret[n+1][0], use channel ret[n+1][1];
i.e. an element reads as, "to get to node_id, travel through short_channel_id"
"""
if amount_msat is not None: assert type(amount_msat) is int
# TODO find multiple paths??
# run Dijkstra
distance_from_start = defaultdict(lambda: float('inf'))
distance_from_start[from_node_id] = 0
prev_node = {}
nodes_to_explore = queue.PriorityQueue()
nodes_to_explore.put((0, from_node_id))
while nodes_to_explore.qsize() > 0:
dist_to_cur_node, cur_node = nodes_to_explore.get()
if cur_node == to_node_id:
break
if dist_to_cur_node != distance_from_start[cur_node]:
# queue.PriorityQueue does not implement decrease_priority,
# so instead of decreasing priorities, we add items again into the queue.
# so there are duplicates in the queue, that we discard now:
continue
for edge_channel_id in self.channel_db.get_channels_for_node(cur_node):
if edge_channel_id in self.blacklist: continue
channel_info = self.channel_db.get_channel_info(edge_channel_id)
node1, node2 = channel_info.node_id_1, channel_info.node_id_2
neighbour = node2 if node1 == cur_node else node1
ignore_cltv_delta_in_edge_cost = cur_node == from_node_id
edge_cost = self._edge_cost(edge_channel_id, cur_node, amount_msat,
ignore_cltv=ignore_cltv_delta_in_edge_cost)
alt_dist_to_neighbour = distance_from_start[cur_node] + edge_cost
if alt_dist_to_neighbour < distance_from_start[neighbour]:
distance_from_start[neighbour] = alt_dist_to_neighbour
prev_node[neighbour] = cur_node, edge_channel_id
nodes_to_explore.put((alt_dist_to_neighbour, neighbour))
else:
return None # no path found
# backtrack from end to start
cur_node = to_node_id
path = []
while cur_node != from_node_id:
prev_node_id, edge_taken = prev_node[cur_node]
path += [(cur_node, edge_taken)]
cur_node = prev_node_id
path.reverse()
return path
def create_route_from_path(self, path, from_node_id: bytes) -> Sequence[RouteEdge]:
assert type(from_node_id) is bytes
if path is None:
raise Exception('cannot create route from None path')
route = []
prev_node_id = from_node_id
for node_id, short_channel_id in path:
channel_info = self.channel_db.get_channel_info(short_channel_id)
if channel_info is None:
raise Exception('cannot find channel info for short_channel_id: {}'.format(bh2u(short_channel_id)))
channel_policy = channel_info.get_policy_for_node(prev_node_id)
if channel_policy is None:
raise Exception('cannot find channel policy for short_channel_id: {}'.format(bh2u(short_channel_id)))
route.append(RouteEdge(node_id, short_channel_id, channel_policy))
prev_node_id = node_id
return route
# bolt 04, "onion" ----->
NUM_MAX_HOPS_IN_PATH = 20
HOPS_DATA_SIZE = 1300 # also sometimes called routingInfoSize in bolt-04
PER_HOP_FULL_SIZE = 65 # HOPS_DATA_SIZE / 20
NUM_STREAM_BYTES = HOPS_DATA_SIZE + PER_HOP_FULL_SIZE
PER_HOP_HMAC_SIZE = 32
class UnsupportedOnionPacketVersion(Exception): pass
class InvalidOnionMac(Exception): pass
class OnionPerHop:
def __init__(self, short_channel_id: bytes, amt_to_forward: bytes, outgoing_cltv_value: bytes):
self.short_channel_id = short_channel_id
self.amt_to_forward = amt_to_forward
self.outgoing_cltv_value = outgoing_cltv_value
def to_bytes(self) -> bytes:
ret = self.short_channel_id
ret += self.amt_to_forward
ret += self.outgoing_cltv_value
ret += bytes(12) # padding
if len(ret) != 32:
raise Exception('unexpected length {}'.format(len(ret)))
return ret
@classmethod
def from_bytes(cls, b: bytes):
if len(b) != 32:
raise Exception('unexpected length {}'.format(len(b)))
return OnionPerHop(
short_channel_id=b[:8],
amt_to_forward=b[8:16],
outgoing_cltv_value=b[16:20]
)
class OnionHopsDataSingle: # called HopData in lnd
def __init__(self, per_hop: OnionPerHop = None):
self.realm = 0
self.per_hop = per_hop
self.hmac = None
def to_bytes(self) -> bytes:
ret = bytes([self.realm])
ret += self.per_hop.to_bytes()
ret += self.hmac if self.hmac is not None else bytes(PER_HOP_HMAC_SIZE)
if len(ret) != PER_HOP_FULL_SIZE:
raise Exception('unexpected length {}'.format(len(ret)))
return ret
@classmethod
def from_bytes(cls, b: bytes):
if len(b) != PER_HOP_FULL_SIZE:
raise Exception('unexpected length {}'.format(len(b)))
ret = OnionHopsDataSingle()
ret.realm = b[0]
if ret.realm != 0:
raise Exception('only realm 0 is supported')
ret.per_hop = OnionPerHop.from_bytes(b[1:33])
ret.hmac = b[33:]
return ret
class OnionPacket:
def __init__(self, public_key: bytes, hops_data: bytes, hmac: bytes):
self.version = 0
self.public_key = public_key
self.hops_data = hops_data # also called RoutingInfo in bolt-04
self.hmac = hmac
def to_bytes(self) -> bytes:
ret = bytes([self.version])
ret += self.public_key
ret += self.hops_data
ret += self.hmac
if len(ret) != 1366:
raise Exception('unexpected length {}'.format(len(ret)))
return ret
@classmethod
def from_bytes(cls, b: bytes):
if len(b) != 1366:
raise Exception('unexpected length {}'.format(len(b)))
version = b[0]
if version != 0:
raise UnsupportedOnionPacketVersion('version {} is not supported'.format(version))
return OnionPacket(
public_key=b[1:34],
hops_data=b[34:1334],
hmac=b[1334:]
)
def get_bolt04_onion_key(key_type: bytes, secret: bytes) -> bytes:
if key_type not in (b'rho', b'mu', b'um', b'ammag'):
raise Exception('invalid key_type {}'.format(key_type))
key = hmac.new(key_type, msg=secret, digestmod=hashlib.sha256).digest()
return key
def get_shared_secrets_along_route(payment_path_pubkeys: Sequence[bytes],
session_key: bytes) -> Sequence[bytes]:
num_hops = len(payment_path_pubkeys)
hop_shared_secrets = num_hops * [b'']
ephemeral_key = session_key
# compute shared key for each hop
for i in range(0, num_hops):
hop_shared_secrets[i] = lnbase.get_ecdh(ephemeral_key, payment_path_pubkeys[i])
ephemeral_pubkey = ecc.ECPrivkey(ephemeral_key).get_public_key_bytes()
blinding_factor = sha256(ephemeral_pubkey + hop_shared_secrets[i])
blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
ephemeral_key_int = int.from_bytes(ephemeral_key, byteorder="big")
ephemeral_key_int = ephemeral_key_int * blinding_factor_int % ecc.CURVE_ORDER
ephemeral_key = ephemeral_key_int.to_bytes(32, byteorder="big")
return hop_shared_secrets
def new_onion_packet(payment_path_pubkeys: Sequence[bytes], session_key: bytes,
hops_data: Sequence[OnionHopsDataSingle], associated_data: bytes) -> OnionPacket:
num_hops = len(payment_path_pubkeys)
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
filler = generate_filler(b'rho', num_hops, PER_HOP_FULL_SIZE, hop_shared_secrets)
mix_header = bytes(HOPS_DATA_SIZE)
next_hmac = bytes(PER_HOP_HMAC_SIZE)
# compute routing info and MAC for each hop
for i in range(num_hops-1, -1, -1):
rho_key = get_bolt04_onion_key(b'rho', hop_shared_secrets[i])
mu_key = get_bolt04_onion_key(b'mu', hop_shared_secrets[i])
hops_data[i].hmac = next_hmac
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
mix_header = mix_header[:-PER_HOP_FULL_SIZE]
mix_header = hops_data[i].to_bytes() + mix_header
mix_header = xor_bytes(mix_header, stream_bytes)
if i == num_hops - 1 and len(filler) != 0:
mix_header = mix_header[:-len(filler)] + filler
packet = mix_header + associated_data
next_hmac = hmac.new(mu_key, msg=packet, digestmod=hashlib.sha256).digest()
return OnionPacket(
public_key=ecc.ECPrivkey(session_key).get_public_key_bytes(),
hops_data=mix_header,
hmac=next_hmac)
def generate_filler(key_type: bytes, num_hops: int, hop_size: int,
shared_secrets: Sequence[bytes]) -> bytes:
filler_size = (NUM_MAX_HOPS_IN_PATH + 1) * hop_size
filler = bytearray(filler_size)
for i in range(0, num_hops-1): # -1, as last hop does not obfuscate
filler = filler[hop_size:]
filler += bytearray(hop_size)
stream_key = get_bolt04_onion_key(key_type, shared_secrets[i])
stream_bytes = generate_cipher_stream(stream_key, filler_size)
filler = xor_bytes(filler, stream_bytes)
return filler[(NUM_MAX_HOPS_IN_PATH-num_hops+2)*hop_size:]
def generate_cipher_stream(stream_key: bytes, num_bytes: int) -> bytes:
algo = algorithms.ChaCha20(stream_key, nonce=bytes(16))
cipher = Cipher(algo, mode=None, backend=default_backend())
encryptor = cipher.encryptor()
return encryptor.update(bytes(num_bytes))
ProcessedOnionPacket = namedtuple("ProcessedOnionPacket", ["are_we_final", "hop_data", "next_packet"])
# TODO replay protection
def process_onion_packet(onion_packet: OnionPacket, associated_data: bytes,
our_onion_private_key: bytes) -> ProcessedOnionPacket:
shared_secret = lnbase.get_ecdh(our_onion_private_key, onion_packet.public_key)
# check message integrity
mu_key = get_bolt04_onion_key(b'mu', shared_secret)
calculated_mac = hmac.new(mu_key, msg=onion_packet.hops_data+associated_data,
digestmod=hashlib.sha256).digest()
if onion_packet.hmac != calculated_mac:
raise InvalidOnionMac()
# peel an onion layer off
rho_key = get_bolt04_onion_key(b'rho', shared_secret)
stream_bytes = generate_cipher_stream(rho_key, NUM_STREAM_BYTES)
padded_header = onion_packet.hops_data + bytes(PER_HOP_FULL_SIZE)
next_hops_data = xor_bytes(padded_header, stream_bytes)
# calc next ephemeral key
blinding_factor = sha256(onion_packet.public_key + shared_secret)
blinding_factor_int = int.from_bytes(blinding_factor, byteorder="big")
next_public_key_int = ecc.ECPubkey(onion_packet.public_key) * blinding_factor_int
next_public_key = next_public_key_int.get_public_key_bytes()
hop_data = OnionHopsDataSingle.from_bytes(next_hops_data[:PER_HOP_FULL_SIZE])
next_onion_packet = OnionPacket(
public_key=next_public_key,
hops_data=next_hops_data[PER_HOP_FULL_SIZE:],
hmac=hop_data.hmac
)
if hop_data.hmac == bytes(PER_HOP_HMAC_SIZE):
# we are the destination / exit node
are_we_final = True
else:
# we are an intermediate node; forwarding
are_we_final = False
return ProcessedOnionPacket(are_we_final, hop_data, next_onion_packet)
class FailedToDecodeOnionError(Exception): pass
class OnionRoutingFailureMessage:
def __init__(self, code: int, data: bytes):
self.code = code
self.data = data
def __repr__(self):
return repr((self.code, self.data))
def _decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
session_key: bytes) -> (bytes, int):
"""Returns the decoded error bytes, and the index of the sender of the error."""
num_hops = len(payment_path_pubkeys)
hop_shared_secrets = get_shared_secrets_along_route(payment_path_pubkeys, session_key)
for i in range(num_hops):
ammag_key = get_bolt04_onion_key(b'ammag', hop_shared_secrets[i])
um_key = get_bolt04_onion_key(b'um', hop_shared_secrets[i])
stream_bytes = generate_cipher_stream(ammag_key, len(error_packet))
error_packet = xor_bytes(error_packet, stream_bytes)
hmac_computed = hmac.new(um_key, msg=error_packet[32:], digestmod=hashlib.sha256).digest()
hmac_found = error_packet[:32]
if hmac_computed == hmac_found:
return error_packet, i
raise FailedToDecodeOnionError()
def decode_onion_error(error_packet: bytes, payment_path_pubkeys: Sequence[bytes],
session_key: bytes) -> (OnionRoutingFailureMessage, int):
"""Returns the failure message, and the index of the sender of the error."""
decrypted_error, sender_index = _decode_onion_error(error_packet, payment_path_pubkeys, session_key)
failure_msg = get_failure_msg_from_onion_error(decrypted_error)
return failure_msg, sender_index
def get_failure_msg_from_onion_error(decrypted_error_packet: bytes) -> OnionRoutingFailureMessage:
# get failure_msg bytes from error packet
failure_len = int.from_bytes(decrypted_error_packet[32:34], byteorder='big')
failure_msg = decrypted_error_packet[34:34+failure_len]
# create failure message object
failure_code = int.from_bytes(failure_msg[:2], byteorder='big')
failure_data = failure_msg[2:]
return OnionRoutingFailureMessage(failure_code, failure_data)
# <----- bolt 04, "onion"

244
lib/lnworker.py Normal file
View File

@ -0,0 +1,244 @@
import json
import binascii
import asyncio
import os
from decimal import Decimal
import threading
from collections import defaultdict
from . import constants
from .bitcoin import sha256, COIN, address_to_scripthash, redeem_script_to_address
from .util import bh2u, bfh, PrintError
from .constants import set_testnet, set_simnet
from .lnbase import Peer, Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, OpenChannel, ChannelConstraints, RevocationStore, calc_short_channel_id, privkey_to_pubkey, funding_output_script
from .lightning_payencode.lnaddr import lnencode, LnAddr, lndecode
from . import lnrouter
from .ecc import ECPrivkey, CURVE_ORDER, der_sig_from_sig_string
from .transaction import Transaction
is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
def maybeDecode(k, v):
if k in ["node_id", "channel_id", "short_channel_id", "pubkey", "privkey", "current_per_commitment_point", "next_per_commitment_point", "per_commitment_secret_seed", "current_commitment_signature"] and v is not None:
return binascii.unhexlify(v)
return v
def decodeAll(v):
return {i: maybeDecode(i, j) for i, j in v.items()} if isinstance(v, dict) else v
def typeWrap(k, v, local):
if is_key(k):
if local:
return Keypair(**v)
else:
return OnlyPubkeyKeypair(**v)
return v
def reconstruct_namedtuples(openingchannel):
openingchannel = decodeAll(openingchannel)
openingchannel=OpenChannel(**openingchannel)
openingchannel = openingchannel._replace(funding_outpoint=Outpoint(**openingchannel.funding_outpoint))
new_local_config = {k: typeWrap(k, decodeAll(v), True) for k, v in openingchannel.local_config.items()}
openingchannel = openingchannel._replace(local_config=ChannelConfig(**new_local_config))
new_remote_config = {k: typeWrap(k, decodeAll(v), False) for k, v in openingchannel.remote_config.items()}
openingchannel = openingchannel._replace(remote_config=ChannelConfig(**new_remote_config))
new_local_state = decodeAll(openingchannel.local_state)
openingchannel = openingchannel._replace(local_state=LocalState(**new_local_state))
new_remote_state = decodeAll(openingchannel.remote_state)
new_remote_state["revocation_store"] = RevocationStore.from_json_obj(new_remote_state["revocation_store"])
openingchannel = openingchannel._replace(remote_state=RemoteState(**new_remote_state))
openingchannel = openingchannel._replace(constraints=ChannelConstraints(**openingchannel.constraints))
return openingchannel
def serialize_channels(channels_dict):
serialized_channels = []
for chan in channels_dict.values():
namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()}
serialized_channels.append({k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in chan._asdict().items()})
class MyJsonEncoder(json.JSONEncoder):
def default(self, o):
if isinstance(o, bytes):
return binascii.hexlify(o).decode("ascii")
if isinstance(o, RevocationStore):
return o.serialize()
return super(MyJsonEncoder, self)
dumped = MyJsonEncoder().encode(serialized_channels)
roundtripped = json.loads(dumped)
reconstructed = set(reconstruct_namedtuples(x) for x in roundtripped)
if reconstructed != set(channels_dict.values()):
raise Exception("Channels did not roundtrip serialization without changes:\n" + repr(reconstructed) + "\n" + repr(channels))
return roundtripped
# hardcoded nodes
node_list = [
('ecdsa.net', '9735', '038370f0e7a03eded3e1d41dc081084a87f0afa1c5b22090b4f3abb391eb15d8ff'),
]
class LNWorker(PrintError):
def __init__(self, wallet, network):
self.wallet = wallet
self.network = network
pk = wallet.storage.get('lightning_privkey')
if pk is None:
pk = bh2u(os.urandom(32))
wallet.storage.put('lightning_privkey', pk)
wallet.storage.write()
self.privkey = bfh(pk)
self.pubkey = ECPrivkey(self.privkey).get_public_key_bytes()
self.config = network.config
self.peers = {}
# view of the network
self.nodes = {} # received node announcements
self.channel_db = lnrouter.ChannelDB()
self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.channels = {x.channel_id: x for x in map(reconstruct_namedtuples, wallet.storage.get("channels", []))}
self.invoices = wallet.storage.get('lightning_invoices', {})
peer_list = network.config.get('lightning_peers', node_list)
self.channel_state = {chan.channel_id: "DISCONNECTED" for chan in self.channels.values()}
for host, port, pubkey in peer_list:
self.add_peer(host, int(port), pubkey)
# wait until we see confirmations
self.network.register_callback(self.on_network_update, ['updated', 'verified']) # thread safe
self.on_network_update('updated') # shortcut (don't block) if funding tx locked and verified
def channels_for_peer(self, node_id):
assert type(node_id) is bytes
return {x: y for (x, y) in self.channels.items() if y.node_id == node_id}
def add_peer(self, host, port, pubkey):
node_id = bfh(pubkey)
for chan_id in self.channels_for_peer(node_id):
chan = self.channels[chan_id]
script = funding_output_script(chan.local_config, chan.remote_config)
funding_address = redeem_script_to_address('p2wsh', script)
scripthash = address_to_scripthash(funding_address)
utxos = self.network.listunspent_for_scripthash(scripthash)
outpoints = [Outpoint(x["tx_hash"], x["tx_pos"]) for x in utxos]
if chan.funding_outpoint not in outpoints:
self.channel_state[chan.channel_id] = "CLOSED"
peer = Peer(self, host, int(port), node_id, request_initial_sync=self.config.get("request_initial_sync", True))
self.network.futures.append(asyncio.run_coroutine_threadsafe(peer.main_loop(), asyncio.get_event_loop()))
self.peers[node_id] = peer
self.lock = threading.Lock()
def save_channel(self, openchannel):
if openchannel.channel_id not in self.channel_state:
self.channel_state[openchannel.channel_id] = "OPENING"
self.channels[openchannel.channel_id] = openchannel
for node_id, peer in self.peers.items():
peer.channels = self.channels_for_peer(node_id)
if openchannel.remote_state.next_per_commitment_point == openchannel.remote_state.current_per_commitment_point:
raise Exception("Tried to save channel with next_point == current_point, this should not happen")
dumped = serialize_channels(self.channels)
self.wallet.storage.put("channels", dumped)
self.wallet.storage.write()
self.network.trigger_callback('channel', openchannel)
def save_short_chan_id(self, chan):
"""
Checks if the Funding TX has been mined. If it has save the short channel ID to disk and return the new OpenChannel.
If the Funding TX has not been mined, return None
"""
assert self.channel_state[chan.channel_id] in ["OPEN", "OPENING"]
peer = self.peers[chan.node_id]
conf = self.wallet.get_tx_height(chan.funding_outpoint.txid)[1]
if conf >= chan.constraints.funding_txn_minimum_depth:
block_height, tx_pos = self.wallet.get_txpos(chan.funding_outpoint.txid)
if tx_pos == -1:
self.print_error('funding tx is not yet SPV verified.. but there are '
'already enough confirmations (currently {})'.format(conf))
return None
chan = chan._replace(short_channel_id = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index))
self.save_channel(chan)
return chan
return None
def on_network_update(self, event, *args):
for chan in self.channels.values():
peer = self.peers[chan.node_id]
if self.channel_state[chan.channel_id] == "OPENING":
chan = self.save_short_chan_id(chan)
if not chan:
self.print_error("network update but funding tx is still not at sufficient depth")
continue
# this results in the channel being marked OPEN
peer.funding_locked(chan)
elif self.channel_state[chan.channel_id] == "OPEN":
conf = self.wallet.get_tx_height(chan.funding_outpoint.txid)[1]
peer.on_network_update(chan, conf)
# not aiosafe because we call .result() which will propagate an exception
async def _open_channel_coroutine(self, node_id, amount_sat, push_sat, password):
if node_id == "":
peer = next(iter(self.peers.values()))
else:
peer = self.peers[bfh(node_id)]
openingchannel = await peer.channel_establishment_flow(self.wallet, self.config, password, amount_sat, push_sat * 1000, temp_channel_id=os.urandom(32))
self.print_error("SAVING OPENING CHANNEL")
self.save_channel(openingchannel)
self.on_channels_updated()
def on_channels_updated(self):
self.network.trigger_callback('channels')
def open_channel(self, node_id, local_amt_sat, push_amt_sat, pw):
coro = self._open_channel_coroutine(node_id, local_amt_sat, push_amt_sat, None if pw == "" else pw)
return asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
def pay(self, invoice):
addr = lndecode(invoice, expected_hrp=constants.net.SEGWIT_HRP)
payment_hash = addr.paymenthash
invoice_pubkey = addr.pubkey.serialize()
amount_msat = int(addr.amount * COIN * 1000)
path = self.path_finder.find_path_for_payment(self.pubkey, invoice_pubkey, amount_msat)
if path is None:
raise Exception("No path found")
node_id, short_channel_id = path[0]
peer = self.peers[node_id]
for chan in self.channels.values():
if chan.short_channel_id == short_channel_id:
break
else:
raise Exception("ChannelDB returned path with short_channel_id that is not in channel list")
coro = peer.pay(path, chan, amount_msat, payment_hash, invoice_pubkey, addr.min_final_cltv_expiry)
return asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
def add_invoice(self, amount_sat, message):
is_open = lambda chan: self.channel_state[chan.channel_id] == "OPEN"
payment_preimage = os.urandom(32)
RHASH = sha256(payment_preimage)
pay_req = lnencode(LnAddr(RHASH, amount_sat/Decimal(COIN), tags=[('d', message)]), self.privkey)
decoded = lndecode(pay_req, expected_hrp=constants.net.SEGWIT_HRP)
assert decoded.pubkey.serialize() == privkey_to_pubkey(self.privkey)
self.invoices[bh2u(payment_preimage)] = pay_req
self.wallet.storage.put('lightning_invoices', self.invoices)
self.wallet.storage.write()
return pay_req
def list_channels(self):
return serialize_channels(self.channels)
def close_channel(self, chan_id):
from .lnhtlc import HTLCStateMachine
chan = self.channels[chan_id]
# local_commitment always gives back the next expected local_commitment,
# but in this case, we want the current one. So substract one ctn number
tx = HTLCStateMachine(chan._replace(local_state=chan.local_state._replace(ctn=chan.local_state.ctn - 1))).local_commitment
tx.sign({bh2u(chan.local_config.multisig_key.pubkey): (chan.local_config.multisig_key.privkey, True)})
remote_sig = chan.local_state.current_commitment_signature
remote_sig = der_sig_from_sig_string(remote_sig) + b"\x01"
none_idx = tx._inputs[0]["signatures"].index(None)
Transaction.add_signature_to_txin(tx._inputs[0], none_idx, bh2u(remote_sig))
tx.raw = None # trigger reserialization
assert tx.is_complete()
suc, msg = self.network.broadcast_transaction(tx)
self.channel_state[chan_id] = "CLOSED"
self.on_channels_updated()
if "transaction already in block chain" in msg:
return
assert suc, msg

View File

@ -20,6 +20,7 @@
# ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
# CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
# SOFTWARE.
import asyncio
import time
import queue
import os
@ -28,7 +29,7 @@ import errno
import random
import re
import select
from collections import defaultdict
from collections import defaultdict, deque
import threading
import socket
import json
@ -153,7 +154,6 @@ def deserialize_server(server_str):
def serialize_server(host, port, protocol):
return str(':'.join([host, port, protocol]))
class Network(util.DaemonThread):
"""The Network class manages a set of connections to remote electrum
servers, each connected socket is handled by an Interface() object.
@ -227,6 +227,8 @@ class Network(util.DaemonThread):
self.socket_queue = queue.Queue()
self.start_network(deserialize_server(self.default_server)[2],
deserialize_proxy(self.config.get('proxy')))
self.asyncio_loop = loop = asyncio.new_event_loop()
self.futures = []
def register_callback(self, callback, events):
with self.lock:
@ -971,13 +973,29 @@ class Network(util.DaemonThread):
b.update_size()
def run(self):
asyncio.set_event_loop(self.asyncio_loop)
self.init_headers_file()
def asyncioThread():
self.asyncio_loop.run_forever()
threading.Thread(target=asyncioThread).start()
while self.is_running():
self.maintain_sockets()
self.wait_on_sockets()
self.maintain_requests()
self.run_jobs() # Synchronizer and Verifier
self.process_pending_sends()
# cancel tasks
[f.cancel() for f in self.futures]
async def loopstop():
self.asyncio_loop.stop()
asyncio.run_coroutine_threadsafe(loopstop(), self.asyncio_loop)
while self.asyncio_loop.is_running():
time.sleep(0.1)
try:
self.asyncio_loop.close()
except:
pass
self.stop_network()
self.on_stop()

View File

@ -59,14 +59,14 @@ def bech32_encode(hrp, data):
return hrp + '1' + ''.join([CHARSET[d] for d in combined])
def bech32_decode(bech):
def bech32_decode(bech, ignore_long_length=False):
"""Validate a Bech32 string, and determine HRP and data."""
if ((any(ord(x) < 33 or ord(x) > 126 for x in bech)) or
(bech.lower() != bech and bech.upper() != bech)):
return (None, None)
bech = bech.lower()
pos = bech.rfind('1')
if pos < 1 or pos + 7 > len(bech) or len(bech) > 90:
if pos < 1 or pos + 7 > len(bech) or (not ignore_long_length and len(bech) > 90):
return (None, None)
if not all(x in CHARSET for x in bech[pos+1:]):
return (None, None)

View File

@ -108,10 +108,11 @@ class SimpleConfig(PrintError):
make_dir(path, allow_symlink=False)
if self.get('testnet'):
path = os.path.join(path, 'testnet')
make_dir(path, allow_symlink=False)
elif self.get('regtest'):
path = os.path.join(path, 'regtest')
make_dir(path, allow_symlink=False)
elif self.get('simnet'):
path = os.path.join(path, 'simnet')
make_dir(path, allow_symlink=False)
self.print_error("electrum directory", path)
return path

97
lib/tests/test_bolt11.py Normal file
View File

@ -0,0 +1,97 @@
from hashlib import sha256
from lib.lightning_payencode.lnaddr import shorten_amount, unshorten_amount, LnAddr, lnencode, lndecode, u5_to_bitarray, bitarray_to_u5
from decimal import Decimal
from binascii import unhexlify, hexlify
from lib.segwit_addr import bech32_encode, bech32_decode
import pprint
import unittest
RHASH=unhexlify('0001020304050607080900010203040506070809000102030405060708090102')
CONVERSION_RATE=1200
PRIVKEY=unhexlify('e126f68f7eafcc8b74f54d269fe206be715000f94dac067d1c04a8ca3b2db734')
PUBKEY=unhexlify('03e7156ae33b0a208d0744199163177e909e80176e55d97a2f221ede0f934dd9ad')
class TestBolt11(unittest.TestCase):
def test_shorten_amount(self):
tests = {
Decimal(10)/10**12: '10p',
Decimal(1000)/10**12: '1n',
Decimal(1200)/10**12: '1200p',
Decimal(123)/10**6: '123u',
Decimal(123)/1000: '123m',
Decimal(3): '3',
}
for i, o in tests.items():
assert shorten_amount(i) == o
assert unshorten_amount(shorten_amount(i)) == i
@staticmethod
def compare(a, b):
if len([t[1] for t in a.tags if t[0] == 'h']) == 1:
h1 = sha256([t[1] for t in a.tags if t[0] == 'h'][0].encode('utf-8')).digest()
h2 = [t[1] for t in b.tags if t[0] == 'h'][0]
assert h1 == h2
# Need to filter out these, since they are being modified during
# encoding, i.e., hashed
a.tags = [t for t in a.tags if t[0] != 'h' and t[0] != 'n']
b.tags = [t for t in b.tags if t[0] != 'h' and t[0] != 'n']
assert b.pubkey.serialize() == PUBKEY, (hexlify(b.pubkey.serialize()), hexlify(PUBKEY))
assert b.signature != None
# Unset these, they are generated during encoding/decoding
b.pubkey = None
b.signature = None
assert a.__dict__ == b.__dict__, (pprint.pformat([a.__dict__, b.__dict__]))
def test_roundtrip(self):
longdescription = ('One piece of chocolate cake, one icecream cone, one'
' pickle, one slice of swiss cheese, one slice of salami,'
' one lollypop, one piece of cherry pie, one sausage, one'
' cupcake, and one slice of watermelon')
tests = [
LnAddr(RHASH, tags=[('d', '')]),
LnAddr(RHASH, amount=Decimal('0.001'),
tags=[('d', '1 cup coffee'), ('x', 60)]),
LnAddr(RHASH, amount=Decimal('1'), tags=[('h', longdescription)]),
LnAddr(RHASH, currency='tb', tags=[('f', 'mk2QpYatsKicvFVuTAQLBryyccRXMUaGHP'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[
('r', [(unhexlify('029e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('0102030405060708'), 1, 20, 3), (unhexlify('039e03a901b85534ff1e92c43c74431f7ce72046060fcf7a95c37e148f78c77255'), unhexlify('030405060708090a'), 2, 30, 4)]), ('f', '1RustyRX2oai4EYYDpQGWvEL62BBGqN9T'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[('f', '3EktnHQD7RiAE6uzMj2ZifT9YgRrkSgzQX'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qw508d6qejxtdg4y5r3zarvary0c5xw7kv8f3t4'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[('f', 'bc1qrp33g0q5c5txsp9arysrx4k6zdkfs4nce4xj0gdcccefvpysxf3qccfmv3'), ('h', longdescription)]),
LnAddr(RHASH, amount=24, tags=[('n', PUBKEY), ('h', longdescription)]),
]
# Roundtrip
for t in tests:
o = lndecode(lnencode(t, PRIVKEY), False, t.currency)
self.compare(t, o)
def test_n_decoding(self):
# We flip the signature recovery bit, which would normally give a different
# pubkey.
hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24, tags=[('d', '')]), PRIVKEY), True)
databits = u5_to_bitarray(data)
databits.invert(-1)
lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), True)
assert lnaddr.pubkey.serialize() != PUBKEY
# But not if we supply expliciy `n` specifier!
hrp, data = bech32_decode(lnencode(LnAddr(RHASH, amount=24,
tags=[('d', ''),
('n', PUBKEY)]),
PRIVKEY), True)
databits = u5_to_bitarray(data)
databits.invert(-1)
lnaddr = lndecode(bech32_encode(hrp, bitarray_to_u5(databits)), True)
assert lnaddr.pubkey.serialize() == PUBKEY
def test_min_final_cltv_expiry(self):
self.assertEquals(lndecode("lnsb500u1pdsgyf3pp5nmrqejdsdgs4n9ukgxcp2kcq265yhrxd4k5dyue58rxtp5y83s3qdqqcqzystrggccm9yvkr5yqx83jxll0qjpmgfg9ywmcd8g33msfgmqgyfyvqhku80qmqm8q6v35zvck2y5ccxsz5avtrauz8hgjj3uahppyq20qp6dvwxe", expected_hrp="sb").min_final_cltv_expiry, 144)

819
lib/tests/test_lnbase.py Normal file
View File

@ -0,0 +1,819 @@
import binascii
import json
import unittest
from lib.util import bh2u, bfh
from lib.lnbase import make_commitment, get_obscured_ctn, Peer, make_offered_htlc, make_received_htlc, make_htlc_tx
from lib.lnbase import secret_to_pubkey, derive_pubkey, derive_privkey, derive_blinded_pubkey, overall_weight
from lib.lnbase import make_htlc_tx_output, make_htlc_tx_inputs, get_per_commitment_secret_from_seed, make_htlc_tx_witness
from lib.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop
from lib.lnbase import RevocationStore
from lib.transaction import Transaction
from lib import bitcoin
import ecdsa.ellipticcurve
from ecdsa.curves import SECP256k1
from lib.util import bfh
from lib import bitcoin, lnbase, lnrouter
funding_tx_id = '8984484a580b825b9972d7adb15050b3ab624ccd731946b3eeddb92f4e7ef6be'
funding_output_index = 0
funding_amount_satoshi = 10000000
commitment_number = 42
local_delay = 144
local_dust_limit_satoshi = 546
local_payment_basepoint = bytes.fromhex('034f355bdcb7cc0af728ef3cceb9615d90684bb5b2ca5f859ab0f0b704075871aa')
remote_payment_basepoint = bytes.fromhex('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991')
obs = get_obscured_ctn(42, local_payment_basepoint, remote_payment_basepoint)
local_funding_privkey = bytes.fromhex('30ff4956bbdd3222d44cc5e8a1261dab1e07957bdac5ae88fe3261ef321f374901')
local_funding_pubkey = bytes.fromhex('023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb')
remote_funding_pubkey = bytes.fromhex('030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c1')
local_privkey = bytes.fromhex('bb13b121cdc357cd2e608b0aea294afca36e2b34cf958e2e6451a2f27469449101')
localpubkey = bytes.fromhex('030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e7')
remotepubkey = bytes.fromhex('0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b')
local_delayedpubkey = bytes.fromhex('03fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c')
local_revocation_pubkey = bytes.fromhex('0212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b19')
# funding wscript = 5221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae
class Test_LNBase(unittest.TestCase):
@staticmethod
def parse_witness_list(witness_bytes):
amount_witnesses = witness_bytes[0]
witness_bytes = witness_bytes[1:]
res = []
for i in range(amount_witnesses):
witness_length = witness_bytes[0]
this_witness = witness_bytes[1:witness_length+1]
assert len(this_witness) == witness_length
witness_bytes = witness_bytes[witness_length+1:]
res += [bytes(this_witness)]
assert witness_bytes == b"", witness_bytes
return res
def test_simple_commitment_tx_with_no_HTLCs(self):
to_local_msat = 7000000000
to_remote_msat = 3000000000
local_feerate_per_kw = 15000
# base commitment transaction fee = 10860
# actual commitment transaction fee = 10860
# to_local amount 6989140 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac
# to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b)
remote_signature = "3045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c0"
# local_signature = 3044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c3836939
htlcs=[]
our_commit_tx = make_commitment(
commitment_number,
local_funding_pubkey, remote_funding_pubkey, remotepubkey,
local_payment_basepoint, remote_payment_basepoint,
local_revocation_pubkey, local_delayedpubkey, local_delay,
funding_tx_id, funding_output_index, funding_amount_satoshi,
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
local_feerate_per_kw, True, we_are_initiator=True, htlcs=[])
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
self.assertEqual(str(our_commit_tx), ref_commit_tx_str)
def sign_and_insert_remote_sig(self, tx, remote_pubkey, remote_signature, pubkey, privkey):
assert type(remote_pubkey) is bytes
assert len(remote_pubkey) == 33
assert type(remote_signature) is str
assert type(pubkey) is bytes
assert type(privkey) is bytes
assert len(pubkey) == 33
assert len(privkey) == 33
tx.sign({bh2u(pubkey): (privkey[:-1], True)})
pubkeys, _x_pubkeys = tx.get_sorted_pubkeys(tx.inputs()[0])
index_of_pubkey = pubkeys.index(bh2u(remote_pubkey))
tx._inputs[0]["signatures"][index_of_pubkey] = remote_signature + "01"
tx.raw = None
def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self):
to_local_msat = 6988000000
to_remote_msat = 3000000000
local_feerate_per_kw = 0
# base commitment transaction fee = 0
# actual commitment transaction fee = 0
per_commitment_secret = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
per_commitment_point = SECP256k1.generator * per_commitment_secret
remote_htlcpubkey = remotepubkey
local_htlcpubkey = localpubkey
htlc2_cltv_timeout = 502
htlc2_payment_preimage = b"\x02" * 32
htlc2 = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc2_payment_preimage))
# HTLC 2 offered amount 2000
ref_htlc2_wscript = "76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868"
self.assertEqual(htlc2, bfh(ref_htlc2_wscript))
htlc3_cltv_timeout = 503
htlc3_payment_preimage = b"\x03" * 32
htlc3 = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc3_payment_preimage))
# HTLC 3 offered amount 3000
ref_htlc3_wscript = "76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868"
self.assertEqual(htlc3, bfh(ref_htlc3_wscript))
htlc0_cltv_timeout = 500
htlc0_payment_preimage = b"\x00" * 32
htlc0 = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc0_payment_preimage), htlc0_cltv_timeout)
# HTLC 0 received amount 1000
ref_htlc0_wscript = "76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac6868"
self.assertEqual(htlc0, bfh(ref_htlc0_wscript))
htlc1_cltv_timeout = 501
htlc1_payment_preimage = b"\x01" * 32
htlc1 = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc1_payment_preimage), htlc1_cltv_timeout)
# HTLC 1 received amount 2000
ref_htlc1_wscript = "76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac6868"
self.assertEqual(htlc1, bfh(ref_htlc1_wscript))
htlc4_cltv_timeout = 504
htlc4_payment_preimage = b"\x04" * 32
htlc4 = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc4_payment_preimage), htlc4_cltv_timeout)
# HTLC 4 received amount 4000
ref_htlc4_wscript = "76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac6868"
self.assertEqual(htlc4, bfh(ref_htlc4_wscript))
# to_local amount 6988000 wscript 63210212a140cd0c6539d07cd08dfe09984dec3251ea808b892efeac3ede9402bf2b1967029000b2752103fd5960528dc152014952efdb702a88f71e3c1653b2314431701ec77e57fde83c68ac
# to_remote amount 3000000 P2WPKH(0394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b)
remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606"
# local_signature = 30440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f06
output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
htlc0_msat = 1000 * 1000
htlc2_msat = 2000 * 1000
htlc3_msat = 3000 * 1000
htlc1_msat = 2000 * 1000
htlc4_msat = 4000 * 1000
htlcs = [(htlc2, htlc2_msat), (htlc3, htlc3_msat), (htlc0, htlc0_msat), (htlc1, htlc1_msat), (htlc4, htlc4_msat)]
our_commit_tx = make_commitment(
commitment_number,
local_funding_pubkey, remote_funding_pubkey, remotepubkey,
local_payment_basepoint, remote_payment_basepoint,
local_revocation_pubkey, local_delayedpubkey, local_delay,
funding_tx_id, funding_output_index, funding_amount_satoshi,
to_local_msat, to_remote_msat, local_dust_limit_satoshi,
local_feerate_per_kw, True, we_are_initiator=True, htlcs=htlcs)
self.sign_and_insert_remote_sig(our_commit_tx, remote_funding_pubkey, remote_signature, local_funding_pubkey, local_funding_privkey)
self.assertEqual(str(our_commit_tx), output_commit_tx)
# (HTLC 0)
signature_for_output_remote_htlc_0 = "304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6"
# (HTLC 2)
signature_for_output_remote_htlc_2 = "3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b"
# (HTLC 1)
signature_for_output_remote_htlc_1 = "304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202"
# (HTLC 3)
signature_for_output_remote_htlc_3 = "3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554"
# (HTLC 4)
signature_for_output_remote_htlc_4 = "304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d"
local_signature_htlc0 = "304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5" # derive ourselves
output_htlc_success_tx_0 = "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000"
local_signature_htlc2 = "3045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be5"
output_htlc_timeout_tx_2 = "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000"
local_signature_htlc1 = "3045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da"
output_htlc_success_tx_1 = "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000"
local_signature_htlc3 = "30440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac08727"
output_htlc_timeout_tx_3 = "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000"
local_signature_htlc4 = "30440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e"
output_htlc_success_tx_4 = "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000"
def test_htlc_tx(htlc, htlc_output_index, amount_msat, ref_local_sig, htlc_payment_preimage, remote_htlc_sig, ref_tx, success, cltv_timeout):
our_htlc_tx_output = make_htlc_tx_output(
amount_msat=amount_msat,
local_feerate=local_feerate_per_kw,
revocationpubkey=local_revocation_pubkey,
local_delayedpubkey=local_delayedpubkey,
success=success,
to_self_delay=local_delay)
our_htlc_tx_inputs = make_htlc_tx_inputs(
htlc_output_txid=our_commit_tx.txid(),
htlc_output_index=htlc_output_index,
revocationpubkey=local_revocation_pubkey,
local_delayedpubkey=local_delayedpubkey,
amount_msat=amount_msat,
witness_script=bh2u(htlc))
our_htlc_tx = make_htlc_tx(cltv_timeout,
inputs=our_htlc_tx_inputs,
output=our_htlc_tx_output)
local_sig = our_htlc_tx.sign_txin(0, local_privkey[:-1])
#self.assertEqual(ref_local_sig + "01", local_sig) # commented out as it is sufficient to compare the serialized txn
our_htlc_tx_witness = make_htlc_tx_witness( # FIXME only correct for success=True
remotehtlcsig=bfh(remote_htlc_sig) + b"\x01", # 0x01 is SIGHASH_ALL
localhtlcsig=bfh(local_sig),
payment_preimage=htlc_payment_preimage if success else b'', # will put 00 on witness if timeout
witness_script=htlc)
our_htlc_tx._inputs[0]['witness'] = bh2u(our_htlc_tx_witness)
self.assertEqual(ref_tx, str(our_htlc_tx))
test_htlc_tx(htlc=htlc0, htlc_output_index=0,
amount_msat=htlc0_msat,
ref_local_sig=local_signature_htlc0,
htlc_payment_preimage=htlc0_payment_preimage,
remote_htlc_sig=signature_for_output_remote_htlc_0,
ref_tx=output_htlc_success_tx_0,
success=True, cltv_timeout=0)
test_htlc_tx(htlc=htlc1, htlc_output_index=2,
amount_msat=htlc1_msat,
ref_local_sig=local_signature_htlc1,
htlc_payment_preimage=htlc1_payment_preimage,
remote_htlc_sig=signature_for_output_remote_htlc_1,
ref_tx=output_htlc_success_tx_1,
success=True, cltv_timeout=0)
test_htlc_tx(htlc=htlc2, htlc_output_index=1,
amount_msat=htlc2_msat,
ref_local_sig=local_signature_htlc2,
htlc_payment_preimage=htlc2_payment_preimage,
remote_htlc_sig=signature_for_output_remote_htlc_2,
ref_tx=output_htlc_timeout_tx_2,
success=False, cltv_timeout=htlc2_cltv_timeout)
test_htlc_tx(htlc=htlc3, htlc_output_index=3,
amount_msat=htlc3_msat,
ref_local_sig=local_signature_htlc3,
htlc_payment_preimage=htlc3_payment_preimage,
remote_htlc_sig=signature_for_output_remote_htlc_3,
ref_tx=output_htlc_timeout_tx_3,
success=False, cltv_timeout=htlc3_cltv_timeout)
test_htlc_tx(htlc=htlc4, htlc_output_index=4,
amount_msat=htlc4_msat,
ref_local_sig=local_signature_htlc4,
htlc_payment_preimage=htlc4_payment_preimage,
remote_htlc_sig=signature_for_output_remote_htlc_4,
ref_tx=output_htlc_success_tx_4,
success=True, cltv_timeout=0)
def test_find_path_for_payment(self):
class fake_ln_worker:
channel_db = lnrouter.ChannelDB()
path_finder = lnrouter.LNPathFinder(channel_db)
privkey = bitcoin.sha256('privkeyseed')
network = None
channel_state = {}
channels = []
invoices = {}
p = Peer(fake_ln_worker, '', 0, 'a')
p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'c', 'short_channel_id': bfh('0000000000000001')})
p.on_channel_announcement({'node_id_1': b'b', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000002')})
p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'b', 'short_channel_id': bfh('0000000000000003')})
p.on_channel_announcement({'node_id_1': b'c', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000004')})
p.on_channel_announcement({'node_id_1': b'd', 'node_id_2': b'e', 'short_channel_id': bfh('0000000000000005')})
p.on_channel_announcement({'node_id_1': b'a', 'node_id_2': b'd', 'short_channel_id': bfh('0000000000000006')})
o = lambda i: i.to_bytes(8, "big")
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000001'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x00', 'cltv_expiry_delta': o(99), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000002'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000003'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000004'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
p.on_channel_update({'short_channel_id': bfh('0000000000000005'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(999)})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x00', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(99999999)})
p.on_channel_update({'short_channel_id': bfh('0000000000000006'), 'flags': b'\x01', 'cltv_expiry_delta': o(10), 'htlc_minimum_msat': o(250), 'fee_base_msat': o(100), 'fee_proportional_millionths': o(150)})
self.assertNotEqual(None, fake_ln_worker.path_finder.find_path_for_payment(b'a', b'e', 100000))
def test_key_derivation(self):
# BOLT3, Appendix E
base_secret = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
per_commitment_secret = 0x1f1e1d1c1b1a191817161514131211100f0e0d0c0b0a09080706050403020100
revocation_basepoint_secret = 0x000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f
base_point = secret_to_pubkey(base_secret)
self.assertEqual(base_point, bfh('036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2'))
per_commitment_point = secret_to_pubkey(per_commitment_secret)
self.assertEqual(per_commitment_point, bfh('025f7117a78150fe2ef97db7cfc83bd57b2e2c0d0dd25eaf467a4a1c2a45ce1486'))
localpubkey = derive_pubkey(base_point, per_commitment_point)
self.assertEqual(localpubkey, bfh('0235f2dbfaa89b57ec7b055afe29849ef7ddfeb1cefdb9ebdc43f5494984db29e5'))
localprivkey = derive_privkey(base_secret, per_commitment_point)
self.assertEqual(localprivkey, 0xcbced912d3b21bf196a766651e436aff192362621ce317704ea2f75d87e7be0f)
revocation_basepoint = secret_to_pubkey(revocation_basepoint_secret)
self.assertEqual(revocation_basepoint, bfh('036d6caac248af96f6afa7f904f550253a0f3ef3f5aa2fe6838a95b216691468e2'))
revocationpubkey = derive_blinded_pubkey(revocation_basepoint, per_commitment_point)
self.assertEqual(revocationpubkey, bfh('02916e326636d19c33f13e8c0c3a03dd157f332f3e99c317c141dd865eb01f8ff0'))
def test_per_commitment_secret_from_seed(self):
self.assertEqual(0x02a40c85b6f28da08dfdbe0926c53fab2de6d28c10301f8f7c4073d5e42e3148.to_bytes(byteorder="big", length=32),
get_per_commitment_secret_from_seed(0x0000000000000000000000000000000000000000000000000000000000000000.to_bytes(byteorder="big", length=32), 281474976710655))
self.assertEqual(0x7cc854b54e3e0dcdb010d7a3fee464a9687be6e8db3be6854c475621e007a5dc.to_bytes(byteorder="big", length=32),
get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder="big", length=32), 281474976710655))
self.assertEqual(0x56f4008fb007ca9acf0e15b054d5c9fd12ee06cea347914ddbaed70d1c13a528.to_bytes(byteorder="big", length=32),
get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder="big", length=32), 0xaaaaaaaaaaa))
self.assertEqual(0x9015daaeb06dba4ccc05b91b2f73bd54405f2be9f217fbacd3c5ac2e62327d31.to_bytes(byteorder="big", length=32),
get_per_commitment_secret_from_seed(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF.to_bytes(byteorder="big", length=32), 0x555555555555))
self.assertEqual(0x915c75942a26bb3a433a8ce2cb0427c29ec6c1775cfc78328b57f6ba7bfeaa9c.to_bytes(byteorder="big", length=32),
get_per_commitment_secret_from_seed(0x0101010101010101010101010101010101010101010101010101010101010101.to_bytes(byteorder="big", length=32), 1))
def test_new_onion_packet(self):
# test vector from bolt-04
payment_path_pubkeys = [
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),
bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),
bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),
bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),
]
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
hops_data = [
OnionHopsDataSingle(OnionPerHop(
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004')
)),
]
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619e5f14350c2a76fc232b5e46d421e9615471ab9e0bc887beff8c95fdb878f7b3a71da571226458c510bbadd1276f045c21c520a07d35da256ef75b4367962437b0dd10f7d61ab590531cf08000178a333a347f8b4072e216400406bdf3bf038659793a86cae5f52d32f3438527b47a1cfc54285a8afec3a4c9f3323db0c946f5d4cb2ce721caad69320c3a469a202f3e468c67eaf7a7cda226d0fd32f7b48084dca885d15222e60826d5d971f64172d98e0760154400958f00e86697aa1aa9d41bee8119a1ec866abe044a9ad635778ba61fc0776dc832b39451bd5d35072d2269cf9b040d6ba38b54ec35f81d7fc67678c3be47274f3c4cc472aff005c3469eb3bc140769ed4c7f0218ff8c6c7dd7221d189c65b3b9aaa71a01484b122846c7c7b57e02e679ea8469b70e14fe4f70fee4d87b910cf144be6fe48eef24da475c0b0bcc6565ae82cd3f4e3b24c76eaa5616c6111343306ab35c1fe5ca4a77c0e314ed7dba39d6f1e0de791719c241a939cc493bea2bae1c1e932679ea94d29084278513c77b899cc98059d06a27d171b0dbdf6bee13ddc4fc17a0c4d2827d488436b57baa167544138ca2e64a11b43ac8a06cd0c2fba2d4d900ed2d9205305e2d7383cc98dacb078133de5f6fb6bed2ef26ba92cea28aafc3b9948dd9ae5559e8bd6920b8cea462aa445ca6a95e0e7ba52961b181c79e73bd581821df2b10173727a810c92b83b5ba4a0403eb710d2ca10689a35bec6c3a708e9e92f7d78ff3c5d9989574b00c6736f84c199256e76e19e78f0c98a9d580b4a658c84fc8f2096c2fbea8f5f8c59d0fdacb3be2802ef802abbecb3aba4acaac69a0e965abd8981e9896b1f6ef9d60f7a164b371af869fd0e48073742825e9434fc54da837e120266d53302954843538ea7c6c3dbfb4ff3b2fdbe244437f2a153ccf7bdb4c92aa08102d4f3cff2ae5ef86fab4653595e6a5837fa2f3e29f27a9cde5966843fb847a4a61f1e76c281fe8bb2b0a181d096100db5a1a5ce7a910238251a43ca556712eaadea167fb4d7d75825e440f3ecd782036d7574df8bceacb397abefc5f5254d2722215c53ff54af8299aaaad642c6d72a14d27882d9bbd539e1cc7a527526ba89b8c037ad09120e98ab042d3e8652b31ae0e478516bfaf88efca9f3676ffe99d2819dcaeb7610a626695f53117665d267d3f7abebd6bbd6733f645c72c389f03855bdf1e4b8075b516569b118233a0f0971d24b83113c0b096f5216a207ca99a7cddc81c130923fe3d91e7508c9ac5f2e914ff5dccab9e558566fa14efb34ac98d878580814b94b73acbfde9072f30b881f7f0fff42d4045d1ace6322d86a97d164aa84d93a60498065cc7c20e636f5862dc81531a88c60305a2e59a985be327a6902e4bed986dbf4a0b50c217af0ea7fdf9ab37f9ea1a1aaa72f54cf40154ea9b269f1a7c09f9f43245109431a175d50e2db0132337baa0ef97eed0fcf20489da36b79a1172faccc2f7ded7c60e00694282d93359c4682135642bc81f433574aa8ef0c97b4ade7ca372c5ffc23c7eddd839bab4e0f14d6df15c9dbeab176bec8b5701cf054eb3072f6dadc98f88819042bf10c407516ee58bce33fbe3b3d86a54255e577db4598e30a135361528c101683a5fcde7e8ba53f3456254be8f45fe3a56120ae96ea3773631fcb3873aa3abd91bcff00bd38bd43697a2e789e00da6077482e7b1b1a677b5afae4c54e6cbdf7377b694eb7d7a5b913476a5be923322d3de06060fd5e819635232a2cf4f0731da13b8546d1d6d4f8d75b9fce6c2341a71b0ea6f780df54bfdb0dd5cd9855179f602f917265f21f9190c70217774a6fbaaa7d63ad64199f4664813b955cff954949076dcf'),
packet.to_bytes())
def test_process_onion_packet(self):
# this test is not from bolt-04, but is based on the one there;
# except here we have the privkeys for these pubkeys
payment_path_pubkeys = [
bfh('03d75c0ee70f68d73d7d13aeb6261d8ace11416800860c7e59407afe4e2e2d42bb'),
bfh('03960a0b830c7b8e76de745b819f252c62508346196b916f5e813cdb0773283cce'),
bfh('0385620e0a571cbc3552620f8bf1bdcdab2d1a4a59c36fa10b8249114ccbdda40d'),
bfh('02ee242cf6c38b7285f0152c33804ff777f5c51fd352ca8132e845e2cf23b3d8ba'),
bfh('025c585fd2e174bf8245b2b4a119e52a417688904228643ea3edaa1728bf2a258e'),
]
payment_path_privkeys = [
bfh('3463a278617b3dd83f79bda7f97673f12609c54386e1f0d2b67b1c6354fda14e'),
bfh('7e1255fddb52db1729fc3ceb21a46f95b8d9fe94cc83425e936a6c5223bb679d'),
bfh('c7ce8c1462c311eec24dff9e2532ac6241e50ae57e7d1833af21942136972f23'),
bfh('3d885f374d79a5e777459b083f7818cdc9493e5c4994ac9c7b843de8b70be661'),
bfh('dd72ab44729527b7942e195e7a835e7c71f9c0ff61844eb21274d9c26166a8f8'),
]
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
associated_data = bfh('4242424242424242424242424242424242424242424242424242424242424242')
hops_data = [
OnionHopsDataSingle(OnionPerHop(
bfh('0000000000000000'), bfh('0000000000000000'), bfh('00000000')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0101010101010101'), bfh('0000000000000001'), bfh('00000001')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0202020202020202'), bfh('0000000000000002'), bfh('00000002')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0303030303030303'), bfh('0000000000000003'), bfh('00000003')
)),
OnionHopsDataSingle(OnionPerHop(
bfh('0404040404040404'), bfh('0000000000000004'), bfh('00000004')
)),
]
packet = new_onion_packet(payment_path_pubkeys, session_key, hops_data, associated_data)
self.assertEqual(bfh('0002eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f28368661954176cd9869da33d713aa219fcef1e5c806fef11e696bcc66844de8271c27974a0fd57c2dbcb2c6dd4e8ef35d96db28d5a0e49b6ab3d6de31af65950723b8cddc108390bebf8d149002e31bdc283056477ba27c8054c248ad7306de31663a7c99ec659da15d0f6fbc7e1687485b39e9be0ec3b70164cb3618a9b546317e7c2d62ae9f0f840704535729262d30c6132d1b390f073edec8fa057176c6268b6ad06a82ff0d16d4c662194873e8b4ecf46eb2c9d4d58d2ee2021adb19840605ac5afd8bd942dd71e8244c83e28b2ed5a3b09e9e7df5c8c747e5765ba366a4f7407a6c6b0a32f74bc5e428f7fa4c3cf70e13ed91563177d94190d5149aa4b9c96d00e40d2ac35ab9c4a621ce0f6f5df7d64a9c8d435db19de192d9db522c7f7b4e201fc1b61a9bd3efd062ae24455d463818b01e2756c7d0691bc3ac4c017be34c9a8b2913bb1b937e31e0ae40f650a7cd820bcb4996825b1cbad1ff7ccc2b513b1104524c34f6573e1b59201c005a632ee5dccd3711a32e3ba1ff00fcffbe636e4b3a84bbe491b836a57ccec138b8cc2ec733846904d872f305d538d51db8e56232ec6e07877075328874cb7b09c7e799100a9ff085dead253886b174fc408a0ea7b48bce2c5d8992285011960af088f7e006ef60089d46ac9aa15acfac6c87c3cf6904764dd785419292fbafa9cca09c8ade24a6cd63f12d1cfc83fa35cf2f1cf503c39cbf78293f06c68a3cece7177169cd872bb49bf69d933a27a887dd9daefa9239fca9f0c3e309ec61d9df947211da98cf11a6e0fb77252629cdf9f2226dd69ca73fa51be4df224592f8d471b69a1aebbdaa2f3a798b3581253d97feb0a12e6606043ca0fc5efc0f49b8061d6796eff31cd8638499e2f25ffb96eec32837438ed7ebebbe587886648f63e35d80f41869f4c308f2e6970bd65fead5e8544e3239a6acc9d996b08d1546455bcafbe88ed3ed547714841946fe2e77180e4d7bf1452414e4b1745a7897184a2c4cbc3ac46f83342a55a48e29dc8f17cf595dd28f51e297ba89fd25ed0dbd1c0081a810beaab09758a36fbfd16fbdc3daa9fe05c8a73195f244ef2743a5df761f01ee6e693eb6c7f1a7834fab3671391e5ddebf611e119a2ae4456e2cee7a6d4f27a2246cdb1f8ef35f0b3d7044b3799d8d0ed0a6470557fd807c065d6d83acba07e96e10770ada8c0b4d4921522944188d5f30086a6ee0a4795331273f32beaaa43363fc58208a257e5c5c434c7325b583642219d81c7d67b908d5263b42ac1991edc69a777da60f38eff138c844af9e549374e8b29b166211bfded24587a29394e33828b784da7e7b62ab7e49ea2693fcdd17fa96186a5ef11ef1a8adffa50f93a3119e95e6c09014f3e3b0709183fa08a826ced6deb4608b7d986ebbcf99ad58e25451d4d9d38d0059734d8501467b97182cd11e0c07c91ca50f61cc31255a3147ade654976a5989097281892aafd8df595c63bd14f1e03f5955a9398d2dd6368bbcae833ae1cc2df31eb0980b4817dfd130020ffb275743fcc01df40e3ecda1c5988e8e1bde965353b0b1bf34ea05f095000c45b6249618d275905a24d3eb58c600aeab4fb552fbf1ccdb2a5c80ace220310f89829d7e53f78c126037b6d8d500220c7a118d9621b4d6bd5379edd7e24bcf540e87aba6b88862db16fa4ee00b009fda80577be67ab94910fd8a7807dfe4ebe66b8fdcd040aa2dc17ec22639298be56b2a2c9d8940647b75f2f6d81746df16e1cb2f05e23397a8c63baea0803441ff4b7d517ff172980a056726235e2f6af85e8aa9b91ba85f14532272d6170df3166b91169dc09d4f4a251610f57ff0885a93364cfaf650bdf436c89795efed5ca934bc7ffc0a4'),
packet.to_bytes())
for i, privkey in enumerate(payment_path_privkeys):
processed_packet = lnrouter.process_onion_packet(packet, associated_data, privkey)
self.assertEqual(hops_data[i].per_hop.to_bytes(), processed_packet.hop_data.per_hop.to_bytes())
packet = processed_packet.next_packet
def test_decode_onion_error(self):
# test vector from bolt-04
payment_path_pubkeys = [
bfh('02eec7245d6b7d2ccb30380bfbe2a3648cd7a942653f5aa340edcea1f283686619'),
bfh('0324653eac434488002cc06bbfb7f10fe18991e35f9fe4302dbea6d2353dc0ab1c'),
bfh('027f31ebc5462c1fdce1b737ecff52d37d75dea43ce11c74d25aa297165faa2007'),
bfh('032c0b7cf95324a07d05398b240174dc0c2be444d96b159aa6c7f7b1e668680991'),
bfh('02edabbd16b41c8371b92ef2f04c1185b4f03b6dcd52ba9b78d9d7c89c8f221145'),
]
session_key = bfh('4141414141414141414141414141414141414141414141414141414141414141')
error_packet_for_node_0 = bfh('9c5add3963fc7f6ed7f148623c84134b5647e1306419dbe2174e523fa9e2fbed3a06a19f899145610741c83ad40b7712aefaddec8c6baf7325d92ea4ca4d1df8bce517f7e54554608bf2bd8071a4f52a7a2f7ffbb1413edad81eeea5785aa9d990f2865dc23b4bc3c301a94eec4eabebca66be5cf638f693ec256aec514620cc28ee4a94bd9565bc4d4962b9d3641d4278fb319ed2b84de5b665f307a2db0f7fbb757366067d88c50f7e829138fde4f78d39b5b5802f1b92a8a820865af5cc79f9f30bc3f461c66af95d13e5e1f0381c184572a91dee1c849048a647a1158cf884064deddbf1b0b88dfe2f791428d0ba0f6fb2f04e14081f69165ae66d9297c118f0907705c9c4954a199bae0bb96fad763d690e7daa6cfda59ba7f2c8d11448b604d12d')
decoded_error, index_of_sender = lnrouter._decode_onion_error(error_packet_for_node_0, payment_path_pubkeys, session_key)
self.assertEqual(bfh('4c2fc8bc08510334b6833ad9c3e79cd1b52ae59dfe5c2a4b23ead50f09f7ee0b0002200200fe0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000'),
decoded_error)
self.assertEqual(4, index_of_sender)
def test_shachain_store(self):
tests = [
{
"name": "insert_secret correct sequence",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1" +\
"a8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab2" +\
"1e9b506fd4998a51d54502e99116",
"successful": True
},
{
"index": 281474976710651,
"secret": "c65716add7aa98ba7acb236352d665cab173" +\
"45fe45b55fb879ff80e6bd0c41dd",
"successful": True
},
{
"index": 281474976710650,
"secret": "969660042a28f32d9be17344e09374b37996" +\
"2d03db1574df5a8a5a47e19ce3f2",
"successful": True
},
{
"index": 281474976710649,
"secret": "a5a64476122ca0925fb344bdc1854c1c0a59" +\
"fc614298e50a33e331980a220f32",
"successful": True
},
{
"index": 281474976710648,
"secret": "05cde6323d949933f7f7b78776bcc1ea6d9b" +\
"31447732e3802e1f7ac44b650e17",
"successful": True
}
]
},
{
"name": "insert_secret #1 incorrect",
"inserts": [
{
"index": 281474976710655,
"secret": "02a40c85b6f28da08dfdbe0926c53fab2d" +\
"e6d28c10301f8f7c4073d5e42e3148",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659" +\
"c1a8b4b5bec0c4b872abeba4cb8964",
"successful": False
}
]
},
{
"name": "insert_secret #2 incorrect (#1 derived from incorrect)",
"inserts": [
{
"index": 281474976710655,
"secret": "02a40c85b6f28da08dfdbe0926c53fab2de6" +\
"d28c10301f8f7c4073d5e42e3148",
"successful": True
},
{
"index": 281474976710654,
"secret": "dddc3a8d14fddf2b68fa8c7fbad274827493" +\
"7479dd0f8930d5ebb4ab6bd866a3",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22a" +\
"b21e9b506fd4998a51d54502e99116",
"successful": False
}
]
},
{
"name": "insert_secret #3 incorrect",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1" +\
"a8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "c51a18b13e8527e579ec56365482c62f180b" +\
"7d5760b46e9477dae59e87ed423a",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab2" +\
"1e9b506fd4998a51d54502e99116",
"successful": False
}
]
},
{
"name": "insert_secret #4 incorrect (1,2,3 derived from incorrect)",
"inserts": [
{
"index": 281474976710655,
"secret": "02a40c85b6f28da08dfdbe0926c53fab2de6" +\
"d28c10301f8f7c4073d5e42e3148",
"successful": True
},
{
"index": 281474976710654,
"secret": "dddc3a8d14fddf2b68fa8c7fbad274827493" +\
"7479dd0f8930d5ebb4ab6bd866a3",
"successful": True
},
{
"index": 281474976710653,
"secret": "c51a18b13e8527e579ec56365482c62f18" +\
"0b7d5760b46e9477dae59e87ed423a",
"successful": True
},
{
"index": 281474976710652,
"secret": "ba65d7b0ef55a3ba300d4e87af29868f39" +\
"4f8f138d78a7011669c79b37b936f4",
"successful": True
},
{
"index": 281474976710651,
"secret": "c65716add7aa98ba7acb236352d665cab1" +\
"7345fe45b55fb879ff80e6bd0c41dd",
"successful": True
},
{
"index": 281474976710650,
"secret": "969660042a28f32d9be17344e09374b379" +\
"962d03db1574df5a8a5a47e19ce3f2",
"successful": True
},
{
"index": 281474976710649,
"secret": "a5a64476122ca0925fb344bdc1854c1c0a" +\
"59fc614298e50a33e331980a220f32",
"successful": True
},
{
"index": 281474976710649,
"secret": "05cde6323d949933f7f7b78776bcc1ea6d9b" +\
"31447732e3802e1f7ac44b650e17",
"successful": False
}
]
},
{
"name": "insert_secret #5 incorrect",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1a" +\
"8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab21" +\
"e9b506fd4998a51d54502e99116",
"successful": True
},
{
"index": 281474976710651,
"secret": "631373ad5f9ef654bb3dade742d09504c567" +\
"edd24320d2fcd68e3cc47e2ff6a6",
"successful": True
},
{
"index": 281474976710650,
"secret": "969660042a28f32d9be17344e09374b37996" +\
"2d03db1574df5a8a5a47e19ce3f2",
"successful": False
}
]
},
{
"name": "insert_secret #6 incorrect (5 derived from incorrect)",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1a" +\
"8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab21" +\
"e9b506fd4998a51d54502e99116",
"successful": True
},
{
"index": 281474976710651,
"secret": "631373ad5f9ef654bb3dade742d09504c567" +\
"edd24320d2fcd68e3cc47e2ff6a6",
"successful": True
},
{
"index": 281474976710650,
"secret": "b7e76a83668bde38b373970155c868a65330" +\
"4308f9896692f904a23731224bb1",
"successful": True
},
{
"index": 281474976710649,
"secret": "a5a64476122ca0925fb344bdc1854c1c0a59f" +\
"c614298e50a33e331980a220f32",
"successful": True
},
{
"index": 281474976710648,
"secret": "05cde6323d949933f7f7b78776bcc1ea6d9b" +\
"31447732e3802e1f7ac44b650e17",
"successful": False
}
]
},
{
"name": "insert_secret #7 incorrect",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1a" +\
"8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab21" +\
"e9b506fd4998a51d54502e99116",
"successful": True
},
{
"index": 281474976710651,
"secret": "c65716add7aa98ba7acb236352d665cab173" +\
"45fe45b55fb879ff80e6bd0c41dd",
"successful": True
},
{
"index": 281474976710650,
"secret": "969660042a28f32d9be17344e09374b37996" +\
"2d03db1574df5a8a5a47e19ce3f2",
"successful": True
},
{
"index": 281474976710649,
"secret": "e7971de736e01da8ed58b94c2fc216cb1d" +\
"ca9e326f3a96e7194fe8ea8af6c0a3",
"successful": True
},
{
"index": 281474976710648,
"secret": "05cde6323d949933f7f7b78776bcc1ea6d" +\
"9b31447732e3802e1f7ac44b650e17",
"successful": False
}
]
},
{
"name": "insert_secret #8 incorrect",
"inserts": [
{
"index": 281474976710655,
"secret": "7cc854b54e3e0dcdb010d7a3fee464a9687b" +\
"e6e8db3be6854c475621e007a5dc",
"successful": True
},
{
"index": 281474976710654,
"secret": "c7518c8ae4660ed02894df8976fa1a3659c1a" +\
"8b4b5bec0c4b872abeba4cb8964",
"successful": True
},
{
"index": 281474976710653,
"secret": "2273e227a5b7449b6e70f1fb4652864038b1" +\
"cbf9cd7c043a7d6456b7fc275ad8",
"successful": True
},
{
"index": 281474976710652,
"secret": "27cddaa5624534cb6cb9d7da077cf2b22ab21" +\
"e9b506fd4998a51d54502e99116",
"successful": True
},
{
"index": 281474976710651,
"secret": "c65716add7aa98ba7acb236352d665cab173" +\
"45fe45b55fb879ff80e6bd0c41dd",
"successful": True
},
{
"index": 281474976710650,
"secret": "969660042a28f32d9be17344e09374b37996" +\
"2d03db1574df5a8a5a47e19ce3f2",
"successful": True
},
{
"index": 281474976710649,
"secret": "a5a64476122ca0925fb344bdc1854c1c0a" +\
"59fc614298e50a33e331980a220f32",
"successful": True
},
{
"index": 281474976710648,
"secret": "a7efbc61aac46d34f77778bac22c8a20c6" +\
"a46ca460addc49009bda875ec88fa4",
"successful": False
}
]
}
]
for test in tests:
receiver = RevocationStore()
for insert in test["inserts"]:
secret = bytes.fromhex(insert["secret"])
try:
receiver.add_next_entry(secret)
except Exception as e:
if insert["successful"]:
raise Exception("Failed ({}): error was received but it shouldn't: {}".format(test["name"], e))
else:
if not insert["successful"]:
raise Exception("Failed ({}): error wasn't received".format(test["name"]))
print("Passed ({})".format(test["name"]))
def test_shachain_produce_consume(self):
seed = bitcoin.sha256(b"shachaintest")
consumer = RevocationStore()
for i in range(10000):
secret = get_per_commitment_secret_from_seed(seed, 2**48 - i - 1)
try:
consumer.add_next_entry(secret)
except Exception as e:
raise Exception("iteration " + str(i) + ": " + str(e))
if i % 1000 == 0: self.assertEqual(consumer.serialize(), RevocationStore.from_json_obj(json.loads(json.dumps(consumer.serialize()))).serialize())

231
lib/tests/test_lnhtlc.py Normal file
View File

@ -0,0 +1,231 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
import unittest
import lib.bitcoin as bitcoin
import lib.lnbase as lnbase
import lib.lnhtlc as lnhtlc
import lib.util as util
import os
import binascii
def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id):
assert local_amount > 0
assert remote_amount > 0
channel_id, _ = lnbase.channel_id_from_funding_tx(funding_txid, funding_index)
their_revocation_store = lnbase.RevocationStore()
local_config=lnbase.ChannelConfig(
payment_basepoint=privkeys[0],
multisig_key=privkeys[1],
htlc_basepoint=privkeys[2],
delayed_basepoint=privkeys[3],
revocation_basepoint=privkeys[4],
to_self_delay=143,
dust_limit_sat=10,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
)
remote_config=lnbase.ChannelConfig(
payment_basepoint=other_pubkeys[0],
multisig_key=other_pubkeys[1],
htlc_basepoint=other_pubkeys[2],
delayed_basepoint=other_pubkeys[3],
revocation_basepoint=other_pubkeys[4],
to_self_delay=143,
dust_limit_sat=10,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
)
return lnbase.OpenChannel(
channel_id=channel_id,
short_channel_id=channel_id[:8],
funding_outpoint=lnbase.Outpoint(funding_txid, funding_index),
local_config=local_config,
remote_config=remote_config,
remote_state=lnbase.RemoteState(
ctn = 0,
next_per_commitment_point=nex,
last_per_commitment_point=cur,
amount_msat=remote_amount,
revocation_store=their_revocation_store,
next_htlc_id = 0
),
local_state=lnbase.LocalState(
ctn = 0,
per_commitment_secret_seed=seed,
amount_msat=local_amount,
next_htlc_id = 0,
funding_locked_received=True,
was_announced=False
),
constraints=lnbase.ChannelConstraints(capacity=funding_sat, feerate=local_feerate, is_initiator=is_initiator, funding_txn_minimum_depth=3),
node_id=other_node_id
)
def bip32(sequence):
xprv, xpub = bitcoin.bip32_root(b"9dk", 'standard')
xprv, xpub = bitcoin.bip32_private_derivation(xprv, "m/", sequence)
xtype, depth, fingerprint, child_number, c, k = bitcoin.deserialize_xprv(xprv)
assert len(k) == 32
assert type(k) is bytes
return k
def create_test_channels():
funding_txid = binascii.hexlify(os.urandom(32)).decode("ascii")
funding_index = 0
funding_sat = bitcoin.COIN * 5
local_amount = (funding_sat * 1000) // 2
remote_amount = (funding_sat * 1000) // 2
alice_raw = [ bip32("m/" + str(i)) for i in range(5) ]
bob_raw = [ bip32("m/" + str(i)) for i in range(5,11) ]
alice_privkeys = [lnbase.Keypair(lnbase.privkey_to_pubkey(x), x) for x in alice_raw]
bob_privkeys = [lnbase.Keypair(lnbase.privkey_to_pubkey(x), x) for x in bob_raw]
alice_pubkeys = [lnbase.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys]
bob_pubkeys = [lnbase.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys]
alice_seed = os.urandom(32)
bob_seed = os.urandom(32)
alice_cur = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 1), "big"))
alice_next = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 2), "big"))
bob_cur = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 1), "big"))
bob_next = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 2), "big"))
return lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 20000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33), "alice"), lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 20000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33), "bob")
one_bitcoin_in_msat = bitcoin.COIN * 1000
class TestLNBaseHTLCStateMachine(unittest.TestCase):
def assertOutputExistsByValue(self, tx, amt_sat):
for typ, scr, val in tx.outputs():
if val == amt_sat:
break
else:
self.assertFalse()
def test_SimpleAddSettleWorkflow(self):
# Create a test channel which will be used for the duration of this
# unittest. The channel will be funded evenly with Alice having 5 BTC,
# and Bob having 5 BTC.
alice_channel, bob_channel = create_test_channels()
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
htlcAmt = one_bitcoin_in_msat
htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = htlcAmt,
cltv_expiry = 5,
total_fee = 0
)
# First Alice adds the outgoing HTLC to her local channel's state
# update log. Then Alice sends this wire message over to Bob who adds
# this htlc to his remote state update log.
aliceHtlcIndex = alice_channel.add_htlc(htlc)
bobHtlcIndex = bob_channel.receive_htlc(htlc)
# Next alice commits this change by sending a signature message. Since
# we expect the messages to be ordered, Bob will receive the HTLC we
# just sent before he receives this signature, so the signature will
# cover the HTLC.
aliceSig, aliceHtlcSigs = alice_channel.sign_next_commitment()
self.assertEqual(len(aliceHtlcSigs), 1, "alice should generate one htlc signature")
# Bob receives this signature message, and checks that this covers the
# state he has in his remote log. This includes the HTLC just sent
# from Alice.
bob_channel.receive_new_commitment(aliceSig, aliceHtlcSigs)
# Bob revokes his prior commitment given to him by Alice, since he now
# has a valid signature for a newer commitment.
bobRevocation, _ = bob_channel.revoke_current_commitment()
# Bob finally send a signature for Alice's commitment transaction.
# This signature will cover the HTLC, since Bob will first send the
# revocation just created. The revocation also acks every received
# HTLC up to the point where Alice sent here signature.
bobSig, bobHtlcSigs = bob_channel.sign_next_commitment()
# Alice then processes this revocation, sending her own revocation for
# her prior commitment transaction. Alice shouldn't have any HTLCs to
# forward since she's sending an outgoing HTLC.
alice_channel.receive_revocation(bobRevocation)
# Alice then processes bob's signature, and since she just received
# the revocation, she expect this signature to cover everything up to
# the point where she sent her signature, including the HTLC.
alice_channel.receive_new_commitment(bobSig, bobHtlcSigs)
# Alice then generates a revocation for bob.
aliceRevocation, _ = alice_channel.revoke_current_commitment()
# Finally Bob processes Alice's revocation, at this point the new HTLC
# is fully locked in within both commitment transactions. Bob should
# also be able to forward an HTLC now that the HTLC has been locked
# into both commitment transactions.
bob_channel.receive_revocation(aliceRevocation)
# At this point, both sides should have the proper number of satoshis
# sent, and commitment height updated within their local channel
# state.
aliceSent = 0
bobSent = 0
self.assertEqual(alice_channel.total_msat_sent, aliceSent, "alice has incorrect milli-satoshis sent")
self.assertEqual(alice_channel.total_msat_received, bobSent, "alice has incorrect milli-satoshis received")
self.assertEqual(bob_channel.total_msat_sent, bobSent, "bob has incorrect milli-satoshis sent")
self.assertEqual(bob_channel.total_msat_received, aliceSent, "bob has incorrect milli-satoshis received")
self.assertEqual(bob_channel.state.local_state.ctn, 1, "bob has incorrect commitment height")
self.assertEqual(alice_channel.state.local_state.ctn, 1, "alice has incorrect commitment height")
# Both commitment transactions should have three outputs, and one of
# them should be exactly the amount of the HTLC.
self.assertEqual(len(alice_channel.local_commitment.outputs()), 3, "alice should have three commitment outputs, instead have %s"% len(alice_channel.local_commitment.outputs()))
self.assertEqual(len(bob_channel.local_commitment.outputs()), 3, "bob should have three commitment outputs, instead have %s"% len(bob_channel.local_commitment.outputs()))
self.assertOutputExistsByValue(alice_channel.local_commitment, htlcAmt // 1000)
self.assertOutputExistsByValue(bob_channel.local_commitment, htlcAmt // 1000)
# Now we'll repeat a similar exchange, this time with Bob settling the
# HTLC once he learns of the preimage.
preimage = paymentPreimage
bob_channel.settle_htlc(preimage, bobHtlcIndex)
alice_channel.receive_htlc_settle(preimage, aliceHtlcIndex)
bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment()
alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2)
aliceRevocation2, _ = alice_channel.revoke_current_commitment()
aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment()
self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures")
bob_channel.receive_revocation(aliceRevocation2)
bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)
bobRevocation2, _ = bob_channel.revoke_current_commitment()
alice_channel.receive_revocation(bobRevocation2)
# At this point, Bob should have 6 BTC settled, with Alice still having
# 4 BTC. Alice's channel should show 1 BTC sent and Bob's channel
# should show 1 BTC received. They should also be at commitment height
# two, with the revocation window extended by 1 (5).
mSatTransferred = one_bitcoin_in_msat
self.assertEqual(alice_channel.total_msat_sent, mSatTransferred, "alice satoshis sent incorrect %s vs %s expected"% (alice_channel.total_msat_sent, mSatTransferred))
self.assertEqual(alice_channel.total_msat_received, 0, "alice satoshis received incorrect %s vs %s expected"% (alice_channel.total_msat_received, 0))
self.assertEqual(bob_channel.total_msat_received, mSatTransferred, "bob satoshis received incorrect %s vs %s expected"% (bob_channel.total_msat_received, mSatTransferred))
self.assertEqual(bob_channel.total_msat_sent, 0, "bob satoshis sent incorrect %s vs %s expected"% (bob_channel.total_msat_sent, 0))
self.assertEqual(bob_channel.l_current_height, 2, "bob has incorrect commitment height, %s vs %s"% (bob_channel.l_current_height, 2))
self.assertEqual(alice_channel.l_current_height, 2, "alice has incorrect commitment height, %s vs %s"% (alice_channel.l_current_height, 2))
# The logs of both sides should now be cleared since the entry adding
# the HTLC should have been removed once both sides receive the
# revocation.
self.assertEqual(alice_channel.local_update_log, [], "alice's local not updated, should be empty, has %s entries instead"% len(alice_channel.local_update_log))
self.assertEqual(alice_channel.remote_update_log, [], "alice's remote not updated, should be empty, has %s entries instead"% len(alice_channel.remote_update_log))

View File

@ -201,7 +201,7 @@ class Enumeration:
def __getattr__(self, attr):
if attr not in self.lookup:
raise AttributeError
raise AttributeError(attr)
return self.lookup[attr]
def whatis(self, value):
return self.reverseLookup[value]
@ -237,7 +237,8 @@ opcodes = Enumeration("Opcodes", [
"OP_HASH256", "OP_CODESEPARATOR", "OP_CHECKSIG", "OP_CHECKSIGVERIFY", "OP_CHECKMULTISIG",
"OP_CHECKMULTISIGVERIFY",
("OP_NOP1", 0xB0),
("OP_CHECKLOCKTIMEVERIFY", 0xB1), ("OP_CHECKSEQUENCEVERIFY", 0xB2),
("OP_CLTV", 0xB1),
("OP_CSV", 0xB2),
"OP_NOP4", "OP_NOP5", "OP_NOP6", "OP_NOP7", "OP_NOP8", "OP_NOP9", "OP_NOP10",
("OP_INVALIDOPCODE", 0xFF),
])
@ -705,11 +706,12 @@ class Transaction:
return d
@classmethod
def from_io(klass, inputs, outputs, locktime=0):
def from_io(klass, inputs, outputs, locktime=0, version=1):
self = klass(None)
self._inputs = inputs
self._outputs = outputs
self.locktime = locktime
self.version = version
return self
@classmethod

View File

@ -435,6 +435,12 @@ def bh2u(x):
return hfu(x).decode('ascii')
def xor_bytes(a: bytes, b: bytes) -> bytes:
size = min(len(a), len(b))
return ((int.from_bytes(a[:size], "big") ^ int.from_bytes(b[:size], "big"))
.to_bytes(size, "big"))
def user_dir():
if 'ANDROID_DATA' in os.environ:
return android_check_data_dir()

View File

@ -26,7 +26,7 @@
# - Standard_Wallet: one keystore, P2PKH
# - Multisig_Wallet: several keystores, P2SH
import asyncio
import os
import threading
import random
@ -503,16 +503,19 @@ class Abstract_Wallet(PrintError):
return TX_HEIGHT_LOCAL, 0, None
def get_txpos(self, tx_hash):
"return position, even if the tx is unverified"
"""Return (block_height, tx_pos_in_block).
If tx is unverified, tx_pos_in_block is -1.
"""
with self.lock:
if tx_hash in self.verified_tx:
height, timestamp, pos = self.verified_tx[tx_hash]
return height, pos
elif tx_hash in self.unverified_tx:
height = self.unverified_tx[tx_hash]
return (height, 0) if height > 0 else ((1e9 - height), 0)
return (height, -1) if height > 0 else ((1e9 - height), -1)
else:
return (1e9+1, 0)
return (1e9+1, -1)
def is_found(self):
return self.history.values() != [[]] * len(self.history)
@ -1308,6 +1311,9 @@ class Abstract_Wallet(PrintError):
self.verifier = SPV(self.network, self)
self.synchronizer = Synchronizer(self, network)
network.add_jobs([self.verifier, self.synchronizer])
asyncio.set_event_loop(network.asyncio_loop)
from .lnworker import LNWorker
self.lnworker = LNWorker(self, network)
else:
self.verifier = None
self.synchronizer = None

View File

@ -54,6 +54,7 @@ setup(
extras_require=extras_require,
packages=[
'electrum',
'electrum.lightning_payencode',
'electrum_gui',
'electrum_gui.qt',
'electrum_plugins',