Skip to content
Merged
21 changes: 16 additions & 5 deletions channeld/channeld.c
Original file line number Diff line number Diff line change
Expand Up @@ -5951,19 +5951,30 @@ static void peer_reconnect(struct peer *peer,
"next_funding_txid not recognized.");
}

/* "none of those channel_reestablish messages contain
Comment thread
nGoline marked this conversation as resolved.
* my_current_funding_locked or next_funding for a splice transaction" */
bool is_splice_active = local_next_funding
|| peer->splice_state->locked_ready[LOCAL]
|| remote_next_funding
|| (recv_tlvs
&& recv_tlvs->my_current_funding_locked
&& !bitcoin_txid_eq(

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't look correct. Where does it say to check if the txid matches in the spec?

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I did some deep diving on how to satisfy the "is splice transaction" requirement and it looks like we need to store some meta data about the inflight in the DB. Comparing against the txid is incorrect.

I asked tbast what they're doing and they're storing a funding_tx_index value to track this which makes some sense. We might want or need that for 0-conf splices down the road anyway.

The idea is funding_tx_index would be 0 for the opening funding (including RBF attempts) and then increment as we do more splices.

Another issue here is logic needs to follow below the BOLT spec reference which it did before this. This code should be moved back to where it was (below the BOLT spec reference), something like funding_tx_index implemented on the inflight, and then a check for funding_tx_index > 0 added to the condition.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I Hey @ddustin! You're right that the txid comparison is wrong, and I want to clarify why I touched channeld.c at all.

BOLT #2 says:

if next_commitment_number is 1 in both the channel_reestablish it sent and received, and none of those channel_reestablish messages contain my_current_funding_locked or next_funding for a splice transaction:

  • MUST retransmit channel_ready

The old code had no check for my_current_funding_locked, so it would retransmit channel_ready even mid-splice. The change was meant to implement that suppression condition.

Where I went wrong was the "for a splice transaction" qualifier. I compared the my_current_funding_locked txid against peer->channel->funding.txid, reasoning that a mismatch means it's a splice txid, but that breaks as soon as a splice completes, because funding.txid gets updated to the splice txid. At that point both sides agree on the same txid, the check falls through, and channel_ready gets incorrectly retransmitted.

Your funding_tx_index approach sounds like the right fix, a completed splice increments the index, so funding_tx_index > 0 correctly identifies splice transactions regardless of whether the txids match.

&recv_tlvs->my_current_funding_locked->my_current_funding_locked_txid,
&peer->channel->funding.txid));

