From bd5bd374fd50eb30eb381148243c46d882e057d5 Mon Sep 17 00:00:00 2001 From: Ahmad Kaouk <56095276+ahmadkaouk@users.noreply.github.com> Date: Tue, 18 Jul 2023 15:03:35 +0300 Subject: [PATCH] Handling `data` and `input` fields in `TransactionRequest` and `CallRequest` (#1096) * Update deserializing data and input fields in TransactionRequest * Custom deserializing for data and input in CallRequest * Manually implement Deserialize for TransactionRequest and CallRequest * Add some unit test for the custom deserialization * add functional tests * fix formatting * deserializer for aliased fields * custom deserializer for data and input fields in TransactionRequest * fix formatting * update functional test * refactor code and fix formatting --------- Co-authored-by: tgmichel --- client/rpc-core/src/types/call_request.rs | 102 +++++++++++++++++- client/rpc-core/src/types/mod.rs | 28 +++++ .../rpc-core/src/types/transaction_request.rs | 101 ++++++++++++++++- ts-tests/tests/test-execute.ts | 25 +++++ 4 files changed, 251 insertions(+), 5 deletions(-) diff --git a/client/rpc-core/src/types/call_request.rs b/client/rpc-core/src/types/call_request.rs index 2e014d3475..b699e0aea1 100644 --- a/client/rpc-core/src/types/call_request.rs +++ b/client/rpc-core/src/types/call_request.rs @@ -18,12 +18,11 @@ use std::collections::BTreeMap; +use crate::types::{deserialize_data_or_input, Bytes}; use ethereum::AccessListItem; use ethereum_types::{H160, H256, U256}; use serde::Deserialize; -use crate::types::Bytes; - /// Call request #[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] #[serde(deny_unknown_fields)] @@ -44,7 +43,7 @@ pub struct CallRequest { /// Value pub value: Option, /// Data - #[serde(alias = "input")] + #[serde(deserialize_with = "deserialize_data_or_input", flatten)] pub data: Option, /// Nonce pub nonce: Option, @@ -73,3 +72,100 @@ pub struct CallStateOverride { /// executing the call. pub state_diff: Option>, } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_deserialize_with_only_input() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "input": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } + + #[test] + fn test_deserialize_with_only_data() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } + + #[test] + fn test_deserialize_with_data_and_input_mismatch() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "input": "0x456def", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_err()); + } + + #[test] + fn test_deserialize_with_data_and_input_equal() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "input": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } +} diff --git a/client/rpc-core/src/types/mod.rs b/client/rpc-core/src/types/mod.rs index 98b6e5b417..eb9fccbc0b 100644 --- a/client/rpc-core/src/types/mod.rs +++ b/client/rpc-core/src/types/mod.rs @@ -36,6 +36,8 @@ mod work; pub mod pubsub; +use serde::{de::Error, Deserialize, Deserializer}; + pub use self::{ account_info::{AccountInfo, EthAccount, ExtAccountInfo, RecoveredAccount, StorageProof}, block::{Block, BlockTransactions, Header, Rich, RichBlock, RichHeader}, @@ -59,3 +61,29 @@ pub use self::{ txpool::{Get, Summary, TransactionMap, TxPoolResult, TxPoolTransaction}, work::Work, }; + +#[derive(Clone, Debug, Default, Eq, PartialEq, Deserialize)] +pub(crate) struct CallOrInputData { + data: Option, + input: Option, +} + +/// Function to deserialize `data` and `input` within `TransactionRequest` and `CallRequest`. +/// It verifies that if both `data` and `input` are provided, they must be identical. +pub(crate) fn deserialize_data_or_input<'d, D: Deserializer<'d>>( + d: D, +) -> Result, D::Error> { + let CallOrInputData { data, input } = CallOrInputData::deserialize(d)?; + match (&data, &input) { + (Some(data), Some(input)) => { + if data == input { + Ok(Some(data.clone())) + } else { + Err(D::Error::custom( + "Ambiguous value for `data` and `input`".to_string(), + )) + } + } + (_, _) => Ok(data.or(input)), + } +} diff --git a/client/rpc-core/src/types/transaction_request.rs b/client/rpc-core/src/types/transaction_request.rs index a263f4a7f1..f94d8376f9 100644 --- a/client/rpc-core/src/types/transaction_request.rs +++ b/client/rpc-core/src/types/transaction_request.rs @@ -24,7 +24,7 @@ use ethereum::{ use ethereum_types::{H160, U256}; use serde::{Deserialize, Serialize}; -use crate::types::Bytes; +use crate::types::{deserialize_data_or_input, Bytes}; pub enum TransactionMessage { Legacy(LegacyTransactionMessage), @@ -55,7 +55,7 @@ pub struct TransactionRequest { /// Value of transaction in wei pub value: Option, /// Additional data sent with transaction - #[serde(alias = "input")] + #[serde(deserialize_with = "deserialize_data_or_input", flatten)] pub data: Option, /// Transaction's nonce pub nonce: Option, @@ -119,3 +119,100 @@ impl From for Option { } } } + +#[cfg(test)] +mod tests { + use super::*; + use serde_json::json; + + #[test] + fn test_deserialize_with_only_input() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "input": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } + + #[test] + fn test_deserialize_with_only_data() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } + + #[test] + fn test_deserialize_with_data_and_input_mismatch() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "input": "0x456def", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_err()); + } + + #[test] + fn test_deserialize_with_data_and_input_equal() { + let data = json!({ + "from": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", + "to": "0x13fe2d1d3665660d22ff9624b7be0551ee1ac91b", + "gasPrice": "0x10", + "maxFeePerGas": "0x20", + "maxPriorityFeePerGas": "0x30", + "gas": "0x40", + "value": "0x50", + "data": "0x123abc", + "input": "0x123abc", + "nonce": "0x60", + "accessList": [{"address": "0x60be2d1d3665660d22ff9624b7be0551ee1ac91b", "storageKeys": []}], + "type": "0x70" + }); + + let request: Result = serde_json::from_value(data); + assert!(request.is_ok()); + + let request = request.unwrap(); + assert_eq!(request.data, Some(Bytes::from(vec![0x12, 0x3a, 0xbc]))); + } +} diff --git a/ts-tests/tests/test-execute.ts b/ts-tests/tests/test-execute.ts index 43bf103d41..aef7adf3a9 100644 --- a/ts-tests/tests/test-execute.ts +++ b/ts-tests/tests/test-execute.ts @@ -238,4 +238,29 @@ describeWithFrontier("Frontier RPC (RPC execution)", (context) => { expect(result.result).to.be.equal(TEST_CONTRACT_DEPLOYED_BYTECODE); }); + + step("Deserializes correctly when data and input are equal", async function () { + const result = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + gas: `0x${(ETH_BLOCK_GAS_LIMIT - 1).toString(16)}`, + data: TEST_CONTRACT_BYTECODE, + input: TEST_CONTRACT_BYTECODE, + }, + ]); + + expect(result.result).to.be.equal(TEST_CONTRACT_DEPLOYED_BYTECODE); + }); + + step("Throws error when data and input are both present and not equal", async function () { + const result = await customRequest(context.web3, "eth_call", [ + { + from: GENESIS_ACCOUNT, + gas: `0x${(ETH_BLOCK_GAS_LIMIT - 1).toString(16)}`, + data: TEST_CONTRACT_BYTECODE, + input: "0x12345678", + }, + ]); + expect((result as any).error.message).to.match(/^Ambiguous value for `data` and `input`/); + }); });