From e6e0f3030321da2f1dc97a0381456d64458a550b Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Fri, 14 Feb 2025 13:12:18 -0500 Subject: [PATCH 1/2] feat: erc2098 signature representation --- .../primitives/src/signature/primitive_sig.rs | 116 ++++++++++++++++++ 1 file changed, 116 insertions(+) diff --git a/crates/primitives/src/signature/primitive_sig.rs b/crates/primitives/src/signature/primitive_sig.rs index 23ee62178..0736a337c 100644 --- a/crates/primitives/src/signature/primitive_sig.rs +++ b/crates/primitives/src/signature/primitive_sig.rs @@ -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 + /// + /// # 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::() == 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 + /// + /// # 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 { @@ -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 + ); + } } From eb374e0d86de2d4558dbd1988167134040630687 Mon Sep 17 00:00:00 2001 From: Oliver Nordbjerg Date: Tue, 25 Feb 2025 16:27:31 +0100 Subject: [PATCH 2/2] fix: correctly normalize s in `as_erc2098` --- crates/primitives/src/signature/primitive_sig.rs | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/crates/primitives/src/signature/primitive_sig.rs b/crates/primitives/src/signature/primitive_sig.rs index 0736a337c..5c958f227 100644 --- a/crates/primitives/src/signature/primitive_sig.rs +++ b/crates/primitives/src/signature/primitive_sig.rs @@ -153,6 +153,8 @@ impl PrimitiveSignature { /// Normalizes the signature into "low S" form as described in /// [BIP 0062: Dealing with Malleability][1]. /// + /// If `s` is already normalized, returns `None`. + /// /// [1]: https://github.com/bitcoin/bips/blob/master/bip-0062.mediawiki #[inline] pub fn normalize_s(&self) -> Option { @@ -303,13 +305,9 @@ impl PrimitiveSignature { /// in the top bit of the `s` value, as described in ERC-2098. /// /// See - /// - /// # 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(); + let normalized_self = self.normalize_s().unwrap_or(*self); // 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