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: erc2098 signature representation #874

Merged
merged 3 commits into from
Feb 25, 2025
Merged
Changes from 2 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
116 changes: 116 additions & 0 deletions crates/primitives/src/signature/primitive_sig.rs
Original file line number Diff line number Diff line change
Expand Up @@ -278,6 +278,50 @@ impl PrimitiveSignature {
sig
}

/// Decode the signature from the ERC-2098 compact representation.
///
/// The first 32 bytes are the `r` value, and the next 32 bytes are the `s` value with `yParity`
/// in the top bit of the `s` value, as described in ERC-2098.
///
/// See <https://eips.ethereum.org/EIPS/eip-2098>
///
/// # Panics
///
/// If the slice is not at least 64 bytes long.
pub fn from_erc2098(bytes: &[u8]) -> Self {
let r = U256::from_be_slice(&bytes[..32]);
let y_and_s = U256::from_be_slice(&bytes[32..64]);
let y_parity = U256::from(y_and_s >> 255u8).to::<u8>() == 1;
let s = y_and_s & (U256::from(U256::from(1) << 255) - U256::from(1));

Self { y_parity, r, s }
}

/// Returns the ERC-2098 compact representation of this signature.
///
/// The first 32 bytes are the `r` value, and the next 32 bytes are the `s` value with `yParity`
/// in the top bit of the `s` value, as described in ERC-2098.
///
/// See <https://eips.ethereum.org/EIPS/eip-2098>
///
/// # Panics
///
/// Panics if `s` is invalid for any of the known Ethereum parity encodings.
#[inline]
pub fn as_erc2098(&self) -> [u8; 64] {
let normalized_self = self.normalize_s().unwrap();

// The top bit of the `s` parameters is always 0, due to the use of canonical
// signatures which flip the solution parity to prevent negative values, which was
// introduced as a constraint in Homestead.
let y_and_s: U256 = (U256::from(normalized_self.y_parity as u8) << 255) | normalized_self.s;

let mut sig = [0u8; 64];
sig[..32].copy_from_slice(&normalized_self.r().to_be_bytes::<32>());
sig[32..64].copy_from_slice(&y_and_s.to_be_bytes::<32>());
sig
}

/// Sets the recovery ID by normalizing a `v` value.
#[inline]
pub const fn with_parity(self, v: bool) -> Self {
Expand Down Expand Up @@ -587,4 +631,76 @@ mod tests {
let expected = Bytes::from_hex("0x28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa63627667cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d831b").unwrap();
assert_eq!(signature.as_bytes(), **expected);
}

#[test]
fn test_as_erc2098_y_false() {
let signature = PrimitiveSignature::new(
U256::from_str(
"47323457007453657207889730243826965761922296599680473886588287015755652701072",
)
.unwrap(),
U256::from_str(
"57228803202727131502949358313456071280488184270258293674242124340113824882788",
)
.unwrap(),
false,
);

let expected = Bytes::from_hex("0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b907e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064").unwrap();
assert_eq!(signature.as_erc2098(), **expected);
}

#[test]
fn test_as_erc2098_y_true() {
let signature = PrimitiveSignature::new(
U256::from_str("0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76")
.unwrap(),
U256::from_str("0x139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793")
.unwrap(),
true,
);

let expected = Bytes::from_hex("0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793").unwrap();
assert_eq!(signature.as_erc2098(), **expected);
}

#[test]
fn from_from_erc2098_y_false() {
let expected = PrimitiveSignature::new(
U256::from_str(
"47323457007453657207889730243826965761922296599680473886588287015755652701072",
)
.unwrap(),
U256::from_str(
"57228803202727131502949358313456071280488184270258293674242124340113824882788",
)
.unwrap(),
false,
);

assert_eq!(
PrimitiveSignature::from_erc2098(
&bytes!("0x68a020a209d3d56c46f38cc50a33f704f4a9a10a59377f8dd762ac66910e9b907e865ad05c4035ab5792787d4a0297a43617ae897930a6fe4d822b8faea52064")
),
expected
);
}

#[test]
fn test_from_erc2098_y_true() {
let expected = PrimitiveSignature::new(
U256::from_str("0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76")
.unwrap(),
U256::from_str("0x139c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793")
.unwrap(),
true,
);

assert_eq!(
PrimitiveSignature::from_erc2098(
&bytes!("0x9328da16089fcba9bececa81663203989f2df5fe1faa6291a45381c81bd17f76939c6d6b623b42da56557e5e734a43dc83345ddfadec52cbe24d0cc64f550793")
),
expected
);
}
}
Loading