diff --git a/common/shutdown_scriptpubkey.c b/common/shutdown_scriptpubkey.c index 106022f90..05d7b22e9 100644 --- a/common/shutdown_scriptpubkey.c +++ b/common/shutdown_scriptpubkey.c @@ -1,6 +1,31 @@ #include "config.h" #include #include +#include + +/* BOLT #2: + * 4. if (and only if) `option_simple_close` is negotiated: + * * `OP_RETURN` followed by one of: + * * `6` to `75` inclusive followed by exactly that many bytes + * * `76` followed by `76` to `80` followed by exactly that many bytes + */ +static bool is_valid_op_return(const u8 *scriptpubkey, size_t scriptpubkey_len) +{ + u8 v; + + if (fromwire_u8(&scriptpubkey, &scriptpubkey_len) != OP_RETURN) + return false; + + v = fromwire_u8(&scriptpubkey, &scriptpubkey_len); + if (v >= 6 && v <= 75) + return scriptpubkey_len == v; + if (v == 76) { + v = fromwire_u8(&scriptpubkey, &scriptpubkey_len); + if (v >= 76 && v <= 80) + return scriptpubkey_len == v; + } + return false; +} /* BOLT #2: * 3. if (and only if) `option_shutdown_anysegwit` is negotiated: @@ -48,7 +73,8 @@ static bool is_valid_witnessprog(const u8 *scriptpubkey, size_t scriptpubkey_len bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey, bool anysegwit, - bool allow_oldstyle) + bool allow_oldstyle, + bool option_simple_close) { const size_t script_len = tal_bytelen(scriptpubkey); if (allow_oldstyle) { @@ -59,5 +85,6 @@ bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey, return is_p2wpkh(scriptpubkey, script_len, NULL) || is_p2wsh(scriptpubkey, script_len, NULL) - || (anysegwit && is_valid_witnessprog(scriptpubkey, script_len)); + || (anysegwit && is_valid_witnessprog(scriptpubkey, script_len)) + || (option_simple_close && is_valid_op_return(scriptpubkey, script_len)); } diff --git a/common/shutdown_scriptpubkey.h b/common/shutdown_scriptpubkey.h index 2834f8327..9bc0f6f44 100644 --- a/common/shutdown_scriptpubkey.h +++ b/common/shutdown_scriptpubkey.h @@ -25,6 +25,7 @@ * never will send such a thing!) if they're not using anchors. */ bool valid_shutdown_scriptpubkey(const u8 *scriptpubkey, bool anysegwit, - bool allow_oldstyle); + bool allow_oldstyle, + bool option_simple_close); #endif /* LIGHTNING_COMMON_SHUTDOWN_SCRIPTPUBKEY_H */ diff --git a/common/test/Makefile b/common/test/Makefile index 2775efeb2..444803342 100644 --- a/common/test/Makefile +++ b/common/test/Makefile @@ -125,4 +125,6 @@ common/test/run-trace: \ common/test/run-htable: \ common/pseudorand.o +common/test/run-shutdown_scriptpubkey: wire/towire.o wire/fromwire.o + check-units: $(COMMON_TEST_PROGRAMS:%=unittest/%) diff --git a/common/test/run-shutdown_scriptpubkey.c b/common/test/run-shutdown_scriptpubkey.c new file mode 100644 index 000000000..db543a9d8 --- /dev/null +++ b/common/test/run-shutdown_scriptpubkey.c @@ -0,0 +1,194 @@ +#include "config.h" +#include "../shutdown_scriptpubkey.c" +#include +#include +#include +#include +#include +#include +#include +#include + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for amount_asset_is_main */ +bool amount_asset_is_main(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_is_main called!\n"); abort(); } +/* Generated stub for amount_asset_to_sat */ +struct amount_sat amount_asset_to_sat(struct amount_asset *asset UNNEEDED) +{ fprintf(stderr, "amount_asset_to_sat called!\n"); abort(); } +/* Generated stub for amount_feerate */ + bool amount_feerate(u32 *feerate UNNEEDED, struct amount_sat fee UNNEEDED, size_t weight UNNEEDED) +{ fprintf(stderr, "amount_feerate called!\n"); abort(); } +/* Generated stub for amount_sat */ +struct amount_sat amount_sat(u64 satoshis UNNEEDED) +{ fprintf(stderr, "amount_sat called!\n"); abort(); } +/* Generated stub for amount_sat_add */ + bool amount_sat_add(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_add called!\n"); abort(); } +/* Generated stub for amount_sat_eq */ +bool amount_sat_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_eq called!\n"); abort(); } +/* Generated stub for amount_sat_greater_eq */ +bool amount_sat_greater_eq(struct amount_sat a UNNEEDED, struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_greater_eq called!\n"); abort(); } +/* Generated stub for amount_sat_sub */ + bool amount_sat_sub(struct amount_sat *val UNNEEDED, + struct amount_sat a UNNEEDED, + struct amount_sat b UNNEEDED) +{ fprintf(stderr, "amount_sat_sub called!\n"); abort(); } +/* Generated stub for amount_sat_to_asset */ +struct amount_asset amount_sat_to_asset(struct amount_sat *sat UNNEEDED, const u8 *asset UNNEEDED) +{ fprintf(stderr, "amount_sat_to_asset called!\n"); abort(); } +/* Generated stub for amount_tx_fee */ +struct amount_sat amount_tx_fee(u32 fee_per_kw UNNEEDED, size_t weight UNNEEDED) +{ fprintf(stderr, "amount_tx_fee called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +/* Thanks ChatGPT! */ +static const u8 *construct_script(const tal_t *ctx, + const u8 *data, size_t data_len) +{ + u8 *script = tal_arr(ctx, u8, 0); + towire_u8(&script, OP_RETURN); + if (data_len >= 6 && data_len <= 75) { + towire_u8(&script, data_len); + towire(&script, data, data_len); + } else if (data_len >= 76 && data_len <= 80) { + towire_u8(&script, 76); + towire_u8(&script, data_len); + towire(&script, data, data_len); + } else { + return tal_free(script); // Invalid case + } + return script; +} + +static void test_valid_op_returns(void) +{ + u8 data[80]; + const u8 *script; + + for (size_t i = 6; i <= 80; i++) { + memset(data, i, sizeof(data)); + script = construct_script(tmpctx, data, i); + assert(is_valid_op_return(script, tal_bytelen(script))); + } +} + +static void test_invalid_op_return_too_short(void) +{ + u8 data[80]; + const u8 *script; + + for (size_t i = 0; i < 6; i++) { + memset(data, i, sizeof(data)); + script = construct_script(tmpctx, data, i); + assert(!is_valid_op_return(script, tal_bytelen(script))); + } +} + +static void test_invalid_op_return_too_long(void) +{ + u8 data[100]; + const u8 *script; + + for (size_t i = 81; i < sizeof(data); i++) { + memset(data, i, sizeof(data)); + script = construct_script(tmpctx, data, i); + assert(!is_valid_op_return(script, tal_bytelen(script))); + } +} + +// Test case: Invalid OP_RETURN with incorrect push length (e.g., 77 bytes using wrong prefix) +static void test_invalid_op_return_wrong_push_length(void) +{ + u8 script[90] = {0x6a, 0x76, 0x77}; // Invalid push of 77 bytes + assert(!is_valid_op_return(script, 3 + 77)); +} + +// Test case: Invalid OP_RETURN with incorrect OP_RETURN opcode +static void test_invalid_op_return_wrong_opcode(void) +{ + u8 script[10] = {0x00, 0x06, 1, 2, 3, 4, 5, 6}; // 0x00 instead of 0x6a + assert(!is_valid_op_return(script, 8)); +} + +// Test case: Invalid OP_RETURN with no data +static void test_invalid_op_return_empty(void) +{ + u8 script[1] = {0x6a}; // Only OP_RETURN, no data + assert(!is_valid_op_return(script, 1)); +} + +static const u8 *construct_witness_script(const tal_t *ctx, u8 version, + const u8 *data, size_t data_len) +{ + u8 *script = tal_arr(ctx, u8, 0); + + if (version < OP_1 || version > OP_16 || data_len < 2 || data_len > 40) + return tal_free(script); // Invalid case + + towire_u8(&script, version); // OP_1 to OP_16 + towire_u8(&script, data_len); // Push length + towire(&script, data, data_len); // Data + + return script; +} + +static void test_valid_witnessprogs(void) +{ + u8 data[40]; + const u8 *script; + + for (u8 version = OP_1; version <= OP_16; version++) { + for (size_t i = 2; i <= 40; i++) { + memset(data, i, sizeof(data)); + script = construct_witness_script(tmpctx, version, data, i); + assert(is_valid_witnessprog(script, tal_bytelen(script))); + } + } +} + +static void test_invalid_witnessprogs(void) +{ + u8 data[41]; + const u8 *script; + + // Test: Invalid versions (0 and >16) + memset(data, 0xAA, sizeof(data)); + script = construct_witness_script(tmpctx, OP_0, data, 20); + assert(!is_valid_witnessprog(script, tal_bytelen(script))); + + script = construct_witness_script(tmpctx, OP_16 + 1, data, 20); + assert(!is_valid_witnessprog(script, tal_bytelen(script))); + + // Test: Invalid data lengths (1 byte and >40 bytes) + script = construct_witness_script(tmpctx, OP_1, data, 1); + assert(!is_valid_witnessprog(script, tal_bytelen(script))); + + script = construct_witness_script(tmpctx, OP_2, data, 41); + assert(!is_valid_witnessprog(script, tal_bytelen(script))); + + // Test: Completely empty script (invalid) + script = tal_arr(tmpctx, u8, 0); + assert(!is_valid_witnessprog(script, tal_bytelen(script))); +} + +// Test runner +int main(int argc, char *argv[]) +{ + common_setup(argv[0]); + + test_valid_op_returns(); + test_invalid_op_return_too_short(); + test_invalid_op_return_too_long(); + test_invalid_op_return_wrong_push_length(); + test_invalid_op_return_wrong_opcode(); + test_invalid_op_return_empty(); + test_valid_witnessprogs(); + test_invalid_witnessprogs(); + common_shutdown(); + return 0; +} diff --git a/lightningd/channel_control.c b/lightningd/channel_control.c index f79e773f3..d344c2f35 100644 --- a/lightningd/channel_control.c +++ b/lightningd/channel_control.c @@ -1227,7 +1227,7 @@ static void peer_got_shutdown(struct channel *channel, const u8 *msg) * - if the `scriptpubkey` is not in one of the above forms: * - SHOULD send a `warning`. */ - if (!valid_shutdown_scriptpubkey(scriptpubkey, anysegwit, !anchors)) { + if (!valid_shutdown_scriptpubkey(scriptpubkey, anysegwit, !anchors, false)) { u8 *warning = towire_warningfmt(NULL, &channel->cid, "Bad shutdown scriptpubkey %s", diff --git a/lightningd/closing_control.c b/lightningd/closing_control.c index a10a1f328..1bf6b4e98 100644 --- a/lightningd/closing_control.c +++ b/lightningd/closing_control.c @@ -759,10 +759,10 @@ static struct command_result *json_close(struct command *cmd, /* In theory, this could happen if the peer had anysegwit when channel was * established, and doesn't now. Doesn't happen, and if it did we could * provide a new address manually. */ - if (!valid_shutdown_scriptpubkey(close_to_script, anysegwit, false)) { + if (!valid_shutdown_scriptpubkey(close_to_script, anysegwit, false, false)) { /* Explicit check for future segwits. */ if (!anysegwit && - valid_shutdown_scriptpubkey(close_to_script, true, false)) { + valid_shutdown_scriptpubkey(close_to_script, true, false, false)) { return command_fail(cmd, JSONRPC2_INVALID_PARAMS, "Peer does not allow v1+ shutdown addresses"); } diff --git a/lightningd/dual_open_control.c b/lightningd/dual_open_control.c index 26407fdcd..49c3d844d 100644 --- a/lightningd/dual_open_control.c +++ b/lightningd/dual_open_control.c @@ -1588,7 +1588,7 @@ static void handle_peer_wants_to_close(struct subd *dualopend, * - if the `scriptpubkey` is not in one of the above forms: * - SHOULD send a `warning` */ - if (!valid_shutdown_scriptpubkey(scriptpubkey, anysegwit, !anchors)) { + if (!valid_shutdown_scriptpubkey(scriptpubkey, anysegwit, !anchors, false)) { u8 *warning = towire_warningfmt(NULL, &channel->cid, "Bad shutdown scriptpubkey %s", diff --git a/openingd/common.c b/openingd/common.c index ebda70035..00f33da45 100644 --- a/openingd/common.c +++ b/openingd/common.c @@ -229,7 +229,7 @@ char *validate_remote_upfront_shutdown(const tal_t *ctx, *state_script = tal_steal(ctx, shutdown_scriptpubkey); if (shutdown_scriptpubkey - && !valid_shutdown_scriptpubkey(shutdown_scriptpubkey, anysegwit, !anchors)) + && !valid_shutdown_scriptpubkey(shutdown_scriptpubkey, anysegwit, !anchors, false)) return tal_fmt(tmpctx, "Unacceptable upfront_shutdown_script %s", tal_hex(tmpctx, shutdown_scriptpubkey));