| Crates.io | bitcoin-core-miniscript-ffi |
| lib.rs | bitcoin-core-miniscript-ffi |
| version | 0.5.2 |
| created_at | 2026-01-03 11:45:39.790042+00 |
| updated_at | 2026-01-11 03:09:34.090779+00 |
| description | FFI bindings to Bitcoin Core's miniscript implementation for cross-verification and reference testing |
| homepage | https://github.com/portlandhodl/rust-bitcoin-core-miniscript-ffi |
| repository | https://github.com/portlandhodl/rust-bitcoin-core-miniscript-ffi |
| max_upload_size | |
| id | 2019947 |
| size | 389,581 |
FFI bindings to Bitcoin Core's miniscript and descriptor implementation.
This crate provides direct access to Bitcoin Core's C++ miniscript parser, analyzer, and descriptor system through safe Rust bindings. It enables cross-verification between Bitcoin Core and other miniscript implementations (like rust-miniscript), ensuring consensus-critical code behaves identically across implementations.
Send + Sync implementationpk(), pkh(), wpkh(), sh(), wsh(), tr()multi(), sortedmulti()Add to your Cargo.toml:
[dependencies]
bitcoin-core-miniscript-ffi = "0.3"
This crate requires:
sudo apt-get install cmake build-essential libboost-dev
brew install cmake boost
# Using vcpkg
vcpkg install boost:x64-windows
# Clone with submodules (includes Bitcoin Core source)
git clone --recursive https://github.com/portlandhodl/rust-bitcoin-core-miniscript-ffi.git
cd rust-bitcoin-core-miniscript-ffi
# Build
cargo build --release
# Run tests
cargo test
If you cloned without --recursive:
git submodule update --init --recursive
┌────────────────────────────────────────────────────────────────┐
│ Rust Application │
├────────────────────────────────────────────────────────────────┤
│ src/lib.rs │ src/descriptor.rs │
│ ┌─────────────────┐ │ ┌────────────────────────────────────┐ │
│ │ Miniscript │ │ │ Descriptor │ │
│ │ - from_str() │ │ │ - parse() │ │
│ │ - is_valid() │ │ │ - expand() │ │
│ │ - is_sane() │ │ │ - get_address() │ │
│ │ - satisfy() │ │ │ - get_pubkeys() │ │
│ │ - to_script() │ │ │ - is_range() │ │
│ └────────┬────────┘ │ └──────┬─────────────────────────────┘ │
├───────────┼──────────┴─────────┼───────────────────────────────┤
│ │ FFI Boundary │ │
│ │ (bindgen) │ │
├───────────┼────────────────────┼───────────────────────────────┤
│ cpp/miniscript_wrapper.h/.cpp │ cpp/descriptor_wrapper.h/.cpp │
│ ┌───────────────────────────────────┴───────────────────────┐ │
│ │ C Wrapper Layer │ │
│ │ - Opaque handles (MiniscriptNode*, DescriptorNode*) │ │
│ │ - C-compatible types and callbacks │ │
│ │ - Memory management (malloc/free) │ │
│ └───────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ Bitcoin Core C++ (vendor/bitcoin) │
│ ┌───────────────────────────────────────────────────────────┐ │
│ │ script/miniscript.h │ script/descriptor.h │ │
│ │ script/script.h │ script/signingprovider.h │ │
│ │ key.h, pubkey.h │ key_io.h │ │
│ └───────────────────────────────────────────────────────────┘ │
├────────────────────────────────────────────────────────────────┤
│ secp256k1 │
└────────────────────────────────────────────────────────────────┘
This crate provides safe Rust wrappers around unsafe FFI calls to Bitcoin Core's C++ implementation. The unsafe code is necessary for FFI interop but is carefully encapsulated.
Drop impl)Miniscript and Descriptor implement Send and SyncThe FFI layer uses:
MiniscriptNode*, DescriptorNode*) for C++ objectsuse miniscript_core_ffi::{Miniscript, Context};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse a simple miniscript (2-of-2 multisig)
let ms = Miniscript::from_str(
"and_v(v:pk(Alice),pk(Bob))",
Context::Wsh
)?;
// Validate the miniscript
assert!(ms.is_valid());
assert!(ms.is_sane());
// Get type properties
println!("Type: {}", ms.get_type().unwrap());
// Get maximum witness size
if let Some(size) = ms.max_satisfaction_size() {
println!("Max witness size: {} bytes", size);
}
// Convert back to string (canonical form)
println!("Canonical: {}", ms.to_string().unwrap());
Ok(())
}
use miniscript_core_ffi::descriptor::{Descriptor, Network};
fn main() -> Result<(), Box<dyn std::error::Error>> {
// Parse a wpkh descriptor with an extended public key
// Use the builder pattern: Descriptor::for_network(network).parse(str)
let desc = Descriptor::for_network(Network::Testnet).parse(
"wpkh(tpubDF81GR3CqbLCT7ND3q4pPWDtpbkKfHihUMwVgQeXV9ZqJ6YJ5gJgd1W1cWbiVRfXfjc1KyRCRCpVUKVHVYjrPLbtbvRLB9L4hWfWyrZqGEL/0/*)"
)?;
// Check if it's a ranged descriptor
println!("Is ranged: {}", desc.is_range());
println!("Is solvable: {}", desc.is_solvable());
// Derive addresses at different indices
// Network is stored in the descriptor, so get_address only takes the index
for i in 0..5 {
if let Some(addr) = desc.get_address(i) {
println!("Address {}: {}", i, addr);
}
}
// Get the script at index 0
if let Some(script) = desc.expand(0) {
println!("Script: {}", hex::encode(&script));
}
Ok(())
}
use miniscript_core_ffi::{Miniscript, Context, SimpleSatisfier, Availability};
fn main() -> Result<(), Box<dyn std::error::Error>> {
let ms = Miniscript::from_str("pk(A)", Context::Wsh)?;
let mut satisfier = SimpleSatisfier::new();
// Add signature for key A (33 zero bytes in WSH context)
let key_bytes = vec![0u8; 33];
let signature = vec![0x30, 0x44, 0x02, 0x20]; // DER signature prefix
satisfier.signatures.insert(key_bytes, signature);
let result = ms.satisfy(satisfier, true)?;
match result.availability {
Availability::Yes => println!("Satisfaction found with {} stack elements", result.stack.len()),
Availability::Maybe => println!("Satisfaction may be possible"),
Availability::No => println!("Cannot satisfy"),
}
Ok(())
}
MiniscriptThe main type representing a parsed miniscript expression.
impl Miniscript {
/// Parse a miniscript from a string
pub fn from_str(input: &str, context: Context) -> Result<Self, Error>;
/// Parse a miniscript from raw script bytes
pub fn from_script_bytes(script: &[u8], context: Context) -> Result<Self, Error>;
/// Convert to canonical string representation
pub fn to_string(&self) -> Option<String>;
/// Convert to raw script bytes
pub fn to_script_bytes(&self) -> Option<Vec<u8>>;
/// Convert to bitcoin::ScriptBuf
pub fn to_script(&self) -> Option<ScriptBuf>;
/// Check if the miniscript is valid (type-checks correctly)
pub fn is_valid(&self) -> bool;
/// Check if the miniscript is sane (no duplicate keys, no timelock mixing, etc.)
pub fn is_sane(&self) -> bool;
/// Get type properties (e.g., "Bdemsu")
pub fn get_type(&self) -> Option<String>;
/// Get maximum witness satisfaction size in bytes
pub fn max_satisfaction_size(&self) -> Option<usize>;
/// Check if non-malleable
pub fn is_non_malleable(&self) -> bool;
/// Check if requires a signature
pub fn needs_signature(&self) -> bool;
/// Produce a witness that satisfies this miniscript
pub fn satisfy<S: Satisfier>(&self, satisfier: S, nonmalleable: bool) -> Result<SatisfyResult, Error>;
}
DescriptorBitcoin Core descriptor with full key derivation support.
impl Descriptor {
/// Create a builder for parsing descriptors with the specified network
/// Use: Descriptor::for_network(Network::Testnet).parse("wpkh(tpub...)")
pub fn for_network(network: Network) -> DescriptorBuilder;
/// Get the network this descriptor was parsed with
pub fn network(&self) -> Network;
/// Check if the descriptor is ranged (contains wildcards)
pub fn is_range(&self) -> bool;
/// Check if the descriptor is solvable
pub fn is_solvable(&self) -> bool;
/// Convert back to string
pub fn to_string(&self) -> Option<String>;
/// Expand to script bytes at a specific index
pub fn expand(&self, index: u32) -> Option<Vec<u8>>;
/// Get address at a specific index (uses stored network)
pub fn get_address(&self, index: u32) -> Option<String>;
/// Get all public keys at a specific index
pub fn get_pubkeys(&self, index: u32) -> Option<Vec<Vec<u8>>>;
/// Get script size
pub fn script_size(&self) -> Option<i64>;
/// Get maximum satisfaction weight
pub fn max_satisfaction_weight(&self, use_max_sig: bool) -> Option<i64>;
}
impl DescriptorBuilder {
/// Parse a descriptor string with this builder's network context
pub fn parse(self, descriptor: &str) -> Result<Descriptor, String>;
/// Get the network this builder is configured for
pub fn network(&self) -> Network;
}
ContextScript context for miniscript parsing.
pub enum Context {
/// P2WSH context (SegWit v0) - 520 byte script limit
Wsh,
/// Tapscript context (SegWit v1) - no script size limit, x-only pubkeys
Tapscript,
}
NetworkNetwork type for address generation.
pub enum Network {
Mainnet,
Testnet,
Signet,
Regtest,
}
The type string returned by get_type() contains single-character flags:
| Flag | Meaning |
|---|---|
B |
Base expression (consumes nothing, produces nonzero) |
V |
Verify expression (consumes nothing, produces nothing, fails if unsatisfied) |
K |
Key expression (consumes nothing, produces a public key) |
W |
Wrapped expression (consumes one stack element) |
z |
Zero-arg property (consumes no stack elements) |
o |
One-arg property (consumes exactly one stack element) |
n |
Nonzero property (never produces zero) |
d |
Dissatisfiable property (has a dissatisfaction) |
u |
Unit property (on satisfaction, puts exactly 1 on stack) |
e |
Expression property (can be used as an expression) |
f |
Forced property (always requires a signature) |
s |
Safe property (cannot be malleated) |
m |
Nonmalleable property (satisfaction is unique) |
x |
Expensive verify property |
k |
Timelock property (contains a timelock) |
use miniscript_core_ffi::{Miniscript, Context};
fn verify_against_core(miniscript_str: &str) -> bool {
// Parse with Bitcoin Core's implementation
let core_result = Miniscript::from_str(miniscript_str, Context::Wsh);
// Compare with your implementation
match core_result {
Ok(ms) => {
// Verify type properties match
let core_type = ms.get_type().unwrap();
// ... compare with your implementation's type
true
}
Err(e) => {
// Bitcoin Core rejected it - your implementation should too
println!("Core rejected: {}", e);
false
}
}
}
use miniscript_core_ffi::{Miniscript, Context};
fn validate_spending_policy(policy: &str) -> Result<(), String> {
let ms = Miniscript::from_str(policy, Context::Wsh)
.map_err(|e| format!("Invalid policy: {}", e))?;
if !ms.is_sane() {
return Err("Policy fails sanity checks".to_string());
}
if let Some(size) = ms.max_satisfaction_size() {
if size > 10000 {
return Err(format!("Witness too large: {} bytes", size));
}
}
Ok(())
}
use miniscript_core_ffi::{Miniscript, Context};
fn analyze_tapscript(script: &str) {
let ms = Miniscript::from_str(script, Context::Tapscript)
.expect("valid tapscript");
println!("Valid: {}", ms.is_valid());
println!("Sane: {}", ms.is_sane());
println!("Type: {}", ms.get_type().unwrap_or_default());
if let Some(size) = ms.max_satisfaction_size() {
println!("Max witness: {} vbytes", size);
}
}
Miniscript and Descriptor implement Send and Sync, making them safe to use across threads:
use miniscript_core_ffi::{Miniscript, Context};
use std::sync::Arc;
use std::thread;
let ms = Arc::new(
Miniscript::from_str("pk(A)", Context::Wsh).unwrap()
);
let handles: Vec<_> = (0..4).map(|_| {
let ms = Arc::clone(&ms);
thread::spawn(move || {
assert!(ms.is_valid());
})
}).collect();
for h in handles {
h.join().unwrap();
}
The library is optimized for production use:
-O3 optimization| Feature | bitcoin-core-miniscript-ffi | rust-miniscript |
|---|---|---|
| Implementation | Bitcoin Core C++ | Pure Rust |
| Consensus compatibility | Reference | Aims to match |
| Dependencies | Bitcoin Core, Boost | Pure Rust |
| Build complexity | Higher | Lower |
| Use case | Cross-verification, reference | Production wallets |
Recommendation: Use this crate for testing and verification. Use rust-miniscript for production applications, but verify critical paths against this crate.
The library includes comprehensive tests covering:
Run tests with:
cargo test
Contributions are welcome! Please feel free to submit a Pull Request.
# Clone with submodules
git clone --recursive https://github.com/portlandhodl/rust-bitcoin-core-miniscript-ffi.git
cd rust-bitcoin-core-miniscript-ffi
# Build in debug mode
cargo build
# Run all tests
cargo test
# Run clippy
cargo clippy
# Build documentation
cargo doc --open
This project is licensed under the MIT License - see the LICENSE file for details.
Bitcoin Core's miniscript implementation is also MIT licensed.
Building the future of Bitcoin, one script at a time.