From 72fefb3d8537abc7e9625d2e5c57a1cf671184db Mon Sep 17 00:00:00 2001 From: Luke Parker Date: Tue, 2 Sep 2025 09:17:55 -0400 Subject: [PATCH] Strongly type `EmbeddedEllipticCurveKeys` Adds a signed variant to validate knowledge and ownership. Add SCALE derivations for `EmbeddedEllipticCurveKeys` --- Cargo.lock | 3 + substrate/primitives/Cargo.toml | 20 ++- substrate/primitives/src/crypto.rs | 248 ++++++++++++++++++++++++++++- 3 files changed, 263 insertions(+), 8 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cc1a1a47..a1074b19 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10259,10 +10259,13 @@ dependencies = [ "ciphersuite 0.4.2", "dalek-ff-group", "dkg-musig", + "embedwards25519", "parity-scale-codec", "rand_core 0.6.4", "scale-info", + "schnorr-signatures", "schnorrkel", + "secq256k1", "sp-core", "zeroize", ] diff --git a/substrate/primitives/Cargo.toml b/substrate/primitives/Cargo.toml index 88401035..87403b3d 100644 --- a/substrate/primitives/Cargo.toml +++ b/substrate/primitives/Cargo.toml @@ -25,7 +25,10 @@ scale-info = { version = "2", default-features = false, features = ["derive"], o sp-core = { git = "https://github.com/serai-dex/patch-polkadot-sdk", rev = "74839cba4a7f48023080215e5194fd6ab7e270e5", default-features = false } ciphersuite = { path = "../../crypto/ciphersuite", default-features = false, features = ["alloc"] } +schnorr-signatures = { path = "../../crypto/schnorr", default-features = false } dalek-ff-group = { path = "../../crypto/dalek-ff-group", default-features = false, features = ["alloc"] } +embedwards25519 = { path = "../../crypto/embedwards25519", default-features = false, features = ["alloc"] } +secq256k1 = { path = "../../crypto/secq256k1", default-features = false, features = ["alloc"] } dkg = { package = "dkg-musig", path = "../../crypto/dkg/musig", default-features = false } schnorrkel = { version = "0.11", default-features = false } @@ -36,7 +39,22 @@ bech32 = { version = "0.11", default-features = false } rand_core = { version = "0.6", default-features = false, features = ["std"] } [features] -std = ["zeroize/std", "borsh/std", "bitvec/std", "scale?/std", "scale-info?/std", "sp-core/std", "ciphersuite/std", "dalek-ff-group/std", "dkg/std", "schnorrkel/std", "bech32/std"] +std = [ + "zeroize/std", + "borsh/std", + "bitvec/std", + "scale?/std", + "scale-info?/std", + "sp-core/std", + "ciphersuite/std", + "schnorr-signatures/std", + "dalek-ff-group/std", + "embedwards25519/std", + "secq256k1/std", + "dkg/std", + "schnorrkel/std", + "bech32/std" +] serde = [] non_canonical_scale_derivations = ["scale", "scale-info"] default = ["std"] diff --git a/substrate/primitives/src/crypto.rs b/substrate/primitives/src/crypto.rs index 92bd2bfe..5ad4b656 100644 --- a/substrate/primitives/src/crypto.rs +++ b/substrate/primitives/src/crypto.rs @@ -3,6 +3,16 @@ use borsh::{BorshSerialize, BorshDeserialize}; use sp_core::{ConstU32, bounded::BoundedVec}; +use ciphersuite::{ + group::{ff::FromUniformBytes, GroupEncoding}, + Ciphersuite, +}; +use embedwards25519::Embedwards25519; +use secq256k1::Secq256k1; +use schnorr_signatures::SchnorrSignature; + +use crate::network_id::ExternalNetworkId; + /// A Ristretto public key. #[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize, BorshSerialize, BorshDeserialize)] #[cfg_attr( @@ -89,7 +99,7 @@ impl Zeroize for ExternalKey { } impl ExternalKey { - /// The maximum length for am external key. + /// The maximum length for an external key. /* This support keys up to 96 bytes (such as BLS12-381 G2, which is the largest elliptic-curve group element we might reasonably use as a key). This can always be increased if we need to @@ -100,12 +110,236 @@ impl ExternalKey { } /// Key(s) on embedded elliptic curve(s). -/// -/// This may be a single key if the external network uses the same embedded elliptic curve as -/// used for the key to oraclize onto Serai. Else, it'll be a key on the embedded elliptic curve -/// used for the key to oraclize onto Serai concatenated with the key on the embedded elliptic -/// curve used for the external network. -pub type EmbeddedEllipticCurveKeys = BoundedVec>; +#[derive(Clone, Copy, PartialEq, Eq, Debug, Zeroize)] +pub enum EmbeddedEllipticCurveKeys { + /// The embedded elliptic curve keys for a Bitcoin validator. + Bitcoin( + <::G as GroupEncoding>::Repr, + <::G as GroupEncoding>::Repr, + ), + /// The embedded elliptic curve keys for an Ethereum validator. + Ethereum( + <::G as GroupEncoding>::Repr, + <::G as GroupEncoding>::Repr, + ), + /// The embedded elliptic curve key for a Monero validator. + Monero(<::G as GroupEncoding>::Repr), +} + +impl EmbeddedEllipticCurveKeys { + /// The network these keys are for. + pub fn network(&self) -> ExternalNetworkId { + match self { + Self::Bitcoin(_, _) => ExternalNetworkId::Bitcoin, + Self::Ethereum(_, _) => ExternalNetworkId::Ethereum, + Self::Monero(_) => ExternalNetworkId::Monero, + } + } +} + +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Encode for EmbeddedEllipticCurveKeys { + fn using_encoded R>(&self, f: F) -> R { + match self { + EmbeddedEllipticCurveKeys::Bitcoin(e, s) | EmbeddedEllipticCurveKeys::Ethereum(e, s) => { + let mut res = [0; 66]; + res[0] = self.network() as u8; + res[1 .. 33].copy_from_slice(e); + res[33 ..].copy_from_slice(s); + f(&res) + } + EmbeddedEllipticCurveKeys::Monero(e) => { + let mut res = [0; 33]; + res[0] = self.network() as u8; + res[1 ..].copy_from_slice(e); + f(&res) + } + } + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::MaxEncodedLen for EmbeddedEllipticCurveKeys { + fn max_encoded_len() -> usize { + 66 + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::EncodeLike for EmbeddedEllipticCurveKeys {} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Decode for EmbeddedEllipticCurveKeys { + fn decode(input: &mut I) -> Result { + let network_id = ExternalNetworkId::decode(&mut *input)?; + let embedwards25519 = + <::G as GroupEncoding>::Repr::decode(&mut *input)?; + Ok(match network_id { + ExternalNetworkId::Bitcoin => { + let secq256k1 = <[u8; 33]>::decode(&mut *input)?; + EmbeddedEllipticCurveKeys::Bitcoin(embedwards25519, secq256k1.into()) + } + ExternalNetworkId::Ethereum => { + let secq256k1 = <[u8; 33]>::decode(&mut *input)?; + EmbeddedEllipticCurveKeys::Ethereum(embedwards25519, secq256k1.into()) + } + ExternalNetworkId::Monero => EmbeddedEllipticCurveKeys::Monero(embedwards25519), + }) + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::DecodeWithMemTracking for EmbeddedEllipticCurveKeys {} + +/// Key(s) on embedded elliptic curve(s) with the required proofs of knowledge. +#[derive(Clone, PartialEq, Eq, Debug, Zeroize)] +pub enum SignedEmbeddedEllipticCurveKeys { + /// The signed embedded elliptic curve keys for a Bitcoin validator. + Bitcoin( + <::G as GroupEncoding>::Repr, + <::G as GroupEncoding>::Repr, + [u8; 64], + [u8; 65], + ), + /// The signed embedded elliptic curve keys for an Ethereum validator. + Ethereum( + <::G as GroupEncoding>::Repr, + <::G as GroupEncoding>::Repr, + [u8; 64], + [u8; 65], + ), + /// The signed embedded elliptic curve key for a Monero validator. + Monero(<::G as GroupEncoding>::Repr, [u8; 64]), +} + +impl SignedEmbeddedEllipticCurveKeys { + /// The network these keys are for. + pub fn network(&self) -> ExternalNetworkId { + match self { + Self::Bitcoin(_, _, _, _) => ExternalNetworkId::Bitcoin, + Self::Ethereum(_, _, _, _) => ExternalNetworkId::Ethereum, + Self::Monero(_, _) => ExternalNetworkId::Monero, + } + } + + /// Verify these key(s)' signature(s), returning the key(s) if valid. + pub fn verify(self, validator: Public) -> Option { + // Sample a unified challenge + let transcript = match &self { + Self::Bitcoin(e, s, e_sig, s_sig) => [ + [ExternalNetworkId::Bitcoin as u8].as_slice(), + &validator.0, + e, + s, + &e_sig[.. 32], + &s_sig[.. 33], + ] + .concat(), + Self::Ethereum(e, s, e_sig, s_sig) => [ + [ExternalNetworkId::Ethereum as u8].as_slice(), + &validator.0, + e, + s, + &e_sig[.. 32], + &s_sig[.. 33], + ] + .concat(), + Self::Monero(e, e_sig) => { + [[ExternalNetworkId::Monero as u8].as_slice(), &validator.0, e, &e_sig[.. 32]].concat() + } + }; + let challenge = sp_core::hashing::blake2_512(&transcript); + + // Verify the Schnorr signatures + match &self { + Self::Bitcoin(e, _, e_sig, _) | Self::Ethereum(e, _, e_sig, _) | Self::Monero(e, e_sig) => { + let sig = SchnorrSignature::::read(&mut e_sig.as_slice()).ok()?; + if !sig.verify( + Embedwards25519::read_G(&mut e.as_slice()).ok()?, + <::F as FromUniformBytes<_>>::from_uniform_bytes( + &challenge, + ), + ) { + None?; + } + } + }; + match &self { + Self::Bitcoin(_, s, _, s_sig) | Self::Ethereum(_, s, _, s_sig) => { + let sig = SchnorrSignature::::read(&mut s_sig.as_slice()).ok()?; + if !sig.verify( + Secq256k1::read_G(&mut s.as_slice()).ok()?, + <::F as FromUniformBytes<_>>::from_uniform_bytes(&challenge), + ) { + None?; + } + } + Self::Monero(_, _) => {} + } + + // Return the keys + Some(match self { + Self::Bitcoin(e, s, _, _) => EmbeddedEllipticCurveKeys::Bitcoin(e, s), + Self::Ethereum(e, s, _, _) => EmbeddedEllipticCurveKeys::Ethereum(e, s), + Self::Monero(e, _) => EmbeddedEllipticCurveKeys::Monero(e), + }) + } +} + +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Encode for SignedEmbeddedEllipticCurveKeys { + fn using_encoded R>(&self, f: F) -> R { + match self { + SignedEmbeddedEllipticCurveKeys::Bitcoin(e, s, e_sig, s_sig) | + SignedEmbeddedEllipticCurveKeys::Ethereum(e, s, e_sig, s_sig) => { + let mut res = [0; 195]; + res[0] = self.network() as u8; + res[1 .. 33].copy_from_slice(e); + res[33 .. 66].copy_from_slice(s); + res[66 .. 130].copy_from_slice(e_sig); + res[130 ..].copy_from_slice(s_sig); + f(&res) + } + SignedEmbeddedEllipticCurveKeys::Monero(e, e_sig) => { + let mut res = [0; 97]; + res[0] = self.network() as u8; + res[1 .. 33].copy_from_slice(e); + res[33 ..].copy_from_slice(e_sig); + f(&res) + } + } + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::EncodeLike for SignedEmbeddedEllipticCurveKeys {} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::Decode for SignedEmbeddedEllipticCurveKeys { + fn decode(input: &mut I) -> Result { + let embedded_elliptic_curve_keys = EmbeddedEllipticCurveKeys::decode(input)?; + let embedwards25519_signature = <[u8; 64]>::decode(&mut *input)?; + Ok(match embedded_elliptic_curve_keys { + EmbeddedEllipticCurveKeys::Bitcoin(e, s) => { + let secq256k1_signature = <[u8; 65]>::decode(&mut *input)?; + SignedEmbeddedEllipticCurveKeys::Bitcoin( + e, + s, + embedwards25519_signature, + secq256k1_signature, + ) + } + EmbeddedEllipticCurveKeys::Ethereum(e, s) => { + let secq256k1_signature = <[u8; 65]>::decode(&mut *input)?; + SignedEmbeddedEllipticCurveKeys::Ethereum( + e, + s, + embedwards25519_signature, + secq256k1_signature, + ) + } + EmbeddedEllipticCurveKeys::Monero(e) => { + SignedEmbeddedEllipticCurveKeys::Monero(e, embedwards25519_signature) + } + }) + } +} +#[cfg(feature = "non_canonical_scale_derivations")] +impl scale::DecodeWithMemTracking for SignedEmbeddedEllipticCurveKeys {} /// The key pair for a validator set. ///