Skip to content
Open
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
115 changes: 110 additions & 5 deletions crates/core/src/surfnet/locker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1603,18 +1603,32 @@ impl SurfnetSvmLocker {
.map(|p| svm_writer.inner.get_account_no_db(p))
.collect::<Vec<Option<Account>>>();
let (sanitized_transaction, versioned_transaction) = if do_propagate {
let address_loader = match (&transaction.message, &loaded_addresses) {
(VersionedMessage::V0(_), Some(loaded_addresses)) => {
SimpleAddressLoader::Enabled(loaded_addresses.clone())
}
// V0 messages without address table lookups still require an enabled loader.
(VersionedMessage::V0(_), None) => {
SimpleAddressLoader::Enabled(LoadedAddresses::default())
}
(VersionedMessage::Legacy(_), _) => SimpleAddressLoader::Disabled,
};

(
SanitizedTransaction::try_create(
transaction.clone(),
transaction.message.hash(),
Some(false),
if let Some(loaded_addresses) = &loaded_addresses {
SimpleAddressLoader::Enabled(loaded_addresses.clone())
} else {
SimpleAddressLoader::Disabled
},
address_loader,
&HashSet::new(), // todo: provide reserved account keys
)
.map_err(|error| {
debug!(
"Failed to sanitize transaction {} for Geyser account updates: {:?}",
signature, error
);
error
})
.ok(),
Some(transaction.clone()),
)
Expand Down Expand Up @@ -4229,6 +4243,97 @@ mod tests {
assert!(result.is_err());
}

#[tokio::test(flavor = "multi_thread")]
async fn test_v0_transaction_without_alt_emits_geyser_account_updates() {
use std::time::Duration;

use crossbeam_channel::{RecvTimeoutError, unbounded};
use solana_keypair::Keypair;
use solana_message::{VersionedMessage, v0};
use solana_signer::Signer;
use solana_system_interface::instruction as system_instruction;
use solana_transaction::versioned::VersionedTransaction;

let (svm, _events_rx, geyser_rx) = SurfnetSvm::default();
let locker = SurfnetSvmLocker::new(svm);

let payer = Keypair::new();
let payer_pubkey = payer.pubkey();
let recipient = Pubkey::new_unique();

let _ = locker
.airdrop(&payer_pubkey, 1_000_000_000)
.expect("airdrop should succeed");

let recent_blockhash = locker.latest_absolute_blockhash();
let message = v0::Message::try_compile(
&payer_pubkey,
&[system_instruction::transfer(
&payer_pubkey,
&recipient,
1_000_000,
)],
&[],
recent_blockhash,
)
.expect("v0 message should compile");

let tx =
VersionedTransaction::try_new(VersionedMessage::V0(message), &[payer.insecure_clone()])
.expect("v0 transaction should sign");

let tx_signature = tx.signatures[0];
let (status_tx, _status_rx) = unbounded();
locker
.process_transaction(&None, tx, status_tx, true, true)
.await
.expect("transaction processing should succeed");

let mut account_updates = vec![];
let mut got_transaction_notify = false;

for _ in 0..32 {
match geyser_rx.recv_timeout(Duration::from_millis(50)) {
Ok(crate::surfnet::GeyserEvent::UpdateAccount(update)) => {
account_updates.push(update);
}
Ok(crate::surfnet::GeyserEvent::NotifyTransaction(_, _)) => {
got_transaction_notify = true;
}
Ok(_) => {}
Err(RecvTimeoutError::Timeout) | Err(RecvTimeoutError::Disconnected) => break,
}
}

assert!(
got_transaction_notify,
"Expected NotifyTransaction geyser event"
);
assert!(
!account_updates.is_empty(),
"Expected account update geyser events for v0 transaction without ALTs"
);
assert!(
account_updates.iter().any(|u| u.pubkey == payer_pubkey),
"Expected payer account update"
);
assert!(
account_updates.iter().any(|u| u.pubkey == recipient),
"Expected recipient account update"
);

for update in account_updates {
let sanitized_transaction = update
.sanitized_transaction
.expect("Expected sanitized transaction on account update");
assert_eq!(
*sanitized_transaction.signature(),
tx_signature,
"Account update should carry transaction signature"
);
}
}

// Snapshot loading tests

#[tokio::test(flavor = "multi_thread")]
Expand Down
40 changes: 13 additions & 27 deletions crates/core/src/surfnet/svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -101,8 +101,8 @@ use crate::{
LogsSubscriptionData, locker::is_supported_token_program, surfnet_lite_svm::SurfnetLiteSvm,
},
types::{
GeyserAccountUpdate, MintAccount, SerializableAccountAdditionalData,
SurfnetTransactionStatus, SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta,
MintAccount, SerializableAccountAdditionalData, SurfnetTransactionStatus,
SyntheticBlockhash, TokenAccount, TransactionWithStatusMeta,
},
};

Expand Down Expand Up @@ -1714,13 +1714,11 @@ impl SurfnetSvm {
///
/// # Returns
/// `Ok(Vec<Signature>)` with confirmed signatures, or `Err(SurfpoolError)` on error.
fn confirm_transactions(&mut self) -> Result<(Vec<Signature>, HashSet<Pubkey>), SurfpoolError> {
fn confirm_transactions(&mut self) -> Result<Vec<Signature>, SurfpoolError> {
let mut confirmed_transactions = vec![];
let slot = self.latest_epoch_info.slot_index;
let current_slot = self.latest_epoch_info.absolute_slot;

let mut all_mutated_account_keys = HashSet::new();

while let Some((tx, status_tx, error)) =
self.transactions_queued_for_confirmation.pop_front()
{
Expand Down Expand Up @@ -1749,7 +1747,6 @@ impl SurfnetSvm {
continue;
};
let (tx_with_status_meta, mutated_account_keys) = tx_data.as_ref();
all_mutated_account_keys.extend(mutated_account_keys);

for pubkey in mutated_account_keys {
self.account_update_slots.insert(*pubkey, current_slot);
Expand All @@ -1768,7 +1765,7 @@ impl SurfnetSvm {
confirmed_transactions.push(signature);
}

Ok((confirmed_transactions, all_mutated_account_keys))
Ok(confirmed_transactions)
}

/// Finalizes transactions queued for finalization, sending finalized events as needed.
Expand Down Expand Up @@ -1955,20 +1952,7 @@ impl SurfnetSvm {
}
self.chain_tip = self.new_blockhash();
// Confirm processed transactions
let (confirmed_signatures, all_mutated_account_keys) = self.confirm_transactions()?;
let write_version = self.increment_write_version();

// Notify Geyser plugin of account updates
for pubkey in all_mutated_account_keys {
let Some(account) = self.inner.get_account(&pubkey)? else {
continue;
};
self.geyser_events_tx
.send(GeyserEvent::UpdateAccount(
GeyserAccountUpdate::block_update(pubkey, account, slot, write_version),
))
.ok();
}
let confirmed_signatures = self.confirm_transactions()?;

let num_transactions = confirmed_signatures.len() as u64;
self.updated_at += self.slot_time;
Expand Down Expand Up @@ -2023,20 +2007,22 @@ impl SurfnetSvm {
let root = new_slot.saturating_sub(FINALIZATION_SLOT_THRESHOLD);
self.notify_slot_subscribers(new_slot, parent_slot, root);

// Notify geyser plugins of slot status (Confirmed)
let geyser_parent_slot = slot.saturating_sub(1);

// Emit confirmation for the same slot used by processed account/transaction updates.
self.geyser_events_tx
.send(GeyserEvent::UpdateSlotStatus {
slot: new_slot,
parent: Some(parent_slot),
slot,
parent: slot.checked_sub(1),
status: GeyserSlotStatus::Confirmed,
})
.ok();

// Notify geyser plugins of block metadata
let block_metadata = GeyserBlockMetadata {
slot: new_slot,
slot,
blockhash: self.chain_tip.hash.clone(),
parent_slot,
parent_slot: geyser_parent_slot,
parent_blockhash: previous_chain_tip.hash.clone(),
block_time: Some(self.updated_at as i64 / 1_000),
block_height: Some(self.chain_tip.index),
Expand All @@ -2052,7 +2038,7 @@ impl SurfnetSvm {
.map(|h| h.to_bytes().to_vec())
.unwrap_or_else(|_| vec![0u8; 32]);
let entry_info = GeyserEntryInfo {
slot: new_slot,
slot,
index: 0, // Single entry per block
num_hashes: 1,
hash: entry_hash,
Expand Down