Browse Source

ln: htlc state machine (not used yet)

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 7 years ago
committed by ThomasV
parent
commit
497706afbf
  1. 8
      lib/lnbase.py
  2. 340
      lib/lnhtlc.py
  3. 237
      lib/tests/test_lnhtlc.py

8
lib/lnbase.py

@ -40,6 +40,10 @@ from .lightning_payencode.lnaddr import lndecode
from collections import namedtuple, defaultdict
def channel_id_from_funding_tx(funding_txid, funding_index):
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
return int.from_bytes(funding_txid_bytes, 'big') ^ funding_index, funding_txid_bytes
class LightningError(Exception):
pass
@ -566,7 +570,6 @@ def is_synced(network):
synced = server_height != 0 and network.is_up_to_date() and local_height >= server_height
return synced
class Peer(PrintError):
def __init__(self, host, port, pubkey, privkey, network, channel_db, path_finder, channel_state, channels, invoices, request_initial_sync=False):
@ -878,8 +881,7 @@ class Peer(PrintError):
funding_txid, funding_index, funding_sat,
remote_amount, local_amount, remote_config.dust_limit_sat, local_feerate, False, htlcs=[])
sig_64 = sign_and_get_sig_string(remote_ctx, local_config, remote_config)
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
channel_id = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_index)
self.send_message(gen_msg("funding_created",
temporary_channel_id=temp_channel_id,
funding_txid=funding_txid_bytes,

340
lib/lnhtlc.py

@ -0,0 +1,340 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
from .lnbase import *
SettleHtlc = namedtuple("SettleHtlc", ["htlc_id"])
RevokeAndAck = namedtuple("RevokeAndAck", ["height", "per_commitment_secret", "next_per_commitment_point"])
class UpdateAddHtlc:
def __init__(self, amount_msat, payment_hash, cltv_expiry, final_cltv_expiry_with_deltas):
self.amount_msat = amount_msat
self.payment_hash = payment_hash
self.cltv_expiry = cltv_expiry
# the height the htlc was locked in at, or None
self.locked_in = None
# this field is not in update_add_htlc but we need to to make the right htlcs
self.final_cltv_expiry_with_deltas = final_cltv_expiry_with_deltas
self.htlc_id = None
def as_tuple(self):
return (self.htlc_id, self.amount_msat, self.payment_hash, self.cltv_expiry, self.locked_in, self.final_cltv_expiry_with_deltas)
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())
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: OpenChannel, name = None):
self.state = state
self.local_update_log = []
self.remote_update_log = []
self.name = name
self.current_height = 0
self.total_msat_sent = 0
self.total_msat_received = 0
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 = len(self.local_update_log)-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.
"""
assert type(htlc) is UpdateAddHtlc
self.remote_update_log.append(htlc)
self.print_error("receive_htlc")
htlc_id = len(self.remote_update_log)-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.remote_update_log:
if not type(htlc) is UpdateAddHtlc: continue
if htlc.locked_in is None: htlc.locked_in = self.current_height
self.print_error("sign_next_commitment")
sig_64 = sign_and_get_sig_string(self.remote_commitment, self.state.local_config, self.state.remote_config)
their_remote_htlc_privkey_number = derive_privkey(
int.from_bytes(self.state.local_config.htlc_basepoint.privkey, 'big'),
self.state.remote_state.next_per_commitment_point)
their_remote_htlc_privkey = their_remote_htlc_privkey_number.to_bytes(32, 'big')
for_us = False
htlcs = self.htlcs_in_remote # TODO also htlcs_in_local
assert len(htlcs) <= 1
htlcsigs = []
for htlc in htlcs:
original_htlc_output_index = 0
we_receive = True # when we do htlcs_in_local, we need to flip this flag
htlc_tx = make_htlc_tx_with_open_channel(self.state, self.state.remote_state.next_per_commitment_point, for_us, we_receive, htlc.amount_msat, htlc.final_cltv_expiry_with_deltas, htlc.payment_hash, self.remote_commitment, original_htlc_output_index)
sig = bfh(htlc_tx.sign_txin(0, their_remote_htlc_privkey))
r, s = sigdecode_der(sig[:-1], SECP256k1.generator.order())
htlc_sig = sigencode_string_canonize(r, s, SECP256k1.generator.order())
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")
# TODO
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.current_height += 1
self.print_error("revoke_current_commitment")
chan = self.state
last_small_num = chan.local_state.ctn
next_small_num = last_small_num + 2
this_small_num = last_small_num + 1
last_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-last_small_num-1)
this_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-this_small_num-1)
this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big'))
next_secret = get_per_commitment_secret_from_seed(chan.local_state.per_commitment_secret_seed, 2**48-next_small_num-1)
next_point = secret_to_pubkey(int.from_bytes(next_secret, 'big'))
self.state = chan._replace(
local_state=chan.local_state._replace(
ctn=chan.local_state.ctn + 1
)
)
return RevokeAndAck(self.current_height, last_secret, next_point), "current htlcs"
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)
if revocation.height is not None:
adds2 = list(x for x in self.htlcs_in_remote if x.locked_in == revocation.height)
class FwdPkg:
adds = adds2
settle_fails = settle_fails2
for x in settle_fails2:
self.total_msat_sent += self.lookup_htlc(self.local_update_log, x.htlc_id).amount_msat
# increase received_msat counter for htlc's that have been settled
adds2 = self.gen_htlc_indices(self.remote_update_log)
for x in adds2:
if SettleHtlc(x) in self.local_update_log:
self.total_msat_received += self.lookup_htlc(self.remote_update_log, x).amount_msat
# 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]
assert len(to_remove) <= 1, to_remove
if len(to_remove) == 1:
self.local_update_log = [x for x in self.local_update_log if x.htlc_id != to_remove[0].htlc_id]
self.remote_update_log = [x for x in self.remote_update_log if x.htlc_id != to_remove[0].htlc_id]
self.state.remote_state.revocation_store.add_next_entry(revocation.per_commitment_secret)
next_point = self.state.remote_state.next_per_commitment_point
self.state = self.state._replace(
remote_state=self.state.remote_state._replace(
ctn=self.state.remote_state.ctn + 1,
last_per_commitment_point=next_point,
next_per_commitment_point=revocation.next_per_commitment_point,
)
)
if revocation.height is not None:
return FwdPkg
else:
return None
@staticmethod
def htlcsum(htlcs):
return sum(x.amount_msat for x in htlcs)
@property
def remote_commitment(self):
local_msat = self.state.local_state.amount_msat -\
self.htlcsum(self.htlcs_in_local)
remote_msat = self.state.remote_state.amount_msat -\
self.htlcsum(self.htlcs_in_remote)
assert local_msat > 0
assert remote_msat > 0
this_point = self.state.remote_state.next_per_commitment_point
remote_htlc_pubkey = derive_pubkey(self.state.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
local_revocation_pubkey = derive_blinded_pubkey(self.state.local_config.revocation_basepoint.pubkey, this_point)
remote_revocation_pubkey = derive_blinded_pubkey(self.state.remote_config.revocation_basepoint.pubkey, this_point)
htlcs_in_local = []
for htlc in self.htlcs_in_local:
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))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
htlcs_in_remote.append(
( make_offered_htlc(remote_revocation_pubkey, remote_htlc_pubkey, local_htlc_pubkey, htlc.payment_hash), htlc.amount_msat))
commit = make_commitment_using_open_channel(self.state, self.state.remote_state.ctn + 1,
True, this_point,
remote_msat, local_msat, htlcs_in_local + htlcs_in_remote)
assert len(commit.outputs()) == 2 + len(htlcs_in_local) + len(htlcs_in_remote)
return commit
@property
def local_commitment(self):
local_msat = self.state.local_state.amount_msat -\
self.htlcsum(self.htlcs_in_local)
remote_msat = self.state.remote_state.amount_msat -\
self.htlcsum(self.htlcs_in_remote)
assert local_msat > 0
assert remote_msat > 0
this_small_num = self.state.local_state.ctn + 1
this_secret = get_per_commitment_secret_from_seed(self.state.local_state.per_commitment_secret_seed, 2**48-this_small_num-1)
this_point = secret_to_pubkey(int.from_bytes(this_secret, 'big'))
remote_htlc_pubkey = derive_pubkey(self.state.remote_config.htlc_basepoint.pubkey, this_point)
local_htlc_pubkey = derive_pubkey(self.state.local_config.htlc_basepoint.pubkey, this_point)
local_revocation_pubkey = derive_blinded_pubkey(self.state.local_config.revocation_basepoint.pubkey, this_point)
remote_revocation_pubkey = derive_blinded_pubkey(self.state.remote_config.revocation_basepoint.pubkey, this_point)
htlcs_in_local = []
for htlc in self.htlcs_in_local:
htlcs_in_local.append(
( make_offered_htlc(local_revocation_pubkey, local_htlc_pubkey, remote_htlc_pubkey, htlc.payment_hash), htlc.amount_msat))
htlcs_in_remote = []
for htlc in self.htlcs_in_remote:
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))
commit = make_commitment_using_open_channel(self.state, self.state.local_state.ctn + 1,
True, this_point,
local_msat, remote_msat, htlcs_in_local + htlcs_in_remote)
assert len(commit.outputs()) == 2 + len(htlcs_in_local) + len(htlcs_in_remote)
return commit
def gen_htlc_indices(self, update_log):
for num, htlc in enumerate(update_log):
if type(htlc) is not UpdateAddHtlc:
continue
if htlc.locked_in is None or htlc.locked_in < self.current_height:
continue
yield num
@property
def htlcs_in_local(self):
return [self.local_update_log[x] for x in self.gen_htlc_indices(self.local_update_log)]
@property
def htlcs_in_remote(self):
return [self.remote_update_log[x] for x in self.gen_htlc_indices(self.remote_update_log)]
def settle_htlc(self, preimage, htlc_id, source_ref, dest_ref, close_key):
"""
SettleHTLC attempts to settle an existing outstanding received 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):
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))

