Skip to content

Commit

Permalink
Paired-key Crypto Scheme (#1705)
Browse files Browse the repository at this point in the history
BEEFY needs two cryptographic keys at the same time. Validators should
sign BEEFY payload using both ECDSA and BLS key. The network will gossip
a payload which contains a valid ECDSA key. The prover nodes aggregate
the BLS keys if aggregation fails to verifies the validator which
provided a valid ECDSA signature but an invalid BLS signature is subject
to slashing.

As such BEEFY session should be initiated with both key. Currently there
is no straight forward way of doing so, beside having a session with
RuntimeApp corresponding to a crypto scheme contains both keys.

This pull request implement a generic paired_crypto scheme as well as
implementing it for (ECDSA, BLS) pair.

---------

Co-authored-by: Davide Galassi <davxy@datawok.net>
Co-authored-by: Robert Hambrock <roberthambrock@gmail.com>
  • Loading branch information
3 people authored Oct 15, 2023
1 parent 9e14470 commit 1b34571
Show file tree
Hide file tree
Showing 6 changed files with 723 additions and 42 deletions.
44 changes: 26 additions & 18 deletions substrate/primitives/core/src/bls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@

//! Simple BLS (Boneh–Lynn–Shacham) Signature API.
#[cfg(feature = "std")]
#[cfg(feature = "serde")]
use crate::crypto::Ss58Codec;
use crate::crypto::{ByteArray, CryptoType, Derive, Public as TraitPublic, UncheckedFrom};
#[cfg(feature = "full_crypto")]
Expand All @@ -28,8 +28,12 @@ use sp_std::vec::Vec;

use codec::{Decode, Encode, MaxEncodedLen};
use scale_info::TypeInfo;
#[cfg(feature = "std")]

#[cfg(feature = "serde")]
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
#[cfg(all(not(feature = "std"), feature = "serde"))]
use sp_std::alloc::{format, string::String};

use w3f_bls::{DoublePublicKey, DoubleSignature, EngineBLS, SerializableToBytes, TinyBLS381};
#[cfg(feature = "full_crypto")]
use w3f_bls::{DoublePublicKeyScheme, Keypair, Message, SecretKey};
Expand All @@ -39,6 +43,7 @@ use sp_std::{convert::TryFrom, marker::PhantomData, ops::Deref};

/// BLS-377 specialized types
pub mod bls377 {
pub use super::{PUBLIC_KEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE};
use crate::crypto::CryptoTypeId;
use w3f_bls::TinyBLS377;

Expand All @@ -60,6 +65,7 @@ pub mod bls377 {

/// BLS-381 specialized types
pub mod bls381 {
pub use super::{PUBLIC_KEY_SERIALIZED_SIZE, SIGNATURE_SERIALIZED_SIZE};
use crate::crypto::CryptoTypeId;
use w3f_bls::TinyBLS381;

Expand All @@ -83,17 +89,17 @@ trait BlsBound: EngineBLS + HardJunctionId + Send + Sync + 'static {}

impl<T: EngineBLS + HardJunctionId + Send + Sync + 'static> BlsBound for T {}

// Secret key serialized size
/// Secret key serialized size
#[cfg(feature = "full_crypto")]
const SECRET_KEY_SERIALIZED_SIZE: usize =
<SecretKey<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE;

// Public key serialized size
const PUBLIC_KEY_SERIALIZED_SIZE: usize =
/// Public key serialized size
pub const PUBLIC_KEY_SERIALIZED_SIZE: usize =
<DoublePublicKey<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE;

// Signature serialized size
const SIGNATURE_SERIALIZED_SIZE: usize =
/// Signature serialized size
pub const SIGNATURE_SERIALIZED_SIZE: usize =
<DoubleSignature<TinyBLS381> as SerializableToBytes>::SERIALIZED_BYTES_SIZE;

/// A secret seed.
Expand Down Expand Up @@ -258,7 +264,7 @@ impl<T> sp_std::fmt::Debug for Public<T> {
}
}

#[cfg(feature = "std")]
#[cfg(feature = "serde")]
impl<T: BlsBound> Serialize for Public<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand All @@ -268,7 +274,7 @@ impl<T: BlsBound> Serialize for Public<T> {
}
}

#[cfg(feature = "std")]
#[cfg(feature = "serde")]
impl<'de, T: BlsBound> Deserialize<'de> for Public<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down Expand Up @@ -317,6 +323,10 @@ impl<T> sp_std::hash::Hash for Signature<T> {
}
}

impl<T> ByteArray for Signature<T> {
const LEN: usize = SIGNATURE_SERIALIZED_SIZE;
}

impl<T> TryFrom<&[u8]> for Signature<T> {
type Error = ();

Expand All @@ -330,7 +340,7 @@ impl<T> TryFrom<&[u8]> for Signature<T> {
}
}

#[cfg(feature = "std")]
#[cfg(feature = "serde")]
impl<T> Serialize for Signature<T> {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
Expand All @@ -340,7 +350,7 @@ impl<T> Serialize for Signature<T> {
}
}

#[cfg(feature = "std")]
#[cfg(feature = "serde")]
impl<'de, T> Deserialize<'de> for Signature<T> {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
Expand Down Expand Up @@ -444,10 +454,9 @@ impl<T: BlsBound> TraitPair for Pair<T> {
path: Iter,
_seed: Option<Seed>,
) -> Result<(Self, Option<Seed>), DeriveError> {
let mut acc: [u8; SECRET_KEY_SERIALIZED_SIZE] =
self.0.secret.to_bytes().try_into().expect(
"Secret key serializer returns a vector of SECRET_KEY_SERIALIZED_SIZE size",
);
let mut acc: [u8; SECRET_KEY_SERIALIZED_SIZE] = self.0.secret.to_bytes().try_into().expect(
"Secret key serializer returns a vector of SECRET_KEY_SERIALIZED_SIZE size; qed",
);
for j in path {
match j {
DeriveJunction::Soft(_cc) => return Err(DeriveError::SoftKeyInPath),
Expand Down Expand Up @@ -529,11 +538,10 @@ mod test {
);
}

// Only passes if the seed = (seed mod ScalarField)
#[test]
fn seed_and_derive_should_work() {
let seed = array_bytes::hex2array_unchecked(
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f00",
"9d61b19deffd5a60ba844af492ec2cc44449c5697b326919703bac031cae7f60",
);
let pair = Pair::from_seed(&seed);
// we are using hash to field so this is not going to work
Expand All @@ -543,7 +551,7 @@ mod test {
assert_eq!(
derived.to_raw_vec(),
array_bytes::hex2array_unchecked::<_, 32>(
"a4f2269333b3e87c577aa00c4a2cd650b3b30b2e8c286a47c251279ff3a26e0d"
"3a0626d095148813cd1642d38254f1cfff7eb8cc1a2fc83b2a135377c3554c12"
)
);
}
Expand Down
2 changes: 1 addition & 1 deletion substrate/primitives/core/src/crypto.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1213,7 +1213,7 @@ macro_rules! impl_from_entropy_base {
[$type; 17], [$type; 18], [$type; 19], [$type; 20], [$type; 21], [$type; 22], [$type; 23], [$type; 24],
[$type; 25], [$type; 26], [$type; 27], [$type; 28], [$type; 29], [$type; 30], [$type; 31], [$type; 32],
[$type; 36], [$type; 40], [$type; 44], [$type; 48], [$type; 56], [$type; 64], [$type; 72], [$type; 80],
[$type; 96], [$type; 112], [$type; 128], [$type; 160], [$type; 192], [$type; 224], [$type; 256]
[$type; 96], [$type; 112], [$type; 128], [$type; 160], [$type; 177], [$type; 192], [$type; 224], [$type; 256]
);
}
}
Expand Down
54 changes: 32 additions & 22 deletions substrate/primitives/core/src/ecdsa.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,12 @@ use sp_std::vec::Vec;
/// An identifier used to match public keys against ecdsa keys
pub const CRYPTO_ID: CryptoTypeId = CryptoTypeId(*b"ecds");

/// The byte length of public key
pub const PUBLIC_KEY_SERIALIZED_SIZE: usize = 33;

/// The byte length of signature
pub const SIGNATURE_SERIALIZED_SIZE: usize = 65;

/// A secret seed (which is bytewise essentially equivalent to a SecretKey).
///
/// We need it as a different type because `Seed` is expected to be AsRef<[u8]>.
Expand All @@ -71,11 +77,11 @@ type Seed = [u8; 32];
PartialOrd,
Ord,
)]
pub struct Public(pub [u8; 33]);
pub struct Public(pub [u8; PUBLIC_KEY_SERIALIZED_SIZE]);

impl crate::crypto::FromEntropy for Public {
fn from_entropy(input: &mut impl codec::Input) -> Result<Self, codec::Error> {
let mut result = Self([0u8; 33]);
let mut result = Self([0u8; PUBLIC_KEY_SERIALIZED_SIZE]);
input.read(&mut result.0[..])?;
Ok(result)
}
Expand All @@ -86,7 +92,7 @@ impl Public {
///
/// NOTE: No checking goes on to ensure this is a real public key. Only use it if
/// you are certain that the array actually is a pubkey. GIGO!
pub fn from_raw(data: [u8; 33]) -> Self {
pub fn from_raw(data: [u8; PUBLIC_KEY_SERIALIZED_SIZE]) -> Self {
Self(data)
}

Expand All @@ -109,7 +115,7 @@ impl Public {
}

impl ByteArray for Public {
const LEN: usize = 33;
const LEN: usize = PUBLIC_KEY_SERIALIZED_SIZE;
}

impl TraitPublic for Public {}
Expand Down Expand Up @@ -148,8 +154,8 @@ impl From<Pair> for Public {
}
}

impl UncheckedFrom<[u8; 33]> for Public {
fn unchecked_from(x: [u8; 33]) -> Self {
impl UncheckedFrom<[u8; PUBLIC_KEY_SERIALIZED_SIZE]> for Public {
fn unchecked_from(x: [u8; PUBLIC_KEY_SERIALIZED_SIZE]) -> Self {
Public(x)
}
}
Expand Down Expand Up @@ -198,14 +204,18 @@ impl<'de> Deserialize<'de> for Public {
/// A signature (a 512-bit value, plus 8 bits for recovery ID).
#[cfg_attr(feature = "full_crypto", derive(Hash))]
#[derive(Encode, Decode, MaxEncodedLen, PassByInner, TypeInfo, PartialEq, Eq)]
pub struct Signature(pub [u8; 65]);
pub struct Signature(pub [u8; SIGNATURE_SERIALIZED_SIZE]);

impl ByteArray for Signature {
const LEN: usize = SIGNATURE_SERIALIZED_SIZE;
}

impl TryFrom<&[u8]> for Signature {
type Error = ();

fn try_from(data: &[u8]) -> Result<Self, Self::Error> {
if data.len() == 65 {
let mut inner = [0u8; 65];
if data.len() == SIGNATURE_SERIALIZED_SIZE {
let mut inner = [0u8; SIGNATURE_SERIALIZED_SIZE];
inner.copy_from_slice(data);
Ok(Signature(inner))
} else {
Expand Down Expand Up @@ -239,26 +249,26 @@ impl<'de> Deserialize<'de> for Signature {

impl Clone for Signature {
fn clone(&self) -> Self {
let mut r = [0u8; 65];
let mut r = [0u8; SIGNATURE_SERIALIZED_SIZE];
r.copy_from_slice(&self.0[..]);
Signature(r)
}
}

impl Default for Signature {
fn default() -> Self {
Signature([0u8; 65])
Signature([0u8; SIGNATURE_SERIALIZED_SIZE])
}
}

impl From<Signature> for [u8; 65] {
fn from(v: Signature) -> [u8; 65] {
impl From<Signature> for [u8; SIGNATURE_SERIALIZED_SIZE] {
fn from(v: Signature) -> [u8; SIGNATURE_SERIALIZED_SIZE] {
v.0
}
}

impl AsRef<[u8; 65]> for Signature {
fn as_ref(&self) -> &[u8; 65] {
impl AsRef<[u8; SIGNATURE_SERIALIZED_SIZE]> for Signature {
fn as_ref(&self) -> &[u8; SIGNATURE_SERIALIZED_SIZE] {
&self.0
}
}
Expand Down Expand Up @@ -287,8 +297,8 @@ impl sp_std::fmt::Debug for Signature {
}
}

impl UncheckedFrom<[u8; 65]> for Signature {
fn unchecked_from(data: [u8; 65]) -> Signature {
impl UncheckedFrom<[u8; SIGNATURE_SERIALIZED_SIZE]> for Signature {
fn unchecked_from(data: [u8; SIGNATURE_SERIALIZED_SIZE]) -> Signature {
Signature(data)
}
}
Expand All @@ -298,7 +308,7 @@ impl Signature {
///
/// NOTE: No checking goes on to ensure this is a real signature. Only use it if
/// you are certain that the array actually is a signature. GIGO!
pub fn from_raw(data: [u8; 65]) -> Signature {
pub fn from_raw(data: [u8; SIGNATURE_SERIALIZED_SIZE]) -> Signature {
Signature(data)
}

Expand All @@ -307,10 +317,10 @@ impl Signature {
/// NOTE: No checking goes on to ensure this is a real signature. Only use it if
/// you are certain that the array actually is a signature. GIGO!
pub fn from_slice(data: &[u8]) -> Option<Self> {
if data.len() != 65 {
if data.len() != SIGNATURE_SERIALIZED_SIZE {
return None
}
let mut r = [0u8; 65];
let mut r = [0u8; SIGNATURE_SERIALIZED_SIZE];
r.copy_from_slice(data);
Some(Signature(r))
}
Expand Down Expand Up @@ -473,7 +483,7 @@ impl Pair {
pub fn verify_deprecated<M: AsRef<[u8]>>(sig: &Signature, message: M, pubkey: &Public) -> bool {
let message = libsecp256k1::Message::parse(&blake2_256(message.as_ref()));

let parse_signature_overflowing = |x: [u8; 65]| {
let parse_signature_overflowing = |x: [u8; SIGNATURE_SERIALIZED_SIZE]| {
let sig = libsecp256k1::Signature::parse_overflowing_slice(&x[..64]).ok()?;
let rid = libsecp256k1::RecoveryId::parse(x[64]).ok()?;
Some((sig, rid))
Expand Down Expand Up @@ -726,7 +736,7 @@ mod test {
let signature = pair.sign(&message[..]);
let serialized_signature = serde_json::to_string(&signature).unwrap();
// Signature is 65 bytes, so 130 chars + 2 quote chars
assert_eq!(serialized_signature.len(), 132);
assert_eq!(serialized_signature.len(), SIGNATURE_SERIALIZED_SIZE * 2 + 2);
let signature = serde_json::from_str(&serialized_signature).unwrap();
assert!(Pair::verify(&signature, &message[..], &pair.public()));
}
Expand Down
2 changes: 1 addition & 1 deletion substrate/primitives/core/src/hexdisplay.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ macro_rules! impl_non_endians {
impl_non_endians!(
[u8; 1], [u8; 2], [u8; 3], [u8; 4], [u8; 5], [u8; 6], [u8; 7], [u8; 8], [u8; 10], [u8; 12],
[u8; 14], [u8; 16], [u8; 20], [u8; 24], [u8; 28], [u8; 32], [u8; 40], [u8; 48], [u8; 56],
[u8; 64], [u8; 65], [u8; 80], [u8; 96], [u8; 112], [u8; 128], [u8; 144]
[u8; 64], [u8; 65], [u8; 80], [u8; 96], [u8; 112], [u8; 128], [u8; 144], [u8; 177]
);

/// Format into ASCII + # + hex, suitable for storage key preimages.
Expand Down
1 change: 1 addition & 0 deletions substrate/primitives/core/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ pub mod hash;
#[cfg(feature = "std")]
mod hasher;
pub mod offchain;
pub mod paired_crypto;
pub mod sr25519;
pub mod testing;
#[cfg(feature = "std")]
Expand Down
Loading

0 comments on commit 1b34571

Please sign in to comment.