use bitcoin::{Network, ScriptBuf};
use serde::{Deserialize, Serialize};
use crate::error::{BitcoinError, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub enum MiniscriptPolicy {
PublicKey(String),
Hash {
hash_type: HashType,
hash: String,
},
Timelock {
timelock_type: TimelockType,
value: u32,
},
Threshold {
k: usize,
policies: Vec<MiniscriptPolicy>,
},
And(Box<MiniscriptPolicy>, Box<MiniscriptPolicy>),
Or(Box<MiniscriptPolicy>, Box<MiniscriptPolicy>),
Multi {
k: usize,
keys: Vec<String>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum HashType {
Sha256,
Hash256,
Ripemd160,
Hash160,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum TimelockType {
After,
Older,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MiniscriptDescriptor {
pub policy: MiniscriptPolicy,
pub network: Network,
pub script_type: MiniscriptScriptType,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
pub enum MiniscriptScriptType {
Bare,
P2SH,
P2WSH,
P2SHP2WSH,
}
impl MiniscriptDescriptor {
pub fn new(
policy: MiniscriptPolicy,
network: Network,
script_type: MiniscriptScriptType,
) -> Self {
Self {
policy,
network,
script_type,
}
}
pub fn compile(&self) -> Result<ScriptBuf> {
self.compile_policy(&self.policy)
}
#[allow(clippy::only_used_in_recursion)]
fn compile_policy(&self, policy: &MiniscriptPolicy) -> Result<ScriptBuf> {
match policy {
MiniscriptPolicy::PublicKey(_key) => {
Ok(ScriptBuf::new())
}
MiniscriptPolicy::Hash { .. } => {
Ok(ScriptBuf::new())
}
MiniscriptPolicy::Timelock { .. } => {
Ok(ScriptBuf::new())
}
MiniscriptPolicy::Threshold { k: _, policies: _ } => {
Ok(ScriptBuf::new())
}
MiniscriptPolicy::And(left, right) => {
let _left_script = self.compile_policy(left)?;
let _right_script = self.compile_policy(right)?;
Ok(ScriptBuf::new())
}
MiniscriptPolicy::Or(left, right) => {
let _left_script = self.compile_policy(left)?;
let _right_script = self.compile_policy(right)?;
Ok(ScriptBuf::new())
}
MiniscriptPolicy::Multi { k: _, keys: _ } => {
Ok(ScriptBuf::new())
}
}
}
pub fn analyze(&self) -> PolicyAnalysis {
PolicyAnalysis {
is_malleable: self.check_malleability(),
is_sane: self.check_sanity(),
max_satisfaction_weight: self.estimate_weight(),
requires_timelock: self.requires_timelock(),
requires_hash_preimage: self.requires_hash_preimage(),
}
}
fn check_malleability(&self) -> bool {
false
}
fn check_sanity(&self) -> bool {
true
}
fn estimate_weight(&self) -> usize {
match &self.policy {
MiniscriptPolicy::PublicKey(_) => 73, MiniscriptPolicy::Multi { k, keys } => {
73 * k + 34 * keys.len()
}
MiniscriptPolicy::Threshold { k: _, policies } => {
policies.iter().map(|_| 100).sum()
}
_ => 100, }
}
fn requires_timelock(&self) -> bool {
self.check_policy_for_feature(&self.policy, |p| {
matches!(p, MiniscriptPolicy::Timelock { .. })
})
}
fn requires_hash_preimage(&self) -> bool {
self.check_policy_for_feature(&self.policy, |p| matches!(p, MiniscriptPolicy::Hash { .. }))
}
#[allow(clippy::only_used_in_recursion)]
fn check_policy_for_feature<F>(&self, policy: &MiniscriptPolicy, check: F) -> bool
where
F: Fn(&MiniscriptPolicy) -> bool + Copy,
{
if check(policy) {
return true;
}
match policy {
MiniscriptPolicy::And(left, right) | MiniscriptPolicy::Or(left, right) => {
self.check_policy_for_feature(left, check)
|| self.check_policy_for_feature(right, check)
}
MiniscriptPolicy::Threshold { policies, .. } => policies
.iter()
.any(|p| self.check_policy_for_feature(p, check)),
_ => false,
}
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PolicyAnalysis {
pub is_malleable: bool,
pub is_sane: bool,
pub max_satisfaction_weight: usize,
pub requires_timelock: bool,
pub requires_hash_preimage: bool,
}
pub struct MiniscriptCompiler {
network: Network,
}
impl MiniscriptCompiler {
pub fn new(network: Network) -> Self {
Self { network }
}
pub fn parse_policy(&self, policy_str: &str) -> Result<MiniscriptPolicy> {
if policy_str.starts_with("pk(") {
let key = policy_str
.trim_start_matches("pk(")
.trim_end_matches(')')
.to_string();
Ok(MiniscriptPolicy::PublicKey(key))
} else if policy_str.starts_with("multi(") {
let inner = policy_str
.trim_start_matches("multi(")
.trim_end_matches(')');
let parts: Vec<&str> = inner.split(',').map(|s| s.trim()).collect();
if parts.len() < 2 {
return Err(BitcoinError::Validation(
"Invalid multi policy: need at least k and one key".to_string(),
));
}
let k = parts[0]
.parse::<usize>()
.map_err(|_| BitcoinError::Validation("Invalid threshold k".to_string()))?;
let keys = parts[1..].iter().map(|s| s.to_string()).collect();
Ok(MiniscriptPolicy::Multi { k, keys })
} else if policy_str.starts_with("after(") {
let value_str = policy_str
.trim_start_matches("after(")
.trim_end_matches(')');
let value = value_str
.parse::<u32>()
.map_err(|_| BitcoinError::Validation("Invalid timelock value".to_string()))?;
Ok(MiniscriptPolicy::Timelock {
timelock_type: TimelockType::After,
value,
})
} else if policy_str.starts_with("older(") {
let value_str = policy_str
.trim_start_matches("older(")
.trim_end_matches(')');
let value = value_str
.parse::<u32>()
.map_err(|_| BitcoinError::Validation("Invalid timelock value".to_string()))?;
Ok(MiniscriptPolicy::Timelock {
timelock_type: TimelockType::Older,
value,
})
} else {
Err(BitcoinError::Validation(format!(
"Unsupported policy: {}",
policy_str
)))
}
}
pub fn compile(
&self,
policy: MiniscriptPolicy,
script_type: MiniscriptScriptType,
) -> Result<MiniscriptDescriptor> {
Ok(MiniscriptDescriptor::new(policy, self.network, script_type))
}
pub fn optimize(&self, policy: MiniscriptPolicy) -> MiniscriptPolicy {
policy
}
}
pub struct PolicyTemplateBuilder {
#[allow(dead_code)]
network: Network,
}
impl PolicyTemplateBuilder {
pub fn new(network: Network) -> Self {
Self { network }
}
pub fn single_key(&self, pubkey: String) -> MiniscriptPolicy {
MiniscriptPolicy::PublicKey(pubkey)
}
pub fn multisig(&self, k: usize, keys: Vec<String>) -> MiniscriptPolicy {
MiniscriptPolicy::Multi { k, keys }
}
pub fn absolute_timelock(&self, value: u32) -> MiniscriptPolicy {
MiniscriptPolicy::Timelock {
timelock_type: TimelockType::After,
value,
}
}
pub fn relative_timelock(&self, value: u32) -> MiniscriptPolicy {
MiniscriptPolicy::Timelock {
timelock_type: TimelockType::Older,
value,
}
}
pub fn hash_preimage(&self, hash_type: HashType, hash: String) -> MiniscriptPolicy {
MiniscriptPolicy::Hash { hash_type, hash }
}
pub fn and(&self, left: MiniscriptPolicy, right: MiniscriptPolicy) -> MiniscriptPolicy {
MiniscriptPolicy::And(Box::new(left), Box::new(right))
}
pub fn or(&self, left: MiniscriptPolicy, right: MiniscriptPolicy) -> MiniscriptPolicy {
MiniscriptPolicy::Or(Box::new(left), Box::new(right))
}
pub fn threshold(&self, k: usize, policies: Vec<MiniscriptPolicy>) -> MiniscriptPolicy {
MiniscriptPolicy::Threshold { k, policies }
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_single_key_policy() {
let builder = PolicyTemplateBuilder::new(Network::Bitcoin);
let policy = builder.single_key(
"0279BE667EF9DCBBAC55A06295CE870B07029BFCDB2DCE28D959F2815B16F81798".to_string(),
);
match policy {
MiniscriptPolicy::PublicKey(key) => {
assert!(key.starts_with("0279BE667"));
}
_ => panic!("Expected PublicKey policy"),
}
}
#[test]
fn test_multisig_policy() {
let builder = PolicyTemplateBuilder::new(Network::Bitcoin);
let keys = vec!["key1".to_string(), "key2".to_string(), "key3".to_string()];
let policy = builder.multisig(2, keys.clone());
match policy {
MiniscriptPolicy::Multi {
k,
keys: policy_keys,
} => {
assert_eq!(k, 2);
assert_eq!(policy_keys, keys);
}
_ => panic!("Expected Multi policy"),
}
}
#[test]
fn test_timelock_policy() {
let builder = PolicyTemplateBuilder::new(Network::Bitcoin);
let policy = builder.absolute_timelock(500000);
match policy {
MiniscriptPolicy::Timelock {
timelock_type,
value,
} => {
assert_eq!(timelock_type, TimelockType::After);
assert_eq!(value, 500000);
}
_ => panic!("Expected Timelock policy"),
}
}
#[test]
fn test_and_policy() {
let builder = PolicyTemplateBuilder::new(Network::Bitcoin);
let left = builder.single_key("key1".to_string());
let right = builder.absolute_timelock(500000);
let policy = builder.and(left, right);
match policy {
MiniscriptPolicy::And(_, _) => {}
_ => panic!("Expected And policy"),
}
}
#[test]
fn test_policy_parsing() {
let compiler = MiniscriptCompiler::new(Network::Bitcoin);
let policy = compiler.parse_policy("pk(key1)").unwrap();
assert!(matches!(policy, MiniscriptPolicy::PublicKey(_)));
let policy = compiler.parse_policy("multi(2, key1, key2, key3)").unwrap();
if let MiniscriptPolicy::Multi { k, keys } = policy {
assert_eq!(k, 2);
assert_eq!(keys.len(), 3);
} else {
panic!("Expected Multi policy");
}
let policy = compiler.parse_policy("after(500000)").unwrap();
assert!(matches!(
policy,
MiniscriptPolicy::Timelock {
timelock_type: TimelockType::After,
..
}
));
}
#[test]
fn test_descriptor_creation() {
let policy = MiniscriptPolicy::PublicKey("key1".to_string());
let descriptor =
MiniscriptDescriptor::new(policy, Network::Bitcoin, MiniscriptScriptType::P2WSH);
assert_eq!(descriptor.script_type, MiniscriptScriptType::P2WSH);
assert_eq!(descriptor.network, Network::Bitcoin);
}
#[test]
fn test_policy_analysis() {
let policy = MiniscriptPolicy::Multi {
k: 2,
keys: vec!["key1".to_string(), "key2".to_string(), "key3".to_string()],
};
let descriptor =
MiniscriptDescriptor::new(policy, Network::Bitcoin, MiniscriptScriptType::P2WSH);
let analysis = descriptor.analyze();
assert!(!analysis.is_malleable);
assert!(analysis.is_sane);
assert!(analysis.max_satisfaction_weight > 0);
}
#[test]
fn test_requires_timelock_detection() {
let builder = PolicyTemplateBuilder::new(Network::Bitcoin);
let policy_with_timelock = builder.and(
builder.single_key("key1".to_string()),
builder.absolute_timelock(500000),
);
let descriptor = MiniscriptDescriptor::new(
policy_with_timelock,
Network::Bitcoin,
MiniscriptScriptType::P2WSH,
);
let analysis = descriptor.analyze();
assert!(analysis.requires_timelock);
}
}