Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Hsmtool: enable dumping output descriptors of onchain wallet #4171

Merged
merged 8 commits into from
Nov 10, 2020
1 change: 1 addition & 0 deletions common/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ COMMON_SRC_NOGEN := \
common/daemon_conn.c \
common/decode_array.c \
common/derive_basepoints.c \
common/descriptor_checksum.c \
common/dev_disconnect.c \
common/dijkstra.c \
common/ecdh_hsmd.c \
Expand Down
82 changes: 82 additions & 0 deletions common/descriptor_checksum.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
#include <ccan/short_types/short_types.h>
#include <common/descriptor_checksum.h>
#include <stdio.h>
#include <stdlib.h>


static const char CHECKSUM_CHARSET[] = "qpzry9x8gf2tvdw0s3jn54khce6mua7l";

static const char INPUT_CHARSET[] =
"0123456789()[],'/*abcdefgh@:$%{}"
"IJKLMNOPQRSTUVWXYZ&+-.;<=>?!^_|~"
"ijklmnopqrstuvwxyzABCDEFGH`#\"\\ ";

static inline int charset_find(char ch) {
for (size_t i = 0; i < sizeof(INPUT_CHARSET); i++) {
if (INPUT_CHARSET[i] == ch)
return i;
}
return -1;
}

static u64 polymod(u64 c, int val)
{
u8 c0 = c >> 35;
c = ((c & 0x7ffffffff) << 5) ^ val;
if (c0 & 1) c ^= 0xf5dee51989;
if (c0 & 2) c ^= 0xa9fdca3312;
if (c0 & 4) c ^= 0x1bab10e32d;
if (c0 & 8) c ^= 0x3706b1677a;
if (c0 & 16) c ^= 0x644d626ffd;
return c;
}


bool descriptor_checksum(const char *descriptor, int desc_size,
struct descriptor_checksum *checksum)
{
checksum->csum[0] = 0;

int j;
u64 c = 1;
int cls = 0;
int clscount = 0;

for (int i = 0; i < desc_size; i++) {
char ch = descriptor[i];
int pos = charset_find(ch);
if (pos == -1) {
checksum->csum[0] = 0;
return false;
}
/* Emit a symbol for the position inside the group, for every
* character. */
c = polymod(c, pos & 31);

/* Accumulate the group numbers */
cls = cls * 3 + (pos >> 5);

if (++clscount == 3) {
c = polymod(c, cls);
cls = 0;
clscount = 0;
}
}

if (clscount > 0)
c = polymod(c, cls);

/* Shift further to determine the checksum. */
for (j = 0; j < DESCRIPTOR_CHECKSUM_LENGTH; ++j)
c = polymod(c, 0);

/* Prevent appending zeroes from not affecting the checksum. */
c ^= 1;

for (j = 0; j < DESCRIPTOR_CHECKSUM_LENGTH; ++j)
checksum->csum[j] = CHECKSUM_CHARSET[(c >> (5 * (7 - j))) & 31];

checksum->csum[DESCRIPTOR_CHECKSUM_LENGTH] = 0;

return true;
}
16 changes: 16 additions & 0 deletions common/descriptor_checksum.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
#ifndef LIGHTNING_COMMON_DESCRIPTOR_CHECKSUM_H
#define LIGHTNING_COMMON_DESCRIPTOR_CHECKSUM_H
#include "config.h"
#include <stdbool.h>

/* https://github.com/bitcoin/bitcoin/blob/master/doc/descriptors.md#reference */
#define DESCRIPTOR_CHECKSUM_LENGTH 8
darosior marked this conversation as resolved.
Show resolved Hide resolved

struct descriptor_checksum {
char csum[DESCRIPTOR_CHECKSUM_LENGTH + 1];
};

bool descriptor_checksum(const char *descriptor, int desc_size,
struct descriptor_checksum *checksum);

#endif /* LIGHTNING_COMMON_DESCRIPTOR_CHECKSUM_H */
8 changes: 8 additions & 0 deletions contrib/pyln-testing/pyln/testing/fixtures.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,14 @@ def bitcoind(directory, teardown_checks):
raise ValueError("bitcoind is too old. At least version 16000 (v0.16.0)"
" is needed, current version is {}".format(info['version']))

