walletrpc: add addresstype "bip86"; make newaddr+listaddresses use it
What changed, and why it matters
This commit adds support for a new Bitcoin address type called 'bip86' in Core Lightning. It lets users generate and list Taproot addresses derived using the BIP86 standard through the lightning-cli tool. The change is a feature addition, not a fix for a known security bug. There is no evidence in the commit or supplied references that this resolves an active vulnerability.
Treat as a routine feature commit. Reviewers may want to verify that bip86_pubkey correctly implements BIP86 (tweaked public key with unspendable script path) and that the separate bip86_max_index prevents address reuse or key-index collisions with the existing bip32 index. No immediate security action is indicated by the supplied materials.
Security signals we found
New RPC address type introduced (bip86 / ADDR_P2TR_MNEMONIC)
Key derivation path now branches on address type (bip32 vs bip86)
Wallet scanning logic extended to track separate bip32_max_index and bip86_max_index
No security-relevant keywords or fixes present in commit message or diff
Evidence from the diff
The patch introduces bip86_pubkey-based derivation and exposes it via the newaddr and listaddresses RPCs as addresstype ‘bip86’ (mapped to ADDR_P2TR_MNEMONIC). It adds wallet_get_new_bip86_index(), updates wallet_can_spend() to scan both bip32 and bip86 key indexes, and adjusts address encoding/output to return only P2TR for BIP86. Test stubs are updated accordingly. No vulnerability, CVE, or security disclosure is referenced in the commit or provided materials.
Changed components
wallet/walletrpc.cwallet/wallet.cwallet/wallet.hwallet/test/run-chain_moves_duplicate-detect.cwallet/test/run-db.cwallet/test/run-migrate_remove_chain_moves_duplicates.cInspect captured patch +127 / −21
diff --git a/wallet/test/run-chain_moves_duplicate-detect.c b/wallet/test/run-chain_moves_duplicate-detect.c
index 88907005..4b80a04e 100644
--- a/wallet/test/run-chain_moves_duplicate-detect.c
+++ b/wallet/test/run-chain_moves_duplicate-detect.c
@@ -28,6 +28,9 @@ static void db_log_(struct logger *log UNUSED, enum log_level level UNUSED, cons
/* Generated stub for bip32_pubkey */
void bip32_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
{ fprintf(stderr, "bip32_pubkey called!\n"); abort(); }
+/* Generated stub for bip86_pubkey */
+void bip86_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
+{ fprintf(stderr, "bip86_pubkey called!\n"); abort(); }
/* Generated stub for bitcoind_getrawblockbyheight_ */
void bitcoind_getrawblockbyheight_(const tal_t *ctx UNNEEDED,
struct bitcoind *bitcoind UNNEEDED,
diff --git a/wallet/test/run-db.c b/wallet/test/run-db.c
index 81e816b7..93d319ec 100644
--- a/wallet/test/run-db.c
+++ b/wallet/test/run-db.c
@@ -25,6 +25,9 @@ static void db_log_(struct logger *log UNUSED, enum log_level level UNUSED, cons
/* Generated stub for bip32_pubkey */
void bip32_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
{ fprintf(stderr, "bip32_pubkey called!\n"); abort(); }
+/* Generated stub for bip86_pubkey */
+void bip86_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
+{ fprintf(stderr, "bip86_pubkey called!\n"); abort(); }
/* Generated stub for bitcoind_getrawblockbyheight_ */
void bitcoind_getrawblockbyheight_(const tal_t *ctx UNNEEDED,
struct bitcoind *bitcoind UNNEEDED,
diff --git a/wallet/test/run-migrate_remove_chain_moves_duplicates.c b/wallet/test/run-migrate_remove_chain_moves_duplicates.c
index 5e312a18..e8cea530 100644
--- a/wallet/test/run-migrate_remove_chain_moves_duplicates.c
+++ b/wallet/test/run-migrate_remove_chain_moves_duplicates.c
@@ -29,6 +29,9 @@ static void db_log_(struct logger *log UNUSED, enum log_level level UNUSED, cons
/* Generated stub for bip32_pubkey */
void bip32_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
{ fprintf(stderr, "bip32_pubkey called!\n"); abort(); }
+/* Generated stub for bip86_pubkey */
+void bip86_pubkey(struct lightningd *ld UNNEEDED, struct pubkey *pubkey UNNEEDED, u32 index UNNEEDED)
+{ fprintf(stderr, "bip86_pubkey called!\n"); abort(); }
/* Generated stub for bitcoind_getrawblockbyheight_ */
void bitcoind_getrawblockbyheight_(const tal_t *ctx UNNEEDED,
struct bitcoind *bitcoind UNNEEDED,
diff --git a/wallet/wallet.c b/wallet/wallet.c
index 3a53ba70..e7619298 100644
--- a/wallet/wallet.c
+++ b/wallet/wallet.c
@@ -16,6 +16,7 @@
#include <lightningd/channel_gossip.h>
#include <lightningd/closed_channel.h>
#include <lightningd/coin_mvts.h>
+#include <lightningd/hsm_control.h>
#include <lightningd/notification.h>
#include <lightningd/peer_htlcs.h>
#include <lightningd/runes.h>
@@ -111,6 +112,25 @@ static void our_addresses_add(struct wallet_address_htable *our_addresses,
wallet_address_htable_add(our_addresses, waddr);
}
+/* Add BIP86 address for a given index */
+static bool our_addresses_add_bip86_for_index(struct wallet *w, u32 i)
+{
+ struct pubkey pubkey;
+ const u8 *scriptpubkey;
+
+ /* Use BIP86 derivation from the base key */
+ bip86_pubkey(w->ld, &pubkey, i);
+
+ /* Create P2TR scriptpubkey from the BIP86 public key */
+ scriptpubkey = scriptpubkey_p2tr(tmpctx, &pubkey);
+ our_addresses_add(w->our_addresses,
+ i,
+ take(scriptpubkey),
+ tal_bytelen(scriptpubkey),
+ ADDR_P2TR_MNEMONIC);
+ return true;
+}
+
static void our_addresses_add_for_index(struct wallet *w, u32 i)
{
struct ext_key ext;
@@ -170,7 +190,11 @@ static void our_addresses_add_for_index(struct wallet *w, u32 i)
ADDR_P2TR);
return;
case ADDR_P2TR_MNEMONIC:
- /* BIP86 addresses not yet implemented */
+ /* BIP86 addresses require HSM derivation */
+ if (our_addresses_add_bip86_for_index(w, i)) {
+ return;
+ }
+ /* If BIP86 derivation fails, skip this address */
return;
}
abort();
@@ -987,13 +1011,17 @@ bool wallet_add_onchaind_utxo(struct wallet *w,
bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len,
u32 *index, enum addrtype *addrtype)
{
- u64 bip32_max_index;
+ u64 bip32_max_index, bip86_max_index;
const struct wallet_address *waddr;
struct script_with_len scriptwl = {script, script_len};
/* Update hash table if we need to */
bip32_max_index = db_get_intvar(w->db, "bip32_max_index", 0);
- while (w->our_addresses_maxindex < bip32_max_index + w->keyscan_gap)
+ bip86_max_index = db_get_intvar(w->db, "bip86_max_index", 0);
+
+ /* Scan both BIP32 and BIP86 addresses */
+ u64 max_index = (bip32_max_index > bip86_max_index) ? bip32_max_index : bip86_max_index;
+ while (w->our_addresses_maxindex < max_index + w->keyscan_gap)
our_addresses_add_for_index(w, ++w->our_addresses_maxindex);
waddr = wallet_address_htable_get(w->our_addresses, &scriptwl);
@@ -1002,8 +1030,13 @@ bool wallet_can_spend(struct wallet *w, const u8 *script, size_t script_len,
/* If we found a used key in the keyscan_gap we should
* remember that. */
- if (waddr->index > bip32_max_index)
- db_set_intvar(w->db, "bip32_max_index", waddr->index);
+ if (waddr->addrtype == ADDR_P2TR_MNEMONIC) {
+ if (waddr->index > bip86_max_index)
+ db_set_intvar(w->db, "bip86_max_index", waddr->index);
+ } else {
+ if (waddr->index > bip32_max_index)
+ db_set_intvar(w->db, "bip32_max_index", waddr->index);
+ }
*index = waddr->index;
if (addrtype)
@@ -1032,6 +1065,27 @@ s64 wallet_get_newindex(struct lightningd *ld, enum addrtype addrtype)
return newidx;
}
+s64 wallet_get_new_bip86_index(struct lightningd *ld)
+{
+ struct db_stmt *db_stmt;
+ u64 newidx = db_get_intvar(ld->wallet->db, "bip86_max_index", 0) + 1;
+
+ if (newidx == BIP32_INITIAL_HARDENED_CHILD)
+ return -1;
+
+ db_set_intvar(ld->wallet->db, "bip86_max_index", newidx);
+ db_stmt = db_prepare_v2(ld->wallet->db,
+ SQL("INSERT INTO addresses ("
+ " keyidx"
+ ", addrtype"
+ ") VALUES (?, ?);"));
+ db_bind_u64(db_stmt, newidx);
+ db_bind_int(db_stmt, wallet_addrtype_in_db(ADDR_P2TR_MNEMONIC));
+ db_exec_prepared_v2(take(db_stmt));
+
+ return newidx;
+}
+
bool wallet_get_addrtype(struct wallet *wallet, u64 idx,
enum addrtype *addrtype)
{
diff --git a/wallet/wallet.h b/wallet/wallet.h
index 81bbe2c7..b4b0ff34 100644
--- a/wallet/wallet.h
+++ b/wallet/wallet.h
@@ -618,6 +618,14 @@ bool wallet_can_spend(struct wallet *w,
*/
s64 wallet_get_newindex(struct lightningd *ld, enum addrtype addrtype);
+/**
+ * wallet_get_new_bip86_index - get a new BIP86 index from the wallet.
+ * @ld: (in) lightning daemon
+ *
+ * Returns -1 on error (key exhaustion).
+ */
+s64 wallet_get_new_bip86_index(struct lightningd *ld);
+
/**
* wallet_get_addrtype - get the address types for this key.
* @wallet: (in) wallet
diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c
index e8fbdb1a..c6229e8f 100644
--- a/wallet/walletrpc.c
+++ b/wallet/walletrpc.c
@@ -96,11 +96,13 @@ static struct command_result *param_newaddr(struct command *cmd,
**addrtype = ADDR_BECH32;
else if (!chainparams->is_elements && json_tok_streq(buffer, tok, "p2tr"))
**addrtype = ADDR_P2TR;
+ else if (!chainparams->is_elements && json_tok_streq(buffer, tok, "bip86"))
+ **addrtype = ADDR_P2TR_MNEMONIC;
else if (json_tok_streq(buffer, tok, "all"))
**addrtype = ADDR_ALL;
else
return command_fail(cmd, JSONRPC2_INVALID_PARAMS,
- "'%s' should be 'p2tr', 'bech32', or 'all', not '%.*s'",
+ "'%s' should be 'p2tr', 'bip86', 'bech32', or 'all', not '%.*s'",
name, tok->end - tok->start, buffer + tok->start);
return NULL;
}
@@ -111,6 +113,19 @@ bool WARN_UNUSED_RESULT newaddr_inner(struct command *cmd, struct pubkey *pubkey
u8 *b32script;
u8 *p2tr_script;
+ /* Handle BIP86 separately since it only supports P2TR */
+ if (addrtype == ADDR_P2TR_MNEMONIC) {
+ keyidx = wallet_get_new_bip86_index(cmd->ld);
+ if (keyidx < 0) return false;
+
+ /* Use HSM for BIP86 derivation */
+ bip86_pubkey(cmd->ld, pubkey, keyidx);
+
+ u8 *script = scriptpubkey_p2tr(tmpctx, pubkey);
+ txfilter_add_scriptpubkey(cmd->ld->owned_txfilter, script);
+ return true;
+ }
+
keyidx = wallet_get_newindex(cmd->ld, addrtype);
if (keyidx < 0) {
// return command_fail(cmd, LIGHTNINGD, "Keys exhausted ");
@@ -148,18 +163,30 @@ static struct command_result *json_newaddr(struct command *cmd,
return command_fail(cmd, LIGHTNINGD, "Keys exhausted ");
};
- bech32 = encode_pubkey_to_addr(cmd, &pubkey, ADDR_BECH32, NULL);
- p2tr = encode_pubkey_to_addr(cmd, &pubkey, ADDR_P2TR, NULL);
- if (!bech32 || !p2tr) {
- return command_fail(cmd, LIGHTNINGD,
- "p2wpkh address encoding failure.");
- }
-
response = json_stream_success(cmd);
- if (*addrtype & ADDR_BECH32)
- json_add_string(response, "bech32", bech32);
- if (*addrtype & ADDR_P2TR)
+
+ /* For BIP86, only return P2TR address */
+ if (*addrtype == ADDR_P2TR_MNEMONIC) {
+ p2tr = encode_pubkey_to_addr(cmd, &pubkey, ADDR_P2TR, NULL);
+ if (!p2tr) {
+ return command_fail(cmd, LIGHTNINGD,
+ "BIP86 P2TR address encoding failure.");
+ }
json_add_string(response, "p2tr", p2tr);
+ } else {
+ /* For other address types, generate both bech32 and p2tr */
+ bech32 = encode_pubkey_to_addr(cmd, &pubkey, ADDR_BECH32, NULL);
+ p2tr = encode_pubkey_to_addr(cmd, &pubkey, ADDR_P2TR, NULL);
+ if (!bech32 || !p2tr) {
+ return command_fail(cmd, LIGHTNINGD,
+ "p2wpkh address encoding failure.");
+ }
+
+ if (*addrtype & ADDR_BECH32)
+ json_add_string(response, "bech32", bech32);
+ if (*addrtype & ADDR_P2TR)
+ json_add_string(response, "p2tr", p2tr);
+ }
return command_success(cmd, response);
}
@@ -172,11 +199,12 @@ AUTODATA(json_command, &newaddr_command);
static void json_add_address_details(struct json_stream *response,
const u64 keyidx,
const char *out_p2wpkh,
- const char *out_p2tr)
+ const char *out_p2tr,
+ enum addrtype addrtype)
{
json_object_start(response, NULL);
json_add_u64(response, "keyidx", keyidx);
- if (!streq(out_p2wpkh, "")) {
+ if (!streq(out_p2wpkh, "") && addrtype != ADDR_P2TR_MNEMONIC) {
json_add_string(response, "bech32", out_p2wpkh);
}
if (!streq(out_p2tr,"")) {
@@ -217,7 +245,14 @@ static struct command_result *json_listaddresses(struct command *cmd,
if (listaddrtypes[i].keyidx == BIP32_INITIAL_HARDENED_CHILD){
break;
}
- bip32_pubkey(cmd->ld, &pubkey, listaddrtypes[i].keyidx);
+ /* Use appropriate derivation based on address type */
+ if (listaddrtypes[i].addrtype == ADDR_P2TR_MNEMONIC) {
+ /* For BIP86 addresses, use BIP86 derivation */
+ bip86_pubkey(cmd->ld, &pubkey, listaddrtypes[i].keyidx);
+ } else {
+ /* For regular addresses, use standard BIP32 derivation */
+ bip32_pubkey(cmd->ld, &pubkey, listaddrtypes[i].keyidx);
+ }
char *out_p2wpkh = "";
char *out_p2tr = "";
if (listaddrtypes[i].addrtype == ADDR_BECH32 || listaddrtypes[i].addrtype == ADDR_ALL) {
@@ -230,7 +265,7 @@ static struct command_result *json_listaddresses(struct command *cmd,
abort();
}
}
- if (listaddrtypes[i].addrtype == ADDR_P2TR || listaddrtypes[i].addrtype == ADDR_ALL) {
+ if (listaddrtypes[i].addrtype == ADDR_P2TR || listaddrtypes[i].addrtype == ADDR_ALL || listaddrtypes[i].addrtype == ADDR_P2TR_MNEMONIC) {
out_p2tr = encode_pubkey_to_addr(cmd,
&pubkey,
ADDR_P2TR,
@@ -240,7 +275,7 @@ static struct command_result *json_listaddresses(struct command *cmd,
}
}
if (!addr || streq(addr, out_p2wpkh) || streq(addr, out_p2tr)) {
- json_add_address_details(response, listaddrtypes[i].keyidx, out_p2wpkh, out_p2tr);
+ json_add_address_details(response, listaddrtypes[i].keyidx, out_p2wpkh, out_p2tr, listaddrtypes[i].addrtype);
if (addr) {
break;
}
Why this scored 20/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.