diff --git a/tests/__init__.py b/tests/__init__.py index 4e239f381..4fac041ca 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -28,6 +28,7 @@ class ElectrumTestCase(unittest.IsolatedAsyncioTestCase, Logger): """Base class for our unit tests.""" TESTNET = False + TEST_ANCHOR_CHANNELS = False # maxDiff = None # for debugging # some unit tests are modifying globals... so we run sequentially: diff --git a/tests/test_lnchannel.py b/tests/test_lnchannel.py index 433fa105f..600e9bb26 100644 --- a/tests/test_lnchannel.py +++ b/tests/test_lnchannel.py @@ -43,17 +43,16 @@ from electrum.coinchooser import PRNG from . import ElectrumTestCase -TEST_ANCHOR_CHANNELS = True - one_bitcoin_in_msat = bitcoin.COIN * 1000 def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, local_amount, remote_amount, privkeys, other_pubkeys, seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, - r_csv, anchor_outputs=TEST_ANCHOR_CHANNELS): + r_csv, anchor_outputs): #assert local_amount > 0 #assert remote_amount > 0 + channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) channel_type = lnutil.ChannelType.OPTION_STATIC_REMOTEKEY if anchor_outputs: @@ -130,7 +129,7 @@ def bip32(sequence): def create_test_channels(*, feerate=6000, local_msat=None, remote_msat=None, alice_name="alice", bob_name="bob", alice_pubkey=b"\x01"*33, bob_pubkey=b"\x02"*33, random_seed=None, - anchor_outputs=TEST_ANCHOR_CHANNELS): + anchor_outputs=False): if random_seed is None: # needed for deterministic randomness random_seed = os.urandom(32) random_gen = PRNG(random_seed) @@ -214,10 +213,12 @@ class TestFee(ElectrumTestCase): https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 """ def test_fee(self): - alice_channel, bob_channel = create_test_channels(feerate=253, - local_msat=10000000000, - remote_msat=5000000000, anchor_outputs=TEST_ANCHOR_CHANNELS) - expected_value = 9999056 if TEST_ANCHOR_CHANNELS else 9999817 + alice_channel, bob_channel = create_test_channels( + feerate=253, + local_msat=10000000000, + remote_msat=5000000000, + anchor_outputs=self.TEST_ANCHOR_CHANNELS) + expected_value = 9999056 if self.TEST_ANCHOR_CHANNELS else 9999817 self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) class TestChannel(ElectrumTestCase): @@ -231,7 +232,7 @@ class TestChannel(ElectrumTestCase): self.assertFalse() def assertNumberNonAnchorOutputs(self, number, tx): - self.assertEqual(number, len(tx.outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(number, len(tx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) @classmethod def setUpClass(cls): @@ -243,7 +244,7 @@ class TestChannel(ElectrumTestCase): # 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(anchor_outputs=TEST_ANCHOR_CHANNELS) + self.alice_channel, self.bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) self.paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(self.paymentPreimage) @@ -566,7 +567,6 @@ class TestChannel(ElectrumTestCase): self.assertEqual(bob_channel.total_msat(RECEIVED), one_bitcoin_in_msat, "bob satoshis received incorrect") self.assertEqual(bob_channel.total_msat(SENT), 5 * one_bitcoin_in_msat, "bob satoshis sent incorrect") - def alice_to_bob_fee_update(self, fee=1111): aoldctx = self.alice_channel.get_next_commitment(REMOTE).outputs() self.alice_channel.update_fee(fee, True) @@ -670,9 +670,13 @@ class TestChannel(ElectrumTestCase): self.assertIn('Not enough local balance', cm.exception.args[0]) +class TestChannelAnchors(TestChannel): + TEST_ANCHOR_CHANNELS = True + + class TestAvailableToSpend(ElectrumTestCase): def test_DesyncHTLCs(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) self.assertEqual(499986152000 if not alice_channel.has_anchors() else 499980692000, alice_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) @@ -718,9 +722,13 @@ class TestAvailableToSpend(ElectrumTestCase): alice_channel.add_htlc(htlc) +class TestAvailableToSpendAnchors(TestAvailableToSpend): + TEST_ANCHOR_CHANNELS = True + + class TestChanReserve(ElectrumTestCase): def setUp(self): - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=False) alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000) # We set Bob's channel reserve to a value that is larger than # his current balance in the channel. This will ensure that @@ -847,10 +855,15 @@ class TestChanReserve(ElectrumTestCase): self.assertEqual(self.alice_channel.available_to_spend(REMOTE), amt2) self.assertEqual(self.bob_channel.available_to_spend(LOCAL), amt2) + +class TestChanReserveAnchors(TestChanReserve): + TEST_ANCHOR_CHANNELS = True + + class TestDust(ElectrumTestCase): def test_DustLimit(self): """Test that addition of an HTLC below the dust limit changes the balances.""" - alice_channel, bob_channel = create_test_channels() + alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_CHANNELS) dust_limit_alice = alice_channel.config[LOCAL].dust_limit_sat dust_limit_bob = bob_channel.config[LOCAL].dust_limit_sat self.assertLess(dust_limit_alice, dust_limit_bob) @@ -860,7 +873,7 @@ class TestDust(ElectrumTestCase): paymentPreimage = b"\x01" * 32 paymentHash = bitcoin.sha256(paymentPreimage) fee_per_kw = alice_channel.get_next_feerate(LOCAL) - success_weight = effective_htlc_tx_weight(success=True, has_anchors=TEST_ANCHOR_CHANNELS) + success_weight = effective_htlc_tx_weight(success=True, has_anchors=self.TEST_ANCHOR_CHANNELS) # we put a single sat less into the htlc than bob can afford # to pay for his htlc success transaction below_dust_for_bob = dust_limit_bob - 1 @@ -882,13 +895,13 @@ class TestDust(ElectrumTestCase): self.assertNotEqual(bobs_original_outputs, bobs_second_outputs) # the htlc appears as an output in alice's ctx, as she has a lower # dust limit (also because her timeout tx costs less) - self.assertEqual(3, len(alice_ctx.outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(3, len(alice_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) # htlc in bob's case goes to miner fees - self.assertEqual(2, len(bob_ctx.outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(2, len(bob_ctx.outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) self.assertEqual(htlc_amt, sum(bobs_original_outputs) - sum(bobs_second_outputs)) empty_ctx_fee = lnutil.calc_fees_for_commitment_tx( num_htlcs=0, feerate=fee_per_kw, is_local_initiator=True, - round_to_sat=True, has_anchors=TEST_ANCHOR_CHANNELS)[LOCAL] // 1000 + round_to_sat=True, has_anchors=self.TEST_ANCHOR_CHANNELS)[LOCAL] // 1000 self.assertEqual(empty_ctx_fee + htlc_amt, bob_channel.get_next_fee(LOCAL)) bob_channel.settle_htlc(paymentPreimage, bob_htlc_id) @@ -899,12 +912,16 @@ class TestDust(ElectrumTestCase): # htlc is added back into the balance self.assertEqual(sum(bobs_original_outputs), sum(bobs_third_outputs)) # balance shifts in bob's direction after settlement - self.assertEqual(htlc_amt, bobs_third_outputs[1 + (2 if TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if TEST_ANCHOR_CHANNELS else 0)]) - self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) - self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(htlc_amt, bobs_third_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)] - bobs_original_outputs[1 + (2 if self.TEST_ANCHOR_CHANNELS else 0)]) + self.assertEqual(2, len(alice_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) + self.assertEqual(2, len(bob_channel.get_next_commitment(LOCAL).outputs()) - (2 if self.TEST_ANCHOR_CHANNELS else 0)) self.assertEqual(htlc_amt, alice_channel.total_msat(SENT) // 1000) +class TestDustAnchors(TestDust): + TEST_ANCHOR_CHANNELS = True + + def force_state_transition(chanA, chanB): chanB.receive_new_commitment(*chanA.sign_next_commitment()) rev = chanB.revoke_current_commitment() diff --git a/tests/test_lnpeer.py b/tests/test_lnpeer.py index c84d5e363..0e51a2740 100644 --- a/tests/test_lnpeer.py +++ b/tests/test_lnpeer.py @@ -45,15 +45,12 @@ from electrum.invoices import PR_PAID, PR_UNPAID from electrum.interface import GracefulDisconnect from electrum.simple_config import SimpleConfig -from .test_lnchannel import create_test_channels as create_test_channels_anchors + + +from .test_lnchannel import create_test_channels +from .test_bitcoin import needs_test_with_all_chacha20_implementations from . import ElectrumTestCase -TEST_ANCHOR_CHANNELS = True - - -def create_test_channels(*args, **kwargs): - return create_test_channels_anchors(*args, **kwargs, anchor_outputs=TEST_ANCHOR_CHANNELS) - def keypair(): priv = ECPrivkey.generate_random_key().get_secret_bytes() @@ -155,7 +152,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): MPP_SPLIT_PART_FRACTION = 1 # this disables the forced splitting MPP_SPLIT_PART_MINAMT_MSAT = 5_000_000 - def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name): + def __init__(self, *, local_keypair: Keypair, chans: Iterable['Channel'], tx_queue, name, has_anchors): self.name = name Logger.__init__(self) NetworkRetryManager.__init__(self, max_retry_delay_normal=1, init_retry_delay_normal=1) @@ -181,7 +178,7 @@ class MockLNWallet(Logger, EventListener, NetworkRetryManager[LNPeerAddr]): self.features |= LnFeatures.OPTION_CHANNEL_TYPE_OPT self.features |= LnFeatures.OPTION_SCID_ALIAS_OPT self.features |= LnFeatures.OPTION_STATIC_REMOTEKEY_OPT - self.config.ENABLE_ANCHOR_CHANNELS = TEST_ANCHOR_CHANNELS + self.config.ENABLE_ANCHOR_CHANNELS = has_anchors self.pending_payments = defaultdict(asyncio.Future) for chan in chans: chan.lnworker = self @@ -559,8 +556,8 @@ class TestPeerDirect(TestPeer): bob_channel.storage['node_id'] = bob_channel.node_id t1, t2 = transport_pair(k1, k2, alice_channel.name, bob_channel.name) q1, q2 = asyncio.Queue(), asyncio.Queue() - w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1, name=bob_channel.name) - w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2, name=alice_channel.name) + w1 = MockLNWallet(local_keypair=k1, chans=[alice_channel], tx_queue=q1, name=bob_channel.name, has_anchors=self.TEST_ANCHOR_CHANNELS) + w2 = MockLNWallet(local_keypair=k2, chans=[bob_channel], tx_queue=q2, name=alice_channel.name, has_anchors=self.TEST_ANCHOR_CHANNELS) self._lnworkers_created.extend([w1, w2]) p1 = PeerInTests(w1, k2.pubkey, t1) p2 = PeerInTests(w2, k1.pubkey, t2) @@ -1439,7 +1436,6 @@ class TestPeerForwarding(TestPeer): transports = {} workers = {} # type: Dict[str, MockLNWallet] peers = {} - # create channels for a, definition in graph_definition.items(): for b, channel_def in definition.get('channels', {}).items(): @@ -1450,6 +1446,7 @@ class TestPeerForwarding(TestPeer): bob_pubkey=keys[b].pubkey, local_msat=channel_def['local_balance_msat'], remote_msat=channel_def['remote_balance_msat'], + anchor_outputs=self.TEST_ANCHOR_CHANNELS ) channels[(a, b)], channels[(b, a)] = channel_ab, channel_ba transport_ab, transport_ba = transport_pair(keys[a], keys[b], channel_ab.name, channel_ba.name) @@ -1463,7 +1460,7 @@ class TestPeerForwarding(TestPeer): # create workers and peers for a, definition in graph_definition.items(): channels_of_node = [c for k, c in channels.items() if k[0] == a] - workers[a] = MockLNWallet(local_keypair=keys[a], chans=channels_of_node, tx_queue=txs_queues[a], name=a) + workers[a] = MockLNWallet(local_keypair=keys[a], chans=channels_of_node, tx_queue=txs_queues[a], name=a, has_anchors=self.TEST_ANCHOR_CHANNELS) self._lnworkers_created.extend(list(workers.values())) # create peers @@ -1500,6 +1497,9 @@ class TestPeerForwarding(TestPeer): print(f" {keys[a].pubkey.hex()}") return graph + async def test_payment_multihop(self): + graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) + async def test_payment_multihop(self): graph = self.prepare_chans_and_peers_in_graph(self.GRAPH_DEFINITIONS['square_graph']) peers = graph.peers.values() @@ -1989,3 +1989,13 @@ class TestPeerForwarding(TestPeer): with self.assertRaises(PaymentDone): graph = self.create_square_graph(direct=False, is_legacy=False) await self._run_trampoline_payment(graph) + +class TestPeerDirectAnchors(TestPeerDirect): + TEST_ANCHOR_CHANNELS = True + +class TestPeerForwardingAnchors(TestPeerForwarding): + TEST_ANCHOR_CHANNELS = True + + +def run(coro): + return asyncio.run_coroutine_threadsafe(coro, loop=util.get_asyncio_loop()).result() diff --git a/tests/test_lnutil.py b/tests/test_lnutil.py index 719892f2a..3d03b9b52 100644 --- a/tests/test_lnutil.py +++ b/tests/test_lnutil.py @@ -797,11 +797,21 @@ class TestLNUtil(ElectrumTestCase): ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220' self.assertEqual(str(our_commit_tx), ref_commit_tx_str) - @unittest.skip("only valid for original anchor ouputs, " - "but invalid due to different fee estimation " - "with anchors-zero-fee-htlcs") @disable_ecdsa_r_value_grinding def test_commitment_tx_anchors_test_vectors(self): + # this test is only valid for the original anchor output test vectors (not anchors-zero-fee-htlcs), + # therefore we patch the effective htlc tx weight to result in a finite weight + from electrum import lnutil + effective_htlc_tx_weight_original = lnutil.effective_htlc_tx_weight + def effective_htlc_tx_weight_patched(success: bool, has_anchors: bool): + return lnutil.HTLC_SUCCESS_WEIGHT_ANCHORS if success else lnutil.HTLC_TIMEOUT_WEIGHT_ANCHORS + lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_patched + try: + self._test_commitment_tx_anchors_test_vectors() + finally: + lnutil.effective_htlc_tx_weight = effective_htlc_tx_weight_original + + def _test_commitment_tx_anchors_test_vectors(self): for test_vector in ANCHOR_TEST_VECTORS: with self.subTest(test_vector['Name']): to_local_msat = test_vector['LocalBalance']