/* BOLT #2:
*
* - if `next_commitment_number` is 1 in both the
* `channel_reestablish` it sent and received:
* `channel_reestablish` it sent and received:
* - MUST retransmit `channel_ready`.
* - otherwise:
* - MUST NOT retransmit `channel_ready`, but MAY send
* `channel_ready` with a different `short_channel_id`
* `alias` field.
* - MUST NOT retransmit `channel_ready`, but MAY send `channel_ready` with
* a different `short_channel_id` `alias` field.
*/
if (peer->channel_ready[LOCAL]
&& peer->next_index[LOCAL] == 1
&& next_commitment_number == 1) {
&& next_commitment_number == 1
&& !is_splice_active) {
struct tlv_channel_ready_tlvs *tlvs = tlv_channel_ready_tlvs_new(tmpctx);

tlvs->short_channel_id = &peer->local_alias;
Expand Down
14 changes: 13 additions & 1 deletion common/bech32_util.c
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ bool from_bech32_charset(const tal_t *ctx,
u5 *u5data;
const char *sep;
bool upper = false, lower = false;
size_t datalen;
size_t datalen, nbits, trailing;

sep = memchr(bech32, '1', bech32_len);
if (!sep)
Expand All @@ -105,6 +105,18 @@ bool from_bech32_charset(const tal_t *ctx,
if (upper && lower)
goto fail;

/* Padding: converting N 5-bit groups to bytes leaves (N*5 % 8) trailing
* bits. These must be zero and fewer than 5 (otherwise a full 5-bit
* group is wasted as padding, which is invalid). */
nbits = datalen * 5;
trailing = nbits % 8;
if (trailing >= 5)
goto fail;
for (size_t i = nbits - trailing; i < nbits; i++) {
if (get_u5_bit(u5data, i))
goto fail;
}

*data = tal_arr(ctx, u8, 0);
if (!bech32_pull_bits(data, u5data, tal_bytelen(u5data) * 5)) {
tal_free(*data);
Expand Down
2 changes: 1 addition & 1 deletion common/bolt11.c
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ struct bolt11 *bolt11_decode(const tal_t *ctx, const char *str,
&sig,
(const u8 *)&hash))
return decode_fail(b11, fail,
"signature recovery failed");
"public-key recovery failed");
node_id_from_pubkey(&b11->receiver_id, &k);
} else {
struct pubkey k;
Expand Down
22 changes: 21 additions & 1 deletion common/bolt12.c
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,8 @@
#include <inttypes.h>
#include <time.h>

/* If chains is NULL, max_num_chains is ignored */
/* If chains is NULL, max_num_chains is ignored.
* If must_be_chain is NULL, only structural validity is checked. */
bool bolt12_chains_match(const struct bitcoin_blkid *chains,
size_t max_num_chains,
const struct chainparams *must_be_chain)
Expand All @@ -31,6 +32,13 @@ bool bolt12_chains_match(const struct bitcoin_blkid *chains,
* - if the node does not accept invoices for at least one of the `chains`:
* - MUST NOT respond to the offer
*/
if (chains && max_num_chains == 0)
return false;

/* No specific chain required: structurally valid. */
if (!must_be_chain)
return true;

if (!chains) {
max_num_chains = 1;
chains = &chainparams_for_network("bitcoin")->genesis_blockhash;
Expand Down Expand Up @@ -187,6 +195,18 @@ struct tlv_offer *offer_decode(const tal_t *ctx,
return NULL;
}

/* BOLT #12:
* - otherwise: (`offer_chains` is set):
* - if the node does not accept invoices for at least one of the `chains`:
* - MUST NOT respond to the offer
*/
for (size_t i = 0; i < tal_count(offer->fields); i++) {
if (offer->fields[i].numtype == 2 && offer->fields[i].length == 0) {
*fail = tal_strdup(ctx, "offer_chains must have at least one entry");
return tal_free(offer);
}
}

*fail = check_features_and_chain(ctx,
our_features, must_be_chain,
offer->offer_features,
Expand Down
2 changes: 1 addition & 1 deletion common/features.h
Original file line number Diff line number Diff line change
Expand Up @@ -113,7 +113,7 @@ struct feature_set *feature_set_dup(const tal_t *ctx,
* | 16/17 | `basic_mpp` |... IN9 ...
* | 18/19 | `option_support_large_channel` |... IN ...
* | 22/23 | `option_anchors` |... INT ...
* | 24/25 | `option_route_blinding` |...IN9 ...
* | 24/25 | `option_route_blinding` |... IN9 ...
* | 26/27 | `option_shutdown_anysegwit` |... IN ...
* | 28/29 | `option_dual_fund` |... IN ...
* | 34/35 | `option_quiesce` |... IN ...
Expand Down
2 changes: 1 addition & 1 deletion common/test/run-bolt11.c
Original file line number Diff line number Diff line change
Expand Up @@ -873,7 +873,7 @@ int main(int argc, char *argv[])
* > lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2
*/
assert(!bolt11_decode(tmpctx, "lnbc2500u1pvjluezpp5qqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqqqsyqcyq5rqwzqfqypqdq5xysxxatsyp3k7enxv4jsxqzpusp5zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zyg3zygs9qrsgqwgt7mcn5yqw3yx0w94pswkpq6j9uh6xfqqqtsk4tnarugeektd4hg5975x9am52rz4qskukxdmjemg92vvqz8nvmsye63r5ykel43pgz7zq0g2", NULL, NULL, NULL, &fail));
assert(streq(fail, "signature recovery failed"));
assert(streq(fail, "public-key recovery failed"));

/* BOLT #11:
* > ### String is too short.
Expand Down
7 changes: 5 additions & 2 deletions common/test/run-bolt12-encode-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -429,15 +429,18 @@ int main(int argc, char *argv[])
/* BOLT #12:
* - if `offer_amount` is set and `offer_description` is not set:
* - MUST NOT respond to the offer.
* - if `offer_amount` is set and is not greater than zero:
* - if `offer_amount` is set and is not greater than zero:
* - MUST NOT respond to the offer.
* - if `offer_currency` is set and `offer_amount` is not set:
* - MUST NOT respond to the offer.
* - if neither `offer_issuer_id` nor `offer_paths` are set:
* - MUST NOT respond to the offer.
*/
offer->offer_amount = tal(offer, u64);
*offer->offer_amount = 10000;

offer->offer_description = NULL;
print_invalid_offer(offer, "Missing offer_description and offer_amount");
print_invalid_offer(offer, "Missing offer_description");
offer->offer_description = tal_utf8(tmpctx, "Test vectors");

offer->offer_amount = tal(offer, u64);
Expand Down
2 changes: 1 addition & 1 deletion common/test/run-bolt12-format-string-test.c
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ int main(int argc, char *argv[])
* - SHOULD omit `offer_chains`, implying that bitcoin is only chain.
* - if a specific minimum `offer_amount` is required for successful payment:
* - MUST set `offer_amount` to the amount expected (per item).
* - MUST set `offer_amount` greater than zero.
* - MUST set `offer_amount` greater than zero.
* - if the currency for `offer_amount` is that of all entries in `chains`:
* - MUST specify `offer_amount` in multiples of the minimum lightning-payable unit
* (e.g. milli-satoshis for bitcoin).
Expand Down
8 changes: 4 additions & 4 deletions lightningd/channel_gossip.c
Original file line number Diff line number Diff line change
Expand Up @@ -687,10 +687,10 @@ static void stash_remote_announce_sigs(struct channel *channel,
* A node:
* - If the `open_channel` message has the `announce_channel` bit set AND a
* `shutdown` message has not been sent:
* - After `channel_ready` has been sent and received AND the funding
* transaction has enough confirmations to ensure that it won't be
* reorganized:
* - MUST send `announcement_signatures` for the funding transaction.
* - After `channel_ready` has been sent and received AND the funding
* transaction has enough confirmations to ensure that it won't be
* reorganized:
* - MUST send `announcement_signatures` for the funding transaction....
* - Otherwise:
* - MUST NOT send the `announcement_signatures` message.
*/
Expand Down
12 changes: 6 additions & 6 deletions openingd/openingd.c
Original file line number Diff line number Diff line change
Expand Up @@ -332,12 +332,12 @@ static u8 *funder_channel_start(struct state *state, u8 channel_flags,
= state->upfront_shutdown_script[LOCAL];

/* BOLT #2:
* - MUST set `channel_type`:
* - MUST set it to a defined type representing the type it wants.
* - MUST use the smallest bitmap possible to represent the channel
* type.
* - SHOULD NOT set it to a type containing a feature which was not
* negotiated.
* - MUST set `channel_type`:
* - MUST set it to a defined type representing the type it wants.
* - MUST use the smallest bitmap possible to represent the channel
* type.
* - SHOULD NOT set it to a type containing a feature which was not
* negotiated.
*/
open_tlvs->channel_type = state->channel_type->features;

Expand Down
7 changes: 7 additions & 0 deletions tests/test_splicing.py
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,8 @@ def test_commit_crash_splice(node_factory, bitcoind):

l1.daemon.wait_for_log(r"Splice initiator: we commit")

# Snapshot log position before restart so we only search post-restart logs below.
pre_restart_logpos = l1.daemon.logsearch_start
l1.restart()

# The splicing inflight should have been left pending in the DB
Expand All @@ -426,6 +428,11 @@ def test_commit_crash_splice(node_factory, bitcoind):
l1.daemon.wait_for_log(r'Splice resume check with local_next_funding: sent, remote_next_funding: received, inflights: 1')
l1.daemon.wait_for_log(r'Splice negotation, will not send commit, not recv commit, send signature, recv signature as initiator')

# channel_ready MUST NOT be retransmitted when either channel_reestablish
# message contains next_funding or my_current_funding_locked
assert not l1.daemon.is_in_log(r'Retransmitting channel_ready for channel',
start=pre_restart_logpos)

assert l1.db_query("SELECT count(*) as c FROM channel_funding_inflights;")[0]['c'] == 1

l2.daemon.wait_for_log(r'CHANNELD_NORMAL to CHANNELD_AWAITING_SPLICE')
Expand Down
6 changes: 3 additions & 3 deletions tests/test_wallet.py
Original file line number Diff line number Diff line change
Expand Up @@ -2548,11 +2548,11 @@ def test_unspend_during_reorg(node_factory, bitcoind):
blockheight, txindex, _ = scid.split('x')

# Use mainnet settings for rescan.
l3 = node_factory.get_node(options={'rescan': 15})
l3 = node_factory.get_node(options={'rescan': 75})
l3.connect(l2)

mine_funding_to_announce(bitcoind, [l1, l2, l3])
bitcoind.generate_block(20)
bitcoind.generate_block(90)
sync_blockheight(bitcoind, [l3])
wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2)

Expand All @@ -2565,7 +2565,7 @@ def test_unspend_during_reorg(node_factory, bitcoind):
bitcoind.generate_block(74, wait_for_mempool=1)
wait_for(lambda: len(l3.rpc.listchannels()['channels']) == 2)

# In one fell swoop it goes through dying, to dead (12 blocks)
# In one fell swoop it goes through dying, to dead (72 blocks)
l3.daemon.wait_for_log(f"Adding block {spentheight}")
l3.daemon.wait_for_log(f"gossipd: channel {scid} closing soon due to the funding outpoint being spent")
l3.daemon.wait_for_log(f"gossipd: Deleting channel {scid} due to the funding outpoint being spent")
Expand Down
Loading