diff --git a/crates/core/src/surfnet/locker.rs b/crates/core/src/surfnet/locker.rs index b2c04f85..dc903e29 100644 --- a/crates/core/src/surfnet/locker.rs +++ b/crates/core/src/surfnet/locker.rs @@ -1603,18 +1603,32 @@ impl SurfnetSvmLocker { .map(|p| svm_writer.inner.get_account_no_db(p)) .collect::>>(); 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()), ) @@ -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")] diff --git a/crates/core/src/surfnet/svm.rs b/crates/core/src/surfnet/svm.rs index bf72704a..76063185 100644 --- a/crates/core/src/surfnet/svm.rs +++ b/crates/core/src/surfnet/svm.rs @@ -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, }, }; @@ -1714,13 +1714,11 @@ impl SurfnetSvm { /// /// # Returns /// `Ok(Vec)` with confirmed signatures, or `Err(SurfpoolError)` on error. - fn confirm_transactions(&mut self) -> Result<(Vec, HashSet), SurfpoolError> { + fn confirm_transactions(&mut self) -> Result, 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() { @@ -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); @@ -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. @@ -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; @@ -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), @@ -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,