Skip to content
Draft
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
1 change: 1 addition & 0 deletions engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -51,6 +51,7 @@ magicblock-engine = { path = ".", features = ["testkit"] }
nucleus = { workspace = true, features = ["testkit"] }
v42-calculator-interface = { workspace = true, features = ["builder"] }

solana-instruction = { workspace = true }
solana-instruction-error = { workspace = true }
solana-packet = { workspace = true }
solana-signer = { workspace = true }
Expand Down
46 changes: 43 additions & 3 deletions engine/src/accessor.rs
Original file line number Diff line number Diff line change
@@ -1,11 +1,11 @@
//! Account- and transaction-scoped operation facades.

use std::{sync::atomic::Ordering, time::Duration};
use std::{collections::BTreeSet, sync::atomic::Ordering, time::Duration};

use keeper::{ExecutionRecord, TransactionView};
use magic_root_interface::MagicRootInstruction;
use processor::{SequencerMessage, Simulation, SimulatorMessage};
use solana_account::OwnedAccount;
use solana_account::{AccountMode, OwnedAccount};
use solana_instruction::Instruction;
use solana_pubkey::Pubkey;
use solana_transaction::TransactionResult;
Expand Down Expand Up @@ -35,18 +35,58 @@ pub struct TransactionAccessor<'a> {
impl AccountAccessor<'_> {
/// Creates the account by patching in every field and finalizing it,
/// optionally running follow-up `actions` once it is finalized.
///
/// Missing writable accounts named by `actions` (ER-only PDAs such as an
/// auction tree) are asserted with [`MagicRootInstruction::Prepare`] in the
/// same transaction, so a follow-up action can initialize them (for example
/// `create_ephemeral`). `Prepare` materializes the placeholder only if the
/// account still does not exist at execution time, so concurrent creates
/// that share such an account converge on the first materialization instead
/// of failing each other.
pub async fn create(
&self,
acc: impl Into<OwnedAccount>,
actions: Option<Vec<Instruction>>,
) -> Result<()> {
let mut instructions = MagicRootInstruction::compose_account(self.pubkey, acc.into())?;
let mut instructions = Vec::new();
if let Some(actions) = actions.as_ref() {
for pubkey in self.missing_writable_action_accounts(actions) {
instructions.push(MagicRootInstruction::Prepare.compose(pubkey)?);
}
}
instructions.extend(MagicRootInstruction::compose_account(
self.pubkey,
acc.into(),
)?);
if let Some(actions) = actions {
instructions.push(MagicRootInstruction::PostFinalize(actions).compose(self.pubkey)?);
}
self.execute(instructions).await
}

/// Writable action accounts that are absent or only a slot-0 Placeholder.
fn missing_writable_action_accounts(&self, actions: &[Instruction]) -> Vec<Pubkey> {
let accounts = self.engine.accounts();
let loader = accounts.loader();
let mut seen = BTreeSet::new();
let mut missing = Vec::new();
for action in actions {
for meta in &action.accounts {
if !meta.is_writable || meta.pubkey == self.pubkey || !seen.insert(meta.pubkey) {
continue;
}
let needs_prepare =
loader.load(&meta.pubkey).ok().flatten().is_none_or(|account| {
account.is(AccountMode::Placeholder) && account.slot() == 0
});
if needs_prepare {
missing.push(meta.pubkey);
}
}
}
missing
}

