Skip to content

Commit 38b42a6

Browse files
feat: charge escrow if present (#3)
* feat: charge escrow if present * chore: refactor & add integration test * chore: refactor * fix: github workflow, add protobuf * fix: deps * chore: refactor * chore: add tests * chore: fix integration tests * fix: restore api compatibility for integration * chore: upadte dep * chore: ignore privileged * chore: hold on first var * fix: no-segfault solana-account * chore: remove duplicated CI runs * chore: add comments * Update src/transaction_processor.rs Co-authored-by: Babur Makhmudov <[email protected]> * chore: fix style * chore: fmt cleanup * fix: propagate feepayer address * fix: allow not delegated fee_payer if fees = 0 * fix: gasless existing feepayer --------- Co-authored-by: Babur Makhmudov <[email protected]> Co-authored-by: Babur Makhmudov <[email protected]>
1 parent 3e6c209 commit 38b42a6

File tree

9 files changed

+564
-146
lines changed

9 files changed

+564
-146
lines changed

.github/workflows/cargo-test.yml

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
name: Cargo tests
2+
3+
on:
4+
push:
5+
branches:
6+
- main
7+
pull_request:
8+
9+
permissions:
10+
contents: read
11+
12+
jobs:
13+
test:
14+
name: Run cargo test
15+
runs-on: ubuntu-latest
16+
17+
steps:
18+
- uses: actions/checkout@v4
19+
20+
- uses: dtolnay/rust-toolchain@stable
21+
22+
- uses: Swatinem/rust-cache@v2
23+
with:
24+
cache-on-failure: true
25+
26+
- name: Show rustc and cargo versions
27+
run: |
28+
rustc -Vv
29+
cargo -Vv
30+
31+
- name: Install protoc (protobuf-compiler)
32+
run: |
33+
sudo apt-get update
34+
sudo apt-get install -y protobuf-compiler
35+
protoc --version
36+
37+
- name: Run tests
38+
run: cargo test --all --locked

Cargo.lock

Lines changed: 1 addition & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 27 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,10 @@ solana-compute-budget = { version = "=2.2.1" }
2525
solana-compute-budget-instruction = { version = "=2.2.1" }
2626
solana-fee-structure = { version = "=2.2.1" }
2727
solana-frozen-abi = { version = "=2.2.1", optional = true, features = [
28-
"frozen-abi",
28+
"frozen-abi",
2929
] }
3030
solana-frozen-abi-macro = { version = "=2.2.1", optional = true, features = [
31-
"frozen-abi",
31+
"frozen-abi",
3232
] }
3333
solana-hash = { version = "=2.2.1" }
3434
solana-instruction = { version = "=2.2.1", features = ["std"] }
@@ -65,15 +65,18 @@ bincode = { version = "1.3.3" }
6565
ed25519-dalek = "=1.0.1"
6666
lazy_static = "1.5.0"
6767
libsecp256k1 = { version = "0.6.0", default-features = false, features = [
68-
"std",
69-
"static-context",
68+
"std",
69+
"static-context",
7070
] }
7171
openssl = "0.10"
7272
prost = "0.11.9"
7373
rand0-7 = { package = "rand", version = "0.7" }
7474
shuttle = "0.7.1"
75+
solana-account = { version = "=2.2.1", features = ["dev-context-only-utils"] }
7576
solana-clock = { version = "=2.2.1" }
76-
solana-compute-budget = { version = "=2.2.1", features = ["dev-context-only-utils"] }
77+
solana-compute-budget = { version = "=2.2.1", features = [
78+
"dev-context-only-utils",
79+
] }
7780
solana-compute-budget-interface = { version = "=2.2.1" }
7881
solana-compute-budget-program = { version = "=2.2.1" }
7982
solana-ed25519-program = { version = "=2.2.1" }
@@ -87,34 +90,41 @@ solana-rent = { version = "=2.2.1" }
8790
solana-sbpf = "0.10"
8891
solana-sdk = { version = "=2.2.1", features = ["dev-context-only-utils"] }
8992
solana-secp256k1-program = { version = "=2.2.1" }
90-
solana-secp256r1-program = { version = "=2.2.1", features = ["openssl-vendored"] }
93+
solana-secp256r1-program = { version = "=2.2.1", features = [
94+
"openssl-vendored",
95+
] }
9196
solana-signature = { version = "=2.2.1" }
9297
solana-signer = { version = "=2.2.1" }
9398
# See order-crates-for-publishing.py for using this unusual `path = "."`
94-
solana-svm = { path = ".", features = ["dev-context-only-utils"] }
9599
solana-svm-conformance = { version = "=2.2.1" }
96100
solana-system-program = { version = "=2.2.1" }
97101
solana-system-transaction = { version = "=2.2.1" }
98102
solana-sysvar = { version = "=2.2.1" }
99103
solana-transaction = { version = "=2.2.1" }
100-
solana-transaction-context = { version = "=2.2.1", features = ["dev-context-only-utils" ] }
104+
solana-transaction-context = { version = "=2.2.1", features = [
105+
"dev-context-only-utils",
106+
] }
101107
test-case = "3.3.1"
108+
solana-svm = { path = ".", features = ["dev-context-only-utils"] }
102109

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

106113
[features]
107114
dev-context-only-utils = ["dep:qualifier_attr"]
108115
frozen-abi = [
109-
"dep:solana-frozen-abi",
110-
"dep:solana-frozen-abi-macro",
111-
"solana-compute-budget/frozen-abi",
112-
"solana-program-runtime/frozen-abi",
113-
"solana-sdk/frozen-abi",
116+
"dep:solana-frozen-abi",
117+
"dep:solana-frozen-abi-macro",
118+
"solana-compute-budget/frozen-abi",
119+
"solana-program-runtime/frozen-abi",
120+
"solana-sdk/frozen-abi",
114121
]
115122
shuttle-test = [
116-
"solana-type-overrides/shuttle-test",
117-
"solana-program-runtime/shuttle-test",
118-
"solana-bpf-loader-program/shuttle-test",
119-
"solana-loader-v4-program/shuttle-test",
123+
"solana-type-overrides/shuttle-test",
124+
"solana-program-runtime/shuttle-test",
125+
"solana-bpf-loader-program/shuttle-test",
126+
"solana-loader-v4-program/shuttle-test",
120127
]
128+
129+
[patch.crates-io]
130+
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "a892d2" }

