Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(primitives): add Signature type and utils #459

Merged
merged 20 commits into from
Jan 4, 2024
Merged
Show file tree
Hide file tree
Changes from 18 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 4 additions & 11 deletions crates/primitives/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@ arbitrary = { workspace = true, optional = true }
derive_arbitrary = { workspace = true, optional = true }
proptest = { workspace = true, optional = true }
proptest-derive = { workspace = true, optional = true }
k256 = { version = "0.13.2", optional = true }

# postgres
postgres-types = { workspace = true, optional = true }
Expand All @@ -67,6 +68,7 @@ std = [
"proptest?/std",
"rand?/std",
"serde?/std",
"k256?/std"
]
postgres = ["dep:postgres-types", "std", "ruint/postgres"]
tiny-keccak = []
Expand All @@ -76,17 +78,8 @@ rand = ["dep:rand", "getrandom", "ruint/rand"]
rlp = ["dep:alloy-rlp", "ruint/alloy-rlp"]
serde = ["dep:serde", "bytes/serde", "hex/serde", "ruint/serde"]
ssz = ["dep:ethereum_ssz", "std", "ruint/ssz"]
arbitrary = [
"std",
"dep:arbitrary",
"dep:derive_arbitrary",
"dep:proptest",
"dep:proptest-derive",
"ruint/arbitrary",
"ruint/proptest",
"ethereum_ssz?/arbitrary",
]

arbitrary = ["std", "dep:arbitrary", "dep:derive_arbitrary", "dep:proptest", "dep:proptest-derive", "ruint/arbitrary", "ruint/proptest", "ethereum_ssz?/arbitrary"]
k256 = ["dep:k256"]
# `const-hex` compatibility feature for `hex`.
# Should not be needed most of the time.
hex-compat = ["hex/hex"]
10 changes: 10 additions & 0 deletions crates/primitives/src/bits/address.rs
Original file line number Diff line number Diff line change
Expand Up @@ -425,6 +425,16 @@ impl Address {
let digest = keccak256(pubkey);
Address::from_slice(&digest[12..])
}

#[cfg(feature = "k256")]
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
/// Converts an ECDSA public key to its corresponding Ethereum address.
#[inline]
pub fn from_public_key(pubkey: &k256::ecdsa::VerifyingKey) -> Self {
DaniPopes marked this conversation as resolved.
Show resolved Hide resolved
use k256::elliptic_curve::sec1::ToEncodedPoint;
let affine: &k256::AffinePoint = pubkey.as_ref();
let encoded = affine.to_encoded_point(false);
Self::from_raw_public_key(&encoded.as_bytes()[1..])
}
}

#[cfg(test)]
Expand Down
11 changes: 11 additions & 0 deletions crates/primitives/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,17 @@ pub use log::Log;
mod signed;
pub use signed::{BigIntConversionError, ParseSignedError, Sign, Signed};

mod signature;
pub use signature::{to_eip155_v, Parity, SignatureError};

/// An ECDSA Signature, consisting of V, R, and S.
#[cfg(feature = "k256")]
pub type Signature = signature::Signature<k256::ecdsa::Signature>;

/// An ECDSA Signature, consisting of V, R, and S.
#[cfg(not(feature = "k256"))]
pub type Signature = signature::Signature<()>;

pub mod utils;
pub use utils::{eip191_hash_message, keccak256};

Expand Down
62 changes: 62 additions & 0 deletions crates/primitives/src/signature/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
use core::convert::Infallible;

/// Errors in signature parsing or verification.
#[derive(Debug)]
#[cfg_attr(not(feature = "k256"), derive(Copy, Clone))]
pub enum SignatureError {
/// Error converting from bytes.
FromBytes(&'static str),

/// Error converting hex to bytes.
FromHex(hex::FromHexError),

/// Invalid parity.
InvalidParity(u64),

/// k256 error
#[cfg(feature = "k256")]
K256(k256::ecdsa::Error),
}

#[cfg(feature = "k256")]
impl From<k256::ecdsa::Error> for SignatureError {
fn from(err: k256::ecdsa::Error) -> Self {
Self::K256(err)
}
}

impl From<hex::FromHexError> for SignatureError {
fn from(err: hex::FromHexError) -> Self {
Self::FromHex(err)
}
}

#[cfg(feature = "std")]
impl std::error::Error for SignatureError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
#[cfg(feature = "k256")]
SignatureError::K256(e) => Some(e),
SignatureError::FromHex(e) => Some(e),
_ => None,
}
}
}

