contracts: add sr25519_verify (#13724)

* wip

* fix

* wip

* fix lint

* rm fixture fix

* missing comment

* fix lint

* add comment to the wsm file

* fix comment

* Apply suggestions from code review

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* wip

* wip weights

* wip weights

* PR comment: test with return code

* wip

* PR review add mock test

* remove

* lint

* Update frame/contracts/fixtures/sr25519_verify.wat

* fix comments

* Update frame/contracts/src/benchmarking/mod.rs

* Update frame/contracts/src/wasm/runtime.rs

* Update frame/contracts/fixtures/sr25519_verify.wat

* Update frame/contracts/src/benchmarking/mod.rs

* fix lint

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts

* Update frame/contracts/src/wasm/runtime.rs

Co-authored-by: Alexander Theißen <alex.theissen@me.com>

* PR: review use unstable + remove arbitrary index 4

* Add benchmark for calculating overhead of calling sr25519_verify

* fix message length encoding

* fix weights

* ".git/.scripts/commands/bench/bench.sh" pallet dev pallet_contracts

* Apply suggestions from code review

* Update frame/contracts/src/wasm/runtime.rs

* Update frame/contracts/src/wasm/runtime.rs

* Update frame/contracts/src/benchmarking/mod.rs

* Update frame/contracts/src/benchmarking/mod.rs

* Update frame/contracts/src/schedule.rs

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* Update frame/contracts/src/schedule.rs

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* Update frame/contracts/src/wasm/runtime.rs

* Update frame/contracts/src/wasm/runtime.rs

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>

* PR review

---------

Co-authored-by: Sasha Gryaznov <hi@agryaznov.com>
Co-authored-by: command-bot <>
Co-authored-by: Alexander Theißen <alex.theissen@me.com>
This commit is contained in:
PG Herveou
2023-04-12 16:49:10 +02:00
committed by GitHub
parent 376a288fb6
commit 03c99fe003
8 changed files with 1332 additions and 894 deletions
@@ -0,0 +1,55 @@
;; This contract:
;; 1) Reads signature, message and public key from the input
;; 2) Calls and return the result of sr25519_verify
(module
;; import the host functions from the seal0 module
(import "seal0" "sr25519_verify" (func $sr25519_verify (param i32 i32 i32 i32) (result i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
;; give the program 1 page of memory
(import "env" "memory" (memory 1 1))
;; [0, 4) length of signature + message + public key - 64 + 11 + 32 = 107 bytes
;; write the length of the input (6b = 107) bytes at offset 0
(data (i32.const 0) "\6b")
(func (export "deploy"))
(func (export "call")
;; define local variables
(local $signature_ptr i32)
(local $pub_key_ptr i32)
(local $message_len i32)
(local $message_ptr i32)
;; set the pointers to the memory locations
;; Memory layout during `call`
;; [10, 74) signature
;; [74, 106) public key
;; [106, 117) message (11 bytes)
(local.set $signature_ptr (i32.const 10))
(local.set $pub_key_ptr (i32.const 74))
(local.set $message_ptr (i32.const 106))
;; store the input into the memory, starting at the signature and
;; up to 107 bytes stored at offset 0
(call $seal_input (local.get $signature_ptr) (i32.const 0))
;; call sr25519_verify and store the return code
(i32.store
(i32.const 0)
(call $sr25519_verify
(local.get $signature_ptr)
(local.get $pub_key_ptr)
(i32.const 11)
(local.get $message_ptr)
)
)
;; exit with success and take transfer return code to the output buffer
(call $seal_return (i32.const 0) (i32.const 0) (i32.const 4))
)
)
@@ -2008,9 +2008,114 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone()); let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![]) }: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
// `n`: Message input length to verify in bytes.
#[pov_mode = Measured]
seal_sr25519_verify_per_byte {
let n in 0 .. T::MaxCodeLen::get() - 255; // need some buffer so the code size does not
// exceed the max code size.
let message = (0..n).zip((32u8..127u8).cycle()).map(|(_, c)| c).collect::<Vec<_>>();
let message_len = message.len() as i32;
let key_type = sp_core::crypto::KeyTypeId(*b"code");
let pub_key = sp_io::crypto::sr25519_generate(key_type, None);
let sig = sp_io::crypto::sr25519_sign(key_type, &pub_key, &message).expect("Generates signature");
let sig = AsRef::<[u8; 64]>::as_ref(&sig).to_vec();
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "seal0",
name: "sr25519_verify",
params: vec![ValueType::I32, ValueType::I32, ValueType::I32, ValueType::I32],
return_type: Some(ValueType::I32),
}],
data_segments: vec![
DataSegment {
offset: 0,
value: sig,
},
DataSegment {
offset: 64,
value: pub_key.to_vec(),
},
DataSegment {
offset: 96,
value: message,
},
],
call_body: Some(body::plain(vec![
Instruction::I32Const(0), // signature_ptr
Instruction::I32Const(64), // pub_key_ptr
Instruction::I32Const(message_len), // message_len
Instruction::I32Const(96), // message_ptr
Instruction::Call(0),
Instruction::Drop,
Instruction::End,
])),
.. Default::default()
});
let instance = Contract::<T>::new(code, vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
// Only calling the function itself with valid arguments. // Only calling the function itself with valid arguments.
// It generates different private keys and signatures for the message "Hello world". // It generates different private keys and signatures for the message "Hello world".
// This is a slow call: We redeuce the number of runs. // This is a slow call: We reduce the number of runs.
#[pov_mode = Measured]
seal_sr25519_verify {
let r in 0 .. API_BENCHMARK_RUNS / 10;
let message = b"Hello world".to_vec();
let message_len = message.len() as i32;
let key_type = sp_core::crypto::KeyTypeId(*b"code");
let sig_params = (0..r)
.map(|i| {
let pub_key = sp_io::crypto::sr25519_generate(key_type, None);
let sig = sp_io::crypto::sr25519_sign(key_type, &pub_key, &message).expect("Generates signature");
let data: [u8; 96] = [AsRef::<[u8]>::as_ref(&sig), AsRef::<[u8]>::as_ref(&pub_key)].concat().try_into().unwrap();
data
})
.flatten()
.collect::<Vec<_>>();
let sig_params_len = sig_params.len() as i32;
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "seal0",
name: "sr25519_verify",
params: vec![ValueType::I32, ValueType::I32, ValueType::I32, ValueType::I32],
return_type: Some(ValueType::I32),
}],
data_segments: vec![
DataSegment {
offset: 0,
value: sig_params
},
DataSegment {
offset: sig_params_len as u32,
value: message,
},
],
call_body: Some(body::repeated_dyn(r, vec![
Counter(0, 96), // signature_ptr
Counter(64, 96), // pub_key_ptr
Regular(Instruction::I32Const(message_len)), // message_len
Regular(Instruction::I32Const(sig_params_len)), // message_ptr
Regular(Instruction::Call(0)),
Regular(Instruction::Drop),
])),
.. Default::default()
});
let instance = Contract::<T>::new(code, vec![])?;
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::MAX, None, vec![])
// Only calling the function itself with valid arguments.
// It generates different private keys and signatures for the message "Hello world".
// This is a slow call: We reduce the number of runs.
#[pov_mode = Measured] #[pov_mode = Measured]
seal_ecdsa_recover { seal_ecdsa_recover {
let r in 0 .. API_BENCHMARK_RUNS / 10; let r in 0 .. API_BENCHMARK_RUNS / 10;
+15 -1
View File
@@ -35,7 +35,10 @@ use frame_support::{
use frame_system::RawOrigin; use frame_system::RawOrigin;
use pallet_contracts_primitives::ExecReturnValue; use pallet_contracts_primitives::ExecReturnValue;
use smallvec::{Array, SmallVec}; use smallvec::{Array, SmallVec};
use sp_core::ecdsa::Public as ECDSAPublic; use sp_core::{
ecdsa::Public as ECDSAPublic,
sr25519::{Public as SR25519Public, Signature as SR25519Signature},
};
use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::blake2_256}; use sp_io::{crypto::secp256k1_ecdsa_recover_compressed, hashing::blake2_256};
use sp_runtime::traits::{Convert, Hash}; use sp_runtime::traits::{Convert, Hash};
use sp_std::{marker::PhantomData, mem, prelude::*, vec::Vec}; use sp_std::{marker::PhantomData, mem, prelude::*, vec::Vec};
@@ -272,6 +275,9 @@ pub trait Ext: sealing::Sealed {
/// Recovers ECDSA compressed public key based on signature and message hash. /// Recovers ECDSA compressed public key based on signature and message hash.
fn ecdsa_recover(&self, signature: &[u8; 65], message_hash: &[u8; 32]) -> Result<[u8; 33], ()>; fn ecdsa_recover(&self, signature: &[u8; 65], message_hash: &[u8; 32]) -> Result<[u8; 33], ()>;
/// Verify a sr25519 signature.
fn sr25519_verify(&self, signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> bool;
/// Returns Ethereum address from the ECDSA compressed public key. /// Returns Ethereum address from the ECDSA compressed public key.
fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()>; fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()>;
@@ -1347,6 +1353,14 @@ where
secp256k1_ecdsa_recover_compressed(signature, message_hash).map_err(|_| ()) secp256k1_ecdsa_recover_compressed(signature, message_hash).map_err(|_| ())
} }
fn sr25519_verify(&self, signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> bool {
sp_io::crypto::sr25519_verify(
&SR25519Signature(*signature),
message,
&SR25519Public(*pub_key),
)
}
fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()> { fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()> {
ECDSAPublic(*pk).to_eth_address() ECDSAPublic(*pk).to_eth_address()
} }
+9 -1
View File
@@ -139,7 +139,7 @@ impl Limits {
/// Describes the weight for all categories of supported wasm instructions. /// Describes the weight for all categories of supported wasm instructions.
/// ///
/// There there is one field for each wasm instruction that describes the weight to /// There there is one field for each wasm instruction that describes the weight to
/// execute one instruction of that name. There are a few execptions: /// execute one instruction of that name. There are a few exceptions:
/// ///
/// 1. If there is a i64 and a i32 variant of an instruction we use the weight /// 1. If there is a i64 and a i32 variant of an instruction we use the weight
/// of the former for both. /// of the former for both.
@@ -409,6 +409,12 @@ pub struct HostFnWeights<T: Config> {
/// Weight of calling `seal_ecdsa_to_eth_address`. /// Weight of calling `seal_ecdsa_to_eth_address`.
pub ecdsa_to_eth_address: Weight, pub ecdsa_to_eth_address: Weight,
/// Weight of calling `sr25519_verify`.
pub sr25519_verify: Weight,
/// Weight per byte of calling `sr25519_verify`.
pub sr25519_verify_per_byte: Weight,
/// Weight of calling `reentrance_count`. /// Weight of calling `reentrance_count`.
pub reentrance_count: Weight, pub reentrance_count: Weight,
@@ -616,6 +622,8 @@ impl<T: Config> Default for HostFnWeights<T> {
hash_blake2_128: cost!(seal_hash_blake2_128), hash_blake2_128: cost!(seal_hash_blake2_128),
hash_blake2_128_per_byte: cost!(seal_hash_blake2_128_per_byte), hash_blake2_128_per_byte: cost!(seal_hash_blake2_128_per_byte),
ecdsa_recover: cost!(seal_ecdsa_recover), ecdsa_recover: cost!(seal_ecdsa_recover),
sr25519_verify: cost!(seal_sr25519_verify),
sr25519_verify_per_byte: cost!(seal_sr25519_verify_per_byte),
ecdsa_to_eth_address: cost!(seal_ecdsa_to_eth_address), ecdsa_to_eth_address: cost!(seal_ecdsa_to_eth_address),
reentrance_count: cost!(seal_reentrance_count), reentrance_count: cost!(seal_reentrance_count),
account_reentrance_count: cost!(seal_account_reentrance_count), account_reentrance_count: cost!(seal_account_reentrance_count),
+66
View File
@@ -2900,6 +2900,72 @@ fn ecdsa_recover() {
}) })
} }
#[test]
fn sr25519_verify() {
let (wasm, _code_hash) = compile_module::<Test>("sr25519_verify").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
// Instantiate the sr25519_verify contract.
let addr = Contracts::bare_instantiate(
ALICE,
100_000,
GAS_LIMIT,
None,
Code::Upload(wasm),
vec![],
vec![],
false,
)
.result
.unwrap()
.account_id;
let call_with = |message: &[u8; 11]| {
// Alice's signature for "hello world"
#[rustfmt::skip]
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,
];
// Alice's public key
#[rustfmt::skip]
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,
];
let mut params = vec![];
params.extend_from_slice(&signature);
params.extend_from_slice(&public_key);
params.extend_from_slice(message);
<Pallet<Test>>::bare_call(
ALICE,
addr.clone(),
0,
GAS_LIMIT,
None,
params,
false,
Determinism::Enforced,
)
.result
.unwrap()
};
// verification should succeed for "hello world"
assert_return_code!(call_with(&b"hello world"), RuntimeReturnCode::Success);
// verification should fail for other messages
assert_return_code!(call_with(&b"hello worlD"), RuntimeReturnCode::Sr25519VerifyFailed);
})
}
#[test] #[test]
fn upload_code_works() { fn upload_code_works() {
let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap(); let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap();
+49
View File
@@ -434,6 +434,7 @@ mod tests {
gas_meter: GasMeter<Test>, gas_meter: GasMeter<Test>,
debug_buffer: Vec<u8>, debug_buffer: Vec<u8>,
ecdsa_recover: RefCell<Vec<([u8; 65], [u8; 32])>>, ecdsa_recover: RefCell<Vec<([u8; 65], [u8; 32])>>,
sr25519_verify: RefCell<Vec<([u8; 64], Vec<u8>, [u8; 32])>>,
code_hashes: Vec<CodeHash<Test>>, code_hashes: Vec<CodeHash<Test>>,
} }
@@ -458,6 +459,7 @@ mod tests {
gas_meter: GasMeter::new(Weight::from_parts(10_000_000_000, 10 * 1024 * 1024)), gas_meter: GasMeter::new(Weight::from_parts(10_000_000_000, 10 * 1024 * 1024)),
debug_buffer: Default::default(), debug_buffer: Default::default(),
ecdsa_recover: Default::default(), ecdsa_recover: Default::default(),
sr25519_verify: Default::default(),
} }
} }
} }
@@ -612,6 +614,10 @@ mod tests {
self.ecdsa_recover.borrow_mut().push((*signature, *message_hash)); self.ecdsa_recover.borrow_mut().push((*signature, *message_hash));
Ok([3; 33]) Ok([3; 33])
} }
fn sr25519_verify(&self, signature: &[u8; 64], message: &[u8], pub_key: &[u8; 32]) -> bool {
self.sr25519_verify.borrow_mut().push((*signature, message.to_vec(), *pub_key));
true
}
fn contract_info(&mut self) -> &mut crate::ContractInfo<Self::T> { fn contract_info(&mut self) -> &mut crate::ContractInfo<Self::T> {
unimplemented!() unimplemented!()
} }
@@ -1319,6 +1325,49 @@ mod tests {
); );
} }
#[test]
fn contract_sr25519() {
const CODE_SR25519: &str = r#"
(module
(import "seal0" "sr25519_verify" (func $sr25519_verify (param i32 i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(drop
(call $sr25519_verify
(i32.const 0) ;; Pointer to signature.
(i32.const 64) ;; Pointer to public key.
(i32.const 16) ;; message length.
(i32.const 96) ;; Pointer to message.
)
)
)
(func (export "deploy"))
;; Signature (64 bytes)
(data (i32.const 0)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
;; public key (32 bytes)
(data (i32.const 64)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
;; message. (16 bytes)
(data (i32.const 96)
"\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01\01"
)
)
"#;
let mut mock_ext = MockExt::default();
assert_ok!(execute(&CODE_SR25519, vec![], &mut mock_ext));
assert_eq!(mock_ext.sr25519_verify.into_inner(), [([1; 64], [1; 16].to_vec(), [1; 32])]);
}
const CODE_GET_STORAGE: &str = r#" const CODE_GET_STORAGE: &str = r#"
(module (module
(import "seal0" "seal_get_storage" (func $seal_get_storage (param i32 i32 i32) (result i32))) (import "seal0" "seal_get_storage" (func $seal_get_storage (param i32 i32 i32) (result i32)))
@@ -109,6 +109,8 @@ pub enum ReturnCode {
/// ECDSA compressed pubkey conversion into Ethereum address failed (most probably /// ECDSA compressed pubkey conversion into Ethereum address failed (most probably
/// wrong pubkey provided). /// wrong pubkey provided).
EcdsaRecoverFailed = 11, EcdsaRecoverFailed = 11,
/// sr25519 signature verification failed.
Sr25519VerifyFailed = 12,
} }
impl From<ExecReturnValue> for ReturnCode { impl From<ExecReturnValue> for ReturnCode {
@@ -251,6 +253,8 @@ pub enum RuntimeCosts {
HashBlake128(u32), HashBlake128(u32),
/// Weight of calling `seal_ecdsa_recover`. /// Weight of calling `seal_ecdsa_recover`.
EcdsaRecovery, EcdsaRecovery,
/// Weight of calling `seal_sr25519_verify` for the given input size.
Sr25519Verify(u32),
/// Weight charged by a chain extension through `seal_call_chain_extension`. /// Weight charged by a chain extension through `seal_call_chain_extension`.
ChainExtension(Weight), ChainExtension(Weight),
/// Weight charged for calling into the runtime. /// Weight charged for calling into the runtime.
@@ -336,6 +340,9 @@ impl RuntimeCosts {
.hash_blake2_128 .hash_blake2_128
.saturating_add(s.hash_blake2_128_per_byte.saturating_mul(len.into())), .saturating_add(s.hash_blake2_128_per_byte.saturating_mul(len.into())),
EcdsaRecovery => s.ecdsa_recover, EcdsaRecovery => s.ecdsa_recover,
Sr25519Verify(len) => s
.sr25519_verify
.saturating_add(s.sr25519_verify_per_byte.saturating_mul(len.into())),
ChainExtension(weight) => weight, ChainExtension(weight) => weight,
CallRuntime(weight) => weight, CallRuntime(weight) => weight,
SetCodeHash => s.set_code_hash, SetCodeHash => s.set_code_hash,
@@ -2466,6 +2473,46 @@ pub mod env {
} }
} }
/// Verify a sr25519 signature
///
/// # Parameters
///
/// - `signature_ptr`: the pointer into the linear memory where the signature is placed. Should
/// be a value of 64 bytes.
/// - `pub_key_ptr`: the pointer into the linear memory where the public key is placed. Should
/// be a value of 32 bytes.
/// - `message_len`: the length of the message payload.
/// - `message_ptr`: the pointer into the linear memory where the message is placed.
///
/// # Errors
///
/// - `ReturnCode::Sr25519VerifyFailed
#[unstable]
fn sr25519_verify(
ctx: _,
memory: _,
signature_ptr: u32,
pub_key_ptr: u32,
message_len: u32,
message_ptr: u32,
) -> Result<ReturnCode, TrapReason> {
ctx.charge_gas(RuntimeCosts::Sr25519Verify(message_len))?;
let mut signature: [u8; 64] = [0; 64];
ctx.read_sandbox_memory_into_buf(memory, signature_ptr, &mut signature)?;
let mut pub_key: [u8; 32] = [0; 32];
ctx.read_sandbox_memory_into_buf(memory, pub_key_ptr, &mut pub_key)?;
let message: Vec<u8> = ctx.read_sandbox_memory(memory, message_ptr, message_len)?;
if ctx.ext.sr25519_verify(&signature, &message, &pub_key) {
Ok(ReturnCode::Success)
} else {
Ok(ReturnCode::Sr25519VerifyFailed)
}
}
/// Replace the contract code at the specified address with new code. /// Replace the contract code at the specified address with new code.
/// ///
/// # Note /// # Note
+985 -891
View File
File diff suppressed because it is too large Load Diff