Browse Source

lightning channel reserves

dependabot/pip/contrib/deterministic-build/ecdsa-0.13.3
Janus 6 years ago
committed by ThomasV
parent
commit
15b0720f5e
  1. 4
      electrum/lnbase.py
  2. 69
      electrum/lnchan.py
  3. 2
      electrum/lnutil.py
  4. 134
      electrum/tests/test_lnchan.py

4
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)

69
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)

2
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?

134
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)

Loading…
Cancel
Save