From ae2c78130de2af8d741c741ad6d5d3ba4925b28b Mon Sep 17 00:00:00 2001 From: Arsenii Kulikov Date: Thu, 31 Oct 2024 23:10:43 +0400 Subject: [PATCH] Revert "chore: replace Signature with PrimitiveSignature (#796)" This reverts commit 9856a37633e7884ee67cdc8335dfd8626964776c. --- crates/primitives/src/lib.rs | 4 +- crates/primitives/src/signature/mod.rs | 6 + crates/primitives/src/signature/parity.rs | 280 +++++++++ .../primitives/src/signature/primitive_sig.rs | 578 ++++++++++++++++++ crates/primitives/src/signature/sig.rs | 503 +++++++++++---- crates/primitives/src/signature/utils.rs | 44 ++ 6 files changed, 1294 insertions(+), 121 deletions(-) create mode 100644 crates/primitives/src/signature/parity.rs create mode 100644 crates/primitives/src/signature/primitive_sig.rs diff --git a/crates/primitives/src/lib.rs b/crates/primitives/src/lib.rs index b81aff933..a4ad0ea93 100644 --- a/crates/primitives/src/lib.rs +++ b/crates/primitives/src/lib.rs @@ -54,7 +54,9 @@ mod signed; pub use signed::{BigIntConversionError, ParseSignedError, Sign, Signed}; mod signature; -pub use signature::{normalize_v, to_eip155_v, Signature, SignatureError}; +pub use signature::{ + normalize_v, to_eip155_v, Parity, PrimitiveSignature, Signature, SignatureError, +}; pub mod utils; pub use utils::{eip191_hash_message, keccak256, Keccak256}; diff --git a/crates/primitives/src/signature/mod.rs b/crates/primitives/src/signature/mod.rs index 554e44770..815b32cdf 100644 --- a/crates/primitives/src/signature/mod.rs +++ b/crates/primitives/src/signature/mod.rs @@ -1,8 +1,14 @@ mod error; pub use error::SignatureError; +mod parity; +pub use parity::Parity; + mod sig; pub use sig::Signature; mod utils; pub use utils::{normalize_v, to_eip155_v}; + +mod primitive_sig; +pub use primitive_sig::PrimitiveSignature; diff --git a/crates/primitives/src/signature/parity.rs b/crates/primitives/src/signature/parity.rs new file mode 100644 index 000000000..b16011b0d --- /dev/null +++ b/crates/primitives/src/signature/parity.rs @@ -0,0 +1,280 @@ +use crate::{ + signature::{utils::normalize_v_to_byte, SignatureError}, + to_eip155_v, ChainId, Uint, U64, +}; + +/// The parity of the signature, stored as either a V value (which may include +/// a chain id), or the y-parity. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +#[cfg_attr(feature = "arbitrary", derive(derive_arbitrary::Arbitrary, proptest_derive::Arbitrary))] +pub enum Parity { + /// Explicit V value. May be EIP-155 modified. + Eip155(u64), + /// Non-EIP155. 27 or 28. + NonEip155(bool), + /// Parity flag. True for odd. + Parity(bool), +} + +impl Default for Parity { + fn default() -> Self { + Self::Parity(false) + } +} + +#[cfg(feature = "k256")] +impl From for Parity { + fn from(value: k256::ecdsa::RecoveryId) -> Self { + Self::Parity(value.is_y_odd()) + } +} + +impl TryFrom for Parity { + type Error = >::Error; + fn try_from(value: U64) -> Result { + value.as_limbs()[0].try_into() + } +} + +impl From> for Parity { + fn from(value: Uint<1, 1>) -> Self { + Self::Parity(!value.is_zero()) + } +} + +impl From for Parity { + fn from(value: bool) -> Self { + Self::Parity(value) + } +} + +impl TryFrom for Parity { + type Error = SignatureError; + + fn try_from(value: u64) -> Result { + match value { + 0 | 1 => Ok(Self::Parity(value != 0)), + 27 | 28 => Ok(Self::NonEip155((value - 27) != 0)), + value @ 35..=u64::MAX => Ok(Self::Eip155(value)), + _ => Err(SignatureError::InvalidParity(value)), + } + } +} + +impl Parity { + /// Returns the chain ID associated with the V value, if this signature is + /// replay-protected by [EIP-155]. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + pub const fn chain_id(&self) -> Option { + match *self { + Self::Eip155(mut v @ 35..) => { + if v % 2 == 0 { + v -= 1; + } + v -= 35; + Some(v / 2) + } + _ => None, + } + } + + /// Returns true if the signature is replay-protected by [EIP-155]. + /// + /// This is true if the V value is 35 or greater. Values less than 35 are + /// either not replay protected (27/28), or are invalid. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + pub const fn has_eip155_value(&self) -> bool { + self.chain_id().is_some() + } + + /// Return the y-parity as a boolean. + pub const fn y_parity(&self) -> bool { + match self { + Self::Eip155(v @ 0..=34) => *v % 2 == 1, + Self::Eip155(v) => (*v ^ 1) % 2 == 1, + Self::NonEip155(b) | Self::Parity(b) => *b, + } + } + + /// Return the y-parity as 0 or 1 + pub const fn y_parity_byte(&self) -> u8 { + self.y_parity() as u8 + } + + /// Return the y-parity byte as 27 or 28, + /// in the case of a non-EIP155 signature. + pub const fn y_parity_byte_non_eip155(&self) -> Option { + match self { + Self::NonEip155(v) | Self::Parity(v) => Some(*v as u8 + 27), + _ => None, + } + } + + /// Return the corresponding u64 V value. + pub const fn to_u64(&self) -> u64 { + match self { + Self::Eip155(v) => *v, + Self::NonEip155(b) => *b as u64 + 27, + Self::Parity(b) => *b as u64, + } + } + + /// Inverts the parity. + pub const fn inverted(&self) -> Self { + match *self { + Self::Parity(b) => Self::Parity(!b), + Self::NonEip155(b) => Self::NonEip155(!b), + Self::Eip155(0) => Self::Eip155(1), + Self::Eip155(v @ 1..=34) => Self::Eip155(if v % 2 == 0 { v - 1 } else { v + 1 }), + Self::Eip155(v @ 35..) => Self::Eip155(v ^ 1), + } + } + + /// Converts an EIP-155 V value to a non-EIP-155 V value. + /// + /// This is a nop for non-EIP-155 values. + pub const fn strip_chain_id(&self) -> Self { + match *self { + Self::Eip155(v) => Self::NonEip155(v % 2 == 1), + this => this, + } + } + + /// Applies EIP-155 with the given chain ID. + pub const fn with_chain_id(self, chain_id: ChainId) -> Self { + let parity = match self { + Self::Eip155(v) => normalize_v_to_byte(v) == 1, + Self::NonEip155(b) | Self::Parity(b) => b, + }; + + Self::Eip155(to_eip155_v(parity as u8, chain_id)) + } + + /// Determines the recovery ID. + #[cfg(feature = "k256")] + pub const fn recid(&self) -> k256::ecdsa::RecoveryId { + let recid_opt = match self { + Self::Eip155(v) => Some(crate::signature::utils::normalize_v_to_recid(*v)), + Self::NonEip155(b) | Self::Parity(b) => k256::ecdsa::RecoveryId::from_byte(*b as u8), + }; + + // manual unwrap for const fn + match recid_opt { + Some(recid) => recid, + None => unreachable!(), + } + } + + /// Convert to a parity bool, dropping any V information. + pub const fn to_parity_bool(self) -> Self { + Self::Parity(self.y_parity()) + } +} + +#[cfg(feature = "rlp")] +impl alloy_rlp::Encodable for Parity { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + match self { + Self::Eip155(v) => v.encode(out), + Self::NonEip155(v) => (*v as u8 + 27).encode(out), + Self::Parity(b) => b.encode(out), + } + } + + fn length(&self) -> usize { + match self { + Self::Eip155(v) => v.length(), + Self::NonEip155(_) => 0u8.length(), + Self::Parity(v) => v.length(), + } + } +} + +#[cfg(feature = "rlp")] +impl alloy_rlp::Decodable for Parity { + fn decode(buf: &mut &[u8]) -> Result { + let v = u64::decode(buf)?; + Ok(match v { + 0 => Self::Parity(false), + 1 => Self::Parity(true), + 27 => Self::NonEip155(false), + 28 => Self::NonEip155(true), + v @ 35..=u64::MAX => Self::try_from(v).expect("checked range"), + _ => return Err(alloy_rlp::Error::Custom("Invalid parity value")), + }) + } +} + +#[cfg(test)] +mod test { + use crate::Parity; + + #[cfg(feature = "rlp")] + #[test] + fn basic_rlp() { + use crate::hex; + use alloy_rlp::{Decodable, Encodable}; + + let vector = vec![ + (hex!("01").as_slice(), Parity::Parity(true)), + (hex!("1b").as_slice(), Parity::NonEip155(false)), + (hex!("25").as_slice(), Parity::Eip155(37)), + (hex!("26").as_slice(), Parity::Eip155(38)), + (hex!("81ff").as_slice(), Parity::Eip155(255)), + ]; + + for test in vector.into_iter() { + let mut buf = vec![]; + test.1.encode(&mut buf); + assert_eq!(test.0, buf.as_slice()); + + assert_eq!(test.1, Parity::decode(&mut buf.as_slice()).unwrap()); + } + } + + #[test] + fn u64_round_trip() { + let parity = Parity::Eip155(37); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + let parity = Parity::Eip155(38); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + let parity = Parity::NonEip155(false); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + let parity = Parity::NonEip155(true); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + let parity = Parity::Parity(false); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + let parity = Parity::Parity(true); + assert_eq!(parity, Parity::try_from(parity.to_u64()).unwrap()); + } + + #[test] + fn round_trip() { + // with chain ID 1 + let p = Parity::Eip155(37); + + assert_eq!(p.to_parity_bool(), Parity::Parity(false)); + + assert_eq!(p.with_chain_id(1), Parity::Eip155(37)); + } + + #[test] + fn invert_parity() { + let p = Parity::Eip155(0); + assert_eq!(p.inverted(), Parity::Eip155(1)); + + let p = Parity::Eip155(22); + assert_eq!(p.inverted(), Parity::Eip155(21)); + + let p = Parity::Eip155(58); + assert_eq!(p.inverted(), Parity::Eip155(59)); + + let p = Parity::NonEip155(false); + assert_eq!(p.inverted(), Parity::NonEip155(true)); + + let p = Parity::Parity(true); + assert_eq!(p.inverted(), Parity::Parity(false)); + } +} diff --git a/crates/primitives/src/signature/primitive_sig.rs b/crates/primitives/src/signature/primitive_sig.rs new file mode 100644 index 000000000..aa030342b --- /dev/null +++ b/crates/primitives/src/signature/primitive_sig.rs @@ -0,0 +1,578 @@ +#![allow(unknown_lints, unnameable_types)] + +use crate::{hex, normalize_v, signature::SignatureError, uint, U256}; +use alloc::vec::Vec; +use core::str::FromStr; + +/// The order of the secp256k1 curve +const SECP256K1N_ORDER: U256 = + uint!(0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFEBAAEDCE6AF48A03BBFD25E8CD0364141_U256); + +/// An Ethereum ECDSA signature. +#[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] +pub struct PrimitiveSignature { + y_parity: bool, + r: U256, + s: U256, +} + +impl<'a> TryFrom<&'a [u8]> for PrimitiveSignature { + type Error = SignatureError; + + /// Parses a raw signature which is expected to be 65 bytes long where + /// the first 32 bytes is the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + fn try_from(bytes: &'a [u8]) -> Result { + if bytes.len() != 65 { + return Err(SignatureError::FromBytes("expected exactly 65 bytes")); + } + let parity = + normalize_v(bytes[64] as u64).ok_or(SignatureError::InvalidParity(bytes[64] as u64))?; + Ok(Self::from_bytes_and_parity(&bytes[..64], parity)) + } +} + +impl FromStr for PrimitiveSignature { + type Err = SignatureError; + + fn from_str(s: &str) -> Result { + let bytes = hex::decode(s)?; + Self::try_from(&bytes[..]) + } +} + +impl From<&PrimitiveSignature> for [u8; 65] { + #[inline] + fn from(value: &PrimitiveSignature) -> [u8; 65] { + value.as_bytes() + } +} + +impl From for [u8; 65] { + #[inline] + fn from(value: PrimitiveSignature) -> [u8; 65] { + value.as_bytes() + } +} + +impl From<&PrimitiveSignature> for Vec { + #[inline] + fn from(value: &PrimitiveSignature) -> Self { + value.as_bytes().to_vec() + } +} + +impl From for Vec { + #[inline] + fn from(value: PrimitiveSignature) -> Self { + value.as_bytes().to_vec() + } +} + +#[cfg(feature = "k256")] +impl From<(k256::ecdsa::Signature, k256::ecdsa::RecoveryId)> for PrimitiveSignature { + fn from(value: (k256::ecdsa::Signature, k256::ecdsa::RecoveryId)) -> Self { + Self::from_signature_and_parity(value.0, value.1.is_y_odd()) + } +} + +#[cfg(feature = "k256")] +impl TryFrom for k256::ecdsa::Signature { + type Error = k256::ecdsa::Error; + + fn try_from(value: PrimitiveSignature) -> Result { + value.to_k256() + } +} + +#[cfg(feature = "rlp")] +impl PrimitiveSignature { + /// Decode an RLP-encoded VRS signature. Accepts `decode_parity` closure which allows to + /// customize parity decoding and possibly extract additional data from it (e.g chain_id for + /// legacy signature). + pub fn decode_rlp_vrs( + buf: &mut &[u8], + decode_parity: impl FnOnce(&mut &[u8]) -> alloy_rlp::Result, + ) -> Result { + use alloy_rlp::Decodable; + + let parity = decode_parity(buf)?; + let r = Decodable::decode(buf)?; + let s = Decodable::decode(buf)?; + + Ok(Self::new(r, s, parity)) + } +} + +impl PrimitiveSignature { + #[doc(hidden)] + pub fn test_signature() -> Self { + Self::from_scalars_and_parity( + b256!("840cfc572845f5786e702984c2a582528cad4b49b2a10b9db1be7fca90058565"), + b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"), + false, + ) + } + + /// Instantiate a new signature from `r`, `s`, and `v` values. + #[allow(clippy::missing_const_for_fn)] + pub fn new(r: U256, s: U256, v: bool) -> Self { + Self { r, s, y_parity: v } + } + + /// Returns the inner ECDSA signature. + #[cfg(feature = "k256")] + #[deprecated(note = "use `Signature::to_k256` instead")] + #[inline] + pub fn into_inner(self) -> k256::ecdsa::Signature { + self.try_into().expect("signature conversion failed") + } + + /// Returns the inner ECDSA signature. + #[cfg(feature = "k256")] + #[inline] + pub fn to_k256(self) -> Result { + k256::ecdsa::Signature::from_scalars(self.r.to_be_bytes(), self.s.to_be_bytes()) + } + + /// Instantiate from a signature and recovery id + #[cfg(feature = "k256")] + pub fn from_signature_and_parity(sig: k256::ecdsa::Signature, v: bool) -> Self { + let r = U256::from_be_slice(sig.r().to_bytes().as_ref()); + let s = U256::from_be_slice(sig.s().to_bytes().as_ref()); + Self { y_parity: v, r, s } + } + + /// Creates a [`PrimitiveSignature`] from the serialized `r` and `s` scalar values, which + /// comprise the ECDSA signature, alongside a `v` value, used to determine the recovery ID. + #[inline] + pub fn from_scalars_and_parity(r: crate::B256, s: crate::B256, parity: bool) -> Self { + Self::new(U256::from_be_slice(r.as_ref()), U256::from_be_slice(s.as_ref()), parity) + } + + /// Normalizes the signature into "low S" form as described in + /// [BIP 0062: Dealing with Malleability][1]. + /// + /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki + #[inline] + pub fn normalize_s(&self) -> Option { + let s = self.s(); + + if s > SECP256K1N_ORDER >> 1 { + Some(Self { y_parity: !self.y_parity, r: self.r, s: SECP256K1N_ORDER - s }) + } else { + None + } + } + + /// Returns the recovery ID. + #[cfg(feature = "k256")] + #[inline] + pub const fn recid(&self) -> k256::ecdsa::RecoveryId { + k256::ecdsa::RecoveryId::new(self.y_parity, false) + } + + #[cfg(feature = "k256")] + #[doc(hidden)] + #[deprecated(note = "use `Signature::recid` instead")] + pub const fn recovery_id(&self) -> k256::ecdsa::RecoveryId { + self.recid() + } + + /// Recovers an [`Address`] from this signature and the given message by first prefixing and + /// hashing the message according to [EIP-191](crate::eip191_hash_message). + /// + /// [`Address`]: crate::Address + #[cfg(feature = "k256")] + #[inline] + pub fn recover_address_from_msg>( + &self, + msg: T, + ) -> Result { + self.recover_from_msg(msg).map(|vk| crate::Address::from_public_key(&vk)) + } + + /// Recovers an [`Address`] from this signature and the given prehashed message. + /// + /// [`Address`]: crate::Address + #[cfg(feature = "k256")] + #[inline] + pub fn recover_address_from_prehash( + &self, + prehash: &crate::B256, + ) -> Result { + self.recover_from_prehash(prehash).map(|vk| crate::Address::from_public_key(&vk)) + } + + /// Recovers a [`VerifyingKey`] from this signature and the given message by first prefixing and + /// hashing the message according to [EIP-191](crate::eip191_hash_message). + /// + /// [`VerifyingKey`]: k256::ecdsa::VerifyingKey + #[cfg(feature = "k256")] + #[inline] + pub fn recover_from_msg>( + &self, + msg: T, + ) -> Result { + self.recover_from_prehash(&crate::eip191_hash_message(msg)) + } + + /// Recovers a [`VerifyingKey`] from this signature and the given prehashed message. + /// + /// [`VerifyingKey`]: k256::ecdsa::VerifyingKey + #[cfg(feature = "k256")] + #[inline] + pub fn recover_from_prehash( + &self, + prehash: &crate::B256, + ) -> Result { + let this = self.normalize_s().unwrap_or(*self); + k256::ecdsa::VerifyingKey::recover_from_prehash( + prehash.as_slice(), + &this.to_k256()?, + this.recid(), + ) + .map_err(Into::into) + } + + /// Parses a signature from a byte slice, with a v value + /// + /// # Panics + /// + /// If the slice is not at least 64 bytes long. + #[inline] + pub fn from_bytes_and_parity(bytes: &[u8], parity: bool) -> Self { + let r = U256::from_be_slice(&bytes[..32]); + let s = U256::from_be_slice(&bytes[32..64]); + Self::new(r, s, parity) + } + + /// Returns the `r` component of this signature. + #[inline] + pub const fn r(&self) -> U256 { + self.r + } + + /// Returns the `s` component of this signature. + #[inline] + pub const fn s(&self) -> U256 { + self.s + } + + /// Returns the recovery ID as a `bool`. + #[inline] + pub const fn v(&self) -> bool { + self.y_parity + } + + /// Returns the byte-array representation of this signature. + /// + /// The first 32 bytes are the `r` value, the second 32 bytes the `s` value + /// and the final byte is the `v` value in 'Electrum' notation. + #[inline] + pub fn as_bytes(&self) -> [u8; 65] { + let mut sig = [0u8; 65]; + sig[..32].copy_from_slice(&self.r.to_be_bytes::<32>()); + sig[32..64].copy_from_slice(&self.s.to_be_bytes::<32>()); + sig[64] = 27 + self.y_parity as u8; + sig + } + + /// Sets the recovery ID by normalizing a `v` value. + #[inline] + pub const fn with_parity(self, v: bool) -> Self { + Self { y_parity: v, r: self.r, s: self.s } + } + + /// Length of RLP RS field encoding + #[cfg(feature = "rlp")] + pub fn rlp_rs_len(&self) -> usize { + alloy_rlp::Encodable::length(&self.r) + alloy_rlp::Encodable::length(&self.s) + } + + /// Write R and S to an RLP buffer in progress. + #[cfg(feature = "rlp")] + pub fn write_rlp_rs(&self, out: &mut dyn alloy_rlp::BufMut) { + alloy_rlp::Encodable::encode(&self.r, out); + alloy_rlp::Encodable::encode(&self.s, out); + } + + /// Write the VRS to the output. + #[cfg(feature = "rlp")] + pub fn write_rlp_vrs(&self, out: &mut dyn alloy_rlp::BufMut, v: impl alloy_rlp::Encodable) { + v.encode(out); + self.write_rlp_rs(out); + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for PrimitiveSignature { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Ok(Self::new(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?)) + } +} + +#[cfg(feature = "arbitrary")] +impl proptest::arbitrary::Arbitrary for PrimitiveSignature { + type Parameters = (); + type Strategy = proptest::strategy::Map< + <(U256, U256, bool) as proptest::arbitrary::Arbitrary>::Strategy, + fn((U256, U256, bool)) -> Self, + >; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + use proptest::strategy::Strategy; + proptest::arbitrary::any::<(U256, U256, bool)>() + .prop_map(|(r, s, parity)| Self::new(r, s, parity)) + } +} + +#[cfg(feature = "serde")] +mod signature_serde { + use serde::{Deserialize, Deserializer, Serialize}; + + use crate::{U256, U64}; + + use super::PrimitiveSignature; + + #[derive(Serialize, Deserialize)] + struct HumanReadableRepr { + r: U256, + s: U256, + #[serde(rename = "yParity")] + y_parity: U64, + } + + #[derive(Serialize, Deserialize)] + #[serde(transparent)] + struct NonHumanReadableRepr((U256, U256, U64)); + + impl Serialize for PrimitiveSignature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // if the serializer is human readable, serialize as a map, otherwise as a tuple + if serializer.is_human_readable() { + HumanReadableRepr { + y_parity: U64::from(self.y_parity as u64), + r: self.r, + s: self.s, + } + .serialize(serializer) + } else { + NonHumanReadableRepr((self.r, self.s, U64::from(self.y_parity as u64))) + .serialize(serializer) + } + } + } + + impl<'de> Deserialize<'de> for PrimitiveSignature { + fn deserialize(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let (y_parity, r, s) = if deserializer.is_human_readable() { + let HumanReadableRepr { y_parity, r, s } = <_>::deserialize(deserializer)?; + (y_parity, r, s) + } else { + let NonHumanReadableRepr((r, s, y_parity)) = <_>::deserialize(deserializer)?; + (y_parity, r, s) + }; + + if y_parity > U64::from(1) { + Err(serde::de::Error::custom("invalid y_parity")) + } else { + Ok(Self::new(r, s, y_parity == U64::from(1))) + } + } + } +} + +#[cfg(test)] +#[allow(unused_imports)] +mod tests { + use super::*; + use crate::Bytes; + use core::str::FromStr; + use hex::FromHex; + + #[cfg(feature = "rlp")] + use alloy_rlp::{Decodable, Encodable}; + + #[test] + #[cfg(feature = "k256")] + fn can_recover_tx_sender_not_normalized() { + let sig = PrimitiveSignature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); + let hash = b256!("5eb4f5a33c621f32a8622d5f943b6b102994dfe4e5aebbefe69bb1b2aa0fc93e"); + let expected = address!("0f65fe9276bc9a24ae7083ae28e2660ef72df99e"); + assert_eq!(sig.recover_address_from_prehash(&hash).unwrap(), expected); + } + + #[test] + #[cfg(feature = "k256")] + fn recover_web3_signature() { + // test vector taken from: + // https://web3js.readthedocs.io/en/v1.2.2/web3-eth-accounts.html#sign + let sig = PrimitiveSignature::from_str( + "b91467e570a6466aa9e9876cbcd013baba02900b8979d43fe208a4a4f339f5fd6007e74cd82e037b800186422fc2da167c747ef045e5d18a5f5d4300f8e1a0291c" + ).expect("could not parse signature"); + let expected = address!("2c7536E3605D9C16a7a3D7b1898e529396a65c23"); + assert_eq!(sig.recover_address_from_msg("Some data").unwrap(), expected); + } + + #[test] + fn signature_from_str() { + let s1 = PrimitiveSignature::from_str( + "0xaa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" + ).expect("could not parse 0x-prefixed signature"); + + let s2 = PrimitiveSignature::from_str( + "aa231fbe0ed2b5418e6ba7c19bee2522852955ec50996c02a2fe3e71d30ddaf1645baf4823fea7cb4fcc7150842493847cfb6a6d63ab93e8ee928ee3f61f503500" + ).expect("could not parse non-prefixed signature"); + + assert_eq!(s1, s2); + } + + #[cfg(feature = "serde")] + #[test] + fn deserialize_with_parity() { + let raw_signature_with_y_parity = serde_json::json!({ + "r": "0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0", + "s": "0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05", + "v": "0x1", + "yParity": "0x1" + }); + + let signature: PrimitiveSignature = + serde_json::from_value(raw_signature_with_y_parity).unwrap(); + + let expected = PrimitiveSignature::new( + U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + true, + ); + + assert_eq!(signature, expected); + } + + #[cfg(feature = "serde")] + #[test] + fn serialize_both_parity() { + // this test should be removed if the struct moves to an enum based on tx type + let signature = PrimitiveSignature::new( + U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + true, + ); + + let serialized = serde_json::to_string(&signature).unwrap(); + assert_eq!( + serialized, + r#"{"r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0","s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05","yParity":"0x1"}"# + ); + } + + #[cfg(feature = "serde")] + #[test] + fn serialize_v_only() { + // this test should be removed if the struct moves to an enum based on tx type + let signature = PrimitiveSignature::new( + U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + true, + ); + + let expected = r#"{"r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0","s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05","yParity":"0x1"}"#; + + let serialized = serde_json::to_string(&signature).unwrap(); + assert_eq!(serialized, expected); + } + + #[cfg(feature = "serde")] + #[test] + fn test_bincode_roundtrip() { + let signature = PrimitiveSignature::new( + U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + true, + ); + + let bin = bincode::serialize(&signature).unwrap(); + assert_eq!(bincode::deserialize::(&bin).unwrap(), signature); + } + + #[cfg(feature = "rlp")] + #[test] + fn signature_rlp_encode() { + // Given a Signature instance + let sig = PrimitiveSignature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); + + // Initialize an empty buffer + let mut buf = vec![]; + + // Encode the Signature into the buffer + sig.write_rlp_vrs(&mut buf, sig.v()); + + // Define the expected hex-encoded string + let expected = "80a048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804"; + + // Assert that the encoded buffer matches the expected hex-encoded string + assert_eq!(hex::encode(&buf), expected); + } + + #[cfg(feature = "rlp")] + #[test] + fn signature_rlp_length() { + // Given a Signature instance + let sig = PrimitiveSignature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); + + // Assert that the length of the Signature matches the expected length + assert_eq!(sig.rlp_rs_len() + sig.v().length(), 67); + } + + #[cfg(feature = "rlp")] + #[test] + fn test_rlp_vrs_len() { + let signature = PrimitiveSignature::test_signature(); + assert_eq!(67, signature.rlp_rs_len() + 1); + } + + #[cfg(feature = "rlp")] + #[test] + fn test_encode_and_decode() { + let signature = PrimitiveSignature::test_signature(); + + let mut encoded = Vec::new(); + signature.write_rlp_vrs(&mut encoded, signature.v()); + assert_eq!(encoded.len(), signature.rlp_rs_len() + signature.v().length()); + let decoded = PrimitiveSignature::decode_rlp_vrs(&mut &*encoded, bool::decode).unwrap(); + assert_eq!(signature, decoded); + } + + #[test] + fn test_as_bytes() { + let signature = PrimitiveSignature::new( + U256::from_str( + "18515461264373351373200002665853028612451056578545711640558177340181847433846", + ) + .unwrap(), + U256::from_str( + "46948507304638947509940763649030358759909902576025900602547168820602576006531", + ) + .unwrap(), + false, + ); + + let expected = Bytes::from_hex("0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d831b").unwrap(); + assert_eq!(signature.as_bytes(), **expected); + } +} diff --git a/crates/primitives/src/signature/sig.rs b/crates/primitives/src/signature/sig.rs index 38aca440a..f121c4730 100644 --- a/crates/primitives/src/signature/sig.rs +++ b/crates/primitives/src/signature/sig.rs @@ -1,6 +1,10 @@ #![allow(unknown_lints, unnameable_types)] -use crate::{hex, normalize_v, signature::SignatureError, uint, U256}; +use crate::{ + hex, + signature::{Parity, SignatureError}, + uint, U256, +}; use alloc::vec::Vec; use core::str::FromStr; @@ -11,7 +15,7 @@ const SECP256K1N_ORDER: U256 = /// An Ethereum ECDSA signature. #[derive(Clone, Copy, Debug, Hash, PartialEq, Eq)] pub struct Signature { - y_parity: bool, + v: Parity, r: U256, s: U256, } @@ -26,9 +30,7 @@ impl<'a> TryFrom<&'a [u8]> for Signature { if bytes.len() != 65 { return Err(SignatureError::FromBytes("expected exactly 65 bytes")); } - let parity = - normalize_v(bytes[64] as u64).ok_or(SignatureError::InvalidParity(bytes[64] as u64))?; - Ok(Self::from_bytes_and_parity(&bytes[..64], parity)) + Self::from_bytes_and_parity(&bytes[..64], bytes[64] as u64) } } @@ -72,7 +74,7 @@ impl From for Vec { #[cfg(feature = "k256")] impl From<(k256::ecdsa::Signature, k256::ecdsa::RecoveryId)> for Signature { fn from(value: (k256::ecdsa::Signature, k256::ecdsa::RecoveryId)) -> Self { - Self::from_signature_and_parity(value.0, value.1.is_y_odd()) + Self::from_signature_and_parity(value.0, value.1).unwrap() } } @@ -87,20 +89,16 @@ impl TryFrom for k256::ecdsa::Signature { #[cfg(feature = "rlp")] impl Signature { - /// Decode an RLP-encoded VRS signature. Accepts `decode_parity` closure which allows to - /// customize parity decoding and possibly extract additional data from it (e.g chain_id for - /// legacy signature). - pub fn decode_rlp_vrs( - buf: &mut &[u8], - decode_parity: impl FnOnce(&mut &[u8]) -> alloy_rlp::Result, - ) -> Result { + /// Decode an RLP-encoded VRS signature. + pub fn decode_rlp_vrs(buf: &mut &[u8]) -> Result { use alloy_rlp::Decodable; - let parity = decode_parity(buf)?; + let parity: Parity = Decodable::decode(buf)?; let r = Decodable::decode(buf)?; let s = Decodable::decode(buf)?; - Ok(Self::new(r, s, parity)) + Self::from_rs_and_parity(r, s, parity) + .map_err(|_| alloy_rlp::Error::Custom("attempted to decode invalid field element")) } } @@ -112,12 +110,13 @@ impl Signature { b256!("25e7109ceb98168d95b09b18bbf6b685130e0562f233877d492b94eee0c5b6d1"), false, ) + .unwrap() } /// Instantiate a new signature from `r`, `s`, and `v` values. #[allow(clippy::missing_const_for_fn)] - pub fn new(r: U256, s: U256, v: bool) -> Self { - Self { r, s, y_parity: v } + pub fn new(r: U256, s: U256, v: Parity) -> Self { + Self { r, s, v } } /// Returns the inner ECDSA signature. @@ -137,17 +136,28 @@ impl Signature { /// Instantiate from a signature and recovery id #[cfg(feature = "k256")] - pub fn from_signature_and_parity(sig: k256::ecdsa::Signature, v: bool) -> Self { + pub fn from_signature_and_parity, E: Into>( + sig: k256::ecdsa::Signature, + parity: T, + ) -> Result { let r = U256::from_be_slice(sig.r().to_bytes().as_ref()); let s = U256::from_be_slice(sig.s().to_bytes().as_ref()); - Self { y_parity: v, r, s } + Ok(Self { v: parity.try_into().map_err(Into::into)?, r, s }) } - /// Creates a [`Signature`] from the serialized `r` and `s` scalar values, which - /// comprise the ECDSA signature, alongside a `v` value, used to determine the recovery ID. + /// Creates a [`Signature`] from the serialized `r` and `s` scalar values, which comprise the + /// ECDSA signature, alongside a `v` value, used to determine the recovery ID. #[inline] - pub fn from_scalars_and_parity(r: crate::B256, s: crate::B256, parity: bool) -> Self { - Self::new(U256::from_be_slice(r.as_ref()), U256::from_be_slice(s.as_ref()), parity) + pub fn from_scalars_and_parity, E: Into>( + r: crate::B256, + s: crate::B256, + parity: T, + ) -> Result { + Self::from_rs_and_parity( + U256::from_be_slice(r.as_ref()), + U256::from_be_slice(s.as_ref()), + parity, + ) } /// Normalizes the signature into "low S" form as described in @@ -159,7 +169,7 @@ impl Signature { let s = self.s(); if s > SECP256K1N_ORDER >> 1 { - Some(Self { y_parity: !self.y_parity, r: self.r, s: SECP256K1N_ORDER - s }) + Some(Self { v: self.v.inverted(), r: self.r, s: SECP256K1N_ORDER - s }) } else { None } @@ -169,7 +179,7 @@ impl Signature { #[cfg(feature = "k256")] #[inline] pub const fn recid(&self) -> k256::ecdsa::RecoveryId { - k256::ecdsa::RecoveryId::new(self.y_parity, false) + self.v.recid() } #[cfg(feature = "k256")] @@ -241,10 +251,36 @@ impl Signature { /// /// If the slice is not at least 64 bytes long. #[inline] - pub fn from_bytes_and_parity(bytes: &[u8], parity: bool) -> Self { + pub fn from_bytes_and_parity, E: Into>( + bytes: &[u8], + parity: T, + ) -> Result { let r = U256::from_be_slice(&bytes[..32]); let s = U256::from_be_slice(&bytes[32..64]); - Self::new(r, s, parity) + Self::from_rs_and_parity(r, s, parity) + } + + /// Instantiate from v, r, s. + pub fn from_rs_and_parity, E: Into>( + r: U256, + s: U256, + parity: T, + ) -> Result { + Ok(Self { v: parity.try_into().map_err(Into::into)?, r, s }) + } + + /// Modifies the recovery ID by applying [EIP-155] to a `v` value. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + #[inline] + pub fn with_chain_id(self, chain_id: u64) -> Self { + self.with_parity(self.v.with_chain_id(chain_id)) + } + + /// Modifies the recovery ID by dropping any [EIP-155] v value, converting + /// to a simple parity bool. + pub fn with_parity_bool(self) -> Self { + self.with_parity(self.v.to_parity_bool()) } /// Returns the `r` component of this signature. @@ -259,10 +295,29 @@ impl Signature { self.s } - /// Returns the recovery ID as a `bool`. + /// Returns the recovery ID as a `u8`. + #[inline] + pub const fn v(&self) -> Parity { + self.v + } + + /// Returns the chain ID associated with the V value, if this signature is + /// replay-protected by [EIP-155]. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 + pub const fn chain_id(&self) -> Option { + self.v.chain_id() + } + + /// Returns true if the signature is replay-protected by [EIP-155]. + /// + /// This is true if the V value is 35 or greater. Values less than 35 are + /// either not replay protected (27/28), or are invalid. + /// + /// [EIP-155]: https://eips.ethereum.org/EIPS/eip-155 #[inline] - pub const fn v(&self) -> bool { - self.y_parity + pub const fn has_eip155_value(&self) -> bool { + self.v().has_eip155_value() } /// Returns the byte-array representation of this signature. @@ -274,14 +329,14 @@ impl Signature { let mut sig = [0u8; 65]; sig[..32].copy_from_slice(&self.r.to_be_bytes::<32>()); sig[32..64].copy_from_slice(&self.s.to_be_bytes::<32>()); - sig[64] = 27 + self.y_parity as u8; + sig[64] = self.v.y_parity_byte_non_eip155().unwrap_or(self.v.y_parity_byte()); sig } /// Sets the recovery ID by normalizing a `v` value. #[inline] - pub const fn with_parity(self, v: bool) -> Self { - Self { y_parity: v, r: self.r, s: self.s } + pub fn with_parity>(self, parity: T) -> Self { + Self { v: parity.into(), r: self.r, s: self.s } } /// Length of RLP RS field encoding @@ -290,6 +345,12 @@ impl Signature { alloy_rlp::Encodable::length(&self.r) + alloy_rlp::Encodable::length(&self.s) } + #[cfg(feature = "rlp")] + /// Length of RLP V field encoding + pub fn rlp_vrs_len(&self) -> usize { + self.rlp_rs_len() + alloy_rlp::Encodable::length(&self.v) + } + /// Write R and S to an RLP buffer in progress. #[cfg(feature = "rlp")] pub fn write_rlp_rs(&self, out: &mut dyn alloy_rlp::BufMut) { @@ -297,95 +358,244 @@ impl Signature { alloy_rlp::Encodable::encode(&self.s, out); } - /// Write the VRS to the output. + /// Write the V to an RLP buffer without using EIP-155. #[cfg(feature = "rlp")] - pub fn write_rlp_vrs(&self, out: &mut dyn alloy_rlp::BufMut, v: impl alloy_rlp::Encodable) { - v.encode(out); + pub fn write_rlp_v(&self, out: &mut dyn alloy_rlp::BufMut) { + alloy_rlp::Encodable::encode(&self.v, out); + } + + /// Write the VRS to the output. The V will always be 27 or 28. + #[cfg(feature = "rlp")] + pub fn write_rlp_vrs(&self, out: &mut dyn alloy_rlp::BufMut) { + self.write_rlp_v(out); self.write_rlp_rs(out); } } -#[cfg(feature = "arbitrary")] -impl<'a> arbitrary::Arbitrary<'a> for Signature { - fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { - Ok(Self::new(u.arbitrary()?, u.arbitrary()?, u.arbitrary()?)) +#[cfg(feature = "rlp")] +impl alloy_rlp::Encodable for Signature { + fn encode(&self, out: &mut dyn alloy_rlp::BufMut) { + alloy_rlp::Header { list: true, payload_length: self.rlp_vrs_len() }.encode(out); + self.write_rlp_vrs(out); + } + + fn length(&self) -> usize { + let payload_length = self.rlp_vrs_len(); + payload_length + alloy_rlp::length_of_length(payload_length) } } -#[cfg(feature = "arbitrary")] -impl proptest::arbitrary::Arbitrary for Signature { - type Parameters = (); - type Strategy = proptest::strategy::Map< - <(U256, U256, bool) as proptest::arbitrary::Arbitrary>::Strategy, - fn((U256, U256, bool)) -> Self, - >; +#[cfg(feature = "rlp")] +impl alloy_rlp::Decodable for Signature { + fn decode(buf: &mut &[u8]) -> Result { + let header = alloy_rlp::Header::decode(buf)?; + let pre_len = buf.len(); + let decoded = Self::decode_rlp_vrs(buf)?; + let consumed = pre_len - buf.len(); + if consumed != header.payload_length { + return Err(alloy_rlp::Error::Custom("consumed incorrect number of bytes")); + } - fn arbitrary_with((): Self::Parameters) -> Self::Strategy { - use proptest::strategy::Strategy; - proptest::arbitrary::any::<(U256, U256, bool)>() - .prop_map(|(r, s, parity)| Self::new(r, s, parity)) + Ok(decoded) } } #[cfg(feature = "serde")] -mod signature_serde { - use serde::{Deserialize, Deserializer, Serialize}; +impl serde::Serialize for Signature { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + // if the serializer is human readable, serialize as a map, otherwise as a tuple + if serializer.is_human_readable() { + use serde::ser::SerializeMap; + + let mut map = serializer.serialize_map(Some(3))?; + + map.serialize_entry("r", &self.r)?; + map.serialize_entry("s", &self.s)?; + + match self.v { + Parity::Eip155(v) => map.serialize_entry("v", &crate::U64::from(v))?, + Parity::NonEip155(b) => { + map.serialize_entry("v", &crate::U64::from(b as u8 + 27))? + } + Parity::Parity(true) => map.serialize_entry("yParity", "0x1")?, + Parity::Parity(false) => map.serialize_entry("yParity", "0x0")?, + } + map.end() + } else { + use serde::ser::SerializeTuple; - use crate::{U256, U64}; + let mut tuple = serializer.serialize_tuple(3)?; + tuple.serialize_element(&self.r)?; + tuple.serialize_element(&self.s)?; + tuple.serialize_element(&crate::U64::from(self.v.to_u64()))?; + tuple.end() + } + } +} - use super::Signature; +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for Signature { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + use serde::de::MapAccess; + + enum Field { + R, + S, + V, + YParity, + Unknown, + } - #[derive(Serialize, Deserialize)] - struct HumanReadableRepr { - r: U256, - s: U256, - #[serde(rename = "yParity")] - y_parity: U64, - } - - #[derive(Serialize, Deserialize)] - #[serde(transparent)] - struct NonHumanReadableRepr((U256, U256, U64)); - - impl Serialize for Signature { - fn serialize(&self, serializer: S) -> Result - where - S: serde::Serializer, - { - // if the serializer is human readable, serialize as a map, otherwise as a tuple - if serializer.is_human_readable() { - HumanReadableRepr { - y_parity: U64::from(self.y_parity as u64), - r: self.r, - s: self.s, + impl<'de> serde::Deserialize<'de> for Field { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + struct FieldVisitor; + + impl serde::de::Visitor<'_> for FieldVisitor { + type Value = Field; + + fn expecting( + &self, + formatter: &mut core::fmt::Formatter<'_>, + ) -> core::fmt::Result { + formatter.write_str("v, r, s, or yParity") + } + + fn visit_str(self, value: &str) -> Result + where + E: serde::de::Error, + { + match value { + "r" => Ok(Field::R), + "s" => Ok(Field::S), + "v" => Ok(Field::V), + "yParity" => Ok(Field::YParity), + _ => Ok(Field::Unknown), + } + } } - .serialize(serializer) - } else { - NonHumanReadableRepr((self.r, self.s, U64::from(self.y_parity as u64))) - .serialize(serializer) + deserializer.deserialize_identifier(FieldVisitor) } } - } - impl<'de> Deserialize<'de> for Signature { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let (y_parity, r, s) = if deserializer.is_human_readable() { - let HumanReadableRepr { y_parity, r, s } = <_>::deserialize(deserializer)?; - (y_parity, r, s) - } else { - let NonHumanReadableRepr((r, s, y_parity)) = <_>::deserialize(deserializer)?; - (y_parity, r, s) - }; - - if y_parity > U64::from(1) { - Err(serde::de::Error::custom("invalid y_parity")) - } else { - Ok(Self::new(r, s, y_parity == U64::from(1))) + struct MapVisitor; + impl<'de> serde::de::Visitor<'de> for MapVisitor { + type Value = Signature; + + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("a JSON signature object containing r, s, and v or yParity") + } + + fn visit_map(self, mut map: A) -> Result + where + A: MapAccess<'de>, + { + let mut v: Option = None; + let mut r = None; + let mut s = None; + + while let Some(key) = map.next_key()? { + match key { + Field::V => { + let value: crate::U64 = map.next_value()?; + let parity = value.try_into().map_err(|_| { + serde::de::Error::invalid_value( + serde::de::Unexpected::Unsigned(value.as_limbs()[0]), + &"a valid v value matching the range 0 | 1 | 27 | 28 | 35..", + ) + })?; + v = Some(parity); + } + Field::YParity => { + let value: crate::Uint<1, 1> = map.next_value()?; + if v.is_none() { + v = Some(value.into()); + } + } + Field::R => { + let value: U256 = map.next_value()?; + r = Some(value); + } + Field::S => { + let value: U256 = map.next_value()?; + s = Some(value); + } + _ => {} + } + } + + let v = v.ok_or_else(|| serde::de::Error::missing_field("v"))?; + let r = r.ok_or_else(|| serde::de::Error::missing_field("r"))?; + let s = s.ok_or_else(|| serde::de::Error::missing_field("s"))?; + + Signature::from_rs_and_parity(r, s, v).map_err(serde::de::Error::custom) } } + + struct TupleVisitor; + impl<'de> serde::de::Visitor<'de> for TupleVisitor { + type Value = Signature; + + fn expecting(&self, formatter: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { + formatter.write_str("a tuple containing r, s, and v") + } + + fn visit_seq(self, mut seq: A) -> Result + where + A: serde::de::SeqAccess<'de>, + { + let r = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(0, &self))?; + let s = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(1, &self))?; + let v: crate::U64 = seq + .next_element()? + .ok_or_else(|| serde::de::Error::invalid_length(2, &self))?; + + Signature::from_rs_and_parity(r, s, v).map_err(serde::de::Error::custom) + } + } + + if deserializer.is_human_readable() { + deserializer.deserialize_map(MapVisitor) + } else { + deserializer.deserialize_tuple(3, TupleVisitor) + } + } +} + +#[cfg(feature = "arbitrary")] +impl<'a> arbitrary::Arbitrary<'a> for Signature { + fn arbitrary(u: &mut arbitrary::Unstructured<'a>) -> arbitrary::Result { + Self::from_rs_and_parity(u.arbitrary()?, u.arbitrary()?, u.arbitrary::()?) + .map_err(|_| arbitrary::Error::IncorrectFormat) + } +} + +#[cfg(feature = "arbitrary")] +impl proptest::arbitrary::Arbitrary for Signature { + type Parameters = (); + type Strategy = proptest::strategy::FilterMap< + <(U256, U256, Parity) as proptest::arbitrary::Arbitrary>::Strategy, + fn((U256, U256, Parity)) -> Option, + >; + + fn arbitrary_with((): Self::Parameters) -> Self::Strategy { + use proptest::strategy::Strategy; + proptest::arbitrary::any::<(U256, U256, Parity)>() + .prop_filter_map("invalid signature", |(r, s, parity)| { + Self::from_rs_and_parity(r, s, parity).ok() + }) } } @@ -434,6 +644,29 @@ mod tests { assert_eq!(s1, s2); } + #[cfg(feature = "serde")] + #[test] + fn deserialize_without_parity() { + let raw_signature_without_y_parity = r#"{ + "r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0", + "s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05", + "v":"0x1" + }"#; + + let signature: Signature = serde_json::from_str(raw_signature_without_y_parity).unwrap(); + + let expected = Signature::from_rs_and_parity( + U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + 1, + ) + .unwrap(); + + assert_eq!(signature, expected); + } + #[cfg(feature = "serde")] #[test] fn deserialize_with_parity() { @@ -446,13 +679,14 @@ mod tests { let signature: Signature = serde_json::from_value(raw_signature_with_y_parity).unwrap(); - let expected = Signature::new( + let expected = Signature::from_rs_and_parity( U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") .unwrap(), U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") .unwrap(), - true, - ); + 1, + ) + .unwrap(); assert_eq!(signature, expected); } @@ -461,13 +695,14 @@ mod tests { #[test] fn serialize_both_parity() { // this test should be removed if the struct moves to an enum based on tx type - let signature = Signature::new( + let signature = Signature::from_rs_and_parity( U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") .unwrap(), U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") .unwrap(), - true, - ); + 1, + ) + .unwrap(); let serialized = serde_json::to_string(&signature).unwrap(); assert_eq!( @@ -480,13 +715,14 @@ mod tests { #[test] fn serialize_v_only() { // this test should be removed if the struct moves to an enum based on tx type - let signature = Signature::new( + let signature = Signature::from_rs_and_parity( U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") .unwrap(), U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") .unwrap(), - true, - ); + 1, + ) + .unwrap(); let expected = r#"{"r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0","s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05","yParity":"0x1"}"#; @@ -494,21 +730,48 @@ mod tests { assert_eq!(serialized, expected); } + #[cfg(feature = "serde")] + #[test] + fn serialize_v_hex() { + let s = r#"{"r":"0x3d43270611ffb1a10fcab841e636e355a787151969b920cf10fef48d3a61aac3","s":"0x11336489e3050e3ec017079dfe16582ce3d167559bcaa8383b665b3fda4eb963","v":"0x1b"}"#; + + let sig = serde_json::from_str::(s).unwrap(); + let serialized = serde_json::to_string(&sig).unwrap(); + assert_eq!(serialized, s); + } + #[cfg(feature = "serde")] #[test] fn test_bincode_roundtrip() { - let signature = Signature::new( + let signature = Signature::from_rs_and_parity( U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") .unwrap(), U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") .unwrap(), - true, - ); + 1, + ) + .unwrap(); let bin = bincode::serialize(&signature).unwrap(); assert_eq!(bincode::deserialize::(&bin).unwrap(), signature); } + #[cfg(feature = "rlp")] + #[test] + fn signature_rlp_decode() { + // Given a hex-encoded byte sequence + let bytes = crate::hex!("f84301a048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a010002cef538bc0c8e21c46080634a93e082408b0ad93f4a7207e63ec5463793d"); + + // Decode the byte sequence into a Signature instance + let result = Signature::decode(&mut &bytes[..]).unwrap(); + + // Assert that the decoded Signature matches the expected Signature + assert_eq!( + result, + Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a3664935310002cef538bc0c8e21c46080634a93e082408b0ad93f4a7207e63ec5463793d01").unwrap() + ); + } + #[cfg(feature = "rlp")] #[test] fn signature_rlp_encode() { @@ -519,10 +782,10 @@ mod tests { let mut buf = vec![]; // Encode the Signature into the buffer - sig.write_rlp_vrs(&mut buf, sig.v()); + sig.encode(&mut buf); // Define the expected hex-encoded string - let expected = "80a048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804"; + let expected = "f8431ba048b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353a0efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c804"; // Assert that the encoded buffer matches the expected hex-encoded string assert_eq!(hex::encode(&buf), expected); @@ -535,14 +798,14 @@ mod tests { let sig = Signature::from_str("48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353efffd310ac743f371de3b9f7f9cb56c0b28ad43601b4ab949f53faa07bd2c8041b").unwrap(); // Assert that the length of the Signature matches the expected length - assert_eq!(sig.rlp_rs_len() + sig.v().length(), 67); + assert_eq!(sig.length(), 69); } #[cfg(feature = "rlp")] #[test] fn test_rlp_vrs_len() { let signature = Signature::test_signature(); - assert_eq!(67, signature.rlp_rs_len() + 1); + assert_eq!(67, signature.rlp_vrs_len()); } #[cfg(feature = "rlp")] @@ -551,9 +814,9 @@ mod tests { let signature = Signature::test_signature(); let mut encoded = Vec::new(); - signature.write_rlp_vrs(&mut encoded, signature.v()); - assert_eq!(encoded.len(), signature.rlp_rs_len() + signature.v().length()); - let decoded = Signature::decode_rlp_vrs(&mut &*encoded, bool::decode).unwrap(); + signature.encode(&mut encoded); + assert_eq!(encoded.len(), signature.length()); + let decoded = Signature::decode(&mut &*encoded).unwrap(); assert_eq!(signature, decoded); } @@ -568,7 +831,7 @@ mod tests { "46948507304638947509940763649030358759909902576025900602547168820602576006531", ) .unwrap(), - false, + Parity::Parity(false), ); let expected = Bytes::from_hex("0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d831b").unwrap(); diff --git a/crates/primitives/src/signature/utils.rs b/crates/primitives/src/signature/utils.rs index 339a6270b..bba5f9d03 100644 --- a/crates/primitives/src/signature/utils.rs +++ b/crates/primitives/src/signature/utils.rs @@ -43,6 +43,43 @@ const fn is_valid_v(v: u64) -> bool { ) } +/// Normalizes a `v` value, respecting raw, legacy, and EIP-155 values. +/// +/// This function covers the entire u64 range, producing v-values as follows: +/// - 0-26 - raw/bare. 0-3 are legal. In order to ensure that all values are covered, we also handle +/// 4-26 here by returning v % 4. +/// - 27-34 - legacy. 27-30 are legal. By legacy bitcoin convention range 27-30 signals uncompressed +/// pubkeys, while 31-34 signals compressed pubkeys. We do not respect the compression convention. +/// All Ethereum keys are uncompressed. +/// - 35+ - EIP-155. By EIP-155 convention, `v = 35 + CHAIN_ID * 2 + 0/1` We return (v-1 % 2) here. +/// +/// NB: raw and legacy support values 2, and 3, while EIP-155 does not. +/// Recovery values of 2 and 3 are unlikely to occur in practice. In the +/// vanishingly unlikely event that you encounter an EIP-155 signature with a +/// recovery value of 2 or 3, you should normalize out of band. +#[cfg(feature = "k256")] +#[inline] +pub(crate) const fn normalize_v_to_recid(v: u64) -> k256::ecdsa::RecoveryId { + let byte = normalize_v_to_byte(v); + debug_assert!(byte <= k256::ecdsa::RecoveryId::MAX); + match k256::ecdsa::RecoveryId::from_byte(byte) { + Some(recid) => recid, + None => unsafe { core::hint::unreachable_unchecked() }, + } +} + +/// Normalize the v value to a single byte. +pub(crate) const fn normalize_v_to_byte(v: u64) -> u8 { + match v { + // Case 1: raw/bare + 0..=26 => (v % 4) as u8, + // Case 2: non-EIP-155 v value + 27..=34 => ((v - 27) % 4) as u8, + // Case 3: EIP-155 V value + 35.. => ((v - 1) % 2) as u8, + } +} + #[cfg(test)] mod test { use super::*; @@ -69,4 +106,11 @@ mod test { assert_eq!(normalize_v(v), Some((v - 35) % 2 != 0)); } } + + #[test] + #[cfg(feature = "k256")] + fn normalizes_v_to_recid() { + assert_eq!(normalize_v_to_recid(27), k256::ecdsa::RecoveryId::from_byte(0).unwrap()); + assert_eq!(normalize_v_to_recid(28), k256::ecdsa::RecoveryId::from_byte(1).unwrap()); + } }