Compare commits

...

277 Commits

Author SHA1 Message Date
Janus
1643acc63a lightning: fixup after rebasing on restructured master 2018-07-13 17:05:04 +02:00
ThomasV
5a29dee9b3 follow-up prev commit (fix test) 2018-07-13 16:44:01 +02:00
ThomasV
38f0f25f8e Refresh LN status in GUI using network callback. 2018-07-13 16:44:01 +02:00
Janus
4e95f5e4c0 lnhtlc: remove unnecessary double application of pending feerate 2018-07-13 16:44:01 +02:00
Janus
3887bf57fd ln: enable receiving dust htlcs 2018-07-13 16:44:01 +02:00
SomberNight
6f292eceaf lnwatcher: naive code to sweep to_local from our ctx (will not wait for timelock yet) 2018-07-13 16:44:01 +02:00
Janus
c8b72f0322 ln: use pending_local_commit while closing (won't be revoked) 2018-07-13 16:44:01 +02:00
Janus
caa82f37da ln: raise our dust/reserve to 546 to be compatible with c-lightning 2018-07-13 16:44:01 +02:00
Janus
da63e34353 ln: test fix: our commit fee is implicit from outputs 2018-07-13 16:44:01 +02:00
Janus
1807bc6f25 lnbase: do not revoke more than once if we have missed reading commitments 2018-07-13 16:44:01 +02:00
SomberNight
8009253582 lnwatcher: sweep to_remote and to_local outputs if they close 2018-07-13 16:44:01 +02:00
Janus
d0f5980d75 ln: revoke before sending bare ctx in pay(), remove subtraction of trimmed amt from fee 2018-07-13 16:44:01 +02:00
Janus
53678e0485 ln: add two trimming tests, avoid negative numbers in htlc trim decision 2018-07-13 16:44:01 +02:00
Janus
16e541f642 lnhtlc: fee update upgrade and passes ReciverCommits and SenderCommits tests, fix NameErrors in lnbase 2018-07-13 16:44:01 +02:00
Janus
0679278eb2 lnaddr: remove remnants of lightning_payencode directory 2018-07-13 16:44:01 +02:00
Janus
b8100f04ea lnaddr: fix imports 2018-07-13 16:44:01 +02:00
ThomasV
7a314a4128 move comment 2018-07-13 16:44:01 +02:00
ThomasV
debfff9482 move lnaddr.py to lib 2018-07-13 16:44:01 +02:00
Janus
e9a48f94ac ln: avoid code duplication 2018-07-13 16:44:01 +02:00
Janus
dbb2ee3b8d ln: avoid recursive dependencies, make new lnutil 2018-07-13 16:44:01 +02:00
Janus
7dfe30da2e ln: merge OpenChannel and HTLCStateMachine 2018-07-13 16:44:01 +02:00
Janus
69f64b55e3 ln: shortcut some OpenChannel fields to traversing too much 2018-07-13 16:44:01 +02:00
Janus
93b8567de4 ln: store HTLCStateMachine in lnworker.channels 2018-07-13 16:44:01 +02:00
SomberNight
2e52899d6f improve Qt Receive tab for LN payment requests 2018-07-13 16:44:01 +02:00
ThomasV
0f66abc2fd Separate open_channel dialog. In open_channel_coroutine, use host and port from channel announcements 2018-07-13 16:44:01 +02:00
Janus
189f17380a ln: use new non-classmethod add_signature_to_txin 2018-07-13 16:44:01 +02:00
Janus
343df943d3 ln: trim dust htlc outputs 2018-07-13 16:44:01 +02:00
ThomasV
dfafb64bb8 do not set channel state in close_channel; the watcher should do it 2018-07-13 16:44:01 +02:00
ThomasV
ca20741b84 lightning: single shared instance of Watcher, ChannelDB and PathFinder 2018-07-13 16:44:01 +02:00
ThomasV
c732ede81d disable lightning on mainnet 2018-07-13 16:44:01 +02:00
Janus
b5ddd295e4 ln: improve lnhtlc, passes test 2018-07-13 16:44:01 +02:00
Janus
594bde07f8 lnhtlc: use current_per_commitment_point, current_commitment_signature 2018-07-13 16:44:01 +02:00
ThomasV
28940ac767 fix reestablish_channel 2018-07-13 16:44:01 +02:00
ThomasV
365060b63f follow-up previous commit 2018-07-13 16:44:01 +02:00
ThomasV
0e7fab5e60 reestablish channels in network callback 2018-07-13 16:44:01 +02:00
ThomasV
683dce0410 channel watcher class 2018-07-13 16:44:01 +02:00
Janus
bd4314f5fa ln: add was_announced in test_lnhtlc 2018-07-13 16:44:01 +02:00
Janus
1266fda8c5 ln: close channels 2018-07-13 16:44:01 +02:00
Janus
1562bb653c ln: don't corrupt channels storage when multiple funding_locked are received 2018-07-13 16:44:01 +02:00
Janus
d0266f798b ln: don't break channel when failing htlc 2018-07-13 16:44:01 +02:00
Janus
3149ef708a ln: announcement reliability fixes for qt, remove asserts forbidding unbalanced channels 2018-07-13 16:44:01 +02:00
Janus
3e18eb716f ln: begin handling htlc failures 2018-07-13 16:44:01 +02:00
SomberNight
a35c3b4903 add minor comment for RouteEdge as clarification 2018-07-13 16:44:01 +02:00
SomberNight
4ac255a3b7 LNPathFinder: cltv delta of first edge in a path should be ignored 2018-07-13 16:44:01 +02:00
Janus
d54335fdaa ln: channel announcements 2018-07-13 16:44:01 +02:00
Janus
b813b9c67f ln: lnpay: revoke until we get a commitment tx without htlcs 2018-07-13 16:44:01 +02:00
Janus
5908961d2e ln: fix reestablishing channel with no mined funding tx 2018-07-13 16:44:01 +02:00
Janus
04e5b35b00 ln: fix repeated payments 2018-07-13 16:44:01 +02:00
Janus
e197e1d27b ln: avoid code duplication 2018-07-13 16:44:01 +02:00
Janus
ed8e02de6a ln: save remote's secrets in RevocationStore, not our secrets. call lnhtlc.receive_revocation 2018-07-13 16:44:01 +02:00
Janus
150d1a9337 lnbase/lnhtlc: use lnhtlc more instead of manually constructing tx'es 2018-07-13 16:44:01 +02:00
Janus
fef5a13dbc lnbase: use sign_next_commitment for initial remote_ctx 2018-07-13 16:44:01 +02:00
Janus
f2d1b00072 daemon: prevent json-rpc-pelix from suppressing stack traces of TypeErrors 2018-07-13 16:44:01 +02:00
Janus
e27a3948fb lnbase: use lnhtlc when verifying our initial commitment tx 2018-07-13 16:44:01 +02:00
Janus
ff4f4c502a lnbase: use broadcast_transaction instead of broadcast (follow up e57e55aad) 2018-07-13 16:44:01 +02:00
Janus
f3038ff4d2 test_lnbase: use new Peer API (with lnworker) 2018-07-13 16:44:01 +02:00
Janus
74b0d38686 ln: remove unneeded forwarding htlc features, check commitment sig using lnhtlc while receiving 2018-07-13 16:44:01 +02:00
Janus
61b5273916 ln: integrate lnhtlc in lnbase, fix multiple lnhtlc bugs 2018-07-13 16:44:01 +02:00
Janus
e1fdb5afb4 ln: remove lnbase global flag 2018-07-13 16:44:01 +02:00
Janus
2d130672c6 ln: request_initial_sync, increase our max_htlc_value, fix receiving payment 2018-07-13 16:44:01 +02:00
ThomasV
8524ea4f7e do not block GUI with open_channel 2018-07-13 16:44:01 +02:00
ThomasV
fe464113eb lightning: display remote balance in gui 2018-07-13 16:44:01 +02:00
ThomasV
69602907fe lnbase: mark_open on startup 2018-07-13 16:44:01 +02:00
ThomasV
fc26f1be37 revert the introduction of add_invoice_coroutine in a612c2b09 2018-07-13 16:44:01 +02:00
ThomasV
6372b44af7 do not pass channel list to update_rows signal, as it is sent to all windows 2018-07-13 16:44:01 +02:00
SomberNight
0621cc6f77 wait for peer.initialized in channel_establishment_flow 2018-07-13 16:44:01 +02:00
ThomasV
0d09e2b2cf follow up 0b3a882e7d57c8a42be48c491a46dc814eab6acb 2018-07-13 16:44:01 +02:00
ThomasV
3a3ecfc00e simplify funding_locked
expose lnworker in peer
update channel_db when channels are open
2018-07-13 16:44:01 +02:00
ThomasV
deb1d9fe3b Display channel status in the GUI.
Do not convert channel_id to integer; there is no reason to do that.
2018-07-13 16:44:01 +02:00
ThomasV
f702bbac11 integrate channels_list with existing framework 2018-07-13 16:44:01 +02:00
ThomasV
eb9434ca48 request list: remove Type column 2018-07-13 16:44:01 +02:00
ThomasV
ed496dca4b qt: fix unit of lnaddr.amount 2018-07-13 16:44:01 +02:00
ThomasV
14201f6463 follow-up a612c2b0983ab4c6798156aebf1cd550fb3e0447 2018-07-13 16:44:01 +02:00
Janus
257e9cabe2 ln: htlc state machine (not used yet) 2018-07-13 16:44:01 +02:00
Janus
ee6dfafcf9 ln: save channels in dict, warn on invoice exceeding max_htlc amount 2018-07-13 16:44:01 +02:00
ThomasV
123f10054e lightning: connect send button 2018-07-13 16:44:01 +02:00
ThomasV
a388fb6ffd lightning GUI: use existing receive and send tabs with lightning invoices 2018-07-13 16:44:01 +02:00
Janus
0a67a824b6 ln: don't make invoice if peer can't possibly pay, append _sat to sat
parameters to avoid confusion
2018-07-13 16:44:01 +02:00
ThomasV
2defce6a42 lnworker: generate and save private key 2018-07-13 16:44:01 +02:00
ThomasV
a012cd1e0d follow up previous commit 2018-07-13 16:44:01 +02:00
ThomasV
8580be30b2 lnworker: separate invoice creation from payment flow 2018-07-13 16:44:01 +02:00
Janus
77136f605c 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-07-13 16:44:01 +02:00
ThomasV
784cd3ecea fix channel_reestablish 2018-07-13 16:44:01 +02:00
ThomasV
7baaa5dcc0 lnbase: fix read_message 2018-07-13 16:44:01 +02:00
Janus
4d34977359 ln: restore functionality 2018-07-13 16:44:01 +02:00
Janus
6b5adeae2b ln: save remote node_id in channel 2018-07-13 16:44:01 +02:00
SomberNight
9d0bb08451 split lnrouter from lnbase 2018-07-13 16:44:01 +02:00
SomberNight
d329d501c4 remove function H256 2018-07-13 16:44:01 +02:00
ThomasV
db75ff04ce fix amount in open_channel, add listchannels command 2018-07-13 16:44:01 +02:00
ThomasV
9598978cc4 move on_funding_locked to lnworker 2018-07-13 16:44:01 +02:00
ThomasV
68f409fd27 lightning: add payment methods to lnworker 2018-07-13 16:44:01 +02:00
ThomasV
a69e72b830 lightning: move lnworker code to its own module 2018-07-13 16:44:01 +02:00
ThomasV
3a9b15bc2d fix lnaddr.py following rebase 2018-07-13 16:44:01 +02:00
ThomasV
82953cb092 update lnbase after crypto refactoring 2018-07-13 16:44:01 +02:00
Janus
9ad0b7a878 lnbase: remove lnbase stub 2018-07-13 16:44:01 +02:00
ThomasV
857b6e99b8 lnbase: pass password to mktx 2018-07-13 16:44:01 +02:00
ThomasV
275f26729f qt: fix password passed to open_channel, cleanup 2018-07-13 16:44:01 +02:00
Janus
e5b89c6c0e lnbase: mSAT hygiene, multiple multi-hop payments can be received 2018-07-13 16:44:01 +02:00
Janus
e550e0241b kivy: port lightning ui to lnbase 2018-07-13 16:44:01 +02:00
Janus
5f3613d8b7 lightning: remove hub based approach, port qt gui to lnbase 2018-07-13 16:43:59 +02:00
Janus
953ea1bea0 lnbase: use small buffer when reading, support new_channel without payment in online test, send channel_reserve_satoshis 2018-07-13 16:43:24 +02:00
Janus
8bd2f2c0ee lnbase: use correct cltv_expiry calculation (use invoice) 2018-07-13 16:43:24 +02:00
Janus
96d2e788b8 lnbase: fix multi-hop payments 2018-07-13 16:43:24 +02:00
Janus
a7c61077cb lnbase: fix onion-hop payload construction again (cltv currently broken) 2018-07-13 16:43:24 +02:00
Janus
2c3870d0fe lnbase: fix multi-hop fees, initial handling of received update_add_htlc during payment 2018-07-13 16:43:24 +02:00
Janus
34cc9e2ef7 lnbase: calculate cltv_expiry for onion_packet correctly 2018-07-13 16:43:24 +02:00
Janus
babbbfcdea lnbase: try multi-hop onion package, type safety 2018-07-13 16:43:24 +02:00
SomberNight
00887ee76e PathFinder: change path element semantics from "from node, take edge" to "to get to node, use edge" 2018-07-13 16:43:24 +02:00
SomberNight
0c70b3af1a create route from path, that includes extra info needed for routing 2018-07-13 16:43:24 +02:00
SomberNight
f389c96814 bolt-04: decryption of errors 2018-07-13 16:43:24 +02:00
Janus
bc11675aae lnbase online test: use random node key when making new channel, save node key, multiple actions per invocation 2018-07-13 16:43:24 +02:00
Janus
85cb9bc7f5 lnbase: fix pay(), save htlc_id's, generate onion packet correctly 2018-07-13 16:43:24 +02:00
Janus
44ffdd1936 lnbase/online_test: save short_channel_id to wallet and build onion packet with it 2018-07-13 16:43:24 +02:00
SomberNight
1fc0b1378d calc short_channel_id after funding locked 2018-07-13 16:43:24 +02:00
Janus
a9cfa65f64 lnbase: initial 'payment to remote' attempt 2018-07-13 16:43:24 +02:00
Janus
3d3fe9976c lnbase: formatting, remove imports 2018-07-13 16:43:24 +02:00
Janus
39df2a50b7 lnbase: verify commitment tx'es again 2018-07-13 16:43:24 +02:00
Janus
783209e951 lnbase: infinite amount of incoming payments 2018-07-13 16:43:24 +02:00
Janus
6c96f3a646 lnbase: two payments working, temporarily disable sig check 2018-07-13 16:43:24 +02:00
Janus
39000763ba lnbase: store remote revocation store, don't store all remote revocation points, verify ctn numbers in reestablish 2018-07-13 16:43:24 +02:00
Janus
fe40f0a275 lnbase: add RevocationStore test, remove unnecessary lnd helper functions 2018-07-13 16:43:24 +02:00
Janus
15f5d6d603 lnbase: compact commitment secret storage 2018-07-13 16:43:24 +02:00
Janus
a509852b36 lnbase: no negative commitment number nonsense 2018-07-13 16:43:24 +02:00
Janus
69f5adea2f lnbase: move channel commitment number increment to function 2018-07-13 16:43:24 +02:00
Janus
a33d74420a lnbase: receive repeated payments 2018-07-13 16:43:24 +02:00
Janus
1643cac047 tests: don't use default lightning_peers in online test 2018-07-13 16:43:24 +02:00
Janus
bd7762186c lnbase: channel reestablishment working 2018-07-13 16:43:24 +02:00
SomberNight
109d672bdb bolt-04: implement processing of onion packets 2018-07-13 16:43:24 +02:00
SomberNight
a3e1e5dd47 minor clean-up of prev. util.xor_bytes 2018-07-13 16:43:24 +02:00
SomberNight
2570fd12f8 implement bolt-04 onion packet construction 2018-07-13 16:43:24 +02:00
Janus
b94f93f8b9 lnbase: save channel details in wallet, enable running online test with reestablishment_mode 2018-07-13 16:43:24 +02:00
Janus
0e9f4c4120 lnbase: move waiting for funding_locked to new function, make function for signing and sig conversion 2018-07-13 16:43:24 +02:00
Janus
1d821cb2c6 lnbase: make function for building htlc_tx depending on if it is for_us/we_receive 2018-07-13 16:43:24 +02:00
Janus
572d01d0c5 lnbase: verify their htlc signature 2018-07-13 16:43:24 +02:00
ThomasV
063c9b2e28 lnbase: standardize to_bytes calls 2018-07-13 16:43:24 +02:00
Janus
50f3c0dd00 lnbase: fix custom local to_self_delay, use node privkey derived from timestamp in online test 2018-07-13 16:43:24 +02:00
Janus
177df75d87 test_lnbase_online: pass password=None to channel_establishment_flow 2018-07-13 16:43:24 +02:00
Janus
73870d0457 lnbase: avoid copying variables, insert newlines 2018-07-13 16:43:24 +02:00
ThomasV
5ebc328ae6 lnbase: derive keys from wallet keystore 2018-07-13 16:43:24 +02:00
Janus
ff7a73ce19 lnbase: avoid local variables, remote useless comments, name basepoints as such 2018-07-13 16:43:24 +02:00
Janus
0a46b1c335 lnbase: set new field in Transaction instead of returning a tuple in make_commitment 2018-07-13 16:43:24 +02:00
Janus
67c5a8ba16 lnbase: set to_self_delay back to 144, defer cltv_expiry problem 2018-07-13 16:43:24 +02:00
Janus
e4434d530d lnbase: use correct delay 2018-07-13 16:43:24 +02:00
Janus
28a6a3c1da lnbase: avoid code duplication, return htlc outpoint dict in make_commitment 2018-07-13 16:43:24 +02:00
Janus
188588936a lnbase: simplify commitment transaction building with open channel 2018-07-13 16:43:24 +02:00
Janus
9a6646c780 lnbase: organize channel data 2018-07-13 16:43:24 +02:00
Janus
07624960e5 lnbase: allow passing KeypairGenerator to channel_establishment_flow, fix derive_privkey 2018-07-13 16:43:24 +02:00
Janus
b433fb24bd lnbase: receiving invoice payment works 2018-07-13 16:43:24 +02:00
Janus
dc5e3e9cfa lnbase: commitment_signed, revoke_and_ack now accepted without errors 2018-07-13 16:43:24 +02:00
SomberNight
0a55216045 bitcoin.py: SCRIPT-related clean-up. transaction.py: construct_witness 2018-07-13 16:43:24 +02:00
Janus
e77c5b3c30 lnbase: fix their new commitment transaction (htlc tx construction still incorrect) 2018-07-13 16:43:24 +02:00
ThomasV
38f3db3712 lnbase: fix bug in message parsing 2018-07-13 16:43:24 +02:00
ThomasV
ab715438c6 follow up b5eb7dd7683f24f03c80ab8f612658b5f3966eb1 2018-07-13 16:43:24 +02:00
Janus
fdef6d5987 lnbase: attempt at making htlc_signature to send (currently remote fails due to wrong num_htlcs in commitment_signed) 2018-07-13 16:43:24 +02:00
ThomasV
612f6958fe simplification 2018-07-13 16:43:24 +02:00
Janus
3b79d58237 lnbase: add TODO explaining how to verify htlc_signature given to us 2018-07-13 16:43:24 +02:00
Janus
12457dea3d lnbase: verification of new local commitment working 2018-07-13 16:43:24 +02:00
Janus
0d1a9f6e6c lnbase: derive next keys when making updated local commitment transaction 2018-07-13 16:43:24 +02:00
Janus
bef837de85 lnbase: try to receive payment, work on commitment tx with htlcs 2018-07-13 16:43:24 +02:00
Janus
0564d93379 lnbase: handle commitment transaction update (receive funds, not working yet) 2018-07-13 16:43:24 +02:00
Janus
c21d2159db simnet/testnet support in bolt11, set max-htlc-value-in-flight 2018-07-13 16:43:24 +02:00
SomberNight
c1fbd55dfe transaction.py: sign_txin. allow override for get_preimage_script.
test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate now passes
2018-07-13 16:43:24 +02:00
ThomasV
e7e3558fba redundant: you subscribed only to 'updated' 2018-07-13 16:43:24 +02:00
ThomasV
eb6be6d77d follow-up prev commit 2018-07-13 16:43:24 +02:00
ThomasV
52b94035d9 lnbase: verify remote signature 2018-07-13 16:43:24 +02:00
SomberNight
e34d6d0885 channel_establishment_flow: use get_per_commitment_secret_from_seed 2018-07-13 16:43:24 +02:00
Janus
0d40f06fcc lnbase: improve htlc_tx generation (only localsig wrong) 2018-07-13 16:43:24 +02:00
SomberNight
f3d9474c06 get_per_commitment_secret_from_seed: small clean-up 2018-07-13 16:43:24 +02:00
Janus
ba265da02a fix derive_secret 2018-07-13 16:43:24 +02:00
SomberNight
3a5e3f19db get_per_commitment_secret_from_seed (not working yet) 2018-07-13 16:43:24 +02:00
Janus
e0d28e3f6e lnbase: avoid race while waiting for funding_locked, wait for un-reversed hash 2018-07-13 16:43:24 +02:00
Janus
af8374b02e complete bolt11 port to ecdsa instead of secp256k1 2018-07-13 16:43:24 +02:00
SomberNight
527712df48 channel_establishment_flow: wait for confirmations of funding txn 2018-07-13 16:43:24 +02:00
ThomasV
82146871eb add processing flow for funding_locked 2018-07-13 16:43:24 +02:00
ThomasV
64c8c74445 lnbase: fix parameters to make_commitment in htlc test 2018-07-13 16:43:24 +02:00
SomberNight
1bd4b5e73e transaction.py: shortcut witness/scriptSig serialisation 2018-07-13 16:43:24 +02:00
Janus
1c84b125fb lightning channels list: add mock server for testing 2018-07-13 16:43:24 +02:00
Janus
c6e621a910 lnbase: add some comments 2018-07-13 16:43:24 +02:00
SomberNight
91c5ddfad8 constants.py: Simnet inherits from Testnet 2018-07-13 16:43:24 +02:00
Janus
30abdbfda3 use same servers for simnet as for regtest 2018-07-13 16:43:24 +02:00
Janus
bb9938c52e lightning-hub: update rpc stubs, do not ignore them in gitignore 2018-07-13 16:43:24 +02:00
ThomasV
836fae809c lnbase: fix initial commitment transaction 2018-07-13 16:43:24 +02:00
SomberNight
e7bae0fe5d refactor storage of channels, path finding 2018-07-13 16:43:24 +02:00
Janus
e6b22e9d56 avoid duplicating bech32 module 2018-07-13 16:43:24 +02:00
Janus
c5a9b2d19a lnbase: more work on make_htlc_tx 2018-07-13 16:43:24 +02:00
Janus
80c8ea15e0 lnbase: make_htlc_tx 2018-07-13 16:43:24 +02:00
ThomasV
f4c7702c0a fix: use remote_per_commitment_point 2018-07-13 16:43:24 +02:00
ThomasV
205cd259fd lnbase: derive blinded pubkey 2018-07-13 16:43:24 +02:00
ThomasV
0c8a6e535e lnbase: fix variable name 2018-07-13 16:43:24 +02:00
ThomasV
3b3732e82d lnbase: add privkey derivation 2018-07-13 16:43:24 +02:00
ThomasV
b9633896da add test for key derivation 2018-07-13 16:43:24 +02:00
ThomasV
d5db39ce27 lnbase: key derivation (WIP) 2018-07-13 16:43:24 +02:00
Janus
66d139b515 lnbase: test signing of first htlc test case 2018-07-13 16:43:24 +02:00
Janus
53ddc373d5 lnbase: make_received_htlc 2018-07-13 16:43:24 +02:00
ThomasV
0edba56315 fix hash in make_offered_htlc 2018-07-13 16:43:24 +02:00
ThomasV
a14b54dbe5 cleanup lnbase tests 2018-07-13 16:43:24 +02:00
Janus
d6145d0685 lightning-hub: remove path hack, use relative imports 2018-07-13 16:43:24 +02:00
SomberNight
9796ef264e naive route finding 2018-07-13 16:43:24 +02:00
Janus
d3d23066e9 lnbase: offered htlc script construction 2018-07-13 16:43:24 +02:00
ThomasV
02d272d790 use acceptable variable names 2018-07-13 16:43:24 +02:00
ThomasV
1ded2fd7b2 lightning: separate testing from main code 2018-07-13 16:43:24 +02:00
ThomasV
5a3cbc0907 lightning: store network view 2018-07-13 16:43:24 +02:00
ThomasV
85e6708b44 lnbase: parse ipv6, fix transport bug 2018-07-13 16:43:24 +02:00
ThomasV
1d6527c2ec lnbase: fix read_message, reduce verbosity 2018-07-13 16:43:24 +02:00
ThomasV
e6c3b27605 lnbase: implement key rotation, request initial sync in localfeatures 2018-07-13 16:43:24 +02:00
ThomasV
dcd4ea23db lnbase: verify signature in node_announcement 2018-07-13 16:43:24 +02:00
SomberNight
47ed815882 bitcoin.py: implement add_number_to_script. fix CSV arg in make_commitment. 2018-07-13 16:43:24 +02:00
ThomasV
13cb82a203 lnbase: fix test 2018-07-13 16:43:24 +02:00
SomberNight
905998e87a implement script_num_to_hex. fix encoding of argument for CSV in make_commitment 2018-07-13 16:43:24 +02:00
Janus
2fe272de02 lightning_channels_list: use signals to avoid segfault 2018-07-13 16:43:24 +02:00
SomberNight
f1017b9ed7 fixes for make_commitment, but still incorrect destination address (csv arg?) 2018-07-13 16:43:24 +02:00
ThomasV
1d319ff22f lnbase: fix tx amounts 2018-07-13 16:43:24 +02:00
Janus
63e345910e lightning-hub: include ln relative to current directory 2018-07-13 16:43:24 +02:00
Janus
446b5c9f45 lnbase_test: add first commitment tx with 5 htlcs test 2018-07-13 16:43:24 +02:00
ThomasV
005f6165fa lnbase: fix locktime and nsequence 2018-07-13 16:43:24 +02:00
Janus
a543ec27fb lnbase_test: insert remote_signature and compare fields independently 2018-07-13 16:43:24 +02:00
ThomasV
5aba0b9538 lnbase: create unit test for commitment tx 2018-07-13 16:43:24 +02:00
Janus
802206bdf3 network: stop loop on loop thread 2018-07-13 16:43:24 +02:00
ThomasV
4a4a521f4b lnbase: fix signature index 2018-07-13 16:43:24 +02:00
ThomasV
90bab35789 lnbase: initial commitment transaction 2018-07-13 16:43:24 +02:00
ThomasV
598a844d6f lnbase: fix funding address, funding_output_index 2018-07-13 16:43:24 +02:00
ThomasV
ca04043781 follow up 1aac9e59ed957898fceef99b29b9cc17d7843569 2018-07-13 16:43:24 +02:00
ThomasV
534bc2997b lnbase: communication privkey belongs to peer 2018-07-13 16:43:24 +02:00
Janus
9949a8fd58 lightning: fix hub backend loop availability 2018-07-13 16:43:24 +02:00
Janus
33c5e63f81 lnbase: more parts of channel establishment 2018-07-13 16:43:24 +02:00
Janus
f2fac43c21 lnbase: remove unnecessary try/except 2018-07-13 16:43:24 +02:00
Janus
86bc6a2345 lnbase: lnbase_test must use threadsafe task submission 2018-07-13 16:43:24 +02:00
ThomasV
a702202e84 lnbase: decorator that handles exceptions 2018-07-13 16:43:24 +02:00
Janus
208a6870ac lnbase: fix shutdown when lnbase has exception in main_loop 2018-07-13 16:43:24 +02:00
Janus
c05e429416 lnbase: print exceptions from main_loop 2018-07-13 16:43:24 +02:00
Janus
903380ad5d lnbase: initialize loop variable in main 2018-07-13 16:43:24 +02:00
Janus
afaef1e7ab network: do not acquire lightninglock for lnbase 2018-07-13 16:43:24 +02:00
Janus
c0a8fd811f lnbase: add lnbase_test 2018-07-13 16:43:24 +02:00
ThomasV
1c90479f55 lnbase: expose wallet object in LNWorker 2018-07-13 16:43:24 +02:00
Janus
0c1e31339a lnbase: merge initialize and main_loop 2018-07-13 16:43:24 +02:00
Janus
c87afe36d2 lnbase: handle error during channel establishment 2018-07-13 16:43:24 +02:00
Janus
ecc1c7a8cc lnbase: channel establishment flow, avoid using Wallet instance 2018-07-13 16:43:24 +02:00
ThomasV
10bb6463e8 lnbase: use relative imports 2018-07-13 16:43:24 +02:00
Janus
4e2ab7cb6b lnbase: support simnet/testnet, create accepted open_channel message 2018-07-13 16:43:24 +02:00
Janus
193b675316 lnbase: use valid pubkeys in open_channel 2018-07-13 16:43:24 +02:00
Janus
aec48f76bd lnbase: try sending open_channel 2018-07-13 16:43:24 +02:00
ThomasV
d247eecfd8 lnbase: add draft handlers 2018-07-13 16:43:24 +02:00
Janus
b24f989b79 lnbase: avoid reimplementing int.to_bytes 2018-07-13 16:43:24 +02:00
Janus
d90490f4d4 lnbase: avoid reimplementing int.from_bytes 2018-07-13 16:43:24 +02:00
ThomasV
4615b9134d fix asyncio loop 2018-07-13 16:43:24 +02:00
Janus
fe7d1ccd7f lnbase: do not catch all exceptions, tolerate calculations with variables from kwargs 2018-07-13 16:43:24 +02:00
ThomasV
b64e97a68b integrate lnbase with network 2018-07-13 16:43:24 +02:00
ThomasV
14d267b554 lnbase: process ping messages 2018-07-13 16:43:24 +02:00
ThomasV
d5bd965aa5 lnbase: create main loop 2018-07-13 16:43:24 +02:00
ThomasV
14837ba01d lnbase: save buffer for next read 2018-07-13 16:43:24 +02:00
ThomasV
26823b0b61 lnbase: Peer class 2018-07-13 16:43:24 +02:00
ThomasV
11b4666880 lnbase: fix json loading and indentation 2018-07-13 16:43:24 +02:00
Janus
40a158e914 lightning: do not list python files as resources, use lightning spec generated serialization 2018-07-13 16:43:21 +02:00
ThomasV
485c2c6c23 lightning network base 2018-07-13 16:35:43 +02:00
Janus
1c8be7d3e7 lightning: qt channel dialog, fix for shutdown when lightning disabled 2018-07-13 16:35:43 +02:00
Janus
7419ddb4c1 lightning: channel details popup 2018-07-13 16:35:43 +02:00
Janus
f2798d09ea kivy: paste test seed using xclip, lightning: do not catch BaseException unnecessarily, fix clearSubscribers, detect passworded wallet correctly 2018-07-13 16:35:43 +02:00
ThomasV
4426ea2e70 simplify parameters, add lndhost to config 2018-07-13 16:35:43 +02:00
Janus
ea5a42d2eb lightning: add --simnet and --lightning switches 2018-07-13 16:35:43 +02:00
Janus
c93ad2bcb2 lightning: paste sample using clipboard 2018-07-13 16:35:42 +02:00
Janus
0a1499edf9 kivy: fix channel list error handling, close functionality for inactive channels 2018-07-13 16:35:42 +02:00
Janus
be5af242ed lightning: assert result type, add invoice qr dialog 2018-07-13 16:35:42 +02:00
Janus
88befdbc92 lightning: kivy: open channel button in invoice 2018-07-13 16:35:42 +02:00
Janus
abf965d58e lightning: fix kivy channel close 2018-07-13 16:35:42 +02:00
Janus
4602f87173 lightning: python3.5 compat 2018-07-13 16:35:42 +02:00
Janus
f9834d4eb6 lightning: fix channels dialog 2018-07-13 16:35:42 +02:00
Janus
911595f90f lightning: add missing import, set console to none initially 2018-07-13 16:35:42 +02:00
Janus
2bb59fe464 lightning: do not require lock for broadcast tx, it is thread-safe 2018-07-13 16:35:42 +02:00
Janus
d1986dd163 lightning: save key derivation point 2018-07-13 16:35:42 +02:00
Janus
3e7d2d29bb lightning: separate thread for publish transaction 2018-07-13 16:35:42 +02:00
Janus
ee576d3ead lightning: use queueing lock 2018-07-13 16:35:42 +02:00
Janus
a13fb91ea4 lightning: less junk on console, quicker shutdown 2018-07-13 16:35:42 +02:00
Janus
7034153c06 lightning: don't receive too much data, workaround by reading byte by byte 2018-07-13 16:35:42 +02:00
Janus
f5bfbf97d3 lightning: fix syntax 2018-07-13 16:35:42 +02:00
Janus
702a2919ec lightning: complete moving of lightning objects, acquire net/wallet lock while answering lightning requests 2018-07-13 16:35:42 +02:00
Janus
920371e350 lightning: misc patches, launch asyncio loop on separate thread 2018-07-13 16:34:23 +02:00
Janus
cce6421340 lightning: march 2018 rebase, without integration 2018-07-13 16:34:23 +02:00
40 changed files with 6142 additions and 127 deletions

4
.gitignore vendored
View File

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

View File

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

View File

@ -8,3 +8,5 @@ dnspython
jsonrpclib-pelix
PySocks>=1.6.6
qdarkstyle<3.0
bitstring
cryptography

View File

@ -680,6 +680,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(bytes.fromhex(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',
@ -727,6 +752,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"),

View File

@ -95,6 +95,16 @@ class BitcoinTestnet:
BIP44_COIN_TYPE = 1
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"
@ -122,6 +132,9 @@ def set_mainnet():
global net
net = BitcoinMainnet
def set_simnet():
global net
net = BitcoinSimnet
def set_testnet():
global net

View File

@ -133,16 +133,18 @@ 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()
def hmac_oneshot(key: bytes, msg: bytes, digest) -> bytes:
if hasattr(hmac, 'digest'):
# requires python 3.7+; faster

View File

@ -284,7 +284,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

@ -252,9 +252,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:
@ -303,6 +303,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)
@ -367,6 +374,9 @@ class ECPrivkey(ECPubkey):
privkey_32bytes = number_to_string(scalar, CURVE_ORDER)
return privkey_32bytes
def get_secret_bytes(self) -> bytes:
return number_to_string(self.secret_scalar, CURVE_ORDER)
def sign(self, data: bytes, sigencode=None, sigdecode=None) -> bytes:
if sigencode is None:
sigencode = sig_string_from_r_and_s
@ -384,12 +394,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
@ -397,7 +407,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

@ -451,6 +451,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

@ -71,6 +71,8 @@ Label.register('Roboto',
from electrum.util import (base_units, NoDynamicFeeEstimates, decimal_point_to_base_unit_name,
base_unit_name_to_decimal_point, NotEnoughFunds)
from .uix.dialogs.lightning_payer import LightningPayerDialog
from .uix.dialogs.lightning_channels import LightningChannelsDialog
class ElectrumWindow(App):
@ -584,6 +586,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()
@ -601,6 +611,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('electrum/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.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:

View File

@ -0,0 +1,116 @@
# -*- coding: utf-8 -*-
from PyQt5 import QtCore, QtWidgets
from PyQt5.QtWidgets import *
from electrum.util import inv_dict, bh2u, bfh
from electrum.i18n import _
from electrum.lnhtlc import HTLCStateMachine
from .util import MyTreeWidget, SortableTreeWidgetItem, WindowModalDialog, Buttons, OkButton, CancelButton
from .amountedit import BTCAmountEdit
class ChannelsList(MyTreeWidget):
update_rows = QtCore.pyqtSignal()
update_single_row = QtCore.pyqtSignal(HTLCStateMachine)
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)
self.status = QLabel('')
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 = QMenu()
channel_id = self.currentItem().data(0, QtCore.Qt.UserRole)
print('ID', bh2u(channel_id))
def close():
suc, msg = self.parent.wallet.lnworker.close_channel(channel_id)
if not suc:
print('channel close broadcast failed:', msg)
assert suc # TODO show error message in dialog
menu.addAction(_("Close channel"), close)
menu.exec_(self.viewport().mapToGlobal(position))
@QtCore.pyqtSlot(HTLCStateMachine)
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):
b = QPushButton(_('Open Channel'))
b.clicked.connect(self.new_channel_dialog)
h = QHBoxLayout()
h.addWidget(self.status)
h.addStretch()
h.addWidget(b)
return h
def update_status(self):
n = len(self.parent.network.lightning_nodes)
nc = len(self.parent.network.channel_db)
np = len(self.parent.wallet.lnworker.peers)
self.status.setText(_('{} peers, {} nodes, {} channels').format(np, n, nc))
def new_channel_dialog(self):
d = WindowModalDialog(self.parent, _('Open Channel'))
d.setFixedWidth(700)
vbox = QVBoxLayout(d)
h = QGridLayout()
local_nodeid = QLineEdit()
local_nodeid.setText(bh2u(self.parent.wallet.lnworker.pubkey))
local_nodeid.setReadOnly(True)
local_nodeid.setCursorPosition(0)
remote_nodeid = QLineEdit()
local_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
local_amt_inp.setAmount(200000)
push_amt_inp = BTCAmountEdit(self.parent.get_decimal_point)
push_amt_inp.setAmount(0)
h.addWidget(QLabel(_('Your Node ID')), 0, 0)
h.addWidget(local_nodeid, 0, 1)
h.addWidget(QLabel(_('Remote Node ID')), 1, 0)
h.addWidget(remote_nodeid, 1, 1)
h.addWidget(QLabel('Local amount'), 2, 0)
h.addWidget(local_amt_inp, 2, 1)
h.addWidget(QLabel('Push amount'), 3, 0)
h.addWidget(push_amt_inp, 3, 1)
vbox.addLayout(h)
vbox.addLayout(Buttons(CancelButton(d), OkButton(d)))
if not d.exec_():
return
nodeid_hex = str(remote_nodeid.text())
local_amt = local_amt_inp.get_amount()
push_amt = push_amt_inp.get_amount()
try:
node_id = bfh(nodeid_hex)
except:
self.parent.show_error(_('Invalid node ID'))
return
if node_id not in self.parent.wallet.lnworker.peers and node_id not in self.parent.network.lightning_nodes:
self.parent.show_error(_('Unknown node:') + ' ' + nodeid_hex)
return
assert local_amt >= 200000
assert local_amt >= push_amt
self.main_window.protect(self.open_channel, (node_id, local_amt, push_amt))
def open_channel(self, *args, **kwargs):
self.parent.wallet.lnworker.open_channel(*args, **kwargs)

View File

@ -60,6 +60,7 @@ from .transaction_dialog import show_transaction
from .fee_slider import FeeSlider
from .util import *
from .installwizard import WIF_HELP_TEXT
from .channels_list import ChannelsList
class StatusBarButton(QPushButton):
@ -124,6 +125,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.create_status_bar()
self.need_update = threading.Event()
self.need_update_ln = threading.Event()
self.decimal_point = config.get('decimal_point', 5)
self.num_zeros = int(config.get('num_zeros',0))
@ -137,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'))
@ -150,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")
@ -182,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', 'ln_status']
# 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
@ -190,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)
@ -201,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)
@ -212,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
@ -298,6 +295,16 @@ 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)
elif event == 'ln_status':
self.need_update_ln.set()
else:
self.print_error("unexpected network message:", event, args)
@ -348,6 +355,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()
@ -358,6 +366,7 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.update_console()
self.clear_receive_tab()
self.request_list.update()
self.channels_list.update()
self.tabs.show()
self.init_geometry()
if self.config.get('hide_gui') and self.gui_object.tray.isVisible():
@ -518,6 +527,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)
@ -638,6 +648,9 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
if self.need_update.is_set():
self.need_update.clear()
self.update_wallet()
if self.need_update_ln.is_set():
self.need_update_ln.clear()
self.channels_list.update_status()
# resolve aliases
# FIXME this is a blocking network call that has a timeout of 5 sec
self.payto_e.resolve()
@ -764,6 +777,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)
@ -789,16 +807,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)
@ -833,23 +841,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
@ -860,14 +875,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)
@ -881,13 +901,18 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.request_list.update()
self.clear_receive_tab()
def delete_lightning_payreq(self, payreq_key):
self.wallet.lnworker.delete_invoice(payreq_key)
self.request_list.update()
self.clear_receive_tab()
def get_request_URI(self, addr):
req = self.wallet.receive_requests[addr]
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'):
@ -918,15 +943,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)
@ -935,9 +979,8 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
self.show_error(_('Error adding payment request') + ':\n' + str(e))
else:
self.sign_payment_request(addr)
self.save_request_button.setEnabled(False)
#self.save_request_button.setEnabled(False)
finally:
self.request_list.update()
self.address_list.update()
def view_and_paste(self, title, msg, data):
@ -963,26 +1006,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('')
@ -1025,14 +1048,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.')
@ -1437,6 +1456,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())
@ -1502,7 +1525,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()
@ -1701,6 +1732,23 @@ class ElectrumWindow(QMainWindow, MessageBoxMixin, PrintError):
else:
self.payment_request_error_signal.emit()
def parse_lightning_invoice(self, invoice):
from electrum.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
@ -1737,6 +1785,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('')
@ -1761,8 +1810,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,36 @@ from electrum.plugin 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 electrum.bitcoin import COIN
from .util import MyTreeWidget, pr_tooltips, pr_icons
class RequestList(MyTreeWidget):
filter_columns = [0, 1, 2, 3, 4] # Date, Account, Address, Description, Amount
REQUEST_TYPE_BITCOIN = 0
REQUEST_TYPE_LN = 1
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,25 +66,25 @@ 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
# hide receive tab if no receive requests available
b = len(self.wallet.receive_requests) > 0
b = len(self.wallet.receive_requests) > 0 or len(self.wallet.lnworker.invoices) > 0
self.setVisible(b)
self.parent.receive_requests_label.setVisible(b)
if not b:
@ -77,12 +92,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,20 +114,51 @@ 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)))
item.setData(0, Qt.UserRole, REQUEST_TYPE_BITCOIN)
item.setData(0, Qt.UserRole+1, address)
self.addTopLevelItem(item)
# lightning
for payreq_key, r in self.wallet.lnworker.invoices.items():
from electrum.lnaddr import lndecode
import electrum.constants as constants
lnaddr = lndecode(r, expected_hrp=constants.net.SEGWIT_HRP)
amount_sat = lnaddr.amount*COIN if lnaddr.amount else None
amount_str = self.parent.format_amount(amount_sat) if amount_sat else ''
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, self.icon_cache.get(":icons/lightning.png"))
item.setData(0, Qt.UserRole, REQUEST_TYPE_LN)
item.setData(0, Qt.UserRole+1, payreq_key)
self.addTopLevelItem(item)
def create_menu(self, position):
item = self.itemAt(position)
if not item:
return
addr = str(item.text(1))
request_type = item.data(0, Qt.UserRole)
menu = None
if request_type == REQUEST_TYPE_BITCOIN:
menu = self.create_menu_bitcoin_payreq(item)
elif request_type == REQUEST_TYPE_LN:
menu = self.create_menu_ln_payreq(item)
if menu:
menu.exec_(self.viewport().mapToGlobal(position))
def create_menu_bitcoin_payreq(self, item):
addr = str(item.data(0, Qt.UserRole + 1))
req = self.wallet.receive_requests.get(addr)
if req is None:
self.update()
@ -126,4 +172,19 @@ class RequestList(MyTreeWidget):
menu.addAction(_("Save as BIP70 file"), lambda: self.parent.export_payment_request(addr))
menu.addAction(_("Delete"), lambda: self.parent.delete_payment_request(addr))
run_hook('receive_list_menu', menu, addr)
menu.exec_(self.viewport().mapToGlobal(position))
return menu
def create_menu_ln_payreq(self, item):
payreq_key = item.data(0, Qt.UserRole + 1)
req = self.wallet.lnworker.invoices.get(payreq_key)
if req is None:
self.update()
return
column = self.currentColumn()
column_title = self.headerItem().text(column)
column_data = item.text(column)
menu = QMenu(self)
menu.addAction(_("Copy {}").format(column_title), lambda: self.parent.app.clipboard().setText(column_data))
menu.addAction(_("Copy URI"), lambda: self.parent.view_and_paste('URI', '', req))
menu.addAction(_("Delete"), lambda: self.parent.delete_lightning_payreq(payreq_key))
return menu

