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
38 changes: 38 additions & 0 deletions .github/workflows/cargo-test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
name: Cargo tests

on:
push:
branches:
- main
pull_request:

permissions:
contents: read

jobs:
test:
name: Run cargo test
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable

- uses: Swatinem/rust-cache@v2
with:
cache-on-failure: true

- name: Show rustc and cargo versions
run: |
rustc -Vv
cargo -Vv

- name: Install protoc (protobuf-compiler)
run: |
sudo apt-get update
sudo apt-get install -y protobuf-compiler
protoc --version

- name: Run tests
run: cargo test --all --locked
4 changes: 1 addition & 3 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

44 changes: 27 additions & 17 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ solana-compute-budget = { version = "=2.2.1" }
solana-compute-budget-instruction = { version = "=2.2.1" }
solana-fee-structure = { version = "=2.2.1" }
solana-frozen-abi = { version = "=2.2.1", optional = true, features = [
"frozen-abi",
"frozen-abi",
] }
solana-frozen-abi-macro = { version = "=2.2.1", optional = true, features = [
"frozen-abi",
"frozen-abi",
] }
solana-hash = { version = "=2.2.1" }
solana-instruction = { version = "=2.2.1", features = ["std"] }
Expand Down Expand Up @@ -65,15 +65,18 @@ bincode = { version = "1.3.3" }
ed25519-dalek = "=1.0.1"
lazy_static = "1.5.0"
libsecp256k1 = { version = "0.6.0", default-features = false, features = [
"std",
"static-context",
"std",
"static-context",
] }
openssl = "0.10"
prost = "0.11.9"
rand0-7 = { package = "rand", version = "0.7" }
shuttle = "0.7.1"
solana-account = { version = "=2.2.1", features = ["dev-context-only-utils"] }
solana-clock = { version = "=2.2.1" }
solana-compute-budget = { version = "=2.2.1", features = ["dev-context-only-utils"] }
solana-compute-budget = { version = "=2.2.1", features = [
"dev-context-only-utils",
] }
solana-compute-budget-interface = { version = "=2.2.1" }
solana-compute-budget-program = { version = "=2.2.1" }
solana-ed25519-program = { version = "=2.2.1" }
Expand All @@ -87,34 +90,41 @@ solana-rent = { version = "=2.2.1" }
solana-sbpf = "0.10"
solana-sdk = { version = "=2.2.1", features = ["dev-context-only-utils"] }
solana-secp256k1-program = { version = "=2.2.1" }
solana-secp256r1-program = { version = "=2.2.1", features = ["openssl-vendored"] }
solana-secp256r1-program = { version = "=2.2.1", features = [
"openssl-vendored",
] }
solana-signature = { version = "=2.2.1" }
solana-signer = { version = "=2.2.1" }
# See order-crates-for-publishing.py for using this unusual `path = "."`
solana-svm = { path = ".", features = ["dev-context-only-utils"] }
solana-svm-conformance = { version = "=2.2.1" }
solana-system-program = { version = "=2.2.1" }
solana-system-transaction = { version = "=2.2.1" }
solana-sysvar = { version = "=2.2.1" }
solana-transaction = { version = "=2.2.1" }
solana-transaction-context = { version = "=2.2.1", features = ["dev-context-only-utils" ] }
solana-transaction-context = { version = "=2.2.1", features = [
"dev-context-only-utils",
] }
test-case = "3.3.1"
solana-svm = { path = ".", features = ["dev-context-only-utils"] }

[package.metadata.docs.rs]
targets = ["x86_64-unknown-linux-gnu"]

[features]
dev-context-only-utils = ["dep:qualifier_attr"]
frozen-abi = [
"dep:solana-frozen-abi",
"dep:solana-frozen-abi-macro",
"solana-compute-budget/frozen-abi",
"solana-program-runtime/frozen-abi",
"solana-sdk/frozen-abi",
"dep:solana-frozen-abi",
"dep:solana-frozen-abi-macro",
"solana-compute-budget/frozen-abi",
"solana-program-runtime/frozen-abi",
"solana-sdk/frozen-abi",
]
shuttle-test = [
"solana-type-overrides/shuttle-test",
"solana-program-runtime/shuttle-test",
"solana-bpf-loader-program/shuttle-test",
"solana-loader-v4-program/shuttle-test",
"solana-type-overrides/shuttle-test",
"solana-program-runtime/shuttle-test",
"solana-bpf-loader-program/shuttle-test",
"solana-loader-v4-program/shuttle-test",
]

