v0.1.0-pre.022
This commit is contained in:
276
kb-lib/src/executor/solana/transaction/nonce.rs
Normal file
276
kb-lib/src/executor/solana/transaction/nonce.rs
Normal file
@@ -0,0 +1,276 @@
|
||||
// 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()
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user