Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add ability for ledger-tool to dump ledger transactions to CSV #10307

Closed
wants to merge 8 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions ledger-tool/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,9 @@ homepage = "https://solana.com/"
[dependencies]
bs58 = "0.3.0"
clap = "2.33.0"
csv = "1.1.3"
histogram = "*"
serde = "1.0"
serde_json = "1.0.46"
serde_yaml = "0.8.11"
solana-clap-utils = { path = "../clap-utils", version = "1.0.25" }
Expand Down
294 changes: 293 additions & 1 deletion ledger-tool/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,17 @@ use solana_ledger::{
snapshot_utils,
};
use solana_sdk::{
clock::Slot, genesis_config::GenesisConfig, native_token::lamports_to_sol, pubkey::Pubkey,
clock::Slot,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you move the implementation of these two new comments into a separate file? main.rs is getting way too big, and this will set a better example for the future

clock::DEFAULT_TICKS_PER_SECOND,
clock::DEFAULT_TICKS_PER_SLOT,
genesis_config::GenesisConfig,
instruction::CompiledInstruction,
native_token::lamports_to_sol, pubkey::Pubkey,
shred_version::compute_shred_version,
program_utils::limited_deserialize,
transaction::Transaction,
};
use solana_transaction_status::RpcTransactionStatusMeta;
use solana_vote_program::vote_state::VoteState;
use std::{
collections::{BTreeMap, HashMap, HashSet},
Expand All @@ -28,13 +36,189 @@ use std::{
process::{exit, Command, Stdio},
str::FromStr,
};
use csv::Writer;

use serde::{
Serialize,
Deserialize,
};

#[derive(PartialEq)]
enum LedgerOutputMethod {
Print,
Json,
}

#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
struct EntryInfo {
index: usize,
num_hashes: u64,
hash: String,
num_transactions: usize,
}

#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
struct InstructionInfo {
txn_sig: Option<String>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
txn_sig: Option<String>,
transaction_signature: Option<String>,

Please globally replace txn with transaction, sig with signature.

program_pubkey: Option<String>,
program_instruction: Option<String>,
instruction_accounts: Vec<String>,
}

#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
struct TransactionInfo {
slot: Option<Slot>,
cluster_unix_timestamp: Option<i64>,
recent_blockhash: Option<String>,
txn_sig: Option<String>,
fee: Option<f64>,
num_instructions: Option<usize>,
num_accounts: Option<usize>,
account_balance_infos: Vec<AccountBalanceInfo>,
}

#[derive(Serialize, Deserialize, Debug, Default, PartialEq)]
struct AccountBalanceInfo {
account_pubkey: String,
account_pre_balance: f64,
account_post_balance: f64,
}

fn output_slot_to_csv(
blockstore: &Blockstore,
slot: Slot,
txn_wtr: &mut Writer<File>,
instruction_wtr: &mut Writer<File>,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
instruction_wtr: &mut Writer<File>,
instruction_writer: &mut Writer<File>,

don't make the reader guess what "wtr" means. water?

ledger_path: &PathBuf,
) -> Result<(), String> {
let entries = blockstore
.get_slot_entries(slot, 0, None)
.map_err(|err| format!("Failed to load entries for slot {}: {}", slot, err))?;

for entry in entries {
for transaction in entry.transactions {
// Skip any vote transactions
let program_pubkey = transaction
.message
.account_keys[transaction.message.instructions[0].program_id_index as usize];
if program_pubkey == solana_vote_program::id() { continue; }

let transaction_status: Option<RpcTransactionStatusMeta> = blockstore
.read_transaction_status((transaction.signatures[0], slot))
.unwrap_or_else(|err| {
eprintln!(
"Failed to read transaction status for {} at slot {}: {}",
transaction.signatures[0], slot, err
);
None
})
.map(|transaction_status| transaction_status.into());

for instruction in &transaction.message.instructions {
let instruction_info = build_instruction_info(
&instruction,
&transaction,
);
instruction_wtr.serialize(&instruction_info);
}

let txn_info = build_txn_info(
slot,
&ledger_path,
&transaction,
&transaction_status
);
txn_wtr.serialize(&txn_info);
}
}
instruction_wtr.flush();
txn_wtr.flush();
Ok(())
}

fn build_instruction_info(
instruction: &CompiledInstruction,
transaction: &Transaction,
) -> InstructionInfo {
let program_pubkey = transaction
.message
.account_keys[instruction.program_id_index as usize];

let mut instruction_info = InstructionInfo{
txn_sig: Some(transaction.signatures[0].to_string()),
program_pubkey: Some(bs58::encode(program_pubkey).into_string()),
..Default::default()
};

if program_pubkey == solana_stake_program::id() {
if let Ok(stake_instruction) = limited_deserialize::<
solana_stake_program::stake_instruction::StakeInstruction,
>(&instruction.data)
{
instruction_info.program_instruction =
Some(format!("{:?}",stake_instruction));
}
} else if program_pubkey == solana_sdk::system_program::id() {
if let Ok(system_instruction) = limited_deserialize::<
solana_sdk::system_instruction::SystemInstruction,
>(&instruction.data)
{
instruction_info.program_instruction =
Some(format!("{:?}", system_instruction));
}
}


for account in &instruction.accounts {
instruction_info
.instruction_accounts
.push(bs58::encode(transaction
.message
.account_keys[*account as usize])
.into_string())
}
instruction_info
}

fn build_txn_info(
slot: Slot,
ledger_path: &PathBuf,
transaction: &Transaction,
transaction_status: &Option<RpcTransactionStatusMeta>
) -> TransactionInfo {
let genesis_creation_time = open_genesis_config(&ledger_path).creation_time;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Calling open_genesis_config on every build_txn_info() is pretty expensive, load it once and pass in a &GenesisConfig?

let seconds_per_slot = DEFAULT_TICKS_PER_SLOT / DEFAULT_TICKS_PER_SECOND;
let seconds_since_genesis = (slot * seconds_per_slot) as i64;

let mut txn_info = TransactionInfo{
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks like this PR need some cargo fmt

slot: Some(slot),
cluster_unix_timestamp: Some(genesis_creation_time + seconds_since_genesis),
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This number is going to be inaccurate once we fix #9874
Not sure what do about that here, the value will likely change per-epoch in the bank and not actually be reflected in the ledger at all. I guess we can 🙈 here for now

recent_blockhash: Some(transaction.message.recent_blockhash.to_string()),
txn_sig: Some(transaction.signatures[0].to_string()),
num_instructions: Some(transaction.message.instructions.len()),
num_accounts: Some(transaction.message.account_keys.len()),
..Default::default()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
..Default::default()
.. TransactionInfo::default()

};

if let Some(transaction_status) = transaction_status {
txn_info.fee = Some(lamports_to_sol(transaction_status.fee));

for (i, (pre, post)) in transaction_status
.pre_balances
.iter()
.zip(transaction_status.post_balances.iter())
.enumerate()
{
txn_info.account_balance_infos.push(AccountBalanceInfo{
account_pubkey: bs58::encode(transaction.message.account_keys[i]).into_string(),
account_pre_balance: lamports_to_sol(*pre),
account_post_balance: lamports_to_sol(*post),
})
};
}
txn_info
}

fn output_slot_rewards(
blockstore: &Blockstore,
slot: Slot,
Expand Down Expand Up @@ -112,6 +296,42 @@ fn output_slot(
output_slot_rewards(blockstore, slot, method)
}

fn output_ledger_to_csv(blockstore: &Blockstore,
ledger_path: &PathBuf,
starting_slot: Slot,
txn_csv_file: String,
instruction_csv_file: String) {
let rooted_slot_iterator =
RootedSlotIterator::new(starting_slot, &blockstore).unwrap_or_else(|err| {
eprintln!(
"Failed to load entries starting from slot {}: {:?}",
starting_slot, err
);
exit(1);
});

let mut txn_wtr = csv::WriterBuilder::new()
.has_headers(false)
.flexible(true)
.from_path(txn_csv_file)
.unwrap();
let mut instruction_wtr = csv::WriterBuilder::new()
.has_headers(false)
.flexible(true)
.from_path(instruction_csv_file)
.unwrap();

for (slot, _slot_meta) in rooted_slot_iterator {
output_slot_to_csv(
blockstore,
slot,
&mut txn_wtr,
&mut instruction_wtr,
ledger_path,
);
}
}

fn output_ledger(blockstore: Blockstore, starting_slot: Slot, method: LedgerOutputMethod) {
let rooted_slot_iterator =
RootedSlotIterator::new(starting_slot, &blockstore).unwrap_or_else(|err| {
Expand Down Expand Up @@ -584,6 +804,18 @@ fn main() {
.multiple(true)
.takes_value(true)
.help("Add a hard fork at this slot");
let txn_csv_file_arg = Arg::with_name("txn_csv_file")
.short("t")
.long("txn-csv-file")
.value_name("PATH")
.takes_value(true)
.help("File path to new or existing CSV file for writing");
let instruction_csv_file_arg = Arg::with_name("instruction_csv_file")
.short("i")
.long("instruction-csv-file")
.value_name("PATH")
.takes_value(true)
.help("File path to new or existing CSV file for writing");

let matches = App::new(crate_name!())
.about(crate_description!())
Expand All @@ -602,6 +834,13 @@ fn main() {
.about("Print the ledger")
.arg(&starting_slot_arg)
)
.subcommand(
SubCommand::with_name("print-csv")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could we get away with removing the "slot-csv" command if "print-csv" supported an --end-slot argument?

.about("Write the ledger to CSV files")
.arg(&starting_slot_arg)
.arg(&txn_csv_file_arg)
.arg(&instruction_csv_file_arg)
)
.subcommand(
SubCommand::with_name("slot")
.about("Print the contents of one or more slots")
Expand All @@ -616,6 +855,22 @@ fn main() {
.help("Slots to print"),
)
)
.subcommand(
SubCommand::with_name("slot-csv")
.about("Print the contents of one or more slots into a CSV file")
.arg(
Arg::with_name("slots")
.index(1)
.value_name("SLOTS")
.validator(is_slot)
.takes_value(true)
.multiple(true)
.required(true)
.help("Slots to print"),
)
.arg(&txn_csv_file_arg)
.arg(&instruction_csv_file_arg)
)
.subcommand(
SubCommand::with_name("set-dead-slot")
.about("Mark one or more slots dead")
Expand Down Expand Up @@ -802,6 +1057,20 @@ fn main() {
LedgerOutputMethod::Print,
);
}
("print-csv", Some(arg_matches)) => {
let starting_slot = value_t_or_exit!(arg_matches, "starting_slot", Slot);
let txn_csv_file = value_t_or_exit!(arg_matches, "txn_csv_file", String);
let instruction_csv_file = value_t_or_exit!(arg_matches, "instruction_csv_file", String);
Comment on lines +1062 to +1063
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These args aren't marked as .required(true) and yet there's value_t_or_exit!() here, which will cause the program to exit if they aren't provided. Should they be required?

let blockstore = open_blockstore(&ledger_path);
output_ledger_to_csv(
&blockstore,
&ledger_path,
starting_slot,
txn_csv_file,
instruction_csv_file,
);
}

("genesis", Some(_arg_matches)) => {
println!("{}", open_genesis_config(&ledger_path));
}
Expand Down Expand Up @@ -845,6 +1114,29 @@ fn main() {
}
}
}
("slot-csv", Some(arg_matches)) => {
let slots = values_t_or_exit!(arg_matches, "slots", Slot);
let blockstore = open_blockstore(&ledger_path);
let txn_csv_file = value_t_or_exit!(arg_matches, "txn_csv_file", String);
let instruction_csv_file = value_t_or_exit!(arg_matches, "instruction_csv_file", String);
let mut txn_wtr = csv::WriterBuilder::new()
.has_headers(false)
.flexible(true)
.from_path(txn_csv_file)
.unwrap();
let mut instruction_wtr = csv::WriterBuilder::new()
.has_headers(false)
.flexible(true)
.from_path(instruction_csv_file)
.unwrap();

for slot in slots {
println!("Slot {}", slot);
if let Err(err) = output_slot_to_csv(&blockstore, slot, &mut txn_wtr, &mut instruction_wtr, &ledger_path) {
eprintln!("{}", err);
}
}
}
("json", Some(arg_matches)) => {
let starting_slot = value_t_or_exit!(arg_matches, "starting_slot", Slot);
output_ledger(
Expand Down