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
2 changes: 2 additions & 0 deletions Cargo.lock

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

1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,7 @@ solana-instruction = { version = "3.0.0", default-features = false }
solana-keypair = { version = "3.0.0", default-features = false }
solana-loader-v3-interface = { version = "6.1.0", default-features = false }
solana-message = { version = "3.0.0", default-features = false }
solana-nonce = { version = "3.0.0", default-features = false }
solana-packet = { version = "3.0.0", default-features = false }
solana-program-option = { version = "3.0.0", default-features = false }
solana-program-pack = { version = "3.0.0", default-features = false }
Expand Down
2 changes: 2 additions & 0 deletions crates/core/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ solana-instruction = { workspace = true }
solana-keypair = { workspace = true }
solana-loader-v3-interface = { workspace = true }
solana-message = { workspace = true }
solana-nonce = { workspace = true }
solana-packet = { workspace = true }
solana-program-option = { workspace = true }
solana-program-pack = { workspace = true }
Expand Down Expand Up @@ -97,6 +98,7 @@ txtx-addon-network-svm = { workspace = true }

[dev-dependencies]
test-case = { workspace = true }
env_logger = "*"

[features]
ignore_tests_ci = []
Expand Down
85 changes: 81 additions & 4 deletions crates/core/src/surfnet/svm.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,9 @@ use solana_hash::Hash;
use solana_inflation::Inflation;
use solana_keypair::Keypair;
use solana_loader_v3_interface::state::UpgradeableLoaderState;
use solana_message::{Message, VersionedMessage, v0::LoadedAddresses};
use solana_message::{
Message, VersionedMessage, inline_nonce::is_advance_nonce_instruction_data, v0::LoadedAddresses,
};
use solana_program_option::COption;
use solana_pubkey::Pubkey;
use solana_rpc_client_api::response::SlotInfo;
Expand Down Expand Up @@ -761,6 +763,81 @@ impl SurfnetSvm {
.any(|entry| entry.blockhash == *recent_blockhash)
}

/// Validates the blockhash of a transaction, considering nonce accounts if present.
/// If the transaction uses a nonce account, the blockhash is validated against the nonce account's stored blockhash.
/// Otherwise, it is validated against the RecentBlockhashes sysvar.
///
/// # Arguments
/// * `tx` - The transaction to validate.
///
/// # Returns
/// `true` if the transaction blockhash is valid, `false` otherwise.
pub fn validate_transaction_blockhash(&self, tx: &VersionedTransaction) -> bool {
let recent_blockhash = tx.message.recent_blockhash();

let some_nonce_account_index = tx
.message
.instructions()
.get(solana_nonce::NONCED_TX_MARKER_IX_INDEX as usize)
.filter(|instruction| {
matches!(
tx.message.static_account_keys().get(instruction.program_id_index as usize),
Some(program_id) if system_program::check_id(program_id)
) && is_advance_nonce_instruction_data(&instruction.data)
})
.map(|instruction| {
// nonce account is the first account in the instruction
instruction.accounts.get(0)
});

debug!(
"Validating tx blockhash: {}; is nonce tx?: {}",
recent_blockhash,
some_nonce_account_index.is_some()
);

if let Some(nonce_account_index) = some_nonce_account_index {
trace!(
"Nonce tx detected. Nonce account index: {:?}",
nonce_account_index
);
let Some(nonce_account_index) = nonce_account_index else {
return false;
};

let Some(nonce_account_pubkey) = tx
.message
.static_account_keys()
.get(*nonce_account_index as usize)
else {
return false;
};

trace!("Nonce account pubkey: {:?}", nonce_account_pubkey,);

let Some(nonce_account) = self.get_account(nonce_account_pubkey) else {
return false;
};
trace!("Nonce account: {:?}", nonce_account);

let Some(nonce_data) =
bincode::deserialize::<solana_nonce::versions::Versions>(&nonce_account.data).ok()
else {
return false;
};
trace!("Nonce account data: {:?}", nonce_data);

let nonce_state = nonce_data.state();
let initialized_state = match nonce_state {
solana_nonce::state::State::Uninitialized => return false,
solana_nonce::state::State::Initialized(data) => data,
};
return initialized_state.blockhash() == *recent_blockhash;
} else {
self.check_blockhash_is_recent(recent_blockhash)
}
}

/// Sets an account in the local SVM state and notifies listeners.
///
/// # Arguments
Expand Down Expand Up @@ -1056,7 +1133,7 @@ impl SurfnetSvm {
}
self.transactions_processed += 1;

