use {
crate::poh::Poh,
crossbeam_channel::{Receiver, Sender},
dlopen2::symbor::{Container, SymBorApi, Symbol},
log::*,
rayon::{ThreadPool, prelude::*},
serde::{Deserialize, Serialize},
smallvec::SmallVec,
solana_address::Address,
solana_hash::Hash,
solana_merkle_tree::MerkleTree,
solana_runtime_transaction::transaction_with_meta::TransactionWithMeta,
solana_signature::Signature,
solana_transaction::{Transaction, versioned::VersionedTransaction},
solana_transaction_error::{TransactionError, TransactionResult as Result},
std::{
ffi::OsStr,
iter::repeat_with,
sync::{Once, OnceLock},
time::Instant,
},
wincode::{SchemaRead, SchemaWrite, containers::Vec as WincodeVec, len::BincodeLen},
};
pub type EntrySender = Sender<Vec<Entry>>;
pub type EntryReceiver = Receiver<Vec<Entry>>;
static API: OnceLock<Container<Api>> = OnceLock::new();
pub fn init_poh() {
init(OsStr::new("libpoh-simd.so"));
}
fn init(name: &OsStr) {
static INIT_HOOK: Once = Once::new();
info!("Loading {name:?}");
INIT_HOOK.call_once(|| {
let path;
let lib_name = if let Some(perf_libs_path) = solana_perf::perf_libs::locate_perf_libs() {
solana_perf::perf_libs::append_to_ld_library_path(
perf_libs_path.to_str().unwrap_or("").to_string(),
);
path = perf_libs_path.join(name);
path.as_os_str()
} else {
name
};
match unsafe { Container::load(lib_name) } {
Ok(api) => _ = API.set(api),
Err(err) => error!("Unable to load {lib_name:?}: {err}"),
}
})
}
pub fn api() -> Option<&'static Container<Api<'static>>> {
{
static INIT_HOOK: Once = Once::new();
INIT_HOOK.call_once(|| {
if std::env::var("TEST_PERF_LIBS").is_ok() {
init_poh()
}
});
}
API.get()
}
#[derive(SymBorApi)]
pub struct Api<'a> {
pub poh_verify_many_simd_avx512skx:
Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>,
pub poh_verify_many_simd_avx2:
Symbol<'a, unsafe extern "C" fn(hashes: *mut u8, num_hashes: *const u64)>,
}
const MAX_DATA_SHREDS_PER_SLOT: usize = 32_768;
pub const MAX_DATA_SHREDS_SIZE: usize = MAX_DATA_SHREDS_PER_SLOT * solana_packet::PACKET_DATA_SIZE;
pub type MaxDataShredsLen = BincodeLen<MAX_DATA_SHREDS_SIZE>;
#[derive(Serialize, Deserialize, Debug, Default, PartialEq, Eq, Clone, SchemaWrite, SchemaRead)]
pub struct Entry {
pub num_hashes: u64,
pub hash: Hash,
#[wincode(with = "WincodeVec<crate::wincode::VersionedTransaction, MaxDataShredsLen>")]
pub transactions: Vec<VersionedTransaction>,
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct EntryVerificationData {
pub num_hashes: u64,
pub hash: Hash,
pub num_transactions: usize,
pub signatures: Vec<Signature>,
}
impl From<&Entry> for EntryVerificationData {
fn from(entry: &Entry) -> Self {
Self {
num_hashes: entry.num_hashes,
hash: entry.hash,
num_transactions: entry.transactions.len(),
signatures: entry
.transactions
.iter()
.flat_map(|tx| tx.signatures.iter().copied())
.collect(),
}
}
}
impl EntryVerificationData {
pub fn verify(&self, start_hash: &Hash) -> bool {
let ref_hash = next_hash_with_signatures(
start_hash,
self.num_hashes,
self.num_transactions,
&self.signatures,
);
if self.hash != ref_hash {
warn!(
"next_hash is invalid expected: {:?} actual: {:?}",
self.hash, ref_hash
);
return false;
}
true
}
}
pub fn entries_to_verification_data(entries: &[Entry]) -> Vec<EntryVerificationData> {
entries.iter().map(Into::into).collect()
}
pub struct EntrySummary {
pub num_hashes: u64,
pub hash: Hash,
pub num_transactions: u64,
}
impl From<&Entry> for EntrySummary {
fn from(entry: &Entry) -> Self {
Self {
num_hashes: entry.num_hashes,
hash: entry.hash,
num_transactions: entry.transactions.len() as u64,
}
}
}
pub enum EntryType<Tx: TransactionWithMeta> {
Transactions(Vec<Tx>),
Tick(Hash),
}
#[derive(Clone, Debug, PartialEq, Eq)]
struct TxVerificationData {
is_simple_vote: bool,
signatures: SmallVec<[Signature; 2]>,
signer_pubkeys: SmallVec<[Address; 2]>,
serialized_message: Vec<u8>,
}
pub struct UnverifiedSignatures {
signatures: Vec<TxVerificationData>,
}
impl UnverifiedSignatures {
fn with_capacity(capacity: usize) -> Self {
Self {
signatures: Vec::with_capacity(capacity),
}
}
pub fn verify(&self) -> Result<()> {
self.signatures.par_iter().try_for_each(|tx_signatures| {
if tx_signatures
.signatures
.iter()
.zip(tx_signatures.signer_pubkeys.iter())
.all(|(signature, pubkey)| {
signature.verify(pubkey.as_ref(), &tx_signatures.serialized_message)
})
{
Ok(())
} else {
Err(TransactionError::SignatureFailure)
}
})
}
pub fn vote_transaction_signatures(&self) -> Vec<Signature> {
self.signatures
.iter()
.filter(|tx_signatures| tx_signatures.is_simple_vote)
.filter_map(|tx_signatures| tx_signatures.signatures.first().copied())
.collect()
}
}
pub struct ValidatedHashedTransactions<Tx: TransactionWithMeta> {
pub entries: Vec<EntryType<Tx>>,
pub unverified_signatures: UnverifiedSignatures,
}
impl Entry {
pub fn new(prev_hash: &Hash, mut num_hashes: u64, transactions: Vec<Transaction>) -> Self {
if num_hashes == 0 && !transactions.is_empty() {
num_hashes = 1;
}
let transactions = transactions.into_iter().map(Into::into).collect::<Vec<_>>();
let hash = next_hash(prev_hash, num_hashes, &transactions);
Entry {
num_hashes,
hash,
transactions,
}
}
pub fn new_mut(
start_hash: &mut Hash,
num_hashes: &mut u64,
transactions: Vec<Transaction>,
) -> Self {
let entry = Self::new(start_hash, *num_hashes, transactions);
*start_hash = entry.hash;
*num_hashes = 0;
entry
}
#[cfg(test)]
pub fn new_tick(num_hashes: u64, hash: &Hash) -> Self {
Entry {
num_hashes,
hash: *hash,
transactions: vec![],
}
}
pub fn verify(&self, start_hash: &Hash) -> bool {
EntryVerificationData::from(self).verify(start_hash)
}
pub fn is_tick(&self) -> bool {
self.transactions.is_empty()
}
}
pub fn hash_signatures(signatures: &[impl AsRef<[u8]>]) -> Hash {
let merkle_tree = MerkleTree::new(signatures);
if let Some(root_hash) = merkle_tree.get_root() {
*root_hash
} else {
Hash::default()
}
}
pub fn hash_transactions(transactions: &[VersionedTransaction]) -> Hash {
let signatures: Vec<_> = transactions
.iter()
.flat_map(|tx| tx.signatures.iter())
.collect();
hash_signatures(&signatures)
}
fn next_hash_with_signatures(
start_hash: &Hash,
num_hashes: u64,
num_transactions: usize,
signatures: &[Signature],
) -> Hash {
if num_hashes == 0 && num_transactions == 0 {
return *start_hash;
}
let mut poh = Poh::new(*start_hash, None);
poh.hash(num_hashes.saturating_sub(1));
if num_transactions == 0 {
poh.tick().unwrap().hash
} else {
poh.record(hash_signatures(signatures)).unwrap().hash
}
}
pub fn next_hash(
start_hash: &Hash,
num_hashes: u64,
transactions: &[VersionedTransaction],
) -> Hash {
let signatures: Vec<_> = transactions
.iter()
.flat_map(|tx| tx.signatures.iter().copied())
.collect();
next_hash_with_signatures(start_hash, num_hashes, transactions.len(), &signatures)
}
pub struct EntryVerificationState {
verification_status: bool,
poh_duration_us: u64,
}
impl EntryVerificationState {
pub fn status(&self) -> bool {
self.verification_status
}
pub fn poh_duration_us(&self) -> u64 {
self.poh_duration_us
}
}
fn validate_and_hash_entry_transactions<Tx: TransactionWithMeta, F>(
entry: Entry,
verify: &F,
unverified_signatures: &mut UnverifiedSignatures,
) -> Result<EntryType<Tx>>
where
F: Fn(VersionedTransaction, &[u8]) -> Result<Tx>,
{
if entry.transactions.is_empty() {
return Ok(EntryType::Tick(entry.hash));
}
let verified_transactions = entry
.transactions
.into_iter()
.map(|versioned_tx| {
let num_signers = usize::from(versioned_tx.message.header().num_required_signatures);
let static_account_keys = versioned_tx.message.static_account_keys();
if static_account_keys.len() < num_signers {
return Err(TransactionError::SanitizeFailure);
}
let signatures = versioned_tx.signatures.iter().copied().collect();
let signer_pubkeys = static_account_keys[..num_signers].iter().copied().collect();
let serialized_message = versioned_tx.message.serialize();
let verified_transaction = verify(versioned_tx, &serialized_message)?;
unverified_signatures.signatures.push(TxVerificationData {
is_simple_vote: verified_transaction.is_simple_vote_transaction(),
signatures,
serialized_message,
signer_pubkeys,
});
Ok(verified_transaction)
})
.collect::<Result<Vec<_>>>()?;
Ok(EntryType::Transactions(verified_transactions))
}
pub fn validate_and_hash_transactions<Tx: TransactionWithMeta + Send + Sync, F>(
entries: Vec<Entry>,
num_txs: usize,
thread_pool: &ThreadPool,
verify: F,
) -> Result<ValidatedHashedTransactions<Tx>>
where
F: Fn(VersionedTransaction, &[u8]) -> Result<Tx> + Send + Sync,
{
const PARALLEL_VERIFY_THRESHOLD: usize = 200;
if num_txs < PARALLEL_VERIFY_THRESHOLD {
let mut unverified_signatures = UnverifiedSignatures::with_capacity(num_txs);
let entries = entries
.into_iter()
.map(|entry| {
validate_and_hash_entry_transactions(entry, &verify, &mut unverified_signatures)
})
.collect::<Result<_>>()?;
return Ok(ValidatedHashedTransactions {
entries,
unverified_signatures,
});
}
let verified = thread_pool.install(|| {
entries
.into_par_iter()
.map(|entry| {
let mut unverified_signatures =
UnverifiedSignatures::with_capacity(entry.transactions.len());
let verified_entry = validate_and_hash_entry_transactions(
entry,
&verify,
&mut unverified_signatures,
)?;
Ok((verified_entry, unverified_signatures))
})
.collect::<Result<Vec<_>>>()
})?;
let mut entries = Vec::with_capacity(verified.len());
let mut unverified_signatures = UnverifiedSignatures::with_capacity(num_txs);
for (entry, mut tx_unverified_signatures) in verified {
entries.push(entry);
unverified_signatures
.signatures
.append(&mut tx_unverified_signatures.signatures);
}
Ok(ValidatedHashedTransactions {
entries,
unverified_signatures,
})
}
fn compare_hashes(computed_hash: Hash, ref_entry: &EntryVerificationData) -> bool {
let actual = if ref_entry.num_transactions != 0 {
let tx_hash = hash_signatures(&ref_entry.signatures);
let mut poh = Poh::new(computed_hash, None);
poh.record(tx_hash).unwrap().hash
} else if ref_entry.num_hashes > 0 {
let mut poh = Poh::new(computed_hash, None);
poh.tick().unwrap().hash
} else {
computed_hash
};
actual == ref_entry.hash
}
pub fn verify_entries_cpu_in_pool(
entries: &[EntryVerificationData],
start_hash: &Hash,
thread_pool: &ThreadPool,
) -> EntryVerificationState {
thread_pool.install(|| verify_entries_cpu(entries, start_hash))
}
fn verify_entries_cpu_generic(
entries: &[EntryVerificationData],
start_hash: &Hash,
) -> EntryVerificationState {
let now = Instant::now();
let genesis = [EntryVerificationData {
num_hashes: 0,
hash: *start_hash,
num_transactions: 0,
signatures: Vec::new(),
}];
let entry_pairs = genesis.par_iter().chain(entries).zip(entries);
let res = entry_pairs.all(|(x0, x1)| {
let r = x1.verify(&x0.hash);
if !r {
warn!(
"entry invalid!: x0: {:?}, x1: {:?} num txs: {}",
x0.hash, x1.hash, x1.num_transactions
);
}
r
});
let poh_duration_us = now.elapsed().as_micros() as u64;
EntryVerificationState {
verification_status: res,
poh_duration_us,
}
}
fn verify_entries_cpu_x86_simd(
entries: &[EntryVerificationData],
start_hash: &Hash,
simd_len: usize,
) -> EntryVerificationState {
use solana_hash::HASH_BYTES;
let now = Instant::now();
let genesis = [EntryVerificationData {
num_hashes: 0,
hash: *start_hash,
num_transactions: 0,
signatures: Vec::new(),
}];
let aligned_len = entries.len().div_ceil(simd_len) * simd_len;
let mut hashes_bytes = vec![0u8; HASH_BYTES * aligned_len];
genesis
.iter()
.chain(entries)
.enumerate()
.for_each(|(i, entry)| {
if i < entries.len() {
let start = i * HASH_BYTES;
let end = start + HASH_BYTES;
hashes_bytes[start..end].copy_from_slice(&entry.hash.to_bytes());
}
});
let mut hashes_chunked: Vec<_> = hashes_bytes.chunks_mut(simd_len * HASH_BYTES).collect();
let mut num_hashes: Vec<u64> = entries
.iter()
.map(|entry| entry.num_hashes.saturating_sub(1))
.collect();
num_hashes.resize(aligned_len, 0);
let num_hashes: Vec<_> = num_hashes.chunks(simd_len).collect();
let res = hashes_chunked
.par_iter_mut()
.zip(num_hashes)
.enumerate()
.all(|(i, (chunk, num_hashes))| {
match simd_len {
8 => unsafe {
(api().unwrap().poh_verify_many_simd_avx2)(
chunk.as_mut_ptr(),
num_hashes.as_ptr(),
);
},
16 => unsafe {
(api().unwrap().poh_verify_many_simd_avx512skx)(
chunk.as_mut_ptr(),
num_hashes.as_ptr(),
);
},
_ => {
panic!("unsupported simd len: {simd_len}");
}
}
let entry_start = i * simd_len;
let entry_end = std::cmp::min(entry_start + simd_len, entries.len());
entries[entry_start..entry_end]
.iter()
.enumerate()
.all(|(j, ref_entry)| {
let start = j * HASH_BYTES;
let end = start + HASH_BYTES;
let hash = <[u8; HASH_BYTES]>::try_from(&chunk[start..end])
.map(Hash::new_from_array)
.unwrap();
compare_hashes(hash, ref_entry)
})
});
let poh_duration_us = now.elapsed().as_micros() as u64;
EntryVerificationState {
verification_status: res,
poh_duration_us,
}
}
pub fn verify_entries_cpu(
entries: &[EntryVerificationData],
start_hash: &Hash,
) -> EntryVerificationState {
#[cfg(any(target_arch = "x86", target_arch = "x86_64"))]
let (has_avx2, has_avx512) = (
is_x86_feature_detected!("avx2"),
is_x86_feature_detected!("avx512f"),
);
#[cfg(not(any(target_arch = "x86", target_arch = "x86_64")))]
let (has_avx2, has_avx512) = (false, false);
if api().is_some() {
if has_avx512 && entries.len() >= 128 {
verify_entries_cpu_x86_simd(entries, start_hash, 16)
} else if has_avx2 && entries.len() >= 48 {
verify_entries_cpu_x86_simd(entries, start_hash, 8)
} else {
verify_entries_cpu_generic(entries, start_hash)
}
} else {
verify_entries_cpu_generic(entries, start_hash)
}
}
pub trait EntrySlice {
fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState;
fn verify_cpu_generic(&self, start_hash: &Hash) -> EntryVerificationState;
fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState;
fn verify(&self, start_hash: &Hash, thread_pool: &ThreadPool) -> EntryVerificationState;
fn verify_tick_hash_count(&self, tick_hash_count: &mut u64, hashes_per_tick: u64) -> bool;
fn tick_count(&self) -> u64;
}
impl EntrySlice for [Entry] {
fn verify(&self, start_hash: &Hash, thread_pool: &ThreadPool) -> EntryVerificationState {
let verification_entries = entries_to_verification_data(self);
verify_entries_cpu_in_pool(&verification_entries, start_hash, thread_pool)
}
fn verify_cpu_generic(&self, start_hash: &Hash) -> EntryVerificationState {
let verification_entries = entries_to_verification_data(self);
verify_entries_cpu_generic(&verification_entries, start_hash)
}
fn verify_cpu_x86_simd(&self, start_hash: &Hash, simd_len: usize) -> EntryVerificationState {
let verification_entries = entries_to_verification_data(self);
verify_entries_cpu_x86_simd(&verification_entries, start_hash, simd_len)
}
fn verify_cpu(&self, start_hash: &Hash) -> EntryVerificationState {
let verification_entries = entries_to_verification_data(self);
verify_entries_cpu(&verification_entries, start_hash)
}
fn verify_tick_hash_count(&self, tick_hash_count: &mut u64, hashes_per_tick: u64) -> bool {
if hashes_per_tick == 0 {
return true;
}
for entry in self {
*tick_hash_count = tick_hash_count.saturating_add(entry.num_hashes);
if entry.is_tick() {
if *tick_hash_count != hashes_per_tick {
warn!(
"invalid tick hash count!: entry: {entry:#?}, tick_hash_count: \
{tick_hash_count}, hashes_per_tick: {hashes_per_tick}"
);
return false;
}
*tick_hash_count = 0;
}
}
*tick_hash_count < hashes_per_tick
}
fn tick_count(&self) -> u64 {
self.iter().filter(|e| e.is_tick()).count() as u64
}
}
pub fn next_entry_mut(start: &mut Hash, num_hashes: u64, transactions: Vec<Transaction>) -> Entry {
let entry = Entry::new(start, num_hashes, transactions);
*start = entry.hash;
entry
}
pub fn create_ticks(num_ticks: u64, hashes_per_tick: u64, mut hash: Hash) -> Vec<Entry> {
repeat_with(|| next_entry_mut(&mut hash, hashes_per_tick, vec![]))
.take(num_ticks as usize)
.collect()
}
pub fn next_entry(prev_hash: &Hash, num_hashes: u64, transactions: Vec<Transaction>) -> Entry {
let transactions = transactions.into_iter().map(Into::into).collect::<Vec<_>>();
next_versioned_entry(prev_hash, num_hashes, transactions)
}
pub fn next_versioned_entry(
prev_hash: &Hash,
num_hashes: u64,
transactions: Vec<VersionedTransaction>,
) -> Entry {
assert!(num_hashes > 0 || transactions.is_empty());
Entry {
num_hashes,
hash: next_hash(prev_hash, num_hashes, &transactions),
transactions,
}
}
pub fn thread_pool_for_tests() -> ThreadPool {
rayon::ThreadPoolBuilder::new()
.num_threads(4)
.thread_name(|i| format!("solEntryTest{i:02}"))
.build()
.expect("new rayon threadpool")
}
#[cfg(feature = "dev-context-only-utils")]
pub fn thread_pool_for_benches() -> ThreadPool {
rayon::ThreadPoolBuilder::new()
.num_threads(num_cpus::get())
.thread_name(|i| format!("solEntryBnch{i:02}"))
.build()
.expect("new rayon threadpool")
}
#[cfg(test)]
mod tests {
use {
super::*,
agave_reserved_account_keys::ReservedAccountKeys,
rand::{Rng, rng},
rayon::ThreadPoolBuilder,
solana_hash::Hash,
solana_keypair::Keypair,
solana_measure::measure::Measure,
solana_message::SimpleAddressLoader,
solana_perf::test_tx::test_tx,
solana_pubkey::Pubkey,
solana_runtime_transaction::runtime_transaction::RuntimeTransaction,
solana_sha256_hasher::hash,
solana_signer::Signer,
solana_system_transaction as system_transaction,
solana_transaction::{
sanitized::{MessageHash, SanitizedTransaction},
versioned::VersionedTransaction,
},
solana_transaction_error::TransactionResult as Result,
};
fn create_random_ticks(num_ticks: u64, max_hashes_per_tick: u64, mut hash: Hash) -> Vec<Entry> {
repeat_with(|| {
let hashes_per_tick = rng().random_range(1..max_hashes_per_tick);
next_entry_mut(&mut hash, hashes_per_tick, vec![])
})
.take(num_ticks as usize)
.collect()
}
#[test]
fn test_entry_verify() {
let zero = Hash::default();
let one = hash(zero.as_ref());
assert!(Entry::new_tick(0, &zero).verify(&zero)); assert!(!Entry::new_tick(0, &zero).verify(&one)); assert!(next_entry(&zero, 1, vec![]).verify(&zero)); assert!(!next_entry(&zero, 1, vec![]).verify(&one)); }
fn test_verify_transactions<Tx: TransactionWithMeta + Send + Sync + 'static>(
entries: Vec<Entry>,
skip_verification: bool,
thread_pool: &ThreadPool,
verify: impl Fn(VersionedTransaction, &[u8]) -> Result<Tx> + Send + Sync,
) -> bool {
let num_txs = entries.iter().map(|entry| entry.transactions.len()).sum();
let txs = validate_and_hash_transactions(entries, num_txs, thread_pool, verify);
let Ok(txs) = txs else {
return false;
};
skip_verification || txs.unverified_signatures.verify().is_ok()
}
#[test]
fn test_entry_transaction_verify() {
let zero = Hash::default();
let keypair = Keypair::new();
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 0, zero);
let tx1 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, zero);
let e0 = Entry::new(&zero, 0, vec![tx0, tx1]);
assert!(e0.verify(&zero));
let tx2 = system_transaction::transfer(&keypair, &keypair.pubkey(), 2, zero);
let tx3 = system_transaction::transfer(&keypair, &keypair.pubkey(), 3, zero);
let e1 = Entry::new(&zero, 0, vec![tx2, tx3]);
assert!(e1.verify(&zero));
let es = vec![e0, e1];
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
let verify_transaction = {
move |versioned_tx: VersionedTransaction,
message_bytes: &[u8]|
-> Result<RuntimeTransaction<SanitizedTransaction>> {
RuntimeTransaction::try_create(
versioned_tx,
MessageHash::Precomputed(solana_message::VersionedMessage::hash_raw_message(
message_bytes,
)),
None,
SimpleAddressLoader::Disabled,
&ReservedAccountKeys::empty_key_set(),
true,
true,
)
}
};
assert!(test_verify_transactions(
es,
false,
&thread_pool,
verify_transaction
));
}
#[test]
fn test_validate_and_hash_transactions_and_signatures() {
let thread_pool = ThreadPoolBuilder::new().build().unwrap();
let zero = Hash::default();
let keypair = Keypair::new();
let tx = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, zero);
let entries = vec![Entry::new(&zero, 0, vec![tx])];
let validate_and_hash_transaction =
move |versioned_tx: VersionedTransaction,
message_bytes: &[u8]|
-> Result<RuntimeTransaction<SanitizedTransaction>> {
RuntimeTransaction::try_create(
versioned_tx,
MessageHash::Precomputed(solana_message::VersionedMessage::hash_raw_message(
message_bytes,
)),
None,
SimpleAddressLoader::Disabled,
&ReservedAccountKeys::empty_key_set(),
true,
true,
)
};
let txs =
validate_and_hash_transactions(entries, 1, &thread_pool, validate_and_hash_transaction)
.expect("transaction validation and hashing must not verify signatures");
assert_eq!(txs.entries.len(), 1);
assert!(txs.unverified_signatures.verify().is_ok());
let mut tx = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, zero);
tx.signatures[0] = solana_signature::Signature::default();
let entries = vec![Entry::new(&zero, 0, vec![tx])];
let txs =
validate_and_hash_transactions(entries, 1, &thread_pool, validate_and_hash_transaction)
.expect("transaction validation and hashing must not verify signatures");
assert_eq!(txs.entries.len(), 1);
assert!(matches!(
txs.unverified_signatures.verify(),
Err(solana_transaction_error::TransactionError::SignatureFailure)
));
}
#[test]
fn test_transaction_reorder_attack() {
let zero = Hash::default();
let keypair = Keypair::new();
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 0, zero);
let tx1 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, zero);
let mut e0 = Entry::new(&zero, 0, vec![tx0.clone(), tx1.clone()]);
assert!(e0.verify(&zero));
e0.transactions[0] = tx1.into(); e0.transactions[1] = tx0.into();
assert!(!e0.verify(&zero));
}
#[test]
fn test_transaction_signing() {
let thread_pool = thread_pool_for_tests();
use solana_signature::Signature;
let zero = Hash::default();
let keypair = Keypair::new();
let tx0 = system_transaction::transfer(&keypair, &keypair.pubkey(), 0, zero);
let tx1 = system_transaction::transfer(&keypair, &keypair.pubkey(), 1, zero);
let mut e0 = [Entry::new(&zero, 0, vec![tx0, tx1])];
assert!(e0.verify(&zero, &thread_pool).status());
let orig_sig = e0[0].transactions[0].signatures[0];
e0[0].transactions[0].signatures[0] = Signature::default();
assert!(!e0.verify(&zero, &thread_pool).status());
e0[0].transactions[0].signatures[0] = orig_sig;
assert!(e0.verify(&zero, &thread_pool).status());
let len = e0[0].transactions[0].signatures.len();
e0[0].transactions[0]
.signatures
.resize(len - 1, Signature::default());
assert!(!e0.verify(&zero, &thread_pool).status());
let e0 = [Entry::new(&zero, 0, vec![])];
assert!(e0.verify(&zero, &thread_pool).status());
}
#[test]
fn test_next_entry() {
let zero = Hash::default();
let tick = next_entry(&zero, 1, vec![]);
assert_eq!(tick.num_hashes, 1);
assert_ne!(tick.hash, zero);
let tick = next_entry(&zero, 0, vec![]);
assert_eq!(tick.num_hashes, 0);
assert_eq!(tick.hash, zero);
let keypair = Keypair::new();
let tx0 = system_transaction::transfer(&keypair, &Pubkey::new_unique(), 42, zero);
let entry0 = next_entry(&zero, 1, vec![tx0.clone()]);
assert_eq!(entry0.num_hashes, 1);
assert_eq!(entry0.hash, next_hash(&zero, 1, &[tx0.into()]));
}
#[test]
#[should_panic]
fn test_next_entry_panic() {
let zero = Hash::default();
let keypair = Keypair::new();
let tx = system_transaction::transfer(&keypair, &keypair.pubkey(), 0, zero);
next_entry(&zero, 0, vec![tx]);
}
#[test]
fn test_verify_slice1() {
agave_logger::setup();
let thread_pool = thread_pool_for_tests();
let zero = Hash::default();
let one = hash(zero.as_ref());
assert!(vec![][..].verify(&zero, &thread_pool).status());
assert!(
vec![Entry::new_tick(0, &zero)][..]
.verify(&zero, &thread_pool)
.status()
);
assert!(
!vec![Entry::new_tick(0, &zero)][..]
.verify(&one, &thread_pool)
.status()
);
assert!(
vec![next_entry(&zero, 0, vec![]); 2][..]
.verify(&zero, &thread_pool)
.status()
);
let mut bad_ticks = vec![next_entry(&zero, 0, vec![]); 2];
bad_ticks[1].hash = one;
assert!(!bad_ticks.verify(&zero, &thread_pool).status());
}
#[test]
fn test_verify_slice_with_hashes1() {
agave_logger::setup();
let thread_pool = thread_pool_for_tests();
let zero = Hash::default();
let one = hash(zero.as_ref());
let two = hash(one.as_ref());
assert!(vec![][..].verify(&one, &thread_pool).status());
assert!(
vec![Entry::new_tick(1, &two)][..]
.verify(&one, &thread_pool)
.status()
);
assert!(
!vec![Entry::new_tick(1, &two)][..]
.verify(&two, &thread_pool)
.status()
);
let mut ticks = vec![next_entry(&one, 1, vec![])];
ticks.push(next_entry(&ticks.last().unwrap().hash, 1, vec![]));
assert!(ticks.verify(&one, &thread_pool).status());
let mut bad_ticks = vec![next_entry(&one, 1, vec![])];
bad_ticks.push(next_entry(&bad_ticks.last().unwrap().hash, 1, vec![]));
bad_ticks[1].hash = one;
assert!(!bad_ticks.verify(&one, &thread_pool).status());
}
#[test]
fn test_verify_slice_with_hashes_and_transactions() {
agave_logger::setup();
let thread_pool = thread_pool_for_tests();
let zero = Hash::default();
let one = hash(zero.as_ref());
let two = hash(one.as_ref());
let alice_keypair = Keypair::new();
let bob_keypair = Keypair::new();
let tx0 = system_transaction::transfer(&alice_keypair, &bob_keypair.pubkey(), 1, one);
let tx1 = system_transaction::transfer(&bob_keypair, &alice_keypair.pubkey(), 1, one);
assert!(vec![][..].verify(&one, &thread_pool).status());
assert!(
vec![next_entry(&one, 1, vec![tx0.clone()])][..]
.verify(&one, &thread_pool)
.status()
);
assert!(
!vec![next_entry(&one, 1, vec![tx0.clone()])][..]
.verify(&two, &thread_pool)
.status()
);
let mut ticks = vec![next_entry(&one, 1, vec![tx0.clone()])];
ticks.push(next_entry(
&ticks.last().unwrap().hash,
1,
vec![tx1.clone()],
));
assert!(ticks.verify(&one, &thread_pool).status());
let mut bad_ticks = vec![next_entry(&one, 1, vec![tx0])];
bad_ticks.push(next_entry(&bad_ticks.last().unwrap().hash, 1, vec![tx1]));
bad_ticks[1].hash = one;
assert!(!bad_ticks.verify(&one, &thread_pool).status());
}
#[test]
fn test_verify_tick_hash_count() {
let hashes_per_tick = 10;
let tx = VersionedTransaction::default();
let no_hash_tx_entry = Entry {
transactions: vec![tx.clone()],
..Entry::default()
};
let single_hash_tx_entry = Entry {
transactions: vec![tx.clone()],
num_hashes: 1,
..Entry::default()
};
let partial_tx_entry = Entry {
num_hashes: hashes_per_tick - 1,
transactions: vec![tx.clone()],
..Entry::default()
};
let full_tx_entry = Entry {
num_hashes: hashes_per_tick,
transactions: vec![tx.clone()],
..Entry::default()
};
let max_hash_tx_entry = Entry {
transactions: vec![tx],
num_hashes: u64::MAX,
..Entry::default()
};
let no_hash_tick_entry = Entry::new_tick(0, &Hash::default());
let single_hash_tick_entry = Entry::new_tick(1, &Hash::default());
let partial_tick_entry = Entry::new_tick(hashes_per_tick - 1, &Hash::default());
let full_tick_entry = Entry::new_tick(hashes_per_tick, &Hash::default());
let max_hash_tick_entry = Entry::new_tick(u64::MAX, &Hash::default());
let mut tick_hash_count = 0;
let mut entries = vec![];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, 0);
tick_hash_count = hashes_per_tick;
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, hashes_per_tick);
tick_hash_count = 0;
entries = vec![max_hash_tx_entry.clone()];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, 0));
assert_eq!(tick_hash_count, 0);
entries = vec![partial_tick_entry.clone()];
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, hashes_per_tick - 1);
tick_hash_count = 0;
entries = vec![no_hash_tx_entry, full_tick_entry.clone()];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, 0);
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick - 1));
assert_eq!(tick_hash_count, hashes_per_tick);
tick_hash_count = 0;
entries = vec![partial_tx_entry];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, hashes_per_tick - 1);
tick_hash_count = 0;
entries = vec![full_tx_entry.clone(), no_hash_tick_entry];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, 0);
entries = vec![full_tx_entry.clone(), single_hash_tick_entry.clone()];
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, hashes_per_tick + 1);
tick_hash_count = 0;
entries = vec![full_tx_entry];
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, hashes_per_tick);
tick_hash_count = 0;
entries = vec![single_hash_tx_entry.clone(), partial_tick_entry];
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, 0);
let tx_entries: Vec<Entry> = (0..hashes_per_tick - 1)
.map(|_| single_hash_tx_entry.clone())
.collect();
entries = [tx_entries, vec![single_hash_tick_entry]].concat();
assert!(entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, 0);
entries = vec![full_tick_entry.clone(), max_hash_tick_entry];
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, u64::MAX);
tick_hash_count = 0;
entries = vec![max_hash_tx_entry, full_tick_entry];
assert!(!entries.verify_tick_hash_count(&mut tick_hash_count, hashes_per_tick));
assert_eq!(tick_hash_count, u64::MAX);
}
#[test]
fn test_poh_verify_fuzz() {
agave_logger::setup();
for _ in 0..100 {
let mut time = Measure::start("ticks");
let num_ticks = rng().random_range(1..100);
info!("create {num_ticks} ticks:");
let mut entries = create_random_ticks(num_ticks, 100, Hash::default());
time.stop();
let mut modified = false;
if rng().random_ratio(1, 2) {
modified = true;
let modify_idx = rng().random_range(0..num_ticks) as usize;
entries[modify_idx].hash = hash(&[1, 2, 3]);
}
info!("done.. {time}");
let mut time = Measure::start("poh");
let res = entries
.verify(&Hash::default(), &thread_pool_for_tests())
.status();
assert_eq!(res, !modified);
time.stop();
info!("{time} {res}");
}
}
#[test]
fn test_hash_transactions() {
let mut transactions: Vec<_> = [test_tx(), test_tx(), test_tx()]
.into_iter()
.map(VersionedTransaction::from)
.collect();
let hash1 = hash_transactions(&transactions);
transactions.swap(0, 1);
let hash2 = hash_transactions(&transactions);
assert_ne!(hash1, hash2);
}
}