From 617abcf2116fcd61e7683bac25ecbce5459d7fdc Mon Sep 17 00:00:00 2001 From: Mykhailo Kremniov Date: Wed, 30 Oct 2024 16:45:44 +0200 Subject: [PATCH] Tests and wasm bindings for SignedTransactionIntent; some renaming and cleanup --- Cargo.lock | 15 + api-server/scanner-lib/src/sync/tests/mod.rs | 6 +- .../inputsig/arbitrary_message/mod.rs | 21 +- .../inputsig/arbitrary_message/tests.rs | 42 +- .../inputsig/authorize_pubkey_spend.rs | 2 +- .../inputsig/authorize_pubkeyhash_spend.rs | 22 +- .../signature/inputsig/standard_signature.rs | 15 +- common/src/chain/transaction/signature/mod.rs | 4 +- .../transaction/signed_transaction_intent.rs | 415 +++++++++++++++++- crypto/src/key/mod.rs | 7 +- wallet/src/signer/mod.rs | 6 +- wallet/src/signer/software_signer/mod.rs | 9 +- .../src/command_handler/mod.rs | 2 +- .../src/wallet_rpc_traits.rs | 4 +- wasm-wrappers/Cargo.toml | 5 +- wasm-wrappers/js-bindings/wasm_test.js | 151 ++++++- wasm-wrappers/src/error.rs | 23 +- wasm-wrappers/src/lib.rs | 147 ++++--- 18 files changed, 759 insertions(+), 137 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index bc39d8505a..7100b5144c 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2832,6 +2832,19 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2fabcfbdc87f4758337ca535fb41a6d701b65693ce38287d856d1674551ec9b" +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "glow" version = "0.13.1" @@ -8882,6 +8895,7 @@ dependencies = [ "consensus", "crypto", "getrandom 0.2.15", + "gloo-utils", "hex", "randomness", "rstest", @@ -8890,6 +8904,7 @@ dependencies = [ "thiserror", "tx-verifier", "wasm-bindgen", + "web-sys", ] [[package]] diff --git a/api-server/scanner-lib/src/sync/tests/mod.rs b/api-server/scanner-lib/src/sync/tests/mod.rs index d386740639..6b69c73189 100644 --- a/api-server/scanner-lib/src/sync/tests/mod.rs +++ b/api-server/scanner-lib/src/sync/tests/mod.rs @@ -790,7 +790,8 @@ async fn reorg_locked_balance(#[case] seed: Seed) { idx, ) .unwrap(); - let signature = sign_public_key_spending(&priv_key, &pub_key, &sighash, &mut rng).unwrap(); + let signature = + sign_public_key_spending(&priv_key, &pub_key, &sighash, &mut rng).unwrap(); InputWitness::Standard(StandardInputSignature::new( SigHashType::default(), signature.encode(), @@ -867,7 +868,8 @@ async fn reorg_locked_balance(#[case] seed: Seed) { idx, ) .unwrap(); - let signature = sign_public_key_spending(&priv_key, &pub_key, &sighash, &mut rng).unwrap(); + let signature = + sign_public_key_spending(&priv_key, &pub_key, &sighash, &mut rng).unwrap(); InputWitness::Standard(StandardInputSignature::new( SigHashType::default(), signature.encode(), diff --git a/common/src/chain/transaction/signature/inputsig/arbitrary_message/mod.rs b/common/src/chain/transaction/signature/inputsig/arbitrary_message/mod.rs index 84dd31831b..f8d568541a 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message/mod.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message/mod.rs @@ -19,7 +19,7 @@ const MESSAGE_MAGIC_SUFFIX: &str = "\n===MINTLAYER MESSAGE END==="; use randomness::{CryptoRng, Rng}; use thiserror::Error; -use serialization::{Decode, Encode}; +use serialization::Encode; use crate::{ chain::{signature::DestinationSigError, ChainConfig, Destination}, @@ -31,7 +31,8 @@ use super::{ sign_public_key_spending, verify_public_key_spending, AuthorizedPublicKeySpend, }, authorize_pubkeyhash_spend::{ - sign_public_key_hash_spending, verify_public_key_hash_spending, AuthorizedPublicKeyHashSpend, + sign_public_key_hash_spending, sign_public_key_hash_spending_unchecked, + verify_public_key_hash_spending, AuthorizedPublicKeyHashSpend, }, classical_multisig::authorize_classical_multisig::{ verify_classical_multisig_spending, AuthorizedClassicalMultisigSpend, @@ -50,7 +51,7 @@ pub enum SignArbitraryMessageError { Unsupported, } -#[derive(Debug, Clone, Eq, Encode, Decode, PartialEq)] +#[derive(Debug, Clone, Eq, PartialEq)] pub struct ArbitraryMessageSignature { raw_signature: Vec, } @@ -148,6 +149,20 @@ impl ArbitraryMessageSignature { raw_signature: signature, }) } + + pub fn produce_uniparty_signature_as_pub_key_hash_spending( + private_key: &crypto::key::PrivateKey, + message: &[u8], + rng: R, + ) -> Result { + let challenge = produce_message_challenge(message); + let signature = sign_public_key_hash_spending_unchecked(private_key, &challenge, rng)?; + let signature = signature.encode(); + + Ok(Self { + raw_signature: signature, + }) + } } #[cfg(test)] diff --git a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs index 2f2a41a979..e776c6dab4 100644 --- a/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs +++ b/common/src/chain/transaction/signature/inputsig/arbitrary_message/tests.rs @@ -68,6 +68,46 @@ fn sign_verify_supported_destinations(#[case] seed: Seed) { } } +// Check that `produce_uniparty_signature_as_pub_key_hash_spending` gives the same result as +// `produce_uniparty_signature` does for `Destination::PublicKeyHash`. +#[rstest] +#[trace] +#[case(Seed::from_entropy())] +fn produce_uniparty_signature_as_pub_key_hash_spending_matches_produce_uniparty_signature( + #[case] seed: Seed, +) { + let mut rng = test_utils::random::make_seedable_rng(seed); + + let chain_config = chain::config::create_testnet(); + + let (private_key, public_key) = PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let message: Vec = (20..40).map(|_| rng.gen()).collect(); + let message_challenge = produce_message_challenge(&message); + + let destination_addr = Destination::PublicKeyHash(PublicKeyHash::from(&public_key)); + // Use the identical rng for both of the signer calls to be able to compare the signatures. + let signer_rng_seed = rng.gen(); + + let sig1 = ArbitraryMessageSignature::produce_uniparty_signature( + &private_key, + &destination_addr, + &message, + test_utils::random::make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + sig1.verify_signature(&chain_config, &destination_addr, &message_challenge) + .unwrap(); + + let sig2 = ArbitraryMessageSignature::produce_uniparty_signature_as_pub_key_hash_spending( + &private_key, + &message, + test_utils::random::make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + + assert_eq!(sig1, sig2); +} + // Try to sign and verify using a destination that is unsupported for signing and/or verification. // Specific errors should be produced in each case. #[rstest] @@ -308,7 +348,7 @@ fn verify_corrupted_signature(#[case] seed: Seed) { // 1) Construct a message containing tx data that would normally be hashed when signing // a transaction. // 2) As a sanity check, hash the message and use one of the "standard" functions -// (sign_pubkey_spending) to produce a tx signature; check that the signature is indeed correct. +// (sign_public_key_spending) to produce a tx signature; check that the signature is indeed correct. // 3) Sign the message via SignedArbitraryMessage::produce_uniparty_signature; check that the // result is NOT a valid transaction signature. #[rstest] diff --git a/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs b/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs index 30b7af8193..91b2f2b0bb 100644 --- a/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs +++ b/common/src/chain/transaction/signature/inputsig/authorize_pubkey_spend.rs @@ -233,7 +233,7 @@ mod test { #[rstest] #[trace] #[case(Seed::from_entropy())] - fn test_sign_pubkey_spending(#[case] seed: Seed) { + fn test_sign_public_key_spending(#[case] seed: Seed) { let mut rng = test_utils::random::make_seedable_rng(seed); let (private_key, public_key) = diff --git a/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs b/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs index d9f089e372..819aa73d65 100644 --- a/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs +++ b/common/src/chain/transaction/signature/inputsig/authorize_pubkeyhash_spend.rs @@ -13,7 +13,7 @@ // See the License for the specific language governing permissions and // limitations under the License. -use crypto::key::{PublicKey, Signature}; +use crypto::key::{PrivateKey, PublicKey, Signature}; use randomness::{CryptoRng, Rng}; use serialization::{Decode, DecodeAll, Encode}; @@ -69,6 +69,26 @@ pub fn sign_public_key_hash_spending( if calculated_addr != *spendee_addr { return Err(DestinationSigError::PublicKeyToAddressMismatch); } + sign_public_key_hash_spending_impl(&private_key, public_key, sighash, rng) +} + +pub fn sign_public_key_hash_spending_unchecked( + private_key: &crypto::key::PrivateKey, + sighash: &H256, + rng: R, +) -> Result { + let public_key = PublicKey::from_private_key(private_key); + sign_public_key_hash_spending_impl(&private_key, public_key, sighash, rng) +} + +fn sign_public_key_hash_spending_impl( + private_key: &PrivateKey, + public_key: PublicKey, + sighash: &H256, + rng: R, +) -> Result { + debug_assert_eq!(public_key, PublicKey::from_private_key(private_key)); + let msg = sighash.encode(); let signature = private_key .sign_message(&msg, rng) diff --git a/common/src/chain/transaction/signature/inputsig/standard_signature.rs b/common/src/chain/transaction/signature/inputsig/standard_signature.rs index 2e87b50e52..a8ab9a8b57 100644 --- a/common/src/chain/transaction/signature/inputsig/standard_signature.rs +++ b/common/src/chain/transaction/signature/inputsig/standard_signature.rs @@ -34,7 +34,8 @@ use super::{ sign_public_key_spending, verify_public_key_spending, AuthorizedPublicKeySpend, }, authorize_pubkeyhash_spend::{ - sign_public_key_hash_spending, verify_public_key_hash_spending, AuthorizedPublicKeyHashSpend, + sign_public_key_hash_spending, verify_public_key_hash_spending, + AuthorizedPublicKeyHashSpend, }, classical_multisig::{ authorize_classical_multisig::{ @@ -143,6 +144,8 @@ impl StandardInputSignature { inputs_utxos: &[Option<&TxOutput>], input_num: usize, ) -> Result { + use super::classical_multisig::multisig_partial_signature::SigsVerifyResult; + let sighash = signature_hash(sighash_type, tx, inputs_utxos, input_num)?; let message = sighash.encode(); @@ -152,9 +155,13 @@ impl StandardInputSignature { let verification_result = verifier.verify_signatures(chain_config)?; match verification_result { - super::classical_multisig::multisig_partial_signature::SigsVerifyResult::CompleteAndValid => (), - super::classical_multisig::multisig_partial_signature::SigsVerifyResult::Incomplete => return Err(DestinationSigError::IncompleteClassicalMultisigAuthorization), - super::classical_multisig::multisig_partial_signature::SigsVerifyResult::Invalid => return Err(DestinationSigError::InvalidClassicalMultisigAuthorization), + SigsVerifyResult::CompleteAndValid => (), + SigsVerifyResult::Incomplete => { + return Err(DestinationSigError::IncompleteClassicalMultisigAuthorization) + } + SigsVerifyResult::Invalid => { + return Err(DestinationSigError::InvalidClassicalMultisigAuthorization) + } } let serialized_sig = authorization.encode(); diff --git a/common/src/chain/transaction/signature/mod.rs b/common/src/chain/transaction/signature/mod.rs index 79cabc2c86..74fcaf862a 100644 --- a/common/src/chain/transaction/signature/mod.rs +++ b/common/src/chain/transaction/signature/mod.rs @@ -54,9 +54,9 @@ pub enum DestinationSigError { SignatureVerificationWithoutSigs, #[error("Input corresponding to output number {0} does not exist (number of inputs is {1})")] InvalidOutputIndexForModeSingle(usize, usize), - #[error("Decoding witness failed ")] + #[error("Decoding witness failed")] DecodingWitnessFailed, - #[error("Signature verification failed ")] + #[error("Signature verification failed")] SignatureVerificationFailed, #[error("Public key to address mismatch")] PublicKeyToAddressMismatch, diff --git a/common/src/chain/transaction/signed_transaction_intent.rs b/common/src/chain/transaction/signed_transaction_intent.rs index 54df915212..cf397b710c 100644 --- a/common/src/chain/transaction/signed_transaction_intent.rs +++ b/common/src/chain/transaction/signed_transaction_intent.rs @@ -20,13 +20,15 @@ use utils::ensure; use crate::{ chain::{ - signature::inputsig::arbitrary_message::{self, ArbitraryMessageSignature}, ChainConfig, - Destination, Transaction, + signature::inputsig::arbitrary_message::{self, ArbitraryMessageSignature}, + ChainConfig, Destination, Transaction, }, primitives::{Id, Idable as _}, }; -use super::signature::{inputsig::arbitrary_message::SignArbitraryMessageError, DestinationSigError}; +use super::signature::{ + inputsig::arbitrary_message::SignArbitraryMessageError, DestinationSigError, +}; /// `SignedTransactionIntent` acts as a proof that a certain transaction was created with the specific intent in mind. /// This is achieved by combining the specified 'intent' string with the transaction id and signing it by private keys @@ -46,17 +48,26 @@ use super::signature::{inputsig::arbitrary_message::SignArbitraryMessageError, D /// are allowed to have - they must have exactly one associated destination. Though `SignedTransactionIntent` itself /// doesn't specify how destinations are obtained from `TxOutput`, in practice only transactions with Transfer and /// LockThenTransfer input destinations will be supported. -#[derive(Debug, Clone, Encode, Decode)] +/// +/// Note: for both `PublicKeyHash` and `PublicKey` destinations, the signature produced by "produce_" and expected by "verify_" +/// is `AuthorizedPublicKeyHashSpend`. This approach was chosen to simplify use-cases where `SignedTransactionIntent` +/// has to be produced manually (such as the wasm bindings). +/// TODO: the distinction between `AuthorizedPublicKeyHashSpend` and `AuthorizedPublicKeySpend` is not really useful for signatures +/// that are not supposed to be included in the blockchain, so probably `ArbitraryMessageSignature` itself could always +/// produce `AuthorizedPublicKeyHashSpend` for uniparty destinations. +#[derive(Debug, Clone, Encode, Decode, Eq, PartialEq)] pub struct SignedTransactionIntent { signed_message: String, - signatures: Vec, + // Note: the inner Vec is the result of `ArbitraryMessageSignature::into_raw` + // (`ArbitraryMessageSignature` itself is deliberately not encodable). + signatures: Vec>, } impl SignedTransactionIntent { /// Create a signed intent given the id of the transaction and its input destinations. - /// + /// /// Only PublicKeyHash and PublicKey destinations are supported by this function. - pub fn from_transaction_id( + pub fn produce_from_transaction_id( tx_id: &Id, input_destinations: &[Destination], intent_str: &str, @@ -73,16 +84,29 @@ impl SignedTransactionIntent { let signatures = input_destinations .iter() .map(|dest| { + match dest { + Destination::PublicKeyHash(_) | Destination::PublicKey(_) => {} + + Destination::AnyoneCanSpend + | Destination::ScriptHash(_) + | Destination::ClassicMultisig(_) => { + return Err(SignedTransactionIntentError::UnsupportedDestination( + dest.clone(), + ) + .into()); + } + } + let prv_key = prv_key_getter(dest)?; - let sig = ArbitraryMessageSignature::produce_uniparty_signature( - &prv_key, - dest, - message_to_sign.as_bytes(), - &mut rng, - ) - .map_err(SignedTransactionIntentError::MessageSigningError)?; + let sig = + ArbitraryMessageSignature::produce_uniparty_signature_as_pub_key_hash_spending( + &prv_key, + message_to_sign.as_bytes(), + &mut rng, + ) + .map_err(SignedTransactionIntentError::MessageSigningError)?; - Ok(sig) + Ok(sig.into_raw()) }) .collect::, Error>>()?; @@ -92,10 +116,10 @@ impl SignedTransactionIntent { }) } - /// Same as `from_transaction_id`, but this one accepts the whole transaction instead of just an id + /// Same as `produce_from_transaction_id`, but this one accepts the whole transaction instead of just an id /// and performs an additional check - that the number of passed destinations matches the number of /// transaction inputs. - pub fn from_transaction( + pub fn produce_from_transaction( transaction: &Transaction, input_destinations: &[Destination], intent_str: &str, @@ -115,7 +139,7 @@ impl SignedTransactionIntent { } ); - Self::from_transaction_id( + Self::produce_from_transaction_id( &transaction.get_id(), input_destinations, intent_str, @@ -124,10 +148,7 @@ impl SignedTransactionIntent { ) } - pub fn from_components_unchecked( - signed_message: String, - signatures: Vec, - ) -> Self { + pub fn from_components_unchecked(signed_message: String, signatures: Vec>) -> Self { Self { signed_message, signatures, @@ -138,7 +159,16 @@ impl SignedTransactionIntent { &self, chain_config: &ChainConfig, input_destinations: &[Destination], + expected_signed_message: &str, ) -> Result<(), SignedTransactionIntentError> { + ensure!( + self.signed_message == expected_signed_message, + SignedTransactionIntentError::WrongSignedMessage { + expected: expected_signed_message.to_owned(), + actual: self.signed_message.clone() + } + ); + ensure!( self.signatures.len() == input_destinations.len(), SignedTransactionIntentError::InvalidDestinationsCount { @@ -153,6 +183,18 @@ impl SignedTransactionIntent { for (idx, (signature, destination)) in self.signatures.iter().zip(input_destinations).enumerate() { + let destination = match destination { + | Destination::PublicKey(pubkey) => Destination::PublicKeyHash(pubkey.into()), + + dest @ (Destination::PublicKeyHash(_) + | Destination::AnyoneCanSpend + | Destination::ScriptHash(_) + | Destination::ClassicMultisig(_)) => dest.clone(), + }; + + // FIXME avoid extra copy by introducing ArbitraryMessageSignatureRef and moving `verify_signature` there. + let signature = ArbitraryMessageSignature::from_data(signature.clone()); + signature .verify_signature(chain_config, &destination, &signed_challenge) .map_err( @@ -170,17 +212,20 @@ impl SignedTransactionIntent { &self.signed_message } - pub fn signatures(&self) -> &[ArbitraryMessageSignature] { + pub fn signatures(&self) -> &[Vec] { &self.signatures } pub fn get_message_to_sign(intent: &str, tx_id: &Id) -> String { - format!("tx:{tx_id:x};intent:{intent}") + format!("") } } #[derive(thiserror::Error, Debug, Clone, Eq, PartialEq)] pub enum SignedTransactionIntentError { + #[error("Wrong signed message: expected '{expected}', got '{actual}'")] + WrongSignedMessage { expected: String, actual: String }, + #[error("Invalid destinations count: expected {expected}, got {actual}")] InvalidDestinationsCount { expected: usize, actual: usize }, @@ -192,4 +237,326 @@ pub enum SignedTransactionIntentError { input_index: u32, error: DestinationSigError, }, + + #[error("Unsupported destination: {0:?}")] + UnsupportedDestination(Destination), +} + +#[cfg(test)] +mod tests { + use std::{collections::BTreeMap, str::FromStr as _}; + + use itertools::Itertools as _; + use rstest::rstest; + + use crypto::key::{KeyKind, PrivateKey}; + use randomness::Rng; + use test_utils::{ + assert_matches, + random::{make_seedable_rng, Seed}, + random_ascii_alphanumeric_string, + }; + + use crate::{ + address::pubkeyhash::PublicKeyHash, + chain::{config, Destination, OutPointSourceId, Transaction, TxInput}, + primitives::H256, + }; + + use super::*; + + #[test] + fn get_message_to_sign_test() { + let tx_id = Id::new( + H256::from_str("DFC2BB0CC4C7F3ED3FE682A48EE9F78BCD4962E55E7BC239BD340EC22AFF8657") + .unwrap(), + ); + let message = SignedTransactionIntent::get_message_to_sign("test intent", &tx_id); + let expected_message = + ""; + assert_eq!(message, expected_message); + } + + // Basic check for signing and verification. + // Also check that: + // 1) using `produce_from_transaction` and `produce_from_transaction_id` gives the same result; + // 2) `Destination::PublicKeyHash` and `PublicKey` can be used interchangeably; + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn signing_verification_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + let chain_config = config::create_unit_test_config(); + + for _ in 0..10 { + let inputs_count = rng.gen_range(1..=10); + let mut prv_keys = BTreeMap::new(); + + let (input_destinations, flipped_input_destinations): (Vec<_>, Vec<_>) = (0 + ..inputs_count) + .map(|_| { + let (prv_key, pub_key) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let pub_key_hash_dest = Destination::PublicKeyHash((&pub_key).into()); + let pub_key_dest = Destination::PublicKey(pub_key); + + prv_keys.insert(pub_key_dest.clone(), prv_key.clone()); + prv_keys.insert(pub_key_hash_dest.clone(), prv_key); + + if rng.gen_bool(0.5) { + (pub_key_dest, pub_key_hash_dest) + } else { + (pub_key_hash_dest, pub_key_dest) + } + }) + .unzip(); + let other_destination = { + let (prv_key, pub_key) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + + let dest = if rng.gen_bool(0.5) { + Destination::PublicKey(pub_key) + } else { + Destination::PublicKeyHash((&pub_key).into()) + }; + + prv_keys.insert(dest.clone(), prv_key); + dest + }; + + let tx_inputs = (0..inputs_count) + .map(|_| { + let tx_id = Id::new(H256::random_using(&mut rng)); + let idx = rng.gen_range(0..10); + TxInput::from_utxo(OutPointSourceId::Transaction(tx_id), idx) + }) + .collect_vec(); + + let tx = Transaction::new(0, tx_inputs, vec![]).unwrap(); + let tx_id = tx.get_id(); + let intent_str = random_ascii_alphanumeric_string(&mut rng, 1..100); + let expected_signed_message = + SignedTransactionIntent::get_message_to_sign(&intent_str, &tx_id); + // Use the same state of rng for all "produce_" calls to be able to compare the signatures. + let signer_rng_seed = rng.gen(); + + let signed_intent1 = SignedTransactionIntent::produce_from_transaction( + &tx, + &input_destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + + let signed_intent2 = SignedTransactionIntent::produce_from_transaction_id( + &tx_id, + &input_destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + + let signed_intent3 = SignedTransactionIntent::produce_from_transaction( + &tx, + &flipped_input_destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + + let signed_intent4 = SignedTransactionIntent::produce_from_transaction_id( + &tx_id, + &flipped_input_destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + make_seedable_rng(signer_rng_seed), + ) + .unwrap(); + + assert_eq!(signed_intent1, signed_intent2); + assert_eq!(signed_intent1, signed_intent3); + assert_eq!(signed_intent1, signed_intent4); + + signed_intent1 + .verify(&chain_config, &input_destinations, &expected_signed_message) + .unwrap(); + signed_intent1 + .verify( + &chain_config, + &flipped_input_destinations, + &expected_signed_message, + ) + .unwrap(); + + let wrong_message = "wrong message"; + let err = signed_intent1 + .verify(&chain_config, &input_destinations, wrong_message) + .unwrap_err(); + assert_eq!( + err, + SignedTransactionIntentError::WrongSignedMessage { + expected: wrong_message.to_owned(), + actual: expected_signed_message.clone() + } + ); + + let input_index_to_replace = rng.gen_range(0..input_destinations.len()); + + let input_destinations_replaced = { + let mut destinations = input_destinations.clone(); + destinations[input_index_to_replace] = other_destination; + destinations + }; + + let err = signed_intent1 + .verify( + &chain_config, + &input_destinations_replaced, + &expected_signed_message, + ) + .unwrap_err(); + assert_matches!( + err, + SignedTransactionIntentError::SignatureVerificationError { + input_index, + error: _ + } if input_index == input_index_to_replace as u32 + ); + } + } + + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn invalid_destinations_count_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let total_destinations_count = rng.gen_range(2..=10); + + let input_destinations = (0..total_destinations_count) + .map(|_| { + let (_prv_key, pub_key) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + Destination::PublicKey(pub_key) + }) + .collect_vec(); + + let tx_inputs = (0..total_destinations_count - 1) + .map(|_| { + let tx_id = Id::new(H256::random_using(&mut rng)); + let idx = rng.gen_range(0..10); + TxInput::from_utxo(OutPointSourceId::Transaction(tx_id), idx) + }) + .collect_vec(); + + let intent_str = random_ascii_alphanumeric_string(&mut rng, 1..100); + let tx = Transaction::new(0, tx_inputs, vec![]).unwrap(); + + let err = SignedTransactionIntent::produce_from_transaction( + &tx, + &input_destinations, + &intent_str, + |_| -> Result<_, SignedTransactionIntentError> { panic!("shouldn't get this far") }, + &mut rng, + ) + .unwrap_err(); + assert_eq!( + err, + SignedTransactionIntentError::InvalidDestinationsCount { + expected: total_destinations_count - 1, + actual: total_destinations_count + } + ); + + let err = SignedTransactionIntent::produce_from_transaction( + &tx, + &input_destinations[..total_destinations_count - 2], + &intent_str, + |_| -> Result<_, SignedTransactionIntentError> { panic!("shouldn't get this far") }, + &mut rng, + ) + .unwrap_err(); + assert_eq!( + err, + SignedTransactionIntentError::InvalidDestinationsCount { + expected: total_destinations_count - 1, + actual: total_destinations_count - 2 + } + ); + } + + #[rstest] + #[trace] + #[case(Seed::from_entropy())] + fn unsupported_destination_when_signing_test(#[case] seed: Seed) { + let mut rng = make_seedable_rng(seed); + + let destinations_count = rng.gen_range(2..=10); + let mut prv_keys = BTreeMap::new(); + + let orig_destinations = (0..destinations_count) + .map(|_| { + let (prv_key, pub_key) = + PrivateKey::new_from_rng(&mut rng, KeyKind::Secp256k1Schnorr); + let dest = Destination::PublicKey(pub_key); + prv_keys.insert(dest.clone(), prv_key); + dest + }) + .collect_vec(); + + let tx_inputs = (0..destinations_count) + .map(|_| { + let tx_id = Id::new(H256::random_using(&mut rng)); + let idx = rng.gen_range(0..10); + TxInput::from_utxo(OutPointSourceId::Transaction(tx_id), idx) + }) + .collect_vec(); + + let intent_str = random_ascii_alphanumeric_string(&mut rng, 1..100); + let tx = Transaction::new(0, tx_inputs, vec![]).unwrap(); + let tx_id = tx.get_id(); + + let unsupported_destinations = vec![ + Destination::AnyoneCanSpend, + Destination::ScriptHash(Id::new(H256::random_using(&mut rng))), + Destination::ClassicMultisig(PublicKeyHash::random_using(&mut rng)), + ]; + let dest_index_to_replace = rng.gen_range(0..destinations_count); + + for unsupported_destination in unsupported_destinations { + let mut destinations = orig_destinations.clone(); + destinations[dest_index_to_replace] = unsupported_destination.clone(); + + let err = SignedTransactionIntent::produce_from_transaction( + &tx, + &destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + &mut rng, + ) + .unwrap_err(); + assert_eq!( + err, + SignedTransactionIntentError::UnsupportedDestination( + unsupported_destination.clone() + ) + ); + + let err = SignedTransactionIntent::produce_from_transaction_id( + &tx_id, + &destinations, + &intent_str, + |dest| Ok::<_, SignedTransactionIntentError>(prv_keys.get(dest).unwrap().clone()), + &mut rng, + ) + .unwrap_err(); + assert_eq!( + err, + SignedTransactionIntentError::UnsupportedDestination(unsupported_destination) + ); + } + } } diff --git a/crypto/src/key/mod.rs b/crypto/src/key/mod.rs index 0935f81637..83aee5a66b 100644 --- a/crypto/src/key/mod.rs +++ b/crypto/src/key/mod.rs @@ -30,12 +30,7 @@ pub use signature::Signature; use self::key_holder::{PrivateKeyHolder, PublicKeyHolder}; #[derive(thiserror::Error, Debug, PartialEq, Eq, PartialOrd, Ord, Clone)] -pub enum SignatureError { - #[error("Data conversion error: {0}")] - DataConversionError(String), - #[error("Signature construction error")] - SignatureConstructionError, -} +pub enum SignatureError {} #[must_use] #[derive(Debug, PartialEq, Eq, Clone, Decode, Encode)] diff --git a/wallet/src/signer/mod.rs b/wallet/src/signer/mod.rs index 6f843efbe1..4cb41035c7 100644 --- a/wallet/src/signer/mod.rs +++ b/wallet/src/signer/mod.rs @@ -14,10 +14,12 @@ // limitations under the License. use common::chain::{ - partially_signed_transaction::PartiallySignedTransaction, signature::{ + partially_signed_transaction::PartiallySignedTransaction, + signature::{ inputsig::arbitrary_message::{ArbitraryMessageSignature, SignArbitraryMessageError}, DestinationSigError, - }, Destination, SignedTransactionIntent, SignedTransactionIntentError, Transaction + }, + Destination, SignedTransactionIntent, SignedTransactionIntentError, Transaction, }; use crypto::key::hdkd::derivable::DerivationError; use wallet_types::signature_status::SignatureStatus; diff --git a/wallet/src/signer/software_signer/mod.rs b/wallet/src/signer/software_signer/mod.rs index 273020e308..754ff35024 100644 --- a/wallet/src/signer/software_signer/mod.rs +++ b/wallet/src/signer/software_signer/mod.rs @@ -16,7 +16,9 @@ use std::sync::Arc; use common::chain::{ - htlc::HtlcSecret, partially_signed_transaction::PartiallySignedTransaction, signature::{ + htlc::HtlcSecret, + partially_signed_transaction::PartiallySignedTransaction, + signature::{ inputsig::{ arbitrary_message::ArbitraryMessageSignature, classical_multisig::{ @@ -32,7 +34,8 @@ use common::chain::{ }, sighash::{sighashtype::SigHashType, signature_hash}, DestinationSigError, - }, ChainConfig, Destination, SignedTransactionIntent, Transaction, TxOutput + }, + ChainConfig, Destination, SignedTransactionIntent, Transaction, TxOutput, }; use crypto::key::{ extended::{ExtendedPrivateKey, ExtendedPublicKey}, @@ -363,7 +366,7 @@ impl<'a, T: WalletStorageReadUnlocked> Signer for SoftwareSigner<'a, T> { intent: &str, key_chain: &impl AccountKeyChains, ) -> SignerResult { - SignedTransactionIntent::from_transaction( + SignedTransactionIntent::produce_from_transaction( transaction, input_destinations, intent, diff --git a/wallet/wallet-cli-commands/src/command_handler/mod.rs b/wallet/wallet-cli-commands/src/command_handler/mod.rs index 0df9ebde47..46a40cd6ce 100644 --- a/wallet/wallet-cli-commands/src/command_handler/mod.rs +++ b/wallet/wallet-cli-commands/src/command_handler/mod.rs @@ -1417,7 +1417,7 @@ where self.config, ) .await?; - + let mut output = format!("The hex encoded transaction is:\n{signed_tx}\n"); writeln!( diff --git a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs index 30d39768d8..0081bab621 100644 --- a/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs +++ b/wallet/wallet-rpc-client/src/wallet_rpc_traits.rs @@ -18,7 +18,9 @@ use std::{collections::BTreeMap, num::NonZeroUsize, path::PathBuf}; use chainstate::{rpc::RpcOutputValueIn, ChainInfo}; use common::{ chain::{ - block::timestamp::BlockTimestamp, partially_signed_transaction::PartiallySignedTransaction, Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, UtxoOutPoint + block::timestamp::BlockTimestamp, partially_signed_transaction::PartiallySignedTransaction, + Block, GenBlock, SignedTransaction, SignedTransactionIntent, Transaction, TxOutput, + UtxoOutPoint, }, primitives::{BlockHeight, DecimalAmount, Id}, }; diff --git a/wasm-wrappers/Cargo.toml b/wasm-wrappers/Cargo.toml index c68d06c43f..aac20d1a49 100644 --- a/wasm-wrappers/Cargo.toml +++ b/wasm-wrappers/Cargo.toml @@ -19,11 +19,14 @@ common = { path = "../common" } tx-verifier = { path = "../chainstate/tx-verifier" } bip39 = { workspace = true, default-features = false, features = ["std", "zeroize"] } +thiserror.workspace = true # This crate is required for rand to work with wasm. See: https://docs.rs/getrandom/latest/getrandom/#webassembly-support getrandom = { version = "0.2", features = ["js"] } +gloo-utils = "0.2" wasm-bindgen = "0.2" -thiserror.workspace = true +# web-sys provides `console::log` and is useful during debugging. +web-sys = { version = "0.3", features = ["console"] } [dev-dependencies] hex.workspace = true diff --git a/wasm-wrappers/js-bindings/wasm_test.js b/wasm-wrappers/js-bindings/wasm_test.js index 5512a64664..3c63097549 100644 --- a/wasm-wrappers/js-bindings/wasm_test.js +++ b/wasm-wrappers/js-bindings/wasm_test.js @@ -47,20 +47,31 @@ import { sign_challenge, verify_challenge, get_token_id, + make_transaction_intent_message_to_sign, + encode_signed_transaction_intent, + verify_transaction_intent, } from "../pkg/wasm_wrappers.js"; function assert_eq_arrays(arr1, arr2) { - if (arr1.length != arr2.length) { - throw new Error("array lengths are different"); - } + assert(arr1.length == arr2.length, "array lengths are different"); arr1.forEach((elem, idx) => { - if (elem != arr2[idx]) { - throw new Error(`Element at index ${idx} is different`); - } + assert(elem == arr2[idx], `element at index ${idx} is different`); }); } +function assert(condition, message) { + if (!condition) { + throw Error('Assertion failed: ' + (message || '')); + } +} + +function run_one_test(test_func) { + console.log(`>> Running ${test_func.name}`); + test_func(); + console.log(`<< Done running ${test_func.name}`); +} + export async function run_test() { // Try signature verification const priv_key = make_private_key(); @@ -259,7 +270,7 @@ export async function run_test() { ); throw new Error("Invalid delegation id encoding worked somehow!"); } catch (e) { - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log("Tested invalid delegation id in account successfully"); @@ -340,7 +351,7 @@ export async function run_test() { ); throw new Error("Invalid token id worked somehow!"); } catch (e) { - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log("Tested invalid token id successfully for token burn"); @@ -406,7 +417,7 @@ export async function run_test() { throw new Error("Invalid token id worked somehow!"); } catch (e) { console.log(`err: ${e}`); - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log("Tested invalid token id successfully"); @@ -445,7 +456,7 @@ export async function run_test() { ); throw new Error("Invalid address worked somehow!"); } catch (e) { - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log( @@ -464,7 +475,7 @@ export async function run_test() { throw new Error("Invalid token id worked somehow!"); } catch (e) { console.log(`err: ${e}`); - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log( @@ -609,7 +620,7 @@ export async function run_test() { throw new Error("Invalid token id worked somehow!"); } catch (e) { console.log(`err: ${e}`); - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log("Tested invalid token id successfully"); @@ -853,7 +864,7 @@ export async function run_test() { ); throw new Error("Invalid address worked somehow!"); } catch (e) { - if (!e.includes("Invalid addressable encoding")) { + if (!e.includes("Invalid addressable")) { throw e; } console.log("Tested invalid address in encode witness successfully"); @@ -1135,10 +1146,12 @@ export async function run_test() { throw new Error(`Effective balance test failed ${eff_bal}`); } } + + run_one_test(test_get_transaction_id); + run_one_test(test_signed_transaction_intent); } -// get_transaction_id tests -{ +function test_get_transaction_id() { const tx_bin = [ 1, 0, 4, 0, 0, 255, 93, 154, 148, 57, 14, 233, 114, 8, 211, 26, 165, 195, 181, 221, 189, 141, 249, 211, 8, 6, 157, 242, 235, 245, 40, 63, 124, 227, @@ -1214,9 +1227,115 @@ export async function run_test() { } catch (e) { if (!e.includes("Invalid transaction encoding")) { throw new Error( - "Invalid transaction encodeing resulted in an unexpected error message!" + "Invalid transaction encoding resulted in an unexpected error message!" ); } } } } + +function test_signed_transaction_intent() { + try { + const invalid_tx_id = "invalid tx id"; + make_transaction_intent_message_to_sign("intent", invalid_tx_id); + throw new Error("Invalid tx id worked somehow!"); + } catch (e) { + if (!e.includes("Invalid transaction id encoding")) { + throw e; + } + } + + const tx_id = "DFC2BB0CC4C7F3ED3FE682A48EE9F78BCD4962E55E7BC239BD340EC22AFF8657"; + const message = make_transaction_intent_message_to_sign("the intent", tx_id); + const expected_message = new TextEncoder().encode( + ""); + assert_eq_arrays(message, expected_message); + + try { + const invalid_signatures = "invalid signatures"; + encode_signed_transaction_intent(message, invalid_signatures); + throw new Error("Invalid signatures worked somehow!"); + } catch (e) { + if (!e.includes("Error decoding a JsValue as an array of arrays of bytes")) { + throw e; + } + } + + { + const prv_key1 = [ + 0, 142, 11, 183, 83, 79, 207, 79, 18, 172, 116, 88, 251, 128, 146, 254, 82, + 156, 229, 110, 160, 187, 104, 237, 182, 59, 95, 108, 203, 22, 138, 173, 147 + ]; + const pubkey_addr1 = "rpmt1qgqqxtunp0gdsysq9g3fke9pesl4w8xg3t7ynssfrvqetae0d9nqn3prq3mdt7"; + const pubkeyhash_addr1 = "rmt1qxtlh84a7fflmeem9g4wtmyp2px42gnxwqprnjlw"; + const prv_key2 = [ + 0, 52, 13, 17, 187, 88, 27, 23, 211, 24, 13, 103, 68, 60, 205, 11, 221, + 141, 15, 97, 7, 234, 184, 222, 38, 85, 151, 118, 0, 154, 109, 134, 42 + ]; + const pubkey_addr2 = "rpmt1qgqqylj755w0rlejn3cjadtrhskkzyxqs9nq7mura3z467fkaam7ppxkjr77n7"; + const pubkeyhash_addr2 = "rmt1qx0y7ktusde6d4hf9474z28dwcsys3uk5qxphddl"; + + const signature1 = sign_challenge(prv_key1, message); + const signature2 = sign_challenge(prv_key2, message); + + const signed_intent = encode_signed_transaction_intent(message, [Array.from(signature1), Array.from(signature2)]); + + verify_transaction_intent(message, signed_intent, [pubkey_addr1, pubkeyhash_addr2], Network.Regtest); + verify_transaction_intent(message, signed_intent, [pubkeyhash_addr1, pubkey_addr2], Network.Regtest); + + try { + verify_transaction_intent(message, signed_intent, [pubkeyhash_addr2, pubkey_addr1], Network.Regtest); + throw new Error("Mismatched addresses worked somehow!"); + } catch (e) { + if (!e.includes("Public key to address mismatch")) { + throw e; + } + } + + const bad_signature1 = sign_challenge(prv_key1, [...message, 123]); + const bad_signed_intent = encode_signed_transaction_intent(message, [Array.from(bad_signature1), Array.from(signature2)]); + + try { + verify_transaction_intent(message, bad_signed_intent, [pubkey_addr1, pubkey_addr2], Network.Regtest); + throw new Error("Bad signature worked somehow!"); + } catch (e) { + if (!e.includes("Signature verification failed")) { + throw e; + } + } + } + + { + // Encode some predefined signatures to ensure stability of the encoding. + const signature1 = [ + 0, 3, 47, 147, 11, 208, 216, 18, 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, + 247, 47, 105, 102, 9, 196, 35, 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, + 14, 233, 201, 16, 104, 217, 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, + 124, 53, 201, 124, 194, 7, 45, 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163 + ]; + const signature2 = [ + 0, 2, 126, 94, 165, 28, 241, 255, 50, 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, + 69, 93, 121, 54, 239, 119, 224, 132, 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, + 9, 21, 200, 244, 222, 219, 172, 85, 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, + 98, 248, 161, 3, 119, 149, 13, 159, 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 + ]; + const expected_encoded_signed_intent = [ + 105, 1, 60, 116, 120, 95, 105, 100, 58, 100, 102, 99, 50, 98, 98, 48, 99, 99, 52, 99, 55, 102, 51, 101, 100, 51, + 102, 101, 54, 56, 50, 97, 52, 56, 101, 101, 57, 102, 55, 56, 98, 99, 100, 52, 57, 54, 50, 101, 53, 53, 101, 55, + 98, 99, 50, 51, 57, 98, 100, 51, 52, 48, 101, 99, 50, 50, 97, 102, 102, 56, 54, 53, 55, 59, 105, 110, 116, 101, + 110, 116, 58, 116, 104, 101, 32, 105, 110, 116, 101, 110, 116, 62, 8, 141, 1, 0, 3, 47, 147, 11, 208, 216, 18, + 0, 42, 34, 155, 100, 161, 204, 63, 87, 28, 200, 138, 252, 73, 194, 9, 27, 1, 149, 247, 47, 105, 102, 9, 196, 35, + 0, 39, 178, 200, 173, 176, 46, 47, 239, 158, 172, 197, 47, 79, 211, 132, 128, 244, 14, 233, 201, 16, 104, 217, + 125, 222, 7, 28, 131, 135, 238, 49, 90, 92, 189, 165, 162, 198, 61, 220, 5, 246, 6, 124, 53, 201, 124, 194, 7, 45, + 119, 49, 69, 224, 32, 150, 128, 29, 230, 95, 107, 173, 190, 82, 163, 141, 1, 0, 2, 126, 94, 165, 28, 241, 255, 50, + 156, 113, 46, 181, 99, 188, 45, 97, 16, 192, 129, 102, 15, 111, 131, 236, 69, 93, 121, 54, 239, 119, 224, 132, + 214, 0, 145, 218, 82, 46, 32, 182, 94, 12, 204, 233, 111, 75, 242, 206, 57, 9, 21, 200, 244, 222, 219, 172, 85, + 205, 117, 95, 76, 200, 144, 172, 226, 162, 65, 26, 15, 93, 181, 72, 45, 209, 98, 248, 161, 3, 119, 149, 13, 159, + 125, 218, 166, 130, 144, 62, 160, 91, 216, 160, 88, 126, 229, 68, 158, 240 + ]; + + const encoded_signed_intent = + encode_signed_transaction_intent(message, [signature1, signature2]); + assert_eq_arrays(encoded_signed_intent, expected_encoded_signed_intent); + } +} diff --git a/wasm-wrappers/src/error.rs b/wasm-wrappers/src/error.rs index 8f0098d1b2..05b27b7283 100644 --- a/wasm-wrappers/src/error.rs +++ b/wasm-wrappers/src/error.rs @@ -13,7 +13,10 @@ // See the License for the specific language governing permissions and // limitations under the License. -use common::chain::{signature::{inputsig::arbitrary_message::SignArbitraryMessageError, DestinationSigError}, TransactionCreationError}; +use common::chain::{ + signature::{inputsig::arbitrary_message::SignArbitraryMessageError, DestinationSigError}, + SignedTransactionIntentError, TransactionCreationError, +}; use wasm_bindgen::JsValue; #[derive(thiserror::Error, Debug, Clone)] @@ -32,8 +35,10 @@ pub enum Error { InvalidKeyIndex, #[error("Invalid outpoint ID encoding")] InvalidOutpointId, - #[error("Invalid addressable encoding")] - InvalidAddressable, + #[error("Invalid addressable: {addressable}")] + InvalidAddressable { addressable: String }, + #[error("Transaction size estimation error: {error}")] + TransactionSizeEstimationError { error: String }, #[error("NFT Creator needs to be a public key address")] InvalidCreatorPublicKey, #[error("Invalid amount")] @@ -60,6 +65,8 @@ pub enum Error { InvalidHtlcSecret, #[error("Invalid htlc secret hash encoding")] InvalidHtlcSecretHash, + #[error("Invalid signed transaction intent encoding")] + InvalidSignedTransactionIntent, #[error("No input outpoint found in transaction")] NoInputOutpointFound, #[error("Invalid multisig challenge encoding")] @@ -76,14 +83,20 @@ pub enum Error { TokenIssuanceError(#[from] tx_verifier::error::TokenIssuanceError), #[error("Transaction creation error: {0}")] TransactionCreationError(#[from] TransactionCreationError), - #[error("Produce signature error: {0}")] - ProduceSignatureError(#[from] DestinationSigError), + #[error("Signature error: {0}")] + DestinationSigError(#[from] DestinationSigError), #[error("No UTXO input found in the provided inputs")] NoUtxoInInputs, #[error("Invalid message signature encoding")] InvalidMessageSignature, #[error("Arbitrary message signing error: {0}")] SignArbitraryMessageError(#[from] SignArbitraryMessageError), + #[error("Error decoding a JsValue as an array of arrays of bytes: {error}")] + JsValueNotArrayOfArraysOfBytes { error: String }, + #[error("Signed transaction intent error: {0}")] + SignedTransactionIntentError(#[from] SignedTransactionIntentError), + #[error("Signed transaction intent message is not a valid string")] + SignedTransactionIntentMessageIsNotAValidString, } // This is required to make an error readable in JavaScript diff --git a/wasm-wrappers/src/lib.rs b/wasm-wrappers/src/lib.rs index 9b8a586463..18a925f0d5 100644 --- a/wasm-wrappers/src/lib.rs +++ b/wasm-wrappers/src/lib.rs @@ -16,6 +16,9 @@ use std::{num::NonZeroU8, str::FromStr}; use bip39::Language; +use gloo_utils::format::JsValueSerdeExt as _; +use wasm_bindgen::prelude::*; + use common::{ address::{pubkeyhash::PublicKeyHash, traits::Addressable, Address}, chain::{ @@ -28,7 +31,6 @@ use common::{ inputsig::{ arbitrary_message::{produce_message_challenge, ArbitraryMessageSignature}, authorize_hashed_timelock_contract_spend::AuthorizedHashedTimelockContractSpend, - authorize_pubkeyhash_spend::AuthorizedPublicKeyHashSpend, classical_multisig::authorize_classical_multisig::{ sign_classical_multisig_spending, AuthorizedClassicalMultisigSpend, }, @@ -60,7 +62,6 @@ use crypto::key::{ }; use error::Error; use serialization::{Decode, DecodeAll, Encode}; -use wasm_bindgen::prelude::*; pub mod error; @@ -100,6 +101,7 @@ impl Amount { } #[wasm_bindgen] +#[derive(Debug, Copy, Clone)] /// The network, for which an operation to be done. Mainnet, testnet, etc. pub enum Network { Mainnet, @@ -344,60 +346,31 @@ pub fn verify_signature_for_spending( /// Given a message and a private key, create and sign a challenge with the given private key. /// This kind of signature is to be used when signing challenges. -/// -/// Note: this is equivalent to calling `sign_challenge_for_address` with the pubkeyhash address. #[wasm_bindgen] pub fn sign_challenge(private_key: &[u8], message: &[u8]) -> Result, Error> { let private_key = PrivateKey::decode_all(&mut &private_key[..]) .map_err(|_| Error::InvalidPrivateKeyEncoding)?; - // Note: this is basically what `ArbitraryMessageSignature::produce_uniparty_signature` does when it's called - // with a PubKeyHash destination. - let challenge = produce_message_challenge(message); - let public_key = PublicKey::from_private_key(&private_key); - - let signature = private_key.sign_message(&challenge.encode(), randomness::make_true_rng())?; - let signature = AuthorizedPublicKeyHashSpend::new(public_key, signature); - Ok(signature.encode()) -} - -/// Given a message and a private key, create and sign a challenge with the given private key. -/// The given address must be either the pubkey or pubkeyhash address corresponding to the provided -/// private key. -/// -/// Note: the kind of the address determines the type of object that will be encoded in the result. -/// And the same address must be passed to `verify_challenge` for it to be able to decode the data -/// properly. -#[wasm_bindgen] -pub fn sign_challenge_for_address( - private_key: &[u8], - address: &str, - network: Network, - message: &[u8], -) -> Result, Error> { - let chain_config = Builder::new(network.into()).build(); - let destination = parse_addressable::(&chain_config, address)?; - - let private_key = PrivateKey::decode_all(&mut &private_key[..]) - .map_err(|_| Error::InvalidPrivateKeyEncoding)?; - - let signature = ArbitraryMessageSignature::produce_uniparty_signature( + let signature = ArbitraryMessageSignature::produce_uniparty_signature_as_pub_key_hash_spending( &private_key, - &destination, message, randomness::make_true_rng(), )?; - - Ok(signature.encode()) + Ok(signature.into_raw()) } /// Given a signed challenge, an address and a message, verify that /// the signature is produced by signing the message with the private key /// that derived the given public key. /// This function is used for verifying messages-related challenges. -/// -/// Note: the provided address must be the same as the one passed to `sign_challenge_for_address` -/// (or if you used `sign_challenge`, it must be the pubkeyhash address). +/// +/// Note: for signatures that were created by `sign_challenge`, the provided address must be +/// a 'pubkeyhash' address. +/// +/// Note: currently this function never returns `false` - it either returns `true` or fails with an error. +// TODO: this has to be changed; the function should either just return `()` (but then it'll be inconsistent +// with `verify_signature_for_spending`, which returns a proper boolean) or return `verify_signature(...).is_ok()` +// (but then the caller will lose the information about the reason for the failure). #[wasm_bindgen] pub fn verify_challenge( address: &str, @@ -418,7 +391,9 @@ fn parse_addressable( address: &str, ) -> Result { let addressable = Address::from_string(chain_config, address) - .map_err(|_| Error::InvalidAddressable)? + .map_err(|_| Error::InvalidAddressable { + addressable: address.to_owned(), + })? .into_object(); Ok(addressable) } @@ -428,41 +403,82 @@ fn parse_addressable( pub fn make_transaction_intent_message_to_sign( intent: &str, transaction_id: &str, -) -> Result { +) -> Result, Error> { let transaction_id = Id::new(H256::from_str(transaction_id).map_err(|_| Error::InvalidTransactionId)?); let message_to_sign = SignedTransactionIntent::get_message_to_sign(intent, &transaction_id); - Ok(message_to_sign) + + Ok(message_to_sign.into_bytes()) } -/// Return a SignedTransactionIntent object as bytes given the message and encoded signatures. +/// Return a `SignedTransactionIntent` object as bytes given the message and encoded signatures. /// /// Note: to produce a valid signed intent one is expected to sign the corresponding message by private keys /// corresponding to each input of the transaction. -/// -/// Here the provided `message` must be produced by `make_transaction_intent_message_to_sign` and the signatures -/// by `sign_challenge_for_address`, where the kind of the provided address must match the type of the corresponding -/// input destination. The number of signatures must be equal to the number of inputs in the corresponding transaction. +/// +/// Parameters: +/// `signed_message` - this must have been produced by `make_transaction_intent_message_to_sign`. +/// `signatures` - this should be an array of arrays of bytes, each of them representing an individual signature +/// of `signed_message` produced by `sign_challenge` using the private key for the corresponding input destination +/// of the transaction. The number of signatures must be equal to the number of inputs in the transaction. #[wasm_bindgen] pub fn encode_signed_transaction_intent( - message: &str, - mut signatures: &[u8], + signed_message: &[u8], + // Note: we could also accept it as `Vec` where the inner JsValue would represent `Vec`. + // But such "semi-structured" approach doesn't make much sense (and accepting `Vec>` directly is not allowed). + signatures: &JsValue, ) -> Result, Error> { - let mut decoded_signatures = vec![]; - while !signatures.is_empty() { - let signature = ArbitraryMessageSignature::decode(&mut signatures) - .map_err(|_| Error::InvalidMessageSignature)?; - decoded_signatures.push(signature); - } + let signed_message_str = String::from_utf8(signed_message.to_owned()) + .map_err(|_| Error::SignedTransactionIntentMessageIsNotAValidString)?; + let signatures: Vec> = + signatures.into_serde().map_err(|err| Error::JsValueNotArrayOfArraysOfBytes { + error: err.to_string(), + })?; - let signed_intent = SignedTransactionIntent::from_components_unchecked( - message.to_owned(), - decoded_signatures, - ); + let signed_intent = + SignedTransactionIntent::from_components_unchecked(signed_message_str, signatures); Ok(signed_intent.encode()) } +/// Verify a signed transaction intent. +/// +/// Parameters: +/// `expected_signed_message` - the message that is supposed to be signed; this must have been +/// produced by `make_transaction_intent_message_to_sign`. +/// `encoded_signed_intent` - the signed transaction intent produced by `encode_signed_transaction_intent`. +/// `input_destinations` - an array of addresses (strings), corresponding to the transaction's input destinations +/// (note that this function treats "pub key" and "pub key hash" addresses interchangeably, so it's ok to pass +/// one instead of the other). +/// `network` - the network being used (needed to decode the addresses). +#[wasm_bindgen] +pub fn verify_transaction_intent( + expected_signed_message: &[u8], + mut encoded_signed_intent: &[u8], + input_destinations: Vec, + network: Network, +) -> Result<(), Error> { + let expected_signed_message_str = String::from_utf8(expected_signed_message.to_owned()) + .map_err(|_| Error::SignedTransactionIntentMessageIsNotAValidString)?; + let chain_config = Builder::new(network.into()).build(); + + let signed_intent = SignedTransactionIntent::decode_all(&mut encoded_signed_intent) + .map_err(|_| Error::InvalidSignedTransactionIntent)?; + + let input_destinations = input_destinations + .iter() + .map(|addr| parse_addressable::(&chain_config, addr)) + .collect::, _>>()?; + + signed_intent.verify( + &chain_config, + &input_destinations, + &expected_signed_message_str, + )?; + + Ok(()) +} + /// Given a destination address, an amount and a network type (mainnet, testnet, etc), this function /// creates an output of type Transfer, and returns it as bytes. #[wasm_bindgen] @@ -1014,8 +1030,11 @@ pub fn estimate_transaction_size( for destination in input_utxos_destinations { let destination = parse_addressable::(&chain_config, &destination)?; let signature_size = - input_signature_size_from_destination(&destination, Option::<&_>::None) - .map_err(|_| Error::InvalidAddressable)?; + input_signature_size_from_destination(&destination, Option::<&_>::None).map_err( + |err| Error::TransactionSizeEstimationError { + error: err.to_string(), + }, + )?; total_size += signature_size; } @@ -1225,7 +1244,7 @@ pub fn encode_witness_htlc_multisig( AuthorizedHashedTimelockContractSpend::from_data(sig.raw_signature())?; match htlc_spend { AuthorizedHashedTimelockContractSpend::Secret(_, _) => { - return Err(Error::ProduceSignatureError( + return Err(Error::DestinationSigError( DestinationSigError::InvalidSignatureEncoding, )); }