Skip to content

svm: test account loader edge cases#3045

Merged
2501babe merged 6 commits intoanza-xyz:masterfrom
2501babe:20241001_loaderv2_tests
Oct 15, 2024
Merged

svm: test account loader edge cases#3045
2501babe merged 6 commits intoanza-xyz:masterfrom
2501babe:20241001_loaderv2_tests

Conversation

@2501babe
Copy link
Copy Markdown
Member

@2501babe 2501babe commented Oct 1, 2024

Problem

the existing account loader has a few unique edge cases:

  • if a program can be found in the cache, the executable flag on the program account retrevied from accounts-db is never checked, unless it is writable or an instruction account
  • if native loader is an instruction account, it is counted against the transaction data size limit, otherwise it is not
  • if any other required loader is also an instruction account, it is counted against the transaction data size limit twice
  • an upgradeable program retrieved from cache has a loaded size that includes its programdata, so size totals differ if the cache is used or skipped

all of these affect consensus because they may determine whether the transaction is executed. as we are implementing a new loader without a feature gate, we must test that the new loader preserves these behaviors

Summary of Changes

add tests for them. we do this as a separate pr from the new loader so we are sure they pass for the old loader

@2501babe 2501babe self-assigned this Oct 1, 2024
@2501babe 2501babe force-pushed the 20241001_loaderv2_tests branch 3 times, most recently from 351748c to 5680496 Compare October 1, 2024 19:55
@2501babe 2501babe marked this pull request as ready for review October 1, 2024 22:22
@2501babe 2501babe requested a review from apfitzge October 1, 2024 22:22
@2501babe 2501babe force-pushed the 20241001_loaderv2_tests branch from 5680496 to 124ebfa Compare October 2, 2024 09:52
@2501babe 2501babe marked this pull request as draft October 2, 2024 10:37
@2501babe 2501babe force-pushed the 20241001_loaderv2_tests branch from e545d22 to 6018221 Compare October 2, 2024 10:38
@2501babe
Copy link
Copy Markdown
Member Author

2501babe commented Oct 2, 2024

(need to wait for dependabot to upstream a dependency version update for this to pass again)

these tests are intended to ensure account loader v2 conforms to existing behavior
@2501babe 2501babe force-pushed the 20241001_loaderv2_tests branch from 6018221 to eb95bfd Compare October 2, 2024 15:15
@2501babe 2501babe marked this pull request as ready for review October 2, 2024 18:26
@2501babe 2501babe marked this pull request as draft October 7, 2024 13:54
@2501babe 2501babe marked this pull request as ready for review October 7, 2024 19:04
Comment thread svm/src/account_loader.rs Outdated
Comment thread svm/src/account_loader.rs Outdated
Comment thread svm/src/account_loader.rs Outdated
Comment thread svm/src/account_loader.rs Outdated
Comment thread svm/src/account_loader.rs
Comment thread svm/src/account_loader.rs
Comment thread svm/src/account_loader.rs Outdated
@apfitzge
Copy link
Copy Markdown

apfitzge commented Oct 7, 2024

Probably good to get a second opionion on these tests, but overall they look good to me (in that they are testing the current behavior)

@2501babe 2501babe requested a review from jstarry October 7, 2024 23:45
Comment thread svm/src/account_loader.rs
}

