From d095db50c4c430da61a0b3d683405fce1726651b Mon Sep 17 00:00:00 2001 From: Dan Cline <6798349+Rjected@users.noreply.github.com> Date: Mon, 17 Jul 2023 07:06:26 -0400 Subject: [PATCH] feat: add `yParity` to rpc signatures (#3800) --- .../rpc/rpc-types/src/eth/transaction/mod.rs | 44 ++++- .../src/eth/transaction/signature.rs | 184 +++++++++++++++++- 2 files changed, 225 insertions(+), 3 deletions(-) diff --git a/crates/rpc/rpc-types/src/eth/transaction/mod.rs b/crates/rpc/rpc-types/src/eth/transaction/mod.rs index c61d6080b9..24005376c8 100644 --- a/crates/rpc/rpc-types/src/eth/transaction/mod.rs +++ b/crates/rpc/rpc-types/src/eth/transaction/mod.rs @@ -182,6 +182,8 @@ impl Transaction { #[cfg(test)] mod tests { + use crate::eth::transaction::signature::Parity; + use super::*; #[test] @@ -198,7 +200,12 @@ mod tests { gas_price: Some(U128::from(9)), gas: U256::from(10), input: Bytes::from(vec![11, 12, 13]), - signature: Some(Signature { v: U256::from(14), r: U256::from(14), s: U256::from(14) }), + signature: Some(Signature { + v: U256::from(14), + r: U256::from(14), + s: U256::from(14), + y_parity: None, + }), chain_id: Some(U64::from(17)), access_list: None, transaction_type: Some(U64::from(20)), @@ -213,4 +220,39 @@ mod tests { let deserialized: Transaction = serde_json::from_str(&serialized).unwrap(); assert_eq!(transaction, deserialized); } + + #[test] + fn serde_transaction_with_parity_bit() { + let transaction = Transaction { + hash: H256::from_low_u64_be(1), + nonce: U256::from(2), + block_hash: Some(H256::from_low_u64_be(3)), + block_number: Some(U256::from(4)), + transaction_index: Some(U256::from(5)), + from: Address::from_low_u64_be(6), + to: Some(Address::from_low_u64_be(7)), + value: U256::from(8), + gas_price: Some(U128::from(9)), + gas: U256::from(10), + input: Bytes::from(vec![11, 12, 13]), + signature: Some(Signature { + v: U256::from(14), + r: U256::from(14), + s: U256::from(14), + y_parity: Some(Parity(true)), + }), + chain_id: Some(U64::from(17)), + access_list: None, + transaction_type: Some(U64::from(20)), + max_fee_per_gas: Some(U128::from(21)), + max_priority_fee_per_gas: Some(U128::from(22)), + }; + let serialized = serde_json::to_string(&transaction).unwrap(); + assert_eq!( + serialized, + r#"{"hash":"0x0000000000000000000000000000000000000000000000000000000000000001","nonce":"0x2","blockHash":"0x0000000000000000000000000000000000000000000000000000000000000003","blockNumber":"0x4","transactionIndex":"0x5","from":"0x0000000000000000000000000000000000000006","to":"0x0000000000000000000000000000000000000007","value":"0x8","gasPrice":"0x9","gas":"0xa","maxFeePerGas":"0x15","maxPriorityFeePerGas":"0x16","input":"0x0b0c0d","r":"0xe","s":"0xe","v":"0xe","yParity":"0x1","chainId":"0x11","type":"0x14"}"# + ); + let deserialized: Transaction = serde_json::from_str(&serialized).unwrap(); + assert_eq!(transaction, deserialized); + } } diff --git a/crates/rpc/rpc-types/src/eth/transaction/signature.rs b/crates/rpc/rpc-types/src/eth/transaction/signature.rs index 6538636b40..3c31a12600 100644 --- a/crates/rpc/rpc-types/src/eth/transaction/signature.rs +++ b/crates/rpc/rpc-types/src/eth/transaction/signature.rs @@ -9,6 +9,8 @@ pub struct Signature { pub r: U256, /// The S field of the signature; the point on the curve. pub s: U256, + // TODO: change these fields to an untagged enum for `v` XOR `y_parity` if/when CLs support it. + // See for more information /// For EIP-155, EIP-2930 and Blob transactions this is set to the parity (0 for even, 1 for /// odd) of the y-value of the secp256k1 signature. /// @@ -16,6 +18,9 @@ pub struct Signature { /// /// See also and pub v: U256, + /// The y parity of the signature. This is only used for typed (non-legacy) transactions. + #[serde(default, rename = "yParity", skip_serializing_if = "Option::is_none")] + pub y_parity: Option, } impl Signature { @@ -28,14 +33,24 @@ impl Signature { signature: PrimitiveSignature, chain_id: Option, ) -> Self { - Self { r: signature.r, s: signature.s, v: U256::from(signature.v(chain_id)) } + Self { + r: signature.r, + s: signature.s, + v: U256::from(signature.v(chain_id)), + y_parity: None, + } } /// Creates a new rpc signature from a non-legacy [primitive /// signature](reth_primitives::Signature). This sets the `v` value to `0` or `1` depending on /// the signature's `odd_y_parity`. pub(crate) fn from_typed_primitive_signature(signature: PrimitiveSignature) -> Self { - Self { r: signature.r, s: signature.s, v: U256::from(signature.odd_y_parity as u8) } + Self { + r: signature.r, + s: signature.s, + v: U256::from(signature.odd_y_parity as u8), + y_parity: Some(Parity(signature.odd_y_parity)), + } } /// Creates a new rpc signature from a legacy [primitive @@ -58,3 +73,168 @@ impl Signature { } } } + +/// Type that represents the signature parity byte, meant for use in RPC. +/// +/// This will be serialized as "0x0" if false, and "0x1" if true. +#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Parity( + #[serde(serialize_with = "serialize_parity", deserialize_with = "deserialize_parity")] pub bool, +); + +fn serialize_parity(parity: &bool, serializer: S) -> Result +where + S: serde::Serializer, +{ + serializer.serialize_str(if *parity { "0x1" } else { "0x0" }) +} + +/// This implementation disallows serialization of the y parity bit that are not `"0x0"` or `"0x1"`. +fn deserialize_parity<'de, D>(deserializer: D) -> Result +where + D: serde::Deserializer<'de>, +{ + let s = String::deserialize(deserializer)?; + match s.as_str() { + "0x0" => Ok(false), + "0x1" => Ok(true), + _ => Err(serde::de::Error::custom(format!( + "invalid parity value, parity should be either \"0x0\" or \"0x1\": {}", + s + ))), + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use super::*; + + #[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 { + r: U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + s: U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + v: U256::from_str("1").unwrap(), + y_parity: None, + }; + + assert_eq!(signature, expected); + } + + #[test] + fn deserialize_with_parity() { + let raw_signature_with_y_parity = r#"{ + "r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0", + "s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05", + "v":"0x1", + "yParity": "0x1" + }"#; + + let signature: Signature = serde_json::from_str(raw_signature_with_y_parity).unwrap(); + let expected = Signature { + r: U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + s: U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + v: U256::from_str("1").unwrap(), + y_parity: Some(Parity(true)), + }; + + assert_eq!(signature, expected); + } + + #[test] + fn serialize_both_parity() { + // this test should be removed if the struct moves to an enum based on tx type + let signature = Signature { + r: U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + s: U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + v: U256::from_str("1").unwrap(), + y_parity: Some(Parity(true)), + }; + + let serialized = serde_json::to_string(&signature).unwrap(); + assert_eq!( + serialized, + r#"{"r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0","s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05","v":"0x1","yParity":"0x1"}"# + ); + } + + #[test] + fn serialize_v_only() { + // this test should be removed if the struct moves to an enum based on tx type + let signature = Signature { + r: U256::from_str("0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0") + .unwrap(), + s: U256::from_str("0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05") + .unwrap(), + v: U256::from_str("1").unwrap(), + y_parity: None, + }; + + let expected = r#"{"r":"0xc569c92f176a3be1a6352dd5005bfc751dcb32f57623dd2a23693e64bf4447b0","s":"0x1a891b566d369e79b7a66eecab1e008831e22daa15f91a0a0cf4f9f28f47ee05","v":"0x1"}"#; + + let serialized = serde_json::to_string(&signature).unwrap(); + assert_eq!(serialized, expected); + } + + #[test] + fn serialize_parity() { + let parity = Parity(true); + let serialized = serde_json::to_string(&parity).unwrap(); + assert_eq!(serialized, r#""0x1""#); + + let parity = Parity(false); + let serialized = serde_json::to_string(&parity).unwrap(); + assert_eq!(serialized, r#""0x0""#); + } + + #[test] + fn deserialize_parity() { + let raw_parity = r#""0x1""#; + let parity: Parity = serde_json::from_str(raw_parity).unwrap(); + assert_eq!(parity, Parity(true)); + + let raw_parity = r#""0x0""#; + let parity: Parity = serde_json::from_str(raw_parity).unwrap(); + assert_eq!(parity, Parity(false)); + } + + #[test] + fn deserialize_parity_invalid() { + let raw_parity = r#""0x2""#; + let parity: Result = serde_json::from_str(raw_parity); + assert!(parity.is_err()); + + let raw_parity = r#""0x""#; + let parity: Result = serde_json::from_str(raw_parity); + assert!(parity.is_err()); + + // In the spec this is defined as a uint, which requires 0x + // yParity: + // + // + // uint: + // + let raw_parity = r#""1""#; + let parity: Result = serde_json::from_str(raw_parity); + assert!(parity.is_err()); + + let raw_parity = r#""0""#; + let parity: Result = serde_json::from_str(raw_parity); + assert!(parity.is_err()); + } +}