import sys import time import threading import queue from hashlib import sha256 from secrets import token_bytes import grpc from lnd_grpc.protos import invoices_pb2 as invoices_pb2, rpc_pb2 from loop_rpc.protos import loop_client_pb2 from test_utils.fixtures import * from test_utils.lnd import LndNode impls = [LndNode] if TEST_DEBUG: logging.basicConfig( level=logging.DEBUG, format="%(name)-12s %(message)s", stream=sys.stdout ) logging.info("Tests running in '%s'", TEST_DIR) FUND_AMT = 10 ** 7 SEND_AMT = 10 ** 3 def get_updates(_queue): """ Get all available updates from a queue.Queue() instance and return them as a list """ _list = [] while not _queue.empty(): _list.append(_queue.get()) return _list def transact_and_mine(btc): """ Generate some transactions and blocks. To make bitcoind's `estimatesmartfee` succeeded. """ addr = btc.rpc.getnewaddress("", "bech32") for i in range(10): for j in range(10): txid = btc.rpc.sendtoaddress(addr, 0.5) btc.rpc.generatetoaddress(1, addr) def wait_for(success, timeout=30, interval=0.25): start_time = time.time() while not success() and time.time() < start_time + timeout: time.sleep(interval) if time.time() > start_time + timeout: raise ValueError("Error waiting for {}", success) def wait_for_bool(success, timeout=30, interval=0.25): start_time = time.time() while not success and time.time() < start_time + timeout: time.sleep(interval) if time.time() > start_time + timeout: raise ValueError("Error waiting for {}", success) def sync_blockheight(btc, nodes): """ Sync blockheight of nodes by checking logs until timeout """ info = btc.rpc.getblockchaininfo() blocks = info["blocks"] for n in nodes: wait_for(lambda: n.get_info().block_height == blocks, interval=1) time.sleep(0.25) def generate_until(btc, success, blocks=30, interval=1): """ Generate new blocks until `success` returns true. Mainly used to wait for transactions to confirm since they might be delayed and we don't want to add a long waiting time to all tests just because some are slow. """ addr = btc.rpc.getnewaddress("", "bech32") for i in range(blocks): time.sleep(interval) if success(): return generate(bitcoind, 1) time.sleep(interval) if not success(): raise ValueError("Generated %d blocks, but still no success", blocks) def gen_and_sync_lnd(bitcoind, nodes): """ generate a few blocks and wait for lnd nodes to sync """ generate(bitcoind, 3) sync_blockheight(bitcoind, nodes=nodes) for node in nodes: wait_for(lambda: node.get_info().synced_to_chain, interval=0.25) time.sleep(0.25) def generate(bitcoind, blocks): addr = bitcoind.rpc.getnewaddress("", "bech32") bitcoind.rpc.generatetoaddress(blocks, addr) def close_all_channels(bitcoind, nodes): """ Recursively close each channel for each node in the list of nodes passed in and assert """ gen_and_sync_lnd(bitcoind, nodes) for node in nodes: for channel in node.list_channels(): channel_point = channel.channel_point node.close_channel(channel_point=channel_point).__next__() gen_and_sync_lnd(bitcoind, nodes) assert not node.list_channels() gen_and_sync_lnd(bitcoind, nodes) def disconnect_all_peers(bitcoind, nodes): """ Recursively disconnect each peer from each node in the list of nodes passed in and assert """ gen_and_sync_lnd(bitcoind, nodes) for node in nodes: peers = [p.pub_key for p in node.list_peers()] for peer in peers: node.disconnect_peer(pub_key=peer) wait_for(lambda: peer not in node.list_peers(), timeout=5) assert peer not in [p.pub_key for p in node.list_peers()] gen_and_sync_lnd(bitcoind, nodes) def get_addresses(node, response="str"): p2wkh_address = node.new_address(address_type="p2wkh") np2wkh_address = node.new_address(address_type="np2wkh") if response == "str": return p2wkh_address.address, np2wkh_address.address return p2wkh_address, np2wkh_address def setup_nodes(bitcoind, nodes, delay=0): """ Break down all nodes, open fresh channels between them with half the balance pushed remotely and assert :return: the setup nodes """ # Needed by lnd in order to have at least one block in the last 2 hours generate(bitcoind, 1) # First break down nodes. This avoids situations where a test fails and breakdown is not called break_down_nodes(bitcoind, nodes, delay) # setup requested nodes and create a single channel from one to the next # capacity in one direction only (alphabetical) setup_channels(bitcoind, nodes, delay) return nodes def setup_channels(bitcoind, nodes, delay): for i, node in enumerate(nodes): if i + 1 == len(nodes): break nodes[i].connect( str(nodes[i + 1].id() + "@localhost:" + str(nodes[i + 1].daemon.port)), perm=1, ) wait_for(lambda: nodes[i].list_peers(), interval=0.25) wait_for(lambda: nodes[i + 1].list_peers(), interval=0.25) time.sleep(delay) nodes[i].add_funds(bitcoind, 1) gen_and_sync_lnd(bitcoind, [nodes[i], nodes[i + 1]]) nodes[i].open_channel_sync( node_pubkey_string=nodes[i + 1].id(), local_funding_amount=FUND_AMT, push_sat=int(FUND_AMT / 2), spend_unconfirmed=True, ) time.sleep(delay) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [nodes[i], nodes[i + 1]]) assert confirm_channel(bitcoind, nodes[i], nodes[i + 1]) def break_down_nodes(bitcoind, nodes, delay=0): close_all_channels(bitcoind, nodes) time.sleep(delay) disconnect_all_peers(bitcoind, nodes) time.sleep(delay) def confirm_channel(bitcoind, n1, n2): """ Confirm that a channel is open between two nodes """ assert n1.id() in [p.pub_key for p in n2.list_peers()] assert n2.id() in [p.pub_key for p in n1.list_peers()] for i in range(10): time.sleep(0.5) if n1.check_channel(n2) and n2.check_channel(n1): return True addr = bitcoind.rpc.getnewaddress("", "bech32") bhash = bitcoind.rpc.generatetoaddress(1, addr)[0] n1.block_sync(bhash) n2.block_sync(bhash) # Last ditch attempt return n1.check_channel(n2) and n2.check_channel(n1) # def idfn(impls): # """ # Not used currently # """ # return "_".join([i.displayName for i in impls]) def wipe_channels_from_disk(node, network="regtest"): """ used to test channel backups """ _channel_backup = node.lnd_dir + f"chain/bitcoin/{network}/channel.backup" _channel_db = node.lnd_dir + f"graph/{network}/channel.db" assert os.path.exists(_channel_backup) assert os.path.exists(_channel_db) os.remove(_channel_backup) os.remove(_channel_db) assert not os.path.exists(_channel_backup) assert not os.path.exists(_channel_db) def random_32_byte_hash(): """ Can generate an invoice preimage and corresponding payment hash :return: 32 byte sha256 hash digest, 32 byte preimage """ preimage = token_bytes(32) _hash = sha256(preimage) return _hash.digest(), preimage ######### # Tests # ######### class TestNonInteractiveLightning: """ Non-interactive tests will share a common lnd instance because test passes/failures will not impact future tests. """ def test_start(self, bitcoind, alice): assert alice.get_info() sync_blockheight(bitcoind, [alice]) def test_wallet_balance(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.get_info(), rpc_pb2.GetInfoResponse) pytest.raises(TypeError, alice.wallet_balance, "please") def test_channel_balance(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.channel_balance(), rpc_pb2.ChannelBalanceResponse) pytest.raises(TypeError, alice.channel_balance, "please") def test_get_transactions(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.get_transactions(), rpc_pb2.TransactionDetails) pytest.raises(TypeError, alice.get_transactions, "please") def test_send_coins(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) alice.add_funds(alice.bitcoin, 1) p2wkh_address, np2wkh_address = get_addresses(alice) # test passes send1 = alice.send_coins(addr=p2wkh_address, amount=100000) generate(alice.bitcoin, 1) time.sleep(0.5) send2 = alice.send_coins(addr=np2wkh_address, amount=100000) assert isinstance(send1, rpc_pb2.SendCoinsResponse) assert isinstance(send2, rpc_pb2.SendCoinsResponse) # test failures pytest.raises( grpc.RpcError, lambda: alice.send_coins( alice.new_address(address_type="p2wkh").address, amount=100000 * -1 ), ) pytest.raises( grpc.RpcError, lambda: alice.send_coins( alice.new_address(address_type="p2wkh").address, amount=1000000000000000 ), ) def test_send_many(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) alice.add_funds(alice.bitcoin, 1) p2wkh_address, np2wkh_address = get_addresses(alice) send_dict = {p2wkh_address: 100000, np2wkh_address: 100000} send = alice.send_many(addr_to_amount=send_dict) alice.bitcoin.rpc.generatetoaddress(1, p2wkh_address) time.sleep(0.5) assert isinstance(send, rpc_pb2.SendManyResponse) def test_list_unspent(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) alice.add_funds(alice.bitcoin, 1) assert isinstance(alice.list_unspent(0, 1000), rpc_pb2.ListUnspentResponse) def test_subscribe_transactions(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) subscription = alice.subscribe_transactions() alice.add_funds(alice.bitcoin, 1) assert isinstance(subscription, grpc._channel._Rendezvous) assert isinstance(subscription.__next__(), rpc_pb2.Transaction) # gen_and_sync_lnd(alice.bitcoin, [alice]) # transaction_updates = queue.LifoQueue() # # def sub_transactions(): # try: # for response in alice.subscribe_transactions(): # transaction_updates.put(response) # except StopIteration: # pass # # alice_sub = threading.Thread(target=sub_transactions(), daemon=True) # alice_sub.start() # time.sleep(1) # while not alice_sub.is_alive(): # time.sleep(0.1) # alice.add_funds(alice.bitcoin, 1) # # assert any(isinstance(update) == rpc_pb2.Transaction for update in get_updates(transaction_updates)) def test_new_address(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) p2wkh_address, np2wkh_address = get_addresses(alice, "response") assert isinstance(p2wkh_address, rpc_pb2.NewAddressResponse) assert isinstance(np2wkh_address, rpc_pb2.NewAddressResponse) def test_sign_verify_message(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) message = "Test message to sign and verify." signature = alice.sign_message(message) assert isinstance(signature, rpc_pb2.SignMessageResponse) verified_message = alice.verify_message(message, signature.signature) assert isinstance(verified_message, rpc_pb2.VerifyMessageResponse) def test_get_info(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.get_info(), rpc_pb2.GetInfoResponse) def test_pending_channels(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.pending_channels(), rpc_pb2.PendingChannelsResponse) # Skipping list_channels and closed_channels as we don't return their responses directly def test_add_invoice(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) invoice = alice.add_invoice(value=SEND_AMT) assert isinstance(invoice, rpc_pb2.AddInvoiceResponse) def test_list_invoices(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.list_invoices(), rpc_pb2.ListInvoiceResponse) def test_lookup_invoice(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) payment_hash = alice.add_invoice(value=SEND_AMT).r_hash assert isinstance(alice.lookup_invoice(r_hash=payment_hash), rpc_pb2.Invoice) def test_subscribe_invoices(self, alice): """ Invoice subscription run as a thread """ gen_and_sync_lnd(alice.bitcoin, [alice]) invoice_updates = queue.LifoQueue() def sub_invoices(): try: for response in alice.subscribe_invoices(): invoice_updates.put(response) except grpc._channel._Rendezvous: pass alice_sub = threading.Thread(target=sub_invoices, daemon=True) alice_sub.start() time.sleep(1) while not alice_sub.is_alive(): time.sleep(0.1) alice.add_invoice(value=SEND_AMT) alice.daemon.wait_for_log("AddIndex") time.sleep(0.1) assert any( isinstance(update, rpc_pb2.Invoice) for update in get_updates(invoice_updates) ) def test_decode_payment_request(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) pay_req = alice.add_invoice(value=SEND_AMT).payment_request decoded_req = alice.decode_pay_req(pay_req=pay_req) assert isinstance(decoded_req, rpc_pb2.PayReq) def test_list_payments(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.list_payments(), rpc_pb2.ListPaymentsResponse) def test_delete_all_payments(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance( alice.delete_all_payments(), rpc_pb2.DeleteAllPaymentsResponse ) def test_describe_graph(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.describe_graph(), rpc_pb2.ChannelGraph) # Skipping get_chan_info, subscribe_chan_events, get_alice_info, query_routes def test_get_network_info(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.get_network_info(), rpc_pb2.NetworkInfo) @pytest.mark.skipif( TRAVIS is True, reason="Travis doesn't like this one. Possibly a race" "condition not worth debugging", ) def test_stop_daemon(self, node_factory): node = node_factory.get_node(implementation=LndNode, node_id="test_stop_node") node.daemon.wait_for_log("Server listening on") node.stop_daemon() # use is_in_log instead of wait_for_log as node daemon should be shutdown node.daemon.is_in_log("Shutdown complete") time.sleep(1) with pytest.raises(grpc.RpcError): node.get_info() def test_debug_level(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance( alice.debug_level(level_spec="warn"), rpc_pb2.DebugLevelResponse ) def test_fee_report(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.fee_report(), rpc_pb2.FeeReportResponse) def test_forwarding_history(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) assert isinstance(alice.forwarding_history(), rpc_pb2.ForwardingHistoryResponse) def test_lightning_stub(self, alice): gen_and_sync_lnd(alice.bitcoin, [alice]) original_stub = alice.lightning_stub # not simulation of actual failure, but failure in the form that should be detected by # connectivity event logger alice.connection_status_change = True # make a call to stimulate stub regeneration alice.get_info() new_stub = alice.lightning_stub assert original_stub != new_stub class TestInteractiveLightning: def test_peer_connection(self, bob, carol, dave, bitcoind): # Needed by lnd in order to have at least one block in the last 2 hours generate(bitcoind, 1) # connection tests connection1 = bob.connect( str(carol.id() + "@localhost:" + str(carol.daemon.port)) ) wait_for(lambda: bob.list_peers(), timeout=5) wait_for(lambda: carol.list_peers(), timeout=5) # check bob connected to carol using connect() and list_peers() assert isinstance(connection1, rpc_pb2.ConnectPeerResponse) assert bob.id() in [p.pub_key for p in carol.list_peers()] assert carol.id() in [p.pub_key for p in bob.list_peers()] dave_ln_addr = dave.lightning_address( pubkey=dave.id(), host="localhost:" + str(dave.daemon.port) ) carol.connect_peer(dave_ln_addr) wait_for(lambda: carol.list_peers(), timeout=5) wait_for(lambda: dave.list_peers(), timeout=5) # check carol connected to dave using connect() and list_peers() assert carol.id() in [p.pub_key for p in dave.list_peers()] assert dave.id() in [p.pub_key for p in carol.list_peers()] generate(bob.bitcoin, 1) gen_and_sync_lnd(bitcoind, [bob, carol]) # Disconnection tests bob.disconnect_peer(pub_key=str(carol.id())) time.sleep(0.25) # check bob not connected to carol using connect() and list_peers() assert bob.id() not in [p.pub_key for p in carol.list_peers()] assert carol.id() not in [p.pub_key for p in bob.list_peers()] carol.disconnect_peer(dave.id()) wait_for(lambda: not carol.list_peers(), timeout=5) wait_for(lambda: not dave.list_peers(), timeout=5) # check carol not connected to dave using connect_peer() and list_peers() assert carol.id() not in [p.pub_key for p in dave.list_peers()] assert dave.id() not in [p.pub_key for p in carol.list_peers()] def test_open_channel_sync(self, bob, carol, bitcoind): # Needed by lnd in order to have at least one block in the last 2 hours generate(bitcoind, 1) disconnect_all_peers(bitcoind, [bob, carol]) bob.connect(str(carol.id() + "@localhost:" + str(carol.daemon.port)), perm=1) wait_for(lambda: bob.list_peers(), interval=1) wait_for(lambda: carol.list_peers(), interval=1) bob.add_funds(bitcoind, 1) gen_and_sync_lnd(bitcoind, [bob, carol]) bob.open_channel_sync( node_pubkey_string=carol.id(), local_funding_amount=FUND_AMT ) gen_and_sync_lnd(bitcoind, [bob, carol]) assert confirm_channel(bitcoind, bob, carol) assert bob.check_channel(carol) assert carol.check_channel(bob) def test_open_channel(self, bob, carol, bitcoind): # Needed by lnd in order to have at least one block in the last 2 hours generate(bitcoind, 1) break_down_nodes(bitcoind, nodes=[bob, carol]) bob.connect(str(carol.id() + "@localhost:" + str(carol.daemon.port)), perm=1) wait_for(lambda: bob.list_peers(), interval=0.5) wait_for(lambda: carol.list_peers(), interval=0.5) bob.add_funds(bitcoind, 1) gen_and_sync_lnd(bitcoind, [bob, carol]) bob.open_channel( node_pubkey_string=carol.id(), local_funding_amount=FUND_AMT ).__next__() generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) assert confirm_channel(bitcoind, bob, carol) assert bob.check_channel(carol) assert carol.check_channel(bob) def test_close_channel(self, bob, carol, bitcoind): bob, carol = setup_nodes(bitcoind, [bob, carol]) channel_point = bob.list_channels()[0].channel_point bob.close_channel(channel_point=channel_point).__next__() generate(bitcoind, 6) gen_and_sync_lnd(bitcoind, [bob, carol]) assert bob.check_channel(carol) is False assert carol.check_channel(bob) is False def test_send_payment_sync(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) # test payment request method invoice = carol.add_invoice(value=SEND_AMT) bob.send_payment_sync(payment_request=invoice.payment_request) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True # test manually specified request invoice2 = carol.add_invoice(value=SEND_AMT) bob.send_payment_sync( dest_string=carol.id(), amt=SEND_AMT, payment_hash=invoice2.r_hash, final_cltv_delta=144, ) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash2 = carol.decode_pay_req(invoice2.payment_request).payment_hash assert payment_hash2 in [p.payment_hash for p in bob.list_payments().payments] assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True # test sending any amount to an invoice which requested 0 invoice3 = carol.add_invoice(value=0) bob.send_payment_sync(payment_request=invoice3.payment_request, amt=SEND_AMT) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash = carol.decode_pay_req(invoice3.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] inv_paid = carol.lookup_invoice(r_hash_str=payment_hash) assert inv_paid.settled is True assert inv_paid.amt_paid_sat == SEND_AMT def test_send_payment(self, bitcoind, bob, carol): # TODO: remove try/except hack for curve generation bob, carol = setup_nodes(bitcoind, [bob, carol]) # test payment request method invoice = carol.add_invoice(value=SEND_AMT) try: bob.send_payment(payment_request=invoice.payment_request).__next__() except StopIteration: pass bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True # test manually specified request invoice2 = carol.add_invoice(value=SEND_AMT) try: bob.send_payment( dest_string=carol.id(), amt=SEND_AMT, payment_hash=invoice2.r_hash, final_cltv_delta=144, ).__next__() except StopIteration: pass bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash2 = carol.decode_pay_req(invoice2.payment_request).payment_hash assert payment_hash2 in [p.payment_hash for p in bob.list_payments().payments] assert carol.lookup_invoice(r_hash_str=payment_hash).settled is True # test sending different amount to invoice where 0 is requested invoice = carol.add_invoice(value=0) try: bob.send_payment( payment_request=invoice.payment_request, amt=SEND_AMT ).__next__() except StopIteration: pass bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) payment_hash = carol.decode_pay_req(invoice.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] inv_paid = carol.lookup_invoice(r_hash_str=payment_hash) assert inv_paid.settled is True assert inv_paid.amt_paid_sat == SEND_AMT def test_send_to_route_sync(self, bitcoind, bob, carol, dave): bob, carol, dave = setup_nodes(bitcoind, [bob, carol, dave]) gen_and_sync_lnd(bitcoind, [bob, carol, dave]) invoice = dave.add_invoice(value=SEND_AMT) route = bob.query_routes(pub_key=dave.id(), amt=SEND_AMT, final_cltv_delta=144) bob.send_to_route_sync(payment_hash=invoice.r_hash, route=route[0]) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol, dave]) payment_hash = dave.decode_pay_req(invoice.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] assert dave.lookup_invoice(r_hash_str=payment_hash).settled is True def test_send_to_route(self, bitcoind, bob, carol, dave): bob, carol, dave = setup_nodes(bitcoind, [bob, carol, dave]) gen_and_sync_lnd(bitcoind, [bob, carol, dave]) invoice = dave.add_invoice(value=SEND_AMT) route = bob.query_routes(pub_key=dave.id(), amt=SEND_AMT, final_cltv_delta=144) try: bob.send_to_route(invoice=invoice, route=route[0]).__next__() except StopIteration: pass bob.daemon.wait_for_log("Closed completed SETTLE circuit", timeout=60) generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol, dave]) payment_hash = dave.decode_pay_req(invoice.payment_request).payment_hash assert payment_hash in [p.payment_hash for p in bob.list_payments().payments] assert dave.lookup_invoice(r_hash_str=payment_hash).settled is True def test_subscribe_channel_events(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) gen_and_sync_lnd(bitcoind, [bob, carol]) chan_updates = queue.LifoQueue() def sub_channel_events(): try: for response in bob.subscribe_channel_events(): chan_updates.put(response) except grpc._channel._Rendezvous: pass bob_sub = threading.Thread(target=sub_channel_events, daemon=True) bob_sub.start() time.sleep(1) while not bob_sub.is_alive(): time.sleep(0.1) channel_point = bob.list_channels()[0].channel_point bob.close_channel(channel_point=channel_point).__next__() generate(bitcoind, 3) gen_and_sync_lnd(bitcoind, [bob, carol]) assert any( update.closed_channel is not None for update in get_updates(chan_updates) ) def test_subscribe_channel_graph(self, bitcoind, bob, carol, dave): bob, carol = setup_nodes(bitcoind, [bob, carol]) new_fee = 5555 subscription = bob.subscribe_channel_graph() carol.update_channel_policy( chan_point=None, base_fee_msat=new_fee, fee_rate=0.5555, time_lock_delta=9, is_global=True, ) assert isinstance(subscription.__next__(), rpc_pb2.GraphTopologyUpdate) def test_update_channel_policy(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) update = bob.update_channel_policy( chan_point=None, base_fee_msat=5555, fee_rate=0.5555, time_lock_delta=9, is_global=True, ) assert isinstance(update, rpc_pb2.PolicyUpdateResponse) class TestChannelBackup: def test_export_verify_restore_multi(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) funding_txid, output_index = bob.list_channels()[0].channel_point.split(":") channel_point = bob.channel_point_generator( funding_txid=funding_txid, output_index=output_index ) all_backup = bob.export_all_channel_backups() assert isinstance(all_backup, rpc_pb2.ChanBackupSnapshot) # assert the multi_chan backup assert bob.verify_chan_backup(multi_chan_backup=all_backup.multi_chan_backup) bob.stop() wipe_channels_from_disk(bob) bob.start() assert not bob.list_channels() assert bob.restore_chan_backup( multi_chan_backup=all_backup.multi_chan_backup.multi_chan_backup ) bob.daemon.wait_for_log("Inserting 1 SCB channel shells into DB") carol.daemon.wait_for_log("Broadcasting force close transaction") generate(bitcoind, 6) bob.daemon.wait_for_log("Publishing sweep tx", timeout=120) generate(bitcoind, 6) assert bob.daemon.wait_for_log( "a contract has been fully resolved!", timeout=120 ) def test_export_verify_restore_single(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) funding_txid, output_index = bob.list_channels()[0].channel_point.split(":") channel_point = bob.channel_point_generator( funding_txid=funding_txid, output_index=output_index ) single_backup = bob.export_chan_backup(chan_point=channel_point) assert isinstance(single_backup, rpc_pb2.ChannelBackup) packed_backup = bob.pack_into_channelbackups(single_backup=single_backup) # assert the single_chan_backup assert bob.verify_chan_backup(single_chan_backups=packed_backup) bob.stop() wipe_channels_from_disk(bob) bob.start() assert not bob.list_channels() assert bob.restore_chan_backup(chan_backups=packed_backup) bob.daemon.wait_for_log("Inserting 1 SCB channel shells into DB") carol.daemon.wait_for_log("Broadcasting force close transaction") generate(bitcoind, 6) bob.daemon.wait_for_log("Publishing sweep tx", timeout=120) generate(bitcoind, 6) assert bob.daemon.wait_for_log( "a contract has been fully resolved!", timeout=120 ) class TestInvoices: def test_all_invoice(self, bitcoind, bob, carol): bob, carol = setup_nodes(bitcoind, [bob, carol]) _hash, preimage = random_32_byte_hash() invoice_queue = queue.LifoQueue() invoice = carol.add_hold_invoice( memo="pytest hold invoice", hash=_hash, value=SEND_AMT ) decoded_invoice = carol.decode_pay_req(pay_req=invoice.payment_request) assert isinstance(invoice, invoices_pb2.AddHoldInvoiceResp) # thread functions def inv_sub_worker(_hash): try: for _response in carol.subscribe_single_invoice(_hash): invoice_queue.put(_response) except grpc._channel._Rendezvous: pass def pay_hold_inv_worker(payment_request): try: bob.pay_invoice(payment_request=payment_request) except grpc._channel._Rendezvous: pass def settle_inv_worker(_preimage): try: carol.settle_invoice(preimage=_preimage) except grpc._channel._Rendezvous: pass # setup the threads inv_sub = threading.Thread( target=inv_sub_worker, name="inv_sub", args=[_hash], daemon=True ) pay_inv = threading.Thread( target=pay_hold_inv_worker, args=[invoice.payment_request] ) settle_inv = threading.Thread(target=settle_inv_worker, args=[preimage]) # start the threads inv_sub.start() # wait for subscription to start while not inv_sub.is_alive(): time.sleep(0.1) pay_inv.start() time.sleep(2) # carol.daemon.wait_for_log(regex=f'Invoice({decoded_invoice.payment_hash}): accepted,') settle_inv.start() while settle_inv.is_alive(): time.sleep(0.1) inv_sub.join(timeout=1) assert any(invoice.settled is True for invoice in get_updates(invoice_queue)) class TestLoop: @pytest.mark.skip(reason="waiting to configure loop swapserver") def test_loop_out_quote(self, bitcoind, alice, bob, loopd): """ 250000 satoshis is currently middle of range of allowed loop amounts """ loop_amount = 250000 alice, bob = setup_nodes(bitcoind, [alice, bob]) if alice.daemon.invoice_rpc_active: quote = loopd.loop_out_quote(amt=loop_amount) assert quote is not None assert isinstance(quote, loop_client_pb2.QuoteResponse) else: logging.info("test_loop_out() skipped as invoice RPC not detected") @pytest.mark.skip(reason="waiting to configure loop swapserver") def test_loop_out_terms(self, bitcoind, alice, bob, loopd): alice, bob = setup_nodes(bitcoind, [alice, bob]) if alice.daemon.invoice_rpc_active: terms = loopd.loop_out_terms() assert terms is not None assert isinstance(terms, loop_client_pb2.TermsResponse) else: logging.info("test_loop_out() skipped as invoice RPC not detected")