diff --git a/channeld/channeld.c b/channeld/channeld.c index e6b91778019d..a38cb50ddbf0 100644 --- a/channeld/channeld.c +++ b/channeld/channeld.c @@ -3945,28 +3945,14 @@ static void check_their_witnesses(struct peer *peer, { struct wally_psbt *checked; struct bitcoin_tx *tx; - bool unified; + const char *bad; size_t j = 0; - if (chainparams->is_elements) - return; - checked = clone_psbt(tmpctx, psbt); for (size_t i = 0; i < checked->num_inputs; i++) { struct wally_psbt_input *in = &checked->inputs[i]; u64 in_serial; - /* Each digest then reads the spent output here, rather than - * hashing the whole prevtx again for every input. */ - if (in->utxo && in->index < in->utxo->num_outputs) { - const struct wally_tx_output *out - = &in->utxo->outputs[in->index]; - psbt_input_set_wit_utxo(checked, i, - tal_dup_arr(tmpctx, u8, - out->script, - out->script_len, 0), - amount_sat(out->satoshi)); - } if (i == splice_funding_index || !psbt_get_serial_id(&in->unknowns, &in_serial) || in_serial % 2 == our_role) @@ -3981,29 +3967,25 @@ static void check_their_witnesses(struct peer *peer, "%zu too many witness elements received", tal_count(inws) - j); - tx = bitcoin_tx_with_psbt(tmpctx, take(checked)); + /* We only know Bitcoin's digests. */ + if (chainparams->is_elements) + return; + tx = psbt_tx_with_spent_outputs(tmpctx, take(checked)); if (!tx) status_failed(STATUS_FAIL_INTERNAL_ERROR, "Could not build splice tx %s to check witnesses", fmt_bitcoin_txid(tmpctx, txid)); - unified = channel_type_sighash(peer->channel->type, SIGHASH_ALL) - & SIGHASH_UNIFIED; - - for (size_t i = 0; i < tx->psbt->num_inputs; i++) { - u64 in_serial; - const char *bad; - - if (i == splice_funding_index - || !psbt_get_serial_id(&tx->psbt->inputs[i].unknowns, - &in_serial) - || in_serial % 2 == our_role) - continue; - bad = psbt_input_witness_invalid(tmpctx, tx, i, unified); - if (bad) - peer_failed_err(peer->pps, &peer->channel_id, - "Bad witness for splice tx %s: %s", - fmt_bitcoin_txid(tmpctx, txid), bad); - } + bad = psbt_their_witnesses_invalid(tmpctx, tx, + our_role == TX_INITIATOR + ? TX_ACCEPTER : TX_INITIATOR, + splice_funding_index, + channel_type_sighash(peer->channel->type, + SIGHASH_ALL) + & SIGHASH_UNIFIED, true); + if (bad) + peer_failed_err(peer->pps, &peer->channel_id, + "Bad witness for splice tx %s: %s", + fmt_bitcoin_txid(tmpctx, txid), bad); } /* Called to finish an ongoing splice OR on restart from channel_reestablish. */ diff --git a/common/psbt_internal.c b/common/psbt_internal.c index 4f31c0f564e3..6ac7740508c4 100644 --- a/common/psbt_internal.c +++ b/common/psbt_internal.c @@ -248,6 +248,20 @@ static const char *check_ecdsa(const tal_t *ctx, const struct bitcoin_tx *tx, } /* A funding output's 2-of-2, as when the peer splices another channel in. */ +static bool is_2of2_script(const u8 *script, struct pubkey *k1, + struct pubkey *k2) +{ + u8 *expected; + + if (tal_bytelen(script) != 71 + || !pubkey_from_der(script + 2, 33, k1) + || !pubkey_from_der(script + 36, 33, k2)) + return false; + expected = bitcoin_redeem_2of2(tmpctx, k1, k2); + return memeq(expected, tal_bytelen(expected), + script, tal_bytelen(script)); +} + static const char *check_p2wsh_2of2(const tal_t *ctx, const struct bitcoin_tx *tx, size_t i, @@ -256,15 +270,9 @@ static const char *check_p2wsh_2of2(const tal_t *ctx, { struct pubkey k1, k2; const char *bad; - u8 *expected; if (stack->num_items != 4 || stack->items[0].witness_len != 0 - || tal_bytelen(script) != 71 - || !pubkey_from_der(script + 2, 33, &k1) - || !pubkey_from_der(script + 36, 33, &k2)) - return tal_fmt(ctx, "P2WSH input %zu is not a 2-of-2 spend", i); - expected = bitcoin_redeem_2of2(tmpctx, &k1, &k2); - if (!memeq(expected, tal_bytelen(expected), script, tal_bytelen(script))) + || !is_2of2_script(script, &k1, &k2)) return tal_fmt(ctx, "P2WSH input %zu is not a 2-of-2 spend", i); /* OP_CHECKMULTISIG takes the signatures in the keys' order. */ @@ -423,3 +431,88 @@ const char *psbt_input_witness_invalid(const tal_t *ctx, return tal_fmt(ctx, "input %zu spends none of P2WPKH, P2WSH or P2TR", i); } + +bool psbt_input_witness_known(const struct bitcoin_tx *tx, size_t i) +{ + const struct wally_psbt *psbt = tx->psbt; + const struct wally_tx_witness_stack *stack; + const struct wally_tx_output *spent; + const struct wally_tx_witness_item *last; + struct sha256 wsh, h; + struct pubkey k1, k2; + u8 *script; + + if (i >= psbt->num_inputs) + return true; + stack = psbt->inputs[i].final_witness; + spent = spent_output(psbt, i); + if (!spent || !stack || stack->num_items == 0) + return true; + + if (is_p2wpkh(spent->script, spent->script_len, NULL)) + return true; + last = &stack->items[stack->num_items - 1]; + /* BIP341: a last item starting 0x50 is an annex, which is + * non-standard, so it's judged (and fails). */ + if (is_p2tr(spent->script, spent->script_len, NULL)) + return stack->num_items == 1 + || (last->witness_len && last->witness[0] == 0x50); + /* Spending any other witness program is non-standard, but for P2A + * with an empty witness, which BOLT 2 doesn't allow either. */ + if (!is_p2wsh(spent->script, spent->script_len, &wsh)) + return true; + + script = tal_dup_arr(tmpctx, u8, last->witness, last->witness_len, 0); + sha256(&h, script, tal_bytelen(script)); + return stack->num_items < 2 + || !sha256_eq(&h, &wsh) + || is_to_remote_anchored_witness_script(script, + tal_bytelen(script)) + || is_2of2_script(script, &k1, &k2); +} + +struct bitcoin_tx *psbt_tx_with_spent_outputs(const tal_t *ctx, + struct wally_psbt *psbt TAKES) +{ + if (!taken(psbt)) + psbt = clone_psbt(tmpctx, psbt); + for (size_t i = 0; i < psbt->num_inputs; i++) { + const struct wally_psbt_input *in = &psbt->inputs[i]; + const struct wally_tx_output *out; + + /* Each digest then reads the spent output here, rather than + * hashing the whole prevtx again for every input. */ + if (!in->utxo || in->index >= in->utxo->num_outputs) + continue; + out = &in->utxo->outputs[in->index]; + psbt_input_set_wit_utxo(psbt, i, + tal_dup_arr(tmpctx, u8, out->script, + out->script_len, 0), + amount_sat(out->satoshi)); + } + return bitcoin_tx_with_psbt(ctx, take(psbt)); +} + +const char *psbt_their_witnesses_invalid(const tal_t *ctx, + const struct bitcoin_tx *tx, + enum tx_role their_role, + int input_index_to_ignore, + bool unified, bool strict) +{ + for (size_t i = 0; i < tx->psbt->num_inputs; i++) { + u64 serial; + const char *bad; + + if (i == input_index_to_ignore + || !psbt_get_serial_id(&tx->psbt->inputs[i].unknowns, + &serial) + || serial % 2 != their_role) + continue; + if (!strict && !psbt_input_witness_known(tx, i)) + continue; + bad = psbt_input_witness_invalid(ctx, tx, i, unified); + if (bad) + return bad; + } + return NULL; +} diff --git a/common/psbt_internal.h b/common/psbt_internal.h index 1b72a3c62998..f61330a19709 100644 --- a/common/psbt_internal.h +++ b/common/psbt_internal.h @@ -53,4 +53,33 @@ const char *psbt_input_witness_invalid(const tal_t *ctx, const struct bitcoin_tx *tx, size_t i, bool unified); +/* psbt_input_witness_known - Can psbt_input_witness_invalid judge it? + * + * False if input @i of @tx->psbt spends an output it doesn't evaluate: a + * P2TR output other than by a key path, or a P2WSH output with any other + * witness script. A missing or empty witness, one that doesn't match its + * P2WSH output, or a spend of any other kind of output (non-standard but + * for P2A with an empty witness) can be judged (invalid). + */ +bool psbt_input_witness_known(const struct bitcoin_tx *tx, size_t i); + +/* psbt_tx_with_spent_outputs - The tx of @psbt, with each input's spent + * output attached from its prevtx, for checking witnesses. NULL if it + * can't be built. */ +struct bitcoin_tx *psbt_tx_with_spent_outputs(const tal_t *ctx, + struct wally_psbt *psbt TAKES); + +/* psbt_their_witnesses_invalid - Check the witnesses of a side's inputs + * + * Returns NULL if every input of @their_role in @tx (but + * @input_index_to_ignore, or -1 for none) has a final witness that + * psbt_input_witness_invalid accepts, otherwise why not. Unless @strict, + * inputs psbt_input_witness_known can't judge are skipped. Bitcoin only. + */ +const char *psbt_their_witnesses_invalid(const tal_t *ctx, + const struct bitcoin_tx *tx, + enum tx_role their_role, + int input_index_to_ignore, + bool unified, bool strict); + #endif /* LIGHTNING_COMMON_PSBT_INTERNAL_H */ diff --git a/common/test/run-psbt_input_witness.c b/common/test/run-psbt_input_witness.c index c815242a243f..15399de6be68 100644 --- a/common/test/run-psbt_input_witness.c +++ b/common/test/run-psbt_input_witness.c @@ -8,71 +8,16 @@ #include "../amount.c" #include "../psbt_internal.c" #include "../psbt_open.c" +#include "../../wire/fromwire.c" +#include "../../wire/towire.c" /* 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_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_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 pseudorand_u64_ */ uint64_t pseudorand_u64_(uint64_t *offset UNNEEDED) { fprintf(stderr, "pseudorand_u64_ 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 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_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 */ /* BIP341 wallet-test-vectors.json, keyPathSpending[0]. */ @@ -231,6 +176,12 @@ static bool ok(struct bitcoin_tx *tx, u8 **stack, bool unified) return psbt_input_witness_invalid(tmpctx, tx, 0, unified) == NULL; } +static bool known(struct bitcoin_tx *tx, u8 **stack) +{ + bitcoin_tx_input_set_witness(tx, 0, stack); + return psbt_input_witness_known(tx, 0); +} + static void test_p2wpkh(void) { struct privkey k = privkey_of(0x01), other = privkey_of(0x02); @@ -258,6 +209,11 @@ static void test_p2wpkh(void) false)); assert(!ok(tx, stack_of(2, sig_item(tx, code, &other, SIGHASH_ALL), qk), false)); + assert(known(tx, stack_of(2, sig_item(tx, code, &other, SIGHASH_ALL), + qk))); + /* An empty witness is judged, and fails. */ + assert(known(tx, stack_of(0))); + assert(!ok(tx, stack_of(0), true)); } static void test_p2wsh_2of2(void) @@ -295,6 +251,8 @@ static void test_p2wsh_2of2(void) assert(!ok(tx, stack_of(4, junk, sa, sb, tal_dup_talarr(tmpctx, u8, script)), unified)); + assert(known(tx, stack_of(4, junk, sa, sb, + tal_dup_talarr(tmpctx, u8, script)))); } } @@ -321,6 +279,8 @@ static void test_p2wsh_to_remote(void) sig = sig_item(tx, script, &k, SIGHASH_ALL); assert(!ok(tx, stack_of(2, sig, tal_dup_talarr(tmpctx, u8, script)), false)); + assert(known(tx, stack_of(2, sig, + tal_dup_talarr(tmpctx, u8, script)))); tx = spend(NULL, script, csv | 0x80000000); sig = sig_item(tx, script, &k, SIGHASH_ALL); @@ -350,6 +310,93 @@ static void test_p2wsh_unknown(void) assert(!ok(tx, stack_of(2, sig_item(tx, script, &k, SIGHASH_ALL), tal_dup_talarr(tmpctx, u8, script)), false)); + /* So it isn't judged, unless the script isn't the output's. */ + assert(!known(tx, stack_of(2, sig_item(tx, script, &k, SIGHASH_ALL), + tal_dup_talarr(tmpctx, u8, script)))); + assert(known(tx, stack_of(2, sig_item(tx, script, &k, SIGHASH_ALL), + bitcoin_redeem_2of2(tmpctx, &p, &p)))); + assert(known(tx, stack_of(1, tal_dup_talarr(tmpctx, u8, script)))); +} + +/* Spends of other witness programs are non-standard, so judged (bad). */ +static void test_other_programs(void) +{ + /* OP_2 <32 bytes>, and P2A (OP_1 <0x4e73>). */ + u8 *v2 = tal_arrz(tmpctx, u8, 34), *p2a = tal_arr(tmpctx, u8, 4); + struct bitcoin_tx *tx; + + v2[0] = 0x52; + v2[1] = 32; + tx = spend(v2, NULL, 0xfffffffd); + assert(known(tx, stack_of(1, tal_arrz(tmpctx, u8, 1)))); + assert(!ok(tx, stack_of(1, tal_arrz(tmpctx, u8, 1)), true)); + + memcpy(p2a, "\x51\x02\x4e\x73", 4); + tx = spend(p2a, NULL, 0xfffffffd); + assert(known(tx, stack_of(0))); + assert(!ok(tx, stack_of(0), true)); + assert(known(tx, stack_of(1, tal_arrz(tmpctx, u8, 1)))); + assert(!ok(tx, stack_of(1, tal_arrz(tmpctx, u8, 1)), true)); +} + +static void test_p2tr_script_path(void) +{ + struct bitcoin_tx *tx = tx_with_witness(inputs[0].sig, inputs[0].input, + false); + u8 *sig = tal_hexdata(tmpctx, inputs[0].sig, strlen(inputs[0].sig)); + /* A tapscript leaf's control block, for an internal key. */ + u8 *control = tal_arrz(tmpctx, u8, 33); + u8 **stack = stack_of(3, sig, tal_arrz(tmpctx, u8, 1), control); + u8 *annex = tal_arrz(tmpctx, u8, 1); + + control[0] = 0xc0; + assert(psbt_input_witness_known(tx, inputs[0].input)); + bitcoin_tx_input_set_witness(tx, inputs[0].input, stack); + assert(!psbt_input_witness_known(tx, inputs[0].input)); + assert(psbt_input_witness_invalid(tmpctx, tx, inputs[0].input, true)); + + /* A key path signature with an annex is judged, and fails. */ + annex[0] = 0x50; + bitcoin_tx_input_set_witness(tx, inputs[0].input, + stack_of(2, sig, annex)); + assert(psbt_input_witness_known(tx, inputs[0].input)); + assert(psbt_input_witness_invalid(tmpctx, tx, inputs[0].input, true)); +} + +/* Only their inputs are checked, and unknown ones only if strict. */ +static void test_their_witnesses(void) +{ + struct privkey k = privkey_of(0x07); + struct pubkey p = pubkey_of(&k); + u8 *script = bitcoin_wscript_to_remote_anchored(tmpctx, &p, 1); + struct bitcoin_tx *tx; + + /* OP_CHECKSIG, which we don't judge. */ + tal_resize(&script, 1 + PUBKEY_CMPR_LEN + 1); + script[0] = PUBKEY_CMPR_LEN; + pubkey_to_der(script + 1, &p); + script[1 + PUBKEY_CMPR_LEN] = 0xac; + tx = spend(NULL, script, 0xfffffffd); + psbt_input_set_serial_id(tx->psbt, &tx->psbt->inputs[0], 1); + bitcoin_tx_input_set_witness(tx, 0, + stack_of(2, sig_item(tx, script, &k, + SIGHASH_ALL), + tal_dup_talarr(tmpctx, u8, script))); + assert(!psbt_their_witnesses_invalid(tmpctx, tx, TX_ACCEPTER, -1, + true, false)); + assert(psbt_their_witnesses_invalid(tmpctx, tx, TX_ACCEPTER, -1, + true, true)); + assert(!psbt_their_witnesses_invalid(tmpctx, tx, TX_INITIATOR, -1, + true, true)); + assert(!psbt_their_witnesses_invalid(tmpctx, tx, TX_ACCEPTER, 0, + true, true)); + + /* A known input which is bad fails either way. */ + bitcoin_tx_input_set_witness(tx, 0, stack_of(1, tal_dup_talarr(tmpctx, + u8, + script))); + assert(psbt_their_witnesses_invalid(tmpctx, tx, TX_ACCEPTER, -1, + true, false)); } int main(int argc, char *argv[]) @@ -371,6 +418,7 @@ int main(int argc, char *argv[]) inputs[i].input, unified); assert(allowed == (bad == NULL)); + assert(psbt_input_witness_known(tx, inputs[i].input)); if (allowed) { tx = tx_with_witness(inputs[i].sig, inputs[i].input, true); @@ -385,6 +433,9 @@ int main(int argc, char *argv[]) test_p2wsh_2of2(); test_p2wsh_to_remote(); test_p2wsh_unknown(); + test_other_programs(); + test_p2tr_script_path(); + test_their_witnesses(); common_shutdown(); return 0; diff --git a/doc/blake2b-upgrade.md b/doc/blake2b-upgrade.md index 730067447a86..89e53f24ec4e 100644 --- a/doc/blake2b-upgrade.md +++ b/doc/blake2b-upgrade.md @@ -130,8 +130,11 @@ Signatures a node makes **alone** opt in past the activation: on-chain sends, funding inputs it contributes, sweeps of its own outputs, anchor spends, and its own half of any second-level transaction it broadcasts. These are `ALL | UNIFIED` (`0x21`), and `0x21` also for taproot key-path spends, which -use a 65-byte signature because `SIGHASH_DEFAULT` cannot carry the bit. No peer -verifies these, so no peer has to agree. +use a 65-byte signature because `SIGHASH_DEFAULT` cannot carry the bit. A peer +checks our inputs to a splice, or to a dual-funded open before it has signed, +when our `tx_signatures` arrive (after it has signed an open, only those of a +kind it can evaluate), and accepts `0x21` on a channel with +`option_unified_sigs`; nothing else here is verified by a peer. One exception, and it is deliberate: the justice transaction handed to a watchtower keeps the pre-activation hash type, because the tower reconstructs diff --git a/openingd/dualopend.c b/openingd/dualopend.c index e8ffe006b086..5f70a9d58a52 100644 --- a/openingd/dualopend.c +++ b/openingd/dualopend.c @@ -1375,6 +1375,9 @@ static void handle_tx_sigs(struct state *state, const u8 *msg) struct bitcoin_txid txid; const struct witness **witnesses; struct tx_state *tx_state = state->tx_state; + struct bitcoin_tx *tx; + const char *bad; + size_t j = 0; struct tlv_txsigs_tlvs *txsig_tlvs = tlv_txsigs_tlvs_new(tmpctx); if (!fromwire_tx_signatures(tmpctx, msg, &cid, &txid, @@ -1431,8 +1434,16 @@ static void handle_tx_sigs(struct state *state, const u8 *msg) "tx_signatures sent before commitment sigs %s", tal_hex(msg, msg)); + /* A retransmit: lightningd has their witnesses already, and may have + * broadcast with them. */ + if (tx_state->remote_funding_sigs_rcvd) { + status_debug("Ignoring repeated tx_signatures for %s", + fmt_bitcoin_txid(tmpctx, &txid)); + return; + } + /* We put the PSBT + sigs all together */ - for (size_t i = 0, j = 0; i < tx_state->psbt->num_inputs; i++) { + for (size_t i = 0; i < tx_state->psbt->num_inputs; i++) { struct wally_psbt_input *in = &tx_state->psbt->inputs[i]; u64 in_serial; @@ -1453,6 +1464,31 @@ static void handle_tx_sigs(struct state *state, const u8 *msg) psbt_finalize_input(tx_state->psbt, in, witnesses[j]); j++; } + if (j != tal_count(witnesses)) + open_err_warn(state, "%zu too many witnesses in %s", + tal_count(witnesses) - j, tal_hex(msg, msg)); + + /* Otherwise we may sign a funding tx that won't relay, while they + * hold a valid copy (with the same txid). Once we've sent our + * signatures they have that anyway, so inputs we can't evaluate, + * such as other P2WSH scripts, are then left to bitcoind. */ + if (!chainparams->is_elements) { + tx = psbt_tx_with_spent_outputs(tmpctx, tx_state->psbt); + if (!tx) + status_failed(STATUS_FAIL_INTERNAL_ERROR, + "Could not build funding tx %s to check" + " witnesses", + fmt_bitcoin_txid(tmpctx, &txid)); + bad = psbt_their_witnesses_invalid(tmpctx, tx, their_role(state), + -1, + channel_type_sighash(state->channel_type, + SIGHASH_ALL) + & SIGHASH_UNIFIED, + !tx_state->local_funding_sigs_sent); + if (bad) + open_err_fatal(state, "Bad witness for funding tx %s: %s", + fmt_bitcoin_txid(tmpctx, &txid), bad); + } tx_state->remote_funding_sigs_rcvd = true; /* Send to the controller, who will broadcast the funding_tx diff --git a/tests/plugins/df_bad_witness.py b/tests/plugins/df_bad_witness.py new file mode 100755 index 000000000000..7af0ba3df0f7 --- /dev/null +++ b/tests/plugins/df_bad_witness.py @@ -0,0 +1,66 @@ +#!/usr/bin/env python3 +"""Puts an input into a dual-funded open we accept, then signs it with a +corrupted signature.""" + +from pyln.client import Plugin +import base64 + +plugin = Plugin() + + +def compact_size(b, i): + n = b[i] + if n < 0xfd: + return n, i + 1 + size = {0xfd: 2, 0xfe: 4, 0xff: 8}[n] + return int.from_bytes(b[i + 1:i + 1 + size], 'little'), i + 1 + size + + +def corrupt_first_partial_sig(psbt): + """Flip a bit in the r value of the first input's partial signature.""" + b = bytearray(base64.b64decode(psbt)) + i = 5 + in_global = True + while i < len(b): + keylen, i = compact_size(b, i) + if keylen == 0: + in_global = False + continue + keytype = b[i] + i += keylen + vallen, i = compact_size(b, i) + # PSBT_IN_PARTIAL_SIG: input maps come before output maps. + if not in_global and keytype == 0x02: + b[i + 7] ^= 1 + return base64.b64encode(bytes(b)).decode() + i += vallen + raise ValueError("no partial signature") + + +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + psbt = plugin.rpc.call('fundpsbt', { + 'satoshi': plugin.amount, + 'feerate': '{}perkw'.format(openchannel2['funding_feerate_per_kw']), + 'startweight': 0, + 'min_witness_weight': 110, + 'excess_as_change': True, + 'locktime': openchannel2['locktime'], + 'nonwrapped': True})['psbt'] + return {'result': 'continue', 'psbt': psbt, + 'our_funding_msat': plugin.amount * 1000} + + +@plugin.hook('openchannel2_sign') +def on_openchannel2_sign(openchannel2_sign, plugin, **kwargs): + signed = plugin.rpc.signpsbt(openchannel2_sign['psbt'])['signed_psbt'] + return {'result': 'continue', 'psbt': corrupt_first_partial_sig(signed)} + + +@plugin.init() +def init(options, configuration, plugin): + plugin.amount = int(options['bad-witness-amount']) + + +plugin.add_option('bad-witness-amount', '1000000', 'Satoshis we put in') +plugin.run() diff --git a/tests/plugins/df_external_input.py b/tests/plugins/df_external_input.py new file mode 100755 index 000000000000..96fdf6db28eb --- /dev/null +++ b/tests/plugins/df_external_input.py @@ -0,0 +1,58 @@ +#!/usr/bin/env python3 +"""Puts an input from bitcoind's wallet into a dual-funded open we accept, +and has bitcoind sign it.""" + +from pyln.client import Plugin +import base64 +import json +import urllib.request + +plugin = Plugin() + + +def bitcoind(method, *params): + req = urllib.request.Request(plugin.url, + data=json.dumps({'jsonrpc': '1.0', + 'id': 'df_external_input', + 'method': method, + 'params': list(params)}).encode()) + req.add_header('Authorization', + 'Basic ' + base64.b64encode(plugin.auth.encode()).decode()) + with urllib.request.urlopen(req) as r: + res = json.load(r) + if res.get('error'): + raise ValueError(res['error']) + return res['result'] + + +@plugin.method('dfext_setup') +def setup(plugin, utxo, change, amount, url, auth): + """The input, change and amount we put in, and bitcoind's wallet RPC.""" + plugin.utxo = utxo + plugin.change = change + plugin.amount = amount + plugin.url = url + plugin.auth = auth + return {} + + +@plugin.hook('openchannel2') +def on_openchannel2(openchannel2, plugin, **kwargs): + psbt = bitcoind('createpsbt', [plugin.utxo], [plugin.change], + openchannel2['locktime']) + psbt = bitcoind('walletprocesspsbt', psbt, False)['psbt'] + return {'result': 'continue', + 'psbt': plugin.rpc.setpsbtversion(psbt, 2)['psbt'], + 'our_funding_msat': plugin.amount * 1000} + + +@plugin.hook('openchannel2_sign') +def on_openchannel2_sign(openchannel2_sign, plugin, **kwargs): + psbt = plugin.rpc.setpsbtversion(openchannel2_sign['psbt'], 0)['psbt'] + psbt = bitcoind('walletprocesspsbt', psbt)['psbt'] + psbt = bitcoind('finalizepsbt', psbt, False)['psbt'] + return {'result': 'continue', + 'psbt': plugin.rpc.setpsbtversion(psbt, 2)['psbt']} + + +plugin.run() diff --git a/tests/test_opening.py b/tests/test_opening.py index 81c290dc86cc..5008d8c86dc5 100644 --- a/tests/test_opening.py +++ b/tests/test_opening.py @@ -5,10 +5,11 @@ only_one, wait_for, sync_blockheight, first_channel_id, calc_lease_fee, check_coin_moves, scriptpubkey_addr, TIMEOUT ) -from pyln.testing.utils import FUNDAMOUNT +from pyln.testing.utils import FUNDAMOUNT, BITCOIND_CONFIG from bitcoin.rpc import JSONRPCError from pathlib import Path +import base64 import coincurve import hashlib import hmac @@ -39,6 +40,27 @@ def _dsha256(b): return hashlib.sha256(hashlib.sha256(b).digest()).digest() +def _multisig_utxo(bitcoind): + """A 600000 sat 1-of-1 multisig P2WSH output in bitcoind's wallet.""" + # A regtest WIF for a fixed key, in bitcoind's wallet as wsh(multi(1,...)). + alphabet = '123456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghijkmnopqrstuvwxyz' + payload = b'\xef' + bytes([0x55] * 32) + b'\x01' + payload += _dsha256(payload)[:4] + n, wif = int.from_bytes(payload, 'big'), '' + while n: + n, r = divmod(n, 58) + wif = alphabet[r] + wif + desc = 'wsh(multi(1,{}))'.format(wif) + desc += '#' + bitcoind.rpc.getdescriptorinfo(desc)['checksum'] + assert only_one(bitcoind.rpc.importdescriptors([{'desc': desc, 'timestamp': 'now'}]))['success'] + addr = only_one(bitcoind.rpc.deriveaddresses(desc)) + txid = bitcoind.rpc.sendtoaddress(addr, 0.006) + bitcoind.generate_block(1, wait_for_mempool=txid) + vout = only_one([o['n'] for o in bitcoind.rpc.getrawtransaction(txid, True)['vout'] + if o['scriptPubKey'].get('address') == addr]) + return {'txid': txid, 'vout': vout} + + def hkdf_sha256(ikm, salt, info, length): """RFC5869 HKDF-SHA256, matching ccan's hkdf_sha256 (salt is the HMAC key).""" prk = hmac.new(salt if salt is not None else b'', ikm, hashlib.sha256).digest() @@ -1893,6 +1915,182 @@ def test_v2_open_accepter_psbt_without_locktime(node_factory, bitcoind): assert tx['locktime'] != 0 and len(tx['vin']) == 2 +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.openchannel('v2') +def test_v2_open_bad_input_witness_from_accepter(node_factory, bitcoind): + """A bad witness on the accepter's funding input fails the channel. + + l1 accepts l2's open, puts in an input and corrupts its signature. It + sends its tx_signatures first, so l2 finds out before sending its own. + """ + plugin = os.path.join(os.getcwd(), 'tests/plugins/df_bad_witness.py') + l1, l2 = node_factory.get_nodes(2, opts=[{'plugin': plugin, + 'disable-plugin': 'funder', + 'allow_warning': True}, + {'allow_warning': True}]) + l1.fundwallet(2000000) + l2.fundwallet(2000000) + l2.rpc.connect(l1.info['id'], 'localhost', l1.port) + + psbt = l2.rpc.fundpsbt(500000, '7500perkw', 250)['psbt'] + start = l2.rpc.openchannel_init(l1.info['id'], 500000, psbt) + update = l2.rpc.openchannel_update(start['channel_id'], start['psbt']) + while not update['commitments_secured']: + update = l2.rpc.openchannel_update(start['channel_id'], update['psbt']) + assert len(bitcoind.rpc.decodepsbt(l2.rpc.setpsbtversion(update['psbt'], 0)['psbt'])['inputs']) == 2 + + line = l2.daemon.wait_for_log(r'Bad witness for funding tx [0-9a-f]{64}: input [0-9]+ has an invalid signature') + funding_txid = re.search(r'funding tx ([0-9a-f]{64})', line).group(1) + assert not l2.daemon.is_in_log(r'peer_out WIRE_TX_SIGNATURES') + wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL') + with pytest.raises(RpcError): + l2.rpc.openchannel_signed(start['channel_id'], + l2.rpc.signpsbt(update['psbt'])['signed_psbt']) + assert not l2.daemon.is_in_log(r'peer_out WIRE_TX_SIGNATURES') + assert not l2.daemon.is_in_log('Broadcasting funding tx {}'.format(funding_txid)) + + +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.openchannel('v2') +def test_v2_open_bad_input_witness_from_opener(node_factory, bitcoind): + """A bad witness on the opener's funding input fails the channel. + + l1 opens and corrupts the signature on its input. l2 has signed + already, so l1 can still broadcast a valid copy, and l2 has to get its + funds back through its commitment. + """ + amount = 500000 + l1, l2 = node_factory.get_nodes(2, opts=[{'allow_warning': True}, + {'allow_warning': True, + 'funder-policy': 'fixed', + 'funder-policy-mod': 1000000, + 'funder-lease-requests-only': False}]) + l1.fundwallet(2000000) + l2.fundwallet(2000000) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + def decode(psbt): + return bitcoind.rpc.decodepsbt(l1.rpc.setpsbtversion(psbt, 0)['psbt']) + + psbt = l1.rpc.fundpsbt(amount, '7500perkw', 250)['psbt'] + start = l1.rpc.openchannel_init(l2.info['id'], amount, psbt) + chan_id = start['channel_id'] + update = l1.rpc.openchannel_update(chan_id, start['psbt']) + while not update['commitments_secured']: + update = l1.rpc.openchannel_update(chan_id, update['psbt']) + assert len(decode(update['psbt'])['inputs']) == 2 + signed = l1.rpc.signpsbt(update['psbt'])['signed_psbt'] + + # Flip a bit in the r value of l1's signature. + sig = bytes.fromhex(next(iter(only_one([i for i in decode(signed)['inputs'] + if 'partial_signatures' in i])['partial_signatures'].values()))) + raw = base64.b64decode(signed) + assert raw.count(sig) == 1 + bad = sig[:7] + bytes([sig[7] ^ 1]) + sig[8:] + try: + l1.rpc.openchannel_signed(chan_id, base64.b64encode(raw.replace(sig, bad)).decode()) + except RpcError: + pass + + line = l2.daemon.wait_for_log(r'Bad witness for funding tx [0-9a-f]{64}: input [0-9]+ has an invalid signature') + funding_txid = re.search(r'funding tx ([0-9a-f]{64})', line).group(1) + assert l2.daemon.is_in_log(r'dualopend-chan#[0-9]+: peer_out WIRE_TX_SIGNATURES') + assert not l2.daemon.is_in_log('Broadcasting funding tx {}'.format(funding_txid)) + + # l1 has l2's signatures, so it can broadcast with a good one of its own. + line = l1.daemon.wait_for_log(r'Broadcasting funding tx {} for channel .*\. [0-9a-f]+'.format(funding_txid)) + tx = line.split('. ')[-1].strip() + assert tx.count(bad.hex()) == 1 + bitcoind.rpc.sendrawtransaction(tx.replace(bad.hex(), sig.hex())) + bitcoind.generate_block(1, wait_for_mempool=funding_txid) + # A commitment spends it, in the same block if one reached the mempool + # in time. + block = bitcoind.rpc.getblock(bitcoind.rpc.getbestblockhash(), 2) + if not any(i.get('txid') == funding_txid for t in block['tx'] for i in t['vin']): + bitcoind.generate_block(1, wait_for_mempool=1) + wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'ONCHAIN') + + +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.openchannel('v2') +def test_v2_open_unrecognized_input_script(node_factory, bitcoind): + """A peer input whose script we don't evaluate is left to bitcoind. + + l1 funds its open from a 1-of-1 multisig P2WSH output in bitcoind's + wallet. That isn't a script we check, and l2 has signed already, so + it lets the witness through and the channel opens. + """ + l1, l2 = node_factory.get_nodes(2, opts=[{}, + {'funder-policy': 'fixed', + 'funder-policy-mod': 1000000, + 'funder-lease-requests-only': False}]) + l2.fundwallet(2000000) + l1.rpc.connect(l2.info['id'], 'localhost', l2.port) + + # 500000 sat to the channel, change back to bitcoind, the rest is fee. + psbt = bitcoind.rpc.createpsbt([_multisig_utxo(bitcoind)], + [{bitcoind.rpc.getnewaddress(): 0.00095}]) + psbt = bitcoind.rpc.walletprocesspsbt(psbt, False)['psbt'] + start = l1.rpc.openchannel_init(l2.info['id'], 500000, psbt, + funding_feerate='3000perkw') + chan_id = start['channel_id'] + update = l1.rpc.openchannel_update(chan_id, start['psbt']) + while not update['commitments_secured']: + update = l1.rpc.openchannel_update(chan_id, update['psbt']) + + signed = bitcoind.rpc.walletprocesspsbt(l1.rpc.setpsbtversion(update['psbt'], 0)['psbt'])['psbt'] + signed = bitcoind.rpc.finalizepsbt(signed, False)['psbt'] + signed = l1.rpc.setpsbtversion(signed, 2)['psbt'] + l1.rpc.openchannel_signed(chan_id, signed) + + bitcoind.generate_block(6, wait_for_mempool=1) + for n in (l1, l2): + n.daemon.wait_for_log(' to CHANNELD_NORMAL') + assert not l2.daemon.is_in_log('Bad witness') + + +@unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') +@pytest.mark.openchannel('v2') +def test_v2_open_unrecognized_input_before_we_sign(node_factory, bitcoind): + """Before we sign, a peer input we can't evaluate fails the channel. + + l1 accepts l2's open with an input from a 1-of-1 multisig P2WSH output + in bitcoind's wallet, and bitcoind signs it. l1 signs first, so l2 has + not signed yet: it fails the channel rather than sign a funding tx it + can't check. + """ + plugin = os.path.join(os.getcwd(), 'tests/plugins/df_external_input.py') + l1, l2 = node_factory.get_nodes(2, opts=[{'plugin': plugin, + 'disable-plugin': 'funder', + 'allow_warning': True}, + {'allow_warning': True}]) + l2.fundwallet(2000000) + l1.rpc.call('dfext_setup', {'utxo': _multisig_utxo(bitcoind), + 'change': {bitcoind.rpc.getnewaddress(): 0.00095}, + 'amount': 500000, + 'url': 'http://127.0.0.1:{}/wallet/{}'.format(bitcoind.rpcport, 'lightningd-tests'), + 'auth': '{}:{}'.format(BITCOIND_CONFIG['rpcuser'], BITCOIND_CONFIG['rpcpassword'])}) + l2.rpc.connect(l1.info['id'], 'localhost', l1.port) + + psbt = l2.rpc.fundpsbt(500000, '3000perkw', 250, excess_as_change=True)['psbt'] + start = l2.rpc.openchannel_init(l1.info['id'], 500000, psbt, + funding_feerate='3000perkw') + update = l2.rpc.openchannel_update(start['channel_id'], start['psbt']) + while not update['commitments_secured']: + update = l2.rpc.openchannel_update(start['channel_id'], update['psbt']) + assert len(bitcoind.rpc.decodepsbt(l2.rpc.setpsbtversion(update['psbt'], 0)['psbt'])['inputs']) == 2 + + line = l2.daemon.wait_for_log(r'Bad witness for funding tx [0-9a-f]{64}: P2WSH input [0-9]+ is not a 2-of-2 spend') + funding_txid = re.search(r'funding tx ([0-9a-f]{64})', line).group(1) + l1.daemon.wait_for_log(r'peer_out WIRE_TX_SIGNATURES') + wait_for(lambda: only_one(l2.rpc.listpeerchannels()['channels'])['state'] == 'AWAITING_UNILATERAL') + with pytest.raises(RpcError): + l2.rpc.openchannel_signed(start['channel_id'], + l2.rpc.signpsbt(update['psbt'])['signed_psbt']) + assert not l2.daemon.is_in_log(r'peer_out WIRE_TX_SIGNATURES') + assert not l2.daemon.is_in_log('Broadcasting funding tx {}'.format(funding_txid)) + + @unittest.skipIf(TEST_NETWORK != 'regtest', 'elementsd doesnt yet support PSBT features we need') @pytest.mark.openchannel('v2') def test_v2_funding_spent_before_lockin(node_factory, bitcoind):