Skip to content

Commit

Permalink
Add sr25519_verify() (#1840)
Browse files Browse the repository at this point in the history
* sr25519 signature verification

* offchain signature verification

* fmt

* schnorrkel optional

* update dictionary

* fix

* update changelog

* add info about unstable function usage

* missing docs

* review suggestions

* added catch for invalid public key or signature in sr25519 verification

public key byte[] and/or signature byte[] may not be constructed into a PublicKey or Signature if it's in the wrong format. These errors originate from the schnorrkel library and need to be caught, otherwise it'll panic and propagate up to smart contracts in ink.

* switched to simple signing context in sr25519_verify fn

there is no need to make the context directly, we can delegate that to the schnorrkel library

* Added docs for sr25519_verify()

Docs + example + notes about context set to "substrate"

* Added docs to sr25519VerifyFailed error

Helpful for debugging why the error has occurred, as there is only ever one of 3 reasons: invalid public key, invalid message or invalid signature

* Added docs link to substrate sr25519 signing context

* cargo fmt

* Remove reference on message variable of sr25519_verify()

Not required according to clippy

* Clippy embed error in panic statement during metadata execution

Clippy's advice

* sr25519 verification tests

Tested invalid public key, invalid signature, invalid message and valid case

* Updated sr25519_verify PR number in CHANGELOG.md

* Fix comment to adhere to spell check in sr25519 verification tests

* add parity as author of sr25519 module

Co-authored-by: Green Baneling <[email protected]>

* ignore semicolon correction in item module

* add warning to docs for sr25519_verify() depending on unstable interfaces

* remove space typo from sr25519 docs

* remove #1741 from changelog

* added documentation to the dummy method in sr25519_verification example

* typo in sr25519 example contract dummy method

* spell check fix in method doc for sr25519

* spell check fix in method doc for sr25519

* move changelog entry for #1741

* changelog typo

---------

Co-authored-by: kziemianek <[email protected]>
Co-authored-by: Green Baneling <[email protected]>
  • Loading branch information
3 people committed Aug 9, 2023
1 parent 6452b89 commit 02a0bf9
Show file tree
Hide file tree
Showing 16 changed files with 369 additions and 1 deletion.
1 change: 1 addition & 0 deletions .config/cargo_spellcheck.dic
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ reentrancy
refcount
scalability
scalable
sr25519
stdin
stdout
subber
Expand Down
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,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)
Expand Down
2 changes: 1 addition & 1 deletion crates/e2e/macro/src/codegen.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<String> {
package
Expand Down
2 changes: 2 additions & 0 deletions crates/engine/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
4 changes: 4 additions & 0 deletions crates/env/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -67,6 +70,7 @@ std = [
"scale-encode",
"scale-info/std",
"secp256k1",
"schnorrkel",
"num-traits/std",
# Enables hashing crates for off-chain environment.
"sha2",
Expand Down
37 changes: 37 additions & 0 deletions crates/env/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<()> {
<EnvInstance as OnInstance>::on_instance(|instance| {
instance.sr25519_verify(signature, message, pub_key)
})
}

/// Checks whether the specified account is a contract.
///
/// # Errors
Expand Down
15 changes: 15 additions & 0 deletions crates/env/src/backend.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
27 changes: 27 additions & 0 deletions crates/env/src/engine/off_chain/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -115,6 +119,7 @@ impl From<ext::Error> for crate::Error {
ext::Error::NotCallable => Self::NotCallable,
ext::Error::LoggingDisabled => Self::LoggingDisabled,
ext::Error::EcdsaRecoveryFailed => Self::EcdsaRecoveryFailed,
ext::Error::Sr25519VerifyFailed => Self::Sr25519VerifyFailed,
}
}
}
Expand Down Expand Up @@ -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<I, T, E, ErrorCode, F, D>(
&mut self,
func_id: u32,
Expand Down
17 changes: 17 additions & 0 deletions crates/env/src/engine/on_chain/ext/riscv32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
25 changes: 25 additions & 0 deletions crates/env/src/engine/on_chain/ext/wasm32.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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()
Expand Down
9 changes: 9 additions & 0 deletions crates/env/src/engine/on_chain/impls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<I, T, E, ErrorCode, F, D>(
&mut self,
func_id: u32,
Expand Down
2 changes: 2 additions & 0 deletions crates/env/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
60 changes: 60 additions & 0 deletions crates/ink/src/env_access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
9 changes: 9 additions & 0 deletions integration-tests/sr25519-verification/.gitignore
Original file line number Diff line number Diff line change
@@ -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
28 changes: 28 additions & 0 deletions integration-tests/sr25519-verification/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
[package]
name = "sr25519_verification"
version = "4.1.0"
authors = ["Parity Technologies <[email protected]>", "George Oastler <[email protected]>"]
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 = []
Loading

0 comments on commit 02a0bf9

Please sign in to comment.