Skip to content

Commit c7e091c

Browse files
committed
api: add a custom BIP85 derivation for Lightning hot wallets
For the generation of a mnemonic for Breez-SDK Lightning hot wallets in the BitBoxApp. Using the regular BIP85-BIP39 path like `m/83696968'/39'/0'/12'/0'` for this is problematic as a user might use the same mnemonics for something else, e.g. another cold storage wallet. In such a case, the derived mnemonic of the user would become hot without the user realizing it. For the purpose of generating entropy for a lightning hot wallet using BIP85, we should instead use a different application number dedicated to this. We are going with number `19534'`, which is unlikely to interfere with other uses. It's hex representation `0x4c4e` spells `LN` in ASCII. The BIP85 derivation would then be `m/83696968'/{app_no}/{language}'/{words}'/` as in BIP85-BIP39, but use `19534'` for the `app_no` instead of `39'`, so: m/83696968'/19534'/0'/12'/0' We restrict to only 16 byte derived entropy (equivalent of 12 words mnemonics) for LN hot wallet mnemonics for simplicity and easier recovery for users. The index represents the account number - for now we only allow `0` (the first account), and can extend to multiple if there is ever a need.
1 parent b554549 commit c7e091c

File tree

14 files changed

+192
-24
lines changed

14 files changed

+192
-24
lines changed

CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,10 @@ customers cannot upgrade their bootloader, its changes are recorded separately.
99
### [Unreleased]
1010
- Add support for deriving BIP-39 mnemonics according to BIP-85
1111

12+
### 9.17.0
13+
- Add support for deriving mnemonics for Lightning hot wallets according to BIP-85 (using a custom
14+
BIP-85 application number)
15+
1216
### 9.16.0
1317
- Disable screensaver when displaying a receive address, confirming a transaction, and other interactive actions
1418
- Add Sepolia testnet for Ethereum

messages/keystore.proto

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,13 +15,19 @@ message ElectrumEncryptionKeyResponse {
1515
}
1616

1717
message BIP85Request {
18+
message AppLn {
19+
uint32 account_number = 1;
20+
}
21+
1822
oneof app {
1923
google.protobuf.Empty bip39 = 1;
24+
AppLn ln = 2;
2025
}
2126
}
2227

2328
message BIP85Response {
2429
oneof app {
2530
google.protobuf.Empty bip39 = 1;
31+
bytes ln = 2;
2632
}
2733
}

py/bitbox02/bitbox02/bitbox02/bitbox02.py

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -690,7 +690,28 @@ def bip85_bip39(self) -> None:
690690
bip39=google.protobuf.empty_pb2.Empty(),
691691
)
692692
)
693-
self._msg_query(request)
693+
response = self._msg_query(request, expected_response="bip85").bip85
694+
assert response.WhichOneof("app") == "bip39"
695+
696+
def bip85_ln(self) -> bytes:
697+
"""
698+
Generates and returns a mnemonic for a hot Lightning wallet from the device using BIP-85.
699+
"""
700+
self._require_atleast(semver.VersionInfo(9, 17, 0))
701+
702+
# Only account_number=0 is allowed for now.
703+
account_number = 0
704+
705+
# pylint: disable=no-member
706+
request = hww.Request()
707+
request.bip85.CopyFrom(
708+
keystore.BIP85Request(
709+
ln=keystore.BIP85Request.AppLn(account_number=account_number),
710+
)
711+
)
712+
response = self._msg_query(request, expected_response="bip85").bip85
713+
assert response.WhichOneof("app") == "ln"
714+
return response.ln
694715

695716
def enable_mnemonic_passphrase(self) -> None:
696717
"""

py/bitbox02/bitbox02/communication/generated/keystore_pb2.py

Lines changed: 7 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

py/bitbox02/bitbox02/communication/generated/keystore_pb2.pyi

Lines changed: 23 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -37,28 +37,45 @@ global___ElectrumEncryptionKeyResponse = ElectrumEncryptionKeyResponse
3737

3838
class BIP85Request(google.protobuf.message.Message):
3939
DESCRIPTOR: google.protobuf.descriptor.Descriptor
40+
class AppLn(google.protobuf.message.Message):
41+
DESCRIPTOR: google.protobuf.descriptor.Descriptor
42+
ACCOUNT_NUMBER_FIELD_NUMBER: builtins.int
43+
account_number: builtins.int
44+
def __init__(self,
45+
*,
46+
account_number: builtins.int = ...,
47+
) -> None: ...
48+
def ClearField(self, field_name: typing_extensions.Literal["account_number",b"account_number"]) -> None: ...
49+
4050
BIP39_FIELD_NUMBER: builtins.int
51+
LN_FIELD_NUMBER: builtins.int
4152
@property
4253
def bip39(self) -> google.protobuf.empty_pb2.Empty: ...
54+
@property
55+
def ln(self) -> global___BIP85Request.AppLn: ...
4356
def __init__(self,
4457
*,
4558
bip39: typing.Optional[google.protobuf.empty_pb2.Empty] = ...,
59+
ln: typing.Optional[global___BIP85Request.AppLn] = ...,
4660
) -> None: ...
47-
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> builtins.bool: ...
48-
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> None: ...
49-
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39"]]: ...
61+
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> builtins.bool: ...
62+
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> None: ...
63+
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39","ln"]]: ...
5064
global___BIP85Request = BIP85Request
5165

5266
class BIP85Response(google.protobuf.message.Message):
5367
DESCRIPTOR: google.protobuf.descriptor.Descriptor
5468
BIP39_FIELD_NUMBER: builtins.int
69+
LN_FIELD_NUMBER: builtins.int
5570
@property
5671
def bip39(self) -> google.protobuf.empty_pb2.Empty: ...
72+
ln: builtins.bytes
5773
def __init__(self,
5874
*,
5975
bip39: typing.Optional[google.protobuf.empty_pb2.Empty] = ...,
76+
ln: builtins.bytes = ...,
6077
) -> None: ...
61-
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> builtins.bool: ...
62-
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39"]) -> None: ...
63-
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39"]]: ...
78+
def HasField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> builtins.bool: ...
79+
def ClearField(self, field_name: typing_extensions.Literal["app",b"app","bip39",b"bip39","ln",b"ln"]) -> None: ...
80+
def WhichOneof(self, oneof_group: typing_extensions.Literal["app",b"app"]) -> typing.Optional[typing_extensions.Literal["bip39","ln"]]: ...
6481
global___BIP85Response = BIP85Response

py/send_message.py

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -341,7 +341,17 @@ def _get_electrum_encryption_key(self) -> None:
341341
)
342342

343343
def _bip85_bip39(self) -> None:
344-
self._device.bip85_bip39()
344+
try:
345+
self._device.bip85_bip39()
346+
except UserAbortException:
347+
print("Aborted by user")
348+
349+
def _bip85_ln(self) -> None:
350+
try:
351+
entropy = self._device.bip85_ln()
352+
print("Derived entropy for a Breez Lightning wallet:", entropy.hex())
353+
except UserAbortException:
354+
print("Aborted by user")
345355

346356
def _btc_address(self) -> None:
347357
def address(display: bool) -> str:
@@ -1392,6 +1402,7 @@ def _menu_init(self) -> None:
13921402
("Cardano", self._cardano),
13931403
("Show Electrum wallet encryption key", self._get_electrum_encryption_key),
13941404
("BIP85 - BIP39", self._bip85_bip39),
1405+
("BIP85 - LN", self._bip85_ln),
13951406
("Reset Device", self._reset_device),
13961407
)
13971408
choice = ask_user(choices)

src/CMakeLists.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -330,6 +330,7 @@ add_custom_target(rust-bindgen
330330
--allowlist-function keystore_get_bip39_word
331331
--allowlist-function keystore_get_ed25519_seed
332332
--allowlist-function keystore_bip85_bip39
333+
--allowlist-function keystore_bip85_ln
333334
--allowlist-function keystore_secp256k1_compressed_to_uncompressed
334335
--allowlist-function keystore_secp256k1_nonce_commit
335336
--allowlist-function keystore_secp256k1_sign

src/keystore.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -864,6 +864,30 @@ bool keystore_bip85_bip39(
864864
return snprintf_result >= 0 && snprintf_result < (int)mnemonic_out_size;
865865
}
866866

867+
bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out)
868+
{
869+
if (index >= BIP32_INITIAL_HARDENED_CHILD) {
870+
return false;
871+
}
872+
873+
const uint32_t keypath[] = {
874+
83696968 + BIP32_INITIAL_HARDENED_CHILD,
875+
19534 + BIP32_INITIAL_HARDENED_CHILD,
876+
0 + BIP32_INITIAL_HARDENED_CHILD,
877+
12 + BIP32_INITIAL_HARDENED_CHILD,
878+
index + BIP32_INITIAL_HARDENED_CHILD,
879+
};
880+
881+
uint8_t entropy[64] = {0};
882+
UTIL_CLEANUP_64(entropy);
883+
if (!_bip85_entropy(keypath, sizeof(keypath) / sizeof(uint32_t), entropy)) {
884+
return false;
885+
}
886+
887+
memcpy(entropy_out, entropy, 16);
888+
return true;
889+
}
890+
867891
USE_RESULT bool keystore_encode_xpub_at_keypath(
868892
const uint32_t* keypath,
869893
size_t keypath_len,

src/keystore.h

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,16 @@ USE_RESULT bool keystore_bip85_bip39(
253253
char* mnemonic_out,
254254
size_t mnemonic_out_size);
255255

256+
/**
257+
* Computes a 16 byte deterministic seed specifically for Lightning hot wallets according to BIP-85.
258+
* It is the same as BIP-85 with app number 39', but instead using app number 19534' (= 0x4c4e =
259+
* 'LN'). https://github.com/bitcoin/bips/blob/master/bip-0085.mediawiki#bip39
260+
* Restricted to 16 byte output entropy.
261+
* @param[in] index must be smaller than `BIP32_INITIAL_HARDENED_CHILD`.
262+
* @param[out] entropy_out resulting entropy, must be at least 16 bytes in size.
263+
*/
264+
USE_RESULT bool keystore_bip85_ln(uint32_t index, uint8_t* entropy_out);
265+
256266
/**
257267
* Encode an xpub at the given `keypath` as 78 bytes according to BIP32. The version bytes are
258268
* the ones corresponding to `xpub`, i.e. 0x0488B21E.

src/rust/bitbox02-rust/Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -107,7 +107,7 @@ app-cardano = [
107107
"ed25519"
108108
]
109109

110-
app-bip85 = []
110+
app-bip85-bip39 = []
111111

112112
testing = [
113113
# enable these deps

0 commit comments

Comments
 (0)