Skip to main content

btc_transaction_utils/
multisig.rs

1// Copyright 2018 The Exonum Team
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//   http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Helpers for manipulating with the redeem scripts which used in multisignature transactions.
16//!
17//! For a more detailed explanation, please visit the official [glossary][glossary].
18//!
19//! [glossary]: https://bitcoin.org/en/glossary/redeem-script
20
21use bitcoin::{
22    blockdata::{
23        opcodes::{all::OP_CHECKMULTISIG, Class},
24        script::{read_uint, Builder, Instruction, Script},
25    },
26    util::psbt::serialize::Serialize,
27    PublicKey,
28};
29use hex;
30use thiserror::Error;
31
32use std::{fmt, str::FromStr};
33
34/// A standard redeem script.
35#[derive(Debug, PartialEq, Clone)]
36pub struct RedeemScript(pub(crate) Script);
37
38impl RedeemScript {
39    /// Tries to parse a raw script as a standard redeem script and returns error
40    /// if the script doesn't satisfy `BIP-16` standard.
41    pub fn from_script(script: Script) -> Result<RedeemScript, RedeemScriptError> {
42        RedeemScriptContent::parse(&script)?;
43        Ok(RedeemScript(script))
44    }
45
46    /// Returns the redeem script content.
47    pub fn content(&self) -> RedeemScriptContent {
48        RedeemScriptContent::parse(&self.0).unwrap()
49    }
50}
51
52impl fmt::Display for RedeemScript {
53    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
54        fmt::LowerHex::fmt(&self.0, f)
55    }
56}
57
58impl FromStr for RedeemScript {
59    type Err = anyhow::Error;
60
61    fn from_str(s: &str) -> Result<Self, Self::Err> {
62        let script = Script::from(hex::decode(s)?);
63        RedeemScript::from_script(script).map_err(Into::into)
64    }
65}
66
67impl From<&'static str> for RedeemScript {
68    fn from(s: &'static str) -> RedeemScript {
69        RedeemScript::from_str(s).unwrap()
70    }
71}
72
73impl From<RedeemScript> for Script {
74    fn from(s: RedeemScript) -> Script {
75        s.0
76    }
77}
78
79impl AsRef<Script> for RedeemScript {
80    fn as_ref(&self) -> &Script {
81        &self.0
82    }
83}
84
85impl ::serde::Serialize for RedeemScript {
86    fn serialize<S>(&self, ser: S) -> ::std::result::Result<S::Ok, S::Error>
87    where
88        S: ::serde::Serializer,
89    {
90        ::serde_str::serialize(self, ser)
91    }
92}
93
94impl<'de> ::serde::Deserialize<'de> for RedeemScript {
95    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
96    where
97        D: ::serde::Deserializer<'de>,
98    {
99        ::serde_str::deserialize(deserializer)
100    }
101}
102
103/// Redeem script content.
104#[derive(Debug, PartialEq)]
105pub struct RedeemScriptContent {
106    /// The public keys of the participants of this redeem script.
107    pub public_keys: Vec<PublicKey>,
108    /// The number of signatures required to spend the input which corresponds
109    /// to the given redeem script.
110    pub quorum: usize,
111}
112
113impl RedeemScriptContent {
114    /// Tries to fetch redeem script content from the given raw script and returns error
115    /// if the script doesn't satisfy `BIP-16` standard.
116    pub fn parse(script: &Script) -> Result<RedeemScriptContent, RedeemScriptError> {
117        fn read_usize(instruction: Instruction) -> Option<usize> {
118            match instruction {
119                Instruction::Op(op) => {
120                    if let Class::PushNum(num) = op.classify() {
121                        Some(num as usize)
122                    } else {
123                        None
124                    }
125                }
126                Instruction::PushBytes(data) => {
127                    let num = read_uint(data, data.len()).ok()?;
128                    Some(num as usize)
129                }
130                _ => None,
131            }
132        };
133
134        let mut instructions = script.iter(true).peekable();
135        // Parses quorum.
136        let quorum = instructions
137            .next()
138            .and_then(read_usize)
139            .ok_or_else(|| RedeemScriptError::NoQuorum)?;
140        let public_keys = {
141            // Parses public keys.
142            let mut public_keys = Vec::new();
143            while let Some(Instruction::PushBytes(slice)) = instructions.peek().cloned() {
144                // HACK: `public_keys_len` can be pushed as `OP_PUSHNUM` or as `OP_PUSHBYTES`
145                // but its length cannot be greater than 1.
146                if slice.len() == 1 {
147                    break;
148                }
149                // Extracts public key from slice.
150                let pub_key =
151                    PublicKey::from_slice(slice).map_err(|_| RedeemScriptError::NotStandard)?;
152                public_keys.push(pub_key);
153                instructions.next();
154            }
155            // Checks tail.
156            let public_keys_len = instructions
157                .next()
158                .and_then(read_usize)
159                .ok_or_else(|| RedeemScriptError::NotStandard)?;
160            ensure!(
161                public_keys.len() == public_keys_len,
162                RedeemScriptError::NotEnoughPublicKeys
163            );
164            ensure!(
165                Some(Instruction::Op(OP_CHECKMULTISIG)) == instructions.next(),
166                RedeemScriptError::NotStandard
167            );
168            public_keys
169        };
170        // Returns parsed script.
171        Ok(RedeemScriptContent {
172            quorum,
173            public_keys,
174        })
175    }
176}
177
178/// The redeem script builder.
179#[derive(Debug)]
180pub struct RedeemScriptBuilder(RedeemScriptContent);
181
182impl RedeemScriptBuilder {
183    /// Creates builder.
184    pub fn new() -> RedeemScriptBuilder {
185        RedeemScriptBuilder(RedeemScriptContent {
186            quorum: 0,
187            public_keys: Vec::default(),
188        })
189    }
190
191    /// Creates builder for the given quorum value.
192    pub fn with_quorum(quorum: usize) -> RedeemScriptBuilder {
193        RedeemScriptBuilder(RedeemScriptContent {
194            quorum,
195            public_keys: Vec::default(),
196        })
197    }
198
199    /// Creates builder for the given bitcoin public keys.
200    pub fn with_public_keys<I: IntoIterator<Item = PublicKey>>(
201        public_keys: I,
202    ) -> RedeemScriptBuilder {
203        let public_keys = public_keys.into_iter().collect::<Vec<_>>();
204        let quorum = public_keys.len();
205
206        RedeemScriptBuilder(RedeemScriptContent {
207            public_keys,
208            quorum,
209        })
210    }
211
212    /// Adds a new bitcoin public key.
213    pub fn public_key<K: Into<PublicKey>>(&mut self, pub_key: K) -> &mut RedeemScriptBuilder {
214        self.0.public_keys.push(pub_key.into());
215        self
216    }
217
218    /// Sets the number of signatures required to spend the input.
219    pub fn quorum(&mut self, quorum: usize) -> &mut RedeemScriptBuilder {
220        self.0.quorum = quorum;
221        self
222    }
223
224    /// Finalizes the redeem script building.
225    pub fn to_script(&self) -> Result<RedeemScript, RedeemScriptError> {
226        let total_count = self.0.public_keys.len();
227        // Check preconditions
228        ensure!(self.0.quorum > 0, RedeemScriptError::NoQuorum);
229        ensure!(total_count != 0, RedeemScriptError::NotEnoughPublicKeys);
230        ensure!(
231            total_count >= self.0.quorum,
232            RedeemScriptError::IncorrectQuorum
233        );
234        // Construct simple redeem script in form like <1 <pubkey1> <pubkey2> 2 CHECKMULTISIG>
235        // See https://github.com/bitcoin/bips/blob/master/bip-0141.mediawiki#p2wsh
236        let mut builder = Builder::default().push_int(self.0.quorum as i64);
237        let compressed_keys = self.0.public_keys.iter().map(Serialize::serialize);
238        for key in compressed_keys {
239            builder = builder.push_slice(key.as_ref());
240        }
241        let inner = builder
242            .push_int(total_count as i64)
243            .push_opcode(OP_CHECKMULTISIG)
244            .into_script();
245        Ok(RedeemScript(inner))
246    }
247}
248
249impl Default for RedeemScriptBuilder {
250    fn default() -> Self {
251        RedeemScriptBuilder::new()
252    }
253}
254
255/// Possible errors related to the redeem script.
256#[derive(Debug, Copy, Clone, Error, PartialEq)]
257pub enum RedeemScriptError {
258    /// Not enough keys for the quorum.
259    #[error("Not enough keys for the quorum.")]
260    IncorrectQuorum,
261    /// Quorum was not set during the redeem script building.
262    #[error("Quorum was not set.")]
263    NoQuorum,
264    /// Not enough public keys. At least one public key must be specified.
265    #[error("Not enough public keys. At least one public key must be specified.")]
266    NotEnoughPublicKeys,
267    /// Given script is not the standard redeem script.
268    #[error("Given script is not the standard redeem script.")]
269    NotStandard,
270}
271
272#[cfg(test)]
273mod tests {
274    use std::str::FromStr;
275
276    use crate::{
277        multisig::{RedeemScript, RedeemScriptBuilder, RedeemScriptError},
278        test_data::keypair_from_wif,
279    };
280
281    #[test]
282    fn test_redeem_script_builder_no_quorum() {
283        assert_eq!(
284            RedeemScriptBuilder::with_quorum(0).to_script(),
285            Err(RedeemScriptError::NoQuorum)
286        );
287    }
288
289    #[test]
290    fn test_redeem_script_builder_not_enough_keys() {
291        assert_eq!(
292            RedeemScriptBuilder::with_quorum(3).to_script(),
293            Err(RedeemScriptError::NotEnoughPublicKeys)
294        );
295    }
296
297    #[test]
298    fn test_redeem_script_builder_incorrect_quorum() {
299        let keys = vec![
300            "cPHmynxvqfr7sXsJcohiGzoPGBShggxL6VWUdW14skohFZ1LQoeV",
301            "cTtSTL1stvg2tmK349WTmQDfHLMLqkkxwuo8ZJeQov9zEhtYtb4u",
302        ]
303        .into_iter()
304        .map(|wif| keypair_from_wif(wif).0)
305        .collect::<Vec<_>>();
306
307        assert_eq!(
308            RedeemScriptBuilder::with_public_keys(keys)
309                .quorum(3)
310                .to_script(),
311            Err(RedeemScriptError::IncorrectQuorum)
312        );
313    }
314
315    #[test]
316    fn test_redeem_script_from_hex_standard_short() {
317        RedeemScript::from(
318            "5321027db7837e51888e94c094703030d162c682c8dba312210f44ff440fbd5e5c24732102bdd272891c9\
319             e4dfc3962b1fdffd5a59732019816f9db4833634dbdaf01a401a52103280883dc31ccaee34218819aaa24\
320             5480c35a33acd91283586ff6d1284ed681e52103e2bc790a6e32bf5a766919ff55b1f9e9914e13aed84f5\
321             02c0e4171976e19deb054ae",
322        );
323    }
324
325    #[test]
326    fn test_redeem_script_from_hex_standard_long() {
327        RedeemScript::from(
328            "5c21031cf96b4fef362af7d86ee6c7159fa89485730dac8e3090163dd0c282dbc84f2221028839757bba9\
329             bdf46ae553c124479e5c3ded609495f3e93e88ab23c0f559e8be521035c70ffb21d1b454ec650e511e76f6\
330             bd3fe76f49c471522ee187abac8d0131a18210234acd7dee22bc23688beed0c7e42c0930cfe024204b7298\
331             b0b59d0e76a46476521033897e8dd88ee04cb42b69838c3167471880da23944c10eb9f67de2b5ca32a9d12\
332             1027a715cf0aeec55482c1d42bfeb75c8f54348ec8b0ca0f9b535ed50a739b8ad632103a2be0380e248ec3\
333             6401e99680e0fb4f8c03a0a5e00d5dda107aee6cba77b639521038bdb47da82981776e8b0e5d4175f27930\
334             339a32e77ee7052ec51a1f2f0a46e88210312c4fb516caeb5eaec8ffdeecd4a507b69d6808651ae02a4a61\
335             165cc56bfe55121039e021ca4d7969e5db181e0905b9baab2afe395e84587b588a6b039207c91135521025\
336             9c9f752846c7bd514a042d53ea305f2d4ca7873cb21937dc6b5e82afbb8fb922102c52c3dc6e080ea4e74b\
337             a2e6797548bd79a692a01baeba1c757a18fd0ef519fb42102f5010ab66dd7a8dc06caefeceb9bb7e6e42c5\
338             d4afdab527a2f02d87b758920612103efbcec8bcc6ea4e58b44214b14eae2677399c28df8bb81fcd120cb4\
339             c88ce3bd92103e88aa50f0d7f43cb3171a69675385f130c6abafacadde87fc84d5a194da5ad9c21025ed88\
340             603b59882c3ec6ef43c0b33ac9db315ecca8e7073e60d9b56145fc0efa02103643277862c4a8ab27913e3d\
341             2bcea109b6637c7454a03410aac8ccad445e81a502103380785c3e1c105e366ff445227cdde68e6a6461d6\
342             793a1437db847ecd04129dc0112ae",
343        );
344    }
345
346    #[test]
347    fn test_redeem_script_from_hex_not_standard() {
348        assert!(
349            "0020e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
350                .parse::<RedeemScript>()
351                .is_err()
352        );
353    }
354
355    #[test]
356    fn test_redeem_script_convert_hex() {
357        let public_keys = vec![
358            "cMs8EwSJwfQ5DrVqYcDgjKV52k3DrGZhK1MDNrabY16WxPjvACgG",
359            "cVwwcsdqRGV1cV1HLX1y7ccg2iu7aSHvSVRW3sPZpgZGr6Wzg9VR",
360            "cNqiotwcBrkLsFMC5wwehvSQ6CcjXu74U4mEeZn6vx3ZLYH2k3QY",
361            "cSAyWaxS6SwWQ5REE1LuNp1Vqi771JsTFRU1ZisUHkKRiYLg6grq",
362        ]
363        .into_iter()
364        .map(|wif| keypair_from_wif(wif).0)
365        .collect::<Vec<_>>();
366
367        let script = RedeemScriptBuilder::with_quorum(3)
368            .public_key(public_keys[0])
369            .public_key(public_keys[1])
370            .public_key(public_keys[2])
371            .public_key(public_keys[3])
372            .to_script()
373            .unwrap();
374        let string = script.to_string();
375        let script2 = RedeemScript::from_str(&string).unwrap();
376        assert_eq!(script, script2);
377    }
378}