Skip to content

Commit

Permalink
Use deterministic RNG & get rid of RwLock
Browse files Browse the repository at this point in the history
  • Loading branch information
popzxc committed Apr 7, 2021
1 parent 71213ca commit ab0e216
Show file tree
Hide file tree
Showing 9 changed files with 165 additions and 76 deletions.
14 changes: 12 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion core/tests/loadnext/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ num = { version = "0.3.1", features = ["serde"] }
tokio = { version = "0.2", features = ["full"] }
futures = "0.3"
anyhow = "1.0"
rand = "0.4"
rand = { version = "0.7", features = ["small_rng"] }
envy = "0.4"

[dev-dependencies]
Expand Down
19 changes: 12 additions & 7 deletions core/tests/loadnext/src/account/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,11 +10,12 @@ use zksync_eth_signer::PrivateKeySigner;
use zksync_types::{Token, H256};

use crate::{
account_pool::AddressPool,
account_pool::{AddressPool, TestWallet},
command::{Command, ExpectedOutcome, IncorrectnessModifier, TxCommand},
config::LoadtestConfig,
constants::{COMMIT_TIMEOUT, POLLING_INTERVAL},
report::{Report, ReportBuilder, ReportLabel},
rng::LoadtestRng,
};

mod batch_command_executor;
Expand All @@ -33,6 +34,8 @@ pub struct AccountLifespan {
/// Ethereum private key of the used wallet.
/// zkSync private key can be obtained from it using `private_key_from_seed` function.
eth_pk: H256,
/// Rng unique to the account.
rng: LoadtestRng,
config: LoadtestConfig,
/// Pool of account addresses, used to generate commands.
addresses: AddressPool,
Expand All @@ -46,17 +49,19 @@ impl AccountLifespan {
pub fn new(
config: &LoadtestConfig,
addresses: AddressPool,
(wallet, eth_pk): (Wallet<PrivateKeySigner, RpcProvider>, H256),
test_account: TestWallet,
report_sink: Sender<Report>,
) -> Self {
let main_token = wallet
let main_token = test_account
.wallet
.tokens
.resolve(config.main_token.as_str().into())
.unwrap();

Self {
wallet,
eth_pk,
wallet: test_account.wallet,
eth_pk: test_account.eth_pk,
rng: test_account.rng,
config: config.clone(),
addresses,
main_token,
Expand Down Expand Up @@ -238,14 +243,14 @@ impl AccountLifespan {
}

/// Prepares a list of random operations to be executed by an account.
fn generate_commands(&self) -> Vec<Command> {
fn generate_commands(&mut self) -> Vec<Command> {
// We start with a CPK just to unlock accounts.
let mut commands = vec![Command::SingleTx(TxCommand::change_pubkey(
self.wallet.address(),
))];

for _ in 0..self.config.operations_per_account {
let command = Command::random(self.wallet.address(), &self.addresses);
let command = Command::random(&mut self.rng, self.wallet.address(), &self.addresses);
commands.push(command)
}

Expand Down
64 changes: 33 additions & 31 deletions core/tests/loadnext/src/account_pool.rs
Original file line number Diff line number Diff line change
@@ -1,44 +1,30 @@
use std::{
collections::VecDeque,
str::FromStr,
sync::{Arc, RwLock},
};
use std::{collections::VecDeque, str::FromStr, sync::Arc};

use rand::{thread_rng, Rng};
use rand::Rng;

use zksync::{utils::private_key_from_seed, RpcProvider, Wallet, WalletCredentials};
use zksync_eth_signer::PrivateKeySigner;
use zksync_types::{tx::PackedEthSignature, Address, H256};

use crate::config::LoadtestConfig;
use crate::{config::LoadtestConfig, rng::LoadtestRng};

/// Thread-safe pool of the addresses of accounts used in the loadtest.
///
/// Sync std `RwLock` is chosen instead of async `tokio` one because of `rand::thread_rng()` usage:
/// since it's not `Send`, using it in the async functions will make all the affected futures also not
/// `Send`, which in its turn will make it impossible to be used in `tokio::spawn`.
///
/// As long as we only use `read` operation on the lock, it doesn't really matter which kind of lock we use.
#[derive(Debug, Clone)]
pub struct AddressPool {
pub addresses: Arc<RwLock<Vec<Address>>>,
addresses: Arc<Vec<Address>>,
}

impl AddressPool {
pub fn new(addresses: Vec<Address>) -> Self {
Self {
addresses: Arc::new(RwLock::new(addresses)),
addresses: Arc::new(addresses),
}
}

/// Randomly chooses one of the addresses stored in the pool.
pub fn random_address(&self) -> Address {
let rng = &mut thread_rng();

let addresses = self.addresses.read().unwrap();
let index = rng.gen_range(0, addresses.len());

addresses[index]
pub fn random_address(&self, rng: &mut LoadtestRng) -> Address {
let index = rng.gen_range(0, self.addresses.len());
self.addresses[index]
}
}

Expand All @@ -54,19 +40,28 @@ pub struct AccountCredentials {

impl AccountCredentials {
/// Generates random credentials.
pub fn rand() -> Self {
let eth_pk = H256::random();
pub fn rand(rng: &mut LoadtestRng) -> Self {
let eth_pk = H256::random_using(rng);
let address = pk_to_address(&eth_pk);

Self { eth_pk, address }
}
}

/// Tuple that consists of pre-initialized wallet and the Ethereum private key.
/// We have to collect private keys, since `Wallet` doesn't expose it, and we may need it to resign transactions
/// (for example, if we want to create a corrupted transaction: `zksync` library won't allow us to do it, thus
/// we will have to sign such a transaction manually).
pub type TestWallet = (Wallet<PrivateKeySigner, RpcProvider>, H256);
/// Type that contains the data required for the test wallet to operate.
#[derive(Debug)]
pub struct TestWallet {
/// Pre-initialized wallet object.
pub wallet: Wallet<PrivateKeySigner, RpcProvider>,
/// Ethereum private key of the wallet.
/// We have to collect private keys, since `Wallet` doesn't expose it, and we may need it to resign transactions
/// (for example, if we want to create a corrupted transaction: `zksync` library won't allow us to do it, thus
/// we will have to sign such a transaction manually).
/// zkSync private key can be restored from the Ethereum one using `private_key_from_seed` function.
pub eth_pk: H256,
/// RNG object derived from a common loadtest seed and the wallet private key.
pub rng: LoadtestRng,
}

/// Pool of accounts to be used in the test.
/// Each account is represented as `zksync::Wallet` in order to provide convenient interface of interation with zkSync.
Expand All @@ -88,6 +83,8 @@ impl AccountPool {
zksync::Network::from_str(&config.eth_network).expect("Invalid network name"),
);

let mut rng = LoadtestRng::new_generic(None);

let master_wallet = {
let eth_pk = H256::from_str(&config.master_wallet_pk)
.expect("Can't parse master wallet private key");
Expand All @@ -105,7 +102,7 @@ impl AccountPool {
let mut addresses = Vec::with_capacity(config.accounts_amount);

for _ in 0..config.accounts_amount {
let eth_credentials = AccountCredentials::rand();
let eth_credentials = AccountCredentials::rand(&mut rng);
let zksync_pk = private_key_from_seed(eth_credentials.eth_pk.as_bytes())
.expect("Can't generate the zkSync private key");
let wallet_credentials = WalletCredentials::<PrivateKeySigner>::from_pk(
Expand All @@ -119,7 +116,12 @@ impl AccountPool {
.expect("Can't create a wallet");

addresses.push(wallet.address());
accounts.push_back((wallet, eth_credentials.eth_pk));
let account = TestWallet {
wallet,
eth_pk: eth_credentials.eth_pk,
rng: rng.derive(eth_credentials.eth_pk),
};
accounts.push_back(account);
}

Self {
Expand Down
18 changes: 7 additions & 11 deletions core/tests/loadnext/src/command/mod.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rand::{thread_rng, Rng};
use rand::Rng;
use zksync_types::Address;

use crate::account_pool::AddressPool;
use crate::{account_pool::AddressPool, rng::LoadtestRng};

pub use self::{
api_command::ApiRequestCommand,
Expand Down Expand Up @@ -30,7 +30,7 @@ enum CommandType {
}

impl CommandType {
fn random() -> Self {
fn random(rng: &mut LoadtestRng) -> Self {
// Chances of a certain event generation.
// You must maintain the sum of these constants to be equal to 1.0f32.
const SINGLE_TX_CHANCE: f32 = 0.7;
Expand All @@ -43,8 +43,6 @@ impl CommandType {
(CHANCES_SUM - 1.0f32).abs() <= f32::EPSILON,
"Sum of chances is not equal to 1.0"
);

let rng = &mut thread_rng();
let chance = rng.gen_range(0.0f32, 1.0f32);

if chance <= SINGLE_TX_CHANCE {
Expand All @@ -60,17 +58,15 @@ impl CommandType {
impl Command {
pub const MAX_BATCH_SIZE: usize = 20;

pub fn random(own_address: Address, addresses: &AddressPool) -> Self {
match CommandType::random() {
CommandType::SingleTx => Self::SingleTx(TxCommand::random(own_address, addresses)),
pub fn random(rng: &mut LoadtestRng, own_address: Address, addresses: &AddressPool) -> Self {
match CommandType::random(rng) {
CommandType::SingleTx => Self::SingleTx(TxCommand::random(rng, own_address, addresses)),
CommandType::Batch => {
let rng = &mut thread_rng();

// 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))
.map(|_| TxCommand::random_batchable(rng, own_address, addresses))
.collect();

if batch_command
Expand Down
51 changes: 28 additions & 23 deletions core/tests/loadnext/src/command/tx_command.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
use std::iter;

use num::BigUint;
use rand::{thread_rng, Rng};
use rand::{seq::SliceRandom, Rng};

use zksync_types::Address;

use crate::account_pool::AddressPool;
use crate::{account_pool::AddressPool, rng::LoadtestRng};

/// Type of transaction. It doesn't copy the zkSync operation list, because
/// it divides some transactions in subcategories (e.g. to new account / to existing account; to self / to other; etc)/
Expand All @@ -23,7 +23,7 @@ pub enum TxType {
impl TxType {
/// Generates a random transaction type. Not all the variants have the equal chance to be generated;
/// specifically transfers are made more likely.
pub fn random() -> Self {
pub fn random(rng: &mut LoadtestRng) -> Self {
// All available options.
let mut options = vec![
Self::Deposit,
Expand Down Expand Up @@ -56,14 +56,13 @@ impl TxType {
);

// Now we can get weighted element by simply choosing the random value from the vector.
let rng = &mut thread_rng();
rng.choose(&options).copied().unwrap()
options.choose(rng).copied().unwrap()
}

/// Generates a random transaction type that can be a part of the batch.
pub fn random_batchable() -> Self {
pub fn random_batchable(rng: &mut LoadtestRng) -> Self {
loop {
let output = Self::random();
let output = Self::random(rng);

// Priority ops and ChangePubKey cannot be inserted into the batch.
if !matches!(output, Self::Deposit | Self::FullExit | Self::ChangePubKey) {
Expand Down Expand Up @@ -112,14 +111,12 @@ pub enum ExpectedOutcome {
}

impl IncorrectnessModifier {
pub fn random() -> Self {
pub fn random(rng: &mut LoadtestRng) -> Self {
// 90% of transactions should be correct.
const NO_MODIFIER_PROBABILITY: f32 = 0.9f32;
// Amount of elements in the enum.
const MODIFIERS_AMOUNT: usize = 7;

let rng = &mut thread_rng();

let chance = rng.gen_range(0f32, 1f32);
if chance <= NO_MODIFIER_PROBABILITY {
return Self::None;
Expand Down Expand Up @@ -179,25 +176,34 @@ impl TxCommand {
}

/// Generates a fully random transaction command.
pub fn random(own_address: Address, addresses: &AddressPool) -> Self {
let command_type = TxType::random();
pub fn random(rng: &mut LoadtestRng, own_address: Address, addresses: &AddressPool) -> Self {
let command_type = TxType::random(rng);

Self::new_with_type(own_address, addresses, command_type)
Self::new_with_type(rng, own_address, addresses, command_type)
}

/// Generates a random transaction command that can be a part of the batch.
pub fn random_batchable(own_address: Address, addresses: &AddressPool) -> Self {
let command_type = TxType::random_batchable();

Self::new_with_type(own_address, addresses, command_type)
pub fn random_batchable(
rng: &mut LoadtestRng,
own_address: Address,
addresses: &AddressPool,
) -> Self {
let command_type = TxType::random_batchable(rng);

Self::new_with_type(rng, own_address, addresses, command_type)
}

fn new_with_type(own_address: Address, addresses: &AddressPool, command_type: TxType) -> Self {
fn new_with_type(
rng: &mut LoadtestRng,
own_address: Address,
addresses: &AddressPool,
command_type: TxType,
) -> Self {
let mut command = Self {
command_type,
modifier: IncorrectnessModifier::random(),
to: addresses.random_address(),
amount: Self::random_amount(),
modifier: IncorrectnessModifier::random(rng),
to: addresses.random_address(rng),
amount: Self::random_amount(rng),
};

// Check whether we should use a non-existent address.
Expand Down Expand Up @@ -244,8 +250,7 @@ impl TxCommand {
command
}

fn random_amount() -> BigUint {
let rng = &mut thread_rng();
fn random_amount(rng: &mut LoadtestRng) -> BigUint {
rng.gen_range(0u64, 2u64.pow(18)).into()
}
}
Loading

0 comments on commit ab0e216

Please sign in to comment.