diff --git a/channeld/channeld.c b/channeld/channeld.c index b84fd0f98dbd..9ff4896f9a90 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -430,7 +430,9 @@ static void check_mutual_splice_locked(struct peer *peer) return; if (short_channel_id_eq(peer->short_channel_ids[LOCAL], - peer->splice_state->short_channel_id)) + peer->splice_state->short_channel_id) + && !short_channel_id_eq(peer->splice_state->short_channel_id, + peer->local_alias)) peer_failed_err(peer->pps, &peer->channel_id, "Duplicate splice_locked events detected" " by scid check"); @@ -510,6 +512,53 @@ static void check_mutual_splice_locked(struct peer *peer) peer->splice_state->remote_locked_txid = tal_free(peer->splice_state->remote_locked_txid); } +static void send_local_splice_locked(struct peer *peer, + struct inflight *inflight, + const struct short_channel_id *scid) +{ + const struct short_channel_id *locked_scid; + bool already_locked = inflight->locked_scid != NULL; + u8 *msg; + + if (already_locked) + locked_scid = inflight->locked_scid; + else + locked_scid = scid ? scid : &peer->local_alias; + + if (!already_locked) { + assert(peer->channel_ready[LOCAL]); + assert(peer->channel_ready[REMOTE]); + + inflight->locked_scid = tal_dup(inflight, struct short_channel_id, + locked_scid); + msg = towire_channeld_update_inflight(NULL, + inflight->psbt, + NULL, + NULL, + inflight->locked_scid, + inflight->i_sent_sigs); + wire_sync_write(MASTER_FD, take(msg)); + } + + peer->splice_state->short_channel_id = *locked_scid; + status_debug("Current channel id is %s, splice_short_channel_id now set to %s", + fmt_short_channel_id(tmpctx, + peer->short_channel_ids[LOCAL]), + fmt_short_channel_id(tmpctx, + peer->splice_state->short_channel_id)); + + peer->splice_state->locked_txid = inflight->outpoint.txid; + + if (!already_locked) { + msg = towire_splice_locked(NULL, &peer->channel_id, + &inflight->outpoint.txid); + peer_write(peer->pps, take(msg)); + } + + peer->splice_state->locked_ready[LOCAL] = true; + check_mutual_splice_locked(peer); +} + static void implied_peer_splice_locked(struct peer *peer, struct bitcoin_txid splice_txid) { @@ -570,6 +619,44 @@ static void handle_peer_splice_locked(struct peer *peer, const u8 *msg) implied_peer_splice_locked(peer, splice_txid); } +static void maybe_handle_cached_splice_locked(struct peer *peer, + const struct inflight *inflight) +{ + const u8 *msg; + + if (!peer->splicing || !peer->splicing->splice_locked_msg) + return; + + if (is_stfu_active(peer)) + return; + + if (!inflight || !inflight->i_sent_sigs) + return; + + msg = tal_steal(tmpctx, peer->splicing->splice_locked_msg); + peer->splicing->splice_locked_msg = NULL; + handle_peer_splice_locked(peer, msg); +} + +static bool maybe_cache_splice_locked(struct peer *peer, const u8 *msg) +{ + if (fromwire_peektype(msg) != WIRE_SPLICE_LOCKED + || !peer->splicing + || peer->channel->minimum_depth != 0) + return false; + + if (peer->splicing->splice_locked_msg) + peer_failed_warn(peer->pps, &peer->channel_id, + "Received SPLICE_LOCKED while we already" + " have one cached"); + + /* Zero-conf peers can send splice_locked while we're still resuming + * the splice negotiation, so defer it until the local signature path + * is complete and mutual splice_locked can safely clear inflights. */ + peer->splicing->splice_locked_msg = tal_steal(peer->splicing, msg); + return true; +} + static void handle_peer_channel_ready(struct peer *peer, const u8 *msg) { struct channel_id chanid; @@ -3783,6 +3870,9 @@ static void resume_splice_negotiation(struct peer *peer, if (peer->splicing) { inws = peer->splicing->inws; their_sig = peer->splicing->their_sig; + + if (peer->splicing->tx_sig_msg) + recv_signature = true; } else { inws = NULL; @@ -3908,6 +3998,9 @@ static void resume_splice_negotiation(struct peer *peer, wire_sync_write(MASTER_FD, take(msg)); peer_write(peer->pps, sigmsg); + + if (peer->channel->minimum_depth == 0) + send_local_splice_locked(peer, inflight, NULL); } their_pubkey = &peer->channel->funding_pubkey[REMOTE]; @@ -3937,13 +4030,27 @@ static void resume_splice_negotiation(struct peer *peer, check_tx_abort(peer, msg, &inflight->outpoint.txid); - if (handle_peer_error_or_warning(peer->pps, msg)) - return; + if (handle_peer_error_or_warning(peer->pps, msg)) + return; - if (type != WIRE_TX_SIGNATURES) - peer_failed_warn(peer->pps, &peer->channel_id, - "Splicing got incorrect message from" - " peer: %s (should be" + if (type == WIRE_CHANNEL_READY + && peer->channel->minimum_depth == 0 + && peer->splice_state->locked_ready[LOCAL]) { + if (is_stfu_active(peer)) + end_stfu_mode(peer); + + handle_peer_channel_ready(peer, msg); + implied_peer_splice_locked(peer, + inflight->outpoint.txid); + if (!tal_count(peer->splice_state->inflights)) + peer->splicing = tal_free(peer->splicing); + return; + } + + if (type != WIRE_TX_SIGNATURES) + peer_failed_warn(peer->pps, &peer->channel_id, + "Splicing got incorrect message from" + " peer: %s (should be" " WIRE_TX_SIGNATURES)", peer_wire_name(type)); @@ -3971,7 +4078,7 @@ static void resume_splice_negotiation(struct peer *peer, * - MUST consider splice negotiation complete. * - MUST consider the connection no longer quiescent. */ - if (send_signature) + if (is_stfu_active(peer)) end_stfu_mode(peer); their_sig = tal(tmpctx, struct bitcoin_signature); @@ -4092,14 +4199,15 @@ static void resume_splice_negotiation(struct peer *peer, peer_write(peer->pps, sigmsg); status_debug("Splice: we signed second"); + + if (peer->channel->minimum_depth == 0) + send_local_splice_locked(peer, inflight, NULL); } if (send_signature) { if (!recv_signature) end_stfu_mode(peer); - peer->splicing = tal_free(peer->splicing); - if (our_role == TX_INITIATOR) calc_weight(TX_INITIATOR, current_psbt, true); @@ -4110,6 +4218,11 @@ static void resume_splice_negotiation(struct peer *peer, } audit_psbt(current_psbt, current_psbt); + + maybe_handle_cached_splice_locked(peer, inflight); + + if (send_signature) + peer->splicing = tal_free(peer->splicing); } static struct inflight *inflights_new(struct peer *peer) @@ -4224,6 +4337,16 @@ static void splice_accepter(struct peer *peer, const u8 *inmsg) } } else if (type == WIRE_TX_INIT_RBF) { + /* BOLT #2: + * The sender: + * - MUST NOT send `tx_init_rbf` if `option_zeroconf` + * has been negotiated. + */ + if (channel_type_has(peer->channel->type, OPT_ZEROCONF)) + peer_failed_warn(peer->pps, &peer->channel_id, + "Peer sent tx_init_rbf but channel" + " uses option_zeroconf"); + if (!fromwire_tx_init_rbf(tmpctx, inmsg, &channel_id, &locktime, @@ -4899,9 +5022,6 @@ static void splice_initiator_user_signed(struct peer *peer, const u8 *inmsg) assert(tal_parent(inflight->psbt) != tmpctx); resume_splice_negotiation(peer, false, false, true, sign_first, 0); - - audit_psbt(inflight->psbt, inflight->psbt); - assert(tal_parent(inflight->psbt) != tmpctx); } /* This occurs once our 'stfu' transition was successful. */ @@ -5001,6 +5121,15 @@ static void handle_splice_init(struct peer *peer, const u8 *inmsg) wire_sync_write(MASTER_FD, take(msg)); return; } + if (last_inflight(peer) + && channel_type_has(peer->channel->type, OPT_ZEROCONF)) { + msg = towire_channeld_splice_state_error(NULL, + "Cannot RBF splice:" + " channel uses" + " option_zeroconf"); + wire_sync_write(MASTER_FD, take(msg)); + return; + } status_debug("Getting handle_splice_init psbt version %d (RBF?: %s)", peer->splicing->current_psbt->version, @@ -5090,11 +5219,13 @@ static void peer_in(struct peer *peer, const u8 *msg) if (peer->splicing && type == WIRE_TX_SIGNATURES) { if (peer->splicing->tx_sig_msg) peer_failed_warn(peer->pps, &peer->channel_id, - "Received TX_SIGNATURES while" - " we already have one cached"); + "Received TX_SIGNATURES while" + " we already have one cached"); peer->splicing->tx_sig_msg = tal_steal(peer->splicing, msg); return; + } else if (maybe_cache_splice_locked(peer, msg)) { + return; } else if (type != WIRE_ANNOUNCEMENT_SIGNATURES) { peer_failed_warn(peer->pps, &peer->channel_id, "Received message %s when only TX_ABORT was" @@ -5127,6 +5258,9 @@ static void peer_in(struct peer *peer, const u8 *msg) peer->stfu_wait_single_msg = false; + if (maybe_cache_splice_locked(peer, msg)) + return; + switch (type) { case WIRE_CHANNEL_READY: handle_peer_channel_ready(peer, msg); @@ -6206,10 +6340,7 @@ static void handle_funding_depth(struct peer *peer, const u8 *msg) if (depth < peer->channel->minimum_depth) return; - assert(peer->channel_ready[LOCAL]); - assert(peer->channel_ready[REMOTE]); - - if(!peer->splice_state->locked_ready[LOCAL]) { + if (!peer->splice_state->locked_ready[LOCAL]) { assert(scid); inflight_match = NULL; @@ -6228,16 +6359,6 @@ static void handle_funding_depth(struct peer *peer, const u8 *msg) fmt_bitcoin_txid(tmpctx, &txid)); assert(scid); assert(inflight->psbt); - inflight->locked_scid = tal_dup(inflight, - struct short_channel_id, - scid); - msg = towire_channeld_update_inflight(NULL, - inflight->psbt, - NULL, - NULL, - inflight->locked_scid, - inflight->i_sent_sigs); - wire_sync_write(MASTER_FD, take(msg)); inflight_match = inflight; } } @@ -6250,25 +6371,7 @@ static void handle_funding_depth(struct peer *peer, const u8 *msg) return; } - /* For splicing we only update the short channel id on mutual - * splice lock */ - peer->splice_state->short_channel_id = *scid; - status_debug("Current channel id is %s, " - "splice_short_channel_id now set to %s", - fmt_short_channel_id(tmpctx, - peer->short_channel_ids[LOCAL]), - fmt_short_channel_id(tmpctx, - peer->splice_state->short_channel_id)); - - peer->splice_state->locked_txid = txid; - - msg = towire_splice_locked(NULL, &peer->channel_id, - &txid); - - peer_write(peer->pps, take(msg)); - - peer->splice_state->locked_ready[LOCAL] = true; - check_mutual_splice_locked(peer); + send_local_splice_locked(peer, inflight_match, scid); } return; diff --git a/channeld/splice.c b/channeld/splice.c index e5356d01af23..dda6aea30a02 100644 --- a/channeld/splice.c +++ b/channeld/splice.c @@ -30,6 +30,7 @@ struct splicing *splicing_new(const tal_t *ctx) splicing->received_tx_complete = false; splicing->sent_tx_complete = false; splicing->tx_sig_msg = NULL; + splicing->splice_locked_msg = NULL; splicing->inws = NULL; splicing->their_sig = NULL; diff --git a/channeld/splice.h b/channeld/splice.h index 47050aee041b..e3c150792bab 100644 --- a/channeld/splice.h +++ b/channeld/splice.h @@ -55,6 +55,8 @@ struct splicing { bool sent_tx_complete; /* If our peer signs early, we allow that and cache it here */ const u8 *tx_sig_msg; + /* Zero-conf peers may send splice_locked before we can process it. */ + const u8 *splice_locked_msg; /* The witness stack data received by peer */ struct witness **inws; /* Their channel funding signature */ diff --git a/lightningd/channel.c b/lightningd/channel.c index b15fdb2c2672..563cc52372c2 100644 --- a/lightningd/channel.c +++ b/lightningd/channel.c @@ -357,6 +357,7 @@ struct channel *new_unsaved_channel(struct peer *peer, channel->state = DUALOPEND_OPEN_INIT; channel->owner = NULL; channel->reestablished = false; + channel->minimum_depth = ld->config.funding_confirms; memset(&channel->billboard, 0, sizeof(channel->billboard)); channel->billboard.transient = tal_fmt(channel, "%s", "Empty channel init'd"); @@ -1309,4 +1310,3 @@ const u8 *channel_update_for_error(const tal_t *ctx, /* FIXME: Call directly from callers */ return channel_gossip_update_for_error(ctx, channel); } - diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index 9735e72a6699..bac1b8c2623c 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -495,6 +495,16 @@ static void handle_splice_lookup_tx(struct lightningd *ld, } tx = wallet_transaction_get(tmpctx, ld->wallet, &txid); + if (!tx + && channel->funding_psbt + && bitcoin_txid_eq(&channel->funding.txid, &txid)) { + /* Zeroconf funding can be used for splicing before topology + * has indexed the funding tx, so reconstruct it from the + * persisted funding PSBT if needed. */ + tx = bitcoin_tx_with_psbt(tmpctx, + clone_psbt(tmpctx, + channel->funding_psbt)); + } if (!tx) { channel_internal_error(channel, @@ -699,6 +709,13 @@ static enum watch_result splice_depth_cb(struct lightningd *ld, return DELETE_WATCH; } + /* Reorged out? OK, we're not committed yet. + * Zero-conf splice_locked is handled directly in channeld once + * tx_signatures have completed. */ + if (depth == 0) { + return KEEP_WATCHING; + } + if (inflight->channel->owner) { log_debug(inflight->channel->log, "splice_depth_cb: sending funding depth scid: %s", fmt_short_channel_id(tmpctx, *inflight->scid)); @@ -986,7 +1003,9 @@ static void handle_update_inflight(struct lightningd *ld, if (last_sig) inflight->last_sig = *last_sig; - inflight->locked_scid = tal_steal(inflight, locked_scid); + inflight->locked_scid = tal_free(inflight->locked_scid); + if (locked_scid) + inflight->locked_scid = tal_steal(inflight, locked_scid); inflight->i_sent_sigs = i_sent_sigs; tal_wally_start(); @@ -1205,8 +1224,10 @@ static void handle_peer_splice_locked(struct channel *channel, const u8 *msg) wallet_channel_clear_inflights(channel->peer->ld->wallet, channel); - /* Update the scid and tell everyone */ - change_scid(channel, *inflight->locked_scid); + /* A zero-conf splice can lock before the new funding tx has a real scid. */ + if (channel->scid + && !short_channel_id_eq(*channel->scid, *inflight->locked_scid)) + change_scid(channel, *inflight->locked_scid); /* That freed watchers in inflights: now watch funding tx */ channel_watch_funding(channel->peer->ld, channel); diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index f5f63b7b827a..4b2c09508ef7 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -710,6 +710,7 @@ openchannel2_hook_cb(struct openchannel2_payload *payload STEALS) payload->psbt, payload->our_shutdown_scriptpubkey, our_shutdown_script_wallet_index, + channel->minimum_depth, payload->rates); subd_send_msg(dualopend, take(msg)); @@ -732,6 +733,7 @@ openchannel2_hook_deserialize(struct openchannel2_payload *payload, } const jsmntok_t *t_result = json_get_member(buffer, toks, "result"); + const jsmntok_t *t_mindepth = json_get_member(buffer, toks, "mindepth"); if (!t_result) fatal("Plugin returned an invalid response to the" " openchannel2 hook: %.*s", @@ -780,6 +782,15 @@ openchannel2_hook_deserialize(struct openchannel2_payload *payload, else payload->our_shutdown_scriptpubkey = shutdown_script; + if (t_mindepth != NULL) { + json_to_u32(buffer, t_mindepth, + &payload->channel->minimum_depth); + log_debug(dualopend->ld->log, + "Setting mindepth=%u for this channel as requested by " + "the openchannel2 hook", + payload->channel->minimum_depth); + } + struct amount_msat fee_base, fee_max_base; /* deserialized may be called multiple times */ @@ -1769,6 +1780,16 @@ static void send_funding_tx(struct channel *channel, sendfunding_done, cs); } +static void maybe_signal_zeroconf_lockin(struct channel *channel) +{ + if (channel->minimum_depth != 0) + return; + + log_debug(channel->log, "Zero-conf funding: signaling immediate lock-in"); + subd_send_msg(channel->owner, + take(towire_dualopend_depth_reached(NULL, 0))); +} + static void handle_peer_tx_sigs_sent(struct subd *dualopend, const int *fds, const u8 *msg) @@ -1847,6 +1868,7 @@ static void handle_peer_tx_sigs_sent(struct subd *dualopend, DUALOPEND_AWAITING_LOCKIN, REASON_UNKNOWN, "Sigs exchanged, waiting for lock-in"); + maybe_signal_zeroconf_lockin(channel); /* Mimic the old behavior, notify a channel has been opened, * for the accepter side */ @@ -1933,6 +1955,7 @@ static void handle_channel_locked(struct subd *dualopend, const u8 *msg) { struct channel *channel = dualopend->channel; + struct channel_inflight *inflight; struct peer_fd *peer_fd; if (!fromwire_dualopend_channel_locked(msg)) { @@ -1943,7 +1966,6 @@ static void handle_channel_locked(struct subd *dualopend, } peer_fd = new_peer_fd_arr(tmpctx, fds); - assert(channel->scid); assert(channel->remote_channel_ready); log_debug(channel->log, "Lockin complete state %s", @@ -1958,15 +1980,26 @@ static void handle_channel_locked(struct subd *dualopend, CHANNELD_NORMAL, REASON_UNKNOWN, "Lockin complete"); - channel_record_open(channel, - short_channel_id_blocknum(*channel->scid), - true); + lockin_has_completed(channel, true); + + inflight = channel_current_inflight(channel); + if (inflight && inflight->funding_psbt) { + tal_free(channel->funding_psbt); + channel->funding_psbt = clone_psbt(channel, + inflight->funding_psbt); + wallet_channel_save(dualopend->ld->wallet, channel); + } /* Empty out the inflights */ wallet_channel_clear_inflights(dualopend->ld->wallet, channel); - /* That freed watchers in inflights: now watch funding tx */ - channel_watch_depth(dualopend->ld, short_channel_id_blocknum(*channel->scid), channel); + /* Zeroconf channels still need to discover their on-chain scid later. */ + if (channel->scid) + channel_watch_depth(dualopend->ld, + short_channel_id_blocknum(*channel->scid), + channel); + else + channel_watch_funding(dualopend->ld, channel); channel_watch_funding_out(dualopend->ld, channel); /* FIXME: LND sigs/update_fee msgs? */ @@ -2193,6 +2226,7 @@ static void handle_peer_tx_sigs_msg(struct subd *dualopend, DUALOPEND_AWAITING_LOCKIN, REASON_UNKNOWN, "Sigs exchanged, waiting for lock-in"); + maybe_signal_zeroconf_lockin(channel); /* Mimic the old behavior, notify a channel has been opened, * for the accepter side */ @@ -3030,6 +3064,7 @@ static struct command_result *openchannel_init(struct command *cmd, const struct wally_psbt *psbt, u32 feerate_per_kw_funding, u32 feerate_per_kw, + u32 minimum_depth, const u8 *our_upfront_shutdown_script, bool announce_channel, const struct lease_rates *rates, @@ -3059,6 +3094,7 @@ static struct command_result *openchannel_init(struct command *cmd, channel->opener = LOCAL; channel->open_attempt = oa = new_channel_open_attempt(channel); channel->channel_flags = OUR_CHANNEL_FLAGS; + channel->minimum_depth = minimum_depth; oa->funding = amount; oa->cmd = cmd; @@ -3124,6 +3160,7 @@ struct openchannel_init_info { struct node_id *id; struct amount_sat *amount, *request_amt; struct wally_psbt *psbt; + u32 *mindepth; u32 *feerate_per_kw_funding, *feerate_per_kw; const u8 *our_upfront_shutdown_script; bool *announce_channel; @@ -3157,6 +3194,7 @@ static void openchannel_init_after_sync(struct chain_topology *topo, *info->request_amt, info->psbt, *info->feerate_per_kw_funding, *info->feerate_per_kw, + *info->mindepth, info->our_upfront_shutdown_script, *info->announce_channel, info->rates, info->ctype); @@ -3179,6 +3217,7 @@ static struct command_result *json_openchannel_init(struct command *cmd, p_req("initialpsbt", param_psbt, &info->psbt), p_opt("commitment_feerate", param_feerate, &info->feerate_per_kw), p_opt("funding_feerate", param_feerate, &info->feerate_per_kw_funding), + p_opt("mindepth", param_u32, &info->mindepth), p_opt_def("announce", param_bool, &info->announce_channel, true), p_opt("close_to", param_bitcoin_address, &info->our_upfront_shutdown_script), p_opt_def("request_amt", param_sat, &info->request_amt, AMOUNT_SAT(0)), @@ -3237,6 +3276,30 @@ static struct command_result *json_openchannel_init(struct command *cmd, info->ctype = desired_channel_type(info, cmd->ld->our_features, peer->their_features); + if (channel_type_has(info->ctype, OPT_ZEROCONF)) { + if (info->mindepth && *info->mindepth != 0) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot set non-zero mindepth for zero-conf channel_type"); + } + if (!info->mindepth) { + info->mindepth = tal(info, u32); + *info->mindepth = 0; + } + } else if (info->mindepth && *info->mindepth == 0) { + if (!feature_negotiated(cmd->ld->our_features, + peer->their_features, + OPT_ZEROCONF)) { + return command_fail(cmd, JSONRPC2_INVALID_PARAMS, + "Cannot set mindepth=0: peer does not support zero-conf channels"); + } + info->ctype = channel_type_dup(info, info->ctype); + channel_type_set_zeroconf(info->ctype); + } + + if (!info->mindepth) + info->mindepth = tal_dup(info, u32, + &cmd->ld->config.funding_confirms); + if (!cmd->ld->dev_any_channel_type && !channel_type_accept(tmpctx, info->ctype->features, @@ -3293,6 +3356,7 @@ static struct command_result *json_openchannel_init(struct command *cmd, *info->request_amt, info->psbt, *info->feerate_per_kw_funding, *info->feerate_per_kw, + *info->mindepth, info->our_upfront_shutdown_script, *info->announce_channel, info->rates, info->ctype); @@ -4171,9 +4235,6 @@ bool peer_start_dualopend(struct peer *peer, * considers reasonable to avoid double-spending of the * funding transaction. */ - /* FIXME: We should override this to 0 in the openchannel2 hook of we want zeroconf*/ - channel->minimum_depth = peer->ld->config.funding_confirms; - msg = towire_dualopend_init(NULL, chainparams, peer->ld->our_features, peer->their_features, diff --git a/lightningd/peer_control.c b/lightningd/peer_control.c index ddcdb27e9dcf..cf8c2d02b6af 100644 --- a/lightningd/peer_control.c +++ b/lightningd/peer_control.c @@ -2287,6 +2287,9 @@ void update_channel_from_inflight(struct lightningd *ld, channel->lease_chan_max_msat = inflight->lease_chan_max_msat; channel->lease_chan_max_ppt = inflight->lease_chan_max_ppt; + tal_free(channel->funding_psbt); + channel->funding_psbt = clone_psbt(channel, inflight->funding_psbt); + tal_free(channel->blockheight_states); channel->blockheight_states = new_height_states(channel, channel->opener, diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 733b0c2828bf..70c06c402bec 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -2548,6 +2548,7 @@ static void accepter_start(struct state *state, const u8 *oc2_msg) &tx_state->psbt, &state->upfront_shutdown_script[LOCAL], &state->local_upfront_shutdown_wallet_index, + &state->minimum_depth, &tx_state->rates)) master_badmsg(WIRE_DUALOPEND_GOT_OFFER_REPLY, msg); diff --git a/openingd/dualopend_wire.csv b/openingd/dualopend_wire.csv index d6b157495437..40f2c10f4eb3 100644 --- a/openingd/dualopend_wire.csv +++ b/openingd/dualopend_wire.csv @@ -108,6 +108,7 @@ msgdata,dualopend_got_offer_reply,psbt,wally_psbt, msgdata,dualopend_got_offer_reply,shutdown_len,u16, msgdata,dualopend_got_offer_reply,our_shutdown_scriptpubkey,?u8,shutdown_len msgdata,dualopend_got_offer_reply,our_shutdown_wallet_index,?u32, +msgdata,dualopend_got_offer_reply,mindepth,u32, # must go last because of embedded tu32 msgdata,dualopend_got_offer_reply,lease_rates,?lease_rates, diff --git a/plugins/spender/openchannel.c b/plugins/spender/openchannel.c index 432963d41ab6..f6665b4a31a4 100644 --- a/plugins/spender/openchannel.c +++ b/plugins/spender/openchannel.c @@ -1009,6 +1009,8 @@ openchannel_init_dest(struct multifundchannel_destination *dest) mfc->feerate_str); } json_add_bool(req->js, "announce", dest->announce); + if (dest->mindepth) + json_add_u32(req->js, "mindepth", *dest->mindepth); if (dest->close_to_str) json_add_string(req->js, "close_to", dest->close_to_str); diff --git a/tests/plugins/zeroconf-selective.py b/tests/plugins/zeroconf-selective.py index 527e12282ca9..efe7872a6a3b 100755 --- a/tests/plugins/zeroconf-selective.py +++ b/tests/plugins/zeroconf-selective.py @@ -9,7 +9,6 @@ @plugin.hook('openchannel') def on_openchannel(openchannel, plugin, **kwargs): - plugin.log(repr(openchannel)) mindepth = int(plugin.options['zeroconf_mindepth']['value']) if openchannel['id'] == plugin.options['zeroconf_allow']['value'] or plugin.options['zeroconf_allow']['value'] == 'any': @@ -19,6 +18,11 @@ def on_openchannel(openchannel, plugin, **kwargs): return {'result': 'continue'} +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + return on_openchannel(openchannel2, plugin, **kwargs) + + plugin.add_option( 'zeroconf_allow', '03864ef025fde8fb587d989186ce6a4a186895ee44a926bfc370e2c366597a3f8f', diff --git a/tests/test_gossip.py b/tests/test_gossip.py index 30f94174ded9..846dead0601f 100644 --- a/tests/test_gossip.py +++ b/tests/test_gossip.py @@ -2401,9 +2401,14 @@ def test_gossip_force_broadcast_channel_msgs(node_factory, bitcoind): del tally['query_channel_range'] del tally['ping'] del tally['gossip_filter'] - assert tally == {'channel_announce': 1, - 'channel_update': 1, - 'node_announce': 1} + # We can see l2 replay the shared channel_announcement while the final + # announcement is propagating. Allow a single duplicate. + assert tally in ({'channel_announce': 1, + 'channel_update': 1, + 'node_announce': 1}, + {'channel_announce': 2, + 'channel_update': 1, + 'node_announce': 1}) # Make sure l1 sees l2's channel update wait_for(lambda: len(l1.rpc.listchannels()['channels']) == 2) diff --git a/tests/test_splice.py b/tests/test_splice.py index 020408075b39..d70b9c898740 100644 --- a/tests/test_splice.py +++ b/tests/test_splice.py @@ -719,3 +719,70 @@ def test_easy_splice_out_into_channel(node_factory, bitcoind, chainparams): end_chan1_balance = Millisatoshi(bkpr_account_balance(l2, chan1)) assert initial_chan1_balance + Millisatoshi(spliceamt * 1000) == end_chan1_balance + + +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_zeroconf(node_factory, bitcoind): + """Test that splicing on a zero-conf channel locks immediately. + + Per BOLT #2 (bolts#1160), when option_zeroconf is negotiated: + - nodes SHOULD send splice_locked immediately after tx_signatures + - nodes MUST NOT send tx_init_rbf + """ + zeroconf_plugin = Path(__file__).parent / "plugins" / "zeroconf-selective.py" + fundamt = 1000000 + + l1, l2 = node_factory.get_nodes(2, opts=[ + { + 'experimental-splicing': None, + 'plugin': str(zeroconf_plugin), + 'zeroconf_allow': 'any', + }, + { + 'experimental-splicing': None, + 'plugin': str(zeroconf_plugin), + 'zeroconf_allow': 'any', + }, + ]) + + # Open a zero-conf channel + l1.fundwallet(10**7) + l1.fundwallet(10**7) + l1.connect(l2) + l1.rpc.fundchannel(l2.info['id'], fundamt, mindepth=0) + + # Wait for both sides to be in CHANNELD_NORMAL (zero-conf, no blocks needed) + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_READY') + l2.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_READY') + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + wait_for(lambda: only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + + # Confirm that this is indeed a zero-conf channel + chan = only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels']) + assert 'option_zeroconf' in chan['features'] + assert 'short_channel_id' not in chan + + # Now splice in 100k sats + spliceamt = 100000 + l1.rpc.splicein("*:?", f"{spliceamt}") + + l1.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights') + l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights') + + # Verify the splice completed immediately: no more inflights, balances updated + p1 = only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels']) + p2 = only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels']) + assert p1['to_us_msat'] == (fundamt + spliceamt) * 1000 + assert p1['total_msat'] == (fundamt + spliceamt) * 1000 + assert 'inflight' not in p1 + assert 'inflight' not in p2 + + # Verify we can still use the channel after splice + inv = l2.rpc.invoice(10000, 'test-post-splice', 'test')['bolt11'] + l1.rpc.pay(inv) + + # The actual funding scid should still update once the splice confirms. + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: 'short_channel_id' in + only_one(l1.rpc.listpeerchannels(peer_id=l2.info['id'])['channels'])) diff --git a/tests/test_splicing_disconnect.py b/tests/test_splicing_disconnect.py index faa3f8f4bf5f..9c43233f4fc5 100644 --- a/tests/test_splicing_disconnect.py +++ b/tests/test_splicing_disconnect.py @@ -1,12 +1,56 @@ from fixtures import * # noqa: F401,F403 +from pathlib import Path import pytest import unittest import time from pyln.testing.utils import EXPERIMENTAL_DUAL_FUND from utils import ( - TEST_NETWORK + TEST_NETWORK, only_one, wait_for ) +def open_zeroconf_splice_channel(node_factory, + l1_options=None, + l2_options=None, + l1_disconnect=None, + l2_disconnect=None, + l1_may_reconnect=False, + l2_may_reconnect=False): + zeroconf_plugin = Path(__file__).parent / "plugins" / "zeroconf-selective.py" + base_opts = { + 'experimental-splicing': None, + 'plugin': str(zeroconf_plugin), + 'zeroconf_allow': 'any', + } + + l1_opts = dict(base_opts) + l2_opts = dict(base_opts) + if l1_options: + l1_opts.update(l1_options) + if l2_options: + l2_opts.update(l2_options) + + l1_kwargs = {'options': l1_opts, 'may_reconnect': l1_may_reconnect} + l2_kwargs = {'options': l2_opts, 'may_reconnect': l2_may_reconnect} + if l1_disconnect: + l1_kwargs['disconnect'] = l1_disconnect + if l2_disconnect: + l2_kwargs['disconnect'] = l2_disconnect + + l1 = node_factory.get_node(**l1_kwargs) + l2 = node_factory.get_node(**l2_kwargs) + + l1.fundwallet(10**7) + l1.fundwallet(10**7) + l1.connect(l2) + l1.rpc.fundchannel(l2.info['id'], 1000000, mindepth=0) + + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_READY') + l2.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_READY') + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + wait_for(lambda: only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + + return l1, l2 + @pytest.mark.openchannel('v1') @pytest.mark.openchannel('v2') @@ -66,6 +110,56 @@ def test_splice_disconnect_sig(node_factory, bitcoind): assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 +@pytest.mark.openchannel('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_disconnect_sig_zeroconf(node_factory): + disconnect = ['-WIRE_TX_SIGNATURES'] + if EXPERIMENTAL_DUAL_FUND: + disconnect = ['=WIRE_TX_SIGNATURES'] + disconnect + + l1, l2 = open_zeroconf_splice_channel(node_factory, + l1_options={'dev-no-reconnect': None}, + l1_disconnect=disconnect, + l1_may_reconnect=True, + l2_may_reconnect=True) + + chan_id = l1.get_channel_id(l2) + + funds_result = l1.rpc.fundpsbt("105790sat", 0, 0, excess_as_change=True) + + result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + assert(result['commitments_secured'] is False) + result = l1.rpc.splice_update(chan_id, result['psbt']) + assert(result['commitments_secured'] is True) + result = l1.rpc.signpsbt(result['psbt']) + l1.rpc.splice_signed(chan_id, result['signed_psbt']) + + l1.daemon.wait_for_log(r'dev_disconnect: \-WIRE_TX_SIGNATURES') + time.sleep(.2) + + print("Killing l1 without sending WIRE_TX_SIGNATURES") + l1.daemon.kill() + + del l1.daemon.opts['dev-no-reconnect'] + del l1.daemon.opts['dev-disconnect'] + + l1.start() + + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') + l2.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_REESTABLISH') + l1.daemon.wait_for_log(r'peer_in WIRE_CHANNEL_READY') + l2.daemon.wait_for_log(r'lightningd, splice_locked clearing inflights') + wait_for(lambda: only_one(l1.rpc.listpeerchannels(l2.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + wait_for(lambda: only_one(l2.rpc.listpeerchannels(l1.info['id'])['channels'])['state'] == 'CHANNELD_NORMAL') + + inv = l2.rpc.invoice(10**2, '3', 'no_3') + l1.rpc.pay(inv['bolt11']) + + 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') @@ -114,3 +208,45 @@ def test_splice_disconnect_commit(node_factory, bitcoind, executor): # 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('v2') +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +def test_splice_disconnect_commit_zeroconf(node_factory, executor): + if EXPERIMENTAL_DUAL_FUND: + disconnects = ['+WIRE_COMMITMENT_SIGNED*2'] + else: + disconnects = ['+WIRE_COMMITMENT_SIGNED'] + + l1, l2 = open_zeroconf_splice_channel(node_factory, + l1_options={'dev-no-reconnect': None}, + l2_options={'dev-no-reconnect': None}, + l2_disconnect=disconnects, + l1_may_reconnect=True, + l2_may_reconnect=True) + + chan_id = l1.get_channel_id(l2) + + funds_result = l1.rpc.fundpsbt("105790sat", 0, 0, excess_as_change=True) + + result = l1.rpc.splice_init(chan_id, 100000, funds_result['psbt']) + result = l1.rpc.splice_update(chan_id, result['psbt']) + assert(result['commitments_secured'] is False) + + executor.submit(l1.rpc.splice_update, chan_id, result['psbt']) + + print("l2 waiting for dev_disconnect msg") + + l2.daemon.wait_for_log(r'dev_disconnect: \+WIRE_COMMITMENT_SIGNED') + + l1.daemon.kill() + + del l1.daemon.opts['dev-no-reconnect'] + + l1.start() + + l1.daemon.wait_for_log(r'billboard: Channel ready for use.') + l2.daemon.wait_for_log(r'billboard: Channel ready for use.') + + time.sleep(5) + assert l1.db_query("SELECT count(*) as c FROM channeltxs;")[0]['c'] == 0 diff --git a/wallet/wallet.c b/wallet/wallet.c index d2a3e215b3a5..7a3e5efa28f3 100644 --- a/wallet/wallet.c +++ b/wallet/wallet.c @@ -1591,7 +1591,8 @@ void wallet_inflight_save(struct wallet *w, struct db_stmt *stmt; /* The *only* thing you can update on an * inflight is the funding PSBT (to add sigs) - * and the last_tx/last_sig or locked_scid if this is for a splice */ + * and the last_tx/last_sig, locked_scid, or i_sent_sigs if this is + * for a splice */ stmt = db_prepare_v2(w->db, SQL("UPDATE channel_funding_inflights SET" " funding_psbt=?" @@ -1599,6 +1600,7 @@ void wallet_inflight_save(struct wallet *w, ", last_tx=?" ", last_sig=?" ", locked_scid=?" + ", i_sent_sigs=?" " WHERE" " channel_id=?" " AND funding_tx_id=?" @@ -1616,6 +1618,7 @@ void wallet_inflight_save(struct wallet *w, db_bind_short_channel_id(stmt, *inflight->locked_scid); else db_bind_null(stmt); + db_bind_int(stmt, inflight->i_sent_sigs); db_bind_u64(stmt, inflight->channel->dbid); db_bind_txid(stmt, &inflight->funding->outpoint.txid); db_bind_int(stmt, inflight->funding->outpoint.n);