feat: add yParity to rpc signatures (#3800)

This commit is contained in:
Dan Cline
2023-07-17 07:06:26 -04:00
committed by GitHub
parent b2b2cbedb5
commit d095db50c4
2 changed files with 225 additions and 3 deletions

View File

@ -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);
}
}

View File

@ -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 <https://github.com/ethereum/go-ethereum/issues/27727> 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 <https://ethereum.github.io/execution-apis/api-documentation/> and <https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash>
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<Parity>,
}
impl Signature {
@ -28,14 +33,24 @@ impl Signature {
signature: PrimitiveSignature,
chain_id: Option<u64>,
) -> 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<S>(parity: &bool, serializer: S) -> Result<S::Ok, S::Error>
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<bool, D::Error>
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<Parity, _> = serde_json::from_str(raw_parity);
assert!(parity.is_err());
let raw_parity = r#""0x""#;
let parity: Result<Parity, _> = serde_json::from_str(raw_parity);
assert!(parity.is_err());
// In the spec this is defined as a uint, which requires 0x
// yParity:
// <https://github.com/ethereum/execution-apis/blob/8fcafbbc86257f6e61fddd9734148e38872a71c9/src/schemas/transaction.yaml#L157>
//
// uint:
// <https://github.com/ethereum/execution-apis/blob/8fcafbbc86257f6e61fddd9734148e38872a71c9/src/schemas/base-types.yaml#L47>
let raw_parity = r#""1""#;
let parity: Result<Parity, _> = serde_json::from_str(raw_parity);
assert!(parity.is_err());
let raw_parity = r#""0""#;
let parity: Result<Parity, _> = serde_json::from_str(raw_parity);
assert!(parity.is_err());
}
}