diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index 43ee77d2d4..d3ca87cef7 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -63,6 +63,7 @@ reentrancy refcount scalability scalable +sr25519 stdin stdout subber diff --git a/CHANGELOG.md b/CHANGELOG.md index 5abe96f342..cfd18def71 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Added - Schema generation - [#1765](https://github.com/paritytech/ink/pull/1765) - Add `set_block_number` to off-chain test api `Engine` - [#1806](https://github.com/paritytech/ink/pull/1806) +- Added `sr25519_verify` function to `ink_env` [#1840](https://github.com/paritytech/ink/pull/1840) ### Changed - E2E: improve call API, remove `build_message` + callback - [#1782](https://github.com/paritytech/ink/pull/1782) diff --git a/crates/e2e/macro/src/codegen.rs b/crates/e2e/macro/src/codegen.rs index b2c26c2379..512a9183cd 100644 --- a/crates/e2e/macro/src/codegen.rs +++ b/crates/e2e/macro/src/codegen.rs @@ -218,7 +218,7 @@ impl ContractManifests { let cmd = cargo_metadata::MetadataCommand::new(); let metadata = cmd .exec() - .unwrap_or_else(|err| panic!("Error invoking `cargo metadata`: {}", err)); + .unwrap_or_else(|err| panic!("Error invoking `cargo metadata`: {err}")); fn maybe_contract_package(package: &cargo_metadata::Package) -> Option { package diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index 54434fa39a..7664b333b9 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -97,6 +97,8 @@ define_error_codes! { LoggingDisabled = 9, /// ECDSA public key recovery failed. Most probably wrong recovery id or signature. EcdsaRecoveryFailed = 11, + /// sr25519 signature verification failed. This may be because of an invalid public key, invalid message or invalid signature. + Sr25519VerifyFailed = 12, } /// The raw return code returned by the host side. diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml index 240a0d5b86..2e71bbcd4f 100644 --- a/crates/env/Cargo.toml +++ b/crates/env/Cargo.toml @@ -43,6 +43,9 @@ blake2 = { workspace = true, optional = true } # ECDSA for the off-chain environment. secp256k1 = { workspace = true, features = ["recovery", "global-context"], optional = true } +# schnorrkel for the off-chain environment. +schnorrkel = { version = "0.10.2", optional = true } + # Only used in the off-chain environment. # # Sadly couldn't be marked as dev-dependency. @@ -67,6 +70,7 @@ std = [ "scale-encode", "scale-info/std", "secp256k1", + "schnorrkel", "num-traits/std", # Enables hashing crates for off-chain environment. "sha2", diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 7ba37fcaf3..3050c21df7 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -550,6 +550,43 @@ pub fn ecdsa_to_eth_address(pubkey: &[u8; 33], output: &mut [u8; 20]) -> Result< }) } +/// Verifies a sr25519 signature. +/// +/// # Example +/// +/// ``` +/// let signature: [u8; 64] = [ +/// 184, 49, 74, 238, 78, 165, 102, 252, 22, 92, 156, 176, 124, 118, 168, 116, 247, +/// 99, 0, 94, 2, 45, 9, 170, 73, 222, 182, 74, 60, 32, 75, 64, 98, 174, 69, 55, 83, +/// 85, 180, 98, 208, 75, 231, 57, 205, 62, 4, 105, 26, 136, 172, 17, 123, 99, 90, +/// 255, 228, 54, 115, 63, 30, 207, 205, 131, +/// ]; +/// let message: &[u8; 11] = b"hello world"; +/// let pub_key: [u8; 32] = [ +/// 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, 44, +/// 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, +/// ]; +/// +/// let result = ink::env::sr25519_verify(&signature, message.as_slice(), &pub_key); +/// assert!(result.is_ok()) +/// ``` +/// +/// # Errors +/// +/// - If sr25519 signature cannot be verified. +/// +/// **WARNING**: this function is from the [unstable interface](https://github.com/paritytech/substrate/tree/master/frame/contracts#unstable-interfaces), +/// which is unsafe and normally is not available on production chains. +pub fn sr25519_verify( + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], +) -> Result<()> { + ::on_instance(|instance| { + instance.sr25519_verify(signature, message, pub_key) + }) +} + /// Checks whether the specified account is a contract. /// /// # Errors diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index edb1c24ff5..99455b5786 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -290,6 +290,21 @@ pub trait EnvBackend { output: &mut [u8; 20], ) -> Result<()>; + /// Verifies a sr25519 signature. + /// + /// # Errors + /// + /// - If the signature verification failed. + /// + /// **WARNING**: this function is from the [unstable interface](https://github.com/paritytech/substrate/tree/master/frame/contracts#unstable-interfaces), + /// which is unsafe and normally is not available on production chains. + fn sr25519_verify( + &mut self, + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], + ) -> Result<()>; + /// Low-level interface to call a chain extension method. /// /// Returns the output of the chain extension of the specified type. diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 88a7ba8e9d..18c92ea17c 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -47,6 +47,10 @@ use ink_engine::{ ext::Engine, }; use ink_storage_traits::Storable; +use schnorrkel::{ + PublicKey, + Signature, +}; /// The capacity of the static buffer. /// This is the same size as the ink! on-chain environment. We chose to use the same size @@ -115,6 +119,7 @@ impl From for crate::Error { ext::Error::NotCallable => Self::NotCallable, ext::Error::LoggingDisabled => Self::LoggingDisabled, ext::Error::EcdsaRecoveryFailed => Self::EcdsaRecoveryFailed, + ext::Error::Sr25519VerifyFailed => Self::Sr25519VerifyFailed, } } } @@ -329,6 +334,28 @@ impl EnvBackend for EnvInstance { Ok(()) } + fn sr25519_verify( + &mut self, + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], + ) -> Result<()> { + // the context associated with the signing (specific to the sr25519 algorithm) + // defaults to "substrate" in substrate, but could be different elsewhere + // https://github.com/paritytech/substrate/blob/c32f5ed2ae6746d6f791f08cecbfc22fa188f5f9/primitives/core/src/sr25519.rs#L60 + let context = b"substrate"; + // attempt to parse a signature from bytes + let signature: Signature = + Signature::from_bytes(signature).map_err(|_| Error::Sr25519VerifyFailed)?; + // attempt to parse a public key from bytes + let public_key: PublicKey = + PublicKey::from_bytes(pub_key).map_err(|_| Error::Sr25519VerifyFailed)?; + // verify the signature + public_key + .verify_simple(context, message, &signature) + .map_err(|_| Error::Sr25519VerifyFailed) + } + fn call_chain_extension( &mut self, func_id: u32, diff --git a/crates/env/src/engine/on_chain/ext/riscv32.rs b/crates/env/src/engine/on_chain/ext/riscv32.rs index 4774aeaf61..90809da9c1 100644 --- a/crates/env/src/engine/on_chain/ext/riscv32.rs +++ b/crates/env/src/engine/on_chain/ext/riscv32.rs @@ -371,6 +371,23 @@ pub fn ecdsa_to_eth_address(pubkey: &[u8; 33], output: &mut [u8; 20]) -> Result ret_code.into() } +/// **WARNING**: this function is from the [unstable interface](https://github.com/paritytech/substrate/tree/master/frame/contracts#unstable-interfaces), +/// which is unsafe and normally is not available on production chains. +pub fn sr25519_verify( + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], +) -> Result { + let ret_code = ( + Ptr32::from_slice(signature), + Ptr32::from_slice(pub_key), + message.len() as u32, + Ptr32::from_slice(message), + ) + .using_encoded(|in_data| sys::call(FUNC_ID, Ptr32::from_slice(in_data))); + ret_code.into() +} + pub fn is_contract(account_id: &[u8]) -> bool { let ret_val = sys::call(FUNC_ID, Ptr32::from_slice(account_id)); ret_val.into_bool() diff --git a/crates/env/src/engine/on_chain/ext/wasm32.rs b/crates/env/src/engine/on_chain/ext/wasm32.rs index 0239653c9e..49f4dc37bc 100644 --- a/crates/env/src/engine/on_chain/ext/wasm32.rs +++ b/crates/env/src/engine/on_chain/ext/wasm32.rs @@ -146,6 +146,15 @@ mod sys { output_ptr: Ptr32Mut<[u8]>, ) -> ReturnCode; + /// **WARNING**: this function is from the [unstable interface](https://github.com/paritytech/substrate/tree/master/frame/contracts#unstable-interfaces), + /// which is unsafe and normally is not available on production chains. + pub fn sr25519_verify( + signature_ptr: Ptr32<[u8]>, + public_key_ptr: Ptr32<[u8]>, + message_len: u32, + message_ptr: Ptr32<[u8]>, + ) -> ReturnCode; + pub fn take_storage( key_ptr: Ptr32<[u8]>, key_len: u32, @@ -595,6 +604,22 @@ pub fn ecdsa_to_eth_address(pubkey: &[u8; 33], output: &mut [u8; 20]) -> Result ret_code.into() } +pub fn sr25519_verify( + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], +) -> Result { + let ret_code = unsafe { + sys::sr25519_verify( + Ptr32::from_slice(signature), + Ptr32::from_slice(pub_key), + message.len() as u32, + Ptr32::from_slice(message), + ) + }; + ret_code.into() +} + pub fn is_contract(account_id: &[u8]) -> bool { let ret_val = unsafe { sys::is_contract(Ptr32::from_slice(account_id)) }; ret_val.into_bool() diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index 38fa62542e..9de712d4dd 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -330,6 +330,15 @@ impl EnvBackend for EnvInstance { ext::ecdsa_to_eth_address(pubkey, output).map_err(Into::into) } + fn sr25519_verify( + &mut self, + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], + ) -> Result<()> { + ext::sr25519_verify(signature, message, pub_key).map_err(Into::into) + } + fn call_chain_extension( &mut self, func_id: u32, diff --git a/crates/env/src/error.rs b/crates/env/src/error.rs index a727bad391..55a71926f4 100644 --- a/crates/env/src/error.rs +++ b/crates/env/src/error.rs @@ -51,6 +51,8 @@ pub enum Error { CallRuntimeFailed, /// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature. EcdsaRecoveryFailed, + /// sr25519 signature verification failed. + Sr25519VerifyFailed, } /// A result of environmental operations. diff --git a/crates/ink/src/env_access.rs b/crates/ink/src/env_access.rs index 76bae839e1..71aac43ffb 100644 --- a/crates/ink/src/env_access.rs +++ b/crates/ink/src/env_access.rs @@ -889,6 +889,66 @@ where .map_err(|_| Error::EcdsaRecoveryFailed) } + /// Verifies a SR25519 signature against a message and a public key. + /// + /// # Example + /// + /// ``` + /// # #[ink::contract] + /// # pub mod my_contract { + /// # #[ink(storage)] + /// # pub struct MyContract { } + /// # + /// # impl MyContract { + /// # #[ink(constructor)] + /// # pub fn new() -> Self { + /// # Self {} + /// # } + /// # + /// #[ink(message)] + /// pub fn sr25519_verify(&self) { + /// let mut signature: [u8; 64] = [ + /// 10, 125, 162, 182, 49, 112, 76, 220, 254, 147, 199, 64, 228, 18, 23, 185, + /// 172, 102, 122, 12, 135, 85, 216, 218, 26, 130, 50, 219, 82, 127, 72, 124, + /// 135, 231, 128, 210, 237, 193, 137, 106, 235, 107, 27, 239, 11, 199, 195, 141, + /// 157, 242, 19, 91, 99, 62, 171, 139, 251, 23, 119, 232, 47, 173, 58, 143, + /// ]; + /// let mut message: [u8; 49] = [ + /// 60, 66, 121, 116, 101, 115, 62, 48, 120, 52, 54, 102, 98, 55, 52, 48, 56, + /// 100, 52, 102, 50, 56, 53, 50, 50, 56, 102, 52, 97, 102, 53, 49, 54, 101, 97, + /// 50, 53, 56, 53, 49, 98, 60, 47, 66, 121, 116, 101, 115, 62, + /// ]; + /// let mut public_key: [u8; 32] = [ + /// 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, 130, + /// 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, 125, + /// ]; + /// let result = ink::env::sr25519_verify(&signature, &message, &public_key); + /// assert_eq!(result, Ok(())); + /// } + /// # + /// # } + /// # } + /// ``` + /// + /// # Note + /// + /// The context for sr25519 signing is hard-coded to "substrate" to match sr25519 + /// signing in substrate. + /// + /// For more details visit: [`ink_env::sr25519_verify`] + /// + /// **WARNING**: this function is from the [unstable interface](https://github.com/paritytech/substrate/tree/master/frame/contracts#unstable-interfaces), + /// which is unsafe and normally is not available on production chains. + pub fn sr25519_verify( + self, + signature: &[u8; 64], + message: &[u8], + pub_key: &[u8; 32], + ) -> Result<()> { + ink_env::sr25519_verify(signature, message, pub_key) + .map_err(|_| Error::Sr25519VerifyFailed) + } + /// Checks whether a specified account belongs to a contract. /// /// # Example diff --git a/integration-tests/sr25519-verification/.gitignore b/integration-tests/sr25519-verification/.gitignore new file mode 100644 index 0000000000..bf910de10a --- /dev/null +++ b/integration-tests/sr25519-verification/.gitignore @@ -0,0 +1,9 @@ +# Ignore build artifacts from the local tests sub-crate. +/target/ + +# Ignore backup files creates by cargo fmt. +**/*.rs.bk + +# Remove Cargo.lock when creating an executable, leave it for libraries +# More information here http://doc.crates.io/guide.html#cargotoml-vs-cargolock +Cargo.lock \ No newline at end of file diff --git a/integration-tests/sr25519-verification/Cargo.toml b/integration-tests/sr25519-verification/Cargo.toml new file mode 100644 index 0000000000..9281779661 --- /dev/null +++ b/integration-tests/sr25519-verification/Cargo.toml @@ -0,0 +1,28 @@ +[package] +name = "sr25519_verification" +version = "4.1.0" +authors = ["Parity Technologies ", "George Oastler "] +edition = "2021" +publish = false + +[dependencies] +ink = { path = "../../crates/ink", default-features = false } + +scale = { package = "parity-scale-codec", version = "3", default-features = false, features = ["derive"] } +scale-info = { version = "2.5", default-features = false, features = ["derive"], optional = true } + +[dev-dependencies] +ink_e2e = { path = "../../crates/e2e" } + +[lib] +path = "lib.rs" + +[features] +default = ["std"] +std = [ + "ink/std", + "scale/std", + "scale-info/std", +] +ink-as-dependency = [] +e2e-tests = [] diff --git a/integration-tests/sr25519-verification/lib.rs b/integration-tests/sr25519-verification/lib.rs new file mode 100644 index 0000000000..a8322b0149 --- /dev/null +++ b/integration-tests/sr25519-verification/lib.rs @@ -0,0 +1,131 @@ +#![cfg_attr(not(feature = "std"), no_std, no_main)] + +#[ink::contract] +pub mod sr25519_verification { + #[ink(storage)] + #[derive(Default)] + pub struct Sr25519Verification {} + + impl Sr25519Verification { + /// Creates a new sr25519 verification smart contract initialized with the given + /// value. + #[ink(constructor)] + pub fn new() -> Self { + Self {} + } + + /// Dummy method to satisfy constaint of more than 0 messages per contract. + #[ink(message)] + pub fn foobar(&self) { + ink::env::debug_println!("foobar - this method does nothing"); + } + } + + #[cfg(test)] + mod tests { + + #[ink::test] + fn test_sr25519_verify_valid() { + // "hello" as bytes + let message: [u8; 49] = [ + 60, 66, 121, 116, 101, 115, 62, 48, 120, 52, 54, 102, 98, 55, 52, 48, 56, + 100, 52, 102, 50, 56, 53, 50, 50, 56, 102, 52, 97, 102, 53, 49, 54, 101, + 97, 50, 53, 56, 53, 49, 98, 60, 47, 66, 121, 116, 101, 115, 62, + ]; + // alice + let public_key: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, + 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, + 125, + ]; + // alice's signature of the message + let signature: [u8; 64] = [ + 10, 125, 162, 182, 49, 112, 76, 220, 254, 147, 199, 64, 228, 18, 23, 185, + 172, 102, 122, 12, 135, 85, 216, 218, 26, 130, 50, 219, 82, 127, 72, 124, + 135, 231, 128, 210, 237, 193, 137, 106, 235, 107, 27, 239, 11, 199, 195, + 141, 157, 242, 19, 91, 99, 62, 171, 139, 251, 23, 119, 232, 47, 173, 58, + 143, + ]; + let result = ink::env::sr25519_verify(&signature, &message, &public_key); + assert_eq!(result, Ok(())); + } + + #[ink::test] + fn test_sr25519_verify_invalid_public_key() { + // "hello" as bytes + let message: [u8; 49] = [ + 60, 66, 121, 116, 101, 115, 62, 48, 120, 52, 54, 102, 98, 55, 52, 48, 56, + 100, 52, 102, 50, 56, 53, 50, 50, 56, 102, 52, 97, 102, 53, 49, 54, 101, + 97, 50, 53, 56, 53, 49, 98, 60, 47, 66, 121, 116, 101, 115, 62, + ]; + // alice - off by 1 at start + let public_key: [u8; 32] = [ + 213, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, + 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, + 125, + ]; + // alice's signature of the message + let signature: [u8; 64] = [ + 10, 125, 162, 182, 49, 112, 76, 220, 254, 147, 199, 64, 228, 18, 23, 185, + 172, 102, 122, 12, 135, 85, 216, 218, 26, 130, 50, 219, 82, 127, 72, 124, + 135, 231, 128, 210, 237, 193, 137, 106, 235, 107, 27, 239, 11, 199, 195, + 141, 157, 242, 19, 91, 99, 62, 171, 139, 251, 23, 119, 232, 47, 173, 58, + 143, + ]; + let result = ink::env::sr25519_verify(&signature, &message, &public_key); + assert_eq!(result, Err(ink::env::Error::Sr25519VerifyFailed)); + } + + #[ink::test] + fn test_sr25519_verify_invalid_message() { + // "hello" as bytes - off by 1 at start + let message: [u8; 49] = [ + 61, 66, 121, 116, 101, 115, 62, 48, 120, 52, 54, 102, 98, 55, 52, 48, 56, + 100, 52, 102, 50, 56, 53, 50, 50, 56, 102, 52, 97, 102, 53, 49, 54, 101, + 97, 50, 53, 56, 53, 49, 98, 60, 47, 66, 121, 116, 101, 115, 62, + ]; + // alice + let public_key: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, + 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, + 125, + ]; + // alice's signature of the message + let signature: [u8; 64] = [ + 10, 125, 162, 182, 49, 112, 76, 220, 254, 147, 199, 64, 228, 18, 23, 185, + 172, 102, 122, 12, 135, 85, 216, 218, 26, 130, 50, 219, 82, 127, 72, 124, + 135, 231, 128, 210, 237, 193, 137, 106, 235, 107, 27, 239, 11, 199, 195, + 141, 157, 242, 19, 91, 99, 62, 171, 139, 251, 23, 119, 232, 47, 173, 58, + 143, + ]; + let result = ink::env::sr25519_verify(&signature, &message, &public_key); + assert_eq!(result, Err(ink::env::Error::Sr25519VerifyFailed)); + } + + #[ink::test] + fn test_sr25519_verify_invalid_signature() { + // "hello" as bytes + let message: [u8; 49] = [ + 60, 66, 121, 116, 101, 115, 62, 48, 120, 52, 54, 102, 98, 55, 52, 48, 56, + 100, 52, 102, 50, 56, 53, 50, 50, 56, 102, 52, 97, 102, 53, 49, 54, 101, + 97, 50, 53, 56, 53, 49, 98, 60, 47, 66, 121, 116, 101, 115, 62, + ]; + // alice + let public_key: [u8; 32] = [ + 212, 53, 147, 199, 21, 253, 211, 28, 97, 20, 26, 189, 4, 169, 159, 214, + 130, 44, 133, 88, 133, 76, 205, 227, 154, 86, 132, 231, 165, 109, 162, + 125, + ]; + // alice's signature of the message - off by 1 at start + let signature: [u8; 64] = [ + 11, 125, 162, 182, 49, 112, 76, 220, 254, 147, 199, 64, 228, 18, 23, 185, + 172, 102, 122, 12, 135, 85, 216, 218, 26, 130, 50, 219, 82, 127, 72, 124, + 135, 231, 128, 210, 237, 193, 137, 106, 235, 107, 27, 239, 11, 199, 195, + 141, 157, 242, 19, 91, 99, 62, 171, 139, 251, 23, 119, 232, 47, 173, 58, + 143, + ]; + let result = ink::env::sr25519_verify(&signature, &message, &public_key); + assert_eq!(result, Err(ink::env::Error::Sr25519VerifyFailed)); + } + } +}