1
0

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:
bitromortac
2021-10-15 11:08:10 +02:00
committed by ThomasV
parent d4222432f4
commit ea584e13fc
7 changed files with 107 additions and 17 deletions

View File

@@ -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()

View File

@@ -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.

View File

@@ -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(

View File

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

View File

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

View File

@@ -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')

View File

@@ -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: