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/// A LOCAL interpreter resource the evaluation ran out of — the TypeScript
32/// SDK's `ScriptResourceLimitError` (`ScriptResource`): `'stack'`,
33/// `'alt-stack'`, `'element-size'`. Resource exhaustion is a verdict about the
34/// EVALUATOR's budget, never about the script's validity on the network, and
35/// a caller (an overlay door, a wallet) must be able to tell the two apart.
36#[derive(Debug, Clone, Copy, PartialEq, Eq)]
37pub enum ScriptResource {
38    /// The main stack's memory budget (`memory_limit`).
39    Stack,
40    /// The alt stack's memory budget (`memory_limit`).
41    AltStack,
42    /// A single element the script asked to allocate (`OP_NUM2BIN`'s size
43    /// operand), refused BEFORE the allocation.
44    ElementSize,
45}
46
47impl fmt::Display for ScriptResource {
48    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
49        match self {
50            ScriptResource::Stack => write!(f, "stack"),
51            ScriptResource::AltStack => write!(f, "alt-stack"),
52            ScriptResource::ElementSize => write!(f, "element-size"),
53        }
54    }
55}
56
57/// The resource a [`ScriptEvaluationError`] ran out of, with the limit and
58/// the attempted usage (the reference's `ScriptResourceLimitError` fields).
59#[derive(Debug, Clone, Copy, PartialEq, Eq)]
60pub struct ScriptResourceLimit {
61    pub resource: ScriptResource,
62    pub limit: usize,
63    pub attempted: usize,
64}
65
66#[derive(Debug, Clone)]
67pub struct ScriptEvaluationError {
68    /// The error message describing what went wrong.
69    pub message: String,
70    /// The TXID of the source UTXO being spent (hex, display order).
71    pub source_txid: String,
72    /// The output index of the source UTXO.
73    pub source_output_index: u32,
74    /// Whether the error occurred in the unlocking or locking script.
75    pub context: ExecutionContext,
76    /// The program counter (chunk index) when the error occurred.
77    pub program_counter: usize,
78    /// The state of the main stack at the time of failure.
79    pub stack: Vec<Vec<u8>>,
80    /// The state of the alt stack at the time of failure.
81    pub alt_stack: Vec<Vec<u8>>,
82    /// The state of the if/else condition stack.
83    pub if_stack: Vec<bool>,
84    /// Memory usage of the main stack in bytes.
85    pub stack_mem: usize,
86    /// Memory usage of the alt stack in bytes.
87    pub alt_stack_mem: usize,
88    /// `Some` when the evaluation stopped because a LOCAL resource budget was
89    /// exhausted (the TypeScript SDK throws a distinct
90    /// `ScriptResourceLimitError` for these); `None` for every verdict about
91    /// the script itself. Added in 0.3.23 (reference parity).
92    pub resource_limit: Option<ScriptResourceLimit>,
93}
94
95impl ScriptEvaluationError {
96    /// Creates a new script evaluation error with the given context.
97    #[allow(clippy::too_many_arguments)]
98    pub fn new(
99        message: impl Into<String>,
100        source_txid: impl Into<String>,
101        source_output_index: u32,
102        context: ExecutionContext,
103        program_counter: usize,
104        stack: Vec<Vec<u8>>,
105        alt_stack: Vec<Vec<u8>>,
106        if_stack: Vec<bool>,
107        stack_mem: usize,
108        alt_stack_mem: usize,
109    ) -> Self {
110        Self {
111            message: message.into(),
112            source_txid: source_txid.into(),
113            source_output_index,
114            context,
115            program_counter,
116            stack,
117            alt_stack,
118            if_stack,
119            stack_mem,
120            alt_stack_mem,
121            resource_limit: None,
122        }
123    }
124
125    /// Mark this error as a LOCAL resource exhaustion (the reference's
126    /// `ScriptResourceLimitError`): the evaluator's budget, not the script's
127    /// validity.
128    pub fn with_resource_limit(mut self, limit: ScriptResourceLimit) -> Self {
129        self.resource_limit = Some(limit);
130        self
131    }
132
133    /// Whether the evaluation stopped on a LOCAL resource budget (see
134    /// [`ScriptEvaluationError::resource_limit`]) rather than on the script.
135    pub fn is_resource_limit(&self) -> bool {
136        self.resource_limit.is_some()
137    }
138
139    /// Formats the stack as a hex string list.
140    fn format_stack(stack: &[Vec<u8>]) -> String {
141        let hex_items: Vec<String> = stack
142            .iter()
143            .map(|item| {
144                if item.is_empty() {
145                    "[]".to_string()
146                } else {
147                    to_hex(item)
148                }
149            })
150            .collect();
151        format!("[{}]", hex_items.join(", "))
152    }
153
154    /// Formats the if stack as a boolean list.
155    fn format_if_stack(if_stack: &[bool]) -> String {
156        let items: Vec<&str> = if_stack
157            .iter()
158            .map(|&b| if b { "true" } else { "false" })
159            .collect();
160        format!("[{}]", items.join(", "))
161    }
162}
163
164impl fmt::Display for ScriptEvaluationError {
165    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166        write!(
167            f,
168            "Script evaluation error: {}\n\
169             TXID: {}, OutputIdx: {}\n\
170             Context: {}, PC: {}\n\
171             Stack: {} (len: {}, mem: {})\n\
172             AltStack: {} (len: {}, mem: {})\n\
173             IfStack: {}",
174            self.message,
175            self.source_txid,
176            self.source_output_index,
177            self.context,
178            self.program_counter,
179            Self::format_stack(&self.stack),
180            self.stack.len(),
181            self.stack_mem,
182            Self::format_stack(&self.alt_stack),
183            self.alt_stack.len(),
184            self.alt_stack_mem,
185            Self::format_if_stack(&self.if_stack),
186        )
187    }
188}
189
190impl std::error::Error for ScriptEvaluationError {}
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_error_display() {
198        let error = ScriptEvaluationError::new(
199            "Stack underflow",
200            "abc123",
201            0,
202            ExecutionContext::LockingScript,
203            5,
204            vec![vec![1, 2, 3], vec![]],
205            vec![],
206            vec![true, false],
207            3,
208            0,
209        );
210
211        let display = format!("{}", error);
212        assert!(display.contains("Stack underflow"));
213        assert!(display.contains("abc123"));
214        assert!(display.contains("LockingScript"));
215        assert!(display.contains("PC: 5"));
216        assert!(display.contains("010203"));
217        assert!(display.contains("[]"));
218    }
219
220    #[test]
221    fn test_execution_context_display() {
222        assert_eq!(
223            format!("{}", ExecutionContext::UnlockingScript),
224            "UnlockingScript"
225        );
226        assert_eq!(
227            format!("{}", ExecutionContext::LockingScript),
228            "LockingScript"
229        );
230    }
231
232    #[test]
233    fn test_format_stack() {
234        let stack = vec![vec![0x01, 0x02], vec![], vec![0xff]];
235        let formatted = ScriptEvaluationError::format_stack(&stack);
236        assert_eq!(formatted, "[0102, [], ff]");
237    }
238
239    #[test]
240    fn test_format_if_stack() {
241        let if_stack = vec![true, false, true];
242        let formatted = ScriptEvaluationError::format_if_stack(&if_stack);
243        assert_eq!(formatted, "[true, false, true]");
244    }
245}