carbon_core/
instruction.rs

1//! Provides structures and traits for decoding and processing instructions
2//! within transactions.
3//!
4//! The module includes the following main components:
5//! - **`InstructionMetadata`**: Metadata associated with an instruction,
6//!   capturing transaction context.
7//! - **`DecodedInstruction`**: Represents an instruction that has been decoded,
8//!   with associated program ID, data, and accounts.
9//! - **`InstructionDecoder`**: A trait for decoding instructions into specific
10//!   types.
11//! - **`InstructionPipe`**: A structure that processes instructions using a
12//!   decoder and a processor.
13//! - **`InstructionPipes`**: An async trait for processing instructions within
14//!   nested contexts.
15//! - **`NestedInstruction`**: Represents instructions with potential nested
16//!   inner instructions, allowing for recursive processing.
17//!
18//! These components enable the `carbon-core` framework to handle Solana
19//! transaction instructions efficiently, decoding them into structured types
20//! and facilitating hierarchical processing.
21
22use {
23    crate::{
24        deserialize::CarbonDeserialize, error::CarbonResult, filter::Filter,
25        metrics::MetricsCollection, processor::Processor, transaction::TransactionMetadata,
26    },
27    async_trait::async_trait,
28    serde::{Deserialize, Serialize},
29    solana_instruction::AccountMeta,
30    solana_pubkey::Pubkey,
31    std::{
32        ops::{Deref, DerefMut},
33        sync::Arc,
34    },
35};
36
37/// Metadata associated with a specific instruction, including transaction-level
38/// details.
39///
40/// `InstructionMetadata` is utilized within the pipeline to associate each
41/// instruction with the broader context of its transaction, as well as its
42/// position within the instruction stack.
43///
44/// # Fields
45///
46/// - `transaction_metadata`: Metadata providing details of the entire
47///   transaction.
48/// - `stack_height`: Represents the instruction's depth within the stack, where
49///   1 is the root level.
50/// - `index`: The index of the instruction in the transaction. The index is
51///   relative within stack height and is 1-based. Note that the inner
52///   instruction indexes are grouped into one vector, so different inner
53///   instructions that have different stack heights may have continuous
54///   indexes.
55
56#[derive(Debug, Clone)]
57pub struct InstructionMetadata {
58    pub transaction_metadata: Arc<TransactionMetadata>,
59    pub stack_height: u32,
60    pub index: u32,
61    pub absolute_path: Vec<u8>,
62}
63
64#[derive(Debug)]
65enum LogType {
66    Start(usize), // stack_height
67    Data,
68    CU,
69    Finish,
70}
71
72impl InstructionMetadata {
73    /// Decodes the log events `T` thrown by this instruction.
74    ///
75    /// # Parameters
76    ///
77    /// - `T`: The event type to decode the instruction's logs into.
78    ///
79    /// # Returns
80    ///
81    /// All successfull events of the type `T` decoded from the logs of the instruction.
82    pub fn decode_log_events<T: CarbonDeserialize>(&self) -> Vec<T> {
83        self.extract_event_log_data()
84            .into_iter()
85            .filter(|log| log.len() >= 8)
86            .filter_map(|log| <T as CarbonDeserialize>::deserialize(&log[8..]))
87            .collect()
88    }
89
90    /// Extracts the `data` from log messages associated with this instruction.
91    ///
92    /// This method filters the transaction's log messages to return only those
93    /// that correspond to the current instruction, based on its stack height and
94    /// absolute path within the instruction stack.
95    ///
96    /// Returns `Vec<Vec<u8>>` containing the `data` bytes (base64 encoded) from log messages.
97    fn extract_event_log_data(&self) -> Vec<Vec<u8>> {
98        let logs = match &self.transaction_metadata.meta.log_messages {
99            Some(logs) => logs,
100            None => return Vec::new(),
101        };
102
103        let mut extracted_logs = Vec::new();
104        let mut current_stack_height = 0usize;
105        let mut last_stack_height = 0usize;
106
107        let mut position_at_level: std::collections::HashMap<usize, u8> =
108            std::collections::HashMap::new();
109
110        for log in logs {
111            let parsed_log = self.parse_log(log);
112
113            match parsed_log {
114                LogType::Start(stack_height) => {
115                    current_stack_height = stack_height;
116
117                    let current_pos = if stack_height > last_stack_height {
118                        0
119                    } else {
120                        position_at_level
121                            .get(&stack_height)
122                            .map(|&pos| pos + 1)
123                            .unwrap_or(0)
124                    };
125
126                    position_at_level.insert(stack_height, current_pos);
127                    last_stack_height = stack_height;
128                }
129                LogType::Finish => {
130                    current_stack_height = current_stack_height.saturating_sub(1);
131                }
132                _ => {}
133            }
134
135            let current_path: Vec<u8> = (1..=current_stack_height)
136                .map(|level| position_at_level.get(&level).copied().unwrap_or(0))
137                .collect();
138
139            if current_path == self.absolute_path && matches!(parsed_log, LogType::Data) {
140                if let Some(data) = log.split_whitespace().last() {
141                    if let Ok(buf) =
142                        base64::Engine::decode(&base64::engine::general_purpose::STANDARD, data)
143                    {
144                        extracted_logs.push(buf);
145                    }
146                }
147            }
148        }
149
150        extracted_logs
151    }
152
153    /// Parses a log line to determine its type
154    fn parse_log(&self, log: &str) -> LogType {
155        if log.starts_with("Program ") && log.contains(" invoke [") {
156            let parts: Vec<&str> = log.split_whitespace().collect();
157            if parts.len() >= 4 && parts[0] == "Program" && parts[2] == "invoke" {
158                let level_str = parts[3].trim_start_matches('[').trim_end_matches(']');
159                if let Ok(level) = level_str.parse::<usize>() {
160                    return LogType::Start(level);
161                }
162            }
163        } else if log.starts_with("Program ")
164            && (log.ends_with(" success") || log.contains(" failed"))
165        {
166            let parts: Vec<&str> = log.split_whitespace().collect();
167            if parts.len() >= 3 && parts[0] == "Program" {
168                return LogType::Finish;
169            }
170        } else if log.contains("consumed") && log.contains("compute units") {
171            return LogType::CU;
172        }
173
174        LogType::Data
175    }
176}
177
178pub type InstructionsWithMetadata = Vec<(InstructionMetadata, solana_instruction::Instruction)>;
179
180/// A decoded instruction containing program ID, data, and associated accounts.
181///
182/// The `DecodedInstruction` struct represents the outcome of decoding a raw
183/// instruction, encapsulating its program ID, parsed data, and the accounts
184/// involved.
185///
186/// # Type Parameters
187///
188/// - `T`: The type representing the decoded data for the instruction.
189///
190/// # Fields
191///
192/// - `program_id`: The program ID that owns the instruction.
193/// - `data`: The decoded data payload for the instruction, of type `T`.
194/// - `accounts`: A vector of `AccountMeta`, representing the accounts involved
195///   in the instruction.
196
197#[derive(Debug, Clone, Deserialize, Serialize)]
198pub struct DecodedInstruction<T> {
199    pub program_id: Pubkey,
200    pub data: T,
201    pub accounts: Vec<AccountMeta>,
202}
203
204/// A trait for decoding Solana instructions into a structured type.
205///
206/// Implement the `InstructionDecoder` trait for types that can decode raw
207/// instructions into a more meaningful structure, providing
208/// application-specific logic.
209///
210/// # Type Parameters
211///
212/// - `InstructionType`: The type into which the instruction data will be
213///   decoded.
214///
215/// # Required Methods
216///
217/// - `decode_instruction`: Decodes a raw Solana `Instruction` into a
218///   `DecodedInstruction`.
219pub trait InstructionDecoder<'a> {
220    type InstructionType;
221
222    fn decode_instruction(
223        &self,
224        instruction: &'a solana_instruction::Instruction,
225    ) -> Option<DecodedInstruction<Self::InstructionType>>;
226}
227
228/// The input type for the instruction processor.
229///
230/// - `T`: The instruction type
231pub type InstructionProcessorInputType<T> = (
232    InstructionMetadata,
233    DecodedInstruction<T>,
234    NestedInstructions,
235    solana_instruction::Instruction,
236);
237
238/// A processing pipeline for instructions, using a decoder and processor.
239///
240/// The `InstructionPipe` structure enables the processing of decoded
241/// instructions, pairing an `InstructionDecoder` with a `Processor`. It
242/// supports generic instruction types.
243///
244/// # Type Parameters
245///
246/// - `T`: The type representing the decoded instruction data.
247///
248/// # Fields
249///
250/// - `decoder`: The decoder used for parsing instructions.
251/// - `processor`: The processor that handles decoded instructions.
252/// - `filters`: A collection of filters that determine which instruction
253///   updates should be processed. Each filter in this collection is applied to
254///   incoming instruction updates, and only updates that pass all filters
255///   (return `true`) will be processed. If this collection is empty, all
256///   updates are processed.
257pub struct InstructionPipe<T: Send> {
258    pub decoder:
259        Box<dyn for<'a> InstructionDecoder<'a, InstructionType = T> + Send + Sync + 'static>,
260    pub processor:
261        Box<dyn Processor<InputType = InstructionProcessorInputType<T>> + Send + Sync + 'static>,
262    pub filters: Vec<Box<dyn Filter + Send + Sync + 'static>>,
263}
264
265/// An async trait for processing instructions within nested contexts.
266///
267/// The `InstructionPipes` trait allows for recursive processing of instructions
268/// that may contain nested instructions. This enables complex, hierarchical
269/// instruction handling for transactions.
270///
271/// # Required Methods
272///
273/// - `run`: Processes a `NestedInstruction`, recursively processing any inner
274///   instructions.
275/// - `filters`: Returns a reference to the filters associated with this pipe,
276///   which are used by the pipeline to determine which instruction updates
277///   should be processed.
278#[async_trait]
279pub trait InstructionPipes<'a>: Send + Sync {
280    async fn run(
281        &mut self,
282        nested_instruction: &NestedInstruction,
283        metrics: Arc<MetricsCollection>,
284    ) -> CarbonResult<()>;
285    fn filters(&self) -> &Vec<Box<dyn Filter + Send + Sync + 'static>>;
286}
287
288#[async_trait]
289impl<T: Send + 'static> InstructionPipes<'_> for InstructionPipe<T> {
290    async fn run(
291        &mut self,
292        nested_instruction: &NestedInstruction,
293        metrics: Arc<MetricsCollection>,
294    ) -> CarbonResult<()> {
295        log::trace!(
296            "InstructionPipe::run(nested_instruction: {:?}, metrics)",
297            nested_instruction,
298        );
299
300        if let Some(decoded_instruction) = self
301            .decoder
302            .decode_instruction(&nested_instruction.instruction)
303        {
304            self.processor
305                .process(
306                    (
307                        nested_instruction.metadata.clone(),
308                        decoded_instruction,
309                        nested_instruction.inner_instructions.clone(),
310                        nested_instruction.instruction.clone(),
311                    ),
312                    metrics.clone(),
313                )
314                .await?;
315        }
316
317        for nested_inner_instruction in nested_instruction.inner_instructions.iter() {
318            self.run(nested_inner_instruction, metrics.clone()).await?;
319        }
320
321        Ok(())
322    }
323
324    fn filters(&self) -> &Vec<Box<dyn Filter + Send + Sync + 'static>> {
325        &self.filters
326    }
327}
328
329/// Represents a nested instruction with metadata, including potential inner
330/// instructions.
331///
332/// The `NestedInstruction` struct allows for recursive instruction handling,
333/// where each instruction may have associated metadata and a list of nested
334/// instructions.
335///
336/// # Fields
337///
338/// - `metadata`: The metadata associated with the instruction.
339/// - `instruction`: The Solana instruction being processed.
340/// - `inner_instructions`: A vector of `NestedInstruction`, representing any
341///   nested instructions.
342#[derive(Debug, Clone)]
343pub struct NestedInstruction {
344    pub metadata: InstructionMetadata,
345    pub instruction: solana_instruction::Instruction,
346    pub inner_instructions: NestedInstructions,
347}
348
349#[derive(Debug, Default)]
350pub struct NestedInstructions(pub Vec<NestedInstruction>);
351
352impl NestedInstructions {
353    pub fn len(&self) -> usize {
354        self.0.len()
355    }
356
357    pub fn is_empty(&self) -> bool {
358        self.len() == 0
359    }
360
361    pub fn push(&mut self, nested_instruction: NestedInstruction) {
362        self.0.push(nested_instruction);
363    }
364}
365
366impl Deref for NestedInstructions {
367    type Target = [NestedInstruction];
368
369    fn deref(&self) -> &[NestedInstruction] {
370        &self.0[..]
371    }
372}
373
374impl DerefMut for NestedInstructions {
375    fn deref_mut(&mut self) -> &mut [NestedInstruction] {
376        &mut self.0[..]
377    }
378}
379
380impl Clone for NestedInstructions {
381    fn clone(&self) -> Self {
382        NestedInstructions(self.0.clone())
383    }
384}
385
386impl IntoIterator for NestedInstructions {
387    type Item = NestedInstruction;
388    type IntoIter = std::vec::IntoIter<NestedInstruction>;
389
390    fn into_iter(self) -> Self::IntoIter {
391        self.0.into_iter()
392    }
393}
394
395/// Nests instructions based on stack height, producing a hierarchy of
396/// `NestedInstruction`.
397///
398/// This function organizes instructions into a nested structure, enabling
399/// hierarchical transaction analysis. Instructions are nested according to
400/// their stack height, forming a tree-like structure.
401///
402/// # Parameters
403///
404/// - `instructions`: A list of tuples containing `InstructionMetadata` and
405///   instructions.
406///
407/// # Returns
408///
409/// A vector of `NestedInstruction`, representing the instructions organized by
410/// stack depth.
411impl From<InstructionsWithMetadata> for NestedInstructions {
412    fn from(instructions: InstructionsWithMetadata) -> Self {
413        log::trace!("from(instructions: {:?})", instructions);
414
415        // To avoid reallocations that result in dangling pointers.
416        // Therefore the number of "push"s must be calculated to set the capacity
417        let estimated_capacity = instructions
418            .iter()
419            .filter(|(meta, _)| meta.stack_height == 1)
420            .count();
421
422        UnsafeNestedBuilder::new(estimated_capacity).build(instructions)
423    }
424}
425
426// https://github.com/anza-xyz/agave/blob/master/program-runtime/src/execution_budget.rs#L7
427pub const MAX_INSTRUCTION_STACK_DEPTH: usize = 5;
428
429pub struct UnsafeNestedBuilder {
430    nested_ixs: Vec<NestedInstruction>,
431    level_ptrs: [Option<*mut NestedInstruction>; MAX_INSTRUCTION_STACK_DEPTH],
432}
433
434impl UnsafeNestedBuilder {
435    /// ## SAFETY:
436    /// Make sure `capacity` is large enough to avoid capacity expansion caused
437    /// by `push`
438    pub fn new(capacity: usize) -> Self {
439        Self {
440            nested_ixs: Vec::with_capacity(capacity),
441            level_ptrs: [None; MAX_INSTRUCTION_STACK_DEPTH],
442        }
443    }
444
445    pub fn build(mut self, instructions: InstructionsWithMetadata) -> NestedInstructions {
446        for (metadata, instruction) in instructions {
447            let stack_height = metadata.stack_height as usize;
448
449            assert!(stack_height > 0);
450            assert!(stack_height <= MAX_INSTRUCTION_STACK_DEPTH);
451
452            for ptr in &mut self.level_ptrs[stack_height..] {
453                *ptr = None;
454            }
455
456            let new_instruction = NestedInstruction {
457                metadata,
458                instruction,
459                inner_instructions: NestedInstructions::default(),
460            };
461
462            // SAFETY:The following operation is safe.
463            // because:
464            // 1. All pointers come from pre-allocated Vec (no extension)
465            // 2. level_ptr does not guarantee any aliasing
466            // 3. Lifecycle is limited to the build() method
467            unsafe {
468                if stack_height == 1 {
469                    self.nested_ixs.push(new_instruction);
470                    let ptr = self.nested_ixs.last_mut().unwrap_unchecked() as *mut _;
471                    self.level_ptrs[0] = Some(ptr);
472                } else if let Some(parent_ptr) = self.level_ptrs[stack_height - 2] {
473                    (*parent_ptr).inner_instructions.push(new_instruction);
474                    let ptr = (*parent_ptr)
475                        .inner_instructions
476                        .last_mut()
477                        .unwrap_unchecked() as *mut _;
478                    self.level_ptrs[stack_height - 1] = Some(ptr);
479                }
480            }
481        }
482
483        NestedInstructions(self.nested_ixs)
484    }
485}
486
487#[cfg(test)]
488mod tests {
489
490    use {
491        super::*, solana_instruction::Instruction, solana_transaction_status::TransactionStatusMeta,
492    };
493
494    fn create_instruction_with_metadata(
495        index: u32,
496        stack_height: u32,
497        absolute_path: Vec<u8>,
498    ) -> (InstructionMetadata, Instruction) {
499        let metadata = InstructionMetadata {
500            transaction_metadata: Arc::new(TransactionMetadata {
501                meta: TransactionStatusMeta {
502                    log_messages: Some(vec!["Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK invoke [1]".to_string(), "Program data: QMbN6CYIceLh9Vdh3ndmrpChVVDCYAykCoHLEYdQWNcAxLJNu7nWNHiJzugda0JT2xgyBCWGtm7/oWjb/wT2kcbwA0JRUuwSV88ABSiDPpXudmLYK2jIBhqh3sTXxnR7WMgtjWsyqjga53NruXU9Dj/hyRRE/RQ9xCEh3052KbW6tbtNksNK4HIr+0wAAAAAAAAAAAAAAACz/t2FxQIAAAAAAAAAAAAAACdJpynsFrOoMAAAAAAAAAD4JhBoAxAAAAAAAAAAAAAAhC8BAA==".to_string(), "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK consumed 91799 of 185765 compute units".to_string(), "Program CAMMCzo5YL8w4VFF8KVHrK22GGUsp5VTaW7grrKgrWqK success".to_string()]),
503                    ..Default::default()
504                },
505                ..Default::default()
506            }),
507            stack_height,
508            index,
509            absolute_path,
510        };
511        let instruction = Instruction {
512            program_id: Pubkey::new_unique(),
513            accounts: vec![AccountMeta::new(Pubkey::new_unique(), false)],
514            data: vec![],
515        };
516        (metadata, instruction)
517    }
518
519    #[test]
520    fn test_nested_instructions_single_level() {
521        let instructions = vec![
522            create_instruction_with_metadata(1, 1, vec![1]),
523            create_instruction_with_metadata(2, 1, vec![2]),
524        ];
525        let nested_instructions: NestedInstructions = instructions.into();
526        assert_eq!(nested_instructions.len(), 2);
527        assert!(nested_instructions[0].inner_instructions.is_empty());
528        assert!(nested_instructions[1].inner_instructions.is_empty());
529    }
530
531    #[test]
532    fn test_nested_instructions_empty() {
533        let instructions: InstructionsWithMetadata = vec![];
534        let nested_instructions: NestedInstructions = instructions.into();
535        assert!(nested_instructions.is_empty());
536    }
537
538    #[test]
539    fn test_deep_nested_instructions() {
540        let instructions = vec![
541            create_instruction_with_metadata(0, 1, vec![0]),
542            create_instruction_with_metadata(0, 1, vec![0]),
543            create_instruction_with_metadata(1, 2, vec![0, 1]),
544            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
545            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
546            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
547            create_instruction_with_metadata(1, 3, vec![0, 1, 1]),
548        ];
549
550        let nested_instructions: NestedInstructions = instructions.into();
551        assert_eq!(nested_instructions.len(), 2);
552        assert_eq!(nested_instructions.0[1].inner_instructions.len(), 1);
553    }
554
555    #[test]
556    fn test_extract_event_log_data() {
557        let logs = create_instruction_with_metadata(0, 1, vec![0])
558            .0
559            .extract_event_log_data();
560        assert_eq!(logs.len(), 1);
561        assert_eq!(
562            logs[0],
563            base64::Engine::decode(
564                &base64::engine::general_purpose::STANDARD,
565                "QMbN6CYIceLh9Vdh3ndmrpChVVDCYAykCoHLEYdQWNcAxLJNu7nWNHiJzugda0JT2xgyBCWGtm7/oWjb/wT2kcbwA0JRUuwSV88ABSiDPpXudmLYK2jIBhqh3sTXxnR7WMgtjWsyqjga53NruXU9Dj/hyRRE/RQ9xCEh3052KbW6tbtNksNK4HIr+0wAAAAAAAAAAAAAAACz/t2FxQIAAAAAAAAAAAAAACdJpynsFrOoMAAAAAAAAAD4JhBoAxAAAAAAAAAAAAAAhC8BAA=="
566            )
567            .unwrap()
568        );
569    }
570}