Skip to content

Commit

Permalink
contracts: add sr25519_verify (paritytech#13724)
Browse files Browse the repository at this point in the history
* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* Update frame/contracts/src/schedule.rs

Co-authored-by: Sasha Gryaznov <[email protected]>

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

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

Co-authored-by: Sasha Gryaznov <[email protected]>

* PR review

---------

Co-authored-by: Sasha Gryaznov <[email protected]>
Co-authored-by: command-bot <>
Co-authored-by: Alexander Theißen <[email protected]>
  • Loading branch information
3 people authored and nathanwhit committed Jul 19, 2023
1 parent ec04256 commit 0898f2a
Show file tree
Hide file tree
Showing 8 changed files with 1,332 additions and 894 deletions.
55 changes: 55 additions & 0 deletions frame/contracts/fixtures/sr25519_verify.wat
Original file line number Diff line number Diff line change
@@ -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))
)
)

107 changes: 106 additions & 1 deletion frame/contracts/src/benchmarking/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2008,9 +2008,114 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone());
}: 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.
// 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]
seal_ecdsa_recover {
let r in 0 .. API_BENCHMARK_RUNS / 10;
Expand Down
16 changes: 15 additions & 1 deletion frame/contracts/src/exec.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,10 @@ use frame_support::{
use frame_system::RawOrigin;
use pallet_contracts_primitives::ExecReturnValue;
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_runtime::traits::{Convert, Hash};
use sp_std::{marker::PhantomData, mem, prelude::*, vec::Vec};
Expand Down Expand Up @@ -272,6 +275,9 @@ pub trait Ext: sealing::Sealed {
/// 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], ()>;

/// 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.
fn ecdsa_to_eth_address(&self, pk: &[u8; 33]) -> Result<[u8; 20], ()>;

Expand Down Expand Up @@ -1347,6 +1353,14 @@ where
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], ()> {
ECDSAPublic(*pk).to_eth_address()
}
Expand Down
10 changes: 9 additions & 1 deletion frame/contracts/src/schedule.rs
Original file line number Diff line number Diff line change
Expand Up @@ -139,7 +139,7 @@ impl Limits {
/// Describes the weight for all categories of supported wasm instructions.
///
/// 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
/// of the former for both.
Expand Down Expand Up @@ -409,6 +409,12 @@ pub struct HostFnWeights<T: Config> {
/// Weight of calling `seal_ecdsa_to_eth_address`.
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`.
pub reentrance_count: Weight,

Expand Down Expand Up @@ -616,6 +622,8 @@ impl<T: Config> Default for HostFnWeights<T> {
hash_blake2_128: cost!(seal_hash_blake2_128),
hash_blake2_128_per_byte: cost!(seal_hash_blake2_128_per_byte),
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),
reentrance_count: cost!(seal_reentrance_count),
account_reentrance_count: cost!(seal_account_reentrance_count),
Expand Down
66 changes: 66 additions & 0 deletions frame/contracts/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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]
fn upload_code_works() {
let (wasm, code_hash) = compile_module::<Test>("dummy").unwrap();
Expand Down
49 changes: 49 additions & 0 deletions frame/contracts/src/wasm/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -434,6 +434,7 @@ mod tests {
gas_meter: GasMeter<Test>,
debug_buffer: Vec<u8>,
ecdsa_recover: RefCell<Vec<([u8; 65], [u8; 32])>>,
sr25519_verify: RefCell<Vec<([u8; 64], Vec<u8>, [u8; 32])>>,
code_hashes: Vec<CodeHash<Test>>,
}

Expand All @@ -458,6 +459,7 @@ mod tests {
gas_meter: GasMeter::new(Weight::from_parts(10_000_000_000, 10 * 1024 * 1024)),
debug_buffer: Default::default(),
ecdsa_recover: Default::default(),
sr25519_verify: Default::default(),
}
}
}
Expand Down Expand Up @@ -612,6 +614,10 @@ mod tests {
self.ecdsa_recover.borrow_mut().push((*signature, *message_hash));
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> {
unimplemented!()
}
Expand Down Expand Up @@ -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#"
(module
(import "seal0" "seal_get_storage" (func $seal_get_storage (param i32 i32 i32) (result i32)))
Expand Down
Loading

0 comments on commit 0898f2a

Please sign in to comment.