src/account_loader.rs

Lines changed: 69 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -80,6 +80,7 @@ pub(crate) struct ValidatedTransactionDetails {
8080
pub(crate) compute_budget_limits: ComputeBudgetLimits,
8181
pub(crate) fee_details: FeeDetails,
8282
pub(crate) loaded_fee_payer_account: LoadedTransactionAccount,
83+
pub(crate) fee_payer_address: Pubkey,
8384
}
8485

8586
#[derive(PartialEq, Eq, Debug, Clone)]
@@ -208,21 +209,48 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> {
208209
&executed_transaction.loaded_transaction.accounts,
209210
);
210211
} else {
212+
let fee_payer_address = self.effective_fee_payer_address_for_failed_tx(message);
211213
self.update_accounts_for_failed_tx(
212-
message,
213214
&executed_transaction.loaded_transaction.rollback_accounts,
215+
&fee_payer_address,
214216
);
215217
}
216218
}
217219

218-
pub(crate) fn update_accounts_for_failed_tx(
220+
/// If the fee payer is delegated, use it. Otherwise, load the escrow
221+
/// account if delegated
222+
pub(crate) fn effective_fee_payer_address_for_failed_tx(
219223
&mut self,
220224
message: &impl SVMMessage,
225+
) -> Pubkey {
226+
use crate::escrow::ephemeral_balance_pda_from_payer;
227+
let fee_payer_address = *message.fee_payer();
228+
let mut is_delegated = |addr: &Pubkey| -> bool {
229+
self.load_account(addr, true)
230+
.map(|acc| acc.account.delegated())
231+
.unwrap_or(false)
232+
};
233+
// If the fee payer is delegated, use it
234+
if is_delegated(&fee_payer_address) {
235+
return fee_payer_address;
236+
}
237+
// Otherwise, load the escrow account if delegated
238+
let escrow_address = ephemeral_balance_pda_from_payer(&fee_payer_address);
239+
if is_delegated(&escrow_address) {
240+
return escrow_address;
241+
}
242+
fee_payer_address
243+
}
244+
245+
pub(crate) fn update_accounts_for_failed_tx(
246+
&mut self,
221247
rollback_accounts: &RollbackAccounts,
248+
fee_payer_address: &Pubkey,
222249
) {
223-
let fee_payer_address = message.fee_payer();
224250
match rollback_accounts {
225-
RollbackAccounts::FeePayerOnly { fee_payer_account } => {
251+
RollbackAccounts::FeePayerOnly {
252+
fee_payer_account, ..
253+
} => {
226254
self.account_cache
227255
.insert(*fee_payer_address, fee_payer_account.clone());
228256
}
@@ -233,6 +261,7 @@ impl<'a, CB: TransactionProcessingCallback> AccountLoader<'a, CB> {
233261
RollbackAccounts::SeparateNonceAndFeePayer {
234262
nonce,
235263
fee_payer_account,
264+
..
236265
} => {
237266
self.account_cache
238267
.insert(*nonce.address(), nonce.account().clone());
@@ -360,6 +389,7 @@ pub(crate) fn load_transaction<CB: TransactionProcessingCallback>(
360389
let load_result = load_transaction_accounts(
361390
account_loader,
362391
message,
392+
&tx_details.fee_payer_address,
363393
tx_details.loaded_fee_payer_account,
364394
&tx_details.compute_budget_limits,
365395
error_metrics,
@@ -399,6 +429,7 @@ struct LoadedTransactionAccounts {
399429
fn load_transaction_accounts<CB: TransactionProcessingCallback>(
400430
account_loader: &mut AccountLoader<CB>,
401431
message: &impl SVMMessage,
432+
fee_payer_address: &Pubkey,
402433
loaded_fee_payer_account: LoadedTransactionAccount,
403434
compute_budget_limits: &ComputeBudgetLimits,
404435
error_metrics: &mut TransactionErrorMetrics,
@@ -434,7 +465,7 @@ fn load_transaction_accounts<CB: TransactionProcessingCallback>(
434465

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

439470
// Attempt to load and collect remaining non-fee payer accounts
440471
for (account_index, account_key) in account_keys.iter().enumerate().skip(1) {
@@ -1043,10 +1074,26 @@ mod tests {
10431074
Arc::new(FeatureSet::all_enabled()),
10441075
0,
10451076
);
1077+
// Build proper ValidatedTransactionDetails with real fee payer
1078+
let fee_payer = *tx.message().fee_payer();
1079+
// In some tests we don't pass actual accounts; default to a zeroed account for fee payer
1080+
let fee_payer_account = callbacks
1081+
.accounts_map
1082+
.get(&fee_payer)
1083+
.cloned()
1084+
.unwrap_or_else(|| AccountSharedData::default());
1085+
let validation_details = ValidatedTransactionDetails {
1086+
fee_payer_address: fee_payer,
1087+
loaded_fee_payer_account: LoadedTransactionAccount {
1088+
account: fee_payer_account,
1089+
..LoadedTransactionAccount::default()
1090+
},
1091+
..ValidatedTransactionDetails::default()
1092+
};
10461093
load_transaction(
10471094
&mut account_loader,
10481095
&tx,
1049-
Ok(ValidatedTransactionDetails::default()),
1096+
Ok(validation_details),
10501097
&mut error_metrics,
10511098
&RentCollector::default(),
10521099
)
@@ -1205,16 +1252,19 @@ mod tests {
12051252
}
12061253
}
12071254

1208-
// If payer account has no balance, expected AccountNotFound Error
1255+
// If payer account has no balance, expected InsufficientFundsForFee Error
12091256
// regardless feature gate status, or if payer is nonce account.
1257+
// NOTE: solana svm returns AccountNotFound, but since we support not existing (signer)
1258+
// accounts as fee payer (when validator fees = 0), we return InsufficientFundsForFee
1259+
// instead.
12101260
{
12111261
for is_nonce in [true, false] {
12121262
validate_fee_payer_account(
12131263
ValidateFeePayerTestParameter {
12141264
is_nonce,
12151265
payer_init_balance: 0,
12161266
fee,
1217-
expected_result: Err(TransactionError::AccountNotFound),
1267+
expected_result: Err(TransactionError::InsufficientFundsForFee),
12181268
payer_post_balance: 0,
12191269
},
12201270
&rent_collector,
@@ -1331,6 +1381,7 @@ mod tests {
13311381
let result = load_transaction_accounts(
13321382
&mut account_loader,
13331383
sanitized_transaction.message(),
1384+
&fee_payer_address,
13341385
LoadedTransactionAccount {
13351386
loaded_size: fee_payer_account.data().len(),
13361387
account: fee_payer_account.clone(),
@@ -1394,6 +1445,7 @@ mod tests {
13941445
let result = load_transaction_accounts(
13951446
&mut account_loader,
13961447
sanitized_transaction.message(),
1448+
&key1.pubkey(),
13971449
LoadedTransactionAccount {
13981450
account: fee_payer_account.clone(),
13991451
..LoadedTransactionAccount::default()
@@ -1454,6 +1506,7 @@ mod tests {
14541506
let result = load_transaction_accounts(
14551507
&mut account_loader,
14561508
sanitized_transaction.message(),
1509+
&key1.pubkey(),
14571510
LoadedTransactionAccount::default(),
14581511
&ComputeBudgetLimits::default(),
14591512
&mut error_metrics,
@@ -1496,6 +1549,7 @@ mod tests {
14961549
let result = load_transaction_accounts(
14971550
&mut account_loader,
14981551
sanitized_transaction.message(),
1552+
&key1.pubkey(),
14991553
LoadedTransactionAccount::default(),
15001554
&ComputeBudgetLimits::default(),
15011555
&mut error_metrics,
@@ -1549,6 +1603,7 @@ mod tests {
15491603
let result = load_transaction_accounts(
15501604
&mut account_loader,
15511605
sanitized_transaction.message(),
1606+
&key2.pubkey(),
15521607
LoadedTransactionAccount {
15531608
account: fee_payer_account.clone(),
15541609
..LoadedTransactionAccount::default()
@@ -1613,6 +1668,7 @@ mod tests {
16131668
let result = load_transaction_accounts(
16141669
&mut account_loader,
16151670
sanitized_transaction.message(),
1671+
&key2.pubkey(),
16161672
LoadedTransactionAccount::default(),
16171673
&ComputeBudgetLimits::default(),
16181674
&mut error_metrics,
@@ -1665,6 +1721,7 @@ mod tests {
16651721
let result = load_transaction_accounts(
16661722
&mut account_loader,
16671723
sanitized_transaction.message(),
1724+
&key2.pubkey(),
16681725
LoadedTransactionAccount::default(),
16691726
&ComputeBudgetLimits::default(),
16701727
&mut error_metrics,
@@ -1725,6 +1782,7 @@ mod tests {
17251782
let result = load_transaction_accounts(
17261783
&mut account_loader,
17271784
sanitized_transaction.message(),
1785+
&key2.pubkey(),
17281786
LoadedTransactionAccount {
17291787
account: fee_payer_account.clone(),
17301788
..LoadedTransactionAccount::default()
@@ -1808,6 +1866,7 @@ mod tests {
18081866
let result = load_transaction_accounts(
18091867
&mut account_loader,
18101868
sanitized_transaction.message(),
1869+
&key2.pubkey(),
18111870
LoadedTransactionAccount {
18121871
account: fee_payer_account.clone(),
18131872
..LoadedTransactionAccount::default()
@@ -1960,6 +2019,7 @@ mod tests {
19602019
account: fee_payer_account,
19612020
..LoadedTransactionAccount::default()
19622021
},
2022+
fee_payer_address: key2.pubkey(),
19632023
..ValidatedTransactionDetails::default()
19642024
});
19652025

@@ -2321,6 +2381,7 @@ mod tests {
23212381
let loaded_transaction_accounts = load_transaction_accounts(
23222382
&mut account_loader,
23232383
&transaction,
2384+
&fee_payer,
23242385
LoadedTransactionAccount {
23252386
account: fee_payer_account.clone(),
23262387
loaded_size: fee_payer_size as usize,

src/escrow.rs

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
use solana_pubkey::{pubkey, Pubkey};
2+
3+
// Delegation program ID used for deriving escrow-related PDAs
4+
pub const DELEGATION_PROGRAM_ID: Pubkey = pubkey!("DELeGGvXpWV2fqJUhqcF5ZSYMS4JTLjteaAMARRSaeSh");
5+
6+
/// Derive the ephemeral balance PDA for a given payer and index, using the
7+
/// delegation program ID.
8+
pub fn ephemeral_balance_pda_from_payer(payer: &Pubkey) -> Pubkey {
9+
Pubkey::find_program_address(&[b"balance", payer.as_ref(), &[0]], &DELEGATION_PROGRAM_ID).0
10+
}

src/lib.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33

44
pub mod account_loader;
55
pub mod account_overrides;
6+
pub mod escrow;
67
pub mod message_processor;
78
pub mod nonce_info;
89
pub mod program_loader;

0 commit comments

Comments
 (0)