Implemented seal_ecdsa_recovery function in the contract pallet (#9686)

* Implemented `seal_ecdsa_recovery` function in the contract pallet.
Added benchmark and unit test.

* Run `cargo fmt`

* Skip fmt for slices

* Changes according comments in pull request.

* Fix build without `unstable-interface` feature

* Applied suggestion from the review

* Apply suggestions from code review

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

* Apply suggestions from code review

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

* Changed RecoveryFailed to EcdsaRecoverFailed

* Manually updated weights.rs

* Apply suggestions from code review

Co-authored-by: Michael Müller <mich@elmueller.net>

Co-authored-by: Alexander Theißen <alex.theissen@me.com>
Co-authored-by: Michael Müller <mich@elmueller.net>
This commit is contained in:
GreenBaneling | Supercolony
2021-09-10 14:30:56 +03:00
committed by GitHub
parent 110ba540ec
commit a36e881783
13 changed files with 985 additions and 654 deletions
+32 -13
View File
@@ -2518,6 +2518,17 @@ dependencies = [
"digest 0.9.0",
]
[[package]]
name = "hmac-drbg"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c6e570451493f10f6581b48cdd530413b63ea9e780f544bfd3bdcaa0d89d1a7b"
dependencies = [
"digest 0.8.1",
"generic-array 0.12.4",
"hmac 0.7.1",
]
[[package]]
name = "hmac-drbg"
version = "0.3.0"
@@ -3659,6 +3670,22 @@ dependencies = [
"libc",
]
[[package]]
name = "libsecp256k1"
version = "0.3.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fc1e2c808481a63dc6da2074752fdd4336a3c8fcc68b83db6f1fd5224ae7962"
dependencies = [
"arrayref",
"crunchy",
"digest 0.8.1",
"hmac-drbg 0.2.0",
"rand 0.7.3",
"sha2 0.8.2",
"subtle 2.4.0",
"typenum",
]
[[package]]
name = "libsecp256k1"
version = "0.5.0"
@@ -3668,7 +3695,7 @@ dependencies = [
"arrayref",
"base64 0.12.3",
"digest 0.9.0",
"hmac-drbg",
"hmac-drbg 0.3.0",
"libsecp256k1-core",
"libsecp256k1-gen-ecmult",
"libsecp256k1-gen-genmult",
@@ -3687,7 +3714,7 @@ dependencies = [
"arrayref",
"base64 0.12.3",
"digest 0.9.0",
"hmac-drbg",
"hmac-drbg 0.3.0",
"libsecp256k1-core",
"libsecp256k1-gen-ecmult",
"libsecp256k1-gen-genmult",
@@ -5022,6 +5049,7 @@ dependencies = [
"frame-support",
"frame-system",
"hex-literal",
"libsecp256k1 0.3.5",
"log 0.4.14",
"pallet-balances",
"pallet-contracts-primitives",
@@ -5032,8 +5060,8 @@ dependencies = [
"parity-scale-codec",
"pretty_assertions 0.7.2",
"pwasm-utils",
"rand 0.8.4",
"rand_pcg 0.3.0",
"rand 0.7.3",
"rand_pcg 0.2.1",
"serde",
"smallvec 1.6.1",
"sp-core",
@@ -6816,15 +6844,6 @@ dependencies = [
"rand_core 0.5.1",
]
[[package]]
name = "rand_pcg"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7de198537002b913568a3847e53535ace266f93526caf5c360ec41d72c5787f0"
dependencies = [
"rand_core 0.6.2",
]
[[package]]
name = "rand_xorshift"
version = "0.1.1"
+17
View File
@@ -468,3 +468,20 @@ algorithms have different inherent complexity so users must expect the above
mentioned crypto hashes to have varying gas costs.
The complexity of each cryptographic hash function highly depends on the underlying
implementation.
### seal_ecdsa_recover
This function receives the following arguments:
- `signature` is 65 bytes buffer,
- `message_hash` is 32 bytes buffer,
- `output` is 33 bytes buffer to return compressed public key,
It consists of the following steps:
1. Loading `signature` buffer from the sandbox memory (see sandboxing memory get).
2. Loading `message_hash` buffer from the sandbox memory.
3. Invoking the executive function `secp256k1_ecdsa_recover_compressed`.
4. Copy the bytes of compressed public key into the contract side output buffer.
**complexity**: Complexity is partially constant(it doesn't depend on input) but still depends on points of ECDSA and calculation.
+5 -2
View File
@@ -27,8 +27,9 @@ smallvec = { version = "1", default-features = false, features = [
wasmi-validation = { version = "0.4", default-features = false }
# Only used in benchmarking to generate random contract code
rand = { version = "0.8", optional = true, default-features = false }
rand_pcg = { version = "0.3", optional = true }
libsecp256k1 = { version = "0.3.5", optional = true, default-features = false, features = ["hmac"] }
rand = { version = "0.7.3", optional = true, default-features = false }
rand_pcg = { version = "0.2", optional = true }
# Substrate Dependencies
frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true }
@@ -73,9 +74,11 @@ std = [
"pallet-contracts-proc-macro/full",
"log/std",
"rand/std",
"libsecp256k1/std",
]
runtime-benchmarks = [
"frame-benchmarking",
"libsecp256k1",
"rand",
"rand_pcg",
"unstable-interface",
@@ -0,0 +1,55 @@
;; This contract:
;; 1) Reads signature and message hash from the input
;; 2) Calls ecdsa_recover
;; 3) Validates that result is Success
;; 4) Returns recovered compressed public key
(module
(import "__unstable__" "seal_ecdsa_recover" (func $seal_ecdsa_recover (param i32 i32 i32) (result i32)))
(import "seal0" "seal_return" (func $seal_return (param i32 i32 i32)))
(import "seal0" "seal_input" (func $seal_input (param i32 i32)))
(import "env" "memory" (memory 1 1))
(func $assert (param i32)
(block $ok
(br_if $ok
(get_local 0)
)
(unreachable)
)
)
(func (export "deploy"))
;; [4, 8) len of signature + message hash - 65 bytes + 32 byte = 97 bytes
(data (i32.const 4) "\61")
;; Memory layout during `call`
;; [10, 75) signature
;; [75, 107) message hash
(func (export "call")
(local $signature_ptr i32)
(local $message_hash_ptr i32)
(local $result i32)
(local.set $signature_ptr (i32.const 10))
(local.set $message_hash_ptr (i32.const 75))
;; Read signature and message hash - 97 bytes
(call $seal_input (local.get $signature_ptr) (i32.const 4))
(local.set
$result
(call $seal_ecdsa_recover
(local.get $signature_ptr)
(local.get $message_hash_ptr)
(local.get $signature_ptr) ;; Store output into message signature ptr, because we don't need it anymore
)
)
(call $assert
(i32.eq
(local.get $result) ;; The result of recovery execution
(i32.const 0x0) ;; 0x0 - Success result
)
)
;; exit with success and return recovered public key
(call $seal_return (i32.const 0) (local.get $signature_ptr) (i32.const 33))
)
)
@@ -492,11 +492,11 @@ pub mod body {
vec![Instruction::I32Const(current as i32)]
},
DynInstr::RandomUnaligned(low, high) => {
let unaligned = rng.gen_range(*low..*high) | 1;
let unaligned = rng.gen_range(*low, *high) | 1;
vec![Instruction::I32Const(unaligned as i32)]
},
DynInstr::RandomI32(low, high) => {
vec![Instruction::I32Const(rng.gen_range(*low..*high))]
vec![Instruction::I32Const(rng.gen_range(*low, *high))]
},
DynInstr::RandomI32Repeated(num) => (&mut rng)
.sample_iter(Standard)
@@ -509,19 +509,19 @@ pub mod body {
.map(|val| Instruction::I64Const(val))
.collect(),
DynInstr::RandomGetLocal(low, high) => {
vec![Instruction::GetLocal(rng.gen_range(*low..*high))]
vec![Instruction::GetLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomSetLocal(low, high) => {
vec![Instruction::SetLocal(rng.gen_range(*low..*high))]
vec![Instruction::SetLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomTeeLocal(low, high) => {
vec![Instruction::TeeLocal(rng.gen_range(*low..*high))]
vec![Instruction::TeeLocal(rng.gen_range(*low, *high))]
},
DynInstr::RandomGetGlobal(low, high) => {
vec![Instruction::GetGlobal(rng.gen_range(*low..*high))]
vec![Instruction::GetGlobal(rng.gen_range(*low, *high))]
},
DynInstr::RandomSetGlobal(low, high) => {
vec![Instruction::SetGlobal(rng.gen_range(*low..*high))]
vec![Instruction::SetGlobal(rng.gen_range(*low, *high))]
},
})
.chain(sp_std::iter::once(Instruction::End))
@@ -1415,6 +1415,60 @@ benchmarks! {
let origin = RawOrigin::Signed(instance.caller.clone());
}: call(origin, instance.addr, 0u32.into(), Weight::max_value(), vec![])
// Only calling the function itself with valid arguments.
// It generates different private keys and signatures for the message "Hello world".
seal_ecdsa_recover {
let r in 0 .. API_BENCHMARK_BATCHES;
use rand::SeedableRng;
let mut rng = rand_pcg::Pcg32::seed_from_u64(123456);
let message_hash = sp_io::hashing::blake2_256("Hello world".as_bytes());
let signatures = (0..r * API_BENCHMARK_BATCH_SIZE)
.map(|i| {
use secp256k1::{SecretKey, Message, sign};
let private_key = SecretKey::random(&mut rng);
let (signature, recovery_id) = sign(&Message::parse(&message_hash), &private_key);
let mut full_signature = [0; 65];
full_signature[..64].copy_from_slice(&signature.serialize());
full_signature[64] = recovery_id.serialize();
full_signature
})
.collect::<Vec<_>>();
let signatures = signatures.iter().flatten().cloned().collect::<Vec<_>>();
let signatures_bytes_len = signatures.len() as i32;
let code = WasmModule::<T>::from(ModuleDefinition {
memory: Some(ImportedMemory::max::<T>()),
imported_functions: vec![ImportedFunction {
module: "__unstable__",
name: "seal_ecdsa_recover",
params: vec![ValueType::I32, ValueType::I32, ValueType::I32],
return_type: Some(ValueType::I32),
}],
data_segments: vec![
DataSegment {
offset: 0,
value: message_hash[..].to_vec(),
},
DataSegment {
offset: 32,
value: signatures,
},
],
call_body: Some(body::repeated_dyn(r * API_BENCHMARK_BATCH_SIZE, vec![
Counter(32, 65), // signature_ptr
Regular(Instruction::I32Const(0)), // message_hash_ptr
Regular(Instruction::I32Const(signatures_bytes_len + 32)), // output_len_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_value(), vec![])
// We make the assumption that pushing a constant and dropping a value takes roughly
// the same amount of time. We follow that `t.load` and `drop` both have the weight
// of this benchmark / 2. We need to make this assumption because there is no way
+8
View File
@@ -30,6 +30,7 @@ use frame_system::RawOrigin;
use pallet_contracts_primitives::ExecReturnValue;
use smallvec::{Array, SmallVec};
use sp_core::crypto::UncheckedFrom;
use sp_io::crypto::secp256k1_ecdsa_recover_compressed;
use sp_runtime::traits::{Convert, Saturating};
use sp_std::{marker::PhantomData, mem, prelude::*};
@@ -205,6 +206,9 @@ pub trait Ext: sealing::Sealed {
/// Call some dispatchable and return the result.
fn call_runtime(&self, call: <Self::T as Config>::Call) -> DispatchResultWithPostInfo;
/// 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], ()>;
}
/// Describes the different functions that can be exported by an [`Executable`].
@@ -1033,6 +1037,10 @@ where
origin.add_filter(T::CallFilter::contains);
call.dispatch(origin)
}
fn ecdsa_recover(&self, signature: &[u8; 65], message_hash: &[u8; 32]) -> Result<[u8; 33], ()> {
secp256k1_ecdsa_recover_compressed(&signature, &message_hash).map_err(|_| ())
}
}
fn deposit_event<T: Config>(topics: Vec<T::Hash>, event: Event<T>) {
@@ -378,6 +378,9 @@ pub struct HostFnWeights<T: Config> {
/// Weight per byte hashed by `seal_hash_blake2_128`.
pub hash_blake2_128_per_byte: Weight,
/// Weight of calling `seal_ecdsa_recover`.
pub ecdsa_recover: Weight,
/// The type parameter is used in the default implementation.
#[codec(skip)]
pub _phantom: PhantomData<T>,
@@ -625,6 +628,7 @@ impl<T: Config> Default for HostFnWeights<T> {
hash_blake2_256_per_byte: cost_byte_batched!(seal_hash_blake2_256_per_kb),
hash_blake2_128: cost_batched!(seal_hash_blake2_128),
hash_blake2_128_per_byte: cost_byte_batched!(seal_hash_blake2_128_per_kb),
ecdsa_recover: cost_batched!(seal_ecdsa_recover),
_phantom: PhantomData,
}
}
+50
View File
@@ -1795,3 +1795,53 @@ fn gas_estimation_call_runtime() {
);
});
}
#[test]
#[cfg(feature = "unstable-interface")]
fn ecdsa_recover() {
let (wasm, code_hash) = compile_module::<Test>("ecdsa_recover").unwrap();
ExtBuilder::default().existential_deposit(50).build().execute_with(|| {
let _ = Balances::deposit_creating(&ALICE, 1_000_000);
// Instantiate the ecdsa_recover contract.
assert_ok!(Contracts::instantiate_with_code(
Origin::signed(ALICE),
100_000,
GAS_LIMIT,
wasm,
vec![],
vec![],
));
let addr = Contracts::contract_address(&ALICE, &code_hash, &[]);
#[rustfmt::skip]
let signature: [u8; 65] = [
161, 234, 203, 74, 147, 96, 51, 212, 5, 174, 231, 9, 142, 48, 137, 201,
162, 118, 192, 67, 239, 16, 71, 216, 125, 86, 167, 139, 70, 7, 86, 241,
33, 87, 154, 251, 81, 29, 160, 4, 176, 239, 88, 211, 244, 232, 232, 52,
211, 234, 100, 115, 230, 47, 80, 44, 152, 166, 62, 50, 8, 13, 86, 175,
28,
];
#[rustfmt::skip]
let message_hash: [u8; 32] = [
162, 28, 244, 179, 96, 76, 244, 178, 188, 83, 230, 248, 143, 106, 77, 117,
239, 95, 244, 171, 65, 95, 62, 153, 174, 166, 182, 28, 130, 73, 196, 208
];
#[rustfmt::skip]
const EXPECTED_COMPRESSED_PUBLIC_KEY: [u8; 33] = [
2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11,
7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23,
152,
];
let mut params = vec![];
params.extend_from_slice(&signature);
params.extend_from_slice(&message_hash);
assert!(params.len() == 65 + 32);
let result = <Pallet<Test>>::bare_call(ALICE, addr.clone(), 0, GAS_LIMIT, params, false)
.result
.unwrap();
assert!(result.is_success());
assert_eq!(result.data.as_ref(), &EXPECTED_COMPRESSED_PUBLIC_KEY);
})
}
+56
View File
@@ -295,6 +295,7 @@ mod tests {
schedule: Schedule<Test>,
gas_meter: GasMeter<Test>,
debug_buffer: Vec<u8>,
ecdsa_recover: RefCell<Vec<([u8; 65], [u8; 32])>>,
}
/// The call is mocked and just returns this hardcoded value.
@@ -315,6 +316,7 @@ mod tests {
schedule: Default::default(),
gas_meter: GasMeter::new(10_000_000_000),
debug_buffer: Default::default(),
ecdsa_recover: Default::default(),
}
}
}
@@ -418,6 +420,15 @@ mod tests {
self.runtime_calls.borrow_mut().push(call);
Ok(Default::default())
}
fn ecdsa_recover(
&self,
signature: &[u8; 65],
message_hash: &[u8; 32],
) -> Result<[u8; 33], ()> {
self.ecdsa_recover.borrow_mut().push((signature.clone(), message_hash.clone()));
Ok([3; 33])
}
}
fn execute<E: BorrowMut<MockExt>>(wat: &str, input_data: Vec<u8>, mut ext: E) -> ExecResult {
@@ -850,6 +861,51 @@ mod tests {
);
}
#[cfg(feature = "unstable-interface")]
const CODE_ECDSA_RECOVER: &str = r#"
(module
;; seal_ecdsa_recover(
;; signature_ptr: u32,
;; message_hash_ptr: u32,
;; output_ptr: u32
;; ) -> u32
(import "__unstable__" "seal_ecdsa_recover" (func $seal_ecdsa_recover (param i32 i32 i32) (result i32)))
(import "env" "memory" (memory 1 1))
(func (export "call")
(drop
(call $seal_ecdsa_recover
(i32.const 36) ;; Pointer to signature.
(i32.const 4) ;; Pointer to message hash.
(i32.const 36) ;; Pointer for output - public key.
)
)
)
(func (export "deploy"))
;; Hash of message.
(data (i32.const 4)
"\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"
)
;; Signature
(data (i32.const 36)
"\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"
"\01"
)
)
"#;
#[test]
#[cfg(feature = "unstable-interface")]
fn contract_ecdsa_recover() {
let mut mock_ext = MockExt::default();
assert_ok!(execute(&CODE_ECDSA_RECOVER, vec![], &mut mock_ext));
assert_eq!(mock_ext.ecdsa_recover.into_inner(), [([1; 65], [1; 32])]);
}
const CODE_GET_STORAGE: &str = r#"
(module
(import "seal0" "seal_get_storage" (func $seal_get_storage (param i32 i32 i32) (result i32)))
@@ -73,6 +73,9 @@ pub enum ReturnCode {
/// The call dispatched by `seal_call_runtime` was executed but returned an error.
#[cfg(feature = "unstable-interface")]
CallRuntimeReturnedError = 10,
/// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature.
#[cfg(feature = "unstable-interface")]
EcdsaRecoverFailed = 11,
}
impl ConvertibleToWasm for ReturnCode {
@@ -199,6 +202,9 @@ pub enum RuntimeCosts {
HashBlake256(u32),
/// Weight of calling `seal_hash_blake2_128` for the given input size.
HashBlake128(u32),
/// Weight of calling `seal_ecdsa_recover`.
#[cfg(feature = "unstable-interface")]
EcdsaRecovery,
/// Weight charged by a chain extension through `seal_call_chain_extension`.
ChainExtension(u64),
/// Weight charged for copying data from the sandbox.
@@ -265,6 +271,8 @@ impl RuntimeCosts {
HashBlake128(len) => s
.hash_blake2_128
.saturating_add(s.hash_blake2_128_per_byte.saturating_mul(len.into())),
#[cfg(feature = "unstable-interface")]
EcdsaRecovery => s.ecdsa_recover,
ChainExtension(amount) => amount,
#[cfg(feature = "unstable-interface")]
CopyIn(len) => s.return_per_byte.saturating_mul(len.into()),
@@ -1712,4 +1720,44 @@ define_env!(Env, <E: Ext>,
Err(_) => Ok(ReturnCode::CallRuntimeReturnedError),
}
},
// Recovers the ECDSA public key from the given message hash and signature.
//
// Writes the public key into the given output buffer.
// Assumes the secp256k1 curve.
//
// # Parameters
//
// - `signature_ptr`: the pointer into the linear memory where the signature
// is placed. Should be decodable as a 65 bytes. Traps otherwise.
// - `message_hash_ptr`: the pointer into the linear memory where the message
// hash is placed. Should be decodable as a 32 bytes. Traps otherwise.
// - `output_ptr`: the pointer into the linear memory where the output
// data is placed. The buffer should be 33 bytes. Traps otherwise.
// The function will write the result directly into this buffer.
//
// # Errors
//
// `ReturnCode::EcdsaRecoverFailed`
[__unstable__] seal_ecdsa_recover(ctx, signature_ptr: u32, message_hash_ptr: u32, output_ptr: u32) -> ReturnCode => {
ctx.charge_gas(RuntimeCosts::EcdsaRecovery)?;
let mut signature: [u8; 65] = [0; 65];
ctx.read_sandbox_memory_into_buf(signature_ptr, &mut signature)?;
let mut message_hash: [u8; 32] = [0; 32];
ctx.read_sandbox_memory_into_buf(message_hash_ptr, &mut message_hash)?;
let result = ctx.ext.ecdsa_recover(&signature, &message_hash);
match result {
Ok(pub_key) => {
// Write the recovered compressed ecdsa public key back into the sandboxed output
// buffer.
ctx.write_sandbox_memory(output_ptr, pub_key.as_ref())?;
Ok(ReturnCode::Success)
},
Err(_) => Ok(ReturnCode::EcdsaRecoverFailed),
}
},
);
File diff suppressed because it is too large Load Diff
+1 -1
View File
@@ -23,7 +23,7 @@ use crate::crypto::KeyTypeId;
pub const ED25519: KeyTypeId = KeyTypeId(*b"ed25");
/// Key type for generic Sr 25519 key.
pub const SR25519: KeyTypeId = KeyTypeId(*b"sr25");
/// Key type for generic Sr 25519 key.
/// Key type for generic ECDSA key.
pub const ECDSA: KeyTypeId = KeyTypeId(*b"ecds");
/// Macro for exporting functions from wasm in with the expected signature for using it with the