/// Updates the account by patching in every field of `account`
pub async fn update(&self, acc: impl Into<OwnedAccount>) -> Result<()> {
let instructions = MagicRootInstruction::compose_account(self.pubkey, acc.into())?;
Expand Down
120 changes: 120 additions & 0 deletions engine/tests/accounts.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ use keeper::testkit::{
};
use magic_root_interface::MagicRootInstruction;
use solana_account::{AccountBuilder, AccountMode, OwnedAccount, ReadableAccount};
use solana_instruction::AccountMeta;
use solana_instruction_error::InstructionError;
use solana_pubkey::Pubkey;
use solana_system_interface::MAX_PERMITTED_DATA_LENGTH;
Expand Down Expand Up @@ -458,3 +459,122 @@ async fn create_runs_post_finalize_actions() {

te.close().await;
}

// A writable action account that is never initialized stays a Placeholder.
// PostFinalize must reject that and roll back, not commit an empty Ephemeral.
#[tokio::test(flavor = "multi_thread")]
async fn create_rejects_uninitialized_writable_action_accounts() {
let te = TestEngine::new().await;

let source = store_v42(&te, 0, AccountMode::Delegated);
let ok_key = Pubkey::new_unique();
let missing = Pubkey::new_unique();
let mut benign = transfer(source, ok_key, 1);
benign.accounts.push(AccountMeta::new(missing, false));
let acc = v42_builder(0, AccountMode::Delegated);
let result = te.account(ok_key).create(acc, Some(vec![benign])).await;
assert!(
result.is_err(),
"untouched writable placeholder must fail PostFinalize"
);
assert!(
te.get_account(ok_key).is_none(),
"create rolls back when a writable action account is left uninitialized"
);
assert!(
te.get_account(missing).is_none(),
"the prepared placeholder rolls back with the transaction"
);

te.close().await;
}

// Concurrent creates of different accounts can name the same missing writable
// in their actions (the auction-tree pattern). Prepare materializes it only if
// it still does not exist at execution time, so no create fails another one:
// each fails solely its own PostFinalize check (the action never initializes
// the shared writable), never with InvalidArgument or AlreadyProcessed.
#[tokio::test(flavor = "multi_thread")]
async fn concurrent_creates_share_one_missing_writable() {
let te = TestEngine::new().await;

let source = store_v42(&te, 0, AccountMode::Delegated);
let shared = Pubkey::new_unique();
let engine = Engine::clone(&te);
let mut handles = Vec::new();
for _ in 0..8 {
let engine = engine.clone();
let target = Pubkey::new_unique();
let mut action = transfer(source, target, 1);
action.accounts.push(AccountMeta::new(shared, false));
handles.push(tokio::spawn(async move {
let result = engine
.account(target)
.create(v42_builder(0, AccountMode::Delegated), Some(vec![action]))
.await;
(target, result)
}));
}
for handle in handles {
let (target, result) = handle.await.expect("create task");
let error = result.expect_err("untouched shared writable fails PostFinalize");
assert!(
matches!(
error,
EngineError::TransactionExecution(TransactionError::InstructionError(
_,
InstructionError::Immutable,
))
),
"each create fails only its own PostFinalize check: {error:?}"
);
assert!(
te.get_account(target).is_none(),
"failed create commits nothing"
);
}
assert!(
te.get_account(shared).is_none(),
"rolled-back creates leave no shared placeholder behind"
);

te.close().await;
}

// Prepare materializes a missing account as an empty placeholder and leaves any
// existing account untouched — the execution-time idempotence the concurrent
// create path relies on.
#[tokio::test(flavor = "multi_thread")]
async fn prepare_creates_missing_and_noops_on_existing() {
let te = TestEngine::new().await;

let fresh = Pubkey::new_unique();
te.execute(&[MagicRootInstruction::Prepare.compose(fresh).unwrap()])
.await
.expect("prepare materializes a missing account");
let placeholder = te.get_account(fresh).expect("placeholder committed");
assert!(placeholder.is(AccountMode::Placeholder));
assert_eq!(placeholder.slot(), 1);

let existing = store_v42(&te, 7, AccountMode::Delegated);
let before = te.get_account(existing).expect("existing account");
let other = Pubkey::new_unique();
te.execute(&[
MagicRootInstruction::Prepare.compose(existing).unwrap(),
MagicRootInstruction::Prepare.compose(other).unwrap(),
])
.await
.expect("prepare of an existing account is a no-op");
let unchanged = te.get_account(existing).expect("existing account remains");
assert!(unchanged.is(AccountMode::Delegated), "mode untouched");
assert_eq!(unchanged.slot(), before.slot(), "slot untouched");
assert_eq!(unchanged.data(), before.data(), "data untouched");
assert!(
te.get_account(other)
.expect("second prepare committed")
.is(AccountMode::Placeholder),
"prepare still materializes the missing account in the same transaction"
);

te.close().await;
}
6 changes: 6 additions & 0 deletions programs/magic-root-interface/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ pub enum MagicRootInstruction {
/// (e.g. initializing a freshly created account); each is invoked via CPI
/// against the accounts it declares.
PostFinalize(Vec<Instruction>),
/// Materialize the target as an empty system-owned placeholder if it still
/// does not exist at execution time; a target that already exists is left
/// untouched. This makes the "create the shared writables my actions need"
/// step idempotent, so concurrent creates that name the same missing
/// account all converge on whichever transaction materializes it first.
Prepare,
}

impl MagicRootInstruction {
Expand Down
1 change: 1 addition & 0 deletions programs/magic-root-program/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@ solana-account = { workspace = true }
solana-instruction = { workspace = true }
solana-instruction-error = { workspace = true }
solana-program-runtime = { workspace = true }
solana-sdk-ids = { workspace = true }
solana-svm-log-collector = { workspace = true }
solana-transaction-context = { workspace = true }

Expand Down
6 changes: 6 additions & 0 deletions programs/magic-root-program/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,12 @@ authorization. The SVM's separate top-level-only privilege rule is unchanged.
placed immediately after the target's `Finalize` by internal composers. It
rejects any immutable instruction account marked writable and any action that
targets MagicRoot itself.
- `Prepare` materializes the target as an empty system-owned placeholder at
slot 1 when the target still does not exist at execution time (loaded as a
slot-0 placeholder); any existing target is left untouched. The check runs
under the transaction's account locks, so concurrent transactions asserting
the same missing account converge on the first materialization instead of
failing each other.

After authority and caller checks pass, MagicRoot does not determine whether a
complete account image is stale. Callers must supply current state; slot and
Expand Down
29 changes: 29 additions & 0 deletions programs/magic-root-program/src/account.rs
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,35 @@ pub(crate) fn finalize(
Ok(())
}

/// Materializes the target as an empty system-owned placeholder when it does
/// not exist yet; a target that already exists in any form is left untouched.
///
/// The existence check runs at execution time under the transaction's account
/// locks, so concurrent transactions asserting the same missing account cannot
/// race: the first one materializes it and the rest no-op.
pub(crate) fn prepare(
ctx: &InvokeContext<'_, '_>,
target: IndexOfAccount,
) -> Result<(), InstructionError> {
let mut account = ctx.transaction_context.accounts().try_borrow_mut(target)?;
if !account.is(AccountMode::Placeholder) || account.slot() != 0 {
ic_msg!(ctx, "MagicRoot: prepare skipped, target exists");
return Ok(());
}
let patches = [
AccountFieldPatch::Owner(solana_sdk_ids::system_program::id()),
AccountFieldPatch::Slot(1),
];
for patch in patches {
if let Err(error) = patch.apply(&mut account) {
ic_msg!(ctx, "MagicRoot: {}", error);
return Err(InstructionError::InvalidArgument);
}
}
ic_msg!(ctx, "MagicRoot: prepared placeholder");
Ok(())
}

/// Marks the target account closed for removal from storage.
pub(crate) fn delete(
ctx: &mut InvokeContext<'_, '_>,
Expand Down
1 change: 1 addition & 0 deletions programs/magic-root-program/src/processor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ fn dispatch(
MagicRootInstruction::Finalize(flags) => account::finalize(ctx, target, flags),
MagicRootInstruction::Delete => account::delete(ctx, target),
MagicRootInstruction::PostFinalize(actions) => post_finalize::process(ctx, actions),
MagicRootInstruction::Prepare => account::prepare(ctx, target),
}
}

Expand Down
Loading