Upfront shutdown script is a script provided on channel opening, which will be used by the peer to enforce us closing to this script on collaborative channel close.
1316 lines
55 KiB
Python
1316 lines
55 KiB
Python
# Copyright (C) 2018 The Electrum developers
|
|
# Distributed under the MIT software license, see the accompanying
|
|
# file LICENCE or http://www.opensource.org/licenses/mit-license.php
|
|
|
|
from enum import IntFlag, IntEnum
|
|
import enum
|
|
import json
|
|
from collections import namedtuple, defaultdict
|
|
from typing import NamedTuple, List, Tuple, Mapping, Optional, TYPE_CHECKING, Union, Dict, Set, Sequence
|
|
import re
|
|
|
|
import attr
|
|
from aiorpcx import NetAddress
|
|
|
|
from .util import bfh, bh2u, inv_dict, UserFacingException
|
|
from .util import list_enabled_bits
|
|
from .crypto import sha256
|
|
from .transaction import (Transaction, PartialTransaction, PartialTxInput, TxOutpoint,
|
|
PartialTxOutput, opcodes, TxOutput)
|
|
from .ecc import CURVE_ORDER, sig_string_from_der_sig, ECPubkey, string_to_number
|
|
from . import ecc, bitcoin, crypto, transaction
|
|
from .bitcoin import (push_script, redeem_script_to_address, address_to_script,
|
|
construct_witness, construct_script)
|
|
from . import segwit_addr
|
|
from .i18n import _
|
|
from .lnaddr import lndecode
|
|
from .bip32 import BIP32Node, BIP32_PRIME
|
|
from .transaction import BCDataStream
|
|
|
|
if TYPE_CHECKING:
|
|
from .lnchannel import Channel, AbstractChannel
|
|
from .lnrouter import LNPaymentRoute
|
|
from .lnonion import OnionRoutingFailureMessage
|
|
|
|
|
|
# defined in BOLT-03:
|
|
HTLC_TIMEOUT_WEIGHT = 663
|
|
HTLC_SUCCESS_WEIGHT = 703
|
|
COMMITMENT_TX_WEIGHT = 724
|
|
HTLC_OUTPUT_WEIGHT = 172
|
|
|
|
LN_MAX_FUNDING_SAT = pow(2, 24) - 1
|
|
LN_MAX_HTLC_VALUE_MSAT = pow(2, 32) - 1
|
|
|
|
# dummy address for fee estimation of funding tx
|
|
def ln_dummy_address():
|
|
return redeem_script_to_address('p2wsh', '')
|
|
|
|
from .json_db import StoredObject
|
|
|
|
|
|
def channel_id_from_funding_tx(funding_txid: str, funding_index: int) -> Tuple[bytes, bytes]:
|
|
funding_txid_bytes = bytes.fromhex(funding_txid)[::-1]
|
|
i = int.from_bytes(funding_txid_bytes, 'big') ^ funding_index
|
|
return i.to_bytes(32, 'big'), funding_txid_bytes
|
|
|
|
hex_to_bytes = lambda v: v if isinstance(v, bytes) else bytes.fromhex(v) if v is not None else None
|
|
json_to_keypair = lambda v: v if isinstance(v, OnlyPubkeyKeypair) else Keypair(**v) if len(v)==2 else OnlyPubkeyKeypair(**v)
|
|
|
|
|
|
@attr.s
|
|
class OnlyPubkeyKeypair(StoredObject):
|
|
pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
|
|
@attr.s
|
|
class Keypair(OnlyPubkeyKeypair):
|
|
privkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
|
|
@attr.s
|
|
class Config(StoredObject):
|
|
# shared channel config fields
|
|
payment_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)
|
|
multisig_key = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)
|
|
htlc_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)
|
|
delayed_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)
|
|
revocation_basepoint = attr.ib(type=OnlyPubkeyKeypair, converter=json_to_keypair)
|
|
to_self_delay = attr.ib(type=int) # applies to OTHER ctx
|
|
dust_limit_sat = attr.ib(type=int) # applies to SAME ctx
|
|
max_htlc_value_in_flight_msat = attr.ib(type=int) # max val of INCOMING htlcs
|
|
max_accepted_htlcs = attr.ib(type=int) # max num of INCOMING htlcs
|
|
initial_msat = attr.ib(type=int)
|
|
reserve_sat = attr.ib(type=int) # applies to OTHER ctx
|
|
htlc_minimum_msat = attr.ib(type=int) # smallest value for INCOMING htlc
|
|
upfront_shutdown_script = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
|
|
def validate_params(self, *, funding_sat: int) -> None:
|
|
conf_name = type(self).__name__
|
|
for key in (
|
|
self.payment_basepoint,
|
|
self.multisig_key,
|
|
self.htlc_basepoint,
|
|
self.delayed_basepoint,
|
|
self.revocation_basepoint
|
|
):
|
|
if not (len(key.pubkey) == 33 and ecc.ECPubkey.is_pubkey_bytes(key.pubkey)):
|
|
raise Exception(f"{conf_name}. invalid pubkey in channel config")
|
|
if self.reserve_sat < self.dust_limit_sat:
|
|
raise Exception(f"{conf_name}. MUST set channel_reserve_satoshis greater than or equal to dust_limit_satoshis")
|
|
# technically this could be using the lower DUST_LIMIT_DEFAULT_SAT_SEGWIT
|
|
# but other implementations are checking against this value too; also let's be conservative
|
|
if self.dust_limit_sat < bitcoin.DUST_LIMIT_DEFAULT_SAT_LEGACY:
|
|
raise Exception(f"{conf_name}. dust limit too low: {self.dust_limit_sat} sat")
|
|
if self.reserve_sat > funding_sat // 100:
|
|
raise Exception(f"{conf_name}. reserve too high: {self.reserve_sat}, funding_sat: {funding_sat}")
|
|
if self.htlc_minimum_msat > 1_000:
|
|
raise Exception(f"{conf_name}. htlc_minimum_msat too high: {self.htlc_minimum_msat} msat")
|
|
HTLC_MINIMUM_MSAT_MIN = 0 # should be at least 1 really, but apparently some nodes are sending zero...
|
|
if self.htlc_minimum_msat < HTLC_MINIMUM_MSAT_MIN:
|
|
raise Exception(f"{conf_name}. htlc_minimum_msat too low: {self.htlc_minimum_msat} msat < {HTLC_MINIMUM_MSAT_MIN}")
|
|
if self.max_accepted_htlcs < 1:
|
|
raise Exception(f"{conf_name}. max_accepted_htlcs too low: {self.max_accepted_htlcs}")
|
|
if self.max_accepted_htlcs > 483:
|
|
raise Exception(f"{conf_name}. max_accepted_htlcs too high: {self.max_accepted_htlcs}")
|
|
if self.to_self_delay > MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED:
|
|
raise Exception(f"{conf_name}. to_self_delay too high: {self.to_self_delay} > {MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED}")
|
|
if self.max_htlc_value_in_flight_msat < min(1000 * funding_sat, 100_000_000):
|
|
raise Exception(f"{conf_name}. max_htlc_value_in_flight_msat is too small: {self.max_htlc_value_in_flight_msat}")
|
|
|
|
|
|
@attr.s
|
|
class LocalConfig(Config):
|
|
channel_seed = attr.ib(type=bytes, converter=hex_to_bytes) # type: Optional[bytes]
|
|
funding_locked_received = attr.ib(type=bool)
|
|
was_announced = attr.ib(type=bool)
|
|
current_commitment_signature = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
current_htlc_signatures = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
per_commitment_secret_seed = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
|
|
@classmethod
|
|
def from_seed(self, **kwargs):
|
|
channel_seed = kwargs['channel_seed']
|
|
static_remotekey = kwargs.pop('static_remotekey')
|
|
node = BIP32Node.from_rootseed(channel_seed, xtype='standard')
|
|
keypair_generator = lambda family: generate_keypair(node, family)
|
|
kwargs['per_commitment_secret_seed'] = keypair_generator(LnKeyFamily.REVOCATION_ROOT).privkey
|
|
kwargs['multisig_key'] = keypair_generator(LnKeyFamily.MULTISIG)
|
|
kwargs['htlc_basepoint'] = keypair_generator(LnKeyFamily.HTLC_BASE)
|
|
kwargs['delayed_basepoint'] = keypair_generator(LnKeyFamily.DELAY_BASE)
|
|
kwargs['revocation_basepoint'] = keypair_generator(LnKeyFamily.REVOCATION_BASE)
|
|
kwargs['payment_basepoint'] = OnlyPubkeyKeypair(static_remotekey) if static_remotekey else keypair_generator(LnKeyFamily.PAYMENT_BASE)
|
|
return LocalConfig(**kwargs)
|
|
|
|
def validate_params(self, *, funding_sat: int) -> None:
|
|
conf_name = type(self).__name__
|
|
# run base checks regardless whether LOCAL/REMOTE config
|
|
super().validate_params(funding_sat=funding_sat)
|
|
# run some stricter checks on LOCAL config (make sure we ourselves do the sane thing,
|
|
# even if we are lenient with REMOTE for compatibility reasons)
|
|
HTLC_MINIMUM_MSAT_MIN = 1
|
|
if self.htlc_minimum_msat < HTLC_MINIMUM_MSAT_MIN:
|
|
raise Exception(f"{conf_name}. htlc_minimum_msat too low: {self.htlc_minimum_msat} msat < {HTLC_MINIMUM_MSAT_MIN}")
|
|
|
|
@attr.s
|
|
class RemoteConfig(Config):
|
|
next_per_commitment_point = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
current_per_commitment_point = attr.ib(default=None, type=bytes, converter=hex_to_bytes)
|
|
|
|
@attr.s
|
|
class FeeUpdate(StoredObject):
|
|
rate = attr.ib(type=int) # in sat/kw
|
|
ctn_local = attr.ib(default=None, type=int)
|
|
ctn_remote = attr.ib(default=None, type=int)
|
|
|
|
@attr.s
|
|
class ChannelConstraints(StoredObject):
|
|
capacity = attr.ib(type=int)
|
|
is_initiator = attr.ib(type=bool) # note: sometimes also called "funder"
|
|
funding_txn_minimum_depth = attr.ib(type=int)
|
|
|
|
|
|
CHANNEL_BACKUP_VERSION = 0
|
|
@attr.s
|
|
class ChannelBackupStorage(StoredObject):
|
|
node_id = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
privkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
funding_txid = attr.ib(type=str)
|
|
funding_index = attr.ib(type=int, converter=int)
|
|
funding_address = attr.ib(type=str)
|
|
host = attr.ib(type=str)
|
|
port = attr.ib(type=int, converter=int)
|
|
is_initiator = attr.ib(type=bool)
|
|
channel_seed = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
local_delay = attr.ib(type=int, converter=int)
|
|
remote_delay = attr.ib(type=int, converter=int)
|
|
remote_payment_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
remote_revocation_pubkey = attr.ib(type=bytes, converter=hex_to_bytes)
|
|
|
|
def funding_outpoint(self):
|
|
return Outpoint(self.funding_txid, self.funding_index)
|
|
|
|
def channel_id(self):
|
|
chan_id, _ = channel_id_from_funding_tx(self.funding_txid, self.funding_index)
|
|
return chan_id
|
|
|
|
def to_bytes(self) -> bytes:
|
|
vds = BCDataStream()
|
|
vds.write_int16(CHANNEL_BACKUP_VERSION)
|
|
vds.write_boolean(self.is_initiator)
|
|
vds.write_bytes(self.privkey, 32)
|
|
vds.write_bytes(self.channel_seed, 32)
|
|
vds.write_bytes(self.node_id, 33)
|
|
vds.write_bytes(bfh(self.funding_txid), 32)
|
|
vds.write_int16(self.funding_index)
|
|
vds.write_string(self.funding_address)
|
|
vds.write_bytes(self.remote_payment_pubkey, 33)
|
|
vds.write_bytes(self.remote_revocation_pubkey, 33)
|
|
vds.write_int16(self.local_delay)
|
|
vds.write_int16(self.remote_delay)
|
|
vds.write_string(self.host)
|
|
vds.write_int16(self.port)
|
|
return bytes(vds.input)
|
|
|
|
@staticmethod
|
|
def from_bytes(s):
|
|
vds = BCDataStream()
|
|
vds.write(s)
|
|
version = vds.read_int16()
|
|
if version != CHANNEL_BACKUP_VERSION:
|
|
raise Exception(f"unknown version for channel backup: {version}")
|
|
return ChannelBackupStorage(
|
|
is_initiator = vds.read_boolean(),
|
|
privkey = vds.read_bytes(32).hex(),
|
|
channel_seed = vds.read_bytes(32).hex(),
|
|
node_id = vds.read_bytes(33).hex(),
|
|
funding_txid = vds.read_bytes(32).hex(),
|
|
funding_index = vds.read_int16(),
|
|
funding_address = vds.read_string(),
|
|
remote_payment_pubkey = vds.read_bytes(33).hex(),
|
|
remote_revocation_pubkey = vds.read_bytes(33).hex(),
|
|
local_delay = vds.read_int16(),
|
|
remote_delay = vds.read_int16(),
|
|
host = vds.read_string(),
|
|
port = vds.read_int16())
|
|
|
|
|
|
|
|
class ScriptHtlc(NamedTuple):
|
|
redeem_script: bytes
|
|
htlc: 'UpdateAddHtlc'
|
|
|
|
|
|
# FIXME duplicate of TxOutpoint in transaction.py??
|
|
@attr.s
|
|
class Outpoint(StoredObject):
|
|
txid = attr.ib(type=str)
|
|
output_index = attr.ib(type=int)
|
|
|
|
def to_str(self):
|
|
return "{}:{}".format(self.txid, self.output_index)
|
|
|
|
|
|
class PaymentAttemptFailureDetails(NamedTuple):
|
|
sender_idx: Optional[int]
|
|
failure_msg: 'OnionRoutingFailureMessage'
|
|
is_blacklisted: bool
|
|
|
|
|
|
class PaymentAttemptLog(NamedTuple):
|
|
success: bool
|
|
route: Optional['LNPaymentRoute'] = None
|
|
preimage: Optional[bytes] = None
|
|
failure_details: Optional[PaymentAttemptFailureDetails] = None
|
|
exception: Optional[Exception] = None
|
|
|
|
def formatted_tuple(self):
|
|
if not self.exception:
|
|
route = self.route
|
|
route_str = '%d'%len(route)
|
|
short_channel_id = None
|
|
if not self.success:
|
|
sender_idx = self.failure_details.sender_idx
|
|
failure_msg = self.failure_details.failure_msg
|
|
if sender_idx is not None:
|
|
try:
|
|
short_channel_id = route[sender_idx + 1].short_channel_id
|
|
except IndexError:
|
|
# payment destination reported error
|
|
short_channel_id = _("Destination node")
|
|
message = failure_msg.code_name()
|
|
else:
|
|
short_channel_id = route[-1].short_channel_id
|
|
message = _('Success')
|
|
chan_str = str(short_channel_id) if short_channel_id else _("Unknown")
|
|
else:
|
|
route_str = 'None'
|
|
chan_str = 'N/A'
|
|
message = str(self.exception)
|
|
return route_str, chan_str, message
|
|
|
|
|
|
class BarePaymentAttemptLog(NamedTuple):
|
|
success: bool
|
|
preimage: Optional[bytes] = None
|
|
error_bytes: Optional[bytes] = None
|
|
failure_message: Optional['OnionRoutingFailureMessage'] = None
|
|
|
|
|
|
class LightningError(Exception): pass
|
|
class LightningPeerConnectionClosed(LightningError): pass
|
|
class UnableToDeriveSecret(LightningError): pass
|
|
class HandshakeFailed(LightningError): pass
|
|
class ConnStringFormatError(LightningError): pass
|
|
class RemoteMisbehaving(LightningError): pass
|
|
class UpfrontShutdownScriptViolation(RemoteMisbehaving): pass
|
|
|
|
class NotFoundChanAnnouncementForUpdate(Exception): pass
|
|
|
|
class PaymentFailure(UserFacingException): pass
|
|
|
|
# TODO make some of these values configurable?
|
|
REDEEM_AFTER_DOUBLE_SPENT_DELAY = 30
|
|
|
|
CHANNEL_OPENING_TIMEOUT = 24*60*60
|
|
|
|
MIN_FUNDING_SAT = 200_000
|
|
|
|
##### CLTV-expiry-delta-related values
|
|
# see https://github.com/lightningnetwork/lightning-rfc/blob/master/02-peer-protocol.md#cltv_expiry_delta-selection
|
|
|
|
# the minimum cltv_expiry accepted for newly received HTLCs
|
|
# note: when changing, consider Blockchain.is_tip_stale()
|
|
MIN_FINAL_CLTV_EXPIRY_ACCEPTED = 144
|
|
# set it a tiny bit higher for invoices as blocks could get mined
|
|
# during forward path of payment
|
|
MIN_FINAL_CLTV_EXPIRY_FOR_INVOICE = MIN_FINAL_CLTV_EXPIRY_ACCEPTED + 3
|
|
|
|
# the deadline for offered HTLCs:
|
|
# the deadline after which the channel has to be failed and timed out on-chain
|
|
NBLOCK_DEADLINE_AFTER_EXPIRY_FOR_OFFERED_HTLCS = 1
|
|
|
|
# the deadline for received HTLCs this node has fulfilled:
|
|
# the deadline after which the channel has to be failed and the HTLC fulfilled on-chain before its cltv_expiry
|
|
NBLOCK_DEADLINE_BEFORE_EXPIRY_FOR_RECEIVED_HTLCS = 72
|
|
|
|
# the cltv_expiry_delta for channels when we are forwarding payments
|
|
NBLOCK_OUR_CLTV_EXPIRY_DELTA = 144
|
|
OUR_FEE_BASE_MSAT = 1000
|
|
OUR_FEE_PROPORTIONAL_MILLIONTHS = 1
|
|
|
|
NBLOCK_CLTV_EXPIRY_TOO_FAR_INTO_FUTURE = 28 * 144
|
|
|
|
MAXIMUM_REMOTE_TO_SELF_DELAY_ACCEPTED = 2016
|
|
|
|
class RevocationStore:
|
|
# closely based on code in lightningnetwork/lnd
|
|
|
|
START_INDEX = 2 ** 48 - 1
|
|
|
|
def __init__(self, storage):
|
|
if len(storage) == 0:
|
|
storage['index'] = self.START_INDEX
|
|
storage['buckets'] = {}
|
|
self.storage = storage
|
|
self.buckets = storage['buckets']
|
|
|
|
def add_next_entry(self, hsh):
|
|
index = self.storage['index']
|
|
new_element = ShachainElement(index=index, secret=hsh)
|
|
bucket = count_trailing_zeros(index)
|
|
for i in range(0, bucket):
|
|
this_bucket = self.buckets[i]
|
|
e = shachain_derive(new_element, this_bucket.index)
|
|
if e != this_bucket:
|
|
raise Exception("hash is not derivable: {} {} {}".format(bh2u(e.secret), bh2u(this_bucket.secret), this_bucket.index))
|
|
self.buckets[bucket] = new_element
|
|
self.storage['index'] = index - 1
|
|
|
|
def retrieve_secret(self, index: int) -> bytes:
|
|
assert index <= self.START_INDEX, index
|
|
for i in range(0, 49):
|
|
bucket = self.buckets.get(i)
|
|
if bucket is None:
|
|
raise UnableToDeriveSecret()
|
|
try:
|
|
element = shachain_derive(bucket, index)
|
|
except UnableToDeriveSecret:
|
|
continue
|
|
return element.secret
|
|
raise UnableToDeriveSecret()
|
|
|
|
def __eq__(self, o):
|
|
return type(o) is RevocationStore and self.serialize() == o.serialize()
|
|
|
|
def __hash__(self):
|
|
return hash(json.dumps(self.serialize(), sort_keys=True))
|
|
|
|
|
|
def count_trailing_zeros(index):
|
|
""" BOLT-03 (where_to_put_secret) """
|
|
try:
|
|
return list(reversed(bin(index)[2:])).index("1")
|
|
except ValueError:
|
|
return 48
|
|
|
|
def shachain_derive(element, to_index):
|
|
def get_prefix(index, pos):
|
|
mask = (1 << 64) - 1 - ((1 << pos) - 1)
|
|
return index & mask
|
|
from_index = element.index
|
|
zeros = count_trailing_zeros(from_index)
|
|
if from_index != get_prefix(to_index, zeros):
|
|
raise UnableToDeriveSecret("prefixes are different; index not derivable")
|
|
return ShachainElement(
|
|
get_per_commitment_secret_from_seed(element.secret, to_index, zeros),
|
|
to_index)
|
|
|
|
ShachainElement = namedtuple("ShachainElement", ["secret", "index"])
|
|
ShachainElement.__str__ = lambda self: "ShachainElement(" + bh2u(self.secret) + "," + str(self.index) + ")"
|
|
|
|
def get_per_commitment_secret_from_seed(seed: bytes, i: int, bits: int = 48) -> bytes:
|
|
"""Generate per commitment secret."""
|
|
per_commitment_secret = bytearray(seed)
|
|
for bitindex in range(bits - 1, -1, -1):
|
|
mask = 1 << bitindex
|
|
if i & mask:
|
|
per_commitment_secret[bitindex // 8] ^= 1 << (bitindex % 8)
|
|
per_commitment_secret = bytearray(sha256(per_commitment_secret))
|
|
bajts = bytes(per_commitment_secret)
|
|
return bajts
|
|
|
|
def secret_to_pubkey(secret: int) -> bytes:
|
|
assert type(secret) is int
|
|
return ecc.ECPrivkey.from_secret_scalar(secret).get_public_key_bytes(compressed=True)
|
|
|
|
def privkey_to_pubkey(priv: bytes) -> bytes:
|
|
return ecc.ECPrivkey(priv[:32]).get_public_key_bytes()
|
|
|
|
def derive_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:
|
|
p = ecc.ECPubkey(basepoint) + ecc.GENERATOR * ecc.string_to_number(sha256(per_commitment_point + basepoint))
|
|
return p.get_public_key_bytes()
|
|
|
|
def derive_privkey(secret: int, per_commitment_point: bytes) -> int:
|
|
assert type(secret) is int
|
|
basepoint_bytes = secret_to_pubkey(secret)
|
|
basepoint = secret + ecc.string_to_number(sha256(per_commitment_point + basepoint_bytes))
|
|
basepoint %= CURVE_ORDER
|
|
return basepoint
|
|
|
|
def derive_blinded_pubkey(basepoint: bytes, per_commitment_point: bytes) -> bytes:
|
|
k1 = ecc.ECPubkey(basepoint) * ecc.string_to_number(sha256(basepoint + per_commitment_point))
|
|
k2 = ecc.ECPubkey(per_commitment_point) * ecc.string_to_number(sha256(per_commitment_point + basepoint))
|
|
return (k1 + k2).get_public_key_bytes()
|
|
|
|
def derive_blinded_privkey(basepoint_secret: bytes, per_commitment_secret: bytes) -> bytes:
|
|
basepoint = ecc.ECPrivkey(basepoint_secret).get_public_key_bytes(compressed=True)
|
|
per_commitment_point = ecc.ECPrivkey(per_commitment_secret).get_public_key_bytes(compressed=True)
|
|
k1 = ecc.string_to_number(basepoint_secret) * ecc.string_to_number(sha256(basepoint + per_commitment_point))
|
|
k2 = ecc.string_to_number(per_commitment_secret) * ecc.string_to_number(sha256(per_commitment_point + basepoint))
|
|
sum = (k1 + k2) % ecc.CURVE_ORDER
|
|
return int.to_bytes(sum, length=32, byteorder='big', signed=False)
|
|
|
|
|
|
def make_htlc_tx_output(amount_msat, local_feerate, revocationpubkey, local_delayedpubkey, success, to_self_delay):
|
|
assert type(amount_msat) is int
|
|
assert type(local_feerate) is int
|
|
assert type(revocationpubkey) is bytes
|
|
assert type(local_delayedpubkey) is bytes
|
|
script = bfh(construct_script([
|
|
opcodes.OP_IF,
|
|
revocationpubkey,
|
|
opcodes.OP_ELSE,
|
|
to_self_delay,
|
|
opcodes.OP_CHECKSEQUENCEVERIFY,
|
|
opcodes.OP_DROP,
|
|
local_delayedpubkey,
|
|
opcodes.OP_ENDIF,
|
|
opcodes.OP_CHECKSIG,
|
|
]))
|
|
|
|
p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
|
|
weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT
|
|
fee = local_feerate * weight
|
|
fee = fee // 1000 * 1000
|
|
final_amount_sat = (amount_msat - fee) // 1000
|
|
assert final_amount_sat > 0, final_amount_sat
|
|
output = PartialTxOutput.from_address_and_value(p2wsh, final_amount_sat)
|
|
return script, output
|
|
|
|
def make_htlc_tx_witness(remotehtlcsig: bytes, localhtlcsig: bytes,
|
|
payment_preimage: bytes, witness_script: bytes) -> bytes:
|
|
assert type(remotehtlcsig) is bytes
|
|
assert type(localhtlcsig) is bytes
|
|
assert type(payment_preimage) is bytes
|
|
assert type(witness_script) is bytes
|
|
return bfh(construct_witness([0, remotehtlcsig, localhtlcsig, payment_preimage, witness_script]))
|
|
|
|
def make_htlc_tx_inputs(htlc_output_txid: str, htlc_output_index: int,
|
|
amount_msat: int, witness_script: str) -> List[PartialTxInput]:
|
|
assert type(htlc_output_txid) is str
|
|
assert type(htlc_output_index) is int
|
|
assert type(amount_msat) is int
|
|
assert type(witness_script) is str
|
|
txin = PartialTxInput(prevout=TxOutpoint(txid=bfh(htlc_output_txid), out_idx=htlc_output_index),
|
|
nsequence=0)
|
|
txin.witness_script = bfh(witness_script)
|
|
txin.script_sig = b''
|
|
txin._trusted_value_sats = amount_msat // 1000
|
|
c_inputs = [txin]
|
|
return c_inputs
|
|
|
|
def make_htlc_tx(*, cltv_expiry: int, inputs: List[PartialTxInput], output: PartialTxOutput) -> PartialTransaction:
|
|
assert type(cltv_expiry) is int
|
|
c_outputs = [output]
|
|
tx = PartialTransaction.from_io(inputs, c_outputs, locktime=cltv_expiry, version=2)
|
|
return tx
|
|
|
|
def make_offered_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
|
|
local_htlcpubkey: bytes, payment_hash: bytes) -> bytes:
|
|
assert type(revocation_pubkey) is bytes
|
|
assert type(remote_htlcpubkey) is bytes
|
|
assert type(local_htlcpubkey) is bytes
|
|
assert type(payment_hash) is bytes
|
|
script = bfh(construct_script([
|
|
opcodes.OP_DUP,
|
|
opcodes.OP_HASH160,
|
|
bitcoin.hash_160(revocation_pubkey),
|
|
opcodes.OP_EQUAL,
|
|
opcodes.OP_IF,
|
|
opcodes.OP_CHECKSIG,
|
|
opcodes.OP_ELSE,
|
|
remote_htlcpubkey,
|
|
opcodes.OP_SWAP,
|
|
opcodes.OP_SIZE,
|
|
32,
|
|
opcodes.OP_EQUAL,
|
|
opcodes.OP_NOTIF,
|
|
opcodes.OP_DROP,
|
|
2,
|
|
opcodes.OP_SWAP,
|
|
local_htlcpubkey,
|
|
2,
|
|
opcodes.OP_CHECKMULTISIG,
|
|
opcodes.OP_ELSE,
|
|
opcodes.OP_HASH160,
|
|
crypto.ripemd(payment_hash),
|
|
opcodes.OP_EQUALVERIFY,
|
|
opcodes.OP_CHECKSIG,
|
|
opcodes.OP_ENDIF,
|
|
opcodes.OP_ENDIF,
|
|
]))
|
|
return script
|
|
|
|
def make_received_htlc(revocation_pubkey: bytes, remote_htlcpubkey: bytes,
|
|
local_htlcpubkey: bytes, payment_hash: bytes, cltv_expiry: int) -> bytes:
|
|
for i in [revocation_pubkey, remote_htlcpubkey, local_htlcpubkey, payment_hash]:
|
|
assert type(i) is bytes
|
|
assert type(cltv_expiry) is int
|
|
|
|
script = bfh(construct_script([
|
|
opcodes.OP_DUP,
|
|
opcodes.OP_HASH160,
|
|
bitcoin.hash_160(revocation_pubkey),
|
|
opcodes.OP_EQUAL,
|
|
opcodes.OP_IF,
|
|
opcodes.OP_CHECKSIG,
|
|
opcodes.OP_ELSE,
|
|
remote_htlcpubkey,
|
|
opcodes.OP_SWAP,
|
|
opcodes.OP_SIZE,
|
|
32,
|
|
opcodes.OP_EQUAL,
|
|
opcodes.OP_IF,
|
|
opcodes.OP_HASH160,
|
|
crypto.ripemd(payment_hash),
|
|
opcodes.OP_EQUALVERIFY,
|
|
2,
|
|
opcodes.OP_SWAP,
|
|
local_htlcpubkey,
|
|
2,
|
|
opcodes.OP_CHECKMULTISIG,
|
|
opcodes.OP_ELSE,
|
|
opcodes.OP_DROP,
|
|
cltv_expiry,
|
|
opcodes.OP_CHECKLOCKTIMEVERIFY,
|
|
opcodes.OP_DROP,
|
|
opcodes.OP_CHECKSIG,
|
|
opcodes.OP_ENDIF,
|
|
opcodes.OP_ENDIF,
|
|
]))
|
|
return script
|
|
|
|
def make_htlc_output_witness_script(is_received_htlc: bool, remote_revocation_pubkey: bytes, remote_htlc_pubkey: bytes,
|
|
local_htlc_pubkey: bytes, payment_hash: bytes, cltv_expiry: Optional[int]) -> bytes:
|
|
if is_received_htlc:
|
|
return make_received_htlc(revocation_pubkey=remote_revocation_pubkey,
|
|
remote_htlcpubkey=remote_htlc_pubkey,
|
|
local_htlcpubkey=local_htlc_pubkey,
|
|
payment_hash=payment_hash,
|
|
cltv_expiry=cltv_expiry)
|
|
else:
|
|
return make_offered_htlc(revocation_pubkey=remote_revocation_pubkey,
|
|
remote_htlcpubkey=remote_htlc_pubkey,
|
|
local_htlcpubkey=local_htlc_pubkey,
|
|
payment_hash=payment_hash)
|
|
|
|
|
|
def get_ordered_channel_configs(chan: 'AbstractChannel', for_us: bool) -> Tuple[Union[LocalConfig, RemoteConfig],
|
|
Union[LocalConfig, RemoteConfig]]:
|
|
conf = chan.config[LOCAL] if for_us else chan.config[REMOTE]
|
|
other_conf = chan.config[LOCAL] if not for_us else chan.config[REMOTE]
|
|
return conf, other_conf
|
|
|
|
|
|
def possible_output_idxs_of_htlc_in_ctx(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner',
|
|
htlc_direction: 'Direction', ctx: Transaction,
|
|
htlc: 'UpdateAddHtlc') -> Set[int]:
|
|
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
|
|
for_us = subject == LOCAL
|
|
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
|
|
|
|
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
|
|
other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)
|
|
htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp)
|
|
preimage_script = make_htlc_output_witness_script(is_received_htlc=htlc_direction == RECEIVED,
|
|
remote_revocation_pubkey=other_revocation_pubkey,
|
|
remote_htlc_pubkey=other_htlc_pubkey,
|
|
local_htlc_pubkey=htlc_pubkey,
|
|
payment_hash=payment_hash,
|
|
cltv_expiry=cltv_expiry)
|
|
htlc_address = redeem_script_to_address('p2wsh', bh2u(preimage_script))
|
|
candidates = ctx.get_output_idxs_from_address(htlc_address)
|
|
return {output_idx for output_idx in candidates
|
|
if ctx.outputs()[output_idx].value == htlc.amount_msat // 1000}
|
|
|
|
|
|
def map_htlcs_to_ctx_output_idxs(*, chan: 'Channel', ctx: Transaction, pcp: bytes,
|
|
subject: 'HTLCOwner', ctn: int) -> Dict[Tuple['Direction', 'UpdateAddHtlc'], Tuple[int, int]]:
|
|
"""Returns a dict from (htlc_dir, htlc) to (ctx_output_idx, htlc_relative_idx)"""
|
|
htlc_to_ctx_output_idx_map = {} # type: Dict[Tuple[Direction, UpdateAddHtlc], int]
|
|
unclaimed_ctx_output_idxs = set(range(len(ctx.outputs())))
|
|
offered_htlcs = chan.included_htlcs(subject, SENT, ctn=ctn)
|
|
offered_htlcs.sort(key=lambda htlc: htlc.cltv_expiry)
|
|
received_htlcs = chan.included_htlcs(subject, RECEIVED, ctn=ctn)
|
|
received_htlcs.sort(key=lambda htlc: htlc.cltv_expiry)
|
|
for direction, htlcs in zip([SENT, RECEIVED], [offered_htlcs, received_htlcs]):
|
|
for htlc in htlcs:
|
|
cands = sorted(possible_output_idxs_of_htlc_in_ctx(chan=chan,
|
|
pcp=pcp,
|
|
subject=subject,
|
|
htlc_direction=direction,
|
|
ctx=ctx,
|
|
htlc=htlc))
|
|
for ctx_output_idx in cands:
|
|
if ctx_output_idx in unclaimed_ctx_output_idxs:
|
|
unclaimed_ctx_output_idxs.discard(ctx_output_idx)
|
|
htlc_to_ctx_output_idx_map[(direction, htlc)] = ctx_output_idx
|
|
break
|
|
# calc htlc_relative_idx
|
|
inverse_map = {ctx_output_idx: (direction, htlc)
|
|
for ((direction, htlc), ctx_output_idx) in htlc_to_ctx_output_idx_map.items()}
|
|
|
|
return {inverse_map[ctx_output_idx]: (ctx_output_idx, htlc_relative_idx)
|
|
for htlc_relative_idx, ctx_output_idx in enumerate(sorted(inverse_map))}
|
|
|
|
|
|
def make_htlc_tx_with_open_channel(*, chan: 'Channel', pcp: bytes, subject: 'HTLCOwner', ctn: int,
|
|
htlc_direction: 'Direction', commit: Transaction, ctx_output_idx: int,
|
|
htlc: 'UpdateAddHtlc', name: str = None) -> Tuple[bytes, PartialTransaction]:
|
|
amount_msat, cltv_expiry, payment_hash = htlc.amount_msat, htlc.cltv_expiry, htlc.payment_hash
|
|
for_us = subject == LOCAL
|
|
conf, other_conf = get_ordered_channel_configs(chan=chan, for_us=for_us)
|
|
|
|
delayedpubkey = derive_pubkey(conf.delayed_basepoint.pubkey, pcp)
|
|
other_revocation_pubkey = derive_blinded_pubkey(other_conf.revocation_basepoint.pubkey, pcp)
|
|
other_htlc_pubkey = derive_pubkey(other_conf.htlc_basepoint.pubkey, pcp)
|
|
htlc_pubkey = derive_pubkey(conf.htlc_basepoint.pubkey, pcp)
|
|
# HTLC-success for the HTLC spending from a received HTLC output
|
|
# if we do not receive, and the commitment tx is not for us, they receive, so it is also an HTLC-success
|
|
is_htlc_success = htlc_direction == RECEIVED
|
|
witness_script_of_htlc_tx_output, htlc_tx_output = make_htlc_tx_output(
|
|
amount_msat = amount_msat,
|
|
local_feerate = chan.get_feerate(subject, ctn=ctn),
|
|
revocationpubkey=other_revocation_pubkey,
|
|
local_delayedpubkey=delayedpubkey,
|
|
success = is_htlc_success,
|
|
to_self_delay = other_conf.to_self_delay)
|
|
preimage_script = make_htlc_output_witness_script(is_received_htlc=is_htlc_success,
|
|
remote_revocation_pubkey=other_revocation_pubkey,
|
|
remote_htlc_pubkey=other_htlc_pubkey,
|
|
local_htlc_pubkey=htlc_pubkey,
|
|
payment_hash=payment_hash,
|
|
cltv_expiry=cltv_expiry)
|
|
htlc_tx_inputs = make_htlc_tx_inputs(
|
|
commit.txid(), ctx_output_idx,
|
|
amount_msat=amount_msat,
|
|
witness_script=bh2u(preimage_script))
|
|
if is_htlc_success:
|
|
cltv_expiry = 0
|
|
htlc_tx = make_htlc_tx(cltv_expiry=cltv_expiry, inputs=htlc_tx_inputs, output=htlc_tx_output)
|
|
return witness_script_of_htlc_tx_output, htlc_tx
|
|
|
|
def make_funding_input(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
|
|
funding_pos: int, funding_txid: str, funding_sat: int) -> PartialTxInput:
|
|
pubkeys = sorted([bh2u(local_funding_pubkey), bh2u(remote_funding_pubkey)])
|
|
# commitment tx input
|
|
prevout = TxOutpoint(txid=bfh(funding_txid), out_idx=funding_pos)
|
|
c_input = PartialTxInput(prevout=prevout)
|
|
c_input.script_type = 'p2wsh'
|
|
c_input.pubkeys = [bfh(pk) for pk in pubkeys]
|
|
c_input.num_sig = 2
|
|
c_input._trusted_value_sats = funding_sat
|
|
return c_input
|
|
|
|
class HTLCOwner(IntFlag):
|
|
LOCAL = 1
|
|
REMOTE = -LOCAL
|
|
|
|
def inverted(self) -> 'HTLCOwner':
|
|
return -self
|
|
|
|
def __neg__(self) -> 'HTLCOwner':
|
|
return HTLCOwner(super().__neg__())
|
|
|
|
|
|
class Direction(IntFlag):
|
|
SENT = -1 # in the context of HTLCs: "offered" HTLCs
|
|
RECEIVED = 1 # in the context of HTLCs: "received" HTLCs
|
|
|
|
SENT = Direction.SENT
|
|
RECEIVED = Direction.RECEIVED
|
|
|
|
LOCAL = HTLCOwner.LOCAL
|
|
REMOTE = HTLCOwner.REMOTE
|
|
|
|
def make_commitment_outputs(*, fees_per_participant: Mapping[HTLCOwner, int], local_amount_msat: int, remote_amount_msat: int,
|
|
local_script: str, remote_script: str, htlcs: List[ScriptHtlc], dust_limit_sat: int) -> Tuple[List[PartialTxOutput], List[PartialTxOutput]]:
|
|
# BOLT-03: "Base commitment transaction fees are extracted from the funder's amount;
|
|
# if that amount is insufficient, the entire amount of the funder's output is used."
|
|
# -> if funder cannot afford feerate, their output might go negative, so take max(0, x) here:
|
|
to_local_amt = max(0, local_amount_msat - fees_per_participant[LOCAL])
|
|
to_local = PartialTxOutput(scriptpubkey=bfh(local_script), value=to_local_amt // 1000)
|
|
to_remote_amt = max(0, remote_amount_msat - fees_per_participant[REMOTE])
|
|
to_remote = PartialTxOutput(scriptpubkey=bfh(remote_script), value=to_remote_amt // 1000)
|
|
|
|
non_htlc_outputs = [to_local, to_remote]
|
|
htlc_outputs = []
|
|
for script, htlc in htlcs:
|
|
addr = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
|
|
htlc_outputs.append(PartialTxOutput(scriptpubkey=bfh(address_to_script(addr)),
|
|
value=htlc.amount_msat // 1000))
|
|
|
|
# trim outputs
|
|
c_outputs_filtered = list(filter(lambda x: x.value >= dust_limit_sat, non_htlc_outputs + htlc_outputs))
|
|
return htlc_outputs, c_outputs_filtered
|
|
|
|
|
|
def offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int:
|
|
# offered htlcs strictly below this amount will be trimmed (from ctx).
|
|
# feerate is in sat/kw
|
|
# returns value in sat
|
|
weight = HTLC_TIMEOUT_WEIGHT
|
|
return dust_limit_sat + weight * feerate // 1000
|
|
|
|
|
|
def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int) -> int:
|
|
# received htlcs strictly below this amount will be trimmed (from ctx).
|
|
# feerate is in sat/kw
|
|
# returns value in sat
|
|
weight = HTLC_SUCCESS_WEIGHT
|
|
return dust_limit_sat + weight * feerate // 1000
|
|
|
|
|
|
def fee_for_htlc_output(*, feerate: int) -> int:
|
|
# feerate is in sat/kw
|
|
# returns fee in msat
|
|
return feerate * HTLC_OUTPUT_WEIGHT
|
|
|
|
|
|
def calc_fees_for_commitment_tx(*, num_htlcs: int, feerate: int,
|
|
is_local_initiator: bool, round_to_sat: bool = True) -> Dict['HTLCOwner', int]:
|
|
# feerate is in sat/kw
|
|
# returns fees in msats
|
|
# note: BOLT-02 specifies that msat fees need to be rounded down to sat.
|
|
# However, the rounding needs to happen for the total fees, so if the return value
|
|
# is to be used as part of additional fee calculation then rounding should be done after that.
|
|
overall_weight = COMMITMENT_TX_WEIGHT + num_htlcs * HTLC_OUTPUT_WEIGHT
|
|
fee = feerate * overall_weight
|
|
if round_to_sat:
|
|
fee = fee // 1000 * 1000
|
|
return {
|
|
LOCAL: fee if is_local_initiator else 0,
|
|
REMOTE: fee if not is_local_initiator else 0,
|
|
}
|
|
|
|
|
|
def make_commitment(
|
|
*,
|
|
ctn: int,
|
|
local_funding_pubkey: bytes,
|
|
remote_funding_pubkey: bytes,
|
|
remote_payment_pubkey: bytes,
|
|
funder_payment_basepoint: bytes,
|
|
fundee_payment_basepoint: bytes,
|
|
revocation_pubkey: bytes,
|
|
delayed_pubkey: bytes,
|
|
to_self_delay: int,
|
|
funding_txid: str,
|
|
funding_pos: int,
|
|
funding_sat: int,
|
|
local_amount: int,
|
|
remote_amount: int,
|
|
dust_limit_sat: int,
|
|
fees_per_participant: Mapping[HTLCOwner, int],
|
|
htlcs: List[ScriptHtlc]
|
|
) -> PartialTransaction:
|
|
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
|
|
funding_pos, funding_txid, funding_sat)
|
|
obs = get_obscured_ctn(ctn, funder_payment_basepoint, fundee_payment_basepoint)
|
|
locktime = (0x20 << 24) + (obs & 0xffffff)
|
|
sequence = (0x80 << 24) + (obs >> 24)
|
|
c_input.nsequence = sequence
|
|
|
|
c_inputs = [c_input]
|
|
|
|
# commitment tx outputs
|
|
local_address = make_commitment_output_to_local_address(revocation_pubkey, to_self_delay, delayed_pubkey)
|
|
remote_address = make_commitment_output_to_remote_address(remote_payment_pubkey)
|
|
# note: it is assumed that the given 'htlcs' are all non-dust (dust htlcs already trimmed)
|
|
|
|
# BOLT-03: "Transaction Input and Output Ordering
|
|
# Lexicographic ordering: see BIP69. In the case of identical HTLC outputs,
|
|
# the outputs are ordered in increasing cltv_expiry order."
|
|
# so we sort by cltv_expiry now; and the later BIP69-sort is assumed to be *stable*
|
|
htlcs = list(htlcs)
|
|
htlcs.sort(key=lambda x: x.htlc.cltv_expiry)
|
|
|
|
htlc_outputs, c_outputs_filtered = make_commitment_outputs(
|
|
fees_per_participant=fees_per_participant,
|
|
local_amount_msat=local_amount,
|
|
remote_amount_msat=remote_amount,
|
|
local_script=address_to_script(local_address),
|
|
remote_script=address_to_script(remote_address),
|
|
htlcs=htlcs,
|
|
dust_limit_sat=dust_limit_sat)
|
|
|
|
assert sum(x.value for x in c_outputs_filtered) <= funding_sat, (c_outputs_filtered, funding_sat)
|
|
|
|
# create commitment tx
|
|
tx = PartialTransaction.from_io(c_inputs, c_outputs_filtered, locktime=locktime, version=2)
|
|
return tx
|
|
|
|
def make_commitment_output_to_local_witness_script(
|
|
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> bytes:
|
|
script = bfh(construct_script([
|
|
opcodes.OP_IF,
|
|
revocation_pubkey,
|
|
opcodes.OP_ELSE,
|
|
to_self_delay,
|
|
opcodes.OP_CHECKSEQUENCEVERIFY,
|
|
opcodes.OP_DROP,
|
|
delayed_pubkey,
|
|
opcodes.OP_ENDIF,
|
|
opcodes.OP_CHECKSIG,
|
|
]))
|
|
return script
|
|
|
|
def make_commitment_output_to_local_address(
|
|
revocation_pubkey: bytes, to_self_delay: int, delayed_pubkey: bytes) -> str:
|
|
local_script = make_commitment_output_to_local_witness_script(revocation_pubkey, to_self_delay, delayed_pubkey)
|
|
return bitcoin.redeem_script_to_address('p2wsh', bh2u(local_script))
|
|
|
|
def make_commitment_output_to_remote_address(remote_payment_pubkey: bytes) -> str:
|
|
return bitcoin.pubkey_to_address('p2wpkh', bh2u(remote_payment_pubkey))
|
|
|
|
def sign_and_get_sig_string(tx: PartialTransaction, local_config, remote_config):
|
|
tx.sign({bh2u(local_config.multisig_key.pubkey): (local_config.multisig_key.privkey, True)})
|
|
sig = tx.inputs()[0].part_sigs[local_config.multisig_key.pubkey]
|
|
sig_64 = sig_string_from_der_sig(sig[:-1])
|
|
return sig_64
|
|
|
|
def funding_output_script(local_config, remote_config) -> str:
|
|
return funding_output_script_from_keys(local_config.multisig_key.pubkey, remote_config.multisig_key.pubkey)
|
|
|
|
def funding_output_script_from_keys(pubkey1: bytes, pubkey2: bytes) -> str:
|
|
pubkeys = sorted([bh2u(pubkey1), bh2u(pubkey2)])
|
|
return transaction.multisig_script(pubkeys, 2)
|
|
|
|
|
|
def get_obscured_ctn(ctn: int, funder: bytes, fundee: bytes) -> int:
|
|
mask = int.from_bytes(sha256(funder + fundee)[-6:], 'big')
|
|
return ctn ^ mask
|
|
|
|
def extract_ctn_from_tx(tx: Transaction, txin_index: int, funder_payment_basepoint: bytes,
|
|
fundee_payment_basepoint: bytes) -> int:
|
|
tx.deserialize()
|
|
locktime = tx.locktime
|
|
sequence = tx.inputs()[txin_index].nsequence
|
|
obs = ((sequence & 0xffffff) << 24) + (locktime & 0xffffff)
|
|
return get_obscured_ctn(obs, funder_payment_basepoint, fundee_payment_basepoint)
|
|
|
|
def extract_ctn_from_tx_and_chan(tx: Transaction, chan: 'AbstractChannel') -> int:
|
|
funder_conf = chan.config[LOCAL] if chan.is_initiator() else chan.config[REMOTE]
|
|
fundee_conf = chan.config[LOCAL] if not chan.is_initiator() else chan.config[REMOTE]
|
|
return extract_ctn_from_tx(tx, txin_index=0,
|
|
funder_payment_basepoint=funder_conf.payment_basepoint.pubkey,
|
|
fundee_payment_basepoint=fundee_conf.payment_basepoint.pubkey)
|
|
|
|
def get_ecdh(priv: bytes, pub: bytes) -> bytes:
|
|
pt = ECPubkey(pub) * string_to_number(priv)
|
|
return sha256(pt.get_public_key_bytes())
|
|
|
|
|
|
class LnFeatureContexts(enum.Flag):
|
|
INIT = enum.auto()
|
|
NODE_ANN = enum.auto()
|
|
CHAN_ANN_AS_IS = enum.auto()
|
|
CHAN_ANN_ALWAYS_ODD = enum.auto()
|
|
CHAN_ANN_ALWAYS_EVEN = enum.auto()
|
|
INVOICE = enum.auto()
|
|
|
|
LNFC = LnFeatureContexts
|
|
|
|
_ln_feature_direct_dependencies = defaultdict(set) # type: Dict[LnFeatures, Set[LnFeatures]]
|
|
_ln_feature_contexts = {} # type: Dict[LnFeatures, LnFeatureContexts]
|
|
|
|
class LnFeatures(IntFlag):
|
|
OPTION_DATA_LOSS_PROTECT_REQ = 1 << 0
|
|
OPTION_DATA_LOSS_PROTECT_OPT = 1 << 1
|
|
_ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_OPT] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
|
|
_ln_feature_contexts[OPTION_DATA_LOSS_PROTECT_REQ] = (LNFC.INIT | LnFeatureContexts.NODE_ANN)
|
|
|
|
INITIAL_ROUTING_SYNC = 1 << 3
|
|
_ln_feature_contexts[INITIAL_ROUTING_SYNC] = LNFC.INIT
|
|
|
|
OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ = 1 << 4
|
|
OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT = 1 << 5
|
|
_ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
_ln_feature_contexts[OPTION_UPFRONT_SHUTDOWN_SCRIPT_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
|
|
GOSSIP_QUERIES_REQ = 1 << 6
|
|
GOSSIP_QUERIES_OPT = 1 << 7
|
|
_ln_feature_contexts[GOSSIP_QUERIES_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
_ln_feature_contexts[GOSSIP_QUERIES_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
|
|
VAR_ONION_REQ = 1 << 8
|
|
VAR_ONION_OPT = 1 << 9
|
|
_ln_feature_contexts[VAR_ONION_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
_ln_feature_contexts[VAR_ONION_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
|
|
GOSSIP_QUERIES_EX_REQ = 1 << 10
|
|
GOSSIP_QUERIES_EX_OPT = 1 << 11
|
|
_ln_feature_direct_dependencies[GOSSIP_QUERIES_EX_OPT] = {GOSSIP_QUERIES_OPT}
|
|
_ln_feature_contexts[GOSSIP_QUERIES_EX_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
_ln_feature_contexts[GOSSIP_QUERIES_EX_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
|
|
OPTION_STATIC_REMOTEKEY_REQ = 1 << 12
|
|
OPTION_STATIC_REMOTEKEY_OPT = 1 << 13
|
|
_ln_feature_contexts[OPTION_STATIC_REMOTEKEY_OPT] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
_ln_feature_contexts[OPTION_STATIC_REMOTEKEY_REQ] = (LNFC.INIT | LNFC.NODE_ANN)
|
|
|
|
PAYMENT_SECRET_REQ = 1 << 14
|
|
PAYMENT_SECRET_OPT = 1 << 15
|
|
_ln_feature_direct_dependencies[PAYMENT_SECRET_OPT] = {VAR_ONION_OPT}
|
|
_ln_feature_contexts[PAYMENT_SECRET_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
_ln_feature_contexts[PAYMENT_SECRET_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
|
|
BASIC_MPP_REQ = 1 << 16
|
|
BASIC_MPP_OPT = 1 << 17
|
|
_ln_feature_direct_dependencies[BASIC_MPP_OPT] = {PAYMENT_SECRET_OPT}
|
|
_ln_feature_contexts[BASIC_MPP_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
_ln_feature_contexts[BASIC_MPP_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.INVOICE)
|
|
|
|
OPTION_SUPPORT_LARGE_CHANNEL_REQ = 1 << 18
|
|
OPTION_SUPPORT_LARGE_CHANNEL_OPT = 1 << 19
|
|
_ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_OPT] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
|
|
_ln_feature_contexts[OPTION_SUPPORT_LARGE_CHANNEL_REQ] = (LNFC.INIT | LNFC.NODE_ANN | LNFC.CHAN_ANN_ALWAYS_EVEN)
|
|
|
|
def validate_transitive_dependencies(self) -> bool:
|
|
# for all even bit set, set corresponding odd bit:
|
|
features = self # copy
|
|
flags = list_enabled_bits(features)
|
|
for flag in flags:
|
|
if flag % 2 == 0:
|
|
features |= 1 << get_ln_flag_pair_of_bit(flag)
|
|
# Check dependencies. We only check that the direct dependencies of each flag set
|
|
# are satisfied: this implies that transitive dependencies are also satisfied.
|
|
flags = list_enabled_bits(features)
|
|
for flag in flags:
|
|
for dependency in _ln_feature_direct_dependencies[1 << flag]:
|
|
if not (dependency & features):
|
|
return False
|
|
return True
|
|
|
|
def for_init_message(self) -> 'LnFeatures':
|
|
features = LnFeatures(0)
|
|
for flag in list_enabled_bits(self):
|
|
if LnFeatureContexts.INIT & _ln_feature_contexts[1 << flag]:
|
|
features |= (1 << flag)
|
|
return features
|
|
|
|
def for_node_announcement(self) -> 'LnFeatures':
|
|
features = LnFeatures(0)
|
|
for flag in list_enabled_bits(self):
|
|
if LnFeatureContexts.NODE_ANN & _ln_feature_contexts[1 << flag]:
|
|
features |= (1 << flag)
|
|
return features
|
|
|
|
def for_invoice(self) -> 'LnFeatures':
|
|
features = LnFeatures(0)
|
|
for flag in list_enabled_bits(self):
|
|
if LnFeatureContexts.INVOICE & _ln_feature_contexts[1 << flag]:
|
|
features |= (1 << flag)
|
|
return features
|
|
|
|
def for_channel_announcement(self) -> 'LnFeatures':
|
|
features = LnFeatures(0)
|
|
for flag in list_enabled_bits(self):
|
|
ctxs = _ln_feature_contexts[1 << flag]
|
|
if LnFeatureContexts.CHAN_ANN_AS_IS & ctxs:
|
|
features |= (1 << flag)
|
|
elif LnFeatureContexts.CHAN_ANN_ALWAYS_EVEN & ctxs:
|
|
if flag % 2 == 0:
|
|
features |= (1 << flag)
|
|
elif LnFeatureContexts.CHAN_ANN_ALWAYS_ODD & ctxs:
|
|
if flag % 2 == 0:
|
|
flag = get_ln_flag_pair_of_bit(flag)
|
|
features |= (1 << flag)
|
|
return features
|
|
|
|
|
|
del LNFC # name is ambiguous without context
|
|
|
|
# features that are actually implemented and understood in our codebase:
|
|
# (note: this is not what we send in e.g. init!)
|
|
# (note: specify both OPT and REQ here)
|
|
LN_FEATURES_IMPLEMENTED = (
|
|
LnFeatures(0)
|
|
| LnFeatures.OPTION_DATA_LOSS_PROTECT_OPT | LnFeatures.OPTION_DATA_LOSS_PROTECT_REQ
|
|
| LnFeatures.GOSSIP_QUERIES_OPT | LnFeatures.GOSSIP_QUERIES_REQ
|
|
| LnFeatures.OPTION_STATIC_REMOTEKEY_OPT | LnFeatures.OPTION_STATIC_REMOTEKEY_REQ
|
|
| LnFeatures.VAR_ONION_OPT | LnFeatures.VAR_ONION_REQ
|
|
| LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ
|
|
)
|
|
|
|
|
|
def get_ln_flag_pair_of_bit(flag_bit: int) -> int:
|
|
"""Ln Feature flags are assigned in pairs, one even, one odd. See BOLT-09.
|
|
Return the other flag from the pair.
|
|
e.g. 6 -> 7
|
|
e.g. 7 -> 6
|
|
"""
|
|
if flag_bit % 2 == 0:
|
|
return flag_bit + 1
|
|
else:
|
|
return flag_bit - 1
|
|
|
|
|
|
|
|
class IncompatibleOrInsaneFeatures(Exception): pass
|
|
class UnknownEvenFeatureBits(IncompatibleOrInsaneFeatures): pass
|
|
class IncompatibleLightningFeatures(IncompatibleOrInsaneFeatures): pass
|
|
|
|
|
|
def ln_compare_features(our_features: 'LnFeatures', their_features: int) -> 'LnFeatures':
|
|
"""Returns negotiated features.
|
|
Raises IncompatibleLightningFeatures if incompatible.
|
|
"""
|
|
our_flags = set(list_enabled_bits(our_features))
|
|
their_flags = set(list_enabled_bits(their_features))
|
|
# check that they have our required features, and disable the optional features they don't have
|
|
for flag in our_flags:
|
|
if flag not in their_flags and get_ln_flag_pair_of_bit(flag) not in their_flags:
|
|
# they don't have this feature we wanted :(
|
|
if flag % 2 == 0: # even flags are compulsory
|
|
raise IncompatibleLightningFeatures(f"remote does not support {LnFeatures(1 << flag)!r}")
|
|
our_features ^= 1 << flag # disable flag
|
|
else:
|
|
# They too have this flag.
|
|
# For easier feature-bit-testing, if this is an even flag, we also
|
|
# set the corresponding odd flag now.
|
|
if flag % 2 == 0 and our_features & (1 << flag):
|
|
our_features |= 1 << get_ln_flag_pair_of_bit(flag)
|
|
# check that we have their required features
|
|
for flag in their_flags:
|
|
if flag not in our_flags and get_ln_flag_pair_of_bit(flag) not in our_flags:
|
|
# we don't have this feature they wanted :(
|
|
if flag % 2 == 0: # even flags are compulsory
|
|
raise IncompatibleLightningFeatures(f"remote wanted feature we don't have: {LnFeatures(1 << flag)!r}")
|
|
return our_features
|
|
|
|
|
|
def validate_features(features: int) -> None:
|
|
"""Raises IncompatibleOrInsaneFeatures if
|
|
- a mandatory feature is listed that we don't recognize, or
|
|
- the features are inconsistent
|
|
"""
|
|
features = LnFeatures(features)
|
|
enabled_features = list_enabled_bits(features)
|
|
for fbit in enabled_features:
|
|
if (1 << fbit) & LN_FEATURES_IMPLEMENTED == 0 and fbit % 2 == 0:
|
|
raise UnknownEvenFeatureBits(fbit)
|
|
if not features.validate_transitive_dependencies():
|
|
raise IncompatibleOrInsaneFeatures(f"not all transitive dependencies are set. "
|
|
f"features={features}")
|
|
|
|
|
|
def derive_payment_secret_from_payment_preimage(payment_preimage: bytes) -> bytes:
|
|
"""Returns secret to be put into invoice.
|
|
Derivation is deterministic, based on the preimage.
|
|
Crucially the payment_hash must be derived in an independent way from this.
|
|
"""
|
|
# Note that this could be random data too, but then we would need to store it.
|
|
# We derive it identically to clightning, so that we cannot be distinguished:
|
|
# https://github.com/ElementsProject/lightning/blob/faac4b28adee5221e83787d64cd5d30b16b62097/lightningd/invoice.c#L115
|
|
modified = bytearray(payment_preimage)
|
|
modified[0] ^= 1
|
|
return sha256(bytes(modified))
|
|
|
|
|
|
class LNPeerAddr:
|
|
# note: while not programmatically enforced, this class is meant to be *immutable*
|
|
|
|
def __init__(self, host: str, port: int, pubkey: bytes):
|
|
assert isinstance(host, str), repr(host)
|
|
assert isinstance(port, int), repr(port)
|
|
assert isinstance(pubkey, bytes), repr(pubkey)
|
|
try:
|
|
net_addr = NetAddress(host, port) # this validates host and port
|
|
except Exception as e:
|
|
raise ValueError(f"cannot construct LNPeerAddr: invalid host or port (host={host}, port={port})") from e
|
|
# note: not validating pubkey as it would be too expensive:
|
|
# if not ECPubkey.is_pubkey_bytes(pubkey): raise ValueError()
|
|
self.host = host
|
|
self.port = port
|
|
self.pubkey = pubkey
|
|
self._net_addr = net_addr
|
|
|
|
def __str__(self):
|
|
return '{}@{}'.format(self.pubkey.hex(), self.net_addr_str())
|
|
|
|
def __repr__(self):
|
|
return f'<LNPeerAddr host={self.host} port={self.port} pubkey={self.pubkey.hex()}>'
|
|
|
|
def net_addr(self) -> NetAddress:
|
|
return self._net_addr
|
|
|
|
def net_addr_str(self) -> str:
|
|
return str(self._net_addr)
|
|
|
|
def __eq__(self, other):
|
|
if not isinstance(other, LNPeerAddr):
|
|
return False
|
|
return (self.host == other.host
|
|
and self.port == other.port
|
|
and self.pubkey == other.pubkey)
|
|
|
|
def __ne__(self, other):
|
|
return not (self == other)
|
|
|
|
def __hash__(self):
|
|
return hash((self.host, self.port, self.pubkey))
|
|
|
|
|
|
def get_compressed_pubkey_from_bech32(bech32_pubkey: str) -> bytes:
|
|
hrp, data_5bits = segwit_addr.bech32_decode(bech32_pubkey)
|
|
if hrp != 'ln':
|
|
raise Exception('unexpected hrp: {}'.format(hrp))
|
|
data_8bits = segwit_addr.convertbits(data_5bits, 5, 8, False)
|
|
# pad with zeroes
|
|
COMPRESSED_PUBKEY_LENGTH = 33
|
|
data_8bits = data_8bits + ((COMPRESSED_PUBKEY_LENGTH - len(data_8bits)) * [0])
|
|
return bytes(data_8bits)
|
|
|
|
|
|
def make_closing_tx(local_funding_pubkey: bytes, remote_funding_pubkey: bytes,
|
|
funding_txid: str, funding_pos: int, funding_sat: int,
|
|
outputs: List[PartialTxOutput]) -> PartialTransaction:
|
|
c_input = make_funding_input(local_funding_pubkey, remote_funding_pubkey,
|
|
funding_pos, funding_txid, funding_sat)
|
|
c_input.nsequence = 0xFFFF_FFFF
|
|
tx = PartialTransaction.from_io([c_input], outputs, locktime=0, version=2)
|
|
return tx
|
|
|
|
|
|
def split_host_port(host_port: str) -> Tuple[str, str]: # port returned as string
|
|
ipv6 = re.compile(r'\[(?P<host>[:0-9a-f]+)\](?P<port>:\d+)?$')
|
|
other = re.compile(r'(?P<host>[^:]+)(?P<port>:\d+)?$')
|
|
m = ipv6.match(host_port)
|
|
if not m:
|
|
m = other.match(host_port)
|
|
if not m:
|
|
raise ConnStringFormatError(_('Connection strings must be in <node_pubkey>@<host>:<port> format'))
|
|
host = m.group('host')
|
|
if m.group('port'):
|
|
port = m.group('port')[1:]
|
|
else:
|
|
port = '9735'
|
|
try:
|
|
int(port)
|
|
except ValueError:
|
|
raise ConnStringFormatError(_('Port number must be decimal'))
|
|
return host, port
|
|
|
|
def extract_nodeid(connect_contents: str) -> Tuple[bytes, str]:
|
|
rest = None
|
|
try:
|
|
# connection string?
|
|
nodeid_hex, rest = connect_contents.split("@", 1)
|
|
except ValueError:
|
|
try:
|
|
# invoice?
|
|
invoice = lndecode(connect_contents)
|
|
nodeid_bytes = invoice.pubkey.serialize()
|
|
nodeid_hex = bh2u(nodeid_bytes)
|
|
except:
|
|
# node id as hex?
|
|
nodeid_hex = connect_contents
|
|
if rest == '':
|
|
raise ConnStringFormatError(_('At least a hostname must be supplied after the at symbol.'))
|
|
try:
|
|
node_id = bfh(nodeid_hex)
|
|
if len(node_id) != 33:
|
|
raise Exception()
|
|
except:
|
|
raise ConnStringFormatError(_('Invalid node ID, must be 33 bytes and hexadecimal'))
|
|
return node_id, rest
|
|
|
|
|
|
# key derivation
|
|
# see lnd/keychain/derivation.go
|
|
class LnKeyFamily(IntEnum):
|
|
MULTISIG = 0 | BIP32_PRIME
|
|
REVOCATION_BASE = 1 | BIP32_PRIME
|
|
HTLC_BASE = 2 | BIP32_PRIME
|
|
PAYMENT_BASE = 3 | BIP32_PRIME
|
|
DELAY_BASE = 4 | BIP32_PRIME
|
|
REVOCATION_ROOT = 5 | BIP32_PRIME
|
|
NODE_KEY = 6
|
|
|
|
|
|
def generate_keypair(node: BIP32Node, key_family: LnKeyFamily) -> Keypair:
|
|
node2 = node.subkey_at_private_derivation([key_family, 0, 0])
|
|
k = node2.eckey.get_secret_bytes()
|
|
cK = ecc.ECPrivkey(k).get_public_key_bytes()
|
|
return Keypair(cK, k)
|
|
|
|
|
|
|
|
NUM_MAX_HOPS_IN_PAYMENT_PATH = 20
|
|
NUM_MAX_EDGES_IN_PAYMENT_PATH = NUM_MAX_HOPS_IN_PAYMENT_PATH
|
|
|
|
|
|
class ShortChannelID(bytes):
|
|
|
|
def __repr__(self):
|
|
return f"<ShortChannelID: {format_short_channel_id(self)}>"
|
|
|
|
def __str__(self):
|
|
return format_short_channel_id(self)
|
|
|
|
@classmethod
|
|
def from_components(cls, block_height: int, tx_pos_in_block: int, output_index: int) -> 'ShortChannelID':
|
|
bh = block_height.to_bytes(3, byteorder='big')
|
|
tpos = tx_pos_in_block.to_bytes(3, byteorder='big')
|
|
oi = output_index.to_bytes(2, byteorder='big')
|
|
return ShortChannelID(bh + tpos + oi)
|
|
|
|
@classmethod
|
|
def normalize(cls, data: Union[None, str, bytes, 'ShortChannelID']) -> Optional['ShortChannelID']:
|
|
if isinstance(data, ShortChannelID) or data is None:
|
|
return data
|
|
if isinstance(data, str):
|
|
assert len(data) == 16
|
|
return ShortChannelID.fromhex(data)
|
|
if isinstance(data, (bytes, bytearray)):
|
|
assert len(data) == 8
|
|
return ShortChannelID(data)
|
|
|
|
@property
|
|
def block_height(self) -> int:
|
|
return int.from_bytes(self[:3], byteorder='big')
|
|
|
|
@property
|
|
def txpos(self) -> int:
|
|
return int.from_bytes(self[3:6], byteorder='big')
|
|
|
|
@property
|
|
def output_index(self) -> int:
|
|
return int.from_bytes(self[6:8], byteorder='big')
|
|
|
|
|
|
def format_short_channel_id(short_channel_id: Optional[bytes]):
|
|
if not short_channel_id:
|
|
return _('Not yet available')
|
|
return str(int.from_bytes(short_channel_id[:3], 'big')) \
|
|
+ 'x' + str(int.from_bytes(short_channel_id[3:6], 'big')) \
|
|
+ 'x' + str(int.from_bytes(short_channel_id[6:], 'big'))
|
|
|
|
|
|
@attr.s(frozen=True)
|
|
class UpdateAddHtlc:
|
|
amount_msat = attr.ib(type=int, kw_only=True)
|
|
payment_hash = attr.ib(type=bytes, kw_only=True, converter=hex_to_bytes)
|
|
cltv_expiry = attr.ib(type=int, kw_only=True)
|
|
timestamp = attr.ib(type=int, kw_only=True)
|
|
htlc_id = attr.ib(type=int, kw_only=True, default=None)
|
|
|
|
@classmethod
|
|
def from_tuple(cls, amount_msat, payment_hash, cltv_expiry, htlc_id, timestamp) -> 'UpdateAddHtlc':
|
|
return cls(amount_msat=amount_msat,
|
|
payment_hash=payment_hash,
|
|
cltv_expiry=cltv_expiry,
|
|
htlc_id=htlc_id,
|
|
timestamp=timestamp)
|
|
|
|
def to_tuple(self):
|
|
return (self.amount_msat, self.payment_hash, self.cltv_expiry, self.htlc_id, self.timestamp)
|
|
|
|
|
|
class OnionFailureCodeMetaFlag(IntFlag):
|
|
BADONION = 0x8000
|
|
PERM = 0x4000
|
|
NODE = 0x2000
|
|
UPDATE = 0x1000
|
|
|