Skip to content

Commit

Permalink
switch to anchors-zero-htlc-tx-fee
Browse files Browse the repository at this point in the history
* set the effective weight for htlc transactions to zero to prevent
  output trimming and fee subtraction
* wip: construct htlc transaction with additional inputs
  • Loading branch information
bitromortac committed Oct 4, 2021
1 parent 6c932dd commit ee2a5d7
Show file tree
Hide file tree
Showing 6 changed files with 83 additions and 23 deletions.
3 changes: 1 addition & 2 deletions electrum/lnpeer.py
Original file line number Diff line number Diff line change
Expand Up @@ -502,8 +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) \
and self.lnworker.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
68 changes: 62 additions & 6 deletions electrum/lnsweep.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

from .util import bfh, bh2u
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 @@ -15,7 +16,7 @@
get_ordered_channel_configs, privkey_to_pubkey, get_per_commitment_secret_from_seed,
RevocationStore, extract_ctn_from_tx_and_chan, UnableToDeriveSecret, SENT, RECEIVED,
map_htlcs_to_ctx_output_idxs, Direction, make_commitment_output_to_remote_witness_script,
derive_payment_basepoint, ctx_has_anchors, SCRIPTPUBKEY_TEMPLATE_FUNDING)
derive_payment_basepoint, ctx_has_anchors, SCRIPT_TEMPLATE_FUNDING)
from .transaction import (Transaction, TxInput, PartialTransaction, PartialTxInput,
PartialTxOutput, TxOutpoint, script_GetOp, match_script_against_template)
from .simple_config import SimpleConfig
Expand Down Expand Up @@ -343,7 +344,7 @@ def extract_funding_pubkeys_from_ctx(txin: TxInput) -> Tuple[bytes, bytes]:
We expect to see a witness script of: OP_2 pk1 pk2 OP_2 OP_CHECKMULTISIG"""
elements = txin.witness_elements()
witness_script = elements[-1]
assert match_script_against_template(witness_script, SCRIPTPUBKEY_TEMPLATE_FUNDING)
assert match_script_against_template(witness_script, SCRIPT_TEMPLATE_FUNDING)
parsed_script = [x for x in script_GetOp(witness_script)]
pubkey1 = parsed_script[1][1]
pubkey2 = parsed_script[2][1]
Expand Down Expand Up @@ -550,7 +551,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 +561,67 @@ 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):
# TODO: set reasonable fee
return wallet.config.estimate_fee_for_feerate(fee_per_kb=10*1000, size=size)
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
28 changes: 15 additions & 13 deletions electrum/lnutil.py
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@

LN_MAX_FUNDING_SAT = pow(2, 24) - 1

SCRIPTPUBKEY_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG]
SCRIPT_TEMPLATE_FUNDING = [opcodes.OP_2, OPPushDataPubkey, OPPushDataPubkey, opcodes.OP_2, opcodes.OP_CHECKMULTISIG]

# dummy address for fee estimation of funding tx
def ln_dummy_address():
Expand Down Expand Up @@ -564,10 +564,7 @@ def make_htlc_tx_output(
]))

p2wsh = bitcoin.redeem_script_to_address('p2wsh', bh2u(script))
if has_anchors:
weight = HTLC_SUCCESS_WEIGHT_ANCHORS if success else HTLC_TIMEOUT_WEIGHT_ANCHORS
else:
weight = HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT
weight = effective_htlc_tx_weight(success=success, has_anchors=has_anchors)
fee = local_feerate * weight
fee = fee // 1000 * 1000
final_amount_sat = (amount_msat - fee) // 1000
Expand Down Expand Up @@ -913,25 +910,30 @@ def make_commitment_outputs(
return htlc_outputs, c_outputs


def effective_htlc_tx_weight(success: bool, has_anchors: bool):
# for anchors-zero-fee-htlc we set an effective weight of zero
# we only trim htlcs below dust, as in the anchors commitment format,
# 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 0 * HTLC_SUCCESS_WEIGHT_ANCHORS if success else 0 * HTLC_TIMEOUT_WEIGHT_ANCHORS
else:
return HTLC_SUCCESS_WEIGHT if success else HTLC_TIMEOUT_WEIGHT


def offered_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int:
# offered htlcs strictly below this amount will be trimmed (from ctx).
# feerate is in sat/kw
# returns value in sat
if has_anchors:
weight = HTLC_TIMEOUT_WEIGHT_ANCHORS
else:
weight = HTLC_TIMEOUT_WEIGHT
weight = effective_htlc_tx_weight(success=False, has_anchors=has_anchors)
return dust_limit_sat + weight * feerate // 1000


def received_htlc_trim_threshold_sat(*, dust_limit_sat: int, feerate: int, has_anchors: bool) -> int:
# received htlcs strictly below this amount will be trimmed (from ctx).
# feerate is in sat/kw
# returns value in sat
if has_anchors:
weight = HTLC_SUCCESS_WEIGHT_ANCHORS
else:
weight = HTLC_SUCCESS_WEIGHT
weight = effective_htlc_tx_weight(success=True, has_anchors=has_anchors)
return dust_limit_sat + weight * feerate // 1000


Expand Down
2 changes: 1 addition & 1 deletion electrum/lnworker.py
Original file line number Diff line number Diff line change
Expand Up @@ -176,7 +176,7 @@ class ErrorAddingPeer(Exception): pass
| LnFeatures.GOSSIP_QUERIES_REQ\
| LnFeatures.BASIC_MPP_OPT\
| LnFeatures.OPTION_TRAMPOLINE_ROUTING_OPT\
| LnFeatures.OPTION_ANCHOR_OUTPUTS_OPT
| LnFeatures.OPTION_ANCHORS_ZERO_FEE_HTLC_OPT

LNGOSSIP_FEATURES = BASE_FEATURES\
| LnFeatures.GOSSIP_QUERIES_OPT\
Expand Down
2 changes: 1 addition & 1 deletion electrum/tests/test_lnchannel.py
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ def test_DustLimit(self):
paymentPreimage = b"\x01" * 32
paymentHash = bitcoin.sha256(paymentPreimage)
fee_per_kw = alice_channel.get_next_feerate(LOCAL)
success_weight = lnutil.HTLC_SUCCESS_WEIGHT_ANCHORS if use_anchor_outputs else lnutil.HTLC_SUCCESS_WEIGHT
success_weight = lnutil.effective_htlc_tx_weight(success=True, has_anchors=use_anchor_outputs)
# we put a single sat less into the htlc than bob can afford
# to pay for his htlc success transaction
below_dust_for_bob = dust_limit_bob - 1
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

0 comments on commit ee2a5d7

Please sign in to comment.