diff --git a/evm-tests/src/config.ts b/evm-tests/src/config.ts index 7fb40e67cb..1d12e3d584 100644 --- a/evm-tests/src/config.ts +++ b/evm-tests/src/config.ts @@ -20,6 +20,22 @@ export const IEd25519VerifyABI = [ }, ]; +export const ISr25519VERIFY_ADDRESS = "0x0000000000000000000000000000000000000403"; +export const ISr25519VerifyABI = [ + { + inputs: [ + { internalType: "bytes32", name: "message", type: "bytes32" }, + { internalType: "bytes32", name: "publicKey", type: "bytes32" }, + { internalType: "bytes32", name: "r", type: "bytes32" }, + { internalType: "bytes32", name: "s", type: "bytes32" }, + ], + name: "verify", + outputs: [{ internalType: "bool", name: "", type: "bool" }], + stateMutability: "pure", + type: "function", + }, +]; + export const IBALANCETRANSFER_ADDRESS = "0x0000000000000000000000000000000000000800"; export const IBalanceTransferABI = [ { diff --git a/evm-tests/test/sr25519.precompile.verify.test.ts b/evm-tests/test/sr25519.precompile.verify.test.ts new file mode 100644 index 0000000000..234638f195 --- /dev/null +++ b/evm-tests/test/sr25519.precompile.verify.test.ts @@ -0,0 +1,118 @@ +import { ISr25519VERIFY_ADDRESS, ISr25519VerifyABI, ETH_LOCAL_URL } from '../src/config' +import { getPublicClient } from "../src/utils"; +import { toHex, toBytes, keccak256, PublicClient } from 'viem' +import { Keyring } from "@polkadot/keyring"; +import * as assert from "assert"; + +describe("Verfication of sr25519 signature", () => { + // init eth part + let ethClient: PublicClient; + + before(async () => { + ethClient = await getPublicClient(ETH_LOCAL_URL); + }); + + it("Verification of sr25519 works", async () => { + const keyring = new Keyring({ type: "sr25519" }); + const alice = keyring.addFromUri("//Alice"); + + ////////////////////////////////////////////////////////////////////// + // Generate a signature + + // Your message to sign + const message = "Sign this message"; + const messageU8a = new TextEncoder().encode(message); + const messageHex = toHex(messageU8a); // Convert message to hex string + const messageHash = keccak256(messageHex); // Hash the message to fit into bytes32 + console.log(`messageHash = ${messageHash}`); + const hashedMessageBytes = toBytes(messageHash); + console.log(`hashedMessageBytes = ${hashedMessageBytes}`); + + // Sign the message + const signature = await alice.sign(hashedMessageBytes); + console.log(`Signature: ${toHex(signature)}`); + + // Verify the signature locally + const isValid = alice.verify( + hashedMessageBytes, + signature, + alice.publicKey + ); + console.log(`Is the signature valid? ${isValid}`); + + ////////////////////////////////////////////////////////////////////// + // Verify the signature using the precompile contract + + const publicKeyBytes = toHex(alice.publicKey); + console.log(`publicKeyBytes = ${publicKeyBytes}`); + + // Split signture into Commitment (R) and response (s) + let r = signature.slice(0, 32); // Commitment, a.k.a. "r" - first 32 bytes + let s = signature.slice(32, 64); // Response, a.k.a. "s" - second 32 bytes + let rBytes = toHex(r); + let sBytes = toHex(s); + + const isPrecompileValid = await ethClient.readContract({ + address: ISr25519VERIFY_ADDRESS, + abi: ISr25519VerifyABI, + functionName: "verify", + args: [messageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract? ${isPrecompileValid}` + ); + assert.equal(isPrecompileValid, true) + + ////////////////////////////////////////////////////////////////////// + // Verify the signature for bad data using the precompile contract + + let brokenHashedMessageBytes = hashedMessageBytes; + brokenHashedMessageBytes[0] = (brokenHashedMessageBytes[0] + 1) % 0xff; + const brokenMessageHash = toHex(brokenHashedMessageBytes); + console.log(`brokenMessageHash = ${brokenMessageHash}`); + + const isPrecompileValidBadData = await ethClient.readContract({ + address: ISr25519VERIFY_ADDRESS, + abi: ISr25519VerifyABI, + functionName: "verify", + args: [brokenMessageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract for broken data? ${isPrecompileValidBadData}` + ); + assert.equal(isPrecompileValidBadData, false) + + ////////////////////////////////////////////////////////////////////// + // Verify the bad signature for good data using the precompile contract + + let brokenR = r; + brokenR[0] = (brokenR[0] + 1) % 0xff; + rBytes = toHex(r); + const isPrecompileValidBadSignature = await ethClient.readContract({ + address: ISr25519VERIFY_ADDRESS, + abi: ISr25519VerifyABI, + functionName: "verify", + args: [messageHash, + publicKeyBytes, + rBytes, + sBytes] + + }); + + console.log( + `Is the signature valid according to the smart contract for broken signature? ${isPrecompileValidBadSignature}` + ); + assert.equal(isPrecompileValidBadSignature, false) + + }); +}); \ No newline at end of file diff --git a/precompiles/src/ed25519.rs b/precompiles/src/ed25519.rs index ae23a70e78..5e186f344f 100644 --- a/precompiles/src/ed25519.rs +++ b/precompiles/src/ed25519.rs @@ -6,7 +6,7 @@ use core::marker::PhantomData; use ed25519_dalek::{Signature, Verifier, VerifyingKey}; use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; -use crate::PrecompileExt; +use crate::{PrecompileExt, parse_slice}; pub(crate) struct Ed25519Verify(PhantomData); @@ -52,21 +52,3 @@ where Ok((ExitSucceed::Returned, buf.to_vec())) } } - -/// Takes a slice from bytes with PrecompileFailure as Error -fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileFailure> { - let maybe_slice = data.get(from..to); - if let Some(slice) = maybe_slice { - Ok(slice) - } else { - log::error!( - "fail to get slice from data, {:?}, from {}, to {}", - &data, - from, - to - ); - Err(PrecompileFailure::Error { - exit_status: ExitError::InvalidRange, - }) - } -} diff --git a/precompiles/src/lib.rs b/precompiles/src/lib.rs index 89902ec9ee..354bee838d 100644 --- a/precompiles/src/lib.rs +++ b/precompiles/src/lib.rs @@ -4,6 +4,7 @@ extern crate alloc; use core::marker::PhantomData; +use fp_evm::{ExitError, PrecompileFailure}; use frame_support::{ dispatch::{GetDispatchInfo, PostDispatchInfo}, pallet_prelude::Decode, @@ -28,6 +29,7 @@ use crate::ed25519::*; use crate::extensions::*; use crate::metagraph::*; use crate::neuron::*; +use crate::sr25519::*; use crate::staking::*; use crate::storage_query::*; use crate::subnet::*; @@ -38,6 +40,7 @@ mod ed25519; mod extensions; mod metagraph; mod neuron; +mod sr25519; mod staking; mod storage_query; mod subnet; @@ -91,7 +94,7 @@ where Self(Default::default()) } - pub fn used_addresses() -> [H160; 17] { + pub fn used_addresses() -> [H160; 18] { [ hash(1), hash(2), @@ -102,6 +105,7 @@ where hash(1024), hash(1025), hash(Ed25519Verify::::INDEX), + hash(Sr25519Verify::::INDEX), hash(BalanceTransferPrecompile::::INDEX), hash(StakingPrecompile::::INDEX), hash(SubnetPrecompile::::INDEX), @@ -150,6 +154,9 @@ where a if a == hash(Ed25519Verify::::INDEX) => { Some(Ed25519Verify::::execute(handle)) } + a if a == hash(Sr25519Verify::::INDEX) => { + Some(Sr25519Verify::::execute(handle)) + } // Subtensor specific precompiles : a if a == hash(BalanceTransferPrecompile::::INDEX) => { BalanceTransferPrecompile::::try_execute::( @@ -193,3 +200,25 @@ where fn hash(a: u64) -> H160 { H160::from_low_u64_be(a) } + +/* + * + * This is used to parse a slice from bytes with PrecompileFailure as Error + * + */ +fn parse_slice(data: &[u8], from: usize, to: usize) -> Result<&[u8], PrecompileFailure> { + let maybe_slice = data.get(from..to); + if let Some(slice) = maybe_slice { + Ok(slice) + } else { + log::error!( + "fail to get slice from data, {:?}, from {}, to {}", + &data, + from, + to + ); + Err(PrecompileFailure::Error { + exit_status: ExitError::InvalidRange, + }) + } +} diff --git a/precompiles/src/solidity/sr25519Verify.abi b/precompiles/src/solidity/sr25519Verify.abi new file mode 100644 index 0000000000..05d75ae6cc --- /dev/null +++ b/precompiles/src/solidity/sr25519Verify.abi @@ -0,0 +1,14 @@ +[ + { + "inputs": [ + { "internalType": "bytes32", "name": "message", "type": "bytes32" }, + { "internalType": "bytes32", "name": "publicKey", "type": "bytes32" }, + { "internalType": "bytes32", "name": "r", "type": "bytes32" }, + { "internalType": "bytes32", "name": "s", "type": "bytes32" } + ], + "name": "verify", + "outputs": [{ "internalType": "bool", "name": "", "type": "bool" }], + "stateMutability": "pure", + "type": "function" + } +] \ No newline at end of file diff --git a/precompiles/src/solidity/sr25519Verify.sol b/precompiles/src/solidity/sr25519Verify.sol new file mode 100644 index 0000000000..1c6cbff45a --- /dev/null +++ b/precompiles/src/solidity/sr25519Verify.sol @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: GPL-3.0 +pragma solidity ^0.8.0; + +address constant ISR25519VERIFY_ADDRESS = 0x0000000000000000000000000000000000000403; + +interface ISR25519Verify { + /** + * @dev Verifies SR25519 signature using provided message and public key. + * + * @param message The 32-byte signature payload message. + * @param publicKey 32-byte public key matching to private key used to sign the message. + * @param r The SR25519 signature commitment (first 32 bytes). + * @param s The SR25519 signature response (second 32 bytes). + * @return bool Returns true if the signature is valid for the given message and public key, false otherwise. + */ + function verify(bytes32 message, bytes32 publicKey, bytes32 r, bytes32 s) external pure returns (bool); +} diff --git a/precompiles/src/sr25519.rs b/precompiles/src/sr25519.rs new file mode 100644 index 0000000000..c81443dc19 --- /dev/null +++ b/precompiles/src/sr25519.rs @@ -0,0 +1,56 @@ +extern crate alloc; + +use alloc::vec::Vec; +use core::marker::PhantomData; +use sp_core::sr25519; +use sp_runtime::traits::Verify; + +use fp_evm::{ExitError, ExitSucceed, LinearCostPrecompile, PrecompileFailure}; + +use crate::{PrecompileExt, parse_slice}; + +pub(crate) struct Sr25519Verify(PhantomData); + +impl PrecompileExt for Sr25519Verify +where + A: From<[u8; 32]>, +{ + const INDEX: u64 = 1027; +} + +impl LinearCostPrecompile for Sr25519Verify +where + A: From<[u8; 32]>, +{ + const BASE: u64 = 15; + const WORD: u64 = 3; + + fn execute(input: &[u8], _: u64) -> Result<(ExitSucceed, Vec), PrecompileFailure> { + if input.len() < 132 { + return Err(PrecompileFailure::Error { + exit_status: ExitError::Other("input must contain 128 bytes".into()), + }); + }; + + let mut buf = [0u8; 32]; + + let msg = parse_slice(input, 4, 36)?; + let pk = sr25519::Public::try_from(parse_slice(input, 36, 68)?).map_err(|_| { + PrecompileFailure::Error { + exit_status: ExitError::Other("Public key recover failed".into()), + } + })?; + let sig = sr25519::Signature::try_from(parse_slice(input, 68, 132)?).map_err(|_| { + PrecompileFailure::Error { + exit_status: ExitError::Other("Signature recover failed".into()), + } + })?; + + let valid = sig.verify(msg, &pk); + if valid { + buf[31] = 1u8; + } + + Ok((ExitSucceed::Returned, buf.to_vec())) + } +}