diff --git a/contrib/msggen/msggen/schema.json b/contrib/msggen/msggen/schema.json index 9a0c0266d..d2a6f4359 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 098a338c4..684a60841 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 88a6bd04a..0a002ba6d 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