View File

@ -333,7 +333,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
electrum/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"
}
}
}
}

391
electrum/lnaddr.py Executable file
View File

@ -0,0 +1,391 @@
#! /usr/bin/env python3
# This was forked from https://github.com/rustyrussell/lightning-payencode/tree/acc16ec13a3fa1dc16c07af6ec67c261bd8aff23
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
from decimal import Decimal
from . import constants
from . import ecc
import bitstring
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)

1006
electrum/lnbase.py Normal file

File diff suppressed because it is too large Load Diff

595
electrum/lnhtlc.py Normal file
View File

@ -0,0 +1,595 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
from collections import namedtuple
import binascii
import json
from .util import bfh, PrintError
from .bitcoin import Hash
from .crypto import sha256
from . import ecc
from .lnutil import Outpoint, ChannelConfig, LocalState, RemoteState, Keypair, OnlyPubkeyKeypair, ChannelConstraints, RevocationStore
from .lnutil import get_per_commitment_secret_from_seed
from .lnutil import secret_to_pubkey, derive_privkey, derive_pubkey, derive_blinded_pubkey
from .lnutil import sign_and_get_sig_string
from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
from contextlib import contextmanager
SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
@contextmanager
def PendingFeerateApplied(machine):
old_local_state = machine.local_state
old_remote_state = machine.remote_state
new_local_feerate = machine.local_state.feerate
new_remote_feerate = machine.remote_state.feerate
if machine.constraints.is_initiator:
if machine.pending_fee_update is not None:
new_remote_feerate = machine.pending_fee_update
if machine.pending_ack_fee_update is not None:
new_local_feerate = machine.pending_ack_fee_update
else:
if machine.pending_fee_update is not None:
new_local_feerate = machine.pending_fee_update
if machine.pending_ack_fee_update is not None:
new_remote_feerate = machine.pending_ack_fee_update
machine.local_state = machine.local_state._replace(feerate=new_local_feerate)
machine.remote_state = machine.remote_state._replace(feerate=new_remote_feerate)
yield
machine.local_state = old_local_state._replace(feerate=old_local_state.feerate)
machine.remote_state = old_remote_state._replace(feerate=old_remote_state.feerate)
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())
is_key = lambda k: k.endswith("_basepoint") or k.endswith("_key")
def maybeDecode(k, v):
assert type(v) is not list
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
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.local_config = state["local_config"]
if type(self.local_config) is not ChannelConfig:
new_local_config = {k: typeWrap(k, decodeAll(v), True) for k, v in self.local_config.items()}
self.local_config = ChannelConfig(**new_local_config)
self.remote_config = state["remote_config"]
if type(self.remote_config) is not ChannelConfig:
new_remote_config = {k: typeWrap(k, decodeAll(v), False) for k, v in self.remote_config.items()}
self.remote_config = ChannelConfig(**new_remote_config)
self.local_state = state["local_state"]
if type(self.local_state) is not LocalState:
self.local_state = LocalState(**decodeAll(self.local_state))
self.remote_state = state["remote_state"]
if type(self.remote_state) is not RemoteState:
self.remote_state = RemoteState(**decodeAll(self.remote_state))
if type(self.remote_state.revocation_store) is not RevocationStore:
self.remote_state = self.remote_state._replace(revocation_store = RevocationStore.from_json_obj(self.remote_state.revocation_store))
self.channel_id = maybeDecode("channel_id", state["channel_id"]) if type(state["channel_id"]) is not bytes else state["channel_id"]
self.constraints = ChannelConstraints(**decodeAll(state["constraints"])) if type(state["constraints"]) is not ChannelConstraints else state["constraints"]
self.funding_outpoint = Outpoint(**decodeAll(state["funding_outpoint"])) if type(state["funding_outpoint"]) is not Outpoint else state["funding_outpoint"]
self.node_id = maybeDecode("node_id", state["node_id"]) if type(state["node_id"]) is not bytes else state["node_id"]
self.short_channel_id = maybeDecode("short_channel_id", state["short_channel_id"]) if type(state["short_channel_id"]) is not bytes else state["short_channel_id"]
self.local_update_log = []
self.remote_update_log = []
self.name = name
self.total_msat_sent = 0
self.total_msat_received = 0
self.pending_fee_update = None
self.pending_ack_fee_update = None
self.local_commitment = self.pending_local_commitment
self.remote_commitment = self.pending_remote_commitment
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.local_state.next_htlc_id
self.local_state=self.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.
"""
self.print_error("receive_htlc")
assert type(htlc) is UpdateAddHtlc
self.remote_update_log.append(htlc)
htlc_id = self.remote_state.next_htlc_id
self.remote_state=self.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.
"""
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.local_state.ctn
self.print_error("sign_next_commitment")
if self.constraints.is_initiator and self.pending_fee_update:
self.pending_ack_fee_update = self.pending_fee_update
self.pending_fee_update = None
sig_64 = sign_and_get_sig_string(self.pending_remote_commitment, self.local_config, self.remote_config)
their_remote_htlc_privkey_number = derive_privkey(
int.from_bytes(self.local_config.htlc_basepoint.privkey, 'big'),
self.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:
weight = HTLC_SUCCESS_WEIGHT if we_receive else HTLC_TIMEOUT_WEIGHT
fee = self.remote_state.feerate // 1000 * weight
if htlc.amount_msat // 1000 < self.remote_config.dust_limit_sat + fee:
print("value too small, skipping. htlc amt: {}, weight: {}, remote feerate {}, remote dust limit {}".format( htlc.amount_msat, weight, self.remote_state.feerate, self.remote_config.dust_limit_sat))
continue
original_htlc_output_index = 0
args = [self.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat + htlc.total_fee, htlc.cltv_expiry, htlc.payment_hash, self.pending_remote_commitment, original_htlc_output_index]
htlc_tx = make_htlc_tx_with_open_channel(self, *args)
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
htlc_sig = ecc.sig_string_from_der_sig(sig[:-1])
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.
"""
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.remote_state.ctn
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
if not self.constraints.is_initiator:
self.pending_ack_fee_update = self.pending_fee_update
self.pending_fee_update = None
preimage_hex = self.pending_local_commitment.serialize_preimage(0)
pre_hash = Hash(bfh(preimage_hex))
if not ecc.verify_signature(self.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 and len(self.pending_local_commitment.outputs()) == 3:
print("CHECKING HTLC SIGS")
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, this_point, True, we_receive, amount_msat, cltv_expiry, payment_hash, self.pending_local_commitment, 0)
pre_hash = Hash(bfh(htlc_tx.serialize_preimage(0)))
remote_htlc_pubkey = derive_pubkey(self.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
new_feerate = self.local_state.feerate
if not self.constraints.is_initiator and self.pending_fee_update is not None:
new_feerate = self.pending_fee_update
self.pending_fee_update = None
self.pending_ack_fee_update = None
elif self.pending_ack_fee_update is not None:
new_feerate = self.pending_ack_fee_update
self.pending_fee_update = None
self.pending_ack_fee_update = None
self.remote_state=self.remote_state._replace(
feerate=new_feerate
)
self.local_state=self.local_state._replace(
ctn=self.local_state.ctn + 1,
feerate=new_feerate
)
self.local_commitment = self.pending_local_commitment
return RevokeAndAck(last_secret, next_point), "current htlcs"
@property
def points(self):
last_small_num = self.local_state.ctn
this_small_num = last_small_num + 1
next_small_num = last_small_num + 2
last_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - last_small_num)
this_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - this_small_num)
this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big'))
next_secret = get_per_commitment_secret_from_seed(self.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - next_small_num)
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, sent_fees = 0, 0
for x in settle_fails2:
htlc = self.lookup_htlc(self.local_update_log, x.htlc_id)
sent_this_batch += htlc.amount_msat
sent_fees += htlc.total_fee
self.total_msat_sent += sent_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]
# 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]
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]
received_this_batch = sum(x.amount_msat for x in to_remove)
self.total_msat_received += received_this_batch
received_fees = sum(x.total_fee for x in to_remove)
self.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
next_point = self.remote_state.next_per_commitment_point
print("RECEIVED", received_this_batch)
print("SENT", sent_this_batch)
self.remote_state=self.remote_state._replace(
ctn=self.remote_state.ctn + 1,
current_per_commitment_point=next_point,
next_per_commitment_point=revocation.next_per_commitment_point,
amount_msat=self.remote_state.amount_msat + (sent_this_batch - received_this_batch) + sent_fees - received_fees,
feerate=self.pending_fee_update if self.pending_fee_update is not None else self.remote_state.feerate
)
self.local_state=self.local_state._replace(
amount_msat = self.local_state.amount_msat + (received_this_batch - sent_this_batch) - sent_fees + received_fees
)
self.local_commitment = self.pending_local_commitment
self.remote_commitment = self.pending_remote_commitment
@staticmethod
def htlcsum(htlcs):
amount_unsettled = 0
fee = 0
for x in htlcs:
amount_unsettled += x.amount_msat
fee += x.total_fee
return amount_unsettled, fee
def amounts(self):
remote_settled_value, remote_settled_fee = self.htlcsum(self.gen_htlc_indices("remote", False))
local_settled_value, local_settled_fee = self.htlcsum(self.gen_htlc_indices("local", False))
htlc_value_local, total_fee_local = self.htlcsum(self.htlcs_in_local)
htlc_value_remote, total_fee_remote = self.htlcsum(self.htlcs_in_remote)
total_fee_local += local_settled_fee
total_fee_remote += remote_settled_fee
local_msat = self.local_state.amount_msat -\
htlc_value_local + remote_settled_value - local_settled_value
remote_msat = self.remote_state.amount_msat -\
htlc_value_remote + local_settled_value - remote_settled_value
return remote_msat, total_fee_remote, local_msat, total_fee_local
@property
def pending_remote_commitment(self):
remote_msat, total_fee_remote, local_msat, total_fee_local = self.amounts()
assert local_msat >= 0
assert remote_msat >= 0
this_point = self.remote_state.next_per_commitment_point
remote_htlc_pubkey = derive_pubkey(self.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.local_config.htlc_basepoint.pubkey, this_point)
local_revocation_pubkey = derive_blinded_pubkey(self.local_config.revocation_basepoint.pubkey, this_point)
with PendingFeerateApplied(self):
htlcs_in_local = []
for htlc in self.htlcs_in_local:
if htlc.amount_msat // 1000 - HTLC_SUCCESS_WEIGHT * (self.remote_state.feerate // 1000) < self.remote_config.dust_limit_sat:
continue
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 + htlc.total_fee))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
if htlc.amount_msat // 1000 - HTLC_TIMEOUT_WEIGHT * (self.remote_state.feerate // 1000) < self.remote_config.dust_limit_sat:
continue
htlcs_in_remote.append(
( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + htlc.total_fee))
commit = self.make_commitment(self.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 pending_local_commitment(self):
remote_msat, total_fee_remote, local_msat, total_fee_local = self.amounts()
assert local_msat >= 0
assert remote_msat >= 0
_, this_point, _ = self.points
remote_htlc_pubkey = derive_pubkey(self.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.local_config.htlc_basepoint.pubkey, this_point)
remote_revocation_pubkey = derive_blinded_pubkey(self.remote_config.revocation_basepoint.pubkey, this_point)
with PendingFeerateApplied(self):
htlcs_in_local = []
for htlc in self.htlcs_in_local:
if htlc.amount_msat // 1000 - HTLC_TIMEOUT_WEIGHT * (self.local_state.feerate // 1000) < self.local_config.dust_limit_sat:
continue
htlcs_in_local.append(
( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat + htlc.total_fee))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
if htlc.amount_msat // 1000 - HTLC_SUCCESS_WEIGHT * (self.local_state.feerate // 1000) < self.local_config.dust_limit_sat:
continue
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 + htlc.total_fee))
commit = self.make_commitment(self.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, just_unsettled=True):
assert subject in ["local", "remote"]
update_log = (self.remote_update_log if subject == "remote" else self.local_update_log)
other_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.local_state.ctn if subject == "remote" else self.remote_state.ctn)
locked_in = (htlc.r_locked_in if subject == "remote" else htlc.l_locked_in)
if locked_in is None or just_unsettled == (SettleHtlc(htlc.htlc_id) in other_log):
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.
"""
self.print_error("settle_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):
self.print_error("receive_htlc_settle")
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 = []
self.print_error("fail_htlc (EMPTIED LOGS)")
@property
def l_current_height(self):
return self.local_state.ctn
@property
def r_current_height(self):
return self.remote_state.ctn
@property
def local_commit_fee(self):
return self.constraints.capacity - sum(x[2] for x in self.local_commitment.outputs())
def update_fee(self, fee):
if not self.constraints.is_initiator:
raise Exception("only initiator can update_fee, this counterparty is not initiator")
self.pending_fee_update = fee
def receive_update_fee(self, fee):
if self.constraints.is_initiator:
raise Exception("only the non-initiator can receive_update_fee, this counterparty is initiator")
self.pending_fee_update = fee
def to_save(self):
return {
"local_config": self.local_config,
"remote_config": self.remote_config,
"local_state": self.local_state,
"remote_state": self.remote_state,
"channel_id": self.channel_id,
"short_channel_id": self.short_channel_id,
"constraints": self.constraints,
"funding_outpoint": self.funding_outpoint,
"node_id": self.node_id,
"channel_id": self.channel_id
}
def serialize(self):
namedtuples_to_dict = lambda v: {i: j._asdict() if isinstance(j, tuple) else j for i, j in v._asdict().items()}
serialized_channel = {k: namedtuples_to_dict(v) if isinstance(v, tuple) else v for k, v in self.to_save().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_channel)
roundtripped = json.loads(dumped)
reconstructed = HTLCStateMachine(roundtripped)
if reconstructed.to_save() != self.to_save():
raise Exception("Channels did not roundtrip serialization without changes:\n" + repr(reconstructed.to_save()) + "\n" + repr(self.to_save()))
return roundtripped
def __str__(self):
return self.serialize()
def make_commitment(chan, ctn, for_us, pcp, local_msat, remote_msat, htlcs=[]):
conf = chan.local_config if for_us else chan.remote_config
other_conf = chan.local_config if not for_us else chan.remote_config
payment_pubkey = derive_pubkey(other_conf.payment_basepoint.pubkey, pcp)
remote_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
return make_commitment(
ctn,
conf.multisig_key.pubkey,
other_conf.multisig_key.pubkey,
payment_pubkey,
chan.local_config.payment_basepoint.pubkey,
chan.remote_config.payment_basepoint.pubkey,
remote_revocation_pubkey,
derive_pubkey(conf.delayed_basepoint.pubkey, pcp),
other_conf.to_self_delay,
*chan.funding_outpoint,
chan.constraints.capacity,
local_msat,
remote_msat,
chan.local_config.dust_limit_sat,
chan.local_state.feerate if for_us else chan.remote_state.feerate,
for_us,
chan.constraints.is_initiator,
htlcs=htlcs)

528
electrum/lnrouter.py Normal file
View File

@ -0,0 +1,528 @@
# -*- 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, profiler, xor_bytes
from .lnutil import get_ecdh
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 __len__(self):
return len(self._id_to_channel_info)
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] = 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 = 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"

374
electrum/lnutil.py Normal file
View File

@ -0,0 +1,374 @@
from .util import bfh, bh2u
from .crypto import sha256
import json
from collections import namedtuple
from .transaction import Transaction
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
from . import ecc, bitcoin, crypto, transaction
from .transaction import opcodes
from .bitcoin import push_script
HTLC_TIMEOUT_WEIGHT = 663
HTLC_SUCCESS_WEIGHT = 703
Keypair = namedtuple("Keypair", ["pubkey", "privkey"])
Outpoint = namedtuple("Outpoint", ["txid", "output_index"])
ChannelConfig = namedtuple("ChannelConfig", [
"payment_basepoint", "multisig_key", "htlc_basepoint", "delayed_basepoint", "revocation_basepoint",
"to_self_delay", "dust_limit_sat", "max_htlc_value_in_flight_msat", "max_accepted_htlcs"])
OnlyPubkeyKeypair = namedtuple("OnlyPubkeyKeypair", ["pubkey"])
RemoteState = namedtuple("RemoteState", ["ctn", "next_per_commitment_point", "amount_msat", "revocation_store", "current_per_commitment_point", "next_htlc_id", "feerate"])
LocalState = namedtuple("LocalState", ["ctn", "per_commitment_secret_seed", "amount_msat", "next_htlc_id", "funding_locked_received", "was_announced", "current_commitment_signature", "feerate"])
ChannelConstraints = namedtuple("ChannelConstraints", ["capacity", "is_initiator", "funding_txn_minimum_depth"])
#OpenChannel = namedtuple("OpenChannel", ["channel_id", "short_channel_id", "funding_outpoint", "local_config", "remote_config", "remote_state", "local_state", "constraints", "node_id"])
class UnableToDeriveSecret(Exception): pass
class RevocationStore:
""" taken from lnd """
START_INDEX = 2 ** 48 - 1
def __init__(self):
self.buckets = [None] * 49
self.index = self.START_INDEX
def add_next_entry(self, hsh):
new_element = ShachainElement(index=self.index, secret=hsh)
bucket = count_trailing_zeros(self.index)
for i in range(0, bucket):
this_bucket = self.buckets[i]
e = shachain_derive(new_element, this_bucket.index)
if e != this_bucket:
raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index))
self.buckets[bucket] = new_element
self.index -= 1
def retrieve_secret(self, index: int) -> bytes:
for bucket in self.buckets:
if bucket is None:
raise UnableToDeriveSecret()
try:
element = shachain_derive(bucket, index)
except UnableToDeriveSecret:
continue
return element.secret
raise UnableToDeriveSecret()
def serialize(self):
return {"index": self.index, "buckets": [[bh2u(k.secret), k.index] if k is not None else None for k in self.buckets]}
@staticmethod
def from_json_obj(decoded_json_obj):
store = RevocationStore()
decode = lambda to_decode: ShachainElement(bfh(to_decode[0]), int(to_decode[1]))
store.buckets = [k if k is None else decode(k) for k in decoded_json_obj["buckets"]]
store.index = decoded_json_obj["index"]
return store
def __eq__(self, o):
return type(o) is RevocationStore and self.serialize() == o.serialize()
def __hash__(self):
return hash(json.dumps(self.serialize(), sort_keys=True))
def count_trailing_zeros(index):
""" BOLT-03 (where_to_put_secret) """
try:
return list(reversed(bin(index)[2:])).index("1")
except ValueError:
return 48
def shachain_derive(element, to_index):
def get_prefix(index, pos):
mask = (1 << 64) - 1 - ((1 << pos) - 1)
return index & mask
from_index = element.index
zeros = count_trailing_zeros(from_index)
if from_index != get_prefix(to_index, zeros):
raise UnableToDeriveSecret("prefixes are different; index not derivable")
return ShachainElement(
get_per_commitment_secret_from_seed(element.secret, to_index, zeros),
to_index)
ShachainElement = namedtuple("ShachainElement", ["secret", "index"])
ShachainElement.__str__ = lambda self: "ShachainElement(" + bh2u(self.secret) + "," + str(self.index) + ")"
def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> bytes:
"""Generate per commitment secret."""
per_commitment_secret = bytearray(seed)
for bitindex in range(bits - 1, -1, -1):
mask = 1 << bitindex
if i & mask:
per_commitment_secret[bitindex // 8] ^= 1 << (bitindex % 8)
per_commitment_secret = bytearray(sha256(per_commitment_secret))
bajts = bytes(per_commitment_secret)
return bajts
def secret_to_pubkey(secret: int) -> bytes:
assert type(secret) is int
return ecc.ECPrivkey.from_secret_scalar(secret).get_public_key_bytes(compressed=True)
def derive_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:
p = ecc.ECPubkey(basepoint) + ecc.generator() * ecc.string_to_number(sha256(per_commitment_point + basepoint))
return p.get_public_key_bytes()
def derive_privkey(secret: int, per_commitment_point: bytes) -> int:
assert type(secret) is int
basepoint = secret_to_pubkey(secret)
basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint))
basepoint %= CURVE_ORDER
return basepoint
def derive_blinded_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:
k1 = ecc.ECPubkey(basepoint) * ecc.string_to_number(sha256(basepoint + per_commitment_point))
k2 = ecc.ECPubkey(per_commitment_point) * ecc.string_to_number(sha256(per_commitment_point + basepoint))
return (k1 + k2).get_public_key_bytes()
def derive_blinded_privkey(basepoint_secret: bytes, per_commitment_secret: bytes) -> bytes:
basepoint = ecc.ECPrivkey(basepoint_secret).get_public_key_bytes(compressed=True)
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
k1 = ecc.string_to_number(basepoint_secret) * ecc.string_to_number(sha256(basepoint + per_commitment_point))
k2 = ecc.string_to_number(per_commitment_secret) * ecc.string_to_number(sha256(per_commitment_point + basepoint))
sum = (k1 + k2) % ecc.CURVE_ORDER
return ecc.number_to_string(sum, CURVE_ORDER)
def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay):
assert type(amount_msat) is int
assert type(local_feerate) is int
assert type(revocationpubkey) is bytes
assert type(local_delayedpubkey) is bytes
script = bytes([opcodes.OP_IF]) \
+ bfh(push_script(bh2u(revocationpubkey))) \
+ bytes([opcodes.OP_ELSE]) \
+ bitcoin.add_number_to_script(to_self_delay) \
+ bytes([opcodes.OP_CSV, opcodes.OP_DROP]) \
+ bfh(push_script(bh2u(local_delayedpubkey))) \
+ bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG])
p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT
fee = local_feerate * weight
final_amount_sat = (amount_msat - fee) // 1000
assert final_amount_sat > 0, final_amount_sat
output = (bitcoin.TYPE_ADDRESS, p2wsh, final_amount_sat)
return output
def make_htlc_tx_witness(remotehtlcsig, localhtlcsig, payment_preimage, witness_script):
assert type(remotehtlcsig) is bytes
assert type(localhtlcsig) is bytes
assert type(payment_preimage) is bytes
assert type(witness_script) is bytes
return bfh(transaction.construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script]))
def make_htlc_tx_inputs(htlc_output_txid, htlc_output_index, revocationpubkey, local_delayedpubkey, amount_msat, witness_script):
assert type(htlc_output_txid) is str
assert type(htlc_output_index) is int
assert type(revocationpubkey) is bytes
assert type(local_delayedpubkey) is bytes
assert type(amount_msat) is int
assert type(witness_script) is str
c_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
'signatures': [],
'num_sig': 0,
'prevout_n': htlc_output_index,
'prevout_hash': htlc_output_txid,
'value': amount_msat // 1000,
'coinbase': False,
'sequence': 0x0,
'preimage_script': witness_script,
}]
return c_inputs
def make_htlc_tx(cltv_timeout, inputs, output):
assert type(cltv_timeout) is int
c_outputs = [output]
tx = Transaction.from_io(inputs, c_outputs, locktime=cltv_timeout, version=2)
tx.BIP_LI01_sort()
return tx
def make_offered_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash):
assert type(revocation_pubkey) is bytes
assert type(remote_htlcpubkey) is bytes
assert type(local_htlcpubkey) is bytes
assert type(payment_hash) is bytes
return bytes([opcodes.OP_DUP, opcodes.OP_HASH160]) + bfh(push_script(bh2u(bitcoin.hash_160(revocation_pubkey))))\
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \
+ bfh(push_script(bh2u(remote_htlcpubkey)))\
+ bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) + bitcoin.add_number_to_script(32) + bytes([opcodes.OP_EQUAL, opcodes.OP_NOTIF, opcodes.OP_DROP])\
+ bitcoin.add_number_to_script(2) + bytes([opcodes.OP_SWAP]) + bfh(push_script(bh2u(local_htlcpubkey))) + bitcoin.add_number_to_script(2)\
+ bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_HASH160])\
+ bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) + bytes([opcodes.OP_EQUALVERIFY, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF])
def make_received_htlc(revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash, cltv_expiry):
for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]:
assert type(i) is bytes
assert type(cltv_expiry) is int
return bytes([opcodes.OP_DUP, opcodes.OP_HASH160]) \
+ bfh(push_script(bh2u(bitcoin.hash_160(revocation_pubkey)))) \
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_CHECKSIG, opcodes.OP_ELSE]) \
+ bfh(push_script(bh2u(remote_htlcpubkey))) \
+ bytes([opcodes.OP_SWAP, opcodes.OP_SIZE]) \
+ bitcoin.add_number_to_script(32) \
+ bytes([opcodes.OP_EQUAL, opcodes.OP_IF, opcodes.OP_HASH160]) \
+ bfh(push_script(bh2u(crypto.ripemd(payment_hash)))) \
+ bytes([opcodes.OP_EQUALVERIFY]) \
+ bitcoin.add_number_to_script(2) \
+ bytes([opcodes.OP_SWAP]) \
+ bfh(push_script(bh2u(local_htlcpubkey))) \
+ bitcoin.add_number_to_script(2) \
+ bytes([opcodes.OP_CHECKMULTISIG, opcodes.OP_ELSE, opcodes.OP_DROP]) \
+ bitcoin.add_number_to_script(cltv_expiry) \
+ bytes([opcodes.OP_CLTV, opcodes.OP_DROP, opcodes.OP_CHECKSIG, opcodes.OP_ENDIF, opcodes.OP_ENDIF])
def make_htlc_tx_with_open_channel(chan, pcp, for_us, we_receive, amount_msat, cltv_expiry, payment_hash, commit, original_htlc_output_index):
conf = chan.local_config if for_us else chan.remote_config
other_conf = chan.local_config if not for_us else chan.remote_config
revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp)
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)
htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp)
# HTLC-success for the HTLC spending from a received HTLC output
# if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success
is_htlc_success = for_us == we_receive
htlc_tx_output = make_htlc_tx_output(
amount_msat = amount_msat,
local_feerate = chan.local_state.feerate if for_us else chan.remote_state.feerate,
revocationpubkey=revocation_pubkey,
local_delayedpubkey=delayedpubkey,
success = is_htlc_success,
to_self_delay = other_conf.to_self_delay)
if is_htlc_success:
preimage_script = make_received_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash, cltv_expiry)
else:
preimage_script = make_offered_htlc(other_revocation_pubkey, other_htlc_pubkey, htlc_pubkey, payment_hash)
htlc_tx_inputs = make_htlc_tx_inputs(
commit.txid(), commit.htlc_output_indices[original_htlc_output_index],
revocationpubkey=revocation_pubkey,
local_delayedpubkey=delayedpubkey,
amount_msat=amount_msat,
witness_script=bh2u(preimage_script))
if is_htlc_success:
cltv_expiry = 0
htlc_tx = make_htlc_tx(cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
return htlc_tx
def make_commitment(ctn, local_funding_pubkey, remote_funding_pubkey,
remote_payment_pubkey, payment_basepoint,
remote_payment_basepoint, revocation_pubkey,
delayed_pubkey, to_self_delay, funding_txid,
funding_pos, funding_sat, local_amount, remote_amount,
dust_limit_sat, local_feerate, for_us, we_are_initiator,
htlcs):
pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
payments = [payment_basepoint, remote_payment_basepoint]
if not we_are_initiator:
payments.reverse()
obs = get_obscured_ctn(ctn, *payments)
locktime = (0x20 << 24) + (obs & 0xffffff)
sequence = (0x80 << 24) + (obs >> 24)
# commitment tx input
c_inputs = [{
'type': 'p2wsh',
'x_pubkeys': pubkeys,
'signatures': [None, None],
'num_sig': 2,
'prevout_n': funding_pos,
'prevout_hash': funding_txid,
'value': funding_sat,
'coinbase': False,
'sequence': sequence
}]
# commitment tx outputs
local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey)
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey)
# TODO trim htlc outputs here while also considering 2nd stage htlc transactions
fee = local_feerate * overall_weight(len(htlcs))
assert type(fee) is int
we_pay_fee = for_us == we_are_initiator
to_local_amt = local_amount - (fee if we_pay_fee else 0)
assert type(to_local_amt) is int
to_local = (bitcoin.TYPE_ADDRESS, local_address, to_local_amt // 1000)
to_remote_amt = remote_amount - (fee if not we_pay_fee else 0)
assert type(to_remote_amt) is int
to_remote = (bitcoin.TYPE_ADDRESS, remote_address, to_remote_amt // 1000)
c_outputs = [to_local, to_remote]
for script, msat_amount in htlcs:
c_outputs += [(bitcoin.TYPE_ADDRESS, bitcoin.redeem_script_to_address('p2wsh', bh2u(script)), msat_amount // 1000)]
# trim outputs
c_outputs_filtered = list(filter(lambda x:x[2]>= dust_limit_sat, c_outputs))
assert sum(x[2] for x in c_outputs) <= funding_sat
# create commitment tx
tx = Transaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
tx.BIP_LI01_sort()
tx.htlc_output_indices = {}
for idx, output in enumerate(c_outputs):
if output in tx.outputs():
# minus the first two outputs (to_local, to_remote)
tx.htlc_output_indices[idx - 2] = tx.outputs().index(output)
return tx
def make_commitment_output_to_local_witness_script(
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> bytes:
local_script = bytes([opcodes.OP_IF]) + bfh(push_script(bh2u(revocation_pubkey))) + bytes([opcodes.OP_ELSE]) + bitcoin.add_number_to_script(to_self_delay) \
+ bytes([opcodes.OP_CSV, opcodes.OP_DROP]) + bfh(push_script(bh2u(delayed_pubkey))) + bytes([opcodes.OP_ENDIF, opcodes.OP_CHECKSIG])
return local_script
def make_commitment_output_to_local_address(
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str:
local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey)
return bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script))
def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
def sign_and_get_sig_string(tx, local_config, remote_config):
pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)})
sig_index = pubkeys.index(bh2u(local_config.multisig_key.pubkey))
sig = bytes.fromhex(tx.inputs()[0]["signatures"][sig_index])
sig_64 = sig_string_from_der_sig(sig[:-1])
return sig_64
def funding_output_script(local_config, remote_config):
pubkeys = sorted([bh2u(local_config.multisig_key.pubkey), bh2u(remote_config.multisig_key.pubkey)])
return transaction.multisig_script(pubkeys, 2)
def calc_short_channel_id(block_height: int, tx_pos_in_block: int, output_index: int) -> bytes:
bh = block_height.to_bytes(3, byteorder='big')
tpos = tx_pos_in_block.to_bytes(3, byteorder='big')
oi = output_index.to_bytes(2, byteorder='big')
return bh + tpos + oi
def get_obscured_ctn(ctn, local, remote):
mask = int.from_bytes(sha256(local + remote)[-6:], 'big')
return ctn ^ mask
def extract_ctn_from_tx(tx, txin_index, local_payment_basepoint, remote_payment_basepoint):
tx.deserialize()
locktime = tx.locktime
sequence = tx.inputs()[txin_index]['sequence']
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
return get_obscured_ctn(obs, local_payment_basepoint, remote_payment_basepoint)
def overall_weight(num_htlc):
return 500 + 172 * num_htlc + 224
def get_ecdh(priv: bytes, pub: bytes) -> bytes:
pt = ECPubkey(pub) * string_to_number(priv)
return sha256(pt.get_public_key_bytes())

