Skip to content

Commit

Permalink
splice: prevent splice going to onchaind & race prevention
Browse files Browse the repository at this point in the history
Don’t send the funding spend to onchaind if we detect it in inflights (aka. a splice). While we already prevented onchaind_funding_spent from being called directly, the call to wallet_channeltxs_add meant onchaind_funding_spent would be called *anyway* on restart. This is now fixed.

Additionally there was a potential for a race problem depending on the firing order of the channel depth and and funding spent events.

Instead of requiring these events fire in a specific order, we make a special “memory only” inflight object to prevent the race regardless of firing order.

Changelog-Fixed: Splice: bugfix for restart related race condition interacting with adversarial close detection.
  • Loading branch information
ddustin authored Aug 16, 2023
1 parent a9ffa37 commit c67f1f9
Show file tree
Hide file tree
Showing 7 changed files with 61 additions and 11 deletions.
1 change: 1 addition & 0 deletions lightningd/channel.c
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ new_inflight(struct channel *channel,
inflight->lease_amt = lease_amt;

inflight->i_am_initiator = i_am_initiator;
inflight->splice_locked_memonly = false;

list_add_tail(&channel->inflights, &inflight->list);
tal_add_destructor(inflight, destroy_inflight);
Expand Down
10 changes: 10 additions & 0 deletions lightningd/channel.h
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,16 @@ struct channel_inflight {

/* Did I initate this splice attempt? */
bool i_am_initiator;

/* Note: This field is not stored in the database.
*
* After splice_locked, we need a way to stop the chain watchers from
* thinking the old channel was spent.
*
* Leaving the inflight in memory-only with splice_locked true leaves
* moves the responsiblity of cleaning up the inflight to the watcher,
* avoiding any potential race conditions. */
bool splice_locked_memonly;
};

struct open_attempt {
Expand Down
26 changes: 22 additions & 4 deletions lightningd/channel_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -645,8 +645,10 @@ static void handle_update_inflight(struct lightningd *ld,
struct bitcoin_txid,
&txid));

if (last_tx)
inflight->last_tx = tal_steal(inflight, last_tx);
if (last_tx) {
tal_free(inflight->last_tx);
inflight->last_tx = clone_bitcoin_tx(inflight, last_tx);
}

if (last_sig)
inflight->last_sig = *last_sig;
Expand Down Expand Up @@ -825,10 +827,21 @@ static void handle_peer_splice_locked(struct channel *channel, const u8 *msg)
/* Remember that we got the lockin */
wallet_channel_save(channel->peer->ld->wallet, channel);

/* Empty out the inflights */
log_debug(channel->log, "lightningd, splice_locked clearing inflights");

/* Take out the successful inflight from the list temporarily */
list_del(&inflight->list);

wallet_channel_clear_inflights(channel->peer->ld->wallet, channel);

/* Put the successful inflight back in as a memory-only object.
* peer_control's funding_spent function will pick this up and clean up
* our inflight.
*
* This prevents any potential race conditions between us and them. */
inflight->splice_locked_memonly = true;
list_add_tail(&channel->inflights, &inflight->list);

lockin_complete(channel, CHANNELD_AWAITING_SPLICE);
}

Expand Down Expand Up @@ -1394,7 +1407,12 @@ bool peer_start_channeld(struct channel *channel,

inflights = tal_arr(tmpctx, struct inflight *, 0);
list_for_each(&channel->inflights, inflight, list) {
struct inflight *infcopy = tal(inflights, struct inflight);
struct inflight *infcopy;

if (inflight->splice_locked_memonly)
continue;

infcopy = tal(inflights, struct inflight);

infcopy->outpoint = inflight->funding->outpoint;
infcopy->amnt = inflight->funding->total_funds;
Expand Down
14 changes: 11 additions & 3 deletions lightningd/peer_control.c
Original file line number Diff line number Diff line change
Expand Up @@ -1953,18 +1953,26 @@ static enum watch_result funding_spent(struct channel *channel,

bitcoin_txid(tx, &txid);

wallet_channeltxs_add(channel->peer->ld->wallet, channel,
WIRE_ONCHAIND_INIT, &txid, 0, block->height);

/* If we're doing a splice, we expect the funding transaction to be
* spent, so don't freak out and just keep watching in that case */
list_for_each(&channel->inflights, inflight, list) {
if (bitcoin_txid_eq(&txid,
&inflight->funding->outpoint.txid)) {
/* splice_locked is a special flag that indicates this
* is a memory-only inflight acting as a race condition
* safeguard. When we see this, it is our responsability
* to clean up this memory-only inflight. */
if (inflight->splice_locked_memonly) {
tal_free(inflight);
return DELETE_WATCH;
}
return KEEP_WATCHING;
}
}

wallet_channeltxs_add(channel->peer->ld->wallet, channel,
WIRE_ONCHAIND_INIT, &txid, 0, block->height);

return onchaind_funding_spent(channel, tx, block->height);
}

Expand Down
13 changes: 10 additions & 3 deletions lightningd/peer_htlcs.c
Original file line number Diff line number Diff line change
Expand Up @@ -2320,7 +2320,8 @@ void peer_got_commitsig(struct channel *channel, const u8 *msg)

i = 0;
list_for_each(&channel->inflights, inflight, list) {
i++;
if (!inflight->splice_locked_memonly)
i++;
}
if (i != tal_count(inflight_commit_sigs)) {
channel_internal_error(channel, "Got commitsig with incorrect "
Expand Down Expand Up @@ -2381,9 +2382,15 @@ void peer_got_commitsig(struct channel *channel, const u8 *msg)
/* Now append htlc sigs for inflights */
i = 0;
list_for_each(&channel->inflights, inflight, list) {
struct commitsig *commit = inflight_commit_sigs[i];
struct commitsig *commit;

if (inflight->splice_locked_memonly)
continue;

commit = inflight_commit_sigs[i];

inflight->last_tx = tal_steal(inflight, commit->tx);
tal_free(inflight->last_tx);
inflight->last_tx = clone_bitcoin_tx(inflight, commit->tx);
inflight->last_tx->chainparams = chainparams;
inflight->last_sig = commit->commit_signature;
wallet_inflight_save(ld->wallet, inflight);
Expand Down
5 changes: 5 additions & 0 deletions tests/test_splicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from utils import TEST_NETWORK
import pytest
import unittest
import time


@pytest.mark.openchannel('v1')
Expand Down Expand Up @@ -34,3 +35,7 @@ def test_splice(node_factory, bitcoind):

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
3 changes: 2 additions & 1 deletion wallet/wallet.c
Original file line number Diff line number Diff line change
Expand Up @@ -2323,7 +2323,8 @@ void wallet_channel_save(struct wallet *w, struct channel *chan)
/* Update the inflights also */
struct channel_inflight *inflight;
list_for_each(&chan->inflights, inflight, list)
wallet_inflight_save(w, inflight);
if (!inflight->splice_locked_memonly)
wallet_inflight_save(w, inflight);

db_bind_talarr(stmt, last_sent_commit);
db_bind_u64(stmt, chan->dbid);
Expand Down

0 comments on commit c67f1f9

Please sign in to comment.