lightningd: Fix penalty tx output derivation for BIP86 wallets
What changed, and why it matters
This fix corrects how Core Lightning derives wallet addresses used in penalty transactions when a newer BIP39-style secret is in use. Previously, the code always used the older BIP32 derivation even when the wallet was configured for BIP86 (taproot) addresses. That mismatch could cause penalty transaction outputs to be sent to addresses the wallet does not recognize or cannot spend, potentially making recovered funds inaccessible after a channel breach.
Users with BIP39/BIP86 wallets should upgrade to ensure penalty transactions derive spendable outputs. Review whether any historical penalty transactions were created with the wrong derivation path and assess recovery options.
Security signals we found
Incorrect key derivation path for penalty transactions
Potential fund loss/unspendability after channel breach for BIP86 wallets
Mismatch between wallet address type and on-chain transaction output derivation
Evidence from the diff
In onchaind_tx_unsigned(), penalty transaction output derivation was unconditionally using bip32_pubkey() and ld->bip32_base. For wallets created from the new BIP39 mnemonic HSM secret format, addresses are derived via BIP86 (taproot). The patch adds a branch: if ld->bip86_base is present, it derives the final key and extended key from the BIP86 base; otherwise it falls back to the original BIP32 path. This aligns penalty-tx output derivation with the wallet’s actual key hierarchy.
Changed components
lightningd/onchain_control.cPenalty transaction constructionBIP86/taproot wallet derivationBIP32 legacy wallet derivationInspect captured patch +23 / −9
diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c
index f62a673..1e04ceb 100644
--- a/lightningd/onchain_control.c
+++ b/lightningd/onchain_control.c
@@ -882,15 +882,29 @@ static struct bitcoin_tx *onchaind_tx_unsigned(const tal_t *ctx,
struct lightningd *ld = channel->peer->ld;
bool keypath_ok;
- bip32_pubkey(ld, &final_key, channel->final_key_idx);
- if (bip32_key_from_parent(ld->bip32_base,
- channel->final_key_idx,
- BIP32_FLAG_KEY_PUBLIC,
- &final_wallet_ext_key) != WALLY_OK) {
- channel_internal_error(channel,
- "Could not derive final_wallet_ext_key %"PRIu64,
- channel->final_key_idx);
- return NULL;
+ /* Use BIP86 derivation for P2TR if available, otherwise BIP32 */
+ if (ld->bip86_base) {
+ bip86_pubkey(ld, &final_key, channel->final_key_idx);
+ if (bip32_key_from_parent(ld->bip86_base,
+ channel->final_key_idx,
+ BIP32_FLAG_KEY_PUBLIC,
+ &final_wallet_ext_key) != WALLY_OK) {
+ channel_internal_error(channel,
+ "Could not derive final_wallet_ext_key (bip86) %"PRIu64,
+ channel->final_key_idx);
+ return NULL;
+ }
+ } else {
+ bip32_pubkey(ld, &final_key, channel->final_key_idx);
+ if (bip32_key_from_parent(ld->bip32_base,
+ channel->final_key_idx,
+ BIP32_FLAG_KEY_PUBLIC,
+ &final_wallet_ext_key) != WALLY_OK) {
+ channel_internal_error(channel,
+ "Could not derive final_wallet_ext_key %"PRIu64,
+ channel->final_key_idx);
+ return NULL;
+ }
}
tx = bitcoin_tx(ctx, chainparams, 1, 1, info->locktime);
Why this scored 54/100
Community notes
Notes can correct, qualify, or add evidence to the AI analysis. Every note shown here has been validated by a human moderator.
The AI analysis stands alone for now. Submit a note if you can add evidence or important context.