277
electrum/lnwatcher.py Normal file
View File

@ -0,0 +1,277 @@
from .util import PrintError, bh2u, bfh, NoDynamicFeeEstimates
from .lnutil import (funding_output_script, extract_ctn_from_tx, derive_privkey,
get_per_commitment_secret_from_seed, derive_pubkey,
make_commitment_output_to_remote_address,
RevocationStore, UnableToDeriveSecret)
from . import lnutil
from .bitcoin import redeem_script_to_address, TYPE_ADDRESS
from . import transaction
from .transaction import Transaction
from . import ecc
class LNWatcher(PrintError):
def __init__(self, network):
self.network = network
self.watched_channels = {}
self.address_status = {} # addr -> status
def parse_response(self, response):
if response.get('error'):
self.print_error("response error:", response)
return None, None
return response['params'], response['result']
def watch_channel(self, chan, callback):
funding_address = funding_address_for_channel(chan)
self.watched_channels[funding_address] = chan, callback
self.network.subscribe_to_addresses([funding_address], self.on_address_status)
def on_address_status(self, response):
params, result = self.parse_response(response)
if not params:
return
addr = params[0]
if self.address_status.get(addr) != result:
self.address_status[addr] = result
self.network.request_address_utxos(addr, self.on_utxos)
def on_utxos(self, response):
params, result = self.parse_response(response)
if not params:
return
addr = params[0]
chan, callback = self.watched_channels[addr]
callback(chan, result)
def funding_address_for_channel(chan):
script = funding_output_script(chan.local_config, chan.remote_config)
return redeem_script_to_address('p2wsh', script)
class LNChanCloseHandler(PrintError):
def __init__(self, network, wallet, chan):
self.network = network
self.wallet = wallet
self.chan = chan
self.funding_address = funding_address_for_channel(chan)
network.request_address_history(self.funding_address, self.on_history)
# TODO: de-duplicate?
def parse_response(self, response):
if response.get('error'):
self.print_error("response error:", response)
return None, None
return response['params'], response['result']
def on_history(self, response):
params, result = self.parse_response(response)
if not params:
return
addr = params[0]
if self.funding_address != addr:
self.print_error("unexpected funding address: {} != {}"
.format(self.funding_address, addr))
return
txids = set(map(lambda item: item['tx_hash'], result))
self.network.get_transactions(txids, self.on_tx_response)
def on_tx_response(self, response):
params, result = self.parse_response(response)
if not params:
return
tx_hash = params[0]
tx = Transaction(result)
try:
tx.deserialize()
except Exception:
self.print_msg("cannot deserialize transaction", tx_hash)
return
if tx_hash != tx.txid():
self.print_error("received tx does not match expected txid ({} != {})"
.format(tx_hash, tx.txid()))
return
funding_outpoint = self.chan.funding_outpoint
for i, txin in enumerate(tx.inputs()):
if txin['prevout_hash'] == funding_outpoint.txid \
and txin['prevout_n'] == funding_outpoint.output_index:
self.print_error("funding outpoint {} is spent by {}"
.format(funding_outpoint, tx_hash))
self.inspect_spending_tx(tx, i)
break
# TODO batch sweeps
# TODO sweep HTLC outputs
# TODO implement nursery that waits for timelocks
def inspect_spending_tx(self, ctx, txin_idx: int):
chan = self.chan
ctn = extract_ctn_from_tx(ctx, txin_idx,
chan.local_config.payment_basepoint.pubkey,
chan.remote_config.payment_basepoint.pubkey)
latest_local_ctn = chan.local_state.ctn
latest_remote_ctn = chan.remote_state.ctn
self.print_error("ctx {} has ctn {}. latest local ctn is {}, latest remote ctn is {}"
.format(ctx.txid(), ctn, latest_local_ctn, latest_remote_ctn))
# see if it is a normal unilateral close by them
if ctn == latest_remote_ctn:
# note that we might also get here if this is our ctx and the ctn just happens to match
their_cur_pcp = chan.remote_state.current_per_commitment_point
if their_cur_pcp is not None:
self.find_and_sweep_their_ctx_to_remote(ctx, their_cur_pcp)
# see if we have a revoked secret for this ctn ("breach")
try:
per_commitment_secret = chan.remote_state.revocation_store.retrieve_secret(
RevocationStore.START_INDEX - ctn)
except UnableToDeriveSecret:
self.print_error("revocation store does not have secret for ctx {}".format(ctx.txid()))
else:
# note that we might also get here if this is our ctx and we just happen to have
# the secret for the symmetric ctn
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
self.find_and_sweep_their_ctx_to_remote(ctx, their_pcp)
self.find_and_sweep_their_ctx_to_local(ctx, per_commitment_secret)
# see if it's our ctx
our_per_commitment_secret = get_per_commitment_secret_from_seed(
chan.local_state.per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
our_per_commitment_point = ecc.ECPrivkey(our_per_commitment_secret).get_public_key_bytes(compressed=True)
self.find_and_sweep_our_ctx_to_local(ctx, our_per_commitment_point)
def find_and_sweep_their_ctx_to_remote(self, ctx, their_pcp: bytes):
payment_bp_privkey = ecc.ECPrivkey(self.chan.local_config.payment_basepoint.privkey)
our_payment_privkey = derive_privkey(payment_bp_privkey.secret_scalar, their_pcp)
our_payment_privkey = ecc.ECPrivkey.from_secret_scalar(our_payment_privkey)
our_payment_pubkey = our_payment_privkey.get_public_key_bytes(compressed=True)
to_remote_address = make_commitment_output_to_remote_address(our_payment_pubkey)
for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
if type_ == TYPE_ADDRESS and addr == to_remote_address:
self.print_error("found to_remote output paying to us: ctx {}:{}".
format(ctx.txid(), output_idx))
#self.print_error("ctx {} is normal unilateral close by them".format(ctx.txid()))
break
else:
return
sweep_tx = self.create_sweeptx_their_ctx_to_remote(ctx, output_idx, our_payment_privkey)
self.network.broadcast_transaction(sweep_tx,
lambda res: self.print_tx_broadcast_result('sweep_their_ctx_to_remote', res))
def create_sweeptx_their_ctx_to_remote(self, ctx, output_idx: int, our_payment_privkey: ecc.ECPrivkey):
our_payment_pubkey = our_payment_privkey.get_public_key_hex(compressed=True)
val = ctx.outputs()[output_idx][2]
sweep_inputs = [{
'type': 'p2wpkh',
'x_pubkeys': [our_payment_pubkey],
'num_sig': 1,
'prevout_n': output_idx,
'prevout_hash': ctx.txid(),
'value': val,
'coinbase': False,
'signatures': [None],
}]
tx_size_bytes = 110 # approx size of p2wpkh->p2wpkh
try:
fee = self.network.config.estimate_fee(tx_size_bytes)
except NoDynamicFeeEstimates:
fee_per_kb = self.network.config.fee_per_kb(dyn=False)
fee = self.network.config.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
sweep_outputs = [(TYPE_ADDRESS, self.wallet.get_receiving_address(), val-fee)]
locktime = self.network.get_local_height()
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, locktime=locktime)
sweep_tx.set_rbf(True)
sweep_tx.sign({our_payment_pubkey: (our_payment_privkey.get_secret_bytes(), True)})
if not sweep_tx.is_complete():
raise Exception('channel close sweep tx is not complete')
return sweep_tx
def find_and_sweep_their_ctx_to_local(self, ctx, per_commitment_secret: bytes):
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
revocation_privkey = lnutil.derive_blinded_privkey(self.chan.local_config.revocation_basepoint.privkey,
per_commitment_secret)
revocation_pubkey = ecc.ECPrivkey(revocation_privkey).get_public_key_bytes(compressed=True)
to_self_delay = self.chan.local_config.to_self_delay
delayed_pubkey = derive_pubkey(self.chan.remote_config.delayed_basepoint.pubkey,
per_commitment_point)
witness_script = bh2u(lnutil.make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, delayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
if type_ == TYPE_ADDRESS and addr == to_local_address:
self.print_error("found to_local output paying to them: ctx {}:{}".
format(ctx.txid(), output_idx))
break
else:
self.print_error('could not find to_local output in their ctx {}'.format(ctx.txid()))
return
sweep_tx = self.create_sweeptx_ctx_to_local(ctx, output_idx, witness_script, revocation_privkey, True)
self.network.broadcast_transaction(sweep_tx,
lambda res: self.print_tx_broadcast_result('sweep_their_ctx_to_local', res))
def find_and_sweep_our_ctx_to_local(self, ctx, our_pcp: bytes):
delayed_bp_privkey = ecc.ECPrivkey(self.chan.local_config.delayed_basepoint.privkey)
our_localdelayed_privkey = derive_privkey(delayed_bp_privkey.secret_scalar, our_pcp)
our_localdelayed_privkey = ecc.ECPrivkey.from_secret_scalar(our_localdelayed_privkey)
our_localdelayed_pubkey = our_localdelayed_privkey.get_public_key_bytes(compressed=True)
revocation_pubkey = lnutil.derive_blinded_pubkey(self.chan.remote_config.revocation_basepoint.pubkey,
our_pcp)
to_self_delay = self.chan.remote_config.to_self_delay
witness_script = bh2u(lnutil.make_commitment_output_to_local_witness_script(
revocation_pubkey, to_self_delay, our_localdelayed_pubkey))
to_local_address = redeem_script_to_address('p2wsh', witness_script)
for output_idx, (type_, addr, val) in enumerate(ctx.outputs()):
if type_ == TYPE_ADDRESS and addr == to_local_address:
self.print_error("found to_local output paying to us (CSV-locked): ctx {}:{}".
format(ctx.txid(), output_idx))
break
else:
self.print_error('could not find to_local output in our ctx {}'.format(ctx.txid()))
return
# TODO if the CSV lock is still pending, this will fail
sweep_tx = self.create_sweeptx_ctx_to_local(ctx, output_idx, witness_script,
our_localdelayed_privkey.get_secret_bytes(),
False, to_self_delay)
self.network.broadcast_transaction(sweep_tx,
lambda res: self.print_tx_broadcast_result('sweep_our_ctx_to_local', res))
def create_sweeptx_ctx_to_local(self, ctx, output_idx: int, witness_script: str,
privkey: bytes, is_revocation: bool, to_self_delay: int=None):
"""Create a txn that sweeps the 'to_local' output of a commitment
transaction into our wallet.
privkey: either revocation_privkey or localdelayed_privkey
is_revocation: tells us which ^
"""
val = ctx.outputs()[output_idx][2]
sweep_inputs = [{
'scriptSig': '',
'type': 'p2wsh',
'signatures': [],
'num_sig': 0,
'prevout_n': output_idx,
'prevout_hash': ctx.txid(),
'value': val,
'coinbase': False,
'preimage_script': witness_script,
}]
if to_self_delay is not None:
sweep_inputs[0]['sequence'] = to_self_delay
tx_size_bytes = 121 # approx size of to_local -> p2wpkh
try:
fee = self.network.config.estimate_fee(tx_size_bytes)
except NoDynamicFeeEstimates:
fee_per_kb = self.network.config.fee_per_kb(dyn=False)
fee = self.network.config.estimate_fee_for_feerate(fee_per_kb, tx_size_bytes)
sweep_outputs = [(TYPE_ADDRESS, self.wallet.get_receiving_address(), val - fee)]
locktime = self.network.get_local_height()
sweep_tx = Transaction.from_io(sweep_inputs, sweep_outputs, locktime=locktime, version=2)
sig = sweep_tx.sign_txin(0, privkey)
witness = transaction.construct_witness([sig, int(is_revocation), witness_script])
sweep_tx.inputs()[0]['witness'] = witness
return sweep_tx
def print_tx_broadcast_result(self, name, res):
error = res.get('error')
if error:
self.print_error('{} broadcast failed: {}'.format(name, error))
else:
self.print_error('{} broadcast succeeded'.format(name))

194
electrum/lnworker.py Normal file
View File

@ -0,0 +1,194 @@
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
from .util import bh2u, bfh, PrintError
from .constants import set_testnet, set_simnet
from .lnbase import Peer, privkey_to_pubkey
from .lnaddr import lnencode, LnAddr, lndecode
from .ecc import der_sig_from_sig_string
from .transaction import Transaction
from .lnhtlc import HTLCStateMachine
from .lnutil import Outpoint, calc_short_channel_id
from .lnwatcher import LNChanCloseHandler
# 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 = privkey_to_pubkey(self.privkey)
self.config = network.config
self.peers = {}
self.channels = {x.channel_id: x for x in map(HTLCStateMachine, 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 chan_id, chan in self.channels.items():
self.network.lnwatcher.watch_channel(chan, self.on_channel_utxos)
for host, port, pubkey in peer_list:
self.add_peer(host, int(port), bfh(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, node_id):
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.network.trigger_callback('ln_status')
def save_channel(self, openchannel):
assert type(openchannel) is HTLCStateMachine
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 = [x.serialize() for x in self.channels.values()]
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 False
chan.short_channel_id = calc_short_channel_id(block_height, tx_pos, chan.funding_outpoint.output_index)
self.save_channel(chan)
return True
return False
def on_channel_utxos(self, chan, utxos):
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"
# FIXME is this properly GC-ed? (or too soon?)
LNChanCloseHandler(self.network, self.wallet, chan)
elif self.channel_state[chan.channel_id] == 'DISCONNECTED':
peer = self.peers[chan.node_id]
coro = peer.reestablish_channel(chan)
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
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":
res = self.save_short_chan_id(chan)
if not res:
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)
async def _open_channel_coroutine(self, node_id, amount_sat, push_sat, password):
if node_id not in self.peers:
node = self.network.lightning_nodes.get(node_id)
if node is None:
return "node not found, peers available are: " + str(self.network.lightning_nodes.keys())
host, port = node['addresses'][0]
self.add_peer(host, port, node_id)
peer = self.peers[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.save_channel(openingchannel)
self.network.lnwatcher.watch_channel(openingchannel, self.on_channel_utxos)
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.network.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):
payment_preimage = os.urandom(32)
RHASH = sha256(payment_preimage)
amount_btc = amount_sat/Decimal(COIN) if amount_sat else None
pay_req = lnencode(LnAddr(RHASH, amount_btc, tags=[('d', message)]), 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 delete_invoice(self, payreq_key):
try:
del self.invoices[payreq_key]
except KeyError:
return
self.wallet.storage.put('lightning_invoices', self.invoices)
self.wallet.storage.write()
def list_channels(self):
return [str(x) for x in self.channels]
def close_channel(self, chan_id):
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
old_local_state = chan.local_state
chan.local_state=chan.local_state._replace(ctn=chan.local_state.ctn - 1)
tx = chan.pending_local_commitment
chan.local_state = old_local_state
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)
tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig))
assert tx.is_complete()
return self.network.broadcast_transaction(tx)

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
@ -27,7 +28,7 @@ import errno
import random
import re
import select
from collections import defaultdict
from collections import defaultdict, deque
import threading
import socket
import json
@ -48,6 +49,10 @@ from . import blockchain
from .version import ELECTRUM_VERSION, PROTOCOL_VERSION
from .i18n import _
# lightning network
from . import lnwatcher
from . import lnrouter
NODES_RETRY_INTERVAL = 60
SERVER_RETRY_INTERVAL = 10
@ -156,7 +161,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.
@ -238,6 +242,13 @@ 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 = []
# lightning network
self.lightning_nodes = {}
self.channel_db = lnrouter.ChannelDB()
self.path_finder = lnrouter.LNPathFinder(self.channel_db)
self.lnwatcher = lnwatcher.LNWatcher(self)
def with_interface_lock(func):
def func_wrapper(self, *args, **kwargs):
@ -1058,13 +1069,28 @@ 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()
@ -1211,6 +1237,11 @@ class Network(util.DaemonThread):
self.h2addr.update({h: address})
self.send([('blockchain.scripthash.get_history', [h])], self.map_scripthash_to_address(callback))
def request_address_utxos(self, address, callback):
h = bitcoin.address_to_scripthash(address)
self.h2addr.update({h: address})
self.send([('blockchain.scripthash.listunspent', [h])], self.map_scripthash_to_address(callback))
# NOTE this method handles exceptions and a special edge case, counter to
# what the other ElectrumX methods do. This is unexpected.
def broadcast_transaction(self, transaction, callback=None):

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

@ -109,13 +109,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)
make_dir(path, allow_symlink=False)
self.print_error("electrum directory", path)
return path

