1
0

tests: lnchannel: rewrite create_test_channels to use LNWallet

This commit is contained in:
SomberNight
2025-12-20 16:43:10 +00:00
parent 17f41044d5
commit dfeb9918d8
4 changed files with 135 additions and 122 deletions

View File

@@ -1022,8 +1022,8 @@ class Channel(AbstractChannel):
elif self.is_static_remotekey_enabled(): elif self.is_static_remotekey_enabled():
our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey our_payment_pubkey = self.config[LOCAL].payment_basepoint.pubkey
addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors()) addr = make_commitment_output_to_remote_address(our_payment_pubkey, has_anchors=self.has_anchors())
#if self.lnworker: if self.lnworker:
# assert self.lnworker.wallet.is_mine(addr) # FIXME xxxxx chan should be deterministic. NEEDS to be fixed before merge assert self.lnworker.wallet.is_mine(addr)
return addr return addr
def has_anchors(self) -> bool: def has_anchors(self) -> bool:

View File

@@ -1611,8 +1611,10 @@ class LNWallet(Logger):
channel_type: ChannelType, channel_type: ChannelType,
multisig_funding_keypair: Optional[Keypair], # if None, will get derived from channel_seed multisig_funding_keypair: Optional[Keypair], # if None, will get derived from channel_seed
peer_features: LnFeatures, peer_features: LnFeatures,
channel_seed: bytes = None,
) -> LocalConfig: ) -> LocalConfig:
channel_seed = os.urandom(32) if channel_seed is None:
channel_seed = os.urandom(32)
initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat initial_msat = funding_sat * 1000 - push_msat if initiator == LOCAL else push_msat
# sending empty bytes as the upfront_shutdown_script will give us the # sending empty bytes as the upfront_shutdown_script will give us the

View File

