electrum

Electrum Bitcoin wallet
git clone https://git.parazyd.org/electrum
Log | Files | Refs | Submodules

commit 2323118bda08dcb62b98c7bf08beba35bf2324a9
parent 37a0315aab8210e18665c1d8719e7b600c6797d4
Author: Janus <ysangkok@gmail.com>
Date:   Tue, 18 Dec 2018 14:35:22 +0100

lnchan: only sign force_close_tx when demanded, assure consistency, fix test

Diffstat:
Melectrum/lnchan.py | 45++++++++++++++++++++++++++++++---------------
Melectrum/tests/test_lnchan.py | 42+++++++++++++++++++++++++++++++++++++++---
2 files changed, 69 insertions(+), 18 deletions(-)

diff --git a/electrum/lnchan.py b/electrum/lnchan.py @@ -147,7 +147,7 @@ class Channel(PrintError): except: return super().diagnostic_name() - def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None): + def __init__(self, state, sweep_address = None, name = None, payment_completed : Optional[Callable[[HTLCOwner, UpdateAddHtlc, bytes], None]] = None, local_commitment = None): self.preimages = {} if not payment_completed: payment_completed = lambda this, x, y, z: None @@ -205,7 +205,12 @@ class Channel(PrintError): for sub in (LOCAL, REMOTE): self.log[sub].locked_in.update(self.log[sub].adds.keys()) - self.set_local_commitment(self.current_commitment(LOCAL)) + if local_commitment: + local_commitment = Transaction(str(local_commitment)) + local_commitment.deserialize(True) + self.set_local_commitment(local_commitment) + else: + self.set_local_commitment(self.current_commitment(LOCAL)) self.set_remote_commitment(self.current_commitment(REMOTE)) def set_local_commitment(self, ctx): @@ -213,6 +218,8 @@ class Channel(PrintError): if self.sweep_address is not None: self.local_sweeptxs = create_sweeptxs_for_our_latest_ctx(self, self.local_commitment, self.sweep_address) + self.assert_signature_fits(ctx) + def set_remote_commitment(self, ctx): self.remote_commitment = ctx if self.sweep_address is not None: @@ -449,7 +456,9 @@ class Channel(PrintError): feerate=new_feerate ) - self.set_local_commitment(self.pending_commitment(LOCAL)) + # since we should not revoke our latest commitment tx, + # we do not update self.local_commitment here, + # it should instead be updated when we receive a new sig return RevokeAndAck(last_secret, next_point), "current htlcs" @@ -541,7 +550,6 @@ class Channel(PrintError): if self.constraints.is_initiator: self.pending_fee[FUNDEE_ACKED] = True - self.set_local_commitment(self.pending_commitment(LOCAL)) self.set_remote_commitment(self.pending_commitment(REMOTE)) self.remote_commitment_to_be_revoked = prev_remote_commitment return received_this_batch, sent_this_batch @@ -773,7 +781,7 @@ class Channel(PrintError): serialized_channel[k] = v dumped = ChannelJsonEncoder().encode(serialized_channel) roundtripped = json.loads(dumped) - reconstructed = Channel(roundtripped) + reconstructed = Channel(roundtripped, local_commitment=self.local_commitment) to_save_new = reconstructed.to_save() if to_save_new != to_save_ref: from pprint import PrettyPrinter @@ -864,19 +872,26 @@ class Channel(PrintError): sig = ecc.sig_string_from_der_sig(der_sig[:-1]) return sig, closing_tx + def assert_signature_fits(self, tx): + remote_sig = self.config[LOCAL].current_commitment_signature + if remote_sig: # only None in test + preimage_hex = tx.serialize_preimage(0) + pre_hash = sha256d(bfh(preimage_hex)) + assert ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash) + def force_close_tx(self): - tx = self.current_commitment(LOCAL) + tx = self.local_commitment + tx = Transaction(str(tx)) + tx.deserialize(True) + self.assert_signature_fits(tx) tx.sign({bh2u(self.config[LOCAL].multisig_key.pubkey): (self.config[LOCAL].multisig_key.privkey, True)}) remote_sig = self.config[LOCAL].current_commitment_signature - - preimage_hex = tx.serialize_preimage(0) - pre_hash = sha256d(bfh(preimage_hex)) - assert ecc.verify_signature(self.config[REMOTE].multisig_key.pubkey, remote_sig, pre_hash) - - remote_sig = ecc.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() + if remote_sig: # only None in test + remote_sig = ecc.der_sig_from_sig_string(remote_sig) + b"\x01" + sigs = tx._inputs[0]["signatures"] + none_idx = sigs.index(None) + tx.add_signature_to_txin(0, none_idx, bh2u(remote_sig)) + assert tx.is_complete() return tx def included_htlcs_in_their_latest_ctxs(self, htlc_initiator) -> Dict[int, List[UpdateAddHtlc]]: diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py @@ -83,7 +83,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate funding_locked_received=True, was_announced=False, # just a random signature - current_commitment_signature=sig_string_from_der_sig(bytes.fromhex('3046022100c66e112e22b91b96b795a6dd5f4b004f3acccd9a2a31bf104840f256855b7aa3022100e711b868b62d87c7edd95a2370e496b9cb6a38aff13c9f64f9ff2f3b2a0052dd')), + current_commitment_signature=None, current_htlc_signatures=None, ), "constraints":lnbase.ChannelConstraints( @@ -134,6 +134,20 @@ def create_test_channels(feerate=6000, local=None, remote=None): alice.set_state('OPEN') bob.set_state('OPEN') + + #old_bob = bob.config[REMOTE] + #bob.config[REMOTE] = bob.config[REMOTE]._replace(ctn=bob.config[REMOTE].ctn) + #sig_from_bob = bob.sign_next_commitment()[0] + #bob.config[REMOTE] = old_bob + + #old_alice = alice.config[REMOTE] + #alice.config[REMOTE] = alice.config[REMOTE]._replace(ctn=alice.config[REMOTE].ctn) + #sig_from_alice = alice.sign_next_commitment()[0] + #alice.config[REMOTE] = old_alice + + #alice.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob) + #bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice) + return alice, bob class TestFee(unittest.TestCase): @@ -204,6 +218,9 @@ class TestChannel(unittest.TestCase): self.assertEqual({0: [], 1: []}, alice_channel.included_htlcs_in_their_latest_ctxs(REMOTE)) self.assertEqual({0: [], 1: []}, bob_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) + # this wouldn't work since we put None in the remote_sig + # alice_channel.force_close_tx() + # 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 @@ -237,8 +254,6 @@ class TestChannel(unittest.TestCase): # forward since she's sending an outgoing HTLC. alice_channel.receive_revocation(bobRevocation) - alice_channel.force_close_tx() - # test serializing with locked_in htlc self.assertEqual(len(alice_channel.to_save()['local_log']), 1) alice_channel.serialize() @@ -248,9 +263,15 @@ class TestChannel(unittest.TestCase): # the point where she sent her signature, including the HTLC. alice_channel.receive_new_commitment(bobSig, bobHtlcSigs) + tx1 = str(alice_channel.force_close_tx()) + # Alice then generates a revocation for bob. aliceRevocation, _ = alice_channel.revoke_current_commitment() + tx2 = str(alice_channel.force_close_tx()) + # since alice already has the signature for the next one, it doesn't change her force close tx (it was already the newer one) + self.assertEqual(tx1, tx2) + # 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 @@ -284,6 +305,10 @@ class TestChannel(unittest.TestCase): alice_channel.receive_htlc_settle(preimage, self.aliceHtlcIndex) + tx3 = str(alice_channel.force_close_tx()) + # just settling a htlc does not change her force close tx + self.assertEqual(tx2, tx3) + bobSig2, bobHtlcSigs2 = bob_channel.sign_next_commitment() self.assertEqual({1: [htlc], 2: []}, alice_channel.included_htlcs_in_their_latest_ctxs(LOCAL)) @@ -293,6 +318,9 @@ class TestChannel(unittest.TestCase): alice_channel.receive_new_commitment(bobSig2, bobHtlcSigs2) + tx4 = str(alice_channel.force_close_tx()) + self.assertNotEqual(tx3, tx4) + aliceRevocation2, _ = alice_channel.revoke_current_commitment() aliceSig2, aliceHtlcSigs2 = alice_channel.sign_next_commitment() self.assertEqual(aliceHtlcSigs2, [], "alice should generate no htlc signatures") @@ -326,8 +354,16 @@ class TestChannel(unittest.TestCase): alice_channel.update_fee(100000, True) bob_channel.update_fee(100000, False) + + tx5 = str(alice_channel.force_close_tx()) + # sending a fee update does not change her force close tx + self.assertEqual(tx4, tx5) + force_state_transition(alice_channel, bob_channel) + tx6 = str(alice_channel.force_close_tx()) + self.assertNotEqual(tx5, tx6) + self.htlc_dict['amount_msat'] *= 5 bob_index = bob_channel.add_htlc(self.htlc_dict) alice_index = alice_channel.receive_htlc(self.htlc_dict)