# Make sure we have a wallet, starting with 0.21 there is no default wallet
# anymore.
# FIXME: if we update the testsuite to use the upcoming 0.21 release we
# could switch to descriptor wallets and speed bitcoind operations
# consequently.
if not bitcoind.rpc.listwallets():
bitcoind.rpc.createwallet("lightningd-tests")
Comment on lines +140 to +141
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is slightly nicer than 90f4ea6


info = bitcoind.rpc.getblockchaininfo()
# Make sure we have some spendable funds
if info['blocks'] < 101:
Expand Down
1 change: 0 additions & 1 deletion doc/FAQ.md
Original file line number Diff line number Diff line change
Expand Up @@ -108,7 +108,6 @@ C-lightning has an internal bitcoin wallet, which you can use to make "on-chain"
transactions, (see [withdraw](https://lightning.readthedocs.io/lightning-withdraw.7.html).
These on-chain funds are backed up via the HD wallet seed, stored in byte-form in `hsm_secret`.

and which you can backup thanks to a seed stored in the `hsm_secret`.
`lightningd` also stores information for funds locked in Lightning Network channels, which are stored
in a database. This database is required for on-going channel updates as well as channel closure.
There is no single-seed backup for funds locked in channels.
Expand Down
13 changes: 12 additions & 1 deletion doc/lightning-hsmtool.8
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,17 @@ Specify \fIpassword\fR if the \fBhsm_secret\fR is encrypted\.
\fBgeneratehsm\fR \fIhsm_secret_path\fR
Generates a new hsm_secret using BIP39\.


\fBdumponchaindescriptors\fR \fIhsm_secret\fR [\fIpassword\fR] [\fInetwork\fR]
Dump output descriptors for our onchain wallet\.
The descriptors can be used by external services to be able to generate
addresses for our onchain wallet\. (for example on \fBbitcoind\fR using the
\fBimportmulti\fR or \fBimportdescriptors\fR RPC calls)
We need the path to the hsm_secret containing the wallet seed, and an optional
(skip using \fB""\fR) password if it was encrypted\.
To generate descriptors using testnet master keys, you may specify \fItestnet\fR as
the last parameter\. By default, mainnet-encoded keys are generated\.

.SH BUGS

You should report bugs on our github issues page, and maybe submit a fix
Expand All @@ -80,4 +91,4 @@ Note: the modules in the ccan/ directory have their own licenses, but
the rest of the code is covered by the BSD-style MIT license\.
Main web site: \fIhttps://github.com/ElementsProject/lightning\fR

\" SHA256STAMP:918981692d3840344e15c539b007b473d5ea0ad481145eccff092bf61ec6ddb0
\" SHA256STAMP:3d847c486363271e0635336caca4fd14f5007a3ff463c223fb5bdb52dbf7b98e
10 changes: 10 additions & 0 deletions doc/lightning-hsmtool.8.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,16 @@ Specify *password* if the `hsm_secret` is encrypted.
**generatehsm** *hsm\_secret\_path*
Generates a new hsm_secret using BIP39.

**dumponchaindescriptors** *hsm_secret* \[*password*\] \[*network*\]
Dump output descriptors for our onchain wallet.
The descriptors can be used by external services to be able to generate
addresses for our onchain wallet. (for example on `bitcoind` using the
`importmulti` or `importdescriptors` RPC calls)
We need the path to the hsm_secret containing the wallet seed, and an optional
(skip using `""`) password if it was encrypted.
To generate descriptors using testnet master keys, you may specify *testnet* as
the last parameter. By default, mainnet-encoded keys are generated.

BUGS
----

Expand Down
1 change: 1 addition & 0 deletions tests/fuzz/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ FUZZ_COMMON_OBJS := \
common/daemon.o \
common/daemon_conn.o \
common/derive_basepoints.o \
common/descriptor_checksum.o \
common/fee_states.o \
common/htlc_state.o \
common/permute_tx.o \
Expand Down
5 changes: 1 addition & 4 deletions tests/fuzz/fuzz-amount.c
Original file line number Diff line number Diff line change
Expand Up @@ -21,10 +21,7 @@ void run(const uint8_t *data, size_t size)

/* We should not crash when parsing any string. */

string = tal_arr(NULL, char, size);
for (size_t i = 0; i < size; i++)
string[i] = (char) data[i] % (CHAR_MAX + 1);

string = to_string(NULL, data, size);
parse_amount_msat(&msat, string, tal_count(string));
parse_amount_sat(&sat, string, tal_count(string));
tal_free(string);
Expand Down
20 changes: 20 additions & 0 deletions tests/fuzz/fuzz-descriptor_checksum.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
#include <tests/fuzz/libfuzz.h>

#include <ccan/tal/tal.h>
#include <common/descriptor_checksum.h>

void init(int *argc, char ***argv)
{
}

void run(const uint8_t *data, size_t size)
{
char *string;
struct descriptor_checksum checksum;

/* We should not crash nor overflow the checksum buffer. */

string = to_string(NULL, data, size);
descriptor_checksum(string, tal_count(string), &checksum);
tal_free(string);
}
10 changes: 10 additions & 0 deletions tests/fuzz/libfuzz.c
Original file line number Diff line number Diff line change
Expand Up @@ -31,3 +31,13 @@ const uint8_t **get_chunks(const void *ctx, const uint8_t *data,

return chunks;
}

char *to_string(const tal_t *ctx, const u8 *data, size_t data_size)
{
char *string = tal_arr(ctx, char, data_size);

for (size_t i = 0; i < data_size; i++)
string[i] = (char) data[i] % (CHAR_MAX + 1);

return string;
}
5 changes: 5 additions & 0 deletions tests/fuzz/libfuzz.h
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
#ifndef LIGHTNING_TESTS_FUZZ_LIBFUZZ_H
#define LIGHTNING_TESTS_FUZZ_LIBFUZZ_H

#include <ccan/ccan/short_types/short_types.h>
#include <ccan/ccan/tal/tal.h>
#include <stddef.h>
#include <stdint.h>

Expand All @@ -15,4 +17,7 @@ void run(const uint8_t *data, size_t size);
const uint8_t **get_chunks(const void *ctx, const uint8_t *data,
size_t data_size, size_t chunk_size);

/* Copy the data as a string. */
char *to_string(const tal_t *ctx, const u8 *data, size_t data_size);

#endif /* LIGHTNING_TESTS_FUZZ_LIBFUZZ_H */
30 changes: 30 additions & 0 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -1045,6 +1045,36 @@ def test_hsmtool_secret_decryption(node_factory):
assert node_id == l1.rpc.getinfo()["id"]


@unittest.skipIf(TEST_NETWORK == 'liquid-regtest', '')
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a comment saying that this is only supported for mainnet and testnet (technically we'd require it to be regtest but that appears to match testnet).

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah descriptors for regtest are the same than for testnet (but addresses the same as mainnet!! 😭 )

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hurray for consistency 🤦

def test_hsmtool_dump_descriptors(node_factory, bitcoind):
l1 = node_factory.get_node()
l1.fundwallet(10**6)

# Get a tpub descriptor of lightningd's wallet
hsm_path = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret")
cmd_line = ["tools/hsmtool", "dumponchaindescriptors", hsm_path, "",
"testnet"]
out = subprocess.check_output(cmd_line).decode("utf8").split("\n")
descriptor = [l for l in out if l.startswith("wpkh(tpub")][0]

# Import the descriptor to bitcoind
# FIXME: if we update the testsuite to use the upcoming 0.21 we could use
# importdescriptors instead.
bitcoind.rpc.importmulti([{
"desc": descriptor,
# No need to rescan, we'll transact afterward
"timestamp": "now",
# The default
"range": [0, 99]
}])

# Funds sent to lightningd can be retrieved by bitcoind
addr = l1.rpc.newaddr()["bech32"]
txid = l1.rpc.withdraw(addr, 10**3)["txid"]
bitcoind.generate_block(1, txid)
assert len(bitcoind.rpc.listunspent(1, 1, [addr])) == 1


# this test does a 'listtransactions' on a yet unconfirmed channel
def test_fundchannel_listtransaction(node_factory, bitcoind):
l1, l2 = node_factory.get_nodes(2)
Expand Down
2 changes: 1 addition & 1 deletion tools/Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ tools/headerversions: FORCE tools/headerversions.o $(CCAN_OBJS)

tools/check-bolt: tools/check-bolt.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS)

tools/hsmtool: tools/hsmtool.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/bech32.o common/bigsize.o common/derive_basepoints.o common/node_id.o common/type_to_string.o wire/fromwire.o wire/towire.o
tools/hsmtool: tools/hsmtool.o $(CCAN_OBJS) $(TOOLS_COMMON_OBJS) $(BITCOIN_OBJS) common/amount.o common/bech32.o common/bigsize.o common/derive_basepoints.o common/descriptor_checksum.o common/node_id.o common/type_to_string.o wire/fromwire.o wire/towire.o

tools/lightning-hsmtool: tools/hsmtool
cp $< $@
Expand Down
77 changes: 77 additions & 0 deletions tools/hsmtool.c
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,10 @@
#include <ccan/read_write_all/read_write_all.h>
#include <ccan/str/str.h>
#include <ccan/tal/path/path.h>
#include <ccan/tal/str/str.h>
#include <common/bech32.h>
#include <common/derive_basepoints.h>
#include <common/descriptor_checksum.h>
#include <common/node_id.h>
#include <common/type_to_string.h>
#include <common/utils.h>
Expand Down Expand Up @@ -39,6 +41,8 @@ static void show_usage(const char *progname)
printf(" - guesstoremote <P2WPKH address> <node id> <tries> "
"<path/to/hsm_secret> [hsm_secret password]\n");
printf(" - generatehsm <path/to/new//hsm_secret>\n");
printf(" - dumponchaindescriptors <path/to/hsm_secret> [password] "
"[network]\n");
exit(0);
}

Expand Down Expand Up @@ -513,6 +517,59 @@ static int generate_hsm(const char *hsm_secret_path)
return 0;
}

