1use solana_client::nonblocking::rpc_client::RpcClient;
2use solana_message::{compiled_instruction::CompiledInstruction, MessageHeader, VersionedMessage};
3use solana_sdk::{
4 instruction::{AccountMeta, Instruction},
5 pubkey::Pubkey,
6 transaction::VersionedTransaction,
7};
8
9use crate::{
10 config::LighthouseConfig,
11 constant::{LIGHTHOUSE_PROGRAM_ID, MAX_TRANSACTION_SIZE},
12 error::KoraError,
13 sanitize_error,
14};
15
16const ASSERT_ACCOUNT_INFO_DISCRIMINATOR: u8 = 5;
18
19const LOG_LEVEL_SILENT: u8 = 0;
21
22const INTEGER_OPERATOR_GTE: u8 = 4;
24
25const ACCOUNT_INFO_ASSERTION_LAMPORTS: u8 = 0;
27
28pub struct LighthouseUtil {}
29
30impl LighthouseUtil {
31 pub async fn add_fee_payer_assertion(
38 transaction: &mut VersionedTransaction,
39 rpc_client: &RpcClient,
40 fee_payer: &Pubkey,
41 estimated_fee: u64,
42 config: &LighthouseConfig,
43 will_send: bool,
44 ) -> Result<(), KoraError> {
45 if !config.enabled || will_send {
46 return Ok(());
47 }
48
49 let current_balance = rpc_client.get_balance(fee_payer).await.map_err(|e| {
50 KoraError::RpcError(format!(
51 "Failed to fetch fee payer balance for Lighthouse assertion: {}",
52 sanitize_error!(e)
53 ))
54 })?;
55 let min_expected = current_balance.saturating_sub(estimated_fee);
56
57 if min_expected == 0 {
58 log::warn!(
59 "Fee payer {} has balance {} which may be insufficient for estimated fee {}",
60 fee_payer,
61 current_balance,
62 estimated_fee
63 );
64 }
65
66 let assertion_ix = Self::build_fee_payer_assertion(fee_payer, min_expected);
67 Self::append_lighthouse_assertion(transaction, assertion_ix, config)
68 }
69
70 fn build_assert_account_info_data(min_lamports: u64) -> Vec<u8> {
72 let mut data = Vec::with_capacity(12);
73
74 data.push(ASSERT_ACCOUNT_INFO_DISCRIMINATOR);
76
77 data.push(LOG_LEVEL_SILENT);
79
80 data.push(ACCOUNT_INFO_ASSERTION_LAMPORTS);
82
83 data.extend_from_slice(&min_lamports.to_le_bytes());
85
86 data.push(INTEGER_OPERATOR_GTE);
88
89 data
90 }
91
92 fn build_fee_payer_assertion(fee_payer: &Pubkey, min_lamports: u64) -> Instruction {
95 let data = Self::build_assert_account_info_data(min_lamports);
96
97 Instruction {
98 program_id: LIGHTHOUSE_PROGRAM_ID,
99 accounts: vec![AccountMeta::new_readonly(*fee_payer, false)],
100 data,
101 }
102 }
103
104 fn find_or_add_account(
106 account_keys: &mut Vec<Pubkey>,
107 pubkey: &Pubkey,
108 ) -> Result<(u8, bool), KoraError> {
109 if let Some(index) = account_keys.iter().position(|k| k == pubkey) {
110 Ok((index as u8, false))
111 } else {
112 if account_keys.len() >= 256 {
113 return Err(KoraError::ValidationError(
114 "Transaction has too many accounts (max 256)".to_string(),
115 ));
116 }
117 let index = account_keys.len() as u8;
118 account_keys.push(*pubkey);
119 Ok((index, true))
120 }
121 }
122
123 fn increment_readonly_unsigned_accounts(header: &mut MessageHeader) -> Result<(), KoraError> {
124 header.num_readonly_unsigned_accounts =
125 header.num_readonly_unsigned_accounts.checked_add(1).ok_or_else(|| {
126 KoraError::ValidationError(
127 "num_readonly_unsigned_accounts overflow when appending instruction"
128 .to_string(),
129 )
130 })?;
131 Ok(())
132 }
133
134 fn append_instruction_to_transaction(
136 transaction: &mut VersionedTransaction,
137 instruction: Instruction,
138 ) -> Result<(), KoraError> {
139 match &mut transaction.message {
140 VersionedMessage::Legacy(message) => {
141 let (program_id_index, program_added) =
142 Self::find_or_add_account(&mut message.account_keys, &instruction.program_id)?;
143 if program_added {
144 Self::increment_readonly_unsigned_accounts(&mut message.header)?;
145 }
146
147 let mut account_indices: Vec<u8> = Vec::with_capacity(instruction.accounts.len());
148 for meta in &instruction.accounts {
149 let (index, added) =
150 Self::find_or_add_account(&mut message.account_keys, &meta.pubkey)?;
151 if added {
152 if meta.is_signer || meta.is_writable {
153 return Err(KoraError::ValidationError(
154 "Appending new signer/writable accounts is not supported"
155 .to_string(),
156 ));
157 }
158 Self::increment_readonly_unsigned_accounts(&mut message.header)?;
159 }
160 account_indices.push(index);
161 }
162
163 message.instructions.push(CompiledInstruction {
164 program_id_index,
165 accounts: account_indices,
166 data: instruction.data,
167 });
168
169 Ok(())
170 }
171 VersionedMessage::V0(message) => {
172 let (program_id_index, program_added) =
173 Self::find_or_add_account(&mut message.account_keys, &instruction.program_id)?;
174 if program_added {
175 Self::increment_readonly_unsigned_accounts(&mut message.header)?;
176 }
177
178 let mut account_indices: Vec<u8> = Vec::with_capacity(instruction.accounts.len());
179 for meta in &instruction.accounts {
180 let (index, added) =
181 Self::find_or_add_account(&mut message.account_keys, &meta.pubkey)?;
182 if added {
183 if meta.is_signer || meta.is_writable {
184 return Err(KoraError::ValidationError(
185 "Appending new signer/writable accounts is not supported"
186 .to_string(),
187 ));
188 }
189 Self::increment_readonly_unsigned_accounts(&mut message.header)?;
190 }
191 account_indices.push(index);
192 }
193
194 message.instructions.push(CompiledInstruction {
195 program_id_index,
196 accounts: account_indices,
197 data: instruction.data,
198 });
199
200 Ok(())
201 }
202 }
203 }
204
205 pub(crate) fn append_lighthouse_assertion(
208 transaction: &mut VersionedTransaction,
209 assertion_ix: Instruction,
210 config: &LighthouseConfig,
211 ) -> Result<(), KoraError> {
212 let mut tx_with_assertion = transaction.clone();
214 Self::append_instruction_to_transaction(&mut tx_with_assertion, assertion_ix)?;
215
216 let new_size = bincode::serialize(&tx_with_assertion)
217 .map_err(|e| {
218 KoraError::SerializationError(sanitize_error!(format!(
219 "Failed to serialize transaction: {e}"
220 )))
221 })?
222 .len();
223
224 if new_size > MAX_TRANSACTION_SIZE {
225 if config.fail_if_transaction_size_overflow {
226 return Err(KoraError::ValidationError(format!(
227 "Adding Lighthouse assertion would exceed transaction size limit ({} > {})",
228 new_size, MAX_TRANSACTION_SIZE
229 )));
230 } else {
231 log::warn!(
232 "Lighthouse assertion would exceed transaction size limit ({} > {}). Skipping.",
233 new_size,
234 MAX_TRANSACTION_SIZE
235 );
236 return Ok(());
237 }
238 }
239
240 *transaction = tx_with_assertion;
242 Ok(())
243 }
244}
245
246#[cfg(test)]
247mod tests {
248 use super::*;
249 use solana_message::{v0, Message, VersionedMessage};
250 use solana_sdk::{hash::Hash, instruction::AccountMeta, signature::Keypair, signer::Signer};
251
252 #[test]
253 fn test_build_assert_account_info_data() {
254 let data = LighthouseUtil::build_assert_account_info_data(1_000_000);
255
256 assert_eq!(data.len(), 12);
258 assert_eq!(data[0], 5); assert_eq!(data[1], 0); assert_eq!(data[2], 0); assert_eq!(u64::from_le_bytes(data[3..11].try_into().unwrap()), 1_000_000);
263 assert_eq!(data[11], 4); }
265
266 #[test]
267 fn test_build_fee_payer_assertion() {
268 let fee_payer = Keypair::new().pubkey();
269 let min_lamports = 1_000_000;
270
271 let ix = LighthouseUtil::build_fee_payer_assertion(&fee_payer, min_lamports);
272
273 assert_eq!(ix.data.len(), 12);
274 assert_eq!(ix.accounts.len(), 1);
275 assert_eq!(ix.accounts[0].pubkey, fee_payer);
276 assert!(!ix.accounts[0].is_signer);
277 assert!(!ix.accounts[0].is_writable);
278 }
279
280 #[test]
281 fn test_append_lighthouse_assertion_legacy() {
282 let keypair = Keypair::new();
283 let program_id = Pubkey::new_unique();
284
285 let instruction = Instruction::new_with_bytes(
286 program_id,
287 &[1, 2, 3],
288 vec![AccountMeta::new(keypair.pubkey(), true)],
289 );
290
291 let message =
292 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
293 let mut transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
294
295 let original_ix_count = transaction.message.instructions().len();
296 let original_readonly_unsigned =
297 transaction.message.header().num_readonly_unsigned_accounts;
298
299 let assertion_ix = LighthouseUtil::build_fee_payer_assertion(&keypair.pubkey(), 1_000_000);
300 let config = LighthouseConfig { enabled: true, fail_if_transaction_size_overflow: true };
301
302 let result =
303 LighthouseUtil::append_lighthouse_assertion(&mut transaction, assertion_ix, &config);
304 assert!(result.is_ok());
305
306 assert_eq!(transaction.message.instructions().len(), original_ix_count + 1);
307 assert_eq!(
308 transaction.message.header().num_readonly_unsigned_accounts,
309 original_readonly_unsigned + 1
310 );
311 assert!(transaction.message.static_account_keys().contains(&LIGHTHOUSE_PROGRAM_ID));
312 }
313
314 #[test]
315 fn test_append_lighthouse_assertion_v0() {
316 let keypair = Keypair::new();
317 let program_id = Pubkey::new_unique();
318
319 let v0_message = v0::Message {
320 header: solana_message::MessageHeader {
321 num_required_signatures: 1,
322 num_readonly_signed_accounts: 0,
323 num_readonly_unsigned_accounts: 1,
324 },
325 account_keys: vec![keypair.pubkey(), program_id],
326 recent_blockhash: Hash::new_unique(),
327 instructions: vec![solana_message::compiled_instruction::CompiledInstruction {
328 program_id_index: 1,
329 accounts: vec![0],
330 data: vec![1, 2, 3],
331 }],
332 address_table_lookups: vec![],
333 };
334
335 let message = VersionedMessage::V0(v0_message);
336 let mut transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
337
338 let original_ix_count = transaction.message.instructions().len();
339 let original_readonly_unsigned =
340 transaction.message.header().num_readonly_unsigned_accounts;
341
342 let assertion_ix = LighthouseUtil::build_fee_payer_assertion(&keypair.pubkey(), 1_000_000);
343 let config = LighthouseConfig { enabled: true, fail_if_transaction_size_overflow: true };
344
345 let result =
346 LighthouseUtil::append_lighthouse_assertion(&mut transaction, assertion_ix, &config);
347 assert!(result.is_ok());
348
349 assert_eq!(transaction.message.instructions().len(), original_ix_count + 1);
350 assert_eq!(
351 transaction.message.header().num_readonly_unsigned_accounts,
352 original_readonly_unsigned + 1
353 );
354 assert!(transaction.message.static_account_keys().contains(&LIGHTHOUSE_PROGRAM_ID));
355 }
356
357 #[test]
358 fn test_append_lighthouse_assertion_header_unchanged_when_lighthouse_program_exists() {
359 let keypair = Keypair::new();
360
361 let instruction = Instruction::new_with_bytes(
362 LIGHTHOUSE_PROGRAM_ID,
363 &[1, 2, 3],
364 vec![AccountMeta::new(keypair.pubkey(), true)],
365 );
366
367 let message =
368 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
369 let mut transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
370 let original_readonly_unsigned =
371 transaction.message.header().num_readonly_unsigned_accounts;
372
373 let assertion_ix = LighthouseUtil::build_fee_payer_assertion(&keypair.pubkey(), 1_000_000);
374 let config = LighthouseConfig { enabled: true, fail_if_transaction_size_overflow: true };
375
376 let result =
377 LighthouseUtil::append_lighthouse_assertion(&mut transaction, assertion_ix, &config);
378 assert!(result.is_ok());
379 assert_eq!(
380 transaction.message.header().num_readonly_unsigned_accounts,
381 original_readonly_unsigned
382 );
383 }
384
385 #[test]
386 fn test_overflow_skip_behavior() {
387 let keypair = Keypair::new();
388 let program_id = Pubkey::new_unique();
389
390 let large_data = vec![0u8; 1100];
391 let instruction = Instruction::new_with_bytes(
392 program_id,
393 &large_data,
394 vec![AccountMeta::new(keypair.pubkey(), true)],
395 );
396
397 let message =
398 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
399 let mut transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
400
401 let original_ix_count = transaction.message.instructions().len();
402
403 let assertion_ix = LighthouseUtil::build_fee_payer_assertion(&keypair.pubkey(), 1_000_000);
404 let config = LighthouseConfig { enabled: true, fail_if_transaction_size_overflow: false };
405
406 let result =
407 LighthouseUtil::append_lighthouse_assertion(&mut transaction, assertion_ix, &config);
408 assert!(result.is_ok());
409
410 assert_eq!(transaction.message.instructions().len(), original_ix_count);
411 }
412
413 #[test]
414 fn test_overflow_fail_behavior() {
415 let keypair = Keypair::new();
416 let program_id = Pubkey::new_unique();
417
418 let large_data = vec![0u8; 1100];
419 let instruction = Instruction::new_with_bytes(
420 program_id,
421 &large_data,
422 vec![AccountMeta::new(keypair.pubkey(), true)],
423 );
424
425 let message =
426 VersionedMessage::Legacy(Message::new(&[instruction], Some(&keypair.pubkey())));
427 let mut transaction = VersionedTransaction::try_new(message, &[&keypair]).unwrap();
428
429 let assertion_ix = LighthouseUtil::build_fee_payer_assertion(&keypair.pubkey(), 1_000_000);
430 let config = LighthouseConfig { enabled: true, fail_if_transaction_size_overflow: true };
431
432 let result =
433 LighthouseUtil::append_lighthouse_assertion(&mut transaction, assertion_ix, &config);
434 assert!(result.is_err());
435
436 if let Err(KoraError::ValidationError(msg)) = result {
437 assert!(msg.contains("exceed transaction size limit"));
438 } else {
439 panic!("Expected ValidationError");
440 }
441 }
442}