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

Commit

Permalink
Implemented seal_ecdsa_recovery function in the contract pallet (#9686
Browse files Browse the repository at this point in the history
)

* 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>
  • Loading branch information
3 people committed Sep 10, 2021
1 parent 873e6b1 commit 7fa40a2
Show file tree
Hide file tree
Showing 13 changed files with 941 additions and 610 deletions.
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"] }
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 }
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;
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);
})
}

0 comments on commit 7fa40a2

Please sign in to comment.