diff --git a/.github/actions/set-monero-env/action.yml b/.github/actions/set-monero-env/action.yml index b1d6735bf7..5912109d9c 100644 --- a/.github/actions/set-monero-env/action.yml +++ b/.github/actions/set-monero-env/action.yml @@ -15,7 +15,7 @@ runs: echo "DEPS_BUILD_LINUX=autoconf nsis mingw-w64 build-essential pkg-config libtool ccache make cmake gcc g++ git curl lbzip2 gperf g++-mingw-w64-x86-64" >> $GITHUB_ENV # Build tooling (macos) - echo "DEPS_BUILD_MACOS=autoconf automake pkg-config libtool ccache make cmake gcc git curl lbzip2 gperf" >> $GITHUB_ENV + echo "DEPS_BUILD_MACOS=autoconf automake pkg-config libtool ccache make cmake gcc git curl gperf" >> $GITHUB_ENV # APT configuration for better reliability echo 'APT_SET_CONF_COMMAND<; using MoneroSysAccountTagsPair = std::pair; void monero_sys_account_tags_pair_dtor(MoneroSysAccountTagsPair* p) asm("_ZNSt6__ndk14pairINS_3mapINS_12basic_stringIcNS_11char_traitsIcEENS_9allocatorIcEEEES7_NS_4lessIS7_EENS5_INS0_IKS7_S7_EEEEEENS_6vectorIS7_NS5_IS7_EEEEED1Ev"); __attribute__((weak)) void monero_sys_account_tags_pair_dtor(MoneroSysAccountTagsPair* p) { p->~MoneroSysAccountTagsPair(); } +#else +// The following is a hack to ensure the linker includes the pair destructor in the binary +static std::pair _monero_sys_pair_instantiation; #endif namespace Monero diff --git a/swap-core/src/bitcoin/lock.rs b/swap-core/src/bitcoin/lock.rs index d8e3e62180..363f09df3c 100644 --- a/swap-core/src/bitcoin/lock.rs +++ b/swap-core/src/bitcoin/lock.rs @@ -54,37 +54,36 @@ impl TxLock { B: PublicKey, btc: Amount, ) -> Result { - let shared_output_candidate = match psbt.unsigned_tx.output.as_slice() { - [shared_output_candidate, _] if shared_output_candidate.value == btc => { - shared_output_candidate - } - [_, shared_output_candidate] if shared_output_candidate.value == btc => { - shared_output_candidate - } - // A single output is possible if Bob funds without any change necessary - [shared_output_candidate] if shared_output_candidate.value == btc => { - shared_output_candidate - } - [_, _] => { - bail!("Neither of the two provided outputs pays the right amount!"); - } - [_] => { - bail!("The provided output does not pay the right amount!"); - } - other => { - let num_outputs = other.len(); - bail!( - "PSBT has {} outputs, expected one or two. Something is fishy!", - num_outputs - ); - } - }; + if psbt.unsigned_tx.output.len() > 2 { + bail!( + "PSBT has {} outputs, expected at most two", + psbt.unsigned_tx.output.len() + ); + } let descriptor = build_shared_output_descriptor(A.0, B.0)?; let legit_shared_output_script = descriptor.script_pubkey(); - if shared_output_candidate.script_pubkey != legit_shared_output_script { - bail!("Output script is not a shared output") + let shared_outputs = psbt + .unsigned_tx + .output + .iter() + .filter(|output| output.script_pubkey == legit_shared_output_script) + .collect::>(); + + let [shared_output] = shared_outputs.as_slice() else { + bail!( + "PSBT must have exactly one output paying to the shared descriptor, found {}", + shared_outputs.len() + ); + }; + + if shared_output.value != btc { + bail!( + "Shared output pays {} but the agreed amount is {}", + shared_output.value, + btc + ); } Ok(TxLock { @@ -253,6 +252,57 @@ mod tests { result.expect_err("PSBT to be invalid"); } + #[tokio::test] + async fn given_two_outputs_pay_to_shared_descriptor_then_reconstructing_txlock_fails() { + let (A, B, wallet) = setup().await; + let agreed_amount = Amount::from_sat(10000); + let spending_fee = Amount::from_sat(1000); + + let mut psbt = bob_make_psbt(A, B, &wallet, agreed_amount, spending_fee).await; + + let descriptor = build_shared_output_descriptor(A.0, B.0).unwrap(); + let dust_shared_output = TxOut { + value: Amount::from_sat(1), + script_pubkey: descriptor.script_pubkey(), + }; + psbt.unsigned_tx.output.insert(0, dust_shared_output); + + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect_err("PSBT with two shared outputs must be rejected"); + } + + #[test] + fn given_more_than_two_outputs_when_reconstructing_txlock_then_fails() { + let (A, B) = alice_and_bob(); + let agreed_amount = Amount::from_sat(10000); + let descriptor = build_shared_output_descriptor(A.0, B.0).unwrap(); + let psbt = Psbt::from_unsigned_tx(Transaction { + version: bitcoin::transaction::Version(2), + lock_time: PackedLockTime::ZERO, + input: vec![], + output: vec![ + TxOut { + value: agreed_amount, + script_pubkey: descriptor.script_pubkey(), + }, + TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::new(), + }, + TxOut { + value: Amount::from_sat(1), + script_pubkey: ScriptBuf::new(), + }, + ], + }) + .unwrap(); + + let result = TxLock::from_psbt(psbt, A, B, agreed_amount); + + result.expect_err("PSBT with more than two outputs must be rejected"); + } + #[tokio::test] async fn given_bob_is_sending_to_a_bad_output_reconstructing_txlock_then_fails() { let (A, B, wallet) = setup().await; diff --git a/swap-machine/src/common/mod.rs b/swap-machine/src/common/mod.rs index d8c3f0b670..a96d14a687 100644 --- a/swap-machine/src/common/mod.rs +++ b/swap-machine/src/common/mod.rs @@ -7,7 +7,7 @@ use async_trait::async_trait; use libp2p::{Multiaddr, PeerId}; use rust_decimal::prelude::FromPrimitive; use serde::{Deserialize, Serialize}; -use sha2::Sha256; +use sha2::{Digest, Sha256}; use sigma_fun::HashTranscript; use sigma_fun::ext::dl_secp256k1_ed25519_eq::{CrossCurveDLEQ, CrossCurveDLEQProof}; use std::convert::TryInto; @@ -16,12 +16,30 @@ use swap_core::bitcoin; use swap_core::monero::{self, MoneroAddressPool}; use uuid::Uuid; +/// BIP-341 NUMS point `H = lift_x(SHA256(uncompressed_encoding(G)))`: +static PEDERSEN_BLINDING_H_SECP256K1: LazyLock = LazyLock::new(|| { + let generator = (*ecdsa_fun::fun::G).normalize().to_bytes_uncompressed(); + let x_coordinate = Sha256::digest(generator).into(); + + ecdsa_fun::fun::Point::::from_xonly_bytes(x_coordinate) + .expect("SHA-256 of the uncompressed secp256k1 generator is a valid x-coordinate") + .normalize() +}); + +fn pedersen_blinding_h_ed25519() -> curve25519_dalek_ng::edwards::EdwardsPoint { + curve25519_dalek_ng::edwards::CompressedEdwardsY::from_slice( + &monero_oxide_wallet::ed25519::CompressedPoint::H.to_bytes(), + ) + .decompress() + .expect("Monero Pedersen H is a valid ed25519 point") +} + pub static CROSS_CURVE_PROOF_SYSTEM: LazyLock< CrossCurveDLEQ>, > = LazyLock::new(|| { CrossCurveDLEQ::>::new( - (*ecdsa_fun::fun::G).normalize(), - curve25519_dalek_ng::constants::ED25519_BASEPOINT_POINT, + *PEDERSEN_BLINDING_H_SECP256K1, + pedersen_blinding_h_ed25519(), ) }); @@ -346,6 +364,83 @@ pub trait Database { mod tests { use super::*; use bitcoin_wallet::{MIN_ABSOLUTE_TX_FEE, MIN_ABSOLUTE_TX_FEE_SATS}; + use rand::SeedableRng; + + #[test] + fn pedersen_blinding_generators_are_not_the_curve_generators() { + let _force_init = &*CROSS_CURVE_PROOF_SYSTEM; + + let secp_generator = (*ecdsa_fun::fun::G).normalize(); + let ed25519_generator = curve25519_dalek_ng::constants::ED25519_BASEPOINT_POINT; + let blinding_h_ed25519 = pedersen_blinding_h_ed25519(); + + assert_ne!( + *PEDERSEN_BLINDING_H_SECP256K1, secp_generator, + "secp256k1 Pedersen blinding generator must differ from the curve generator" + ); + assert_ne!( + blinding_h_ed25519, ed25519_generator, + "ed25519 Pedersen blinding generator must differ from the curve generator" + ); + } + + #[test] + fn secp256k1_pedersen_blinding_generator_matches_bip341() { + const BIP341_NUMS_X_COORDINATE: [u8; 32] = [ + 0x50, 0x92, 0x9b, 0x74, 0xc1, 0xa0, 0x49, 0x54, 0xb7, 0x8b, 0x4b, 0x60, 0x35, 0xe9, + 0x7a, 0x5e, 0x07, 0x8a, 0x5a, 0x0f, 0x28, 0xec, 0x96, 0xd5, 0x47, 0xbf, 0xee, 0x9a, + 0xce, 0x80, 0x3a, 0xc0, + ]; + + assert_eq!( + PEDERSEN_BLINDING_H_SECP256K1.coordinates().0, + BIP341_NUMS_X_COORDINATE + ); + assert!(PEDERSEN_BLINDING_H_SECP256K1.is_y_even()); + } + + #[test] + fn honest_cross_curve_proof_verifies() { + use curve25519_dalek_ng::scalar::Scalar; + + let mut rng = rand_chacha::ChaCha20Rng::from_seed([7u8; 32]); + let secret = clamp_to_252_bits(Scalar::random(&mut rng)); + + let (proof, claim) = CROSS_CURVE_PROOF_SYSTEM.prove(&secret, &mut rng); + + assert!( + CROSS_CURVE_PROOF_SYSTEM.verify(&proof, claim), + "an honestly generated cross-curve proof must verify" + ); + } + + #[test] + fn cross_curve_proof_does_not_verify_against_mismatched_ed25519_key() { + use curve25519_dalek_ng::constants::ED25519_BASEPOINT_TABLE; + use curve25519_dalek_ng::scalar::Scalar; + + let mut rng = rand_chacha::ChaCha20Rng::from_seed([9u8; 32]); + let secret = clamp_to_252_bits(Scalar::random(&mut rng)); + + let (proof, (claim_secp, _claim_ed25519)) = + CROSS_CURVE_PROOF_SYSTEM.prove(&secret, &mut rng); + + let unrelated_ed25519 = + &clamp_to_252_bits(Scalar::random(&mut rng)) * &ED25519_BASEPOINT_TABLE; + + assert!( + !CROSS_CURVE_PROOF_SYSTEM.verify(&proof, (claim_secp, unrelated_ed25519)), + "a proof must not verify when the ed25519 key has a different discrete log" + ); + } + + fn clamp_to_252_bits( + scalar: curve25519_dalek_ng::scalar::Scalar, + ) -> curve25519_dalek_ng::scalar::Scalar { + let mut bytes = scalar.to_bytes(); + bytes[31] &= 0b0000_1111; + curve25519_dalek_ng::scalar::Scalar::from_bytes_mod_order(bytes) + } /// 1 BTC lock amount. const LOCK: bitcoin::Amount = bitcoin::Amount::from_sat(100_000_000);