diff --git a/applications/tari_app_grpc/Cargo.toml b/applications/tari_app_grpc/Cargo.toml index 002eefa59c..f60e707bcb 100644 --- a/applications/tari_app_grpc/Cargo.toml +++ b/applications/tari_app_grpc/Cargo.toml @@ -30,4 +30,3 @@ zeroize = "1.3" [build-dependencies] tonic-build = "0.6.2" - diff --git a/applications/tari_console_wallet/Cargo.toml b/applications/tari_console_wallet/Cargo.toml index a9c55a3cac..1b0f1d5ea2 100644 --- a/applications/tari_console_wallet/Cargo.toml +++ b/applications/tari_console_wallet/Cargo.toml @@ -18,8 +18,7 @@ tari_p2p = { path = "../../base_layer/p2p", features = ["auto-update"] } tari_app_grpc = { path = "../tari_app_grpc" } tari_shutdown = { path = "../../infrastructure/shutdown" } tari_key_manager = { path = "../../base_layer/key_manager" } -tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag="v0.4.7" } -zeroize = "1.3" +tari_utilities = { git = "https://github.com/tari-project/tari_utilities.git", tag = "v0.4.7" } # Uncomment for tokio tracing via tokio-console (needs "tracing" featurs) #console-subscriber = "0.1.3" @@ -53,6 +52,7 @@ tracing-opentelemetry = "0.15.0" tracing-subscriber = "0.2.20" unicode-segmentation = "1.6.0" unicode-width = "0.1" +zeroize = "1.3" # network tracing, rt-tokio for async batch export opentelemetry = { version = "0.16", default-features = false, features = ["trace", "rt-tokio"] } diff --git a/applications/tari_console_wallet/src/automation/commands.rs b/applications/tari_console_wallet/src/automation/commands.rs index 8eda5cb36e..a8e3f28f2c 100644 --- a/applications/tari_console_wallet/src/automation/commands.rs +++ b/applications/tari_console_wallet/src/automation/commands.rs @@ -21,7 +21,7 @@ // USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. use std::{ - convert::TryInto, + convert::{From, TryInto}, fs, fs::File, io, @@ -86,6 +86,7 @@ pub enum WalletCommand { SendTari, SendOneSided, CreateKeyPair, + CreateAggregateSignatureUtxo, MakeItRain, CoinSplit, DiscoverPeer, @@ -141,6 +142,24 @@ pub async fn burn_tari( .map_err(CommandError::TransactionServiceError) } +pub async fn create_aggregate_signature_utxo( + mut wallet_transaction_service: TransactionServiceHandle, + amount: MicroTari, + fee_per_gram: MicroTari, + n: u8, + m: u8, + public_keys: Vec, + message: String, +) -> Result<(TxId, FixedHash), CommandError> { + let mut msg = [0u8; 32]; + msg.copy_from_slice(message.as_bytes()); + + wallet_transaction_service + .create_aggregate_signature_utxo(amount, fee_per_gram, n, m, public_keys, msg) + .await + .map_err(CommandError::TransactionServiceError) +} + /// publishes a tari-SHA atomic swap HTLC transaction pub async fn init_sha_atomic_swap( mut wallet_transaction_service: TransactionServiceHandle, @@ -652,6 +671,32 @@ pub async fn command_runner( }, Err(e) => eprintln!("CreateKeyPair error! {}", e), }, + CreateAggregateSignatureUtxo(args) => match create_aggregate_signature_utxo( + transaction_service.clone(), + args.amount, + args.fee_per_gram, + args.n, + args.m, + args.public_keys + .iter() + .map(|pk| PublicKey::from(pk.clone())) + .collect::>(), + args.message, + ) + .await + { + Ok((tx_id, output_hash)) => { + println!( + "Create a utxo with n-of-m aggregate public key, with: + 1. n = {}, + 2. m = {}, + 3. tx id = {}, + 4. output hash = {}", + args.n, args.m, tx_id, output_hash + ) + }, + Err(e) => eprintln!("CreateAggregateSignatureUtxo error! {}", e), + }, SendTari(args) => { match send_tari( transaction_service.clone(), diff --git a/applications/tari_console_wallet/src/cli.rs b/applications/tari_console_wallet/src/cli.rs index 6cc735be6b..c7f5a2e7d0 100644 --- a/applications/tari_console_wallet/src/cli.rs +++ b/applications/tari_console_wallet/src/cli.rs @@ -117,6 +117,7 @@ pub enum CliCommands { SendTari(SendTariArgs), BurnTari(BurnTariArgs), CreateKeyPair(CreateKeyPairArgs), + CreateAggregateSignatureUtxo(CreateAggregateSignatureUtxoArgs), SendOneSided(SendTariArgs), SendOneSidedToStealthAddress(SendTariArgs), MakeItRain(MakeItRainArgs), @@ -161,6 +162,17 @@ pub struct CreateKeyPairArgs { pub key_branch: String, } +#[derive(Debug, Args, Clone)] +pub struct CreateAggregateSignatureUtxoArgs { + pub amount: MicroTari, + pub fee_per_gram: MicroTari, + pub n: u8, + pub m: u8, + pub message: String, + #[clap(long)] + pub public_keys: Vec, +} + #[derive(Debug, Args, Clone)] pub struct MakeItRainArgs { pub destination: UniPublicKey, diff --git a/applications/tari_console_wallet/src/wallet_modes.rs b/applications/tari_console_wallet/src/wallet_modes.rs index c3219fb4b9..1315d33a9d 100644 --- a/applications/tari_console_wallet/src/wallet_modes.rs +++ b/applications/tari_console_wallet/src/wallet_modes.rs @@ -429,6 +429,10 @@ mod test { create-key-pair pie + create-aggregate-signature-utxo 125T 100 10 1 ff \ + --public-keys=5c4f2a4b3f3f84e047333218a84fd24f581a9d7e4f23b78e3714e9d174427d61 \ + --public-keys=f6b2ca781342a3ebe30ee1643655c96f1d7c14f4d49f077695395de98ae73665 + coin-split --message Make_many_dust_UTXOs! --fee-per-gram 2 0.001T 499 make-it-rain --duration 100 --transactions-per-second 10 --start-amount 0.009200T --increase-amount 0T \ @@ -445,6 +449,7 @@ mod test { let mut send_tari = false; let mut burn_tari = false; let mut create_key_pair = false; + let mut create_aggregate_signature_utxo = false; let mut make_it_rain = false; let mut coin_split = false; let mut discover_peer = false; @@ -455,6 +460,7 @@ mod test { CliCommands::SendTari(_) => send_tari = true, CliCommands::BurnTari(_) => burn_tari = true, CliCommands::CreateKeyPair(_) => create_key_pair = true, + CliCommands::CreateAggregateSignatureUtxo(_) => create_aggregate_signature_utxo = true, CliCommands::SendOneSided(_) => {}, CliCommands::SendOneSidedToStealthAddress(_) => {}, CliCommands::MakeItRain(_) => make_it_rain = true, @@ -479,6 +485,7 @@ mod test { send_tari && burn_tari && create_key_pair && + create_aggregate_signature_utxo && make_it_rain && coin_split && discover_peer && diff --git a/base_layer/wallet/src/transaction_service/handle.rs b/base_layer/wallet/src/transaction_service/handle.rs index fda68fc370..5275806912 100644 --- a/base_layer/wallet/src/transaction_service/handle.rs +++ b/base_layer/wallet/src/transaction_service/handle.rs @@ -31,7 +31,7 @@ use chacha20poly1305::XChaCha20Poly1305; use chrono::NaiveDateTime; use tari_common_types::{ transaction::{ImportStatus, TxId}, - types::PublicKey, + types::{FixedHash, PublicKey}, }; use tari_comms::types::CommsPublicKey; use tari_core::{ @@ -88,6 +88,14 @@ pub enum TransactionServiceRequest { fee_per_gram: MicroTari, message: String, }, + CreateNMUtxo { + amount: MicroTari, + fee_per_gram: MicroTari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + }, SendOneSidedTransaction { dest_pubkey: CommsPublicKey, amount: MicroTari, @@ -156,6 +164,17 @@ impl fmt::Display for TransactionServiceRequest { message )), Self::BurnTari { amount, message, .. } => f.write_str(&format!("Burning Tari ({}, {})", amount, message)), + Self::CreateNMUtxo { + amount, + fee_per_gram: _, + n, + m, + public_keys: _, + message: _, + } => f.write_str(&format!( + "Creating a new n-of-m aggregate uxto with: amount = {}, n = {}, m = {}", + amount, n, m + )), Self::SendOneSidedTransaction { dest_pubkey, amount, @@ -228,6 +247,7 @@ impl fmt::Display for TransactionServiceRequest { #[derive(Debug)] pub enum TransactionServiceResponse { TransactionSent(TxId), + TransactionSentWithOutputHash(TxId, FixedHash), TransactionCancelled, PendingInboundTransactions(HashMap), PendingOutboundTransactions(HashMap), @@ -504,6 +524,32 @@ impl TransactionServiceHandle { } } + pub async fn create_aggregate_signature_utxo( + &mut self, + amount: MicroTari, + fee_per_gram: MicroTari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + ) -> Result<(TxId, FixedHash), TransactionServiceError> { + match self + .handle + .call(TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + }) + .await?? + { + TransactionServiceResponse::TransactionSentWithOutputHash(tx_id, output_hash) => Ok((tx_id, output_hash)), + _ => Err(TransactionServiceError::UnexpectedApiResponse), + } + } + pub async fn send_one_sided_to_stealth_address_transaction( &mut self, dest_pubkey: CommsPublicKey, diff --git a/base_layer/wallet/src/transaction_service/service.rs b/base_layer/wallet/src/transaction_service/service.rs index 33f484398b..474b87869a 100644 --- a/base_layer/wallet/src/transaction_service/service.rs +++ b/base_layer/wallet/src/transaction_service/service.rs @@ -35,7 +35,7 @@ use rand::rngs::OsRng; use sha2::Sha256; use tari_common_types::{ transaction::{ImportStatus, TransactionDirection, TransactionStatus, TxId}, - types::{PrivateKey, PublicKey}, + types::{FixedHash, PrivateKey, PublicKey}, }; use tari_comms::{peer_manager::NodeIdentity, types::CommsPublicKey}; use tari_comms_dht::outbound::OutboundMessageRequester; @@ -70,7 +70,7 @@ use tari_crypto::{ tari_utilities::ByteArray, }; use tari_p2p::domain_message::DomainMessage; -use tari_script::{inputs, script, TariScript}; +use tari_script::{inputs, script, slice_to_boxed_message, TariScript}; use tari_service_framework::{reply_channel, reply_channel::Receiver}; use tari_shutdown::ShutdownSignal; use tokio::{ @@ -649,6 +649,25 @@ where ) .await .map(TransactionServiceResponse::TransactionSent), + TransactionServiceRequest::CreateNMUtxo { + amount, + fee_per_gram, + n, + m, + public_keys, + message, + } => self + .create_aggregate_signature_utxo( + amount, + fee_per_gram, + n, + m, + public_keys, + message, + transaction_broadcast_join_handles, + ) + .await + .map(|(tx_id, _)| TransactionServiceResponse::TransactionSent(tx_id)), TransactionServiceRequest::SendShaAtomicSwapTransaction( dest_pubkey, amount, @@ -986,6 +1005,177 @@ where Ok(()) } + /// Creates a utxo with aggregate public key out of m-of-n public keys + #[allow(clippy::too_many_lines)] + pub async fn create_aggregate_signature_utxo( + &mut self, + amount: MicroTari, + fee_per_gram: MicroTari, + n: u8, + m: u8, + public_keys: Vec, + message: [u8; 32], + transaction_broadcast_join_handles: &mut FuturesUnordered< + JoinHandle>>, + >, + ) -> Result<(TxId, FixedHash), TransactionServiceError> { + let tx_id = TxId::new_random(); + + let msg = slice_to_boxed_message(message.as_bytes()); + let script = script!(CheckMultiSigVerifyAggregatePubKey(n, m, public_keys, msg)); + + // Empty covenant + let covenant = Covenant::default(); + + // Default range proof + let minimum_value_promise = MicroTari::zero(); + + // Prepare sender part of transaction + let mut stp = self + .output_manager_service + .prepare_transaction_to_send( + tx_id, + amount, + UtxoSelectionCriteria::default(), + OutputFeatures::default(), + fee_per_gram, + TransactionMetadata::default(), + "".to_string(), + script.clone(), + covenant, + minimum_value_promise, + ) + .await?; + + // This call is needed to advance the state from `SingleRoundMessageReady` to `CollectingSingleSignature`, + // but the returned value is not used + let _single_round_sender_data = stp + .build_single_round_message() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + self.output_manager_service + .confirm_pending_transaction(tx_id) + .await + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + // Prepare receiver part of the transaction + + // In generating an aggregate public key utxo, we can use a randomly generated spend key + let spend_key = PrivateKey::random(&mut OsRng); + + let sender_message = TransactionSenderMessage::new_single_round_message(stp.get_single_round_message()?); + let rewind_blinding_key = PrivateKey::from_bytes(&hash_secret_key(&spend_key.clone()))?; + let encryption_key = PrivateKey::from_bytes(&hash_secret_key(&rewind_blinding_key))?; + + let rewind_data = RewindData { + rewind_blinding_key: rewind_blinding_key.clone(), + encryption_key: encryption_key.clone(), + }; + + let rtp = ReceiverTransactionProtocol::new_with_rewindable_output( + sender_message, + PrivateKey::random(&mut OsRng), + spend_key.clone(), + &self.resources.factories, + &rewind_data, + ); + + // we don't want the given utxo to be spendable as an input to a later transaction, so we set + // spendable height of the current utxo to be u64::MAx + let height = u64::MAX; + + let recipient_reply = rtp.get_signed_data()?.clone(); + let output = recipient_reply.output.clone(); + let commitment = self + .resources + .factories + .commitment + .commit_value(&spend_key.clone(), amount.into()); + + let encrypted_value = EncryptedValue::encrypt_value(&rewind_data.encryption_key, &commitment, amount)?; + let minimum_value_promise = MicroTari::zero(); + + let covenant = Covenant::default(); + + let unblinded_output = UnblindedOutput::new_current_version( + amount, + spend_key.clone(), + output.features.clone(), + script, + inputs!(PublicKey::from_secret_key(&spend_key)), /* TODO: refactor this, when we have implemented the + * necessary logic */ + self.node_identity.secret_key().clone(), + output.sender_offset_public_key.clone(), + output.metadata_signature.clone(), + height, + covenant, + encrypted_value, + minimum_value_promise, + ); + + // Start finalize + stp.add_single_recipient_info(recipient_reply, &self.resources.factories.range_proof) + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + + // Finalize + stp.finalize(&self.resources.factories, None, height).map_err(|e| { + error!( + target: LOG_TARGET, + "Transaction (TxId: {}) could not be finalized. Failure error: {:?}", tx_id, e, + ); + TransactionServiceProtocolError::new(tx_id, e.into()) + })?; + info!( + target: LOG_TARGET, + "Finalized create n of m transaction TxId: {}", tx_id + ); + + // This event being sent is important, but not critical to the protocol being successful. Send only fails if + // there are no subscribers. + let _size = self + .event_publisher + .send(Arc::new(TransactionEvent::TransactionCompletedImmediately(tx_id))); + + // Broadcast create n of m aggregate public key transaction + let tx = stp + .get_transaction() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + let fee = stp + .get_fee_amount() + .map_err(|e| TransactionServiceProtocolError::new(tx_id, e.into()))?; + self.output_manager_service + .add_rewindable_output_with_tx_id( + tx_id, + unblinded_output, + Some(SpendingPriority::Normal), + Some(rewind_data), + ) + .await?; + self.submit_transaction( + transaction_broadcast_join_handles, + CompletedTransaction::new( + tx_id, + self.resources.node_identity.public_key().clone(), + self.resources.node_identity.public_key().clone(), + amount, + fee, + tx.clone(), + TransactionStatus::Completed, + "".to_string(), + Utc::now().naive_utc(), + TransactionDirection::Outbound, + None, + None, + None, + ), + )?; + + let output_hash = output.hash(); + + // we want to print out the hash of the utxo + Ok((tx_id, output_hash)) + } + /// broadcasts a SHA-XTR atomic swap transaction /// # Arguments /// 'dest_pubkey': The Comms pubkey of the recipient node @@ -1043,7 +1233,7 @@ where ) .await?; - // This call is needed to advance the state from `SingleRoundMessageReady` to `SingleRoundMessageReady`, + // This call is needed to advance the state from `SingleRoundMessageReady` to `CollectingSingleSignature`, // but the returned value is not used let _single_round_sender_data = stp .build_single_round_message() @@ -1127,7 +1317,10 @@ where ); TransactionServiceProtocolError::new(tx_id, e.into()) })?; - info!(target: LOG_TARGET, "Finalized one-side transaction TxId: {}", tx_id); + info!( + target: LOG_TARGET, + "Finalized sha atomic swap transaction TxId: {}", tx_id + ); // This event being sent is important, but not critical to the protocol being successful. Send only fails if // there are no subscribers. @@ -1135,7 +1328,7 @@ where .event_publisher .send(Arc::new(TransactionEvent::TransactionCompletedImmediately(tx_id))); - // Broadcast one-sided transaction + // Broadcast sha atomic swap transaction let tx = stp .get_transaction() @@ -1205,7 +1398,7 @@ where ) .await?; - // This call is needed to advance the state from `SingleRoundMessageReady` to `SingleRoundMessageReady`, + // This call is needed to advance the state from `SingleRoundMessageReady` to `CollectingSingleSignature`, // but the returned value is not used let _single_round_sender_data = stp .build_single_round_message() @@ -1373,7 +1566,7 @@ where ) .await?; - // This call is needed to advance the state from `SingleRoundMessageReady` to `SingleRoundMessageReady`, + // This call is needed to advance the state from `SingleRoundMessageReady` to `CollectingSingleSignature`, // but the returned value is not used let _single_round_sender_data = stp .build_single_round_message() diff --git a/infrastructure/tari_script/src/lib.rs b/infrastructure/tari_script/src/lib.rs index e796c55a4d..5a50a52b72 100644 --- a/infrastructure/tari_script/src/lib.rs +++ b/infrastructure/tari_script/src/lib.rs @@ -24,7 +24,7 @@ mod serde; mod stack; pub use error::ScriptError; -pub use op_codes::{slice_to_boxed_hash, slice_to_hash, HashValue, Opcode}; +pub use op_codes::{slice_to_boxed_hash, slice_to_boxed_message, slice_to_hash, HashValue, Opcode}; pub use script::TariScript; pub use script_commitment::{ScriptCommitment, ScriptCommitmentError, ScriptCommitmentFactory}; pub use script_context::ScriptContext; diff --git a/infrastructure/tari_script/src/op_codes.rs b/infrastructure/tari_script/src/op_codes.rs index 50350e0dbb..745f3a0eeb 100644 --- a/infrastructure/tari_script/src/op_codes.rs +++ b/infrastructure/tari_script/src/op_codes.rs @@ -118,6 +118,7 @@ pub const OP_HASH_BLAKE256: u8 = 0xb0; pub const OP_HASH_SHA256: u8 = 0xb1; pub const OP_HASH_SHA3: u8 = 0xb2; pub const OP_TO_RISTRETTO_POINT: u8 = 0xb3; +pub const OP_CHECK_MULTI_SIG_VERIFY_AGGREGATE_PUB_KEY: u8 = 0xb4; // Opcode constants: Miscellaneous pub const OP_RETURN: u8 = 0x60; @@ -237,6 +238,9 @@ pub enum Opcode { /// Pops the top element which must be a valid 32-byte scalar or hash and calculates the corresponding Ristretto /// point, and pushes the result to the stack. Fails with EMPTY_STACK if the stack is empty. ToRistrettoPoint, + /// Pop m signatures from the stack. If m signatures out of the provided n public keys sign the 32-byte message, + /// push the aggregate of the public keys to the stack, otherwise fails with VERIFY_FAILED. + CheckMultiSigVerifyAggregatePubKey(u8, u8, Vec, Box), // Miscellaneous /// Always fails with VERIFY_FAILED. @@ -356,6 +360,10 @@ impl Opcode { Ok((CheckMultiSigVerify(m, n, keys, msg), &bytes[end..])) }, OP_TO_RISTRETTO_POINT => Ok((ToRistrettoPoint, &bytes[1..])), + OP_CHECK_MULTI_SIG_VERIFY_AGGREGATE_PUB_KEY => { + let (m, n, keys, msg, end) = Opcode::read_multisig_args(bytes)?; + Ok((CheckMultiSigVerifyAggregatePubKey(m, n, keys, msg), &bytes[end..])) + }, OP_RETURN => Ok((Return, &bytes[1..])), OP_IF_THEN => Ok((IfThen, &bytes[1..])), OP_ELSE => Ok((Else, &bytes[1..])), @@ -465,6 +473,13 @@ impl Opcode { array.extend_from_slice(msg.deref()); }, ToRistrettoPoint => array.push(OP_TO_RISTRETTO_POINT), + CheckMultiSigVerifyAggregatePubKey(m, n, public_keys, msg) => { + array.extend_from_slice(&[OP_CHECK_MULTI_SIG_VERIFY_AGGREGATE_PUB_KEY, *m, *n]); + for public_key in public_keys { + array.extend(public_key.to_vec()); + } + array.extend_from_slice(msg.deref()); + }, Return => array.push(OP_RETURN), IfThen => array.push(OP_IF_THEN), Else => array.push(OP_ELSE), @@ -531,10 +546,20 @@ impl fmt::Display for Opcode { ) }, ToRistrettoPoint => write!(fmt, "ToRistrettoPoint"), - Return => write!(fmt, "Return"), - IfThen => write!(fmt, "IfThen"), - Else => write!(fmt, "Else"), - EndIf => write!(fmt, "EndIf"), + CheckMultiSigVerifyAggregatePubKey(m, n, public_keys, msg) => { + let keys: Vec = public_keys.iter().map(|p| p.to_hex()).collect(); + fmt.write_str(&format!( + "CheckMultiSigVerifyAggregatePubKey({}, {}, [{}], {})", + *m, + *n, + keys.join(", "), + (*msg).to_hex() + )) + }, + Return => fmt.write_str("Return"), + IfThen => fmt.write_str("IfThen"), + Else => fmt.write_str("Else"), + EndIf => fmt.write_str("EndIf"), } } } @@ -766,12 +791,20 @@ mod test { 6c9cb4d3e57351462122310fa22c90b1e6dfb528d64615363d1261a75da3e401)", ); test_checkmultisig( - &Opcode::CheckMultiSigVerify(1, 2, keys, Box::new(*msg)), + &Opcode::CheckMultiSigVerify(1, 2, keys.clone(), Box::new(*msg)), OP_CHECK_MULTI_SIG_VERIFY, "CheckMultiSigVerify(1, 2, [9c8bc5f90d221191748e8dd7686f09e1114b4bada4c367ed58ae199c51eb100b, \ 56e9f018b138ba843521b3243a29d81730c3a4c25108b108b1ca47c2132db569], \ 6c9cb4d3e57351462122310fa22c90b1e6dfb528d64615363d1261a75da3e401)", ); + test_checkmultisig( + &Opcode::CheckMultiSigVerifyAggregatePubKey(1, 2, keys, Box::new(*msg)), + OP_CHECK_MULTI_SIG_VERIFY_AGGREGATE_PUB_KEY, + "CheckMultiSigVerifyAggregatePubKey(1, 2, \ + [9c8bc5f90d221191748e8dd7686f09e1114b4bada4c367ed58ae199c51eb100b, \ + 56e9f018b138ba843521b3243a29d81730c3a4c25108b108b1ca47c2132db569], \ + 6c9cb4d3e57351462122310fa22c90b1e6dfb528d64615363d1261a75da3e401)", + ); } #[test] diff --git a/infrastructure/tari_script/src/script.rs b/infrastructure/tari_script/src/script.rs index b91fce8480..fd2bc9f8ff 100644 --- a/infrastructure/tari_script/src/script.rs +++ b/infrastructure/tari_script/src/script.rs @@ -248,20 +248,27 @@ impl TariScript { } }, CheckMultiSig(m, n, public_keys, msg) => { - if self.check_multisig(stack, *m, *n, public_keys, *msg.deref())? { + if self.check_multisig(stack, *m, *n, public_keys, *msg.deref())?.is_some() { stack.push(Number(1)) } else { stack.push(Number(0)) } }, CheckMultiSigVerify(m, n, public_keys, msg) => { - if self.check_multisig(stack, *m, *n, public_keys, *msg.deref())? { + if self.check_multisig(stack, *m, *n, public_keys, *msg.deref())?.is_some() { Ok(()) } else { Err(ScriptError::VerifyFailed) } }, ToRistrettoPoint => self.handle_to_ristretto_point(stack), + CheckMultiSigVerifyAggregatePubKey(m, n, public_keys, msg) => { + if let Some(agg_pub_key) = self.check_multisig(stack, *m, *n, public_keys, *msg.deref())? { + stack.push(PublicKey(agg_pub_key)) + } else { + Err(ScriptError::VerifyFailed) + } + }, Return => Err(ScriptError::Return), IfThen => TariScript::handle_if_then(stack, state), Else => TariScript::handle_else(state), @@ -505,9 +512,9 @@ impl TariScript { n: u8, public_keys: &[RistrettoPublicKey], message: Message, - ) -> Result { - if m == 0 || n == 0 || m > n || n > MAX_MULTISIG_LIMIT { - return Err(ScriptError::InvalidData); + ) -> Result, ScriptError> { + if m == 0 || n == 0 || m > n || n > MAX_MULTISIG_LIMIT || public_keys.len() != n as usize { + return Err(ScriptError::ValueExceedsBounds); } // pop m sigs let m = m as usize; @@ -524,20 +531,25 @@ impl TariScript { #[allow(clippy::mutable_key_type)] let mut sig_set = HashSet::new(); + let mut agg_pub_key = RistrettoPublicKey::default(); for s in &signatures { for (i, pk) in public_keys.iter().enumerate() { if !sig_set.contains(s) && !key_signed[i] && s.verify_challenge(pk, &message) { key_signed[i] = true; sig_set.insert(s); + agg_pub_key = agg_pub_key + pk; break; } } if !sig_set.contains(s) { - return Ok(false); + return Ok(None); } } - - Ok(sig_set.len() == m) + if sig_set.len() == m { + Ok(Some(agg_pub_key)) + } else { + Ok(None) + } } fn handle_to_ristretto_point(&self, stack: &mut ExecutionStack) -> Result<(), ScriptError> { @@ -625,6 +637,7 @@ mod test { inputs, op_codes::{slice_to_boxed_hash, slice_to_boxed_message, HashValue, Message}, ExecutionStack, + Opcode::CheckMultiSigVerifyAggregatePubKey, ScriptContext, StackItem, StackItem::{Commitment, Hash, Number}, @@ -1145,21 +1158,21 @@ mod test { let script = TariScript::new(ops); let inputs = inputs!(s_alice.clone()); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); let keys = vec![p_alice.clone(), p_bob.clone()]; let ops = vec![CheckMultiSig(1, 0, keys, msg.clone())]; let script = TariScript::new(ops); let inputs = inputs!(s_alice.clone()); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); let keys = vec![p_alice, p_bob]; let ops = vec![CheckMultiSig(2, 1, keys, msg)]; let script = TariScript::new(ops); let inputs = inputs!(s_alice); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); // max n is 32 let (msg, data) = multisig_data(33); @@ -1169,7 +1182,7 @@ mod test { let items = sigs.map(StackItem::Signature).collect(); let inputs = ExecutionStack::new(items); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); // 3 of 4 let (msg, data) = multisig_data(4); @@ -1258,7 +1271,7 @@ mod test { // 1 of 3 let keys = vec![p_alice.clone(), p_bob.clone(), p_carol.clone()]; - let ops = vec![CheckMultiSigVerify(1, 2, keys, msg.clone())]; + let ops = vec![CheckMultiSigVerify(1, 3, keys, msg.clone())]; let script = TariScript::new(ops); let inputs = inputs!(Number(1), s_alice.clone()); @@ -1292,6 +1305,31 @@ mod test { let err = script.execute(&inputs).unwrap_err(); assert_eq!(err, ScriptError::VerifyFailed); + // 2 of 3 (returning the aggregate public key of the signatories) + let keys = vec![p_alice.clone(), p_bob.clone(), p_carol.clone()]; + let ops = vec![CheckMultiSigVerifyAggregatePubKey(2, 3, keys, msg.clone())]; + let script = TariScript::new(ops); + + let inputs = inputs!(s_alice.clone(), s_bob.clone()); + let agg_pub_key = script.execute(&inputs).unwrap(); + assert_eq!(agg_pub_key, StackItem::PublicKey(p_alice.clone() + p_bob.clone())); + + let inputs = inputs!(s_alice.clone(), s_carol.clone()); + let agg_pub_key = script.execute(&inputs).unwrap(); + assert_eq!(agg_pub_key, StackItem::PublicKey(p_alice.clone() + p_carol.clone())); + + let inputs = inputs!(s_bob.clone(), s_carol.clone()); + let agg_pub_key = script.execute(&inputs).unwrap(); + assert_eq!(agg_pub_key, StackItem::PublicKey(p_bob.clone() + p_carol.clone())); + + let inputs = inputs!(s_alice.clone(), s_carol.clone(), s_bob.clone()); + let err = script.execute(&inputs).unwrap_err(); + assert_eq!(err, ScriptError::NonUnitLengthStack); + + let inputs = inputs!(p_bob.clone()); + let err = script.execute(&inputs).unwrap_err(); + assert_eq!(err, ScriptError::StackUnderflow); + // 3 of 3 let keys = vec![p_alice.clone(), p_bob.clone(), p_carol]; let ops = vec![CheckMultiSigVerify(3, 3, keys, msg.clone())]; @@ -1313,21 +1351,21 @@ mod test { let script = TariScript::new(ops); let inputs = inputs!(s_alice.clone()); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); let keys = vec![p_alice.clone(), p_bob.clone()]; let ops = vec![CheckMultiSigVerify(1, 0, keys, msg.clone())]; let script = TariScript::new(ops); let inputs = inputs!(s_alice.clone()); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); let keys = vec![p_alice, p_bob]; let ops = vec![CheckMultiSigVerify(2, 1, keys, msg)]; let script = TariScript::new(ops); let inputs = inputs!(s_alice); let err = script.execute(&inputs).unwrap_err(); - assert_eq!(err, ScriptError::InvalidData); + assert_eq!(err, ScriptError::ValueExceedsBounds); // 3 of 4 let (msg, data) = multisig_data(4); diff --git a/integration_tests/features/WalletCli.feature b/integration_tests/features/WalletCli.feature index f53669e00d..5dc8fdcd19 100644 --- a/integration_tests/features/WalletCli.feature +++ b/integration_tests/features/WalletCli.feature @@ -66,7 +66,7 @@ Feature: Wallet CLI When I create a burn transaction of 201552500000 uT from WALLET via command line When I mine 5 blocks on BASE Then all nodes are at height 20 - Then I get balance of wallet WALLET is at least 20000000000 uT via command line + Then I get balance of wallet WALLET is at least 15000000000 uT via command line # TODO: verify the actual burned kernel @long-running