commit 15b0720f5e4130286c5639e1f8c7720fb89a86fc
parent 54edc9488a5b774d672a150b470dc4f2fc7a9ce8
Author: Janus <ysangkok@gmail.com>
Date: Tue, 23 Oct 2018 20:32:18 +0200
lightning channel reserves
Diffstat:
4 files changed, 176 insertions(+), 33 deletions(-)
diff --git a/electrum/lnbase.py b/electrum/lnbase.py
@@ -390,6 +390,7 @@ class Peer(PrintError):
ctn=-1,
next_htlc_id=0,
amount_msat=initial_msat,
+ reserve_sat=546,
)
per_commitment_secret_seed = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
return local_config, per_commitment_secret_seed
@@ -451,6 +452,7 @@ class Peer(PrintError):
ctn = -1,
amount_msat=push_msat,
next_htlc_id = 0,
+ reserve_sat = int.from_bytes(payload["channel_reserve_satoshis"], 'big'),
next_per_commitment_point=remote_per_commitment_point,
current_per_commitment_point=None,
@@ -561,6 +563,7 @@ class Peer(PrintError):
ctn = -1,
amount_msat=remote_balance_sat,
next_htlc_id = 0,
+ reserve_sat = int.from_bytes(payload['channel_reserve_satoshis'], 'big'),
next_per_commitment_point=payload['first_per_commitment_point'],
current_per_commitment_point=None,
@@ -941,7 +944,6 @@ class Peer(PrintError):
assert final_cltv <= cltv, (final_cltv, cltv)
secret_key = os.urandom(32)
onion = new_onion_packet([x.node_id for x in route], secret_key, hops_data, associated_data=payment_hash)
- chan.check_can_pay(amount_msat)
# create htlc
htlc = {'amount_msat':amount_msat, 'payment_hash':payment_hash, 'cltv_expiry':cltv}
htlc_id = chan.add_htlc(htlc)
diff --git a/electrum/lnchan.py b/electrum/lnchan.py
@@ -18,7 +18,7 @@ from .lnutil import sign_and_get_sig_string, privkey_to_pubkey, make_htlc_tx_wit
from .lnutil import make_htlc_tx_with_open_channel, make_commitment, make_received_htlc, make_offered_htlc
from .lnutil import HTLC_TIMEOUT_WEIGHT, HTLC_SUCCESS_WEIGHT
from .lnutil import funding_output_script, LOCAL, REMOTE, HTLCOwner, make_closing_tx, make_commitment_outputs
-from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees
+from .lnutil import ScriptHtlc, SENT, RECEIVED, PaymentFailure, calc_onchain_fees, RemoteMisbehaving
from .transaction import Transaction, TxOutput, construct_witness
from .simple_config import SimpleConfig, FEERATE_FALLBACK_STATIC_FEE
@@ -131,6 +131,7 @@ class Channel(PrintError):
template = lambda: {
'adds': {}, # Dict[HTLC_ID, UpdateAddHtlc]
'settles': [], # List[HTLC_ID]
+ 'fails': [], # List[HTLC_ID]
}
self.log = {LOCAL: template(), REMOTE: template()}
for strname, subject in [('remote_log', REMOTE), ('local_log', LOCAL)]:
@@ -159,23 +160,23 @@ class Channel(PrintError):
def get_state(self):
return self._state
- def check_can_pay(self, amount_msat: int) -> None:
- # FIXME channel reserve
+ def _check_can_pay(self, amount_msat: int) -> None:
if self.get_state() != 'OPEN':
raise PaymentFailure('Channel not open')
if self.available_to_spend(LOCAL) < amount_msat:
raise PaymentFailure('Not enough local balance')
if len(self.htlcs(LOCAL, only_pending=True)) + 1 > self.config[REMOTE].max_accepted_htlcs:
raise PaymentFailure('Too many HTLCs already in channel')
- if htlcsum(self.htlcs(LOCAL, only_pending=True)) + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
- raise PaymentFailure('HTLC value sum would exceed max allowed: {} msat'.format(self.config[REMOTE].max_htlc_value_in_flight_msat))
+ current_htlc_sum = htlcsum(self.htlcs(LOCAL, only_pending=True))
+ if current_htlc_sum + amount_msat > self.config[REMOTE].max_htlc_value_in_flight_msat:
+ raise PaymentFailure(f'HTLC value sum (sum of pending htlcs: {current_htlc_sum/1000} sat plus new htlc: {amount_msat/1000} sat) would exceed max allowed: {self.config[REMOTE].max_htlc_value_in_flight_msat/1000} sat')
if amount_msat <= 0: # FIXME htlc_minimum_msat
raise PaymentFailure(f'HTLC value too small: {amount_msat} msat')
def can_pay(self, amount_msat):
try:
- self.check_can_pay(amount_msat)
- except:
+ self._check_can_pay(amount_msat)
+ except PaymentFailure:
return False
return True
@@ -196,6 +197,7 @@ class Channel(PrintError):
should be called when preparing to send an outgoing HTLC.
"""
assert type(htlc) is dict
+ self._check_can_pay(htlc['amount_msat'])
htlc = UpdateAddHtlc(**htlc, htlc_id=self.config[LOCAL].next_htlc_id)
self.log[LOCAL]['adds'][htlc.htlc_id] = htlc
self.print_error("add_htlc")
@@ -210,7 +212,10 @@ class Channel(PrintError):
"""
assert type(htlc) is dict
htlc = UpdateAddHtlc(**htlc, htlc_id = self.config[REMOTE].next_htlc_id)
- self.log[REMOTE]['adds'][htlc.htlc_id] = htlc
+ if self.available_to_spend(REMOTE) < htlc.amount_msat:
+ raise RemoteMisbehaving('Remote dipped below channel reserve')
+ adds = self.log[REMOTE]['adds']
+ adds[htlc.htlc_id] = htlc
self.print_error("receive_htlc")
self.config[REMOTE]=self.config[REMOTE]._replace(next_htlc_id=htlc.htlc_id + 1)
return htlc.htlc_id
@@ -228,10 +233,8 @@ class Channel(PrintError):
any). The HTLC signatures are sorted according to the BIP 69 order of the
HTLC's on the commitment transaction.
"""
- for htlc in self.log[LOCAL]['adds'].values():
- if htlc.locked_in[LOCAL] is None:
- htlc.locked_in[LOCAL] = self.config[LOCAL].ctn
self.print_error("sign_next_commitment")
+ self.lock_in_htlc_changes(LOCAL)
pending_remote_commitment = self.pending_remote_commitment
sig_64 = sign_and_get_sig_string(pending_remote_commitment, self.config[LOCAL], self.config[REMOTE])
@@ -265,6 +268,17 @@ class Channel(PrintError):
return sig_64, htlcsigs
+ def lock_in_htlc_changes(self, subject):
+ for sub in (LOCAL, REMOTE):
+ for htlc_id in self.log[-sub]['fails']:
+ adds = self.log[sub]['adds']
+ htlc = adds.pop(htlc_id)
+ self.log[-sub]['fails'].clear()
+
+ for htlc in self.log[subject]['adds'].values():
+ if htlc.locked_in[subject] is None:
+ htlc.locked_in[subject] = self.config[subject].ctn
+
def receive_new_commitment(self, sig, htlc_sigs):
"""
ReceiveNewCommitment process a signature for a new commitment state sent by
@@ -276,11 +290,8 @@ class Channel(PrintError):
state, then this newly added commitment becomes our current accepted channel
state.
"""
-
self.print_error("receive_new_commitment")
- for htlc in self.log[REMOTE]['adds'].values():
- if htlc.locked_in[REMOTE] is None:
- htlc.locked_in[REMOTE] = self.config[REMOTE].ctn
+ self.lock_in_htlc_changes(REMOTE)
assert len(htlc_sigs) == 0 or type(htlc_sigs[0]) is bytes
pending_local_commitment = self.pending_local_commitment
@@ -479,9 +490,16 @@ class Channel(PrintError):
return initial
def available_to_spend(self, subject):
- # FIXME what about channel_reserve_satoshis? will the remote fail the channel if we go below? test.
- # FIXME what about tx fees
- return self.balance(subject) - htlcsum(self.htlcs(subject, only_pending=True))
+ return self.balance(subject)\
+ - htlcsum(self.log[subject]['adds'].values())\
+ - self.config[subject].reserve_sat * 1000\
+ - calc_onchain_fees(
+ # TODO should we include a potential new htlc, when we are called from receive_htlc?
+ len(list(self.included_htlcs(subject, LOCAL)) + list(self.included_htlcs(subject, REMOTE))),
+ self.pending_feerate(subject),
+ subject == LOCAL,
+ self.constraints.is_initiator,
+ )[subject]
def amounts(self):
remote_settled= htlcsum(self.htlcs(REMOTE, False))
@@ -536,8 +554,11 @@ class Channel(PrintError):
res = []
for htlc in update_log['adds'].values():
locked_in = htlc.locked_in[subject]
-
- if locked_in is None or only_pending == (htlc.htlc_id in other_log['settles']):
+ settled = htlc.htlc_id in other_log['settles']
+ failed = htlc.htlc_id in other_log['fails']
+ if locked_in is None:
+ continue
+ if only_pending == (settled or failed):
continue
res.append(htlc)
return res
@@ -561,11 +582,11 @@ class Channel(PrintError):
def fail_htlc(self, htlc_id):
self.print_error("fail_htlc")
- self.log[REMOTE]['adds'].pop(htlc_id)
+ self.log[LOCAL]['fails'].append(htlc_id)
def receive_fail_htlc(self, htlc_id):
self.print_error("receive_fail_htlc")
- self.log[LOCAL]['adds'].pop(htlc_id)
+ self.log[REMOTE]['fails'].append(htlc_id)
@property
def current_height(self):
@@ -665,8 +686,8 @@ class Channel(PrintError):
def make_commitment(self, subject, this_point) -> Transaction:
remote_msat, local_msat = self.amounts()
- assert local_msat >= 0
- assert remote_msat >= 0
+ assert local_msat >= 0, local_msat
+ assert remote_msat >= 0, remote_msat
this_config = self.config[subject]
other_config = self.config[-subject]
other_htlc_pubkey = derive_pubkey(other_config.htlc_basepoint.pubkey, this_point)
diff --git a/electrum/lnutil.py b/electrum/lnutil.py
@@ -37,6 +37,7 @@ common = [
('max_htlc_value_in_flight_msat' , int),
('max_accepted_htlcs' , int),
('initial_msat' , int),
+ ('reserve_sat', int),
]
ChannelConfig = NamedTuple('ChannelConfig', common)
@@ -65,6 +66,7 @@ class HandshakeFailed(LightningError): pass
class PaymentFailure(LightningError): pass
class ConnStringFormatError(LightningError): pass
class UnknownPaymentHash(LightningError): pass
+class RemoteMisbehaving(LightningError): pass
# TODO make configurable?
diff --git a/electrum/tests/test_lnchan.py b/electrum/tests/test_lnchan.py
@@ -11,6 +11,8 @@ import binascii
from electrum.lnutil import SENT, LOCAL, REMOTE, RECEIVED
+one_bitcoin_in_msat = bitcoin.COIN * 1000
+
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, l_dust, r_dust, l_csv, r_csv):
assert local_amount > 0
assert remote_amount > 0
@@ -29,12 +31,13 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
revocation_basepoint=other_pubkeys[4],
to_self_delay=r_csv,
dust_limit_sat=r_dust,
- max_htlc_value_in_flight_msat=500000 * 1000,
+ max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5,
max_accepted_htlcs=5,
initial_msat=remote_amount,
ctn = 0,
next_htlc_id = 0,
amount_msat=remote_amount,
+ reserve_sat=0,
next_per_commitment_point=nex,
current_per_commitment_point=cur,
@@ -48,12 +51,13 @@ def create_channel_state(funding_txid, funding_index, funding_sat, local_feerate
revocation_basepoint=privkeys[4],
to_self_delay=l_csv,
dust_limit_sat=l_dust,
- max_htlc_value_in_flight_msat=500000 * 1000,
+ max_htlc_value_in_flight_msat=one_bitcoin_in_msat * 5,
max_accepted_htlcs=5,
initial_msat=local_amount,
ctn = 0,
next_htlc_id = 0,
amount_msat=local_amount,
+ reserve_sat=0,
per_commitment_secret_seed=seed,
funding_locked_received=True,
@@ -101,13 +105,15 @@ def create_test_channels(feerate=6000, local=None, remote=None):
bob_cur = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX), "big"))
bob_next = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
- return \
+ alice, bob = \
lnchan.Channel(
create_channel_state(funding_txid, funding_index, funding_sat, feerate, True, local_amount, remote_amount, alice_privkeys, bob_pubkeys, alice_seed, bob_cur, bob_next, b"\x02"*33, l_dust=200, r_dust=1300, l_csv=5, r_csv=4), "alice"), \
lnchan.Channel(
create_channel_state(funding_txid, funding_index, funding_sat, feerate, False, remote_amount, local_amount, bob_privkeys, alice_pubkeys, bob_seed, alice_cur, alice_next, b"\x01"*33, l_dust=1300, r_dust=200, l_csv=4, r_csv=5), "bob")
-one_bitcoin_in_msat = bitcoin.COIN * 1000
+ alice.set_state('OPEN')
+ bob.set_state('OPEN')
+ return alice, bob
class TestFee(unittest.TestCase):
"""
@@ -134,7 +140,7 @@ class TestChannel(unittest.TestCase):
self.paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(self.paymentPreimage)
- self.htlc = {
+ self.htlc_dict = {
'payment_hash' : paymentHash,
'amount_msat' : one_bitcoin_in_msat,
'cltv_expiry' : 5,
@@ -143,9 +149,9 @@ class TestChannel(unittest.TestCase):
# 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.
- self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc)
+ self.aliceHtlcIndex = self.alice_channel.add_htlc(self.htlc_dict)
- self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc)
+ self.bobHtlcIndex = self.bob_channel.receive_htlc(self.htlc_dict)
self.htlc = self.bob_channel.log[lnutil.REMOTE]['adds'][0]
def test_SimpleAddSettleWorkflow(self):
@@ -318,7 +324,120 @@ class TestChannel(unittest.TestCase):
bob_channel.receive_revocation(alice_revocation)
self.assertEqual(fee, bob_channel.constraints.feerate)
+ def test_AddHTLCNegativeBalance(self):
+ self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
+ self.alice_channel.add_htlc(self.htlc_dict)
+ self.htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x03')
+ self.alice_channel.add_htlc(self.htlc_dict)
+ # now there are three htlcs (one was in setUp)
+
+ # Alice now has an available balance of 2 BTC. We'll add a new HTLC of
+ # value 2 BTC, which should make Alice's balance negative (since she
+ # has to pay a commitment fee).
+ new = dict(self.htlc_dict)
+ new['amount_msat'] *= 2
+ new['payment_hash'] = bitcoin.sha256(32 * b'\x04')
+ with self.assertRaises(lnutil.PaymentFailure) as cm:
+ self.alice_channel.add_htlc(new)
+ self.assertEqual(('Not enough local balance',), cm.exception.args)
+
+class TestAvailableToSpend(unittest.TestCase):
+ def test_DesyncHTLCs(self):
+ alice_channel, bob_channel = create_test_channels()
+
+ paymentPreimage = b"\x01" * 32
+ paymentHash = bitcoin.sha256(paymentPreimage)
+ htlc_dict = {
+ 'payment_hash' : paymentHash,
+ 'amount_msat' : int(4.1 * one_bitcoin_in_msat),
+ 'cltv_expiry' : 5,
+ }
+
+ alice_idx = alice_channel.add_htlc(htlc_dict)
+ bob_idx = bob_channel.receive_htlc(htlc_dict)
+ force_state_transition(alice_channel, bob_channel)
+ bob_channel.fail_htlc(bob_idx)
+ alice_channel.receive_fail_htlc(alice_idx)
+ # Alice now has gotten all her original balance (5 BTC) back, however,
+ # adding a new HTLC at this point SHOULD fail, since if she adds the
+ # HTLC and signs the next state, Bob cannot assume she received the
+ # FailHTLC, and must assume she doesn't have the necessary balance
+ # available.
+ # We try adding an HTLC of value 1 BTC, which should fail because the
+ # balance is unavailable.
+ htlc_dict = {
+ 'payment_hash' : paymentHash,
+ 'amount_msat' : one_bitcoin_in_msat,
+ 'cltv_expiry' : 5,
+ }
+ with self.assertRaises(lnutil.PaymentFailure):
+ alice_channel.add_htlc(htlc_dict)
+ # Now do a state transition, which will ACK the FailHTLC, making Alice
+ # able to add the new HTLC.
+ force_state_transition(alice_channel, bob_channel)
+ alice_channel.add_htlc(htlc_dict)
+class TestChanReserve(unittest.TestCase):
+ def setUp(self):
+ alice_channel, bob_channel = create_test_channels()
+ alice_min_reserve = int(.5 * one_bitcoin_in_msat // 1000)
+ alice_channel.config[LOCAL] =\
+ alice_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve)
+ bob_channel.config[REMOTE] =\
+ bob_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve)
+ # We set Bob's channel reserve to a value that is larger than
+ # his current balance in the channel. This will ensure that
+ # after a channel is first opened, Bob can still receive HTLCs
+ # even though his balance is less than his channel reserve.
+ bob_min_reserve = 6 * one_bitcoin_in_msat // 1000
+ bob_channel.config[LOCAL] =\
+ bob_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve)
+ alice_channel.config[REMOTE] =\
+ alice_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve)
+
+ self.bob_min = bob_min_reserve
+ self.alice_min = bob_min_reserve
+ self.alice_channel = alice_channel
+ self.bob_channel = bob_channel
+
+ def test_part1(self):
+ # Add an HTLC that will increase Bob's balance. This should succeed,
+ # since Alice stays above her channel reserve, and Bob increases his
+ # balance (while still being below his channel reserve).
+ #
+ # Resulting balances:
+ # Alice: 4.5
+ # Bob: 5.0
+ paymentPreimage = b"\x01" * 32
+ paymentHash = bitcoin.sha256(paymentPreimage)
+ htlc_dict = {
+ 'payment_hash' : paymentHash,
+ 'amount_msat' : int(.5 * one_bitcoin_in_msat),
+ 'cltv_expiry' : 5,
+ }
+ self.alice_channel.add_htlc(htlc_dict)
+ self.bob_channel.receive_htlc(htlc_dict)
+ # Force a state transition, making sure this HTLC is considered valid
+ # even though the channel reserves are not met.
+ force_state_transition(self.alice_channel, self.bob_channel)
+
+ aliceSelfBalance = self.alice_channel.balance(LOCAL)\
+ - lnchan.htlcsum(self.alice_channel.htlcs(LOCAL, True))
+ bobBalance = self.bob_channel.balance(REMOTE)\
+ - lnchan.htlcsum(self.alice_channel.htlcs(REMOTE, True))
+ self.assertEqual(aliceSelfBalance, one_bitcoin_in_msat*4.5)
+ self.assertEqual(bobBalance, one_bitcoin_in_msat*5)
+ # Now let Bob try to add an HTLC. This should fail, since it will
+ # decrease his balance, which is already below the channel reserve.
+ #
+ # Resulting balances:
+ # Alice: 4.5
+ # Bob: 5.0
+ with self.assertRaises(lnutil.PaymentFailure):
+ htlc_dict['payment_hash'] = bitcoin.sha256(32 * b'\x02')
+ self.bob_channel.add_htlc(htlc_dict)
+ with self.assertRaises(lnutil.RemoteMisbehaving):
+ self.alice_channel.receive_htlc(htlc_dict)
class TestDust(unittest.TestCase):
def test_DustLimit(self):
@@ -339,7 +458,6 @@ class TestDust(unittest.TestCase):
aliceHtlcIndex = alice_channel.add_htlc(htlc)
bobHtlcIndex = bob_channel.receive_htlc(htlc)
force_state_transition(alice_channel, bob_channel)
- self.assertEqual(alice_channel.available_to_spend(LOCAL), alice_channel.balance(LOCAL) - htlc['amount_msat'])
self.assertEqual(len(alice_channel.local_commitment.outputs()), 3)
self.assertEqual(len(bob_channel.local_commitment.outputs()), 2)
default_fee = calc_static_fee(0)