diff --git a/crates/sui-core/src/unit_tests/authority_tests.rs b/crates/sui-core/src/unit_tests/authority_tests.rs index 9776e1e3055db..69dccc8f81511 100644 --- a/crates/sui-core/src/unit_tests/authority_tests.rs +++ b/crates/sui-core/src/unit_tests/authority_tests.rs @@ -893,7 +893,7 @@ async fn test_handle_transfer_transaction_bad_signature() { bad_signature_transfer_transaction .data_mut_for_testing() .tx_signature = - Signature::new_secure(&transfer_transaction.data().intent_message, &unknown_key); + Signature::new_secure(&transfer_transaction.data().intent_message, &unknown_key).into(); assert!(client .handle_transaction(bad_signature_transfer_transaction) diff --git a/crates/sui-json-rpc-types/src/lib.rs b/crates/sui-json-rpc-types/src/lib.rs index 0c9961a3ac113..c29472c7fff3d 100644 --- a/crates/sui-json-rpc-types/src/lib.rs +++ b/crates/sui-json-rpc-types/src/lib.rs @@ -36,7 +36,7 @@ use sui_types::base_types::{ }; use sui_types::coin::CoinMetadata; use sui_types::committee::EpochId; -use sui_types::crypto::{AuthorityStrongQuorumSignInfo, Signature}; +use sui_types::crypto::AuthorityStrongQuorumSignInfo; use sui_types::dynamic_field::DynamicFieldInfo; use sui_types::error::{ExecutionError, SuiError}; use sui_types::event::{BalanceChangeType, Event, EventID}; @@ -52,6 +52,7 @@ use sui_types::messages::{ }; use sui_types::messages_checkpoint::CheckpointSequenceNumber; use sui_types::move_package::{disassemble_modules, MovePackage}; +use sui_types::multisig::GenericSignature; use sui_types::object::{ Data, MoveObject, Object, ObjectFormatOptions, ObjectRead, Owner, PastObjectRead, }; @@ -1774,7 +1775,7 @@ pub struct SuiCertifiedTransaction { pub transaction_digest: TransactionDigest, pub data: SuiTransactionData, /// tx_signature is signed by the transaction sender, committing to the intent message containing the transaction data and intent. - pub tx_signature: Signature, + pub tx_signature: GenericSignature, /// authority signature information, if available, is signed by an authority, applied on `data`. pub auth_sign_info: AuthorityStrongQuorumSignInfo, } diff --git a/crates/sui-open-rpc/spec/openrpc.json b/crates/sui-open-rpc/spec/openrpc.json index cefeac4466b02..3edd66a2cbb93 100644 --- a/crates/sui-open-rpc/spec/openrpc.json +++ b/crates/sui-open-rpc/spec/openrpc.json @@ -2742,7 +2742,7 @@ "description": "tx_signature is signed by the transaction sender, committing to the intent message containing the transaction data and intent.", "allOf": [ { - "$ref": "#/components/schemas/Signature" + "$ref": "#/components/schemas/GenericSignature" } ] } @@ -2842,6 +2842,58 @@ } } }, + "CompressedSignature": { + "oneOf": [ + { + "type": "object", + "required": [ + "Ed25519" + ], + "properties": { + "Ed25519": { + "$ref": "#/components/schemas/Base64" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Secp256k1" + ], + "properties": { + "Secp256k1": { + "$ref": "#/components/schemas/Base64" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "Secp256r1" + ], + "properties": { + "Secp256r1": { + "$ref": "#/components/schemas/Base64" + } + }, + "additionalProperties": false + }, + { + "type": "object", + "required": [ + "BLS12381" + ], + "properties": { + "BLS12381": { + "$ref": "#/components/schemas/Base64" + } + }, + "additionalProperties": false + } + ] + }, "Data": { "oneOf": [ { @@ -3856,6 +3908,16 @@ } } }, + "GenericSignature": { + "anyOf": [ + { + "$ref": "#/components/schemas/MultiSignature" + }, + { + "$ref": "#/components/schemas/Signature" + } + ] + }, "Hex": { "description": "Hex string encoding.", "type": "string" @@ -4092,6 +4154,48 @@ } ] }, + "MultiPublicKey": { + "type": "object", + "required": [ + "pks", + "threshold" + ], + "properties": { + "pks": { + "type": "array", + "items": { + "$ref": "#/components/schemas/PublicKey" + } + }, + "threshold": { + "type": "integer", + "format": "uint", + "minimum": 0.0 + } + } + }, + "MultiSignature": { + "type": "object", + "required": [ + "bitmap", + "multi_pk", + "sigs" + ], + "properties": { + "bitmap": { + "$ref": "#/components/schemas/Base64" + }, + "multi_pk": { + "$ref": "#/components/schemas/MultiPublicKey" + }, + "sigs": { + "type": "array", + "items": { + "$ref": "#/components/schemas/CompressedSignature" + } + } + } + }, "Object": { "type": "object", "required": [ diff --git a/crates/sui-open-rpc/src/examples.rs b/crates/sui-open-rpc/src/examples.rs index a655a8b2ce869..279c28bf0491c 100644 --- a/crates/sui-open-rpc/src/examples.rs +++ b/crates/sui-open-rpc/src/examples.rs @@ -28,7 +28,7 @@ use sui_types::base_types::{ ObjectDigest, ObjectID, ObjectType, SequenceNumber, SuiAddress, TransactionDigest, }; use sui_types::crypto::{ - get_key_pair_from_rng, AccountKeyPair, AuthorityKeyPair, AuthorityPublicKeyBytes, Signature, + get_key_pair_from_rng, AccountKeyPair, AuthorityKeyPair, AuthorityPublicKeyBytes, }; use sui_types::crypto::{AuthorityQuorumSignInfo, SuiSignature}; use sui_types::event::EventID; @@ -37,6 +37,7 @@ use sui_types::messages::{ CallArg, ExecuteTransactionRequestType, MoveCall, SingleTransactionKind, TransactionData, TransactionKind, TransferObject, }; +use sui_types::multisig::GenericSignature; use sui_types::object::{Owner, PACKAGE_VERSION}; use sui_types::query::EventQuery; use sui_types::query::TransactionQuery; @@ -168,7 +169,10 @@ impl RpcExampleProvider { fn execute_transaction_example(&mut self) -> Examples { let (data, signature, _, _, result, _) = self.get_transfer_data_response(); - + let s = match signature { + GenericSignature::Signature(s) => s, + _ => panic!("Unexpected signature type"), + }; Examples::new( "sui_executeTransaction", vec![ExamplePairing::new( @@ -178,15 +182,9 @@ impl RpcExampleProvider { "tx_bytes", json!(Base64::from_bytes(bcs::to_bytes(&data).unwrap().as_slice())), ), - ("sig_scheme", json!(signature.scheme())), - ( - "signature", - json!(Base64::from_bytes(signature.signature_bytes())), - ), - ( - "pub_key", - json!(Base64::from_bytes(signature.public_key_bytes())), - ), + ("sig_scheme", json!(s.scheme())), + ("signature", json!(Base64::from_bytes(s.signature_bytes()))), + ("pub_key", json!(Base64::from_bytes(s.public_key_bytes()))), ( "request_type", json!(ExecuteTransactionRequestType::WaitForLocalExecution), @@ -199,6 +197,10 @@ impl RpcExampleProvider { fn execute_transaction_serialized_sig_example(&mut self) -> Examples { let (data, signature, _, _, result, _) = self.get_transfer_data_response(); + let s = match signature { + GenericSignature::Signature(s) => s, + _ => panic!("Unexpected signature type"), + }; let tx_bytes = TransactionBytes::from_data(data).unwrap(); Examples::new( @@ -207,7 +209,7 @@ impl RpcExampleProvider { "Execute an transaction with serialized signature", vec![ ("tx_bytes", json!(tx_bytes.tx_bytes)), - ("signature", json!(Base64::from_bytes(signature.as_ref()))), + ("signature", json!(Base64::from_bytes(s.as_ref()))), ( "request_type", json!(ExecuteTransactionRequestType::WaitForLocalExecution), @@ -443,7 +445,7 @@ impl RpcExampleProvider { &mut self, ) -> ( TransactionData, - Signature, + GenericSignature, SuiAddress, ObjectID, SuiTransactionResponse, diff --git a/crates/sui-types/src/base_types.rs b/crates/sui-types/src/base_types.rs index c5de8ceaa3188..add1857b54a1b 100644 --- a/crates/sui-types/src/base_types.rs +++ b/crates/sui-types/src/base_types.rs @@ -29,6 +29,7 @@ use crate::error::ExecutionError; use crate::error::ExecutionErrorKind; use crate::error::SuiError; use crate::gas_coin::GasCoin; +use crate::multisig::MultiPublicKey; use crate::object::{Object, Owner}; use crate::sui_serde::Readable; use fastcrypto::encoding::{Base58, Base64, Encoding, Hex}; @@ -261,6 +262,23 @@ impl From<&PublicKey> for SuiAddress { } } +impl From for SuiAddress { + fn from(multi_pk: MultiPublicKey) -> Self { + let mut hasher = Sha3_256::default(); + hasher.update(multi_pk.threshold().to_be_bytes()); + multi_pk.pubkeys().iter().for_each(|(pk, i)| { + hasher.update(pk.as_ref()); + hasher.update(i.to_be_bytes()); + }); + let g_arr = hasher.finalize(); + + let mut res = [0u8; SUI_ADDRESS_LENGTH]; + // OK to access slice because Sha3_256 should never be shorter than SUI_ADDRESS_LENGTH. + res.copy_from_slice(&AsRef::<[u8]>::as_ref(&g_arr)[..SUI_ADDRESS_LENGTH]); + SuiAddress(res) + } +} + impl TryFrom<&[u8]> for SuiAddress { type Error = SuiError; diff --git a/crates/sui-types/src/crypto.rs b/crates/sui-types/src/crypto.rs index aa2ebf505f6e9..fb3827e326b5c 100644 --- a/crates/sui-types/src/crypto.rs +++ b/crates/sui-types/src/crypto.rs @@ -96,10 +96,13 @@ pub enum SuiKeyPair { Secp256r1(Secp256r1KeyPair), } -#[derive(Debug, Clone, PartialEq, Eq, From)] +#[derive(Debug, Clone, PartialEq, Eq, From, JsonSchema)] pub enum PublicKey { + #[schemars(with = "Base64")] Ed25519(Ed25519PublicKey), + #[schemars(with = "Base64")] Secp256k1(Secp256k1PublicKey), + #[schemars(with = "Base64")] Secp256r1(Secp256r1PublicKey), } @@ -677,6 +680,41 @@ impl Signature { { secret.sign(&bcs::to_bytes(&value).expect("Message serialization should not fail")) } + /// Parse CompressedSignature from the SuiSignature `flag || sig || pk`. + /// This is useful for the multisig to combine partial signature into a multi-public key. + pub fn to_compressed(&self) -> CompressedSignature { + let bytes = self.signature_bytes(); + match self.scheme() { + SignatureScheme::ED25519 => { + CompressedSignature::Ed25519(Ed25519Signature::from_bytes(bytes).unwrap()) + } + SignatureScheme::Secp256k1 => { + CompressedSignature::Secp256k1(Secp256k1Signature::from_bytes(bytes).unwrap()) + } + SignatureScheme::Secp256r1 => { + CompressedSignature::Secp256r1(Secp256r1Signature::from_bytes(bytes).unwrap()) + } + _ => todo!("Unsupported signature scheme in multisig"), + } + } + + /// Parse PublicKey from the SuiSignature `flag || sig || pk`. + /// This is useful for the multisig to combine partial signature into a multi-public key. + pub fn to_public_key(&self) -> PublicKey { + let bytes = self.public_key_bytes(); + match self.scheme() { + SignatureScheme::ED25519 => { + PublicKey::Ed25519(Ed25519PublicKey::from_bytes(bytes).unwrap()) + } + SignatureScheme::Secp256k1 => { + PublicKey::Secp256k1(Secp256k1PublicKey::from_bytes(bytes).unwrap()) + } + SignatureScheme::Secp256r1 => { + PublicKey::Secp256r1(Secp256r1PublicKey::from_bytes(bytes).unwrap()) + } + _ => todo!("Unsupported signature scheme in multisig"), + } + } } impl AsRef<[u8]> for Signature { @@ -1550,3 +1588,28 @@ impl SignatureScheme { } } } + +/// Unlike `enum Signature`, `enum CompressedSignature` does not contain public key. +#[derive(Debug, From, Clone, Serialize, Deserialize, PartialEq, Eq, JsonSchema)] +pub enum CompressedSignature { + #[schemars(with = "Base64")] + Ed25519(Ed25519Signature), + #[schemars(with = "Base64")] + Secp256k1(Secp256k1Signature), + #[schemars(with = "Base64")] + Secp256r1(Secp256r1Signature), +} + +// impl FromStr for Signature { +// type Err = eyre::Report; +// fn from_str(s: &str) -> Result { +// Self::decode_base64(s).map_err(|e| eyre!("Fail to decode base64 {}", e.to_string())) +// } +// } + +// impl FromStr for PublicKey { +// type Err = eyre::Report; +// fn from_str(s: &str) -> Result { +// Self::decode_base64(s).map_err(|e| eyre!("Fail to decode base64 {}", e.to_string())) +// } +// } \ No newline at end of file diff --git a/crates/sui-types/src/lib.rs b/crates/sui-types/src/lib.rs index 5b42ec8893abe..14bc15f39749c 100644 --- a/crates/sui-types/src/lib.rs +++ b/crates/sui-types/src/lib.rs @@ -30,6 +30,7 @@ pub mod committee; pub mod crypto; pub mod dynamic_field; pub mod event; +pub mod filter; pub mod gas; pub mod gas_coin; pub mod id; @@ -39,6 +40,7 @@ pub mod message_envelope; pub mod messages; pub mod messages_checkpoint; pub mod move_package; +pub mod multisig; pub mod object; pub mod query; pub mod quorum_driver_types; @@ -48,8 +50,6 @@ pub mod sui_serde; pub mod sui_system_state; pub mod temporary_store; -pub mod filter; - #[path = "./unit_tests/utils.rs"] pub mod utils; diff --git a/crates/sui-types/src/messages.rs b/crates/sui-types/src/messages.rs index 20776c1856b97..4ed3cbf23ffa1 100644 --- a/crates/sui-types/src/messages.rs +++ b/crates/sui-types/src/messages.rs @@ -15,6 +15,8 @@ use crate::message_envelope::{Envelope, Message, TrustedEnvelope, VerifiedEnvelo use crate::messages_checkpoint::{ AuthenticatedCheckpoint, CheckpointSequenceNumber, CheckpointSignatureMessage, }; +use crate::multisig::AuthenticatorTrait; +use crate::multisig::GenericSignature; use crate::object::{MoveObject, Object, ObjectFormatOptions, Owner, PACKAGE_VERSION}; use crate::storage::{DeleteKind, WriteKind}; use crate::{SUI_SYSTEM_STATE_OBJECT_ID, SUI_SYSTEM_STATE_OBJECT_SHARED_VERSION}; @@ -862,11 +864,11 @@ impl TransactionData { #[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Hash)] pub struct SenderSignedData { pub intent_message: IntentMessage, - pub tx_signature: Signature, + pub tx_signature: GenericSignature, } impl SenderSignedData { - pub fn new(tx_data: TransactionData, intent: Intent, tx_signature: Signature) -> Self { + pub fn new(tx_data: TransactionData, intent: Intent, tx_signature: GenericSignature) -> Self { Self { intent_message: IntentMessage::new(intent, tx_data), tx_signature, @@ -886,7 +888,7 @@ impl Message for SenderSignedData { return Ok(()); } self.tx_signature - .verify_secure(&self.intent_message, self.intent_message.value.sender) + .verify_secure_generic(&self.intent_message, self.intent_message.value.sender) } } @@ -951,31 +953,39 @@ impl Transaction { let intent1 = intent.clone(); let intent_msg = IntentMessage::new(intent, data); let signature = Signature::new_secure(&intent_msg, signer); - Self::new(SenderSignedData::new(data1, intent1, signature)) + Self::new(SenderSignedData::new(data1, intent1, signature.into())) } pub fn from_data(data: TransactionData, intent: Intent, signature: Signature) -> Self { - Self::new(SenderSignedData::new(data, intent, signature)) + Self::new(SenderSignedData::new(data, intent, signature.into())) } // TODO(joyqvq): remove and prefer to_tx_bytes_and_signature() pub fn to_network_data_for_execution(&self) -> (Base64, SignatureScheme, Base64, Base64) { + let s = match &self.tx_signature { + GenericSignature::Signature(s) => s, + _ => panic!("unexpected signature scheme"), + }; ( Base64::from_bytes( bcs::to_bytes(&self.intent_message.value) .unwrap() .as_slice(), ), - self.tx_signature.scheme(), - Base64::from_bytes(self.tx_signature.signature_bytes()), - Base64::from_bytes(self.tx_signature.public_key_bytes()), + s.scheme(), + Base64::from_bytes(s.signature_bytes()), + Base64::from_bytes(s.public_key_bytes()), ) } pub fn to_tx_bytes_and_signature(&self) -> (Base64, Base64) { + let bytes = match &self.data().tx_signature { + GenericSignature::Signature(s) => s.as_ref(), + _ => panic!("unexpected signature scheme"), + }; ( Base64::from_bytes(&bcs::to_bytes(&self.data().intent_message.value).unwrap()), - Base64::from_bytes(self.data().tx_signature.as_ref()), + Base64::from_bytes(bytes), ) } } @@ -1016,9 +1026,9 @@ impl VerifiedTransaction { }) .pipe(|data| SenderSignedData { intent_message: IntentMessage::new(Intent::default(), data), - tx_signature: Ed25519SuiSignature::from_bytes(&[0; Ed25519SuiSignature::LENGTH]) + tx_signature: GenericSignature::Signature(Ed25519SuiSignature::from_bytes(&[0; Ed25519SuiSignature::LENGTH]) .unwrap() - .into(), + .into()), }) .pipe(Transaction::new) .pipe(Self::new_from_verified) diff --git a/crates/sui-types/src/multisig.rs b/crates/sui-types/src/multisig.rs new file mode 100644 index 0000000000000..16110a9234a6e --- /dev/null +++ b/crates/sui-types/src/multisig.rs @@ -0,0 +1,223 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + crypto::{CompressedSignature, SuiSignature}, + sui_serde::SuiBitmap, +}; +pub use enum_dispatch::enum_dispatch; +use fastcrypto::{ + ed25519::Ed25519PublicKey, encoding::Base64, secp256k1::Secp256k1PublicKey, + secp256r1::Secp256r1PublicKey, traits::ToFromBytes, Verifier, +}; +use roaring::RoaringBitmap; +use schemars::JsonSchema; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use std::hash::{Hash, Hasher}; + +use crate::{ + base_types::SuiAddress, + crypto::{PublicKey, Signature}, + error::SuiError, + intent::IntentMessage, +}; + +#[cfg(test)] +#[path = "unit_tests/multisig_tests.rs"] +mod multisig_tests; + +pub type WeightUnit = u8; +pub type ThresholdUnit = u16; +pub const MAX_PKS_IN_MULTISIG: usize = 10; +#[enum_dispatch] +pub trait AuthenticatorTrait { + fn verify_secure_generic( + &self, + value: &IntentMessage, + author: SuiAddress, + ) -> Result<(), SuiError> + where + T: Serialize; +} + +#[enum_dispatch(AuthenticatorTrait)] +#[derive(Debug, Serialize, Deserialize, Clone, PartialEq, Eq, JsonSchema, Hash)] +#[serde(untagged)] +pub enum GenericSignature { + MultiSignature, + Signature, +} + +#[serde_as] +#[derive(Debug, Serialize, Deserialize, Clone, JsonSchema)] +pub struct MultiSignature { + sigs: Vec, + #[schemars(with = "Base64")] + #[serde_as(as = "SuiBitmap")] + bitmap: RoaringBitmap, + multi_pk: MultiPublicKey, +} + +impl PartialEq for MultiSignature { + fn eq(&self, other: &Self) -> bool { + self.sigs == other.sigs && self.bitmap == other.bitmap && self.multi_pk == other.multi_pk + } +} +impl Eq for MultiSignature {} + +impl Hash for MultiSignature { + fn hash(&self, _state: &mut H) { + todo!() + } +} + +impl MultiSignature { + pub fn size(&self) -> usize { + self.sigs.len() + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, JsonSchema)] +pub struct MultiPublicKey { + pk_map: Vec<(PublicKey, WeightUnit)>, + threshold: ThresholdUnit, +} + +impl MultiPublicKey { + pub fn new( + pks: Vec, + weights: Vec, + threshold: ThresholdUnit, + ) -> Result { + if pks.len() != weights.len() || pks.len() > 10 { + return Err(SuiError::InvalidSignature { + error: "Invalid number of public keys".to_string(), + }); + } + Ok(MultiPublicKey { + pk_map: pks.into_iter().zip(weights.into_iter()).collect(), + threshold, + }) + } + + pub fn get_index(&self, pk: PublicKey) -> Option { + self.pk_map.iter().position(|x| x.0 == pk).map(|x| x as u32) + } + + pub fn threshold(&self) -> &ThresholdUnit { + &self.threshold + } + + pub fn pubkeys(&self) -> &Vec<(PublicKey, WeightUnit)> { + &self.pk_map + } +} + +impl AuthenticatorTrait for MultiSignature { + fn verify_secure_generic( + &self, + value: &IntentMessage, + author: SuiAddress, + ) -> Result<(), SuiError> + where + T: Serialize, + { + if self.multi_pk.pk_map.len() > MAX_PKS_IN_MULTISIG { + return Err(SuiError::InvalidSignature { + error: "Invalid number of public keys".to_string(), + }); + } + if (self.multi_pk.pk_map.len() as u16) < self.multi_pk.threshold { + return Err(SuiError::InvalidSignature { + error: "Invalid number of public keys".to_string(), + }); + } + + if >::from(self.multi_pk.clone()) != author { + return Err(SuiError::InvalidSignature { + error: "Invalid address".to_string(), + }); + } + let mut weight_sum = 0; + let msg = &bcs::to_bytes(value).unwrap(); + + for (sig, i) in self.sigs.iter().zip(&self.bitmap) { + let pk_map = self + .multi_pk + .pk_map + .get(i as usize) + .ok_or(SuiError::InvalidSignature { + error: "Invalid public keys index".to_string(), + }) + .unwrap(); + let res = match sig { + CompressedSignature::Ed25519(s) => { + let pk = Ed25519PublicKey::from_bytes(pk_map.0.as_ref()) + .map_err(|_| SuiError::InvalidSignature { + error: "Invalid public key".to_string(), + }) + .unwrap(); + pk.verify(msg, s) + } + CompressedSignature::Secp256k1(s) => { + let pk = Secp256k1PublicKey::from_bytes(pk_map.0.as_ref()) + .map_err(|_| SuiError::InvalidSignature { + error: "Invalid public key".to_string(), + }) + .unwrap(); + pk.verify(msg, s) + } + CompressedSignature::Secp256r1(s) => { + let pk = Secp256r1PublicKey::from_bytes(pk_map.0.as_ref()).map_err(|_| { + SuiError::InvalidSignature { + error: "Invalid public key".to_string(), + } + })?; + pk.verify(msg, s) + } + }; + if res.is_ok() { + weight_sum += pk_map.1 as u16; + } + } + + if weight_sum >= self.multi_pk.threshold { + Ok(()) + } else { + Err(SuiError::InvalidSignature { + error: "Insufficient weight".to_string(), + }) + } + } +} +impl MultiSignature { + pub fn combine(full_sigs: Vec, multi_pk: MultiPublicKey) -> Result { + let mut bitmap = RoaringBitmap::new(); + let mut sigs = Vec::new(); + full_sigs.iter().for_each(|s| { + bitmap.insert(multi_pk.get_index(s.to_public_key()).unwrap()); + sigs.push(s.to_compressed()); + }); + + Ok(MultiSignature { + sigs, + bitmap, + multi_pk, + }) + } +} + +/// Port to the verify_secure defined on Single Signature. +impl AuthenticatorTrait for Signature { + fn verify_secure_generic( + &self, + value: &IntentMessage, + author: SuiAddress, + ) -> Result<(), SuiError> + where + T: Serialize, + { + self.verify_secure(value, author) + } +} diff --git a/crates/sui-types/src/unit_tests/base_types_tests.rs b/crates/sui-types/src/unit_tests/base_types_tests.rs index 0c544d6c820da..4cc9c8a1eca99 100644 --- a/crates/sui-types/src/unit_tests/base_types_tests.rs +++ b/crates/sui-types/src/unit_tests/base_types_tests.rs @@ -349,6 +349,11 @@ fn test_move_package_size_for_gas_metering() { assert_eq!(size + 2, serialized.len()); } +#[test] +fn test_address_from_multisig_public_keys() { + // TODO +} + // A sample address in hex generated by the current address derivation algorithm. #[cfg(test)] const SAMPLE_ADDRESS: &str = "32866f0109fa1ba911392dcd2d4260f1d8243133"; diff --git a/crates/sui-types/src/unit_tests/messages_tests.rs b/crates/sui-types/src/unit_tests/messages_tests.rs index 1726531902fbc..73b117ecd75dd 100644 --- a/crates/sui-types/src/unit_tests/messages_tests.rs +++ b/crates/sui-types/src/unit_tests/messages_tests.rs @@ -659,11 +659,12 @@ fn verify_sender_signature_correctly_with_flag() { AuthorityPublicKeyBytes::from(sec1.public()), ); + let s = match &transaction.data().tx_signature { + GenericSignature::Signature(s) => s, + _ => panic!("invalid"), + }; // signature contains the correct Secp256k1 flag - assert_eq!( - transaction.data().tx_signature.scheme().flag(), - Secp256k1SuiSignature::SCHEME.flag() - ); + assert_eq!(s.scheme().flag(), Secp256k1SuiSignature::SCHEME.flag()); // authority accepts signs tx after verification assert!(signed_tx @@ -682,12 +683,13 @@ fn verify_sender_signature_correctly_with_flag() { &sec1, AuthorityPublicKeyBytes::from(sec1.public()), ); + let s = match &transaction_1.data().tx_signature { + GenericSignature::Signature(s) => s, + _ => panic!("unexpected signature scheme"), + }; // signature contains the correct Ed25519 flag - assert_eq!( - transaction_1.data().tx_signature.scheme().flag(), - Ed25519SuiSignature::SCHEME.flag() - ); + assert_eq!(s.scheme().flag(), Ed25519SuiSignature::SCHEME.flag()); // signature verified assert!(signed_tx_1 diff --git a/crates/sui-types/src/unit_tests/multisig_tests.rs b/crates/sui-types/src/unit_tests/multisig_tests.rs new file mode 100644 index 0000000000000..c771a9eabe828 --- /dev/null +++ b/crates/sui-types/src/unit_tests/multisig_tests.rs @@ -0,0 +1,110 @@ +// Copyright (c) Mysten Labs, Inc. +// SPDX-License-Identifier: Apache-2.0 + +use crate::{ + base_types::SuiAddress, + crypto::{get_key_pair, Signature, SuiKeyPair}, + intent::{Intent, IntentMessage, PersonalMessage}, + multisig::AuthenticatorTrait, +}; + +use super::{MultiPublicKey, MultiSignature}; + +#[test] +fn multisig_scenarios() { + let ed_kp: SuiKeyPair = SuiKeyPair::Ed25519(get_key_pair().1); + let k1_kp: SuiKeyPair = SuiKeyPair::Secp256k1(get_key_pair().1); + let r1_kp: SuiKeyPair = SuiKeyPair::Secp256r1(get_key_pair().1); + + let pk1 = ed_kp.public(); + let pk2 = k1_kp.public(); + let pk3 = r1_kp.public(); + + let multi_pk = MultiPublicKey::new( + vec![pk1.clone(), pk2.clone(), pk3.clone()], + vec![1, 1, 1], + 2, + ) + .unwrap(); + let addr = SuiAddress::from(multi_pk.clone()); + let msg = IntentMessage::new( + Intent::default(), + PersonalMessage { + message: "Hello".as_bytes().to_vec(), + }, + ); + let sig1 = Signature::new_secure(&msg, &ed_kp); + let sig2 = Signature::new_secure(&msg, &k1_kp); + let sig3 = Signature::new_secure(&msg, &r1_kp); + + // Any 2 of 3 signatures verifies ok. + let multisig1 = + MultiSignature::combine(vec![sig1.clone(), sig2.clone()], multi_pk.clone()).unwrap(); + assert!(multisig1.verify_secure_generic(&msg, addr).is_ok()); + + let multisig2 = + MultiSignature::combine(vec![sig1.clone(), sig3.clone()], multi_pk.clone()).unwrap(); + assert!(multisig2.verify_secure_generic(&msg, addr).is_ok()); + + let multisig3 = + MultiSignature::combine(vec![sig2.clone(), sig3.clone()], multi_pk.clone()).unwrap(); + assert!(multisig3.verify_secure_generic(&msg, addr).is_ok()); + + // 1 of 3 signature verify fails. + let multisig4 = MultiSignature::combine(vec![sig2.clone()], multi_pk).unwrap(); + assert!(multisig4.verify_secure_generic(&msg, addr).is_err()); + + // Incorrect address fails. + let kp4: SuiKeyPair = SuiKeyPair::Secp256r1(get_key_pair().1); + let pk4 = kp4.public(); + let multi_pk_1 = MultiPublicKey::new( + vec![pk1.clone(), pk2.clone(), pk3.clone(), pk4], + vec![1, 1, 1, 1], + 1, + ) + .unwrap(); + let multisig5 = MultiSignature::combine(vec![sig1.clone(), sig2.clone()], multi_pk_1).unwrap(); + assert!(multisig5.verify_secure_generic(&msg, addr).is_err()); + + // Weight of pk1: 1, pk2: 2, pk3: 3, threshold 3. + let multi_pk_2 = MultiPublicKey::new(vec![pk1, pk2, pk3], vec![1, 2, 3], 3).unwrap(); + let addr_2 = SuiAddress::from(multi_pk_2.clone()); + + // sig1 and sig2 (3 of 6) verifies ok. + let multi_sig_6 = + MultiSignature::combine(vec![sig1, sig2.clone()], multi_pk_2.clone()).unwrap(); + assert!(multi_sig_6.verify_secure_generic(&msg, addr_2).is_ok()); + + // sig3 (3 of 6) itself verifies ok. + let multi_sig_7 = MultiSignature::combine(vec![sig3], multi_pk_2.clone()).unwrap(); + assert!(multi_sig_7.verify_secure_generic(&msg, addr_2).is_ok()); + + // sig2 (2 of 6)itself verifies fail. + let multi_sig_8 = MultiSignature::combine(vec![sig2], multi_pk_2).unwrap(); + assert!(multi_sig_8.verify_secure_generic(&msg, addr_2).is_err()); +} + +#[test] +fn test_serde() { + // multi_pk + + // multi_sig + + // pubkey + + // compressed sig +} + +#[test] +fn single_sig_port_works() { + let kp: SuiKeyPair = SuiKeyPair::Ed25519(get_key_pair().1); + let addr = SuiAddress::from(&kp.public()); + let msg = IntentMessage::new( + Intent::default(), + PersonalMessage { + message: "Hello".as_bytes().to_vec(), + }, + ); + let sig = Signature::new_secure(&msg, &kp); + assert!(sig.verify_secure_generic(&msg, addr).is_ok()); +} diff --git a/crates/sui/src/sui_commands.rs b/crates/sui/src/sui_commands.rs index f975caf5246b5..13f7287064c31 100644 --- a/crates/sui/src/sui_commands.rs +++ b/crates/sui/src/sui_commands.rs @@ -132,6 +132,7 @@ impl SuiCommand { .unwrap_or(sui_config_dir()?.join(SUI_NETWORK_CONFIG)); let network_config: NetworkConfig = PersistedConfig::read(&network_config_path) .map_err(|err| { + println!("err+{}", &err); err.context(format!( "Cannot open Sui network config file at {:?}", network_config_path diff --git a/crates/sui/src/unit_tests/cli_tests.rs b/crates/sui/src/unit_tests/cli_tests.rs index d0397b589ff6c..82a764c88d18b 100644 --- a/crates/sui/src/unit_tests/cli_tests.rs +++ b/crates/sui/src/unit_tests/cli_tests.rs @@ -1200,3 +1200,22 @@ async fn test_serialize_tx() -> Result<(), anyhow::Error> { .await?; Ok(()) } + +#[sim_test] +async fn test_execute_multisig_tx() -> Result<(), anyhow::Error> { + // TODO + + // let mut test_cluster = TestClusterBuilder::new().build().await?; + // let context = &mut test_cluster.wallet; + // let mut txns = make_transactions_with_wallet_context(context, 1).await; + // let txn = txns.swap_remove(0); + + // let (tx_data, signature) = txn.to_tx_bytes_and_signature(); + // SuiClientCommands::ExecuteSignedTx { + // tx_bytes: tx_data.encoded(), + // signature: signature.encoded(), + // } + // .execute(context) + // .await?; + Ok(()) +} diff --git a/crates/sui/src/unit_tests/keytool_tests.rs b/crates/sui/src/unit_tests/keytool_tests.rs index d4ec5dfa2d96a..a86d23eea2a66 100644 --- a/crates/sui/src/unit_tests/keytool_tests.rs +++ b/crates/sui/src/unit_tests/keytool_tests.rs @@ -302,3 +302,15 @@ fn test_keytool_bls12381() -> Result<(), anyhow::Error> { .execute(&mut keystore)?; Ok(()) } + +#[test] +fn test_keytool_multisig_address() -> Result<(), anyhow::Error> { + // TODO + Ok(()) +} + +#[test] +fn test_keytool_combine_multisig() -> Result<(), anyhow::Error> { + // TODO + Ok(()) +}