Skip to content

Commit bccc710

Browse files
authored
chore: improve/fix ledger tooling (#294)
## Summary Fix: prevent account file deletion during ledger replay and ledger tool access Feat: genx supports many more accounts loaded into test-validator than before ## Details ### Fix When an `AppendVec` is dropped the underlying file is removed. This results in an account file being deleted each time we replay the ledger or access the accounts via a ledger tool, i.e. `ledger-stats` or `genx`. After attempting to fix this by setting a flag to prevent the file from being removed, I resorted to just copying the account file before using it as underlying storage for the `AppendVec`. This copy is cleaned up when the tool or ledger replay completes ### Feat Instead of passing accounts to be loaded from chain as arguments when starting the validator, _genx_ now downloads the accounts into a directory (100 in parallel which is the limit) and provides that directory to the validator in the generated script instead. <!-- greptile_comment --> ## Greptile Summary This PR improves ledger tooling by addressing account file persistence and enhancing account loading capabilities in the test validator environment. - Added file copying mechanism in `load_most_recent_store` to prevent account file deletion when `AppendVec` is dropped - Implemented batch downloading of accounts (100 at a time) in `tools/genx/src/test_validator.rs` to work within RPC limits - Added JSON serialization support for account data storage with new dependencies in `tools/genx/Cargo.toml` - Modified test validator to use `--account-dir` instead of `--maybe-clone` for more efficient account loading - Introduced temporary directory management for storing downloaded account data during validator operation <!-- /greptile_comment -->
1 parent a7fca91 commit bccc710

File tree

5 files changed

+92
-17
lines changed

5 files changed

+92
-17
lines changed

Cargo.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

magicblock-accounts-db/src/persist/accounts_persister.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -316,6 +316,17 @@ impl AccountsPersister {
316316
return Ok(None);
317317
};
318318

319+
// When we drop the AppendVec the underlying file is removed from the
320+
// filesystem. There is no way to configure this via public methods.
321+
// Thus we copy the file before using it for the AppendVec. This way
322+
// we prevent account files being removed when we point a tool at the ledger
323+
// or replay it.
324+
let file = {
325+
let copy = file.with_extension("copy");
326+
fs::copy(&file, &copy)?;
327+
copy
328+
};
329+
319330
// Create a AccountStorageEntry from the file
320331
let file_size = fs::metadata(&file)?.len() as usize;
321332
let (append_vec, num_accounts) =

test-integration/Cargo.lock

Lines changed: 5 additions & 5 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

tools/genx/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,11 @@ license.workspace = true
88
edition.workspace = true
99

1010
[dependencies]
11+
base64 = { workspace = true }
1112
clap = { version = "4.5.23", features = ["derive"] }
1213
ledger-stats = { workspace = true }
1314
magicblock-accounts-db = { workspace = true }
15+
serde_json = { workspace = true }
16+
solana-rpc-client = { workspace = true }
1417
solana-sdk = { workspace = true }
1518
tempfile = { workspace = true }

tools/genx/src/test_validator.rs

Lines changed: 70 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1-
use std::{fs, os::unix::fs::PermissionsExt, path::PathBuf};
1+
use serde_json::{json, Value};
2+
use solana_rpc_client::rpc_client::RpcClient;
3+
use std::{
4+
fs,
5+
os::unix::fs::PermissionsExt,
6+
path::{Path, PathBuf},
7+
};
28

39
use ledger_stats::{accounts_storage_from_ledger, open_ledger};
410
use magicblock_accounts_db::utils::all_accounts;
5-
use solana_sdk::pubkey::Pubkey;
11+
use solana_sdk::{commitment_config::CommitmentConfig, pubkey::Pubkey};
612
use tempfile::tempdir;
713

814
pub struct TestValidatorConfig {
@@ -16,6 +22,9 @@ pub(crate) fn gen_test_validator_start_script(
1622
) {
1723
let temp_dir = tempdir().expect("Failed to create temporary directory");
1824
let temp_dir_path = temp_dir.into_path();
25+
let accounts_dir = temp_dir_path.join("accounts");
26+
fs::create_dir(&accounts_dir).expect("Failed to create accounts directory");
27+
1928
let file_path = temp_dir_path.join("run-validator.sh");
2029
let accounts: Vec<Pubkey> = if let Some(ledger_path) = ledger_path {
2130
let ledger = open_ledger(ledger_path);
@@ -39,16 +48,14 @@ pub(crate) fn gen_test_validator_start_script(
3948
"10000".to_string(),
4049
];
4150

42-
for pubkey in accounts {
43-
// NOTE: we may need to treat executables differently if just cloning
44-
// at startup is not sufficient even though we also will clone the
45-
// executable data account.
46-
// For now we don't run programs in the test validator, but just
47-
// want to make sure they can be cloned by the ephemeral, so it is not
48-
// yet important.
49-
args.push("--maybe-clone".to_string());
50-
args.push(pubkey.to_string());
51-
}
51+
download_accounts_into_from_url_into_dir(
52+
&accounts,
53+
config.url.clone(),
54+
&accounts_dir,
55+
);
56+
57+
args.push("--account-dir".into());
58+
args.push(accounts_dir.to_string_lossy().to_string());
5259

5360
args.push("--url".into());
5461
args.push(config.url);
@@ -71,3 +78,54 @@ solana-test-validator \\\n {}",
7178
file_path.display()
7279
);
7380
}
81+
82+
fn download_accounts_into_from_url_into_dir(
83+
pubkeys: &[Pubkey],
84+
url: String,
85+
dir: &Path,
86+
) {
87+
// Derived from error from helius RPC: Failed to download accounts: Error { request: Some(GetMultipleAccounts), kind: RpcError(RpcResponseError { code: -32602, message: "Too many inputs provided; max 100", data: Empty }) }
88+
const MAX_ACCOUNTS: usize = 100;
89+
let rpc_client =
90+
RpcClient::new_with_commitment(url, CommitmentConfig::confirmed());
91+
let total_len = pubkeys.len();
92+
for (idx, pubkeys) in pubkeys.chunks(MAX_ACCOUNTS).enumerate() {
93+
let start = idx * MAX_ACCOUNTS;
94+
let end = start + pubkeys.len();
95+
eprintln!("Downloading {}..{}/{} accounts", start, end, total_len);
96+
match rpc_client.get_multiple_accounts(pubkeys) {
97+
Ok(accs) => accs
98+
.into_iter()
99+
.zip(pubkeys)
100+
.filter_map(|(acc, pubkey)| acc.map(|x| (x, pubkey)))
101+
.for_each(|(acc, pubkey)| {
102+
let path = dir.join(format!("{pubkey}.json"));
103+
let pk = pubkey.to_string();
104+
let lamports = acc.lamports;
105+
let data = [
106+
#[allow(deprecated)] // this is just a dev tool
107+
base64::encode(&acc.data),
108+
"base64".to_string(),
109+
];
110+
let owner = acc.owner.to_string();
111+
let executable = acc.executable;
112+
let rent_epoch = acc.rent_epoch;
113+
let space = acc.data.len();
114+
let json: Value = json! {{
115+
"pubkey": pk,
116+
"account": {
117+
"lamports": lamports,
118+
"data": data,
119+
"owner": owner,
120+
"executable": executable,
121+
"space": space,
122+
"rentEpoch": rent_epoch
123+
},
124+
}};
125+
fs::write(&path, format!("{:#}", json))
126+
.expect("Failed to write account");
127+
}),
128+
Err(err) => eprintln!("Failed to download accounts: {:?}", err),
129+
}
130+
}
131+
}

0 commit comments

Comments
 (0)