Skip to content

Commit e4104f4

Browse files
committed
Merge branch 'master' into thlorenz/the-great-unborking
* master: feat: use latest svm version (#657) chore: update solana account (#660) fix: better transaction diagnostics & rent exemption check (#642) chore: add access-control-max-age header to cors (#654) fix(aperture): prevent racy getLatestBlockhash (#649) fix: await until sub is established and perform them in parallel (#650) feat: persist all accounts (#648)
2 parents ef27ffa + b5823d8 commit e4104f4

27 files changed

Lines changed: 366 additions & 196 deletions

File tree

‎Cargo.lock‎

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

‎Cargo.toml‎

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,7 @@ serde = "1.0.217"
151151
serde_derive = "1.0"
152152
serde_json = "1.0"
153153
sha3 = "0.10.8"
154-
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "731fa50" }
154+
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "5afcedc" }
155155
solana-account-decoder = { version = "2.2" }
156156
solana-accounts-db = { version = "2.2" }
157157
solana-account-decoder-client-types = { version = "2.2" }
@@ -220,13 +220,13 @@ vergen = "8.3.1"
220220

221221
[workspace.dependencies.solana-svm]
222222
git = "https://github.com/magicblock-labs/magicblock-svm.git"
223-
rev = "11bbaf2"
223+
rev = "e480fa2"
224224
features = ["dev-context-only-utils"]
225225

226226
[patch.crates-io]
227227
# some solana dependencies have solana-storage-proto as dependency
228228
# we need to patch them with our version, because they use protobuf-src v1.1.0
229229
# and we use protobuf-src v2.1.1. Otherwise compilation fails
230-
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "731fa50" }
230+
solana-account = { git = "https://github.com/magicblock-labs/solana-account.git", rev = "5afcedc" }
231231
solana-storage-proto = { path = "./storage-proto" }
232-
solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "11bbaf2" }
232+
solana-svm = { git = "https://github.com/magicblock-labs/magicblock-svm.git", rev = "e480fa2" }

‎magicblock-aperture/Cargo.toml‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ tokio = { workspace = true }
2020
tokio-util = { workspace = true }
2121

2222
# containers
23+
arc-swap = { workspace = true }
2324
scc = { workspace = true }
2425

2526
# sync

‎magicblock-aperture/src/processor.rs‎

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -83,16 +83,20 @@ impl EventProcessor {
8383
}
8484

