Skip to content

Commit 284e4e5

Browse files
authored
Implemented ECDSA recover function. (#914)
* Implemented ecdsa recovery function. Added method `to_eth_address` and `to_account_id`. Added tests. * Cargo fmt * Added `ECDSA` and `Ethereum` to dictionary * Fixed comments according a new spellcheck * Fixes according comments in review. * Fixed build issue for wasm * Use struct instead of alias for `EthereumAddress`. * cargo fmt --all * Simplified `ecdsa_recover`. USed symbolic links instead files. * Added documentation for `to_eth_address` and `to_account_id` methods. * Renamed `to_account_id` into `to_default_account_id` * Cargo fmt * Removed DeRef trait. Now field of `EthereumAddress` and `ECDSAPublicKey` is private. * Fixed doc test for ecdsa_recover in EnvAccess
1 parent 2f4f4f0 commit 284e4e5

File tree

17 files changed

+443
-0
lines changed

17 files changed

+443
-0
lines changed

.config/cargo_spellcheck.dic

+2
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@ AST
55
BLAKE2
66
BLAKE2b
77
DApp
8+
ECDSA
89
ERC
10+
Ethereum
911
FFI
1012
Gnosis
1113
GPL

crates/engine/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,9 @@ sha2 = { version = "0.9" }
2222
sha3 = { version = "0.9" }
2323
blake2 = { version = "0.9" }
2424

25+
# ECDSA for the off-chain environment.
26+
libsecp256k1 = { version = "0.3.5", default-features = false }
27+
2528
[features]
2629
default = ["std"]
2730
std = [

crates/engine/src/ext.rs

+41
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,8 @@ define_error_codes! {
9494
/// The call to `seal_debug_message` had no effect because debug message
9595
/// recording was disabled.
9696
LoggingDisabled = 9,
97+
/// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature.
98+
EcdsaRecoverFailed = 11,
9799
}
98100

99101
/// The raw return code returned by the host side.
@@ -417,6 +419,45 @@ impl Engine {
417419
"off-chain environment does not yet support `call_chain_extension`"
418420
);
419421
}
422+
423+
/// Recovers the compressed ECDSA public key for given `signature` and `message_hash`,
424+
/// and stores the result in `output`.
425+
pub fn ecdsa_recover(
426+
&mut self,
427+
signature: &[u8; 65],
428+
message_hash: &[u8; 32],
429+
output: &mut [u8; 33],
430+
) -> Result {
431+
use secp256k1::{
432+
recover,
433+
Message,
434+
RecoveryId,
435+
Signature,
436+
};
437+
438+
// In most implementations, the v is just 0 or 1 internally, but 27 was added
439+
// as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well.
440+
let recovery_byte = if signature[64] > 26 {
441+
signature[64] - 27
442+
} else {
443+
signature[64]
444+
};
445+
let message = Message::parse(message_hash);
446+
let signature = Signature::parse_slice(&signature[0..64])
447+
.unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error));
448+
449+
let recovery_id = RecoveryId::parse(recovery_byte)
450+
.unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error));
451+
452+
let pub_key = recover(&message, &signature, &recovery_id);
453+
match pub_key {
454+
Ok(pub_key) => {
455+
*output = pub_key.serialize_compressed();
456+
Ok(())
457+
}
458+
Err(_) => Err(Error::EcdsaRecoverFailed),
459+
}
460+
}
420461
}
421462

422463
/// Copies the `slice` into `output`.

crates/env/Cargo.toml

+3
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,9 @@ sha2 = { version = "0.9", optional = true }
3535
sha3 = { version = "0.9", optional = true }
3636
blake2 = { version = "0.9", optional = true }
3737

38+
# ECDSA for the off-chain environment.
39+
libsecp256k1 = { version = "0.3.5", default-features = false }
40+
3841
# Only used in the off-chain environment.
3942
#
4043
# Sadly couldn't be marked as dev-dependency.

crates/env/src/api.rs