#[test]
fn test_load_transaction_accounts_program_account_executable_bypass() {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks good, this is really crazy behavior. Do you mind adding some comments explaining why this happens?

From my current understanding:

  1. All builtins and any account owned by a loader are added to the program cache before tx account loading
  2. If a tx account loads an account which is not writable and not used as an instruction account input, we check if it's in the program cache
  3. If the tx account is in the program cache, then load it as if it's a valid program for execution
  4. Later if that account is invoked as a program, the bpf loaders will check the actual program cache entry to see if it's a valid program. If not, the tx is "executed" but will return an error.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

thats correct. the issue is the current code just checks if something exists in the cache, and then assumes its executable, so it attempts to execute and it fails. it does load the data anyway (so using the cache isnt really an optimization) but just to see if it exists, which i assume was added to fix a bug

    } else if let Some(program) = (!is_instruction_account && !is_writable)
        .then_some(())
        .and_then(|_| loaded_programs.find(account_key))
    {
        callbacks
            .get_account_shared_data(account_key)
            .ok_or(TransactionError::AccountNotFound)?;
        // Optimization to skip loading of accounts which are only used as
        // programs in top-level instructions and not passed as instruction accounts.
        LoadedTransactionAccount {
            loaded_size: program.account_size,
            account: account_shared_data_from_program(&program),
            rent_collected: 0,
        }
    } else {

checking executable on the account here requires a feature gate because (until fee-only txn are enabled) it would change whether the transaction pays fees and thus affects consensus. but i talked with pankaj the other week and he said that the status on the cache item is sufficient to determine if it is in fact executable or not (this is what i meant in the tombstone comment, ill expand it in next commit). so in the future when we fix this i believe we can just do this

        } else if let Some(ref program) = (!is_instruction_account && !is_writable)
            .then_some(())
            .and_then(|_| program_cache.find(account_key))
            .filter(|program| !program.is_tombstone())
        {   
            loaded_accounts_map.insert_cached_program(*account_key, program);
        } else if ... {

Comment thread svm/src/account_loader.rs
Comment on lines +1429 to +1439
let transaction =
SanitizedTransaction::from_transaction_for_tests(Transaction::new_signed_with_payer(
&[Instruction::new_with_bytes(
program_keypair.pubkey(),
&[],
vec![],
)],
Some(&account_keypair.pubkey()),
&[&account_keypair],
Hash::default(),
));
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you add separate variants of this test where the program is 1) loaded as writeable and 2) passed as an instruction account to show the difference in behavior?

Comment thread svm/src/account_loader.rs

assert_eq!(actual_inspected_accounts, expected_inspected_accounts,);
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Can you also add a test case for loading a tx which invokes native loader directly?

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

For posterity, native loader is an edge case because account loading for some reason doesn't reject transactions that invoke the native loader as a program despite the native loader not being a normal "builtin" (therefore doesn't get added to the program cache) as well as not actually existing onchain as an executable account.

Comment thread svm/src/account_loader.rs
program2_size + programdata2_size + upgradeable_loader_size + fee_payer_size,
);

// program as instruction account bypasses the cache
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

We should also have a way to set invoked programs as writable when the upgradeable loader is present because that will also bypass the cache.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i dont think i understand the point about the upgradeable loader being present but ive added test cases for both data size and executable flag check which use a message with a writable, non-instruction, invoked program

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

i dont think i understand the point about the upgradeable loader being present

Invoked programs that are loaded as writable by a transaction will be demoted to read-only unless the upgradeable loader is loaded by the transaction. Since you directly set the is_writable_account_cache yourself in the test, this isn't really relevant, never mind!

Comment thread svm/src/account_loader.rs Outdated
Comment on lines +2627 to +2628
// also we will need to create a fake vm and use ProgramCacheEntryType::Loaded
// since the old loader does not check tombstones, but the new one does
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Looks like process_instruction_inner is called for both the old and the upgradeable loader and will check that program cache entries are valid. Can you explain more about what you mean that the old loader doesn't check tombstones?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

i deleted this part of the comment since it is no longer relevant (i previously wrote a batched account loader that checked tombstones with an escape hatch to emulate old behavior, i now have a jit account loader which retains the old behavior unmodified), but sorry this was unclear, i meant the old and new account loaders, not the bpf loaders

@2501babe
Copy link
Copy Markdown
Member Author

spent the rest of the week writing a jit account loader (good news: its diff against master is about 1300 fewer lines than the batch account loader), will get to your suggestions on these tests on monday!

@2501babe 2501babe requested a review from jstarry October 14, 2024 18:55
@2501babe 2501babe merged commit 5488793 into anza-xyz:master Oct 15, 2024
@2501babe 2501babe deleted the 20241001_loaderv2_tests branch October 15, 2024 21:52
ray-kast pushed a commit to abklabs/agave that referenced this pull request Nov 27, 2024
tests intended to ensure account loader v2 conforms to existing behavior
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.

3 participants