What changed, and why it matters
This commit only adds tests and documentation for a new wallet address type called BIP86. It does not change the actual wallet or address-generation code. There is no security bug visible in this patch.
No security action needed. If reviewing the broader BIP86 feature, verify the actual implementation commit(s) that this test commit exercises.
Security signals we found
No strong security signals were identified.
Evidence from the diff
The commit updates JSON schema documentation for the newaddr RPC and adds regtest-only Python tests for BIP86-derived taproot addresses. It introduces a test helper that creates a node with use-bip86-derivation, generates addresses, checks deterministic derivation against known vectors, and verifies deposits/withdrawals. No implementation code is modified.
Changed components
tests/test_wallet.pydoc/schemas/newaddr.jsoncontrib/msggen/msggen/schema.jsonInspect captured patch +316 / −4
diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json
index 9a0c0266..d2a6f435 100644
--- a/contrib/msggen/msggen/schema.json
+++ b/contrib/msggen/msggen/schema.json
@@ -27135,12 +27135,13 @@
"addresstype": {
"type": "string",
"description": [
- "It specifies the type of address wanted; currently *bech32* (e.g. `tb1qu9j4lg5f9rgjyfhvfd905vw46eg39czmktxqgg` on bitcoin testnet or `bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej` on bitcoin mainnet), or *p2tr* taproot addresses. The special value *all* generates all known address types for the same underlying key."
+ "It specifies the type of address wanted; currently *bech32* (e.g. `tb1qu9j4lg5f9rgjyfhvfd905vw46eg39czmktxqgg` on bitcoin testnet or `bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej` on bitcoin mainnet), *p2tr* taproot addresses, or *bip86* for BIP86-derived taproot addresses. The special value *all* generates all known address types for the same underlying key."
],
"default": "*bech32* address",
"enum": [
"bech32",
"p2tr",
+ "bip86",
"all"
]
}
@@ -27154,7 +27155,7 @@
"added": "v23.08",
"type": "string",
"description": [
- "The taproot address."
+ "The taproot address (returned for both 'p2tr' and 'bip86' addresstype)."
]
},
"bech32": {
@@ -27202,6 +27203,18 @@
"response": {
"p2tr": "bcrt1p2gppccw6ywewmg74qqxxmqfdpjds3rpr0mf22y9tm9xcc0muggwsea9nkf"
}
+ },
+ {
+ "request": {
+ "id": "example:newaddr#3",
+ "method": "newaddr",
+ "params": {
+ "addresstype": "bip86"
+ }
+ },
+ "response": {
+ "p2tr": "bcrt1p2gppccw6ywewmg74qqxxmqfdpjds3rpr0mf22y9tm9xcc0muggwsea9nkf"
+ }
}
]
},
diff --git a/doc/schemas/newaddr.json b/doc/schemas/newaddr.json
index 098a338c..684a6084 100644
--- a/doc/schemas/newaddr.json
+++ b/doc/schemas/newaddr.json
@@ -17,12 +17,13 @@
"addresstype": {
"type": "string",
"description": [
- "It specifies the type of address wanted; currently *bech32* (e.g. `tb1qu9j4lg5f9rgjyfhvfd905vw46eg39czmktxqgg` on bitcoin testnet or `bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej` on bitcoin mainnet), or *p2tr* taproot addresses. The special value *all* generates all known address types for the same underlying key."
+ "It specifies the type of address wanted; currently *bech32* (e.g. `tb1qu9j4lg5f9rgjyfhvfd905vw46eg39czmktxqgg` on bitcoin testnet or `bc1qwqdg6squsna38e46795at95yu9atm8azzmyvckulcc7kytlcckxswvvzej` on bitcoin mainnet), *p2tr* taproot addresses, or *bip86* for BIP86-derived taproot addresses. The special value *all* generates all known address types for the same underlying key."
],
"default": "*bech32* address",
"enum": [
"bech32",
"p2tr",
+ "bip86",
"all"
]
}
@@ -36,7 +37,7 @@
"added": "v23.08",
"type": "string",
"description": [
- "The taproot address."
+ "The taproot address (returned for both 'p2tr' and 'bip86' addresstype)."
]
},
"bech32": {
@@ -84,6 +85,18 @@
"response": {
"p2tr": "bcrt1p2gppccw6ywewmg74qqxxmqfdpjds3rpr0mf22y9tm9xcc0muggwsea9nkf"
}
+ },
+ {
+ "request": {
+ "id": "example:newaddr#3",
+ "method": "newaddr",
+ "params": {
+ "addresstype": "bip86"
+ }
+ },
+ "response": {
+ "p2tr": "bcrt1p2gppccw6ywewmg74qqxxmqfdpjds3rpr0mf22y9tm9xcc0muggwsea9nkf"
+ }
}
]
}
diff --git a/tests/test_wallet.py b/tests/test_wallet.py
index 88a6bd04..0a002ba6 100644
--- a/tests/test_wallet.py
+++ b/tests/test_wallet.py
@@ -1692,6 +1692,251 @@ def test_hsmtool_deterministic_node_ids(node_factory):
assert normal_node_id == generated_node_id, f"Node IDs don't match: {normal_node_id} != {generated_node_id}"
+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})
+
+ # Set up node with a mnemonic HSM secret
+ hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
+ if os.path.exists(hsm_path):
+ os.remove(hsm_path)
+
+ # Generate hsm_secret with the specified mnemonic
+ hsmtool = HsmTool(node_factory.directory, "generatehsm", hsm_path)
+ master_fd, slave_fd = os.openpty()
+ hsmtool.start(stdin=slave_fd)
+ hsmtool.wait_for_log(r"Introduce your BIP39 word list")
+ write_all(master_fd, f"{mnemonic}\n".encode("utf-8"))
+ hsmtool.wait_for_log(r"Enter your passphrase:")
+ write_all(master_fd, "\n".encode("utf-8")) # No passphrase
+ assert hsmtool.proc.wait(WAIT_TIMEOUT) == 0
+ os.close(master_fd)
+ os.close(slave_fd)
+
+ l1.start()
+ return l1
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+def test_bip86_newaddr_rpc(node_factory, chainparams):
+ """Test that BIP86 addresses can be generated via newaddr RPC"""
+ l1 = setup_bip86_node(node_factory)
+
+ # Test BIP86 address generation
+ bip86_addr = l1.rpc.newaddr(addresstype="bip86")
+ assert 'p2tr' in bip86_addr
+ assert 'bech32' not in bip86_addr
+
+ # Verify address format (taproot addresses are longer)
+ p2tr_addr = bip86_addr['p2tr']
+ assert len(p2tr_addr) > 50
+
+ # In regtest, should start with bcrt1p (or appropriate prefix)
+ assert p2tr_addr.startswith('bcrt1p')
+
+ # Test that we're using the correct 64-byte seed from the mnemonic
+ # Expected seed for "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about":
+ # "5eb00bbddcf069084889a8ab9155568165f5c453ccb85e70811aaed6f6da5fc19a5ac40b389cd370d086206dec8aa6c43daea6690f20ad3d8d48b2d2ce9e38e4"
+
+ # Test that our BIP86 implementation follows the correct derivation path m/86'/0'/0'/0/index
+ # Generate the same address again and verify it's identical
+ bip86_addr2 = l1.rpc.newaddr(addresstype="bip86")
+ p2tr_addr2 = bip86_addr2['p2tr']
+
+ # The second address should be different (next index)
+ assert p2tr_addr != p2tr_addr2, "Consecutive BIP86 addresses should be different"
+
+ # Test against known test vectors for the exact derivation path
+ # The mainnet test vectors are:
+ # m/86'/0'/0'/0/0: bc1p5cyxnuxmeuwuvkwfem96lqzszd02n6xdcjrs20cac6yqjjwudpxqkedrcr
+ # m/86'/0'/0'/0/1: bc1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0was9fqzwh
+ # m/86'/0'/0'/0/2: bc1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzs9khxz8
+
+ # For regtest, the addresses should be the same but with bcrt1p prefix
+ # Our addresses are for indices 1 and 2, so they should match the regtest versions
+ expected_regtest_addr_1 = "bcrt1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0waslcutpz" # index 1
+ expected_regtest_addr_2 = "bcrt1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzsl8t0dj" # index 2
+
+ # Assert on the exact test vectors since we have the correct seed
+ assert p2tr_addr == expected_regtest_addr_1, f"First address should match test vector for index 1. Expected: {expected_regtest_addr_1}, Got: {p2tr_addr}"
+ assert p2tr_addr2 == expected_regtest_addr_2, f"Second address should match test vector for index 2. Expected: {expected_regtest_addr_2}, Got: {p2tr_addr2}"
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+def test_bip86_listaddresses(node_factory, chainparams):
+ """Test that listaddresses includes BIP86 addresses and verifies first 10 addresses"""
+ l1 = setup_bip86_node(node_factory)
+
+ # Expected addresses for the first 10 indices (m/86'/0'/0'/0/1 through m/86'/0'/0'/0/10)
+ # These are derived from the test mnemonic "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+ # Note: newaddr starts from index 1, not 0
+ # Actual regtest addresses generated by the implementation
+ expected_addrs = [
+ "bcrt1p4qhjn9zdvkux4e44uhx8tc55attvtyu358kutcqkudyccelu0waslcutpz", # index 1
+ "bcrt1p0d0rhyynq0awa9m8cqrcr8f5nxqx3aw29w4ru5u9my3h0sfygnzsl8t0dj", # index 2
+ "bcrt1py0vryk8aqusz65yzuudypggvswzkcpwtau8q0sjm0stctwup0xlqv86kkk", # index 3
+ "bcrt1pjpp8nwqvhkx6kdna6vpujdqglvz2304twfd308ve5ppyxpmcjufsy8x0tk", # index 4
+ "bcrt1pl4frjws098l3nslfjlnry6jxt46w694kuexvs5ar0cmkvxyahfkq42fumu", # index 5
+ "bcrt1p5sxs429uz2s2yn6tt98sf67qwytwvffae4dqnzracq586cu0t6zsn63pre", # index 6
+ "bcrt1pxsvy7ep2awd5x9lg90tgm4xre8wxcuj5cpgun8hmzwqnltqha8pqv84cl7", # index 7
+ "bcrt1ptk8pqtszta5pv5tymccfqkezf3f2q39765q4fj8zcr79np6wmj6qeek4z3", # index 8
+ "bcrt1p7pkeudt8wq7fc6nzj6yxkqmnrjg25fu4s9l777ca3w3qrxanjehq4tphz0", # index 9
+ "bcrt1pzhnqyfvxe08zl0d9e592t62pwvp3l2xwhau5a8dsfjcker6xmjuqppvxka", # index 10
+ ]
+
+ # Generate the first 10 BIP86 addresses and verify they match expected values
+ for i in range(10):
+ addr_result = l1.rpc.newaddr('bip86')
+ assert addr_result['p2tr'] == expected_addrs[i]
+
+ # Use listaddresses with start and limit parameters to verify the addresses were generated
+ addrs = l1.rpc.listaddresses(start=1, limit=10)
+ assert len(addrs['addresses']) == 10, f"Expected 10 addresses, got {len(addrs['addresses'])}"
+
+ # Verify that listaddresses returns the correct addresses and key indices
+ for i, addr_info in enumerate(addrs['addresses']):
+ assert addr_info['keyidx'] == i + 1, f"Expected keyidx {i + 1}, got {addr_info['keyidx']}"
+ # BIP86 addresses should have a p2tr field with the correct address
+ assert 'p2tr' in addr_info, f"BIP86 address should have p2tr field, got: {addr_info}"
+ assert addr_info['p2tr'] == expected_addrs[i], f"Address mismatch at index {i + 1}: expected {expected_addrs[i]}, got {addr_info['p2tr']}"
+ # BIP86 addresses should NOT have a bech32 field (they're P2TR only)
+ assert 'bech32' not in addr_info, f"BIP86 address should not have bech32 field, got: {addr_info}"
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+def test_bip86_deterministic_addresses(node_factory):
+ """Test that BIP86 addresses are deterministic and unique"""
+ # Create two nodes with the same mnemonic
+ mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+ l1 = setup_bip86_node(node_factory, mnemonic)
+ l2 = setup_bip86_node(node_factory, mnemonic)
+
+ # Generate addresses with the same index
+ addr1_0 = l1.rpc.newaddr('bip86')['p2tr']
+ addr2_0 = l2.rpc.newaddr('bip86')['p2tr']
+
+ addr1_1 = l1.rpc.newaddr('bip86')['p2tr']
+ addr2_1 = l2.rpc.newaddr('bip86')['p2tr']
+
+ # Addresses should be identical for the same index
+ assert addr1_0 == addr2_0, f"Addresses for index 0 don't match: {addr1_0} != {addr2_0}"
+ assert addr1_1 == addr2_1, f"Addresses for index 1 don't match: {addr1_1} != {addr2_1}"
+
+ # Addresses should be different for different indices
+ assert addr1_0 != addr1_1, f"Addresses for different indices should be different"
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+def test_bip86_vs_regular_p2tr(node_factory):
+ """Test that BIP86 addresses are different from regular P2TR addresses"""
+ l1 = setup_bip86_node(node_factory)
+
+ # Generate addresses of both types
+ bip86_addr = l1.rpc.newaddr('bip86')['p2tr']
+ p2tr_addr = l1.rpc.newaddr('p2tr')['p2tr']
+
+ # They should be different
+ assert bip86_addr != p2tr_addr, "BIP86 and regular P2TR addresses should be different"
+
+ # Both should be valid Taproot addresses (start with bcrt1p for regtest)
+ assert bip86_addr.startswith('bcrt1p')
+ assert p2tr_addr.startswith('bcrt1p')
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+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)
+
+ # Generate a BIP86 address
+ bip86_addr = l1.rpc.newaddr('bip86')['p2tr']
+
+ # Send funds to the BIP86 address
+ amount = 1000000 # 0.01 BTC
+ bitcoind.rpc.sendtoaddress(bip86_addr, amount / 10**8)
+
+ # Mine a block to confirm the transaction
+ bitcoind.generate_block(1)
+
+ # Wait for the node to see the transaction
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > 0)
+
+ # Check that the funds are visible
+ funds = l1.rpc.listfunds()
+ bip86_outputs = [out for out in funds['outputs'] if out['address'] == bip86_addr]
+
+ assert len(bip86_outputs) == 1, f"Expected 1 output, got {len(bip86_outputs)}"
+ assert bip86_outputs[0]['amount_msat'] == amount * 1000, f"Amount mismatch: {bip86_outputs[0]['amount_msat']} != {amount * 1000}"
+ assert bip86_outputs[0]['status'] == 'confirmed'
+
+ # Test withdrawal from BIP86 address
+ # Use bitcoind to generate withdrawal address since this node only supports BIP86
+ withdraw_addr = bitcoind.rpc.getnewaddress()
+ withdraw_amount = 500000 # 0.005 BTC
+
+ l1.rpc.withdraw(withdraw_addr, withdraw_amount)
+
+ # Mine another block
+ bitcoind.generate_block(1)
+
+ # Check that the withdrawal worked
+ wait_for(lambda: len([out for out in l1.rpc.listfunds()['outputs'] if out['address'] == bip86_addr and out['status'] == 'confirmed']) == 0)
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+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
+ mnemonic = "abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon abandon about"
+
+ # Create first node and fund it
+ l1 = setup_bip86_node(node_factory, mnemonic)
+ bip86_addr = l1.rpc.newaddr('bip86')['p2tr']
+
+ # Send funds
+ amount = 1000000 # 0.01 BTC
+ bitcoind.rpc.sendtoaddress(bip86_addr, amount / 10**8)
+ bitcoind.generate_block(1)
+
+ # Wait for funds to be visible
+ wait_for(lambda: len(l1.rpc.listfunds()['outputs']) > 0)
+
+ # Create a second node with the same mnemonic
+ l2 = setup_bip86_node(node_factory, mnemonic)
+
+ # Wait for it to sync and see the funds
+ wait_for(lambda: len(l2.rpc.listfunds()['outputs']) > 0)
+
+ # Check that the second node can see the same funds
+ funds2 = l2.rpc.listfunds()
+ bip86_outputs2 = [out for out in funds2['outputs'] if out['address'] == bip86_addr]
+
+ assert len(bip86_outputs2) == 1, f"Expected 1 output in recovered wallet, got {len(bip86_outputs2)}"
+ assert bip86_outputs2[0]['amount_msat'] == amount * 1000, f"Amount mismatch in recovered wallet: {bip86_outputs2[0]['amount_msat']} != {amount * 1000}"
+
+
+@unittest.skipIf(TEST_NETWORK != 'regtest', "BIP86 tests are regtest-specific")
+def test_bip86_address_type_validation(node_factory):
+ """Test address type validation for BIP86 addresses"""
+ l1 = setup_bip86_node(node_factory)
+
+ # Test that 'bip86' is a valid address type
+ bip86_addr = l1.rpc.newaddr('bip86')['p2tr']
+
+ # Test that we can list addresses
+ addrs = l1.rpc.listaddresses()
+ assert len(addrs['addresses']) >= 1, "No addresses found in listaddresses"
+
+ # Verify the address structure
+ for addr in addrs['addresses']:
+ assert 'keyidx' in addr
+ assert isinstance(addr['keyidx'], int)
+
+ # We can find our address right?
+ assert bip86_addr in [a.get('p2tr') for a in addrs['addresses']]
+
+
# this test does a 'listtransactions' on a yet unconfirmed channel
def test_fundchannel_listtransaction(node_factory, bitcoind):
l1, l2 = node_factory.get_nodes(2)
@@ -1927,6 +2172,47 @@ def test_p2tr_deposit_withdrawal(node_factory, bitcoind):
# make sure tap derivation is embedded in PSBT output
+@unittest.skipIf(TEST_NETWORK != 'regtest', "Elements-based schnorr is not yet supported")
+def test_p2tr_deposit_withdrawal_with_bip86(node_factory, bitcoind):
+ """Test P2TR deposit and withdrawal with BIP86 addresses included"""
+
+ # 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')
+
+ # 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)
+
+ 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)
+
+ # Check that we have funds
+ funds = l1.rpc.listfunds()
+ assert len(funds['outputs']) >= 6, f"Expected at least 6 outputs, got {len(funds['outputs'])}"
+
+ l1.rpc.withdraw(withdrawal_addr['p2tr'], 100000000 * 5)
+ wait_for(lambda: len(bitcoind.rpc.getrawmempool()) == 1)
+ 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
+ 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)
+
+
@unittest.skipIf(TEST_NETWORK != 'regtest', "Address is network specific")
def test_upgradewallet(node_factory, bitcoind):
# Make sure bitcoind doesn't think it's going backwards
Why this scored 18/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.