impl core::fmt::Display for SignatureError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
match self {
#[cfg(feature = "k256")]
SignatureError::K256(e) => e.fmt(f),
SignatureError::FromBytes(e) => f.write_str(e),
SignatureError::FromHex(e) => e.fmt(f),
SignatureError::InvalidParity(v) => write!(f, "invalid parity: {}", v),
}
}
}

impl From<Infallible> for SignatureError {
fn from(_: Infallible) -> Self {
unreachable!()
}
}
11 changes: 11 additions & 0 deletions crates/primitives/src/signature/mod.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
mod error;
pub use error::SignatureError;

mod parity;
pub use parity::Parity;

mod sig;
pub(crate) use sig::Signature;

mod utils;
pub use utils::to_eip155_v;
198 changes: 198 additions & 0 deletions crates/primitives/src/signature/parity.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,198 @@
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(Debug, Copy, Clone, PartialEq, Eq)]
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),
}

#[cfg(feature = "k256")]
impl From<k256::ecdsa::RecoveryId> for Parity {
fn from(value: k256::ecdsa::RecoveryId) -> Self {
Self::Parity(value.is_y_odd())
}
}

impl TryFrom<U64> for Parity {
type Error = <Self as TryFrom<u64>>::Error;
fn try_from(value: U64) -> Result<Self, Self::Error> {
value.as_limbs()[0].try_into()
}
}

impl From<Uint<1, 1>> for Parity {
fn from(value: Uint<1, 1>) -> Self {
Parity::Parity(!value.is_zero())
}
}

impl From<bool> for Parity {
fn from(value: bool) -> Self {
Parity::Parity(value)
}
}

impl TryFrom<u64> for Parity {
type Error = SignatureError;

fn try_from(value: u64) -> Result<Self, Self::Error> {
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 {
/// Get the chain_id of the V value, if any.
pub const fn chain_id(&self) -> Option<ChainId> {
match *self {
Parity::Eip155(mut v @ 35..) => {
if v % 2 == 0 {
v -= 1;
}
v -= 35;
Some(v / 2)
}
_ => None,
}
}

/// Return the y-parity as a boolean.
pub const fn y_parity(&self) -> bool {
match self {
Parity::Eip155(v @ 0..=34) => *v % 2 == 1,
Parity::Eip155(v) => (*v ^ 1) % 2 == 0,
Parity::NonEip155(b) | Parity::Parity(b) => *b,
}
}

/// Return the y-parity as 0 or 1
pub const fn y_parity_byte(&self) -> u8 {
self.y_parity() as u8
}

/// Invert the parity.
pub fn inverted(&self) -> Self {
match self {
Parity::Parity(b) => Parity::Parity(!b),
Parity::NonEip155(b) => Parity::NonEip155(!b),
Parity::Eip155(v @ 0..=34) => Parity::Eip155(if v % 2 == 0 { v - 1 } else { v + 1 }),
Parity::Eip155(v @ 35..) => Parity::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 fn strip_chain_id(&self) -> Self {
match self {
Parity::Eip155(v) => Parity::NonEip155(v % 2 == 1),
_ => *self,
}
}

/// Apply EIP 155 to the V value. This is a nop for parity values.
pub const fn with_chain_id(self, chain_id: ChainId) -> Self {
let parity = match self {
Parity::Eip155(v) => normalize_v_to_byte(v) == 1,
Parity::NonEip155(b) => b,
Parity::Parity(_) => return self,
};

Self::Eip155(to_eip155_v(parity as u8, chain_id))
}

#[cfg(feature = "k256")]
/// Determine the recovery id.
prestwich marked this conversation as resolved.
Show resolved Hide resolved
pub const fn recid(&self) -> k256::ecdsa::RecoveryId {
let recid_opt = match self {
Parity::Eip155(v) => Some(crate::signature::utils::normalize_v(*v)),
Parity::NonEip155(b) | Parity::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) -> Parity {
Parity::Parity(self.y_parity())
}
}

#[cfg(feature = "rlp")]
impl alloy_rlp::Encodable for Parity {
fn encode(&self, out: &mut dyn alloy_rlp::BufMut) {
match self {
Parity::Eip155(v) => v.encode(out),
Parity::NonEip155(v) => (*v as u8 + 27).encode(out),
Parity::Parity(b) => b.encode(out),
}
}

fn length(&self) -> usize {
match self {
Parity::Eip155(v) => v.length(),
Parity::NonEip155(_) => 0u8.length(),
Parity::Parity(v) => v.length(),
}
}
}

#[cfg(feature = "rlp")]
impl alloy_rlp::Decodable for Parity {
fn decode(buf: &mut &[u8]) -> Result<Self, alloy_rlp::Error> {
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 {
#[cfg(feature = "rlp")]
#[test]
fn basic_rlp() {
use crate::{hex, Parity};

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());
}
}
}
Loading