Skip to content

Commit 83c7004

Browse files
committed
PARTIAL: Support for full transaction fields
1 parent 61f9497 commit 83c7004

File tree

3 files changed

+217
-18
lines changed

3 files changed

+217
-18
lines changed

ethportal-api/src/utils/rethtypes.rs

+7-1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
use ethereum_types::U256;
1+
use ethereum_types::{U256, U64};
22
use ruint::Uint;
33

44
pub fn u256_to_uint256(u256: U256) -> Uint<256, 4> {
@@ -16,3 +16,9 @@ pub fn u64_to_uint256(val: u64) -> Uint<256, 4> {
1616
.expect("8 bytes + 24 bytes should be 32 bytes");
1717
Uint::from_be_bytes(bytes)
1818
}
19+
20+
pub fn ethtype_u64_to_uint256(val: U64) -> Uint<256, 4> {
21+
let mut bytes = [0u8; 32];
22+
val.to_big_endian(&mut bytes);
23+
Uint::from_be_bytes(bytes)
24+
}

rpc/src/eth_rpc.rs

+140-16
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
1-
use ethereum_types::{H256, U256};
2-
use reth_rpc_types::{Block, BlockTransactions};
1+
use ethereum_types::{H256, U256, U64};
2+
use reth_rpc_types::{Block, BlockTransactions, Parity, Signature, Transaction as RethTransaction};
33
use tokio::sync::mpsc;
44

55
use ethportal_api::types::execution::block_body::BlockBody;
6+
use ethportal_api::types::execution::transaction::Transaction;
67
use ethportal_api::types::jsonrpc::request::HistoryJsonRpcRequest;
8+
use ethportal_api::utils::rethtypes::{ethtype_u64_to_uint256, u256_to_uint256};
79
use ethportal_api::EthApiServer;
810
use trin_validation::constants::CHAIN_ID;
911

10-
use crate::errors::RpcServeError;
1112
use crate::fetch::{find_block_body_by_hash, find_header_by_hash};
1213
use crate::jsonrpsee::core::{async_trait, RpcResult};
1314

@@ -21,6 +22,126 @@ impl EthApi {
2122
}
2223
}
2324

