Skip to content
Merged
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
86 changes: 64 additions & 22 deletions engine/tests/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ use engine::{Engine, EngineError, testkit::TestEngine};
use keeper::testkit::{
V42_ID, load_v42_data, load_v42_lamports, patterned_bytes, store_v42, v42_builder,
};
use magic_root_interface::MagicRootInstruction;
use solana_account::{AccountBuilder, AccountMode, OwnedAccount, ReadableAccount};
use solana_instruction_error::InstructionError;
use solana_pubkey::Pubkey;
Expand Down Expand Up @@ -261,34 +262,75 @@ async fn account_create_accepts_max_data_with_post_finalize() {
te.close().await;
}

// Replacing the seeded executable at a newer slot exercises MagicRoot's large
// account reconstruction and executable finalization, then proves the resulting
// program remains usable by executing it through the normal client path.
/// Proves program-cache entries follow the complete v42 account lifecycle:
/// transaction-local deletion hides a loaded program immediately but rolls back
/// on a later instruction failure, while committed deletion evicts the shared
/// entry so invalid executable data restored at the same key cannot use stale code.
#[tokio::test(flavor = "multi_thread")]
async fn account_update_replaces_v42_program_at_new_slot() {
async fn account_program_cache_tracks_v42_lifecycle() {
let te = TestEngine::new().await;
let program = te.get_account(V42_ID).expect("v42 program is seeded");
let data = program.data().to_vec();
let owner = *program.owner();
let lamports = program.lamports();
let slot = program.slot() + 1;
let replacement = AccountBuilder::from(program).slot(slot);

te.account(V42_ID).update(replacement).await.unwrap();

let replaced = te.get_account(V42_ID).expect("replaced v42 program exists");
assert_eq!(replaced.lamports(), lamports);
assert_eq!(replaced.owner(), &owner);
assert_eq!(replaced.slot(), slot);
assert!(replaced.executable());
assert_eq!(replaced.data(), data);
let seeded = te.get_account(V42_ID).expect("v42 program is seeded");
let program = Pubkey::new_unique();
let closeable = AccountBuilder::from(seeded.clone())
.mode(AccountMode::Ephemeral)
.slot(seeded.slot() + 1);
te.account(program).create(closeable, None).await.unwrap();

let output = Pubkey::new_unique();
te.accounts()
.store(&[(
output,
v42_builder(0, AccountMode::Ephemeral).owner(program).build(),
)])
.unwrap();
let invoke = |value| {
let mut instruction = E::lit(value).compose(output, &[]);
instruction.program_id = program;
instruction.accounts.last_mut().unwrap().pubkey = program;
instruction
};

let output = store_v42(&te, 0, AccountMode::Ephemeral);
te.execute(&[E::lit(42).compose(output, &[])])
te.execute(&[invoke(42)])
.await
.expect("replaced v42 program executes");
.expect("fresh v42 program executes and primes the shared cache");
assert_eq!(load_v42_data(&te, output), Some(42));

let delete = MagicRootInstruction::Delete.compose(program).unwrap();
assert_eq!(
te.execute(&[delete, invoke(7)]).await,
Err(TransactionError::InstructionError(
1,
InstructionError::UnsupportedProgramId
)),
"deletion hides the program from later instructions in the transaction"
);
assert!(
te.get_account(program).is_some(),
"failed transaction rolls back account deletion"
);

te.execute(&[invoke(7)])
.await
.expect("rolled-back deletion preserves the shared cache entry");
assert_eq!(load_v42_data(&te, output), Some(7));

te.account(program).delete().await.unwrap();
assert!(
te.get_account(program).is_none(),
"committed deletion removes the account"
);

let invalid = AccountBuilder::from(seeded).mode(AccountMode::Ephemeral).data(vec![0]);
te.accounts().store(&[(program, invalid.build())]).unwrap();
assert_eq!(
te.execute(&[invoke(9)]).await,
Err(TransactionError::InstructionError(
0,
InstructionError::UnsupportedProgramId
)),
"invalid restored executable cannot run through a stale compiled entry"
);

te.close().await;
}

Expand Down
2 changes: 1 addition & 1 deletion programs/magic-root-interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ pub enum MagicRootInstruction {
/// Replace the target's complete flag value and, when executable, load it
/// into the transaction's program cache. Does not change lamports.
Finalize(StateFlags),
/// Close the target account.
/// Close the target account and hide any cached executable immediately.
Delete,
/// Run follow-up instructions immediately after finalizing the same target
/// (e.g. initializing a freshly created account); each is invoked via CPI
Expand Down
4 changes: 3 additions & 1 deletion programs/magic-root-program/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,9 @@ authorization. The SVM's separate top-level-only privilege rule is unchanged.
loads an executable target into the transaction program cache. It does not
change lamports; failed executable loading rolls back the installed flags.
- `Delete` transitions read-only, placeholder, or ephemeral targets to
`AccountMode::Closed`, and accountsdb removes them during writeback. Modes that
`AccountMode::Closed`, immediately hides any transaction-local cached program,
and removes its shared cache entry after successful execution and access
validation. Accountsdb removes closed accounts during writeback. Modes that
cannot transition to closed are rejected.
- `PostFinalize` invokes follow-up instructions through native CPI and is
placed immediately after the target's `Finalize` by internal composers. It
Expand Down
7 changes: 6 additions & 1 deletion programs/magic-root-program/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -72,7 +72,7 @@ pub(crate) fn finalize(

/// Marks the target account closed for removal from storage.
pub(crate) fn delete(
ctx: &InvokeContext<'_, '_>,
ctx: &mut InvokeContext<'_, '_>,
target: IndexOfAccount,
) -> Result<(), InstructionError> {
let mut acc = ctx.transaction_context.accounts().try_borrow_mut(target)?;
Expand All @@ -81,6 +81,11 @@ pub(crate) fn delete(
ic_msg!(ctx, "MagicRoot: {}", error);
return Err(InstructionError::InvalidArgument);
}
let pubkey = *ctx.transaction_context.get_key_of_account_at_index(target)?;
if acc.executable() {
let entry = ProgramCacheEntry::default().into();
ctx.program_cache_for_tx_batch.store_modified_entry(pubkey, entry);
}
ic_msg!(ctx, "MagicRoot: removed");
Ok(())
}
8 changes: 5 additions & 3 deletions solana/program-runtime/src/loaded_programs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,8 +69,6 @@ impl PartialEq for ProgramRuntimeEnvironment {

impl Eq for ProgramRuntimeEnvironment {}

pub const MAX_LOADED_ENTRY_COUNT: usize = 512;

/// Actual payload of [ProgramCacheEntry].
#[derive(Default)]
pub enum ProgramCacheEntryType {
Expand Down Expand Up @@ -286,7 +284,11 @@ impl ProgramCache {

pub fn merge(&self, modified_entries: &HashMap<Pubkey, Arc<ProgramCacheEntry>>) {
for (key, entry) in modified_entries {
self.assign_program(*key, entry.clone());
if matches!(&entry.program, ProgramCacheEntryType::Closed) {
self.index.remove_sync(key);
} else {
self.assign_program(*key, entry.clone());
}
}
}
}
Expand Down
Loading