options: remove --use-bip86-derivation
What changed, and why it matters
This commit removes a command-line option called --use-bip86-derivation from Core Lightning and makes BIP86-style wallet addresses the automatic default whenever the node is set up with a mnemonic-based secret. It also disables a large batch of automated tests that were checking BIP86 and related wallet behavior, with comments saying the tests are being skipped because of derivation mismatch issues. The change itself is a feature/configuration cleanup rather than a clear security fix, but it touches how wallet keys are derived and how funds are tracked, which can affect user funds if the derivation logic is wrong.
Treat this as a code-quality and wallet-correctness change that needs follow-up. Re-enable or rewrite the skipped regression tests to verify that BIP86 and BIP32 derivation paths produce the expected addresses, can spend funds, and that the fallback for legacy HSM secrets does not break existing wallets. Audit the HSM BIP86 derivation implementation and the address-generation RPCs before the next release.
Security signals we found
Change in key-derivation policy: BIP86 becomes automatic for mnemonic HSM secrets instead of opt-in.
Fallback path added: if BIP86 derivation fails, the node silently continues with BIP32 derivation.
Many wallet/BIP86 regression tests disabled without being fixed.
Test skip reasons mention 'script verification issues with BIP86/BIP32 derivation mismatch', indicating unresolved correctness concerns in the same code area.
Evidence from the diff
The patch drops the experimental –use-bip86-derivation flag and the corresponding use_bip86_derivation boolean in struct lightningd. hsm_init() now always attempts to derive the BIP86 base key from the HSM; if the HSM secret is mnemonic-based the derivation succeeds and BIP86 is used for on-chain addresses, otherwise it falls back silently to the legacy BIP32 path. A large number of wallet and BIP86 tests are skipped with pytest.mark.skip, citing ‘script verification issues with BIP86/BIP32 derivation mismatch’ or ‘derivation implementation changes’. The commit does not contain an explicit security fix, CVE, or vendor security advisory.
Changed components
lightningd/hsm_control.clightningd/lightningd.hlightningd/options.ctests/test_wallet.pyInspect captured patch +56 / −45
diff --git a/lightningd/hsm_control.c b/lightningd/hsm_control.c
index c1fa743b..ae13a08a 100644
--- a/lightningd/hsm_control.c
+++ b/lightningd/hsm_control.c
@@ -188,17 +188,19 @@ struct ext_key *hsm_init(struct lightningd *ld)
fatal("--experimental-splicing needs HSM capable of signing splices!");
}
- /* Check if BIP86 derivation is requested and supported */
- if (ld->use_bip86_derivation) {
- /* Get BIP86 base key from HSM */
- ld->bip86_base = tal(ld, struct ext_key);
- msg = towire_hsmd_derive_bip86_key(NULL, 0, false);
- const u8 *reply = hsm_sync_req(tmpctx, ld, take(msg));
- if (!fromwire_hsmd_derive_bip86_key_reply(reply, ld->bip86_base)) {
- errx(EXITCODE_HSM_GENERIC_ERROR, "Failed to get BIP86 base key from HSM");
- }
+ /* Try to get BIP86 base key from HSM (works only for mnemonic secrets) */
+ ld->bip86_base = tal(ld, struct ext_key);
+ msg = towire_hsmd_derive_bip86_key(NULL, 0, false);
+ const u8 *reply = hsm_sync_req(tmpctx, ld, take(msg));
+ if (fromwire_hsmd_derive_bip86_key_reply(reply, ld->bip86_base)) {
+ /* BIP86 derivation succeeded - we have a mnemonic-based secret */
+ log_info(ld->log, "Using BIP86 for new addresses, BIP32 for channels (mnemonic HSM secret)");
+ /* Keep bip32_base for channel operations, database, etc. */
} else {
- ld->bip86_base = NULL;
+ /* BIP86 derivation failed - we have a legacy secret */
+ log_info(ld->log, "Using BIP32 derivation for all operations (legacy HSM secret)");
+ ld->bip86_base = tal_free(ld->bip86_base);
+ /* bip32_base was already set by the HSM init reply */
}
/* This is equivalent to makesecret("bolt12-invoice-base") */
diff --git a/lightningd/lightningd.h b/lightningd/lightningd.h
index 0ec7ee6d..6415e659 100644
--- a/lightningd/lightningd.h
+++ b/lightningd/lightningd.h
@@ -389,9 +389,6 @@ struct lightningd {
/* HSM passphrase for any format that needs it */
char *hsm_passphrase;
- /* Enable BIP86 derivation for mnemonic-based HSM secrets */
- bool use_bip86_derivation;
-
/* What (additional) messages the HSM accepts */
u32 *hsm_capabilities;
diff --git a/lightningd/options.c b/lightningd/options.c
index 02e34ed4..2175d434 100644
--- a/lightningd/options.c
+++ b/lightningd/options.c
@@ -632,11 +632,6 @@ static char *opt_set_hsm_passphrase(struct lightningd *ld)
return read_hsm_passphrase(ld);
}
-static char *opt_set_bip86_derivation(struct lightningd *ld)
-{
- ld->use_bip86_derivation = true;
- return NULL;
-}
static char *opt_force_privkey(const char *optarg, struct lightningd *ld)
{
@@ -1559,9 +1554,6 @@ static void register_opts(struct lightningd *ld)
opt_register_noarg("--hsm-passphrase", opt_set_hsm_passphrase, ld,
"Prompt for passphrase for encrypted hsm_secret (replaces --encrypted-hsm)");
- opt_register_noarg("--use-bip86-derivation", opt_set_bip86_derivation, ld,
- "Use BIP86 derivation for mnemonic-based HSM secrets (experimental)");
-
opt_register_arg("--rpc-file-mode", &opt_set_mode, &opt_show_mode,
&ld->rpc_filemode,
"Set the file mode (permissions) for the "
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index b81d2d74..c4f705e2 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -25,6 +25,7 @@ HSM_BAD_PASSWORD = 22
@unittest.skipIf(TEST_NETWORK != 'regtest', "Test relies on a number of example addresses valid only in regtest")
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_withdraw(node_factory, bitcoind):
amount = 1000000
# Don't get any funds from previous runs.
@@ -203,6 +204,7 @@ def test_withdraw(node_factory, bitcoind):
l1.rpc.withdraw(l1.rpc.newaddr()["bech32"], 10**5, feerate="1000perkb")
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_minconf_withdraw(node_factory, bitcoind):
"""Issue 2518: ensure that ridiculous confirmation levels don't overflow
@@ -231,6 +233,7 @@ def test_minconf_withdraw(node_factory, bitcoind):
l1.rpc.withdraw(destination=addr, satoshi=10000, feerate='normal', minconf=9999999)
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_addfunds_from_block(node_factory, bitcoind):
"""Send funds to the daemon without telling it explicitly
"""
@@ -271,6 +274,7 @@ def test_addfunds_from_block(node_factory, bitcoind):
check_utxos_channel(l1, [], expected_utxos)
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_txprepare_multi(node_factory, bitcoind):
amount = 10000000
l1 = node_factory.get_node(random_hsm=True)
@@ -303,6 +307,7 @@ def feerate_from_psbt(chainparams, bitcoind, node, psbt):
return fee / weight * 1000
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_txprepare(node_factory, bitcoind, chainparams):
amount = 1000000
l1 = node_factory.get_node(random_hsm=True, options={'dev-warn-on-overgrind': None},
@@ -1196,6 +1201,7 @@ def test_sign_and_send_psbt(node_factory, bitcoind, chainparams):
check_coin_moves(l1, 'wallet', wallet_coin_mvts, chainparams)
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_txsend(node_factory, bitcoind, chainparams):
amount = 1000000
l1 = node_factory.get_node(random_hsm=True)
@@ -1694,7 +1700,7 @@ def test_hsmtool_deterministic_node_ids(node_factory):
def setup_bip86_node(node_factory, mnemonic="abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"):
"""Helper function to set up a node with BIP86 support using a mnemonic-based HSM secret"""
- l1 = node_factory.get_node(start=False, options={'use-bip86-derivation': None})
+ l1 = node_factory.get_node(start=False)
# Set up node with a mnemonic HSM secret
hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
@@ -1718,6 +1724,7 @@ def setup_bip86_node(node_factory, mnemonic="abandon abandon abandon abandon aba
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_newaddr_rpc(node_factory, chainparams):
"""Test that BIP86 addresses can be generated via newaddr RPC"""
l1 = setup_bip86_node(node_factory)
@@ -1763,6 +1770,7 @@ def test_bip86_newaddr_rpc(node_factory, chainparams):
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_listaddresses(node_factory, chainparams):
"""Test that listaddresses includes BIP86 addresses and verifies first 10 addresses"""
l1 = setup_bip86_node(node_factory)
@@ -1804,6 +1812,7 @@ def test_bip86_listaddresses(node_factory, chainparams):
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_deterministic_addresses(node_factory):
"""Test that BIP86 addresses are deterministic and unique"""
# Create two nodes with the same mnemonic
@@ -1828,6 +1837,7 @@ def test_bip86_deterministic_addresses(node_factory):
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_vs_regular_p2tr(node_factory):
"""Test that BIP86 addresses are different from regular P2TR addresses"""
l1 = setup_bip86_node(node_factory)
@@ -1845,6 +1855,7 @@ def test_bip86_vs_regular_p2tr(node_factory):
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_full_bitcoin_integration(node_factory, bitcoind):
"""Test full Bitcoin integration: generate addresses, receive funds, list outputs"""
l1 = setup_bip86_node(node_factory)
@@ -1885,6 +1896,7 @@ def test_bip86_full_bitcoin_integration(node_factory, bitcoind):
@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_bip86_mnemonic_recovery(node_factory, bitcoind):
"""Test that funds can be recovered using the same mnemonic in a new wallet"""
# Use a known mnemonic for predictable recovery
@@ -2224,6 +2236,7 @@ def test_withdraw_bech32m(node_factory, bitcoind):
@unittest.skipIf(TEST_NETWORK != 'regtest', "Elements-based schnorr is not yet supported")
+@pytest.mark.skip(reason="Skipping due to script verification issues with BIP86/BIP32 derivation mismatch")
def test_p2tr_deposit_withdrawal(node_factory, bitcoind):
# Don't get any funds from previous runs.
@@ -2262,44 +2275,51 @@ def test_p2tr_deposit_withdrawal(node_factory, bitcoind):
@unittest.skipIf(TEST_NETWORK != 'regtest', "Elements-based schnorr is not yet supported")
+@pytest.mark.skip(reason="Skipping BIP86 tests due to derivation implementation changes")
def test_p2tr_deposit_withdrawal_with_bip86(node_factory, bitcoind):
- """Test P2TR deposit and withdrawal with BIP86 addresses included"""
+ """Test P2TR deposit and withdrawal with BIP86 derivation (default for mnemonic nodes)"""
- # Don't get any funds from previous runs.
- l1 = node_factory.get_node(random_hsm=True)
-
- # Can fetch p2tr addresses through 'all' or specifically, including BIP86
- deposit_addrs = [l1.rpc.newaddr('all')] * 3
- withdrawal_addr = l1.rpc.newaddr('p2tr')
+ # Set up a node with BIP86 support (mnemonic-based HSM secret)
+ l1 = setup_bip86_node(node_factory)
- # Add some funds to withdraw (including BIP86 addresses)
- for addr_type in ['p2tr', 'bech32', 'p2tr-mnemonic']:
- for i in range(3):
- if addr_type in deposit_addrs[i]:
- l1.bitcoin.rpc.sendtoaddress(deposit_addrs[i][addr_type], 1)
+ # Generate a BIP86 P2TR address for deposit
+ deposit_addr = l1.rpc.newaddr('bip86')
+ # Send some funds to the BIP86 P2TR address
+ l1.bitcoin.rpc.sendtoaddress(deposit_addr['p2tr'], 1)
bitcoind.generate_block(1)
- # Wait for funds to be visible (should be more than 6 now due to BIP86)
- wait_for(lambda: len(l1.rpc.listfunds()['outputs']) >= 6)
+ # Wait for the deposit to be visible
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
- # Check that we have funds
+ # Check that we have the deposit
funds = l1.rpc.listfunds()
- assert len(funds['outputs']) >= 6, f"Expected at least 6 outputs, got {len(funds['outputs'])}"
+ assert len(funds['outputs']) == 1
+ assert funds['outputs'][0]['amount_msat'] == 100000000000 # 1 BTC in msat
- l1.rpc.withdraw(withdrawal_addr['p2tr'], 100000000 * 5)
+ # Generate another BIP86 P2TR address for withdrawal
+ withdrawal_addr = l1.rpc.newaddr('bip86')
+
+ # Withdraw to the new BIP86 P2TR address
+ l1.rpc.withdraw(withdrawal_addr['p2tr'], 50000000) # 0.5 BTC
wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 1)
+
+ # Check the withdrawal transaction
raw_tx = bitcoind.rpc.getrawtransaction(bitcoind.rpc.getrawmempool()[0], 1)
- assert len(raw_tx['vin']) >= 6 # Should be at least 6 inputs (including BIP86)
- assert len(raw_tx['vout']) == 2
- # Change goes to p2tr
+ assert len(raw_tx['vin']) == 1 # Should use our 1 BTC input
+ assert len(raw_tx['vout']) == 2 # Withdrawal output + change
+
+ # Both outputs should be P2TR (BIP86)
for output in raw_tx['vout']:
assert output["scriptPubKey"]["type"] == "witness_v1_taproot"
+
bitcoind.generate_block(1)
- wait_for(lambda: len(l1.rpc.listtransactions()['transactions']) >= 7)
- # Only self-send + change is left
- wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 2)
+ # After withdrawal, we should have change left (new output from the withdrawal transaction)
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == 1)
+ funds = l1.rpc.listfunds()
+ # Should have exactly 0.5 BTC (the withdrawal amount went to our own BIP86 address)
+ assert funds['outputs'][0]['amount_msat'] == 50000000000 # 0.5 BTC in msat
@unittest.skipIf(TEST_NETWORK != 'regtest', "Address is network specific")
Why this scored 32/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.