+36
Original file line numberDiff line numberDiff line change
@@ -604,3 +604,39 @@ where
604604
instance.hash_encoded::<H, T>(input, output)
605605
})
606606
}
607+
608+
/// Recovers the compressed ECDSA public key for given `signature` and `message_hash`,
609+
/// and stores the result in `output`.
610+
///
611+
/// # Example
612+
///
613+
/// ```
614+
/// const signature: [u8; 65] = [
615+
/// 161, 234, 203, 74, 147, 96, 51, 212, 5, 174, 231, 9, 142, 48, 137, 201,
616+
/// 162, 118, 192, 67, 239, 16, 71, 216, 125, 86, 167, 139, 70, 7, 86, 241,
617+
/// 33, 87, 154, 251, 81, 29, 160, 4, 176, 239, 88, 211, 244, 232, 232, 52,
618+
/// 211, 234, 100, 115, 230, 47, 80, 44, 152, 166, 62, 50, 8, 13, 86, 175,
619+
/// 28,
620+
/// ];
621+
/// const message_hash: [u8; 32] = [
622+
/// 162, 28, 244, 179, 96, 76, 244, 178, 188, 83, 230, 248, 143, 106, 77, 117,
623+
/// 239, 95, 244, 171, 65, 95, 62, 153, 174, 166, 182, 28, 130, 73, 196, 208
624+
/// ];
625+
/// const EXPECTED_COMPRESSED_PUBLIC_KEY: [u8; 33] = [
626+
/// 2, 121, 190, 102, 126, 249, 220, 187, 172, 85, 160, 98, 149, 206, 135, 11,
627+
/// 7, 2, 155, 252, 219, 45, 206, 40, 217, 89, 242, 129, 91, 22, 248, 23,
628+
/// 152,
629+
/// ];
630+
/// let mut output = [0; 33];
631+
/// ink_env::ecdsa_recover(&signature, &message_hash, &mut output);
632+
/// assert_eq!(output, EXPECTED_COMPRESSED_PUBLIC_KEY);
633+
/// ```
634+
pub fn ecdsa_recover(
635+
signature: &[u8; 65],
636+
message_hash: &[u8; 32],
637+
output: &mut [u8; 33],
638+
) -> Result<()> {
639+
<EnvInstance as OnInstance>::on_instance(|instance| {
640+
instance.ecdsa_recover(signature, message_hash, output)
641+
})
642+
}

crates/env/src/backend.rs

+9
Original file line numberDiff line numberDiff line change
@@ -135,6 +135,15 @@ pub trait EnvBackend {
135135
H: CryptoHash,
136136
T: scale::Encode;
137137

138+
/// Recovers the compressed ECDSA public key for given `signature` and `message_hash`,
139+
/// and stores the result in `output`.
140+
fn ecdsa_recover(
141+
&mut self,
142+
signature: &[u8; 65],
143+
message_hash: &[u8; 32],
144+
output: &mut [u8; 33],
145+
) -> Result<()>;
146+
138147
/// Low-level interface to call a chain extension method.
139148
///
140149
/// Returns the output of the chain extension of the specified type.

crates/env/src/engine/experimental_off_chain/impls.rs

+38
Original file line numberDiff line numberDiff line change
@@ -112,6 +112,7 @@ impl From<ext::Error> for crate::Error {
112112
ext::Error::CodeNotFound => Self::CodeNotFound,
113113
ext::Error::NotCallable => Self::NotCallable,
114114
ext::Error::LoggingDisabled => Self::LoggingDisabled,
115+
ext::Error::EcdsaRecoverFailed => Self::EcdsaRecoverFailed,
115116
}
116117
}
117118
}
@@ -248,6 +249,43 @@ impl EnvBackend for EnvInstance {
248249
<H as CryptoHash>::hash(enc_input, output)
249250
}
250251

252+
fn ecdsa_recover(
253+
&mut self,
254+
signature: &[u8; 65],
255+
message_hash: &[u8; 32],
256+
output: &mut [u8; 33],
257+
) -> Result<()> {
258+
use secp256k1::{
259+
recover,
260+
Message,
261+
RecoveryId,
262+
Signature,
263+
};
264+
265+
// In most implementations, the v is just 0 or 1 internally, but 27 was added
266+
// as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well.
267+
let recovery_byte = if signature[64] > 26 {
268+
signature[64] - 27
269+
} else {
270+
signature[64]
271+
};
272+
let message = Message::parse(message_hash);
273+
let signature = Signature::parse_slice(&signature[0..64])
274+
.unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error));
275+
276+
let recovery_id = RecoveryId::parse(recovery_byte)
277+
.unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error));
278+
279+
let pub_key = recover(&message, &signature, &recovery_id);
280+
match pub_key {
281+
Ok(pub_key) => {
282+
*output = pub_key.serialize_compressed();
283+
Ok(())
284+
}
285+
Err(_) => Err(crate::Error::EcdsaRecoverFailed),
286+
}
287+
}
288+
251289
fn call_chain_extension<I, T, E, ErrorCode, F, D>(
252290
&mut self,
253291
func_id: u32,

crates/env/src/engine/off_chain/impls.rs

+37
Original file line numberDiff line numberDiff line change
@@ -195,6 +195,43 @@ impl EnvBackend for EnvInstance {
195195
self.hash_bytes::<H>(&encoded[..], output)
196196
}
197197

198+
fn ecdsa_recover(
199+
&mut self,
200+
signature: &[u8; 65],
201+
message_hash: &[u8; 32],
202+
output: &mut [u8; 33],
203+
) -> Result<()> {
204+
use secp256k1::{
205+
recover,
206+
Message,
207+
RecoveryId,
208+
Signature,
209+
};
210+
211+
// In most implementations, the v is just 0 or 1 internally, but 27 was added
212+
// as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well.
213+
let recovery_byte = if signature[64] > 26 {
214+
signature[64] - 27
215+
} else {
216+
signature[64]
217+
};
218+
let message = Message::parse(message_hash);
219+
let signature = Signature::parse_slice(&signature[0..64])
220+
.unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error));
221+
222+
let recovery_id = RecoveryId::parse(recovery_byte)
223+
.unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error));
224+
225+
let pub_key = recover(&message, &signature, &recovery_id);
226+
match pub_key {
227+
Ok(pub_key) => {
228+
*output = pub_key.serialize_compressed();
229+
Ok(())
230+
}
231+
Err(_) => Err(Error::EcdsaRecoverFailed),
232+
}
233+
}
234+
198235
fn call_chain_extension<I, T, E, ErrorCode, F, D>(
199236
&mut self,
200237
func_id: u32,

crates/env/src/engine/on_chain/ext.rs

+25
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,8 @@ define_error_codes! {
7979
/// The call to `seal_debug_message` had no effect because debug message
8080
/// recording was disabled.
8181
LoggingDisabled = 9,
82+
/// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature.
83+
EcdsaRecoverFailed = 11,
8284
}
8385

8486
/// Thin-wrapper around a `u32` representing a pointer for Wasm32.
@@ -358,6 +360,14 @@ mod sys {
358360
output_ptr: Ptr32Mut<[u8]>,
359361
output_len_ptr: Ptr32Mut<u32>,
360362
);
363+
364+
pub fn seal_ecdsa_recover(
365+
// 65 bytes of ecdsa signature
366+
signature_ptr: Ptr32<[u8]>,
367+
// 32 bytes hash of the message
368+
message_hash_ptr: Ptr32<[u8]>,
369+
output_ptr: Ptr32Mut<[u8]>,
370+
) -> ReturnCode;
361371
}
362372
}
363373

