diff --git a/Cargo.lock b/Cargo.lock index ec039398..0b59f378 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8141,6 +8141,14 @@ dependencies = [ "sp-runtime", ] +[[package]] +name = "pallet-evm-precompile-ed25519" +version = "0.1.0" +dependencies = [ + "ed25519-dalek", + "fp-evm", +] + [[package]] name = "pallet-evm-precompile-modexp" version = "2.0.0-dev" @@ -9370,6 +9378,7 @@ dependencies = [ "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dispatch", + "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", "pallet-evm-precompile-p256verify", "pallet-evm-precompile-parachain-staking", @@ -9513,6 +9522,7 @@ dependencies = [ "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dispatch", + "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", "pallet-evm-precompile-p256verify", "pallet-evm-precompile-parachain-staking", @@ -10127,6 +10137,7 @@ dependencies = [ "pallet-evm-precompile-blake2", "pallet-evm-precompile-bn128", "pallet-evm-precompile-dispatch", + "pallet-evm-precompile-ed25519", "pallet-evm-precompile-modexp", "pallet-evm-precompile-p256verify", "pallet-evm-precompile-parachain-staking", diff --git a/Cargo.toml b/Cargo.toml index dbbd3670..943ebd25 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,7 +15,8 @@ members = [ "precompiles/peaq-rbac", "precompiles/parachain-staking", "precompiles/vesting", - "precompiles/p256verify" + "precompiles/p256verify", + "precompiles/ed25519" ] resolver = "2" @@ -54,6 +55,7 @@ impl-trait-for-tuples = "0.2.2" jsonrpsee = { version = "0.24.9", default-features = false } libsecp256k1 = { version = "0.7", default-features = false } p256 = { version = "0.13.2", default-features = false, features = [ "ecdsa" ] } +ed25519-dalek = { version = "2.1.0", default-features = false, features = [ "alloc" ] } log = { version = "0.4.17", default-features = false } macrotest = { version = "1.0.9", default-features = false } num_enum = { version = "0.5.3", default-features = false } diff --git a/precompiles/ed25519/Cargo.toml b/precompiles/ed25519/Cargo.toml new file mode 100644 index 00000000..79e0dad6 --- /dev/null +++ b/precompiles/ed25519/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "pallet-evm-precompile-ed25519" +authors = [ "peaq" ] +description = "Ed25519 signature verification precompile for Frontier EVM (vendored from Frontier, Apache-2.0)." +edition = "2021" +version = "0.1.0" + +[dependencies] +ed25519-dalek = { workspace = true } +fp-evm = { workspace = true, default-features = false } + +[features] +default = [ "std" ] +std = [ + "ed25519-dalek/std", + "fp-evm/std", +] diff --git a/precompiles/ed25519/src/lib.rs b/precompiles/ed25519/src/lib.rs new file mode 100644 index 00000000..c9958ffe --- /dev/null +++ b/precompiles/ed25519/src/lib.rs @@ -0,0 +1,140 @@ +// SPDX-License-Identifier: Apache-2.0 +// This file is part of Frontier; vendored and adapted for peaq. +// +// Copyright (c) 2020-2022 Parity Technologies (UK) Ltd. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#![cfg_attr(not(feature = "std"), no_std)] +#![warn(unused_crate_dependencies)] + +extern crate alloc; + +use alloc::vec::Vec; + +use ed25519_dalek::{Signature, Verifier, VerifyingKey}; +use fp_evm::{ExitSucceed, LinearCostPrecompile, PrecompileFailure}; + +/// Ed25519 signature verification precompile (vendored from Frontier, adapted for peaq). +/// +/// Input (128 bytes): message(32) || public key(32) || signature(64). +/// Output: a 32-byte big-endian `1` if the signature is valid, empty output otherwise -- +/// identical to P256VERIFY (RIP-7212). The call never reverts; malformed input (wrong +/// length or bad key/signature encoding) is treated as an invalid signature (empty output). +pub struct Ed25519Verify; + +impl Ed25519Verify { + /// True iff `input` is a well-formed 128-byte `message || public_key || signature` blob + /// carrying a valid Ed25519 signature. Never panics: the length check guards the + /// fixed-width slicing, and every decode is fallible. + fn is_valid(input: &[u8]) -> bool { + // Exactly 128 bytes, mirroring P256VERIFY's exact-length rule (RIP-7212 rejects + // wrong-length input); trailing garbage is not silently ignored. + if input.len() != 128 { + return false; + } + let msg = &input[0..32]; + let pk = match VerifyingKey::try_from(&input[32..64]) { + Ok(pk) => pk, + Err(_) => return false, + }; + let sig = match Signature::try_from(&input[64..128]) { + Ok(sig) => sig, + Err(_) => return false, + }; + // Standard RFC 8032 verification via ed25519-dalek (cofactorless; rejects non-canonical + // s, so classic (R, s+L) malleability is blocked). NOTE: `verify` (not `verify_strict`) + // accepts small-order public keys -- callers must treat the pubkey as a trusted binding. + pk.verify(msg, &sig).is_ok() + } +} + +impl LinearCostPrecompile for Ed25519Verify { + // Frontier's upstream default (BASE=15, WORD=3) underprices an ed25519 verify ~100x vs + // its real CPU cost -> block-time DoS. Price at ecrecover parity: flat 3000 gas. + const BASE: u64 = 3000; + const WORD: u64 = 0; + + fn execute(input: &[u8], _: u64) -> Result<(ExitSucceed, Vec), PrecompileFailure> { + // Output matches P256VERIFY / RIP-7212: 32-byte big-endian 1 if valid, empty otherwise. + let output = if Self::is_valid(input) { + let mut out = [0u8; 32]; + out[31] = 1; + out.to_vec() + } else { + Vec::new() + }; + Ok((ExitSucceed::Returned, output)) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use ed25519_dalek::{Signer, SigningKey}; + + fn valid_output() -> Vec { + let mut out = [0u8; 32]; + out[31] = 1; + out.to_vec() + } + + #[test] + fn test_empty_input() { + // Malformed (too short) input -> empty output, NOT a revert. + let (exit, out) = Ed25519Verify::execute(&[], 1).expect("must not revert"); + assert_eq!(exit, ExitSucceed::Returned); + assert!(out.is_empty()); + } + + #[test] + fn test_verify() { + #[allow(clippy::zero_prefixed_literal)] + let secret_key_bytes: [u8; ed25519_dalek::SECRET_KEY_LENGTH] = [ + 157, 097, 177, 157, 239, 253, 090, 096, 186, 132, 074, 244, 146, 236, 044, 196, 068, + 073, 197, 105, 123, 050, 105, 025, 112, 059, 172, 003, 028, 174, 127, 096, + ]; + let keypair = SigningKey::from_bytes(&secret_key_bytes); + let public_key = keypair.verifying_key(); + + let msg: &[u8] = b"abcdefghijklmnopqrstuvwxyz123456"; + assert_eq!(msg.len(), 32); + let signature = keypair.sign(msg); + + // input: message(32) || pubkey(32) || signature(64) + let mut input: Vec = Vec::with_capacity(128); + input.extend_from_slice(msg); + input.extend_from_slice(&public_key.to_bytes()); + input.extend_from_slice(&signature.to_bytes()); + assert_eq!(input.len(), 128); + + // valid -> 32-byte 0x..01 + let (_, out) = Ed25519Verify::execute(&input, 1).expect("must not revert"); + assert_eq!(out, valid_output()); + + // oversized input (trailing byte) -> rejected -> empty + let mut input_long = input.clone(); + input_long.push(0u8); + let (_, out) = Ed25519Verify::execute(&input_long, 1).expect("must not revert"); + assert!(out.is_empty()); + + // wrong message -> invalid -> empty + let bad_msg: &[u8] = b"BAD_MESSAGE_mnopqrstuvwxyz123456"; + let mut input2: Vec = Vec::with_capacity(128); + input2.extend_from_slice(bad_msg); + input2.extend_from_slice(&public_key.to_bytes()); + input2.extend_from_slice(&signature.to_bytes()); + let (_, out) = Ed25519Verify::execute(&input2, 1).expect("must not revert"); + assert!(out.is_empty()); + } +} diff --git a/runtime/krest/Cargo.toml b/runtime/krest/Cargo.toml index 9ed751b4..0f089636 100644 --- a/runtime/krest/Cargo.toml +++ b/runtime/krest/Cargo.toml @@ -124,6 +124,7 @@ pallet-evm-precompile-xcm-utils = { path = "../../precompiles/xcm-utils", defaul pallet-evm-precompile-parachain-staking = { path = "../../precompiles/parachain-staking", default-features = false } pallet-evm-precompile-vesting = { path = "../../precompiles/vesting", default-features = false } pallet-evm-precompile-p256verify = { path = "../../precompiles/p256verify", default-features = false } +pallet-evm-precompile-ed25519 = { path = "../../precompiles/ed25519", default-features = false } pallet-evm-precompile-balances-erc20 = { path = "../../precompiles/balances-erc20", default-features = false} runtime-common = { path = "../common", default-features = false } pallet-scheduler = { workspace = true, default-features = false } @@ -299,6 +300,7 @@ std = [ "pallet-evm-precompile-parachain-staking/std", "pallet-evm-precompile-vesting/std", "pallet-evm-precompile-p256verify/std", + "pallet-evm-precompile-ed25519/std", "pallet-xcm/std", "inflation-manager/std", "pallet-message-queue/std", diff --git a/runtime/krest/src/precompiles.rs b/runtime/krest/src/precompiles.rs index 6184310b..2633e406 100644 --- a/runtime/krest/src/precompiles.rs +++ b/runtime/krest/src/precompiles.rs @@ -6,6 +6,7 @@ use pallet_evm_precompile_balances_erc20::{Erc20BalancesPrecompile, Erc20Metadat use pallet_evm_precompile_batch::BatchPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; +use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_p256verify::P256Verify; use pallet_evm_precompile_parachain_staking::ParachainStakingPrecompile; @@ -109,6 +110,13 @@ pub type PeaqPrecompiles = PrecompileSetBuilder< ECRecoverPublicKey, (CallableByContract, CallableByPrecompile), >, + // Ed25519 signature verification (peaq-specific address; Ed25519 has no ecosystem + // standard). Output matches P256VERIFY: 32-byte 1 = valid, empty = invalid. + PrecompileAt< + AddressU64<1027>, + Ed25519Verify, + (CallableByContract, CallableByPrecompile), + >, PrecompileAt< AddressU64<2048>, PeaqDIDPrecompile, diff --git a/runtime/peaq-dev/Cargo.toml b/runtime/peaq-dev/Cargo.toml index 481412b3..be869253 100644 --- a/runtime/peaq-dev/Cargo.toml +++ b/runtime/peaq-dev/Cargo.toml @@ -125,6 +125,7 @@ pallet-evm-precompile-xcm-utils = { path = "../../precompiles/xcm-utils", defaul pallet-evm-precompile-parachain-staking = { path = "../../precompiles/parachain-staking", default-features = false } pallet-evm-precompile-vesting = { path = "../../precompiles/vesting", default-features = false } pallet-evm-precompile-p256verify = { path = "../../precompiles/p256verify", default-features = false } +pallet-evm-precompile-ed25519 = { path = "../../precompiles/ed25519", default-features = false } runtime-common = { path = "../common", default-features = false } peaq-pallet-mor = { workspace = true, default-features = false } xc-asset-config = { path = "../../pallets/xc-asset-config", default-features = false } @@ -301,6 +302,7 @@ std = [ "pallet-evm-precompile-parachain-staking/std", "pallet-evm-precompile-vesting/std", "pallet-evm-precompile-p256verify/std", + "pallet-evm-precompile-ed25519/std", "pallet-xcm/std", "inflation-manager/std", "pallet-message-queue/std", diff --git a/runtime/peaq-dev/src/precompiles.rs b/runtime/peaq-dev/src/precompiles.rs index c7b73d21..4146104d 100644 --- a/runtime/peaq-dev/src/precompiles.rs +++ b/runtime/peaq-dev/src/precompiles.rs @@ -6,6 +6,7 @@ use pallet_evm_precompile_balances_erc20::{Erc20BalancesPrecompile, Erc20Metadat use pallet_evm_precompile_batch::BatchPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; +use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_p256verify::P256Verify; use pallet_evm_precompile_parachain_staking::ParachainStakingPrecompile; @@ -109,6 +110,13 @@ pub type PeaqPrecompiles = PrecompileSetBuilder< ECRecoverPublicKey, (CallableByContract, CallableByPrecompile), >, + // Ed25519 signature verification (peaq-specific address; Ed25519 has no ecosystem + // standard). Output matches P256VERIFY: 32-byte 1 = valid, empty = invalid. + PrecompileAt< + AddressU64<1027>, + Ed25519Verify, + (CallableByContract, CallableByPrecompile), + >, PrecompileAt< AddressU64<2048>, PeaqDIDPrecompile, diff --git a/runtime/peaq/Cargo.toml b/runtime/peaq/Cargo.toml index 34fd6a48..644dcc5c 100644 --- a/runtime/peaq/Cargo.toml +++ b/runtime/peaq/Cargo.toml @@ -123,6 +123,7 @@ pallet-evm-precompile-xcm-utils = { path = "../../precompiles/xcm-utils", defaul pallet-evm-precompile-parachain-staking = { path = "../../precompiles/parachain-staking", default-features = false } pallet-evm-precompile-vesting = { path = "../../precompiles/vesting", default-features = false } pallet-evm-precompile-p256verify = { path = "../../precompiles/p256verify", default-features = false } +pallet-evm-precompile-ed25519 = { path = "../../precompiles/ed25519", default-features = false } pallet-evm-precompile-balances-erc20 = { path = "../../precompiles/balances-erc20", default-features = false} runtime-common = { path = "../common", default-features = false } pallet-scheduler = { workspace = true, default-features = false } @@ -296,6 +297,7 @@ std = [ "pallet-evm-precompile-parachain-staking/std", "pallet-evm-precompile-vesting/std", "pallet-evm-precompile-p256verify/std", + "pallet-evm-precompile-ed25519/std", "pallet-xcm/std", "inflation-manager/std", "pallet-message-queue/std", diff --git a/runtime/peaq/src/precompiles.rs b/runtime/peaq/src/precompiles.rs index 7f7981a5..2626f170 100644 --- a/runtime/peaq/src/precompiles.rs +++ b/runtime/peaq/src/precompiles.rs @@ -6,6 +6,7 @@ use pallet_evm_precompile_balances_erc20::{Erc20BalancesPrecompile, Erc20Metadat use pallet_evm_precompile_batch::BatchPrecompile; use pallet_evm_precompile_blake2::Blake2F; use pallet_evm_precompile_bn128::{Bn128Add, Bn128Mul, Bn128Pairing}; +use pallet_evm_precompile_ed25519::Ed25519Verify; use pallet_evm_precompile_modexp::Modexp; use pallet_evm_precompile_p256verify::P256Verify; use pallet_evm_precompile_parachain_staking::ParachainStakingPrecompile; @@ -109,6 +110,13 @@ pub type PeaqPrecompiles = PrecompileSetBuilder< ECRecoverPublicKey, (CallableByContract, CallableByPrecompile), >, + // Ed25519 signature verification (peaq-specific address; Ed25519 has no ecosystem + // standard). Output matches P256VERIFY: 32-byte 1 = valid, empty = invalid. + PrecompileAt< + AddressU64<1027>, + Ed25519Verify, + (CallableByContract, CallableByPrecompile), + >, PrecompileAt< AddressU64<2048>, PeaqDIDPrecompile,