use std::{
collections::{HashMap, HashSet},
time::Duration,
};
use zksync_dal::{
transactions_web3_dal::ExtendedTransactionReceipt, Connection, Core, CoreDal, DalError,
};
use zksync_instrument::filter::{report_filter, ReportFilter};
use zksync_multivm::zk_evm_latest::ethereum_types::{Address, U256};
use zksync_state::LruCache;
use zksync_system_constants::CONTRACT_DEPLOYER_ADDRESS;
use zksync_types::{
api::TransactionReceipt,
get_code_key, get_is_account_key, get_nonce_key, h256_to_u256,
tx::execute::DeploymentParams,
utils::{decompose_full_nonce, deployed_address_create, deployed_address_evm_create},
L2BlockNumber,
};
use zksync_web3_decl::error::Web3Error;
use super::metrics::{TxReceiptStage, TX_RECEIPT_METRICS};
static FILTER: ReportFilter = report_filter!(Duration::from_secs(60));
pub(crate) async fn fill_transaction_receipts(
storage: &mut Connection<'_, Core>,
mut receipts: Vec<ExtendedTransactionReceipt>,
) -> Result<Vec<TransactionReceipt>, Web3Error> {
let log_stats = FILTER.should_report();
TX_RECEIPT_METRICS.total_count.observe(receipts.len());
if log_stats {
tracing::debug!(
receipts.len = receipts.len(),
"filling transaction receipts"
);
}
receipts.sort_unstable_by_key(|receipt| receipt.inner.transaction_index);
let deployments = receipts.iter().map(|receipt| {
if receipt.inner.to.is_none() {
Some(DeploymentTransactionType::Evm)
} else if receipt.inner.to == Some(CONTRACT_DEPLOYER_ADDRESS) {
DeploymentParams::decode(&receipt.calldata.0).map(DeploymentTransactionType::EraVm)
} else {
None
}
});
let deployments: Vec<_> = deployments.collect();
if log_stats {
let mut evm_deployments = 0;
let mut eravm_deployments = 0;
for deployment in &deployments {
match deployment {
Some(DeploymentTransactionType::Evm) => {
evm_deployments += 1;
}
Some(DeploymentTransactionType::EraVm(_)) => {
eravm_deployments += 1;
}
None => { }
}
}
tracing::debug!(
evm_deployments,
eravm_deployments,
"determined deployment types"
);
}
let deployment_receipts = receipts
.iter()
.zip(&deployments)
.filter_map(|(receipt, deployment)| deployment.is_some().then_some(receipt));
let account_types = if let Some(first_receipt) = deployment_receipts.clone().next() {
let block_number = L2BlockNumber(first_receipt.inner.block_number.as_u32());
let from_addresses = deployment_receipts.map(|receipt| receipt.inner.from);
let latency = TX_RECEIPT_METRICS.stage_latency[&TxReceiptStage::AccountTypes].start();
let account_types =
get_external_account_types(storage, from_addresses, block_number).await?;
let latency = latency.observe();
TX_RECEIPT_METRICS
.initiator_address_count
.observe(account_types.len());
if log_stats {
let mut account_type_counts = HashMap::<_, usize>::new();
for &ty in account_types.values() {
*account_type_counts.entry(ty).or_default() += 1;
}
tracing::debug!(
?latency,
address_count = account_types.len(),
?account_type_counts,
"got account types"
);
}
account_types
} else {
HashMap::new()
};
let mut filled_receipts = Vec::with_capacity(receipts.len());
let mut receipt_indexes_with_unknown_nonce = HashSet::new();
for (i, (mut receipt, mut deployment)) in receipts.into_iter().zip(deployments).enumerate() {
if deployment.is_some()
&& matches!(
account_types[&receipt.inner.from],
ExternalAccountType::Custom
)
{
deployment = None;
}
receipt.inner.contract_address = deployment.and_then(|deployment| match deployment {
DeploymentTransactionType::Evm => Some(deployed_address_evm_create(
receipt.inner.from,
receipt.nonce,
)),
DeploymentTransactionType::EraVm(
DeploymentParams::Create | DeploymentParams::CreateAccount,
) => {
receipt_indexes_with_unknown_nonce.insert(i);
None
}
DeploymentTransactionType::EraVm(
DeploymentParams::Create2(data) | DeploymentParams::Create2Account(data),
) => Some(data.derive_address(receipt.inner.from)),
});
filled_receipts.push(receipt.inner);
}
TX_RECEIPT_METRICS
.unknown_nonces_count
.observe(receipt_indexes_with_unknown_nonce.len());
if let Some(&first_idx) = receipt_indexes_with_unknown_nonce.iter().next() {
if log_stats {
tracing::debug!(
count = receipt_indexes_with_unknown_nonce.len(),
"getting unknown nonces"
);
}
let block_number = L2BlockNumber(filled_receipts[first_idx].block_number.as_u32());
let unknown_receipts = filled_receipts
.iter_mut()
.enumerate()
.filter_map(|(i, receipt)| {
receipt_indexes_with_unknown_nonce
.contains(&i)
.then_some(receipt)
})
.collect();
fill_receipts_with_unknown_nonce(storage, block_number, unknown_receipts, log_stats)
.await?;
}
Ok(filled_receipts)
}
#[derive(Debug)]
enum DeploymentTransactionType {
Evm,
EraVm(DeploymentParams),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub(crate) enum ExternalAccountType {
Default,
Custom,
}
async fn get_external_account_types(
storage: &mut Connection<'_, Core>,
addresses: impl Iterator<Item = Address>,
block_number: L2BlockNumber,
) -> Result<HashMap<Address, ExternalAccountType>, Web3Error> {
let code_keys_to_addresses: HashMap<_, _> = addresses
.map(|from| (get_code_key(&from).hashed_key(), from))
.collect();
let code_keys: Vec<_> = code_keys_to_addresses.keys().copied().collect();
let storage_values = storage
.storage_logs_dal()
.get_storage_values(&code_keys, block_number)
.await
.map_err(DalError::generalize)?;
Ok(code_keys_to_addresses
.into_iter()
.map(|(code_key, address)| {
let value = storage_values
.get(&code_key)
.copied()
.flatten()
.unwrap_or_default();
let account_type = if value.is_zero() {
ExternalAccountType::Default
} else {
ExternalAccountType::Custom
};
(address, account_type)
})
.collect())
}
async fn fill_receipts_with_unknown_nonce(
storage: &mut Connection<'_, Core>,
block_number: L2BlockNumber,
receipts: Vec<&mut TransactionReceipt>,
log_stats: bool,
) -> Result<(), Web3Error> {
let nonce_keys: Vec<_> = receipts
.iter()
.map(|receipt| get_nonce_key(&receipt.from).hashed_key())
.collect();
let latency = TX_RECEIPT_METRICS.stage_latency[&TxReceiptStage::StoredNonces].start();
let nonces_at_block_start = storage
.storage_logs_dal()
.get_storage_values(&nonce_keys, block_number - 1)
.await
.map_err(DalError::generalize)?;
let latency = latency.observe();
if log_stats {
tracing::debug!(?latency, %block_number, "got nonces at block start");
}
let latency = TX_RECEIPT_METRICS.stage_latency[&TxReceiptStage::DeploymentEvents].start();
let deployment_events = storage
.events_web3_dal()
.get_contract_deployment_logs(block_number)
.await
.map_err(DalError::generalize)?;
let latency = latency.observe();
if log_stats {
tracing::debug!(?latency, %block_number, count = deployment_events.len(), "got deployment events in block");
}
for (receipt, nonce_key) in receipts.into_iter().zip(nonce_keys) {
let sender = receipt.from;
let tx_index = receipt.transaction_index.as_u64();
let initial_nonce = nonces_at_block_start
.get(&nonce_key)
.copied()
.flatten()
.unwrap_or_default();
let (_, initial_deploy_nonce) = decompose_full_nonce(h256_to_u256(initial_nonce));
let nonce_increment = deployment_events
.iter()
.take_while(|event| event.transaction_index_in_block < tx_index)
.filter(|event| event.deployer == sender)
.count();
let deploy_nonce = initial_deploy_nonce + nonce_increment;
receipt.contract_address = Some(deployed_address_create(sender, deploy_nonce));
}
Ok(())
}
#[derive(Debug, Clone, Copy)]
pub(crate) enum AccountType {
External(ExternalAccountType),
Contract,
}
impl AccountType {
fn is_default(self) -> bool {
matches!(self, Self::External(ExternalAccountType::Default))
}
pub(crate) fn is_external(self) -> bool {
matches!(self, Self::External(_))
}
}
impl AccountType {
async fn with_full_nonce(
storage: &mut Connection<'_, Core>,
address: Address,
block_number: L2BlockNumber,
) -> Result<(Self, U256), Web3Error> {
let code_key = get_code_key(&address).hashed_key();
let is_account_key = get_is_account_key(&address).hashed_key();
let nonce_key = get_nonce_key(&address).hashed_key();
let values = storage
.storage_logs_dal()
.get_storage_values(&[nonce_key, code_key, is_account_key], block_number)
.await
.map_err(DalError::generalize)?;
let full_nonce = values[&nonce_key].unwrap_or_default();
let code_hash = values[&code_key].unwrap_or_default();
let account_info = values[&is_account_key].unwrap_or_default();
let ty = if code_hash.is_zero() {
Self::External(ExternalAccountType::Default)
} else if account_info.is_zero() {
Self::Contract
} else {
Self::External(ExternalAccountType::Custom)
};
Ok((ty, h256_to_u256(full_nonce)))
}
}
#[derive(Debug, Clone)]
pub(crate) struct AccountTypesCache {
cache: LruCache<Address, AccountType>,
}
impl Default for AccountTypesCache {
fn default() -> Self {
Self {
cache: LruCache::uniform("account_types", 1 << 20 ),
}
}
}
impl AccountTypesCache {
pub(crate) async fn get_with_nonce(
&self,
storage: &mut Connection<'_, Core>,
address: Address,
block_number: L2BlockNumber,
) -> Result<(AccountType, U256), Web3Error> {
let (ty, full_nonce) = if let Some(ty) = self.cache.get(&address) {
let nonce_key = get_nonce_key(&address).hashed_key();
let full_nonce = storage
.storage_web3_dal()
.get_historical_value_unchecked(nonce_key, block_number)
.await
.map_err(DalError::generalize)?;
(ty, h256_to_u256(full_nonce))
} else {
let (ty, full_nonce) =
AccountType::with_full_nonce(storage, address, block_number).await?;
if !ty.is_default() || !full_nonce.is_zero() {
self.cache.insert(address, ty);
}
(ty, full_nonce)
};
let (account_nonce, deployment_nonce) = decompose_full_nonce(full_nonce);
let effective_nonce = match ty {
AccountType::Contract => deployment_nonce,
AccountType::External(_) => account_nonce,
};
Ok((ty, effective_nonce))
}
}
#[cfg(test)]
mod tests {
use assert_matches::assert_matches;
use zksync_dal::{events_web3_dal::ContractDeploymentLog, ConnectionPool};
use zksync_node_genesis::{insert_genesis_batch, GenesisParams};
use zksync_test_contracts::{Account, TestContract, TxType};
use zksync_types::{
ethabi, utils::storage_key_for_eth_balance, Address, Execute, ExecuteTransactionCommon,
StorageLog, Transaction, H256,
};
use super::*;
use crate::testonly::{
default_fee, persist_block_with_transactions, StateBuilder, TestAccount,
};
async fn prepare_storage(storage: &mut Connection<'_, Core>, rich_account: Address) {
insert_genesis_batch(storage, &GenesisParams::mock())
.await
.unwrap();
let balance_key = storage_key_for_eth_balance(&rich_account);
let balance_log = StorageLog::new_write_log(balance_key, H256::from_low_u64_be(u64::MAX));
storage
.storage_logs_dal()
.append_storage_logs(L2BlockNumber(0), &[balance_log])
.await
.unwrap();
}
#[tokio::test]
async fn fill_transaction_receipts_basics() {
let mut alice = Account::random();
let transfer = alice.create_transfer(1.into());
let transfer_hash = transfer.hash();
let deployment = alice
.get_deploy_tx(TestContract::counter().bytecode, None, TxType::L2)
.tx;
let deployment_hash = deployment.hash();
let pool = ConnectionPool::test_pool().await;
let mut storage = pool.connection().await.unwrap();
prepare_storage(&mut storage, alice.address()).await;
persist_block_with_transactions(&pool, vec![transfer.into(), deployment]).await;
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&[transfer_hash])
.await
.unwrap();
assert_eq!(receipts.len(), 1);
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
assert_eq!(filled_receipts.len(), 1);
let transfer_receipt = filled_receipts.into_iter().next().unwrap();
assert_eq!(transfer_receipt.status, 1.into());
assert_eq!(transfer_receipt.contract_address, None);
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&[deployment_hash])
.await
.unwrap();
assert_eq!(receipts.len(), 1);
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
assert_eq!(filled_receipts.len(), 1);
let deploy_receipt = filled_receipts.into_iter().next().unwrap();
assert_eq!(deploy_receipt.status, 1.into());
assert_eq!(
deploy_receipt.contract_address,
Some(deployed_address_create(alice.address(), 0.into()))
);
}
#[tokio::test]
async fn contract_address_not_filled_for_bogus_deployment() {
let mut alice = Account::random();
let mut calldata = Execute::encode_deploy_params_create(H256::zero(), H256::zero(), vec![]);
calldata.truncate(5);
let bogus_deployment = alice.get_l2_tx_for_execute(
Execute {
contract_address: Some(CONTRACT_DEPLOYER_ADDRESS),
calldata,
value: 0.into(),
factory_deps: vec![],
},
None,
);
let deployment_hash = bogus_deployment.hash();
let pool = ConnectionPool::test_pool().await;
let mut storage = pool.connection().await.unwrap();
prepare_storage(&mut storage, alice.address()).await;
persist_block_with_transactions(&pool, vec![bogus_deployment]).await;
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&[deployment_hash])
.await
.unwrap();
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
assert_eq!(filled_receipts.len(), 1);
assert_eq!(filled_receipts[0].to, Some(CONTRACT_DEPLOYER_ADDRESS));
assert_eq!(filled_receipts[0].contract_address, None);
assert_eq!(filled_receipts[0].status, 0.into());
}
#[tokio::test]
async fn various_deployments() {
let mut alice = Account::random();
let (create2_execute, create2_params) = Execute::for_create2_deploy(
H256::zero(),
TestContract::counter().bytecode.to_vec(),
&[],
);
let txs = vec![
alice.create_transfer(1.into()).into(),
alice
.get_deploy_tx(TestContract::counter().bytecode, None, TxType::L2)
.tx,
alice.create_transfer(1.into()).into(),
alice
.get_deploy_tx(
TestContract::load_test().bytecode,
Some(&[ethabi::Token::Uint(u64::MAX.into())]),
TxType::L2,
)
.tx,
alice.get_l2_tx_for_execute(create2_execute.clone(), None),
alice.get_l2_tx_for_execute(create2_execute, None),
alice
.get_deploy_tx(
TestContract::load_test().bytecode,
Some(&[ethabi::Token::Uint(10.into())]),
TxType::L2,
)
.tx,
];
let tx_hashes: Vec<_> = txs.iter().map(Transaction::hash).collect();
let expected_statuses_and_contract_addresses = [
(1_u32, None),
(1, Some(deployed_address_create(alice.address(), 0.into()))),
(1, None),
(0, Some(deployed_address_create(alice.address(), 1.into()))),
(1, Some(create2_params.derive_address(alice.address()))),
(0, Some(create2_params.derive_address(alice.address()))),
(1, Some(deployed_address_create(alice.address(), 2.into()))),
];
let pool = ConnectionPool::test_pool().await;
let mut storage = pool.connection().await.unwrap();
prepare_storage(&mut storage, alice.address()).await;
persist_block_with_transactions(&pool, txs).await;
let deployment_events = storage
.events_web3_dal()
.get_contract_deployment_logs(L2BlockNumber(1))
.await
.unwrap();
let expected_events: Vec<_> = expected_statuses_and_contract_addresses
.iter()
.enumerate()
.filter_map(|(i, &(status, address))| {
if let (1, Some(deployed_address)) = (status, address) {
Some(ContractDeploymentLog {
transaction_index_in_block: i as u64,
deployer: alice.address(),
deployed_address,
})
} else {
None
}
})
.collect();
assert_eq!(deployment_events, expected_events);
for (&tx_hash, &(status, expected_address)) in tx_hashes
.iter()
.zip(&expected_statuses_and_contract_addresses)
{
println!("Fetching receipt for {tx_hash:?}");
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&[tx_hash])
.await
.unwrap();
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
assert_eq!(filled_receipts.len(), 1);
let receipt = filled_receipts.into_iter().next().unwrap();
assert_eq!(receipt.status, status.into());
assert_eq!(receipt.contract_address, expected_address);
}
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&tx_hashes)
.await
.unwrap();
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
let statuses_and_contract_addresses: Vec<_> = filled_receipts
.iter()
.map(|receipt| (receipt.status.as_u32(), receipt.contract_address))
.collect();
assert_eq!(
statuses_and_contract_addresses,
expected_statuses_and_contract_addresses
);
}
#[tokio::test]
async fn deployments_with_custom_account() {
let mut alice = Account::random();
let (account_deploy_tx, account_addr) =
alice.create2_account(TestContract::permissive_account().bytecode.to_vec());
let pool = ConnectionPool::test_pool().await;
let mut storage = pool.connection().await.unwrap();
insert_genesis_batch(&mut storage, &GenesisParams::mock())
.await
.unwrap();
StateBuilder::default()
.with_balance(alice.address(), u64::MAX.into())
.apply(storage)
.await;
let deploy_execute = Execute {
contract_address: Some(account_addr),
calldata: TestContract::permissive_account()
.function("deploy")
.encode_input(&[ethabi::Token::Uint(5.into())])
.unwrap(),
value: 0.into(),
factory_deps: vec![TestContract::permissive_account().dependencies[0]
.bytecode
.to_vec()],
};
let mut txs = vec![
account_deploy_tx.into(),
alice
.create_transfer_with_fee(account_addr, (u64::MAX / 2).into(), default_fee())
.into(),
alice.get_l2_tx_for_execute(deploy_execute, None),
];
let mut deploy_tx_from_account = Account::random()
.get_deploy_tx(TestContract::counter().bytecode, None, TxType::L2)
.tx;
let ExecuteTransactionCommon::L2(data) = &mut deploy_tx_from_account.common_data else {
unreachable!();
};
data.initiator_address = account_addr;
txs.push(deploy_tx_from_account);
let tx_hashes: Vec<_> = txs.iter().map(Transaction::hash).collect();
let expected_contract_addresses = [Some(account_addr), None, None, None];
persist_block_with_transactions(&pool, txs).await;
let mut storage = pool.connection().await.unwrap();
let account_types = get_external_account_types(
&mut storage,
[alice.address, account_addr].into_iter(),
L2BlockNumber(1),
)
.await
.unwrap();
assert_matches!(account_types[&alice.address], ExternalAccountType::Default);
assert_matches!(account_types[&account_addr], ExternalAccountType::Custom);
let counter_addr = deployed_address_create(account_addr, 0.into());
let account_types = AccountTypesCache::default();
for _ in 0..2 {
let (ty, nonce) = account_types
.get_with_nonce(&mut storage, alice.address, L2BlockNumber(1))
.await
.unwrap();
assert_matches!(ty, AccountType::External(ExternalAccountType::Default));
assert_eq!(nonce, 3.into()); let (ty, nonce) = account_types
.get_with_nonce(&mut storage, account_addr, L2BlockNumber(1))
.await
.unwrap();
assert_matches!(ty, AccountType::External(ExternalAccountType::Custom));
assert_eq!(nonce, 1.into()); let (ty, nonce) = account_types
.get_with_nonce(&mut storage, counter_addr, L2BlockNumber(1))
.await
.unwrap();
assert_matches!(ty, AccountType::Contract);
assert_eq!(nonce, 0.into());
}
let empty_address = Address::repeat_byte(0xee);
let (ty, nonce) = account_types
.get_with_nonce(&mut storage, empty_address, L2BlockNumber(1))
.await
.unwrap();
assert_matches!(ty, AccountType::External(ExternalAccountType::Default));
assert_eq!(nonce, 0.into());
assert!(account_types.cache.get(&alice.address).is_some());
assert!(account_types.cache.get(&account_addr).is_some());
assert!(account_types.cache.get(&counter_addr).is_some());
assert!(account_types.cache.get(&empty_address).is_none());
let receipts = storage
.transactions_web3_dal()
.get_transaction_receipts(&tx_hashes)
.await
.unwrap();
let filled_receipts = fill_transaction_receipts(&mut storage, receipts)
.await
.unwrap();
let contract_addresses: Vec<_> = filled_receipts
.iter()
.map(|receipt| {
assert_eq!(receipt.status, 1.into());
receipt.contract_address
})
.collect();
assert_eq!(contract_addresses, expected_contract_addresses);
}
#[tokio::test]
async fn getting_nonce_for_evm_contract() {
let mut alice = Account::random();
let pool = ConnectionPool::test_pool().await;
let mut storage = pool.connection().await.unwrap();
insert_genesis_batch(&mut storage, &GenesisParams::mock())
.await
.unwrap();
StateBuilder::default()
.enable_evm_deployments()
.with_balance(alice.address(), u64::MAX.into())
.apply(storage)
.await;
let deploy_tx = alice.create_evm_counter_deployment(0.into());
let counter_address = deployed_address_evm_create(alice.address(), 0.into());
persist_block_with_transactions(&pool, vec![deploy_tx.into()]).await;
let account_types = AccountTypesCache::default();
let mut storage = pool.connection().await.unwrap();
let (ty, nonce) = account_types
.get_with_nonce(&mut storage, counter_address, L2BlockNumber(1))
.await
.unwrap();
assert_matches!(ty, AccountType::Contract);
assert_eq!(nonce, 1.into());
}
}