tests: add BIP86 support

This commit is contained in:
Sangbida Chaudhuri
2025-10-24 13:57:47 +10:30
committed by Rusty Russell
parent bf508387a3
commit 7f3a57cc41
3 changed files with 316 additions and 4 deletions

View File

@@ -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"
}
}
]
},

View File

@@ -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"
}
}
]
}

View File

@@ -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