mirror of
https://github.com/LBRYFoundation/LBRY-Vault.git
synced 2025-08-23 17:47:31 +00:00
Use attr.s instead of namedtuples for channel config
This commit is contained in:
parent
9bd633fb0b
commit
757467782a
7 changed files with 117 additions and 116 deletions
|
@ -12,3 +12,4 @@ bitstring
|
||||||
pycryptodomex>=3.7
|
pycryptodomex>=3.7
|
||||||
jsonrpcserver
|
jsonrpcserver
|
||||||
jsonrpcclient
|
jsonrpcclient
|
||||||
|
attrs
|
||||||
|
|
|
@ -29,6 +29,7 @@ import json
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence, TYPE_CHECKING
|
from typing import Optional, Dict, List, Tuple, NamedTuple, Set, Callable, Iterable, Sequence, TYPE_CHECKING
|
||||||
import time
|
import time
|
||||||
|
import threading
|
||||||
|
|
||||||
from . import ecc
|
from . import ecc
|
||||||
from .util import bfh, bh2u
|
from .util import bfh, bh2u
|
||||||
|
@ -99,6 +100,8 @@ class ChannelJsonEncoder(json.JSONEncoder):
|
||||||
return o.serialize()
|
return o.serialize()
|
||||||
if isinstance(o, set):
|
if isinstance(o, set):
|
||||||
return list(o)
|
return list(o)
|
||||||
|
if hasattr(o, 'to_json') and callable(o.to_json):
|
||||||
|
return o.to_json()
|
||||||
return super().default(o)
|
return super().default(o)
|
||||||
|
|
||||||
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
|
RevokeAndAck = namedtuple("RevokeAndAck", ["per_commitment_secret", "next_per_commitment_point"])
|
||||||
|
@ -110,7 +113,7 @@ class RemoteCtnTooFarInFuture(Exception): pass
|
||||||
def decodeAll(d, local):
|
def decodeAll(d, local):
|
||||||
for k, v in d.items():
|
for k, v in d.items():
|
||||||
if k == 'revocation_store':
|
if k == 'revocation_store':
|
||||||
yield (k, RevocationStore.from_json_obj(v))
|
yield (k, RevocationStore(v))
|
||||||
elif k.endswith("_basepoint") or k.endswith("_key"):
|
elif k.endswith("_basepoint") or k.endswith("_key"):
|
||||||
if local:
|
if local:
|
||||||
yield (k, Keypair(**dict(decodeAll(v, local))))
|
yield (k, Keypair(**dict(decodeAll(v, local))))
|
||||||
|
@ -152,6 +155,7 @@ class Channel(Logger):
|
||||||
self.lnworker = lnworker # type: Optional[LNWallet]
|
self.lnworker = lnworker # type: Optional[LNWallet]
|
||||||
self.sweep_address = sweep_address
|
self.sweep_address = sweep_address
|
||||||
assert 'local_state' not in state
|
assert 'local_state' not in state
|
||||||
|
self.db_lock = self.lnworker.wallet.storage.db.lock if self.lnworker else threading.RLock()
|
||||||
self.config = {}
|
self.config = {}
|
||||||
self.config[LOCAL] = state["local_config"]
|
self.config[LOCAL] = state["local_config"]
|
||||||
if type(self.config[LOCAL]) is not LocalConfig:
|
if type(self.config[LOCAL]) is not LocalConfig:
|
||||||
|
@ -181,6 +185,7 @@ class Channel(Logger):
|
||||||
self.peer_state = peer_states.DISCONNECTED
|
self.peer_state = peer_states.DISCONNECTED
|
||||||
self.sweep_info = {} # type: Dict[str, Dict[str, SweepInfo]]
|
self.sweep_info = {} # type: Dict[str, Dict[str, SweepInfo]]
|
||||||
self._outgoing_channel_update = None # type: Optional[bytes]
|
self._outgoing_channel_update = None # type: Optional[bytes]
|
||||||
|
self.revocation_store = RevocationStore(state["revocation_store"])
|
||||||
|
|
||||||
def get_feerate(self, subject, ctn):
|
def get_feerate(self, subject, ctn):
|
||||||
return self.hm.get_feerate(subject, ctn)
|
return self.hm.get_feerate(subject, ctn)
|
||||||
|
@ -211,12 +216,13 @@ class Channel(Logger):
|
||||||
return out
|
return out
|
||||||
|
|
||||||
def open_with_first_pcp(self, remote_pcp, remote_sig):
|
def open_with_first_pcp(self, remote_pcp, remote_sig):
|
||||||
self.config[REMOTE] = self.config[REMOTE]._replace(current_per_commitment_point=remote_pcp,
|
with self.db_lock:
|
||||||
next_per_commitment_point=None)
|
self.config[REMOTE].current_per_commitment_point=remote_pcp
|
||||||
self.config[LOCAL] = self.config[LOCAL]._replace(current_commitment_signature=remote_sig)
|
self.config[REMOTE].next_per_commitment_point=None
|
||||||
self.hm.channel_open_finished()
|
self.config[LOCAL].current_commitment_signature=remote_sig
|
||||||
self.peer_state = peer_states.GOOD
|
self.hm.channel_open_finished()
|
||||||
self.set_state(channel_states.OPENING)
|
self.peer_state = peer_states.GOOD
|
||||||
|
self.set_state(channel_states.OPENING)
|
||||||
|
|
||||||
def set_state(self, state):
|
def set_state(self, state):
|
||||||
""" set on-chain state """
|
""" set on-chain state """
|
||||||
|
@ -279,7 +285,8 @@ class Channel(Logger):
|
||||||
self._check_can_pay(htlc.amount_msat)
|
self._check_can_pay(htlc.amount_msat)
|
||||||
if htlc.htlc_id is None:
|
if htlc.htlc_id is None:
|
||||||
htlc = htlc._replace(htlc_id=self.hm.get_next_htlc_id(LOCAL))
|
htlc = htlc._replace(htlc_id=self.hm.get_next_htlc_id(LOCAL))
|
||||||
self.hm.send_htlc(htlc)
|
with self.db_lock:
|
||||||
|
self.hm.send_htlc(htlc)
|
||||||
self.logger.info("add_htlc")
|
self.logger.info("add_htlc")
|
||||||
return htlc
|
return htlc
|
||||||
|
|
||||||
|
@ -300,7 +307,8 @@ class Channel(Logger):
|
||||||
raise RemoteMisbehaving('Remote dipped below channel reserve.' +\
|
raise RemoteMisbehaving('Remote dipped below channel reserve.' +\
|
||||||
f' Available at remote: {self.available_to_spend(REMOTE)},' +\
|
f' Available at remote: {self.available_to_spend(REMOTE)},' +\
|
||||||
f' HTLC amount: {htlc.amount_msat}')
|
f' HTLC amount: {htlc.amount_msat}')
|
||||||
self.hm.recv_htlc(htlc)
|
with self.db_lock:
|
||||||
|
self.hm.recv_htlc(htlc)
|
||||||
self.logger.info("receive_htlc")
|
self.logger.info("receive_htlc")
|
||||||
return htlc
|
return htlc
|
||||||
|
|
||||||
|
@ -346,9 +354,8 @@ class Channel(Logger):
|
||||||
htlcsigs.append((ctx_output_idx, htlc_sig))
|
htlcsigs.append((ctx_output_idx, htlc_sig))
|
||||||
htlcsigs.sort()
|
htlcsigs.sort()
|
||||||
htlcsigs = [x[1] for x in htlcsigs]
|
htlcsigs = [x[1] for x in htlcsigs]
|
||||||
|
with self.db_lock:
|
||||||
self.hm.send_ctx()
|
self.hm.send_ctx()
|
||||||
|
|
||||||
return sig_64, htlcsigs
|
return sig_64, htlcsigs
|
||||||
|
|
||||||
def receive_new_commitment(self, sig, htlc_sigs):
|
def receive_new_commitment(self, sig, htlc_sigs):
|
||||||
|
@ -395,11 +402,10 @@ class Channel(Logger):
|
||||||
pcp=pcp,
|
pcp=pcp,
|
||||||
ctx=pending_local_commitment,
|
ctx=pending_local_commitment,
|
||||||
ctx_output_idx=ctx_output_idx)
|
ctx_output_idx=ctx_output_idx)
|
||||||
|
with self.db_lock:
|
||||||
self.hm.recv_ctx()
|
self.hm.recv_ctx()
|
||||||
self.config[LOCAL]=self.config[LOCAL]._replace(
|
self.config[LOCAL].current_commitment_signature=sig
|
||||||
current_commitment_signature=sig,
|
self.config[LOCAL].current_htlc_signatures=htlc_sigs_string
|
||||||
current_htlc_signatures=htlc_sigs_string)
|
|
||||||
|
|
||||||
def verify_htlc(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,
|
def verify_htlc(self, *, htlc: UpdateAddHtlc, htlc_sig: bytes, htlc_direction: Direction,
|
||||||
pcp: bytes, ctx: Transaction, ctx_output_idx: int) -> None:
|
pcp: bytes, ctx: Transaction, ctx_output_idx: int) -> None:
|
||||||
|
@ -429,7 +435,8 @@ class Channel(Logger):
|
||||||
if not self.signature_fits(new_ctx):
|
if not self.signature_fits(new_ctx):
|
||||||
# this should never fail; as receive_new_commitment already did this test
|
# this should never fail; as receive_new_commitment already did this test
|
||||||
raise Exception("refusing to revoke as remote sig does not fit")
|
raise Exception("refusing to revoke as remote sig does not fit")
|
||||||
self.hm.send_rev()
|
with self.db_lock:
|
||||||
|
self.hm.send_rev()
|
||||||
received = self.hm.received_in_ctn(new_ctn)
|
received = self.hm.received_in_ctn(new_ctn)
|
||||||
sent = self.hm.sent_in_ctn(new_ctn)
|
sent = self.hm.sent_in_ctn(new_ctn)
|
||||||
if self.lnworker:
|
if self.lnworker:
|
||||||
|
@ -451,13 +458,12 @@ class Channel(Logger):
|
||||||
if cur_point != derived_point:
|
if cur_point != derived_point:
|
||||||
raise Exception('revoked secret not for current point')
|
raise Exception('revoked secret not for current point')
|
||||||
|
|
||||||
self.config[REMOTE].revocation_store.add_next_entry(revocation.per_commitment_secret)
|
with self.db_lock:
|
||||||
##### start applying fee/htlc changes
|
self.revocation_store.add_next_entry(revocation.per_commitment_secret)
|
||||||
self.hm.recv_rev()
|
##### start applying fee/htlc changes
|
||||||
self.config[REMOTE]=self.config[REMOTE]._replace(
|
self.hm.recv_rev()
|
||||||
current_per_commitment_point=self.config[REMOTE].next_per_commitment_point,
|
self.config[REMOTE].current_per_commitment_point=self.config[REMOTE].next_per_commitment_point
|
||||||
next_per_commitment_point=revocation.next_per_commitment_point,
|
self.config[REMOTE].next_per_commitment_point=revocation.next_per_commitment_point
|
||||||
)
|
|
||||||
|
|
||||||
def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None):
|
def balance(self, whose, *, ctx_owner=HTLCOwner.LOCAL, ctn=None):
|
||||||
"""
|
"""
|
||||||
|
@ -548,7 +554,7 @@ class Channel(Logger):
|
||||||
secret = None
|
secret = None
|
||||||
point = conf.current_per_commitment_point
|
point = conf.current_per_commitment_point
|
||||||
else:
|
else:
|
||||||
secret = conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
|
secret = self.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
|
||||||
point = secret_to_pubkey(int.from_bytes(secret, 'big'))
|
point = secret_to_pubkey(int.from_bytes(secret, 'big'))
|
||||||
else:
|
else:
|
||||||
secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
|
secret = get_per_commitment_secret_from_seed(self.config[LOCAL].per_commitment_secret_seed, RevocationStore.START_INDEX - ctn)
|
||||||
|
@ -624,15 +630,18 @@ class Channel(Logger):
|
||||||
htlc = log['adds'][htlc_id]
|
htlc = log['adds'][htlc_id]
|
||||||
assert htlc.payment_hash == sha256(preimage)
|
assert htlc.payment_hash == sha256(preimage)
|
||||||
assert htlc_id not in log['settles']
|
assert htlc_id not in log['settles']
|
||||||
self.hm.recv_settle(htlc_id)
|
with self.db_lock:
|
||||||
|
self.hm.recv_settle(htlc_id)
|
||||||
|
|
||||||
def fail_htlc(self, htlc_id):
|
def fail_htlc(self, htlc_id):
|
||||||
self.logger.info("fail_htlc")
|
self.logger.info("fail_htlc")
|
||||||
self.hm.send_fail(htlc_id)
|
with self.db_lock:
|
||||||
|
self.hm.send_fail(htlc_id)
|
||||||
|
|
||||||
def receive_fail_htlc(self, htlc_id):
|
def receive_fail_htlc(self, htlc_id):
|
||||||
self.logger.info("receive_fail_htlc")
|
self.logger.info("receive_fail_htlc")
|
||||||
self.hm.recv_fail(htlc_id)
|
with self.db_lock:
|
||||||
|
self.hm.recv_fail(htlc_id)
|
||||||
|
|
||||||
def pending_local_fee(self):
|
def pending_local_fee(self):
|
||||||
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
|
return self.constraints.capacity - sum(x.value for x in self.get_next_commitment(LOCAL).outputs())
|
||||||
|
@ -641,10 +650,11 @@ class Channel(Logger):
|
||||||
# feerate uses sat/kw
|
# feerate uses sat/kw
|
||||||
if self.constraints.is_initiator != from_us:
|
if self.constraints.is_initiator != from_us:
|
||||||
raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
|
raise Exception(f"Cannot update_fee: wrong initiator. us: {from_us}")
|
||||||
if from_us:
|
with self.db_lock:
|
||||||
self.hm.send_update_fee(feerate)
|
if from_us:
|
||||||
else:
|
self.hm.send_update_fee(feerate)
|
||||||
self.hm.recv_update_fee(feerate)
|
else:
|
||||||
|
self.hm.recv_update_fee(feerate)
|
||||||
|
|
||||||
def to_save(self):
|
def to_save(self):
|
||||||
to_save = {
|
to_save = {
|
||||||
|
@ -656,6 +666,7 @@ class Channel(Logger):
|
||||||
"funding_outpoint": self.funding_outpoint,
|
"funding_outpoint": self.funding_outpoint,
|
||||||
"node_id": self.node_id,
|
"node_id": self.node_id,
|
||||||
"log": self.hm.to_save(),
|
"log": self.hm.to_save(),
|
||||||
|
"revocation_store": self.revocation_store,
|
||||||
"onion_keys": str_bytes_dict_to_save(self.onion_keys),
|
"onion_keys": str_bytes_dict_to_save(self.onion_keys),
|
||||||
"state": self._state.name,
|
"state": self._state.name,
|
||||||
"data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp),
|
"data_loss_protect_remote_pcp": str_bytes_dict_to_save(self.data_loss_protect_remote_pcp),
|
||||||
|
|
|
@ -548,7 +548,6 @@ class Peer(Logger):
|
||||||
if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
|
if remote_to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
|
||||||
raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," +
|
raise Exception(f"Remote Lightning peer reports to_self_delay={remote_to_self_delay}," +
|
||||||
f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
|
f" which is above Electrums required maximum ({MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED})")
|
||||||
their_revocation_store = RevocationStore()
|
|
||||||
remote_config = RemoteConfig(
|
remote_config = RemoteConfig(
|
||||||
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
|
payment_basepoint=OnlyPubkeyKeypair(payload['payment_basepoint']),
|
||||||
multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
|
multisig_key=OnlyPubkeyKeypair(payload["funding_pubkey"]),
|
||||||
|
@ -564,7 +563,6 @@ class Peer(Logger):
|
||||||
htlc_minimum_msat = htlc_min,
|
htlc_minimum_msat = htlc_min,
|
||||||
next_per_commitment_point=remote_per_commitment_point,
|
next_per_commitment_point=remote_per_commitment_point,
|
||||||
current_per_commitment_point=None,
|
current_per_commitment_point=None,
|
||||||
revocation_store=their_revocation_store,
|
|
||||||
)
|
)
|
||||||
# replace dummy output in funding tx
|
# replace dummy output in funding tx
|
||||||
redeem_script = funding_output_script(local_config, remote_config)
|
redeem_script = funding_output_script(local_config, remote_config)
|
||||||
|
@ -592,6 +590,7 @@ class Peer(Logger):
|
||||||
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
|
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=True, funding_txn_minimum_depth=funding_txn_minimum_depth),
|
||||||
"remote_update": None,
|
"remote_update": None,
|
||||||
"state": channel_states.PREOPENING.name,
|
"state": channel_states.PREOPENING.name,
|
||||||
|
"revocation_store": {},
|
||||||
}
|
}
|
||||||
chan = Channel(chan_dict,
|
chan = Channel(chan_dict,
|
||||||
sweep_address=self.lnworker.sweep_address,
|
sweep_address=self.lnworker.sweep_address,
|
||||||
|
@ -645,7 +644,6 @@ class Peer(Logger):
|
||||||
funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big')
|
funding_idx = int.from_bytes(funding_created['funding_output_index'], 'big')
|
||||||
funding_txid = bh2u(funding_created['funding_txid'][::-1])
|
funding_txid = bh2u(funding_created['funding_txid'][::-1])
|
||||||
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
|
channel_id, funding_txid_bytes = channel_id_from_funding_tx(funding_txid, funding_idx)
|
||||||
their_revocation_store = RevocationStore()
|
|
||||||
remote_balance_sat = funding_sat * 1000 - push_msat
|
remote_balance_sat = funding_sat * 1000 - push_msat
|
||||||
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate
|
remote_dust_limit_sat = int.from_bytes(payload['dust_limit_satoshis'], byteorder='big') # TODO validate
|
||||||
remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
|
remote_reserve_sat = self.validate_remote_reserve(payload['channel_reserve_satoshis'], remote_dust_limit_sat, funding_sat)
|
||||||
|
@ -669,12 +667,12 @@ class Peer(Logger):
|
||||||
htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate
|
htlc_minimum_msat=int.from_bytes(payload['htlc_minimum_msat'], 'big'), # TODO validate
|
||||||
next_per_commitment_point=payload['first_per_commitment_point'],
|
next_per_commitment_point=payload['first_per_commitment_point'],
|
||||||
current_per_commitment_point=None,
|
current_per_commitment_point=None,
|
||||||
revocation_store=their_revocation_store,
|
|
||||||
),
|
),
|
||||||
"local_config": local_config,
|
"local_config": local_config,
|
||||||
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth),
|
"constraints": ChannelConstraints(capacity=funding_sat, is_initiator=False, funding_txn_minimum_depth=min_depth),
|
||||||
"remote_update": None,
|
"remote_update": None,
|
||||||
"state": channel_states.PREOPENING.name,
|
"state": channel_states.PREOPENING.name,
|
||||||
|
"revocation_store": {},
|
||||||
}
|
}
|
||||||
chan = Channel(chan_dict,
|
chan = Channel(chan_dict,
|
||||||
sweep_address=self.lnworker.sweep_address,
|
sweep_address=self.lnworker.sweep_address,
|
||||||
|
@ -740,9 +738,8 @@ class Peer(Logger):
|
||||||
if oldest_unrevoked_remote_ctn == 0:
|
if oldest_unrevoked_remote_ctn == 0:
|
||||||
last_rev_secret = 0
|
last_rev_secret = 0
|
||||||
else:
|
else:
|
||||||
revocation_store = chan.config[REMOTE].revocation_store
|
|
||||||
last_rev_index = oldest_unrevoked_remote_ctn - 1
|
last_rev_index = oldest_unrevoked_remote_ctn - 1
|
||||||
last_rev_secret = revocation_store.retrieve_secret(RevocationStore.START_INDEX - last_rev_index)
|
last_rev_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - last_rev_index)
|
||||||
latest_secret, latest_point = chan.get_secret_and_point(LOCAL, latest_local_ctn)
|
latest_secret, latest_point = chan.get_secret_and_point(LOCAL, latest_local_ctn)
|
||||||
self.send_message(
|
self.send_message(
|
||||||
"channel_reestablish",
|
"channel_reestablish",
|
||||||
|
@ -895,10 +892,8 @@ class Peer(Logger):
|
||||||
if not chan.config[LOCAL].funding_locked_received:
|
if not chan.config[LOCAL].funding_locked_received:
|
||||||
our_next_point = chan.config[REMOTE].next_per_commitment_point
|
our_next_point = chan.config[REMOTE].next_per_commitment_point
|
||||||
their_next_point = payload["next_per_commitment_point"]
|
their_next_point = payload["next_per_commitment_point"]
|
||||||
new_remote_state = chan.config[REMOTE]._replace(next_per_commitment_point=their_next_point)
|
chan.config[REMOTE].next_per_commitment_point = their_next_point
|
||||||
new_local_state = chan.config[LOCAL]._replace(funding_locked_received = True)
|
chan.config[LOCAL].funding_locked_received = True
|
||||||
chan.config[REMOTE]=new_remote_state
|
|
||||||
chan.config[LOCAL]=new_local_state
|
|
||||||
self.lnworker.save_channel(chan)
|
self.lnworker.save_channel(chan)
|
||||||
if chan.short_channel_id:
|
if chan.short_channel_id:
|
||||||
self.mark_open(chan)
|
self.mark_open(chan)
|
||||||
|
@ -913,9 +908,9 @@ class Peer(Logger):
|
||||||
# don't announce our channels
|
# don't announce our channels
|
||||||
# FIXME should this be a field in chan.local_state maybe?
|
# FIXME should this be a field in chan.local_state maybe?
|
||||||
return
|
return
|
||||||
chan.config[LOCAL]=chan.config[LOCAL]._replace(was_announced=True)
|
chan.config[LOCAL].was_announced = True
|
||||||
coro = self.handle_announcements(chan)
|
|
||||||
self.lnworker.save_channel(chan)
|
self.lnworker.save_channel(chan)
|
||||||
|
coro = self.handle_announcements(chan)
|
||||||
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
|
asyncio.run_coroutine_threadsafe(coro, self.network.asyncio_loop)
|
||||||
|
|
||||||
@log_exceptions
|
@log_exceptions
|
||||||
|
|
|
@ -293,7 +293,7 @@ def analyze_ctx(chan: 'Channel', ctx: Transaction):
|
||||||
is_revocation = False
|
is_revocation = False
|
||||||
elif ctn < oldest_unrevoked_remote_ctn: # breach
|
elif ctn < oldest_unrevoked_remote_ctn: # breach
|
||||||
try:
|
try:
|
||||||
per_commitment_secret = their_conf.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
|
per_commitment_secret = chan.revocation_store.retrieve_secret(RevocationStore.START_INDEX - ctn)
|
||||||
except UnableToDeriveSecret:
|
except UnableToDeriveSecret:
|
||||||
return
|
return
|
||||||
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
|
their_pcp = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
|
||||||
|
|
|
@ -7,6 +7,7 @@ import json
|
||||||
from collections import namedtuple
|
from collections import namedtuple
|
||||||
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
||||||
import re
|
import re
|
||||||
|
import attr
|
||||||
|
|
||||||
from aiorpcx import NetAddress
|
from aiorpcx import NetAddress
|
||||||
|
|
||||||
|
@ -37,55 +38,56 @@ LN_MAX_FUNDING_SAT = pow(2, 24) - 1
|
||||||
def ln_dummy_address():
|
def ln_dummy_address():
|
||||||
return redeem_script_to_address('p2wsh', '')
|
return redeem_script_to_address('p2wsh', '')
|
||||||
|
|
||||||
class Keypair(NamedTuple):
|
|
||||||
pubkey: bytes
|
class StoredAttr:
|
||||||
privkey: bytes
|
|
||||||
|
def to_json(self):
|
||||||
|
return dict(vars(self))
|
||||||
|
|
||||||
|
|
||||||
class OnlyPubkeyKeypair(NamedTuple):
|
@attr.s
|
||||||
pubkey: bytes
|
class OnlyPubkeyKeypair(StoredAttr):
|
||||||
|
pubkey = attr.ib(type=bytes)
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class Keypair(OnlyPubkeyKeypair):
|
||||||
|
privkey = attr.ib(type=bytes)
|
||||||
|
|
||||||
# NamedTuples cannot subclass NamedTuples :'( https://github.com/python/typing/issues/427
|
@attr.s
|
||||||
class LocalConfig(NamedTuple):
|
class Config(StoredAttr):
|
||||||
# shared channel config fields (DUPLICATED code!!)
|
# shared channel config fields
|
||||||
payment_basepoint: 'Keypair'
|
payment_basepoint = attr.ib(type=OnlyPubkeyKeypair)
|
||||||
multisig_key: 'Keypair'
|
multisig_key = attr.ib(type=OnlyPubkeyKeypair)
|
||||||
htlc_basepoint: 'Keypair'
|
htlc_basepoint = attr.ib(type=OnlyPubkeyKeypair)
|
||||||
delayed_basepoint: 'Keypair'
|
delayed_basepoint = attr.ib(type=OnlyPubkeyKeypair)
|
||||||
revocation_basepoint: 'Keypair'
|
revocation_basepoint = attr.ib(type=OnlyPubkeyKeypair)
|
||||||
to_self_delay: int
|
to_self_delay = attr.ib(type=int)
|
||||||
dust_limit_sat: int
|
dust_limit_sat = attr.ib(type=int)
|
||||||
max_htlc_value_in_flight_msat: int
|
max_htlc_value_in_flight_msat = attr.ib(type=int)
|
||||||
max_accepted_htlcs: int
|
max_accepted_htlcs = attr.ib(type=int)
|
||||||
initial_msat: int
|
initial_msat = attr.ib(type=int)
|
||||||
reserve_sat: int
|
reserve_sat = attr.ib(type=int)
|
||||||
# specific to "LOCAL" config
|
|
||||||
per_commitment_secret_seed: bytes
|
|
||||||
funding_locked_received: bool
|
|
||||||
was_announced: bool
|
|
||||||
current_commitment_signature: Optional[bytes]
|
|
||||||
current_htlc_signatures: bytes
|
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class LocalConfig(Config):
|
||||||
|
per_commitment_secret_seed = attr.ib(type=bytes)
|
||||||
|
funding_locked_received = attr.ib(type=bool)
|
||||||
|
was_announced = attr.ib(type=bool)
|
||||||
|
current_commitment_signature = attr.ib(type=bytes)
|
||||||
|
current_htlc_signatures = attr.ib(type=bytes)
|
||||||
|
|
||||||
|
@attr.s
|
||||||
|
class RemoteConfig(Config):
|
||||||
|
htlc_minimum_msat = attr.ib(type=int)
|
||||||
|
next_per_commitment_point = attr.ib(type=bytes)
|
||||||
|
current_per_commitment_point = attr.ib(default=None, type=bytes)
|
||||||
|
|
||||||
|
#@attr.s
|
||||||
|
#class FeeUpdate(StoredAttr):
|
||||||
|
# rate = attr.ib(type=int) # in sat/kw
|
||||||
|
# ctn_local = attr.ib(default=None, type=int)
|
||||||
|
# ctn_remote = attr.ib(default=None, type=int)
|
||||||
|
|
||||||
class RemoteConfig(NamedTuple):
|
|
||||||
# shared channel config fields (DUPLICATED code!!)
|
|
||||||
payment_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
|
|
||||||
multisig_key: Union['Keypair', 'OnlyPubkeyKeypair']
|
|
||||||
htlc_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
|
|
||||||
delayed_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
|
|
||||||
revocation_basepoint: Union['Keypair', 'OnlyPubkeyKeypair']
|
|
||||||
to_self_delay: int
|
|
||||||
dust_limit_sat: int
|
|
||||||
max_htlc_value_in_flight_msat: int
|
|
||||||
max_accepted_htlcs: int
|
|
||||||
initial_msat: int
|
|
||||||
reserve_sat: int
|
|
||||||
# specific to "REMOTE" config
|
|
||||||
htlc_minimum_msat: int
|
|
||||||
next_per_commitment_point: bytes
|
|
||||||
revocation_store: 'RevocationStore'
|
|
||||||
current_per_commitment_point: Optional[bytes]
|
|
||||||
|
|
||||||
|
|
||||||
class FeeUpdate(NamedTuple):
|
class FeeUpdate(NamedTuple):
|
||||||
|
@ -187,9 +189,13 @@ class RevocationStore:
|
||||||
|
|
||||||
START_INDEX = 2 ** 48 - 1
|
START_INDEX = 2 ** 48 - 1
|
||||||
|
|
||||||
def __init__(self):
|
def __init__(self, storage):
|
||||||
self.buckets = [None] * 49
|
self.buckets = [None] * 49
|
||||||
self.index = self.START_INDEX
|
self.index = self.START_INDEX
|
||||||
|
if storage:
|
||||||
|
decode = lambda to_decode: ShachainElement(bfh(to_decode[0]), int(to_decode[1]))
|
||||||
|
self.buckets = [k if k is None else decode(k) for k in storage["buckets"]]
|
||||||
|
self.index = storage["index"]
|
||||||
|
|
||||||
def add_next_entry(self, hsh):
|
def add_next_entry(self, hsh):
|
||||||
new_element = ShachainElement(index=self.index, secret=hsh)
|
new_element = ShachainElement(index=self.index, secret=hsh)
|
||||||
|
@ -197,7 +203,6 @@ class RevocationStore:
|
||||||
for i in range(0, bucket):
|
for i in range(0, bucket):
|
||||||
this_bucket = self.buckets[i]
|
this_bucket = self.buckets[i]
|
||||||
e = shachain_derive(new_element, this_bucket.index)
|
e = shachain_derive(new_element, this_bucket.index)
|
||||||
|
|
||||||
if e != this_bucket:
|
if e != this_bucket:
|
||||||
raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index))
|
raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index))
|
||||||
self.buckets[bucket] = new_element
|
self.buckets[bucket] = new_element
|
||||||
|
@ -218,14 +223,6 @@ class RevocationStore:
|
||||||
def serialize(self):
|
def serialize(self):
|
||||||
return {"index": self.index, "buckets": [[bh2u(k.secret), k.index] if k is not None else None for k in self.buckets]}
|
return {"index": self.index, "buckets": [[bh2u(k.secret), k.index] if k is not None else None for k in self.buckets]}
|
||||||
|
|
||||||
@staticmethod
|
|
||||||
def from_json_obj(decoded_json_obj):
|
|
||||||
store = RevocationStore()
|
|
||||||
decode = lambda to_decode: ShachainElement(bfh(to_decode[0]), int(to_decode[1]))
|
|
||||||
store.buckets = [k if k is None else decode(k) for k in decoded_json_obj["buckets"]]
|
|
||||||
store.index = decoded_json_obj["index"]
|
|
||||||
return store
|
|
||||||
|
|
||||||
def __eq__(self, o):
|
def __eq__(self, o):
|
||||||
return type(o) is RevocationStore and self.serialize() == o.serialize()
|
return type(o) is RevocationStore and self.serialize() == o.serialize()
|
||||||
|
|
||||||
|
|
|
@ -45,7 +45,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
|
||||||
assert local_amount > 0
|
assert local_amount > 0
|
||||||
assert remote_amount > 0
|
assert remote_amount > 0
|
||||||
channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index)
|
channel_id, _ = lnpeer.channel_id_from_funding_tx(funding_txid, funding_index)
|
||||||
their_revocation_store = lnpeer.RevocationStore()
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
"channel_id":channel_id,
|
"channel_id":channel_id,
|
||||||
|
@ -67,7 +66,6 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
|
||||||
|
|
||||||
next_per_commitment_point=nex,
|
next_per_commitment_point=nex,
|
||||||
current_per_commitment_point=cur,
|
current_per_commitment_point=cur,
|
||||||
revocation_store=their_revocation_store,
|
|
||||||
),
|
),
|
||||||
"local_config":lnpeer.LocalConfig(
|
"local_config":lnpeer.LocalConfig(
|
||||||
payment_basepoint=privkeys[0],
|
payment_basepoint=privkeys[0],
|
||||||
|
@ -96,6 +94,7 @@ def create_channel_state(funding_txid, funding_index, funding_sat, is_initiator,
|
||||||
"node_id":other_node_id,
|
"node_id":other_node_id,
|
||||||
'onion_keys': {},
|
'onion_keys': {},
|
||||||
'state': 'PREOPENING',
|
'state': 'PREOPENING',
|
||||||
|
'revocation_store': {},
|
||||||
}
|
}
|
||||||
|
|
||||||
def bip32(sequence):
|
def bip32(sequence):
|
||||||
|
@ -151,14 +150,16 @@ def create_test_channels(feerate=6000, local=None, remote=None):
|
||||||
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.config[LOCAL] = alice.config[LOCAL]._replace(current_commitment_signature=sig_from_bob)
|
alice.config[LOCAL].current_commitment_signature = sig_from_bob
|
||||||
bob.config[LOCAL] = bob.config[LOCAL]._replace(current_commitment_signature=sig_from_alice)
|
bob.config[LOCAL].current_commitment_signature = 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(lnutil.get_per_commitment_secret_from_seed(alice_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||||
bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
bob_second = lnutil.secret_to_pubkey(int.from_bytes(lnutil.get_per_commitment_secret_from_seed(bob_seed, lnutil.RevocationStore.START_INDEX - 1), "big"))
|
||||||
|
|
||||||
alice.config[REMOTE] = alice.config[REMOTE]._replace(next_per_commitment_point=bob_second, current_per_commitment_point=bob_first)
|
alice.config[REMOTE].next_per_commitment_point = bob_second
|
||||||
bob.config[REMOTE] = bob.config[REMOTE]._replace(next_per_commitment_point=alice_second, current_per_commitment_point=alice_first)
|
alice.config[REMOTE].current_per_commitment_point = bob_first
|
||||||
|
bob.config[REMOTE].next_per_commitment_point = alice_second
|
||||||
|
bob.config[REMOTE].current_per_commitment_point = alice_first
|
||||||
|
|
||||||
alice.hm.channel_open_finished()
|
alice.hm.channel_open_finished()
|
||||||
bob.hm.channel_open_finished()
|
bob.hm.channel_open_finished()
|
||||||
|
@ -663,15 +664,11 @@ class TestChanReserve(ElectrumTestCase):
|
||||||
bob_min_reserve = 6 * one_bitcoin_in_msat // 1000
|
bob_min_reserve = 6 * one_bitcoin_in_msat // 1000
|
||||||
# bob min reserve was decided by alice, but applies to bob
|
# bob min reserve was decided by alice, but applies to bob
|
||||||
|
|
||||||
alice_channel.config[LOCAL] =\
|
alice_channel.config[LOCAL].reserve_sat = bob_min_reserve
|
||||||
alice_channel.config[LOCAL]._replace(reserve_sat=bob_min_reserve)
|
alice_channel.config[REMOTE].reserve_sat = alice_min_reserve
|
||||||
alice_channel.config[REMOTE] =\
|
|
||||||
alice_channel.config[REMOTE]._replace(reserve_sat=alice_min_reserve)
|
|
||||||
|
|
||||||
bob_channel.config[LOCAL] =\
|
bob_channel.config[LOCAL].reserve_sat = alice_min_reserve
|
||||||
bob_channel.config[LOCAL]._replace(reserve_sat=alice_min_reserve)
|
bob_channel.config[REMOTE].reserve_sat = bob_min_reserve
|
||||||
bob_channel.config[REMOTE] =\
|
|
||||||
bob_channel.config[REMOTE]._replace(reserve_sat=bob_min_reserve)
|
|
||||||
|
|
||||||
self.alice_channel = alice_channel
|
self.alice_channel = alice_channel
|
||||||
self.bob_channel = bob_channel
|
self.bob_channel = bob_channel
|
||||||
|
|
|
@ -422,7 +422,7 @@ class TestLNUtil(ElectrumTestCase):
|
||||||
]
|
]
|
||||||
|
|
||||||
for test in tests:
|
for test in tests:
|
||||||
receiver = RevocationStore()
|
receiver = RevocationStore({})
|
||||||
for insert in test["inserts"]:
|
for insert in test["inserts"]:
|
||||||
secret = bytes.fromhex(insert["secret"])
|
secret = bytes.fromhex(insert["secret"])
|
||||||
|
|
||||||
|
@ -445,14 +445,14 @@ class TestLNUtil(ElectrumTestCase):
|
||||||
|
|
||||||
def test_shachain_produce_consume(self):
|
def test_shachain_produce_consume(self):
|
||||||
seed = bitcoin.sha256(b"shachaintest")
|
seed = bitcoin.sha256(b"shachaintest")
|
||||||
consumer = RevocationStore()
|
consumer = RevocationStore({})
|
||||||
for i in range(10000):
|
for i in range(10000):
|
||||||
secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - i)
|
secret = get_per_commitment_secret_from_seed(seed, RevocationStore.START_INDEX - i)
|
||||||
try:
|
try:
|
||||||
consumer.add_next_entry(secret)
|
consumer.add_next_entry(secret)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception("iteration " + str(i) + ": " + str(e))
|
raise Exception("iteration " + str(i) + ": " + str(e))
|
||||||
if i % 1000 == 0: self.assertEqual(consumer.serialize(), RevocationStore.from_json_obj(json.loads(json.dumps(consumer.serialize()))).serialize())
|
if i % 1000 == 0: self.assertEqual(consumer.serialize(), RevocationStore(json.loads(json.dumps(consumer.serialize()))).serialize())
|
||||||
|
|
||||||
def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self):
|
def test_commitment_tx_with_all_five_HTLCs_untrimmed_minimum_feerate(self):
|
||||||
to_local_msat = 6988000000
|
to_local_msat = 6988000000
|
||||||
|
|
Loading…
Add table
Reference in a new issue