@@ -707,3 +717,18 @@ impl_hash_fn!(sha2_256, 32);
707717
impl_hash_fn!(keccak_256, 32);
708718
impl_hash_fn!(blake2_256, 32);
709719
impl_hash_fn!(blake2_128, 16);
720+
721+
pub fn ecdsa_recover(
722+
signature: &[u8; 65],
723+
message_hash: &[u8; 32],
724+
output: &mut [u8; 33],
725+
) -> Result {
726+
let ret_code = unsafe {
727+
sys::seal_ecdsa_recover(
728+
Ptr32::from_slice(signature),
729+
Ptr32::from_slice(message_hash),
730+
Ptr32Mut::from_slice(output),
731+
)
732+
};
733+
ret_code.into()
734+
}

crates/env/src/engine/on_chain/impls.rs

+10
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,7 @@ impl From<ext::Error> for Error {
111111
ext::Error::CodeNotFound => Self::CodeNotFound,
112112
ext::Error::NotCallable => Self::NotCallable,
113113
ext::Error::LoggingDisabled => Self::LoggingDisabled,
114+
ext::Error::EcdsaRecoverFailed => Self::EcdsaRecoverFailed,
114115
}
115116
}
116117
}
@@ -277,6 +278,15 @@ impl EnvBackend for EnvInstance {
277278
<H as CryptoHash>::hash(enc_input, output)
278279
}
279280

281+
fn ecdsa_recover(
282+
&mut self,
283+
signature: &[u8; 65],
284+
message_hash: &[u8; 32],
285+
output: &mut [u8; 33],
286+
) -> Result<()> {
287+
ext::ecdsa_recover(signature, message_hash, output).map_err(Into::into)
288+
}
289+
280290
fn call_chain_extension<I, T, E, ErrorCode, F, D>(
281291
&mut self,
282292
func_id: u32,

crates/env/src/error.rs

+2
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ pub enum Error {
4949
/// The call to `seal_debug_message` had no effect because debug message
5050
/// recording was disabled.
5151
LoggingDisabled,
52+
/// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature.
53+
EcdsaRecoverFailed,
5254
}
5355

5456
/// A result of environmental operations.

crates/eth_compatibility/Cargo.toml

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
[package]
2+
name = "ink_eth_compatibility"
3+
version = "3.0.0-rc5"
4+
authors = ["Parity Technologies <[email protected]>"]
5+
edition = "2018"
6+
7+
license = "Apache-2.0"
8+
readme = "README.md"
9+
repository = "https://github.com/paritytech/ink"
10+
documentation = "https://docs.rs/ink_eth_compatibility/"
11+
homepage = "https://www.parity.io/"
12+
description = "[ink!] Ethereum related stuff."
13+
keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl", "ethereum"]
14+
categories = ["no-std", "embedded"]
15+
include = ["Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"]
16+
17+
[dependencies]
18+
ink_env = { version = "3.0.0-rc5", path = "../env", default-features = false }
19+
libsecp256k1 = { version = "0.3.5", default-features = false }
20+
21+
[features]
22+
default = ["std"]
23+
std = [
24+
"ink_env/std",
25+
]

crates/eth_compatibility/LICENSE

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../LICENSE

crates/eth_compatibility/README.md

+1
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../../README.md

0 commit comments

Comments
 (0)