Unverified Commit 284e4e55 authored by GreenBaneling | Supercolony's avatar GreenBaneling | Supercolony Committed by GitHub
Browse files

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
parent 2f4f4f05
Pipeline #162238 passed with stages
in 51 minutes and 31 seconds
......@@ -5,7 +5,9 @@ AST
BLAKE2
BLAKE2b
DApp
ECDSA
ERC
Ethereum
FFI
Gnosis
GPL
......
......@@ -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 = [
......
......@@ -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`.
......
......@@ -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.
......
......@@ -604,3 +604,39 @@ where
instance.hash_encoded::<H, T>(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<()> {
<EnvInstance as OnInstance>::on_instance(|instance| {
instance.ecdsa_recover(signature, message_hash, output)
})
}
......@@ -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.
......
......@@ -112,6 +112,7 @@ impl From<ext::Error> 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 {
<H as CryptoHash>::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<I, T, E, ErrorCode, F, D>(
&mut self,
func_id: u32,
......
......@@ -195,6 +195,43 @@ impl EnvBackend for EnvInstance {
self.hash_bytes::<H>(&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<I, T, E, ErrorCode, F, D>(
&mut self,
func_id: u32,
......
......@@ -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<u32>,
);
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()
}
......@@ -111,6 +111,7 @@ impl From<ext::Error> 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 {
<H as CryptoHash>::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<I, T, E, ErrorCode, F, D>(
&mut self,
func_id: u32,
......
......@@ -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.
......
[package]
name = "ink_eth_compatibility"
version = "3.0.0-rc5"
authors = ["Parity Technologies <admin@parity.io>"]
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",
]
../../LICENSE
\ No newline at end of file
../../README.md
\ No newline at end of file
// 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 = <hash::Keccak256 as hash::HashOutput>::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::<hash::Keccak256>(&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,
) -> <DefaultEnvironment as Environment>::AccountId {
use ink_env::hash;
let mut output = <hash::Blake2x256 as hash::HashOutput>::Type::default();
ink_env::hash_bytes::<hash::Blake2x256>(&self.0[..], &mut output);
output.into()
}
}
......@@ -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"] }
......
......@@ -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::<H, V>(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<ECDSAPublicKey> {
let mut output = [0; 33];
ink_env::ecdsa_recover(signature, message_hash, &mut output)
.map(|_| output.into())
.map_err(|_| Error::EcdsaRecoverFailed)
}
}
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment