Skip to content

Commit

Permalink
Fix a bunch of bugs in test and server; make loadtest pass locally
Browse files Browse the repository at this point in the history
  • Loading branch information
popzxc committed Apr 4, 2021
1 parent 61d17e8 commit 6104f93
Show file tree
Hide file tree
Showing 11 changed files with 113 additions and 41 deletions.
1 change: 1 addition & 0 deletions changelog/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ All notable changes to the core components will be documented in this file.
- (`zksync_api`): Internal error with tokens not listed on CoinGecko.
- Fix wrong block info cache behavior in the `api_server`.
- Bug with gas price limit being used instead of average gas price when storing data to DB in gas adjuster.
- Bug with Withdraw amount not checked to be packable in `Withdraw::check_correctness`.

## Release 2021-02-19

Expand Down
3 changes: 2 additions & 1 deletion core/lib/types/src/tx/withdraw.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use crate::{
helpers::{is_fee_amount_packable, pack_fee_amount},
helpers::{is_fee_amount_packable, is_token_amount_packable, pack_fee_amount},
AccountId, Nonce, TokenId,
};
use num::{BigUint, ToPrimitive};
Expand Down Expand Up @@ -140,6 +140,7 @@ impl Withdraw {
/// - zkSync signature must correspond to the PubKeyHash of the account.
pub fn check_correctness(&mut self) -> bool {
let mut valid = self.amount <= BigUint::from(u128::max_value())
&& is_token_amount_packable(&self.amount)
&& is_fee_amount_packable(&self.fee)
&& self.account_id <= max_account_id()
&& self.token <= max_token_id()
Expand Down
32 changes: 24 additions & 8 deletions core/tests/loadnext/src/account/batch_command_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use zksync::{error::ClientError, operations::SyncTransactionHandle};

use crate::{
account::AccountLifespan,
command::{IncorrectnessModifier, TxCommand, TxType},
command::{ExpectedOutcome, IncorrectnessModifier, TxCommand, TxType},
report::ReportLabel,
};

Expand All @@ -13,18 +13,23 @@ impl AccountLifespan {
) -> Result<ReportLabel, ClientError> {
let mut batch = vec![];

// Since we're manually building the batch, we have to increment nonce by ourselves.
// Otherwise all the pre-built transactions will have the same (currently committed) nonce.
let mut nonce = self.wallet.account_info().await?.committed.nonce;

for command in batch_command {
let (tx, signature) = match command.command_type {
TxType::TransferToExisting | TxType::TransferToNew => {
self.build_transfer(command).await?
self.build_transfer(command, Some(nonce)).await?
}
TxType::WithdrawToOther | TxType::WithdrawToSelf => {
self.build_withdraw(command).await?
self.build_withdraw(command, Some(nonce)).await?
}
_ => unreachable!("Other tx types are not suitable for batches"),
};

batch.push((tx, signature));
*nonce += 1;
}

// Batch result can be identified by a hash of a single transaction from this batch.
Expand All @@ -33,16 +38,27 @@ impl AccountLifespan {
// If we have multiple bad transactions in the batch, the fail reason will be equal to the
// fail reason of the first incorrect transaction.
// This goes both to failures on API side and on the state side.
// However, if there is a failure reason that will be declined by API, it should be prioritized,
// since in that case even if the first error must result in a tx rejection by state, it just
// wouldn't reach there.
let modifier = batch_command
.iter()
.find_map(|cmd| match cmd.modifier {
IncorrectnessModifier::None => None,
other => Some(other),
.map(|cmd| cmd.modifier)
.find(|modifier| {
// First attempt: find the API error-causing topic.
modifier.expected_outcome() == ExpectedOutcome::ApiRequestFailed
})
.unwrap_or(IncorrectnessModifier::None);
.unwrap_or_else(|| {
// Second attempt: find any error-causing topic.
batch_command
.iter()
.map(|cmd| cmd.modifier)
.find(|modifier| *modifier != IncorrectnessModifier::None)
.unwrap_or(IncorrectnessModifier::None)
});

let provider = self.wallet.provider.clone();
self.sumbit(modifier, || async {
self.submit(modifier, || async {
provider.send_txs_batch(batch, None).await?;
Ok(SyncTransactionHandle::new(main_hash, provider))
})
Expand Down
10 changes: 7 additions & 3 deletions core/tests/loadnext/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,10 @@ impl AccountLifespan {
retries: usize,
command: Command,
) {
if matches!(label, ReportLabel::ActionFailed { .. }) {
vlog::error!("Command failed: {:#?}", command);
}

let report = ReportBuilder::new()
.label(label)
.reporter(self.wallet.address())
Expand All @@ -159,7 +163,7 @@ impl AccountLifespan {
/// execution result.
/// Once result is obtained, it's compared to the expected operation outcome in order to check whether
/// command was completed as planned.
async fn sumbit<F, Fut>(
async fn submit<F, Fut>(
&self,
modifier: IncorrectnessModifier,
send: F,
Expand Down Expand Up @@ -210,8 +214,8 @@ impl AccountLifespan {
other => {
// Transaction status didn't match expected one.
let error = format!(
"Unexpected transaction status: expected {:#?}, receipt {:#?}",
other, transaction_receipt
"Unexpected transaction status: expected {:#?} because of modifier {:?}, receipt {:#?}",
other, modifier, transaction_receipt
);
Ok(ReportLabel::failed(&error))
}
Expand Down
60 changes: 41 additions & 19 deletions core/tests/loadnext/src/account/tx_command_executor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ use zksync::{
error::ClientError, ethereum::PriorityOpHolder, operations::SyncTransactionHandle,
provider::Provider,
};
use zksync_types::{tx::PackedEthSignature, TokenId, ZkSyncTx, H256};
use zksync_types::{tx::PackedEthSignature, Nonce, TokenId, ZkSyncTx, H256};

use crate::{
account::AccountLifespan,
Expand Down Expand Up @@ -102,9 +102,17 @@ impl AccountLifespan {
.try_into()
.unwrap_or_else(|_| u128::max_value())
.into();
let eth_tx_hash = ethereum
let eth_tx_hash = match ethereum
.deposit(self.main_token.id, amount, self.wallet.address())
.await?;
.await
{
Ok(hash) => hash,
Err(_err) => {
// Most likely we don't have enough ETH to perform operations.
// Just mark the operations as skipped.
return Ok(ReportLabel::skipped("Unable to perform an L1 operation"));
}
};

self.handle_priority_op(eth_tx_hash).await
}
Expand All @@ -127,7 +135,14 @@ impl AccountLifespan {
};

let ethereum = self.wallet.ethereum(&self.config.web3_url).await?;
let eth_tx_hash = ethereum.full_exit(exit_token_id, account_id).await?;
let eth_tx_hash = match ethereum.full_exit(exit_token_id, account_id).await {
Ok(hash) => hash,
Err(_err) => {
// Most likely we don't have enough ETH to perform operations.
// Just mark the operations as skipped.
return Ok(ReportLabel::skipped("Unable to perform an L1 operation"));
}
};

self.handle_priority_op(eth_tx_hash).await
}
Expand Down Expand Up @@ -162,7 +177,7 @@ impl AccountLifespan {
let (tx, eth_signature) = self.build_change_pubkey(command).await?;

let provider = self.wallet.provider.clone();
self.sumbit(command.modifier, || async {
self.submit(command.modifier, || async {
let tx_hash = provider.send_tx(tx, eth_signature).await?;
Ok(SyncTransactionHandle::new(tx_hash, provider))
})
Expand All @@ -186,10 +201,10 @@ impl AccountLifespan {
}

async fn execute_transfer(&self, command: &TxCommand) -> Result<ReportLabel, ClientError> {
let (tx, eth_signature) = self.build_transfer(command).await?;
let (tx, eth_signature) = self.build_transfer(command, None).await?;

let provider = self.wallet.provider.clone();
self.sumbit(command.modifier, || async {
self.submit(command.modifier, || async {
let tx_hash = provider.send_tx(tx, eth_signature).await?;
Ok(SyncTransactionHandle::new(tx_hash, provider))
})
Expand All @@ -199,26 +214,30 @@ impl AccountLifespan {
pub(super) async fn build_transfer(
&self,
command: &TxCommand,
nonce: Option<Nonce>,
) -> Result<(ZkSyncTx, Option<PackedEthSignature>), ClientError> {
let (tx, eth_signature) = self
let mut builder = self
.wallet
.start_transfer()
.to(command.to)
.amount(command.amount.clone())
.token(self.config.main_token.as_str())
.unwrap()
.tx()
.await
.map_err(Self::tx_creation_error)?;
.unwrap();

if let Some(nonce) = nonce {
builder = builder.nonce(nonce);
}

let (tx, eth_signature) = builder.tx().await.map_err(Self::tx_creation_error)?;

Ok(self.apply_modifier(tx, eth_signature, command.modifier))
}

async fn execute_withdraw(&self, command: &TxCommand) -> Result<ReportLabel, ClientError> {
let (tx, eth_signature) = self.build_withdraw(command).await?;
let (tx, eth_signature) = self.build_withdraw(command, None).await?;

let provider = self.wallet.provider.clone();
self.sumbit(command.modifier, || async {
self.submit(command.modifier, || async {
let tx_hash = provider.send_tx(tx, eth_signature).await?;
Ok(SyncTransactionHandle::new(tx_hash, provider))
})
Expand All @@ -228,17 +247,20 @@ impl AccountLifespan {
pub(super) async fn build_withdraw(
&self,
command: &TxCommand,
nonce: Option<Nonce>,
) -> Result<(ZkSyncTx, Option<PackedEthSignature>), ClientError> {
let (tx, eth_signature) = self
let mut builder = self
.wallet
.start_withdraw()
.to(command.to)
.amount(command.amount.clone())
.token(self.config.main_token.as_str())
.unwrap()
.tx()
.await
.map_err(Self::tx_creation_error)?;
.unwrap();
if let Some(nonce) = nonce {
builder = builder.nonce(nonce);
}

let (tx, eth_signature) = builder.tx().await.map_err(Self::tx_creation_error)?;

Ok(self.apply_modifier(tx, eth_signature, command.modifier))
}
Expand Down
20 changes: 18 additions & 2 deletions core/tests/loadnext/src/command/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -30,10 +30,26 @@ impl Command {
if chance < 0.7 {
Self::SingleTx(TxCommand::random(own_address, addresses))
} else {
let batch_size = rng.gen_range(1, Self::MAX_BATCH_SIZE + 1);
let batch_command = (0..batch_size)
// TODO: For some reason, batches of size 1 are being rejected because of nonce mistmatch.
// It may be either bug in loadtest or server code, thus it should be investigated.
let batch_size = rng.gen_range(2, Self::MAX_BATCH_SIZE + 1);
let mut batch_command: Vec<_> = (0..batch_size)
.map(|_| TxCommand::random_batchable(own_address, addresses))
.collect();

if batch_command
.iter()
.any(|cmd| cmd.modifier == IncorrectnessModifier::ZeroFee)
{
// Zero fee modifier is kinda weird for batches, since the summary fee may be enough to cover
// cost of one tx with zero fee. Thus in that case we set zero fee modifier to all the transactions.
// Note that behavior in the statement above is not a bug: to live in the volatile world of Ethereum,
// server may accept batches with the fee slightly below that what has been reported to user via API.
for command in batch_command.iter_mut() {
command.modifier = IncorrectnessModifier::ZeroFee;
}
}

Self::Batch(batch_command)
}
}
Expand Down
2 changes: 1 addition & 1 deletion core/tests/loadnext/src/command/tx_command.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ pub enum IncorrectnessModifier {
/// Expected outcome of transaction:
/// Since we may create erroneous transactions on purpose,
/// we may expect different outcomes for each transaction.
#[derive(Debug, Copy, Clone)]
#[derive(Debug, Copy, Clone, PartialEq)]
pub enum ExpectedOutcome {
/// Transactions was successfully executed.
TxSucceed,
Expand Down
13 changes: 10 additions & 3 deletions core/tests/loadnext/src/corrupted_tx.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use num::BigUint;
use zksync::utils::{closest_packable_token_amount, private_key_from_seed};
use zksync::utils::{
closest_packable_token_amount, is_fee_amount_packable, is_token_amount_packable,
private_key_from_seed,
};
use zksync_types::{
tx::{PackedEthSignature, TxSignature},
TokenId, ZkSyncTx, H256,
Expand Down Expand Up @@ -135,7 +138,9 @@ impl Corrupted for (ZkSyncTx, Option<PackedEthSignature>) {
}

fn not_packable_amount(mut self, eth_pk: H256, token_symbol: &str, decimals: u8) -> Self {
let bad_amount = BigUint::from(10u64.pow(18)) + BigUint::from(1u64);
let bad_amount = BigUint::from(10u128.pow(24)) + BigUint::from(1u64);
assert!(!is_token_amount_packable(&bad_amount));

match &mut self.0 {
ZkSyncTx::ChangePubKey(_tx) => unreachable!("CPK doesn't have amount"),
ZkSyncTx::ForcedExit(_tx) => unreachable!("ForcedExit doesn't have amount"),
Expand All @@ -154,6 +159,8 @@ impl Corrupted for (ZkSyncTx, Option<PackedEthSignature>) {

fn not_packable_fee(mut self, eth_pk: H256, token_symbol: &str, decimals: u8) -> Self {
let bad_fee = BigUint::from(10u64.pow(18)) + BigUint::from(1u64);
debug_assert!(!is_fee_amount_packable(&bad_fee));

match &mut self.0 {
ZkSyncTx::ChangePubKey(tx) => {
tx.fee = bad_fee;
Expand All @@ -176,7 +183,7 @@ impl Corrupted for (ZkSyncTx, Option<PackedEthSignature>) {

fn too_big_amount(mut self, eth_pk: H256, token_symbol: &str, decimals: u8) -> Self {
// We want to fail tx because of the amount, not because of packability.
let big_amount = closest_packable_token_amount(&BigUint::from(u64::max_value()));
let big_amount = closest_packable_token_amount(&BigUint::from(u128::max_value() >> 32));
match &mut self.0 {
ZkSyncTx::ChangePubKey(_tx) => unreachable!("CPK doesn't have amount"),
ZkSyncTx::ForcedExit(_tx) => unreachable!("ForcedExit doesn't have amount"),
Expand Down
4 changes: 2 additions & 2 deletions core/tests/loadnext/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ async fn main() -> anyhow::Result<()> {
zksync_rpc_addr: "http://127.0.0.1:3030".into(),
web3_url: "http://127.0.0.1:8545".into(),
master_wallet_pk: "74d8b3a188f7260f67698eb44da07397a298df5427df681ef68c45b34b61f998".into(),
accounts_amount: 101,
operations_per_account: 10,
accounts_amount: 20,
operations_per_account: 40,
main_token: "DAI".into(),
};

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ impl FailureCollector {
Self::default()
}

pub fn add_status(&mut self, status: ReportLabel) {
pub fn add_status(&mut self, status: &ReportLabel) {
match status {
ReportLabel::ActionDone => self.successes += 1,
ReportLabel::ActionSkipped { .. } => self.skipped += 1,
Expand Down
7 changes: 6 additions & 1 deletion core/tests/loadnext/src/report_collector/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,7 +41,12 @@ impl ReportCollector {
.add_metric(report.action, report.time);
}

self.failure_collector.add_status(report.label);
self.failure_collector.add_status(&report.label);

// Report failure, if it exists.
if let ReportLabel::ActionFailed { error } = &report.label {
vlog::warn!("Operation failed: {}", error);
}
}

// All the receivers are gone, it's likely the end of the test.
Expand Down

0 comments on commit 6104f93

Please sign in to comment.