diff --git a/common/hsm_secret.c b/common/hsm_secret.c index 253553973cc8..9f3fff801b6a 100644 --- a/common/hsm_secret.c +++ b/common/hsm_secret.c @@ -1,11 +1,13 @@ #include "config.h" #include +#include #include #include #include #include #include #include +#include #include #include #include @@ -28,6 +30,26 @@ #define PASSPHRASE_HASH_LEN 32 #define HSM_SECRET_PLAIN_SIZE 32 +void derive_channel_seed(struct secret *channel_seed, + const u8 hsm_secret[32], + const struct node_id *peer_id, + u64 dbid) +{ + struct secret channel_base; + u8 input[sizeof(peer_id->k) + sizeof(dbid)]; + + /* These labels and the native-endian dbid are historical and must + * never change: existing channel keys depend on them. */ + hkdf_sha256(&channel_base, sizeof(channel_base), NULL, 0, + hsm_secret, 32, "peer seed", strlen("peer seed")); + memcpy(input, peer_id->k, sizeof(peer_id->k)); + BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); + memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); + hkdf_sha256(channel_seed, sizeof(*channel_seed), + input, sizeof(input), &channel_base, sizeof(channel_base), + "per-peer seed", strlen("per-peer seed")); +} + /* Helper function to validate a mnemonic string */ enum hsm_secret_error validate_mnemonic(const char *mnemonic) { diff --git a/common/hsm_secret.h b/common/hsm_secret.h index 1f6314e63248..427cf24014ac 100644 --- a/common/hsm_secret.h +++ b/common/hsm_secret.h @@ -8,6 +8,14 @@ #include #include +struct node_id; + +/* Derive the seed for one channel from the first 32 bytes of hsm_secret. */ +void derive_channel_seed(struct secret *channel_seed, + const u8 hsm_secret[32], + const struct node_id *peer_id, + u64 dbid); + /* Length constants for encrypted HSM secret files */ #define HS_HEADER_LEN crypto_secretstream_xchacha20poly1305_HEADERBYTES #define HS_CIPHERTEXT_LEN \ diff --git a/devtools/checkchannels.c b/devtools/checkchannels.c index 1ca9babcf2c4..af66eeb40714 100644 --- a/devtools/checkchannels.c +++ b/devtools/checkchannels.c @@ -9,51 +9,17 @@ #include #include #include +#include #include #include #include #include -static void hsm_channel_secret_base(struct secret *channel_seed_base, - const struct secret *hsm_secret) -{ - hkdf_sha256(channel_seed_base, sizeof(struct secret), NULL, 0, - hsm_secret, sizeof(*hsm_secret), - /*~ Initially, we didn't support multiple channels per - * peer at all: a channel had to be completely forgotten - * before another could exist. That was slightly relaxed, - * but the phrase "peer seed" is wired into the seed - * generation here, so we need to keep it that way for - * existing clients, rather than using "channel seed". */ - "peer seed", strlen("peer seed")); -} - static void get_channel_seed(const struct secret *hsm_secret, const struct node_id *peer_id, u64 dbid, struct secret *channel_seed) { - struct secret channel_base; - u8 input[sizeof(peer_id->k) + sizeof(dbid)]; - /*~ Again, "per-peer" should be "per-channel", but Hysterical Raisins */ - const char *info = "per-peer seed"; - - /*~ We use the DER encoding of the pubkey, because it's platform - * independent. Since the dbid is unique, however, it's completely - * unnecessary, but again, existing users can't be broken. */ - /* FIXME: lnd has a nicer BIP32 method for deriving secrets which we - * should migrate to. */ - hsm_channel_secret_base(&channel_base, hsm_secret); - memcpy(input, peer_id->k, sizeof(peer_id->k)); - BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); - /*~ For all that talk about platform-independence, note that this - * field is endian-dependent! But let's face it, little-endian won. - * In related news, we don't support EBCDIC or middle-endian. */ - memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); - - hkdf_sha256(channel_seed, sizeof(*channel_seed), - input, sizeof(input), - &channel_base, sizeof(channel_base), - info, strlen(info)); + derive_channel_seed(channel_seed, hsm_secret->data, peer_id, dbid); } struct keys { diff --git a/doc/lightning-hsmtool.8.md b/doc/lightning-hsmtool.8.md index 6f128f1f2a35..f845f730ba1a 100644 --- a/doc/lightning-hsmtool.8.md +++ b/doc/lightning-hsmtool.8.md @@ -40,6 +40,13 @@ of the specified channel with the specified peer, identified by the channel database index. Specify *password* if the `hsm_secret_path` is encrypted. +**dumpcommitments** **--point-only** *node\_id* *channel\_dbid* *commitment\_number* *hsm\_secret\_path* + + Show only the per-commitment point of commitment *commitment\_number* +of the specified channel. +Unlike the form above, this prints no secrets, so the result is safe to +give to the peer of that channel. + **guesstoremote** *p2wpkh* *node\_id* *max\_channel\_dbid* *hsm\_secret\_path* \[*password*\] Brute-force the private key to our funds from a remote unilateral close @@ -62,6 +69,82 @@ If *commitment\_point* is omitted, then the channel is assumed to have been negotiated with `option_static_remotekey`; otherwise, *commitment\_point* is the remote per-commitment point. +**recoverhtlc** *bundle\_path* *destination* *hsm\_secret\_path* + + Sign transactions which spend our HTLC outputs of a commitment +transaction which the peer broadcast after we lost channel state. +In that case **lightningd** logs `Unknown commitment #N, recovering our +funds!`: it recovers our main output, but it cannot find the HTLC outputs +without the peer's per-commitment point. + +Nothing is broadcast, and no private key is printed. +Each signed transaction pays the address *destination*, and is printed +with a description of what it spends, so you can check it and then +broadcast it yourself. + +*bundle\_path* is a JSON file describing the commitment transaction and +the HTLCs to recover from it: + +```json +{ + "version": 1, + "network": "bitcoin", + "peer_id": "02...", + "channel_dbid": 123, + "remote_htlc_basepoint": "03...", + "commitment_point": "03...", + "commitment_tx": "0200...", + "feerate_perkw": 2500, + "htlcs": [ + {"direction": "out", "payment_hash": "...", "cltv_expiry": 952295}, + {"direction": "in", "preimage": "...", "cltv_expiry": 952300} + ] +} +``` + + * *version* is always 1. + * *network* is the network name, such as `bitcoin`, `testnet4`, `signet` + or `regtest`. + * *peer\_id* and *channel\_dbid* are the node id of the peer and the `id` + of the channel in the `channels` table of our database. + * *remote\_htlc\_basepoint* is `htlc_basepoint_remote` in the same table. + The peer knows it too: it is their own HTLC basepoint. + * *commitment\_point* is the peer's per-commitment point for the + commitment they broadcast, and only the peer has it. + A Core Lightning peer can print it, and nothing secret, with + `lightning-hsmtool dumpcommitments --point-only N `, where *N* is the number in our + `Unknown commitment #N` log message. + * *commitment\_tx* is the hex of the commitment transaction, as printed by + `bitcoin-cli getrawtransaction`. + * *feerate\_perkw* is optional, in satoshi per 1000 weight: 250 is + 1 sat/vB. The default is 2500. + * Each of *htlcs* with *direction* `out` is an HTLC which we offered: it + needs the *payment\_hash* and *cltv\_expiry*, and we take it back once + it has expired. + Each with *direction* `in` is an HTLC which the peer offered: it needs + the *preimage*, and must confirm before the peer takes it back at + *cltv\_expiry*. Its *cltv\_expiry* and *payment\_hash* are optional. + +The values from our database can be read from a copy of it, for example: + +``` +sqlite3 lightningd.sqlite3 "SELECT c.id, hex(p.node_id), \ + hex(c.htlc_basepoint_remote) FROM channels c \ + JOIN peers p ON p.id = c.peer_id" +``` + +A transaction is only signed for an HTLC whose script, rebuilt from these +values, is paid by an output of *commitment\_tx*: a wrong value cannot +produce a wrong spend. +An HTLC without such an output is skipped with a message, and the others +are still recovered. An HTLC too small to have an output in the +commitment transaction cannot be recovered. +Parts of one payment which have identical scripts are spent by a single +transaction. +Both anchor and non-anchor channels are supported; Elements networks are +not. + **generatehsm** *hsm\_secret\_path* \[*lang* *seed\_phrase* \[*passphrase*\]\] Generates a new hsm\_secret using BIP39. If lang, seed\_phrase and optional passphrase are not provided they will be prompted for. *lang* can be "en" (English), "es" (Spanish), "fr" (French), "it" ("Italian"), "jp" (Japanese), "zhs" (Chinese Simplified) or "zht" ("Chinese Traditional"). Note that the seed phrase consists of multiple words, so should be surrounded by quotes. diff --git a/hsmd/libhsmd.c b/hsmd/libhsmd.c index 9e48a362a4db..a5cfa620ab96 100644 --- a/hsmd/libhsmd.c +++ b/hsmd/libhsmd.c @@ -293,21 +293,6 @@ static void node_schnorrkey(secp256k1_keypair *node_keypair) "Failed to derive keypair"); } -/*~ This secret is the basis for all per-channel secrets: the per-channel seeds - * will be generated by mixing in the dbid and the peer node_id. */ -static void hsm_channel_secret_base(struct secret *channel_seed_base) -{ - hkdf_sha256(channel_seed_base, sizeof(struct secret), NULL, 0, - secretstuff.bip32_seed, 32, /* Use first 32 bytes */ - /*~ Initially, we didn't support multiple channels per - * peer at all: a channel had to be completely forgotten - * before another could exist. That was slightly relaxed, - * but the phrase "peer seed" is wired into the seed - * generation here, so we need to keep it that way for - * existing clients, rather than using "channel seed". */ - "peer seed", strlen("peer seed")); -} - /* This will derive pseudorandom secret Key from a derived key */ static u8 *handle_derive_secret(struct hsmd_client *c, const u8 *msg_in) { @@ -328,28 +313,7 @@ static u8 *handle_derive_secret(struct hsmd_client *c, const u8 *msg_in) static void get_channel_seed(const struct node_id *peer_id, u64 dbid, struct secret *channel_seed) { - struct secret channel_base; - u8 input[sizeof(peer_id->k) + sizeof(dbid)]; - /*~ Again, "per-peer" should be "per-channel", but Hysterical Raisins */ - const char *info = "per-peer seed"; - - /*~ We use the DER encoding of the pubkey, because it's platform - * independent. Since the dbid is unique, however, it's completely - * unnecessary, but again, existing users can't be broken. */ - /* FIXME: lnd has a nicer BIP32 method for deriving secrets which we - * should migrate to. */ - hsm_channel_secret_base(&channel_base); - memcpy(input, peer_id->k, sizeof(peer_id->k)); - BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); - /*~ For all that talk about platform-independence, note that this - * field is endian-dependent! But let's face it, little-endian won. - * In related news, we don't support EBCDIC or middle-endian. */ - memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); - - hkdf_sha256(channel_seed, sizeof(*channel_seed), - input, sizeof(input), - &channel_base, sizeof(channel_base), - info, strlen(info)); + derive_channel_seed(channel_seed, secretstuff.bip32_seed, peer_id, dbid); } /* ~This stub implementation is overriden by fully validating signers diff --git a/tests/test_connection.py b/tests/test_connection.py index c1dd24f779c0..f558557d6051 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -1,5 +1,6 @@ from fixtures import * # noqa: F401,F403 from fixtures import TEST_NETWORK +from bitcoin.rpc import JSONRPCError from decimal import Decimal from pathlib import Path from pyln.client import RpcError, Millisatoshi @@ -20,12 +21,14 @@ import coincurve import hashlib import hmac +import json import os import pytest import random import re import statistics import struct +import subprocess import time import unittest import websocket @@ -3249,6 +3252,213 @@ def test_dataloss_protection(node_factory, bitcoind): assert (closetxid, "confirmed") in set([(o['txid'], o['status']) for o in l2.rpc.listfunds()['outputs']]) +@unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "sqlite3-specific DB rollback") +@pytest.mark.parametrize("anchors", [False, True]) +def test_dataloss_protection_hsmtool_recoverhtlc(node_factory, bitcoind, tmp_path, anchors): + """l2 loses state with HTLCs pending in both directions, and l1 closes + with a commitment l2 never saw. onchaind recovers to_remote, but it + cannot find the HTLC outputs without l1's per-commitment point (#9200). + + l1 gives l2 that point (`hsmtool dumpcommitments --point-only`), and + `hsmtool recoverhtlc` must sign transactions we can really broadcast: + one timeout spend for both MPP parts l2 offered, and a preimage spend + for the HTLC l1 offered, which has the same payment hash.""" + plugin = os.path.join(os.getcwd(), 'tests/plugins/hold_htlcs.py') + # Both nodes hold incoming HTLCs, so an HTLC in each direction stays + # unresolved across the state l2 is about to lose. + opts = {'plugin': plugin, + 'hold-time': 10000, + 'hold-result': 'continue'} + if not anchors: + opts['dev-force-features'] = '-23' + l1 = node_factory.get_node(may_reconnect=True, + allow_warning=True, + options=opts, + feerates=(7500, 7500, 7500, 7500)) + l2 = node_factory.get_node(may_reconnect=True, + options=opts, + broken_log='Cannot broadcast our commitment tx: they have a future one|Unknown commitment .*, recovering our funds', + feerates=(7500, 7500, 7500, 7500)) + + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + # l2 holds every payment, so push it funds to offer HTLCs with. + l1.fundchannel(l2, 10**6, push_msat=500000000) + scid = first_scid(l1, l2) + + def send_held(src, dst, amt, inv, **kwargs): + route = [{'amount_msat': amt, + 'id': dst.info['id'], + 'delay': 10, + 'channel': scid}] + src.rpc.sendpay(route, inv['payment_hash'], + payment_secret=inv['payment_secret'], **kwargs) + # The plugin sleeps in its first hook call, so it only logs once: + # lightningd logs every HTLC it hands over. + dst.daemon.wait_for_log('Calling htlc_accepted hook of plugin hold_htlcs.py') + + def l2_htlcs(state): + return [h for h in l2.rpc.listhtlcs()['htlcs'] if h['state'] == state] + + # The same hash and expiry in both directions. The two MPP parts l2 + # offers have identical scripts, so one transaction must sweep both + # without confusing them with l1's HTLC. + preimage = '55' * 32 + amt = 200000000 + inv_out = l1.rpc.invoice(2 * amt, 'held-out', 'desc', preimage=preimage) + for partid in (1, 2): + send_held(l2, l1, amt, inv_out, + amount_msat=2 * amt, partid=partid, groupid=1) + inv_in = l2.rpc.invoice(amt, 'held-in', 'desc', preimage=preimage) + send_held(l1, l2, amt, inv_in) + payment_hash = inv_out['payment_hash'] + assert inv_in['payment_hash'] == payment_hash + + wait_for(lambda: len(l2_htlcs('SENT_ADD_ACK_REVOCATION')) == 2) + wait_for(lambda: len(l2_htlcs('RCVD_ADD_ACK_REVOCATION')) == 1) + cltv = only_one(list(set(h['expiry'] for h in l2.rpc.listhtlcs()['htlcs']))) + + # Snapshot l2's db with the HTLCs pending. + l2.stop() + dbpath = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "lightningd.sqlite3") + orig_db = Path(dbpath).read_bytes() + l2.start() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + # Any payment would be held too: fee updates make new commitments, + # so l1 ends up with one that l2's old db has never seen. + def l1_commit_num(): + return only_one(l1.db_query("SELECT next_index_local FROM channels"))['next_index_local'] + + for rate in (8000, 8500): + before = l1_commit_num() + l1.rpc.call('dev-feerate', [l2.info['id'], rate]) + wait_for(lambda: l1_commit_num() > before) + wait_for(lambda: len(only_one(l2.rpc.listpeerchannels()['channels'])['htlcs']) == 3) + + # Now move l2 back in time. + l2.stop() + Path(dbpath).write_bytes(orig_db) + l2.start() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l2.daemon.wait_for_log("Cannot broadcast our commitment tx: they have a future one") + + # l1 drops to chain (see test_dataloss_protection for the reconnect). + try: + l1.daemon.wait_for_log("They sent ERROR.*Awaiting unilateral close", timeout=10) + except TimeoutError: + try: + l1.rpc.disconnect(l2.info['id'], force=True) + except RpcError as err: + assert "Peer not connected" in err.error['message'] + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.wait_for_channel_onchain(l2.info['id']) + closetxid = only_one(bitcoind.rpc.getrawmempool(False)) + bitcoind.generate_block(1) + + # onchaind only finds to_remote: the HTLC outputs are stranded. + line = l2.daemon.wait_for_log("ERROR: Unknown commitment #[0-9]+, recovering our funds!") + commit_num = int(re.search(r'Unknown commitment #([0-9]+)', line).group(1)) + + # l1 can give out exactly this point, and none of its secrets. + l1_hsm = os.path.join(l1.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + l1_dbid = only_one(l1.db_query("SELECT id FROM channels"))['id'] + point = subprocess.check_output( + ["tools/lightning-hsmtool", "dumpcommitments", "--point-only", + l2.info['id'], str(l1_dbid), str(commit_num), l1_hsm], + text=True).strip() + assert re.fullmatch(r'0[23][0-9a-f]{64}', point) + + # Everything else is in l2's stale db, or on the blockchain. + chan = only_one(l2.db_query("SELECT id, hex(htlc_basepoint_remote) AS hb FROM channels")) + l2_hsm = os.path.join(l2.daemon.lightning_dir, TEST_NETWORK, "hsm_secret") + dest = l2.rpc.newaddr()['p2tr'] + htlc_out = {'direction': 'out', 'payment_hash': payment_hash, 'cltv_expiry': cltv} + htlc_in = {'direction': 'in', 'preimage': preimage, 'cltv_expiry': cltv} + bundle = {'version': 1, + 'network': TEST_NETWORK, + 'peer_id': l1.info['id'], + 'channel_dbid': chan['id'], + 'remote_htlc_basepoint': chan['hb'].lower(), + 'commitment_point': point, + 'commitment_tx': bitcoind.rpc.getrawtransaction(closetxid), + 'feerate_perkw': 7500, + # Listing both MPP parts must not sweep them twice. + 'htlcs': [htlc_out, htlc_out, htlc_in]} + + def recoverhtlc(**changes): + path = tmp_path / 'bundle.json' + path.write_text(json.dumps({**bundle, **changes})) + return subprocess.run(["tools/lightning-hsmtool", "recoverhtlc", + str(path), dest, l2_hsm], + capture_output=True, text=True) + + def rawtxs(stdout): + """Each signed tx is printed on a line of its own, as bare hex.""" + return re.findall(r'^[0-9a-f]+$', stdout, re.MULTILINE) + + # A point which is not the one for this commitment matches nothing. + bad = recoverhtlc(commitment_point=l1.info['id']) + assert bad.returncode != 0 + assert "no commitment output matches it" in bad.stderr + assert "No HTLC could be recovered" in bad.stderr + assert rawtxs(bad.stdout) == [] + + # A misspelt key is refused, rather than quietly ignored. + bad = recoverhtlc(feerate_per_kw=253) + assert bad.returncode != 0 + assert "unknown key 'feerate_per_kw'" in bad.stderr + + # An HTLC which is not there (wrong expiry) is skipped, with a reason: + # the others are still recovered. + missing = {**htlc_out, 'cltv_expiry': cltv + 1} + good = recoverhtlc(htlcs=[missing, htlc_out, htlc_out, htlc_in]) + assert good.returncode == 0, good.stderr + assert "Skipping HTLC 0 (payment_hash {}): no commitment output matches it".format(payment_hash) in good.stderr + assert "privkey" not in good.stdout + assert good.stdout.count("channel type : {}".format("anchors" if anchors else "no anchors")) == 2 + rawtx_out, rawtx_in = rawtxs(good.stdout) + + # The same, without the missing one. + good = recoverhtlc() + assert good.returncode == 0, good.stderr + assert good.stderr == '' + assert rawtxs(good.stdout) == [rawtx_out, rawtx_in] + + # Both MPP parts are swept by a single timeout transaction. + decoded = bitcoind.rpc.decoderawtransaction(rawtx_out) + assert [i['txid'] for i in decoded['vin']] == [closetxid] * 2 + assert len(set(i['vout'] for i in decoded['vin'])) == 2 + assert decoded['locktime'] == cltv + assert "valid : once block {} is mined".format(cltv) in good.stdout + + # The preimage claim has no locktime. + decoded = bitcoind.rpc.decoderawtransaction(rawtx_in) + assert only_one(decoded['vin'])['txid'] == closetxid + assert decoded['locktime'] == 0 + assert "deadline : the peer can take this back once block {} is mined".format(cltv) in good.stdout + + def wallet_output(txid): + bitcoind.generate_block(1) + sync_blockheight(bitcoind, [l2]) + out = only_one([o for o in l2.rpc.listfunds()['outputs'] if o['txid'] == txid]) + assert out['status'] == 'confirmed' + assert out['address'] == dest + return out + + # The timeout cannot be broadcast before the HTLC expires, but the + # preimage claim can. + assert bitcoind.rpc.getblockcount() < cltv + with pytest.raises(JSONRPCError, match='non-final'): + bitcoind.rpc.sendrawtransaction(rawtx_out) + txid_in = bitcoind.rpc.sendrawtransaction(rawtx_in) + assert wallet_output(txid_in)['amount_msat'] > amt - 50000000 + + # Locktime is checked against the height of the *next* block. + bitcoind.generate_block(cltv - bitcoind.rpc.getblockcount()) + txid_out = bitcoind.rpc.sendrawtransaction(rawtx_out) + assert wallet_output(txid_out)['amount_msat'] > 2 * amt - 50000000 + + @unittest.skipIf(os.getenv('TEST_DB_PROVIDER', 'sqlite3') != 'sqlite3', "sqlite3-specific DB rollback") def test_dataloss_protection_reconnect(node_factory, bitcoind): """l2's first ERROR never hits the wire: reconnect must still work.""" diff --git a/tools/Makefile b/tools/Makefile index 2d86221df962..daff4fce3890 100644 --- a/tools/Makefile +++ b/tools/Makefile @@ -1,11 +1,11 @@ #! /usr/bin/make TOOLS := tools/lightning-hsmtool tools/lightning-downgrade -TOOLS_SRC := $(TOOLS:=.c) +TOOLS_SRC := $(TOOLS:=.c) tools/hsmtool-recoverhtlc.c TOOLS_OBJ := $(TOOLS_SRC:.c=.o) # Make sure these depend on everything. ALL_C_SOURCES += $(TOOLS_SRC) -ALL_C_HEADERS += +ALL_C_HEADERS += tools/hsmtool-recoverhtlc.h tools/lightning-hsmtool.h ALL_PROGRAMS += $(TOOLS) # We force make to relink this every time, to detect version changes. @@ -16,7 +16,7 @@ tools/headerversions: $(FORCE) tools/headerversions.o libccan.a $(TOOLS): libcommon.a tools/headerversions.o: ccan/config.h -tools/lightning-hsmtool: tools/lightning-hsmtool.o +tools/lightning-hsmtool: tools/lightning-hsmtool.o tools/hsmtool-recoverhtlc.o tools/lightning-downgrade.o: CFLAGS:=$(CFLAGS) -DCLN_PREV_VERSION=$(CLN_PREV_VERSION) tools/lightning-downgrade: \ diff --git a/tools/hsmtool-recoverhtlc.c b/tools/hsmtool-recoverhtlc.c new file mode 100644 index 000000000000..5f59487a55eb --- /dev/null +++ b/tools/hsmtool-recoverhtlc.c @@ -0,0 +1,693 @@ +#include "config.h" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#define ERROR_USAGE 2 + +static bool derive_channel_basepoints( + const struct node_id *peer_id, + u64 channel_dbid, + const char *hsm_secret_path, + struct recoverhtlc_keys *keys, + struct secret *htlc_basepoint_secret) +{ + struct secret hsm_secret, channel_seed; + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); + + /* Channel derivation always uses the first 32 bytes. */ + memcpy(hsm_secret.data, hsms->secret_data, sizeof(hsm_secret.data)); + derive_channel_seed(&channel_seed, hsm_secret.data, + peer_id, channel_dbid); + + if (!derive_htlc_basepoint(&channel_seed, &keys->our_htlc_basepoint, + htlc_basepoint_secret)) + return false; + return derive_revocation_basepoint(&channel_seed, + &keys->our_revocation_basepoint, + NULL); +} + +/* Derive keys to spend our HTLC outputs on the peer's commitment. */ +bool hsmtool_recoverhtlc_derive_keys( + const struct node_id *peer_id, + u64 channel_dbid, + const struct pubkey *commitment_point, + const struct pubkey *remote_htlc_basepoint, + const char *hsm_secret_path, + struct recoverhtlc_keys *keys) +{ + struct secret htlc_basepoint_secret; + + if (!derive_channel_basepoints(peer_id, channel_dbid, hsm_secret_path, + keys, &htlc_basepoint_secret)) + return false; + + /* BOLT #3: + * + * pubkey = basepoint + * + SHA256(per_commitment_point || basepoint) * G + */ + if (!derive_simple_privkey(&htlc_basepoint_secret, + &keys->our_htlc_basepoint, + commitment_point, + &keys->our_htlc_privkey) + || !derive_simple_key(&keys->our_htlc_basepoint, + commitment_point, + &keys->our_htlc_key) + || !derive_simple_key(remote_htlc_basepoint, + commitment_point, + &keys->their_htlc_key)) + return false; + + /* Peer commitment: our basepoint, their commitment point. */ + return derive_revocation_key(&keys->our_revocation_basepoint, + commitment_point, + &keys->revocation_key); +} + +/* Rebuild an HTLC witness script from the peer's commitment. */ +static u8 *recovery_wscript( + const tal_t *ctx, + enum recoverhtlc_kind kind, + bool anchors, + const struct recoverhtlc_keys *keys, + const struct ripemd160 *payment_ripemd, + u32 cltv_expiry) +{ + struct abs_locktime expiry; + struct keyset keyset = { + .self_revocation_key = keys->revocation_key, + .self_htlc_key = keys->their_htlc_key, + .other_htlc_key = keys->our_htlc_key, + }; + + switch (kind) { + /* The peer offered it, so this is their offered HTLC script. */ + case HTLC_THEY_OFFERED: + return htlc_offered_wscript(ctx, payment_ripemd, &keyset, + anchors, false); + + /* We offered it, so this is the peer's received HTLC script. */ + case HTLC_WE_OFFERED: + if (!blocks_to_abs_locktime(cltv_expiry, &expiry)) + return NULL; + return htlc_received_wscript(ctx, payment_ripemd, &expiry, + &keyset, anchors, false); + } + abort(); +} + +/* Collect every output with this witness script, including identical MPPs. */ +struct recoverhtlc_match *hsmtool_recoverhtlc_match_script( + const tal_t *ctx, + const struct bitcoin_tx *tx, + enum recoverhtlc_kind kind, + bool anchors, + const u8 *wscript) +{ + struct recoverhtlc_match *match = tal(ctx, struct recoverhtlc_match); + /* Commitment outputs contain the P2WSH hash, not the witness script. */ + const u8 *scriptpubkey = scriptpubkey_p2wsh(tmpctx, wscript); + + match->kind = kind; + match->anchors = anchors; + match->wscript = tal_dup_arr(match, u8, wscript, tal_bytelen(wscript), 0); + match->outnums = tal_arr(match, size_t, 0); + + for (size_t i = 0; i < tx->wtx->num_outputs; i++) { + const struct wally_tx_output *output = &tx->wtx->outputs[i]; + + if (!memeq(output->script, output->script_len, + scriptpubkey, tal_bytelen(scriptpubkey))) + continue; + tal_arr_expand(&match->outnums, i); + } + + if (tal_count(match->outnums) == 0) + return tal_free(match); + return match; +} + +/* Find the requested HTLC without requiring its channel type. */ +struct recoverhtlc_match *hsmtool_recoverhtlc_find_outputs( + const tal_t *ctx, + const struct bitcoin_tx *tx, + enum recoverhtlc_kind kind, + const struct recoverhtlc_keys *keys, + const struct sha256 *payment_hash, + u32 cltv_expiry) +{ + struct ripemd160 payment_ripemd; + + ripemd160(&payment_ripemd, + payment_hash->u.u8, sizeof(payment_hash->u.u8)); + + /* Anchors append `1 CSV DROP`, changing the P2WSH hash. Try both + * forms; only the negotiated form can match a commitment output. */ + for (int anchors = 0; anchors <= 1; anchors++) { + struct recoverhtlc_match *match; + const u8 *wscript = recovery_wscript( + tmpctx, kind, anchors, keys, + &payment_ripemd, cltv_expiry); + + if (!wscript) + return NULL; + match = hsmtool_recoverhtlc_match_script( + ctx, tx, kind, anchors, wscript); + /* A match also records which anchor form was used. */ + if (match) + return match; + } + return NULL; +} + +static void validate_recovery_inputs( + const struct recoverhtlc_bundle *bundle, + const char *destination, + const char *hsm_secret_path, + struct bitcoin_tx **commitment_tx, + u8 **destination_script, + struct recoverhtlc_keys *keys) +{ + chainparams = chainparams_for_network(bundle->network); + if (!chainparams || chainparams->is_elements) + errx(ERROR_USAGE, "Unsupported recovery network '%s'", + bundle->network); + if (!decode_scriptpubkey_from_addr(tmpctx, chainparams, destination, + destination_script)) + errx(ERROR_USAGE, "Invalid destination for network '%s'", + bundle->network); + *commitment_tx = bitcoin_tx_from_hex( + tmpctx, bundle->commitment_tx_hex, + strlen(bundle->commitment_tx_hex)); + if (!*commitment_tx) + errx(ERROR_USAGE, "Recovery bundle has an invalid commitment_tx"); + if (!hsmtool_recoverhtlc_derive_keys( + bundle->peer_id, bundle->channel_dbid, + &bundle->commitment_point, &bundle->remote_htlc_basepoint, + hsm_secret_path, keys)) + errx(ERROR_USAGE, "Could not derive HTLC recovery keys"); +} + +/* Say when the transaction becomes valid, and when that is too late. */ +static void print_recovery_timing(const struct recoverhtlc_match *match, + u32 cltv_expiry) +{ + /* Anchor outputs carry a one-block relative delay. */ + const char *confirmed = match->anchors + ? "the commitment transaction has one confirmation" : NULL; + + /* nLockTime keeps our timeout out until the HTLC has expired. */ + if (match->kind == HTLC_WE_OFFERED) { + printf("valid : once block %u is mined%s%s\n", + cltv_expiry, + confirmed ? " and " : "", confirmed ? confirmed : ""); + return; + } + + printf("valid : %s%s\n", + confirmed ? "once " : "now", confirmed ? confirmed : ""); + /* After expiry the peer's own timeout races our preimage claim. */ + if (cltv_expiry) + printf("deadline : the peer can take this back once" + " block %u is mined\n", cltv_expiry); + else + printf("deadline : the peer can take this back once" + " the HTLC expires\n"); +} + +static void print_recovery_tx( + const char *destination, + const struct bitcoin_tx *commitment_tx, + const struct recoverhtlc_match *match, + u32 cltv_expiry, + const struct bitcoin_tx *tx, + const struct recoverhtlc_amounts *amounts) +{ + struct bitcoin_txid commitment_txid, txid; + size_t weight = bitcoin_tx_weight(tx); + u32 feerate; + + bitcoin_txid(commitment_tx, &commitment_txid); + bitcoin_txid(tx, &txid); + if (!amount_feerate(&feerate, amounts->fee, weight)) + feerate = 0; + + printf("kind : %s\n", + match->kind == HTLC_THEY_OFFERED + ? "claim with preimage" : "reclaim after CLTV"); + printf("channel type : %s\n", match->anchors ? "anchors" : "no anchors"); + for (size_t i = 0; i < tal_count(match->outnums); i++) { + struct amount_sat amount = bitcoin_tx_output_get_amount_sat( + commitment_tx, match->outnums[i]); + + printf("input[%zu] : %s:%zu (%s)\n", i, + fmt_bitcoin_txid(tmpctx, &commitment_txid), + match->outnums[i], fmt_amount_sat(tmpctx, amount)); + } + printf("output : %s (%s)\n", destination, + fmt_amount_sat(tmpctx, amounts->output)); + printf("fee : %s (%zu weight, %u sat/kw)\n", + fmt_amount_sat(tmpctx, amounts->fee), weight, feerate); + printf("txid : %s\n", fmt_bitcoin_txid(tmpctx, &txid)); + print_recovery_timing(match, cltv_expiry); + printf("\n" + "Verify with `bitcoin-cli decoderawtransaction` and" + " `bitcoin-cli testmempoolaccept '[\"\"]'`.\n" + "Broadcast with `bitcoin-cli sendrawtransaction `.\n\n"); + printf("%s\n\n", fmt_bitcoin_tx(tmpctx, tx)); +} + +/* Parts of one payment share a script: the first claim sweeps them all. */ +static bool claim_already_swept(const struct recoverhtlc_claim *claims, + size_t n) +{ + for (size_t i = 0; i < n; i++) { + if (claims[i].kind != claims[n].kind + || !sha256_eq(&claims[i].payment_hash, + &claims[n].payment_hash)) + continue; + /* Only the script of an HTLC we offered has its expiry. */ + if (claims[n].kind == HTLC_THEY_OFFERED + || claims[i].cltv_expiry == claims[n].cltv_expiry) + return true; + } + return false; +} + +/* Sign and print one claim, or return why it has to be left out. */ +static const char *recover_claim(const struct recoverhtlc_bundle *bundle, + const struct recoverhtlc_claim *claim, + const struct bitcoin_tx *commitment_tx, + const struct recoverhtlc_keys *keys, + const char *destination, + const u8 *destination_script) +{ + const struct preimage *preimage + = claim->kind == HTLC_THEY_OFFERED ? &claim->preimage : NULL; + struct recoverhtlc_match *match; + struct recoverhtlc_amounts amounts; + struct bitcoin_tx *tx; + + match = hsmtool_recoverhtlc_find_outputs( + tmpctx, commitment_tx, claim->kind, keys, + &claim->payment_hash, claim->cltv_expiry); + /* An HTLC below the peer's dust limit never had an output. */ + if (!match) + return "no commitment output matches it"; + tx = hsmtool_recoverhtlc_sign( + tmpctx, commitment_tx, match, keys, destination_script, + bundle->feerate_perkw, claim->cltv_expiry, preimage, &amounts); + if (!tx) + return "its value does not cover the fee"; + if (amount_sat_less(amounts.output, chainparams->dust_limit)) + return "its value after the fee is below the dust limit"; + if (amount_sat_greater(amounts.fee, amount_sat_div(amounts.input, 4))) + fprintf(stderr, + "Warning: fee %s exceeds a quarter of the" + " recovered amount\n", + fmt_amount_sat(tmpctx, amounts.fee)); + print_recovery_tx(destination, commitment_tx, match, + claim->cltv_expiry, tx, &amounts); + return NULL; +} + +void hsmtool_recoverhtlc(const char *argv0, + const char *bundle_path, + const char *destination, + const char *hsm_secret_path) +{ + struct recoverhtlc_bundle bundle; + struct recoverhtlc_keys keys; + struct bitcoin_tx *commitment_tx; + const char *err; + u8 *destination_script; + size_t num_recovered = 0; + + /* Transaction and signature helpers require libwally's tal allocator. */ + common_setup(argv0); + err = hsmtool_recoverhtlc_parse_bundle(tmpctx, bundle_path, &bundle); + if (err) + errx(ERROR_USAGE, "%s", err); + + validate_recovery_inputs(&bundle, destination, hsm_secret_path, + &commitment_tx, &destination_script, &keys); + if (bundle.feerate_perkw < FEERATE_FLOOR) + fprintf(stderr, + "Warning: feerate %u sat/kw is below the usual" + " relay minimum of %u sat/kw\n", + bundle.feerate_perkw, FEERATE_FLOOR); + + /* One bad or trimmed HTLC must not block recovery of the others. */ + for (size_t i = 0; i < tal_count(bundle.claims); i++) { + const struct recoverhtlc_claim *claim = &bundle.claims[i]; + + if (claim_already_swept(bundle.claims, i)) + continue; + err = recover_claim(&bundle, claim, commitment_tx, &keys, + destination, destination_script); + if (!err) { + num_recovered++; + continue; + } + fprintf(stderr, "Skipping HTLC %zu (payment_hash %s): %s\n", i, + tal_hexstr(tmpctx, &claim->payment_hash, + sizeof(claim->payment_hash)), + err); + } + if (num_recovered == 0) + errx(ERROR_USAGE, + "No HTLC could be recovered. If no output matched," + " check the bundle's commitment_point," + " remote_htlc_basepoint, peer_id and channel_dbid"); +} + +/* Witness stack count, signature, selector, and witness script. */ +static size_t recovery_witness_weight(const struct recoverhtlc_match *match) +{ + size_t wscript_len = tal_bytelen(match->wscript); + + return 1 + bitcoin_tx_input_sig_weight() + + (match->kind == HTLC_THEY_OFFERED ? 1 + 32 : 1) + + varint_size(wscript_len) + wscript_len; +} + +/* Deduct the signed transaction's fee from its single output. */ +static bool set_recovery_output_amount( + struct bitcoin_tx *tx, + const struct recoverhtlc_match *match, + u32 feerate_perkw, + struct recoverhtlc_amounts *amounts) +{ + /* The transaction is unsigned, so add each input's future witness. */ + size_t weight = bitcoin_tx_weight(tx) + + tal_count(match->outnums) * recovery_witness_weight(match); + + amounts->fee = amount_tx_fee(feerate_perkw, weight); + /* Refuse a recovery whose fee consumes all of the HTLC value. */ + if (!amount_sat_sub(&amounts->output, amounts->input, amounts->fee)) + return false; + bitcoin_tx_output_set_amount(tx, 0, amounts->output); + return true; +} + +static struct bitcoin_tx *new_recovery_tx( + const tal_t *ctx, + const struct bitcoin_tx *commitment_tx, + const struct recoverhtlc_match *match, + const u8 *destination_script, + u32 feerate_perkw, + struct recoverhtlc_amounts *amounts, + u32 cltv_expiry) +{ + struct bitcoin_txid commitment_txid; + struct bitcoin_tx *tx; + size_t num_inputs = tal_count(match->outnums); + + /* Our timeout path requires nLockTime; the preimage path does not. */ + tx = bitcoin_tx(ctx, chainparams, num_inputs, 1, + match->kind == HTLC_WE_OFFERED ? cltv_expiry : 0); + bitcoin_txid(commitment_tx, &commitment_txid); + amounts->input = AMOUNT_SAT(0); + + for (size_t i = 0; i < num_inputs; i++) { + struct bitcoin_outpoint outpoint = { + .txid = commitment_txid, + .n = match->outnums[i], + }; + struct amount_sat amount = bitcoin_tx_output_get_amount_sat( + commitment_tx, match->outnums[i]); + struct amount_sat total; + + if (!amount_sat_add(&total, amounts->input, amount)) + return tal_free(tx); + amounts->input = total; + + /* Segwit signatures commit to each input's amount and script. */ + /* Anchor HTLC spends must wait one confirmation. */ + bitcoin_tx_add_input(tx, &outpoint, match->anchors ? 1 : 0, + NULL, amount, NULL, match->wscript); + } + + /* The complete transaction shape is needed to calculate its fee. */ + bitcoin_tx_add_output(tx, destination_script, NULL, amounts->input); + if (!set_recovery_output_amount(tx, match, feerate_perkw, amounts)) + return tal_free(tx); + return tx; +} + +static bool sign_recovery_tx(struct bitcoin_tx *tx, + const struct recoverhtlc_match *match, + const struct recoverhtlc_keys *keys, + const struct preimage *preimage) +{ + /* SIGHASH_ALL commits every input to the single destination output. */ + for (size_t i = 0; i < tal_count(match->outnums); i++) { + struct bitcoin_signature signature; + + sign_tx_input(tx, i, NULL, match->wscript, + &keys->our_htlc_privkey, &keys->our_htlc_key, + SIGHASH_ALL, &signature); + if (!check_tx_sig(tx, i, NULL, match->wscript, + &keys->our_htlc_key, &signature)) + return false; + /* Witness: signature, optional preimage, then the script. */ + bitcoin_tx_input_set_witness( + tx, i, + take(bitcoin_witness_sig_and_element( + tx, &signature, + preimage ? preimage->r : NULL, + preimage ? sizeof(preimage->r) : 0, + match->wscript))); + } + return true; +} + +/* Calculate fees, then build and sign a transaction sweeping every match. */ +struct bitcoin_tx *hsmtool_recoverhtlc_sign( + const tal_t *ctx, + const struct bitcoin_tx *commitment_tx, + const struct recoverhtlc_match *match, + const struct recoverhtlc_keys *keys, + const u8 *destination_script, + u32 feerate_perkw, + u32 cltv_expiry, + const struct preimage *preimage, + struct recoverhtlc_amounts *amounts) +{ + struct bitcoin_tx *tx; + + /* Peer-offered HTLCs need a preimage; our HTLCs use timeout. */ + if (match->kind == HTLC_THEY_OFFERED && !preimage) + return NULL; + if (match->kind == HTLC_WE_OFFERED && preimage) + return NULL; + + tx = new_recovery_tx(ctx, commitment_tx, match, destination_script, + feerate_perkw, amounts, cltv_expiry); + if (!tx) + return NULL; + if (!sign_recovery_tx(tx, match, keys, preimage)) + return tal_free(tx); + return tx; +} + +/* A misspelt key would otherwise be silently ignored. */ +static const char *unknown_key(const tal_t *ctx, + const char *buffer, + const jsmntok_t *obj, + const char *const *known, + size_t num_known) +{ + const jsmntok_t *key; + size_t i; + + json_for_each_obj(i, key, obj) { + bool found = false; + + for (size_t j = 0; j < num_known; j++) + found |= json_tok_streq(buffer, key, known[j]); + if (!found) + return tal_fmt(ctx, "unknown key '%.*s'", + key->end - key->start, + buffer + key->start); + } + return NULL; +} + +static const char *parse_bundle_claim(const tal_t *ctx, + const char *buffer, + const jsmntok_t *tok, + struct recoverhtlc_claim *claim) +{ + static const char *const keys[] = { + "direction", "preimage", "payment_hash", "cltv_expiry", + }; + const jsmntok_t *direction, *preimage, *hash, *cltv; + struct abs_locktime expiry; + const char *err; + + if (tok->type != JSMN_OBJECT) + return tal_strdup(ctx, "HTLC is not a JSON object"); + err = unknown_key(ctx, buffer, tok, keys, ARRAY_SIZE(keys)); + if (err) + return err; + + direction = json_get_member(buffer, tok, "direction"); + preimage = json_get_member(buffer, tok, "preimage"); + hash = json_get_member(buffer, tok, "payment_hash"); + cltv = json_get_member(buffer, tok, "cltv_expiry"); + if (!direction) + return tal_strdup(ctx, "HTLC is missing direction"); + + memset(claim, 0, sizeof(*claim)); + if (hash && !json_to_sha256(buffer, hash, &claim->payment_hash)) + return tal_strdup(ctx, "HTLC has an invalid payment_hash"); + if (cltv + && (!json_to_u32(buffer, cltv, &claim->cltv_expiry) + || !blocks_to_abs_locktime(claim->cltv_expiry, &expiry))) + return tal_strdup(ctx, "HTLC has an invalid cltv_expiry"); + + /* The peer offered it: we claim it with the preimage. */ + if (json_tok_streq(buffer, direction, "in")) { + struct sha256 preimage_hash; + + claim->kind = HTLC_THEY_OFFERED; + if (!preimage + || !json_to_preimage(buffer, preimage, &claim->preimage)) + return tal_strdup(ctx, "HTLC has an invalid preimage"); + sha256(&preimage_hash, &claim->preimage, + sizeof(claim->preimage)); + /* The hash and expiry are optional: the script needs neither. */ + if (hash && !sha256_eq(&preimage_hash, &claim->payment_hash)) + return tal_strdup(ctx, "HTLC preimage does not match" + " its payment_hash"); + claim->payment_hash = preimage_hash; + return NULL; + } + + /* We offered it: we take it back once it has timed out. */ + if (json_tok_streq(buffer, direction, "out")) { + claim->kind = HTLC_WE_OFFERED; + if (preimage) + return tal_strdup(ctx, "HTLC we offered is reclaimed" + " by timeout, not preimage"); + if (!hash) + return tal_strdup(ctx, "HTLC is missing payment_hash"); + if (!cltv) + return tal_strdup(ctx, "HTLC is missing cltv_expiry"); + return NULL; + } + + return tal_strdup(ctx, "HTLC direction is not 'in' or 'out'"); +} + +const char *hsmtool_recoverhtlc_parse_bundle( + const tal_t *ctx, const char *path, struct recoverhtlc_bundle *bundle) +{ + static const char *const keys[] = { + "version", "network", "peer_id", "channel_dbid", + "remote_htlc_basepoint", "commitment_point", "commitment_tx", + "feerate_perkw", "htlcs", + }; + const jsmntok_t *toks, *htlcs, *value; + const char *err; + char *buffer; + u32 version; + + memset(bundle, 0, sizeof(*bundle)); + buffer = grab_file_str(ctx, path); + if (!buffer) + return tal_fmt(ctx, "Could not read recovery bundle '%s'", path); + toks = json_parse_simple(ctx, buffer, strlen(buffer)); + if (!toks || toks->type != JSMN_OBJECT) + return tal_strdup(ctx, "Recovery bundle is not a JSON object"); + err = unknown_key(ctx, buffer, toks, keys, ARRAY_SIZE(keys)); + if (err) + return tal_fmt(ctx, "Recovery bundle has an %s", err); + + bundle->peer_id = tal(ctx, struct node_id); + bundle->feerate_perkw = RECOVERHTLC_DEFAULT_FEERATE; + value = json_get_member(buffer, toks, "version"); + if (!value || !json_to_u32(buffer, value, &version)) + return tal_strdup(ctx, "Recovery bundle has an invalid version"); + if (version != 1) + return tal_fmt(ctx, "Unsupported recovery bundle version %u", + version); + + value = json_get_member(buffer, toks, "network"); + bundle->network = value ? json_strdup(ctx, buffer, value) : NULL; + if (!bundle->network) + return tal_strdup(ctx, "Recovery bundle has an invalid network"); + value = json_get_member(buffer, toks, "peer_id"); + if (!value || !json_to_node_id(buffer, value, bundle->peer_id)) + return tal_strdup(ctx, "Recovery bundle has an invalid peer_id"); + value = json_get_member(buffer, toks, "channel_dbid"); + if (!value || !json_to_u64(buffer, value, &bundle->channel_dbid)) + return tal_strdup(ctx, "Recovery bundle has an invalid channel_dbid"); + if (!bundle->channel_dbid) + return tal_strdup(ctx, "Recovery bundle channel_dbid must be >= 1"); + value = json_get_member(buffer, toks, "feerate_perkw"); + if (value + && (!json_to_u32(buffer, value, &bundle->feerate_perkw) + || !bundle->feerate_perkw)) + return tal_strdup(ctx, + "Recovery bundle has an invalid feerate_perkw"); + + value = json_get_member(buffer, toks, "remote_htlc_basepoint"); + if (!value || !json_to_pubkey(buffer, value, + &bundle->remote_htlc_basepoint)) + return tal_strdup(ctx, + "Recovery bundle has an invalid remote_htlc_basepoint"); + value = json_get_member(buffer, toks, "commitment_point"); + if (!value + || !json_to_pubkey(buffer, value, &bundle->commitment_point)) + return tal_strdup(ctx, + "Recovery bundle has an invalid commitment_point"); + value = json_get_member(buffer, toks, "commitment_tx"); + bundle->commitment_tx_hex + = value ? json_strdup(ctx, buffer, value) : NULL; + if (!bundle->commitment_tx_hex) + return tal_strdup(ctx, + "Recovery bundle has an invalid commitment_tx"); + + htlcs = json_get_member(buffer, toks, "htlcs"); + if (!htlcs) + return tal_strdup(ctx, "Recovery bundle is missing HTLCs"); + if (htlcs->type != JSMN_ARRAY || htlcs->size == 0) + return tal_strdup(ctx, "Recovery bundle has no HTLCs"); + + bundle->claims = tal_arr(ctx, struct recoverhtlc_claim, htlcs->size); + for (size_t i = 0; i < htlcs->size; i++) { + err = parse_bundle_claim(ctx, buffer, json_get_arr(htlcs, i), + &bundle->claims[i]); + if (err) + return tal_fmt(ctx, "Invalid HTLC %zu: %s", i, err); + } + return NULL; +} diff --git a/tools/hsmtool-recoverhtlc.h b/tools/hsmtool-recoverhtlc.h new file mode 100644 index 000000000000..b45d0dee9584 --- /dev/null +++ b/tools/hsmtool-recoverhtlc.h @@ -0,0 +1,105 @@ +#ifndef LIGHTNING_TOOLS_HSMTOOL_RECOVERHTLC_H +#define LIGHTNING_TOOLS_HSMTOOL_RECOVERHTLC_H +#include "config.h" +#include +#include +#include +#include +#include +#include +#include + +struct node_id; +struct ripemd160; + +#define RECOVERHTLC_DEFAULT_FEERATE 2500 + +enum recoverhtlc_kind { + HTLC_THEY_OFFERED, + HTLC_WE_OFFERED, +}; + +struct recoverhtlc_match { + enum recoverhtlc_kind kind; + bool anchors; + u8 *wscript; + size_t *outnums; +}; + +struct recoverhtlc_amounts { + struct amount_sat input; + struct amount_sat fee; + struct amount_sat output; +}; + +struct recoverhtlc_claim { + enum recoverhtlc_kind kind; + struct sha256 payment_hash; + struct preimage preimage; + u32 cltv_expiry; +}; + +struct recoverhtlc_bundle { + char *network; + char *commitment_tx_hex; + struct node_id *peer_id; + struct pubkey remote_htlc_basepoint; + struct pubkey commitment_point; + u64 channel_dbid; + u32 feerate_perkw; + struct recoverhtlc_claim *claims; +}; + +/* Key names are from the commitment owner's perspective. */ +struct recoverhtlc_keys { + struct pubkey our_htlc_basepoint; + struct pubkey our_revocation_basepoint; + struct privkey our_htlc_privkey; + struct pubkey our_htlc_key; /* remote_htlcpubkey */ + struct pubkey their_htlc_key; /* local_htlcpubkey */ + struct pubkey revocation_key; +}; + +bool hsmtool_recoverhtlc_derive_keys( + const struct node_id *peer_id, + u64 channel_dbid, + const struct pubkey *commitment_point, + const struct pubkey *remote_htlc_basepoint, + const char *hsm_secret_path, + struct recoverhtlc_keys *keys); + +struct recoverhtlc_match *hsmtool_recoverhtlc_match_script( + const tal_t *ctx, + const struct bitcoin_tx *tx, + enum recoverhtlc_kind kind, + bool anchors, + const u8 *wscript); + +struct recoverhtlc_match *hsmtool_recoverhtlc_find_outputs( + const tal_t *ctx, + const struct bitcoin_tx *tx, + enum recoverhtlc_kind kind, + const struct recoverhtlc_keys *keys, + const struct sha256 *payment_hash, + u32 cltv_expiry); + +struct bitcoin_tx *hsmtool_recoverhtlc_sign( + const tal_t *ctx, + const struct bitcoin_tx *commitment_tx, + const struct recoverhtlc_match *match, + const struct recoverhtlc_keys *keys, + const u8 *destination_script, + u32 feerate_perkw, + u32 cltv_expiry, + const struct preimage *preimage, + struct recoverhtlc_amounts *amounts); + +const char *hsmtool_recoverhtlc_parse_bundle( + const tal_t *ctx, const char *path, struct recoverhtlc_bundle *bundle); + +void hsmtool_recoverhtlc(const char *argv0, + const char *bundle_path, + const char *destination, + const char *hsm_secret_path); + +#endif /* LIGHTNING_TOOLS_HSMTOOL_RECOVERHTLC_H */ diff --git a/tools/lightning-hsmtool.c b/tools/lightning-hsmtool.c index 95d0b2633557..3c85acf3367d 100644 --- a/tools/lightning-hsmtool.c +++ b/tools/lightning-hsmtool.c @@ -23,6 +23,8 @@ #include #include #include +#include +#include #include #include #include @@ -48,11 +50,15 @@ static void show_usage(const char *progname) printf(" - encrypt [LEGACY - binary format only]\n"); printf(" - dumpcommitments " "\n"); + printf(" - dumpcommitments --point-only " + " \n"); printf(" - guesstoremote " "\n"); printf(" - generatehsm \n"); printf(" - derivetoremote [] " "\n"); + printf(" - recoverhtlc " + "\n"); printf(" - checkhsm \n"); printf(" - dumponchaindescriptors [--show-secrets] [network]\n"); printf(" - makerune \n"); @@ -85,7 +91,8 @@ static bool ensure_hsm_secret_exists(int fd, const char *path) return true; } /* Load hsm_secret using the unified interface */ -static struct hsm_secret *load_hsm_secret(const tal_t *ctx, const char *hsm_secret_path) +struct hsm_secret *hsmtool_load_hsm_secret(const tal_t *ctx, + const char *hsm_secret_path) { u8 *contents = grab_file_raw(tmpctx, hsm_secret_path); const char *passphrase = NULL; @@ -133,7 +140,7 @@ static void decrypt_hsm(const char *hsm_secret_path) } /* Load the hsm_secret */ - hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsms = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); dir = path_dirname(NULL, hsm_secret_path); backup = path_join(dir, dir, "hsm_secret.backup"); @@ -190,7 +197,7 @@ static void encrypt_hsm(const char *hsm_secret_path) } /* Load the hsm_secret */ - hsms = load_hsm_secret(tmpctx, hsm_secret_path); + hsms = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); printf("Enter hsm_secret password:\n"); fflush(stdout); @@ -252,21 +259,7 @@ static void encrypt_hsm(const char *hsm_secret_path) static void get_channel_seed(struct secret *channel_seed, const struct node_id *peer_id, u64 dbid, struct secret *hsm_secret) { - struct secret channel_base; - u8 input[sizeof(peer_id->k) + sizeof(dbid)]; - const char *info = "per-peer seed"; - - hkdf_sha256(&channel_base, sizeof(struct secret), NULL, 0, - hsm_secret, sizeof(*hsm_secret), - "peer seed", strlen("peer seed")); - memcpy(input, peer_id->k, sizeof(peer_id->k)); - BUILD_ASSERT(sizeof(peer_id->k) == PUBKEY_CMPR_LEN); - memcpy(input + PUBKEY_CMPR_LEN, &dbid, sizeof(dbid)); - - hkdf_sha256(channel_seed, sizeof(*channel_seed), - input, sizeof(input), - &channel_base, sizeof(channel_base), - info, strlen(info)); + derive_channel_seed(channel_seed, hsm_secret->data, peer_id, dbid); } static void print_secret(const char *hsm_secret_path, const char *id, bool must_be_oldstyle) @@ -274,7 +267,8 @@ static void print_secret(const char *hsm_secret_path, const char *id, bool must_ struct secret hsm_secret; char *bip93; const char *err; - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); switch (hsms->type) { case HSM_SECRET_ENCRYPTED: @@ -328,7 +322,8 @@ static void dump_commitments_infos(struct node_id *node_id, u64 channel_id, struct sha256 shaseed; struct secret hsm_secret, channel_seed, per_commitment_secret; struct pubkey per_commitment_point; - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); /* Extract first 32 bytes for legacy compatibility */ memcpy(hsm_secret.data, hsms->secret_data, 32); get_channel_seed(&channel_seed, node_id, channel_id, &hsm_secret); @@ -348,6 +343,36 @@ static void dump_commitments_infos(struct node_id *node_id, u64 channel_id, } } +/* Unlike atoll, refuses anything which is not entirely a number. */ +static bool parse_u64(const char *arg, u64 *val) +{ + char *end; + + errno = 0; + *val = strtoull(arg, &end, 10); + return !errno && end != arg && *end == '\0' && arg[0] != '-'; +} + +static void print_commitment_point(const struct node_id *node_id, + u64 channel_id, + u64 commitment_number, + const char *hsm_secret_path) +{ + struct sha256 shaseed; + struct secret hsm_secret, channel_seed; + struct pubkey point; + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); + + memcpy(hsm_secret.data, hsms->secret_data, sizeof(hsm_secret.data)); + get_channel_seed(&channel_seed, node_id, channel_id, &hsm_secret); + derive_shaseed(&channel_seed, &shaseed); + if (!per_commit_point(&shaseed, &point, commitment_number)) + errx(ERROR_KEYDERIV, "Could not derive point #%"PRIu64, + commitment_number); + printf("%s\n", fmt_pubkey(tmpctx, &point)); +} + static void guess_to_remote(const char *address, struct node_id *node_id, u64 tries, char *hsm_secret_path) { @@ -365,7 +390,8 @@ static void guess_to_remote(const char *address, struct node_id *node_id, if (segwit_addr_decode(&witver, goal_pubkeyhash, &witlen, hrp, address) != 1) errx(ERROR_USAGE, "Wrong bech32 address"); - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); memcpy(hsm_secret.data, hsms->secret_data, 32); for (u64 dbid = 1; dbid < tries ; dbid++) { @@ -459,7 +485,8 @@ static void derive_to_remote(const struct unilateral_close_info *info, const cha secp256k1_ctx = secp256k1_context_create(SECP256K1_CONTEXT_VERIFY | SECP256K1_CONTEXT_SIGN); - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); /* Copy first 32 bytes of secret_data to the local secret struct */ memcpy(hsm_secret.data, hsms->secret_data, sizeof(hsm_secret.data)); get_channel_seed(&channel_seed, &info->peer_id, info->channel_id, &hsm_secret); @@ -485,7 +512,8 @@ static void dumponchaindescriptors(const char *hsm_secret_path, struct ext_key master_extkey; char *enc_xkey, *descriptor; struct descriptor_checksum checksum; - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); const char *path; /* For BIP 86, we derive from subkey: m/86'/0'/0' */ @@ -574,7 +602,8 @@ static void check_hsm(const char *hsm_secret_path) enum hsm_secret_error err; /* Load the hsm_secret (handles decryption automatically if needed) */ - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); /* Extract first 32 bytes for legacy compatibility */ memcpy(file_secret.data, hsms->secret_data, 32); @@ -614,7 +643,8 @@ static void make_rune(const char *hsm_secret_path) { struct secret hsm_secret, derived_secret, rune_secret; struct rune *master_rune, *rune; - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); /* Extract first 32 bytes for legacy compatibility */ memcpy(hsm_secret.data, hsms->secret_data, 32); @@ -642,7 +672,8 @@ static void print_node_id(const char *hsm_secret_path) struct secret hsm_secret; struct privkey node_privkey; struct pubkey node_id; - struct hsm_secret *hsms = load_hsm_secret(tmpctx, hsm_secret_path); + struct hsm_secret *hsms + = hsmtool_load_hsm_secret(tmpctx, hsm_secret_path); /* Extract first 32 bytes for legacy compatibility */ memcpy(hsm_secret.data, hsms->secret_data, 32); @@ -677,7 +708,11 @@ int main(int argc, char *argv[]) if (!method) show_usage(argv[0]); - if (streq(method, "decrypt")) { + if (streq(method, "recoverhtlc")) { + if (argc != 5) + show_usage(argv[0]); + hsmtool_recoverhtlc(argv[0], argv[2], argv[3], argv[4]); + } else if (streq(method, "decrypt")) { if (argc < 3) show_usage(argv[0]); decrypt_hsm(argv[2]); @@ -687,14 +722,31 @@ int main(int argc, char *argv[]) encrypt_hsm(argv[2]); } else if (streq(method, "dumpcommitments")) { - /* node_id channel_id depth hsm_secret */ - if (argc < 6) - show_usage(argv[0]); struct node_id node_id; - if (!node_id_from_hexstr(argv[2], strlen(argv[2]), &node_id)) - errx(ERROR_USAGE, "Bad node id"); - dump_commitments_infos(&node_id, atol(argv[3]), atol(argv[4]), - argv[5]); + + if (argc > 2 && streq(argv[2], "--point-only")) { + u64 dbid, commitment_number; + + if (argc != 7) + show_usage(argv[0]); + if (!node_id_from_hexstr(argv[3], strlen(argv[3]), &node_id)) + errx(ERROR_USAGE, "Bad node id"); + /* A mistyped number must not print the wrong point. */ + if (!parse_u64(argv[4], &dbid) || dbid == 0) + errx(ERROR_USAGE, "Bad channel dbid"); + if (!parse_u64(argv[5], &commitment_number)) + errx(ERROR_USAGE, "Bad commitment number"); + print_commitment_point(&node_id, dbid, + commitment_number, argv[6]); + } else { + /* node_id channel_id depth hsm_secret */ + if (argc < 6) + show_usage(argv[0]); + if (!node_id_from_hexstr(argv[2], strlen(argv[2]), &node_id)) + errx(ERROR_USAGE, "Bad node id"); + dump_commitments_infos(&node_id, atol(argv[3]), + atol(argv[4]), argv[5]); + } } else if (streq(method, "guesstoremote")) { /* address node_id depth hsm_secret */ diff --git a/tools/lightning-hsmtool.h b/tools/lightning-hsmtool.h new file mode 100644 index 000000000000..75832a2e52cd --- /dev/null +++ b/tools/lightning-hsmtool.h @@ -0,0 +1,11 @@ +#ifndef LIGHTNING_TOOLS_LIGHTNING_HSMTOOL_H +#define LIGHTNING_TOOLS_LIGHTNING_HSMTOOL_H +#include "config.h" +#include + +struct hsm_secret; + +struct hsm_secret *hsmtool_load_hsm_secret(const tal_t *ctx, + const char *hsm_secret_path); + +#endif /* LIGHTNING_TOOLS_LIGHTNING_HSMTOOL_H */