diff --git a/Cargo.lock b/Cargo.lock index 74ebc9d81659e3..7e3f2f0bba6e54 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -7723,6 +7723,7 @@ dependencies = [ "solana-builtins-default-costs", "solana-compute-budget", "solana-program", + "solana-pubkey", "solana-sdk", "solana-svm-transaction", "thiserror", diff --git a/programs/sbf/Cargo.lock b/programs/sbf/Cargo.lock index 29c585ff6ac4ec..5b307630646440 100644 --- a/programs/sbf/Cargo.lock +++ b/programs/sbf/Cargo.lock @@ -6050,6 +6050,7 @@ dependencies = [ "log", "solana-builtins-default-costs", "solana-compute-budget", + "solana-pubkey", "solana-sdk", "solana-svm-transaction", "thiserror", diff --git a/runtime-transaction/Cargo.toml b/runtime-transaction/Cargo.toml index 772a9e28af93df..9ccb325593da89 100644 --- a/runtime-transaction/Cargo.toml +++ b/runtime-transaction/Cargo.toml @@ -14,6 +14,7 @@ agave-transaction-view = { workspace = true } log = { workspace = true } solana-builtins-default-costs = { workspace = true } solana-compute-budget = { workspace = true } +solana-pubkey = { workspace = true } solana-sdk = { workspace = true } solana-svm-transaction = { workspace = true } thiserror = { workspace = true } @@ -35,5 +36,9 @@ targets = ["x86_64-unknown-linux-gnu"] name = "process_compute_budget_instructions" harness = false +[[bench]] +name = "get_signature_details" +harness = false + [lints] workspace = true diff --git a/runtime-transaction/benches/get_signature_details.rs b/runtime-transaction/benches/get_signature_details.rs new file mode 100644 index 00000000000000..3a576574909a08 --- /dev/null +++ b/runtime-transaction/benches/get_signature_details.rs @@ -0,0 +1,132 @@ +use { + criterion::{black_box, criterion_group, criterion_main, Criterion, Throughput}, + solana_runtime_transaction::signature_details::get_precompile_signature_details, + solana_sdk::{instruction::CompiledInstruction, pubkey::Pubkey}, + solana_svm_transaction::instruction::SVMInstruction, +}; + +fn bench_get_signature_details_empty(c: &mut Criterion) { + let instructions = std::iter::empty(); + + c.benchmark_group("bench_get_signature_details_empty") + .throughput(Throughput::Elements(1)) + .bench_function("0 instructions", |bencher| { + bencher.iter(|| { + let instructions = black_box(instructions.clone()); + let _ = get_precompile_signature_details(instructions); + }); + }); +} + +fn bench_get_signature_details_no_sigs_unique(c: &mut Criterion) { + let program_ids = vec![Pubkey::new_unique(); 32]; + for num_instructions in [4, 32] { + let instructions = (0..num_instructions) + .map(|i| { + let program_id = &program_ids[i]; + ( + program_id, + CompiledInstruction { + program_id_index: i as u8, + accounts: vec![], + data: vec![], + }, + ) + }) + .collect::>(); + + c.benchmark_group("bench_get_signature_details_no_sigs_unique") + .throughput(Throughput::Elements(1)) + .bench_function(format!("{num_instructions} instructions"), |bencher| { + bencher.iter(|| { + let instructions = + black_box(instructions.iter().map(|(program_id, instruction)| { + (*program_id, SVMInstruction::from(instruction)) + })); + let _ = get_precompile_signature_details(instructions); + }); + }); + } +} + +fn bench_get_signature_details_packed_sigs(c: &mut Criterion) { + let program_ids = [ + solana_sdk::secp256k1_program::id(), + solana_sdk::ed25519_program::id(), + ]; + for num_instructions in [4, 64] { + let instructions = (0..num_instructions) + .map(|i| { + let index = i % 2; + let program_id = &program_ids[index]; + ( + program_id, + CompiledInstruction { + program_id_index: index as u8, + accounts: vec![], + data: vec![4], // some dummy number of signatures + }, + ) + }) + .collect::>(); + + c.benchmark_group("bench_get_signature_details_packed_sigs") + .throughput(Throughput::Elements(1)) + .bench_function(format!("{num_instructions} instructions"), |bencher| { + bencher.iter(|| { + let instructions = + black_box(instructions.iter().map(|(program_id, instruction)| { + (*program_id, SVMInstruction::from(instruction)) + })); + let _ = get_precompile_signature_details(instructions); + }); + }); + } +} + +fn bench_get_signature_details_mixed_sigs(c: &mut Criterion) { + let program_ids = [ + solana_sdk::secp256k1_program::id(), + solana_sdk::ed25519_program::id(), + ] + .into_iter() + .chain((0..6).map(|_| Pubkey::new_unique())) + .collect::>(); + for num_instructions in [4, 64] { + let instructions = (0..num_instructions) + .map(|i| { + let index = i % 8; + let program_id = &program_ids[index]; + ( + program_id, + CompiledInstruction { + program_id_index: index as u8, + accounts: vec![], + data: vec![4], // some dummy number of signatures + }, + ) + }) + .collect::>(); + + c.benchmark_group("bench_get_signature_details_mixed_sigs") + .throughput(Throughput::Elements(1)) + .bench_function(format!("{num_instructions} instructions"), |bencher| { + bencher.iter(|| { + let instructions = + black_box(instructions.iter().map(|(program_id, instruction)| { + (*program_id, SVMInstruction::from(instruction)) + })); + let _ = get_precompile_signature_details(instructions); + }); + }); + } +} + +criterion_group!( + benches, + bench_get_signature_details_empty, + bench_get_signature_details_no_sigs_unique, + bench_get_signature_details_packed_sigs, + bench_get_signature_details_mixed_sigs +); +criterion_main!(benches); diff --git a/runtime-transaction/src/lib.rs b/runtime-transaction/src/lib.rs index 9b79a97d40e874..40c31d4b4d653a 100644 --- a/runtime-transaction/src/lib.rs +++ b/runtime-transaction/src/lib.rs @@ -5,4 +5,5 @@ mod compute_budget_instruction_details; mod compute_budget_program_id_filter; pub mod instructions_processor; pub mod runtime_transaction; +pub mod signature_details; pub mod transaction_meta; diff --git a/runtime-transaction/src/signature_details.rs b/runtime-transaction/src/signature_details.rs new file mode 100644 index 00000000000000..8972873116a236 --- /dev/null +++ b/runtime-transaction/src/signature_details.rs @@ -0,0 +1,173 @@ +// static account keys has max +use { + agave_transaction_view::static_account_keys_frame::MAX_STATIC_ACCOUNTS_PER_PACKET as FILTER_SIZE, + solana_pubkey::Pubkey, solana_svm_transaction::instruction::SVMInstruction, +}; + +pub struct PrecompileSignatureDetails { + pub num_secp256k1_instruction_signatures: u64, + pub num_ed25519_instruction_signatures: u64, +} + +/// Get transaction signature details. +pub fn get_precompile_signature_details<'a>( + instructions: impl Iterator)>, +) -> PrecompileSignatureDetails { + let mut filter = SignatureDetailsFilter::new(); + + // Wrapping arithmetic is safe below because the maximum number of signatures + // per instruction is 255, and the maximum number of instructions per transaction + // is low enough that the sum of all signatures will not overflow a u64. + let mut num_secp256k1_instruction_signatures: u64 = 0; + let mut num_ed25519_instruction_signatures: u64 = 0; + for (program_id, instruction) in instructions { + let program_id_index = instruction.program_id_index; + match filter.is_signature(program_id_index, program_id) { + ProgramIdStatus::NotSignature => {} + ProgramIdStatus::Secp256k1 => { + num_secp256k1_instruction_signatures = num_secp256k1_instruction_signatures + .wrapping_add(get_num_signatures_in_instruction(&instruction)); + } + ProgramIdStatus::Ed25519 => { + num_ed25519_instruction_signatures = num_ed25519_instruction_signatures + .wrapping_add(get_num_signatures_in_instruction(&instruction)); + } + } + } + + PrecompileSignatureDetails { + num_secp256k1_instruction_signatures, + num_ed25519_instruction_signatures, + } +} + +#[inline] +fn get_num_signatures_in_instruction(instruction: &SVMInstruction) -> u64 { + u64::from(instruction.data.first().copied().unwrap_or(0)) +} + +#[derive(Copy, Clone)] +enum ProgramIdStatus { + NotSignature, + Secp256k1, + Ed25519, +} + +struct SignatureDetailsFilter { + // array of slots for all possible static and sanitized program_id_index, + // each slot indicates if a program_id_index has not been checked, or is + // already checked with result that can be reused. + flags: [Option; FILTER_SIZE as usize], +} + +impl SignatureDetailsFilter { + #[inline] + fn new() -> Self { + Self { + flags: [None; FILTER_SIZE as usize], + } + } + + #[inline] + fn is_signature(&mut self, index: u8, program_id: &Pubkey) -> ProgramIdStatus { + let flag = &mut self.flags[usize::from(index)]; + match flag { + Some(status) => *status, + None => { + *flag = Some(Self::check_program_id(program_id)); + *flag.as_ref().unwrap() + } + } + } + + #[inline] + fn check_program_id(program_id: &Pubkey) -> ProgramIdStatus { + if program_id == &solana_sdk::secp256k1_program::ID { + ProgramIdStatus::Secp256k1 + } else if program_id == &solana_sdk::ed25519_program::ID { + ProgramIdStatus::Ed25519 + } else { + ProgramIdStatus::NotSignature + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + // simple convenience function so avoid having inconsistent program_id and program_id_index + fn make_instruction<'a>( + program_ids: &'a [Pubkey], + program_id_index: u8, + data: &'a [u8], + ) -> (&'a Pubkey, SVMInstruction<'a>) { + ( + &program_ids[program_id_index as usize], + SVMInstruction { + program_id_index, + accounts: &[], + data, + }, + ) + } + + #[test] + fn test_get_signature_details_no_instructions() { + let instructions = std::iter::empty(); + let signature_details = get_precompile_signature_details(instructions); + + assert_eq!(signature_details.num_secp256k1_instruction_signatures, 0); + assert_eq!(signature_details.num_ed25519_instruction_signatures, 0); + } + + #[test] + fn test_get_signature_details_no_sigs_unique() { + let program_ids = [Pubkey::new_unique(), Pubkey::new_unique()]; + let instructions = [ + make_instruction(&program_ids, 0, &[]), + make_instruction(&program_ids, 1, &[]), + ]; + + let signature_details = get_precompile_signature_details(instructions.into_iter()); + assert_eq!(signature_details.num_secp256k1_instruction_signatures, 0); + assert_eq!(signature_details.num_ed25519_instruction_signatures, 0); + } + + #[test] + fn test_get_signature_details_signatures_mixed() { + let program_ids = [ + Pubkey::new_unique(), + solana_sdk::secp256k1_program::ID, + solana_sdk::ed25519_program::ID, + ]; + let instructions = [ + make_instruction(&program_ids, 1, &[5]), + make_instruction(&program_ids, 2, &[3]), + make_instruction(&program_ids, 0, &[]), + make_instruction(&program_ids, 2, &[2]), + make_instruction(&program_ids, 1, &[1]), + make_instruction(&program_ids, 0, &[]), + ]; + + let signature_details = get_precompile_signature_details(instructions.into_iter()); + assert_eq!(signature_details.num_secp256k1_instruction_signatures, 6); + assert_eq!(signature_details.num_ed25519_instruction_signatures, 5); + } + + #[test] + fn test_get_signature_details_missing_num_signatures() { + let program_ids = [ + solana_sdk::secp256k1_program::ID, + solana_sdk::ed25519_program::ID, + ]; + let instructions = [ + make_instruction(&program_ids, 0, &[]), + make_instruction(&program_ids, 1, &[]), + ]; + + let signature_details = get_precompile_signature_details(instructions.into_iter()); + assert_eq!(signature_details.num_secp256k1_instruction_signatures, 0); + assert_eq!(signature_details.num_ed25519_instruction_signatures, 0); + } +} diff --git a/sdk/program/src/message/sanitized.rs b/sdk/program/src/message/sanitized.rs index 33b8c3fc3495d7..1f9c6a08187fcf 100644 --- a/sdk/program/src/message/sanitized.rs +++ b/sdk/program/src/message/sanitized.rs @@ -433,6 +433,18 @@ pub struct TransactionSignatureDetails { } impl TransactionSignatureDetails { + pub fn new( + num_transaction_signatures: u64, + num_secp256k1_instruction_signatures: u64, + num_ed25519_instruction_signatures: u64, + ) -> Self { + Self { + num_transaction_signatures, + num_secp256k1_instruction_signatures, + num_ed25519_instruction_signatures, + } + } + /// return total number of signature, treating pre-processor operations as signature pub(crate) fn total_signatures(&self) -> u64 { self.num_transaction_signatures