Skip to content

Commit

Permalink
feat: dry run multiple transactions (FuelLabs#1631)
Browse files Browse the repository at this point in the history
closes FuelLabs#1533

---------

Co-authored-by: Hannes Karppila <[email protected]>
  • Loading branch information
matt-user and Dentosal authored Feb 2, 2024
1 parent 114d450 commit 7e54cb8
Show file tree
Hide file tree
Showing 19 changed files with 415 additions and 144 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@ Description of the upcoming release here.
#### Breaking
- [#1639](https://github.com/FuelLabs/fuel-core/pull/1639): Make Merkle metadata, i.e. `SparseMerkleMetadata` and `DenseMerkleMetadata` type version-able enums
- [#1632](https://github.com/FuelLabs/fuel-core/pull/1632): Make `Message` type a version-able enum
- [#1631](https://github.com/FuelLabs/fuel-core/pull/1631): Modify api endpoint to dry run multiple transactions.
- [#1629](https://github.com/FuelLabs/fuel-core/pull/1629): Use a separate database for each data domain. Each database has its own folder where data is stored.
- [#1628](https://github.com/FuelLabs/fuel-core/pull/1628): Make `CompressedCoin` type a version-able enum
- [#1616](https://github.com/FuelLabs/fuel-core/pull/1616): Make `BlockHeader` type a version-able enum
Expand Down
10 changes: 10 additions & 0 deletions bin/e2e-test-client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,16 @@ pub fn main_body(config: SuiteConfig, mut args: Arguments) {
Ok(())
}),
),
Trial::test(
"can dry run multiple transfer scripts and get receipts",
with_cloned(&config, |config| {
async_execute(async {
let ctx = TestContext::new(config).await;
tests::script::dry_run_multiple_txs(&ctx).await
})?;
Ok(())
}),
),
Trial::test(
"dry run script that touches the contract with large state",
with_cloned(&config, |config| {
Expand Down
77 changes: 57 additions & 20 deletions bin/e2e-test-client/src/tests/script.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ use fuel_core_types::{
Receipt,
ScriptExecutionResult,
Transaction,
UniqueIdentifier,
},
fuel_types::canonical::Deserialize,
services::executor::TransactionExecutionResult,
};
use libtest_mimic::Failed;
use std::time::Duration;
Expand Down Expand Up @@ -67,7 +69,29 @@ pub async fn dry_run(ctx: &TestContext) -> Result<(), Failed> {
)
.await??;

_dry_runs(ctx, &transaction, 1000, DryRunResult::Successful).await
_dry_runs(ctx, &[transaction], 1000, DryRunResult::Successful).await
}

// Dry run multiple transactions
pub async fn dry_run_multiple_txs(ctx: &TestContext) -> Result<(), Failed> {
let transaction1 = tokio::time::timeout(
ctx.config.sync_timeout(),
ctx.alice.transfer_tx(ctx.bob.address, 0, None),
)
.await??;
let transaction2 = tokio::time::timeout(
ctx.config.sync_timeout(),
ctx.alice.transfer_tx(ctx.alice.address, 0, None),
)
.await??;

_dry_runs(
ctx,
&[transaction1, transaction2],
1000,
DryRunResult::Successful,
)
.await
}

// Maybe deploy a contract with large state and execute the script
Expand Down Expand Up @@ -98,7 +122,7 @@ pub async fn run_contract_large_state(ctx: &TestContext) -> Result<(), Failed> {
timeout(Duration::from_secs(300), deployment_request).await??;
}

_dry_runs(ctx, &dry_run, 1000, DryRunResult::MayFail).await
_dry_runs(ctx, &[dry_run], 1000, DryRunResult::MayFail).await
}

// Send non specific transaction from `non_specific_tx.raw` file
Expand All @@ -114,12 +138,12 @@ pub async fn non_specific_transaction(ctx: &TestContext) -> Result<(), Failed> {
script.set_gas_price(0);
}

_dry_runs(ctx, &dry_run, 1000, DryRunResult::MayFail).await
_dry_runs(ctx, &[dry_run], 1000, DryRunResult::MayFail).await
}

async fn _dry_runs(
ctx: &TestContext,
transaction: &Transaction,
transactions: &[Transaction],
count: usize,
expect: DryRunResult,
) -> Result<(), Failed> {
Expand All @@ -128,7 +152,11 @@ async fn _dry_runs(
for i in 0..count {
queries.push(async move {
let before = tokio::time::Instant::now();
let query = ctx.alice.client.dry_run_opt(transaction, Some(false)).await;
let query = ctx
.alice
.client
.dry_run_opt(transactions, Some(false))
.await;
println!(
"Received the response for the query number {i} for {}ms",
before.elapsed().as_millis()
Expand All @@ -141,27 +169,36 @@ async fn _dry_runs(
let queries =
tokio::time::timeout(Duration::from_secs(60), futures::future::join_all(queries))
.await?;

let chain_info = ctx.alice.client.chain_info().await?;
for query in queries {
let (query, query_number) = query;
if let Err(e) = &query {
println!("The query {query_number} failed with {e}");
}

let receipts = query?;
if receipts.is_empty() {
return Err(
format!("Receipts are empty for query_number {query_number}").into(),
)
}

if expect == DryRunResult::Successful {
assert!(matches!(
receipts.last(),
Some(Receipt::ScriptResult {
result: ScriptExecutionResult::Success,
..
})
));
let tx_statuses = query?;
for (tx_status, tx) in tx_statuses.iter().zip(transactions.iter()) {
if tx_status.receipts.is_empty() {
return Err(
format!("Receipts are empty for query_number {query_number}").into(),
)
}

assert!(tx.id(&chain_info.consensus_parameters.chain_id) == tx_status.id);
if expect == DryRunResult::Successful {
assert!(matches!(
&tx_status.result,
TransactionExecutionResult::Success { result: _result }
));
assert!(matches!(
tx_status.receipts.last(),
Some(Receipt::ScriptResult {
result: ScriptExecutionResult::Success,
..
})
));
}
}
}
Ok(())
Expand Down
14 changes: 9 additions & 5 deletions bin/fuel-core-client/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ enum TransactionCommands {
/// Submit a JSON encoded transaction for predicate estimation.
EstimatePredicates { tx: String },
/// Submit a JSON encoded transaction for a dry-run execution
DryRun { tx: String },
DryRun { txs: Vec<String> },
/// Get the transactions associated with a particular transaction id
Get { id: String },
/// Get the receipts for a particular transaction id
Expand Down Expand Up @@ -64,11 +64,15 @@ impl CliArgs {
.expect("Should be able to estimate predicates");
println!("{:?}", tx);
}
TransactionCommands::DryRun { tx } => {
let tx: Transaction =
serde_json::from_str(tx).expect("invalid transaction json");
TransactionCommands::DryRun { txs } => {
let txs: Vec<Transaction> = txs
.iter()
.map(|tx| {
serde_json::from_str(tx).expect("invalid transaction json")
})
.collect();

let result = client.dry_run(&tx).await;
let result = client.dry_run(&txs).await;
println!("{:?}", result.unwrap());
}
TransactionCommands::Get { id } => {
Expand Down
21 changes: 19 additions & 2 deletions crates/client/assets/schema.sdl
Original file line number Diff line number Diff line change
Expand Up @@ -255,6 +255,23 @@ type ContractParameters {

union DependentCost = LightOperation | HeavyOperation

type DryRunFailureStatus {
programState: ProgramState
reason: String!
}

type DryRunSuccessStatus {
programState: ProgramState
}

type DryRunTransactionExecutionStatus {
id: TransactionId!
status: DryRunTransactionStatus!
receipts: [Receipt!]!
}

union DryRunTransactionStatus = DryRunSuccessStatus | DryRunFailureStatus

input ExcludeInput {
"""
Utxos to exclude from the selection.
Expand Down Expand Up @@ -619,9 +636,9 @@ type Mutation {
"""
continueTx(id: ID!): RunResult!
"""
Execute a dry-run of the transaction using a fork of current state, no changes are committed.
Execute a dry-run of multiple transactions using a fork of current state, no changes are committed.
"""
dryRun(tx: HexString!, utxoValidation: Boolean): [Receipt!]!
dryRun(txs: [HexString!]!, utxoValidation: Boolean): [DryRunTransactionExecutionStatus!]!
"""
Submits transaction to the `TxPool`.

Expand Down
36 changes: 23 additions & 13 deletions crates/client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,10 @@ use fuel_core_types::{
BlockHeight,
Nonce,
},
services::p2p::PeerInfo,
services::{
executor::TransactionExecutionStatus,
p2p::PeerInfo,
},
};
#[cfg(feature = "subscriptions")]
use futures::StreamExt;
Expand Down Expand Up @@ -358,26 +361,33 @@ impl FuelClient {
}

/// Default dry run, matching the exact configuration as the node
pub async fn dry_run(&self, tx: &Transaction) -> io::Result<Vec<Receipt>> {
self.dry_run_opt(tx, None).await
pub async fn dry_run(
&self,
txs: &[Transaction],
) -> io::Result<Vec<TransactionExecutionStatus>> {
self.dry_run_opt(txs, None).await
}

/// Dry run with options to override the node behavior
pub async fn dry_run_opt(
&self,
tx: &Transaction,
txs: &[Transaction],
// Disable utxo input checks (exists, unspent, and valid signature)
utxo_validation: Option<bool>,
) -> io::Result<Vec<Receipt>> {
let tx = tx.clone().to_bytes();
let query = schema::tx::DryRun::build(DryRunArg {
tx: HexString(Bytes(tx)),
utxo_validation,
});
let receipts = self.query(query).await.map(|r| r.dry_run)?;
receipts
) -> io::Result<Vec<TransactionExecutionStatus>> {
let txs = txs
.iter()
.map(|tx| HexString(Bytes(tx.to_bytes())))
.collect::<Vec<HexString>>();
let query: Operation<schema::tx::DryRun, DryRunArg> =
schema::tx::DryRun::build(DryRunArg {
txs,
utxo_validation,
});
let tx_statuses = self.query(query).await.map(|r| r.dry_run)?;
tx_statuses
.into_iter()
.map(|receipt| receipt.try_into().map_err(Into::into))
.map(|tx_status| tx_status.try_into().map_err(Into::into))
.collect()
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,40 +2,59 @@
source: crates/client/src/client/schema/tx.rs
expression: query.query
---
mutation($tx: HexString!, $utxoValidation: Boolean) {
dryRun(tx: $tx, utxoValidation: $utxoValidation) {
param1
param2
amount
assetId
gas
digest
contract {
id
mutation($txs: [HexString!]!, $utxoValidation: Boolean) {
dryRun(txs: $txs, utxoValidation: $utxoValidation) {
id
status {
__typename
... on DryRunSuccessStatus {
programState {
returnType
data
}
}
... on DryRunFailureStatus {
reason
programState {
returnType
data
}
}
}
is
pc
ptr
ra
rb
rc
rd
reason
receiptType
to {
id
receipts {
param1
param2
amount
assetId
gas
digest
contract {
id
}
is
pc
ptr
ra
rb
rc
rd
reason
receiptType
to {
id
}
toAddress
val
len
result
gasUsed
data
sender
recipient
nonce
contractId
subId
}
toAddress
val
len
result
gasUsed
data
sender
recipient
nonce
contractId
subId
}
}

Expand Down
Loading

0 comments on commit 7e54cb8

Please sign in to comment.