anchors: switch to zero-fee-htlcs
* sets the weight of htlc transactions to zero, thereby putting a zero fee for the htlc transactions * add inputs to htlc-tx for fee bumping * switches feature flags * disable anchor test vectors, which are now partially invalid
This commit is contained in:
@@ -903,7 +903,7 @@ class Channel(AbstractChannel):
|
||||
|
||||
def has_anchors(self) -> bool:
|
||||
channel_type = ChannelType(self.storage.get('channel_type'))
|
||||
return bool(channel_type & ChannelType.OPTION_ANCHOR_OUTPUTS)
|
||||
return bool(channel_type & ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)
|
||||
|
||||
def get_wallet_addresses_channel_might_want_reserved(self) -> Sequence[str]:
|
||||
assert self.is_static_remotekey_enabled()
|
||||
|
||||
@@ -657,7 +657,7 @@ class Peer(Logger, EventListener):
|
||||
return self.features.supports(LnFeatures.OPTION_UPFRONT_SHUTDOWN_SCRIPT_OPT)
|
||||
|
||||
def use_anchors(self) -> bool:
|
||||
return self.features.supports(LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT)
|
||||
return self.features.supports(LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT)
|
||||
|
||||
def upfront_shutdown_script_from_payload(self, payload, msg_identifier: str) -> Optional[bytes]:
|
||||
if msg_identifier not in ['accept', 'open']:
|
||||
@@ -774,7 +774,7 @@ class Peer(Logger, EventListener):
|
||||
assert self.their_features.supports(LnFeatures.OPTION_STATIC_REMOTEKEY_OPT)
|
||||
our_channel_type = ChannelType(ChannelType.OPTION_STATIC_REMOTEKEY)
|
||||
if self.use_anchors():
|
||||
our_channel_type |= ChannelType(ChannelType.OPTION_ANCHOR_OUTPUTS)
|
||||
our_channel_type |= ChannelType(ChannelType.OPTION_ANCHORS_ZERO_FEE_HTLC_TX)
|
||||
if zeroconf:
|
||||
our_channel_type |= ChannelType(ChannelType.OPTION_ZEROCONF)
|
||||
# We do not set the option_scid_alias bit in channel_type because LND rejects it.
|
||||
|
||||
@@ -7,11 +7,13 @@ from enum import Enum, auto
|
||||
|
||||
import electrum_ecc as ecc
|
||||
|
||||
from .util import bfh
|
||||
from .util import bfh, UneconomicFee
|
||||
from .crypto import privkey_to_pubkey
|
||||
from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness
|
||||
from .invoices import PR_PAID
|
||||
from . import descriptor
|
||||
from . import coinchooser
|
||||
|
||||
from .lnutil import (make_commitment_output_to_remote_address, make_commitment_output_to_local_witness_script,
|
||||
derive_privkey, derive_pubkey, derive_blinded_pubkey, derive_blinded_privkey,
|
||||
make_htlc_tx_witness, make_htlc_tx_with_open_channel, UpdateAddHtlc,
|
||||
@@ -32,6 +34,9 @@ if TYPE_CHECKING:
|
||||
_logger = get_logger(__name__)
|
||||
# note: better to use chan.logger instead, when applicable
|
||||
|
||||
HTLC_TRANSACTION_DEADLINE_FRACTION = 4
|
||||
HTLC_TRANSACTION_SWEEP_TARGET = 10
|
||||
|
||||
|
||||
class SweepInfo(NamedTuple):
|
||||
name: str
|
||||
@@ -315,12 +320,15 @@ def create_sweeptxs_for_our_ctx(
|
||||
continue
|
||||
else:
|
||||
preimage = None
|
||||
create_txns_for_htlc(
|
||||
htlc=htlc,
|
||||
htlc_direction=direction,
|
||||
ctx_output_idx=ctx_output_idx,
|
||||
htlc_relative_idx=htlc_relative_idx,
|
||||
preimage=preimage)
|
||||
try:
|
||||
create_txns_for_htlc(
|
||||
htlc=htlc,
|
||||
htlc_direction=direction,
|
||||
ctx_output_idx=ctx_output_idx,
|
||||
htlc_relative_idx=htlc_relative_idx,
|
||||
preimage=preimage)
|
||||
except UneconomicFee:
|
||||
continue
|
||||
return txs
|
||||
|
||||
|
||||
@@ -580,7 +588,7 @@ def create_htlctx_that_spends_from_our_ctx(
|
||||
assert (htlc_direction == RECEIVED) == bool(preimage), 'preimage is required iff htlc is received'
|
||||
preimage = preimage or b''
|
||||
ctn = extract_ctn_from_tx_and_chan(ctx, chan)
|
||||
witness_script_out, htlc_tx = make_htlc_tx_with_open_channel(
|
||||
witness_script_out, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel(
|
||||
chan=chan,
|
||||
pcp=our_pcp,
|
||||
subject=LOCAL,
|
||||
@@ -590,13 +598,87 @@ def create_htlctx_that_spends_from_our_ctx(
|
||||
htlc=htlc,
|
||||
ctx_output_idx=ctx_output_idx,
|
||||
name=f'our_ctx_{ctx_output_idx}_htlc_tx_{htlc.payment_hash.hex()}')
|
||||
|
||||
# we need to attach inputs that pay for the transaction fee
|
||||
if chan.has_anchors():
|
||||
wallet = chan.lnworker.wallet
|
||||
coins = wallet.get_spendable_coins(None)
|
||||
|
||||
def fee_estimator(size):
|
||||
if htlc_direction == SENT:
|
||||
# we deal with an offered HTLC and therefore with a timeout transaction
|
||||
# in this case it is not time critical for us to sweep unless we
|
||||
# become a forwarding node
|
||||
fee_per_kb = wallet.config.eta_target_to_fee(HTLC_TRANSACTION_SWEEP_TARGET)
|
||||
else:
|
||||
# in the case of a received HTLC, if we have the hash preimage,
|
||||
# we should sweep before the timelock expires
|
||||
expiry_height = htlc.cltv_abs
|
||||
current_height = wallet.network.blockchain().height()
|
||||
deadline_blocks = expiry_height - current_height
|
||||
# target block inclusion with a safety buffer
|
||||
target = int(deadline_blocks / HTLC_TRANSACTION_DEADLINE_FRACTION)
|
||||
fee_per_kb = wallet.config.eta_target_to_fee(target)
|
||||
if not fee_per_kb: # testnet and other cases
|
||||
fee_per_kb = wallet.config.fee_per_kb()
|
||||
fee = wallet.config.estimate_fee_for_feerate(fee_per_kb=fee_per_kb, size=size)
|
||||
# we only sweep if it is makes sense economically
|
||||
if fee > htlc.amount_msat // 1000:
|
||||
raise UneconomicFee
|
||||
return fee
|
||||
|
||||
coin_chooser = coinchooser.get_coin_chooser(wallet.config)
|
||||
change_address = wallet.get_single_change_address_for_new_transaction()
|
||||
funded_htlc_tx = coin_chooser.make_tx(
|
||||
coins=coins,
|
||||
inputs=maybe_zero_fee_htlc_tx.inputs(),
|
||||
outputs=maybe_zero_fee_htlc_tx.outputs(),
|
||||
change_addrs=[change_address],
|
||||
fee_estimator_vb=fee_estimator,
|
||||
dust_threshold=wallet.dust_threshold())
|
||||
|
||||
# place htlc input/output at corresponding indices (due to sighash single)
|
||||
htlc_outpoint = TxOutpoint(txid=bfh(ctx.txid()), out_idx=ctx_output_idx)
|
||||
htlc_input_idx = funded_htlc_tx.get_input_idx_that_spent_prevout(htlc_outpoint)
|
||||
|
||||
htlc_out_address = maybe_zero_fee_htlc_tx.outputs()[0].address
|
||||
htlc_output_idx = funded_htlc_tx.get_output_idxs_from_address(htlc_out_address).pop()
|
||||
inputs = funded_htlc_tx.inputs()
|
||||
outputs = funded_htlc_tx.outputs()
|
||||
if htlc_input_idx != 0:
|
||||
htlc_txin = inputs.pop(htlc_input_idx)
|
||||
inputs.insert(0, htlc_txin)
|
||||
if htlc_output_idx != 0:
|
||||
htlc_txout = outputs.pop(htlc_output_idx)
|
||||
outputs.insert(0, htlc_txout)
|
||||
final_htlc_tx = PartialTransaction.from_io(
|
||||
inputs,
|
||||
outputs,
|
||||
locktime=maybe_zero_fee_htlc_tx.locktime,
|
||||
version=maybe_zero_fee_htlc_tx.version,
|
||||
BIP69_sort=False
|
||||
)
|
||||
|
||||
for fee_input_idx in range(1, len(funded_htlc_tx.inputs())):
|
||||
txin = final_htlc_tx.inputs()[fee_input_idx]
|
||||
pubkey = wallet.get_public_key(txin.address)
|
||||
index = wallet.get_address_index(txin.address)
|
||||
privkey, _ = wallet.keystore.get_private_key(index, wallet.get_unlocked_password())
|
||||
desc = descriptor.get_singlesig_descriptor_from_legacy_leaf(pubkey=pubkey, script_type='p2wpkh')
|
||||
txin.script_descriptor = desc
|
||||
fee_input_sig = final_htlc_tx.sign_txin(fee_input_idx, privkey)
|
||||
final_htlc_tx.add_signature_to_txin(txin_idx=fee_input_idx, signing_pubkey=bfh(pubkey), sig=fee_input_sig)
|
||||
else:
|
||||
final_htlc_tx = maybe_zero_fee_htlc_tx
|
||||
|
||||
# sign HTLC output
|
||||
remote_htlc_sig = chan.get_remote_htlc_sig_for_htlc(htlc_relative_idx=htlc_relative_idx)
|
||||
local_htlc_sig = htlc_tx.sign_txin(0, local_htlc_privkey)
|
||||
txin = htlc_tx.inputs()[0]
|
||||
local_htlc_sig = final_htlc_tx.sign_txin(0, local_htlc_privkey)
|
||||
txin = final_htlc_tx.inputs()[0]
|
||||
witness_script_in = txin.witness_script
|
||||
assert witness_script_in
|
||||
txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_script_in)
|
||||
return witness_script_out, htlc_tx
|
||||
return witness_script_out, final_htlc_tx
|
||||
|
||||
|
||||
def create_sweeptx_their_ctx_htlc(
|
||||
|
||||
@@ -1061,7 +1061,7 @@ def effective_htlc_tx_weight(success: bool, has_anchors: bool):
|
||||
# the fees for the hltc transaction don't need to be subtracted from
|
||||
# the htlc output, but fees are taken from extra attached inputs
|
||||
if has_anchors:
|
||||
return HTLC_SUCCESS_WEIGHT_ANCHORS if success else HTLC_TIMEOUT_WEIGHT_ANCHORS
|
||||
return 0 * HTLC_SUCCESS_WEIGHT_ANCHORS if success else 0 * HTLC_TIMEOUT_WEIGHT_ANCHORS
|
||||
else:
|
||||
return HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT
|
||||
|
||||
@@ -1552,7 +1552,7 @@ LN_FEATURES_IMPLEMENTED = (
|
||||
| LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_OPT | LnFeatures.OPTION_SHUTDOWN_ANYSEGWIT_REQ
|
||||
| LnFeatures.OPTION_CHANNEL_TYPE_OPT | LnFeatures.OPTION_CHANNEL_TYPE_REQ
|
||||
| LnFeatures.OPTION_SCID_ALIAS_OPT | LnFeatures.OPTION_SCID_ALIAS_REQ
|
||||
| LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT | LnFeatures.OPTION_ANCHOR_OUTPUTS_REQ
|
||||
| LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT | LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_REQ
|
||||
)
|
||||
|
||||
|
||||
|
||||
@@ -834,7 +834,7 @@ class LNWallet(LNWorker):
|
||||
Logger.__init__(self)
|
||||
features = LNWALLET_FEATURES
|
||||
if self.config.ENABLE_ANCHOR_CHANNELS:
|
||||
features |= LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT
|
||||
features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT
|
||||
if self.config.ACCEPT_ZEROCONF_CHANNELS:
|
||||
features |= LnFeatures.OPTION_ZEROCONF_OPT
|
||||
LNWorker.__init__(self, self.node_keypair, features, config=self.config)
|
||||
|
||||
@@ -145,6 +145,11 @@ class NotEnoughFunds(Exception):
|
||||
return _("Insufficient funds")
|
||||
|
||||
|
||||
class UneconomicFee(Exception):
|
||||
def __str__(self):
|
||||
return _("The fee for the transaction is higher than the funds gained from it.")
|
||||
|
||||
|
||||
class NoDynamicFeeEstimates(Exception):
|
||||
def __str__(self):
|
||||
return _('Dynamic fee estimates not available')
|
||||
|
||||
@@ -797,6 +797,9 @@ class TestLNUtil(ElectrumTestCase):
|
||||
ref_commit_tx_str = '02000000000101bef67e4e2fb9ddeeb3461973cd4c62abb35050b1add772995b820b584a488489000000000038b02b8002c0c62d0000000000160014ccf1af2f2aabee14bb40fa3851ab2301de84311054a56a00000000002200204adb4e2f00643db396dd120d4e7dc17625f5f2c11a40d857accc862d6b7dd80e0400473044022051b75c73198c6deee1a875871c3961832909acd297c6b908d59e3319e5185a46022055c419379c5051a78d00dbbce11b5b664a0c22815fbcc6fcef6b1937c383693901483045022100f51d2e566a70ba740fc5d8c0f07b9b93d2ed741c3c0860c613173de7d39e7968022041376d520e9c0e1ad52248ddf4b22e12be8763007df977253ef45a4ca3bdb7c001475221023da092f6980e58d2c037173180e9a465476026ee50f96695963e8efe436f54eb21030e9f7b623d2ccc7c9bd44d66d5ce21ce504c0acf6385a132cec6d3c39fa711c152ae3e195220'
|
||||
self.assertEqual(str(our_commit_tx), ref_commit_tx_str)
|
||||
|
||||
@unittest.skip("only valid for original anchor ouputs, "
|
||||
"but invalid due to different fee estimation "
|
||||
"with anchors-zero-fee-htlcs")
|
||||
@disable_ecdsa_r_value_grinding
|
||||
def test_commitment_tx_anchors_test_vectors(self):
|
||||
for test_vector in ANCHOR_TEST_VECTORS:
|
||||
|
||||
Reference in New Issue
Block a user