Skip to main content

bsv_rs/script/
script_num.rs

1//! Bitcoin Script number encoding.
2//!
3//! Bitcoin Script uses a special little-endian sign-magnitude encoding for numbers
4//! on the stack. This module provides conversion between this format and BigNumber.
5//!
6//! # Format
7//!
8//! - Little-endian byte order
9//! - Sign bit in the MSB of the last byte (0x80)
10//! - Zero is represented as an empty array
11//! - Negative zero `[0x80]` is treated as false
12//! - Numbers must be minimally encoded (no unnecessary padding)
13
14use crate::primitives::BigNumber;
15use crate::Result;
16
17/// Bitcoin Script number utilities.
18///
19/// Provides conversion between stack byte arrays and BigNumber values,
20/// following Bitcoin's sign-magnitude little-endian encoding.
21pub struct ScriptNum;
22
23impl ScriptNum {
24    /// Converts stack bytes to a BigNumber.
25    ///
26    /// # Arguments
27    ///
28    /// * `bytes` - The stack element bytes
29    /// * `require_minimal` - If true, rejects non-minimally encoded numbers
30    ///
31    /// # Returns
32    ///
33    /// The decoded BigNumber value, or an error if encoding is invalid
34    pub fn from_bytes(bytes: &[u8], require_minimal: bool) -> Result<BigNumber> {
35        // Empty array is zero
36        if bytes.is_empty() {
37            return Ok(BigNumber::zero());
38        }
39
40        // Check minimal encoding if required
41        if require_minimal && !Self::is_minimally_encoded(bytes) {
42            return Err(crate::error::Error::ScriptExecutionError(
43                "Non-minimally encoded script number".to_string(),
44            ));
45        }
46
47        // Extract sign bit from the last byte
48        let last_byte = bytes[bytes.len() - 1];
49        let is_negative = (last_byte & 0x80) != 0;
50
51        // Create magnitude bytes (clear sign bit from last byte)
52        let mut magnitude = bytes.to_vec();
53        if let Some(last) = magnitude.last_mut() {
54            *last &= 0x7f;
55        }
56
57        // Convert from little-endian to BigNumber
58        let bn = BigNumber::from_bytes_le(&magnitude);
59
60        // Apply sign
61        if is_negative {
62            Ok(bn.neg())
63        } else {
64            Ok(bn)
65        }
66    }
67
68    /// Converts a BigNumber to stack bytes (minimal encoding).
69    ///
70    /// # Arguments
71    ///
72    /// * `value` - The BigNumber to encode
73    ///
74    /// # Returns
75    ///
76    /// The minimally-encoded byte array
77    pub fn to_bytes(value: &BigNumber) -> Vec<u8> {
78        // Zero is represented as empty array
79        if value.is_zero() {
80            return Vec::new();
81        }
82
83        let is_negative = value.is_negative();
84        let abs_value = value.abs();
85
86        // Get magnitude bytes in little-endian
87        let mut bytes = abs_value.to_bytes_le_min();
88
89        // If the high bit of the last byte is set, we need an extra byte for the sign
90        if let Some(&last) = bytes.last() {
91            if (last & 0x80) != 0 {
92                bytes.push(if is_negative { 0x80 } else { 0x00 });
93            } else if is_negative {
94                // Set sign bit on existing last byte
95                if let Some(last_mut) = bytes.last_mut() {
96                    *last_mut |= 0x80;
97                }
98            }
99        } else if is_negative {
100            // Edge case: magnitude was zero but we're negative (shouldn't happen)
101            bytes.push(0x80);
102        }
103
104        bytes
105    }
106
107    /// Checks if bytes are minimally encoded as a script number.
108    ///
109    /// A number is minimally encoded if:
110    /// - It's empty (zero), or
111    /// - The last byte is non-zero after removing the sign bit, or
112    /// - The second-to-last byte has its high bit set (justifying the extra byte)
113    pub fn is_minimally_encoded(bytes: &[u8]) -> bool {
114        if bytes.is_empty() {
115            return true;
116        }
117
118        // If the last byte is non-zero when we mask out the sign bit,
119        // the encoding is minimal
120        let last_byte = bytes[bytes.len() - 1];
121        if (last_byte & 0x7f) != 0 {
122            return true;
123        }
124
125        // The last byte is either 0x00 or 0x80 (just a sign byte)
126        // This is only valid if we have more than one byte and the
127        // second-to-last byte has its high bit set
128        if bytes.len() > 1 && (bytes[bytes.len() - 2] & 0x80) != 0 {
129            return true;
130        }
131
132        false
133    }
134
135    /// Casts a byte array to a boolean value.
136    ///
137    /// Returns false for:
138    /// - Empty array
139    /// - Array of all zeros
140    /// - Negative zero `[0x80]` or `[0x00, ..., 0x80]`
141    ///
142    /// Returns true otherwise.
143    pub fn cast_to_bool(bytes: &[u8]) -> bool {
144        if bytes.is_empty() {
145            return false;
146        }
147
148        for (i, &byte) in bytes.iter().enumerate() {
149            if byte != 0 {
150                // Check for negative zero: all zeros except 0x80 in the last byte
151                if i == bytes.len() - 1 && byte == 0x80 {
152                    return false;
153                }
154                return true;
155            }
156        }
157
158        false
159    }
160
161    /// Minimally encodes bytes in-place.
162    ///
163    /// Removes unnecessary trailing zeros while preserving the sign.
164    pub fn minimally_encode(bytes: &[u8]) -> Vec<u8> {
165        if bytes.is_empty() {
166            return Vec::new();
167        }
168
169        // Decode and re-encode to get minimal form
170        match Self::from_bytes(bytes, false) {
171            Ok(bn) => Self::to_bytes(&bn),
172            Err(_) => bytes.to_vec(),
173        }
174    }
175}
176
177#[cfg(test)]
178mod tests {
179    use super::*;
180
181    #[test]
182    fn test_zero() {
183        // Zero should encode to empty array
184        assert_eq!(ScriptNum::to_bytes(&BigNumber::zero()), Vec::<u8>::new());
185
186        // Empty array should decode to zero
187        let bn = ScriptNum::from_bytes(&[], true).unwrap();
188        assert!(bn.is_zero());
189    }
190
191    #[test]
192    fn test_positive_numbers() {
193        // 1 -> [0x01]
194        let bn = BigNumber::from_i64(1);
195        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x01]);
196
197        // 127 -> [0x7f]
198        let bn = BigNumber::from_i64(127);
199        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x7f]);
200
201        // 128 -> [0x80, 0x00] (needs extra byte for sign)
202        let bn = BigNumber::from_i64(128);
203        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x80, 0x00]);
204
205        // 255 -> [0xff, 0x00]
206        let bn = BigNumber::from_i64(255);
207        assert_eq!(ScriptNum::to_bytes(&bn), vec![0xff, 0x00]);
208
209        // 256 -> [0x00, 0x01]
210        let bn = BigNumber::from_i64(256);
211        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x00, 0x01]);
212    }
213
214    #[test]
215    fn test_negative_numbers() {
216        // -1 -> [0x81]
217        let bn = BigNumber::from_i64(-1);
218        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x81]);
219
220        // -127 -> [0xff]
221        let bn = BigNumber::from_i64(-127);
222        assert_eq!(ScriptNum::to_bytes(&bn), vec![0xff]);
223
224        // -128 -> [0x80, 0x80]
225        let bn = BigNumber::from_i64(-128);
226        assert_eq!(ScriptNum::to_bytes(&bn), vec![0x80, 0x80]);
227
228        // -255 -> [0xff, 0x80]
229        let bn = BigNumber::from_i64(-255);
230        assert_eq!(ScriptNum::to_bytes(&bn), vec![0xff, 0x80]);
231    }
232
233    #[test]
234    fn test_roundtrip() {
235        let test_values = [
236            0i64, 1, -1, 127, -127, 128, -128, 255, -255, 256, -256, 1000, -1000,
237        ];
238
239        for val in test_values {
240            let bn = BigNumber::from_i64(val);
241            let bytes = ScriptNum::to_bytes(&bn);
242            let decoded = ScriptNum::from_bytes(&bytes, true).unwrap();
243            assert_eq!(bn, decoded, "Roundtrip failed for {}", val);
244        }
245    }
246
247    #[test]
248    fn test_minimal_encoding() {
249        // Minimally encoded
250        assert!(ScriptNum::is_minimally_encoded(&[]));
251        assert!(ScriptNum::is_minimally_encoded(&[0x01]));
252        assert!(ScriptNum::is_minimally_encoded(&[0x7f]));
253        assert!(ScriptNum::is_minimally_encoded(&[0x80, 0x00]));
254        assert!(ScriptNum::is_minimally_encoded(&[0x81]));
255
256        // Not minimally encoded
257        assert!(!ScriptNum::is_minimally_encoded(&[0x00])); // Should be empty
258        assert!(!ScriptNum::is_minimally_encoded(&[0x80])); // Negative zero should be empty
259        assert!(!ScriptNum::is_minimally_encoded(&[0x01, 0x00])); // 1 with extra byte
260    }
261
262    #[test]
263    fn test_cast_to_bool() {
264        // False cases
265        assert!(!ScriptNum::cast_to_bool(&[]));
266        assert!(!ScriptNum::cast_to_bool(&[0x00]));
267        assert!(!ScriptNum::cast_to_bool(&[0x00, 0x00]));
268        assert!(!ScriptNum::cast_to_bool(&[0x80])); // Negative zero
269        assert!(!ScriptNum::cast_to_bool(&[0x00, 0x80])); // Negative zero
270
271        // True cases
272        assert!(ScriptNum::cast_to_bool(&[0x01]));
273        assert!(ScriptNum::cast_to_bool(&[0x81])); // -1
274        assert!(ScriptNum::cast_to_bool(&[0x00, 0x01]));
275        assert!(ScriptNum::cast_to_bool(&[0x7f]));
276    }
277
278    #[test]
279    fn test_non_minimal_decoding() {
280        // Non-minimal encoding should fail with require_minimal=true
281        let result = ScriptNum::from_bytes(&[0x00], true);
282        assert!(result.is_err());
283
284        // But should succeed with require_minimal=false
285        let bn = ScriptNum::from_bytes(&[0x00], false).unwrap();
286        assert!(bn.is_zero());
287    }
288}