Skip to content
This repository has been archived by the owner on Nov 15, 2023. It is now read-only.

Implemented seal_ecdsa_recovery function in the contract pallet #9686

Merged
16 commits merged into from Sep 10, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
45 changes: 32 additions & 13 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions frame/contracts/COMPLEXITY.md
Expand Up @@ -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.
7 changes: 5 additions & 2 deletions frame/contracts/Cargo.toml
Expand Up @@ -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"] }
athei marked this conversation as resolved.
Show resolved Hide resolved
rand = { version = "0.7.3", optional = true, default-features = false }
rand_pcg = { version = "0.2", optional = true }
athei marked this conversation as resolved.
Show resolved Hide resolved

# Substrate Dependencies
frame-benchmarking = { version = "4.0.0-dev", default-features = false, path = "../benchmarking", optional = true }
Expand Down Expand Up @@ -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",
Expand Down
55 changes: 55 additions & 0 deletions frame/contracts/fixtures/ecdsa_recover.wat
@@ -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))
)
)
14 changes: 7 additions & 7 deletions frame/contracts/src/benchmarking/code.rs
Expand Up @@ -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;
athei marked this conversation as resolved.
Show resolved Hide resolved
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)
Expand All @@ -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))
Expand Down
54 changes: 54 additions & 0 deletions frame/contracts/src/benchmarking/mod.rs
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions frame/contracts/src/exec.rs
Expand Up @@ -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::*};

Expand Down Expand Up @@ -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`].
Expand Down Expand Up @@ -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>) {
Expand Down
4 changes: 4 additions & 0 deletions frame/contracts/src/schedule.rs
Expand Up @@ -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>,
Expand Down Expand Up @@ -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,
}
}
Expand Down
50 changes: 50 additions & 0 deletions frame/contracts/src/tests.rs
Expand Up @@ -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);
})
}