light_client/interface/
tx_size.rs

1//! Transaction size estimation and instruction batching.
2
3use solana_instruction::Instruction;
4use solana_pubkey::Pubkey;
5
6/// Maximum transaction size in bytes (1280 MTU - 40 IPv6 header - 8 fragment header).
7pub const PACKET_DATA_SIZE: usize = 1232;
8
9/// Error when a single instruction exceeds the maximum transaction size.
10#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct InstructionTooLargeError {
12    /// Index of the oversized instruction in the input vector.
13    pub instruction_index: usize,
14    /// Estimated size of a transaction containing only this instruction.
15    pub estimated_size: usize,
16    /// Maximum allowed transaction size.
17    pub max_size: usize,
18}
19
20impl std::fmt::Display for InstructionTooLargeError {
21    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
22        write!(
23            f,
24            "instruction at index {} exceeds max transaction size: {} > {}",
25            self.instruction_index, self.estimated_size, self.max_size
26        )
27    }
28}
29
30impl std::error::Error for InstructionTooLargeError {}
31
32/// Split instructions into groups that fit within transaction size limits.
33///
34/// Signer count is derived from instruction AccountMeta.is_signer flags plus the payer.
35///
36/// # Arguments
37/// * `instructions` - Instructions to split
38/// * `payer` - Fee payer pubkey (always counted as a signer)
39/// * `max_size` - Max tx size (defaults to PACKET_DATA_SIZE)
40///
41/// # Errors
42/// Returns `InstructionTooLargeError` if any single instruction alone exceeds `max_size`.
43pub fn split_by_tx_size(
44    instructions: Vec<Instruction>,
45    payer: &Pubkey,
46    max_size: Option<usize>,
47) -> Result<Vec<Vec<Instruction>>, InstructionTooLargeError> {
48    let max_size = max_size.unwrap_or(PACKET_DATA_SIZE);
49
50    if instructions.is_empty() {
51        return Ok(vec![]);
52    }
53
54    let mut batches = Vec::new();
55    let mut current_batch = Vec::new();
56
57    for (idx, ix) in instructions.into_iter().enumerate() {
58        let mut trial = current_batch.clone();
59        trial.push(ix.clone());
60
61        if estimate_tx_size(&trial, payer) > max_size {
62            // Check if this single instruction alone exceeds max_size
63            let single_ix_size = estimate_tx_size(std::slice::from_ref(&ix), payer);
64            if single_ix_size > max_size {
65                return Err(InstructionTooLargeError {
66                    instruction_index: idx,
67                    estimated_size: single_ix_size,
68                    max_size,
69                });
70            }
71
72            if !current_batch.is_empty() {
73                batches.push(current_batch);
74            }
75            current_batch = vec![ix];
76        } else {
77            current_batch.push(ix);
78        }
79    }
80
81    if !current_batch.is_empty() {
82        batches.push(current_batch);
83    }
84
85    Ok(batches)
86}
87
88/// Count unique signers from instructions plus the payer.
89fn count_signers(instructions: &[Instruction], payer: &Pubkey) -> usize {
90    let mut signers = vec![*payer];
91    for ix in instructions {
92        for meta in &ix.accounts {
93            if meta.is_signer && !signers.contains(&meta.pubkey) {
94                signers.push(meta.pubkey);
95            }
96        }
97    }
98    signers.len()
99}
100
101/// Estimate transaction size including signatures.
102///
103/// Signer count is derived from instruction AccountMeta.is_signer flags plus the payer.
104fn estimate_tx_size(instructions: &[Instruction], payer: &Pubkey) -> usize {
105    let num_signers = count_signers(instructions, payer);
106
107    // Collect unique accounts
108    let mut accounts = vec![*payer];
109    for ix in instructions {
110        if !accounts.contains(&ix.program_id) {
111            accounts.push(ix.program_id);
112        }
113        for meta in &ix.accounts {
114            if !accounts.contains(&meta.pubkey) {
115                accounts.push(meta.pubkey);
116            }
117        }
118    }
119
120    // Header: 3 bytes
121    let mut size = 3;
122    // Account keys: compact-u16 len + 32 bytes each
123    size += compact_len(accounts.len()) + accounts.len() * 32;
124    // Blockhash: 32 bytes
125    size += 32;
126    // Instructions
127    size += compact_len(instructions.len());
128    for ix in instructions {
129        size += 1; // program_id index
130        size += compact_len(ix.accounts.len()) + ix.accounts.len();
131        size += compact_len(ix.data.len()) + ix.data.len();
132    }
133    // Signatures
134    size += compact_len(num_signers) + num_signers * 64;
135
136    size
137}
138
139#[inline]
140fn compact_len(val: usize) -> usize {
141    if val < 0x80 {
142        1
143    } else if val < 0x4000 {
144        2
145    } else {
146        3
147    }
148}
149
150#[cfg(test)]
151mod tests {
152    use solana_instruction::AccountMeta;
153
154    use super::*;
155
156    #[test]
157    fn test_split_by_tx_size() {
158        let payer = Pubkey::new_unique();
159        let instructions: Vec<Instruction> = (0..10)
160            .map(|_| Instruction {
161                program_id: Pubkey::new_unique(),
162                accounts: (0..10)
163                    .map(|_| AccountMeta::new(Pubkey::new_unique(), false))
164                    .collect(),
165                data: vec![0u8; 200],
166            })
167            .collect();
168
169        let batches = split_by_tx_size(instructions, &payer, None).unwrap();
170        assert!(batches.len() > 1);
171
172        for batch in &batches {
173            assert!(estimate_tx_size(batch, &payer) <= PACKET_DATA_SIZE);
174        }
175    }
176
177    #[test]
178    fn test_split_by_tx_size_oversized_instruction() {
179        let payer = Pubkey::new_unique();
180
181        // Create an instruction that exceeds PACKET_DATA_SIZE on its own
182        let oversized_ix = Instruction {
183            program_id: Pubkey::new_unique(),
184            accounts: (0..5)
185                .map(|_| AccountMeta::new(Pubkey::new_unique(), false))
186                .collect(),
187            data: vec![0u8; 2000], // Large data payload
188        };
189
190        let small_ix = Instruction {
191            program_id: Pubkey::new_unique(),
192            accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
193            data: vec![0u8; 10],
194        };
195
196        // Oversized instruction at index 1
197        let instructions = vec![small_ix.clone(), oversized_ix, small_ix];
198
199        let result = split_by_tx_size(instructions, &payer, None);
200        assert!(result.is_err());
201
202        let err = result.unwrap_err();
203        assert_eq!(err.instruction_index, 1);
204        assert!(err.estimated_size > err.max_size);
205        assert_eq!(err.max_size, PACKET_DATA_SIZE);
206    }
207
208    #[test]
209    fn test_signer_count_derived_from_metadata() {
210        let payer = Pubkey::new_unique();
211        let extra_signer = Pubkey::new_unique();
212
213        // Instruction with an additional signer
214        let ix_with_signer = Instruction {
215            program_id: Pubkey::new_unique(),
216            accounts: vec![
217                AccountMeta::new(Pubkey::new_unique(), false),
218                AccountMeta::new(extra_signer, true), // is_signer = true
219            ],
220            data: vec![0u8; 10],
221        };
222
223        // Instruction without additional signers
224        let ix_no_signer = Instruction {
225            program_id: Pubkey::new_unique(),
226            accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
227            data: vec![0u8; 10],
228        };
229
230        // Payer only
231        assert_eq!(
232            count_signers(std::slice::from_ref(&ix_no_signer), &payer),
233            1
234        );
235
236        // Payer + extra signer
237        assert_eq!(
238            count_signers(std::slice::from_ref(&ix_with_signer), &payer),
239            2
240        );
241
242        // Payer duplicated in instruction should still be 1
243        let ix_payer_signer = Instruction {
244            program_id: Pubkey::new_unique(),
245            accounts: vec![AccountMeta::new(payer, true)],
246            data: vec![0u8; 10],
247        };
248        assert_eq!(count_signers(&[ix_payer_signer], &payer), 1);
249    }
250}