8585
/// The main event processing loop for a single worker instance.
86-
///
87-
/// This function listens on all event channels concurrently and processes messages
88-
/// as they arrive. The `tokio::select!` macro is biased to prioritize account
89-
/// processing, as it is typically the most frequent and time-sensitive event.
9086
async fn run(self, id: usize, cancel: CancellationToken) {
9187
info!("event processor {id} is running");
9288
loop {
9389
tokio::select! {
9490
biased;
9591

92+
// Process a new block.
93+
Ok(latest) = self.block_update_rx.recv_async() => {
94+
// Notify subscribers waiting on slot updates.
95+
self.subscriptions.send_slot(latest.meta.slot);
96+
// Update the global blocks cache with the latest block.
97+
self.blocks.set_latest(latest);
98+
}
99+
96100
// Process a new account state update.
97101
Ok(state) = self.account_update_rx.recv_async() => {
98102
// Notify subscribers for this specific account.
@@ -121,14 +125,6 @@ impl EventProcessor {
121125
self.transactions.push(status.signature, Some(result));
122126
}
123127

124-
// Process a new block.
125-
Ok(latest) = self.block_update_rx.recv_async() => {
126-
// Notify subscribers waiting on slot updates.
127-
self.subscriptions.send_slot(latest.meta.slot);
128-
// Update the global blocks cache with the latest block.
129-
self.blocks.set_latest(latest);
130-
}
131-
132128
// Listen for the cancellation signal to gracefully shut down.
133129
_ = cancel.cancelled() => {
134130
break;

‎magicblock-aperture/src/server/http/dispatch.rs‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -176,13 +176,14 @@ impl HttpDispatcher {
176176
fn handle_cors_preflight() -> Result<Response<JsonBody>, Infallible> {
177177
use hyper::header::{
178178
ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS,
179-
ACCESS_CONTROL_ALLOW_ORIGIN,
179+
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_MAX_AGE,
180180
};
181181

182182
let response = Response::builder()
183183
.header(ACCESS_CONTROL_ALLOW_ORIGIN, "*")
184-
.header(ACCESS_CONTROL_ALLOW_METHODS, "POST, OPTIONS")
184+
.header(ACCESS_CONTROL_ALLOW_METHODS, "POST, OPTIONS, GET")
185185
.header(ACCESS_CONTROL_ALLOW_HEADERS, "*")
186+
.header(ACCESS_CONTROL_MAX_AGE, "86400")
186187
.body(JsonBody::from(""))
187188
// SAFETY: This is safe with static, valid headers
188189
.expect("Building CORS response failed");

‎magicblock-aperture/src/state/blocks.rs‎

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
1-
use std::{ops::Deref, time::Duration};
1+
use std::{ops::Deref, sync::Arc, time::Duration};
22

3+
use arc_swap::ArcSwapAny;
34
use magicblock_core::{
45
link::blocks::{BlockHash, BlockMeta, BlockUpdate},
56
Slot,
67
};
7-
use magicblock_ledger::LatestBlock;
88
use solana_rpc_client_api::response::RpcBlockhash;
99

1010
use super::ExpiringCache;
@@ -23,12 +23,20 @@ pub(crate) struct BlocksCache {
2323
/// The number of slots for which a blockhash is considered valid.
2424
/// This is calculated based on the host ER's block time relative to Solana's.
2525
block_validity: u64,
26-
/// The most recent block update received, protected by a `RwLock` for concurrent access.
27-
latest: LatestBlock,
26+
/// Latest observed block (updated whenever the ledger transitions to new slot)
27+
latest: ArcSwapAny<Arc<LastCachedBlock>>,
2828
/// An underlying time-based cache for storing `BlockHash` to `BlockMeta` mappings.
2929
cache: ExpiringCache<BlockHash, BlockMeta>,
3030
}
3131

32+
/// Last produced block that has been put into cache. We need to keep this separately,
33+
/// as there's no way to access the cache efficiently to find the latest inserted entry
34+
#[derive(Default, Debug, Clone, Copy)]
35+
pub(crate) struct LastCachedBlock {
36+
pub(crate) blockhash: BlockHash,
37+
pub(crate) slot: Slot,
38+
}
39+
3240
impl Deref for BlocksCache {
3341
type Target = ExpiringCache<BlockHash, BlockMeta>;
3442
fn deref(&self) -> &Self::Target {
@@ -44,7 +52,7 @@ impl BlocksCache {
4452
///
4553
/// # Panics
4654
/// Panics if `blocktime` is zero.
47-
pub(crate) fn new(blocktime: u64, latest: LatestBlock) -> Self {
55+
pub(crate) fn new(blocktime: u64, latest: LastCachedBlock) -> Self {
4856
const BLOCK_CACHE_TTL: Duration = Duration::from_secs(60);
4957
assert!(blocktime != 0, "blocktime cannot be zero");
5058

@@ -54,16 +62,23 @@ impl BlocksCache {
5462
let block_validity = blocktime_ratio * MAX_VALID_BLOCKHASH_SLOTS;
5563
let cache = ExpiringCache::new(BLOCK_CACHE_TTL);
5664
Self {
57-
latest,
65+
latest: ArcSwapAny::new(latest.into()),
5866
block_validity: block_validity as u64,
5967
cache,
6068
}
6169
}
6270

6371
/// Updates the latest block information in the cache.
6472
pub(crate) fn set_latest(&self, latest: BlockUpdate) {
65-
// The `push` method adds the blockhash to the underlying expiring cache.
73+
let last = LastCachedBlock {
74+
blockhash: latest.hash,
75+
slot: latest.meta.slot,
76+
};
77+
78+
// Register the block in the expiring cache
6679
self.cache.push(latest.hash, latest.meta);
80+
// And mark it as latest observed
81+
self.latest.swap(last.into());
6782
}
6883

6984
/// Retrieves information about the latest block, including its calculated validity period.
@@ -83,6 +98,7 @@ impl BlocksCache {
8398
}
8499

85100
/// A data structure containing essential details about a blockhash for RPC responses.
101+
#[derive(Default)]
86102
pub(crate) struct BlockHashInfo {
87103
/// The blockhash.
88104
pub(crate) hash: BlockHash,

‎magicblock-aperture/src/state/mod.rs‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
use std::{sync::Arc, time::Duration};
22

3-
use blocks::BlocksCache;
3+
use blocks::{BlocksCache, LastCachedBlock};
44
use cache::ExpiringCache;
55
use magicblock_account_cloner::ChainlinkCloner;
66
use magicblock_accounts_db::AccountsDb;
@@ -81,7 +81,11 @@ impl SharedState {
8181
blocktime: u64,
8282
) -> Self {
8383
const TRANSACTIONS_CACHE_TTL: Duration = Duration::from_secs(75);
84-
let latest = ledger.latest_block().clone();
84+
let block = ledger.latest_block().load();
85+
let latest = LastCachedBlock {
86+
blockhash: block.blockhash,
87+
slot: block.slot,
88+
};
8589
Self {
8690
context,
8791
accountsdb,

‎magicblock-aperture/src/tests.rs‎

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,8 @@ use crate::{
2727
EventProcessor,
2828
};
2929

30+
const LAMPORTS_PER_SOL: u64 = 1_000_000_000;
31+
3032
/// A test helper to create a unique WebSocket connection channel pair.
3133
fn ws_channel() -> (WsConnectionChannel, Receiver<Bytes>) {
3234
static CHAN_ID: AtomicU32 = AtomicU32::new(0);
@@ -100,7 +102,9 @@ mod event_processor {
100102
#[tokio::test]
101103
async fn test_account_update() {
102104
let (state, env) = setup();
103-
let acc = env.create_account_with_config(1, 1, guinea::ID).pubkey();
105+
let acc = env
106+
.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID)
107+
.pubkey();
104108
let (tx, mut rx) = ws_channel();
105109

106110
// Subscribe to both the specific account and the program that owns it.
@@ -140,7 +144,9 @@ mod event_processor {
140144
#[tokio::test]
141145
async fn test_transaction_update() {
142146
let (state, env) = setup();
143-
let acc = env.create_account_with_config(1, 42, guinea::ID).pubkey();
147+
let acc = env
148+
.create_account_with_config(LAMPORTS_PER_SOL, 42, guinea::ID)
149+
.pubkey();
144150
let (tx, mut rx) = ws_channel();
145151

146152
let ix = Instruction::new_with_bincode(
@@ -201,7 +207,9 @@ mod event_processor {
201207
let (state, env) = setup();
202208

203209
// Test multiple subscriptions to the same ACCOUNT.
204-
let acc1 = env.create_account_with_config(1, 1, guinea::ID).pubkey();
210+
let acc1 = env
211+
.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID)
212+
.pubkey();
205213
let (acc_tx1, mut acc_rx1) = ws_channel();
206214
let (acc_tx2, mut acc_rx2) = ws_channel();
207215

@@ -227,7 +235,9 @@ mod event_processor {
227235
assert_receives_update(&mut acc_rx2, "second account subscriber").await;
228236

229237
// Test multiple subscriptions to the same PROGRAM.
230-
let acc2 = env.create_account_with_config(1, 1, guinea::ID).pubkey();
238+
let acc2 = env
239+
.create_account_with_config(LAMPORTS_PER_SOL, 1, guinea::ID)
240+
.pubkey();
231241
let (prog_tx1, mut prog_rx1) = ws_channel();
232242
let (prog_tx2, mut prog_rx2) = ws_channel();
233243
let prog_encoder = ProgramAccountEncoder {

‎magicblock-api/src/magic_validator.rs‎

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -157,7 +157,6 @@ impl MagicValidator {
157157
let GenesisConfigInfo {
158158
genesis_config,
159159
validator_pubkey,
160-
..
161160
} = create_genesis_config_with_leader(
162161
u64::MAX,
163162
&validator_pubkey,
@@ -274,7 +273,7 @@ impl MagicValidator {
274273
});
275274

276275
validator::init_validator_authority(identity_keypair);
277-
276+
let base_fee = config.validator.base_fees.unwrap_or_default();
278277
let txn_scheduler_state = TransactionSchedulerState {
279278
accountsdb: accountsdb.clone(),
280279
ledger: ledger.clone(),
@@ -303,7 +302,7 @@ impl MagicValidator {
303302
let node_context = NodeContext {
304303
identity: validator_pubkey,
305304
faucet,
306-
base_fee: config.validator.base_fees.unwrap_or_default(),
305+
base_fee,
307306
featureset: txn_scheduler_state.environment.feature_set.clone(),
308307
};
309308
let transaction_scheduler =

‎magicblock-chainlink/src/chainlink/mod.rs‎

Lines changed: 13 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -244,43 +244,37 @@ Kept: {} delegated, {} blacklisted",
244244
.copied()
245245
.collect::<Vec<_>>();
246246
let feepayer = tx.message().fee_payer();
247-
// In the case of transactions we need to clone the feepayer account
248-
let clone_escrow = {
249-
// If the fee payer account is in the bank we only clone the balance
250-
// escrow account if the fee payer is not delegated
251-
// If it is not in the bank we include it just in case, it is fine
252-
// if it doesn't exist and once we cloned the feepayer account itself
253-
// and it turns out to be delegated, then we will avoid cloning the
254-
// escrow account next time
255-
self.accounts_bank
256-
.get_account(feepayer)
257-
.is_none_or(|a| !a.delegated())
258-
};
259247

260-
// Always allow the fee payer to be treated as empty-if-not-found so that
261-
// transactions can still be processed in gasless mode
262-
let mut mark_empty_if_not_found = vec![*feepayer];
248+
// Determine if we need to clone the escrow account for the feepayer
249+
let clone_escrow = self
250+
.accounts_bank
251+
.get_account(feepayer)
252+
.is_none_or(|a| !a.delegated());
263253

254+
// If cloning escrow, add the balance PDA
264255
if clone_escrow {
265256
let balance_pda = ephemeral_balance_pda_from_payer(feepayer, 0);
266257
trace!("Adding balance PDA {balance_pda} for feepayer {feepayer}");
267258
pubkeys.push(balance_pda);
268-
mark_empty_if_not_found.push(balance_pda);
269259
}
270-
let mark_empty_if_not_found = (!mark_empty_if_not_found.is_empty())
271-
.then(|| &mark_empty_if_not_found[..]);
260+
261+
// Mark *all* pubkeys as empty-if-not-found
262+
let mark_empty_if_not_found = Some(pubkeys.as_slice());
263+
264+
// Ensure accounts
272265
let res = self
273266
.ensure_accounts(&pubkeys, mark_empty_if_not_found)
274267
.await?;
275268

276-
// Best-effort auto airdrop for fee payer if configured and still empty locally
269+
// Best-effort auto airdrop for fee payer if configured
277270
if self.auto_airdrop_lamports > 0 {
278271
if let Some(fetch_cloner) = self.fetch_cloner() {
279272
let lamports = self
280273
.accounts_bank
281274
.get_account(feepayer)
282275
.map(|a| a.lamports())
283276
.unwrap_or(0);
277+
284278
if lamports == 0 {
285279
if let Err(err) = fetch_cloner
286280
.airdrop_account_if_empty(

0 commit comments

Comments
 (0)