diff --git a/common/features.c b/common/features.c index 9c1c73ea8..fa8c20c8b 100644 --- a/common/features.c +++ b/common/features.c @@ -78,6 +78,10 @@ static const struct feature_style feature_styles[] = { .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } }, + { OPT_SHUTDOWN_ANYSEGWIT, + .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, + [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, + [CHANNEL_FEATURE] = FEATURE_DONT_REPRESENT } }, { OPT_DUAL_FUND, .copy_style = { [INIT_FEATURE] = FEATURE_REPRESENT, [NODE_ANNOUNCE_FEATURE] = FEATURE_REPRESENT, @@ -385,9 +389,12 @@ static const char *feature_name(const tal_t *ctx, size_t f) "option_basic_mpp", "option_support_large_channel", "option_anchor_outputs", + "option_anchors_zero_fee_htlc_tx", + NULL, + "option_shutdown_anysegwit", }; - if (f / 2 >= ARRAY_SIZE(fnames)) + if (f / 2 >= ARRAY_SIZE(fnames) || !fnames[f / 2]) return tal_fmt(ctx, "option_unknown_%zu/%s", COMPULSORY_FEATURE(f), (f & 1) ? "odd" : "even"); diff --git a/common/features.h b/common/features.h index e928a73c9..a0ce2d2a7 100644 --- a/common/features.h +++ b/common/features.h @@ -113,6 +113,12 @@ u8 *featurebits_or(const tal_t *ctx, const u8 *f1 TAKES, const u8 *f2 TAKES); #define OPT_LARGE_CHANNELS 18 #define OPT_ANCHOR_OUTPUTS 20 +/* BOLT-4e329271a358ee52bf43ddbd96776943c5d74508 #9: + * + * | 26/27 | `option_shutdown_anysegwit` |... IN ... + */ +#define OPT_SHUTDOWN_ANYSEGWIT 26 + /* BOLT-7b04b1461739c5036add61782d58ac490842d98b #9: * | 222/223 | `option_dual_fund` | ... IN9 ... */ diff --git a/common/shutdown_scriptpubkey.c b/common/shutdown_scriptpubkey.c index 8c7bcee24..e875e60a9 100644 --- a/common/shutdown_scriptpubkey.c +++ b/common/shutdown_scriptpubkey.c @@ -1,10 +1,63 @@ #include #include -bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey) +#include + +/* BOLT-4e329271a358ee52bf43ddbd96776943c5d74508 #2: + * 5. if (and only if) `option_shutdown_anysegwit` is negotiated: + * * `OP_1` through `OP_16` inclusive, followed by a single + * push of 2 to 40 bytes + * (witness program versions 1 through 16) + */ +static bool is_valid_witnessprog(const u8 *scriptpubkey) +{ + size_t pushlen; + + if (tal_bytelen(scriptpubkey) < 2) + return false; + + switch (scriptpubkey[0]) { + case OP_1: + case OP_2: + case OP_3: + case OP_4: + case OP_5: + case OP_6: + case OP_7: + case OP_8: + case OP_9: + case OP_10: + case OP_11: + case OP_12: + case OP_13: + case OP_14: + case OP_15: + case OP_16: + break; + default: + fprintf(stderr, "op = %u (invalid)\n", scriptpubkey[0]); + return false; + } + + pushlen = scriptpubkey[1]; + /* Must be all of the rest of scriptpubkey */ + if (2 + pushlen != tal_bytelen(scriptpubkey)) { + fprintf(stderr, "2 + %zu != %zu\n", pushlen, tal_bytelen(scriptpubkey)); + return false; + } + + if (!(pushlen >= 2 && pushlen <= 40)) + fprintf(stderr, "pushlen == %zu\n", pushlen); + + return pushlen >= 2 && pushlen <= 40; +} + +bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey, + bool anysegwit) { return is_p2pkh(scriptpubkey, NULL) || is_p2sh(scriptpubkey, NULL) || is_p2wpkh(scriptpubkey, NULL) - || is_p2wsh(scriptpubkey, NULL); + || is_p2wsh(scriptpubkey, NULL) + || (anysegwit && is_valid_witnessprog(scriptpubkey)); } diff --git a/common/shutdown_scriptpubkey.h b/common/shutdown_scriptpubkey.h index 1e70e91fd..942a112ea 100644 --- a/common/shutdown_scriptpubkey.h +++ b/common/shutdown_scriptpubkey.h @@ -16,6 +16,7 @@ * - if the `scriptpubkey` is not in one of the above forms: * - SHOULD fail the connection. */ -bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey); +bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey, + bool anysegwit); #endif /* LIGHTNING_COMMON_SHUTDOWN_SCRIPTPUBKEY_H */ diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index de2109baf..edb7c2aef 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -227,6 +227,9 @@ static void peer_got_shutdown(struct channel *channel, const u8 *msg) { u8 *scriptpubkey; struct lightningd *ld = channel->peer->ld; + bool anysegwit = feature_negotiated(ld->our_features, + channel->peer->their_features, + OPT_SHUTDOWN_ANYSEGWIT); if (!fromwire_channeld_got_shutdown(channel, msg, &scriptpubkey)) { channel_internal_error(channel, "bad channel_got_shutdown %s", @@ -238,7 +241,7 @@ static void peer_got_shutdown(struct channel *channel, const u8 *msg) tal_free(channel->shutdown_scriptpubkey[REMOTE]); channel->shutdown_scriptpubkey[REMOTE] = scriptpubkey; - if (!valid_shutdown_scriptpubkey(scriptpubkey)) { + if (!valid_shutdown_scriptpubkey(scriptpubkey, anysegwit)) { channel_fail_permanent(channel, REASON_PROTOCOL, "Bad shutdown scriptpubkey %s", diff --git a/lightningd/lightningd.c b/lightningd/lightningd.c index 3bbbfb14f..2bd055735 100644 --- a/lightningd/lightningd.c +++ b/lightningd/lightningd.c @@ -809,6 +809,7 @@ static struct feature_set *default_features(const tal_t *ctx) #if EXPERIMENTAL_FEATURES OPTIONAL_FEATURE(OPT_ANCHOR_OUTPUTS), OPTIONAL_FEATURE(OPT_ONION_MESSAGES), + OPTIONAL_FEATURE(OPT_SHUTDOWN_ANYSEGWIT), #endif }; diff --git a/openingd/openingd.c b/openingd/openingd.c index 421493f27..ca0d7285e 100644 --- a/openingd/openingd.c +++ b/openingd/openingd.c @@ -325,11 +325,15 @@ static bool setup_channel_funder(struct state *state) static void set_remote_upfront_shutdown(struct state *state, u8 *shutdown_scriptpubkey STEALS) { + bool anysegwit = feature_negotiated(state->our_features, + state->their_features, + OPT_SHUTDOWN_ANYSEGWIT); + state->upfront_shutdown_script[REMOTE] = tal_steal(state, shutdown_scriptpubkey); if (shutdown_scriptpubkey - && !valid_shutdown_scriptpubkey(shutdown_scriptpubkey)) + && !valid_shutdown_scriptpubkey(shutdown_scriptpubkey, anysegwit)) peer_failed_err(state->pps, &state->channel_id, "Unacceptable upfront_shutdown_script %s", diff --git a/tests/test_closing.py b/tests/test_closing.py index 2a2a74a26..2b35bfa00 100644 --- a/tests/test_closing.py +++ b/tests/test_closing.py @@ -5,6 +5,7 @@ from shutil import copyfile from utils import ( only_one, sync_blockheight, wait_for, DEVELOPER, TIMEOUT, account_balance, first_channel_id, basic_fee, TEST_NETWORK, + EXPERIMENTAL_FEATURES, ) import os @@ -2609,3 +2610,80 @@ def test_invalid_upfront_shutdown_script(node_factory, bitcoind, executor): l1.rpc.connect(l2.info['id'], 'localhost', l2.port) with pytest.raises(RpcError, match=r'Unacceptable upfront_shutdown_script'): l1.fundchannel(l2, 1000000, False) + + +@unittest.skipIf(not DEVELOPER, "needs to set upfront_shutdown_script") +@pytest.mark.slow_test +def test_segwit_shutdown_script(node_factory, bitcoind, executor): + """ +Try a range of future segwit versions as shutdown scripts. We create many nodes, so this is quite slow under valgrind +""" + l1 = node_factory.get_node(allow_warning=True) + + # BOLT-4e329271a358ee52bf43ddbd96776943c5d74508 #2: + # 5. if (and only if) `option_shutdown_anysegwit` is negotiated: + # * `OP_1` through `OP_16` inclusive, followed by a single push of 2 to 40 bytes + # (witness program versions 1 through 16) + valid = ['51020000', '5128' + '00' * 0x28, + '52020000', '5228' + '00' * 0x28, + '53020000', '5328' + '00' * 0x28, + '54020000', '5428' + '00' * 0x28, + '55020000', '5528' + '00' * 0x28, + '56020000', '5628' + '00' * 0x28, + '57020000', '5728' + '00' * 0x28, + '58020000', '5828' + '00' * 0x28, + '59020000', '5928' + '00' * 0x28, + '5A020000', '5A28' + '00' * 0x28, + '5B020000', '5B28' + '00' * 0x28, + '5C020000', '5C28' + '00' * 0x28, + '5D020000', '5D28' + '00' * 0x28, + '5E020000', '5E28' + '00' * 0x28, + '5F020000', '5F28' + '00' * 0x28, + '60020000', '6028' + '00' * 0x28] + invalid = ['50020000', # Not OP_1-OP_16 + '61020000', # Not OP_1-OP_16 + '5102000000', # Extra bytes + '510100', # Too short + '5129' + '00' * 0x29] # Too long + + if EXPERIMENTAL_FEATURES: + xsuccess = valid + xfail = invalid + else: + xsuccess = [] + xfail = valid + invalid + + # More efficient to create them all up-front. + nodes = node_factory.get_nodes(len(xfail) + len(xsuccess)) + + # Give it one UTXO to spend for each node. + addresses = {} + for n in nodes: + addresses[l1.rpc.newaddr()['bech32']] = (10**6 + 100000) / 10**8 + bitcoind.rpc.sendmany("", addresses) + bitcoind.generate_block(1) + wait_for(lambda: len(l1.rpc.listfunds()['outputs']) == len(addresses)) + + # FIXME: Since we don't support other non-v0 encodings, we need a protocol + # test for this (we're actually testing our upfront check, not the real + # shutdown one!), + for script in xsuccess: + # Insist on upfront script we're not going to match. + l1.stop() + l1.daemon.env["DEV_OPENINGD_UPFRONT_SHUTDOWN_SCRIPT"] = script + l1.start() + + l2 = nodes.pop() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + l1.rpc.fundchannel(l2.info['id'], 10**6) + + for script in xfail: + # Insist on upfront script we're not going to match. + l1.stop() + l1.daemon.env["DEV_OPENINGD_UPFRONT_SHUTDOWN_SCRIPT"] = script + l1.start() + + l2 = nodes.pop() + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + with pytest.raises(RpcError, match=r'Unacceptable upfront_shutdown_script'): + l1.rpc.fundchannel(l2.info['id'], 10**6) diff --git a/tests/test_misc.py b/tests/test_misc.py index 11a472c80..9a2f3ed04 100644 --- a/tests/test_misc.py +++ b/tests/test_misc.py @@ -1895,6 +1895,7 @@ def test_list_features_only(node_factory): ] if EXPERIMENTAL_FEATURES: expected += ['option_anchor_outputs/odd'] + expected += ['option_shutdown_anysegwit/odd'] expected += ['option_unknown_102/odd'] assert features == expected diff --git a/tests/utils.py b/tests/utils.py index e91a92715..9f8cf046d 100644 --- a/tests/utils.py +++ b/tests/utils.py @@ -26,6 +26,8 @@ def expected_peer_features(wumbo_channels=False, extra=[]): features += [103] # option_anchor_outputs features += [21] + # option_shutdown_anysegwit + features += [27] if wumbo_channels: features += [19] return hex_bits(features + extra) @@ -41,6 +43,8 @@ def expected_node_features(wumbo_channels=False, extra=[]): features += [103] # option_anchor_outputs features += [21] + # option_shutdown_anysegwit + features += [27] if wumbo_channels: features += [19] return hex_bits(features + extra)