Skip to content

Commit

Permalink
anchors: switch to zero-fee-htlcs
Browse files Browse the repository at this point in the history
* 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
  • Loading branch information
bitromortac committed Oct 15, 2021
1 parent d899bf2 commit e70a918
Show file tree
Hide file tree
Showing 6 changed files with 105 additions and 14 deletions.
2 changes: 1 addition & 1 deletion electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,7 +502,7 @@ def is_upfront_shutdown_script(self):
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']:
Expand Down
103 changes: 93 additions & 10 deletions electrum/lnsweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,9 @@
from typing import Optional, Dict, List, Tuple, TYPE_CHECKING, NamedTuple, Callable
from enum import Enum, auto

from .util import bfh, bh2u
from .util import bfh, bh2u, UneconomicFee
from .bitcoin import redeem_script_to_address, dust_threshold, construct_witness
from . import coinchooser
from . import ecc
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,
Expand All @@ -27,6 +28,9 @@

_logger = get_logger(__name__)

HTLC_TRANSACTION_DEADLINE_FRACTION = 4
HTLC_TRANSACTION_SWEEP_TARGET = 10


class SweepInfo(NamedTuple):
name: str
Expand Down Expand Up @@ -300,11 +304,14 @@ def create_txns_for_htlc(
subject=LOCAL,
ctn=ctn)
for (direction, htlc), (ctx_output_idx, htlc_relative_idx) in htlc_to_ctx_output_idx_map.items():
create_txns_for_htlc(
htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
try:
create_txns_for_htlc(
htlc=htlc,
htlc_direction=direction,
ctx_output_idx=ctx_output_idx,
htlc_relative_idx=htlc_relative_idx)
except UneconomicFee:
continue
return txs


Expand Down Expand Up @@ -550,7 +557,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, htlc_tx = make_htlc_tx_with_open_channel(
witness_script, maybe_zero_fee_htlc_tx = make_htlc_tx_with_open_channel(
chan=chan,
pcp=our_pcp,
subject=LOCAL,
Expand All @@ -560,12 +567,88 @@ 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_{bh2u(htlc.payment_hash)}')

# 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_expiry
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
)

# sign the fee input(s): TODO: how to deal with password-protected wallets?
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, None)
txin.num_sig = 1
txin.script_type = 'p2wpkh'
txin.pubkeys = [bfh(pubkey)]
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=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 = bfh(htlc_tx.sign_txin(0, local_htlc_privkey))
txin = htlc_tx.inputs()[0]
local_htlc_sig = bfh(final_htlc_tx.sign_txin(0, local_htlc_privkey))
txin = final_htlc_tx.inputs()[0]
witness_program = bfh(Transaction.get_preimage_script(txin))
txin.witness = make_htlc_tx_witness(remote_htlc_sig, local_htlc_sig, preimage, witness_program)
return witness_script, htlc_tx
return witness_script, final_htlc_tx


def create_sweeptx_their_ctx_htlc(
Expand Down
4 changes: 2 additions & 2 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -916,7 +916,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

Expand Down Expand Up @@ -1302,7 +1302,7 @@ def supports(self, feature: 'LnFeatures') -> bool:
| LnFeatures.PAYMENT_SECRET_OPT | LnFeatures.PAYMENT_SECRET_REQ
| LnFeatures.BASIC_MPP_OPT | LnFeatures.BASIC_MPP_REQ
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT | LnFeatures.OPTION_TRAMPOLINE_ROUTING_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
)


Expand Down
2 changes: 1 addition & 1 deletion electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -657,7 +657,7 @@ def maybe_enable_anchors_store_password(self, password):
if self.wallet.is_watching_only():
raise UserFacingException("Watch-only wallet cannot use anchor channels.")
self.logger.info("anchor channels are enabled")
self.features |= LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT
self.features |= LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT
self.wallet_password = password
else:
if self.has_anchor_channels():
Expand Down
3 changes: 3 additions & 0 deletions electrum/tests/test_lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,9 @@ def test_simple_commitment_tx_with_no_HTLCs(self):
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:
Expand Down
5 changes: 5 additions & 0 deletions electrum/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,11 @@ def __str__(self):
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')
Expand Down

0 comments on commit e70a918

Please sign in to comment.