miniscript_core_sys/
lib.rs

1//! # miniscript-core-sys
2//!
3//! FFI bindings to Bitcoin Core's miniscript implementation.
4//!
5//! This crate provides low-level bindings for cross-verification between
6//! Bitcoin Core's C++ miniscript and other implementations like rust-miniscript.
7//!
8//! ## Example
9//!
10//! ```rust,no_run
11//! use miniscript_core_sys::{Miniscript, Context};
12//!
13//! let ms = Miniscript::from_str("and_v(v:pk(A),pk(B))", Context::Wsh)
14//!     .expect("valid miniscript");
15//!
16//! assert!(ms.is_valid());
17//! println!("Type: {}", ms.get_type().unwrap());
18//! println!("String: {}", ms.to_string().unwrap());
19//! ```
20
21#![allow(non_upper_case_globals)]
22#![allow(non_camel_case_types)]
23#![allow(non_snake_case)]
24
25// Include the generated bindings
26include!(concat!(env!("OUT_DIR"), "/bindings.rs"));
27
28use std::ffi::{CStr, CString};
29use std::fmt;
30use std::ptr;
31
32/// Script context for miniscript parsing.
33#[derive(Debug, Clone, Copy, PartialEq, Eq)]
34pub enum Context {
35    /// P2WSH context (SegWit v0)
36    Wsh,
37    /// Tapscript context (SegWit v1)
38    Tapscript,
39}
40
41impl From<Context> for MiniscriptContext {
42    fn from(ctx: Context) -> Self {
43        match ctx {
44            Context::Wsh => MiniscriptContext::MINISCRIPT_CONTEXT_WSH,
45            Context::Tapscript => MiniscriptContext::MINISCRIPT_CONTEXT_TAPSCRIPT,
46        }
47    }
48}
49
50/// Error type for miniscript operations.
51#[derive(Debug)]
52pub struct Error {
53    message: String,
54}
55
56impl fmt::Display for Error {
57    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
58        write!(f, "{}", self.message)
59    }
60}
61
62impl std::error::Error for Error {}
63
64/// A parsed miniscript node.
65///
66/// This is a safe wrapper around the C++ miniscript implementation.
67pub struct Miniscript {
68    ptr: *mut MiniscriptNode,
69    context: Context,
70}
71
72// SAFETY: The underlying C++ object is self-contained and doesn't use thread-local storage.
73// The node is immutable after creation, so it's safe to send between threads.
74unsafe impl Send for Miniscript {}
75
76// SAFETY: All methods on Miniscript take &self and the underlying object is immutable.
77unsafe impl Sync for Miniscript {}
78
79impl Miniscript {
80    /// Parse a miniscript from a string.
81    ///
82    /// # Arguments
83    ///
84    /// * `input` - The miniscript string (e.g., "and_v(v:pk(A),pk(B))")
85    /// * `context` - The script context (WSH or Tapscript)
86    ///
87    /// # Errors
88    ///
89    /// Returns an error if parsing fails.
90    pub fn from_str(input: &str, context: Context) -> Result<Self, Error> {
91        let c_input = CString::new(input).map_err(|_| Error {
92            message: "input contains null byte".to_string(),
93        })?;
94
95        let mut node_ptr: *mut MiniscriptNode = ptr::null_mut();
96
97        // SAFETY: We're passing valid pointers and the C code handles null checks.
98        let result =
99            unsafe { miniscript_from_string(c_input.as_ptr(), context.into(), &mut node_ptr) };
100
101        if result.success {
102            Ok(Miniscript {
103                ptr: node_ptr,
104                context,
105            })
106        } else {
107            let message = if !result.error_message.is_null() {
108                // SAFETY: error_message is a valid C string if not null
109                let msg = unsafe { CStr::from_ptr(result.error_message) }
110                    .to_string_lossy()
111                    .into_owned();
112                unsafe { miniscript_free_string(result.error_message) };
113                msg
114            } else {
115                "unknown error".to_string()
116            };
117            Err(Error { message })
118        }
119    }
120
121    /// Convert the miniscript back to a string.
122    pub fn to_string(&self) -> Option<String> {
123        // SAFETY: self.ptr is valid while self exists
124        let c_str = unsafe { miniscript_to_string(self.ptr) };
125        if c_str.is_null() {
126            return None;
127        }
128
129        // SAFETY: c_str is a valid C string
130        let result = unsafe { CStr::from_ptr(c_str) }
131            .to_string_lossy()
132            .into_owned();
133        unsafe { miniscript_free_string(c_str) };
134
135        Some(result)
136    }
137
138    /// Check if the miniscript is valid (type-checks correctly).
139    pub fn is_valid(&self) -> bool {
140        // SAFETY: self.ptr is valid while self exists
141        unsafe { miniscript_is_valid(self.ptr) }
142    }
143
144    /// Check if the miniscript is sane.
145    ///
146    /// This includes checks for:
147    /// - No duplicate keys
148    /// - No timelock mixing
149    /// - Within resource limits
150    pub fn is_sane(&self) -> bool {
151        // SAFETY: self.ptr is valid while self exists
152        unsafe { miniscript_is_sane(self.ptr) }
153    }
154
155    /// Get the type properties of the miniscript.
156    ///
157    /// Returns a string like "Bdems" where each letter indicates a property.
158    pub fn get_type(&self) -> Option<String> {
159        // SAFETY: self.ptr is valid while self exists
160        let c_str = unsafe { miniscript_get_type(self.ptr) };
161        if c_str.is_null() {
162            return None;
163        }
164
165        // SAFETY: c_str is a valid C string
166        let result = unsafe { CStr::from_ptr(c_str) }
167            .to_string_lossy()
168            .into_owned();
169        unsafe { miniscript_free_string(c_str) };
170
171        Some(result)
172    }
173
174    /// Get the maximum witness size for satisfying this miniscript.
175    pub fn max_satisfaction_size(&self) -> Option<usize> {
176        let mut size: usize = 0;
177        // SAFETY: self.ptr is valid while self exists
178        if unsafe { miniscript_max_satisfaction_size(self.ptr, &mut size) } {
179            Some(size)
180        } else {
181            None
182        }
183    }
184
185    /// Get the context this miniscript was parsed with.
186    pub fn context(&self) -> Context {
187        self.context
188    }
189}
190
191impl Drop for Miniscript {
192    fn drop(&mut self) {
193        if !self.ptr.is_null() {
194            // SAFETY: ptr was allocated by miniscript_from_string
195            unsafe { miniscript_node_free(self.ptr) };
196        }
197    }
198}
199
200impl fmt::Debug for Miniscript {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.debug_struct("Miniscript")
203            .field("context", &self.context)
204            .field("string", &self.to_string())
205            .field("type", &self.get_type())
206            .finish()
207    }
208}
209
210/// Get the library version string.
211pub fn version() -> &'static str {
212    // SAFETY: miniscript_version returns a static string
213    unsafe {
214        CStr::from_ptr(miniscript_version())
215            .to_str()
216            .unwrap_or("unknown")
217    }
218}
219
220#[cfg(test)]
221mod tests {
222    use super::*;
223
224    #[test]
225    fn test_version() {
226        let v = version();
227        assert!(!v.is_empty());
228        println!("Version: {}", v);
229    }
230
231    // These tests require the library to be built with Bitcoin Core
232    // They're ignored by default since CI might not have the sources
233    #[test]
234    #[ignore]
235    fn test_parse_simple() {
236        let ms = Miniscript::from_str("pk(A)", Context::Wsh).expect("should parse");
237        assert!(ms.is_valid());
238        assert_eq!(ms.to_string(), Some("pk(A)".to_string()));
239    }
240
241    #[test]
242    #[ignore]
243    fn test_parse_and_v() {
244        let ms = Miniscript::from_str("and_v(v:pk(A),pk(B))", Context::Wsh).expect("should parse");
245        assert!(ms.is_valid());
246    }
247
248    #[test]
249    #[ignore]
250    fn test_invalid_miniscript() {
251        let result = Miniscript::from_str("invalid", Context::Wsh);
252        assert!(result.is_err());
253    }
254
255    #[test]
256    #[ignore]
257    fn test_type_properties() {
258        let ms = Miniscript::from_str("pk(A)", Context::Wsh).expect("should parse");
259        let type_str = ms.get_type().expect("should have type");
260        // pk should be type B (base) with various modifiers
261        assert!(type_str.contains('B'));
262    }
263}