From 64a26df9df5c02a2cb1aa64b057373f618ea9754 Mon Sep 17 00:00:00 2001 From: Dusty Daemon Date: Tue, 19 Sep 2023 17:35:57 -0400 Subject: [PATCH] splice: Add test for splice-out and fix errors Added a test for splicing out that exposed some behavior and code glitches that are addressed in this commit. Added tests around invalid splices and forced restarts mid-splice and fixed related functionality. Also added documentation for how to do a splice out . ChangeLog-Fixed: Added docs, testing, and some fixes related to splicing out, insufficent balance handling, and restarting during a splice. --- channeld/channeld.c | 151 ++++++++++++++++++--------------- channeld/inflight.c | 18 +++- common/jsonrpc_errors.h | 4 + doc/lightning-splice_init.7.md | 59 +++++++++---- lightningd/channel_control.c | 58 ++++++------- tests/test_splicing.py | 144 +++++++++++++++++++++++++++++++ 6 files changed, 314 insertions(+), 120 deletions(-) diff --git a/channeld/channeld.c b/channeld/channeld.c index a2ee13dec561..3c01c8141281 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -61,6 +61,8 @@ ((msg) == WIRE_SPLICE || \ (msg) == WIRE_SPLICE_ACK) +#define SAT_MIN(a, b) (amount_sat_less((a), (b)) ? (a) : (b)) + struct peer { struct per_peer_state *pps; bool channel_ready[NUM_SIDES]; @@ -1487,7 +1489,8 @@ static u8 *send_commit_part(struct peer *peer, const struct htlc **changed_htlcs, bool notify_master, s64 splice_amnt, - s64 remote_splice_amnt) + s64 remote_splice_amnt, + u64 remote_index) { u8 *msg; struct bitcoin_signature commit_sig, *htlc_sigs; @@ -1515,14 +1518,14 @@ static u8 *send_commit_part(struct peer *peer, txs = channel_splice_txs(tmpctx, funding, funding_sats, &htlc_map, direct_outputs, &funding_wscript, peer->channel, &peer->remote_per_commit, - peer->next_index[REMOTE], REMOTE, + remote_index, REMOTE, splice_amnt, remote_splice_amnt); htlc_sigs = calc_commitsigs(tmpctx, peer, txs, funding_wscript, htlc_map, - peer->next_index[REMOTE], &commit_sig); + remote_index, &commit_sig); if (direct_outputs[LOCAL] != NULL) { - pbase = penalty_base_new(tmpctx, peer->next_index[REMOTE], + pbase = penalty_base_new(tmpctx, remote_index, txs[0], direct_outputs[LOCAL]); /* Add the penalty_base to our in-memory list as well, so we @@ -1543,8 +1546,7 @@ static u8 *send_commit_part(struct peer *peer, status_debug("Telling master we're about to commit..."); /* Tell master to save this next commit to database, then wait. */ - msg = sending_commitsig_msg(NULL, peer->next_index[REMOTE], - pbase, + msg = sending_commitsig_msg(NULL, remote_index, pbase, peer->channel->fee_states, peer->channel->blockheight_states, changed_htlcs, @@ -1692,7 +1694,7 @@ static void send_commit(struct peer *peer) msgs[0] = send_commit_part(peer, &peer->channel->funding, peer->channel->funding_sats, changed_htlcs, - true, 0, 0); + true, 0, 0, peer->next_index[REMOTE]); /* Loop over current inflights * BOLT-0d8b701614b09c6ee4172b04da2203e73deec7e2 #2: @@ -1715,7 +1717,8 @@ static void send_commit(struct peer *peer) peer->splice_state->inflights[i]->amnt, changed_htlcs, false, peer->splice_state->inflights[i]->splice_amnt, - remote_splice_amnt)); + remote_splice_amnt, + peer->next_index[REMOTE])); } peer->next_index[REMOTE]++; @@ -2907,7 +2910,7 @@ static size_t calc_weight(enum tx_role role, const struct wally_psbt *psbt) weight += psbt_input_get_weight(psbt, i); for (size_t i = 0; i < psbt->num_outputs; i++) - if (is_initiators_serial(&psbt->inputs[i].unknowns)) { + if (is_initiators_serial(&psbt->outputs[i].unknowns)) { if (role == TX_INITIATOR) weight += psbt_output_get_weight(psbt, i); } @@ -2928,7 +2931,7 @@ static struct amount_sat check_balances(struct peer *peer, { struct amount_sat min_initiator_fee, min_accepter_fee, max_initiator_fee, max_accepter_fee, - funding_amount_res; + funding_amount_res, min_multiplied; struct amount_msat funding_amount, initiator_fee, accepter_fee; struct amount_msat in[NUM_TX_ROLES], out[NUM_TX_ROLES]; @@ -2977,45 +2980,23 @@ static struct amount_sat check_balances(struct peer *peer, * While we're, here, adjust the output counts by splice amount. */ - if(peer->splicing->opener_relative > 0) { - if (!amount_msat_add_sat(&funding_amount, funding_amount, - amount_sat((u64)peer->splicing->opener_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to add opener funding"); - if (!amount_msat_add_sat(&out[TX_INITIATOR], out[TX_INITIATOR], - amount_sat((u64)peer->splicing->opener_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to add opener funding to out amnt."); - } else { - if (!amount_msat_sub_sat(&funding_amount, funding_amount, - amount_sat((u64)-peer->splicing->opener_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to sub opener funding"); - if (!amount_msat_sub_sat(&out[TX_INITIATOR], out[TX_INITIATOR], - amount_sat((u64)peer->splicing->opener_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to sub opener funding from out amnt."); - } + if (!amount_msat_add_sat_s64(&funding_amount, funding_amount, + peer->splicing->opener_relative)) + peer_failed_warn(peer->pps, &peer->channel_id, + "Unable to add opener funding"); + if (!amount_msat_add_sat_s64(&out[TX_INITIATOR], out[TX_INITIATOR], + peer->splicing->opener_relative)) + peer_failed_warn(peer->pps, &peer->channel_id, + "Unable to add opener funding to out amnt."); - if(peer->splicing->accepter_relative > 0) { - if (!amount_msat_add_sat(&funding_amount, funding_amount, - amount_sat((u64)peer->splicing->accepter_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to add accepter funding"); - if (!amount_msat_add_sat(&out[TX_ACCEPTER], out[TX_ACCEPTER], - amount_sat((u64)peer->splicing->accepter_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to add accepter funding to out amnt."); - } else { - if (!amount_msat_sub_sat(&funding_amount, funding_amount, - amount_sat((u64)-peer->splicing->accepter_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to subtract accepter funding"); - if (!amount_msat_sub_sat(&out[TX_ACCEPTER], out[TX_ACCEPTER], - amount_sat((u64)-peer->splicing->accepter_relative))) - peer_failed_warn(peer->pps, &peer->channel_id, - "Unable to sub accepter funding from out amnt."); - } + if (!amount_msat_add_sat_s64(&funding_amount, funding_amount, + peer->splicing->accepter_relative)) + peer_failed_warn(peer->pps, &peer->channel_id, + "Unable to add accepter funding"); + if (!amount_msat_add_sat_s64(&out[TX_ACCEPTER], out[TX_ACCEPTER], + peer->splicing->accepter_relative)) + peer_failed_warn(peer->pps, &peer->channel_id, + "Unable to add accepter funding to out amnt."); if (amount_msat_less(in[TX_INITIATOR], out[TX_INITIATOR])) { msg = towire_channeld_splice_funding_error(NULL, in[TX_INITIATOR], @@ -3064,6 +3045,14 @@ static struct amount_sat check_balances(struct peer *peer, max_initiator_fee = amount_tx_fee(peer->feerate_max, calc_weight(TX_INITIATOR, psbt)); + /* Sometimes feerate_max is some absurdly high value, in that case we + * give a fee warning based of a multiple of the min value. */ + amount_sat_mul(&min_multiplied, min_accepter_fee, 5); + max_accepter_fee = SAT_MIN(min_multiplied, max_accepter_fee); + + amount_sat_mul(&min_multiplied, min_initiator_fee, 5); + max_initiator_fee = SAT_MIN(min_multiplied, max_initiator_fee); + /* Check initiator fee */ if (amount_msat_less_sat(initiator_fee, min_initiator_fee)) { msg = towire_channeld_splice_feerate_error(NULL, initiator_fee, @@ -3302,11 +3291,11 @@ static void resume_splice_negotiation(struct peer *peer, txsig_tlvs); if (do_i_sign_first(peer, current_psbt, our_role)) { - status_debug("Splice: we sign first"); msg = towire_channeld_update_inflight(NULL, current_psbt, NULL, NULL); wire_sync_write(MASTER_FD, take(msg)); peer_write(peer->pps, sigmsg); + status_debug("Splice: we signed first"); } msg = peer_read(tmpctx, peer->pps); @@ -3423,8 +3412,8 @@ static void resume_splice_negotiation(struct peer *peer, wire_sync_write(MASTER_FD, take(msg)); if (!do_i_sign_first(peer, current_psbt, our_role)) { - status_debug("Splice: we sign second"); peer_write(peer->pps, sigmsg); + status_debug("Splice: we signed second"); } peer->splicing = tal_free(peer->splicing); @@ -4263,12 +4252,8 @@ static int cmp_changed_htlc_id(const struct changed_htlc *a, static void resend_commitment(struct peer *peer, struct changed_htlc *last) { size_t i; - struct bitcoin_signature commit_sig, *htlc_sigs; u8 *msg; - struct bitcoin_tx **txs; - const u8 *funding_wscript; - const struct htlc **htlc_map; - struct wally_tx_output *direct_outputs[NUM_SIDES]; + u8 **msgs = tal_arr(tmpctx, u8*, 1); status_debug("Retransmitting commitment, feerate LOCAL=%u REMOTE=%u," " blockheight LOCAL=%u REMOTE=%u", @@ -4359,19 +4344,37 @@ static void resend_commitment(struct peer *peer, struct changed_htlc *last) } } - /* Re-send the commitment_signed itself. */ - txs = channel_txs(tmpctx, &htlc_map, direct_outputs, - &funding_wscript, peer->channel, &peer->remote_per_commit, - peer->next_index[REMOTE]-1, REMOTE); + msgs[0] = send_commit_part(peer, &peer->channel->funding, + peer->channel->funding_sats, NULL, + false, 0, 0, peer->next_index[REMOTE] - 1); - htlc_sigs = calc_commitsigs(tmpctx, peer, txs, funding_wscript, htlc_map, peer->next_index[REMOTE]-1, - &commit_sig); + /* Loop over current inflights + * BOLT-0d8b701614b09c6ee4172b04da2203e73deec7e2 #2: + * + * A sending node: + *... + * - MUST first send a `commitment_signed` for the active channel then immediately + * send a `commitment_signed` for each splice awaiting confirmation, in increasing + * feerate order. + */ + for (i = 0; i < tal_count(peer->splice_state->inflights); i++) { + s64 funding_diff = sats_diff(peer->splice_state->inflights[i]->amnt, + peer->channel->funding_sats); + s64 remote_splice_amnt = funding_diff + - peer->splice_state->inflights[i]->splice_amnt; - msg = towire_commitment_signed(NULL, &peer->channel_id, - &commit_sig.s, - raw_sigs(tmpctx, htlc_sigs), - NULL); - peer_write(peer->pps, take(msg)); + tal_arr_expand(&msgs, + send_commit_part(peer, + &peer->splice_state->inflights[i]->outpoint, + peer->splice_state->inflights[i]->amnt, + NULL, false, + peer->splice_state->inflights[i]->splice_amnt, + remote_splice_amnt, + peer->next_index[REMOTE] - 1)); + } + + for(i = 0; i < tal_count(msgs); i++) + peer_write(peer->pps, take(msgs[i])); /* If we have already received the revocation for the previous, the * other side shouldn't be asking for a retransmit! */ @@ -4638,8 +4641,14 @@ static void peer_reconnect(struct peer *peer, send_tlvs = tlv_channel_reestablish_tlvs_new(peer); /* If inflight with no sigs on it, send next_funding */ - if (inflight && !inflight->last_tx) + if (inflight && !inflight->last_tx) { + status_debug("Reestablish with an inflight but missing" + " last_tx, will send next_funding %s", + type_to_string(tmpctx, + struct bitcoin_txid, + &inflight->outpoint.txid)); send_tlvs->next_funding = &inflight->outpoint.txid; + } /* BOLT-upgrade_protocol #2: * A node sending `channel_reestablish`, if it supports upgrading channels: @@ -4772,9 +4781,12 @@ static void peer_reconnect(struct peer *peer, tal_hex(msg, msg)); } - status_debug("Got reestablish commit=%"PRIu64" revoke=%"PRIu64, + status_debug("Got reestablish commit=%"PRIu64" revoke=%"PRIu64 + " inflights: %lu, active splices: %"PRIu32, next_commitment_number, - next_revocation_number); + next_revocation_number, + tal_count(peer->splice_state->inflights), + peer->splice_state->count); /* BOLT #2: * @@ -5079,6 +5091,7 @@ static void peer_reconnect(struct peer *peer, &peer->channel->funding.txid)); } else { + status_info("Resuming splice negotation"); resume_splice_negotiation(peer, inflight, false, inflight->i_am_initiator ? TX_INITIATOR diff --git a/channeld/inflight.c b/channeld/inflight.c index 77f0fa996381..3c7796960f72 100644 --- a/channeld/inflight.c +++ b/channeld/inflight.c @@ -12,8 +12,15 @@ struct inflight *fromwire_inflight(const tal_t *ctx, const u8 **cursor, size_t * inflight->amnt = fromwire_amount_sat(cursor, max); inflight->psbt = fromwire_wally_psbt(inflight, cursor, max); inflight->splice_amnt = fromwire_s64(cursor, max); - inflight->last_tx = fromwire_bitcoin_tx(inflight, cursor, max); - fromwire_bitcoin_signature(cursor, max, &inflight->last_sig); + int has_tx = fromwire_u8(cursor, max); + if(has_tx) { + inflight->last_tx = fromwire_bitcoin_tx(inflight, cursor, max); + fromwire_bitcoin_signature(cursor, max, &inflight->last_sig); + } + else { + inflight->last_tx = NULL; + memset(&inflight->last_sig, 0, sizeof(inflight->last_sig)); + } inflight->i_am_initiator = fromwire_bool(cursor, max); return inflight; @@ -25,8 +32,11 @@ void towire_inflight(u8 **pptr, const struct inflight *inflight) towire_amount_sat(pptr, inflight->amnt); towire_wally_psbt(pptr, inflight->psbt); towire_s64(pptr, inflight->splice_amnt); - towire_bitcoin_tx(pptr, inflight->last_tx); - towire_bitcoin_signature(pptr, &inflight->last_sig); + towire_u8(pptr, inflight->last_tx ? 1 : 0); + if(inflight->last_tx) { + towire_bitcoin_tx(pptr, inflight->last_tx); + towire_bitcoin_signature(pptr, &inflight->last_sig); + } towire_bool(pptr, inflight->i_am_initiator); } diff --git a/common/jsonrpc_errors.h b/common/jsonrpc_errors.h index 30163d1790a6..39c19960a316 100644 --- a/common/jsonrpc_errors.h +++ b/common/jsonrpc_errors.h @@ -73,6 +73,10 @@ enum jsonrpc_errcode { SPLICE_NOT_SUPPORTED = 354, SPLICE_BUSY_ERROR = 355, SPLICE_INPUT_ERROR = 356, + SPLICE_FUNDING_LOW = 357, + SPLICE_STATE_ERROR = 358, + SPLICE_LOW_FEE = 359, + SPLICE_HIGH_FEE = 360, /* `connect` errors */ CONNECT_NO_KNOWN_ADDRESS = 400, diff --git a/doc/lightning-splice_init.7.md b/doc/lightning-splice_init.7.md index b461cc135f6a..ae46fd24e89d 100644 --- a/doc/lightning-splice_init.7.md +++ b/doc/lightning-splice_init.7.md @@ -29,29 +29,58 @@ our\_bytes\_in\_splice\_tx / 1000. provided looks too high. This is to protect against accidentally setting your fee higher than intended. Set `force_feerate` to true to skip this saftey check. +Note you may need to add a double dash (\-\-) after splice\_init if using a negative +*relative\_amount* so it is not interpretted as a command modifier. For example: +```shell +lightning-cli splice_init -- $CHANNEL_ID -100000 +``` + Here is an example set of splice commands that will splice in 100,000 sats to the first channel that comes out of `listpeerchannels`. The example assumes you already have at least one confirmed channel. ```shell -RESULT=$(lightning-cli listpeerchannels) -CHANNEL_ID=$(echo $RESULT| jq -r ".channels[0].channel_id") -echo $RESULT +RESULT=$(lightning-cli listpeerchannels); +CHANNEL_ID=$(echo $RESULT| jq -r ".channels[0].channel_id"); +echo $RESULT; + +RESULT=$(lightning-cli fundpsbt -k satoshi=100000sat feerate=urgent startweight=800 excess_as_change=true); +INITIALPSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; + +RESULT=$(lightning-cli splice_init $CHANNEL_ID 100000 $INITIALPSBT); +PSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; + +RESULT=$(lightning-cli splice_update $CHANNEL_ID $PSBT); +PSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; -RESULT=$(lightning-cli fundpsbt -k satoshi=100000sat feerate=urgent startweight=800 excess_as_change=true) -INITIALPSBT=$(echo $RESULT | jq -r ".psbt") -echo $RESULT +RESULT=$(lightning-cli signpsbt -k psbt="$PSBT"); +PSBT=$(echo $RESULT | jq -r ".signed_psbt"); +echo $RESULT; + +lightning-cli splice_signed $CHANNEL_ID $PSBT +``` + +Here is an example set of splice commands that will splice out 100,000 sats from + first channel that comes out of `listpeerchannels`. The example assumes +you already have at least one confirmed channel. +```shell +RESULT=$(lightning-cli listpeerchannels); +CHANNEL_ID=$(echo $RESULT| jq -r ".channels[0].channel_id"); +echo $RESULT; -RESULT=$(lightning-cli splice_init $CHANNEL_ID 100000 $INITIALPSBT) -PSBT=$(echo $RESULT | jq -r ".psbt") -echo $RESULT +RESULT=$(lightning-cli newoutput 100000); +INITIALPSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; -RESULT=$(lightning-cli splice_update $CHANNEL_ID $PSBT) -PSBT=$(echo $RESULT | jq -r ".psbt") -echo $RESULT +RESULT=$(lightning-cli splice_init -- $CHANNEL_ID -100500 $INITIALPSBT); +PSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; -RESULT=$(lightning-cli signpsbt -k psbt="$PSBT") -PSBT=$(echo $RESULT | jq -r ".signed_psbt") -echo $RESULT +RESULT=$(lightning-cli splice_update $CHANNEL_ID $PSBT); +PSBT=$(echo $RESULT | jq -r ".psbt"); +echo $RESULT; lightning-cli splice_signed $CHANNEL_ID $PSBT ``` diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 1d03efcad41a..768370b0cf28 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -192,24 +192,20 @@ static void handle_splice_funding_error(struct lightningd *ld, cc = splice_command_for_chan(ld, channel); if (cc) { - struct json_stream *response = json_stream_success(cc->cmd); - json_add_string(response, "message", "Splice funding too low"); - json_add_string(response, "error", - tal_fmt(tmpctx, - "%s provided %s but committed to %s.", - opener_error ? "You" : "Peer", - fmt_amount_msat(tmpctx, funding), - fmt_amount_msat(tmpctx, req_funding))); - - was_pending(command_success(cc->cmd, response)); + was_pending(command_fail(cc->cmd, SPLICE_FUNDING_LOW, + "%s provided %s but committed to %s.", + opener_error ? "You" : "Peer", + fmt_amount_msat(tmpctx, funding), + fmt_amount_msat(tmpctx, req_funding))); } - else + else { log_peer_unusual(ld->log, &channel->peer->id, "Splice funding too low. %s provided but %s" " commited to %s", opener_error ? "peer" : "you", fmt_amount_msat(tmpctx, funding), fmt_amount_msat(tmpctx, req_funding)); + } } static void handle_splice_state_error(struct lightningd *ld, @@ -227,13 +223,9 @@ static void handle_splice_state_error(struct lightningd *ld, } cc = splice_command_for_chan(ld, channel); - if (cc) { - struct json_stream *response = json_stream_success(cc->cmd); - json_add_string(response, "message", "Splice state error"); - json_add_string(response, "error", error_msg); - - was_pending(command_success(cc->cmd, response)); - } + if (cc) + was_pending(command_fail(cc->cmd, SPLICE_STATE_ERROR, + "%s", error_msg)); else log_peer_unusual(ld->log, &channel->peer->id, "Splice state error: %s", error_msg); @@ -246,6 +238,7 @@ static void handle_splice_feerate_error(struct lightningd *ld, struct splice_command *cc; struct amount_msat fee; bool too_high; + char *error_msg; if (!fromwire_channeld_splice_feerate_error(msg, &fee, &too_high)) { channel_internal_error(channel, @@ -256,27 +249,25 @@ static void handle_splice_feerate_error(struct lightningd *ld, cc = splice_command_for_chan(ld, channel); if (cc) { - struct json_stream *response = json_stream_success(cc->cmd); - json_add_string(response, "message", "Splice feerate failed"); - if (too_high) - json_add_string(response, "error", - tal_fmt(tmpctx, "Feerate too high. Do you " - "really want to spend %s on fees?", - fmt_amount_msat(tmpctx, fee))); + error_msg = tal_fmt(tmpctx, "Feerate too high. Do you " + "really want to spend %s on fees?", + fmt_amount_msat(tmpctx, fee)); else - json_add_string(response, "error", - tal_fmt(tmpctx, "Feerate too low. Your " - "funding only provided %s in fees", - fmt_amount_msat(tmpctx, fee))); + error_msg = tal_fmt(tmpctx, "Feerate too low. Your " + "funding only provided %s in fees", + fmt_amount_msat(tmpctx, fee)); - was_pending(command_success(cc->cmd, response)); + was_pending(command_fail(cc->cmd, + too_high ? SPLICE_HIGH_FEE : SPLICE_LOW_FEE, + "%s", error_msg)); } - else + else { log_peer_unusual(ld->log, &channel->peer->id, "Peer gave us a" " splice pkg with too low of feerate (fee was" " %s), we rejected it.", fmt_amount_msat(tmpctx, fee)); + } } /* When channeld finishes processing the `splice_init` command, this is called */ @@ -1417,7 +1408,10 @@ bool peer_start_channeld(struct channel *channel, infcopy->outpoint = inflight->funding->outpoint; infcopy->amnt = inflight->funding->total_funds; infcopy->splice_amnt = inflight->funding->splice_amnt; - infcopy->last_tx = tal_dup(infcopy, struct bitcoin_tx, inflight->last_tx); + if (inflight->last_tx) + infcopy->last_tx = tal_dup(infcopy, struct bitcoin_tx, inflight->last_tx); + else + infcopy->last_tx = NULL; infcopy->last_sig = inflight->last_sig; infcopy->i_am_initiator = inflight->i_am_initiator; tal_wally_start(); diff --git a/tests/test_splicing.py b/tests/test_splicing.py index 959e4d4dc614..b964ca67efe5 100644 --- a/tests/test_splicing.py +++ b/tests/test_splicing.py @@ -3,6 +3,7 @@ import pytest import unittest import time +from pyln.client import RpcError @pytest.mark.openchannel('v1') @@ -39,3 +40,146 @@ def test_splice(node_factory, bitcoind): # Check that the splice doesn't generate a unilateral close transaction time.sleep(5) assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_out(node_factory, bitcoind): + l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None}) + + chan_id = l1.get_channel_id(l2) + + funds_result = l1.rpc.newoutput(100000) + + # Pay with fee by subjtracting 5000 from channel balance + result = l1.rpc.splice_init(chan_id, -105000, funds_result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + result = l1.rpc.splice_signed(chan_id, result['psbt']) + + l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + + mempool = bitcoind.rpc.getrawmempool(True) + assert len(list(mempool.keys())) == 1 + assert result['txid'] in list(mempool.keys()) + + bitcoind.generate_block(6, wait_for_mempool=1) + + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + inv = l2.rpc.invoice(10**2, '3', 'no_3') + l1.rpc.pay(inv['bolt11']) + + # Check that the splice doesn't generate a unilateral close transaction + time.sleep(5) + assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_invalid_splice(node_factory, bitcoind): + # Here we do a splice but underfund it purposefully + l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None, + 'may_reconnect': True, + 'allow_warning': True}) + + chan_id = l1.get_channel_id(l2) + + # We claim to add 100000 but in fact add nothing + result = l1.rpc.splice_init(chan_id, 100000) + + with pytest.raises(RpcError) as rpc_error: + result = l1.rpc.splice_update(chan_id, result['psbt']) + + assert rpc_error.value.error["code"] == 357 + assert rpc_error.value.error["message"] == "You provided 1000000000msat but committed to 1100000000msat." + + # The splicing inflight should have been left pending in the DB + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0 + + l1.daemon.wait_for_log(r'Peer has reconnected, state CHANNELD_NORMAL') + + # Startup should have cleared the pending inflight + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0 + + # Now we do a real splice to confirm everything works after restart + funds_result = l1.rpc.fundpsbt("109000sat", "slow", 166, excess_as_change=True) + + result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + result = l1.rpc.signpsbt(result['psbt']) + result = l1.rpc.splice_signed(chan_id, result['signed_psbt']) + + l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + + mempool = bitcoind.rpc.getrawmempool(True) + assert len(list(mempool.keys())) == 1 + assert result['txid'] in list(mempool.keys()) + + bitcoind.generate_block(6, wait_for_mempool=1) + + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + inv = l2.rpc.invoice(10**2, '3', 'no_3') + l1.rpc.pay(inv['bolt11']) + + # Check that the splice doesn't generate a unilateral close transaction + time.sleep(5) + assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 + + +@pytest.mark.openchannel('v1') +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_commit_crash_splice(node_factory, bitcoind): + # Here we do a splice but underfund it purposefully + l1, l2 = node_factory.line_graph(2, fundamount=1000000, wait_for_announce=True, opts={'experimental-splicing': None, + 'may_reconnect': True}) + + chan_id = l1.get_channel_id(l2) + + result = l1.rpc.splice_init(chan_id, -105000, l1.rpc.newoutput(100000)['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + + l1.daemon.wait_for_log(r"Splice initiator: we commit") + + l1.restart() + + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1 + + l1.daemon.wait_for_log(r'Peer has reconnected, state CHANNELD_NORMAL') + + # TODO: Is there a scenario where we want to clear out the inflights? + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1 + + result = l1.rpc.splice_init(chan_id, -105000, l1.rpc.newoutput(100000)['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + result = l1.rpc.splice_signed(chan_id, result['psbt']) + + l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + l1.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE') + + mempool = bitcoind.rpc.getrawmempool(True) + assert len(list(mempool.keys())) == 1 + assert result['txid'] in list(mempool.keys()) + + bitcoind.generate_block(6, wait_for_mempool=1) + + l2.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + l1.daemon.wait_for_log(r'CHANNELD_AWAITING_SPLICE to CHANNELD_NORMAL') + + time.sleep(1) + + assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 0 + + inv = l2.rpc.invoice(10**2, '3', 'no_3') + l1.rpc.pay(inv['bolt11']) + + # Check that the splice doesn't generate a unilateral close transaction + time.sleep(5) + assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0