static int dumponchaindescriptors(const char *hsm_secret_path, const char *passwd,
const bool is_testnet)
{
struct secret hsm_secret;
u8 bip32_seed[BIP32_ENTROPY_LEN_256];
u32 salt = 0;
u32 version = is_testnet ?
BIP32_VER_TEST_PRIVATE : BIP32_VER_MAIN_PRIVATE;
struct ext_key master_extkey;
char *enc_xpub, *descriptor;
struct descriptor_checksum checksum;

if (passwd)
get_encrypted_hsm_secret(&hsm_secret, hsm_secret_path, passwd);
else
get_hsm_secret(&hsm_secret, hsm_secret_path);

/* We use m/0/0/k as the derivation tree for onchain funds. */
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Needs reference. Where in the codebase do we do this?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

lightning/hsmd/hsmd.c

Lines 389 to 462 in 15b2546

/*~ Called at startup to derive the bip32 field. */
static void populate_secretstuff(void)
{
u8 bip32_seed[BIP32_ENTROPY_LEN_256];
u32 salt = 0;
struct ext_key master_extkey, child_extkey;
assert(bip32_key_version.bip32_pubkey_version == BIP32_VER_MAIN_PUBLIC
|| bip32_key_version.bip32_pubkey_version == BIP32_VER_TEST_PUBLIC);
assert(bip32_key_version.bip32_privkey_version == BIP32_VER_MAIN_PRIVATE
|| bip32_key_version.bip32_privkey_version == BIP32_VER_TEST_PRIVATE);
/* Fill in the BIP32 tree for bitcoin addresses. */
/* In libwally-core, the version BIP32_VER_TEST_PRIVATE is for testnet/regtest,
* and BIP32_VER_MAIN_PRIVATE is for mainnet. For litecoin, we also set it like
* bitcoin else.*/
do {
hkdf_sha256(bip32_seed, sizeof(bip32_seed),
&salt, sizeof(salt),
&secretstuff.hsm_secret,
sizeof(secretstuff.hsm_secret),
"bip32 seed", strlen("bip32 seed"));
salt++;
} while (bip32_key_from_seed(bip32_seed, sizeof(bip32_seed),
bip32_key_version.bip32_privkey_version,
0, &master_extkey) != WALLY_OK);
#if DEVELOPER
/* In DEVELOPER mode, we can override with --dev-force-bip32-seed */
if (dev_force_bip32_seed) {
if (bip32_key_from_seed(dev_force_bip32_seed->data,
sizeof(dev_force_bip32_seed->data),
bip32_key_version.bip32_privkey_version,
0, &master_extkey) != WALLY_OK)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Can't derive bip32 master key");
}
#endif /* DEVELOPER */
/* BIP 32:
*
* The default wallet layout
*
* An HDW is organized as several 'accounts'. Accounts are numbered,
* the default account ("") being number 0. Clients are not required
* to support more than one account - if not, they only use the
* default account.
*
* Each account is composed of two keypair chains: an internal and an
* external one. The external keychain is used to generate new public
* addresses, while the internal keychain is used for all other
* operations (change addresses, generation addresses, ..., anything
* that doesn't need to be communicated). Clients that do not support
* separate keychains for these should use the external one for
* everything.
*
* - m/iH/0/k corresponds to the k'th keypair of the external chain of
* account number i of the HDW derived from master m.
*/
/* Hence child 0, then child 0 again to get extkey to derive from. */
if (bip32_key_from_parent(&master_extkey, 0, BIP32_FLAG_KEY_PRIVATE,
&child_extkey) != WALLY_OK)
/*~ status_failed() is a helper which exits and sends lightningd
* a message about what happened. For hsmd, that's fatal to
* lightningd. */
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Can't derive child bip32 key");
if (bip32_key_from_parent(&child_extkey, 0, BIP32_FLAG_KEY_PRIVATE,
&secretstuff.bip32) != WALLY_OK)
status_failed(STATUS_FAIL_INTERNAL_ERROR,
"Can't derive private bip32 key");
}
...

Copy link
Collaborator

@niftynei niftynei Nov 6, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For the record, the intention here is that you'd update the code comment so other readers know where you're getting the info from, not just post a reply in github.


/* The root seed is derived from hsm_secret using hkdf.. */
do {
hkdf_sha256(bip32_seed, sizeof(bip32_seed),
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Duplicating this code from hsmd/hsmd.c#populate_secretstuff is a bad idea. Needs to be a shared routine that both hsmd and here use.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's how we've created tool since now ?..

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't follow. We really want this code to be shared so that if one changes, the other also gets updated.

&salt, sizeof(salt),
&hsm_secret, sizeof(hsm_secret),
"bip32 seed", strlen("bip32 seed"));
salt++;
/* ..Which is used to derive m/ */
} while (bip32_key_from_seed(bip32_seed, sizeof(bip32_seed),
version, 0, &master_extkey) != WALLY_OK);

if (bip32_key_to_base58(&master_extkey, BIP32_FLAG_KEY_PUBLIC, &enc_xpub) != WALLY_OK)
errx(ERROR_LIBWALLY, "Can't encode xpub");

/* Now we format the descriptor strings (we only ever create P2WPKH and
* P2SH-P2WPKH outputs). */
darosior marked this conversation as resolved.
Show resolved Hide resolved

descriptor = tal_fmt(NULL, "wpkh(%s/0/0/*)", enc_xpub);
if (!descriptor_checksum(descriptor, strlen(descriptor), &checksum))
errx(ERROR_LIBWALLY, "Can't derive descriptor checksum for wpkh");
printf("%s#%s\n", descriptor, checksum.csum);
tal_free(descriptor);

descriptor = tal_fmt(NULL, "sh(wpkh(%s/0/0/*))", enc_xpub);
if (!descriptor_checksum(descriptor, strlen(descriptor), &checksum))
errx(ERROR_LIBWALLY, "Can't derive descriptor checksum for sh(wpkh)");
printf("%s#%s\n", descriptor, checksum.csum);
tal_free(descriptor);

wally_free_string(enc_xpub);

return 0;
}

int main(int argc, char *argv[])
{
const char *method;
Expand Down Expand Up @@ -573,5 +630,25 @@ int main(int argc, char *argv[])
return generate_hsm(hsm_secret_path);
}

if (streq(method, "dumponchaindescriptors")) {
bool is_testnet;
if (argc < 3)
show_usage(argv[0]);

if (argc > 4) {
is_testnet = streq(argv[4], "testnet");
if (!is_testnet && !streq(argv[4], "bitcoin"))
errx(ERROR_USAGE, "Network '%s' not supported."
" Supported networks: bitcoin (default),"
" testnet",
argv[4]);
} else
is_testnet = false;

return dumponchaindescriptors(argv[2],
argc > 3 && !streq(argv[3], "") ? argv[3] : NULL,
is_testnet);
}

show_usage(argv[0]);
}