diff --git a/networks/movement/movement-full-node/src/admin/l1_migration/validate/compare.rs b/networks/movement/movement-full-node/src/admin/l1_migration/validate/compare.rs index 2aaedc9f8..5f04dcebf 100644 --- a/networks/movement/movement-full-node/src/admin/l1_migration/validate/compare.rs +++ b/networks/movement/movement-full-node/src/admin/l1_migration/validate/compare.rs @@ -2,36 +2,28 @@ use aptos_api_types::transaction::UserTransaction; use aptos_api_types::{Event, WriteSetChange}; use tracing::error; +pub struct CompareResult { + pub events_match: bool, + pub changes_match: bool, +} + pub fn compare_transaction_outputs( - movement_txn: UserTransaction, - aptos_txn: UserTransaction, + movement_txn: &UserTransaction, + aptos_txn: &UserTransaction, show_diff: bool, -) -> anyhow::Result { +) -> CompareResult { let txn_hash = movement_txn.info.hash.0.to_hex_literal(); - if movement_txn.info.hash != aptos_txn.info.hash { - error!( - "Transaction hash mismatch:\nMovement transaction hash:{}\nAptos transaction hash:{}", - txn_hash, - aptos_txn.info.hash.0.to_hex_literal() - ); - return Ok(false); - } - let movement_events = movement_txn.events.iter().map(Into::::into).collect::>(); let aptos_events = aptos_txn.events.iter().map(Into::::into).collect::>(); - if movement_events != aptos_events { - if show_diff { - error!( - "Transaction events mismatch ({})\n{}", - txn_hash, - display_diff(&movement_txn.events, &aptos_txn.events)? - ); - } else { - error!("Transaction events mismatch ({})", txn_hash,); - } - return Ok(false); + let events_match = movement_events == aptos_events; + if !events_match && show_diff { + error!( + "Transaction events mismatch ({})\n{}", + txn_hash, + display_diff(&movement_txn.events, &aptos_txn.events) + ); } let movement_changes = movement_txn @@ -46,32 +38,32 @@ pub fn compare_transaction_outputs( .iter() .map(Into::::into) .collect::>(); - if movement_changes != aptos_changes { - if show_diff { - error!( - "Transaction write-set mismatch ({})\n{}", - txn_hash, - display_diff(&movement_txn.info.changes, &aptos_txn.info.changes)? - ); - } else { - error!("Transaction write-set mismatch ({})", txn_hash,); - } - return Ok(false); + let changes_match = movement_changes == aptos_changes; + if !changes_match && show_diff { + error!( + "Transaction write-set mismatch ({})\n{}", + txn_hash, + display_diff(&movement_txn.info.changes, &aptos_txn.info.changes) + ); } - Ok(true) + CompareResult { events_match, changes_match } } -fn display_diff(movement_values: &[T], aptos_values: &[T]) -> anyhow::Result +fn display_diff(movement_values: &[T], aptos_values: &[T]) -> String where T: serde::Serialize, { - let movement_json = serde_json::to_string_pretty(movement_values)?; - let aptos_json = serde_json::to_string_pretty(aptos_values)?; - Ok(create_diff(&movement_json, &aptos_json)?) + let movement_json = serde_json::to_string_pretty(movement_values); + let aptos_json = serde_json::to_string_pretty(aptos_values); + if let (Ok(movement_json), Ok(aptos_json)) = (movement_json, aptos_json) { + create_diff(&movement_json, &aptos_json) + } else { + "Failed to create diff".into() + } } -fn create_diff(movement: &str, aptos: &str) -> anyhow::Result { +fn create_diff(movement: &str, aptos: &str) -> String { use console::Style; use similar::{ChangeTag, TextDiff}; use std::fmt::Write; @@ -85,8 +77,8 @@ fn create_diff(movement: &str, aptos: &str) -> anyhow::Result { .collect::>(); let last_hunk_idx = hunks.len() - 1; - writeln!(out, "--- Movement full-node")?; - writeln!(out, "+++ Aptos validator-node")?; + writeln!(out, "--- Movement full-node").unwrap(); + writeln!(out, "+++ Aptos validator-node").unwrap(); for (idx, hunk) in hunks.iter().enumerate() { for op in hunk.iter() { for change in diff.iter_changes(op) { @@ -95,15 +87,15 @@ fn create_diff(movement: &str, aptos: &str) -> anyhow::Result { ChangeTag::Insert => ("+", Style::new().green()), ChangeTag::Equal => (" ", Style::new()), }; - write!(out, "{}{}", style.apply_to(sign).bold(), style.apply_to(change))?; + write!(out, "{}{}", style.apply_to(sign).bold(), style.apply_to(change)).unwrap(); } } if idx < last_hunk_idx { - writeln!(out, "===")?; + writeln!(out, "===").unwrap(); } } - Ok(out) + out } struct EventCompare<'a>(&'a Event); diff --git a/networks/movement/movement-full-node/src/admin/l1_migration/validate/replay.rs b/networks/movement/movement-full-node/src/admin/l1_migration/validate/replay.rs index 952924644..be8fdaef5 100644 --- a/networks/movement/movement-full-node/src/admin/l1_migration/validate/replay.rs +++ b/networks/movement/movement-full-node/src/admin/l1_migration/validate/replay.rs @@ -2,17 +2,30 @@ use crate::admin::l1_migration::validate::compare::compare_transaction_outputs; use crate::admin::l1_migration::validate::types::api::{AptosRestClient, MovementRestClient}; use crate::admin::l1_migration::validate::types::da::{get_da_block_height, DaSequencerClient}; use anyhow::Context; +use aptos_api_types::{AptosError, AptosErrorCode, Transaction}; use aptos_crypto::HashValue; -use aptos_types::transaction::SignedTransaction; +use aptos_rest_client::error::RestError; +use aptos_rest_client::Response; +use aptos_types::transaction::{SignedTransaction, TransactionPayload}; use clap::{Args, Parser}; -use std::collections::HashSet; +use std::collections::HashMap; +use std::fmt::Display; use std::path::PathBuf; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::Duration; use tokio::sync::mpsc; use tokio::task::JoinSet; use tokio_stream::StreamExt; use tracing::{debug, error, info, warn}; +const DEFAULT_MAX_SERVER_LAG_WAIT_DURATION: Duration = Duration::from_secs(60); +const LOG_PREFIX: &str = "@R:"; +const SUBMISSION: &str = "S:"; +const EXECUTION: &str = "E:"; +const APTOS_FAILED: &str = "AF:"; +const MOVEMENT_FAILED: &str = "MF:"; +const BOTH_FAILED: &str = "BF:"; +const BOTH_SUCCEEDED: &str = "BS:"; + #[derive(Parser, Debug)] #[clap(name = "replay", about = "Stream transactions from DA-sequencer blocks")] pub struct DaReplayTransactions { @@ -54,23 +67,38 @@ impl DaReplayTransactions { let da_sequencer_client = DaSequencerClient::try_connect(&self.da_sequencer_url).await?; let aptos_rest_client = AptosRestClient::try_connect(&self.aptos_api_url).await?; + let gas_price = aptos_rest_client.estimate_gas_price().await?.into_inner(); + info!("Gas price Aptos: {:?}", gas_price); + // Spawn a task which compares transaction outputs from the Movement node and Aptos node - let tx_hashes = if let Some(ref movement_api_url) = self.movement_api_url { + let tx_validate_submission = if let Some(ref movement_api_url) = self.movement_api_url { let movement_rest_client = MovementRestClient::try_connect(movement_api_url).await?; - let (tx_hashes, rx_hashes) = mpsc::unbounded_channel::(); - tasks.spawn(validate_transactions( + + let gas_price = aptos_rest_client.estimate_gas_price().await?.into_inner(); + info!("Gas price Movement: {:?}", gas_price); + + let (tx_validate_execution, rx_validate_execution) = + mpsc::unbounded_channel::(); + let (tx_validate_submission, rx_validate_submission) = + mpsc::unbounded_channel::(); + tasks.spawn(validate_transaction_execution( aptos_rest_client.clone(), - movement_rest_client, - rx_hashes, + movement_rest_client.clone(), + rx_validate_execution, self.show_diff, )); - Some(tx_hashes) + tasks.spawn(validate_transaction_submission( + movement_rest_client, + rx_validate_submission, + tx_validate_execution, + )); + Some(tx_validate_submission) } else { None }; // Spawn a task which submits transaction batches to the validator node - tasks.spawn(submit_transactions(aptos_rest_client, rx_batches, tx_hashes)); + tasks.spawn(submit_transactions(aptos_rest_client, rx_batches, tx_validate_submission)); // Spawn a task which fetches transaction batches ahead tasks.spawn(stream_transactions(da_sequencer_client, tx_batches, block_height)); @@ -126,29 +154,34 @@ async fn stream_transactions( async fn submit_transactions( aptos_rest_client: AptosRestClient, mut rx_batches: mpsc::Receiver>, - tx_hashes: Option>, + tx_validate_submission: Option>, ) { while let Some(txns) = rx_batches.recv().await { match aptos_rest_client.submit_batch_bcs(&txns).await { Ok(result) => { debug!("Submitted {} Aptos transaction(s)", txns.len()); - let mut failed_txns = HashSet::new(); - for failure in result.into_inner().transaction_failures { - failed_txns.insert(failure.transaction_index); - let txn = &txns[failure.transaction_index]; - let hash = txn.committed_hash().to_hex_literal(); - error!("Failed to submit Aptos transaction {}: {}", hash, failure.error); - } - if let Some(ref tx_hashes) = tx_hashes { + let mut errors = result + .into_inner() + .transaction_failures + .into_iter() + .map(|item| (item.transaction_index, item.error)) + .collect::>(); + + if let Some(ref tx_validate_submission) = tx_validate_submission { if txns .iter() .enumerate() - .filter_map(|item| match item { - (idx, _) if failed_txns.contains(&idx) => None, - (_, txn) => Some(txn.committed_hash()), + .try_for_each(|(idx, txn)| { + tx_validate_submission.send(ValidateSubmission { + txn_info: TransactionInfo { + hash: txn.committed_hash(), + payload: payload_info(txn), + expires: txn.expiration_timestamp_secs(), + }, + error: errors.remove(&idx), + }) }) - .try_for_each(|hash| tx_hashes.send(hash)) .is_err() { // channel is closed @@ -165,20 +198,29 @@ async fn submit_transactions( warn!("Stream of transaction batches ended unexpectedly"); } -async fn validate_transactions( +async fn validate_transaction_execution( aptos_rest_client: AptosRestClient, movement_rest_client: MovementRestClient, - mut rx_hashes: mpsc::UnboundedReceiver, + mut rx_validate_execution: mpsc::UnboundedReceiver, show_diff: bool, ) { use aptos_api_types::transaction::Transaction; - while let Some(hash) = rx_hashes.recv().await { - let hash_str = hash.to_hex_literal(); - let timeout = SystemTime::now().duration_since(UNIX_EPOCH).unwrap().as_secs() + 60; + while let Some(ValidateExecution { txn_info }) = rx_validate_execution.recv().await { + let hash = txn_info.hash; let result = tokio::join!( - movement_rest_client.wait_for_transaction_by_hash(hash, timeout, None, None), - aptos_rest_client.wait_for_transaction_by_hash(hash, timeout, None, None) + movement_rest_client.wait_for_transaction_by_hash( + hash, + txn_info.expires, + Some(DEFAULT_MAX_SERVER_LAG_WAIT_DURATION), + None + ), + aptos_rest_client.wait_for_transaction_by_hash( + hash, + txn_info.expires, + Some(DEFAULT_MAX_SERVER_LAG_WAIT_DURATION), + None, + ) ); match result { @@ -189,27 +231,235 @@ async fn validate_transactions( let Transaction::UserTransaction(txn_aptos) = txn_aptos.into_inner() else { unreachable!() }; + let result = compare_transaction_outputs(&txn_movement, &txn_aptos, show_diff); + let (is_error, msg) = match (result.events_match, result.changes_match) { + (true, true) => (false, "ok"), + (true, false) => (true, "changes mismatch"), + (false, true) => (true, "events mismatch"), + (false, false) => (true, "events mismatch, changes mismatch"), + }; + let version_movement = Some(*txn_movement.info.version.inner()); + let gas_movement = Some(*txn_movement.info.gas_used.inner()); + let version_aptos = Some(*txn_aptos.info.version.inner()); + let gas_aptos = Some(*txn_aptos.info.gas_used.inner()); + log_execution( + is_error, + BOTH_SUCCEEDED, + hash, + &txn_info.payload, + msg, + version_movement, + gas_movement, + version_aptos, + gas_aptos, + ); + } + (Ok(txn_movement), Err(error_aptos)) => { + let Transaction::UserTransaction(txn_movement) = txn_movement.into_inner() else { + unreachable!() + }; + let version_movement = Some(*txn_movement.info.version.inner()); + let gas_movement = Some(*txn_movement.info.gas_used.inner()); + log_execution( + true, + APTOS_FAILED, + hash, + &txn_info.payload, + error_aptos, + version_movement, + gas_movement, + None, + None, + ); + } + (Err(error_movement), Ok(txn_aptos)) => { + let Transaction::UserTransaction(txn_aptos) = txn_aptos.into_inner() else { + unreachable!() + }; + let version_aptos = Some(*txn_aptos.info.version.inner()); + let gas_aptos = Some(*txn_aptos.info.gas_used.inner()); + log_execution( + true, + MOVEMENT_FAILED, + hash, + &txn_info.payload, + error_movement, + None, + None, + version_aptos, + gas_aptos, + ); + } + (Err(error_movement), Err(error_aptos)) => { + let error_movement = format!("{}", error_movement); + let error_aptos = format!("{}", error_aptos); - match compare_transaction_outputs(*txn_movement, *txn_aptos, show_diff) { - Ok(valid) if valid => debug!("Validated transaction {}", hash_str), - Ok(_) => {} // invalid, errors logged elsewhere - Err(e) => error!("Failed to validate transaction {}: {}", hash_str, e), + if error_movement == error_aptos { + log_execution( + false, + BOTH_FAILED, + hash, + &txn_info.payload, + "same error", + None, + None, + None, + None, + ); + } else { + log_execution( + true, + BOTH_FAILED, + hash, + &txn_info.payload, + format!("(movement: {} // aptos: {})", error_movement, error_aptos), + None, + None, + None, + None, + ); } } - (Ok(_), Err(error_aptos)) => { - error!( - "The execution of the transaction {} failed on Aptos: {}", - hash_str, error_aptos - ) + } + } + warn!("Stream of transaction hashes ended unexpectedly"); +} + +fn log_execution( + is_error: bool, + result: &str, + hash: HashValue, + payload: &str, + message: impl Display, + version_movement: Option, + gas_movement: Option, + version_aptos: Option, + gas_aptos: Option, +) { + let msg = format!( + "{}{}{}({}/{})[A:{}:{}/M:{}:{}]: {}", + LOG_PREFIX, + EXECUTION, + result, + hash.to_hex_literal(), + payload, + version_aptos.map_or("none".to_string(), |version| version.to_string()), + gas_aptos.map_or("none".to_string(), |gas| gas.to_string()), + version_movement.map_or("none".to_string(), |version| version.to_string()), + gas_movement.map_or("none".to_string(), |gas| gas.to_string()), + message + ); + if is_error { + error!("{msg}"); + } else { + info!("{msg}"); + } +} + +async fn validate_transaction_submission( + movement_rest_client: MovementRestClient, + mut rx_validate_submission: mpsc::UnboundedReceiver, + tx_validate_execution: mpsc::UnboundedSender, +) { + while let Some(ValidateSubmission { txn_info, error }) = rx_validate_submission.recv().await { + let hash = txn_info.hash; + let execute = error.is_none(); + let result = get_transaction_by_hash(&movement_rest_client, hash, 3).await; + + match (result, error) { + (Ok(_), None) => { + log_submission(false, BOTH_SUCCEEDED, hash, "ok"); } - (Err(error_movement), Ok(_)) => error!( - "The execution of the transaction {} failed on Movement but succeeded on Aptos: {}", - hash_str, error_movement - ), - _ => { - // ignore if the execution failed on both sides??? + (Ok(_), Some(error_aptos)) => { + log_submission(true, APTOS_FAILED, hash, error_aptos); + } + (Err(error_movement), None) => { + log_submission(true, MOVEMENT_FAILED, hash, error_movement); + } + (Err(mvmt_err), Some(error_aptos)) => { + if let RestError::Http(_, _) = mvmt_err { + log_submission(false, BOTH_FAILED, hash, "not executed on movement"); + } else { + // Submission of the txn succeeded on Movement, but the execution failed. + // However, we are logging only the Aptos submission error here + // because we assume that if the submission had succeeded, the execution + // would have failed on Aptos in the same way. + log_submission(true, APTOS_FAILED, hash, error_aptos); + } + } + }; + + if execute { + if tx_validate_execution.send(ValidateExecution { txn_info }).is_err() { + // channel is closed + break; } } } - warn!("Stream of transaction hashes ended unexpectedly"); +} + +async fn get_transaction_by_hash( + movement_rest_client: &MovementRestClient, + hash: HashValue, + retries: u8, +) -> Result, RestError> { + for idx in (0..retries).into_iter().rev() { + let result = movement_rest_client.get_transaction_by_hash(hash).await; + match result { + Ok(txn_movement) => { + return Ok(txn_movement); + } + Err(err) if idx == 0 => { + return Err(err); + } + Err(err) => match err { + RestError::Api(ref api_err) => { + if let AptosErrorCode::TransactionNotFound = api_err.error.error_code { + tokio::time::sleep(Duration::from_secs(1)).await; + continue; + } else { + return Err(err); + } + } + _ => return Err(err), + }, + } + } + unreachable!() +} + +fn log_submission(is_error: bool, result: &str, hash: HashValue, message: impl Display) { + let msg = + format!("{}{}{}({}): {}", LOG_PREFIX, SUBMISSION, result, hash.to_hex_literal(), message); + if is_error { + error!("{msg}"); + } else { + info!("{msg}"); + } +} + +fn payload_info(txn: &SignedTransaction) -> String { + match txn.payload() { + TransactionPayload::Script(_) => "script".to_string(), + TransactionPayload::ModuleBundle(_) => "depricated".to_string(), + TransactionPayload::EntryFunction(ef) => { + format!("entry:{}::{}", ef.module(), ef.function()) + } + TransactionPayload::Multisig(sig) => format!("multisig:{}", sig.multisig_address.to_hex()), + } +} + +struct TransactionInfo { + pub hash: HashValue, + pub payload: String, + pub expires: u64, +} + +struct ValidateExecution { + pub txn_info: TransactionInfo, +} + +struct ValidateSubmission { + pub txn_info: TransactionInfo, + pub error: Option, } diff --git a/scripts/l1_migration/process_replay_logs.sh b/scripts/l1_migration/process_replay_logs.sh new file mode 100755 index 000000000..046b6e218 --- /dev/null +++ b/scripts/l1_migration/process_replay_logs.sh @@ -0,0 +1,219 @@ +#!/bin/bash + +# Exit on first error +set -e + +# Check if input file parameter is provided +if [ $# -ne 1 ]; then + echo "Usage: $0 " + echo "Example: $0 replay_logs" + exit 1 +fi + +INPUT_FILE="$1" + +# Check if input file exists +if [ ! -f "$INPUT_FILE" ]; then + echo "Error: Input file '$INPUT_FILE' does not exist." + exit 1 +fi + +# Get the directory of the input file +INPUT_DIR=$(dirname "$INPUT_FILE") + +# Function to create output file path in the same directory as input +get_output_path() { + echo "$INPUT_DIR/$1" +} + +# Function to log file creation +log_creation() { + echo "Creating file: $1" +} + +echo "Processing log file: $INPUT_FILE" +echo "Output directory: $INPUT_DIR" +echo + +# Step 1: Extract replay logs +CLEAN_LOGS=$(get_output_path "clean_logs") +log_creation "$CLEAN_LOGS" +grep "@R:" "$INPUT_FILE" | sed -e "s|.*@R:|@|g" > "$CLEAN_LOGS" + +# Step 2: Extract submission results from replay logs +S_LOGS=$(get_output_path "S_logs") +log_creation "$S_LOGS" +grep "@S:" "$CLEAN_LOGS" | sed -e "s|@S:|@|g" > "$S_LOGS" + +# Step 3: Split submission results into different scenarios +S_BS_LOGS=$(get_output_path "S_BS_logs") +log_creation "$S_BS_LOGS" +grep "@BS:" "$S_LOGS" | sed -E "s|@BS:(.*)|@\1|" > "$S_BS_LOGS" + +S_BF_LOGS=$(get_output_path "S_BF_logs") +log_creation "$S_BF_LOGS" +grep "@BF:" "$S_LOGS" | sed -E "s|@BF:(.*)|@\1|" > "$S_BF_LOGS" + +S_AF_LOGS=$(get_output_path "S_AF_logs") +log_creation "$S_AF_LOGS" +grep "@AF:" "$S_LOGS" | sed -E "s|@AF:(.*)|@\1|" > "$S_AF_LOGS" + +S_MF_LOGS=$(get_output_path "S_MF_logs") +log_creation "$S_MF_LOGS" +grep "@MF:" "$S_LOGS" | sed -E "s|@MF:(.*)|@\1|" > "$S_MF_LOGS" + +# Step 4: Extract execution results from replay logs +E_LOGS=$(get_output_path "E_logs") +log_creation "$E_LOGS" +grep "@E:" "$CLEAN_LOGS" | sed -e "s|@E:|@|g" > "$E_LOGS" + +# Step 5: Split execution results into different scenarios +E_BS_LOGS=$(get_output_path "E_BS_logs") +log_creation "$E_BS_LOGS" +grep "@BS:" "$E_LOGS" | sed -E "s|@BS:(.*)|@\1|" > "$E_BS_LOGS" + +E_BF_LOGS=$(get_output_path "E_BF_logs") +log_creation "$E_BF_LOGS" +grep "@BF:" "$E_LOGS" | sed -E "s|@BF:(.*)|@\1|" > "$E_BF_LOGS" + +E_AF_LOGS=$(get_output_path "E_AF_logs") +log_creation "$E_AF_LOGS" +grep "@AF:" "$E_LOGS" | sed -E "s|@AF:(.*)|@\1|" > "$E_AF_LOGS" + +E_MF_LOGS=$(get_output_path "E_MF_logs") +log_creation "$E_MF_LOGS" +grep "@MF:" "$E_LOGS" | sed -E "s|@MF:(.*)|@\1|" > "$E_MF_LOGS" + +# Step 6: Filter for different outputs (events, changes) and errors +E_BS_DIFF_LOGS=$(get_output_path "E_BS_diff_logs") +log_creation "$E_BS_DIFF_LOGS" +grep -v "): ok" "$E_BS_LOGS" > "$E_BS_DIFF_LOGS" + +E_BF_DIFF_LOGS=$(get_output_path "E_BF_diff_logs") +log_creation "$E_BF_DIFF_LOGS" +grep -v "same error" "$E_BF_LOGS" > "$E_BF_DIFF_LOGS" + +# Step 7: Extract entry functions and abort functions for failed executions +E_AF_ENTRY=$(get_output_path "E_AF_entry") +log_creation "$E_AF_ENTRY" +grep "entry:" "$E_AF_LOGS" | sed -E "s|.*/entry:([_:a-zA-Z0-9]*)\).*|\1|" | sort | uniq > "$E_AF_ENTRY" + +E_AF_ABORT=$(get_output_path "E_AF_abort") +log_creation "$E_AF_ABORT" +grep "Move abort in" "$E_AF_LOGS" | sed -E "s|.*Move abort in ([_:a-zA-Z0-9]*).*|\1|" | sort | uniq > "$E_AF_ABORT" + +# Step 8: Clean up and prepare frequency files for failed entry and abort functions +E_AF_ENTRY_FREQ=$(get_output_path "E_AF_entry_freq") +E_AF_ABORT_FREQ=$(get_output_path "E_AF_abort_freq") + +rm -f "$E_AF_ENTRY_FREQ" +rm -f "$E_AF_ABORT_FREQ" + +# Step 9: Generate frequency counts for failed entry functions +log_creation "$E_AF_ENTRY_FREQ" +while IFS='' read -r LINE || [ -n "${LINE}" ]; do + COUNT=$(grep -c "${LINE}" "$CLEAN_LOGS") + echo "${LINE} ${COUNT}" >> "$E_AF_ENTRY_FREQ" +done < "$E_AF_ENTRY" + +# Step 10: Generate frequency counts for abort functions +log_creation "$E_AF_ABORT_FREQ" +while IFS='' read -r LINE || [ -n "${LINE}" ]; do + COUNT=$(grep -c "${LINE}" "$CLEAN_LOGS") + echo "${LINE} ${COUNT}" >> "$E_AF_ABORT_FREQ" +done < "$E_AF_ABORT" + +# Step 11: Generate statistics for transaction submissions +S_LOGS_STATS=$(get_output_path "S_logs_stats") +log_creation "$S_LOGS_STATS" + +# Get number of submitted transactions +S_LOGS_COUNT=$(wc -l < "$S_LOGS") + +# Get counts from individual files +S_BS_COUNT=$(wc -l < "$S_BS_LOGS") +S_BF_COUNT=$(wc -l < "$S_BF_LOGS") +S_AF_COUNT=$(wc -l < "$S_AF_LOGS") +S_MF_COUNT=$(wc -l < "$S_MF_LOGS") + +# Calculate percentages +if [ "$S_LOGS_COUNT" -gt 0 ]; then + S_BS_PERCENT=$(echo "scale=2; $S_BS_COUNT * 100 / $S_LOGS_COUNT" | bc) + S_BF_PERCENT=$(echo "scale=2; $S_BF_COUNT * 100 / $S_LOGS_COUNT" | bc) + S_AF_PERCENT=$(echo "scale=2; $S_AF_COUNT * 100 / $S_LOGS_COUNT" | bc) + S_MF_PERCENT=$(echo "scale=2; $S_MF_COUNT * 100 / $S_LOGS_COUNT" | bc) +else + S_BS_PERCENT=0.00 + S_BF_PERCENT=0.00 + S_AF_PERCENT=0.00 + S_MF_PERCENT=0.00 +fi + +# Write statistics to file +cat > "$S_LOGS_STATS" << EOF +Submission Statistics Report +============================ + +Base file: S_logs +Total submitted transactions: $S_LOGS_COUNT + +Breakdown by category: +---------------------- +Both succeeded: $S_BS_COUNT (${S_BS_PERCENT}%) +Both failed: $S_BF_COUNT (${S_BF_PERCENT}%) +Aptos submission failed: $S_AF_COUNT (${S_AF_PERCENT}%) +Movement submission failed: $S_MF_COUNT (${S_MF_PERCENT}%) + +EOF + +# Step 12: Generate statistics for transaction executions +E_LOGS_STATS=$(get_output_path "E_logs_stats") +log_creation "$E_LOGS_STATS" + +# Get number of executed transactions +E_LOGS_COUNT=$(wc -l < "$E_LOGS") + +# Get counts from individual files +E_BS_COUNT=$(wc -l < "$E_BS_LOGS") +E_BS_DIFF_COUNT=$(wc -l < "$E_BS_DIFF_LOGS") +E_BF_COUNT=$(wc -l < "$E_BF_LOGS") +E_BF_DIFF_COUNT=$(wc -l < "$E_BF_DIFF_LOGS") +E_AF_COUNT=$(wc -l < "$E_AF_LOGS") +E_MF_COUNT=$(wc -l < "$E_MF_LOGS") + +# Calculate percentages +if [ "$E_LOGS_COUNT" -gt 0 ]; then + E_BS_PERCENT=$(echo "scale=2; $E_BS_COUNT * 100 / $E_LOGS_COUNT" | bc) + E_BF_PERCENT=$(echo "scale=2; $E_BF_COUNT * 100 / $E_LOGS_COUNT" | bc) + E_AF_PERCENT=$(echo "scale=2; $E_AF_COUNT * 100 / $E_LOGS_COUNT" | bc) + E_MF_PERCENT=$(echo "scale=2; $E_MF_COUNT * 100 / $E_LOGS_COUNT" | bc) +else + E_BS_PERCENT=0.00 + E_BF_PERCENT=0.00 + E_AF_PERCENT=0.00 + E_MF_PERCENT=0.00 +fi + +# Write statistics to file +cat > "$E_LOGS_STATS" << EOF +Execution Statistics Report +=========================== + +Base file: E_logs +Total executed transactions: $E_LOGS_COUNT + +Breakdown by category: +---------------------- +Both succeeded: $E_BS_COUNT (${E_BS_PERCENT}%). Different outputs (events, changes): $E_BS_DIFF_COUNT. +Both failed: $E_BF_COUNT (${E_BF_PERCENT}%). Different errors: $E_BF_DIFF_COUNT +Aptos execution failed: $E_AF_COUNT (${E_AF_PERCENT}%) +Movement execution failed: $E_MF_COUNT (${E_MF_PERCENT}%) + +EOF + +echo +echo "Processing completed successfully!" +echo "All output files have been created in: $INPUT_DIR" +echo "Statistics summaries written to:" +echo " - $S_LOGS_STATS" +echo " - $E_LOGS_STATS"