View File

@ -0,0 +1,97 @@
from hashlib import sha256
from electrum.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 electrum.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)

View File

@ -0,0 +1,344 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
import unittest
import electrum.bitcoin as bitcoin
import electrum.lnbase as lnbase
import electrum.lnhtlc as lnhtlc
import electrum.lnutil as lnutil
import electrum.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, l_dust, r_dust, l_csv, r_csv):
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=l_csv,
dust_limit_sat=l_dust,
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=r_csv,
dust_limit_sat=r_dust,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
)
return {
"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,
current_per_commitment_point=cur,
amount_msat=remote_amount,
revocation_store=their_revocation_store,
next_htlc_id = 0,
feerate=local_feerate
),
"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,
current_commitment_signature=None,
feerate=local_feerate
),
"constraints":lnbase.ChannelConstraints(capacity=funding_sat, 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 * 10
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 = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 1), "big"))
alice_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 2), "big"))
bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 1), "big"))
bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 2), "big"))
return \
lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 6000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \
lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 6000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "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 setUp(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.
self.alice_channel, self.bob_channel = create_test_channels()
self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage)
self.htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = one_bitcoin_in_msat,
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.
self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc)
self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc)
def test_SimpleAddSettleWorkflow(self):
alice_channel, bob_channel = self.alice_channel, self.bob_channel
htlc = self.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.local_state.ctn, 1, "bob has incorrect commitment height")
self.assertEqual(alice_channel.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, htlc.amount_msat // 1000)
self.assertOutputExistsByValue(bob_channel.local_commitment, htlc.amount_msat // 1000)
# Now we'll repeat a similar exchange, this time with Bob settling the
# HTLC once he learns of the preimage.
preimage = self.paymentPreimage
bob_channel.settle_htlc(preimage, self.bobHtlcIndex)
alice_channel.receive_htlc_settle(preimage, self.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))
def alice_to_bob_fee_update(self):
fee = 111
self.alice_channel.update_fee(fee)
self.bob_channel.receive_update_fee(fee)
return fee
def test_UpdateFeeSenderCommits(self):
fee = self.alice_to_bob_fee_update()
alice_channel, bob_channel = self.alice_channel, self.bob_channel
alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
self.assertNotEqual(fee, bob_channel.local_state.feerate)
rev, _ = bob_channel.revoke_current_commitment()
self.assertEqual(fee, bob_channel.local_state.feerate)
bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
alice_channel.receive_revocation(rev)
alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
self.assertNotEqual(fee, alice_channel.local_state.feerate)
rev, _ = alice_channel.revoke_current_commitment()
self.assertEqual(fee, alice_channel.local_state.feerate)
bob_channel.receive_revocation(rev)
def test_UpdateFeeReceiverCommits(self):
fee = self.alice_to_bob_fee_update()
alice_channel, bob_channel = self.alice_channel, self.bob_channel
bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
alice_revocation, _ = alice_channel.revoke_current_commitment()
bob_channel.receive_revocation(alice_revocation)
alice_sig, alice_htlc_sigs = alice_channel.sign_next_commitment()
bob_channel.receive_new_commitment(alice_sig, alice_htlc_sigs)
self.assertNotEqual(fee, bob_channel.local_state.feerate)
bob_revocation, _ = bob_channel.revoke_current_commitment()
self.assertEqual(fee, bob_channel.local_state.feerate)
bob_sig, bob_htlc_sigs = bob_channel.sign_next_commitment()
alice_channel.receive_revocation(bob_revocation)
alice_channel.receive_new_commitment(bob_sig, bob_htlc_sigs)
self.assertNotEqual(fee, alice_channel.local_state.feerate)
alice_revocation, _ = alice_channel.revoke_current_commitment()
self.assertEqual(fee, alice_channel.local_state.feerate)
bob_channel.receive_revocation(alice_revocation)
class TestLNHTLCDust(unittest.TestCase):
def test_HTLCDustLimit(self):
alice_channel, bob_channel = create_test_channels()
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
fee_per_kw = alice_channel.local_state.feerate
self.assertEqual(fee_per_kw, 6000)
htlcAmt = 500 + lnutil.HTLC_TIMEOUT_WEIGHT * (fee_per_kw // 1000)
self.assertEqual(htlcAmt, 4478)
htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = 1000 * htlcAmt,
cltv_expiry = 5, # also in create_test_channels
total_fee = 0
)
aliceHtlcIndex = alice_channel.add_htlc(htlc)
bobHtlcIndex = bob_channel.receive_htlc(htlc)
force_state_transition(alice_channel, bob_channel)
self.assertEqual(len(alice_channel.local_commitment.outputs()), 3)
self.assertEqual(len(bob_channel.local_commitment.outputs()), 2)
default_fee = calc_static_fee(0)
self.assertEqual(bob_channel.local_commit_fee, default_fee + htlcAmt)
bob_channel.settle_htlc(paymentPreimage, htlc.htlc_id)
alice_channel.receive_htlc_settle(paymentPreimage, aliceHtlcIndex)
force_state_transition(bob_channel, alice_channel)
self.assertEqual(len(alice_channel.local_commitment.outputs()), 2)
self.assertEqual(alice_channel.total_msat_sent // 1000, htlcAmt)
def force_state_transition(chanA, chanB):
chanB.receive_new_commitment(*chanA.sign_next_commitment())
rev, _ = chanB.revoke_current_commitment()
bob_sig, bob_htlc_sigs = chanB.sign_next_commitment()
chanA.receive_revocation(rev)
chanA.receive_new_commitment(bob_sig, bob_htlc_sigs)
chanB.receive_revocation(chanA.revoke_current_commitment()[0])
# calcStaticFee calculates appropriate fees for commitment transactions. This
# function provides a simple way to allow test balance assertions to take fee
# calculations into account.
def calc_static_fee(numHTLCs):
commitWeight = 724
htlcWeight = 172
feePerKw = 24//4 * 1000
return feePerKw * (commitWeight + htlcWeight*numHTLCs) // 1000

View File

@ -0,0 +1,151 @@
import unittest
from electrum.util import bh2u, bfh
from electrum.lnbase import Peer
from electrum.lnrouter import OnionHopsDataSingle, new_onion_packet, OnionPerHop
from electrum import bitcoin, lnrouter
class Test_LNRouter(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_find_path_for_payment(self):
class fake_network:
channel_db = lnrouter.ChannelDB()
trigger_callback = lambda x: None
class fake_ln_worker:
path_finder = lnrouter.LNPathFinder(fake_network.channel_db)
privkey = bitcoin.sha256('privkeyseed')
network = fake_network
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_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)

View File

@ -0,0 +1,677 @@
import unittest
import json
from electrum import bitcoin
from electrum.lnutil import (RevocationStore, get_per_commitment_secret_from_seed, make_offered_htlc,
make_received_htlc, make_commitment, make_htlc_tx_witness, make_htlc_tx_output,
make_htlc_tx_inputs, secret_to_pubkey, derive_blinded_pubkey, derive_privkey,
derive_pubkey, make_htlc_tx, extract_ctn_from_tx, UnableToDeriveSecret)
from electrum.util import bh2u, bfh
from electrum.transaction import Transaction
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 TestLNUtil(unittest.TestCase):
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"]))
for insert in test["inserts"]:
secret = bytes.fromhex(insert["secret"])
index = insert["index"]
if insert["successful"]:
self.assertEqual(secret, receiver.retrieve_secret(index))
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())
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 = secret_to_pubkey(per_commitment_secret)
remote_htlcpubkey = remotepubkey
local_htlcpubkey = localpubkey
htlc_cltv_timeout = {}
htlc_payment_preimage = {}
htlc = {}
htlc_cltv_timeout[2] = 502
htlc_payment_preimage[2] = b"\x02" * 32
htlc[2] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[2]))
htlc_cltv_timeout[3] = 503
htlc_payment_preimage[3] = b"\x03" * 32
htlc[3] = make_offered_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[3]))
htlc_cltv_timeout[0] = 500
htlc_payment_preimage[0] = b"\x00" * 32
htlc[0] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[0]), htlc_cltv_timeout[0])
htlc_cltv_timeout[1] = 501
htlc_payment_preimage[1] = b"\x01" * 32
htlc[1] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[1]), htlc_cltv_timeout[1])
htlc_cltv_timeout[4] = 504
htlc_payment_preimage[4] = b"\x04" * 32
htlc[4] = make_received_htlc(local_revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, bitcoin.sha256(htlc_payment_preimage[4]), htlc_cltv_timeout[4])
remote_signature = "304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b70606"
output_commit_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
htlc_msat = {}
htlc_msat[0] = 1000 * 1000
htlc_msat[2] = 2000 * 1000
htlc_msat[1] = 2000 * 1000
htlc_msat[3] = 3000 * 1000
htlc_msat[4] = 4000 * 1000
htlcs = [(htlc[x], htlc_msat[x]) for x in range(5)]
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)
signature_for_output_remote_htlc = {}
signature_for_output_remote_htlc[0] = "304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a6"
signature_for_output_remote_htlc[2] = "3045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b"
signature_for_output_remote_htlc[1] = "304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f202"
signature_for_output_remote_htlc[3] = "3045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554"
signature_for_output_remote_htlc[4] = "304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d"
output_htlc_tx = {}
SUCCESS = True
TIMEOUT = False
output_htlc_tx[0] = (SUCCESS, "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219700000000000000000001e8030000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402206a6e59f18764a5bf8d4fa45eebc591566689441229c918b480fb2af8cc6a4aeb02205248f273be447684b33e3c8d1d85a8e0ca9fa0bae9ae33f0527ada9c162919a60147304402207cb324fa0de88f452ffa9389678127ebcf4cabe1dd848b8e076c1a1962bf34720220116ed922b12311bd602d67e60d2529917f21c5b82f25ff6506c0f87886b4dfd5012000000000000000000000000000000000000000000000000000000000000000008a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a914b8bcb07f6344b42ab04250c86a6e8b75d3fdbbc688527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f401b175ac686800000000")
output_htlc_tx[2] = (TIMEOUT, "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219701000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100d5275b3619953cb0c3b5aa577f04bc512380e60fa551762ce3d7a1bb7401cff9022037237ab0dac3fe100cde094e82e2bed9ba0ed1bb40154b48e56aa70f259e608b01483045022100c89172099507ff50f4c925e6c5150e871fb6e83dd73ff9fbb72f6ce829a9633f02203a63821d9162e99f9be712a68f9e589483994feae2661e4546cd5b6cec007be501008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a914b43e1b38138a41b37f7cd9a1d274bc63e3a9b5d188ac6868f6010000")
output_htlc_tx[1] = (SUCCESS, "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219702000000000000000001d0070000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402201b63ec807771baf4fdff523c644080de17f1da478989308ad13a58b51db91d360220568939d38c9ce295adba15665fa68f51d967e8ed14a007b751540a80b325f20201483045022100def389deab09cee69eaa1ec14d9428770e45bcbe9feb46468ecf481371165c2f022015d2e3c46600b2ebba8dcc899768874cc6851fd1ecb3fffd15db1cc3de7e10da012001010101010101010101010101010101010101010101010101010101010101018a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a9144b6b2e5444c2639cc0fb7bcea5afba3f3cdce23988527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f501b175ac686800000000")
output_htlc_tx[3] = (TIMEOUT, "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219703000000000000000001b80b0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0500483045022100daee1808f9861b6c3ecd14f7b707eca02dd6bdfc714ba2f33bc8cdba507bb182022026654bf8863af77d74f51f4e0b62d461a019561bb12acb120d3f7195d148a554014730440220643aacb19bbb72bd2b635bc3f7375481f5981bace78cdd8319b2988ffcc6704202203d27784ec8ad51ed3bd517a05525a5139bb0b755dd719e0054332d186ac0872701008576a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c820120876475527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae67a9148a486ff2e31d6158bf39e2608864d63fefd09d5b88ac6868f7010000")
output_htlc_tx[4] = (SUCCESS, "020000000001018154ecccf11a5fb56c39654c4deb4d2296f83c69268280b94d021370c94e219704000000000000000001a00f0000000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e050047304402207e0410e45454b0978a623f36a10626ef17b27d9ad44e2760f98cfa3efb37924f0220220bd8acd43ecaa916a80bd4f919c495a2c58982ce7c8625153f8596692a801d014730440220549e80b4496803cbc4a1d09d46df50109f546d43fbbf86cd90b174b1484acd5402205f12a4f995cb9bded597eabfee195a285986aa6d93ae5bb72507ebc6a4e2349e012004040404040404040404040404040404040404040404040404040404040404048a76a91414011f7254d96b819c76986c277d115efce6f7b58763ac67210394854aa6eab5b2a8122cc726e9dded053a2184d88256816826d6231c068d4a5b7c8201208763a91418bc1a114ccf9c052d3d23e28d3b0a9d1227434288527c21030d417a46946384f88d5f3337267c5e579765875dc4daca813e21734b140639e752ae677502f801b175ac686800000000")
htlc_output_index = {0: 0, 1: 2, 2: 1, 3: 3, 4: 4}
for i in range(5):
self.assertEqual(output_htlc_tx[i][1], self.htlc_tx(htlc[i], htlc_output_index[i],
htlc_msat[i],
htlc_payment_preimage[i],
signature_for_output_remote_htlc[i],
output_htlc_tx[i][0], htlc_cltv_timeout[i] if not output_htlc_tx[i][0] else 0,
local_feerate_per_kw,
our_commit_tx))
def htlc_tx(self, htlc, htlc_output_index, amount_msat, htlc_payment_preimage, remote_htlc_sig, success, cltv_timeout, local_feerate_per_kw, our_commit_tx):
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])
our_htlc_tx_witness = make_htlc_tx_witness(
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)
return str(our_htlc_tx)
def test_commitment_tx_with_one_output(self):
to_local_msat= 6988000000
to_remote_msat= 3000000000
local_feerate_per_kw= 9651181
remote_signature = "3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e"
output_commit_tx= "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
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)
self.assertEqual(str(our_commit_tx), output_commit_tx)
def test_commitment_tx_with_fee_greater_than_funder_amount(self):
to_local_msat= 6988000000
to_remote_msat= 3000000000
local_feerate_per_kw= 9651936
remote_signature = "3044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e"
output_commit_tx= "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8001c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de8431100400473044022031a82b51bd014915fe68928d1abf4b9885353fb896cac10c3fdd88d7f9c7f2e00220716bda819641d2c63e65d3549b6120112e1aeaf1742eed94a471488e79e206b101473044022064901950be922e62cbe3f2ab93de2b99f37cff9fc473e73e394b27f88ef0731d02206d1dfa227527b4df44a07599289e207d6fd9cca60c0365682dcd3deaf739567e01475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
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)
self.assertEqual(str(our_commit_tx), output_commit_tx)
def test_extract_commitment_number_from_tx(self):
raw_tx = "02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8007e80300000000000022002052bfef0479d7b293c27e0f1eb294bea154c63a3294ef092c19af51409bce0e2ad007000000000000220020403d394747cae42e98ff01734ad5c08f82ba123d3d9a620abda88989651e2ab5d007000000000000220020748eba944fedc8827f6b06bc44678f93c0f9e6078b35c6331ed31e75f8ce0c2db80b000000000000220020c20b5d1f8584fd90443e7b7b720136174fa4b9333c261d04dbbd012635c0f419a00f0000000000002200208c48d15160397c9731df9bc3b236656efb6665fbfe92b4a6878e88a499f741c4c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de843110e0a06a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e04004730440220275b0c325a5e9355650dc30c0eccfbc7efb23987c24b556b9dfdd40effca18d202206caceb2c067836c51f296740c7ae807ffcbfbf1dd3a0d56b6de9a5b247985f060147304402204fd4928835db1ccdfc40f5c78ce9bd65249b16348df81f0c44328dcdefc97d630220194d3869c38bc732dd87d13d2958015e2fc16829e74cd4377f84d215c0b7060601475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220"
tx = Transaction(raw_tx)
self.assertEqual(commitment_number, extract_ctn_from_tx(tx, 0, local_payment_basepoint, remote_payment_basepoint))
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_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_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

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),
])
@ -729,11 +730,14 @@ 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
# TODO set_rbf by default ? note: inputs might have nSequence set
# TODO maybe BIP_LI01_sort here? but note: add_outputs and add_inputs
return self
@classmethod

View File

@ -438,6 +438,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
@ -500,16 +500,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)
@ -1298,6 +1301,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

@ -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

@ -397,6 +397,8 @@ if __name__ == '__main__':
constants.set_regtest()
elif config.get('simnet'):
constants.set_simnet()
else:
raise Exception('lightning branch not available on mainnet')
# run non-RPC commands separately
if cmdname in ['create', 'restore']: