use bitcoin::{
blockdata::{
opcodes::{all::OP_CHECKMULTISIG, Class},
script::{read_uint, Builder, Instruction, Script},
},
util::psbt::serialize::Serialize,
PublicKey,
};
use hex;
use thiserror::Error;
use std::{fmt, str::FromStr};
#[derive(Debug, PartialEq, Clone)]
pub struct RedeemScript(pub(crate) Script);
impl RedeemScript {
pub fn from_script(script: Script) -> Result<RedeemScript, RedeemScriptError> {
RedeemScriptContent::parse(&script)?;
Ok(RedeemScript(script))
}
pub fn content(&self) -> RedeemScriptContent {
RedeemScriptContent::parse(&self.0).unwrap()
}
}
impl fmt::Display for RedeemScript {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
fmt::LowerHex::fmt(&self.0, f)
}
}
impl FromStr for RedeemScript {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let script = Script::from(hex::decode(s)?);
RedeemScript::from_script(script).map_err(Into::into)
}
}
impl From<&'static str> for RedeemScript {
fn from(s: &'static str) -> RedeemScript {
RedeemScript::from_str(s).unwrap()
}
}
impl From<RedeemScript> for Script {
fn from(s: RedeemScript) -> Script {
s.0
}
}
impl AsRef<Script> for RedeemScript {
fn as_ref(&self) -> &Script {
&self.0
}
}
impl ::serde::Serialize for RedeemScript {
fn serialize<S>(&self, ser: S) -> ::std::result::Result<S::Ok, S::Error>
where
S: ::serde::Serializer,
{
::serde_str::serialize(self, ser)
}
}
impl<'de> ::serde::Deserialize<'de> for RedeemScript {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: ::serde::Deserializer<'de>,
{
::serde_str::deserialize(deserializer)
}
}
#[derive(Debug, PartialEq)]
pub struct RedeemScriptContent {
pub public_keys: Vec<PublicKey>,
pub quorum: usize,
}
impl RedeemScriptContent {
pub fn parse(script: &Script) -> Result<RedeemScriptContent, RedeemScriptError> {
fn read_usize(instruction: Instruction) -> Option<usize> {
match instruction {
Instruction::Op(op) => {
if let Class::PushNum(num) = op.classify() {
Some(num as usize)
} else {
None
}
}
Instruction::PushBytes(data) => {
let num = read_uint(data, data.len()).ok()?;
Some(num as usize)
}
_ => None,
}
};
let mut instructions = script.iter(true).peekable();
let quorum = instructions
.next()
.and_then(read_usize)
.ok_or_else(|| RedeemScriptError::NoQuorum)?;
let public_keys = {
let mut public_keys = Vec::new();
while let Some(Instruction::PushBytes(slice)) = instructions.peek().cloned() {
if slice.len() == 1 {
break;
}
let pub_key =
PublicKey::from_slice(slice).map_err(|_| RedeemScriptError::NotStandard)?;
public_keys.push(pub_key);
instructions.next();
}
let public_keys_len = instructions
.next()
.and_then(read_usize)
.ok_or_else(|| RedeemScriptError::NotStandard)?;
ensure!(
public_keys.len() == public_keys_len,
RedeemScriptError::NotEnoughPublicKeys
);
ensure!(
Some(Instruction::Op(OP_CHECKMULTISIG)) == instructions.next(),
RedeemScriptError::NotStandard
);
public_keys
};
Ok(RedeemScriptContent {
quorum,
public_keys,
})
}
}
#[derive(Debug)]
pub struct RedeemScriptBuilder(RedeemScriptContent);
impl RedeemScriptBuilder {
pub fn new() -> RedeemScriptBuilder {
RedeemScriptBuilder(RedeemScriptContent {
quorum: 0,
public_keys: Vec::default(),
})
}
pub fn with_quorum(quorum: usize) -> RedeemScriptBuilder {
RedeemScriptBuilder(RedeemScriptContent {
quorum,
public_keys: Vec::default(),
})
}
pub fn with_public_keys<I: IntoIterator<Item = PublicKey>>(
public_keys: I,
) -> RedeemScriptBuilder {
let public_keys = public_keys.into_iter().collect::<Vec<_>>();
let quorum = public_keys.len();
RedeemScriptBuilder(RedeemScriptContent {
public_keys,
quorum,
})
}
pub fn public_key<K: Into<PublicKey>>(&mut self, pub_key: K) -> &mut RedeemScriptBuilder {
self.0.public_keys.push(pub_key.into());
self
}
pub fn quorum(&mut self, quorum: usize) -> &mut RedeemScriptBuilder {
self.0.quorum = quorum;
self
}
pub fn to_script(&self) -> Result<RedeemScript, RedeemScriptError> {
let total_count = self.0.public_keys.len();
ensure!(self.0.quorum > 0, RedeemScriptError::NoQuorum);
ensure!(total_count != 0, RedeemScriptError::NotEnoughPublicKeys);
ensure!(
total_count >= self.0.quorum,
RedeemScriptError::IncorrectQuorum
);
let mut builder = Builder::default().push_int(self.0.quorum as i64);
let compressed_keys = self.0.public_keys.iter().map(Serialize::serialize);
for key in compressed_keys {
builder = builder.push_slice(key.as_ref());
}
let inner = builder
.push_int(total_count as i64)
.push_opcode(OP_CHECKMULTISIG)
.into_script();
Ok(RedeemScript(inner))
}
}
impl Default for RedeemScriptBuilder {
fn default() -> Self {
RedeemScriptBuilder::new()
}
}
#[derive(Debug, Copy, Clone, Error, PartialEq)]
pub enum RedeemScriptError {
#[error("Not enough keys for the quorum.")]
IncorrectQuorum,
#[error("Quorum was not set.")]
NoQuorum,
#[error("Not enough public keys. At least one public key must be specified.")]
NotEnoughPublicKeys,
#[error("Given script is not the standard redeem script.")]
NotStandard,
}
#[cfg(test)]
mod tests {
use std::str::FromStr;
use crate::{
multisig::{RedeemScript, RedeemScriptBuilder, RedeemScriptError},
test_data::keypair_from_wif,
};
#[test]
fn test_redeem_script_builder_no_quorum() {
assert_eq!(
RedeemScriptBuilder::with_quorum(0).to_script(),
Err(RedeemScriptError::NoQuorum)
);
}
#[test]
fn test_redeem_script_builder_not_enough_keys() {
assert_eq!(
RedeemScriptBuilder::with_quorum(3).to_script(),
Err(RedeemScriptError::NotEnoughPublicKeys)
);
}
#[test]
fn test_redeem_script_builder_incorrect_quorum() {
let keys = vec![
"cPHmynxvqfr7sXsJcohiGzoPGBShggxL6VWUdW14skohFZ1LQoeV",
"cTtSTL1stvg2tmK349WTmQDfHLMLqkkxwuo8ZJeQov9zEhtYtb4u",
]
.into_iter()
.map(|wif| keypair_from_wif(wif).0)
.collect::<Vec<_>>();
assert_eq!(
RedeemScriptBuilder::with_public_keys(keys)
.quorum(3)
.to_script(),
Err(RedeemScriptError::IncorrectQuorum)
);
}
#[test]
fn test_redeem_script_from_hex_standard_short() {
RedeemScript::from(
"5321027db7837e51888e94c094703030d162c682c8dba312210f44ff440fbd5e5c24732102bdd272891c9\
e4dfc3962b1fdffd5a59732019816f9db4833634dbdaf01a401a52103280883dc31ccaee34218819aaa24\
5480c35a33acd91283586ff6d1284ed681e52103e2bc790a6e32bf5a766919ff55b1f9e9914e13aed84f5\
02c0e4171976e19deb054ae",
);
}
#[test]
fn test_redeem_script_from_hex_standard_long() {
RedeemScript::from(
"5c21031cf96b4fef362af7d86ee6c7159fa89485730dac8e3090163dd0c282dbc84f2221028839757bba9\
bdf46ae553c124479e5c3ded609495f3e93e88ab23c0f559e8be521035c70ffb21d1b454ec650e511e76f6\
bd3fe76f49c471522ee187abac8d0131a18210234acd7dee22bc23688beed0c7e42c0930cfe024204b7298\
b0b59d0e76a46476521033897e8dd88ee04cb42b69838c3167471880da23944c10eb9f67de2b5ca32a9d12\
1027a715cf0aeec55482c1d42bfeb75c8f54348ec8b0ca0f9b535ed50a739b8ad632103a2be0380e248ec3\
6401e99680e0fb4f8c03a0a5e00d5dda107aee6cba77b639521038bdb47da82981776e8b0e5d4175f27930\
339a32e77ee7052ec51a1f2f0a46e88210312c4fb516caeb5eaec8ffdeecd4a507b69d6808651ae02a4a61\
165cc56bfe55121039e021ca4d7969e5db181e0905b9baab2afe395e84587b588a6b039207c91135521025\
9c9f752846c7bd514a042d53ea305f2d4ca7873cb21937dc6b5e82afbb8fb922102c52c3dc6e080ea4e74b\
a2e6797548bd79a692a01baeba1c757a18fd0ef519fb42102f5010ab66dd7a8dc06caefeceb9bb7e6e42c5\
d4afdab527a2f02d87b758920612103efbcec8bcc6ea4e58b44214b14eae2677399c28df8bb81fcd120cb4\
c88ce3bd92103e88aa50f0d7f43cb3171a69675385f130c6abafacadde87fc84d5a194da5ad9c21025ed88\
603b59882c3ec6ef43c0b33ac9db315ecca8e7073e60d9b56145fc0efa02103643277862c4a8ab27913e3d\
2bcea109b6637c7454a03410aac8ccad445e81a502103380785c3e1c105e366ff445227cdde68e6a6461d6\
793a1437db847ecd04129dc0112ae",
);
}
#[test]
fn test_redeem_script_from_hex_not_standard() {
assert!(
"0020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
.parse::<RedeemScript>()
.is_err()
);
}
#[test]
fn test_redeem_script_convert_hex() {
let public_keys = vec![
"cMs8EwSJwfQ5DrVqYcDgjKV52k3DrGZhK1MDNrabY16WxPjvACgG",
"cVwwcsdqRGV1cV1HLX1y7ccg2iu7aSHvSVRW3sPZpgZGr6Wzg9VR",
"cNqiotwcBrkLsFMC5wwehvSQ6CcjXu74U4mEeZn6vx3ZLYH2k3QY",
"cSAyWaxS6SwWQ5REE1LuNp1Vqi771JsTFRU1ZisUHkKRiYLg6grq",
]
.into_iter()
.map(|wif| keypair_from_wif(wif).0)
.collect::<Vec<_>>();
let script = RedeemScriptBuilder::with_quorum(3)
.public_key(public_keys[0])
.public_key(public_keys[1])
.public_key(public_keys[2])
.public_key(public_keys[3])
.to_script()
.unwrap();
let string = script.to_string();
let script2 = RedeemScript::from_str(&string).unwrap();
assert_eq!(script, script2);
}
}