From 3f7478ef7b8fb1e768628b7ac6d70a1457040afc Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Fri, 11 Sep 2026 09:49:07 +0100 Subject: [PATCH 1/3] dualopen: add test for large output amount During the negotiation of a dualopen channel a peer can send a tx_add_output with a very large amount. dualopend should handle it with tx_abort. Changelog-None Signed-off-by: Lagrang3 --- tests/test_connection.py | 179 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 179 insertions(+) diff --git a/tests/test_connection.py b/tests/test_connection.py index 4b07bda708c9..c8915adfeaaf 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -15,6 +15,7 @@ ) from pyln.testing.utils import VALGRIND, EXPERIMENTAL_DUAL_FUND, FUNDAMOUNT, RUST, SLOW_MACHINE +import hashlib import os import pytest import random @@ -4983,6 +4984,12 @@ def test_constant_packet_size(node_factory, tcp_capture): WIRE_OPEN_CHANNEL = 32 WIRE_ACCEPT_CHANNEL = 33 WIRE_FUNDING_CREATED = 34 +WIRE_OPEN_CHANNEL2 = 64 +WIRE_ACCEPT_CHANNEL2 = 65 +WIRE_TX_ADD_INPUT = 66 +WIRE_TX_ADD_OUTPUT = 67 +WIRE_TX_COMPLETE = 70 +WIRE_TX_ABORT = 74 # bitcoin/chainparams.c: max_supply, which is also libwally's WALLY_SATOSHI_MAX. MAX_SUPPLY_SAT = 2100000000000000 @@ -5136,3 +5143,175 @@ def test_open_channel_funding_above_max_supply(node_factory, bitcoind): funding_sat, push_msat) assert l1.rpc.getinfo()['id'] == l1.info['id'] + + +def tlv_encode(type_num, value): + """Minimal bigsize TLV encode (type and length fit in one byte here).""" + assert type_num < 0xFD + assert len(value) < 0xFD + return bytes([type_num, len(value)]) + value + + +def derive_tmp_channel_id(opener_revocation_basepoint): + """BOLT #2 temporary_channel_id for open_channel2 (zeroed accepter basepoint).""" + return hashlib.sha256(bytes(33) + opener_revocation_basepoint).digest() + + +def derive_channel_id_v2(basepoint_a, basepoint_b): + """BOLT #2 v2 channel_id: SHA256(lesser-revocation-basepoint || greater...).""" + if basepoint_a < basepoint_b: + lesser, greater = basepoint_a, basepoint_b + else: + lesser, greater = basepoint_b, basepoint_a + return hashlib.sha256(lesser + greater).digest() + + +def send_open_channel2( + lconn, chain_hash, funding_sat, feerate_per_kw, channel_type, keys +): + """Send open_channel2; keys has 7 points (funding .. second_per_commitment). + + keys[1] is our revocation basepoint (used for temporary_channel_id). + Returns temporary_channel_id. + """ + revocation = keys[1] + temp_chan_id = derive_tmp_channel_id(revocation) + + msg = struct.pack(">H", WIRE_OPEN_CHANNEL2) + msg += chain_hash + msg += temp_chan_id + msg += struct.pack(">I", feerate_per_kw) # funding_feerate_perkw + msg += struct.pack(">I", feerate_per_kw) # commitment_feerate_perkw + msg += struct.pack(">Q", funding_sat) # funding_satoshis + msg += struct.pack(">Q", 546) # dust_limit_satoshis + msg += struct.pack(">Q", 0xFFFFFFFFFFFF) # max_htlc_value_in_flight_msat + msg += struct.pack(">Q", 0) # htlc_minimum_msat + msg += struct.pack(">H", 144) # to_self_delay + msg += struct.pack(">H", 483) # max_accepted_htlcs + msg += struct.pack(">I", 0) # locktime + for k in keys: + msg += k + msg += struct.pack(">B", 0) # channel_flags + # opening_tlvs: type 1 channel_type + msg += tlv_encode(1, channel_type) + + lconn.send_message(msg) + return temp_chan_id + + +def read_accept_channel2(lconn): + """Read past gossip to dualopend's accept_channel2 (or failure). + + Returns (mtype, msg_payload_after_type). + """ + for _ in range(40): + msg = lconn.read_message() + mtype = int.from_bytes(msg[0:2], "big") + if mtype in (WIRE_ACCEPT_CHANNEL2, WIRE_WARNING, WIRE_ERROR, WIRE_TX_ABORT): + return mtype, msg[2:] + raise AssertionError("no reply to open_channel2") + + +def parse_accept_channel2_revocation(payload): + """Extract accepter revocation_basepoint from accept_channel2 payload. + + Layout after type: + channel_id(32) funding_satoshis(8) dust(8) max_in_flight(8) htlc_min(8) + min_depth(4) to_self_delay(2) max_htlcs(2) + funding_pubkey(33) revocation(33) ... + """ + off = 32 + 8 + 8 + 8 + 8 + 4 + 2 + 2 + 33 + return payload[off:off + 33] + + +def send_tx_add_output(lconn, channel_id, serial_id, sats, script): + msg = struct.pack(">H", WIRE_TX_ADD_OUTPUT) + msg += channel_id + msg += struct.pack(">Q", serial_id) + msg += struct.pack(">Q", sats) + msg += struct.pack(">H", len(script)) + msg += script + lconn.send_message(msg) + + +def read_tx_interactive_reply(lconn): + """Read dualopend's response during interactive tx construction.""" + for _ in range(40): + msg = lconn.read_message() + mtype = int.from_bytes(msg[0:2], "big") + # dualopend with empty local PSBT replies tx_complete, or aborts. + if mtype in ( + WIRE_TX_COMPLETE, + WIRE_TX_ABORT, + WIRE_TX_ADD_OUTPUT, + WIRE_TX_ADD_INPUT, + WIRE_WARNING, + WIRE_ERROR, + ): + return mtype, msg + raise AssertionError("no reply during interactive tx") + + +@pytest.mark.xfail +@pytest.mark.openchannel("v2") +@unittest.skipIf( + TEST_NETWORK != "regtest", "elementsd doesnt yet support PSBT features we need" +) +def test_open_channel2_tx_add_output_above_max_supply(node_factory, bitcoind): + """tx_add_output sats above MAX_MONEY must not crash dualopend. + + Before the fix, dualopend passed oversized amounts straight to + psbt_append_output(); libwally then asserted. BOLT #2 requires failing + the negotiation when sats > MAX_MONEY (or the running output total would + exceed max supply). + """ + l1 = node_factory.get_node(options={"experimental-dual-fund": None}) + + chain_hash = bytes.fromhex(bitcoind.rpc.getblockhash(0))[::-1] + feerate = l1.rpc.feerates("perkw")["perkw"]["opening"] + + # Standard P2WPKH scriptpubkey (contents only need to pass is_known_scripttype). + script = bytes([0x00, 0x14]) + bytes(range(20)) + + cases = ( + MAX_SUPPLY_SAT + 1, + MAX_SUPPLY_SAT * 2, + 0xFFFFFFFFFFFFFFFF, + MAX_SUPPLY_SAT + 200000, + (MAX_SUPPLY_SAT + 100) * 1000, + ) + + for sats in cases: + lconn, channel_type = raw_peer_connect(l1) + + # Seven distinct valid compressed points for open_channel2 key fields. + keys = [ + wire.PrivateKey(bytes([i + 2] * 32)).public_key().serializeCompressed() + for i in range(7) + ] + + send_open_channel2(lconn, chain_hash, FUNDAMOUNT, feerate, channel_type, keys) + + mtype, payload = read_accept_channel2(lconn) + assert mtype == WIRE_ACCEPT_CHANNEL2, ( + "open_channel2 rejected before interactive tx (got msgtype {})".format( + mtype + ) + ) + + their_revocation = parse_accept_channel2_revocation(payload) + # accept_channel2 is sent with the temporary id, but dualopend then + # switches to the final v2 channel_id for interactive tx messages. + channel_id = derive_channel_id_v2(keys[1], their_revocation) + + # First message of interactive construction: oversized output. + # serial_id even => from initiator. + send_tx_add_output(lconn, channel_id, 0, sats, script) + + mtype, msg = read_tx_interactive_reply(lconn) + assert mtype == WIRE_TX_ABORT, ( + "tx_add_output sats={} was not aborted (got msgtype {})".format(sats, mtype) + ) + + # node is still up + assert l1.rpc.getinfo()["id"] == l1.info["id"] From e89fc4c97015ec79ba983cd3e7c666ffbee7516c Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Thu, 10 Sep 2026 17:18:10 +0100 Subject: [PATCH 2/3] dualopend: handle tx_add_output with over MAX_MONEY Harden the handling of tx_add_output messages. Fail the negotiation if the sum of the outputs exceeds 21M BTC. Changelog-None. Signed-off-by: Lagrang3 --- common/interactivetx.c | 35 +++++++++++++++++++++++++++++++++++ openingd/dualopend.c | 36 ++++++++++++++++++++++++++++++++++++ tests/test_connection.py | 1 - 3 files changed, 71 insertions(+), 1 deletion(-) diff --git a/common/interactivetx.c b/common/interactivetx.c index 317a571dc6d0..b53e78279cd4 100644 --- a/common/interactivetx.c +++ b/common/interactivetx.c @@ -679,6 +679,7 @@ char *process_interactivetx_updates(const tal_t *ctx, u8 *scriptpubkey; struct wally_psbt_output *out; struct amount_sat amt; + struct amount_sat out_total; if (!fromwire_tx_add_output(ctx, msg, &cid, &serial_id, &value, &scriptpubkey)) @@ -739,6 +740,40 @@ char *process_interactivetx_updates(const tal_t *ctx, ictx->current_psbt->num_outputs + 1, MAX_FUNDING_OUTPUTS); + /* BOLT #2: + * The receiving node: ... + * - MUST fail the negotiation if: ... + * - the `sats` amount is greater than 2,100,000,000,000,000 (`MAX_MONEY`) + */ + out_total = AMOUNT_SAT(0); + for (size_t i = 0; i < ictx->current_psbt->num_outputs; + i++) { + struct amount_sat output_amt = + psbt_output_get_amount(ictx->current_psbt, + i); + if (!amount_sat_add(&out_total, out_total, + output_amt)) + return tal_fmt( + ctx, + "Output amount total overflow " + "(partial sum is %s, current " + "output is %s at output number %d)", + fmt_amount_sat(tmpctx, out_total), + fmt_amount_sat(tmpctx, output_amt), + (int)i); + } + if (!amount_sat_add(&out_total, out_total, amt) || + amount_sat_greater(out_total, + chainparams->max_supply)) + return tal_fmt( + ctx, + "Adding output amount %s would exceed max " + "supply (current total is %s over %d " + "outputs)", + fmt_amount_sat(tmpctx, amt), + fmt_amount_sat(tmpctx, out_total), + (int)ictx->current_psbt->num_outputs); + out = psbt_append_output(ictx->current_psbt, scriptpubkey, amt); diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 2f1849773496..6a16dc7f324a 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -1930,6 +1930,7 @@ static bool run_tx_interactive(struct state *state, u8 *scriptpubkey; struct wally_psbt_output *out; struct amount_sat amt; + struct amount_sat out_total; if (!fromwire_tx_add_output(tmpctx, msg, &cid, &serial_id, &value, &scriptpubkey)) @@ -1987,6 +1988,41 @@ static bool run_tx_interactive(struct state *state, return false; } + /* BOLT #2: + * The receiving node: ... + * - MUST fail the negotiation if: ... + * - the `sats` amount is greater than 2,100,000,000,000,000 (`MAX_MONEY`) + */ + out_total = AMOUNT_SAT(0); + for (size_t i = 0; i < psbt->num_outputs; i++) { + struct amount_sat output_amt = + psbt_output_get_amount(psbt, i); + if (!amount_sat_add(&out_total, out_total, + output_amt)) { + open_abort( + state, + "Output amount total overflow " + "(partial sum is %s, current " + "output is %s at output number %d)", + fmt_amount_sat(tmpctx, out_total), + fmt_amount_sat(tmpctx, output_amt), + (int)i); + return false; + } + } + if (!amount_sat_add(&out_total, out_total, amt) || + amount_sat_greater(out_total, + chainparams->max_supply)) { + open_abort(state, + "Adding output amount %s would " + "exceed max supply (current total " + "is %s over %d outputs)", + fmt_amount_sat(tmpctx, amt), + fmt_amount_sat(tmpctx, out_total), + (int)psbt->num_outputs); + return false; + } + out = psbt_append_output(psbt, scriptpubkey, amt); psbt_output_set_serial_id(psbt, out, serial_id); break; diff --git a/tests/test_connection.py b/tests/test_connection.py index c8915adfeaaf..304d2301d15d 100644 --- a/tests/test_connection.py +++ b/tests/test_connection.py @@ -5252,7 +5252,6 @@ def read_tx_interactive_reply(lconn): raise AssertionError("no reply during interactive tx") -@pytest.mark.xfail @pytest.mark.openchannel("v2") @unittest.skipIf( TEST_NETWORK != "regtest", "elementsd doesnt yet support PSBT features we need" From 5dde81de64de3341bf77cb6f034e284c6eaf015f Mon Sep 17 00:00:00 2001 From: Lagrang3 Date: Fri, 11 Sep 2026 08:04:19 +0100 Subject: [PATCH 3/3] interactivetx: put the MAX_MONEY checks into one place Make the MAX_MONEY checks of tx_add_output into an internal API that we can re-use in interactivetx (used by splices) and dualopend. Changelog-None Signed-off-by: Lagrang3 --- common/interactivetx.c | 62 ++++--- common/interactivetx.h | 6 + common/test/run-interactivetx.c | 317 ++++++++++++++++++++++++++++++++ openingd/dualopend.c | 47 +---- 4 files changed, 364 insertions(+), 68 deletions(-) create mode 100644 common/test/run-interactivetx.c diff --git a/common/interactivetx.c b/common/interactivetx.c index b53e78279cd4..f027fcf01797 100644 --- a/common/interactivetx.c +++ b/common/interactivetx.c @@ -389,6 +389,35 @@ bool interactivetx_has_changes(struct interactivetx_context *ictx, || tal_count(set->added_outs) || tal_count(set->rm_outs); } +char *interactive_tx_add_output_check_max_money(const tal_t *ctx, + struct wally_psbt *psbt, + struct amount_sat amt) +{ + struct amount_sat output_amt; + struct amount_sat out_total = AMOUNT_SAT(0); + for (size_t i = 0; i < psbt->num_outputs; i++) { + output_amt = psbt_output_get_amount(psbt, i); + if (!amount_sat_add(&out_total, out_total, output_amt)) + return tal_fmt(ctx, + "Output amount total overflow " + "(partial sum is %s, current " + "output is %s at output number %d)", + fmt_amount_sat(tmpctx, out_total), + fmt_amount_sat(tmpctx, output_amt), + (int)i); + } + if (!amount_sat_add(&out_total, out_total, amt) || + amount_sat_greater(out_total, chainparams->max_supply)) + return tal_fmt(ctx, + "Adding output amount %s would exceed max " + "supply (current total is %s over %d " + "outputs)", + fmt_amount_sat(tmpctx, amt), + fmt_amount_sat(tmpctx, out_total), + (int)psbt->num_outputs); + return NULL; +} + char *process_interactivetx_updates(const tal_t *ctx, struct interactivetx_context *ictx, bool *received_tx_complete, @@ -679,7 +708,6 @@ char *process_interactivetx_updates(const tal_t *ctx, u8 *scriptpubkey; struct wally_psbt_output *out; struct amount_sat amt; - struct amount_sat out_total; if (!fromwire_tx_add_output(ctx, msg, &cid, &serial_id, &value, &scriptpubkey)) @@ -745,34 +773,10 @@ char *process_interactivetx_updates(const tal_t *ctx, * - MUST fail the negotiation if: ... * - the `sats` amount is greater than 2,100,000,000,000,000 (`MAX_MONEY`) */ - out_total = AMOUNT_SAT(0); - for (size_t i = 0; i < ictx->current_psbt->num_outputs; - i++) { - struct amount_sat output_amt = - psbt_output_get_amount(ictx->current_psbt, - i); - if (!amount_sat_add(&out_total, out_total, - output_amt)) - return tal_fmt( - ctx, - "Output amount total overflow " - "(partial sum is %s, current " - "output is %s at output number %d)", - fmt_amount_sat(tmpctx, out_total), - fmt_amount_sat(tmpctx, output_amt), - (int)i); - } - if (!amount_sat_add(&out_total, out_total, amt) || - amount_sat_greater(out_total, - chainparams->max_supply)) - return tal_fmt( - ctx, - "Adding output amount %s would exceed max " - "supply (current total is %s over %d " - "outputs)", - fmt_amount_sat(tmpctx, amt), - fmt_amount_sat(tmpctx, out_total), - (int)ictx->current_psbt->num_outputs); + error = interactive_tx_add_output_check_max_money( + ctx, ictx->current_psbt, amt); + if (error) + return error; out = psbt_append_output(ictx->current_psbt, scriptpubkey, diff --git a/common/interactivetx.h b/common/interactivetx.h index 967bb167b79d..ff0f8674073b 100644 --- a/common/interactivetx.h +++ b/common/interactivetx.h @@ -66,6 +66,12 @@ struct interactivetx_context { struct psbt_changeset *change_set; }; +/* Sanity checks on the transaction outputs amount and the new value to be added + * so that MAX_MONEY (21M BTC) is not exceeded. */ +char *interactive_tx_add_output_check_max_money(const tal_t *ctx, + struct wally_psbt *psbt, + struct amount_sat amt); + /* Builds a new default interactivetx context with default values */ struct interactivetx_context *new_interactivetx_context(const tal_t *ctx, enum tx_role our_role, diff --git a/common/test/run-interactivetx.c b/common/test/run-interactivetx.c new file mode 100644 index 000000000000..10b86a1d586f --- /dev/null +++ b/common/test/run-interactivetx.c @@ -0,0 +1,317 @@ +#include "config.h" +#include "../amount.c" +#include "../interactivetx.c" +#include +#include +#include +#include +#include +#include + +/* AUTOGENERATED MOCKS START */ +/* Generated stub for fromwire */ +const u8 *fromwire(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, void *copy UNNEEDED, size_t n UNNEEDED) +{ fprintf(stderr, "fromwire called!\n"); abort(); } +/* Generated stub for fromwire_bool */ +bool fromwire_bool(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_bool called!\n"); abort(); } +/* Generated stub for fromwire_fail */ +void *fromwire_fail(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_fail called!\n"); abort(); } +/* Generated stub for fromwire_peektype */ +int fromwire_peektype(const u8 *cursor UNNEEDED) +{ fprintf(stderr, "fromwire_peektype called!\n"); abort(); } +/* Generated stub for fromwire_secp256k1_ecdsa_signature */ +void fromwire_secp256k1_ecdsa_signature(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, + secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "fromwire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for fromwire_sha256 */ +void fromwire_sha256(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "fromwire_sha256 called!\n"); abort(); } +/* Generated stub for fromwire_tal_arrn */ +u8 *fromwire_tal_arrn(const tal_t *ctx UNNEEDED, + const u8 **cursor UNNEEDED, size_t *max UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_tal_arrn called!\n"); abort(); } +/* Generated stub for fromwire_tx_add_input */ +bool fromwire_tx_add_input(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct channel_id *channel_id UNNEEDED, u64 *serial_id UNNEEDED, u8 **prevtx UNNEEDED, u32 *prevtx_vout UNNEEDED, u32 *sequence UNNEEDED, struct tlv_tx_add_input_tlvs **tlvs UNNEEDED) +{ fprintf(stderr, "fromwire_tx_add_input called!\n"); abort(); } +/* Generated stub for fromwire_tx_add_output */ +bool fromwire_tx_add_output(const tal_t *ctx UNNEEDED, const void *p UNNEEDED, struct channel_id *channel_id UNNEEDED, u64 *serial_id UNNEEDED, u64 *sats UNNEEDED, u8 **script UNNEEDED) +{ fprintf(stderr, "fromwire_tx_add_output called!\n"); abort(); } +/* Generated stub for fromwire_tx_complete */ +bool fromwire_tx_complete(const void *p UNNEEDED, struct channel_id *channel_id UNNEEDED) +{ fprintf(stderr, "fromwire_tx_complete called!\n"); abort(); } +/* Generated stub for fromwire_tx_remove_input */ +bool fromwire_tx_remove_input(const void *p UNNEEDED, struct channel_id *channel_id UNNEEDED, u64 *serial_id UNNEEDED) +{ fprintf(stderr, "fromwire_tx_remove_input called!\n"); abort(); } +/* Generated stub for fromwire_tx_remove_output */ +bool fromwire_tx_remove_output(const void *p UNNEEDED, struct channel_id *channel_id UNNEEDED, u64 *serial_id UNNEEDED) +{ fprintf(stderr, "fromwire_tx_remove_output called!\n"); abort(); } +/* Generated stub for fromwire_u32 */ +u32 fromwire_u32(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u32 called!\n"); abort(); } +/* Generated stub for fromwire_u64 */ +u64 fromwire_u64(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u64 called!\n"); abort(); } +/* Generated stub for fromwire_u8 */ +u8 fromwire_u8(const u8 **cursor UNNEEDED, size_t *max UNNEEDED) +{ fprintf(stderr, "fromwire_u8 called!\n"); abort(); } +/* Generated stub for fromwire_u8_array */ +void fromwire_u8_array(const u8 **cursor UNNEEDED, size_t *max UNNEEDED, u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "fromwire_u8_array called!\n"); abort(); } +/* Generated stub for is_peer_error */ +const char *is_peer_error(const tal_t *ctx UNNEEDED, const u8 *msg UNNEEDED) +{ fprintf(stderr, "is_peer_error called!\n"); abort(); } +/* Generated stub for is_peer_warning */ +const char *is_peer_warning(const tal_t *ctx UNNEEDED, const u8 *msg UNNEEDED) +{ fprintf(stderr, "is_peer_warning called!\n"); abort(); } +/* Generated stub for is_unknown_msg_discardable */ +bool is_unknown_msg_discardable(const u8 *cursor UNNEEDED) +{ fprintf(stderr, "is_unknown_msg_discardable called!\n"); abort(); } +/* Generated stub for peer_read */ +u8 *peer_read(const tal_t *ctx UNNEEDED, struct per_peer_state *pps UNNEEDED) +{ fprintf(stderr, "peer_read called!\n"); abort(); } +/* Generated stub for peer_write */ +void peer_write(struct per_peer_state *pps UNNEEDED, const void *msg TAKES UNNEEDED) +{ fprintf(stderr, "peer_write called!\n"); abort(); } +/* Generated stub for psbt_find_serial_input */ +int psbt_find_serial_input(struct wally_psbt *psbt UNNEEDED, u64 serial_id UNNEEDED) +{ fprintf(stderr, "psbt_find_serial_input called!\n"); abort(); } +/* Generated stub for psbt_find_serial_output */ +int psbt_find_serial_output(struct wally_psbt *psbt UNNEEDED, u64 serial_id UNNEEDED) +{ fprintf(stderr, "psbt_find_serial_output called!\n"); abort(); } +/* Generated stub for psbt_get_changeset */ +struct psbt_changeset *psbt_get_changeset(const tal_t *ctx UNNEEDED, + struct wally_psbt *orig UNNEEDED, + struct wally_psbt *new UNNEEDED) +{ fprintf(stderr, "psbt_get_changeset called!\n"); abort(); } +/* Generated stub for psbt_get_serial_id */ + bool psbt_get_serial_id(const struct wally_map *map UNNEEDED, + u64 *serial_id UNNEEDED) +{ fprintf(stderr, "psbt_get_serial_id called!\n"); abort(); } +/* Generated stub for psbt_input_set_serial_id */ +void psbt_input_set_serial_id(const tal_t *ctx UNNEEDED, + struct wally_psbt_input *input UNNEEDED, + u64 serial_id UNNEEDED) +{ fprintf(stderr, "psbt_input_set_serial_id called!\n"); abort(); } +/* Generated stub for psbt_output_set_serial_id */ +void psbt_output_set_serial_id(const tal_t *ctx UNNEEDED, + struct wally_psbt_output *output UNNEEDED, + u64 serial_id UNNEEDED) +{ fprintf(stderr, "psbt_output_set_serial_id called!\n"); abort(); } +/* Generated stub for psbt_sort_by_serial_id */ +void psbt_sort_by_serial_id(struct wally_psbt *psbt UNNEEDED) +{ fprintf(stderr, "psbt_sort_by_serial_id called!\n"); abort(); } +/* Generated stub for siphash_seed */ +const struct siphash_seed *siphash_seed(void) +{ fprintf(stderr, "siphash_seed called!\n"); abort(); } +/* Generated stub for status_fmt */ +void status_fmt(enum log_level level UNNEEDED, + const struct node_id *peer UNNEEDED, + const char *fmt UNNEEDED, ...) + +{ fprintf(stderr, "status_fmt called!\n"); abort(); } +/* Generated stub for tlv_tx_add_input_tlvs_new */ +struct tlv_tx_add_input_tlvs *tlv_tx_add_input_tlvs_new(const tal_t *ctx UNNEEDED) +{ fprintf(stderr, "tlv_tx_add_input_tlvs_new called!\n"); abort(); } +/* Generated stub for towire */ +void towire(u8 **pptr UNNEEDED, const void *data UNNEEDED, size_t len UNNEEDED) +{ fprintf(stderr, "towire called!\n"); abort(); } +/* Generated stub for towire_bool */ +void towire_bool(u8 **pptr UNNEEDED, bool v UNNEEDED) +{ fprintf(stderr, "towire_bool called!\n"); abort(); } +/* Generated stub for towire_secp256k1_ecdsa_signature */ +void towire_secp256k1_ecdsa_signature(u8 **pptr UNNEEDED, + const secp256k1_ecdsa_signature *signature UNNEEDED) +{ fprintf(stderr, "towire_secp256k1_ecdsa_signature called!\n"); abort(); } +/* Generated stub for towire_sha256 */ +void towire_sha256(u8 **pptr UNNEEDED, const struct sha256 *sha256 UNNEEDED) +{ fprintf(stderr, "towire_sha256 called!\n"); abort(); } +/* Generated stub for towire_tx_add_input */ +u8 *towire_tx_add_input(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED, u64 serial_id UNNEEDED, const u8 *prevtx UNNEEDED, u32 prevtx_vout UNNEEDED, u32 sequence UNNEEDED, const struct tlv_tx_add_input_tlvs *tlvs UNNEEDED) +{ fprintf(stderr, "towire_tx_add_input called!\n"); abort(); } +/* Generated stub for towire_tx_add_output */ +u8 *towire_tx_add_output(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED, u64 serial_id UNNEEDED, u64 sats UNNEEDED, const u8 *script UNNEEDED) +{ fprintf(stderr, "towire_tx_add_output called!\n"); abort(); } +/* Generated stub for towire_tx_complete */ +u8 *towire_tx_complete(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED) +{ fprintf(stderr, "towire_tx_complete called!\n"); abort(); } +/* Generated stub for towire_tx_remove_input */ +u8 *towire_tx_remove_input(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED, u64 serial_id UNNEEDED) +{ fprintf(stderr, "towire_tx_remove_input called!\n"); abort(); } +/* Generated stub for towire_tx_remove_output */ +u8 *towire_tx_remove_output(const tal_t *ctx UNNEEDED, const struct channel_id *channel_id UNNEEDED, u64 serial_id UNNEEDED) +{ fprintf(stderr, "towire_tx_remove_output called!\n"); abort(); } +/* Generated stub for towire_u32 */ +void towire_u32(u8 **pptr UNNEEDED, u32 v UNNEEDED) +{ fprintf(stderr, "towire_u32 called!\n"); abort(); } +/* Generated stub for towire_u64 */ +void towire_u64(u8 **pptr UNNEEDED, u64 v UNNEEDED) +{ fprintf(stderr, "towire_u64 called!\n"); abort(); } +/* Generated stub for towire_u8 */ +void towire_u8(u8 **pptr UNNEEDED, u8 v UNNEEDED) +{ fprintf(stderr, "towire_u8 called!\n"); abort(); } +/* Generated stub for towire_u8_array */ +void towire_u8_array(u8 **pptr UNNEEDED, const u8 *arr UNNEEDED, size_t num UNNEEDED) +{ fprintf(stderr, "towire_u8_array called!\n"); abort(); } +/* AUTOGENERATED MOCKS END */ + +/* Placeholder P2WPKH scriptpubkey for psbt_append_output. The helper under + * test only inspects output amounts, so the script contents do not matter. + * + * Byte layout: + * 0x00 - OP_0 (witness version 0) + * 0x14 - OP_DATA_20: push the following 20 bytes + * 0x00..0x13 - 20-byte witness program (dummy pubkey hash, not a real key) + */ +static const u8 dummy_script[] = { + 0x00, 0x14, 0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, + 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13}; + +static struct wally_psbt *empty_psbt(const tal_t *ctx) +{ + return create_psbt(ctx, 0, 0, 0); +} + +static void add_output(struct wally_psbt *psbt, struct amount_sat sat) +{ + struct wally_psbt_output *out; + u8 *script = + tal_dup_arr(tmpctx, u8, dummy_script, ARRAY_SIZE(dummy_script), 0); + + out = psbt_append_output(psbt, script, sat); + assert(out); +} + +/* Force an output amount without going through libwally's MAX_MONEY checks. */ +static void set_output_amount(struct wally_psbt *psbt, size_t idx, u64 sats) +{ + assert(idx < psbt->num_outputs); + psbt->outputs[idx].amount = sats; + psbt->outputs[idx].has_amount = 1; + if (psbt->tx && idx < psbt->tx->num_outputs) + psbt->tx->outputs[idx].satoshi = sats; +} + +static void test_empty_psbt_accepts_valid_amounts(void) +{ + struct wally_psbt *psbt = empty_psbt(tmpctx); + char *err; + + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(0)); + assert(!err); + + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(1)); + assert(!err); + + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(100000000)); + assert(!err); + + /* Exactly max supply is allowed (amount_sat_greater, not greater_eq). + */ + err = interactive_tx_add_output_check_max_money( + tmpctx, psbt, chainparams->max_supply); + assert(!err); +} + +static void test_empty_psbt_rejects_over_max_supply(void) +{ + struct wally_psbt *psbt = empty_psbt(tmpctx); + struct amount_sat over; + char *err; + + assert(amount_sat_add(&over, chainparams->max_supply, AMOUNT_SAT(1))); + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, over); + assert(err); + assert(strstr(err, "would exceed max supply")); +} + +static void test_existing_outputs_sum_with_new_amount(void) +{ + struct wally_psbt *psbt = empty_psbt(tmpctx); + /* 10M BTC and 11M BTC - sum is 21M, so +1 sat exceeds max supply. */ + struct amount_sat ten_m = AMOUNT_SAT(1000000000000000); + struct amount_sat eleven_m = AMOUNT_SAT(1100000000000000); + struct amount_sat almost_eleven_m; + char *err; + + add_output(psbt, ten_m); + add_output(psbt, eleven_m); + + /* 10M + 11M + 1 sat > 21M */ + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(1)); + assert(err); + assert(strstr(err, "would exceed max supply")); + + /* Rebuild: 10M + (11M - 1) + 1 == 21M, should pass */ + psbt = empty_psbt(tmpctx); + add_output(psbt, ten_m); + assert(amount_sat_sub(&almost_eleven_m, eleven_m, AMOUNT_SAT(1))); + add_output(psbt, almost_eleven_m); + + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(1)); + assert(!err); + + /* One more sat would exceed */ + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(2)); + assert(err); + assert(strstr(err, "would exceed max supply")); +} + +static void test_output_total_overflow(void) +{ + struct wally_psbt *psbt = empty_psbt(tmpctx); + char *err; + + /* Two legal individual outputs whose sum overflows u64. */ + add_output(psbt, AMOUNT_SAT(1)); + add_output(psbt, AMOUNT_SAT(1)); + set_output_amount(psbt, 0, UINT64_MAX); + set_output_amount(psbt, 1, 1); + + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(1)); + assert(err); + assert(strstr(err, "Output amount total overflow")); +} + +static void test_add_amount_overflow(void) +{ + struct wally_psbt *psbt = empty_psbt(tmpctx); + char *err; + + add_output(psbt, AMOUNT_SAT(1)); + set_output_amount(psbt, 0, UINT64_MAX); + + /* Existing total is UINT64_MAX; adding anything overflows. */ + err = interactive_tx_add_output_check_max_money(tmpctx, psbt, + AMOUNT_SAT(1)); + assert(err); + /* This path uses the max-supply error (add fails or exceeds). */ + assert(strstr(err, "would exceed max supply") || + strstr(err, "Output amount total overflow")); +} + +int main(int argc, char *argv[]) +{ + common_setup(argv[0]); + chainparams = chainparams_for_network("bitcoin"); + + assert(amount_sat_eq(chainparams->max_supply, + AMOUNT_SAT(2100000000000000))); + + test_empty_psbt_accepts_valid_amounts(); + test_empty_psbt_rejects_over_max_supply(); + test_existing_outputs_sum_with_new_amount(); + test_output_total_overflow(); + test_add_amount_overflow(); + + common_shutdown(); + return 0; +} diff --git a/openingd/dualopend.c b/openingd/dualopend.c index 6a16dc7f324a..01a38798fa1f 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -46,15 +47,6 @@ #define REQ_FD STDIN_FILENO #define HSM_FD 4 -/* tx_add_input, tx_add_output, tx_rm_input, tx_rm_output */ -#define NUM_TX_MSGS (TX_RM_OUTPUT + 1) -enum tx_msgs { - TX_ADD_INPUT, - TX_ADD_OUTPUT, - TX_RM_INPUT, - TX_RM_OUTPUT, -}; - /* * BOLT #2: * The maximum inputs and outputs are capped at 252. This effectively fixes @@ -97,7 +89,7 @@ struct tx_state { u64 funding_serial; /* Track how many of each tx collab msg we receive */ - u16 tx_msg_count[NUM_TX_MSGS]; + u16 tx_msg_count[INTERACTIVETX_NUM_TX_MSGS]; /* Have we gotten the peer's tx-sigs yet? */ bool remote_funding_sigs_rcvd; @@ -144,7 +136,7 @@ static struct tx_state *new_tx_state(const tal_t *ctx) /* no max_htlc_dust_exposure on remoteconf, we exclusively use the local's */ tx_state->remoteconf.max_dust_htlc_exposure_msat = AMOUNT_MSAT(0); - for (size_t i = 0; i < NUM_TX_MSGS; i++) + for (size_t i = 0; i < INTERACTIVETX_NUM_TX_MSGS; i++) tx_state->tx_msg_count[i] = 0; return tx_state; @@ -1930,7 +1922,7 @@ static bool run_tx_interactive(struct state *state, u8 *scriptpubkey; struct wally_psbt_output *out; struct amount_sat amt; - struct amount_sat out_total; + const char *error; if (!fromwire_tx_add_output(tmpctx, msg, &cid, &serial_id, &value, &scriptpubkey)) @@ -1993,33 +1985,10 @@ static bool run_tx_interactive(struct state *state, * - MUST fail the negotiation if: ... * - the `sats` amount is greater than 2,100,000,000,000,000 (`MAX_MONEY`) */ - out_total = AMOUNT_SAT(0); - for (size_t i = 0; i < psbt->num_outputs; i++) { - struct amount_sat output_amt = - psbt_output_get_amount(psbt, i); - if (!amount_sat_add(&out_total, out_total, - output_amt)) { - open_abort( - state, - "Output amount total overflow " - "(partial sum is %s, current " - "output is %s at output number %d)", - fmt_amount_sat(tmpctx, out_total), - fmt_amount_sat(tmpctx, output_amt), - (int)i); - return false; - } - } - if (!amount_sat_add(&out_total, out_total, amt) || - amount_sat_greater(out_total, - chainparams->max_supply)) { - open_abort(state, - "Adding output amount %s would " - "exceed max supply (current total " - "is %s over %d outputs)", - fmt_amount_sat(tmpctx, amt), - fmt_amount_sat(tmpctx, out_total), - (int)psbt->num_outputs); + error = interactive_tx_add_output_check_max_money( + tmpctx, psbt, amt); + if (error) { + open_abort(state, error); return false; }