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
1 change: 1 addition & 0 deletions crates/cli/src/cli/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -395,6 +395,7 @@ impl StartSimnet {
Some(self.log_bytes_limit)
},
feature_config: self.feature_config(),
skip_signature_verification: false,
}
}

Expand Down
195 changes: 190 additions & 5 deletions crates/core/src/rpc/full.rs
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,15 @@ use crate::{

const MAX_PRIORITIZATION_FEE_BLOCKS_CACHE: usize = 150;

#[derive(Debug, Clone, Default, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct SurfpoolRpcSendTransactionConfig {
#[serde(flatten)]
pub base: RpcSendTransactionConfig,
/// skip sign verification for this txn (overrides global config)
pub skip_sig_verify: Option<bool>,
}

#[rpc]
pub trait Full {
type Metadata;
Expand Down Expand Up @@ -511,7 +520,7 @@ pub trait Full {
&self,
meta: Self::Metadata,
data: String,
config: Option<RpcSendTransactionConfig>,
config: Option<SurfpoolRpcSendTransactionConfig>,
) -> Result<String>;

/// Simulates a transaction without sending it to the network.
Expand Down Expand Up @@ -1535,10 +1544,13 @@ impl Full for SurfpoolFullRpc {
&self,
meta: Self::Metadata,
data: String,
config: Option<RpcSendTransactionConfig>,
config: Option<SurfpoolRpcSendTransactionConfig>,
) -> Result<String> {
let config = config.unwrap_or_default();
let tx_encoding = config.encoding.unwrap_or(UiTransactionEncoding::Base58);
let tx_encoding = config
.base
.encoding
.unwrap_or(UiTransactionEncoding::Base58);
let binary_encoding = tx_encoding.into_binary_encoding().ok_or_else(|| {
Error::invalid_params(format!(
"unsupported encoding: {tx_encoding}. Supported encodings: base58, base64"
Expand All @@ -1563,7 +1575,8 @@ impl Full for SurfpoolFullRpc {
ctx.id,
unsanitized_tx,
status_update_tx,
config.skip_preflight,
config.base.skip_preflight,
config.skip_sig_verify,
))
.map_err(|_| RpcCustomError::NodeUnhealthy {
num_slots_behind: None,
Expand Down Expand Up @@ -2495,7 +2508,7 @@ mod tests {
.unwrap();
loop {
match mempool_rx.recv() {
Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _)) => {
Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
let mut writer = setup.context.svm_locker.0.write().await;
let slot = writer.get_latest_absolute_slot();
writer.transactions_queued_for_confirmation.push_back((
Expand Down Expand Up @@ -4447,4 +4460,176 @@ mod tests {
})
)
}

/// tests for skip_sig_verify feature
mod test_skip_sig_verify {
use solana_client::rpc_config::RpcSendTransactionConfig;
use solana_signature::Signature;

use super::*;

fn build_transaction_with_invalid_signature(
payer: &Keypair,
recipient: &Pubkey,
recent_blockhash: &Hash,
) -> VersionedTransaction {
let msg = VersionedMessage::Legacy(LegacyMessage::new_with_blockhash(
&[system_instruction::transfer(
&payer.pubkey(),
recipient,
LAMPORTS_PER_SOL,
)],
Some(&payer.pubkey()),
recent_blockhash,
));

VersionedTransaction {
signatures: vec![Signature::new_unique()],
message: msg,
}
}

#[tokio::test(flavor = "multi_thread")]
async fn test_send_transaction_with_skip_sig_verify_succeeds() {
let payer = Keypair::new();
let recipient = Pubkey::new_unique();
let (mempool_tx, mempool_rx) = crossbeam_channel::unbounded();
let setup = TestSetup::new_with_mempool(SurfpoolFullRpc, mempool_tx);
let recent_blockhash = setup
.context
.svm_locker
.with_svm_reader(|svm_reader| svm_reader.latest_blockhash());

let _ = setup
.context
.svm_locker
.0
.write()
.await
.airdrop(&payer.pubkey(), 2 * LAMPORTS_PER_SOL);

let tx =
build_transaction_with_invalid_signature(&payer, &recipient, &recent_blockhash);
let tx_encoded = bs58::encode(bincode::serialize(&tx).unwrap()).into_string();

let config = SurfpoolRpcSendTransactionConfig {
base: RpcSendTransactionConfig::default(),
skip_sig_verify: Some(true),
};

let setup_clone = setup.clone();
let handle = hiro_system_kit::thread_named("send_tx_skip_verify")
.spawn(move || {
setup_clone.rpc.send_transaction(
Some(setup_clone.context),
tx_encoded,
Some(config),
)
})
.unwrap();

loop {
match mempool_rx.recv() {
Ok(SimnetCommand::ProcessTransaction(_, tx, status_tx, _, _)) => {
let mut writer = setup.context.svm_locker.0.write().await;
let slot = writer.get_latest_absolute_slot();
writer.transactions_queued_for_confirmation.push_back((
tx.clone(),
status_tx.clone(),
None,
));
let sig = tx.signatures[0];
let tx_with_status_meta = TransactionWithStatusMeta {
slot,
transaction: tx,
..Default::default()
};
let mutated_accounts = std::collections::HashSet::new();
writer.transactions.insert(
sig,
SurfnetTransactionStatus::processed(
tx_with_status_meta,
mutated_accounts,
),
);
status_tx
.send(TransactionStatusEvent::Success(
TransactionConfirmationStatus::Processed,
))
.unwrap();
break;
}
_ => continue,
}
}

let result = handle.join().unwrap();
assert!(
result.is_ok(),
"Transaction with skip_sig_verify=true should succeed: {:?}",
result
);
}

#[test]
fn test_surfpool_rpc_send_transaction_config_json_serialization() {
// Test that the config serializes correctly with serde flatten
let config = SurfpoolRpcSendTransactionConfig {
base: RpcSendTransactionConfig {
skip_preflight: true,
..Default::default()
},
skip_sig_verify: Some(true),
};

let json = serde_json::to_string(&config).unwrap();
assert!(json.contains("skipSigVerify"));
assert!(json.contains("skipPreflight"));

// Verify it can be deserialized back
let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(&json).unwrap();
assert_eq!(parsed.skip_sig_verify, Some(true));
assert!(parsed.base.skip_preflight);
}

#[test]
fn test_surfpool_rpc_send_transaction_config_backwards_compatible() {
// Test that a standard Solana RPC config can be parsed (skip_sig_verify absent)
let json = r#"{"skipPreflight": true}"#;
let parsed: SurfpoolRpcSendTransactionConfig = serde_json::from_str(json).unwrap();
assert!(parsed.base.skip_preflight);
assert!(
parsed.skip_sig_verify.is_none(),
"skip_sig_verify should be None when not provided"
);
}

#[test]
fn test_surfpool_rpc_send_transaction_config_defaults() {
let config = SurfpoolRpcSendTransactionConfig::default();
assert!(
config.skip_sig_verify.is_none(),
"skip_sig_verify should default to None"
);
assert!(
!config.base.skip_preflight,
"skip_preflight should default to false"
);
}

#[test]
fn test_surfpool_rpc_send_transaction_config_with_skip_sig_verify() {
let config = SurfpoolRpcSendTransactionConfig {
base: RpcSendTransactionConfig::default(),
skip_sig_verify: Some(true),
};
assert_eq!(config.skip_sig_verify, Some(true));

let config_false = SurfpoolRpcSendTransactionConfig {
base: RpcSendTransactionConfig::default(),
skip_sig_verify: Some(false),
};
assert_eq!(config_false.skip_sig_verify, Some(false));
}
}
}
11 changes: 8 additions & 3 deletions crates/core/src/runloops/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ use solana_transaction::sanitized::{MessageHash, SanitizedTransaction};
use surfpool_subgraph::SurfpoolSubgraphPlugin;
use surfpool_types::{
BlockProductionMode, ClockCommand, ClockEvent, DEFAULT_RPC_URL, DataIndexingCommand,
SimnetCommand, SimnetEvent, SubgraphCommand, SubgraphPluginConfig, SurfpoolConfig,
SimnetCommand, SimnetConfig, SimnetEvent, SubgraphCommand, SubgraphPluginConfig,
SurfpoolConfig,
};
type PluginConstructor = unsafe fn() -> *mut dyn GeyserPlugin;
use txtx_addon_kit::helpers::fs::FileLocation;
Expand Down Expand Up @@ -148,6 +149,7 @@ pub async fn start_local_surfnet_runloop(
block_production_mode,
&remote_rpc_client,
simnet_config.expiry.map(|e| e * 1000),
&simnet_config,
)
.await
}
Expand All @@ -162,6 +164,7 @@ pub async fn start_block_production_runloop(
mut block_production_mode: BlockProductionMode,
remote_rpc_client: &Option<SurfnetRemoteClient>,
expiry_duration_ms: Option<u64>,
simnet_config: &SimnetConfig,
) -> Result<(), Box<dyn std::error::Error>> {
let remote_client_with_commitment = remote_rpc_client.as_ref().map(|c| {
(
Expand All @@ -171,7 +174,7 @@ pub async fn start_block_production_runloop(
});
let mut next_scheduled_expiry_check: Option<u64> =
expiry_duration_ms.map(|expiry_val| Utc::now().timestamp_millis() as u64 + expiry_val);
let sigverify = true; // always verify signatures during block production
let global_skip_sig_verify = simnet_config.skip_signature_verification;
loop {
let mut do_produce_block = false;

Expand Down Expand Up @@ -306,7 +309,9 @@ pub async fn start_block_production_runloop(
block_production_mode = update;
continue
}
SimnetCommand::ProcessTransaction(_key, transaction, status_tx, skip_preflight) => {
SimnetCommand::ProcessTransaction(_key, transaction, status_tx, skip_preflight, skip_sig_verify_override) => {
let skip_sig_verify = skip_sig_verify_override.unwrap_or(global_skip_sig_verify);
let sigverify = !skip_sig_verify;
if let Err(e) = svm_locker.process_transaction(&remote_client_with_commitment, transaction, status_tx, skip_preflight, sigverify).await {
let _ = svm_locker.simnet_events_tx().send(SimnetEvent::error(format!("Failed to process transaction: {}", e)));
}
Expand Down
3 changes: 3 additions & 0 deletions crates/types/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -491,6 +491,7 @@ pub enum SimnetCommand {
VersionedTransaction,
Sender<TransactionStatusEvent>,
bool,
Option<bool>,
),
Terminate(Option<(Hash, String)>),
StartRunbookExecution(String),
Expand Down Expand Up @@ -547,6 +548,7 @@ pub struct SimnetConfig {
pub max_profiles: usize,
pub log_bytes_limit: Option<usize>,
pub feature_config: SvmFeatureConfig,
pub skip_signature_verification: bool,
}

impl Default for SimnetConfig {
Expand All @@ -563,6 +565,7 @@ impl Default for SimnetConfig {
max_profiles: DEFAULT_PROFILING_MAP_CAPACITY,
log_bytes_limit: Some(10_000),
feature_config: SvmFeatureConfig::default(),
skip_signature_verification: false,
}
}
}
Expand Down