Skip to main content

bsv_rs/script/
evaluation_error.rs

1//! Script evaluation error with full execution context.
2//!
3//! This module provides a rich error type that captures the complete state
4//! of the script interpreter at the time of failure, enabling detailed debugging.
5
6use crate::primitives::to_hex;
7use std::fmt;
8
9/// The execution context within which an error occurred.
10#[derive(Debug, Clone, Copy, PartialEq, Eq)]
11pub enum ExecutionContext {
12    /// Error occurred while executing the unlocking script (scriptSig).
13    UnlockingScript,
14    /// Error occurred while executing the locking script (scriptPubKey).
15    LockingScript,
16}
17
18impl fmt::Display for ExecutionContext {
19    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
20        match self {
21            ExecutionContext::UnlockingScript => write!(f, "UnlockingScript"),
22            ExecutionContext::LockingScript => write!(f, "LockingScript"),
23        }
24    }
25}
26
27/// A rich error type for script evaluation failures.
28///
29/// Contains the full execution state at the time of failure, enabling
30/// detailed debugging and error reporting.
31#[derive(Debug, Clone)]
32pub struct ScriptEvaluationError {
33    /// The error message describing what went wrong.
34    pub message: String,
35    /// The TXID of the source UTXO being spent (hex, display order).
36    pub source_txid: String,
37    /// The output index of the source UTXO.
38    pub source_output_index: u32,
39    /// Whether the error occurred in the unlocking or locking script.
40    pub context: ExecutionContext,
41    /// The program counter (chunk index) when the error occurred.
42    pub program_counter: usize,
43    /// The state of the main stack at the time of failure.
44    pub stack: Vec<Vec<u8>>,
45    /// The state of the alt stack at the time of failure.
46    pub alt_stack: Vec<Vec<u8>>,
47    /// The state of the if/else condition stack.
48    pub if_stack: Vec<bool>,
49    /// Memory usage of the main stack in bytes.
50    pub stack_mem: usize,
51    /// Memory usage of the alt stack in bytes.
52    pub alt_stack_mem: usize,
53}
54
55impl ScriptEvaluationError {
56    /// Creates a new script evaluation error with the given context.
57    #[allow(clippy::too_many_arguments)]
58    pub fn new(
59        message: impl Into<String>,
60        source_txid: impl Into<String>,
61        source_output_index: u32,
62        context: ExecutionContext,
63        program_counter: usize,
64        stack: Vec<Vec<u8>>,
65        alt_stack: Vec<Vec<u8>>,
66        if_stack: Vec<bool>,
67        stack_mem: usize,
68        alt_stack_mem: usize,
69    ) -> Self {
70        Self {
71            message: message.into(),
72            source_txid: source_txid.into(),
73            source_output_index,
74            context,
75            program_counter,
76            stack,
77            alt_stack,
78            if_stack,
79            stack_mem,
80            alt_stack_mem,
81        }
82    }
83
84    /// Formats the stack as a hex string list.
85    fn format_stack(stack: &[Vec<u8>]) -> String {
86        let hex_items: Vec<String> = stack
87            .iter()
88            .map(|item| {
89                if item.is_empty() {
90                    "[]".to_string()
91                } else {
92                    to_hex(item)
93                }
94            })
95            .collect();
96        format!("[{}]", hex_items.join(", "))
97    }
98
99    /// Formats the if stack as a boolean list.
100    fn format_if_stack(if_stack: &[bool]) -> String {
101        let items: Vec<&str> = if_stack
102            .iter()
103            .map(|&b| if b { "true" } else { "false" })
104            .collect();
105        format!("[{}]", items.join(", "))
106    }
107}
108
109impl fmt::Display for ScriptEvaluationError {
110    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
111        write!(
112            f,
113            "Script evaluation error: {}\n\
114             TXID: {}, OutputIdx: {}\n\
115             Context: {}, PC: {}\n\
116             Stack: {} (len: {}, mem: {})\n\
117             AltStack: {} (len: {}, mem: {})\n\
118             IfStack: {}",
119            self.message,
120            self.source_txid,
121            self.source_output_index,
122            self.context,
123            self.program_counter,
124            Self::format_stack(&self.stack),
125            self.stack.len(),
126            self.stack_mem,
127            Self::format_stack(&self.alt_stack),
128            self.alt_stack.len(),
129            self.alt_stack_mem,
130            Self::format_if_stack(&self.if_stack),
131        )
132    }
133}
134
135impl std::error::Error for ScriptEvaluationError {}
136
137#[cfg(test)]
138mod tests {
139    use super::*;
140
141    #[test]
142    fn test_error_display() {
143        let error = ScriptEvaluationError::new(
144            "Stack underflow",
145            "abc123",
146            0,
147            ExecutionContext::LockingScript,
148            5,
149            vec![vec![1, 2, 3], vec![]],
150            vec![],
151            vec![true, false],
152            3,
153            0,
154        );
155
156        let display = format!("{}", error);
157        assert!(display.contains("Stack underflow"));
158        assert!(display.contains("abc123"));
159        assert!(display.contains("LockingScript"));
160        assert!(display.contains("PC: 5"));
161        assert!(display.contains("010203"));
162        assert!(display.contains("[]"));
163    }
164
165    #[test]
166    fn test_execution_context_display() {
167        assert_eq!(
168            format!("{}", ExecutionContext::UnlockingScript),
169            "UnlockingScript"
170        );
171        assert_eq!(
172            format!("{}", ExecutionContext::LockingScript),
173            "LockingScript"
174        );
175    }
176
177    #[test]
178    fn test_format_stack() {
179        let stack = vec![vec![0x01, 0x02], vec![], vec![0xff]];
180        let formatted = ScriptEvaluationError::format_stack(&stack);
181        assert_eq!(formatted, "[0102, [], ff]");
182    }
183
184    #[test]
185    fn test_format_if_stack() {
186        let if_stack = vec![true, false, true];
187        let formatted = ScriptEvaluationError::format_if_stack(&if_stack);
188        assert_eq!(formatted, "[true, false, true]");
189    }
190}