@@ -37,10 +37,11 @@ from electrum import bitcoin
from electrum import lnpeer from electrum import lnpeer
from electrum import lnchannel from electrum import lnchannel
from electrum import lnutil from electrum import lnutil
from electrum import bip32 as bip32_utils
from electrum.crypto import privkey_to_pubkey from electrum.crypto import privkey_to_pubkey
from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc from electrum.lnutil import (
from electrum.lnutil import effective_htlc_tx_weight SENT, LOCAL, REMOTE, RECEIVED, UpdateAddHtlc, LnFeatures, secret_to_pubkey, ChannelType,
effective_htlc_tx_weight, LocalConfig, RemoteConfig, OnlyPubkeyKeypair,
)
from electrum.logging import console_stderr_handler from electrum.logging import console_stderr_handler
from electrum.lnchannel import ChannelState, Channel from electrum.lnchannel import ChannelState, Channel
from electrum.json_db import StoredDict from electrum.json_db import StoredDict
@@ -55,63 +56,53 @@ if TYPE_CHECKING:
one_bitcoin_in_msat = bitcoin.COIN * 1000 one_bitcoin_in_msat = bitcoin.COIN * 1000
def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator, def _convert_to_rconfig_from_lconfig(lconfig: LocalConfig) -> RemoteConfig:
local_amount, remote_amount, privkeys, other_pubkeys, """converts Alice's local config to Bob's remote config (neutering private keys, etc)"""
seed, cur, nex, other_node_id, l_dust, r_dust, l_csv, ctn = 0
r_csv, anchor_outputs, local_max_inflight, remote_max_inflight, pcp_secret = lnutil.get_per_commitment_secret_from_seed(
max_accepted_htlcs): lconfig.per_commitment_secret_seed,
#assert local_amount > 0 lnutil.RevocationStore.START_INDEX - ctn)
#assert remote_amount > 0 pcp_point = secret_to_pubkey(int.from_bytes(pcp_secret, 'big'))
rconfig = RemoteConfig(
payment_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.payment_basepoint.pubkey),
multisig_key=OnlyPubkeyKeypair(pubkey=lconfig.multisig_key.pubkey),
htlc_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.htlc_basepoint.pubkey),
delayed_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.delayed_basepoint.pubkey),
revocation_basepoint=OnlyPubkeyKeypair(pubkey=lconfig.revocation_basepoint.pubkey),
to_self_delay=lconfig.to_self_delay,
dust_limit_sat=lconfig.dust_limit_sat,
max_htlc_value_in_flight_msat=lconfig.max_htlc_value_in_flight_msat,
max_accepted_htlcs=lconfig.max_accepted_htlcs,
initial_msat=lconfig.initial_msat,
reserve_sat=lconfig.reserve_sat,
htlc_minimum_msat=lconfig.htlc_minimum_msat,
upfront_shutdown_script=lconfig.upfront_shutdown_script,
announcement_node_sig=lconfig.announcement_node_sig,
announcement_bitcoin_sig=lconfig.announcement_bitcoin_sig,
next_per_commitment_point=pcp_point,
current_per_commitment_point=None,
)
return rconfig
def create_channel_state(
*,
funding_txid: str,
funding_index: int,
funding_sat: int,
is_initiator: bool,
other_node_id: bytes,
channel_type: ChannelType,
local_config: LocalConfig,
remote_config: RemoteConfig,
):
channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index) channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index)
channel_type = lnutil.ChannelType.OPTION_STATIC_REMOTEKEY
if anchor_outputs:
channel_type |= lnutil.ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX
state = { state = {
"channel_id":channel_id.hex(), "channel_id":channel_id.hex(),
"short_channel_id":channel_id[:8], "short_channel_id":channel_id[:8],
"funding_outpoint":lnpeer.Outpoint(funding_txid, funding_index), "funding_outpoint":lnpeer.Outpoint(funding_txid, funding_index),
"remote_config":lnpeer.RemoteConfig( "remote_config": remote_config,
payment_basepoint=other_pubkeys[0], "local_config": local_config,
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=remote_max_inflight,
max_accepted_htlcs=max_accepted_htlcs,
initial_msat=remote_amount,
reserve_sat=0,
htlc_minimum_msat=1,
next_per_commitment_point=nex,
current_per_commitment_point=cur,
upfront_shutdown_script=b'',
announcement_node_sig=b'',
announcement_bitcoin_sig=b'',
),
"local_config":lnpeer.LocalConfig(
channel_seed = None,
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=local_max_inflight,
max_accepted_htlcs=max_accepted_htlcs,
initial_msat=local_amount,
reserve_sat=0,
per_commitment_secret_seed=seed,
funding_locked_received=True,
current_commitment_signature=None,
current_htlc_signatures=None,
htlc_minimum_msat=1,
upfront_shutdown_script=b'',
announcement_node_sig=b'',
announcement_bitcoin_sig=b'',
),
"constraints":lnpeer.ChannelConstraints( "constraints":lnpeer.ChannelConstraints(
flags=0, flags=0,
capacity=funding_sat, capacity=funding_sat,
@@ -130,15 +121,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
return StoredDict(state, None) return StoredDict(state, None)
@lru_cache()
def bip32(sequence):
node = bip32_utils.BIP32Node.from_rootseed(b"9dk", xtype='standard').subkey_at_private_derivation(sequence)
k = node.eckey.get_secret_bytes()
assert len(k) == 32
assert type(k) is bytes
return k
def create_test_channels( def create_test_channels(
*, *,
alice_lnwallet: 'MockLNWallet' = None, alice_lnwallet: 'MockLNWallet' = None,
@@ -147,7 +129,7 @@ def create_test_channels(
local_msat=None, local_msat=None,
remote_msat=None, remote_msat=None,
random_seed=None, random_seed=None,
anchor_outputs=False, anchor_outputs: bool = False,
local_max_inflight=None, local_max_inflight=None,
remote_max_inflight=None, remote_max_inflight=None,
max_accepted_htlcs=5, max_accepted_htlcs=5,
@@ -155,50 +137,75 @@ def create_test_channels(
if random_seed is None: # needed for deterministic randomness if random_seed is None: # needed for deterministic randomness
random_seed = os.urandom(32) random_seed = os.urandom(32)
random_gen = PRNG(random_seed) random_gen = PRNG(random_seed)
if alice_lnwallet or bob_lnwallet: if alice_lnwallet is None:
assert alice_lnwallet and bob_lnwallet, "either both or neither lnwallet must be set" from .test_lnpeer import create_mock_lnwallet
alice_name = alice_lnwallet.name alice_lnwallet = create_mock_lnwallet(name="alice", has_anchors=anchor_outputs)
bob_name = bob_lnwallet.name if bob_lnwallet is None:
alice_pubkey = alice_lnwallet.node_keypair.pubkey from .test_lnpeer import create_mock_lnwallet
bob_pubkey = bob_lnwallet.node_keypair.pubkey bob_lnwallet = create_mock_lnwallet(name="bob", has_anchors=anchor_outputs)
else: alice_name = alice_lnwallet.name
alice_name = "alice" bob_name = bob_lnwallet.name
bob_name = "bob", alice_pubkey = alice_lnwallet.node_keypair.pubkey
alice_pubkey = b"\x01" * 33 bob_pubkey = bob_lnwallet.node_keypair.pubkey
bob_pubkey = b"\x02" * 33 funding_txid = random_gen.get_bytes(32).hex()
funding_txid = binascii.hexlify(random_gen.get_bytes(32)).decode("ascii")
funding_index = 0 funding_index = 0
funding_sat = ((local_msat + remote_msat) // 1000) if local_msat is not None and remote_msat is not None else (bitcoin.COIN * 10) funding_sat = ((local_msat + remote_msat) // 1000) if local_msat is not None and remote_msat is not None else (bitcoin.COIN * 10)
local_amount = local_msat if local_msat is not None else (funding_sat * 1000 // 2) local_msat = local_msat if local_msat is not None else (funding_sat * 1000 // 2)
remote_amount = remote_msat if remote_msat is not None else (funding_sat * 1000 // 2) remote_msat = remote_msat if remote_msat is not None else (funding_sat * 1000 // 2)
local_max_inflight = funding_sat * 1000 if local_max_inflight is None else local_max_inflight local_max_inflight = funding_sat * 1000 if local_max_inflight is None else local_max_inflight
remote_max_inflight = funding_sat * 1000 if remote_max_inflight is None else remote_max_inflight remote_max_inflight = funding_sat * 1000 if remote_max_inflight is None else remote_max_inflight
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 = [lnutil.Keypair(privkey_to_pubkey(x), x) for x in alice_raw] # TODO make it depend on alice_lnwallet
bob_privkeys = [lnutil.Keypair(privkey_to_pubkey(x), x) for x in bob_raw]
alice_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in alice_privkeys]
bob_pubkeys = [lnutil.OnlyPubkeyKeypair(x.pubkey) for x in bob_privkeys]
alice_seed = random_gen.get_bytes(32) # TODO make it depend on alice_lnwallet for config in [alice_lnwallet.config, bob_lnwallet.config]:
bob_seed = random_gen.get_bytes(32) config.LIGHTNING_MAX_FUNDING_SAT = max(config.LIGHTNING_MAX_FUNDING_SAT, funding_sat)
alice_first = lnutil.secret_to_pubkey( peer_features = alice_lnwallet.features | LnFeatures.OPTION_SUPPORT_LARGE_CHANNEL_OPT
int.from_bytes(lnutil.get_per_commitment_secret_from_seed( channel_type = ChannelType.OPTION_STATIC_REMOTEKEY
alice_seed, lnutil.RevocationStore.START_INDEX), "big")) if anchor_outputs:
bob_first = lnutil.secret_to_pubkey( channel_type |= ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX
int.from_bytes(lnutil.get_per_commitment_secret_from_seed( # create alice's local config
bob_seed, lnutil.RevocationStore.START_INDEX), "big")) alice_lconfig = alice_lnwallet.make_local_config_for_new_channel(
funding_sat=funding_sat,
push_msat=remote_msat,
initiator=LOCAL,
channel_type=channel_type,
multisig_funding_keypair=None,
peer_features=peer_features,
channel_seed=random_gen.get_bytes(32),
)
alice_lconfig.funding_locked_received = True
alice_lconfig.dust_limit_sat = 200
alice_lconfig.to_self_delay = 5
alice_lconfig.reserve_sat = 0
alice_lconfig.max_accepted_htlcs = max_accepted_htlcs
alice_lconfig.max_htlc_value_in_flight_msat = local_max_inflight
# create bob's local config
bob_lconfig = bob_lnwallet.make_local_config_for_new_channel(
funding_sat=funding_sat,
push_msat=remote_msat,
initiator=REMOTE,
channel_type=channel_type,
multisig_funding_keypair=None,
peer_features=peer_features,
channel_seed=random_gen.get_bytes(32),
)
bob_lconfig.funding_locked_received = True
bob_lconfig.dust_limit_sat = 1300
bob_lconfig.to_self_delay = 4
bob_lconfig.reserve_sat = 0
bob_lconfig.max_accepted_htlcs = max_accepted_htlcs
bob_lconfig.max_htlc_value_in_flight_msat = remote_max_inflight
alice, bob = ( alice, bob = (
lnchannel.Channel( lnchannel.Channel(
create_channel_state( create_channel_state(
funding_txid, funding_index, funding_sat, True, local_amount, funding_txid=funding_txid,
remote_amount, alice_privkeys, bob_pubkeys, alice_seed, None, funding_index=funding_index,
bob_first, other_node_id=bob_pubkey, l_dust=200, r_dust=1300, funding_sat=funding_sat,
l_csv=5, r_csv=4, anchor_outputs=anchor_outputs, is_initiator=True,
local_max_inflight=local_max_inflight, remote_max_inflight=remote_max_inflight, other_node_id=bob_pubkey,
max_accepted_htlcs=max_accepted_htlcs, channel_type=channel_type,
local_config=alice_lconfig,
remote_config=_convert_to_rconfig_from_lconfig(bob_lconfig),
), ),
name=f"{alice_name}->{bob_name}", name=f"{alice_name}->{bob_name}",
initial_feerate=feerate, initial_feerate=feerate,
@@ -206,12 +213,14 @@ def create_test_channels(
), ),
lnchannel.Channel( lnchannel.Channel(
create_channel_state( create_channel_state(
funding_txid, funding_index, funding_sat, False, remote_amount, funding_txid=funding_txid,
local_amount, bob_privkeys, alice_pubkeys, bob_seed, None, funding_index=funding_index,
alice_first, other_node_id=alice_pubkey, l_dust=1300, r_dust=200, funding_sat=funding_sat,
l_csv=4, r_csv=5, anchor_outputs=anchor_outputs, is_initiator=False,
local_max_inflight=remote_max_inflight, remote_max_inflight=local_max_inflight, other_node_id=alice_pubkey,
max_accepted_htlcs=max_accepted_htlcs, channel_type=channel_type,
local_config=bob_lconfig,
remote_config=_convert_to_rconfig_from_lconfig(alice_lconfig),
), ),
name=f"{bob_name}->{alice_name}", name=f"{bob_name}->{alice_name}",
initial_feerate=feerate, initial_feerate=feerate,
@@ -235,11 +244,13 @@ def create_test_channels(
assert len(a_htlc_sigs) == 0 assert len(a_htlc_sigs) == 0
assert len(b_htlc_sigs) == 0 assert len(b_htlc_sigs) == 0
alice.open_with_first_pcp(bob_first, sig_from_bob) alice.open_with_first_pcp(alice.config[REMOTE].next_per_commitment_point, sig_from_bob)
bob.open_with_first_pcp(alice_first, sig_from_alice) bob.open_with_first_pcp(bob.config[REMOTE].next_per_commitment_point, sig_from_alice)
alice_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) alice_second = lnutil.secret_to_pubkey(int.from_bytes(
bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big")) lnutil.get_per_commitment_secret_from_seed(alice.config[LOCAL].per_commitment_secret_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
bob_second = lnutil.secret_to_pubkey(int.from_bytes(
lnutil.get_per_commitment_secret_from_seed(bob.config[LOCAL].per_commitment_secret_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
# from funding_locked: # from funding_locked:
alice.config[REMOTE].next_per_commitment_point = bob_second alice.config[REMOTE].next_per_commitment_point = bob_second
@@ -256,13 +267,13 @@ class TestFee(ElectrumTestCase):
test test
https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2 https://github.com/lightningnetwork/lightning-rfc/blob/e0c436bd7a3ed6a028e1cb472908224658a14eca/03-transactions.md#requirements-2
""" """
def test_fee(self): async def test_fee(self):
alice_channel, bob_channel = create_test_channels( alice_channel, bob_channel = create_test_channels(
feerate=253, feerate=253,
local_msat=10000000000, local_msat=10_000_000_000,
remote_msat=5000000000, remote_msat=5_000_000_000,
anchor_outputs=self.TEST_ANCHOR_CHANNELS) anchor_outputs=self.TEST_ANCHOR_CHANNELS)
expected_value = 9999056 if self.TEST_ANCHOR_CHANNELS else 9999817 expected_value = 9_999_056 if self.TEST_ANCHOR_CHANNELS else 9_999_817
self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()]) self.assertIn(expected_value, [x.value for x in alice_channel.get_latest_commitment(LOCAL).outputs()])
@@ -284,8 +295,8 @@ class TestChannel(ElectrumTestCase):
super().setUpClass() super().setUpClass()
console_stderr_handler.setLevel(logging.DEBUG) console_stderr_handler.setLevel(logging.DEBUG)
def setUp(self): async def asyncSetUp(self):
super().setUp() await super().asyncSetUp()
# Create a test channel which will be used for the duration of this # 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, # unittest. The channel will be funded evenly with Alice having 5 BTC,
# and Bob having 5 BTC. # and Bob having 5 BTC.
@@ -341,7 +352,7 @@ class TestChannel(ElectrumTestCase):
self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE)) self.assertNumberNonAnchorOutputs(2, self.alice_channel.get_latest_commitment(REMOTE))
self.assertNumberNonAnchorOutputs(4, self.alice_channel.get_next_commitment(REMOTE)) self.assertNumberNonAnchorOutputs(4, self.alice_channel.get_next_commitment(REMOTE))
def test_SimpleAddSettleWorkflow(self): async def test_SimpleAddSettleWorkflow(self):
alice_channel, bob_channel = self.alice_channel, self.bob_channel alice_channel, bob_channel = self.alice_channel, self.bob_channel
htlc = self.htlc htlc = self.htlc
@@ -774,7 +785,7 @@ class TestChannelAnchors(TestChannel):
class TestAvailableToSpend(ElectrumTestCase): class TestAvailableToSpend(ElectrumTestCase):
def test_DesyncHTLCs(self): async def test_DesyncHTLCs(self):
alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_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(499986152000 if not alice_channel.has_anchors() else 499980692000, alice_channel.available_to_spend(LOCAL))
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
@@ -820,7 +831,7 @@ class TestAvailableToSpend(ElectrumTestCase):
self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL)) self.assertEqual(500000000000, bob_channel.available_to_spend(LOCAL))
alice_channel.add_htlc(htlc) alice_channel.add_htlc(htlc)
def test_single_payment(self): async def test_single_payment(self):
alice_channel, bob_channel = create_test_channels( alice_channel, bob_channel = create_test_channels(
anchor_outputs=self.TEST_ANCHOR_CHANNELS, anchor_outputs=self.TEST_ANCHOR_CHANNELS,
local_msat=4000000000, local_msat=4000000000,
@@ -1016,7 +1027,7 @@ class TestChanReserveAnchors(TestChanReserve):
class TestDust(ElectrumTestCase): class TestDust(ElectrumTestCase):
def test_DustLimit(self): async def test_DustLimit(self):
"""Test that addition of an HTLC below the dust limit changes the balances.""" """Test that addition of an HTLC below the dust limit changes the balances."""
alice_channel, bob_channel = create_test_channels(anchor_outputs=self.TEST_ANCHOR_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_alice = alice_channel.config[LOCAL].dust_limit_sat

View File

@@ -138,7 +138,7 @@ class MockStandardWallet(Standard_Wallet):
return passphrase # lol, super secure name return passphrase # lol, super secure name
def create_mock_lnwallet(*, name, has_anchors) -> 'MockLNWallet': def create_mock_lnwallet(*, name, has_anchors) -> 'MockLNWallet':
_user_dir = tempfile.mkdtemp(prefix="electrum-lnpeer-test-") _user_dir = tempfile.mkdtemp(prefix="electrum-lnpeer-test-") # TODO clean-up after??
config = SimpleConfig({}, read_user_dir_function=lambda: _user_dir) config = SimpleConfig({}, read_user_dir_function=lambda: _user_dir)
config.ENABLE_ANCHOR_CHANNELS = has_anchors config.ENABLE_ANCHOR_CHANNELS = has_anchors
config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0 config.INITIAL_TRAMPOLINE_FEE_LEVEL = 0