From 1ad23430126711d6fca8899e4f00306e76bf98f8 Mon Sep 17 00:00:00 2001 From: Joshy Orndorff Date: Thu, 15 Feb 2024 14:06:22 -0500 Subject: [PATCH] Improve Verifier Trait: Redeemer type and block number access (#172) Signed-off-by: Matteo Muraca Co-authored-by: Matteo Muraca Co-authored-by: Gorzorg --- Cargo.lock | 18 +- Cargo.toml | 1 + runtime-expanded.rs | 0 tuxedo-core/aggregator/Cargo.toml | 1 + tuxedo-core/aggregator/src/lib.rs | 75 ++++- tuxedo-core/src/executive.rs | 27 +- tuxedo-core/src/verifier.rs | 335 +++---------------- tuxedo-core/src/verifier/multi_signature.rs | 237 +++++++++++++ tuxedo-core/src/verifier/simple_signature.rs | 159 +++++++++ tuxedo-parachain-core/src/validate_block.rs | 1 + 10 files changed, 547 insertions(+), 307 deletions(-) create mode 100644 runtime-expanded.rs create mode 100644 tuxedo-core/src/verifier/multi_signature.rs create mode 100644 tuxedo-core/src/verifier/simple_signature.rs diff --git a/Cargo.lock b/Cargo.lock index e18c2717f..006e61606 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -151,6 +151,7 @@ dependencies = [ name = "aggregator" version = "0.1.0" dependencies = [ + "convert_case 0.6.0", "quote", "syn 2.0.39", ] @@ -1661,6 +1662,15 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" +[[package]] +name = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + [[package]] name = "core-foundation" version = "0.9.3" @@ -2913,7 +2923,7 @@ version = "0.99.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" dependencies = [ - "convert_case", + "convert_case 0.4.0", "proc-macro2", "quote", "rustc_version", @@ -14141,6 +14151,12 @@ dependencies = [ "tinyvec", ] +[[package]] +name = "unicode-segmentation" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202" + [[package]] name = "unicode-width" version = "0.1.11" diff --git a/Cargo.toml b/Cargo.toml index 524a68b43..62b8f5292 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,7 @@ async-io = "2.0" async-trait = "0.1.73" clap = "4.3.0" color-print = "0.3.4" +convert_case = "0.6.0" hex-literal = "0.4.1" jsonrpsee = "0.16.2" log = "0.4" diff --git a/runtime-expanded.rs b/runtime-expanded.rs new file mode 100644 index 000000000..e69de29bb diff --git a/tuxedo-core/aggregator/Cargo.toml b/tuxedo-core/aggregator/Cargo.toml index 82ce5153f..512352b44 100644 --- a/tuxedo-core/aggregator/Cargo.toml +++ b/tuxedo-core/aggregator/Cargo.toml @@ -4,6 +4,7 @@ name = "aggregator" version = "0.1.0" [dependencies] +convert_case = { workspace = true } quote = { workspace = true } syn = { features = [ "extra-traits", "full" ], workspace = true } diff --git a/tuxedo-core/aggregator/src/lib.rs b/tuxedo-core/aggregator/src/lib.rs index f12f78f8b..5d7eaa8b9 100644 --- a/tuxedo-core/aggregator/src/lib.rs +++ b/tuxedo-core/aggregator/src/lib.rs @@ -1,3 +1,4 @@ +use convert_case::{Case, Casing}; use proc_macro::TokenStream; use quote::quote; use syn::{parse_macro_input, Ident, ItemEnum}; @@ -73,9 +74,45 @@ pub fn aggregate(_: TokenStream, body: TokenStream) -> TokenStream { pub fn tuxedo_verifier(_: TokenStream, body: TokenStream) -> TokenStream { let ast = parse_macro_input!(body as ItemEnum); let original_code = ast.clone(); + let vis = ast.vis; let outer_type = ast.ident; - let variants = ast.variants.into_iter().map(|v| v.ident); + let variant_type_pairs = ast.variants.iter().map(|variant| { + // Make sure there is only a single field, and if not, give a helpful error + assert!( + variant.fields.len() == 1, + "Each variant must have a single unnamed field" + ); + ( + variant.ident.clone(), + variant + .fields + .iter() + .next() + .expect("exactly one field per variant") + .ty + .clone(), + ) + }); + let variants = variant_type_pairs.clone().map(|(v, _t)| v); + let inner_types = variant_type_pairs.map(|(_v, t)| t); + + // Set up the name of the new associated type. + let mut redeemer_type_name = outer_type.to_string(); + redeemer_type_name.push_str("Redeemer"); + let redeemer_type = Ident::new(&redeemer_type_name, outer_type.span()); + + // TODO there must be a better way to do this, right? + let inner_types2 = inner_types.clone(); + let variants2 = variants.clone(); + let variants3 = variants.clone(); + + let as_variants = variants.clone().map(|v| { + let s = format!("as_{}", v); + let s = s.to_case(Case::Snake); + Ident::new(&s, v.span()) + }); + let as_variants2 = as_variants.clone(); let output = quote! { @@ -83,11 +120,43 @@ pub fn tuxedo_verifier(_: TokenStream, body: TokenStream) -> TokenStream { #[tuxedo_core::aggregate] #original_code + /// This type is generated by the `#[tuxedo_verifier]` macro. + /// It is a combined redeemer type for the redeemers of each individual verifier. + /// + /// This type is accessible downstream as `::Redeemer` + #[derive(Debug, Decode)] + #vis enum #redeemer_type { + #( + #variants(<#inner_types as tuxedo_core::Verifier>::Redeemer), + )* + } + + // Put a bunch of methods like `.as_variant1()` on the aggregate redeemer type + // These are necessary when unwrapping the onion. + // Might be that we should have a helper macro for this as well + impl #redeemer_type { + #( + pub fn #as_variants(&self) -> Option<&<#inner_types2 as tuxedo_core::Verifier>::Redeemer> { + match self { + Self::#variants2(inner) => Some(inner), + _ => None, + } + } + )* + } + impl tuxedo_core::Verifier for #outer_type { - fn verify(&self, simplified_tx: &[u8], redeemer: &[u8]) -> bool { + + type Redeemer = #redeemer_type; + + fn verify(&self, simplified_tx: &[u8], block_number: u32, redeemer: &Self::Redeemer) -> bool { match self { #( - Self::#variants(inner) => inner.verify(simplified_tx, redeemer), + Self::#variants3(inner) => inner.verify( + simplified_tx, + block_number, + redeemer.#as_variants2().expect("redeemer variant exists because the macro constructed that type.") + ), )* } } diff --git a/tuxedo-core/src/executive.rs b/tuxedo-core/src/executive.rs index aaec674c2..93cddf0d6 100644 --- a/tuxedo-core/src/executive.rs +++ b/tuxedo-core/src/executive.rs @@ -35,8 +35,12 @@ use sp_std::{collections::btree_set::BTreeSet, vec::Vec}; /// in the proper generic types. pub struct Executive(PhantomData<(B, V, C)>); -impl>, V: Verifier, C: ConstraintChecker> - Executive +impl Executive +where + B: BlockT>, + B::Header: HeaderT, // Tuxedo always uses u32 for block number. + V: Verifier, + C: ConstraintChecker, { /// Does pool-style validation of a tuxedo transaction. /// Does not commit anything to storage. @@ -75,10 +79,12 @@ impl>, V: Verifier, C: ConstraintChecker let mut missing_inputs = Vec::new(); for input in transaction.inputs.iter() { if let Some(input_utxo) = TransparentUtxoSet::::peek_utxo(&input.output_ref) { + let redeemer = V::Redeemer::decode(&mut &input.redeemer[..]) + .map_err(|_| UtxoError::VerifierError)?; ensure!( input_utxo .verifier - .verify(&stripped_encoded, &input.redeemer), + .verify(&stripped_encoded, Self::block_height(), &redeemer), UtxoError::VerifierError ); input_utxos.push(input_utxo); @@ -475,7 +481,7 @@ impl>, V: Verifier, C: ConstraintChecker mod tests { use sp_core::H256; use sp_io::TestExternalities; - use sp_runtime::transaction_validity::ValidTransactionBuilder; + use sp_runtime::{generic::Header, transaction_validity::ValidTransactionBuilder}; use crate::{ constraint_checker::testing::TestConstraintChecker, @@ -619,10 +625,15 @@ mod tests { ext.insert(output_ref.encode(), output.encode()); } - // Write the pre-header - if let Some(pre_header) = self.pre_header { - ext.insert(HEADER_KEY.to_vec(), pre_header.encode()); - } + // Write a pre-header. If none was supplied, create a use a default one. + let pre_header = self.pre_header.unwrap_or(Header { + parent_hash: Default::default(), + number: 0, + state_root: H256::zero(), + extrinsics_root: H256::zero(), + digest: Default::default(), + }); + ext.insert(HEADER_KEY.to_vec(), pre_header.encode()); // Write the noted extrinsics ext.insert(EXTRINSIC_KEY.to_vec(), self.noted_extrinsics.encode()); diff --git a/tuxedo-core/src/verifier.rs b/tuxedo-core/src/verifier.rs index 140648954..06afd4cfe 100644 --- a/tuxedo-core/src/verifier.rs +++ b/tuxedo-core/src/verifier.rs @@ -8,44 +8,29 @@ use parity_scale_codec::{Decode, Encode}; use scale_info::TypeInfo; use serde::{Deserialize, Serialize}; -use sp_core::sr25519::{Public, Signature}; -use sp_core::H256; -use sp_std::collections::btree_map::BTreeMap; -use sp_std::collections::btree_set::BTreeSet; use sp_std::fmt::Debug; -use sp_std::vec::Vec; -/// A means of checking that an output can be verified (aka spent). This check is made on a +mod multi_signature; +mod simple_signature; + +pub use multi_signature::ThresholdMultiSignature; +pub use simple_signature::{Sr25519Signature, P2PKH}; + +/// A means of checking that an output can be spent. This check is made on a /// per-output basis and neither knows nor cares anything about the validation logic that will /// be applied to the transaction as a whole. Nonetheless, in order to avoid malleability, we /// we take the entire stripped and serialized transaction as a parameter. +/// +/// Information available when verifying an input includes: +/// * The simplified transaction - a stripped encoded version of the transaction +/// * Some environmental information such as the block current block number +/// * An redeemer supplied by the user attempting to spend the input. pub trait Verifier: Debug + Encode + Decode + Clone { - fn verify(&self, simplified_tx: &[u8], redeemer: &[u8]) -> bool; -} - -/// A typical verifier that checks an sr25519 signature -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub struct Sr25519Signature { - pub owner_pubkey: H256, -} + /// The type that will be supplied to satisfy the verifier and redeem the UTXO. + type Redeemer: Decode; -impl Sr25519Signature { - pub fn new>(value: T) -> Self { - Sr25519Signature { - owner_pubkey: value.into(), - } - } -} - -impl Verifier for Sr25519Signature { - fn verify(&self, simplified_tx: &[u8], redeemer: &[u8]) -> bool { - let sig = match Signature::try_from(redeemer) { - Ok(s) => s, - Err(_) => return false, - }; - - sp_io::crypto::sr25519_verify(&sig, simplified_tx, &Public::from_h256(self.owner_pubkey)) - } + /// Main function in the trait. Does the checks to make sure an output can be spent. + fn verify(&self, simplified_tx: &[u8], block_height: u32, redeemer: &Self::Redeemer) -> bool; } /// A simple verifier that allows anyone to consume an output at any time @@ -55,98 +40,32 @@ impl Verifier for Sr25519Signature { pub struct UpForGrabs; impl Verifier for UpForGrabs { - fn verify(&self, _simplified_tx: &[u8], _redeemer: &[u8]) -> bool { - true - } -} - -/// A Threshold multisignature. Some number of member signatories collectively own inputs -/// guarded by this verifier. A valid redeemer must supply valid signatures by at least -/// `threshold` of the signatories. If the threshold is greater than the number of signatories -/// the input can never be consumed. -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] -pub struct ThresholdMultiSignature { - /// The minimum number of valid signatures needed to consume this input - pub threshold: u8, - /// All the member signatories, some (or all depending on the threshold) of whom must - /// produce signatures over the transaction that will consume this input. - /// This should include no duplicates - pub signatories: Vec, -} + type Redeemer = (); -impl ThresholdMultiSignature { - pub fn new(threshold: u8, signatories: Vec) -> Self { - ThresholdMultiSignature { - threshold, - signatories, - } - } - - pub fn has_duplicate_signatories(&self) -> bool { - let set: BTreeSet<_> = self.signatories.iter().collect(); - set.len() < self.signatories.len() + fn verify(&self, _simplified_tx: &[u8], _: u32, _: &()) -> bool { + true } } -#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] -/// Combination of a signature plus and index so that the signer can specify which -/// index this signature pertains too of the available signatories for a `ThresholdMultiSignature` -pub struct SignatureAndIndex { - /// The signature of the signer - pub signature: Signature, - /// The index of this signer in the signatory vector - pub index: u8, -} - -impl Verifier for ThresholdMultiSignature { - fn verify(&self, simplified_tx: &[u8], redeemer: &[u8]) -> bool { - if self.has_duplicate_signatories() { - return false; - } - - let sigs = match Vec::::decode(&mut &redeemer[..]) { - Ok(s) => s, - Err(_) => return false, - }; - - if sigs.len() < self.threshold.into() { - return false; - } - - { - // Check range of indicies - let index_out_of_bounds = sigs.iter().any(|sig| sig.index as usize >= sigs.len()); - if index_out_of_bounds { - return false; - } - } - - { - let set: BTreeMap = sigs - .iter() - .map(|sig_and_index| (sig_and_index.index, sig_and_index.signature.clone())) - .collect(); - - if set.len() < sigs.len() { - return false; - } - } +/// A simple verifier that allows no one to consume an output ever. +/// +/// This is useful for UTXOs that are expected to only ever be consumed by evictions, +/// such as inherents for example. +#[derive( + Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo, Default, +)] +pub struct Unspendable; - let valid_sigs: Vec<_> = sigs - .iter() - .map(|sig| { - sp_io::crypto::sr25519_verify( - &sig.signature, - simplified_tx, - &Public::from_h256(self.signatories[sig.index as usize]), - ); - }) - .collect(); +impl Verifier for Unspendable { + type Redeemer = (); - valid_sigs.len() >= self.threshold.into() + fn verify(&self, _simplified_tx: &[u8], _: u32, _: &()) -> bool { + false } } +// Idea: It could be useful to allow delay deciding whether the redemption should succeed +// until spend-time. In that case you could pass it in as a verifier. /// A testing verifier that passes or depending on the enclosed /// boolean value. #[cfg(feature = "std")] @@ -158,7 +77,9 @@ pub struct TestVerifier { #[cfg(feature = "std")] impl Verifier for TestVerifier { - fn verify(&self, _simplified_tx: &[u8], _redeemer: &[u8]) -> bool { + type Redeemer = (); + + fn verify(&self, _simplified_tx: &[u8], _: u32, _: &()) -> bool { self.verifies } } @@ -169,7 +90,7 @@ mod test { use sp_core::{crypto::Pair as _, sr25519::Pair}; /// Generate a bunch of test keypairs - fn generate_n_pairs(n: u8) -> Vec { + pub(crate) fn generate_n_pairs(n: u8) -> Vec { let mut seed = [0u8; 32]; let mut pairs = Vec::new(); @@ -186,194 +107,18 @@ mod test { #[test] fn up_for_grabs_always_verifies() { - assert!(UpForGrabs.verify(&[], &[])) - } - - #[test] - fn sr25519_signature_with_good_sig() { - let pair = Pair::from_seed(&[0u8; 32]); - let simplified_tx = b"hello world".as_slice(); - let sig = pair.sign(simplified_tx); - let redeemer: &[u8] = sig.as_ref(); - - let sr25519_signature = Sr25519Signature { - owner_pubkey: pair.public().into(), - }; - - assert!(sr25519_signature.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_with_enough_sigs_passes() { - let threshold = 2; - let pairs = generate_n_pairs(threshold); - - let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); - - let simplified_tx = b"hello_world".as_slice(); - let sigs: Vec<_> = pairs - .iter() - .enumerate() - .map(|(i, p)| SignatureAndIndex { - signature: p.sign(simplified_tx), - index: i.try_into().unwrap(), - }) - .collect(); - - let redeemer: &[u8] = &sigs.encode()[..]; - let threshold_multisig = ThresholdMultiSignature { - threshold, - signatories, - }; - - assert!(threshold_multisig.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_not_enough_sigs_fails() { - let threshold = 3; - let pairs = generate_n_pairs(threshold); - - let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); - - let simplified_tx = b"hello_world".as_slice(); - let sigs: Vec<_> = pairs - .iter() - .take(threshold as usize - 1) - .enumerate() - .map(|(i, p)| SignatureAndIndex { - signature: p.sign(simplified_tx), - index: i.try_into().unwrap(), - }) - .collect(); - - let redeemer: &[u8] = &sigs.encode()[..]; - let threshold_multisig = ThresholdMultiSignature { - threshold, - signatories, - }; - - assert!(!threshold_multisig.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_extra_sigs_still_passes() { - let threshold = 2; - let pairs = generate_n_pairs(threshold + 1); - - let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); - - let simplified_tx = b"hello_world".as_slice(); - let sigs: Vec<_> = pairs - .iter() - .enumerate() - .map(|(i, p)| SignatureAndIndex { - signature: p.sign(simplified_tx), - index: i.try_into().unwrap(), - }) - .collect(); - - let redeemer: &[u8] = &sigs.encode()[..]; - let threshold_multisig = ThresholdMultiSignature { - threshold, - signatories, - }; - - assert!(threshold_multisig.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_replay_sig_attack_fails() { - let threshold = 2; - let pairs = generate_n_pairs(threshold); - - let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); - - let simplified_tx = b"hello_world".as_slice(); - - let sigs: Vec = vec![ - SignatureAndIndex { - signature: pairs[0].sign(simplified_tx), - index: 0.try_into().unwrap(), - }, - SignatureAndIndex { - signature: pairs[0].sign(simplified_tx), - index: 0.try_into().unwrap(), - }, - ]; - - let redeemer: &[u8] = &sigs.encode()[..]; - let threshold_multisig = ThresholdMultiSignature { - threshold, - signatories, - }; - - assert!(!threshold_multisig.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_has_duplicate_signatories_fails() { - let threshold = 2; - let pairs = generate_n_pairs(threshold); - - let signatories: Vec = - vec![H256::from(pairs[0].public()), H256::from(pairs[0].public())]; - - let simplified_tx = b"hello_world".as_slice(); - - let sigs: Vec<_> = pairs - .iter() - .enumerate() - .map(|(i, p)| SignatureAndIndex { - signature: p.sign(simplified_tx), - index: i.try_into().unwrap(), - }) - .collect(); - let redeemer: &[u8] = &sigs.encode()[..]; - - let threshold_multisig = ThresholdMultiSignature { - threshold, - signatories, - }; - - assert!(!threshold_multisig.verify(simplified_tx, redeemer)); - } - - #[test] - fn threshold_multisig_bogus_redeemer_encoding_fails() { - use crate::dynamic_typing::testing::Bogus; - - let bogus = Bogus; - - let threshold_multisig = ThresholdMultiSignature { - threshold: 3, - signatories: vec![], - }; - - assert!(!threshold_multisig.verify(b"bogus_message".as_slice(), bogus.encode().as_slice())) - } - - #[test] - fn sr25519_signature_with_bad_sig() { - let simplified_tx = b"hello world".as_slice(); - let redeemer = b"bogus_signature".as_slice(); - - let sr25519_signature = Sr25519Signature { - owner_pubkey: H256::zero(), - }; - - assert!(!sr25519_signature.verify(simplified_tx, redeemer)); + assert!(UpForGrabs.verify(&[], 0, &())) } #[test] fn test_verifier_passes() { - let result = TestVerifier { verifies: true }.verify(&[], &[]); + let result = TestVerifier { verifies: true }.verify(&[], 0, &()); assert!(result); } #[test] fn test_verifier_fails() { - let result = TestVerifier { verifies: false }.verify(&[], &[]); + let result = TestVerifier { verifies: false }.verify(&[], 0, &()); assert!(!result); } } diff --git a/tuxedo-core/src/verifier/multi_signature.rs b/tuxedo-core/src/verifier/multi_signature.rs new file mode 100644 index 000000000..a4a6f1d38 --- /dev/null +++ b/tuxedo-core/src/verifier/multi_signature.rs @@ -0,0 +1,237 @@ +//! This module contains a `Verifier` implementation that acts as an N of M multisig. +//! It also contains the necessary auxiliary types. + +/// A Threshold multisignature. Some number of member signatories collectively own inputs +/// guarded by this verifier. A valid redeemer must supply valid signatures by at least +/// `threshold` of the signatories. If the threshold is greater than the number of signatories +/// the input can never be consumed. +use super::Verifier; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_core::{ + sr25519::{Public, Signature}, + H256, +}; +use sp_std::{ + collections::{btree_map::BTreeMap, btree_set::BTreeSet}, + vec::Vec, +}; + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct ThresholdMultiSignature { + /// The minimum number of valid signatures needed to consume this input + pub threshold: u8, + /// All the member signatories, some (or all depending on the threshold) of whom must + /// produce signatures over the transaction that will consume this input. + /// This should include no duplicates + pub signatories: Vec, +} + +impl ThresholdMultiSignature { + pub fn new(threshold: u8, signatories: Vec) -> Self { + ThresholdMultiSignature { + threshold, + signatories, + } + } + + pub fn has_duplicate_signatories(&self) -> bool { + let set: BTreeSet<_> = self.signatories.iter().collect(); + set.len() < self.signatories.len() + } +} + +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone)] +/// Combination of a signature plus and index so that the signer can specify which +/// index this signature pertains too of the available signatories for a `ThresholdMultiSignature` +pub struct SignatureAndIndex { + /// The signature of the signer + pub signature: Signature, + /// The index of this signer in the signatory vector + pub index: u8, +} + +impl Verifier for ThresholdMultiSignature { + type Redeemer = Vec; + + fn verify(&self, simplified_tx: &[u8], _: u32, sigs: &Vec) -> bool { + if self.has_duplicate_signatories() { + return false; + } + + if sigs.len() < self.threshold.into() { + return false; + } + + { + // Check range of indicies + let index_out_of_bounds = sigs.iter().any(|sig| sig.index as usize >= sigs.len()); + if index_out_of_bounds { + return false; + } + } + + { + let set: BTreeMap = sigs + .iter() + .map(|sig_and_index| (sig_and_index.index, sig_and_index.signature.clone())) + .collect(); + + if set.len() < sigs.len() { + return false; + } + } + + let valid_sigs: Vec<_> = sigs + .iter() + .map(|sig| { + sp_io::crypto::sr25519_verify( + &sig.signature, + simplified_tx, + &Public::from_h256(self.signatories[sig.index as usize]), + ); + }) + .collect(); + + valid_sigs.len() >= self.threshold.into() + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::verifier::test::generate_n_pairs; + use sp_core::crypto::Pair as _; + + #[test] + fn threshold_multisig_with_enough_sigs_passes() { + let threshold = 2; + let pairs = generate_n_pairs(threshold); + + let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); + + let simplified_tx = b"hello_world".as_slice(); + let sigs: Vec<_> = pairs + .iter() + .enumerate() + .map(|(i, p)| SignatureAndIndex { + signature: p.sign(simplified_tx), + index: i.try_into().unwrap(), + }) + .collect(); + + let threshold_multisig = ThresholdMultiSignature { + threshold, + signatories, + }; + + assert!(threshold_multisig.verify(simplified_tx, 0, &sigs)); + } + + #[test] + fn threshold_multisig_not_enough_sigs_fails() { + let threshold = 3; + let pairs = generate_n_pairs(threshold); + + let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); + + let simplified_tx = b"hello_world".as_slice(); + let sigs: Vec<_> = pairs + .iter() + .take(threshold as usize - 1) + .enumerate() + .map(|(i, p)| SignatureAndIndex { + signature: p.sign(simplified_tx), + index: i.try_into().unwrap(), + }) + .collect(); + + let threshold_multisig = ThresholdMultiSignature { + threshold, + signatories, + }; + + assert!(!threshold_multisig.verify(simplified_tx, 0, &sigs)); + } + + #[test] + fn threshold_multisig_extra_sigs_still_passes() { + let threshold = 2; + let pairs = generate_n_pairs(threshold + 1); + + let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); + + let simplified_tx = b"hello_world".as_slice(); + let sigs: Vec<_> = pairs + .iter() + .enumerate() + .map(|(i, p)| SignatureAndIndex { + signature: p.sign(simplified_tx), + index: i.try_into().unwrap(), + }) + .collect(); + + let threshold_multisig = ThresholdMultiSignature { + threshold, + signatories, + }; + + assert!(threshold_multisig.verify(simplified_tx, 0, &sigs)); + } + + #[test] + fn threshold_multisig_replay_sig_attack_fails() { + let threshold = 2; + let pairs = generate_n_pairs(threshold); + + let signatories: Vec = pairs.iter().map(|p| H256::from(p.public())).collect(); + + let simplified_tx = b"hello_world".as_slice(); + + let sigs: Vec = vec![ + SignatureAndIndex { + signature: pairs[0].sign(simplified_tx), + index: 0.try_into().unwrap(), + }, + SignatureAndIndex { + signature: pairs[0].sign(simplified_tx), + index: 0.try_into().unwrap(), + }, + ]; + + let threshold_multisig = ThresholdMultiSignature { + threshold, + signatories, + }; + + assert!(!threshold_multisig.verify(simplified_tx, 0, &sigs)); + } + + #[test] + fn threshold_multisig_has_duplicate_signatories_fails() { + let threshold = 2; + let pairs = generate_n_pairs(threshold); + + let signatories: Vec = + vec![H256::from(pairs[0].public()), H256::from(pairs[0].public())]; + + let simplified_tx = b"hello_world".as_slice(); + + let sigs: Vec<_> = pairs + .iter() + .enumerate() + .map(|(i, p)| SignatureAndIndex { + signature: p.sign(simplified_tx), + index: i.try_into().unwrap(), + }) + .collect(); + + let threshold_multisig = ThresholdMultiSignature { + threshold, + signatories, + }; + + assert!(!threshold_multisig.verify(simplified_tx, 0, &sigs)); + } +} diff --git a/tuxedo-core/src/verifier/simple_signature.rs b/tuxedo-core/src/verifier/simple_signature.rs new file mode 100644 index 000000000..8a08af1a9 --- /dev/null +++ b/tuxedo-core/src/verifier/simple_signature.rs @@ -0,0 +1,159 @@ +//! This module contains `Verifier` implementations for simple signature checking. +//! This is the most common way to implement private ownership in a UTXO chain and will +//! likely be used by most chains. +//! +//! Directly locking a UTXO to a public key is supported as well as locking behind a +//! public key hash like bitcoin's P2PKH. For the merits of each approach see: +//! https://bitcoin.stackexchange.com/q/72184 +//! +//! Currently there are only implementations for SR25519 signatures that makes use of +//! Substrate's host functions to do the actual cryptography. Other signature schemes or +//! pure wasm implementations are also welcome here. + +/// A very commonly used verifier that checks an sr25519 signature. +/// +/// This verifier relies on Substrate's host functions to perform the signature checking +/// natively and gain performance. +use super::Verifier; +use parity_scale_codec::{Decode, Encode}; +use scale_info::TypeInfo; +use serde::{Deserialize, Serialize}; +use sp_core::{ + sr25519::{Public, Signature}, + H256, +}; +use sp_runtime::traits::{BlakeTwo256, Hash}; + +/// Require a signature from the private key corresponding to the given public key. +/// This is the simplest way to require a signature. If you prefer not to expose the +/// public key until spend time, use P2PKH instead. +/// +/// Uses the Sr25519 signature scheme and Substrate's host functions. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct Sr25519Signature { + pub owner_pubkey: H256, +} + +impl Sr25519Signature { + /// Create a new instance that requires a signature from the given public key + pub fn new>(owner_pubkey: T) -> Self { + Sr25519Signature { + owner_pubkey: owner_pubkey.into(), + } + } +} + +impl Verifier for Sr25519Signature { + type Redeemer = Signature; + + fn verify(&self, simplified_tx: &[u8], _: u32, sig: &Signature) -> bool { + sp_io::crypto::sr25519_verify(sig, simplified_tx, &Public::from_h256(self.owner_pubkey)) + } +} + +/// Pay To Public Key Hash (P2PKH) +/// +/// Require a signature from the private key corresponding to the public key whose _hash_ is given. +/// This is the most common way to represent private ownership in UTXO networks like Bitcoin. +/// It is more complex than providing the public key directly but does not reveal the public key until spend time. +/// +/// Uses the Sr25519 signature scheme and BlakeTwo256 hashing algorithm via Substrate's host functions. +#[derive(Serialize, Deserialize, Encode, Decode, Debug, PartialEq, Eq, Clone, TypeInfo)] +pub struct P2PKH { + pub owner_pubkey_hash: H256, +} + +impl Verifier for P2PKH { + type Redeemer = (Public, Signature); + + fn verify(&self, simplified_tx: &[u8], _: u32, (pubkey, signature): &Self::Redeemer) -> bool { + BlakeTwo256::hash(pubkey) == self.owner_pubkey_hash + && sp_io::crypto::sr25519_verify(signature, simplified_tx, pubkey) + } +} + +#[cfg(test)] +mod test { + use super::*; + use sp_core::{crypto::Pair as _, sr25519::Pair}; + + fn bad_sig() -> Signature { + Signature::from_slice( + b"bogus_signature_bogus_signature_bogus_signature_bogus_signature!".as_slice(), + ) + .expect("Should be able to create a bogus signature.") + } + + #[test] + fn sr25519_signature_with_good_sig() { + let pair = Pair::from_seed(&[0u8; 32]); + let simplified_tx = b"hello world".as_slice(); + let sig = pair.sign(simplified_tx); + + let sr25519_signature = Sr25519Signature { + owner_pubkey: pair.public().into(), + }; + + assert!(sr25519_signature.verify(simplified_tx, 0, &sig)); + } + + #[test] + fn sr25519_signature_with_bad_sig() { + let simplified_tx = b"hello world".as_slice(); + let sr25519_signature = Sr25519Signature { + owner_pubkey: H256::zero(), + }; + + assert!(!sr25519_signature.verify(simplified_tx, 0, &bad_sig())); + } + + #[test] + fn p2pkh_success() { + let pair = Pair::from_seed(&[0u8; 32]); + let owner_pubkey_hash = BlakeTwo256::hash(&pair.public()); + let simplified_tx = b"hello world".as_slice(); + let sig = pair.sign(simplified_tx); + + let p2pkh = P2PKH { owner_pubkey_hash }; + + assert!(p2pkh.verify(simplified_tx, 0, &(pair.public(), sig))); + } + + #[test] + fn p2pkh_correct_pubkey_bad_sig() { + let pair = Pair::from_seed(&[0u8; 32]); + let owner_pubkey_hash = BlakeTwo256::hash(&pair.public()); + let simplified_tx = b"hello world".as_slice(); + + let p2pkh = P2PKH { owner_pubkey_hash }; + + assert!(!p2pkh.verify(simplified_tx, 0, &(pair.public(), bad_sig()))); + } + + #[test] + fn p2pkh_incorrect_pubkey_but_valid_sig_from_provided_pubkey() { + let owner_pair = Pair::from_seed(&[0u8; 32]); + let owner_pubkey_hash = BlakeTwo256::hash(&owner_pair.public()); + let simplified_tx = b"hello world".as_slice(); + + let p2pkh = P2PKH { owner_pubkey_hash }; + + let attacker_pair = Pair::from_seed(&[1u8; 32]); + let attacker_sig = attacker_pair.sign(simplified_tx); + + assert!(!p2pkh.verify(simplified_tx, 0, &(attacker_pair.public(), attacker_sig))); + } + + #[test] + fn p2pkh_incorrect_pubkey_and_bogus_sig() { + let owner_pair = Pair::from_seed(&[0u8; 32]); + let owner_pubkey_hash = BlakeTwo256::hash(&owner_pair.public()); + let simplified_tx = b"hello world".as_slice(); + + let p2pkh = P2PKH { owner_pubkey_hash }; + + let attacker_pair = Pair::from_seed(&[1u8; 32]); + + assert!(!p2pkh.verify(simplified_tx, 0, &(attacker_pair.public(), bad_sig()))); + } +} diff --git a/tuxedo-parachain-core/src/validate_block.rs b/tuxedo-parachain-core/src/validate_block.rs index 3fe7d6f95..70c1cb063 100644 --- a/tuxedo-parachain-core/src/validate_block.rs +++ b/tuxedo-parachain-core/src/validate_block.rs @@ -71,6 +71,7 @@ pub fn validate_block( ) -> ValidationResult where B: BlockT>, + B::Header: HeaderT, // Tuxedo always uses u32 for block number. Transaction: Extrinsic, V: TypeInfo + Verifier + 'static, C: TypeInfo + ConstraintChecker + 'static, // + Into>,