From cfca26b12944e558c2cbf46f04d3cd26c1fea57e Mon Sep 17 00:00:00 2001 From: Matt Whitlock Date: Sun, 13 Apr 2025 11:31:28 -0400 Subject: [PATCH] common: calculate correct weight of Taproot spends MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit utxo_spend_weight(…) was assuming that witnesses always include a pubkey, but that is not true of Taproot (key-path) spends. The effect was that wallet- funded commands (e.g., withdraw, fundchannel, multifundchannel, etc.) were overpaying on fees due to calculating Taproot input weights as 34 sipa greater than they actually are. Also as a consequence of the miscalculation, users who carefully select a set of UTxOs to spend so as to avoid producing a change output would experience an error when trying to spend them. As an example, I tried to spend a tiny 1390-sat P2TR output to a new 1275-sat output at minimum feerate. The actual weight of the transaction is 451 sipa, so the 115-sat fee yields a feerate of 255 sat/kw. However, CLN refuses the transaction, claiming: { "code": 301, "message": "Could not afford 1275sat using UTXOs totalling 1390sat with weight 485 at feerate 253" } The reported weight of 485 sipa is wrong, as it assumes the input will include a 34-sipa public key when in fact it will not. This commit amends the bitcoin_tx_simple_input_witness_weight() and bitcoin_tx_simple_input_weight(…) functions to take a parameter specifying the scriptPubKey type of the spend. The implementation of the former is corrected so that P2TR spends are not charged for a public key that they actually lack. An enum type is introduced to enumerate the known scriptPubKey types, and a new scriptpubkey_type(…) utility function is implemented to discern the type of a given scriptPubKey. The existing is_known_scripttype(…) function is reimplemented as an inline wrapper around the new function. The default_lease_rates(…) function in plugins/funder_policy.c is amended so as to state explicitly an assumption that it has been making: that the two inputs it assumes for its default max weight calculation will not be Taproot inputs. Fixes: https://github.com/ElementsProject/lightning/issues/8164 Changelog-Fixed: P2TR inputs are assessed the correct weight in fee calculations. --- bitcoin/script.c | 20 +++++++++++++------- bitcoin/script.h | 18 +++++++++++++++++- bitcoin/tx.c | 22 +++++++++++++++------- bitcoin/tx.h | 5 +++-- common/utxo.c | 5 ++++- plugins/funder_policy.c | 3 ++- 6 files changed, 54 insertions(+), 19 deletions(-) diff --git a/bitcoin/script.c b/bitcoin/script.c index 63974644a34c..ab023b19eee2 100644 --- a/bitcoin/script.c +++ b/bitcoin/script.c @@ -550,13 +550,19 @@ bool is_p2tr(const u8 *script, size_t script_len, u8 xonly_pubkey[32]) return true; } -bool is_known_scripttype(const u8 *script, size_t script_len) -{ - return is_p2wpkh(script, script_len, NULL) - || is_p2wsh(script, script_len, NULL) - || is_p2sh(script, script_len, NULL) - || is_p2pkh(script, script_len, NULL) - || is_p2tr(script, script_len, NULL); +enum scriptpubkey_type scriptpubkey_type(const u8 *script, size_t script_len) +{ + if (is_p2wpkh(script, script_len, NULL)) + return scriptpubkey_type_p2wpkh; + if (is_p2wsh(script, script_len, NULL)) + return scriptpubkey_type_p2wsh; + if (is_p2sh(script, script_len, NULL)) + return scriptpubkey_type_p2sh; + if (is_p2pkh(script, script_len, NULL)) + return scriptpubkey_type_p2pkh; + if (is_p2tr(script, script_len, NULL)) + return scriptpubkey_type_p2tr; + return scriptpubkey_type_unknown; } bool is_known_segwit_scripttype(const u8 *script, size_t script_len) diff --git a/bitcoin/script.h b/bitcoin/script.h index 5b96d696ff15..f0df5f9d3113 100644 --- a/bitcoin/script.h +++ b/bitcoin/script.h @@ -13,6 +13,16 @@ struct ripemd160; struct rel_locktime; struct abs_locktime; +enum scriptpubkey_type { + scriptpubkey_type_unknown = 0, + scriptpubkey_type_p2pk, + scriptpubkey_type_p2pkh, + scriptpubkey_type_p2sh, + scriptpubkey_type_p2wpkh, + scriptpubkey_type_p2wsh, + scriptpubkey_type_p2tr, +}; + /* tal_count() gives the length of the script. */ u8 *bitcoin_redeem_2of2(const tal_t *ctx, const struct pubkey *key1, @@ -173,8 +183,14 @@ bool is_p2wpkh(const u8 *script, size_t script_len, struct bitcoin_address *addr /* Is this a taproot output? (extract xonly_pubkey bytes if not NULL) */ bool is_p2tr(const u8 *script, size_t script_len, u8 xonly_pubkey[32]); +/* What type of script is this? */ +enum scriptpubkey_type scriptpubkey_type(const u8 *script, size_t script_len); + /* Is this one of the above script types? */ -bool is_known_scripttype(const u8 *script, size_t script_len); +static inline bool is_known_scripttype(const u8 *script, size_t script_len) +{ + return scriptpubkey_type(script, script_len) != scriptpubkey_type_unknown; +} /* Is this a witness script type? */ bool is_known_segwit_scripttype(const u8 *script, size_t script_len); diff --git a/bitcoin/tx.c b/bitcoin/tx.c index cee06b032b40..1af7bcbccd45 100644 --- a/bitcoin/tx.c +++ b/bitcoin/tx.c @@ -912,17 +912,25 @@ size_t bitcoin_tx_input_weight(bool p2sh, size_t witness_weight) return weight; } -size_t bitcoin_tx_simple_input_witness_weight(void) +size_t bitcoin_tx_simple_input_witness_weight(enum scriptpubkey_type spend_type) { - /* Account for witness (1 byte count + sig + key) */ - return 1 + (bitcoin_tx_input_sig_weight() + 1 + 33); + size_t witness_weight = 1; /* byte count */ + + /* All spend types include a signature */ + witness_weight += bitcoin_tx_input_sig_weight(); + + /* All spend types except P2TR include a public key */ + if (spend_type != scriptpubkey_type_p2tr) + witness_weight += 1 + 33; + + return witness_weight; } -/* We only do segwit inputs, and we assume witness is sig + key */ -size_t bitcoin_tx_simple_input_weight(bool p2sh) +/* We only do segwit inputs */ +size_t bitcoin_tx_simple_input_weight(enum scriptpubkey_type spend_type) { - return bitcoin_tx_input_weight(p2sh, - bitcoin_tx_simple_input_witness_weight()); + return bitcoin_tx_input_weight(spend_type == scriptpubkey_type_p2sh, + bitcoin_tx_simple_input_witness_weight(spend_type)); } size_t bitcoin_tx_2of2_input_witness_weight(void) diff --git a/bitcoin/tx.h b/bitcoin/tx.h index 860b635ff741..3633fa7fae1d 100644 --- a/bitcoin/tx.h +++ b/bitcoin/tx.h @@ -17,6 +17,7 @@ struct wally_psbt; struct ripemd160; +enum scriptpubkey_type; struct bitcoin_txid { struct sha256_double shad; @@ -319,10 +320,10 @@ size_t bitcoin_tx_input_sig_weight(void); size_t bitcoin_tx_input_weight(bool p2sh, size_t witness_weight); /* The witness weight for a simple (sig + key) input */ -size_t bitcoin_tx_simple_input_witness_weight(void); +size_t bitcoin_tx_simple_input_witness_weight(enum scriptpubkey_type spend_type); /* We only do segwit inputs, and we assume witness is sig + key */ -size_t bitcoin_tx_simple_input_weight(bool p2sh); +size_t bitcoin_tx_simple_input_weight(enum scriptpubkey_type spend_type); /* The witness for our 2of2 input (closing or commitment tx). */ size_t bitcoin_tx_2of2_input_witness_weight(void); diff --git a/common/utxo.c b/common/utxo.c index a28e99cd3b79..ffd4488fdf88 100644 --- a/common/utxo.c +++ b/common/utxo.c @@ -1,4 +1,5 @@ #include "config.h" +#include #include #include @@ -64,7 +65,9 @@ struct utxo *fromwire_utxo(const tal_t *ctx, const u8 **ptr, size_t *max) size_t utxo_spend_weight(const struct utxo *utxo, size_t min_witness_weight) { - size_t wit_weight = bitcoin_tx_simple_input_witness_weight(); + size_t wit_weight = bitcoin_tx_simple_input_witness_weight( + scriptpubkey_type(utxo->scriptPubkey, + tal_bytelen(utxo->scriptPubkey))); /* If the min is less than what we'd use for a 'normal' tx, * we return the value with the greater added/calculated */ if (wit_weight < min_witness_weight) diff --git a/plugins/funder_policy.c b/plugins/funder_policy.c index 4d94bf267da8..cb93c9d8a20a 100644 --- a/plugins/funder_policy.c +++ b/plugins/funder_policy.c @@ -128,8 +128,9 @@ default_lease_rates(const tal_t *ctx) /* Let's set our default max weight to two inputs + an output * (use helpers b/c elements) */ + /* Assume worst case although Taproot inputs will be lighter */ rates->funding_weight - = 2 * bitcoin_tx_simple_input_weight(false) + = 2 * bitcoin_tx_simple_input_weight(scriptpubkey_type_p2wpkh) + bitcoin_tx_output_weight(BITCOIN_SCRIPTPUBKEY_P2WPKH_LEN); return rates;