diff --git a/Cargo.lock b/Cargo.lock index c6ee13301..4fde652ed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1594,14 +1594,44 @@ dependencies = [ [[package]] name = "eip712-account-claim" version = "0.1.0" +dependencies = [ + "eip712-common", + "eip712-common-test-utils", + "hex-literal", +] + +[[package]] +name = "eip712-common" +version = "0.1.0" +dependencies = [ + "hex-literal", + "primitives-ethereum", + "sp-core", + "sp-core-hashing-proc-macro", + "sp-io", + "sp-std", +] + +[[package]] +name = "eip712-common-test-utils" +version = "0.1.0" dependencies = [ "eip-712", + "eip712-common", "hex-literal", + "primitives-ethereum", "secp256k1 0.22.1", "serde_json", "sp-core", - "sp-core-hashing-proc-macro", - "sp-io", +] + +[[package]] +name = "eip712-token-claim" +version = "0.1.0" +dependencies = [ + "eip712-common", + "eip712-common-test-utils", + "hex-literal", ] [[package]] @@ -3204,6 +3234,7 @@ dependencies = [ "chrono", "crypto-utils", "eip712-account-claim", + "eip712-common", "fp-rpc", "fp-self-contained", "frame-benchmarking", @@ -5509,6 +5540,7 @@ dependencies = [ "frame-support", "frame-system", "parity-scale-codec", + "primitives-ethereum", "scale-info", "sp-core", ] @@ -6142,6 +6174,7 @@ dependencies = [ "pallet-evm-accounts-mapping", "parity-scale-codec", "primitive-types", + "primitives-ethereum", "scale-info", "sp-core", "sp-io", diff --git a/crates/eip712-account-claim/Cargo.toml b/crates/eip712-account-claim/Cargo.toml index f4f08eaad..ff2ccafa0 100644 --- a/crates/eip712-account-claim/Cargo.toml +++ b/crates/eip712-account-claim/Cargo.toml @@ -4,16 +4,13 @@ version = "0.1.0" edition = "2021" [dependencies] -sp-core-hashing-proc-macro = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } -sp-io = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } +eip712-common = { version = "0.1", path = "../eip712-common", default-features = false } [dev-dependencies] -eth-eip-712 = { package = "eip-712", version = "=0.1.0" } +eip712-common-test-utils = { version = "0.1", path = "../eip712-common-test-utils" } + hex-literal = "0.3" -secp256k1 = "0.22" -serde_json = "1" -sp-core = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } [features] default = ["std"] -std = ["sp-core/std", "sp-io/std"] +std = ["eip712-common/std"] diff --git a/crates/eip712-account-claim/src/lib.rs b/crates/eip712-account-claim/src/lib.rs index f74a6108e..c29503704 100644 --- a/crates/eip712-account-claim/src/lib.rs +++ b/crates/eip712-account-claim/src/lib.rs @@ -2,65 +2,13 @@ #![cfg_attr(not(feature = "std"), no_std)] -use sp_core_hashing_proc_macro::keccak_256 as const_keccak_256; -use sp_io::hashing::keccak_256; +use eip712_common::{ + const_keccak_256, keccak_256, Domain, EcdsaSignature, EthBytes, EthereumAddress, +}; -/// EIP712Domain typehash. -const EIP712_DOMAIN_TYPEHASH: [u8; 32] = const_keccak_256!( - b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" -); /// Account claim typehash. const ACCOUNT_CLAIM_TYPEHASH: [u8; 32] = const_keccak_256!(b"Claim(bytes substrateAddress)"); -/// A type alias representing a `string` solidity type. -type EthString = str; -/// A type alias representing a `uint256` solidity type. -type EthUint265 = [u8; 32]; -/// A type alias representing an `address` solidity type. -type EthAddress = [u8; 20]; -/// A type alias representing the `bytes` solidity type. -type EthBytes = [u8]; - -/// A first number of an EIP191 message. -const EIP191_MAGIC_BYTE: u8 = 0x19; -/// The EIP191 version for the EIP-712 structured data. -const EIP191_VERSION_STRUCTURED_DATA: u8 = 0x01; - -/// Prepare a hash for the whole EIP-712 message. -fn make_eip712_message_hash(domain_separator: &[u8; 32], payload_hash: &[u8; 32]) -> [u8; 32] { - let mut msg: [u8; 66] = [0; 66]; - msg[0] = EIP191_MAGIC_BYTE; - msg[1] = EIP191_VERSION_STRUCTURED_DATA; - msg[2..34].copy_from_slice(domain_separator); - msg[34..66].copy_from_slice(payload_hash); - keccak_256(&msg) -} - -/// The EIP712 domain. -pub struct Domain<'a> { - /// The name of the domain. - pub name: &'a EthString, - /// The version of the domain. - /// Bump this value if you need to make the old signed messages obsolete. - pub version: &'a EthString, - /// The Chain ID of the Ethereum chain this code runs at. - pub chain_id: &'a EthUint265, - /// The verifying contract, indeteded for the address of the contract that will be verifying - /// the signature. - pub verifying_contract: &'a EthAddress, -} - -/// Prepare a hash for EIP712Domain data type. -fn make_domain_hash(domain: Domain<'_>) -> [u8; 32] { - let mut buf = [0u8; 160]; - buf[0..32].copy_from_slice(&EIP712_DOMAIN_TYPEHASH); - buf[32..64].copy_from_slice(&keccak_256(domain.name.as_bytes())); - buf[64..96].copy_from_slice(&keccak_256(domain.version.as_bytes())); - buf[96..128].copy_from_slice(domain.chain_id); - buf[140..160].copy_from_slice(domain.verifying_contract); - keccak_256(&buf) -} - /// Prepare a hash for our account claim data type. /// To be used at EIP-712 message payload. fn make_account_claim_hash(account: &EthBytes) -> [u8; 32] { @@ -70,63 +18,28 @@ fn make_account_claim_hash(account: &EthBytes) -> [u8; 32] { keccak_256(&buf) } -/// A signature (a 512-bit value, plus 8 bits for recovery ID). -pub type Signature = [u8; 65]; - /// Verify EIP-712 typed signature based on provided domain_separator and entire message. pub fn verify_account_claim( + signature: &EcdsaSignature, domain: Domain<'_>, account: &[u8], - signature: Signature, -) -> Option<[u8; 20]> { - let domain_hash = make_domain_hash(domain); - let account_claim_hash = make_account_claim_hash(account); - let msg_hash = make_eip712_message_hash(&domain_hash, &account_claim_hash); - recover_signer(signature, &msg_hash) -} - -/// Extract the signer address from the signatue and the message. -fn recover_signer(sig: Signature, msg: &[u8; 32]) -> Option<[u8; 20]> { - let pubkey = sp_io::crypto::secp256k1_ecdsa_recover(&sig, msg).ok()?; - Some(ecdsa_public_key_to_evm_address(&pubkey)) -} - -/// Convert the ECDSA public key to EVM address. -fn ecdsa_public_key_to_evm_address(pubkey: &[u8; 64]) -> [u8; 20] { - let mut address = [0u8; 20]; - address.copy_from_slice(&sp_io::hashing::keccak_256(pubkey)[12..]); - address +) -> Option { + let payload_hash = make_account_claim_hash(account); + eip712_common::verify_signature(signature, domain, &payload_hash) } #[cfg(test)] mod tests { - use eth_eip_712::{hash_structured_data, EIP712}; + use eip712_common_test_utils::{ + ecdsa, ecdsa_pair, ecdsa_sign_typed_data, ethereum_address_from_seed, U256, + }; use hex_literal::hex; - use sp_core::{crypto::Pair, ecdsa, U256}; use super::*; - fn ecdsa_pair(seed: &[u8]) -> ecdsa::Pair { - ecdsa::Pair::from_seed(&keccak_256(seed)) - } - - fn ecdsa_sign(pair: &ecdsa::Pair, msg: [u8; 32]) -> Signature { - pair.sign_prehashed(&msg).0 - } - - fn evm_address_from_seed(seed: &[u8]) -> [u8; 20] { - use secp256k1::{PublicKey, Secp256k1, SecretKey}; - let secret = SecretKey::from_slice(&keccak_256(seed)).unwrap(); - let context = Secp256k1::signing_only(); - let public = PublicKey::from_secret_key(&context, &secret); - let mut public_bytes = [0u8; 64]; - public_bytes.copy_from_slice(&public.serialize_uncompressed()[1..]); - ecdsa_public_key_to_evm_address(&public_bytes) - } - // A helper function to construct test EIP-712 signature. - fn test_input(pair: &ecdsa::Pair) -> Signature { - let claim_eip_712_json = r#"{ + fn test_input(pair: &ecdsa::Pair) -> EcdsaSignature { + let typed_data_json = r#"{ "primaryType": "Claim", "domain": { "name": "Humanode EVM Claim", @@ -149,10 +62,7 @@ mod tests { ] } }"#; - let typed_data: EIP712 = serde_json::from_str(claim_eip_712_json).unwrap(); - let msg_bytes: [u8; 32] = hash_structured_data(typed_data).unwrap().into(); - - ecdsa_sign(pair, msg_bytes) + ecdsa_sign_typed_data(pair, typed_data_json) } // A helper function to prepare alice account claim typed data. @@ -166,35 +76,6 @@ mod tests { } } - #[test] - fn verify_test_chain_id() { - let domain = prepare_sample_domain(); - let expected_chain_id: [u8; 32] = U256::from(5234).into(); - assert_eq!(domain.chain_id, &expected_chain_id); - } - - #[test] - fn verify_domain_separator() { - // See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol - - // From https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L101 - let sample_separator: [u8; 32] = - U256::from("0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f").into(); - - // Sample test data - // See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L38-L44 - let verifying_contract: [u8; 20] = hex!("CcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"); - let chain_id: [u8; 32] = U256::from("0x1").into(); - let domain = Domain { - name: "Ether Mail", - version: "1", - chain_id: &chain_id, - verifying_contract: &verifying_contract, - }; - - assert_eq!(make_domain_hash(domain), sample_separator); - } - const SAMPLE_ACCOUNT: [u8; 32] = hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"); @@ -204,8 +85,8 @@ mod tests { let signature = test_input(&pair); let domain = prepare_sample_domain(); - let evm_address = verify_account_claim(domain, &SAMPLE_ACCOUNT, signature).unwrap(); - assert_eq!(evm_address, evm_address_from_seed(b"Alice")); + let ethereum_address = verify_account_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap(); + assert_eq!(ethereum_address, ethereum_address_from_seed(b"Alice")); } #[test] @@ -214,8 +95,8 @@ mod tests { let signature = test_input(&pair); let domain = prepare_sample_domain(); - let evm_address = verify_account_claim(domain, &SAMPLE_ACCOUNT, signature).unwrap(); - assert_ne!(evm_address, evm_address_from_seed(b"Bob")); + let ethereum_address = verify_account_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap(); + assert_ne!(ethereum_address, ethereum_address_from_seed(b"Bob")); } #[test] @@ -231,9 +112,10 @@ mod tests { hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"); let signature = hex!("151d5f52e6c249db84b8705374c6f51dd08b50ddad5b1175ec20a7e00cbc48f55a23470ab0db16146b3b7d2a8565aaf2b700f548c9e9882a0034e654bd214e821b"); - let evm_address = verify_account_claim(domain, &account_to_claim, signature).unwrap(); + let ethereum_address = + verify_account_claim(&EcdsaSignature(signature), domain, &account_to_claim).unwrap(); assert_eq!( - evm_address, + ethereum_address.0, hex!("e9726f3d0a7736034e2a4c63ea28b3ab95622cb9"), ); } diff --git a/crates/eip712-common-test-utils/Cargo.toml b/crates/eip712-common-test-utils/Cargo.toml new file mode 100644 index 000000000..e2a6c506c --- /dev/null +++ b/crates/eip712-common-test-utils/Cargo.toml @@ -0,0 +1,14 @@ +[package] +name = "eip712-common-test-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +eip712-common = { version = "0.1", path = "../eip712-common" } +primitives-ethereum = { version = "0.1", path = "../primitives-ethereum" } + +eth-eip-712 = { package = "eip-712", version = "=0.1.0" } +hex-literal = "0.3" +secp256k1 = "0.22" +serde_json = "1" +sp-core = { git = "https://github.com/humanode-network/substrate", branch = "master" } diff --git a/crates/eip712-common-test-utils/src/lib.rs b/crates/eip712-common-test-utils/src/lib.rs new file mode 100644 index 000000000..78714f576 --- /dev/null +++ b/crates/eip712-common-test-utils/src/lib.rs @@ -0,0 +1,40 @@ +//! Common test utils for EIP-712 typed data message construction and signature verification. + +use eip712_common::*; +use primitives_ethereum::{EcdsaSignature, EthereumAddress}; +pub use sp_core::{crypto::Pair, ecdsa, H256, U256}; + +/// Create a new ECDSA keypair from the seed. +pub fn ecdsa_pair(seed: &[u8]) -> ecdsa::Pair { + ecdsa::Pair::from_seed(&keccak_256(seed)) +} + +/// Sign a given message with the given ECDSA keypair. +pub fn ecdsa_sign(pair: &ecdsa::Pair, msg: [u8; 32]) -> EcdsaSignature { + EcdsaSignature(pair.sign_prehashed(&msg).0) +} + +/// Sign a given EIP-712 typed data JSON with the given ECDSA keypair. +pub fn ecdsa_sign_typed_data(pair: &ecdsa::Pair, type_data_json: &str) -> EcdsaSignature { + let typed_data: eth_eip_712::EIP712 = serde_json::from_str(type_data_json).unwrap(); + let msg_bytes: [u8; 32] = eth_eip_712::hash_structured_data(typed_data) + .unwrap() + .into(); + ecdsa_sign(pair, msg_bytes) +} + +/// Create an Ethereum address from the given seed. +/// +/// This algorithm will return the addresses corresponding to the [`ecdsa::Pair`]s generated +/// by [`ecdsa_pair`] with the same `seed`. +pub fn ethereum_address_from_seed(seed: &[u8]) -> EthereumAddress { + use secp256k1::{PublicKey, Secp256k1, SecretKey}; + let secret = SecretKey::from_slice(&keccak_256(seed)).unwrap(); + let context = Secp256k1::signing_only(); + let public = PublicKey::from_secret_key(&context, &secret); + let mut public_bytes = [0u8; 64]; + public_bytes.copy_from_slice(&public.serialize_uncompressed()[1..]); + let mut address = [0u8; 20]; + address.copy_from_slice(&keccak_256(&public_bytes)[12..]); + EthereumAddress(address) +} diff --git a/crates/eip712-common/Cargo.toml b/crates/eip712-common/Cargo.toml new file mode 100644 index 000000000..ddd7af75b --- /dev/null +++ b/crates/eip712-common/Cargo.toml @@ -0,0 +1,19 @@ +[package] +name = "eip712-common" +version = "0.1.0" +edition = "2021" + +[dependencies] +primitives-ethereum = { version = "0.1", path = "../primitives-ethereum", default-features = false } + +sp-core-hashing-proc-macro = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } +sp-io = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } +sp-std = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } + +[dev-dependencies] +hex-literal = "0.3" +sp-core = { git = "https://github.com/humanode-network/substrate", branch = "master" } + +[features] +default = ["std"] +std = ["primitives-ethereum/std", "sp-io/std", "sp-std/std"] diff --git a/crates/eip712-common/src/lib.rs b/crates/eip712-common/src/lib.rs new file mode 100644 index 000000000..09b5347f3 --- /dev/null +++ b/crates/eip712-common/src/lib.rs @@ -0,0 +1,149 @@ +//! Common logic for EIP-712 typed data message construction and signature verification. + +#![cfg_attr(not(feature = "std"), no_std)] + +pub use primitives_ethereum::{EcdsaSignature, EthereumAddress}; +pub use sp_core_hashing_proc_macro::keccak_256 as const_keccak_256; +pub use sp_io::hashing::keccak_256; + +/// EIP712Domain typehash. +const EIP712_DOMAIN_TYPEHASH: [u8; 32] = const_keccak_256!( + b"EIP712Domain(string name,string version,uint256 chainId,address verifyingContract)" +); + +/// A type alias representing a `string` solidity type. +pub type EthString = str; +/// A type alias representing a `uint256` solidity type. +pub type EthUint265 = [u8; 32]; +/// A type alias representing an `address` solidity type. +pub type EthAddress = [u8; 20]; +/// A type alias representing the `bytes` solidity type. +pub type EthBytes = [u8]; + +/// A first number of an EIP191 message. +const EIP191_MAGIC_BYTE: u8 = 0x19; +/// The EIP191 version for the EIP-712 structured data. +const EIP191_VERSION_STRUCTURED_DATA: u8 = 0x01; + +/// Prepare a hash for the whole EIP-712 message. +fn make_eip712_message_hash(domain_separator: &[u8; 32], payload_hash: &[u8; 32]) -> [u8; 32] { + let mut msg: [u8; 66] = [0; 66]; + msg[0] = EIP191_MAGIC_BYTE; + msg[1] = EIP191_VERSION_STRUCTURED_DATA; + msg[2..34].copy_from_slice(domain_separator); + msg[34..66].copy_from_slice(payload_hash); + keccak_256(&msg) +} + +/// The EIP712 domain. +pub struct Domain<'a> { + /// The name of the domain. + pub name: &'a EthString, + /// The version of the domain. + /// Bump this value if you need to make the old signed messages obsolete. + pub version: &'a EthString, + /// The Chain ID of the Ethereum chain this code runs at. + pub chain_id: &'a EthUint265, + /// The verifying contract, indeteded for the address of the contract that will be verifying + /// the signature. + pub verifying_contract: &'a EthAddress, +} + +/// Prepare a hash for EIP712Domain data type. +fn make_domain_hash(domain: Domain<'_>) -> [u8; 32] { + let mut buf = [0u8; 160]; + buf[0..32].copy_from_slice(&EIP712_DOMAIN_TYPEHASH); + buf[32..64].copy_from_slice(&keccak_256(domain.name.as_bytes())); + buf[64..96].copy_from_slice(&keccak_256(domain.version.as_bytes())); + buf[96..128].copy_from_slice(domain.chain_id); + buf[140..160].copy_from_slice(domain.verifying_contract); + keccak_256(&buf) +} + +/// Prepare a hash for the payload. +/// To be used at EIP-712 message payload. +pub fn make_payload_hash<'a>( + typehash: &[u8; 32], + datahashes: impl IntoIterator, +) -> [u8; 32] { + let datahashes = datahashes.into_iter(); + let (datahashes_size, _) = datahashes.size_hint(); + let mut buf = sp_std::prelude::Vec::with_capacity(32 + datahashes_size * 32); + buf.extend_from_slice(typehash); + for item in datahashes { + buf.extend_from_slice(item); + } + keccak_256(&buf) +} + +/// Verify EIP-712 typed signature based on provided domain and payload hash. +pub fn verify_signature( + signature: &EcdsaSignature, + domain: Domain<'_>, + payload_hash: &[u8; 32], +) -> Option { + let domain_hash = make_domain_hash(domain); + let msg_hash = make_eip712_message_hash(&domain_hash, payload_hash); + recover_signer(signature, &msg_hash) +} + +/// Extract the signer address from the signatue and the message. +fn recover_signer(sig: &EcdsaSignature, msg: &[u8; 32]) -> Option { + let pubkey = sp_io::crypto::secp256k1_ecdsa_recover(&sig.0, msg).ok()?; + Some(ecdsa_public_key_to_ethereum_address(&pubkey)) +} + +/// Convert the ECDSA public key to Ethereum address. +fn ecdsa_public_key_to_ethereum_address(pubkey: &[u8; 64]) -> EthereumAddress { + let mut address = [0u8; 20]; + address.copy_from_slice(&sp_io::hashing::keccak_256(pubkey)[12..]); + EthereumAddress(address) +} + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use sp_core::U256; + + use super::*; + + // A helper function to prepare alice account claim typed data. + fn prepare_sample_domain() -> Domain<'static> { + Domain { + name: "Humanode EVM Claim", + version: "1", + // Chain ID is 5234 in hex. + chain_id: &hex!("0000000000000000000000000000000000000000000000000000000000001472"), + verifying_contract: &hex!("CcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"), + } + } + + #[test] + fn verify_test_chain_id() { + let domain = prepare_sample_domain(); + let expected_chain_id: [u8; 32] = U256::from(5234).into(); + assert_eq!(domain.chain_id, &expected_chain_id); + } + + #[test] + fn verify_domain_separator() { + // See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol + + // From https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L101 + let sample_separator: [u8; 32] = + U256::from("0xf2cee375fa42b42143804025fc449deafd50cc031ca257e0b194a650a912090f").into(); + + // Sample test data + // See https://github.com/ethereum/EIPs/blob/fcaec3dc70e758fe80abd86f0c70bbbedbec6e61/assets/eip-712/Example.sol#L38-L44 + let verifying_contract: [u8; 20] = hex!("CcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"); + let chain_id: [u8; 32] = U256::from("0x1").into(); + let domain = Domain { + name: "Ether Mail", + version: "1", + chain_id: &chain_id, + verifying_contract: &verifying_contract, + }; + + assert_eq!(make_domain_hash(domain), sample_separator); + } +} diff --git a/crates/eip712-token-claim/Cargo.toml b/crates/eip712-token-claim/Cargo.toml new file mode 100644 index 000000000..1cb35d320 --- /dev/null +++ b/crates/eip712-token-claim/Cargo.toml @@ -0,0 +1,16 @@ +[package] +name = "eip712-token-claim" +version = "0.1.0" +edition = "2021" + +[dependencies] +eip712-common = { version = "0.1", path = "../eip712-common", default-features = false } + +[dev-dependencies] +eip712-common-test-utils = { version = "0.1", path = "../eip712-common-test-utils" } + +hex-literal = "0.3" + +[features] +default = ["std"] +std = ["eip712-common/std"] diff --git a/crates/eip712-token-claim/src/lib.rs b/crates/eip712-token-claim/src/lib.rs new file mode 100644 index 000000000..f06e6d4e0 --- /dev/null +++ b/crates/eip712-token-claim/src/lib.rs @@ -0,0 +1,96 @@ +//! Implements EIP-712 typed verification logic for the token claiming. + +#![cfg_attr(not(feature = "std"), no_std)] + +use eip712_common::{ + const_keccak_256, keccak_256, make_payload_hash, Domain, EcdsaSignature, EthBytes, + EthereumAddress, +}; + +/// Token claim typehash. +const TOKEN_CLAIM_TYPEHASH: [u8; 32] = const_keccak_256!(b"TokenClaim(bytes substrateAddress)"); + +/// Make the data hash from the `TokenClaim` payload. +fn hash_token_claim_data(account: &EthBytes) -> [u8; 32] { + keccak_256(account) +} + +/// Verify EIP-712 typed signature based on provided domain_separator and entire message. +pub fn verify_token_claim( + signature: &EcdsaSignature, + domain: Domain<'_>, + account: &[u8], +) -> Option { + let payload_hash = make_payload_hash(&TOKEN_CLAIM_TYPEHASH, [&hash_token_claim_data(account)]); + eip712_common::verify_signature(signature, domain, &payload_hash) +} + +#[cfg(test)] +mod tests { + use eip712_common_test_utils::{ + ecdsa, ecdsa_pair, ecdsa_sign_typed_data, ethereum_address_from_seed, + }; + use hex_literal::hex; + + use super::*; + + fn test_input(pair: &ecdsa::Pair) -> EcdsaSignature { + let type_data_json = r#"{ + "primaryType": "TokenClaim", + "domain": { + "name": "Humanode Token Claim", + "version": "1", + "chainId": "0x1472", + "verifyingContract": "0xCcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC" + }, + "message": { + "substrateAddress": "0xd43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" + }, + "types": { + "EIP712Domain": [ + { "name": "name", "type": "string" }, + { "name": "version", "type": "string" }, + { "name": "chainId", "type": "uint256" }, + { "name": "verifyingContract", "type": "address" } + ], + "TokenClaim": [ + { "name": "substrateAddress", "type": "bytes" } + ] + } + }"#; + ecdsa_sign_typed_data(pair, type_data_json) + } + + fn prepare_sample_domain() -> Domain<'static> { + Domain { + name: "Humanode Token Claim", + version: "1", + // Chain ID is 5234 in hex. + chain_id: &hex!("0000000000000000000000000000000000000000000000000000000000001472"), + verifying_contract: &hex!("CcCCccccCCCCcCCCCCCcCcCccCcCCCcCcccccccC"), + } + } + + const SAMPLE_ACCOUNT: [u8; 32] = + hex!("d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d"); + + #[test] + fn valid_signature() { + let pair = ecdsa_pair(b"Alice"); + let signature = test_input(&pair); + let domain = prepare_sample_domain(); + + let ethereum_address = verify_token_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap(); + assert_eq!(ethereum_address, ethereum_address_from_seed(b"Alice")); + } + + #[test] + fn invalid_signature() { + let pair = ecdsa_pair(b"Alice"); + let signature = test_input(&pair); + let domain = prepare_sample_domain(); + + let ethereum_address = verify_token_claim(&signature, domain, &SAMPLE_ACCOUNT).unwrap(); + assert_ne!(ethereum_address, ethereum_address_from_seed(b"Bob")); + } +} diff --git a/crates/humanode-runtime/Cargo.toml b/crates/humanode-runtime/Cargo.toml index 6ea817eff..efbef57d5 100644 --- a/crates/humanode-runtime/Cargo.toml +++ b/crates/humanode-runtime/Cargo.toml @@ -12,6 +12,7 @@ substrate-wasm-builder = { git = "https://github.com/humanode-network/substrate" author-ext-api = { version = "0.1", path = "../author-ext-api", default-features = false } bioauth-flow-api = { version = "0.1", path = "../bioauth-flow-api", default-features = false } eip712-account-claim = { version = "0.1", path = "../eip712-account-claim", default-features = false } +eip712-common = { version = "0.1", path = "../eip712-common", default-features = false } frontier-api = { version = "0.1", path = "../frontier-api", default-features = false } keystore-bioauth-account-id = { version = "0.1", path = "../keystore-bioauth-account-id", default-features = false } pallet-bioauth = { version = "0.1", path = "../pallet-bioauth", default-features = false } diff --git a/crates/humanode-runtime/src/eip712.rs b/crates/humanode-runtime/src/eip712.rs index 542ff1227..16755c594 100644 --- a/crates/humanode-runtime/src/eip712.rs +++ b/crates/humanode-runtime/src/eip712.rs @@ -1,5 +1,7 @@ //! Various EIP-712 implementations. +use eip712_common::{EcdsaSignature, EthereumAddress}; + use super::*; /// The verifier for the EIP-712 signature of the EVM accout claim message. @@ -8,21 +10,17 @@ pub enum AccountClaimVerifier {} impl pallet_evm_accounts_mapping::SignedClaimVerifier for AccountClaimVerifier { type AccountId = AccountId; - fn verify( - account_id: Self::AccountId, - signature: pallet_evm_accounts_mapping::Secp256k1EcdsaSignature, - ) -> Option { + fn verify(account_id: &Self::AccountId, signature: &EcdsaSignature) -> Option { let chain_id: [u8; 32] = U256::from(EthereumChainId::chain_id()).into(); let genesis_hash: [u8; 32] = System::block_hash(0).into(); let mut verifying_contract = [0u8; 20]; verifying_contract.copy_from_slice(&genesis_hash[0..20]); - let domain = eip712_account_claim::Domain { + let domain = eip712_common::Domain { name: "Humanode EVM Account Claim", version: "1", chain_id: &chain_id, verifying_contract: &verifying_contract, }; - eip712_account_claim::verify_account_claim(domain, account_id.as_ref(), signature) - .map(Into::into) + eip712_account_claim::verify_account_claim(signature, domain, account_id.as_ref()) } } diff --git a/crates/pallet-evm-accounts-mapping/Cargo.toml b/crates/pallet-evm-accounts-mapping/Cargo.toml index d788acb4e..266581a6a 100644 --- a/crates/pallet-evm-accounts-mapping/Cargo.toml +++ b/crates/pallet-evm-accounts-mapping/Cargo.toml @@ -5,6 +5,8 @@ edition = "2021" publish = false [dependencies] +primitives-ethereum = { version = "0.1", path = "../primitives-ethereum", default-features = false } + codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } frame-support = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } frame-system = { default-features = false, git = "https://github.com/humanode-network/substrate", branch = "master" } @@ -13,5 +15,5 @@ sp-core = { default-features = false, git = "https://github.com/humanode-network [features] default = ["std"] -std = ["codec/std", "frame-support/std", "frame-system/std", "scale-info/std", "sp-core/std"] +std = ["primitives-ethereum/std", "codec/std", "frame-support/std", "frame-system/std", "scale-info/std", "sp-core/std"] try-runtime = ["frame-support/try-runtime"] diff --git a/crates/pallet-evm-accounts-mapping/src/lib.rs b/crates/pallet-evm-accounts-mapping/src/lib.rs index ec4e6acd0..9372e2ce3 100644 --- a/crates/pallet-evm-accounts-mapping/src/lib.rs +++ b/crates/pallet-evm-accounts-mapping/src/lib.rs @@ -5,12 +5,7 @@ use frame_support::pallet_prelude::*; use frame_system::pallet_prelude::*; pub use pallet::*; - -/// An EVM address. -pub type EvmAddress = sp_core::H160; - -/// A signature (a 512-bit value, plus 8 bits for recovery ID). -pub type Secp256k1EcdsaSignature = [u8; 65]; +use primitives_ethereum::{EcdsaSignature, EthereumAddress}; /// The verifier for the ethereum signature. pub trait SignedClaimVerifier { @@ -18,16 +13,13 @@ pub trait SignedClaimVerifier { type AccountId; /// Verify the provided `signature` against a message declaring a claim of the provided - /// `account_id`, and extract the signer's EVM address if the verification passes. + /// `account_id`, and extract the signer's Ethereum address if the verification passes. /// /// Typically, the `account_id` would be either the message itself, or be used in one way or /// another within the message to validate the signature against. /// /// This abstraction built with EIP-712 in mind. - fn verify( - account_id: Self::AccountId, - signature: Secp256k1EcdsaSignature, - ) -> Option; + fn verify(account_id: &Self::AccountId, signature: &EcdsaSignature) -> Option; } // We have to temporarily allow some clippy lints. Later on we'll send patches to substrate to @@ -53,8 +45,8 @@ pub mod pallet { ClaimAccount { /// AccountId that does claiming. account_id: T::AccountId, - /// EVM address that is claimed. - evm_address: EvmAddress, + /// Ethereum address that is claimed. + ethereum_address: EthereumAddress, }, } @@ -62,25 +54,25 @@ pub mod pallet { pub enum Error { /// The native address has already been mapped. NativeAddressAlreadyMapped, - /// The EVM address has already been mapped. - EvmAddressAlreadyMapped, + /// The Ethereum address has already been mapped. + EthereumAddressAlreadyMapped, /// Bad ethereum signature. BadEthereumSignature, /// Invalid ethereum signature. InvalidEthereumSignature, } - /// `EvmAddress` -> `AccountId` storage map. + /// `EthereumAddress` -> `AccountId` storage map. #[pallet::storage] #[pallet::getter(fn accounts)] pub type Accounts = - StorageMap<_, Twox64Concat, EvmAddress, T::AccountId, OptionQuery>; + StorageMap<_, Twox64Concat, EthereumAddress, T::AccountId, OptionQuery>; - /// `AccountId` -> `EvmAddress` storage map. + /// `AccountId` -> `EthereumAddress` storage map. #[pallet::storage] - #[pallet::getter(fn evm_addresses)] - pub type EvmAddresses = - StorageMap<_, Twox64Concat, T::AccountId, EvmAddress, OptionQuery>; + #[pallet::getter(fn ethereum_addresses)] + pub type EthereumAddresses = + StorageMap<_, Twox64Concat, T::AccountId, EthereumAddress, OptionQuery>; #[pallet::pallet] pub struct Pallet(_); @@ -88,7 +80,7 @@ pub mod pallet { #[pallet::genesis_config] pub struct GenesisConfig { /// The mappings to set at genesis. - pub mappings: Vec<(T::AccountId, EvmAddress)>, + pub mappings: Vec<(T::AccountId, EthereumAddress)>, } // The default value for the genesis config type. @@ -104,9 +96,9 @@ pub mod pallet { #[pallet::genesis_build] impl GenesisBuild for GenesisConfig { fn build(&self) { - for (account_id, evm_address) in &self.mappings { - Accounts::::insert(evm_address, account_id); - EvmAddresses::::insert(account_id, evm_address); + for (account_id, ethereum_address) in &self.mappings { + Accounts::::insert(ethereum_address, account_id); + EthereumAddresses::::insert(account_id, ethereum_address); } } } @@ -119,38 +111,38 @@ pub mod pallet { #[pallet::weight(10_000)] pub fn claim_account( origin: OriginFor, - // According to the fact that evm address can be extracted from any signature, - // we should clarify that we've got a proper one evm address. + // Due to the fact that ethereum address can be extracted from any signature + // we must ensure that the address we've got matches the requested one. // The address that was used to be claimed. - evm_address: EvmAddress, - signature: Secp256k1EcdsaSignature, + ethereum_address: EthereumAddress, + ecdsa_signature: EcdsaSignature, ) -> DispatchResult { let who = ensure_signed(origin)?; ensure!( - !EvmAddresses::::contains_key(&who), + !EthereumAddresses::::contains_key(&who), Error::::NativeAddressAlreadyMapped ); ensure!( - !Accounts::::contains_key(evm_address), - Error::::EvmAddressAlreadyMapped + !Accounts::::contains_key(ethereum_address), + Error::::EthereumAddressAlreadyMapped ); - let expected_evm_address = T::Verifier::verify(who.clone(), signature) + let expected_ethereum_address = T::Verifier::verify(&who, &ecdsa_signature) .ok_or(Error::::BadEthereumSignature)?; ensure!( - evm_address == expected_evm_address, + ethereum_address == expected_ethereum_address, Error::::InvalidEthereumSignature ); - Accounts::::insert(evm_address, &who); - EvmAddresses::::insert(&who, evm_address); + Accounts::::insert(ethereum_address, &who); + EthereumAddresses::::insert(&who, ethereum_address); Self::deposit_event(Event::ClaimAccount { account_id: who, - evm_address, + ethereum_address, }); Ok(()) diff --git a/crates/precompile-evm-accounts-mapping/Cargo.toml b/crates/precompile-evm-accounts-mapping/Cargo.toml index 7f60366ee..82ff0c244 100644 --- a/crates/precompile-evm-accounts-mapping/Cargo.toml +++ b/crates/precompile-evm-accounts-mapping/Cargo.toml @@ -6,6 +6,7 @@ publish = false [dependencies] pallet-evm-accounts-mapping = { version = "0.1", path = "../pallet-evm-accounts-mapping", default-features = false } +primitives-ethereum = { version = "0.1", path = "../primitives-ethereum", default-features = false } codec = { package = "parity-scale-codec", version = "3.0.0", default-features = false, features = ["derive"] } fp-evm = { git = "https://github.com/humanode-network/frontier", branch = "locked/humanode-2022-06-01", default-features = false } @@ -29,4 +30,5 @@ std = [ "fp-evm/std", "sp-std/std", "pallet-evm-accounts-mapping/std", + "primitives-ethereum/std", ] diff --git a/crates/precompile-evm-accounts-mapping/src/lib.rs b/crates/precompile-evm-accounts-mapping/src/lib.rs index 33f06dd87..540528b56 100644 --- a/crates/precompile-evm-accounts-mapping/src/lib.rs +++ b/crates/precompile-evm-accounts-mapping/src/lib.rs @@ -1,4 +1,4 @@ -//! A precompile to check and return a proper native account for provided evm address. +//! A precompile to check and return a proper native account for provided ethereum address. #![cfg_attr(not(feature = "std"), no_std)] @@ -7,6 +7,7 @@ use fp_evm::{ ExitError, ExitSucceed, Precompile, PrecompileFailure, PrecompileHandle, PrecompileOutput, PrecompileResult, }; +use primitives_ethereum::EthereumAddress; use sp_std::marker::PhantomData; #[cfg(test)] @@ -29,16 +30,16 @@ where fn execute(handle: &mut impl PrecompileHandle) -> PrecompileResult { handle.record_cost(GAS_COST)?; - let evm_address_bytes: [u8; 20] = + let ethereum_address_bytes: [u8; 20] = handle .input() .try_into() .map_err(|_| PrecompileFailure::Error { - exit_status: ExitError::Other("input must be a valid evm address".into()), + exit_status: ExitError::Other("input must be a valid ethereum address".into()), })?; - let evm_address = pallet_evm_accounts_mapping::EvmAddress::from(evm_address_bytes); - let native_account = pallet_evm_accounts_mapping::Accounts::::get(evm_address); + let ethereum_address = EthereumAddress(ethereum_address_bytes); + let native_account = pallet_evm_accounts_mapping::Accounts::::get(ethereum_address); let precompile_output = match native_account { Some(account) => account.encode(), diff --git a/crates/precompile-evm-accounts-mapping/src/mock.rs b/crates/precompile-evm-accounts-mapping/src/mock.rs index a8c00ba1c..011e1d068 100644 --- a/crates/precompile-evm-accounts-mapping/src/mock.rs +++ b/crates/precompile-evm-accounts-mapping/src/mock.rs @@ -3,6 +3,7 @@ use frame_support::traits::{ConstU16, ConstU64}; use frame_system as system; use mockall::predicate::*; use mockall::*; +use primitives_ethereum::{EcdsaSignature, EthereumAddress}; use sp_core::H256; use sp_runtime::{ testing::Header, @@ -66,10 +67,7 @@ pub struct MockVerifier; impl pallet_evm_accounts_mapping::SignedClaimVerifier for MockVerifier { type AccountId = AccountId; - fn verify( - _account_id: AccountId, - _signature: pallet_evm_accounts_mapping::Secp256k1EcdsaSignature, - ) -> Option { + fn verify(_account_id: &AccountId, _signature: &EcdsaSignature) -> Option { panic!("should be unused in tests") } } diff --git a/crates/precompile-evm-accounts-mapping/src/tests.rs b/crates/precompile-evm-accounts-mapping/src/tests.rs index 27a8b8ac7..45502991b 100644 --- a/crates/precompile-evm-accounts-mapping/src/tests.rs +++ b/crates/precompile-evm-accounts-mapping/src/tests.rs @@ -1,8 +1,9 @@ use hex_literal::hex; +use primitives_ethereum::EthereumAddress; use crate::{mock::*, *}; -// This test denies invalid evm address input. +// This test denies invalid ethereum address input. #[test] fn test_error_invalid_input() { new_test_ext().execute_with(|| { @@ -15,32 +16,33 @@ fn test_error_invalid_input() { assert_eq!( err, PrecompileFailure::Error { - exit_status: ExitError::Other("input must be a valid evm address".into()) + exit_status: ExitError::Other("input must be a valid ethereum address".into()) } ); }) } -// This test returns a corresponding native account for provided evm address. +// This test returns a corresponding native account for provided ethereum address. #[test] -fn test_success_mapped_evm_address() { +fn test_success_mapped_ethereum_address() { new_test_ext().execute_with(|| { // Test data. - let evm_address = pallet_evm_accounts_mapping::EvmAddress::from(hex!( - "6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b" - )); + let ethereum_address = EthereumAddress(hex!("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b")); let native_account = ::AccountId::from(hex!( "d43593c715fdd31c61141abd04a99fd6822c8558854ccde39a5684e7a56da27d" )); // Current [`Accounts`] storage map. - pallet_evm_accounts_mapping::Accounts::::insert(evm_address, native_account.clone()); + pallet_evm_accounts_mapping::Accounts::::insert( + ethereum_address, + native_account.clone(), + ); let mut mock_handle = MockPrecompileHandle::new(); mock_handle.expect_record_cost().returning(|_| Ok(())); mock_handle .expect_input() - .return_const(evm_address.as_bytes().to_vec()); + .return_const(ethereum_address.0.to_vec()); let handle = &mut mock_handle as _; let val = crate::EvmAccountsMapping::::execute(handle).unwrap(); @@ -54,20 +56,18 @@ fn test_success_mapped_evm_address() { }) } -// This test returns an empty output for unmapped evm address. +// This test returns an empty output for unmapped ethereum address. #[test] -fn test_success_unmapped_evm_address() { +fn test_success_unmapped_ethereum_address() { new_test_ext().execute_with(|| { // Test data. - let evm_address = pallet_evm_accounts_mapping::EvmAddress::from(hex!( - "6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b" - )); + let ethereum_address = EthereumAddress(hex!("6Be02d1d3665660d22FF9624b7BE0551ee1Ac91b")); let mut mock_handle = MockPrecompileHandle::new(); mock_handle.expect_record_cost().returning(|_| Ok(())); mock_handle .expect_input() - .return_const(evm_address.as_bytes().to_vec()); + .return_const(ethereum_address.0.to_vec()); let handle = &mut mock_handle as _; let val = crate::EvmAccountsMapping::::execute(handle).unwrap();