Skip to main content

bitcoin_rs_script/
stack.rs

1use smallvec::SmallVec;
2use thiserror::Error;
3use tinyvec::ArrayVec;
4
5/// One stack item in the future hand-rolled interpreter.
6#[derive(Clone, Debug, PartialEq, Eq)]
7pub enum ScriptItem {
8    /// A minimally encoded script integer.
9    Num(i64),
10    /// A byte vector kept inline for common small pushes.
11    Bytes(SmallVec<[u8; 32]>),
12}
13
14impl Default for ScriptItem {
15    fn default() -> Self {
16        Self::Bytes(SmallVec::new())
17    }
18}
19
20/// Bounded script stack with Core's 1000-item maximum depth.
21#[derive(Clone, Debug, Default, PartialEq, Eq)]
22pub struct Stack {
23    items: ArrayVec<[ScriptItem; Self::MAX_DEPTH]>,
24}
25
26impl Stack {
27    /// Maximum stack depth permitted by consensus script evaluation.
28    pub const MAX_DEPTH: usize = 1000;
29
30    /// Creates an empty stack.
31    #[must_use]
32    pub fn new() -> Self {
33        Self::default()
34    }
35
36    /// Pushes one item, rejecting capacity overflow instead of panicking.
37    pub fn push(&mut self, item: ScriptItem) -> Result<(), StackError> {
38        match self.items.try_push(item) {
39            Some(_) => Err(StackError::Overflow),
40            None => Ok(()),
41        }
42    }
43
44    /// Pops the top item.
45    pub fn pop(&mut self) -> Result<ScriptItem, StackError> {
46        self.items.pop().ok_or(StackError::Underflow)
47    }
48
49    /// Returns the top item without removing it.
50    pub fn peek(&self) -> Result<&ScriptItem, StackError> {
51        self.items.last().ok_or(StackError::Underflow)
52    }
53
54    /// Returns the number of stack items.
55    #[must_use]
56    pub fn len(&self) -> usize {
57        self.items.len()
58    }
59
60    /// Returns true when the stack is empty.
61    #[must_use]
62    pub fn is_empty(&self) -> bool {
63        self.items.is_empty()
64    }
65
66    /// Removes all stack items.
67    pub fn clear(&mut self) {
68        self.items.clear();
69    }
70}
71
72/// Errors returned by bounded stack operations.
73#[derive(Copy, Clone, Debug, Error, PartialEq, Eq)]
74pub enum StackError {
75    /// Pushing would exceed the 1000-item consensus maximum.
76    #[error("script stack overflow")]
77    Overflow,
78    /// Popping or peeking an empty stack was requested.
79    #[error("script stack underflow")]
80    Underflow,
81}
82
83#[cfg(test)]
84mod tests {
85    use super::{ScriptItem, Stack, StackError};
86
87    #[test]
88    fn stack_rejects_overflow_and_reports_underflow() {
89        let mut stack = Stack::new();
90        assert_eq!(stack.pop(), Err(StackError::Underflow));
91        for value in 0..Stack::MAX_DEPTH {
92            let num = i64::try_from(value)
93                .unwrap_or_else(|error| panic!("stack test index should fit in i64: {error}"));
94            assert_eq!(stack.push(ScriptItem::Num(num)), Ok(()));
95        }
96        assert_eq!(stack.len(), Stack::MAX_DEPTH);
97        assert_eq!(stack.push(ScriptItem::Num(1)), Err(StackError::Overflow));
98    }
99}