diff --git a/Cargo.lock b/Cargo.lock index 20fed84eb..d80b7de60 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -18,6 +18,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "320119579fcad9c21884f5c4861d16174d0e06250625266f50fe6898340abefa" +[[package]] +name = "aead" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d122413f284cf2d62fb1b7db97e02edb8cda96d769b16e443a4f6195e35662b0" +dependencies = [ + "crypto-common", + "generic-array 0.14.9", +] + [[package]] name = "aes" version = "0.8.4" @@ -89,6 +99,12 @@ dependencies = [ "arrayvec", ] +[[package]] +name = "arrayref" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" + [[package]] name = "arrayvec" version = "0.7.6" @@ -628,6 +644,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" dependencies = [ "generic-array 0.14.9", + "rand_core 0.6.4", "typenum", ] @@ -882,6 +899,7 @@ dependencies = [ "defuse-num-utils", "defuse-sep53", "defuse-serde-utils", + "defuse-sr25519", "defuse-test-utils", "defuse-tip191", "defuse-token-id", @@ -912,6 +930,7 @@ dependencies = [ "near-sdk", "p256", "rstest", + "schnorrkel", "serde_with", "strum 0.27.2", "thiserror 2.0.17", @@ -1123,6 +1142,19 @@ dependencies = [ "tlb-ton", ] +[[package]] +name = "defuse-sr25519" +version = "0.1.0" +dependencies = [ + "defuse-crypto", + "defuse-test-utils", + "hex-literal", + "near-sdk", + "rand 0.8.5", + "rstest", + "schnorrkel", +] + [[package]] name = "defuse-test-utils" version = "0.1.0" @@ -1747,6 +1779,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom_or_panic" +version = "0.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea1015b5a70616b688dc230cfe50c8af89d972cb132d5a622814d29773b10b9" +dependencies = [ + "rand 0.8.5", + "rand_core 0.6.4", +] + [[package]] name = "glob" version = "0.3.3" @@ -2402,6 +2444,18 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" +[[package]] +name = "merlin" +version = "3.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58c38e2799fc0978b65dfff8023ec7843e2330bb462f19198840b34b6582397d" +dependencies = [ + "byteorder", + "keccak", + "rand_core 0.6.4", + "zeroize", +] + [[package]] name = "mime" version = "0.3.17" @@ -3905,6 +3959,25 @@ dependencies = [ "syn 2.0.111", ] +[[package]] +name = "schnorrkel" +version = "0.11.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e9fcb6c2e176e86ec703e22560d99d65a5ee9056ae45a08e13e84ebf796296f" +dependencies = [ + "aead", + "arrayref", + "arrayvec", + "curve25519-dalek", + "getrandom_or_panic", + "merlin", + "rand_core 0.6.4", + "serde_bytes", + "sha2 0.10.9", + "subtle", + "zeroize", +] + [[package]] name = "scopeguard" version = "1.2.0" @@ -3986,6 +4059,16 @@ dependencies = [ "serde_derive", ] +[[package]] +name = "serde_bytes" +version = "0.11.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a5d440709e79d88e51ac01c4b72fc6cb7314017bb7da9eeff678aa94c10e3ea8" +dependencies = [ + "serde", + "serde_core", +] + [[package]] name = "serde_core" version = "1.0.228" diff --git a/Cargo.toml b/Cargo.toml index d7ba8bb0c..b84be7659 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,7 @@ members = [ "erc191", "escrow-swap", "fees", + "sr25519", "io-utils", "map-utils", "near-utils", @@ -57,6 +58,7 @@ defuse-decimal.path = "decimal" defuse-erc191.path = "erc191" defuse-escrow-swap.path = "escrow-swap" defuse-fees.path = "fees" +defuse-sr25519.path = "sr25519" defuse-io-utils.path = "io-utils" defuse-map-utils.path = "map-utils" defuse-near-utils.path = "near-utils" @@ -113,6 +115,7 @@ rand = "0.9" rand_chacha = "0.9" rstest = "0.25" schemars = "0.8" +schnorrkel = { version = "0.11.5", default-features = false } serde_json = "1" serde_with = "3.9" stellar-strkey = "0.0.13" diff --git a/core/Cargo.toml b/core/Cargo.toml index c92688ad0..520a76695 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -12,6 +12,7 @@ defuse-crypto = { workspace = true, features = ["serde"] } defuse-deadline.workspace = true defuse-erc191.workspace = true defuse-fees.workspace = true +defuse-sr25519.workspace = true defuse-nep245.workspace = true defuse-nep413.workspace = true defuse-map-utils.workspace = true diff --git a/core/src/payload/mod.rs b/core/src/payload/mod.rs index 880e3cbb3..28a8e24aa 100644 --- a/core/src/payload/mod.rs +++ b/core/src/payload/mod.rs @@ -3,6 +3,7 @@ pub mod multi; pub mod nep413; pub mod raw; pub mod sep53; +pub mod sr25519; pub mod tip191; pub mod ton_connect; pub mod webauthn; diff --git a/core/src/payload/multi.rs b/core/src/payload/multi.rs index 921fa46c7..80874736c 100644 --- a/core/src/payload/multi.rs +++ b/core/src/payload/multi.rs @@ -2,6 +2,7 @@ use defuse_crypto::{Payload, PublicKey, SignedPayload}; use defuse_erc191::SignedErc191Payload; use defuse_nep413::SignedNep413Payload; use defuse_sep53::SignedSep53Payload; +use defuse_sr25519::SignedSr25519Payload; use defuse_tip191::SignedTip191Payload; use defuse_ton_connect::SignedTonConnectPayload; use derive_more::derive::From; @@ -51,6 +52,10 @@ pub enum MultiPayload { /// SEP-53: The standard for signing data off-chain for Stellar accounts. /// See [SEP-53](https://github.com/stellar/stellar-protocol/blob/master/ecosystem/sep-0053.md) Sep53(SignedSep53Payload), + + /// Sr25519: Message signing for Polkadot/Substrate chains using Sr25519 signatures. + /// Follows Substrate's signing conventions. Compatible with Polkadot.js and other Substrate wallets. + Sr25519(SignedSr25519Payload), } impl Payload for MultiPayload { @@ -68,6 +73,7 @@ impl Payload for MultiPayload { Self::WebAuthn(payload) => payload.hash(), Self::TonConnect(payload) => payload.hash(), Self::Sep53(payload) => payload.hash(), + Self::Sr25519(payload) => payload.hash(), } } } @@ -85,6 +91,7 @@ impl SignedPayload for MultiPayload { Self::WebAuthn(payload) => payload.verify(), Self::TonConnect(payload) => payload.verify().map(PublicKey::Ed25519), Self::Sep53(payload) => payload.verify().map(PublicKey::Ed25519), + Self::Sr25519(payload) => payload.verify().map(PublicKey::Sr25519), } } } @@ -105,6 +112,7 @@ where Self::WebAuthn(payload) => payload.extract_defuse_payload(), Self::TonConnect(payload) => payload.extract_defuse_payload(), Self::Sep53(payload) => payload.extract_defuse_payload(), + Self::Sr25519(payload) => payload.extract_defuse_payload(), } } } diff --git a/core/src/payload/sr25519.rs b/core/src/payload/sr25519.rs new file mode 100644 index 000000000..2fb502990 --- /dev/null +++ b/core/src/payload/sr25519.rs @@ -0,0 +1,15 @@ +use defuse_sr25519::SignedSr25519Payload; +use near_sdk::{serde::de::DeserializeOwned, serde_json}; + +use super::{DefusePayload, ExtractDefusePayload}; + +impl ExtractDefusePayload for SignedSr25519Payload +where + T: DeserializeOwned, +{ + type Error = serde_json::Error; + + fn extract_defuse_payload(self) -> Result, Self::Error> { + serde_json::from_str(&self.payload.payload) + } +} diff --git a/crypto/Cargo.toml b/crypto/Cargo.toml index f20394213..ef7612085 100644 --- a/crypto/Cargo.toml +++ b/crypto/Cargo.toml @@ -11,6 +11,7 @@ generic-array = { workspace = true, features = ["compat-0_14"] } hex.workspace = true near-sdk = { workspace = true, features = ["unstable"] } p256.workspace = true +schnorrkel.workspace = true strum.workspace = true thiserror.workspace = true diff --git a/crypto/src/curve/mod.rs b/crypto/src/curve/mod.rs index 3d66a545b..ae9ff780b 100644 --- a/crypto/src/curve/mod.rs +++ b/crypto/src/curve/mod.rs @@ -1,10 +1,11 @@ mod ed25519; mod p256; mod secp256k1; +mod sr25519; use crate::{ParseCurveError, parse::checked_base58_decode_array}; -pub use self::{ed25519::*, p256::*, secp256k1::*}; +pub use self::{ed25519::*, p256::*, secp256k1::*, sr25519::*}; use near_sdk::bs58; use strum::{Display, EnumString, IntoStaticStr}; @@ -33,6 +34,7 @@ pub enum CurveType { Ed25519 = 0, Secp256k1 = 1, P256 = 2, + Sr25519 = 3, } pub trait TypedCurve: Curve { diff --git a/crypto/src/curve/sr25519.rs b/crypto/src/curve/sr25519.rs new file mode 100644 index 000000000..291f6a1fd --- /dev/null +++ b/crypto/src/curve/sr25519.rs @@ -0,0 +1,40 @@ +use schnorrkel::{PublicKey, Signature}; + +use super::{Curve, CurveType, TypedCurve}; + +pub struct Sr25519; + +impl Sr25519 { + // using "substrate" as the default context following Substrate/Polkadot convention + pub const SIGNING_CTX: &[u8] = b"substrate"; +} + +impl Curve for Sr25519 { + /// A Ristretto Schnorr public key represented as a 32-byte Ristretto compressed point + type PublicKey = [u8; schnorrkel::PUBLIC_KEY_LENGTH]; + /// A 64-byte Ristretto Schnorr signature + type Signature = [u8; schnorrkel::SIGNATURE_LENGTH]; + + type Message = [u8]; + type VerifyingKey = Self::PublicKey; + + #[inline] + fn verify( + signature: &Self::Signature, + message: &Self::Message, + public_key: &Self::VerifyingKey, + ) -> Option { + let public_key_parsed = PublicKey::from_bytes(public_key).ok()?; + let signature_parsed = Signature::from_bytes(signature).ok()?; + + public_key_parsed + .verify_simple(Self::SIGNING_CTX, message, &signature_parsed) + .is_ok() + .then_some(public_key) + .copied() + } +} + +impl TypedCurve for Sr25519 { + const CURVE_TYPE: CurveType = CurveType::Sr25519; +} diff --git a/crypto/src/public_key.rs b/crypto/src/public_key.rs index f8da94311..9c12d1b6d 100644 --- a/crypto/src/public_key.rs +++ b/crypto/src/public_key.rs @@ -6,7 +6,8 @@ use core::{ use near_sdk::{AccountId, AccountIdRef, bs58, env, near}; use crate::{ - Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1, parse::checked_base58_decode_array, + Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1, Sr25519, + parse::checked_base58_decode_array, }; #[cfg_attr(feature = "arbitrary", derive(arbitrary::Arbitrary))] @@ -21,6 +22,7 @@ pub enum PublicKey { Ed25519(::PublicKey) = 0, Secp256k1(::PublicKey) = 1, P256(::PublicKey) = 2, + Sr25519(::PublicKey) = 3, } impl PublicKey { @@ -30,6 +32,7 @@ impl PublicKey { Self::Ed25519(_) => CurveType::Ed25519, Self::Secp256k1(_) => CurveType::Secp256k1, Self::P256(_) => CurveType::P256, + Self::Sr25519(_) => CurveType::Sr25519, } } @@ -40,6 +43,7 @@ impl PublicKey { Self::Ed25519(data) => data, Self::Secp256k1(data) => data, Self::P256(data) => data, + Self::Sr25519(data) => data, } } @@ -73,6 +77,16 @@ impl PublicKey { hex::encode(&env::keccak256_array([b"p256".as_slice(), pk].concat())[12..32]) ) } + Self::Sr25519(pk) => { + // Similar to P256, we use the Eth Implicit schema with + // "sr25519" prefix to avoid collisions + format!( + "0x{}", + hex::encode( + &env::keccak256_array([b"sr25519".as_slice(), pk].concat())[12..32] + ) + ) + } } .try_into() .unwrap_or_else(|_| unreachable!()) @@ -123,6 +137,7 @@ impl FromStr for PublicKey { CurveType::Ed25519 => checked_base58_decode_array(data).map(Self::Ed25519), CurveType::Secp256k1 => checked_base58_decode_array(data).map(Self::Secp256k1), CurveType::P256 => checked_base58_decode_array(data).map(Self::P256), + CurveType::Sr25519 => checked_base58_decode_array(data).map(Self::Sr25519), } } } diff --git a/crypto/src/signature.rs b/crypto/src/signature.rs index 365cf3e41..e27fe6b97 100644 --- a/crypto/src/signature.rs +++ b/crypto/src/signature.rs @@ -6,7 +6,8 @@ use core::{ use near_sdk::{bs58, near}; use crate::{ - Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1, parse::checked_base58_decode_array, + Curve, CurveType, Ed25519, P256, ParseCurveError, Secp256k1, Sr25519, + parse::checked_base58_decode_array, }; #[near(serializers = [borsh(use_discriminant = true)])] @@ -20,6 +21,7 @@ pub enum Signature { Ed25519(::Signature) = 0, Secp256k1(::Signature) = 1, P256(::Signature) = 2, + Sr25519(::Signature) = 3, } impl Signature { @@ -29,6 +31,7 @@ impl Signature { Self::Ed25519(_) => CurveType::Ed25519, Self::Secp256k1(_) => CurveType::Secp256k1, Self::P256(_) => CurveType::P256, + Self::Sr25519(_) => CurveType::Sr25519, } } @@ -39,6 +42,7 @@ impl Signature { Self::Ed25519(data) => data, Self::Secp256k1(data) => data, Self::P256(data) => data, + Self::Sr25519(data) => data, } } } @@ -79,6 +83,7 @@ impl FromStr for Signature { CurveType::Ed25519 => checked_base58_decode_array(data).map(Self::Ed25519), CurveType::Secp256k1 => checked_base58_decode_array(data).map(Self::Secp256k1), CurveType::P256 => checked_base58_decode_array(data).map(Self::P256), + CurveType::Sr25519 => checked_base58_decode_array(data).map(Self::Sr25519), } } } diff --git a/sr25519/Cargo.toml b/sr25519/Cargo.toml new file mode 100644 index 000000000..427d5d7a0 --- /dev/null +++ b/sr25519/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "defuse-sr25519" +version.workspace = true +edition.workspace = true +rust-version.workspace = true +repository.workspace = true + +[lints] +workspace = true + +[dependencies] +defuse-crypto = { workspace = true, features = ["serde"] } + +near-sdk.workspace = true + +[dev-dependencies] +defuse-test-utils.workspace = true + +hex-literal.workspace = true +near-sdk = { workspace = true, features = ["unit-testing"] } +# Use rand 0.8 to match schnorrkel's rand_core 0.6 dependency +rand = "0.8" +rstest.workspace = true +schnorrkel = { workspace = true, features = ["std"] } diff --git a/sr25519/src/lib.rs b/sr25519/src/lib.rs new file mode 100644 index 000000000..646976c8e --- /dev/null +++ b/sr25519/src/lib.rs @@ -0,0 +1,277 @@ +//! Sr25519 message signing support for Polkadot/Substrate chains +//! +//! This crate implements message signing for Polkadot/Substrate-based chains +//! using the Sr25519 (Schnorr on Ristretto255) signature scheme. +//! +//! Compatible with Polkadot.js, Talisman, Subwallet, and other Substrate wallets. + +use defuse_crypto::{Curve, Payload, SignedPayload, Sr25519, serde::AsCurve}; +use near_sdk::{env, near}; + +/// Raw message payload for Sr25519 signing +/// +/// Uses the "substrate" signing context following Substrate/Polkadot conventions. +/// This matches how Polkadot.js and other Substrate wallets sign messages. +/// +/// Note: Substrate wallets (Polkadot.js, Talisman, Subwallet, etc.) wrap messages +/// in `...` tags when signing. This wrapping is handled automatically +/// during verification, so the payload should contain only the inner message content. +#[near(serializers = [json])] +#[derive(Debug, Clone)] +pub struct Sr25519Payload { + /// The message to be signed (without `` wrapper) + pub payload: String, +} + +impl Sr25519Payload { + /// Create a new payload with a message. + /// + /// NOTE: This will use the standard "substrate" signing context + /// which is the default for Polkadot/Substrate chains. + #[inline] + pub fn new(message: impl Into) -> Self { + Self { + payload: message.into(), + } + } +} + +impl Payload for Sr25519Payload { + #[inline] + fn hash(&self) -> near_sdk::CryptoHash { + // Hash just the message content. The signing context is handled + // by schnorrkel during signature verification. + // + // SHA-256 is chosen because it's one of two cryptographic hash + // functions commonly used in Polkadot/Substrate ecosystem. The + // second one is Blake2, which is not natively supported on Near + env::sha256_array(&self.payload) + } +} + +/// Signed Sr25519 message with signature +#[near(serializers = [json])] +#[derive(Debug, Clone)] +pub struct SignedSr25519Payload { + #[serde(flatten)] + pub payload: Sr25519Payload, + + /// sr25519 public key: Ristretto Schnorr public key + /// represented as a 32-byte Ristretto compressed point + #[serde_as(as = "AsCurve")] + pub public_key: ::PublicKey, + + /// sr25519 signature: 64-byte Ristretto Schnorr signature + #[serde_as(as = "AsCurve")] + pub signature: ::Signature, +} + +impl Payload for SignedSr25519Payload { + #[inline] + fn hash(&self) -> near_sdk::CryptoHash { + self.payload.hash() + } +} + +impl SignedPayload for SignedSr25519Payload { + type PublicKey = ::PublicKey; + + #[inline] + fn verify(&self) -> Option { + // Substrate wallets (Polkadot.js, Talisman, Subwallet, etc.) wrap messages + // in ... tags when signing. We need to apply the same wrapping + // during verification to match what was actually signed by the wallet. + let wrapped_message = format!("{}", self.payload.payload); + + Sr25519::verify( + &self.signature, + wrapped_message.as_bytes(), + &self.public_key, + ) + } +} + +#[cfg(test)] +mod tests { + use hex_literal::hex; + use rand::thread_rng; + use schnorrkel::Keypair; + + use super::*; + + #[test] + fn test_payload_creation() { + let payload = Sr25519Payload::new("Hello, Sr25519!".to_string()); + assert_eq!(payload.payload, "Hello, Sr25519!"); + } + + #[test] + fn test_payload_hashing() { + let payload = Sr25519Payload::new("test message".to_string()); + let hash = payload.hash(); + // Verify hash is 32 bytes + assert_eq!(hash.len(), 32); + + // Same message should produce same hash + let payload2 = Sr25519Payload::new("test message".to_string()); + assert_eq!(payload.hash(), payload2.hash()); + } + + #[test] + fn test_sr25519_signature_verification() { + // Generate a keypair + let mut rng = thread_rng(); + let keypair = Keypair::generate_with(&mut rng); + + // Create a message + let message = "Hello, Sr25519!"; + + // Sign the message with wrapper (matching wallet behavior) + let wrapped_message = format!("{message}"); + let signature = keypair.sign_simple(Sr25519::SIGNING_CTX, wrapped_message.as_bytes()); + + // Create the signed payload with unwrapped message + let signed_payload = SignedSr25519Payload { + payload: Sr25519Payload::new(message.to_string()), + public_key: keypair.public.to_bytes(), + signature: signature.to_bytes(), + }; + + // Verify the signature + let verified_key = signed_payload.verify(); + assert!(verified_key.is_some(), "Signature verification failed"); + assert_eq!( + verified_key.unwrap(), + keypair.public.to_bytes(), + "Recovered public key doesn't match" + ); + } + + #[test] + fn test_sr25519_invalid_signature() { + // Generate a keypair + let mut rng = thread_rng(); + let keypair = Keypair::generate_with(&mut rng); + + // Create a message + let message = "Hello, Sr25519!"; + + // Create an invalid signature (all zeros) + let invalid_signature = [0u8; 64]; + + // Create the signed payload with invalid signature + let signed_payload = SignedSr25519Payload { + payload: Sr25519Payload::new(message.to_string()), + public_key: keypair.public.to_bytes(), + signature: invalid_signature, + }; + + // Verify should fail + let verified_key = signed_payload.verify(); + assert!( + verified_key.is_none(), + "Invalid signature was incorrectly verified" + ); + } + + #[test] + fn test_sr25519_different_messages() { + // Generate a keypair + let mut rng = thread_rng(); + let keypair = Keypair::generate_with(&mut rng); + + // Sign two different messages with wrapper (matching wallet behavior) + let message1 = "First message"; + let message2 = "Second message"; + + let wrapped1 = format!("{message1}"); + let wrapped2 = format!("{message2}"); + + let signature1 = keypair.sign_simple(Sr25519::SIGNING_CTX, wrapped1.as_bytes()); + let signature2 = keypair.sign_simple(Sr25519::SIGNING_CTX, wrapped2.as_bytes()); + + // Verify first signature + let signed_payload1 = SignedSr25519Payload { + payload: Sr25519Payload::new(message1.to_string()), + public_key: keypair.public.to_bytes(), + signature: signature1.to_bytes(), + }; + assert!( + signed_payload1.verify().is_some(), + "First signature verification failed" + ); + + // Verify second signature + let signed_payload2 = SignedSr25519Payload { + payload: Sr25519Payload::new(message2.to_string()), + public_key: keypair.public.to_bytes(), + signature: signature2.to_bytes(), + }; + assert!( + signed_payload2.verify().is_some(), + "Second signature verification failed" + ); + + // Cross-verification should fail (signature1 with message2) + let cross_payload = SignedSr25519Payload { + payload: Sr25519Payload::new(message2.to_string()), + public_key: keypair.public.to_bytes(), + signature: signature1.to_bytes(), + }; + assert!( + cross_payload.verify().is_none(), + "Cross-verification should fail" + ); + } + + #[test] + fn test_payload_hash_consistency() { + let payload1 = Sr25519Payload::new("test".to_string()); + let payload2 = Sr25519Payload::new("test".to_string()); + + // Same payload should produce same hash + assert_eq!(payload1.hash(), payload2.hash()); + + let payload3 = Sr25519Payload::new("different".to_string()); + // Different payload should produce different hash + assert_ne!(payload1.hash(), payload3.hash()); + } + + #[test] + fn test_real_wallet_signature() { + // Real signature from Polkadot.js Extension + // Wallet: Polkadot.js Extension (https://polkadot.js.org/extension/) + // Test dApp: https://polkadot.js.org/apps/#/signing + // Date: 2025-12-18 + // Address: 167y8dsUr7kaM1FNoCtXWy2unEnjGHiN7ML3vawR6Nwywbci + // Original message: "Hello from Intents!" + // Note: Polkadot.js wraps messages in tags when signing. + // The wrapping is handled automatically during verification. + + let message = "Hello from Intents!"; + + // Public key (32 bytes) + let public_key = hex!("e27d987db9ed2a7a48f4137c997d610226dc93bf256c9026268b0b8489bb9862"); + + // Signature (64 bytes) signed via Polkadot.js + let signature = hex!( + "e2c01abbd53c89d6302475827b62c7e2168a93a407ebafd94fee3fb2e286e539 + ee1877c15df48c55c59f9d5e032f1f9a1b63a2dc4085517d705ec174e6c9cf8c" + ); + + // Create the signed payload with the message + let signed_payload = SignedSr25519Payload { + payload: Sr25519Payload::new(message.to_string()), + public_key, + signature, + }; + + // Verify the signature + let verified_key = signed_payload.verify(); + assert_eq!( + verified_key, + Some(public_key), + "Signature verification failed or Recovered public key doesn't match" + ); + } +}