// file: kb-lib/src/executor/solana/transaction/nonce.rs // version: 6 //! Durable nonce account state parsing and validation. use std::str::FromStr; // rust-rules: trait-import /// Validated current durable nonce account state used for transaction assembly. #[derive(Clone, Debug, Eq, PartialEq)] pub struct ExSolanaDurableNonceAccountState { account: crate::MdPubkey, authority: crate::MdPubkey, blockhash: std::string::String, lamports_per_signature: u64, } impl crate::ExSolanaDurableNonceAccountState { /// Returns the nonce account address. pub fn account(&self) -> &crate::MdPubkey { return &self.account; } /// Returns the authority that must sign the nonce advance instruction. pub fn authority(&self) -> &crate::MdPubkey { return &self.authority; } /// Returns the durable nonce value used as the transaction message blockhash. pub fn blockhash(&self) -> &str { return self.blockhash.as_str(); } /// Returns the fee rate stored when the nonce was last advanced. pub fn lamports_per_signature(&self) -> u64 { return self.lamports_per_signature; } } /// Returns the exact serialized byte length of a Solana nonce account state. pub const fn executor_solana_durable_nonce_account_data_length() -> usize { return solana_nonce::state::State::size(); } /// Parses and validates a complete System Program-owned durable nonce account snapshot. pub fn executor_solana_parse_durable_nonce_account( account: &crate::MdPubkey, owner: &crate::MdProgramId, executable: bool, space: u64, data: &[u8], ) -> kb_core::Result { let account_address = match solana_pubkey::Pubkey::from_str(account.0.as_str()) { std::result::Result::Ok(account_address) => account_address, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_address_invalid", format!("invalid durable nonce account address {}: {error}", account.0), )); }, }; let owner_address = match solana_pubkey::Pubkey::from_str(owner.0.as_str()) { std::result::Result::Ok(owner_address) => owner_address, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_owner_invalid", format!("invalid durable nonce account owner {}: {error}", owner.0), )); }, }; if owner_address != solana_sdk_ids::system_program::id() { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_owner_mismatch", format!( "durable nonce account {account_address} is owned by {owner_address} instead of the System Program" ), )); } if executable { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_executable", format!("durable nonce account {account_address} must not be executable"), )); } let expected_length = crate::executor_solana_durable_nonce_account_data_length(); if space != expected_length as u64 { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_space_invalid", format!( "durable nonce account {account_address} reports {space} bytes instead of {expected_length}" ), )); } if data.len() != expected_length { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_data_length_invalid", format!( "durable nonce account {account_address} contains {} decoded bytes instead of {expected_length}", data.len() ), )); } let versions = match wincode::deserialize_exact::(data) { std::result::Result::Ok(versions) => versions, std::result::Result::Err(error) => { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_state_invalid", format!("cannot decode durable nonce account {account_address}: {error}"), )); }, }; let state = match versions { solana_nonce::versions::Versions::Legacy(_) => { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_legacy_version", format!( "durable nonce account {account_address} uses the legacy nonce domain and must be upgraded before use" ), )); }, solana_nonce::versions::Versions::Current(state) => state, }; let initialized = match *state { solana_nonce::state::State::Uninitialized => { return std::result::Result::Err(kb_core::Error::new( "execution_nonce_account_uninitialized", format!("durable nonce account {account_address} is not initialized"), )); }, solana_nonce::state::State::Initialized(initialized) => initialized, }; let authority = crate::MdPubkey(initialized.authority.to_string()); let blockhash = initialized.blockhash().to_string(); let lamports_per_signature = initialized.get_lamports_per_signature(); return std::result::Result::Ok(crate::ExSolanaDurableNonceAccountState { account: account.clone(), authority, blockhash, lamports_per_signature, }); } #[cfg(test)] mod tests { fn initialized_bytes( authority: solana_pubkey::Pubkey, ) -> (std::vec::Vec, std::string::String) { let source_blockhash = solana_hash::Hash::new_unique(); let durable_nonce = solana_nonce::state::DurableNonce::from_blockhash(&source_blockhash); let expected_blockhash = durable_nonce.as_hash().to_string(); let state = solana_nonce::versions::Versions::new( solana_nonce::state::State::new_initialized(&authority, durable_nonce, 5_000), ); let bytes = wincode::serialize(&state) .unwrap_or_else(|error| panic!("unexpected nonce serialization error: {error}")); return (bytes, expected_blockhash); } fn account(value: solana_pubkey::Pubkey) -> crate::MdPubkey { return crate::MdPubkey(value.to_string()); } #[test] fn current_initialized_nonce_state_is_decoded_exactly() { let account = account(solana_pubkey::Pubkey::new_unique()); let authority = solana_pubkey::Pubkey::new_unique(); let (bytes, expected_blockhash) = initialized_bytes(authority); assert_eq!(bytes.len(), crate::executor_solana_durable_nonce_account_data_length()); let parsed = crate::executor_solana_parse_durable_nonce_account( &account, &crate::MdProgramId(solana_sdk_ids::system_program::id().to_string()), false, bytes.len() as u64, bytes.as_slice(), ) .unwrap_or_else(|error| panic!("unexpected nonce parse error: {error}")); assert_eq!(parsed.account(), &account); assert_eq!(parsed.authority().0, authority.to_string()); assert_eq!(parsed.blockhash(), expected_blockhash); assert_eq!(parsed.lamports_per_signature(), 5_000); } #[test] fn malformed_owner_shape_and_state_are_rejected() { let saccount = account(solana_pubkey::Pubkey::new_unique()); let authority = solana_pubkey::Pubkey::new_unique(); let (bytes, _blockhash) = initialized_bytes(authority); let owner = crate::MdProgramId(solana_sdk_ids::system_program::id().to_string()); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &crate::MdProgramId(solana_pubkey::Pubkey::new_unique().to_string()), false, bytes.len() as u64, bytes.as_slice(), ) .is_err() ); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, true, bytes.len() as u64, bytes.as_slice(), ) .is_err() ); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, false, (bytes.len() + 1) as u64, bytes.as_slice(), ) .is_err() ); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, false, bytes.len() as u64, &bytes[..bytes.len() - 1], ) .is_err() ); let mut malformed = bytes.clone(); malformed[0] = 255; assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, false, malformed.len() as u64, malformed.as_slice(), ) .is_err() ); } #[test] fn legacy_and_uninitialized_nonce_states_are_rejected() { let saccount = account(solana_pubkey::Pubkey::new_unique()); let owner = crate::MdProgramId(solana_sdk_ids::system_program::id().to_string()); let legacy = solana_nonce::versions::Versions::Legacy(std::boxed::Box::new( solana_nonce::state::State::Initialized(solana_nonce::state::Data::default()), )); let legacy_bytes = wincode::serialize(&legacy) .unwrap_or_else(|error| panic!("unexpected legacy serialization error: {error}")); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, false, legacy_bytes.len() as u64, legacy_bytes.as_slice(), ) .is_err() ); let uninitialized = solana_nonce::versions::Versions::new(solana_nonce::state::State::Uninitialized); let uninitialized_bytes = wincode::serialize(&uninitialized) .unwrap_or_else(|error| panic!("unexpected state serialization error: {error}")); assert!( crate::executor_solana_parse_durable_nonce_account( &saccount, &owner, false, uninitialized_bytes.len() as u64, uninitialized_bytes.as_slice(), ) .is_err() ); } }