Skip to content

Conversation

@maurodelazeri
Copy link

Problem

When testing programs with liteSVM, if a transaction fails with AccountNotFound, there's no way to know which account is missing. You just have to guess or read through program source code to figure it out.

This is especially painful with programs that do CPI calls - the called program might access PDAs, oracles, or config accounts that you didn't know it needed.

Solution

Added optional account tracking. When you enable it with .with_account_tracking(true), the transaction result includes all the accounts that were accessed.

let mut svm = LiteSVM::new()
    .with_account_tracking(true);

match svm.send_transaction(tx) {
    Err(failed) => {
        if let Some(accessed) = &failed.meta.accessed_accounts {
            let missing: Vec<_> = accessed.iter()
                .filter(|pk| svm.get_account(pk).is_none())
                .collect();
            println!("Missing accounts: {:?}", missing);
        }
    }
    Ok(_) => {}
}

Now you can see exactly which accounts are missing instead of guessing.

Important: This tracks ALL account accesses, including those made by programs called via CPI. If Program A calls Program B, and Program B tries to access an account, it shows up in the list. This is the most useful part - you can see what accounts nested program calls need without reading their source code.

Implementation

  • Added accessed_accounts: Option<Vec<Pubkey>> field to TransactionMetadata
  • Tracks accesses in AccountsDb::get_account() when enabled
  • Disabled by default, so zero overhead unless you opt in
  • Works with both send_transaction and simulate_transaction
  • Added get_accessed_accounts() method for manual retrieval (useful for debugging setup failures)

Backward compatible - existing code doesn't need to change.

Tests

Added 9 tests covering the various cases (default disabled, enabled, missing accounts, simulations, manual retrieval, etc). All passing.

When testing programs with liteSVM, if a transaction fails with AccountNotFound,
there's no way to know which account is missing. This is especially painful with
programs that do CPI calls.

Added optional .with_account_tracking(true) that tracks all account accesses
during execution, including those made via CPI. The transaction result includes
accessed_accounts field with all pubkeys that were accessed.

- Added accessed_accounts field to TransactionMetadata
- Tracks accesses in AccountsDb::get_account() when enabled
- Disabled by default (zero overhead)
- Works with both send_transaction and simulate_transaction
- Added 8 comprehensive tests

Fully backward compatible.
- Tracking now enabled immediately when .with_account_tracking(true) is called
- Added public get_accessed_accounts() method for manual retrieval
- Useful for debugging setup failures (program loading, etc)
- Added test for manual retrieval
- Updated README with setup failure example
- All 9 tests passing
@kevinheavey
Copy link
Collaborator

I think the logging already covers this? If replace #[test] with #[test_log::test] in test_missing_account_detection I get:

[ERROR litesvm] Program account 1117mWrzzrZr312ebPDHu8tbfMwFNvCvMbr6WepCNG is not executable.

@maurodelazeri
Copy link
Author

test_missing_account_detection

Good catch on the logging! You're right that test_log::test shows some errors.

The main use case I ran into was with CPI calls. When my program calls another program and that fails with a generic AccountNotFound, the log shows the outer error but doesn't tell me which account the nested program tried to access. With tracking enabled, I can see all the accounts accessed during the CPI chain and filter for the missing ones.

For example, if I call a program that internally accesses a PDA or oracle account I didn't know about, the log might just say "Program failed" but tracking shows me accessed_accounts = [account1, account2, missing_pda] so I know exactly what to add.

The other benefit is programmatic access - I can do accessed.iter().filter(|pk| svm.get_account(pk).is_none()) to find missing accounts without parsing log strings.

That said, if you think this is too niche or overlaps too much with logging, I totally understand. Just wanted to share the use case that led me to build it.

@maurodelazeri
Copy link
Author

test_missing_account_detection

Good catch on the logging! You're right that test_log::test shows some errors.

The main use case I ran into was with CPI calls. When my program calls another program and that fails with a generic AccountNotFound, the log shows the outer error but doesn't tell me which account the nested program tried to access. With tracking enabled, I can see all the accounts accessed during the CPI chain and filter for the missing ones.

For example, if I call a program that internally accesses a PDA or oracle account I didn't know about, the log might just say "Program failed" but tracking shows me accessed_accounts = [account1, account2, missing_pda] so I know exactly what to add.

The other benefit is programmatic access - I can do accessed.iter().filter(|pk| svm.get_account(pk).is_none()) to find missing accounts without parsing log strings.

That said, if you think this is too niche or overlaps too much with logging, I totally understand. Just wanted to share the use case that led me to build it.

here is an example for what im using

[2025-11-09T17:21:05Z ERROR x::xx::bedrock::simulation] 📋 Transaction accessed 36 accounts
[2025-11-09T17:21:05Z ERROR x::xx::bedrock::simulation] ❌ Missing accounts (1):
[2025-11-09T17:21:05Z ERROR x::xx::bedrock::simulation]    AQjz6RZK93SLjxfDGKL9nCYQNSjEbQSdETxwR63jXV8m

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants