diff --git a/.config/cargo_spellcheck.dic b/.config/cargo_spellcheck.dic index a56820143c3..57e94fb1720 100644 --- a/.config/cargo_spellcheck.dic +++ b/.config/cargo_spellcheck.dic @@ -5,7 +5,9 @@ AST BLAKE2 BLAKE2b DApp +ECDSA ERC +Ethereum FFI Gnosis GPL diff --git a/crates/engine/Cargo.toml b/crates/engine/Cargo.toml index dcc95308428..34947def790 100644 --- a/crates/engine/Cargo.toml +++ b/crates/engine/Cargo.toml @@ -22,6 +22,9 @@ sha2 = { version = "0.9" } sha3 = { version = "0.9" } blake2 = { version = "0.9" } +# ECDSA for the off-chain environment. +libsecp256k1 = { version = "0.3.5", default-features = false } + [features] default = ["std"] std = [ diff --git a/crates/engine/src/ext.rs b/crates/engine/src/ext.rs index 303a9de4464..a302df6390a 100644 --- a/crates/engine/src/ext.rs +++ b/crates/engine/src/ext.rs @@ -94,6 +94,8 @@ define_error_codes! { /// The call to `seal_debug_message` had no effect because debug message /// recording was disabled. LoggingDisabled = 9, + /// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature. + EcdsaRecoverFailed = 11, } /// The raw return code returned by the host side. @@ -417,6 +419,45 @@ impl Engine { "off-chain environment does not yet support `call_chain_extension`" ); } + + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, + /// and stores the result in `output`. + pub fn ecdsa_recover( + &mut self, + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result { + use secp256k1::{ + recover, + Message, + RecoveryId, + Signature, + }; + + // In most implementations, the v is just 0 or 1 internally, but 27 was added + // as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well. + let recovery_byte = if signature[64] > 26 { + signature[64] - 27 + } else { + signature[64] + }; + let message = Message::parse(message_hash); + let signature = Signature::parse_slice(&signature[0..64]) + .unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error)); + + let recovery_id = RecoveryId::parse(recovery_byte) + .unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error)); + + let pub_key = recover(&message, &signature, &recovery_id); + match pub_key { + Ok(pub_key) => { + *output = pub_key.serialize_compressed(); + Ok(()) + } + Err(_) => Err(Error::EcdsaRecoverFailed), + } + } } /// Copies the `slice` into `output`. diff --git a/crates/env/Cargo.toml b/crates/env/Cargo.toml index 6a6e0fcc837..c35b5d58295 100644 --- a/crates/env/Cargo.toml +++ b/crates/env/Cargo.toml @@ -35,6 +35,9 @@ sha2 = { version = "0.9", optional = true } sha3 = { version = "0.9", optional = true } blake2 = { version = "0.9", optional = true } +# ECDSA for the off-chain environment. +libsecp256k1 = { version = "0.3.5", default-features = false } + # Only used in the off-chain environment. # # Sadly couldn't be marked as dev-dependency. diff --git a/crates/env/src/api.rs b/crates/env/src/api.rs index 3a9081db162..4036a4cab79 100644 --- a/crates/env/src/api.rs +++ b/crates/env/src/api.rs @@ -604,3 +604,39 @@ where instance.hash_encoded::(input, output) }) } + +/// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, +/// and stores the result in `output`. +/// +/// # Example +/// +/// ``` +/// const 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, +/// ]; +/// const 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 +/// ]; +/// 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 output = [0; 33]; +/// ink_env::ecdsa_recover(&signature, &message_hash, &mut output); +/// assert_eq!(output, EXPECTED_COMPRESSED_PUBLIC_KEY); +/// ``` +pub fn ecdsa_recover( + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], +) -> Result<()> { + ::on_instance(|instance| { + instance.ecdsa_recover(signature, message_hash, output) + }) +} diff --git a/crates/env/src/backend.rs b/crates/env/src/backend.rs index c901135a328..67c2f1bc5a5 100644 --- a/crates/env/src/backend.rs +++ b/crates/env/src/backend.rs @@ -135,6 +135,15 @@ pub trait EnvBackend { H: CryptoHash, T: scale::Encode; + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, + /// and stores the result in `output`. + fn ecdsa_recover( + &mut self, + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result<()>; + /// Low-level interface to call a chain extension method. /// /// Returns the output of the chain extension of the specified type. diff --git a/crates/env/src/engine/experimental_off_chain/impls.rs b/crates/env/src/engine/experimental_off_chain/impls.rs index 434b205875c..db773c0ffce 100644 --- a/crates/env/src/engine/experimental_off_chain/impls.rs +++ b/crates/env/src/engine/experimental_off_chain/impls.rs @@ -112,6 +112,7 @@ impl From for crate::Error { ext::Error::CodeNotFound => Self::CodeNotFound, ext::Error::NotCallable => Self::NotCallable, ext::Error::LoggingDisabled => Self::LoggingDisabled, + ext::Error::EcdsaRecoverFailed => Self::EcdsaRecoverFailed, } } } @@ -248,6 +249,43 @@ impl EnvBackend for EnvInstance { ::hash(enc_input, output) } + fn ecdsa_recover( + &mut self, + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result<()> { + use secp256k1::{ + recover, + Message, + RecoveryId, + Signature, + }; + + // In most implementations, the v is just 0 or 1 internally, but 27 was added + // as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well. + let recovery_byte = if signature[64] > 26 { + signature[64] - 27 + } else { + signature[64] + }; + let message = Message::parse(message_hash); + let signature = Signature::parse_slice(&signature[0..64]) + .unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error)); + + let recovery_id = RecoveryId::parse(recovery_byte) + .unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error)); + + let pub_key = recover(&message, &signature, &recovery_id); + match pub_key { + Ok(pub_key) => { + *output = pub_key.serialize_compressed(); + Ok(()) + } + Err(_) => Err(crate::Error::EcdsaRecoverFailed), + } + } + fn call_chain_extension( &mut self, func_id: u32, diff --git a/crates/env/src/engine/off_chain/impls.rs b/crates/env/src/engine/off_chain/impls.rs index 074f0d3cbe3..b86631242b3 100644 --- a/crates/env/src/engine/off_chain/impls.rs +++ b/crates/env/src/engine/off_chain/impls.rs @@ -195,6 +195,43 @@ impl EnvBackend for EnvInstance { self.hash_bytes::(&encoded[..], output) } + fn ecdsa_recover( + &mut self, + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result<()> { + use secp256k1::{ + recover, + Message, + RecoveryId, + Signature, + }; + + // In most implementations, the v is just 0 or 1 internally, but 27 was added + // as an arbitrary number for signing Bitcoin messages and Ethereum adopted that as well. + let recovery_byte = if signature[64] > 26 { + signature[64] - 27 + } else { + signature[64] + }; + let message = Message::parse(message_hash); + let signature = Signature::parse_slice(&signature[0..64]) + .unwrap_or_else(|error| panic!("Unable to parse the signature: {}", error)); + + let recovery_id = RecoveryId::parse(recovery_byte) + .unwrap_or_else(|error| panic!("Unable to parse the recovery id: {}", error)); + + let pub_key = recover(&message, &signature, &recovery_id); + match pub_key { + Ok(pub_key) => { + *output = pub_key.serialize_compressed(); + Ok(()) + } + Err(_) => Err(Error::EcdsaRecoverFailed), + } + } + fn call_chain_extension( &mut self, func_id: u32, diff --git a/crates/env/src/engine/on_chain/ext.rs b/crates/env/src/engine/on_chain/ext.rs index 3e80830a618..92b2be4e0b3 100644 --- a/crates/env/src/engine/on_chain/ext.rs +++ b/crates/env/src/engine/on_chain/ext.rs @@ -79,6 +79,8 @@ define_error_codes! { /// The call to `seal_debug_message` had no effect because debug message /// recording was disabled. LoggingDisabled = 9, + /// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature. + EcdsaRecoverFailed = 11, } /// Thin-wrapper around a `u32` representing a pointer for Wasm32. @@ -358,6 +360,14 @@ mod sys { output_ptr: Ptr32Mut<[u8]>, output_len_ptr: Ptr32Mut, ); + + pub fn seal_ecdsa_recover( + // 65 bytes of ecdsa signature + signature_ptr: Ptr32<[u8]>, + // 32 bytes hash of the message + message_hash_ptr: Ptr32<[u8]>, + output_ptr: Ptr32Mut<[u8]>, + ) -> ReturnCode; } } @@ -707,3 +717,18 @@ impl_hash_fn!(sha2_256, 32); impl_hash_fn!(keccak_256, 32); impl_hash_fn!(blake2_256, 32); impl_hash_fn!(blake2_128, 16); + +pub fn ecdsa_recover( + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], +) -> Result { + let ret_code = unsafe { + sys::seal_ecdsa_recover( + Ptr32::from_slice(signature), + Ptr32::from_slice(message_hash), + Ptr32Mut::from_slice(output), + ) + }; + ret_code.into() +} diff --git a/crates/env/src/engine/on_chain/impls.rs b/crates/env/src/engine/on_chain/impls.rs index f5b801aa6fb..ed0351b2806 100644 --- a/crates/env/src/engine/on_chain/impls.rs +++ b/crates/env/src/engine/on_chain/impls.rs @@ -111,6 +111,7 @@ impl From for Error { ext::Error::CodeNotFound => Self::CodeNotFound, ext::Error::NotCallable => Self::NotCallable, ext::Error::LoggingDisabled => Self::LoggingDisabled, + ext::Error::EcdsaRecoverFailed => Self::EcdsaRecoverFailed, } } } @@ -277,6 +278,15 @@ impl EnvBackend for EnvInstance { ::hash(enc_input, output) } + fn ecdsa_recover( + &mut self, + signature: &[u8; 65], + message_hash: &[u8; 32], + output: &mut [u8; 33], + ) -> Result<()> { + ext::ecdsa_recover(signature, message_hash, output).map_err(Into::into) + } + fn call_chain_extension( &mut self, func_id: u32, diff --git a/crates/env/src/error.rs b/crates/env/src/error.rs index fd47ce0f53e..a6b9be65de7 100644 --- a/crates/env/src/error.rs +++ b/crates/env/src/error.rs @@ -49,6 +49,8 @@ pub enum Error { /// The call to `seal_debug_message` had no effect because debug message /// recording was disabled. LoggingDisabled, + /// ECDSA pubkey recovery failed. Most probably wrong recovery id or signature. + EcdsaRecoverFailed, } /// A result of environmental operations. diff --git a/crates/eth_compatibility/Cargo.toml b/crates/eth_compatibility/Cargo.toml new file mode 100644 index 00000000000..1389aa330c3 --- /dev/null +++ b/crates/eth_compatibility/Cargo.toml @@ -0,0 +1,25 @@ +[package] +name = "ink_eth_compatibility" +version = "3.0.0-rc5" +authors = ["Parity Technologies "] +edition = "2018" + +license = "Apache-2.0" +readme = "README.md" +repository = "https://github.com/paritytech/ink" +documentation = "https://docs.rs/ink_eth_compatibility/" +homepage = "https://www.parity.io/" +description = "[ink!] Ethereum related stuff." +keywords = ["wasm", "parity", "webassembly", "blockchain", "edsl", "ethereum"] +categories = ["no-std", "embedded"] +include = ["Cargo.toml", "src/**/*.rs", "/README.md", "/LICENSE"] + +[dependencies] +ink_env = { version = "3.0.0-rc5", path = "../env", default-features = false } +libsecp256k1 = { version = "0.3.5", default-features = false } + +[features] +default = ["std"] +std = [ + "ink_env/std", +] diff --git a/crates/eth_compatibility/LICENSE b/crates/eth_compatibility/LICENSE new file mode 120000 index 00000000000..30cff7403da --- /dev/null +++ b/crates/eth_compatibility/LICENSE @@ -0,0 +1 @@ +../../LICENSE \ No newline at end of file diff --git a/crates/eth_compatibility/README.md b/crates/eth_compatibility/README.md new file mode 120000 index 00000000000..fe840054137 --- /dev/null +++ b/crates/eth_compatibility/README.md @@ -0,0 +1 @@ +../../README.md \ No newline at end of file diff --git a/crates/eth_compatibility/src/lib.rs b/crates/eth_compatibility/src/lib.rs new file mode 100644 index 00000000000..4c27cad1ef3 --- /dev/null +++ b/crates/eth_compatibility/src/lib.rs @@ -0,0 +1,144 @@ +// Copyright 2018-2021 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![no_std] +use ink_env::{ + DefaultEnvironment, + Environment, +}; + +/// The ECDSA compressed public key. +#[derive(Debug, Copy, Clone)] +pub struct ECDSAPublicKey([u8; 33]); + +impl Default for ECDSAPublicKey { + fn default() -> Self { + // Default is not implemented for [u8; 33], so we can't derive it for ECDSAPublicKey + // But clippy thinks that it is possible. So it is workaround for clippy. + let empty = [0; 33]; + Self { 0: empty } + } +} + +impl AsRef<[u8; 33]> for ECDSAPublicKey { + fn as_ref(&self) -> &[u8; 33] { + &self.0 + } +} + +impl AsMut<[u8; 33]> for ECDSAPublicKey { + fn as_mut(&mut self) -> &mut [u8; 33] { + &mut self.0 + } +} + +impl From<[u8; 33]> for ECDSAPublicKey { + fn from(bytes: [u8; 33]) -> Self { + Self { 0: bytes } + } +} + +/// The address of an Ethereum account. +#[derive(Debug, Default, Copy, Clone)] +pub struct EthereumAddress([u8; 20]); + +impl AsRef<[u8; 20]> for EthereumAddress { + fn as_ref(&self) -> &[u8; 20] { + &self.0 + } +} + +impl AsMut<[u8; 20]> for EthereumAddress { + fn as_mut(&mut self) -> &mut [u8; 20] { + &mut self.0 + } +} + +impl From<[u8; 20]> for EthereumAddress { + fn from(bytes: [u8; 20]) -> Self { + Self { 0: bytes } + } +} + +impl ECDSAPublicKey { + /// Returns Ethereum address from the ECDSA compressed public key. + /// + /// # Example + /// + /// ``` + /// use ink_eth_compatibility::{ECDSAPublicKey, EthereumAddress}; + /// let pub_key: ECDSAPublicKey = [ + /// 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, + /// ].into(); + /// + /// let EXPECTED_ETH_ADDRESS: EthereumAddress = [ + /// 126, 95, 69, 82, 9, 26, 105, 18, 93, 93, 252, 183, 184, 194, 101, 144, 41, 57, 91, 223 + /// ].into(); + /// + /// assert_eq!(pub_key.to_eth_address().as_ref(), EXPECTED_ETH_ADDRESS.as_ref()); + /// ``` + pub fn to_eth_address(&self) -> EthereumAddress { + use ink_env::hash; + use secp256k1::PublicKey; + + // Transform compressed public key into uncompressed. + let pub_key = PublicKey::parse_compressed(&self.0) + .expect("Unable to parse the compressed ECDSA public key"); + let uncompressed = pub_key.serialize(); + + // Hash the uncompressed public key by Keccak256 algorithm. + let mut hash = ::Type::default(); + // The first byte indicates that the public key is uncompressed. + // Let's skip it for hashing the public key directly. + ink_env::hash_bytes::(&uncompressed[1..], &mut hash); + + // Take the last 20 bytes as an Address + let mut result = EthereumAddress::default(); + result.as_mut().copy_from_slice(&hash[12..]); + + result + } + + /// Returns the default Substrate's `AccountId` from the ECDSA compressed public key. + /// It hashes the compressed public key with the blake2b256 algorithm like in substrate. + /// + /// # Example + /// + /// ``` + /// use ink_eth_compatibility::ECDSAPublicKey; + /// let pub_key: ECDSAPublicKey = [ + /// 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, + /// ].into(); + /// + /// const EXPECTED_ACCOUNT_ID: [u8; 32] = [ + /// 41, 117, 241, 210, 139, 146, 182, 232, 68, 153, 184, 59, 7, 151, 239, 82, + /// 53, 85, 62, 235, 126, 218, 160, 206, 162, 67, 193, 18, 140, 47, 231, 55, + /// ]; + /// + /// assert_eq!(pub_key.to_default_account_id(), EXPECTED_ACCOUNT_ID.into()); + pub fn to_default_account_id( + &self, + ) -> ::AccountId { + use ink_env::hash; + + let mut output = ::Type::default(); + ink_env::hash_bytes::(&self.0[..], &mut output); + + output.into() + } +} diff --git a/crates/lang/Cargo.toml b/crates/lang/Cargo.toml index 0a91a88fd46..60732596578 100644 --- a/crates/lang/Cargo.toml +++ b/crates/lang/Cargo.toml @@ -20,6 +20,7 @@ ink_storage = { version = "3.0.0-rc5", path = "../storage", default-features = f ink_primitives = { version = "3.0.0-rc5", path = "../primitives", default-features = false } ink_metadata = { version = "3.0.0-rc5", path = "../metadata", default-features = false, optional = true } ink_prelude = { version = "3.0.0-rc5", path = "../prelude", default-features = false } +ink_eth_compatibility = { version = "3.0.0-rc5", path = "../eth_compatibility", default-features = false } ink_lang_macro = { version = "3.0.0-rc5", path = "macro", default-features = false } scale = { package = "parity-scale-codec", version = "2", default-features = false, features = ["derive", "full"] } diff --git a/crates/lang/src/env_access.rs b/crates/lang/src/env_access.rs index 86be0b81e01..79f5b76af65 100644 --- a/crates/lang/src/env_access.rs +++ b/crates/lang/src/env_access.rs @@ -24,6 +24,7 @@ use ink_env::{ HashOutput, }, Environment, + Error, RentParams, RentStatus, Result, @@ -31,6 +32,7 @@ use ink_env::{ use ink_primitives::Key; use crate::ChainExtensionInstance; +use ink_eth_compatibility::ECDSAPublicKey; /// The environment of the compiled ink! smart contract. pub trait ContractEnv { @@ -1022,4 +1024,67 @@ where ink_env::hash_encoded::(value, &mut output); output } + + /// Recovers the compressed ECDSA public key for given `signature` and `message_hash`, + /// and stores the result in `output`. + /// + /// # Example + /// + /// ``` + /// # use ink_lang as ink; + /// # #[ink::contract] + /// # pub mod my_contract { + /// # #[ink(storage)] + /// # pub struct MyContract { } + /// # + /// # impl MyContract { + /// # #[ink(constructor)] + /// # pub fn new() -> Self { + /// # Self {} + /// # } + /// # + /// /// Recovery from pre-defined signature and message hash + /// #[ink(message)] + /// pub fn ecdsa_recover(&self) { + /// const 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, + /// ]; + /// const 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 + /// ]; + /// let 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, + /// ].into(); + /// let result = self.env().ecdsa_recover(&signature, &message_hash); + /// assert!(result.is_ok()); + /// assert_eq!(result.unwrap().as_ref(), EXPECTED_COMPRESSED_PUBLIC_KEY.as_ref()); + /// + /// // Pass invalid zero message hash + /// let failed_result = self.env().ecdsa_recover(&signature, &[0; 32]); + /// assert!(failed_result.is_err()); + /// if let Err(e) = failed_result { + /// assert_eq!(e, ink_env::Error::EcdsaRecoverFailed); + /// } + /// } + /// # + /// # } + /// # } + /// ``` + pub fn ecdsa_recover( + self, + signature: &[u8; 65], + message_hash: &[u8; 32], + ) -> Result { + let mut output = [0; 33]; + ink_env::ecdsa_recover(signature, message_hash, &mut output) + .map(|_| output.into()) + .map_err(|_| Error::EcdsaRecoverFailed) + } }