diff --git a/.github/workflows/verify-pr-commit.yml b/.github/workflows/verify-pr-commit.yml index d033524b83..7c9932dde0 100644 --- a/.github/workflows/verify-pr-commit.yml +++ b/.github/workflows/verify-pr-commit.yml @@ -362,6 +362,11 @@ jobs: # with: # path: ${{env.WASM_DIR}}/${{env.BUILT_WASM_FILENAME}} # key: runtimes-${{runner.os}}-${{matrix.network}}-${{github.head_ref}} + - name: Login to DockerHub + uses: docker/login-action@v3 + with: + username: ${{secrets.DOCKERHUB_USERNAME}} + password: ${{secrets.DOCKERHUB_TOKEN}} - name: Build Deterministic WASM id: srtool_build if: steps.cache-wasm.outputs.cache-hit != 'true' diff --git a/Cargo.lock b/Cargo.lock index 0833edfc39..b79199c0f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1514,6 +1514,8 @@ dependencies = [ "frame-support", "frame-system", "impl-serde", + "libsecp256k1", + "log", "numtoa", "parity-scale-codec", "scale-info", @@ -3744,6 +3746,7 @@ dependencies = [ "sp-block-builder", "sp-consensus-aura", "sp-core", + "sp-debug-derive 14.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.13.0)", "sp-genesis-builder", "sp-inherents", "sp-io", @@ -3903,9 +3906,9 @@ dependencies = [ [[package]] name = "futures-channel" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" dependencies = [ "futures-core", "futures-sink", @@ -3913,9 +3916,9 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" [[package]] name = "futures-executor" @@ -3931,9 +3934,9 @@ dependencies = [ [[package]] name = "futures-io" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a44623e20b9681a318efdd71c299b6b222ed6f231972bfe2f224ebad6311f0c1" +checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" [[package]] name = "futures-lite" @@ -3965,9 +3968,9 @@ dependencies = [ [[package]] name = "futures-macro" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" dependencies = [ "proc-macro2", "quote", @@ -3987,15 +3990,15 @@ dependencies = [ [[package]] name = "futures-sink" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" +checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" [[package]] name = "futures-task" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" +checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" [[package]] name = "futures-timer" @@ -4005,9 +4008,9 @@ checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" [[package]] name = "futures-util" -version = "0.3.30" +version = "0.3.31" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" dependencies = [ "futures-channel", "futures-core", diff --git a/Cargo.toml b/Cargo.toml index e9c4ab451a..a589e13568 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -54,6 +54,7 @@ serde = { version = "1.0", default-features = false } serial_test = { version = "0.9.0", default-features = false } base64-url = { version = "3.0.0", default-features = false } p256 = { version = "0.13.2", default-features = false, features = ["ecdsa"] } +libsecp256k1 = { version = "0.7", default-features = false } # substrate pallets pallet-aura = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.13.0", default-features = false } @@ -159,6 +160,7 @@ substrate-test-utils = { git = "https://github.com/paritytech/polkadot-sdk", bra substrate-frame-rpc-system = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.13.0" } substrate-prometheus-endpoint = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.13.0" } +sp-debug-derive = { git = "https://github.com/paritytech/polkadot-sdk", branch = "release-polkadot-v1.13.0", default-features = false } [profile.release] panic = "unwind" diff --git a/common/primitives/Cargo.toml b/common/primitives/Cargo.toml index 94b4cdef51..ce363befaf 100644 --- a/common/primitives/Cargo.toml +++ b/common/primitives/Cargo.toml @@ -30,11 +30,14 @@ sp-std = { workspace = true } numtoa = { workspace = true } sp-externalities = { workspace = true } sp-runtime-interface = { workspace = true } +libsecp256k1 = { workspace = true, features = ["hmac"] } +log = "0.4.22" [features] default = ['std'] runtime-benchmarks = [] std = [ + 'libsecp256k1/std', 'parity-scale-codec/std', 'frame-support/std', 'frame-system/std', @@ -47,4 +50,4 @@ std = [ 'sp-externalities/std', 'sp-runtime-interface/std' ] -test = [] \ No newline at end of file +test = [] diff --git a/common/primitives/src/lib.rs b/common/primitives/src/lib.rs index e232c2e9cc..1db06f6efa 100644 --- a/common/primitives/src/lib.rs +++ b/common/primitives/src/lib.rs @@ -41,3 +41,6 @@ pub mod offchain; #[cfg(feature = "runtime-benchmarks")] /// Benchmarking helper trait pub mod benchmarks; + +/// Signature support for ethereum +pub mod signatures; diff --git a/common/primitives/src/node.rs b/common/primitives/src/node.rs index f137246997..0813b00b16 100644 --- a/common/primitives/src/node.rs +++ b/common/primitives/src/node.rs @@ -6,6 +6,7 @@ pub use sp_runtime::{ }; use sp_std::{boxed::Box, vec::Vec}; +use crate::signatures::UnifiedSignature; use frame_support::dispatch::DispatchResultWithPostInfo; /// Some way of identifying an account on the chain. We intentionally make it equivalent @@ -31,7 +32,7 @@ pub type BlockNumber = u32; pub type Header = generic::Header; /// Alias to 512-bit hash when used in the context of a transaction signature on the chain. -pub type Signature = MultiSignature; +pub type Signature = UnifiedSignature; /// Index of a transaction in the chain. pub type Index = u32; diff --git a/common/primitives/src/signatures.rs b/common/primitives/src/signatures.rs new file mode 100644 index 0000000000..fae08b0565 --- /dev/null +++ b/common/primitives/src/signatures.rs @@ -0,0 +1,382 @@ +#![cfg_attr(not(feature = "std"), no_std)] +#[cfg(feature = "serde")] +use frame_support::{Deserialize, Serialize}; +use frame_support::{ + __private::{codec, RuntimeDebug}, + pallet_prelude::{Decode, Encode, MaxEncodedLen, TypeInfo}, +}; +use scale_info::prelude::format; +use sp_core::{ + crypto, + crypto::{AccountId32, FromEntropy}, + ecdsa, ed25519, + hexdisplay::HexDisplay, + sr25519, ByteArray, H256, +}; +use sp_runtime::{ + traits, + traits::{Lazy, Verify}, + MultiSignature, +}; + +/// Signature verify that can work with any known signature types. +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +#[derive(Eq, PartialEq, Clone, Encode, Decode, MaxEncodedLen, RuntimeDebug, TypeInfo)] +pub enum UnifiedSignature { + /// An Ed25519 signature. + Ed25519(ed25519::Signature), + /// An Sr25519 signature. + Sr25519(sr25519::Signature), + /// An ECDSA/SECP256k1 signature compatible with Ethereum + Ecdsa(ecdsa::Signature), +} + +impl From for UnifiedSignature { + fn from(x: ed25519::Signature) -> Self { + Self::Ed25519(x) + } +} + +impl TryFrom for ed25519::Signature { + type Error = (); + fn try_from(m: UnifiedSignature) -> Result { + if let UnifiedSignature::Ed25519(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +impl From for UnifiedSignature { + fn from(x: sr25519::Signature) -> Self { + Self::Sr25519(x) + } +} + +impl TryFrom for sr25519::Signature { + type Error = (); + fn try_from(m: UnifiedSignature) -> Result { + if let UnifiedSignature::Sr25519(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +impl From for UnifiedSignature { + fn from(x: ecdsa::Signature) -> Self { + Self::Ecdsa(x) + } +} + +impl TryFrom for ecdsa::Signature { + type Error = (); + fn try_from(m: UnifiedSignature) -> Result { + if let UnifiedSignature::Ecdsa(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +impl Verify for UnifiedSignature { + type Signer = UnifiedSigner; + fn verify>(&self, msg: L, signer: &AccountId32) -> bool { + match (self, signer) { + (Self::Ed25519(ref sig), who) => match ed25519::Public::from_slice(who.as_ref()) { + Ok(signer) => sig.verify(msg, &signer), + Err(()) => false, + }, + (Self::Sr25519(ref sig), who) => match sr25519::Public::from_slice(who.as_ref()) { + Ok(signer) => sig.verify(msg, &signer), + Err(()) => false, + }, + (Self::Ecdsa(ref sig), who) => check_ethereum_signature(sig, msg, who), + } + } +} + +/// Public key for any known crypto algorithm. +#[derive(Eq, PartialEq, Ord, PartialOrd, Clone, Encode, Decode, RuntimeDebug, TypeInfo)] +#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))] +pub enum UnifiedSigner { + /// An Ed25519 identity. + Ed25519(ed25519::Public), + /// An Sr25519 identity. + Sr25519(sr25519::Public), + /// An SECP256k1/ECDSA identity (12 bytes of zeros + 20 bytes of ethereum address). + Ecdsa(ecdsa::Public), +} + +impl FromEntropy for UnifiedSigner { + fn from_entropy(input: &mut impl codec::Input) -> Result { + Ok(match input.read_byte()? % 3 { + 0 => Self::Ed25519(FromEntropy::from_entropy(input)?), + 1 => Self::Sr25519(FromEntropy::from_entropy(input)?), + 2.. => Self::Ecdsa(FromEntropy::from_entropy(input)?), + }) + } +} + +/// NOTE: This implementations is required by `SimpleAddressDeterminer`, +/// we convert the hash into some AccountId, it's fine to use any scheme. +impl> crypto::UncheckedFrom for UnifiedSigner { + fn unchecked_from(x: T) -> Self { + ed25519::Public::unchecked_from(x.into()).into() + } +} + +impl AsRef<[u8]> for UnifiedSigner { + fn as_ref(&self) -> &[u8] { + match *self { + Self::Ed25519(ref who) => who.as_ref(), + Self::Sr25519(ref who) => who.as_ref(), + Self::Ecdsa(ref who) => who.as_ref(), + } + } +} + +impl traits::IdentifyAccount for UnifiedSigner { + type AccountId = AccountId32; + fn into_account(self) -> AccountId32 { + match self { + Self::Ed25519(who) => <[u8; 32]>::from(who).into(), + Self::Sr25519(who) => <[u8; 32]>::from(who).into(), + Self::Ecdsa(who) => { + let decompressed_result = libsecp256k1::PublicKey::parse_slice( + who.as_ref(), + Some(libsecp256k1::PublicKeyFormat::Compressed), + ); + match decompressed_result { + Ok(public_key) => { + // calculating ethereum address prefixed with zeros + let decompressed = public_key.serialize(); + let mut hashed = sp_io::hashing::keccak_256(&decompressed[1..65]); + hashed[..12].fill(0); + hashed.into() + }, + Err(_) => { + log::error!("Invalid compressed public key provided"); + AccountId32::new([0u8; 32]) + }, + } + }, + } + } +} + +impl From for UnifiedSigner { + fn from(x: ed25519::Public) -> Self { + Self::Ed25519(x) + } +} + +impl TryFrom for ed25519::Public { + type Error = (); + fn try_from(m: UnifiedSigner) -> Result { + if let UnifiedSigner::Ed25519(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +impl From for UnifiedSigner { + fn from(x: sr25519::Public) -> Self { + Self::Sr25519(x) + } +} + +impl TryFrom for sr25519::Public { + type Error = (); + fn try_from(m: UnifiedSigner) -> Result { + if let UnifiedSigner::Sr25519(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +impl From for UnifiedSigner { + fn from(x: ecdsa::Public) -> Self { + Self::Ecdsa(x) + } +} + +impl TryFrom for ecdsa::Public { + type Error = (); + fn try_from(m: UnifiedSigner) -> Result { + if let UnifiedSigner::Ecdsa(x) = m { + Ok(x) + } else { + Err(()) + } + } +} + +#[cfg(feature = "std")] +impl std::fmt::Display for UnifiedSigner { + fn fmt(&self, fmt: &mut std::fmt::Formatter) -> std::fmt::Result { + match *self { + Self::Ed25519(ref who) => write!(fmt, "ed25519: {}", who), + Self::Sr25519(ref who) => write!(fmt, "sr25519: {}", who), + Self::Ecdsa(ref who) => write!(fmt, "ecdsa: {}", who), + } + } +} + +impl Into for MultiSignature { + fn into(self: MultiSignature) -> UnifiedSignature { + match self { + MultiSignature::Ed25519(who) => UnifiedSignature::Ed25519(who), + MultiSignature::Sr25519(who) => UnifiedSignature::Sr25519(who), + MultiSignature::Ecdsa(who) => UnifiedSignature::Ecdsa(who), + } + } +} + +fn check_secp256k1_signature(signature: &[u8; 65], msg: &[u8; 32], signer: &AccountId32) -> bool { + match sp_io::crypto::secp256k1_ecdsa_recover(signature, &msg) { + Ok(pubkey) => { + let mut hashed = sp_io::hashing::keccak_256(pubkey.as_ref()); + hashed[..12].fill(0); + log::debug!(target:"ETHEREUM", "eth hashed={:?} signer={:?}", + HexDisplay::from(&hashed),HexDisplay::from(>::as_ref(signer)), + ); + &hashed == >::as_ref(signer) + }, + _ => false, + } +} + +fn eth_message_hash(message: &str) -> [u8; 32] { + let prefixed = format!("{}{}{}", "\x19Ethereum Signed Message:\n", message.len(), message); + log::debug!(target:"ETHEREUM", "prefixed {:?}",prefixed); + sp_io::hashing::keccak_256(prefixed.as_bytes()) +} + +fn check_ethereum_signature>( + signature: &ecdsa::Signature, + mut msg: L, + signer: &AccountId32, +) -> bool { + let verify_signature = |signature: &[u8; 65], payload: &[u8; 32], signer: &AccountId32| { + check_secp256k1_signature(signature, payload, signer) + }; + + // signature of ethereum prefixed message eip-191 + let message_prefixed = eth_message_hash(&format!("0x{:?}", HexDisplay::from(&msg.get()))); + if verify_signature(&signature.as_ref(), &message_prefixed, signer) { + return true + } + + // signature of raw payload, compatible with polkadotJs signatures + let hashed = sp_io::hashing::keccak_256(&msg.get()); + if verify_signature(signature.as_ref(), &hashed, signer) { + return true + } + + // frequency wrapped for Metamask compatibility + let frequency_wrapped = + eth_message_hash(&format!("0x{:?}", HexDisplay::from(&msg.get()))); + verify_signature(&signature.as_ref(), &frequency_wrapped, signer) +} + +#[cfg(test)] +mod tests { + use crate::signatures::{UnifiedSignature, UnifiedSigner}; + use impl_serde::serialize::from_hex; + use sp_core::{ecdsa, Pair}; + use sp_runtime::traits::{IdentifyAccount, Verify}; + + #[test] + fn polkadot_ecdsa_should_not_work_due_to_using_wrong_hash() { + let msg = &b"test-message"[..]; + let (pair, _) = ecdsa::Pair::generate(); + + let signature = pair.sign(&msg); + let unified_sig = UnifiedSignature::from(signature); + let unified_signer = UnifiedSigner::from(pair.public()); + assert_eq!(unified_sig.verify(msg, &unified_signer.into_account()), false); + } + + #[test] + fn ethereum_prefixed_eip191_signatures_should_work() { + // payload is random and the signature is generated over that payload by a standard EIP-191 signer + let payload = from_hex("0x0a0300e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e028c7d0a3500000000830000000100000026c1147602cf6557f4e0068a78cd4b22b6f6b03e106d05618cde8537e4ffe454c1f285c69f563934857a63463571d57723fbad6ac7de44611ed674f02c04c2ae00").expect("Should convert"); + let signature_raw = from_hex("0x056ca64d31251a1f20733ce2a741e2963c87a9674a35f8619b6b97210ae8c8b54c2853da1b943dd95ac3b893b37f69ca7dc38c13f8ec92a235b00ae03426505900").expect("Should convert"); + let unified_signature = UnifiedSignature::from(ecdsa::Signature::from_raw( + signature_raw.try_into().expect("should convert"), + )); + + let public_key = ecdsa::Public::from_raw( + from_hex("0x025b107c7f38d5ac7d618e626f9fa57eec683adf373b1352cd20e5e5c684747079") + .expect("should convert") + .try_into() + .expect("invalid size"), + ); + let unified_signer = UnifiedSigner::from(public_key); + assert!(unified_signature.verify(&payload[..], &unified_signer.into_account())); + } + + #[test] + fn ethereum_raw_signatures_should_work() { + // payload is random and the signature is generated over that payload by PolkadotJs and ethereum keypair + let payload = from_hex("0x0a0300e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e028c7d0a3500000000830000000100000026c1147602cf6557f4e0068a78cd4b22b6f6b03e106d05618cde8537e4ffe454b63f7774106903a22684c02eeebe2fdc903ac945bf25962fd9d05e7e0ddfb44f00").expect("Should convert"); + let signature_raw = from_hex("0xd740c8294967b36236c5e05861a55bad75d0866c4a6f63d4918a39769a9582b872299a3411cc0f31b5f631261d669fc21ce427ee23999a91df5f0e74dfbbfc6c00").expect("Should convert"); + let unified_signature = UnifiedSignature::from(ecdsa::Signature::from_raw( + signature_raw.try_into().expect("should convert"), + )); + + let public_key = ecdsa::Public::from_raw( + from_hex("0x025b107c7f38d5ac7d618e626f9fa57eec683adf373b1352cd20e5e5c684747079") + .expect("should convert") + .try_into() + .expect("invalid size"), + ); + let unified_signer = UnifiedSigner::from(public_key); + assert!(unified_signature.verify(&payload[..], &unified_signer.into_account())); + } + + #[test] + fn ethereum_custom_wrapped_signatures_should_work() { + // payload is random and the signature is generated over that payload by Metamask signer + let payload = from_hex("0x0a0300e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e028c7d0a3500000000830000000100000026c1147602cf6557f4e0068a78cd4b22b6f6b03e106d05618cde8537e4ffe4548de1bcb12a1d42e58b218a7abb03cb629111625cf3449640d837c5aa98b87d8e00").expect("Should convert"); + let signature_raw = from_hex("0x9633e747bcd951bdb9d98ff84c65562e1f62bd059c578a942859e1695f2472aa0dbaab48c28f6dbc795baa73c27252d97e8dc2170fd7d69694d5cd1863fb968c00").expect("Should convert"); + let unified_signature = UnifiedSignature::from(ecdsa::Signature::from_raw( + signature_raw.try_into().expect("should convert"), + )); + + let public_key = ecdsa::Public::from_raw( + from_hex("0x025b107c7f38d5ac7d618e626f9fa57eec683adf373b1352cd20e5e5c684747079") + .expect("should convert") + .try_into() + .expect("invalid size"), + ); + let unified_signer = UnifiedSigner::from(public_key); + assert!(unified_signature.verify(&payload[..], &unified_signer.into_account())); + } + + #[test] + fn ethereum_invalid_signatures_should_fail() { + let payload = from_hex("0x0a0300e659a7a1628cdd93febc04a4e0646ea20e9f5f0ce097d9a05290d4a9e054df4e028c7d0a3500000000830000000100000026c1147602cf6557f4e0068a78cd4b22b6f6b03e106d05618cde8537e4ffe4548de1bcb12a1d42e58b218a7abb03cb629111625cf3449640d837c5aa98b87d8e00").expect("Should convert"); + let signature_raw = from_hex("0x9633e747bcd951bdb9d98ff84c65562e1f62bd059c578a942859e1695f2472aa0dbaab48c28f6dbc795baa73c27252d97e8dc2170fd7d69694d5cd1863fb968c01").expect("Should convert"); + let unified_signature = UnifiedSignature::from(ecdsa::Signature::from_raw( + signature_raw.try_into().expect("should convert"), + )); + + let public_key = ecdsa::Public::from_raw( + from_hex("0x025b107c7f38d5ac7d618e626f9fa57eec683adf373b1352cd20e5e5c684747079") + .expect("should convert") + .try_into() + .expect("invalid size"), + ); + let unified_signer = UnifiedSigner::from(public_key); + assert_eq!(unified_signature.verify(&payload[..], &unified_signer.into_account()), false); + } +} diff --git a/deny.toml b/deny.toml index 30bdd8c158..31bde6b494 100644 --- a/deny.toml +++ b/deny.toml @@ -75,6 +75,8 @@ ignore = [ { id = "RUSTSEC-2024-0344", reason = "We are only able to remove this once parity updates its dependencies. Older versions of curve25519-dalek should get replaces with >= 4.1.3" }, { id = "RUSTSEC-2022-0093", reason = "The vulnerable code is not exploitable in Frequency because the signing function is not exposed in a way that allows the use of arbitrary public keys, ensuring protection against the described vulnerability." }, { id = "RUSTSEC-2024-0370", reason = "proc-macro-error is used by a few dependencies, and while unmaintained, is not currently an issue." }, + { id = "RUSTSEC-2024-0388", reason = "This is an inner dependency that would get updated when cumulus-primitives-core v0.7.0 is updated to a newer version"}, + { id = "RUSTSEC-2024-0384", reason = "This is an inner dependency that would get updated when libp2p v0.51.4 and wasm-timer v0.2.5 are updated to a newer version"}, ] # If this is true, then cargo deny will use the git executable to fetch advisory database. # If this is false, then it uses a built-in git library. @@ -103,6 +105,7 @@ allow = [ "OpenSSL", "Unicode-DFS-2016", "Zlib", + "Unicode-3.0", ] # The confidence threshold for detecting a license from license text. # The higher the value, the more closely the license text must be to the diff --git a/e2e/capacity/list_unclaimed_rewards.test.ts b/e2e/capacity/list_unclaimed_rewards.test.ts index 6ed2b1661e..904818e7fc 100644 --- a/e2e/capacity/list_unclaimed_rewards.test.ts +++ b/e2e/capacity/list_unclaimed_rewards.test.ts @@ -12,6 +12,7 @@ import { } from '../scaffolding/helpers'; import { isTestnet } from '../scaffolding/env'; import { KeyringPair } from '@polkadot/keyring/types'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; const fundingSource = getFundingSource('capacity-list-unclaimed-rewards'); @@ -29,7 +30,9 @@ describe('Capacity: list_unclaimed_rewards', function () { it('can be called', async function () { const [_provider, booster] = await setUpForBoosting('booster1', 'provider1'); - const result = await ExtrinsicHelper.apiPromise.call.capacityRuntimeApi.listUnclaimedRewards(booster.address); + const result = await ExtrinsicHelper.apiPromise.call.capacityRuntimeApi.listUnclaimedRewards( + getUnifiedAddress(booster) + ); assert.equal(result.length, 0, `result should have been empty but had ${result.length} items`); }); @@ -46,7 +49,9 @@ describe('Capacity: list_unclaimed_rewards', function () { await ExtrinsicHelper.runToBlock(await getNextRewardEraBlock()); await ExtrinsicHelper.runToBlock(await getNextRewardEraBlock()); - const result = await ExtrinsicHelper.apiPromise.call.capacityRuntimeApi.listUnclaimedRewards(booster.address); + const result = await ExtrinsicHelper.apiPromise.call.capacityRuntimeApi.listUnclaimedRewards( + getUnifiedAddress(booster) + ); assert(result.length >= 4, `Length should be >= 4 but is ${result.length}`); diff --git a/e2e/capacity/staking.test.ts b/e2e/capacity/staking.test.ts index 0c93431af0..a2b13c5ebf 100644 --- a/e2e/capacity/staking.test.ts +++ b/e2e/capacity/staking.test.ts @@ -45,7 +45,7 @@ describe('Capacity Staking Tests', function () { await assert.doesNotReject(stakeToProvider(fundingSource, stakeKeys, stakeProviderId, tokenMinStake)); // Confirm that the tokens were locked in the stakeKeys account using the query API - const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address); + const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); assert.equal( stakedAcctInfo.data.frozen, tokenMinStake, @@ -107,7 +107,7 @@ describe('Capacity Staking Tests', function () { assert.equal(amount, tokenMinStake, 'should return a StakeWithdrawn event with 1M amount'); // Confirm that the tokens were unstaked in the stakeKeys account using the query API - const unStakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address); + const unStakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); assert.equal(unStakedAcctInfo.data.frozen, 0, 'should return an account with 0 frozen balance'); // Confirm that the staked capacity was removed from the stakeProviderId account using the query API @@ -143,7 +143,7 @@ describe('Capacity Staking Tests', function () { trackedFrozenBalance += tokenMinStake; // Confirm that the tokens were staked in the stakeKeys account using the query API - const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address); + const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); const increasedFrozen: bigint = stakedAcctInfo.data.frozen.toBigInt(); @@ -233,7 +233,7 @@ describe('Capacity Staking Tests', function () { ); // Confirm that the tokens were not staked in the stakeKeys account using the query API - const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(additionalKeys.address); + const stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(additionalKeys); const increasedFrozen: bigint = stakedAcctInfo.data.frozen.toBigInt(); diff --git a/e2e/capacity/transactions.test.ts b/e2e/capacity/transactions.test.ts index 929715295d..1498e8e89b 100644 --- a/e2e/capacity/transactions.test.ts +++ b/e2e/capacity/transactions.test.ts @@ -537,10 +537,10 @@ describe('Capacity Transactions', function () { it('successfully pays with Capacity for eligible transaction - claimHandle [available balance < ED]', async function () { await assert.doesNotReject(stakeToProvider(fundingSource, capacityKeys, capacityProvider, amountStaked)); // Empty the account to ensure the balance is less than ED - await ExtrinsicHelper.emptyAccount(capacityKeys, fundingSource.address).signAndSend(); + await ExtrinsicHelper.emptyAccount(capacityKeys, fundingSource).signAndSend(); // Confirm that the available balance is less than ED // The available balance is the free balance minus the frozen balance - const capacityAcctInfo = await ExtrinsicHelper.getAccountInfo(capacityKeys.address); + const capacityAcctInfo = await ExtrinsicHelper.getAccountInfo(capacityKeys); assert.equal(capacityAcctInfo.data.frozen.toBigInt(), amountStaked); assert.equal(capacityAcctInfo.data.free.toBigInt(), amountStaked); diff --git a/e2e/load-tests/signatureRegistry.test.ts b/e2e/load-tests/signatureRegistry.test.ts index 41cdbac247..ee2547edd2 100644 --- a/e2e/load-tests/signatureRegistry.test.ts +++ b/e2e/load-tests/signatureRegistry.test.ts @@ -13,6 +13,7 @@ import { KeyringPair } from '@polkadot/keyring/types'; import { AddKeyData, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { u64, Option } from '@polkadot/types'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; interface GeneratedMsa { id: u64; @@ -73,12 +74,15 @@ async function checkKeys(startingNumber: number, keysToTest: KeyringPair[]) { let msaOption = await getMsaFromKey(key); if (!msaOption.isSome) { console.log( - `The ${startingNumber + i} key (${key.address}) failed to be associated with an MSA. Trying another block...` + `The ${startingNumber + i} key (${getUnifiedAddress(key)}) failed to be associated with an MSA. Trying another block...` ); await createBlock(); msaOption = await getMsaFromKey(key); } - assert(msaOption.isSome, `The ${startingNumber + i} key (${key.address}) failed to be associated with an MSA.`); + assert( + msaOption.isSome, + `The ${startingNumber + i} key (${getUnifiedAddress(key)}) failed to be associated with an MSA.` + ); } } @@ -129,7 +133,7 @@ async function createBlock(wait: number = 300) { } function getMsaFromKey(keys: KeyringPair): Promise> { - return ExtrinsicHelper.apiPromise.query.msa.publicKeyToMsaId(keys.address); + return ExtrinsicHelper.apiPromise.query.msa.publicKeyToMsaId(getUnifiedAddress(keys)); } async function createMsa(keys: KeyringPair): Promise { diff --git a/e2e/miscellaneous/frequency.test.ts b/e2e/miscellaneous/frequency.test.ts index bf89a161b4..d0848032dc 100644 --- a/e2e/miscellaneous/frequency.test.ts +++ b/e2e/miscellaneous/frequency.test.ts @@ -6,6 +6,7 @@ import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; import { u8, Option } from '@polkadot/types'; import { u8aToHex } from '@polkadot/util/u8a/toHex'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; const fundingSource: KeyringPair = getFundingSource('frequency-misc'); @@ -26,7 +27,7 @@ describe('Frequency', function () { const beforeBlockNumber = await getBlockNumber(); const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairB.address, 1n * DOLLARS), + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(keypairB), 1n * DOLLARS), keypairA, ExtrinsicHelper.api.events.balances.Transfer ); @@ -56,7 +57,7 @@ describe('Frequency', function () { const nonce = await getNonce(keypairB); for (let i = 0; i < 10; i += 2) { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairA.address, 1n * DOLLARS), + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(keypairA), 1n * DOLLARS), keypairB, ExtrinsicHelper.api.events.balances.Transfer ); @@ -71,7 +72,7 @@ describe('Frequency', function () { // applying the missing nonce values to next transactions to unblock the stuck ones for (const missing of missingNonce) { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(keypairA.address, 1n * DOLLARS), + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(keypairA), 1n * DOLLARS), keypairB, ExtrinsicHelper.api.events.balances.Transfer ); diff --git a/e2e/miscellaneous/utilityBatch.test.ts b/e2e/miscellaneous/utilityBatch.test.ts index b4f06e8f14..2a8cf6f32d 100644 --- a/e2e/miscellaneous/utilityBatch.test.ts +++ b/e2e/miscellaneous/utilityBatch.test.ts @@ -4,6 +4,7 @@ import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { DOLLARS, createAndFundKeypair } from '../scaffolding/helpers'; import { ApiTypes, SubmittableExtrinsic } from '@polkadot/api/types'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; const fundingSource = getFundingSource('misc-util-batch'); @@ -19,7 +20,7 @@ describe('Utility Batch Filtering', function () { it('should successfully execute ✅ batch with allowed calls', async function () { // good batch: with only allowed calls const goodBatch: SubmittableExtrinsic[] = []; - goodBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(recipient.address, 1000)); + goodBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(recipient), 1000)); goodBatch.push(ExtrinsicHelper.api.tx.system.remark('Hello From Batch')); goodBatch.push(ExtrinsicHelper.api.tx.msa.create()); const batch = ExtrinsicHelper.executeUtilityBatchAll(sender, goodBatch); @@ -32,7 +33,7 @@ describe('Utility Batch Filtering', function () { // bad batch: with a mix of allowed and disallowed calls const badBatch: SubmittableExtrinsic[] = []; //allowed - badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(recipient.address, 1000)); + badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(recipient), 1000)); badBatch.push(ExtrinsicHelper.api.tx.system.remark('Hello From Batch')); // not allowed badBatch.push(ExtrinsicHelper.api.tx.handles.retireHandle()); @@ -51,7 +52,7 @@ describe('Utility Batch Filtering', function () { it('should fail to execute ❌ batch with disallowed calls', async function () { // bad batch: with a mix of allowed and disallowed calls const badBatch: SubmittableExtrinsic[] = []; - badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(recipient.address, 1000)); + badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(recipient), 1000)); badBatch.push(ExtrinsicHelper.api.tx.system.remark('Hello From Batch')); badBatch.push(ExtrinsicHelper.api.tx.handles.retireHandle()); badBatch.push(ExtrinsicHelper.api.tx.msa.retireMsa()); @@ -67,7 +68,7 @@ describe('Utility Batch Filtering', function () { it('should fail to execute ❌ forceBatch with disallowed calls', async function () { // bad batch: with a mix of allowed and disallowed calls const badBatch: SubmittableExtrinsic[] = []; - badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(recipient.address, 1000)); + badBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(recipient), 1000)); badBatch.push(ExtrinsicHelper.api.tx.system.remark('Hello From Batch')); badBatch.push(ExtrinsicHelper.api.tx.handles.retireHandle()); badBatch.push(ExtrinsicHelper.api.tx.msa.retireMsa()); @@ -127,7 +128,7 @@ describe('Utility Batch Filtering', function () { // batch with nested batch const nestedBatch: SubmittableExtrinsic[] = []; const innerBatch: SubmittableExtrinsic[] = []; - innerBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(recipient.address, 1000)); + innerBatch.push(ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(recipient), 1000)); innerBatch.push(ExtrinsicHelper.api.tx.system.remark('Hello From Batch')); nestedBatch.push(ExtrinsicHelper.api.tx.utility.batch(innerBatch)); const batch = ExtrinsicHelper.executeUtilityBatchAll(sender, nestedBatch); diff --git a/e2e/package-lock.json b/e2e/package-lock.json index 00b2a0789e..dc4ccab7cd 100644 --- a/e2e/package-lock.json +++ b/e2e/package-lock.json @@ -2197,7 +2197,7 @@ "node_modules/@frequency-chain/api-augment": { "version": "0.0.0", "resolved": "file:../js/api-augment/dist/frequency-chain-api-augment-0.0.0.tgz", - "integrity": "sha512-9NlVmYzNNFWJJOIVtiXXe+5MK235syUMFPia+tEZDJXiLh2A8yNfjFFrNUVj+nDqmUrPBztGcoRHviSUxOtZ6w==", + "integrity": "sha512-97GotBBJyin/o81PafQguWvOalvSND4jbx/tx12lf/hMNNswpaNQEClGosMJUxXdCXUCBkptntNsSk/NDi2RyQ==", "license": "Apache-2.0", "dependencies": { "@polkadot/api": "^14.3.1", diff --git a/e2e/passkey/passkeyProxy.ethereum.test.ts b/e2e/passkey/passkeyProxy.ethereum.test.ts new file mode 100644 index 0000000000..2aa71b72e6 --- /dev/null +++ b/e2e/passkey/passkeyProxy.ethereum.test.ts @@ -0,0 +1,77 @@ +import '@frequency-chain/api-augment'; +import assert from 'assert'; +import { + createAndFundKeypair, + EcdsaSignature, + getBlockNumber, + getNextEpochBlock, + getNonce, + Sr25519Signature, +} from '../scaffolding/helpers'; +import { KeyringPair } from '@polkadot/keyring/types'; +import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; +import { getFundingSource } from '../scaffolding/funding'; +import { getConvertedEthereumPublicKey, getUnifiedAddress } from '../scaffolding/ethereum'; +import { createPassKeyAndSignAccount, createPassKeyCall, createPasskeyPayload } from '../scaffolding/P256'; +import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; +const fundingSource = getFundingSource('passkey-proxy-ethereum'); + +describe('Passkey Pallet Ethereum Tests', function () { + describe('passkey ethereum tests', function () { + let fundedSr25519Keys: KeyringPair; + let fundedEthereumKeys: KeyringPair; + let receiverKeys: KeyringPair; + + before(async function () { + fundedSr25519Keys = await createAndFundKeypair(fundingSource, 300_000_000n); + fundedEthereumKeys = await createAndFundKeypair(fundingSource, 300_000_000n, undefined, undefined, 'ethereum'); + receiverKeys = await createAndFundKeypair(fundingSource, undefined, undefined, undefined, 'ethereum'); + }); + + it('should transfer via passkeys with root sr25519 key into an ethereum style account', async function () { + const initialReceiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys); + const accountPKey = fundedSr25519Keys.publicKey; + const nonce = await getNonce(fundedSr25519Keys); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( + getUnifiedAddress(receiverKeys), + 55_000_000n + ); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); + const accountSignature = fundedSr25519Keys.sign(u8aWrapBytes(passKeyPublicKey)); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, transferCalls); + const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, false); + const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundedSr25519Keys, passkeyPayload); + await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); + // adding some delay before fetching the nonce to ensure it is updated + await new Promise((resolve) => setTimeout(resolve, 1000)); + const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedSr25519Keys)).nonce.toNumber(); + assert.equal(nonce + 1, nonceAfter); + }); + + it('should transfer via passkeys with root ethereum style key into another one', async function () { + const accountPKey = getConvertedEthereumPublicKey(fundedEthereumKeys); + console.log(`accountPKey ${u8aToHex(accountPKey)}`); + const nonce = await getNonce(fundedEthereumKeys); + const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive( + getUnifiedAddress(receiverKeys), + 66_000_000n + ); + const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); + // ethereum keys should not have wrapping + const accountSignature = fundedEthereumKeys.sign(passKeyPublicKey); + console.log(`accountSignature ${u8aToHex(accountSignature)}`); + const multiSignature: EcdsaSignature = { Ecdsa: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, transferCalls); + const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, false); + const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundingSource, passkeyPayload); + await assert.doesNotReject(passkeyProxy.sendUnsigned()); + await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); + // adding some delay before fetching the nonce to ensure it is updated + await new Promise((resolve) => setTimeout(resolve, 1000)); + const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedEthereumKeys)).nonce.toNumber(); + assert.equal(nonce + 1, nonceAfter); + }); + }); +}); diff --git a/e2e/passkey/passkeyProxy.test.ts b/e2e/passkey/passkeyProxy.test.ts index 8ce4236217..4b59e4e807 100644 --- a/e2e/passkey/passkeyProxy.test.ts +++ b/e2e/passkey/passkeyProxy.test.ts @@ -1,10 +1,16 @@ import '@frequency-chain/api-augment'; import assert from 'assert'; -import { createAndFundKeypair, getBlockNumber, getNextEpochBlock, getNonce } from '../scaffolding/helpers'; +import { + createAndFundKeypair, + getBlockNumber, + getNextEpochBlock, + getNonce, + Sr25519Signature, +} from '../scaffolding/helpers'; import { KeyringPair } from '@polkadot/keyring/types'; import { ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; -import { u8aWrapBytes } from '@polkadot/util'; +import { u8aToHex, u8aWrapBytes } from '@polkadot/util'; import { createPassKeyAndSignAccount, createPassKeyCall, createPasskeyPayload } from '../scaffolding/P256'; const fundingSource = getFundingSource('passkey-proxy'); @@ -25,11 +31,12 @@ describe('Passkey Pallet Tests', function () { const remarksCalls = ExtrinsicHelper.api.tx.system.remark('passkey-test'); const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); - const passkeyCall = await createPassKeyCall(accountPKey, nonce, accountSignature, remarksCalls); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, remarksCalls); const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, false); const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundedKeys, passkeyPayload); - assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); }); it('should fail to transfer balance due to bad account ownership proof', async function () { @@ -38,11 +45,12 @@ describe('Passkey Pallet Tests', function () { const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 0n); const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign('badPasskeyPublicKey'); - const passkeyCall = await createPassKeyCall(accountPKey, nonce, accountSignature, transferCalls); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, transferCalls); const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, false); const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundedKeys, passkeyPayload); - assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); }); it('should fail to transfer balance due to bad passkey signature', async function () { @@ -51,11 +59,12 @@ describe('Passkey Pallet Tests', function () { const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 0n); const { passKeyPrivateKey, passKeyPublicKey, passkeySignature } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); - const passkeyCall = await createPassKeyCall(accountPKey, nonce, accountSignature, transferCalls); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, transferCalls); const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, true); const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundedKeys, passkeyPayload); - assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.rejects(passkeyProxy.fundAndSendUnsigned(fundingSource)); }); it('should transfer small balance from fundedKeys to receiverKeys', async function () { @@ -64,15 +73,16 @@ describe('Passkey Pallet Tests', function () { const transferCalls = ExtrinsicHelper.api.tx.balances.transferKeepAlive(receiverKeys.publicKey, 100_000_000n); const { passKeyPrivateKey, passKeyPublicKey } = createPassKeyAndSignAccount(accountPKey); const accountSignature = fundedKeys.sign(u8aWrapBytes(passKeyPublicKey)); - const passkeyCall = await createPassKeyCall(accountPKey, nonce, accountSignature, transferCalls); + const multiSignature: Sr25519Signature = { Sr25519: u8aToHex(accountSignature) }; + const passkeyCall = await createPassKeyCall(accountPKey, nonce, multiSignature, transferCalls); const passkeyPayload = await createPasskeyPayload(passKeyPrivateKey, passKeyPublicKey, passkeyCall, false); const passkeyProxy = ExtrinsicHelper.executePassKeyProxy(fundedKeys, passkeyPayload); - assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource)); + await assert.doesNotReject(passkeyProxy.fundAndSendUnsigned(fundingSource)); await ExtrinsicHelper.waitForFinalization((await getBlockNumber()) + 2); - const receiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys.address); + const receiverBalance = await ExtrinsicHelper.getAccountInfo(receiverKeys); // adding some delay before fetching the nonce to ensure it is updated await new Promise((resolve) => setTimeout(resolve, 2000)); - const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys.address)).nonce.toNumber(); + const nonceAfter = (await ExtrinsicHelper.getAccountInfo(fundedKeys)).nonce.toNumber(); assert.equal(nonce + 1, nonceAfter); assert(receiverBalance.data.free.toBigInt() > 0n); }); diff --git a/e2e/proxy-pallet/proxy.test.ts b/e2e/proxy-pallet/proxy.test.ts index c052f7d28a..d30f26d11c 100644 --- a/e2e/proxy-pallet/proxy.test.ts +++ b/e2e/proxy-pallet/proxy.test.ts @@ -4,6 +4,7 @@ import { createAndFundKeypair } from '../scaffolding/helpers'; import { KeyringPair } from '@polkadot/keyring/types'; import { Extrinsic, ExtrinsicHelper } from '../scaffolding/extrinsicHelpers'; import { getFundingSource } from '../scaffolding/funding'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; const DOLLARS = 100000000n; // 100_000_000 @@ -21,7 +22,7 @@ describe('Proxy', function () { it('Creates a Proxy', async function () { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.proxy.addProxy(proxyKeys.address, 'Any', 0), + () => ExtrinsicHelper.api.tx.proxy.addProxy(getUnifiedAddress(proxyKeys), 'Any', 0), stashKeys, ExtrinsicHelper.api.events.proxy.ProxyAdded ); @@ -34,9 +35,9 @@ describe('Proxy', function () { const extrinsic = new Extrinsic( () => ExtrinsicHelper.api.tx.proxy.proxy( - stashKeys.address, + getUnifiedAddress(stashKeys), 'Any', - ExtrinsicHelper.api.tx.balances.transferAllowDeath(proxyKeys.address, 1n * DOLLARS) + ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(proxyKeys), 1n * DOLLARS) ), proxyKeys, ExtrinsicHelper.api.events.balances.Transfer @@ -48,7 +49,7 @@ describe('Proxy', function () { it('Can remove the proxy', async function () { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.proxy.removeProxy(proxyKeys.address, 'Any', 0), + () => ExtrinsicHelper.api.tx.proxy.removeProxy(getUnifiedAddress(proxyKeys), 'Any', 0), stashKeys, ExtrinsicHelper.api.events.proxy.ProxyRemoved ); @@ -69,7 +70,7 @@ describe('Proxy', function () { it('Creates a Proxy', async function () { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.proxy.addProxy(proxyKeys.address, 'NonTransfer', 0), + () => ExtrinsicHelper.api.tx.proxy.addProxy(getUnifiedAddress(proxyKeys), 'NonTransfer', 0), stashKeys, ExtrinsicHelper.api.events.proxy.ProxyAdded ); @@ -82,9 +83,9 @@ describe('Proxy', function () { const extrinsic = new Extrinsic( () => ExtrinsicHelper.api.tx.proxy.proxy( - stashKeys.address, + getUnifiedAddress(stashKeys), 'Any', - ExtrinsicHelper.api.tx.balances.transferAllowDeath(proxyKeys.address, 1n * DOLLARS) + ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(proxyKeys), 1n * DOLLARS) ), proxyKeys, ExtrinsicHelper.api.events.system.ExtrinsicFailed @@ -100,10 +101,10 @@ describe('Proxy', function () { const extrinsic = new Extrinsic( () => ExtrinsicHelper.api.tx.proxy.proxy( - stashKeys.address, + getUnifiedAddress(stashKeys), 'Any', ExtrinsicHelper.api.tx.utility.batch([ - ExtrinsicHelper.api.tx.balances.transferAllowDeath(proxyKeys.address, 1n * DOLLARS), + ExtrinsicHelper.api.tx.balances.transferAllowDeath(getUnifiedAddress(proxyKeys), 1n * DOLLARS), ]) ), proxyKeys, @@ -118,7 +119,7 @@ describe('Proxy', function () { it('Can remove the proxy', async function () { const extrinsic = new Extrinsic( - () => ExtrinsicHelper.api.tx.proxy.removeProxy(proxyKeys.address, 'NonTransfer', 0), + () => ExtrinsicHelper.api.tx.proxy.removeProxy(getUnifiedAddress(proxyKeys), 'NonTransfer', 0), stashKeys, ExtrinsicHelper.api.events.proxy.ProxyRemoved ); diff --git a/e2e/scaffolding/P256.ts b/e2e/scaffolding/P256.ts index 7c97794f93..25aa16ce69 100644 --- a/e2e/scaffolding/P256.ts +++ b/e2e/scaffolding/P256.ts @@ -1,5 +1,5 @@ import { SubmittableExtrinsic } from '@polkadot/api/types'; -import { base64UrlToUint8Array } from './helpers'; +import { base64UrlToUint8Array, Sr25519Signature, Ed25519Signature, EcdsaSignature } from './helpers'; import { secp256r1 } from '@noble/curves/p256'; import { ISubmittableResult } from '@polkadot/types/types'; import { u8aWrapBytes } from '@polkadot/util'; @@ -16,16 +16,14 @@ export function createPassKeyAndSignAccount(accountPKey: Uint8Array) { export async function createPassKeyCall( accountPKey: Uint8Array, nonce: number, - accountSignature: Uint8Array, + accountSignature: Sr25519Signature | Ed25519Signature | EcdsaSignature, call: SubmittableExtrinsic<'rxjs', ISubmittableResult> ) { const ext_call_type = ExtrinsicHelper.api.registry.createType('Call', call); const passkeyCall = { accountId: accountPKey, accountNonce: nonce, - accountOwnershipProof: { - Sr25519: accountSignature, - }, + accountOwnershipProof: accountSignature, call: ext_call_type, }; diff --git a/e2e/scaffolding/autoNonce.ts b/e2e/scaffolding/autoNonce.ts index 562a7988f5..55fc7a1e8d 100644 --- a/e2e/scaffolding/autoNonce.ts +++ b/e2e/scaffolding/autoNonce.ts @@ -7,26 +7,27 @@ import type { KeyringPair } from '@polkadot/keyring/types'; import { ExtrinsicHelper } from './extrinsicHelpers'; +import { getUnifiedAddress } from './ethereum'; export type AutoNonce = number | 'auto' | 'current'; const nonceCache = new Map(); const getNonce = async (keys: KeyringPair) => { - return (await ExtrinsicHelper.getAccountInfo(keys.address)).nonce.toNumber(); + return (await ExtrinsicHelper.getAccountInfo(keys)).nonce.toNumber(); }; const reset = (keys: KeyringPair) => { - nonceCache.delete(keys.address); + nonceCache.delete(getUnifiedAddress(keys)); }; const current = async (keys: KeyringPair): Promise => { - return nonceCache.get(keys.address) || (await getNonce(keys)); + return nonceCache.get(getUnifiedAddress(keys)) || (await getNonce(keys)); }; const increment = async (keys: KeyringPair) => { const nonce = await current(keys); - nonceCache.set(keys.address, nonce + 1); + nonceCache.set(getUnifiedAddress(keys), nonce + 1); return nonce; }; @@ -46,7 +47,7 @@ const auto = (keys: KeyringPair, inputNonce: AutoNonce = 'auto'): Promise => { + const sig = ethereumPair.sign(prefixEthereumTags(payload.data)); + const prefixedSignature = new Uint8Array(sig.length + 1); + prefixedSignature[0] = 2; + prefixedSignature.set(sig, 1); + const hex = u8aToHex(prefixedSignature); + return { + signature: hex, + } as SignerResult; + }, + }; +} + +/** + * This is a helper method to allow being able to create a signature that might be created by metamask + * @param hexPayload + */ +function wrapCustomFrequencyTag(hexPayload: string): Uint8Array { + // wrapping in frequency tags to show this is a Frequency related payload + const frequencyWrapped = `${hexPayload.toLowerCase()}`; + return prefixEthereumTags(frequencyWrapped); +} + +/** + * prefixing with the EIP-191 for personal_sign messages (this gets wrapped automatically in metamask) + * @param hexPayload + */ +function prefixEthereumTags(hexPayload: string): Uint8Array { + const wrapped = `\x19Ethereum Signed Message:\n${hexPayload.length}${hexPayload}`; + const buffer = Buffer.from(wrapped, 'utf-8'); + return new Uint8Array(buffer.buffer, buffer.byteOffset, buffer.length); +} + +export function getAccountId20MultiAddress(pair: KeyringPair): Address20MultiAddress { + const etheAddress = ethereumEncode(pair.publicKey); + const ethAddress20 = Array.from(hexToU8a(etheAddress)); + return { Address20: ethAddress20 }; +} + +/** + * Returns ethereum style public key with prefixed zeros example: 0x00000000000000000000000019a701d23f0ee1748b5d5f883cb833943096c6c4 + * @param pair + */ +export function getConvertedEthereumPublicKey(pair: KeyringPair): Uint8Array { + const publicKeyBytes = hexToU8a(ethereumEncode(pair.publicKey)); + const result = new Uint8Array(32); + result.fill(0, 0, 12); + result.set(publicKeyBytes, 12); + return result; +} + +/** + * converts an ethereum account to SS58 format + * @param accountId20Hex + */ +function getConvertedEthereumAccount(accountId20Hex: string): string { + const addressBytes = hexToU8a(accountId20Hex); + const result = new Uint8Array(32); + result.fill(0, 0, 12); + result.set(addressBytes, 12); + return encodeAddress(result); +} + +/** + * + * @param secretKey of secp256k1 keypair exported from any wallet (should be 32 bytes) + */ +export function getKeyringPairFromSecp256k1PrivateKey(secretKey: Uint8Array): KeyringPair { + const publicKey = secp256k1.getPublicKey(secretKey, true); + const keypair: Keypair = { + secretKey, + publicKey, + }; + const keyring = new Keyring({ type: 'ethereum' }); + return keyring.addFromPair(keypair, undefined, 'ethereum'); +} diff --git a/e2e/scaffolding/extrinsicHelpers.ts b/e2e/scaffolding/extrinsicHelpers.ts index ad003c670c..aff5840827 100644 --- a/e2e/scaffolding/extrinsicHelpers.ts +++ b/e2e/scaffolding/extrinsicHelpers.ts @@ -24,6 +24,7 @@ import { u8aToHex } from '@polkadot/util/u8a/toHex'; import { u8aWrapBytes } from '@polkadot/util'; import type { AccountId32, Call, H256 } from '@polkadot/types/interfaces/runtime'; import { hasRelayChain } from './env'; +import { getUnifiedAddress } from './ethereum'; export interface ReleaseSchedule { start: number; @@ -246,7 +247,7 @@ export class Extrinsic freeBalance) { @@ -256,17 +257,42 @@ export class Extrinsic { + // If we learn a transaction has an error status (this does NOT include RPC errors) + // Then throw an error + if (result.isError) { + throw new CallError(result, `Failed Transaction for ${this.event?.meta.name || 'unknown'}`); + } + }), + filter(({ status }) => status.isInBlock || status.isFinalized), + this.parseResult(this.event) + ) + ); + } catch (e) { + if ((e as any).name === 'RpcError') { + console.error("WARNING: Unexpected RPC Error! If it is expected, use 'current' for the nonce."); + } + throw e; + } + } + + public async sendUnsigned() { const op = this.extrinsic(); try { return await firstValueFrom(op.send().pipe(this.parseResult(this.event))); } catch (e) { + console.error(e); if ((e as any).name === 'RpcError') { console.error("WARNING: Unexpected RPC Error! If it is expected, use 'current' for the nonce."); } @@ -336,8 +362,8 @@ export class ExtrinsicHelper { } /** Query Extrinsics */ - public static getAccountInfo(address: string): Promise { - return ExtrinsicHelper.apiPromise.query.system.account(address); + public static getAccountInfo(keyPair: KeyringPair): Promise { + return ExtrinsicHelper.apiPromise.query.system.account(getUnifiedAddress(keyPair)); } public static getSchemaMaxBytes() { @@ -347,15 +373,15 @@ export class ExtrinsicHelper { /** Balance Extrinsics */ public static transferFunds(source: KeyringPair, dest: KeyringPair, amount: Compact | AnyNumber) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(dest.address, amount), + () => ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(dest), amount), source, ExtrinsicHelper.api.events.balances.Transfer ); } - public static emptyAccount(source: KeyringPair, dest: KeyringPair['address']) { + public static emptyAccount(source: KeyringPair, dest: KeyringPair) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.balances.transferAll(dest, false), + () => ExtrinsicHelper.api.tx.balances.transferAll(getUnifiedAddress(dest), false), source, ExtrinsicHelper.api.events.balances.Transfer ); @@ -737,7 +763,7 @@ export class ExtrinsicHelper { public static timeReleaseTransfer(keys: KeyringPair, who: KeyringPair, schedule: ReleaseSchedule) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.timeRelease.transfer(who.address, schedule), + () => ExtrinsicHelper.api.tx.timeRelease.transfer(getUnifiedAddress(who), schedule), keys, ExtrinsicHelper.api.events.timeRelease.ReleaseScheduleAdded ); @@ -913,7 +939,7 @@ export class ExtrinsicHelper { public static submitProposal(keys: KeyringPair, spendAmount: AnyNumber | Compact) { return new Extrinsic( - () => ExtrinsicHelper.api.tx.treasury.proposeSpend(spendAmount, keys.address), + () => ExtrinsicHelper.api.tx.treasury.proposeSpend(spendAmount, getUnifiedAddress(keys)), keys, ExtrinsicHelper.api.events.treasury.Proposed ); diff --git a/e2e/scaffolding/funding.ts b/e2e/scaffolding/funding.ts index 3dcfb0dc32..927acb51e1 100644 --- a/e2e/scaffolding/funding.ts +++ b/e2e/scaffolding/funding.ts @@ -28,6 +28,7 @@ export const fundingSources = [ 'msa-create-msa', 'msa-key-management', 'passkey-proxy', + 'passkey-proxy-ethereum', 'proxy-pallet', 'scenarios-grant-delegation', 'schemas-create', diff --git a/e2e/scaffolding/globalHooks.ts b/e2e/scaffolding/globalHooks.ts index 33314d4c7f..c30132598e 100644 --- a/e2e/scaffolding/globalHooks.ts +++ b/e2e/scaffolding/globalHooks.ts @@ -6,18 +6,19 @@ import { ExtrinsicHelper } from './extrinsicHelpers'; import { fundingSources, getFundingSource, getRootFundingSource, getSudo } from './funding'; import { TEST_EPOCH_LENGTH, drainKeys, getNonce, setEpochLength } from './helpers'; import { isDev, providerUrl } from './env'; +import { getUnifiedAddress } from './ethereum'; const SOURCE_AMOUNT = 100_000_000_000_000n; // 1,000,000 UNIT per source async function fundAllSources() { const root = getRootFundingSource().keys; - console.log('Root funding source: ', root.address); + console.log('Root funding source: ', getUnifiedAddress(root)); const nonce = await getNonce(root); await Promise.all( fundingSources.map((dest, i) => { try { const testFundingSource = getFundingSource(dest); - console.log(dest, testFundingSource.address.toString()); + console.log(dest, getUnifiedAddress(testFundingSource).toString()); return ExtrinsicHelper.transferFunds(root, testFundingSource, SOURCE_AMOUNT).signAndSend(nonce + i); } catch (e) { console.error('Unable to fund soruce', { dest, nonce: nonce + i }); @@ -37,7 +38,7 @@ async function devSudoActions() { function drainAllSources() { const keys = fundingSources.map((source) => getFundingSource(source)); const root = getRootFundingSource().keys; - return drainKeys(keys, root.address); + return drainKeys(keys, root); } export async function mochaGlobalSetup() { diff --git a/e2e/scaffolding/helpers.ts b/e2e/scaffolding/helpers.ts index a87a3e6be0..733066478d 100644 --- a/e2e/scaffolding/helpers.ts +++ b/e2e/scaffolding/helpers.ts @@ -37,6 +37,8 @@ import assert from 'assert'; import { AVRO_GRAPH_CHANGE } from '../schemas/fixtures/avroGraphChangeSchemaType'; import { PARQUET_BROADCAST } from '../schemas/fixtures/parquetBroadcastSchemaType'; import { AVRO_CHAT_MESSAGE } from '../stateful-pallet-storage/fixtures/itemizedSchemaType'; +import { getUnifiedAddress } from './ethereum'; +import { KeypairType } from '@polkadot/util-crypto/types'; export interface Account { uri: string; @@ -47,6 +49,18 @@ export interface Sr25519Signature { Sr25519: `0x${string}`; } +export interface Ed25519Signature { + Ed25519: `0x${string}`; +} + +export interface EcdsaSignature { + Ecdsa: `0x${string}`; +} + +export interface Address20MultiAddress { + Address20: number[]; +} + export const TEST_EPOCH_LENGTH = 50; export const CENTS = 1000000n; export const DOLLARS = 100n * CENTS; @@ -233,18 +247,18 @@ export async function generatePaginatedDeleteSignaturePayloadV2( // Keep track of all the funded keys so that we can drain them at the end of the test const createdKeys = new Map(); -export function drainFundedKeys(dest: string) { +export function drainFundedKeys(dest: KeyringPair) { return drainKeys([...createdKeys.values()], dest); } -export function createKeys(name: string = 'first pair'): KeyringPair { +export function createKeys(name: string = 'first pair', keyType: KeypairType = 'sr25519'): KeyringPair { const mnemonic = mnemonicGenerate(); // create & add the pair to the keyring with the type and some additional // metadata specified - const keyring = new Keyring({ type: 'sr25519' }); - const keypair = keyring.addFromUri(mnemonic, { name }, 'sr25519'); + const keyring = new Keyring({ type: keyType }); + const keypair = keyring.addFromUri(mnemonic, { name }, keyType); - createdKeys.set(keypair.address, keypair); + createdKeys.set(getUnifiedAddress(keypair), keypair); return keypair; } @@ -257,11 +271,11 @@ function canDrainAccount(info: FrameSystemAccountInfo): boolean { ); } -export async function drainKeys(keyPairs: KeyringPair[], dest: string) { +export async function drainKeys(keyPairs: KeyringPair[], dest: KeyringPair) { try { await Promise.all( keyPairs.map(async (keypair) => { - const info = await ExtrinsicHelper.getAccountInfo(keypair.address); + const info = await ExtrinsicHelper.getAccountInfo(keypair); // Only drain keys that can be if (canDrainAccount(info)) await ExtrinsicHelper.emptyAccount(keypair, dest).signAndSend(); }) @@ -284,12 +298,13 @@ export async function createAndFundKeypair( source: KeyringPair, amount?: bigint, keyName?: string, - nonce?: number + nonce?: number, + keyType: KeypairType = 'sr25519' ): Promise { - const keypair = createKeys(keyName); + const keypair = createKeys(keyName, keyType); await fundKeypair(source, keypair, amount || (await getExistentialDeposit()), nonce); - log('Funded', `Name: ${keyName || 'None provided'}`, `Address: ${keypair.address}`); + log('Funded', `Name: ${keyName || 'None provided'}`, `Address: ${getUnifiedAddress(keypair)}`); return keypair; } @@ -297,13 +312,14 @@ export async function createAndFundKeypair( export async function createAndFundKeypairs( source: KeyringPair, keyNames: string[], - amountOverExDep: bigint = 100_000_000n + amountOverExDep: bigint = 100_000_000n, + keyType: KeypairType = 'sr25519' ): Promise { const nonce = await getNonce(source); const existentialDeposit = await getExistentialDeposit(); const wait: Promise[] = keyNames.map((keyName, i) => { - const keypair = createKeys(keyName + ` ${i}th`); + const keypair = createKeys(keyName + ` ${i}th`, keyType); return fundKeypair(source, keypair, existentialDeposit + amountOverExDep, nonce + i).then(() => keypair); }); @@ -618,7 +634,7 @@ export async function getCapacity(providerId: u64): Promise { - const nonce = await ExtrinsicHelper.apiPromise.call.accountNonceApi.accountNonce(keys.address); + const nonce = await ExtrinsicHelper.apiPromise.call.accountNonceApi.accountNonce(getUnifiedAddress(keys)); return nonce.toNumber(); } diff --git a/e2e/scaffolding/rootHooks.ts b/e2e/scaffolding/rootHooks.ts index f01495e371..0fa0dde4d0 100644 --- a/e2e/scaffolding/rootHooks.ts +++ b/e2e/scaffolding/rootHooks.ts @@ -26,8 +26,7 @@ export const mochaHooks = { try { // Any key created using helpers `createKeys` is kept in the module // then any value remaining is drained here at the end - const rootAddress = getRootFundingSource().keys.address; - await drainFundedKeys(rootAddress); + await drainFundedKeys(getRootFundingSource().keys); console.log('ENDING ROOT hook shutdown', testSuite); } catch (e) { console.error('Failed to run afterAll root hook: ', testSuite, e); diff --git a/e2e/signed-extensions/checkMetadataHash.test.ts b/e2e/signed-extensions/checkMetadataHash.test.ts index b480d1b736..f99169dd24 100644 --- a/e2e/signed-extensions/checkMetadataHash.test.ts +++ b/e2e/signed-extensions/checkMetadataHash.test.ts @@ -13,6 +13,7 @@ import { } from '../scaffolding/helpers'; import { getFundingSource } from '../scaffolding/funding'; import { u8aToHex } from '@polkadot/util'; +import { getUnifiedAddress } from '../scaffolding/ethereum'; const fundingSource = getFundingSource('check-metadata-hash'); @@ -30,7 +31,7 @@ describe.skip('Check Metadata Hash', function () { }); it('should successfully transfer funds', async function () { - const tx = ExtrinsicHelper.api.tx.balances.transferKeepAlive(accountWithNoFunds.address, 5_000_000n); + const tx = ExtrinsicHelper.api.tx.balances.transferKeepAlive(getUnifiedAddress(accountWithNoFunds), 5_000_000n); const api = ExtrinsicHelper.apiPromise; const metadata = await api.call.metadata.metadataAtVersion(15); diff --git a/e2e/sudo/sudo.test.ts b/e2e/sudo/sudo.test.ts index 689907e659..4539953919 100644 --- a/e2e/sudo/sudo.test.ts +++ b/e2e/sudo/sudo.test.ts @@ -205,7 +205,7 @@ describe('Sudo required', function () { assert.notEqual(proposalEvent, undefined, 'should return a Proposal event'); // Confirm that the tokens were reserved/hold in the stakeKeys account using the query API - let stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address); + let stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); assert.equal( stakedAcctInfo.data.reserved, proposalBond, @@ -222,7 +222,7 @@ describe('Sudo required', function () { assert.notEqual(slashEvent, undefined, 'should return a Treasury event'); // Confirm that the tokens were slashed from the stakeKeys account using the query API - stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys.address); + stakedAcctInfo = await ExtrinsicHelper.getAccountInfo(stakeKeys); assert.equal( stakedAcctInfo.data.reserved, 0n, diff --git a/runtime/common/src/signature.rs b/runtime/common/src/signature.rs index 3af704f289..4332e709ce 100644 --- a/runtime/common/src/signature.rs +++ b/runtime/common/src/signature.rs @@ -1,53 +1,91 @@ -use common_primitives::utils::wrap_binary_data; +use common_primitives::{signatures::UnifiedSignature, utils::wrap_binary_data}; use sp_runtime::{traits::Verify, AccountId32, MultiSignature}; use sp_std::vec::Vec; pub fn check_signature(signature: &MultiSignature, signer: AccountId32, payload: Vec) -> bool { - let verify_signature = |payload: &[u8]| signature.verify(payload, &signer.clone().into()); + let unified_signature: UnifiedSignature = signature.clone().into(); + let verify_signature = + |payload: &[u8]| unified_signature.verify(payload, &signer.clone().into()); if verify_signature(&payload) { return true; } - let wrapped_payload = wrap_binary_data(payload); - verify_signature(&wrapped_payload) + match unified_signature { + // we don't need to check the wrapped bytes for ethereum signatures + UnifiedSignature::Ecdsa(_) => false, + _ => { + let wrapped_payload = wrap_binary_data(payload); + verify_signature(&wrapped_payload) + }, + } } #[cfg(test)] -use sp_core::{sr25519, Pair}; +mod tests { + use super::*; + use common_primitives::signatures::UnifiedSigner; + use sp_core::{ecdsa, keccak_256, sr25519, Pair}; + use sp_runtime::traits::IdentifyAccount; -#[test] -fn test_verify_signature_with_wrapped_bytes() { - let (key_pair_delegator, _) = sr25519::Pair::generate(); + #[test] + fn test_verify_signature_with_wrapped_bytes() { + let (key_pair_delegator, _) = sr25519::Pair::generate(); - let payload = b"test_payload".to_vec(); - let encode_add_provider_data = wrap_binary_data(payload.clone()); + let payload = b"test_payload".to_vec(); + let encode_add_provider_data = wrap_binary_data(payload.clone()); - let signature: MultiSignature = key_pair_delegator.sign(&encode_add_provider_data).into(); + let signature: MultiSignature = key_pair_delegator.sign(&encode_add_provider_data).into(); - assert!(check_signature(&signature, key_pair_delegator.public().into(), payload.clone())); -} + assert!(check_signature(&signature, key_pair_delegator.public().into(), payload.clone())); + } -#[test] -fn test_verify_signature_without_wrapped_bytes() { - let (signer, _) = sr25519::Pair::generate(); + #[test] + fn test_verify_signature_without_wrapped_bytes() { + let (signer, _) = sr25519::Pair::generate(); - let payload = b"test_payload".to_vec(); + let payload = b"test_payload".to_vec(); - let signature: MultiSignature = signer.sign(&payload).into(); + let signature: MultiSignature = signer.sign(&payload).into(); - assert!(check_signature(&signature, signer.public().into(), payload)); -} + assert!(check_signature(&signature, signer.public().into(), payload)); + } + + #[test] + fn test_check_signature_with_invalid_signature() { + let (signer, _) = sr25519::Pair::generate(); -#[test] -fn test_check_signature_with_invalid_signature() { - let (signer, _) = sr25519::Pair::generate(); + let payload = b"test_payload".to_vec(); - let payload = b"test_payload".to_vec(); + let signature: MultiSignature = signer.sign(&payload).into(); - let signature: MultiSignature = signer.sign(&payload).into(); + let invalid_payload = b"invalid_payload".to_vec(); + + assert!(!check_signature(&signature, signer.public().into(), invalid_payload)); + } - let invalid_payload = b"invalid_payload".to_vec(); + #[test] + fn test_ethereum_verify_signature_without_wrapped_bytes_should_work() { + let (signer, _) = ecdsa::Pair::generate(); - assert!(!check_signature(&signature, signer.public().into(), invalid_payload)); + let payload = b"test_payload".to_vec(); + + let signature: MultiSignature = signer.sign_prehashed(&keccak_256(&payload)).into(); + let unified_signer = UnifiedSigner::from(signer.public()); + + assert!(check_signature(&signature, unified_signer.into_account(), payload)); + } + + #[test] + fn test_ethereum_verify_signature_wrapped_bytes_should_fail() { + let (signer, _) = ecdsa::Pair::generate(); + + let payload = b"test_payload".to_vec(); + let encode_add_provider_data = wrap_binary_data(payload.clone()); + let signature: MultiSignature = + signer.sign_prehashed(&keccak_256(&encode_add_provider_data)).into(); + let unified_signer = UnifiedSigner::from(signer.public()); + + assert_eq!(check_signature(&signature, unified_signer.into_account(), payload), false); + } } diff --git a/runtime/frequency/Cargo.toml b/runtime/frequency/Cargo.toml index b904dae80a..8028b49c85 100644 --- a/runtime/frequency/Cargo.toml +++ b/runtime/frequency/Cargo.toml @@ -101,6 +101,7 @@ cumulus-primitives-timestamp = { workspace = true } cumulus-primitives-aura = { workspace = true } pallet-collator-selection = { workspace = true } parachain-info = { workspace = true } +sp-debug-derive = { workspace = true, optional = true } [features] default = ["std"] @@ -263,3 +264,6 @@ metadata-hash = ["substrate-wasm-builder/metadata-hash"] frequency-lint-check = [] test = [] parameterized-consensus-hook = [] +force-debug=[ + "sp-debug-derive/force-debug", +] diff --git a/runtime/frequency/src/ethereum.rs b/runtime/frequency/src/ethereum.rs new file mode 100644 index 0000000000..7b36c884a6 --- /dev/null +++ b/runtime/frequency/src/ethereum.rs @@ -0,0 +1,68 @@ +use parity_scale_codec::Codec; +use scale_info::StaticTypeInfo; +use sp_core::hexdisplay::HexDisplay; +use sp_runtime::{ + traits::{LookupError, StaticLookup}, + MultiAddress, +}; +use sp_std::{fmt::Debug, marker::PhantomData}; + +/// A lookup implementation returning the `AccountId` from a `MultiAddress`. +pub struct EthereumCompatibleAccountIdLookup( + PhantomData<(AccountId, AccountIndex)>, +); +impl StaticLookup + for EthereumCompatibleAccountIdLookup +where + AccountId: Codec + Clone + PartialEq + Debug, + AccountIndex: Codec + Clone + PartialEq + Debug, + MultiAddress: Codec + StaticTypeInfo, +{ + type Source = MultiAddress; + type Target = AccountId; + fn lookup(x: Self::Source) -> Result { + match x { + MultiAddress::Id(i) => Ok(i), + MultiAddress::Address20(acc20) => { + log::debug!(target: "ETHEREUM", "lookup 0x{:?}", HexDisplay::from(&acc20)); + let mut buffer = [0u8; 32]; + buffer[12..].copy_from_slice(&acc20); + let decoded = Self::Target::decode(&mut &buffer[..]).map_err(|_| LookupError)?; + Ok(decoded) + }, + _ => Err(LookupError), + } + } + fn unlookup(x: Self::Target) -> Self::Source { + // We are not converting back to 20 bytes since everywhere we are using Id + MultiAddress::Id(x) + } +} + +#[cfg(test)] +mod tests { + use crate::ethereum::EthereumCompatibleAccountIdLookup; + use sp_core::{bytes::from_hex, crypto::AccountId32}; + use sp_runtime::{traits::StaticLookup, MultiAddress}; + + #[test] + fn address20_should_get_decoded_correctly() { + let lookup = + EthereumCompatibleAccountIdLookup::::lookup(MultiAddress::Address20( + from_hex("0x19a701d23f0ee1748b5d5f883cb833943096c6c4") + .expect("should convert") + .try_into() + .expect("invalid size"), + )); + assert!(lookup.is_ok()); + + let converted = lookup.unwrap(); + let expected = AccountId32::new( + from_hex("0x00000000000000000000000019a701d23f0ee1748b5d5f883cb833943096c6c4") + .expect("should convert") + .try_into() + .expect("invalid size"), + ); + assert_eq!(converted, expected) + } +} diff --git a/runtime/frequency/src/lib.rs b/runtime/frequency/src/lib.rs index 026f04a641..70be9373f7 100644 --- a/runtime/frequency/src/lib.rs +++ b/runtime/frequency/src/lib.rs @@ -23,10 +23,7 @@ use cumulus_pallet_parachain_system::{RelayNumberMonotonicallyIncreases, Relaych use sp_core::{crypto::KeyTypeId, OpaqueMetadata}; use sp_runtime::{ create_runtime_str, generic, impl_opaque_keys, - traits::{ - AccountIdConversion, AccountIdLookup, BlakeTwo256, Block as BlockT, ConvertInto, - IdentityLookup, - }, + traits::{AccountIdConversion, BlakeTwo256, Block as BlockT, ConvertInto, IdentityLookup}, transaction_validity::{TransactionSource, TransactionValidity}, ApplyExtrinsicResult, DispatchError, }; @@ -118,6 +115,7 @@ pub use common_runtime::{ }; use frame_support::traits::Contains; +mod ethereum; mod genesis; /// Interface to collective pallet to propose a proposal. @@ -404,7 +402,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("frequency"), impl_name: create_runtime_str!("frequency"), authoring_version: 1, - spec_version: 131, + spec_version: 132, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -418,7 +416,7 @@ pub const VERSION: RuntimeVersion = RuntimeVersion { spec_name: create_runtime_str!("frequency-testnet"), impl_name: create_runtime_str!("frequency"), authoring_version: 1, - spec_version: 131, + spec_version: 132, impl_version: 0, apis: RUNTIME_API_VERSIONS, transaction_version: 1, @@ -474,7 +472,7 @@ impl frame_system::Config for Runtime { /// The aggregated dispatch type that is available for extrinsics. type RuntimeCall = RuntimeCall; /// The lookup mechanism to get account ID from whatever is passed in dispatchers. - type Lookup = AccountIdLookup; + type Lookup = EthereumCompatibleAccountIdLookup; /// The index type for storing how many extrinsics an account has signed. type Nonce = Index; /// The block type. @@ -938,6 +936,7 @@ impl pallet_transaction_payment::Config for Runtime { type OperationalFeeMultiplier = TransactionPaymentOperationalFeeMultiplier; } +use crate::ethereum::EthereumCompatibleAccountIdLookup; use pallet_frequency_tx_payment::Call as FrequencyPaymentCall; use pallet_handles::Call as HandlesCall; use pallet_messages::Call as MessagesCall; diff --git a/scripts/init.sh b/scripts/init.sh index 520518f05a..c9805dfe9a 100755 --- a/scripts/init.sh +++ b/scripts/init.sh @@ -106,7 +106,7 @@ start-paseo-collator-bob) start-frequency-instant) printf "\nBuilding Frequency without relay. Running with instant sealing ...\n" - cargo build --features frequency-no-relay + cargo build --features frequency-no-relay,force-debug parachain_dir=$base_dir/parachain/${para_id} mkdir -p $parachain_dir;