25+
fn rpc_transaction(
26+
transaction: Transaction,
27+
block_hash: H256,
28+
block_number: u64,
29+
transaction_index: usize,
30+
) -> RethTransaction {
31+
// Fields not extractable from the transaction itself
32+
let block_hash = block_hash.as_fixed_bytes().into();
33+
let block_number = U256::from(block_number);
34+
let transaction_index = Some(U256::from(transaction_index));
35+
36+
// Fields calculated on the full transaction envelope
37+
let hash = transaction.hash().as_fixed_bytes().into();
38+
let type_id = match transaction.type_id() {
39+
0 => None,
40+
n => Some(U64::from(n)),
41+
};
42+
// TODO: generate 'from' address from signature
43+
let from = None;
44+
45+
// Fields internal to the transaction, sometimes varying by transaction type
46+
let (
47+
nonce,
48+
gas_price,
49+
gas,
50+
to,
51+
value,
52+
input,
53+
v,
54+
r,
55+
s,
56+
max_fee_per_gas,
57+
max_priority_fee_per_gas,
58+
access_list,
59+
y_parity,
60+
) = match transaction {
61+
Transaction::Legacy(tx) => (
62+
tx.nonce,
63+
Some(tx.gas_price),
64+
tx.gas,
65+
tx.to,
66+
tx.value,
67+
tx.data,
68+
tx.v,
69+
tx.r,
70+
tx.s,
71+
None,
72+
None,
73+
None,
74+
None,
75+
),
76+
Transaction::AccessList(tx) => (
77+
tx.nonce,
78+
Some(tx.gas_price),
79+
tx.gas,
80+
tx.to,
81+
tx.value,
82+
tx.data,
83+
tx.y_parity,
84+
tx.r,
85+
tx.s,
86+
None,
87+
None,
88+
Some(tx.access_list),
89+
Some(tx.y_parity),
90+
),
91+
Transaction::EIP1559(tx) => (
92+
tx.nonce,
93+
None,
94+
tx.gas,
95+
tx.to,
96+
tx.value,
97+
tx.data,
98+
tx.y_parity,
99+
tx.r,
100+
tx.s,
101+
Some(tx.max_fee_per_gas),
102+
Some(tx.max_priority_fee_per_gas),
103+
Some(tx.access_list),
104+
Some(tx.y_parity),
105+
),
106+
};
107+
108+
// Convert types
109+
let (gas, value) = (u256_to_uint256(gas), u256_to_uint256(value));
110+
let signature = Some(Signature {
111+
r: u256_to_uint256(r),
112+
s: u256_to_uint256(s),
113+
v: ethtype_u64_to_uint256(v),
114+
y_parity: y_parity.map(|y| Parity(!y.is_zero())),
115+
});
116+
117+
// Fields that are hardcoded, for now
118+
let max_fee_per_blob_gas = None;
119+
let chain_id = Some(CHAIN_ID.into());
120+
let blob_versioned_hashes = vec![];
121+
122+
RethTransaction {
123+
hash,
124+
nonce,
125+
block_hash,
126+
block_number,
127+
transaction_index,
128+
from,
129+
to,
130+
value,
131+
gas_price,
132+
gas,
133+
max_fee_per_gas,
134+
max_priority_fee_per_gas,
135+
max_fee_per_blob_gas,
136+
input,
137+
signature,
138+
chain_id,
139+
blob_versioned_hashes,
140+
access_list,
141+
transaction_type,
142+
}
143+
}
144+
24145
#[async_trait]
25146
impl EthApiServer for EthApi {
26147
async fn chain_id(&self) -> RpcResult<U256> {
@@ -32,26 +153,29 @@ impl EthApiServer for EthApi {
32153
block_hash: H256,
33154
hydrated_transactions: bool,
34155
) -> RpcResult<Block> {
35-
if hydrated_transactions {
36-
return Err(RpcServeError::Message(
37-
"replying with all transaction bodies is not supported yet".into(),
38-
)
39-
.into());
40-
}
41-
42156
let header = find_header_by_hash(&self.network, block_hash).await?;
43157
let body = find_block_body_by_hash(&self.network, block_hash).await?;
44158
let transactions = match body {
45159
BlockBody::Legacy(body) => body.txs,
46160
BlockBody::Merge(body) => body.txs,
47161
BlockBody::Shanghai(body) => body.txs,
48162
};
49-
let transactions = BlockTransactions::Hashes(
50-
transactions
51-
.into_iter()
52-
.map(|tx| tx.hash().as_fixed_bytes().into())
53-
.collect(),
54-
);
163+
let transactions = if hydrated_transactions {
164+
BlockTransactions::Full(
165+
transactions
166+
.into_iter()
167+
.enumerate()
168+
.map(|(idx, tx)| rpc_transaction(tx, block_hash, header.number, idx))
169+
.collect(),
170+
)
171+
} else {
172+
BlockTransactions::Hashes(
173+
transactions
174+
.into_iter()
175+
.map(|tx| tx.hash().as_fixed_bytes().into())
176+
.collect(),
177+
)
178+
};
55179

56180
// Combine header and block body into the single json representation of the block.
57181
let block = Block {

tests/rpc_server.rs

+70-1
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,10 @@ async fn test_eth_chain_id() {
7070

7171
#[tokio::test(flavor = "multi_thread")]
7272
#[serial]
73-
async fn test_eth_get_block_by_hash() {
73+
/// Test that a 3rd-party web3 client can understand our JSON-RPC API
74+
/// for eth_getBlockByHash
75+
/// Return the transactions as hashes, aka not "hydrated"
76+
async fn test_eth_get_block_by_hash_dehydrated() {
7477
let (web3_server, web3_client, native_client) = setup_web3_server().await;
7578

7679
let (hwp, body) = get_full_block();
@@ -183,6 +186,72 @@ async fn test_eth_get_block_by_hash() {
183186
);
184187
}
185188

189+
#[tokio::test(flavor = "multi_thread")]
190+
#[serial]
191+
/// Test that a 3rd-party web3 client can understand our JSON-RPC API
192+
/// for eth_getBlockByHash
193+
/// Return the transactions as fully formed, aka "hydrated"
194+
async fn test_eth_get_block_by_hash_hydrated() {
195+
let (web3_server, web3_client, native_client) = setup_web3_server().await;
196+
197+
let (hwp, body) = get_full_block();
198+
// Save values for later comparison
199+
let (block_number, block_hash) = (hwp.header.number.into(), hwp.header.hash());
200+
201+
let BlockBody::Shanghai(shanghai_body) = body.clone() else { panic!("expected shanghai body") };
202+
203+
// Store header with proof in server
204+
let content_key = HistoryContentKey::BlockHeaderWithProof(block_hash.into());
205+
let content_value = HistoryContentValue::BlockHeaderWithProof(hwp);
206+
let result = native_client
207+
.store(content_key, content_value)
208+
.await
209+
.unwrap();
210+
assert!(result);
211+
212+
// Store block in server
213+
let content_key = HistoryContentKey::BlockBody(block_hash.into());
214+
let content_value = HistoryContentValue::BlockBody(body);
215+
let result = native_client
216+
.store(content_key, content_value)
217+
.await
218+
.unwrap();
219+
assert!(result);
220+
221+
// The meat of the test is here:
222+
// Retrieve block over json-rpc
223+
let block_id = ethers_core::types::H256::from(&block_hash.0);
224+
let block = web3_client
225+
.get_block_with_txs(block_id)
226+
.await
227+
.expect("request to get block failed")
228+
.expect("specified block not found");
229+
web3_server.stop().unwrap();
230+
231+
let block_hash = block_hash.as_fixed_bytes().into();
232+
assert_eq!(block.number.expect("number must be present"), block_number);
233+
assert_eq!(block.hash.expect("hash must be present"), block_hash);
234+
assert_eq!(block.transactions.len(), shanghai_body.txs.len());
235+
236+
// Spot check the fields of a few transactions:
237+
// First tx
238+
assert_eq!(
239+
hex_encode(block.transactions[0].hash),
240+
"0xd06a110de42d674a84b2091cbd85ef514fb4e903f9a80dd7b640c48365a1a832"
241+
);
242+
// Last tx
243+
assert_eq!(
244+
hex_encode(block.transactions[84].hash),
245+
"0x27e9e8fb3745d990c7d775268539fa17bbf06255e24a882c3153bf3b513ced9e"
246+
);
247+
// Legacy block
248+
assert_eq!(
249+
hex_encode(block.transactions[5].hash),
250+
"0x2f678341f550f7073a514c4b34f09824119f31dfbe7cc73ffccb21b7a2ba5710"
251+
);
252+
panic!("TODO: Test more fields");
253+
}
254+
186255
fn get_full_block() -> (HeaderWithProof, BlockBody) {
187256
let file = fs::read_to_string("trin-validation/src/assets/hive/blocks.yaml").unwrap();
188257
let value: Value = serde_yaml::from_str(&file).unwrap();

0 commit comments

Comments
 (0)