277 lines
10 KiB
Rust
277 lines
10 KiB
Rust
// 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<crate::ExSolanaDurableNonceAccountState> {
|
|
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::<solana_nonce::versions::Versions>(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<u8>, 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()
|
|
);
|
|
}
|
|
}
|