[patch.crates-io]
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "a892d2" }
77 changes: 69 additions & 8 deletions src/account_loader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,7 @@ pub(crate) struct ValidatedTransactionDetails {
pub(crate) compute_budget_limits: ComputeBudgetLimits,
pub(crate) fee_details: FeeDetails,
pub(crate) loaded_fee_payer_account: LoadedTransactionAccount,
pub(crate) fee_payer_address: Pubkey,
}

#[derive(PartialEq, Eq, Debug, Clone)]
Expand Down Expand Up @@ -208,21 +209,48 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> {
&executed_transaction.loaded_transaction.accounts,
);
} else {
let fee_payer_address = self.effective_fee_payer_address_for_failed_tx(message);
self.update_accounts_for_failed_tx(
message,
&executed_transaction.loaded_transaction.rollback_accounts,
&fee_payer_address,
);
}
}

pub(crate) fn update_accounts_for_failed_tx(
/// If the fee payer is delegated, use it. Otherwise, load the escrow
/// account if delegated
pub(crate) fn effective_fee_payer_address_for_failed_tx(
&mut self,
message: &impl SVMMessage,
) -> Pubkey {
use crate::escrow::ephemeral_balance_pda_from_payer;
let fee_payer_address = *message.fee_payer();
let mut is_delegated = |addr: &Pubkey| -> bool {
self.load_account(addr, true)
.map(|acc| acc.account.delegated())
.unwrap_or(false)
};
// If the fee payer is delegated, use it
if is_delegated(&fee_payer_address) {
return fee_payer_address;
}
// Otherwise, load the escrow account if delegated
let escrow_address = ephemeral_balance_pda_from_payer(&fee_payer_address);
if is_delegated(&escrow_address) {
return escrow_address;
}
fee_payer_address
}

pub(crate) fn update_accounts_for_failed_tx(
&mut self,
rollback_accounts: &RollbackAccounts,
fee_payer_address: &Pubkey,
) {
let fee_payer_address = message.fee_payer();
match rollback_accounts {
RollbackAccounts::FeePayerOnly { fee_payer_account } => {
RollbackAccounts::FeePayerOnly {
fee_payer_account, ..
} => {
self.account_cache
.insert(*fee_payer_address, fee_payer_account.clone());
}
Expand All @@ -233,6 +261,7 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> {
RollbackAccounts::SeparateNonceAndFeePayer {
nonce,
fee_payer_account,
..
} => {
self.account_cache
.insert(*nonce.address(), nonce.account().clone());
Expand Down Expand Up @@ -360,6 +389,7 @@ pub(crate) fn load_transaction<CB: TransactionProcessingCallback>(
let load_result = load_transaction_accounts(
account_loader,
message,
&tx_details.fee_payer_address,
tx_details.loaded_fee_payer_account,
&tx_details.compute_budget_limits,
error_metrics,
Expand Down Expand Up @@ -399,6 +429,7 @@ struct LoadedTransactionAccounts {
fn load_transaction_accounts<CB: TransactionProcessingCallback>(
account_loader: &mut AccountLoader<CB>,
message: &impl SVMMessage,
fee_payer_address: &Pubkey,
loaded_fee_payer_account: LoadedTransactionAccount,
compute_budget_limits: &ComputeBudgetLimits,
error_metrics: &mut TransactionErrorMetrics,
Expand Down Expand Up @@ -434,7 +465,7 @@ fn load_transaction_accounts<CB: TransactionProcessingCallback>(

// Since the fee payer is always the first account, collect it first.
// We can use it directly because it was already loaded during validation.
collect_loaded_account(message.fee_payer(), loaded_fee_payer_account)?;
collect_loaded_account(fee_payer_address, loaded_fee_payer_account)?;

// Attempt to load and collect remaining non-fee payer accounts
for (account_index, account_key) in account_keys.iter().enumerate().skip(1) {
Expand Down Expand Up @@ -1043,10 +1074,26 @@ mod tests {
Arc::new(FeatureSet::all_enabled()),
0,
);
// Build proper ValidatedTransactionDetails with real fee payer
let fee_payer = *tx.message().fee_payer();
// In some tests we don't pass actual accounts; default to a zeroed account for fee payer
let fee_payer_account = callbacks
.accounts_map
.get(&fee_payer)
.cloned()
.unwrap_or_else(|| AccountSharedData::default());
let validation_details = ValidatedTransactionDetails {
fee_payer_address: fee_payer,
loaded_fee_payer_account: LoadedTransactionAccount {
account: fee_payer_account,
..LoadedTransactionAccount::default()
},
..ValidatedTransactionDetails::default()
};
load_transaction(
&mut account_loader,
&tx,
Ok(ValidatedTransactionDetails::default()),
Ok(validation_details),
&mut error_metrics,
&RentCollector::default(),
)
Expand Down Expand Up @@ -1205,16 +1252,19 @@ mod tests {
}
}

// If payer account has no balance, expected AccountNotFound Error
// If payer account has no balance, expected InsufficientFundsForFee Error
// regardless feature gate status, or if payer is nonce account.
// NOTE: solana svm returns AccountNotFound, but since we support not existing (signer)
// accounts as fee payer (when validator fees = 0), we return InsufficientFundsForFee
// instead.
{
for is_nonce in [true, false] {
validate_fee_payer_account(
ValidateFeePayerTestParameter {
is_nonce,
payer_init_balance: 0,
fee,
expected_result: Err(TransactionError::AccountNotFound),
expected_result: Err(TransactionError::InsufficientFundsForFee),
payer_post_balance: 0,
},
&rent_collector,
Expand Down Expand Up @@ -1331,6 +1381,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&fee_payer_address,
LoadedTransactionAccount {
loaded_size: fee_payer_account.data().len(),
account: fee_payer_account.clone(),
Expand Down Expand Up @@ -1394,6 +1445,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key1.pubkey(),
LoadedTransactionAccount {
account: fee_payer_account.clone(),
..LoadedTransactionAccount::default()
Expand Down Expand Up @@ -1454,6 +1506,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key1.pubkey(),
LoadedTransactionAccount::default(),
&ComputeBudgetLimits::default(),
&mut error_metrics,
Expand Down Expand Up @@ -1496,6 +1549,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key1.pubkey(),
LoadedTransactionAccount::default(),
&ComputeBudgetLimits::default(),
&mut error_metrics,
Expand Down Expand Up @@ -1549,6 +1603,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key2.pubkey(),
LoadedTransactionAccount {
account: fee_payer_account.clone(),
..LoadedTransactionAccount::default()
Expand Down Expand Up @@ -1613,6 +1668,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key2.pubkey(),
LoadedTransactionAccount::default(),
&ComputeBudgetLimits::default(),
&mut error_metrics,
Expand Down Expand Up @@ -1665,6 +1721,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key2.pubkey(),
LoadedTransactionAccount::default(),
&ComputeBudgetLimits::default(),
&mut error_metrics,
Expand Down Expand Up @@ -1725,6 +1782,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key2.pubkey(),
LoadedTransactionAccount {
account: fee_payer_account.clone(),
..LoadedTransactionAccount::default()
Expand Down Expand Up @@ -1808,6 +1866,7 @@ mod tests {
let result = load_transaction_accounts(
&mut account_loader,
sanitized_transaction.message(),
&key2.pubkey(),
LoadedTransactionAccount {
account: fee_payer_account.clone(),
..LoadedTransactionAccount::default()
Expand Down Expand Up @@ -1960,6 +2019,7 @@ mod tests {
account: fee_payer_account,
..LoadedTransactionAccount::default()
},
fee_payer_address: key2.pubkey(),
..ValidatedTransactionDetails::default()
});

Expand Down Expand Up @@ -2321,6 +2381,7 @@ mod tests {
let loaded_transaction_accounts = load_transaction_accounts(
&mut account_loader,
&transaction,
&fee_payer,
LoadedTransactionAccount {
account: fee_payer_account.clone(),
loaded_size: fee_payer_size as usize,
Expand Down
10 changes: 10 additions & 0 deletions src/escrow.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
use solana_pubkey::{pubkey, Pubkey};

// Delegation program ID used for deriving escrow-related PDAs
pub const DELEGATION_PROGRAM_ID: Pubkey = pubkey!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh");

/// Derive the ephemeral balance PDA for a given payer and index, using the
/// delegation program ID.
pub fn ephemeral_balance_pda_from_payer(payer: &Pubkey) -> Pubkey {
Pubkey::find_program_address(&[b"balance", payer.as_ref(), &[0]], &DELEGATION_PROGRAM_ID).0
}
1 change: 1 addition & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

pub mod account_loader;
pub mod account_overrides;
pub mod escrow;
pub mod message_processor;
pub mod nonce_info;
pub mod program_loader;
Expand Down
Loading