237
lib/tests/test_lnhtlc.py

@ -0,0 +1,237 @@
# ported from lnd 42de4400bff5105352d0552155f73589166d162b
import unittest
import lib.bitcoin as bitcoin
import lib.lnbase as lnbase
import lib.lnhtlc as lnhtlc
import lib.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):
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=143,
dust_limit_sat=10,
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=143,
dust_limit_sat=10,
max_htlc_value_in_flight_msat=500000 * 1000,
max_accepted_htlcs=5
)
return lnbase.OpenChannel(
channel_id=channel_id,
short_channel_id=channel_id.to_bytes(32, "big")[: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,
last_per_commitment_point=cur,
amount_msat=remote_amount,
revocation_store=their_revocation_store,
next_htlc_id = 0
),
local_state=lnbase.LocalState(
ctn = 0,
per_commitment_secret_seed=seed,
amount_msat=local_amount,
next_htlc_id = 0,
funding_locked_received=True
),
constraints=lnbase.ChannelConstraints(capacity=funding_sat, feerate=local_feerate, 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 * 5
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 = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 1), "big"))
alice_next = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(alice_seed, 2**48 - 2), "big"))
bob_cur = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 1), "big"))
bob_next = lnbase.secret_to_pubkey(int.from_bytes(lnbase.get_per_commitment_secret_from_seed(bob_seed, 2**48 - 2), "big"))
return lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 20000, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33), "alice"), lnhtlc.HTLCStateMachine(
create_channel_state(funding_txid, funding_index, funding_sat, 20000, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33), "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 test_SimpleAddSettleWorkflow(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.
alice_channel, bob_channel = create_test_channels()
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
htlcAmt = one_bitcoin_in_msat
htlc = lnhtlc.UpdateAddHtlc(
payment_hash = paymentHash,
amount_msat = htlcAmt,
cltv_expiry = 5,
final_cltv_expiry_with_deltas = 5
)
# 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.
aliceHtlcIndex = alice_channel.add_htlc(htlc)
bobHtlcIndex = bob_channel.receive_htlc(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()
# 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.
fwdPkg = alice_channel.receive_revocation(bobRevocation)
self.assertEqual(len(fwdPkg.adds), 0, "alice forwards %s add htlcs, should forward none"% len(fwdPkg.adds))
self.assertEqual(len(fwdPkg.settle_fails), 0, "alice forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
# 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.
fwdPkg = bob_channel.receive_revocation(aliceRevocation)
self.assertEqual(len(fwdPkg.adds), 1, "bob forwards %s add htlcs, should only forward one"% len(fwdPkg.adds))
self.assertEqual(len(fwdPkg.settle_fails), 0, "bob forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
# 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: %s vs %s"% (alice_channel.total_msat_sent, aliceSent))
self.assertEqual(alice_channel.total_msat_received, bobSent, "alice has incorrect milli-satoshis received %s vs %s"% (alice_channel.total_msat_received, bobSent))
self.assertEqual(bob_channel.total_msat_sent, bobSent, "bob has incorrect milli-satoshis sent %s vs %s"% (bob_channel.total_msat_sent, bobSent))
self.assertEqual(bob_channel.total_msat_received, aliceSent, "bob has incorrect milli-satoshis received %s vs %s"% (bob_channel.total_msat_received, aliceSent))
self.assertEqual(bob_channel.current_height, 1, "bob has incorrect commitment height, %s vs %s"% (bob_channel.current_height, 1))
self.assertEqual(alice_channel.current_height, 1, "alice has incorrect commitment height, %s vs %s"% (alice_channel.current_height, 1))
# 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, htlcAmt // 1000)
self.assertOutputExistsByValue(bob_channel.local_commitment, htlcAmt // 1000)
# Now we'll repeat a similar exchange, this time with Bob settling the
# HTLC once he learns of the preimage.
preimage = paymentPreimage
bob_channel.settle_htlc(preimage, bobHtlcIndex, None, None, None)
alice_channel.receive_htlc_settle(preimage, 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()
fwdPkg = bob_channel.receive_revocation(aliceRevocation2)
self.assertEqual(fwdPkg.adds, [], "bob forwards %s add htlcs, should forward none"% len(fwdPkg.adds))
self.assertEqual(fwdPkg.settle_fails, [], "bob forwards %s settle/fail htlcs, should forward none"% len(fwdPkg.settle_fails))
bob_channel.receive_new_commitment(aliceSig2, aliceHtlcSigs2)
bobRevocation2, _ = bob_channel.revoke_current_commitment()
fwdPkg = alice_channel.receive_revocation(bobRevocation2)
# Alice should now be able to forward the settlement HTLC to
# any down stream peers.
self.assertEqual(fwdPkg.adds, [] , "alice should be forwarding an add HTLC, instead forwarding %s: %s"% (len(fwdPkg.adds), fwdPkg.adds))
self.assertEqual(len(fwdPkg.settle_fails), 1, "alice should be forwarding one settle/fails HTLC, instead forwarding: %s"% len(fwdPkg.settle_fails))
# 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.current_height, 2, "bob has incorrect commitment height, %s vs %s"% (bob_channel.current_height, 2))
self.assertEqual(alice_channel.current_height, 2, "alice has incorrect commitment height, %s vs %s"% (alice_channel.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))
Loading…
Cancel
Save