pub type TnPubkey = [u8; 32];
pub type TnHash = [u8; 32];
pub type TnSignature = [u8; 64];
use crate::{
StateProofType,
tn_signature::{sign_transaction, verify_transaction},
tn_state_proof::StateProof,
};
use bytemuck::{Pod, Zeroable, bytes_of, pod_read_unaligned};
pub const TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT: u8 = 0; pub const TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT: u8 = 1;
pub const TN_STATE_PROOF_TYPE_EXISTING: u64 = 0x0;
pub const TN_STATE_PROOF_TYPE_UPDATING: u64 = 0x1;
pub const TN_STATE_PROOF_TYPE_CREATION: u64 = 0x2;
pub const TN_STATE_PROOF_HDR_SIZE: usize = 40; pub const TN_ACCOUNT_META_FOOTPRINT: usize = 64;
#[derive(Debug, PartialEq)]
pub enum RpcError {
InvalidTransactionSize { size: usize, max_size: usize },
TrailingBytes { expected: usize, found: usize },
TooManyAccounts { count: usize, max_count: usize },
InvalidTransactionSignature,
InvalidParams(&'static str),
InvalidFormat,
InvalidVersion,
InvalidFlags,
InvalidFeePayerStateProofType,
InvalidChainId,
DuplicateAccount,
UnsortedReadwriteAccounts,
UnsortedReadonlyAccounts,
}
impl std::fmt::Display for RpcError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
RpcError::InvalidTransactionSize { size, max_size } => {
write!(
f,
"Transaction size {} exceeds maximum allowed size {}",
size, max_size
)
}
RpcError::TrailingBytes { expected, found } => {
write!(
f,
"Transaction has trailing bytes: expected {} bytes, found {} bytes",
expected, found
)
}
RpcError::TooManyAccounts { count, max_count } => {
write!(
f,
"Too many accounts: {} exceeds maximum {}",
count, max_count
)
}
RpcError::InvalidTransactionSignature => {
write!(f, "Invalid transaction signature")
}
RpcError::InvalidParams(msg) => {
write!(f, "Invalid parameters: {}", msg)
}
RpcError::InvalidFormat => {
write!(f, "Invalid transaction format")
}
RpcError::InvalidVersion => {
write!(f, "Invalid transaction version")
}
RpcError::InvalidFlags => {
write!(f, "Invalid transaction flags")
}
RpcError::InvalidFeePayerStateProofType => {
write!(f, "Invalid fee payer state proof type")
}
RpcError::InvalidChainId => {
write!(f, "Invalid chain ID: chain_id cannot be zero")
}
RpcError::DuplicateAccount => {
write!(f, "Duplicate account in transaction")
}
RpcError::UnsortedReadwriteAccounts => {
write!(f, "Read-write accounts are not strictly ascending")
}
RpcError::UnsortedReadonlyAccounts => {
write!(f, "Read-only accounts are not strictly ascending")
}
}
}
}
impl std::error::Error for RpcError {}
impl RpcError {
pub fn invalid_transaction_size(size: usize, max_size: usize) -> Self {
Self::InvalidTransactionSize { size, max_size }
}
pub fn trailing_bytes(expected: usize, found: usize) -> Self {
Self::TrailingBytes { expected, found }
}
pub fn too_many_accounts(count: usize, max_count: usize) -> Self {
Self::TooManyAccounts { count, max_count }
}
pub fn invalid_transaction_signature() -> Self {
Self::InvalidTransactionSignature
}
pub fn invalid_params(msg: &'static str) -> Self {
Self::InvalidParams(msg)
}
pub fn invalid_format() -> Self {
Self::InvalidFormat
}
pub fn invalid_version() -> Self {
Self::InvalidVersion
}
pub fn invalid_flags() -> Self {
Self::InvalidFlags
}
pub fn invalid_fee_payer_state_proof_type() -> Self {
Self::InvalidFeePayerStateProofType
}
pub fn invalid_chain_id() -> Self {
Self::InvalidChainId
}
pub fn duplicate_account() -> Self {
Self::DuplicateAccount
}
pub fn unsorted_readwrite_accounts() -> Self {
Self::UnsortedReadwriteAccounts
}
pub fn unsorted_readonly_accounts() -> Self {
Self::UnsortedReadonlyAccounts
}
}
#[repr(C)]
#[derive(Clone, Copy, Debug)]
pub struct WireTxnHdrV1 {
pub transaction_version: u8,
pub flags: u8,
pub readwrite_accounts_cnt: u16,
pub readonly_accounts_cnt: u16,
pub instr_data_sz: u16,
pub req_compute_units: u32,
pub req_state_units: u16,
pub req_memory_units: u16,
pub fee: u64,
pub nonce: u64,
pub start_slot: u64,
pub expiry_after: u32,
pub chain_id: u16,
pub padding_0: [u8; 2],
pub fee_payer_pubkey: [u8; 32],
pub program_pubkey: [u8; 32],
}
pub const TN_TXN_SIGNATURE_SZ: usize = 64;
pub const TN_TXN_MAX_ACCOUNTS: usize = 1024;
impl Default for WireTxnHdrV1 {
fn default() -> Self {
Self {
transaction_version: 0,
flags: 0,
readwrite_accounts_cnt: 0,
readonly_accounts_cnt: 0,
instr_data_sz: 0,
req_compute_units: 0,
req_state_units: 0,
req_memory_units: 0,
fee: 0,
nonce: 0,
start_slot: 0,
expiry_after: 0,
chain_id: 0,
padding_0: [0u8; 2],
fee_payer_pubkey: [0u8; 32],
program_pubkey: [0u8; 32],
}
}
}
unsafe impl Pod for WireTxnHdrV1 {}
unsafe impl Zeroable for WireTxnHdrV1 {}
#[derive(Clone, Debug, Default)]
pub struct Transaction {
pub fee_payer: TnPubkey, pub program: TnPubkey,
pub rw_accs: Option<Vec<TnPubkey>>, pub r_accs: Option<Vec<TnPubkey>>,
pub instructions: Option<Vec<u8>>,
pub fee: u64, pub req_compute_units: u32, pub req_state_units: u16, pub req_memory_units: u16, pub expiry_after: u32, pub start_slot: u64, pub nonce: u64, pub flags: u8, pub chain_id: u16,
pub signature: Option<TnSignature>,
pub fee_payer_state_proof: Option<StateProof>,
pub fee_payer_account_meta_raw: Option<Vec<u8>>,
}
impl Transaction {
pub fn new(fee_payer: TnPubkey, program: TnPubkey, fee: u64, nonce: u64) -> Self {
Self {
fee_payer,
program,
rw_accs: None,
r_accs: None,
instructions: None,
fee,
req_compute_units: 0,
req_state_units: 0,
req_memory_units: 0,
expiry_after: 0,
start_slot: 0,
nonce,
flags: 0,
chain_id: 1,
signature: None,
fee_payer_state_proof: None,
fee_payer_account_meta_raw: None,
}
}
pub fn new_raw_instruction(
fee_payer: &TnPubkey,
program: &TnPubkey,
instruction_data: &[u8],
) -> Result<Self, Box<dyn std::error::Error>> {
Ok(Self {
fee_payer: *fee_payer,
program: *program,
rw_accs: None,
r_accs: None,
instructions: Some(instruction_data.to_vec()),
fee: 0,
req_compute_units: 0,
req_state_units: 0,
req_memory_units: 0,
expiry_after: 0,
start_slot: 0,
nonce: 0,
flags: 0,
chain_id: 1,
signature: None,
fee_payer_state_proof: None,
fee_payer_account_meta_raw: None,
})
}
pub fn has_fee_payer_state_proof(&self) -> bool {
(self.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
}
pub fn may_compress_account(&self) -> bool {
(self.flags & (1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT)) != 0
}
pub fn get_signature(&self) -> Option<crate::Signature> {
if let Some(sig) = &self.signature {
return Some(crate::Signature::from_bytes(&sig));
}
None
}
pub fn with_may_compress_account(mut self) -> Self {
self.flags |= 1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT;
self
}
pub fn with_fee_payer_state_proof(mut self, state_proof: &StateProof) -> Self {
self.fee_payer_state_proof = Some(state_proof.clone());
self.flags |= 1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT;
self
}
pub fn with_fee_payer_account_meta_raw(mut self, account_meta_raw: Vec<u8>) -> Self {
self.fee_payer_account_meta_raw = Some(account_meta_raw);
self
}
pub fn without_fee_payer_state_proof(mut self) -> Self {
self.fee_payer_state_proof = None;
self.flags &= !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
self
}
pub fn with_rw_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
self.rw_accs = Some(accounts);
self
}
pub fn with_r_accounts(mut self, accounts: Vec<TnPubkey>) -> Self {
self.r_accs = Some(accounts);
self
}
pub fn add_rw_account(mut self, account: TnPubkey) -> Self {
match self.rw_accs {
Some(ref mut accounts) => accounts.push(account),
None => self.rw_accs = Some(vec![account]),
}
self
}
pub fn add_r_account(mut self, account: TnPubkey) -> Self {
match self.r_accs {
Some(ref mut accounts) => accounts.push(account),
None => self.r_accs = Some(vec![account]),
}
self
}
pub fn with_instructions(mut self, instructions: Vec<u8>) -> Self {
self.instructions = Some(instructions);
self
}
pub fn with_compute_units(mut self, units: u32) -> Self {
self.req_compute_units = units;
self
}
pub fn with_state_units(mut self, units: u16) -> Self {
self.req_state_units = units;
self
}
pub fn with_memory_units(mut self, units: u16) -> Self {
self.req_memory_units = units;
self
}
pub fn with_expiry_after(mut self, expiry: u32) -> Self {
self.expiry_after = expiry;
self
}
pub fn with_nonce(mut self, nonce: u64) -> Self {
self.nonce = nonce;
self
}
pub fn with_start_slot(mut self, slot: u64) -> Self {
self.start_slot = slot;
self
}
pub fn with_chain_id(mut self, chain_id: u16) -> Self {
self.chain_id = chain_id;
self
}
pub fn sign(&mut self, private_key: &[u8; 32]) -> Result<(), Box<dyn std::error::Error>> {
self.validate()
.map_err(|e| Box::<dyn std::error::Error>::from(e))?;
self.sign_unchecked(private_key)
}
pub fn sign_unchecked(
&mut self,
private_key: &[u8; 32],
) -> Result<(), Box<dyn std::error::Error>> {
let wire_bytes = self.to_wire_for_signing();
let sig = sign_transaction(&wire_bytes, &self.fee_payer, private_key)
.map_err(|e| Box::<dyn std::error::Error>::from(e))?;
self.signature = Some(sig);
Ok(())
}
pub fn validate(&self) -> Result<(), RpcError> {
let rw_accs = self.rw_accs.as_deref().unwrap_or(&[]);
let r_accs = self.r_accs.as_deref().unwrap_or(&[]);
validate_account_layout(rw_accs, r_accs, &self.fee_payer, &self.program)
}
pub fn verify(&self) -> bool {
if let Some(sig_bytes) = &self.signature {
let wire_bytes = self.to_wire_for_signing();
return verify_transaction(&wire_bytes, sig_bytes, &self.fee_payer).is_ok();
}
false
}
fn to_wire_for_signing(&self) -> Vec<u8> {
let mut wire: WireTxnHdrV1 = unsafe { core::mem::zeroed() };
wire.transaction_version = 1;
wire.flags = self.flags;
wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
wire.req_compute_units = self.req_compute_units;
wire.req_state_units = self.req_state_units;
wire.req_memory_units = self.req_memory_units;
wire.expiry_after = self.expiry_after;
wire.chain_id = self.chain_id;
wire.fee = self.fee;
wire.nonce = self.nonce;
wire.start_slot = self.start_slot;
wire.fee_payer_pubkey = self.fee_payer;
wire.program_pubkey = self.program;
let mut result = bytes_of(&wire).to_vec();
if let Some(ref rw_accs) = self.rw_accs {
for acc in rw_accs {
result.extend_from_slice(acc);
}
}
if let Some(ref r_accs) = self.r_accs {
for acc in r_accs {
result.extend_from_slice(acc);
}
}
if let Some(ref instructions) = self.instructions {
result.extend_from_slice(instructions);
}
if let Some(ref state_proof) = self.fee_payer_state_proof {
result.extend_from_slice(&state_proof.to_wire());
}
if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
result.extend_from_slice(fee_payer_account_meta_raw);
}
result
}
pub fn to_wire(&self) -> Vec<u8> {
self.serialize_wire()
}
pub fn try_to_wire(&self) -> Result<Vec<u8>, RpcError> {
self.validate()?;
Ok(self.serialize_wire())
}
fn serialize_wire(&self) -> Vec<u8> {
let mut wire = WireTxnHdrV1::default();
wire.transaction_version = 1;
wire.flags = self.flags;
wire.readwrite_accounts_cnt = self.rw_accs.as_ref().map_or(0, |v| v.len() as u16);
wire.readonly_accounts_cnt = self.r_accs.as_ref().map_or(0, |v| v.len() as u16);
wire.instr_data_sz = self.instructions.as_ref().map_or(0, |v| v.len() as u16);
wire.req_compute_units = self.req_compute_units;
wire.req_state_units = self.req_state_units;
wire.req_memory_units = self.req_memory_units;
wire.expiry_after = self.expiry_after;
wire.chain_id = self.chain_id;
wire.fee = self.fee;
wire.nonce = self.nonce;
wire.start_slot = self.start_slot;
wire.fee_payer_pubkey = self.fee_payer;
wire.program_pubkey = self.program;
let mut result = bytes_of(&wire).to_vec();
if let Some(ref rw_accs) = self.rw_accs {
for acc in rw_accs {
result.extend_from_slice(acc);
}
}
if let Some(ref r_accs) = self.r_accs {
for acc in r_accs {
result.extend_from_slice(acc);
}
}
if let Some(ref instructions) = self.instructions {
result.extend_from_slice(instructions);
}
if let Some(ref state_proof) = self.fee_payer_state_proof {
result.extend_from_slice(&state_proof.to_wire());
}
if let Some(ref fee_payer_account_meta_raw) = self.fee_payer_account_meta_raw {
result.extend_from_slice(fee_payer_account_meta_raw);
}
if let Some(sig) = &self.signature {
result.extend_from_slice(sig);
} else {
result.extend_from_slice(&[0u8; TN_TXN_SIGNATURE_SZ]);
}
result
}
pub fn from_wire(bytes: &[u8]) -> Option<Self> {
if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
return None;
}
let wire: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
let mut offset = core::mem::size_of::<WireTxnHdrV1>();
let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
let mut signature = [0u8; TN_TXN_SIGNATURE_SZ];
signature.copy_from_slice(&bytes[sig_start..]);
let rw_accs = if wire.readwrite_accounts_cnt > 0 {
let mut accounts = Vec::new();
for _ in 0..wire.readwrite_accounts_cnt {
if offset + 32 > sig_start {
return None;
}
let mut acc = [0u8; 32];
acc.copy_from_slice(&bytes[offset..offset + 32]);
accounts.push(acc);
offset += 32;
}
Some(accounts)
} else {
None
};
let r_accs = if wire.readonly_accounts_cnt > 0 {
let mut accounts = Vec::new();
for _ in 0..wire.readonly_accounts_cnt {
if offset + 32 > sig_start {
return None;
}
let mut acc = [0u8; 32];
acc.copy_from_slice(&bytes[offset..offset + 32]);
accounts.push(acc);
offset += 32;
}
Some(accounts)
} else {
None
};
let instructions = if wire.instr_data_sz > 0 {
if offset + wire.instr_data_sz as usize > sig_start {
return None;
}
let instr = bytes[offset..offset + wire.instr_data_sz as usize].to_vec();
offset += wire.instr_data_sz as usize;
Some(instr)
} else {
None
};
let mut fee_payer_account_meta_raw: Option<Vec<u8>> = None;
let fee_payer_state_proof = if has_fee_payer_state_proof(wire.flags) {
if offset >= sig_start {
return None;
}
let state_proof_bytes = &bytes[offset..sig_start];
if let Some(state_proof) = StateProof::from_wire(state_proof_bytes) {
offset += state_proof.footprint();
if state_proof.header.proof_type == StateProofType::Existing {
if offset + TN_ACCOUNT_META_FOOTPRINT > sig_start {
return None;
}
let account_meta_bytes = &bytes[offset..offset + TN_ACCOUNT_META_FOOTPRINT];
fee_payer_account_meta_raw = Some(account_meta_bytes.to_vec());
offset += TN_ACCOUNT_META_FOOTPRINT;
}
Some(state_proof)
} else {
return None;
}
} else {
None
};
if offset != sig_start {
log::warn!(
"Transaction::from_wire: offset != sig_start ({} != {})",
offset,
sig_start
);
return None;
}
Some(Transaction {
fee_payer: wire.fee_payer_pubkey,
program: wire.program_pubkey,
rw_accs,
r_accs,
instructions,
flags: wire.flags,
chain_id: wire.chain_id,
fee: wire.fee,
req_compute_units: wire.req_compute_units,
req_state_units: wire.req_state_units,
req_memory_units: wire.req_memory_units,
expiry_after: wire.expiry_after,
start_slot: wire.start_slot,
nonce: wire.nonce,
signature: Some(signature),
fee_payer_state_proof,
fee_payer_account_meta_raw,
})
}
pub fn get_field_from_wire(bytes: &[u8], field: &str) -> Option<Vec<u8>> {
if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
return None;
}
let wire: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
match field {
"fee_payer_signature" => {
let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
Some(bytes[sig_start..].to_vec())
}
"transaction_version" => Some(vec![wire.transaction_version]),
"flags" => Some(vec![wire.flags]),
"readwrite_accounts_cnt" => Some(wire.readwrite_accounts_cnt.to_le_bytes().to_vec()),
"readonly_accounts_cnt" => Some(wire.readonly_accounts_cnt.to_le_bytes().to_vec()),
"instr_data_sz" => Some(wire.instr_data_sz.to_le_bytes().to_vec()),
"req_compute_units" => Some(wire.req_compute_units.to_le_bytes().to_vec()),
"req_state_units" => Some(wire.req_state_units.to_le_bytes().to_vec()),
"req_memory_units" => Some(wire.req_memory_units.to_le_bytes().to_vec()),
"expiry_after" => Some(wire.expiry_after.to_le_bytes().to_vec()),
"chain_id" => Some(wire.chain_id.to_le_bytes().to_vec()),
"fee" => Some(wire.fee.to_le_bytes().to_vec()),
"nonce" => Some(wire.nonce.to_le_bytes().to_vec()),
"start_slot" => Some(wire.start_slot.to_le_bytes().to_vec()),
"fee_payer_pubkey" => Some(wire.fee_payer_pubkey.to_vec()),
"program_pubkey" => Some(wire.program_pubkey.to_vec()),
_ => None,
}
}
}
fn has_fee_payer_state_proof(flags: u8) -> bool {
(flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT)) != 0
}
fn extract_state_proof_type(type_slot: u64) -> u64 {
(type_slot >> 62) & 0x3 }
fn calculate_state_proof_footprint(state_proof_data: &[u8]) -> Result<usize, RpcError> {
if state_proof_data.len() < TN_STATE_PROOF_HDR_SIZE {
return Err(RpcError::invalid_format());
}
let type_slot = u64::from_le_bytes([
state_proof_data[0],
state_proof_data[1],
state_proof_data[2],
state_proof_data[3],
state_proof_data[4],
state_proof_data[5],
state_proof_data[6],
state_proof_data[7],
]);
let mut sibling_hash_cnt = 0u32;
for i in 0..4 {
let start = 8 + i * 8;
let word = u64::from_le_bytes([
state_proof_data[start],
state_proof_data[start + 1],
state_proof_data[start + 2],
state_proof_data[start + 3],
state_proof_data[start + 4],
state_proof_data[start + 5],
state_proof_data[start + 6],
state_proof_data[start + 7],
]);
sibling_hash_cnt += word.count_ones();
}
let proof_type = extract_state_proof_type(type_slot);
let body_sz = (proof_type + sibling_hash_cnt as u64) * 32;
Ok(TN_STATE_PROOF_HDR_SIZE + body_sz as usize)
}
pub fn tn_txn_size(bytes: &[u8]) -> Result<usize, RpcError> {
if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
return Err(RpcError::invalid_format());
}
let hdr: WireTxnHdrV1 =
unsafe { std::ptr::read_unaligned(bytes.as_ptr() as *const WireTxnHdrV1) };
let hdr = &hdr;
let mut offset = core::mem::size_of::<WireTxnHdrV1>();
let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
if offset + accs_sz > bytes.len() {
return Err(RpcError::invalid_format());
}
offset += accs_sz;
let instr_sz = hdr.instr_data_sz as usize;
if offset + instr_sz > bytes.len() {
return Err(RpcError::invalid_format());
}
offset += instr_sz;
if has_fee_payer_state_proof(hdr.flags) {
if offset + TN_STATE_PROOF_HDR_SIZE > bytes.len() {
return Err(RpcError::invalid_format());
}
let state_proof_data = &bytes[offset..];
let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
if offset + state_proof_sz > bytes.len() {
return Err(RpcError::invalid_format());
}
offset += state_proof_sz;
let type_slot = u64::from_le_bytes([
state_proof_data[0],
state_proof_data[1],
state_proof_data[2],
state_proof_data[3],
state_proof_data[4],
state_proof_data[5],
state_proof_data[6],
state_proof_data[7],
]);
let proof_type = extract_state_proof_type(type_slot);
if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
if offset + TN_ACCOUNT_META_FOOTPRINT > bytes.len() {
return Err(RpcError::invalid_format());
}
offset += TN_ACCOUNT_META_FOOTPRINT;
}
}
offset += TN_TXN_SIGNATURE_SZ;
if offset > bytes.len() {
return Err(RpcError::invalid_format());
}
Ok(offset)
}
pub fn validate_account_layout(
rw_accs: &[TnPubkey],
r_accs: &[TnPubkey],
fee_payer: &TnPubkey,
program: &TnPubkey,
) -> Result<(), RpcError> {
let total_accounts = 2usize
.checked_add(rw_accs.len())
.and_then(|v| v.checked_add(r_accs.len()))
.ok_or_else(|| RpcError::too_many_accounts(usize::MAX, TN_TXN_MAX_ACCOUNTS))?;
if total_accounts > TN_TXN_MAX_ACCOUNTS {
return Err(RpcError::too_many_accounts(total_accounts, TN_TXN_MAX_ACCOUNTS));
}
if fee_payer == program {
return Err(RpcError::duplicate_account());
}
let (mut i, mut j) = (0usize, 0usize);
let mut prev_rw: Option<&[u8; 32]> = None;
let mut prev_ro: Option<&[u8; 32]> = None;
while i < rw_accs.len() || j < r_accs.len() {
let use_rw = j >= r_accs.len() || (i < rw_accs.len() && rw_accs[i] <= r_accs[j]);
if use_rw {
let key = &rw_accs[i];
if j < r_accs.len() && *key == r_accs[j] {
return Err(RpcError::duplicate_account());
}
if let Some(prev) = prev_rw {
if prev == key {
return Err(RpcError::duplicate_account());
}
if prev > key {
return Err(RpcError::unsorted_readwrite_accounts());
}
}
prev_rw = Some(key);
i += 1;
} else {
let key = &r_accs[j];
if let Some(prev) = prev_ro {
if prev == key {
return Err(RpcError::duplicate_account());
}
if prev > key {
return Err(RpcError::unsorted_readonly_accounts());
}
}
prev_ro = Some(key);
j += 1;
}
}
for account in rw_accs.iter().chain(r_accs.iter()) {
if account == fee_payer || account == program {
return Err(RpcError::duplicate_account());
}
}
Ok(())
}
pub fn validate_wire_transaction(bytes: &[u8]) -> Result<(), RpcError> {
const TN_TXN_MTU: usize = 32_768;
const TN_TXN_VERSION_OFFSET: usize = 0;
const TN_TXN_FLAGS_OFFSET: usize = 1;
use bytemuck::pod_read_unaligned;
if bytes.len() > TN_TXN_MTU {
return Err(RpcError::invalid_transaction_size(bytes.len(), TN_TXN_MTU));
}
if bytes.len() < core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ {
return Err(RpcError::invalid_format());
}
let transaction_version = bytes[TN_TXN_VERSION_OFFSET];
if transaction_version != 0x01 {
return Err(RpcError::invalid_version());
}
let flags = bytes[TN_TXN_FLAGS_OFFSET];
let flags_without_proof_bit = flags & !(1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT);
let flags_cleared = flags_without_proof_bit & !(1 << TN_TXN_FLAG_MAY_COMPRESS_ACCOUNT_BIT);
if flags_cleared != 0 {
return Err(RpcError::invalid_flags());
}
let hdr: WireTxnHdrV1 = pod_read_unaligned(&bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
let mut offset = core::mem::size_of::<WireTxnHdrV1>();
let sig_start = bytes.len() - TN_TXN_SIGNATURE_SZ;
if hdr.chain_id == 0 {
return Err(RpcError::invalid_chain_id());
}
let accs_sz = (hdr.readwrite_accounts_cnt as usize + hdr.readonly_accounts_cnt as usize) * 32;
if offset + accs_sz > sig_start {
return Err(RpcError::invalid_format());
}
offset += accs_sz;
let instr_sz = hdr.instr_data_sz as usize;
if offset + instr_sz > sig_start {
return Err(RpcError::invalid_format());
}
offset += instr_sz;
if has_fee_payer_state_proof(flags) {
if offset + TN_STATE_PROOF_HDR_SIZE > sig_start {
return Err(RpcError::invalid_format());
}
let state_proof_data = &bytes[offset..sig_start];
let type_slot = u64::from_le_bytes([
state_proof_data[0],
state_proof_data[1],
state_proof_data[2],
state_proof_data[3],
state_proof_data[4],
state_proof_data[5],
state_proof_data[6],
state_proof_data[7],
]);
let proof_type = extract_state_proof_type(type_slot);
if proof_type != TN_STATE_PROOF_TYPE_EXISTING && proof_type != TN_STATE_PROOF_TYPE_CREATION
{
return Err(RpcError::invalid_fee_payer_state_proof_type());
}
let state_proof_sz = calculate_state_proof_footprint(state_proof_data)?;
if offset + state_proof_sz > sig_start {
return Err(RpcError::invalid_format());
}
offset += state_proof_sz;
if proof_type == TN_STATE_PROOF_TYPE_EXISTING {
if offset + TN_ACCOUNT_META_FOOTPRINT > sig_start {
return Err(RpcError::invalid_format());
}
offset += TN_ACCOUNT_META_FOOTPRINT;
}
}
if offset != sig_start {
return Err(RpcError::trailing_bytes(
offset + TN_TXN_SIGNATURE_SZ,
bytes.len(),
));
}
{
let accs_start = core::mem::size_of::<WireTxnHdrV1>();
let rw_cnt = hdr.readwrite_accounts_cnt as usize;
let ro_cnt = hdr.readonly_accounts_cnt as usize;
let mut rw_accs: Vec<TnPubkey> = Vec::with_capacity(rw_cnt);
for k in 0..rw_cnt {
let s = accs_start + k * 32;
let mut a = [0u8; 32];
a.copy_from_slice(&bytes[s..s + 32]);
rw_accs.push(a);
}
let ro_start = accs_start + rw_cnt * 32;
let mut ro_accs: Vec<TnPubkey> = Vec::with_capacity(ro_cnt);
for k in 0..ro_cnt {
let s = ro_start + k * 32;
let mut a = [0u8; 32];
a.copy_from_slice(&bytes[s..s + 32]);
ro_accs.push(a);
}
validate_account_layout(
&rw_accs,
&ro_accs,
&hdr.fee_payer_pubkey,
&hdr.program_pubkey,
)?;
}
let wire_for_signing = &bytes[..sig_start];
let signature = &bytes[sig_start..];
if verify_transaction(
wire_for_signing,
signature.try_into().expect("signature should be 64 bytes"),
&hdr.fee_payer_pubkey,
)
.is_err()
{
return Err(RpcError::invalid_transaction_signature());
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use ed25519_dalek::SigningKey;
fn make_valid_txn_bytes_with_flags(flags: u8) -> Vec<u8> {
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
tx.rw_accs = Some(vec![[3u8; 32], [4u8; 32]]);
tx.r_accs = Some(vec![[5u8; 32]]);
tx.instructions = Some(vec![1, 2, 3, 4]);
tx.flags = flags;
tx.sign(&signing_key.to_bytes()).unwrap();
tx.to_wire()
}
fn make_valid_txn_bytes() -> Vec<u8> {
make_valid_txn_bytes_with_flags(0)
}
#[test]
fn test_tn_txn_size_basic_transaction() {
let bytes = make_valid_txn_bytes();
let calculated_size = tn_txn_size(&bytes).unwrap();
assert_eq!(calculated_size, bytes.len());
}
#[test]
fn test_tn_txn_size_with_state_proof() {
use crate::tn_state_proof::StateProof;
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let path_bitset = [0u8; 32]; let existing_leaf_pubkey = [7u8; 32];
let existing_leaf_hash = [8u8; 32];
let state_proof = StateProof::creation(
100,
path_bitset,
existing_leaf_pubkey,
existing_leaf_hash,
vec![],
);
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
.with_rw_accounts(vec![[3u8; 32]])
.with_instructions(vec![1, 2, 3])
.with_fee_payer_state_proof(&state_proof);
tx.sign(&signing_key.to_bytes()).unwrap();
let bytes = tx.to_wire();
let calculated_size = tn_txn_size(&bytes).unwrap();
assert_eq!(calculated_size, bytes.len());
}
#[test]
fn test_tn_txn_size_minimal_transaction() {
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
tx.sign(&signing_key.to_bytes()).unwrap();
let bytes = tx.to_wire();
let calculated_size = tn_txn_size(&bytes).unwrap();
assert_eq!(calculated_size, bytes.len());
let expected_min_size = core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ;
assert_eq!(calculated_size, expected_min_size);
}
#[test]
fn test_tn_txn_size_invalid_format() {
let short_bytes = vec![0u8; 50];
let result = tn_txn_size(&short_bytes);
assert!(matches!(result, Err(RpcError::InvalidFormat)));
let mut bytes = make_valid_txn_bytes();
bytes.truncate(core::mem::size_of::<WireTxnHdrV1>() + 10); let result = tn_txn_size(&bytes);
assert!(matches!(result, Err(RpcError::InvalidFormat)));
}
#[test]
fn test_tn_txn_size_consistency_with_validation() {
let bytes = make_valid_txn_bytes();
assert!(validate_wire_transaction(&bytes).is_ok());
assert!(tn_txn_size(&bytes).is_ok());
let calculated_size = tn_txn_size(&bytes).unwrap();
assert_eq!(calculated_size, bytes.len());
}
#[test]
fn test_valid_transaction() {
let bytes = make_valid_txn_bytes();
assert!(validate_wire_transaction(&bytes).is_ok());
}
#[test]
fn test_transaction_duplicate_accounts_rejected() {
let signing_key = SigningKey::from(&[1u8; 32]);
let fee = signing_key.verifying_key().to_bytes();
let program = [2u8; 32];
let a = [3u8; 32];
let b = [4u8; 32];
let cases = [
Transaction::new(fee, fee, 100, 42),
Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![fee]),
Transaction::new(fee, program, 100, 42).with_r_accounts(vec![fee]),
Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![program]),
Transaction::new(fee, program, 100, 42).with_r_accounts(vec![program]),
Transaction::new(fee, program, 100, 42).with_rw_accounts(vec![a, a]),
Transaction::new(fee, program, 100, 42).with_r_accounts(vec![a, a]),
Transaction::new(fee, program, 100, 42)
.with_rw_accounts(vec![a])
.with_r_accounts(vec![a, b]),
];
for mut tx in cases {
assert!(matches!(tx.validate(), Err(RpcError::DuplicateAccount)));
assert!(matches!(tx.try_to_wire(), Err(RpcError::DuplicateAccount)));
assert!(tx.sign(&signing_key.to_bytes()).is_err());
}
}
#[test]
fn test_oversize_transaction() {
let mut bytes = make_valid_txn_bytes();
bytes.resize(32_769, 0);
let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(
err,
RpcError::InvalidTransactionSize {
size: 32_769,
max_size: 32_768
}
));
}
#[test]
fn test_trailing_bytes() {
let mut bytes = make_valid_txn_bytes();
let orig_len = bytes.len();
bytes.insert(orig_len - TN_TXN_SIGNATURE_SZ, 0);
let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(err, RpcError::TrailingBytes { .. }));
}
#[test]
fn test_invalid_transaction_version() {
let mut bytes = make_valid_txn_bytes();
bytes[0] = 0x00; let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(err, RpcError::InvalidVersion));
}
#[test]
fn test_invalid_flags() {
let bytes = make_valid_txn_bytes_with_flags(0x07);
let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(err, RpcError::InvalidFlags));
}
#[test]
fn test_invalid_chain_id_zero() {
let mut bytes = make_valid_txn_bytes();
let hdr: &mut WireTxnHdrV1 =
bytemuck::from_bytes_mut(&mut bytes[0..core::mem::size_of::<WireTxnHdrV1>()]);
hdr.chain_id = 0;
let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(err, RpcError::InvalidChainId));
}
#[test]
fn test_transaction_too_short() {
let bytes = vec![0u8; 50]; let err = validate_wire_transaction(&bytes).unwrap_err();
assert!(matches!(err, RpcError::InvalidFormat));
}
#[test]
fn test_transaction_with_state_proof() {
use crate::tn_state_proof::{StateProof, StateProofType};
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let path_bitset = [0u8; 32]; let existing_leaf_pubkey = [7u8; 32];
let existing_leaf_hash = [8u8; 32];
let state_proof = StateProof::creation(
100,
path_bitset,
existing_leaf_pubkey,
existing_leaf_hash,
vec![],
);
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
.with_fee_payer_state_proof(&state_proof);
assert!(tx.has_fee_payer_state_proof());
assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 1);
tx.sign(&signing_key.to_bytes()).unwrap();
let bytes = tx.to_wire();
assert!(bytes.len() > core::mem::size_of::<WireTxnHdrV1>() + TN_TXN_SIGNATURE_SZ);
assert!(validate_wire_transaction(&bytes).is_ok());
let decoded_tx = Transaction::from_wire(&bytes).unwrap();
assert!(decoded_tx.has_fee_payer_state_proof());
assert!(decoded_tx.fee_payer_state_proof.is_some());
let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
assert_eq!(decoded_proof.proof_type(), StateProofType::Creation);
assert_eq!(decoded_proof.slot(), 100);
}
#[test]
fn test_transaction_with_state_proof_serialization_round_trip() {
use crate::tn_state_proof::StateProof;
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let mut path_bitset = [0u8; 32];
path_bitset[0] = 0b11; let existing_leaf_pubkey = [7u8; 32];
let existing_leaf_hash = [8u8; 32];
let sibling_hashes = vec![[9u8; 32], [10u8; 32]];
let state_proof = StateProof::creation(
200,
path_bitset,
existing_leaf_pubkey,
existing_leaf_hash,
sibling_hashes.clone(),
);
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
.with_rw_accounts(vec![[3u8; 32], [4u8; 32]])
.with_r_accounts(vec![[5u8; 32]])
.with_instructions(vec![1, 2, 3, 4])
.with_fee_payer_state_proof(&state_proof);
tx.sign(&signing_key.to_bytes()).unwrap();
let bytes = tx.to_wire();
assert!(validate_wire_transaction(&bytes).is_ok());
let decoded_tx = Transaction::from_wire(&bytes).unwrap();
assert_eq!(decoded_tx.fee_payer, tx.fee_payer);
assert_eq!(decoded_tx.program, tx.program);
assert_eq!(decoded_tx.rw_accs, tx.rw_accs);
assert_eq!(decoded_tx.r_accs, tx.r_accs);
assert_eq!(decoded_tx.instructions, tx.instructions);
assert_eq!(decoded_tx.flags, tx.flags);
assert!(decoded_tx.has_fee_payer_state_proof());
let decoded_proof = decoded_tx.fee_payer_state_proof.unwrap();
assert_eq!(decoded_proof.slot(), 200);
assert_eq!(decoded_proof.path_bitset(), &path_bitset);
}
#[test]
fn test_transaction_without_state_proof() {
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let mut tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42);
assert!(!tx.has_fee_payer_state_proof());
assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
assert!(tx.fee_payer_state_proof.is_none());
tx.sign(&signing_key.to_bytes()).unwrap();
let bytes = tx.to_wire();
assert!(validate_wire_transaction(&bytes).is_ok());
let decoded_tx = Transaction::from_wire(&bytes).unwrap();
assert!(!decoded_tx.has_fee_payer_state_proof());
assert!(decoded_tx.fee_payer_state_proof.is_none());
}
#[test]
fn test_transaction_remove_state_proof() {
use crate::tn_state_proof::StateProof;
let signing_key = SigningKey::from(&[1u8; 32]);
let verifying_key = signing_key.verifying_key();
let path_bitset = [0u8; 32];
let existing_leaf_pubkey = [7u8; 32];
let existing_leaf_hash = [8u8; 32];
let state_proof = StateProof::creation(
100,
path_bitset,
existing_leaf_pubkey,
existing_leaf_hash,
vec![],
);
let tx = Transaction::new(verifying_key.to_bytes(), [2u8; 32], 100, 42)
.with_fee_payer_state_proof(&state_proof)
.without_fee_payer_state_proof();
assert!(!tx.has_fee_payer_state_proof());
assert_eq!(tx.flags & (1 << TN_TXN_FLAG_HAS_FEE_PAYER_PROOF_BIT), 0);
assert!(tx.fee_payer_state_proof.is_none());
}
}