if !self.check_blockhash_is_recent(tx.message.recent_blockhash()) {
if !self.validate_transaction_blockhash(&tx) {
let meta = TransactionMetadata::default();
let err = solana_transaction_error::TransactionError::BlockhashNotFound;

Expand Down Expand Up @@ -1101,7 +1178,7 @@ impl SurfnetSvm {
&self,
transaction: &VersionedTransaction,
) -> ComputeUnitsEstimationResult {
if !self.check_blockhash_is_recent(transaction.message.recent_blockhash()) {
if !self.validate_transaction_blockhash(transaction) {
return ComputeUnitsEstimationResult {
success: false,
compute_units_consumed: 0,
Expand Down Expand Up @@ -1148,7 +1225,7 @@ impl SurfnetSvm {
});
}

if !self.check_blockhash_is_recent(tx.message.recent_blockhash()) {
if !self.validate_transaction_blockhash(&tx) {
let meta = TransactionMetadata::default();
let err = TransactionError::BlockhashNotFound;

Expand Down
117 changes: 116 additions & 1 deletion crates/core/src/tests/integration.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use std::{str::FromStr, sync::Arc, time::Duration};
use std::{str::FromStr, sync::Arc, thread::sleep, time::Duration};

use base64::Engine;
use crossbeam_channel::{unbounded, unbounded as crossbeam_unbounded};
Expand Down Expand Up @@ -6269,3 +6269,118 @@ async fn test_token2022_freeze_thaw() {

println!("✓ All freeze/thaw operations work correctly!");
}

use std::sync::Once;

static INIT_LOGGER: Once = Once::new();

fn setup() {
INIT_LOGGER.call_once(|| {
env_logger::builder().is_test(true).try_init().unwrap();
});
}

#[test]
fn test_nonce_accounts() {
setup();
use solana_system_interface::instruction::create_nonce_account;

let (svm_instance, _simnet_events_rx, _geyser_events_rx) = SurfnetSvm::new();
let svm_locker = SurfnetSvmLocker::new(svm_instance);

let payer = Keypair::new();
let nonce_account = Keypair::new();
println!("Payer Pubkey: {}", payer.pubkey());
println!("Nonce Account Pubkey: {}", nonce_account.pubkey());
println!("Nonce authority: {}", payer.pubkey());

svm_locker
.airdrop(&payer.pubkey(), 5 * LAMPORTS_PER_SOL)
.unwrap();

let nonce_rent = svm_locker.with_svm_reader(|svm_reader| {
svm_reader
.inner
.minimum_balance_for_rent_exemption(solana_nonce::state::State::size())
});
let create_nonce_ix = create_nonce_account(
&payer.pubkey(),
&nonce_account.pubkey(),
&payer.pubkey(), // Make the fee payer the nonce account authority
nonce_rent,
);

let recent_blockhash = svm_locker.latest_absolute_blockhash();

let create_nonce_msg =
Message::new_with_blockhash(&create_nonce_ix, Some(&payer.pubkey()), &recent_blockhash);
let create_nonce_tx = VersionedTransaction::try_new(
VersionedMessage::Legacy(create_nonce_msg),
&[&payer, &nonce_account],
)
.unwrap();

let create_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(create_nonce_tx, false, false));
assert!(
create_result.is_ok(),
"Create nonce account failed: {:?}",
create_result.err()
);

// Fetch and verify nonce account state
let nonce_account_data = svm_locker
.get_account_local(&nonce_account.pubkey())
.inner
.map_account()
.expect("Failed to fetch nonce account");

let state: solana_nonce::versions::Versions = bincode::deserialize(&nonce_account_data.data)
.expect("Failed to deserialize nonce account state");

let state = state.state();

let nonce_hash = match state {
solana_nonce::state::State::Initialized(nonce_data) => {
println!(
"✓ Nonce account initialized with nonce: {:?}",
nonce_data.durable_nonce
);
nonce_data.blockhash()
}
_ => panic!("Nonce account is not initialized"),
};

let to_pubkey = Pubkey::new_unique();
// Use the nonce in a transaction
let transfer_ix =
solana_system_interface::instruction::transfer(&payer.pubkey(), &to_pubkey, 1_000_000);
let mut nonce_msg = Message::new_with_nonce(
vec![transfer_ix],
Some(&payer.pubkey()),
&nonce_account.pubkey(),
&payer.pubkey(),
);
nonce_msg.recent_blockhash = nonce_hash;
let nonce_tx =
VersionedTransaction::try_new(VersionedMessage::Legacy(nonce_msg), &[&payer]).unwrap();

let nonce_result =
svm_locker.with_svm_writer(|svm| svm.send_transaction(nonce_tx, false, false));
assert!(
nonce_result.is_ok(),
"Transaction using nonce failed: {:?}",
nonce_result.err()
);

// Verify to_pubkey received the funds
let to_account_data = svm_locker
.get_account_local(&to_pubkey)
.inner
.map_account()
.expect("Failed to fetch recipient account");
assert_eq!(
to_account_data.lamports, 1_000_000,
"Recipient account did not receive correct amount"
);
}