From 5dcbcfdeb683b02d17b77031b0a2200fa69ac778 Mon Sep 17 00:00:00 2001 From: Lech <88630083+Artemka374@users.noreply.github.com> Date: Tue, 30 Jan 2024 18:45:57 +0200 Subject: [PATCH] feat: add eth_getBlockReceipts (#887) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## What ❔ Add `eth_getBlockReceipts` to API server ## Why ❔ For better Ethereum API compatibility. ## Checklist - [x] PR title corresponds to the body of PR (we generate changelog entries from PRs). - [x] Tests for the changes have been added / updated. - [x] Documentation comments have been added / updated. - [x] Code has been formatted via `zk fmt` and `zk lint`. - [x] Spellcheck has been run via `zk spellcheck`. --- ...4f3670813e5a5356ddcb7ac482a0201d045f7.json | 108 ------- ...d2d08a708c3af75fee57379a709baa3c4bed.json} | 6 +- ...cc77bd2dcee4a04d1afc9779714854623a79.json} | 6 +- ...c332a122d0b88271ae0127c65c4612b41a619.json | 108 +++++++ core/lib/dal/src/events_dal.rs | 122 +++++-- .../lib/dal/src/models/storage_transaction.rs | 81 ++++- core/lib/dal/src/tests/mod.rs | 7 +- core/lib/dal/src/transactions_web3_dal.rs | 304 ++++++++---------- core/lib/web3_decl/src/namespaces/eth.rs | 5 +- .../web3/backend_jsonrpsee/namespaces/eth.rs | 6 + .../src/api_server/web3/namespaces/eth.rs | 63 +++- .../src/api_server/web3/tests/mod.rs | 51 +++ core/lib/zksync_core/src/sync_layer/tests.rs | 4 +- .../ts-integration/tests/api/web3.test.ts | 11 + 14 files changed, 567 insertions(+), 315 deletions(-) delete mode 100644 core/lib/dal/.sqlx/query-02285b8d0bc76c8cfd259872ac24f3670813e5a5356ddcb7ac482a0201d045f7.json rename core/lib/dal/.sqlx/{query-c038cecd8184e5e8d9f498116bff995b654adfe328cb825a44ad36b4bf9ec8f2.json => query-8cd11172fc47ff8d37c22ba4163cd2d08a708c3af75fee57379a709baa3c4bed.json} (60%) rename core/lib/dal/.sqlx/{query-be16d820c124dba9f4a272f54f0b742349e78e6e4ce3e7c9a0dcf6447eedc6d8.json => query-b259e6bacd98fa68003e0c87bb28cc77bd2dcee4a04d1afc9779714854623a79.json} (88%) create mode 100644 core/lib/dal/.sqlx/query-b6837d2deed935da748339538c2c332a122d0b88271ae0127c65c4612b41a619.json diff --git a/core/lib/dal/.sqlx/query-02285b8d0bc76c8cfd259872ac24f3670813e5a5356ddcb7ac482a0201d045f7.json b/core/lib/dal/.sqlx/query-02285b8d0bc76c8cfd259872ac24f3670813e5a5356ddcb7ac482a0201d045f7.json deleted file mode 100644 index 41a37726f48..00000000000 --- a/core/lib/dal/.sqlx/query-02285b8d0bc76c8cfd259872ac24f3670813e5a5356ddcb7ac482a0201d045f7.json +++ /dev/null @@ -1,108 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "\n WITH\n sl AS (\n SELECT\n *\n FROM\n storage_logs\n WHERE\n storage_logs.address = $1\n AND storage_logs.tx_hash = $2\n ORDER BY\n storage_logs.miniblock_number DESC,\n storage_logs.operation_number DESC\n LIMIT\n 1\n )\n SELECT\n transactions.hash AS tx_hash,\n transactions.index_in_block AS index_in_block,\n transactions.l1_batch_tx_index AS l1_batch_tx_index,\n transactions.miniblock_number AS \"block_number!\",\n transactions.error AS error,\n transactions.effective_gas_price AS effective_gas_price,\n transactions.initiator_address AS initiator_address,\n transactions.data -> 'to' AS \"transfer_to?\",\n transactions.data -> 'contractAddress' AS \"execute_contract_address?\",\n transactions.tx_format AS \"tx_format?\",\n transactions.refunded_gas AS refunded_gas,\n transactions.gas_limit AS gas_limit,\n miniblocks.hash AS \"block_hash\",\n miniblocks.l1_batch_number AS \"l1_batch_number?\",\n sl.key AS \"contract_address?\"\n FROM\n transactions\n JOIN miniblocks ON miniblocks.number = transactions.miniblock_number\n LEFT JOIN sl ON sl.value != $3\n WHERE\n transactions.hash = $2\n ", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "tx_hash", - "type_info": "Bytea" - }, - { - "ordinal": 1, - "name": "index_in_block", - "type_info": "Int4" - }, - { - "ordinal": 2, - "name": "l1_batch_tx_index", - "type_info": "Int4" - }, - { - "ordinal": 3, - "name": "block_number!", - "type_info": "Int8" - }, - { - "ordinal": 4, - "name": "error", - "type_info": "Varchar" - }, - { - "ordinal": 5, - "name": "effective_gas_price", - "type_info": "Numeric" - }, - { - "ordinal": 6, - "name": "initiator_address", - "type_info": "Bytea" - }, - { - "ordinal": 7, - "name": "transfer_to?", - "type_info": "Jsonb" - }, - { - "ordinal": 8, - "name": "execute_contract_address?", - "type_info": "Jsonb" - }, - { - "ordinal": 9, - "name": "tx_format?", - "type_info": "Int4" - }, - { - "ordinal": 10, - "name": "refunded_gas", - "type_info": "Int8" - }, - { - "ordinal": 11, - "name": "gas_limit", - "type_info": "Numeric" - }, - { - "ordinal": 12, - "name": "block_hash", - "type_info": "Bytea" - }, - { - "ordinal": 13, - "name": "l1_batch_number?", - "type_info": "Int8" - }, - { - "ordinal": 14, - "name": "contract_address?", - "type_info": "Bytea" - } - ], - "parameters": { - "Left": [ - "Bytea", - "Bytea", - "Bytea" - ] - }, - "nullable": [ - false, - true, - true, - true, - true, - true, - false, - null, - null, - true, - false, - true, - false, - true, - false - ] - }, - "hash": "02285b8d0bc76c8cfd259872ac24f3670813e5a5356ddcb7ac482a0201d045f7" -} diff --git a/core/lib/dal/.sqlx/query-c038cecd8184e5e8d9f498116bff995b654adfe328cb825a44ad36b4bf9ec8f2.json b/core/lib/dal/.sqlx/query-8cd11172fc47ff8d37c22ba4163cd2d08a708c3af75fee57379a709baa3c4bed.json similarity index 60% rename from core/lib/dal/.sqlx/query-c038cecd8184e5e8d9f498116bff995b654adfe328cb825a44ad36b4bf9ec8f2.json rename to core/lib/dal/.sqlx/query-8cd11172fc47ff8d37c22ba4163cd2d08a708c3af75fee57379a709baa3c4bed.json index 8161e8c1fc8..ba3ee8a51d7 100644 --- a/core/lib/dal/.sqlx/query-c038cecd8184e5e8d9f498116bff995b654adfe328cb825a44ad36b4bf9ec8f2.json +++ b/core/lib/dal/.sqlx/query-8cd11172fc47ff8d37c22ba4163cd2d08a708c3af75fee57379a709baa3c4bed.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n address,\n topic1,\n topic2,\n topic3,\n topic4,\n value,\n NULL::bytea AS \"block_hash\",\n NULL::BIGINT AS \"l1_batch_number?\",\n miniblock_number,\n tx_hash,\n tx_index_in_block,\n event_index_in_block,\n event_index_in_tx\n FROM\n events\n WHERE\n tx_hash = $1\n ORDER BY\n miniblock_number ASC,\n event_index_in_block ASC\n ", + "query": "\n SELECT\n address,\n topic1,\n topic2,\n topic3,\n topic4,\n value,\n NULL::bytea AS \"block_hash\",\n NULL::BIGINT AS \"l1_batch_number?\",\n miniblock_number,\n tx_hash,\n tx_index_in_block,\n event_index_in_block,\n event_index_in_tx\n FROM\n events\n WHERE\n tx_hash = ANY ($1)\n ORDER BY\n miniblock_number ASC,\n tx_index_in_block ASC,\n event_index_in_block ASC\n ", "describe": { "columns": [ { @@ -71,7 +71,7 @@ ], "parameters": { "Left": [ - "Bytea" + "ByteaArray" ] }, "nullable": [ @@ -90,5 +90,5 @@ false ] }, - "hash": "c038cecd8184e5e8d9f498116bff995b654adfe328cb825a44ad36b4bf9ec8f2" + "hash": "8cd11172fc47ff8d37c22ba4163cd2d08a708c3af75fee57379a709baa3c4bed" } diff --git a/core/lib/dal/.sqlx/query-be16d820c124dba9f4a272f54f0b742349e78e6e4ce3e7c9a0dcf6447eedc6d8.json b/core/lib/dal/.sqlx/query-b259e6bacd98fa68003e0c87bb28cc77bd2dcee4a04d1afc9779714854623a79.json similarity index 88% rename from core/lib/dal/.sqlx/query-be16d820c124dba9f4a272f54f0b742349e78e6e4ce3e7c9a0dcf6447eedc6d8.json rename to core/lib/dal/.sqlx/query-b259e6bacd98fa68003e0c87bb28cc77bd2dcee4a04d1afc9779714854623a79.json index 695be9f2b8c..90c940c3977 100644 --- a/core/lib/dal/.sqlx/query-be16d820c124dba9f4a272f54f0b742349e78e6e4ce3e7c9a0dcf6447eedc6d8.json +++ b/core/lib/dal/.sqlx/query-b259e6bacd98fa68003e0c87bb28cc77bd2dcee4a04d1afc9779714854623a79.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "\n SELECT\n miniblock_number,\n log_index_in_miniblock,\n log_index_in_tx,\n tx_hash,\n NULL::bytea AS \"block_hash\",\n NULL::BIGINT AS \"l1_batch_number?\",\n shard_id,\n is_service,\n tx_index_in_miniblock,\n tx_index_in_l1_batch,\n sender,\n key,\n value\n FROM\n l2_to_l1_logs\n WHERE\n tx_hash = $1\n ORDER BY\n log_index_in_tx ASC\n ", + "query": "\n SELECT\n miniblock_number,\n log_index_in_miniblock,\n log_index_in_tx,\n tx_hash,\n NULL::bytea AS \"block_hash\",\n NULL::BIGINT AS \"l1_batch_number?\",\n shard_id,\n is_service,\n tx_index_in_miniblock,\n tx_index_in_l1_batch,\n sender,\n key,\n value\n FROM\n l2_to_l1_logs\n WHERE\n tx_hash = ANY ($1)\n ORDER BY\n tx_index_in_l1_batch ASC,\n log_index_in_tx ASC\n ", "describe": { "columns": [ { @@ -71,7 +71,7 @@ ], "parameters": { "Left": [ - "Bytea" + "ByteaArray" ] }, "nullable": [ @@ -90,5 +90,5 @@ false ] }, - "hash": "be16d820c124dba9f4a272f54f0b742349e78e6e4ce3e7c9a0dcf6447eedc6d8" + "hash": "b259e6bacd98fa68003e0c87bb28cc77bd2dcee4a04d1afc9779714854623a79" } diff --git a/core/lib/dal/.sqlx/query-b6837d2deed935da748339538c2c332a122d0b88271ae0127c65c4612b41a619.json b/core/lib/dal/.sqlx/query-b6837d2deed935da748339538c2c332a122d0b88271ae0127c65c4612b41a619.json new file mode 100644 index 00000000000..acd2d51f6ea --- /dev/null +++ b/core/lib/dal/.sqlx/query-b6837d2deed935da748339538c2c332a122d0b88271ae0127c65c4612b41a619.json @@ -0,0 +1,108 @@ +{ + "db_name": "PostgreSQL", + "query": "\n WITH\n sl AS (\n SELECT DISTINCT\n ON (storage_logs.tx_hash) *\n FROM\n storage_logs\n WHERE\n storage_logs.address = $1\n AND storage_logs.tx_hash = ANY ($3)\n ORDER BY\n storage_logs.tx_hash,\n storage_logs.miniblock_number DESC,\n storage_logs.operation_number DESC\n )\n SELECT\n transactions.hash AS tx_hash,\n transactions.index_in_block AS index_in_block,\n transactions.l1_batch_tx_index AS l1_batch_tx_index,\n transactions.miniblock_number AS \"block_number!\",\n transactions.error AS error,\n transactions.effective_gas_price AS effective_gas_price,\n transactions.initiator_address AS initiator_address,\n transactions.data -> 'to' AS \"transfer_to?\",\n transactions.data -> 'contractAddress' AS \"execute_contract_address?\",\n transactions.tx_format AS \"tx_format?\",\n transactions.refunded_gas AS refunded_gas,\n transactions.gas_limit AS gas_limit,\n miniblocks.hash AS \"block_hash\",\n miniblocks.l1_batch_number AS \"l1_batch_number?\",\n sl.key AS \"contract_address?\"\n FROM\n transactions\n JOIN miniblocks ON miniblocks.number = transactions.miniblock_number\n LEFT JOIN sl ON sl.value != $2\n AND sl.tx_hash = transactions.hash\n WHERE\n transactions.hash = ANY ($3)\n ", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "tx_hash", + "type_info": "Bytea" + }, + { + "ordinal": 1, + "name": "index_in_block", + "type_info": "Int4" + }, + { + "ordinal": 2, + "name": "l1_batch_tx_index", + "type_info": "Int4" + }, + { + "ordinal": 3, + "name": "block_number!", + "type_info": "Int8" + }, + { + "ordinal": 4, + "name": "error", + "type_info": "Varchar" + }, + { + "ordinal": 5, + "name": "effective_gas_price", + "type_info": "Numeric" + }, + { + "ordinal": 6, + "name": "initiator_address", + "type_info": "Bytea" + }, + { + "ordinal": 7, + "name": "transfer_to?", + "type_info": "Jsonb" + }, + { + "ordinal": 8, + "name": "execute_contract_address?", + "type_info": "Jsonb" + }, + { + "ordinal": 9, + "name": "tx_format?", + "type_info": "Int4" + }, + { + "ordinal": 10, + "name": "refunded_gas", + "type_info": "Int8" + }, + { + "ordinal": 11, + "name": "gas_limit", + "type_info": "Numeric" + }, + { + "ordinal": 12, + "name": "block_hash", + "type_info": "Bytea" + }, + { + "ordinal": 13, + "name": "l1_batch_number?", + "type_info": "Int8" + }, + { + "ordinal": 14, + "name": "contract_address?", + "type_info": "Bytea" + } + ], + "parameters": { + "Left": [ + "Bytea", + "Bytea", + "ByteaArray" + ] + }, + "nullable": [ + false, + true, + true, + true, + true, + true, + false, + null, + null, + true, + false, + true, + false, + true, + true + ] + }, + "hash": "b6837d2deed935da748339538c2c332a122d0b88271ae0127c65c4612b41a619" +} diff --git a/core/lib/dal/src/events_dal.rs b/core/lib/dal/src/events_dal.rs index b7087985f52..9fedee44457 100644 --- a/core/lib/dal/src/events_dal.rs +++ b/core/lib/dal/src/events_dal.rs @@ -1,13 +1,17 @@ -use std::fmt; +use std::{collections::HashMap, fmt}; use sqlx::types::chrono::Utc; use zksync_types::{ + api, l2_to_l1_log::{L2ToL1Log, UserL2ToL1Log}, tx::IncludedTxLocation, MiniblockNumber, VmEvent, H256, }; -use crate::{models::storage_event::StorageL2ToL1Log, SqlxError, StorageProcessor}; +use crate::{ + models::storage_event::{StorageL2ToL1Log, StorageWeb3Log}, + SqlxError, StorageProcessor, +}; /// Wrapper around an optional event topic allowing to hex-format it for `COPY` instructions. #[derive(Debug)] @@ -182,11 +186,65 @@ impl EventsDal<'_, '_> { .unwrap(); } - pub(crate) async fn l2_to_l1_logs( + pub(crate) async fn get_logs_by_tx_hashes( &mut self, - tx_hash: H256, - ) -> Result, SqlxError> { - sqlx::query_as!( + hashes: &[H256], + ) -> Result>, SqlxError> { + let hashes = hashes + .iter() + .map(|hash| hash.as_bytes().to_vec()) + .collect::>(); + let logs: Vec<_> = sqlx::query_as!( + StorageWeb3Log, + r#" + SELECT + address, + topic1, + topic2, + topic3, + topic4, + value, + NULL::bytea AS "block_hash", + NULL::BIGINT AS "l1_batch_number?", + miniblock_number, + tx_hash, + tx_index_in_block, + event_index_in_block, + event_index_in_tx + FROM + events + WHERE + tx_hash = ANY ($1) + ORDER BY + miniblock_number ASC, + tx_index_in_block ASC, + event_index_in_block ASC + "#, + &hashes[..], + ) + .fetch_all(self.storage.conn()) + .await?; + + let mut result = HashMap::>::new(); + + for storage_log in logs { + let current_log = api::Log::from(storage_log); + let tx_hash = current_log.transaction_hash.unwrap(); + result.entry(tx_hash).or_default().push(current_log); + } + + Ok(result) + } + + pub(crate) async fn get_l2_to_l1_logs_by_hashes( + &mut self, + hashes: &[H256], + ) -> Result>, SqlxError> { + let hashes = &hashes + .iter() + .map(|hash| hash.as_bytes().to_vec()) + .collect::>(); + let logs: Vec<_> = sqlx::query_as!( StorageL2ToL1Log, r#" SELECT @@ -206,14 +264,27 @@ impl EventsDal<'_, '_> { FROM l2_to_l1_logs WHERE - tx_hash = $1 + tx_hash = ANY ($1) ORDER BY + tx_index_in_l1_batch ASC, log_index_in_tx ASC "#, - tx_hash.as_bytes() + &hashes[..] ) .fetch_all(self.storage.conn()) - .await + .await?; + + let mut result = HashMap::>::new(); + + for storage_log in logs { + let current_log = api::L2ToL1Log::from(storage_log); + result + .entry(current_log.transaction_hash) + .or_default() + .push(current_log); + } + + Ok(result) } } @@ -355,34 +426,41 @@ mod tests { let logs = conn .events_dal() - .l2_to_l1_logs(H256([1; 32])) + .get_l2_to_l1_logs_by_hashes(&[H256([1; 32])]) .await .unwrap(); + + let logs = logs.get(&H256([1; 32])).unwrap().clone(); + assert_eq!(logs.len(), first_logs.len()); for (i, log) in logs.iter().enumerate() { - assert_eq!(log.log_index_in_miniblock as usize, i); - assert_eq!(log.log_index_in_tx as usize, i); + assert_eq!(log.log_index.as_usize(), i); + assert_eq!(log.transaction_log_index.as_usize(), i); } for (log, expected_log) in logs.iter().zip(&first_logs) { - assert_eq!(log.key, expected_log.0.key.as_bytes()); - assert_eq!(log.value, expected_log.0.value.as_bytes()); - assert_eq!(log.sender, expected_log.0.sender.as_bytes()); + assert_eq!(log.key.as_bytes(), expected_log.0.key.as_bytes()); + assert_eq!(log.value.as_bytes(), expected_log.0.value.as_bytes()); + assert_eq!(log.sender.as_bytes(), expected_log.0.sender.as_bytes()); } let logs = conn .events_dal() - .l2_to_l1_logs(H256([2; 32])) + .get_l2_to_l1_logs_by_hashes(&[H256([2; 32])]) .await - .unwrap(); + .unwrap() + .get(&H256([2; 32])) + .unwrap() + .clone(); + assert_eq!(logs.len(), second_logs.len()); for (i, log) in logs.iter().enumerate() { - assert_eq!(log.log_index_in_miniblock as usize, i + first_logs.len()); - assert_eq!(log.log_index_in_tx as usize, i); + assert_eq!(log.log_index.as_usize(), i + first_logs.len()); + assert_eq!(log.transaction_log_index.as_usize(), i); } for (log, expected_log) in logs.iter().zip(&second_logs) { - assert_eq!(log.key, expected_log.0.key.as_bytes()); - assert_eq!(log.value, expected_log.0.value.as_bytes()); - assert_eq!(log.sender, expected_log.0.sender.as_bytes()); + assert_eq!(log.key.as_bytes(), expected_log.0.key.as_bytes()); + assert_eq!(log.value.as_bytes(), expected_log.0.value.as_bytes()); + assert_eq!(log.sender.as_bytes(), expected_log.0.sender.as_bytes()); } } } diff --git a/core/lib/dal/src/models/storage_transaction.rs b/core/lib/dal/src/models/storage_transaction.rs index 9146c36fbe5..82732f5ab99 100644 --- a/core/lib/dal/src/models/storage_transaction.rs +++ b/core/lib/dal/src/models/storage_transaction.rs @@ -9,7 +9,7 @@ use sqlx::{ }; use zksync_types::{ api, - api::{TransactionDetails, TransactionStatus}, + api::{TransactionDetails, TransactionReceipt, TransactionStatus}, fee::Fee, l1::{OpProcessingType, PriorityQueueType}, l2::TransactionType, @@ -21,7 +21,7 @@ use zksync_types::{ Nonce, PackedEthSignature, PriorityOpId, Transaction, EIP_1559_TX_TYPE, EIP_2930_TX_TYPE, EIP_712_TX_TYPE, H160, H256, PRIORITY_OPERATION_L2_TX_TYPE, PROTOCOL_UPGRADE_TX_TYPE, U256, }; -use zksync_utils::bigdecimal_to_u256; +use zksync_utils::{bigdecimal_to_u256, h256_to_account_address}; use crate::BigDecimal; @@ -322,6 +322,83 @@ impl From for Transaction { } } +#[derive(sqlx::FromRow)] +pub(crate) struct StorageTransactionReceipt { + pub error: Option, + pub tx_format: Option, + pub index_in_block: Option, + pub block_hash: Vec, + pub tx_hash: Vec, + pub block_number: i64, + pub l1_batch_tx_index: Option, + pub l1_batch_number: Option, + pub transfer_to: Option, + pub execute_contract_address: Option, + pub refunded_gas: i64, + pub gas_limit: Option, + pub effective_gas_price: Option, + pub contract_address: Option>, + pub initiator_address: Vec, +} + +impl From for TransactionReceipt { + fn from(storage_receipt: StorageTransactionReceipt) -> Self { + let status = storage_receipt.error.map_or_else(U64::one, |_| U64::zero()); + + let tx_type = storage_receipt + .tx_format + .map_or_else(Default::default, U64::from); + let transaction_index = storage_receipt + .index_in_block + .map_or_else(Default::default, U64::from); + + let block_hash = H256::from_slice(&storage_receipt.block_hash); + TransactionReceipt { + transaction_hash: H256::from_slice(&storage_receipt.tx_hash), + transaction_index, + block_hash, + block_number: storage_receipt.block_number.into(), + l1_batch_tx_index: storage_receipt.l1_batch_tx_index.map(U64::from), + l1_batch_number: storage_receipt.l1_batch_number.map(U64::from), + from: H160::from_slice(&storage_receipt.initiator_address), + to: storage_receipt + .transfer_to + .or(storage_receipt.execute_contract_address) + .map(|addr| { + serde_json::from_value::
(addr) + .expect("invalid address value in the database") + }) + // For better compatibility with various clients, we never return null. + .or_else(|| Some(Address::default())), + cumulative_gas_used: Default::default(), // TODO: Should be actually calculated (SMA-1183). + gas_used: { + let refunded_gas: U256 = storage_receipt.refunded_gas.into(); + storage_receipt.gas_limit.map(|val| { + let gas_limit = bigdecimal_to_u256(val); + gas_limit - refunded_gas + }) + }, + effective_gas_price: Some( + storage_receipt + .effective_gas_price + .map(bigdecimal_to_u256) + .unwrap_or_default(), + ), + contract_address: storage_receipt + .contract_address + .map(|addr| h256_to_account_address(&H256::from_slice(&addr))), + logs: vec![], + l2_to_l1_logs: vec![], + status, + root: block_hash, + logs_bloom: Default::default(), + // Even though the Rust SDK recommends us to supply "None" for legacy transactions + // we always supply some number anyway to have the same behavior as most popular RPCs + transaction_type: Some(tx_type), + } + } +} + #[derive(Serialize, Deserialize)] pub struct StorageApiTransaction { #[serde(flatten)] diff --git a/core/lib/dal/src/tests/mod.rs b/core/lib/dal/src/tests/mod.rs index 5b285ff04f8..378ba3435d3 100644 --- a/core/lib/dal/src/tests/mod.rs +++ b/core/lib/dal/src/tests/mod.rs @@ -250,9 +250,10 @@ async fn remove_stuck_txs() { // We shouldn't collect executed tx let storage = transactions_dal.storage; let mut transactions_web3_dal = TransactionsWeb3Dal { storage }; - transactions_web3_dal - .get_transaction_receipt(executed_tx.hash()) + let receipts = transactions_web3_dal + .get_transaction_receipts(&[executed_tx.hash()]) .await - .unwrap() .unwrap(); + + assert_eq!(receipts.len(), 1); } diff --git a/core/lib/dal/src/transactions_web3_dal.rs b/core/lib/dal/src/transactions_web3_dal.rs index abc243dd1a1..251d1db7f49 100644 --- a/core/lib/dal/src/transactions_web3_dal.rs +++ b/core/lib/dal/src/transactions_web3_dal.rs @@ -1,18 +1,16 @@ use sqlx::types::chrono::NaiveDateTime; use zksync_types::{ - api, Address, L2ChainId, MiniblockNumber, Transaction, ACCOUNT_CODE_STORAGE_ADDRESS, - FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH, H160, H256, U256, U64, + api, api::TransactionReceipt, Address, L2ChainId, MiniblockNumber, Transaction, + ACCOUNT_CODE_STORAGE_ADDRESS, FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH, H256, U256, }; -use zksync_utils::{bigdecimal_to_u256, h256_to_account_address}; use crate::{ instrument::InstrumentExt, models::{ storage_block::{bind_block_where_sql_params, web3_block_where_sql}, - storage_event::StorageWeb3Log, storage_transaction::{ extract_web3_transaction, web3_transaction_select_sql, StorageTransaction, - StorageTransactionDetails, + StorageTransactionDetails, StorageTransactionReceipt, }, }, SqlxError, StorageProcessor, @@ -24,171 +22,106 @@ pub struct TransactionsWeb3Dal<'a, 'c> { } impl TransactionsWeb3Dal<'_, '_> { - pub async fn get_transaction_receipt( + /// Returns receipts by transactions hashes. + /// Hashes are expected to be unique. + pub async fn get_transaction_receipts( &mut self, - hash: H256, - ) -> Result, SqlxError> { - { - let receipt = sqlx::query!( - r#" - WITH - sl AS ( - SELECT - * - FROM - storage_logs - WHERE - storage_logs.address = $1 - AND storage_logs.tx_hash = $2 - ORDER BY - storage_logs.miniblock_number DESC, - storage_logs.operation_number DESC - LIMIT - 1 - ) - SELECT - transactions.hash AS tx_hash, - transactions.index_in_block AS index_in_block, - transactions.l1_batch_tx_index AS l1_batch_tx_index, - transactions.miniblock_number AS "block_number!", - transactions.error AS error, - transactions.effective_gas_price AS effective_gas_price, - transactions.initiator_address AS initiator_address, - transactions.data -> 'to' AS "transfer_to?", - transactions.data -> 'contractAddress' AS "execute_contract_address?", - transactions.tx_format AS "tx_format?", - transactions.refunded_gas AS refunded_gas, - transactions.gas_limit AS gas_limit, - miniblocks.hash AS "block_hash", - miniblocks.l1_batch_number AS "l1_batch_number?", - sl.key AS "contract_address?" - FROM - transactions - JOIN miniblocks ON miniblocks.number = transactions.miniblock_number - LEFT JOIN sl ON sl.value != $3 - WHERE - transactions.hash = $2 - "#, - ACCOUNT_CODE_STORAGE_ADDRESS.as_bytes(), - hash.as_bytes(), - FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes() - ) - .instrument("get_transaction_receipt") - .with_arg("hash", &hash) - .fetch_optional(self.storage.conn()) - .await? - .map(|db_row| { - let status = db_row.error.map(|_| U64::zero()).unwrap_or_else(U64::one); - - let tx_type = db_row.tx_format.map(U64::from).unwrap_or_default(); - let transaction_index = db_row.index_in_block.map(U64::from).unwrap_or_default(); - - let block_hash = H256::from_slice(&db_row.block_hash); - api::TransactionReceipt { - transaction_hash: H256::from_slice(&db_row.tx_hash), - transaction_index, - block_hash, - block_number: db_row.block_number.into(), - l1_batch_tx_index: db_row.l1_batch_tx_index.map(U64::from), - l1_batch_number: db_row.l1_batch_number.map(U64::from), - from: H160::from_slice(&db_row.initiator_address), - to: db_row - .transfer_to - .or(db_row.execute_contract_address) - .map(|addr| { - serde_json::from_value::
(addr) - .expect("invalid address value in the database") - }) - // For better compatibility with various clients, we never return null. - .or_else(|| Some(Address::default())), - cumulative_gas_used: Default::default(), // TODO: Should be actually calculated (SMA-1183). - gas_used: { - let refunded_gas: U256 = db_row.refunded_gas.into(); - db_row.gas_limit.map(|val| { - let gas_limit = bigdecimal_to_u256(val); - gas_limit - refunded_gas - }) - }, - effective_gas_price: Some( - db_row - .effective_gas_price - .map(bigdecimal_to_u256) - .unwrap_or_default(), - ), - contract_address: db_row - .contract_address - .map(|addr| h256_to_account_address(&H256::from_slice(&addr))), - logs: vec![], - l2_to_l1_logs: vec![], - status, - root: block_hash, - logs_bloom: Default::default(), - // Even though the Rust SDK recommends us to supply "None" for legacy transactions - // we always supply some number anyway to have the same behavior as most popular RPCs - transaction_type: Some(tx_type), - } - }); - match receipt { - Some(mut receipt) => { - let logs: Vec<_> = sqlx::query_as!( - StorageWeb3Log, - r#" - SELECT - address, - topic1, - topic2, - topic3, - topic4, - value, - NULL::bytea AS "block_hash", - NULL::BIGINT AS "l1_batch_number?", - miniblock_number, - tx_hash, - tx_index_in_block, - event_index_in_block, - event_index_in_tx - FROM - events - WHERE - tx_hash = $1 - ORDER BY - miniblock_number ASC, - event_index_in_block ASC - "#, - hash.as_bytes() - ) - .instrument("get_transaction_receipt_events") - .with_arg("hash", &hash) - .fetch_all(self.storage.conn()) - .await? + hashes: &[H256], + ) -> Result, SqlxError> { + let mut receipts: Vec = sqlx::query_as!( + StorageTransactionReceipt, + r#" + WITH + sl AS ( + SELECT DISTINCT + ON (storage_logs.tx_hash) * + FROM + storage_logs + WHERE + storage_logs.address = $1 + AND storage_logs.tx_hash = ANY ($3) + ORDER BY + storage_logs.tx_hash, + storage_logs.miniblock_number DESC, + storage_logs.operation_number DESC + ) + SELECT + transactions.hash AS tx_hash, + transactions.index_in_block AS index_in_block, + transactions.l1_batch_tx_index AS l1_batch_tx_index, + transactions.miniblock_number AS "block_number!", + transactions.error AS error, + transactions.effective_gas_price AS effective_gas_price, + transactions.initiator_address AS initiator_address, + transactions.data -> 'to' AS "transfer_to?", + transactions.data -> 'contractAddress' AS "execute_contract_address?", + transactions.tx_format AS "tx_format?", + transactions.refunded_gas AS refunded_gas, + transactions.gas_limit AS gas_limit, + miniblocks.hash AS "block_hash", + miniblocks.l1_batch_number AS "l1_batch_number?", + sl.key AS "contract_address?" + FROM + transactions + JOIN miniblocks ON miniblocks.number = transactions.miniblock_number + LEFT JOIN sl ON sl.value != $2 + AND sl.tx_hash = transactions.hash + WHERE + transactions.hash = ANY ($3) + "#, + ACCOUNT_CODE_STORAGE_ADDRESS.as_bytes(), + FAILED_CONTRACT_DEPLOYMENT_BYTECODE_HASH.as_bytes(), + &hashes + .iter() + .map(|h| h.as_bytes().to_vec()) + .collect::>()[..] + ) + .fetch_all(self.storage.conn()) + .await? + .into_iter() + .map(Into::into) + .collect(); + + let mut logs = self + .storage + .events_dal() + .get_logs_by_tx_hashes(hashes) + .await?; + + let mut l2_to_l1_logs = self + .storage + .events_dal() + .get_l2_to_l1_logs_by_hashes(hashes) + .await?; + + for receipt in &mut receipts { + let logs_for_tx = logs.remove(&receipt.transaction_hash); + + if let Some(logs) = logs_for_tx { + receipt.logs = logs .into_iter() - .map(|storage_log| { - let mut log = api::Log::from(storage_log); + .map(|mut log| { log.block_hash = Some(receipt.block_hash); log.l1_batch_number = receipt.l1_batch_number; log }) .collect(); + } - receipt.logs = logs; - - let l2_to_l1_logs = self.storage.events_dal().l2_to_l1_logs(hash).await?; - let l2_to_l1_logs: Vec<_> = l2_to_l1_logs - .into_iter() - .map(|storage_l2_to_l1_log| { - let mut l2_to_l1_log = api::L2ToL1Log::from(storage_l2_to_l1_log); - l2_to_l1_log.block_hash = Some(receipt.block_hash); - l2_to_l1_log.l1_batch_number = receipt.l1_batch_number; - l2_to_l1_log - }) - .collect(); - receipt.l2_to_l1_logs = l2_to_l1_logs; - - Ok(Some(receipt)) - } - None => Ok(None), + let l2_to_l1_logs_for_tx = l2_to_l1_logs.remove(&receipt.transaction_hash); + if let Some(l2_to_l1_logs) = l2_to_l1_logs_for_tx { + receipt.l2_to_l1_logs = l2_to_l1_logs + .into_iter() + .map(|mut log| { + log.block_hash = Some(receipt.block_hash); + log.l1_batch_number = receipt.l1_batch_number; + log + }) + .collect(); } } + + Ok(receipts) } pub async fn get_transaction( @@ -413,26 +346,33 @@ mod tests { ConnectionPool, }; - async fn prepare_transaction(conn: &mut StorageProcessor<'_>, tx: L2Tx) { + async fn prepare_transactions(conn: &mut StorageProcessor<'_>, txs: Vec) { conn.blocks_dal() .delete_miniblocks(MiniblockNumber(0)) .await .unwrap(); - conn.transactions_dal() - .insert_transaction_l2(tx.clone(), TransactionExecutionMetrics::default()) - .await; + + for tx in &txs { + conn.transactions_dal() + .insert_transaction_l2(tx.clone(), TransactionExecutionMetrics::default()) + .await; + } conn.blocks_dal() .insert_miniblock(&create_miniblock_header(0)) .await .unwrap(); let mut miniblock_header = create_miniblock_header(1); - miniblock_header.l2_tx_count = 1; + miniblock_header.l2_tx_count = txs.len() as u16; conn.blocks_dal() .insert_miniblock(&miniblock_header) .await .unwrap(); - let tx_results = [mock_execution_result(tx)]; + let tx_results = txs + .into_iter() + .map(mock_execution_result) + .collect::>(); + conn.transactions_dal() .mark_txs_as_executed_in_miniblock(MiniblockNumber(1), &tx_results, U256::from(1)) .await; @@ -447,7 +387,7 @@ mod tests { .await; let tx = mock_l2_transaction(); let tx_hash = tx.hash(); - prepare_transaction(&mut conn, tx).await; + prepare_transactions(&mut conn, vec![tx]).await; let block_hash = MiniblockHasher::new(MiniblockNumber(1), 0, H256::zero()) .finalize(ProtocolVersionId::latest()); @@ -501,6 +441,34 @@ mod tests { } } + #[tokio::test] + async fn getting_receipts() { + let connection_pool = ConnectionPool::test_pool().await; + let mut conn = connection_pool.access_storage().await.unwrap(); + conn.protocol_versions_dal() + .save_protocol_version_with_tx(ProtocolVersion::default()) + .await; + + let tx1 = mock_l2_transaction(); + let tx1_hash = tx1.hash(); + let tx2 = mock_l2_transaction(); + let tx2_hash = tx2.hash(); + + prepare_transactions(&mut conn, vec![tx1.clone(), tx2.clone()]).await; + + let mut receipts = conn + .transactions_web3_dal() + .get_transaction_receipts(&[tx1_hash, tx2_hash]) + .await + .unwrap(); + + receipts.sort_unstable_by_key(|receipt| receipt.transaction_index); + + assert_eq!(receipts.len(), 2); + assert_eq!(receipts[0].transaction_hash, tx1_hash); + assert_eq!(receipts[1].transaction_hash, tx2_hash); + } + #[tokio::test] async fn getting_miniblock_transactions() { let connection_pool = ConnectionPool::test_pool().await; @@ -510,7 +478,7 @@ mod tests { .await; let tx = mock_l2_transaction(); let tx_hash = tx.hash(); - prepare_transaction(&mut conn, tx).await; + prepare_transactions(&mut conn, vec![tx]).await; let raw_txs = conn .transactions_web3_dal() diff --git a/core/lib/web3_decl/src/namespaces/eth.rs b/core/lib/web3_decl/src/namespaces/eth.rs index 5ed49355fdd..8fa3b153205 100644 --- a/core/lib/web3_decl/src/namespaces/eth.rs +++ b/core/lib/web3_decl/src/namespaces/eth.rs @@ -3,7 +3,7 @@ use jsonrpsee::{ proc_macros::rpc, }; use zksync_types::{ - api::{BlockIdVariant, BlockNumber, Transaction, TransactionVariant}, + api::{BlockId, BlockIdVariant, BlockNumber, Transaction, TransactionVariant}, transaction_request::CallRequest, Address, H256, }; @@ -86,6 +86,9 @@ pub trait EthNamespace { block_number: BlockNumber, ) -> RpcResult>; + #[method(name = "getBlockReceipts")] + async fn get_block_receipts(&self, block_id: BlockId) -> RpcResult>; + #[method(name = "getBlockTransactionCountByHash")] async fn get_block_transaction_count_by_hash( &self, diff --git a/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/eth.rs b/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/eth.rs index 5f3dfcd3417..17256c50fe4 100644 --- a/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/eth.rs +++ b/core/lib/zksync_core/src/api_server/web3/backend_jsonrpsee/namespaces/eth.rs @@ -112,6 +112,12 @@ impl EthNamespaceServer for EthNamespace { .map_err(into_jsrpc_error) } + async fn get_block_receipts(&self, block_id: BlockId) -> RpcResult> { + self.get_block_receipts_impl(block_id) + .await + .map_err(into_jsrpc_error) + } + async fn get_block_transaction_count_by_hash( &self, block_hash: H256, diff --git a/core/lib/zksync_core/src/api_server/web3/namespaces/eth.rs b/core/lib/zksync_core/src/api_server/web3/namespaces/eth.rs index 70b445cd8fc..0098eacdbf0 100644 --- a/core/lib/zksync_core/src/api_server/web3/namespaces/eth.rs +++ b/core/lib/zksync_core/src/api_server/web3/namespaces/eth.rs @@ -323,6 +323,60 @@ impl EthNamespace { Ok(tx_count?.map(|(_, count)| count)) } + #[tracing::instrument(skip(self))] + pub async fn get_block_receipts_impl( + &self, + block_id: BlockId, + ) -> Result, Web3Error> { + const METHOD_NAME: &str = "get_block_receipts"; + + let method_latency = API_METRICS.start_block_call(METHOD_NAME, block_id); + + self.state.start_info.ensure_not_pruned(block_id)?; + + let block = self + .state + .connection_pool + .access_storage_tagged("api") + .await + .map_err(|err| internal_error(METHOD_NAME, err))? + .blocks_web3_dal() + .get_block_by_web3_block_id(block_id, false, self.state.api_config.l2_chain_id) + .await + .map_err(|err| internal_error(METHOD_NAME, err))?; + + let transactions: &[TransactionVariant] = + block.as_ref().map_or(&[], |block| &block.transactions); + let hashes: Vec<_> = transactions + .iter() + .map(|tx| match tx { + TransactionVariant::Full(tx) => tx.hash, + TransactionVariant::Hash(hash) => *hash, + }) + .collect(); + + let mut receipts = self + .state + .connection_pool + .access_storage_tagged("api") + .await + .map_err(|err| internal_error(METHOD_NAME, err))? + .transactions_web3_dal() + .get_transaction_receipts(&hashes) + .await + .map_err(|err| internal_error(METHOD_NAME, err))?; + + receipts.sort_unstable_by_key(|receipt| receipt.transaction_index); + + if let Some(block) = block { + self.report_latency_with_block_id(method_latency, block.number.as_u32().into()); + } else { + method_latency.observe_without_diff(); + } + + Ok(receipts) + } + #[tracing::instrument(skip(self))] pub async fn get_code_impl( &self, @@ -495,19 +549,20 @@ impl EthNamespace { const METHOD_NAME: &str = "get_transaction_receipt"; let method_latency = API_METRICS.start_call(METHOD_NAME); - let receipt = self + let receipts = self .state .connection_pool .access_storage_tagged("api") .await .unwrap() .transactions_web3_dal() - .get_transaction_receipt(hash) + .get_transaction_receipts(&[hash]) .await - .map_err(|err| internal_error(METHOD_NAME, err)); + .map_err(|err| internal_error(METHOD_NAME, err))?; method_latency.observe(); - receipt + + Ok(receipts.into_iter().next()) } #[tracing::instrument(skip(self))] diff --git a/core/lib/zksync_core/src/api_server/web3/tests/mod.rs b/core/lib/zksync_core/src/api_server/web3/tests/mod.rs index 67470b3080b..6b5f8a2fa1b 100644 --- a/core/lib/zksync_core/src/api_server/web3/tests/mod.rs +++ b/core/lib/zksync_core/src/api_server/web3/tests/mod.rs @@ -13,6 +13,7 @@ use zksync_dal::{transactions_dal::L2TxSubmissionResult, ConnectionPool, Storage use zksync_health_check::CheckHealth; use zksync_types::{ api, + api::BlockId, block::MiniblockHeader, fee::TransactionExecutionMetrics, get_nonce_key, @@ -819,3 +820,53 @@ impl HttpTest for TransactionCountAfterSnapshotRecoveryTest { async fn getting_transaction_count_for_account_after_snapshot_recovery() { test_http_server(TransactionCountAfterSnapshotRecoveryTest).await; } + +#[derive(Debug)] +struct TransactionReceiptsTest; + +#[async_trait] +impl HttpTest for TransactionReceiptsTest { + async fn test(&self, client: &HttpClient, pool: &ConnectionPool) -> anyhow::Result<()> { + let mut storage = pool.access_storage().await?; + let miniblock_number = MiniblockNumber(1); + + let tx1 = create_l2_transaction(10, 200); + let tx2 = create_l2_transaction(10, 200); + + let tx_results = vec![ + execute_l2_transaction(tx1.clone()), + execute_l2_transaction(tx2.clone()), + ]; + + store_miniblock(&mut storage, miniblock_number, &tx_results).await?; + + let mut expected_receipts = Vec::new(); + + for tx in &tx_results { + expected_receipts.push( + client + .get_transaction_receipt(tx.hash) + .await? + .expect("Receipt found"), + ); + } + + for (tx_result, receipt) in tx_results.iter().zip(&expected_receipts) { + assert_eq!(tx_result.hash, receipt.transaction_hash); + } + + let receipts = client + .get_block_receipts(BlockId::Number(miniblock_number.0.into())) + .await?; + assert_eq!(receipts.len(), 2); + for (receipt, expected_receipt) in receipts.iter().zip(&expected_receipts) { + assert_eq!(receipt, expected_receipt); + } + Ok(()) + } +} + +#[tokio::test] +async fn transaction_receipts() { + test_http_server(TransactionReceiptsTest).await; +} diff --git a/core/lib/zksync_core/src/sync_layer/tests.rs b/core/lib/zksync_core/src/sync_layer/tests.rs index 3e508accefc..5500a6c3f49 100644 --- a/core/lib/zksync_core/src/sync_layer/tests.rs +++ b/core/lib/zksync_core/src/sync_layer/tests.rs @@ -181,9 +181,11 @@ async fn external_io_basics() { let tx_receipt = storage .transactions_web3_dal() - .get_transaction_receipt(tx_hash) + .get_transaction_receipts(&[tx_hash]) .await .unwrap() + .get(0) + .cloned() .expect("Transaction not persisted"); assert_eq!(tx_receipt.block_number, 1.into()); assert_eq!(tx_receipt.transaction_index, 0.into()); diff --git a/core/tests/ts-integration/tests/api/web3.test.ts b/core/tests/ts-integration/tests/api/web3.test.ts index f8d0ea284df..70ea34816e2 100644 --- a/core/tests/ts-integration/tests/api/web3.test.ts +++ b/core/tests/ts-integration/tests/api/web3.test.ts @@ -39,12 +39,23 @@ describe('web3 API compatibility tests', () => { const blockWithTxsByNumber = await alice.provider.getBlockWithTransactions(blockNumber); expect(blockWithTxsByNumber.gasLimit).bnToBeGt(0); let sumTxGasUsed = ethers.BigNumber.from(0); + for (const tx of blockWithTxsByNumber.transactions) { const receipt = await alice.provider.getTransactionReceipt(tx.hash); sumTxGasUsed = sumTxGasUsed.add(receipt.gasUsed); } expect(blockWithTxsByNumber.gasUsed).bnToBeGte(sumTxGasUsed); + let expectedReceipts = []; + + for (const tx of blockWithTxsByNumber.transactions) { + const receipt = await alice.provider.send('eth_getTransactionReceipt', [tx.hash]); + expectedReceipts.push(receipt); + } + + let receipts = await alice.provider.send('eth_getBlockReceipts', [blockNumberHex]); + expect(receipts).toEqual(expectedReceipts); + // eth_getBlockByHash await alice.provider.getBlock(blockHash); const blockWithTxsByHash = await alice.provider.getBlockWithTransactions(blockHash);