diff --git a/common/coin_mvt.c b/common/coin_mvt.c index f523d1b26feb..e6e698a3ac92 100644 --- a/common/coin_mvt.c +++ b/common/coin_mvt.c @@ -102,6 +102,7 @@ static struct chain_coin_mvt *new_chain_coin_mvt(const tal_t *ctx, mvt->tx_txid = tx_txid; mvt->outpoint = outpoint; + mvt->originating_acct = NULL; /* for htlc's that are filled onchain, we also have a * preimage, NULL otherwise */ @@ -260,11 +261,14 @@ struct chain_coin_mvt *new_coin_external_deposit(const tal_t *ctx, struct amount_sat amount, enum mvt_tag tag) { - return new_chain_coin_mvt(ctx, EXTERNAL, NULL, - outpoint, NULL, - blockheight, - take(new_tag_arr(NULL, tag)), - AMOUNT_MSAT(0), true, amount); + return new_chain_coin_mvt_sat(ctx, EXTERNAL, NULL, outpoint, NULL, + blockheight, take(new_tag_arr(NULL, tag)), + amount, true); +} + +bool chain_mvt_is_external(const struct chain_coin_mvt *mvt) +{ + return streq(mvt->account_name, EXTERNAL); } struct chain_coin_mvt *new_coin_wallet_deposit(const tal_t *ctx, @@ -327,6 +331,11 @@ struct coin_mvt *finalize_chain_mvt(const tal_t *ctx, struct coin_mvt *mvt = tal(ctx, struct coin_mvt); mvt->account_id = tal_strdup(mvt, chain_mvt->account_name); + if (chain_mvt->originating_acct) + mvt->originating_acct = + tal_strdup(mvt, chain_mvt->originating_acct); + else + mvt->originating_acct = NULL; mvt->hrp_name = tal_strdup(mvt, hrp_name); mvt->type = CHAIN_MVT; @@ -359,6 +368,8 @@ struct coin_mvt *finalize_channel_mvt(const tal_t *ctx, mvt->account_id = type_to_string(mvt, struct channel_id, &chan_mvt->chan_id); + /* channel moves don't have external events! */ + mvt->originating_acct = NULL; mvt->hrp_name = tal_strdup(mvt, hrp_name); mvt->type = CHANNEL_MVT; mvt->id.payment_hash = chan_mvt->payment_hash; @@ -388,6 +399,12 @@ void towire_chain_coin_mvt(u8 **pptr, const struct chain_coin_mvt *mvt) } else towire_bool(pptr, false); + if (mvt->originating_acct) { + towire_bool(pptr, true); + towire_wirestring(pptr, mvt->originating_acct); + } else + towire_bool(pptr, false); + towire_bitcoin_outpoint(pptr, mvt->outpoint); if (mvt->tx_txid) { @@ -419,6 +436,11 @@ void fromwire_chain_coin_mvt(const u8 **cursor, size_t *max, struct chain_coin_m } else mvt->account_name = NULL; + if (fromwire_bool(cursor, max)) { + mvt->originating_acct = fromwire_wirestring(mvt, cursor, max); + } else + mvt->originating_acct = NULL; + /* Read into non-const version */ struct bitcoin_outpoint *outpoint = tal(mvt, struct bitcoin_outpoint); diff --git a/common/coin_mvt.h b/common/coin_mvt.h index 6905c3257461..0f3b974755c2 100644 --- a/common/coin_mvt.h +++ b/common/coin_mvt.h @@ -83,6 +83,10 @@ struct chain_coin_mvt { /* total value of output (useful for tracking external outs) */ struct amount_sat output_val; + + /* When we pay to external accounts, it's useful + * to track which internal account it originated from */ + const char *originating_acct; }; /* differs depending on type!? */ @@ -97,6 +101,9 @@ struct coin_mvt { /* name of 'account': wallet, external, */ const char *account_id; + /* if account_id is external, the account this 'impacted' */ + const char *originating_acct; + /* Chain name: BIP 173, except signet lightning-style: tbs not tb */ const char *hrp_name; @@ -249,6 +256,9 @@ struct coin_mvt *finalize_channel_mvt(const tal_t *ctx, const struct node_id *node_id) NON_NULL_ARGS(2, 3, 5); +/* Is this an xternal account? */ +bool chain_mvt_is_external(const struct chain_coin_mvt *mvt); + const char *mvt_type_str(enum mvt_type type); const char *mvt_tag_str(enum mvt_tag tag); diff --git a/common/psbt_open.c b/common/psbt_open.c index b7a6f330c9ee..646b4ea029a5 100644 --- a/common/psbt_open.c +++ b/common/psbt_open.c @@ -491,6 +491,23 @@ bool psbt_has_our_input(const struct wally_psbt *psbt) return false; } +void psbt_output_mark_as_external(const tal_t *ctx, + struct wally_psbt_output *output) +{ + u8 *key = psbt_make_key(tmpctx, PSBT_TYPE_OUTPUT_EXTERNAL, NULL); + beint16_t bev = cpu_to_be16(1); + + psbt_output_set_unknown(ctx, output, key, &bev, sizeof(bev)); +} + +bool psbt_output_to_external(const struct wally_psbt_output *output) +{ + size_t unused; + void *result = psbt_get_lightning(&output->unknowns, + PSBT_TYPE_OUTPUT_EXTERNAL, &unused); + return !(!result); +} + bool psbt_contribs_changed(struct wally_psbt *orig, struct wally_psbt *new) { diff --git a/common/psbt_open.h b/common/psbt_open.h index 8e41a4e13b58..134a5da65754 100644 --- a/common/psbt_open.h +++ b/common/psbt_open.h @@ -37,6 +37,7 @@ struct psbt_changeset { #define PSBT_TYPE_SERIAL_ID 0x01 #define PSBT_TYPE_INPUT_MARKER 0x02 +#define PSBT_TYPE_OUTPUT_EXTERNAL 0x04 /* psbt_get_serial_id - Returns the serial_id from an unknowns map * @@ -177,6 +178,17 @@ bool psbt_input_is_ours(const struct wally_psbt_input *input); */ bool psbt_has_our_input(const struct wally_psbt *psbt); +/* psbt_output_mark_external - Marks an output as a deposit to + * an external address. + * Used when withdrawing from internal + * wallet */ +void psbt_output_mark_as_external(const tal_t *ctx, + struct wally_psbt_output *output); + +/* psbt_output_to_external - Is this an output we're paying to an external + * party? */ +bool psbt_output_to_external(const struct wally_psbt_output *output); + /* psbt_contribs_changed - Returns true if the psbt's inputs/outputs * have changed. * diff --git a/doc/PLUGINS.md b/doc/PLUGINS.md index f214080f6e7d..c6e1af1ac0e1 100644 --- a/doc/PLUGINS.md +++ b/doc/PLUGINS.md @@ -701,6 +701,7 @@ i.e. only definitively resolved HTLCs or confirmed bitcoin transactions. "node_id":"03a7103a2322b811f7369cbb27fb213d30bbc0b012082fed3cad7e4498da2dc56b", "type":"chain_mvt", "account_id":"wallet", + "originating_account": "wallet", // (`chain_mvt` only, optional) "txid":"0159693d8f3876b4def468b208712c630309381e9d106a9836fa0a9571a28722", // (`chain_mvt` only, optional) "utxo_txid":"0159693d8f3876b4def468b208712c630309381e9d106a9836fa0a9571a28722", // (`chain_mvt` only) "vout":1, // (`chain_mvt` only) @@ -731,6 +732,9 @@ notification adheres to. `account_id` is the name of this account. The node's wallet is named 'wallet', all channel funds' account are the channel id. +`originating_account` is the account that this movement originated from. +*Only* tagged on external events (deposits/withdrawals to an external party). + `txid` is the transaction id of the bitcoin transaction that triggered this ledger event. `utxo_txid` and `vout` identify the bitcoin output which triggered this notification. (`chain_mvt` only). Notifications tagged @@ -767,24 +771,26 @@ both the debit/credit contain fees. Technically routed debits are the - `invoice`: funds paid to or recieved from an invoice. - `routed`: funds routed through this node. - `pushed`: funds pushed to peer. - - channel_open : channel is opened, initial channel balance - - channel_close: channel is closed, final channel balance - - delayed_to_us : on-chain output to us, spent back into our wallet - - htlc_timeout : on-chain htlc timeout output - - htlc_fulfill : on-chian htlc fulfill output - - htlc_tx : on-chain htlc tx has happened - - to_wallet : output being spent into our wallet - - ignored : output is being ignored - - anchor : an anchor output - - to_them : output intended to peer's wallet - - penalized : output we've 'lost' due to a penalty (failed cheat attempt) - - stolen : output we've 'lost' due to peer's cheat - - to_miner : output we've burned to miner (OP_RETURN) - - opener : tags channel_open, we are the channel opener - - lease_fee: amount paid as lease fee - - leased: tags channel_open, channel contains leased funds - -`blockheight` is the block the txid is included in. + - `channel_open` : channel is opened, initial channel balance + - `channel_close`: channel is closed, final channel balance + - `delayed_to_us`: on-chain output to us, spent back into our wallet + - `htlc_timeout`: on-chain htlc timeout output + - `htlc_fulfill`: on-chian htlc fulfill output + - `htlc_tx`: on-chain htlc tx has happened + - `to_wallet`: output being spent into our wallet + - `ignored`: output is being ignored + - `anchor`: an anchor output + - `to_them`: output intended to peer's wallet + - `penalized`: output we've 'lost' due to a penalty (failed cheat attempt) + - `stolen`: output we've 'lost' due to peer's cheat + - `to_miner`: output we've burned to miner (OP_RETURN) + - `opener`: tags channel_open, we are the channel opener + - `lease_fee`: amount paid as lease fee + - `leased`: tags channel_open, channel contains leased funds + +`blockheight` is the block the txid is included in. `channel_mvt`s will be null, +so will the blockheight for withdrawals to external parties (we issue these events +when we send the tx containing them, before they're included in the chain). The `timestamp` is seconds since Unix epoch of the node's machine time at the time lightningd broadcasts the notification. diff --git a/lightningd/notification.c b/lightningd/notification.c index a93a4d6e5202..13f9154b9779 100644 --- a/lightningd/notification.c +++ b/lightningd/notification.c @@ -470,6 +470,9 @@ static void coin_movement_notification_serialize(struct json_stream *stream, json_add_node_id(stream, "node_id", mvt->node_id); json_add_string(stream, "type", mvt_type_str(mvt->type)); json_add_string(stream, "account_id", mvt->account_id); + if (mvt->originating_acct) + json_add_string(stream, "originating_account", + mvt->originating_acct); json_mvt_id(stream, mvt->type, &mvt->id); json_add_amount_msat_only(stream, "credit", mvt->credit); json_add_amount_msat_only(stream, "debit", mvt->debit); diff --git a/lightningd/onchain_control.c b/lightningd/onchain_control.c index d4f81de27165..b1122a79ac18 100644 --- a/lightningd/onchain_control.c +++ b/lightningd/onchain_control.c @@ -265,6 +265,9 @@ static void handle_onchain_log_coin_move(struct channel *channel, const u8 *msg) if (!mvt->account_name) mvt->account_name = type_to_string(mvt, struct channel_id, &channel->cid); + else if (chain_mvt_is_external(mvt)) + mvt->originating_acct = type_to_string(mvt, struct channel_id, + &channel->cid); notify_chain_mvt(channel->peer->ld, mvt); tal_free(mvt); } diff --git a/plugins/spender/multiwithdraw.c b/plugins/spender/multiwithdraw.c index afe7a3534c7f..b89638c68536 100644 --- a/plugins/spender/multiwithdraw.c +++ b/plugins/spender/multiwithdraw.c @@ -6,6 +6,7 @@ #include #include #include +#include #include #include #include @@ -44,6 +45,8 @@ struct multiwithdraw_destination { struct amount_sat amount; /* Whether the amount was "all". */ bool all; + /* Whether this is to an external addr (all passed in are assumed) */ + bool is_to_external; }; struct multiwithdraw_command { @@ -93,6 +96,7 @@ param_outputs_array(struct command *cmd, enum address_parse_result res; dest = &(*outputs)[i]; + dest->is_to_external = true; if (e->type != JSMN_OBJECT) goto err; @@ -537,6 +541,7 @@ mw_after_newaddr(struct command *cmd, change.script = script; change.amount = mw->change_amount; change.all = false; + change.is_to_external = false; tal_arr_expand(&mw->outputs, change); @@ -560,15 +565,18 @@ mw_load_outputs(struct multiwithdraw_command *mw) { /* Insert outputs at random locations. */ for (size_t i = 0; i < tal_count(mw->outputs); ++i) { + struct wally_psbt_output *out; /* There are already `i` outputs at this point, * select from 0 to `i` inclusive, with 0 meaning * "before first output" and `i` meaning "after * last output". */ size_t point = pseudorand(i + 1); - psbt_insert_output(mw->psbt, - mw->outputs[i].script, - mw->outputs[i].amount, - point); + out = psbt_insert_output(mw->psbt, + mw->outputs[i].script, + mw->outputs[i].amount, + point); + if (mw->outputs[i].is_to_external) + psbt_output_mark_as_external(mw->psbt, out); } if (chainparams->is_elements) { diff --git a/plugins/txprepare.c b/plugins/txprepare.c index 82776adc0f19..4de70d44fb47 100644 --- a/plugins/txprepare.c +++ b/plugins/txprepare.c @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -13,6 +14,7 @@ struct tx_output { struct amount_sat amount; const u8 *script; + bool is_to_external; }; struct txprepare { @@ -79,6 +81,9 @@ static struct command_result *param_outputs(struct command *cmd, enum address_parse_result res; struct tx_output *out = &txp->outputs[i]; + /* We assume these are accounted for elsewhere */ + out->is_to_external = false; + /* output format: {destination: amount} */ if (t->type != JSMN_OBJECT) return command_fail(cmd, JSONRPC2_INVALID_PARAMS, @@ -190,6 +195,11 @@ static struct command_result *finish_txprepare(struct command *cmd, struct amount_sat, &txp->outputs[i].amount)); psbt_add_output(txp->psbt, out, i); + + if (txp->outputs[i].is_to_external) + psbt_output_mark_as_external(txp->psbt, + &txp->psbt->outputs[i]); + wally_tx_output_free(out); } @@ -239,6 +249,7 @@ static struct command_result *newaddr_done(struct command *cmd, sizeof(txp->outputs[0]) * (num - pos)); txp->outputs[pos].amount = txp->change_amount; + txp->outputs[pos].is_to_external = false; if (json_to_address_scriptpubkey(txp, chainparams, buf, addr, &txp->outputs[pos].script) != ADDRESS_PARSE_SUCCESS) { @@ -516,6 +527,7 @@ static struct command_result *json_withdraw(struct command *cmd, } txp->outputs[0].amount = *amount; txp->outputs[0].script = scriptpubkey; + txp->outputs[0].is_to_external = true; txp->weight = bitcoin_tx_core_weight(1, tal_count(txp->outputs)) + bitcoin_tx_output_weight(tal_bytelen(scriptpubkey)); diff --git a/tests/test_misc.py b/tests/test_misc.py index 3cde1fad72e7..3f13b1cadbff 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -10,7 +10,7 @@ wait_for, TailableProc, env ) from utils import ( - account_balance, scriptpubkey_addr + account_balance, scriptpubkey_addr, check_coin_moves ) from ephemeral_port_reserve import reserve from utils import EXPERIMENTAL_FEATURES @@ -623,6 +623,16 @@ def dont_spend_outputs(n, txid): sync_blockheight(bitcoind, [l1]) assert account_balance(l1, 'wallet') == 0 + external_moves = [ + {'type': 'chain_mvt', 'credit': 2000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 2000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 2000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 2000000000, 'debit': 0, 'tags': ['deposit']}, + {'type': 'chain_mvt', 'credit': 11957603000, 'debit': 0, 'tags': ['deposit']}, + ] + + check_coin_moves(l1, 'external', external_moves, chainparams) + def test_io_logging(node_factory, executor): l1 = node_factory.get_node(options={'log-level': 'io'}) diff --git a/tests/test_wallet.py b/tests/test_wallet.py index 4aaf7557d613..f7c601b26e2e 100644 --- a/tests/test_wallet.py +++ b/tests/test_wallet.py @@ -7,6 +7,7 @@ from utils import ( only_one, wait_for, sync_blockheight, EXPERIMENTAL_FEATURES, VALGRIND, check_coin_moves, TailableProc, scriptpubkey_addr, + check_utxos_channel ) import os @@ -223,7 +224,8 @@ def test_addfunds_from_block(node_factory, bitcoind): """Send funds to the daemon without telling it explicitly """ # Previous runs with same bitcoind can leave funds! - l1 = node_factory.get_node(random_hsm=True) + coin_plugin = os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py') + l1 = node_factory.get_node(random_hsm=True, options={'plugin': coin_plugin}) addr = l1.rpc.newaddr()['bech32'] bitcoind.rpc.sendtoaddress(addr, 0.1) @@ -248,6 +250,15 @@ def test_addfunds_from_block(node_factory, bitcoind): output = only_one(l1.rpc.listfunds()['outputs']) assert output['address'] == addr + # We don't print a 'external deposit' event + # for funds that come back to our own wallet + expected_utxos = { + '0': [('wallet', ['deposit'], ['withdrawal'], 'A')], + 'A': [('wallet', ['deposit'], None, None)], + } + + check_utxos_channel(l1, [], expected_utxos) + def test_txprepare_multi(node_factory, bitcoind): amount = 10000000 @@ -1298,11 +1309,13 @@ def test_withdraw_nlocktime_fuzz(node_factory, bitcoind): raise Exception("No transaction with fuzzed nLockTime !") -def test_multiwithdraw_simple(node_factory, bitcoind): +def test_multiwithdraw_simple(node_factory, bitcoind, chainparams): """ Test simple multiwithdraw usage. """ - l1, l2, l3 = node_factory.get_nodes(3) + coin_plugin = os.path.join(os.getcwd(), 'tests/plugins/coin_movements.py') + l1, l2, l3 = node_factory.get_nodes(3, opts=[{'plugin': coin_plugin}, + {}, {}]) l1.fundwallet(10**8) addr2 = l2.rpc.newaddr()['bech32'] @@ -1329,6 +1342,15 @@ def test_multiwithdraw_simple(node_factory, bitcoind): assert only_one(funds3)["status"] == "confirmed" assert only_one(funds3)["amount_msat"] == amount3 + expected_utxos = { + '0': [('wallet', ['deposit'], ['withdrawal'], 'A')], + 'A': [('wallet', ['deposit'], None, None), + ('external', ['deposit'], None, None), + ('external', ['deposit'], None, None)], + } + + check_utxos_channel(l1, [], expected_utxos) + @unittest.skipIf( TEST_NETWORK == 'liquid-regtest', diff --git a/tests/utils.py b/tests/utils.py index d10fdada3b4c..1d417a2da77c 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -136,7 +136,7 @@ def check_coin_moves(n, account_id, expected_moves, chainparams): assert mv['timestamp'] > 0 assert mv['coin_type'] == chainparams['bip173_prefix'] # chain moves should have blockheights - if mv['type'] == 'chain_mvt': + if mv['type'] == 'chain_mvt' and mv['account_id'] != 'external': assert mv['blockheight'] is not None for num, m in enumerate(expected_moves): diff --git a/wallet/walletrpc.c b/wallet/walletrpc.c index 5aac2c19daa0..584dfa8158a0 100644 --- a/wallet/walletrpc.c +++ b/wallet/walletrpc.c @@ -10,11 +10,13 @@ #include #include #include +#include #include #include #include #include #include +#include #include #include #include @@ -753,9 +755,48 @@ struct sending_psbt { struct command *cmd; struct utxo **utxos; struct wally_tx *wtx; + /* Hold onto b/c has data about + * which are to external addresses */ + struct wally_psbt *psbt; u32 reserve_blocks; }; +static void maybe_notify_new_external_send(struct lightningd *ld, + struct bitcoin_txid *txid, + u32 outnum, + struct wally_psbt *psbt) +{ + struct chain_coin_mvt *mvt; + struct bitcoin_outpoint outpoint; + struct amount_sat amount; + u32 index; + bool is_p2sh; + const u8 *script; + + /* If it's not going to an external address, ignore */ + if (!psbt_output_to_external(&psbt->outputs[outnum])) + return; + + /* If it's going to our wallet, ignore */ + script = wally_tx_output_get_script(tmpctx, + &psbt->tx->outputs[outnum]); + if (wallet_can_spend(ld->wallet, script, &index, &is_p2sh)) + return; + + outpoint.txid = *txid; + outpoint.n = outnum; + amount = psbt_output_get_amount(psbt, outnum); + + mvt = new_coin_external_deposit(NULL, &outpoint, + 0, amount, + DEPOSIT); + + mvt->originating_acct = WALLET; + notify_chain_mvt(ld, mvt); + tal_free(mvt); +} + + static void sendpsbt_done(struct bitcoind *bitcoind UNUSED, bool success, const char *msg, struct sending_psbt *sending) @@ -787,9 +828,12 @@ static void sendpsbt_done(struct bitcoind *bitcoind UNUSED, /* Extract the change output and add it to the DB */ wallet_extract_owned_outputs(ld->wallet, sending->wtx, NULL, &change); + wally_txid(sending->wtx, &txid); + + for (size_t i = 0; i < sending->psbt->num_outputs; i++) + maybe_notify_new_external_send(ld, &txid, i, sending->psbt); response = json_stream_success(sending->cmd); - wally_txid(sending->wtx, &txid); json_add_hex_talarr(response, "tx", linearize_wtx(tmpctx, sending->wtx)); json_add_txid(response, "txid", &txid); was_pending(command_success(sending->cmd, response)); @@ -818,6 +862,12 @@ static struct command_result *json_sendpsbt(struct command *cmd, psbt_finalize(psbt); sending->wtx = psbt_final_tx(sending, psbt); + + /* psbt contains info about which outputs are to external, + * and thus need a coin_move issued for them. We only + * notify if the transaction broadcasts */ + sending->psbt = tal_steal(sending, psbt); + if (!sending->wtx) return command_fail(cmd, LIGHTNINGD, "PSBT not finalizeable %s",