diff --git a/CHANGELOG.md b/CHANGELOG.md index 02b676698c6..b5d5ebfc11d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,7 @@ Description of the upcoming release here. - [#1436](https://github.com/FuelLabs/fuel-core/pull/1436): Add a github action to continuously test beta-4. - [#1430](https://github.com/FuelLabs/fuel-core/pull/1430): Add "sanity" benchmarks for crypto opcodes. +- [#1437](https://github.com/FuelLabs/fuel-core/pull/1437): Add some transaction throughput tests for basic transfers. - [#1432](https://github.com/FuelLabs/fuel-core/pull/1432): Add a new `--api-request-timeout` argument to control TTL for GraphQL requests. - [#1426](https://github.com/FuelLabs/fuel-core/pull/1426) Split keygen into a create and a binary - [#1419](https://github.com/FuelLabs/fuel-core/pull/1419): Add additional "sanity" benchmarks for arithmetic op code instructions. diff --git a/Cargo.lock b/Cargo.lock index a389f07cc47..3095f0253b6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2772,12 +2772,14 @@ dependencies = [ "fuel-core-storage", "fuel-core-sync", "fuel-core-types", + "futures", "p256 0.13.2", "rand 0.7.3", "rand 0.8.5", "serde", "serde_json", "serde_yaml", + "test-helpers", "tikv-jemallocator", "tokio", ] @@ -3155,6 +3157,7 @@ dependencies = [ "serde_json", "tempfile", "test-case", + "test-helpers", "tokio", ] @@ -7883,6 +7886,23 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "test-helpers" +version = "0.0.0" +dependencies = [ + "fuel-core", + "fuel-core-client", + "fuel-core-p2p", + "fuel-core-poa", + "fuel-core-relayer", + "fuel-core-storage", + "fuel-core-trace", + "fuel-core-txpool", + "fuel-core-types", + "itertools 0.10.5", + "rand 0.8.5", +] + [[package]] name = "test-strategy" version = "0.3.1" diff --git a/benches/Cargo.toml b/benches/Cargo.toml index bc95bd636d5..5b926ef478c 100644 --- a/benches/Cargo.toml +++ b/benches/Cargo.toml @@ -19,11 +19,13 @@ fuel-core-services = { path = "./../crates/services" } fuel-core-storage = { path = "./../crates/storage" } fuel-core-sync = { path = "./../crates/services/sync", features = ["benchmarking"] } fuel-core-types = { path = "./../crates/types", features = ["test-helpers"] } +futures = "0.3" p256 = { version = "0.13", default-features = false, features = ["digest", "ecdsa"] } rand = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } serde_yaml = "0.9.13" +test-helpers = { path = "../tests/test-helpers" } tikv-jemallocator = { workspace = true } tokio = { workspace = true, features = ["full"] } @@ -45,3 +47,7 @@ default = ["fuel-core/rocksdb"] [[bench]] harness = false name = "block_target_gas" + +[[bench]] +harness = false +name = "transaction_throughput" diff --git a/benches/benches/transaction_throughput.rs b/benches/benches/transaction_throughput.rs new file mode 100644 index 00000000000..64313ac20af --- /dev/null +++ b/benches/benches/transaction_throughput.rs @@ -0,0 +1,299 @@ +//! Tests throughput of various transaction types + +use criterion::{ + criterion_group, + criterion_main, + measurement::WallTime, + BenchmarkGroup, + Criterion, + SamplingMode, +}; +use fuel_core::service::config::Trigger; +use fuel_core_benches::*; +use fuel_core_types::{ + fuel_asm::{ + op, + GMArgs, + GTFArgs, + RegId, + }, + fuel_crypto::*, + fuel_tx::{ + Finalizable, + Input, + Output, + Script, + Transaction, + TransactionBuilder, + }, + fuel_types::{ + AssetId, + Immediate12, + Immediate18, + }, + fuel_vm::checked_transaction::{ + CheckPredicateParams, + EstimatePredicates, + }, +}; +use rand::{ + rngs::StdRng, + SeedableRng, +}; +use std::{ + sync::Arc, + time::Duration, +}; +use test_helpers::builder::{ + TestContext, + TestSetupBuilder, +}; + +// Use Jemalloc during benchmarks +#[global_allocator] +static GLOBAL: tikv_jemallocator::Jemalloc = tikv_jemallocator::Jemalloc; + +fn bench_txs(group_id: &str, c: &mut Criterion, f: F) +where + F: Fn(&mut StdRng) -> Script, +{ + let inner_bench = |c: &mut BenchmarkGroup, n: u64| { + let id = format!("{}", n); + c.bench_function(id.as_str(), |b| { + let rt = tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap(); + let _drop = rt.enter(); + + let mut rng = rand::rngs::StdRng::seed_from_u64(2322u64); + + let mut transactions = vec![]; + for _ in 0..n { + transactions.push(f(&mut rng)); + } + + let mut test_builder = TestSetupBuilder::new(2322); + // setup genesis block with coins that transactions can spend + test_builder.config_coin_inputs_from_transactions( + &transactions.iter().collect::>(), + ); + // disable automated block production + test_builder.trigger = Trigger::Never; + test_builder.utxo_validation = true; + + // spin up node + let transactions: Vec = + transactions.into_iter().map(|tx| tx.into()).collect(); + let transactions = Arc::new(transactions); + + b.to_async(&rt).iter_custom(|iters| { + let mut elapsed_time = Duration::default(); + let test_builder = test_builder.clone(); + let transactions = transactions.clone(); + + async move { + for _ in 0..iters { + let mut test_builder = test_builder.clone(); + let sealed_block = { + let transactions = transactions + .iter() + .map(|tx| Arc::new(tx.clone())) + .collect(); + // start the producer node + let TestContext { srv, client, .. } = + test_builder.finalize().await; + + // insert all transactions + srv.shared.txpool.insert(transactions).await; + let _ = client.produce_blocks(1, None).await; + + // sanity check block to ensure the transactions were actually processed + let block = srv + .shared + .database + .get_sealed_block_by_height(&1.into()) + .unwrap() + .unwrap(); + assert_eq!( + block.entity.transactions().len(), + (n + 1) as usize + ); + block + }; + + // start the validator node + let TestContext { srv, .. } = test_builder.finalize().await; + + let start = std::time::Instant::now(); + + srv.shared + .block_importer + .execute_and_commit(sealed_block) + .await + .expect("Should validate the block"); + + elapsed_time += start.elapsed(); + } + elapsed_time + } + }); + }); + }; + + let mut group = c.benchmark_group(group_id); + + for i in [100, 500, 1000, 1500] { + group.throughput(criterion::Throughput::Elements(i)); + group.sampling_mode(SamplingMode::Flat); + group.sample_size(10); + inner_bench(&mut group, i); + } + + group.finish(); +} + +fn signed_transfers(c: &mut Criterion) { + let generator = |rng: &mut StdRng| { + TransactionBuilder::script(vec![], vec![]) + .gas_limit(10000) + .gas_price(1) + .add_unsigned_coin_input( + SecretKey::random(rng), + rng.gen(), + 1000, + Default::default(), + Default::default(), + Default::default(), + ) + .add_unsigned_coin_input( + SecretKey::random(rng), + rng.gen(), + 1000, + Default::default(), + Default::default(), + Default::default(), + ) + .add_output(Output::coin(rng.gen(), 50, AssetId::default())) + .add_output(Output::change(rng.gen(), 0, AssetId::default())) + .finalize() + }; + bench_txs("signed transfers", c, generator); +} + +fn predicate_transfers(c: &mut Criterion) { + let generator = |rng: &mut StdRng| { + let predicate = op::ret(RegId::ONE).to_bytes().to_vec(); + let owner = Input::predicate_owner(&predicate); + + let mut tx = TransactionBuilder::script(vec![], vec![]) + .gas_limit(10000) + .gas_price(1) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + 1000, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + predicate.clone(), + vec![], + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + 1000, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + predicate, + vec![], + )) + .add_output(Output::coin(rng.gen(), 50, AssetId::default())) + .add_output(Output::change(rng.gen(), 0, AssetId::default())) + .finalize(); + tx.estimate_predicates(&CheckPredicateParams::default()) + .expect("Predicate check failed"); + tx + }; + bench_txs("predicate transfers", c, generator); +} + +fn predicate_transfers_eck1(c: &mut Criterion) { + let generator = |rng: &mut StdRng| { + let secret = SecretKey::random(rng); + let public = secret.public_key(); + + let message = b"The gift of words is the gift of deception and illusion."; + let message = Message::new(message); + + let signature = Signature::sign(&secret, &message); + + let predicate = vec![ + op::gm_args(0x20, GMArgs::GetVerifyingPredicate), + op::gtf_args(0x20, 0x20, GTFArgs::InputCoinPredicateData), + op::addi(0x21, 0x20, signature.as_ref().len() as Immediate12), + op::addi(0x22, 0x21, message.as_ref().len() as Immediate12), + op::movi(0x10, PublicKey::LEN as Immediate18), + op::aloc(0x10), + op::move_(0x11, RegId::HP), + op::eck1(0x11, 0x20, 0x21), + op::meq(0x12, 0x22, 0x11, 0x10), + op::ret(0x12), + ] + .into_iter() + .collect::>(); + let owner = Input::predicate_owner(&predicate); + + let predicate_data: Vec = signature + .as_ref() + .iter() + .copied() + .chain(message.as_ref().iter().copied()) + .chain(public.as_ref().iter().copied()) + .collect(); + + let mut tx = TransactionBuilder::script(vec![], vec![]) + .gas_limit(10000) + .gas_price(1) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + 1000, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + predicate.clone(), + predicate_data.clone(), + )) + .add_input(Input::coin_predicate( + rng.gen(), + owner, + 1000, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + predicate, + predicate_data, + )) + .add_output(Output::coin(rng.gen(), 50, AssetId::default())) + .add_output(Output::change(rng.gen(), 0, AssetId::default())) + .finalize(); + tx.estimate_predicates(&CheckPredicateParams::default()) + .expect("Predicate check failed"); + tx + }; + bench_txs("predicate transfers eck1", c, generator); +} + +criterion_group!( + benches, + signed_transfers, + predicate_transfers, + predicate_transfers_eck1 +); +criterion_main!(benches); diff --git a/tests/Cargo.toml b/tests/Cargo.toml index d25689e8000..599880345fd 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -42,6 +42,7 @@ rstest = "0.15" serde_json = { workspace = true } tempfile = "3.3" test-case = { workspace = true } +test-helpers = { path = "./test-helpers" } tokio = { workspace = true, features = [ "macros", "rt-multi-thread", diff --git a/tests/test-helpers/Cargo.toml b/tests/test-helpers/Cargo.toml new file mode 100644 index 00000000000..f648bb0a9c1 --- /dev/null +++ b/tests/test-helpers/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "test-helpers" +version = "0.0.0" +edition = { workspace = true } +license = { workspace = true } +publish = false + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +fuel-core = { path = "../../crates/fuel-core", default-features = false, features = ["test-helpers"] } +fuel-core-client = { path = "../../crates/client", features = ["test-helpers"] } +fuel-core-p2p = { path = "../../crates/services/p2p", features = ["test-helpers"], optional = true } +fuel-core-poa = { path = "../../crates/services/consensus_module/poa" } +fuel-core-relayer = { path = "../../crates/services/relayer", features = [ + "test-helpers", +], optional = true } +fuel-core-storage = { path = "../../crates/storage", features = ["test-helpers"] } +fuel-core-trace = { path = "../../crates/trace" } +fuel-core-txpool = { path = "../../crates/services/txpool", features = ["test-helpers"] } +fuel-core-types = { path = "../../crates/types", features = ["test-helpers"] } +itertools = { workspace = true } +rand = { workspace = true } \ No newline at end of file diff --git a/tests/test-helpers/src/builder.rs b/tests/test-helpers/src/builder.rs new file mode 100644 index 00000000000..8c36892d3a3 --- /dev/null +++ b/tests/test-helpers/src/builder.rs @@ -0,0 +1,232 @@ +use fuel_core::{ + chain_config::{ + ChainConfig, + CoinConfig, + ContractConfig, + StateConfig, + }, + service::{ + Config, + FuelService, + }, +}; +use fuel_core_client::client::FuelClient; +use fuel_core_poa::Trigger; +use fuel_core_types::{ + fuel_asm::op, + fuel_tx::{ + field::Inputs, + input::coin::{ + CoinPredicate, + CoinSigned, + }, + *, + }, + fuel_types::BlockHeight, +}; +use itertools::Itertools; +use rand::{ + rngs::StdRng, + Rng, + SeedableRng, +}; +use std::{ + collections::HashMap, + io, +}; + +/// Helper for wrapping a currently running node environment +pub struct TestContext { + pub srv: FuelService, + pub rng: StdRng, + pub client: FuelClient, +} + +impl TestContext { + pub async fn new(seed: u64) -> Self { + let rng = StdRng::seed_from_u64(seed); + let srv = FuelService::new_node(Config::local_node()).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + Self { srv, rng, client } + } + + pub async fn transfer( + &mut self, + from: Address, + to: Address, + amount: u64, + ) -> io::Result { + let script = op::ret(0x10).to_bytes().to_vec(); + let tx = Transaction::script( + Default::default(), + 1_000_000, + Default::default(), + script, + vec![], + vec![Input::coin_signed( + self.rng.gen(), + from, + amount, + Default::default(), + Default::default(), + Default::default(), + Default::default(), + )], + vec![Output::coin(to, amount, Default::default())], + vec![vec![].into()], + ) + .into(); + self.client.submit_and_await_commit(&tx).await?; + Ok(tx.id(&Default::default())) + } +} + +/// Helper for configuring the genesis block in tests +#[derive(Clone)] +pub struct TestSetupBuilder { + pub rng: StdRng, + pub contracts: HashMap, + pub initial_coins: Vec, + pub min_gas_price: u64, + pub gas_limit: u64, + pub starting_block: Option, + pub utxo_validation: bool, + pub trigger: Trigger, +} + +impl TestSetupBuilder { + pub fn new(seed: u64) -> TestSetupBuilder { + Self { + rng: StdRng::seed_from_u64(seed), + ..Default::default() + } + } + + /// setup a contract and add to genesis configuration + pub fn setup_contract( + &mut self, + code: Vec, + balances: Option>, + utxo_id: Option, + tx_pointer: Option, + ) -> (Salt, ContractId) { + let contract = Contract::from(code.clone()); + let root = contract.root(); + let salt: Salt = self.rng.gen(); + let contract_id = + contract.id(&salt.clone(), &root, &Contract::default_state_root()); + + self.contracts.insert( + contract_id, + ContractConfig { + contract_id, + code, + salt, + state: None, + balances, + tx_id: utxo_id.map(|utxo_id| *utxo_id.tx_id()), + output_index: utxo_id.map(|utxo_id| utxo_id.output_index()), + tx_pointer_block_height: tx_pointer.map(|pointer| pointer.block_height()), + tx_pointer_tx_idx: tx_pointer.map(|pointer| pointer.tx_index()), + }, + ); + + (salt, contract_id) + } + + /// add input coins from a set of transaction to the genesis config + pub fn config_coin_inputs_from_transactions( + &mut self, + transactions: &[&Script], + ) -> &mut Self { + self.initial_coins.extend( + transactions + .iter() + .flat_map(|t| t.inputs()) + .filter_map(|input| { + if let Input::CoinSigned(CoinSigned { + amount, + owner, + asset_id, + utxo_id, + tx_pointer, + .. + }) + | Input::CoinPredicate(CoinPredicate { + amount, + owner, + asset_id, + utxo_id, + tx_pointer, + .. + }) = input + { + Some(CoinConfig { + tx_id: Some(*utxo_id.tx_id()), + output_index: Some(utxo_id.output_index()), + tx_pointer_block_height: Some(tx_pointer.block_height()), + tx_pointer_tx_idx: Some(tx_pointer.tx_index()), + maturity: None, + owner: *owner, + amount: *amount, + asset_id: *asset_id, + }) + } else { + None + } + }), + ); + + self + } + + // setup chainspec and spin up a fuel-node + pub async fn finalize(&mut self) -> TestContext { + let mut chain_config = ChainConfig { + initial_state: Some(StateConfig { + coins: Some(self.initial_coins.clone()), + contracts: Some(self.contracts.values().cloned().collect_vec()), + height: self.starting_block, + ..StateConfig::default() + }), + ..ChainConfig::local_testnet() + }; + chain_config.consensus_parameters.tx_params.max_gas_per_tx = self.gas_limit; + chain_config.block_gas_limit = self.gas_limit; + let config = Config { + utxo_validation: self.utxo_validation, + txpool: fuel_core_txpool::Config { + chain_config: chain_config.clone(), + min_gas_price: self.min_gas_price, + ..fuel_core_txpool::Config::default() + }, + chain_conf: chain_config, + block_production: self.trigger, + ..Config::local_node() + }; + + let srv = FuelService::new_node(config).await.unwrap(); + let client = FuelClient::from(srv.bound_address); + + TestContext { + srv, + rng: self.rng.clone(), + client, + } + } +} + +impl Default for TestSetupBuilder { + fn default() -> Self { + TestSetupBuilder { + rng: StdRng::seed_from_u64(2322u64), + contracts: Default::default(), + initial_coins: vec![], + min_gas_price: 0, + gas_limit: u64::MAX, + starting_block: None, + utxo_validation: true, + trigger: Trigger::Instant, + } + } +} diff --git a/tests/test-helpers/src/lib.rs b/tests/test-helpers/src/lib.rs new file mode 100644 index 00000000000..5575a85eb34 --- /dev/null +++ b/tests/test-helpers/src/lib.rs @@ -0,0 +1 @@ +pub mod builder; diff --git a/tests/tests/helpers.rs b/tests/tests/helpers.rs index 30899785ac7..35ffec77651 100644 --- a/tests/tests/helpers.rs +++ b/tests/tests/helpers.rs @@ -1,193 +1,4 @@ -use fuel_core::{ - chain_config::{ - ChainConfig, - CoinConfig, - ContractConfig, - StateConfig, - }, - service::{ - Config, - FuelService, - }, +pub use test_helpers::builder::{ + TestContext, + TestSetupBuilder, }; -use fuel_core_client::client::FuelClient; -use fuel_core_types::{ - fuel_tx::{ - field::Inputs, - input::coin::{ - CoinPredicate, - CoinSigned, - }, - *, - }, - fuel_types::BlockHeight, -}; -use itertools::Itertools; -use rand::{ - rngs::StdRng, - Rng, - SeedableRng, -}; -use std::collections::HashMap; - -/// Helper for wrapping a currently running node environment -pub struct TestContext { - pub srv: FuelService, - pub rng: StdRng, - pub client: FuelClient, -} - -impl TestContext { - pub async fn new(seed: u64) -> Self { - let rng = StdRng::seed_from_u64(seed); - let srv = FuelService::new_node(Config::local_node()).await.unwrap(); - let client = FuelClient::from(srv.bound_address); - Self { srv, rng, client } - } -} - -/// Helper for configuring the genesis block in tests -pub struct TestSetupBuilder { - pub rng: StdRng, - pub contracts: HashMap, - pub initial_coins: Vec, - pub min_gas_price: u64, - pub gas_limit: u64, - pub starting_block: Option, - pub utxo_validation: bool, -} - -impl TestSetupBuilder { - pub fn new(seed: u64) -> TestSetupBuilder { - Self { - rng: StdRng::seed_from_u64(seed), - ..Default::default() - } - } - - /// setup a contract and add to genesis configuration - pub fn setup_contract( - &mut self, - code: Vec, - balances: Option>, - utxo_id: Option, - tx_pointer: Option, - ) -> (Salt, ContractId) { - let contract = Contract::from(code.clone()); - let root = contract.root(); - let salt: Salt = self.rng.gen(); - let contract_id = - contract.id(&salt.clone(), &root, &Contract::default_state_root()); - - self.contracts.insert( - contract_id, - ContractConfig { - contract_id, - code, - salt, - state: None, - balances, - tx_id: utxo_id.map(|utxo_id| *utxo_id.tx_id()), - output_index: utxo_id.map(|utxo_id| utxo_id.output_index()), - tx_pointer_block_height: tx_pointer.map(|pointer| pointer.block_height()), - tx_pointer_tx_idx: tx_pointer.map(|pointer| pointer.tx_index()), - }, - ); - - (salt, contract_id) - } - - /// add input coins from a set of transaction to the genesis config - pub fn config_coin_inputs_from_transactions( - &mut self, - transactions: &[&Script], - ) -> &mut Self { - self.initial_coins.extend( - transactions - .iter() - .flat_map(|t| t.inputs()) - .filter_map(|input| { - if let Input::CoinSigned(CoinSigned { - amount, - owner, - asset_id, - utxo_id, - tx_pointer, - .. - }) - | Input::CoinPredicate(CoinPredicate { - amount, - owner, - asset_id, - utxo_id, - tx_pointer, - .. - }) = input - { - Some(CoinConfig { - tx_id: Some(*utxo_id.tx_id()), - output_index: Some(utxo_id.output_index()), - tx_pointer_block_height: Some(tx_pointer.block_height()), - tx_pointer_tx_idx: Some(tx_pointer.tx_index()), - maturity: None, - owner: *owner, - amount: *amount, - asset_id: *asset_id, - }) - } else { - None - } - }), - ); - - self - } - - // setup chainspec and spin up a fuel-node - pub async fn finalize(&mut self) -> TestContext { - let mut chain_config = ChainConfig { - initial_state: Some(StateConfig { - coins: Some(self.initial_coins.clone()), - contracts: Some(self.contracts.values().cloned().collect_vec()), - height: self.starting_block, - ..StateConfig::default() - }), - ..ChainConfig::local_testnet() - }; - chain_config.consensus_parameters.tx_params.max_gas_per_tx = self.gas_limit; - chain_config.block_gas_limit = self.gas_limit; - let config = Config { - utxo_validation: self.utxo_validation, - txpool: fuel_core_txpool::Config { - chain_config: chain_config.clone(), - min_gas_price: self.min_gas_price, - ..fuel_core_txpool::Config::default() - }, - chain_conf: chain_config, - ..Config::local_node() - }; - - let srv = FuelService::new_node(config).await.unwrap(); - let client = FuelClient::from(srv.bound_address); - - TestContext { - srv, - rng: self.rng.clone(), - client, - } - } -} - -impl Default for TestSetupBuilder { - fn default() -> Self { - TestSetupBuilder { - rng: StdRng::seed_from_u64(2322u64), - contracts: Default::default(), - initial_coins: vec![], - min_gas_price: 0, - gas_limit: u64::MAX, - starting_block: None, - utxo_validation: true, - } - } -} diff --git a/tests/tests/tx.rs b/tests/tests/tx.rs index 61cba76ffbf..e96c8d53378 100644 --- a/tests/tests/tx.rs +++ b/tests/tests/tx.rs @@ -37,10 +37,7 @@ use rand::{ Rng, SeedableRng, }; -use std::{ - io, - io::ErrorKind::NotFound, -}; +use std::io::ErrorKind::NotFound; mod predicates; mod tx_pointer; @@ -649,38 +646,6 @@ async fn get_owned_transactions() { assert_eq!(&charlie_txs, &[tx1, tx2, tx3]); } -impl TestContext { - async fn transfer( - &mut self, - from: Address, - to: Address, - amount: u64, - ) -> io::Result { - let script = op::ret(0x10).to_bytes().to_vec(); - let tx = Transaction::script( - Default::default(), - 1_000_000, - Default::default(), - script, - vec![], - vec![Input::coin_signed( - self.rng.gen(), - from, - amount, - Default::default(), - Default::default(), - Default::default(), - Default::default(), - )], - vec![Output::coin(to, amount, Default::default())], - vec![vec![].into()], - ) - .into(); - self.client.submit_and_await_commit(&tx).await?; - Ok(tx.id(&Default::default())) - } -} - fn get_executor_and_db() -> (Executor, Database) { let db = Database::default(); let relayer = MaybeRelayerAdapter {