Skip to main content

dubp_wallet/script/
v10.rs

1//  Copyright (C) 2020  Éloïs SANCHEZ.
2//
3// This program is free software: you can redistribute it and/or modify
4// it under the terms of the GNU Affero General Public License as
5// published by the Free Software Foundation, either version 3 of the
6// License, or (at your option) any later version.
7//
8// This program is distributed in the hope that it will be useful,
9// but WITHOUT ANY WARRANTY; without even the implied warranty of
10// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
11// GNU Affero General Public License for more details.
12//
13// You should have received a copy of the GNU Affero General Public License
14// along with this program.  If not, see <https://www.gnu.org/licenses/>.
15
16//! Define DUBP Wallet script V10.
17
18use crate::*;
19
20/// Wrap a transaction unlock proof
21#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
22pub enum WalletUnlockProofV10 {
23    /// Indicates that the signature of the corresponding key is at the bottom of the document
24    Sig(usize),
25    /// Provides the code to unlock the corresponding funds
26    Xhx(String),
27}
28
29impl Default for WalletUnlockProofV10 {
30    fn default() -> Self {
31        WalletUnlockProofV10::Sig(0)
32    }
33}
34
35impl ToString for WalletUnlockProofV10 {
36    fn to_string(&self) -> String {
37        match *self {
38            Self::Sig(ref index) => format!("SIG({})", index),
39            Self::Xhx(ref hash) => format!("XHX({})", hash),
40        }
41    }
42}
43
44/// Wrap a wallet sub script (= conditions for unlocking the sources of this wallet)
45#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Deserialize, Serialize)]
46pub enum WalletSubScriptV10 {
47    /// Single
48    Single(WalletConditionV10),
49    /// Brackets
50    Brackets(usize),
51    /// And operator
52    And(usize, usize),
53    /// Or operator
54    Or(usize, usize),
55}
56
57impl WalletSubScriptV10 {
58    fn to_raw_text(self, nodes: &[WalletSubScriptV10]) -> String {
59        match self {
60            Self::Single(cond) => cond.to_string(),
61            Self::Brackets(sub_script) => format!("({})", nodes[sub_script].to_raw_text(nodes)),
62            Self::And(sub_script_1, sub_script_2) => format!(
63                "{} && {}",
64                nodes[sub_script_1].to_raw_text(nodes),
65                nodes[sub_script_2].to_raw_text(nodes),
66            ),
67            Self::Or(sub_script_1, sub_script_2) => format!(
68                "{} || {}",
69                nodes[sub_script_1].to_raw_text(nodes),
70                nodes[sub_script_2].to_raw_text(nodes),
71            ),
72        }
73    }
74}
75
76impl WalletSubScriptV10 {
77    pub fn as_single_sig(&self) -> Option<PublicKey> {
78        if let Self::Single(WalletConditionV10::Sig(pubkey)) = self {
79            Some(*pubkey)
80        } else {
81            None
82        }
83    }
84    pub fn is_single_sig(&self) -> bool {
85        matches!(self, Self::Single(WalletConditionV10::Sig(_)))
86    }
87    fn unlockable_on(
88        self,
89        nodes: &[WalletSubScriptV10],
90        signers: &HashSet<&[u8]>,
91        codes_hash: &HashSet<Hash>,
92        source_written_on: u64,
93    ) -> Result<(u64, HashSet<UsedProofV10>), ScriptNeverUnlockableError> {
94        match self {
95            Self::Single(cond) => cond
96                .unlockable_on(signers, codes_hash, source_written_on)
97                .map(|(cond_unlockable_on, used_proof_opt)| {
98                    if let Some(used_proof) = used_proof_opt {
99                        let mut used_proofs_set = HashSet::with_capacity(1);
100                        used_proofs_set.insert(used_proof);
101                        (cond_unlockable_on, used_proofs_set)
102                    } else {
103                        (cond_unlockable_on, HashSet::with_capacity(0))
104                    }
105                }),
106            Self::Brackets(sub_script_index) => {
107                nodes[sub_script_index].unlockable_on(nodes, signers, codes_hash, source_written_on)
108            }
109            Self::And(sub_script1_index, sub_script2_index) => {
110                let (script1_unlockable_on, script1_used_proofs) = nodes[sub_script1_index]
111                    .unlockable_on(nodes, signers, codes_hash, source_written_on)?;
112                let (script2_unlockable_on, script2_used_proofs) = nodes[sub_script2_index]
113                    .unlockable_on(nodes, signers, codes_hash, source_written_on)?;
114                Ok((
115                    std::cmp::max(script1_unlockable_on, script2_unlockable_on),
116                    script1_used_proofs
117                        .union(&script2_used_proofs)
118                        .copied()
119                        .collect(),
120                ))
121            }
122            Self::Or(sub_script1_index, sub_script2_index) => {
123                let script1_unlockable_on_res = nodes[sub_script1_index].unlockable_on(
124                    nodes,
125                    signers,
126                    codes_hash,
127                    source_written_on,
128                );
129                let script2_unlockable_on_res = nodes[sub_script2_index].unlockable_on(
130                    nodes,
131                    signers,
132                    codes_hash,
133                    source_written_on,
134                );
135                match script1_unlockable_on_res {
136                    Ok((script1_unlockable_on, script1_used_proofs)) => {
137                        match script2_unlockable_on_res {
138                            Ok((script2_unlockable_on, script2_used_proofs)) => Ok((
139                                std::cmp::min(script1_unlockable_on, script2_unlockable_on),
140                                if script2_used_proofs.len() < script1_used_proofs.len() {
141                                    script2_used_proofs
142                                } else {
143                                    script1_used_proofs
144                                },
145                            )),
146                            Err(_) => Ok((script1_unlockable_on, script1_used_proofs)),
147                        }
148                    }
149                    Err(_) => script2_unlockable_on_res,
150                }
151            }
152        }
153    }
154}
155
156pub type WalletScriptNodesV10 = SmallVec<[WalletSubScriptV10; 8]>;
157
158#[derive(Debug, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
159pub struct WalletScriptV10 {
160    pub root: WalletSubScriptV10,
161    pub nodes: WalletScriptNodesV10,
162}
163
164impl ToString for WalletScriptV10 {
165    fn to_string(&self) -> String {
166        self.root.to_raw_text(&self.nodes)
167    }
168}
169
170impl WalletScriptV10 {
171    pub fn single(condition: WalletConditionV10) -> Self {
172        WalletScriptV10 {
173            root: WalletSubScriptV10::Single(condition),
174            nodes: SmallVec::new(),
175        }
176    }
177    pub fn single_sig(pubkey: PublicKey) -> Self {
178        WalletScriptV10 {
179            root: WalletSubScriptV10::Single(WalletConditionV10::Sig(pubkey)),
180            nodes: SmallVec::new(),
181        }
182    }
183    pub fn as_single_sig(&self) -> Option<PublicKey> {
184        if self.nodes.is_empty() {
185            self.root.as_single_sig()
186        } else {
187            None
188        }
189    }
190    pub fn is_single_sig(&self) -> bool {
191        self.nodes.is_empty() && self.root.is_single_sig()
192    }
193    pub fn and(cond1: WalletConditionV10, cond2: WalletConditionV10) -> Self {
194        let mut nodes = SmallVec::new();
195        nodes.push(WalletSubScriptV10::Single(cond1));
196        nodes.push(WalletSubScriptV10::Single(cond2));
197
198        WalletScriptV10 {
199            root: WalletSubScriptV10::And(0, 1),
200            nodes,
201        }
202    }
203    pub fn and_and(
204        cond1: WalletConditionV10,
205        cond2: WalletConditionV10,
206        cond3: WalletConditionV10,
207    ) -> Self {
208        let mut nodes = SmallVec::new();
209        nodes.push(WalletSubScriptV10::Single(cond1));
210        nodes.push(WalletSubScriptV10::And(2, 3));
211        nodes.push(WalletSubScriptV10::Single(cond2));
212        nodes.push(WalletSubScriptV10::Single(cond3));
213
214        WalletScriptV10 {
215            root: WalletSubScriptV10::And(0, 1),
216            nodes,
217        }
218    }
219    pub fn or(cond1: WalletConditionV10, cond2: WalletConditionV10) -> Self {
220        let mut nodes = SmallVec::new();
221        nodes.push(WalletSubScriptV10::Single(cond1));
222        nodes.push(WalletSubScriptV10::Single(cond2));
223
224        WalletScriptV10 {
225            root: WalletSubScriptV10::Or(0, 1),
226            nodes,
227        }
228    }
229    pub fn pubkeys(&self) -> BTreeSet<PublicKey> {
230        let mut pubkeys = BTreeSet::new();
231        if let WalletSubScriptV10::Single(WalletConditionV10::Sig(pubkey)) = self.root {
232            pubkeys.insert(pubkey);
233        }
234        for node in &self.nodes {
235            if let WalletSubScriptV10::Single(WalletConditionV10::Sig(pubkey)) = node {
236                pubkeys.insert(*pubkey);
237            }
238        }
239        pubkeys
240    }
241    pub(crate) fn unlockable_on(
242        &self,
243        signers: &HashSet<&[u8]>,
244        codes_hash: &HashSet<Hash>,
245        source_written_on: u64,
246    ) -> Result<(u64, HashSet<UsedProofV10>), ScriptNeverUnlockableError> {
247        self.root
248            .unlockable_on(&self.nodes, signers, codes_hash, source_written_on)
249    }
250}
251
252#[derive(Clone, Copy, Debug, Error, PartialEq)]
253#[error("Script never unlockable")]
254pub struct ScriptNeverUnlockableError;
255
256/// Wrap wallet condition (= one condition in wallet script)
257#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Deserialize, Serialize)]
258pub enum WalletConditionV10 {
259    /// The consumption of funds will require a valid signature of the specified key
260    Sig(PublicKey),
261    /// The consumption of funds will require to provide a code with the hash indicated
262    Xhx(Hash),
263    /// Funds may not be consumed until the blockchain reaches the timestamp indicated.
264    Cltv(u64),
265    /// Funds may not be consumed before the duration indicated, starting from the timestamp of the block where the transaction is written.
266    Csv(u64),
267}
268
269impl ToString for WalletConditionV10 {
270    fn to_string(&self) -> String {
271        match *self {
272            Self::Sig(ref pubkey) => format!("SIG({})", pubkey),
273            Self::Xhx(ref hash) => format!("XHX({})", hash),
274            Self::Cltv(timestamp) => format!("CLTV({})", timestamp),
275            Self::Csv(duration) => format!("CSV({})", duration),
276        }
277    }
278}
279
280#[derive(Copy, Clone, Debug, Eq, Hash, PartialEq)]
281pub(crate) enum UsedProofV10 {
282    Sig(PublicKey),
283    CodeHash(Hash),
284}
285
286impl WalletConditionV10 {
287    pub(crate) fn unlockable_on(
288        &self,
289        signers: &HashSet<&[u8]>,
290        codes_hash: &HashSet<Hash>,
291        source_written_on: u64,
292    ) -> Result<(u64, Option<UsedProofV10>), ScriptNeverUnlockableError> {
293        match self {
294            Self::Sig(pubkey) => {
295                if signers.contains(&pubkey.as_ref()[..32]) {
296                    Ok((0, Some(UsedProofV10::Sig(*pubkey))))
297                } else {
298                    Err(ScriptNeverUnlockableError)
299                }
300            }
301            Self::Xhx(code_hash) => {
302                if codes_hash.contains(code_hash) {
303                    Ok((0, Some(UsedProofV10::CodeHash(*code_hash))))
304                } else {
305                    Err(ScriptNeverUnlockableError)
306                }
307            }
308            Self::Cltv(timestamp) => Ok((*timestamp, None)),
309            Self::Csv(duration_secs) => Ok((source_written_on + *duration_secs, None)),
310        }
311    }
312}
313
314#[cfg(test)]
315mod tests {
316    use super::*;
317    use dubp_common::crypto::keys::PublicKey as _;
318    use maplit::hashset;
319    use smallvec::smallvec as svec;
320    use unwrap::unwrap;
321
322    #[test]
323    fn test_script_or_unlockable_on() {
324        let p1 = unwrap!(PublicKey::from_base58(
325            "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C"
326        ));
327        let cond1 = WalletConditionV10::Sig(p1);
328        let cond2 = WalletConditionV10::Cltv(123);
329        let script = WalletScriptV10::or(cond1, cond2);
330
331        assert_eq!(
332            Ok((0, hashset![])),
333            script.unlockable_on(&hashset![&p1.as_ref()[..32]], &hashset![], 0),
334        );
335        assert_eq!(
336            Ok((123, hashset![])),
337            script.unlockable_on(&hashset![&[0u8; 32][..]], &hashset![], 0),
338        );
339    }
340
341    #[test]
342    fn test_script_and_unlockable_on() {
343        let p1 = unwrap!(PublicKey::from_base58(
344            "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C"
345        ));
346        let cond1 = WalletConditionV10::Sig(p1);
347        let cond2 = WalletConditionV10::Cltv(123);
348        let script = WalletScriptV10::and(cond1, cond2);
349
350        assert_eq!(
351            Ok((123, hashset![UsedProofV10::Sig(p1)])),
352            script.unlockable_on(&hashset![&p1.as_ref()[..32]], &hashset![], 0)
353        );
354        assert_eq!(
355            Err(ScriptNeverUnlockableError),
356            script.unlockable_on(&hashset![&[0u8; 32][..]], &hashset![], 0),
357        );
358    }
359
360    #[test]
361    fn test_script_complex() {
362        let p1 = unwrap!(PublicKey::from_base58(
363            "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C"
364        ));
365        let h1 = unwrap!(Hash::from_hex(
366            "3D8BF2B661155EA073D80A1E1171212261AD4D21F2E41737BDE192871C469ABE"
367        ));
368        let cond1 = WalletConditionV10::Sig(p1);
369        let cond2 = WalletConditionV10::Cltv(123);
370        let cond3 = WalletConditionV10::Xhx(h1);
371
372        let script = WalletScriptV10 {
373            root: WalletSubScriptV10::Or(0, 4),
374            nodes: svec![
375                WalletSubScriptV10::Single(cond3),
376                WalletSubScriptV10::Single(cond1),
377                WalletSubScriptV10::Single(cond2),
378                WalletSubScriptV10::And(1, 2),
379                WalletSubScriptV10::Brackets(3),
380            ],
381        };
382
383        assert_eq!(
384            "XHX(3D8BF2B661155EA073D80A1E1171212261AD4D21F2E41737BDE192871C469ABE) || (SIG(D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C) && CLTV(123))",
385            script.to_string()
386        );
387        assert_eq!(
388            Ok((123, hashset![UsedProofV10::Sig(p1)])),
389            script.unlockable_on(&hashset![&p1.as_ref()[..32]], &hashset![], 0),
390        );
391    }
392
393    #[test]
394    fn test_sig_cond_unlockable_on() {
395        let p1 = unwrap!(PublicKey::from_base58(
396            "D7CYHJXjaH4j7zRdWngUbsURPnSnjsCYtvo6f8dvW3C"
397        ));
398        let cond = WalletConditionV10::Sig(p1);
399
400        assert_eq!(
401            Ok((0, Some(UsedProofV10::Sig(p1)))),
402            cond.unlockable_on(&hashset![&p1.as_ref()[..32]], &hashset![], 0),
403        );
404        assert_eq!(
405            Err(ScriptNeverUnlockableError),
406            cond.unlockable_on(&hashset![&[0u8; 32][..]], &hashset![], 0),
407        );
408    }
409
410    #[test]
411    fn test_xhx_cond_unlockable_on() {
412        let h1 = Hash::compute(b"1");
413        let cond = WalletConditionV10::Xhx(h1);
414
415        assert_eq!(
416            Ok((0, Some(UsedProofV10::CodeHash(h1)))),
417            cond.unlockable_on(&hashset![], &hashset![h1], 0),
418        );
419        assert_eq!(
420            Err(ScriptNeverUnlockableError),
421            cond.unlockable_on(&hashset![&[0u8; 32][..]], &hashset![], 0),
422        );
423    }
424
425    #[test]
426    fn test_cltv_cond_unlockable_on() {
427        let cond = WalletConditionV10::Cltv(123);
428
429        assert_eq!(
430            Ok((123, None)),
431            cond.unlockable_on(&hashset![], &hashset![], 0),
432        );
433    }
434
435    #[test]
436    fn test_csv_cond_unlockable_on() {
437        let cond = WalletConditionV10::Csv(123);
438
439        assert_eq!(
440            Ok((369, None)),
441            cond.unlockable_on(&hashset![], &hashset![], 246),
442        );
443    }
444
445    #[test]
446    fn test_and_and() {
447        let cond1 = WalletConditionV10::Csv(123);
448        let cond2 = WalletConditionV10::Csv(456);
449        let cond3 = WalletConditionV10::Csv(789);
450
451        let script = WalletScriptV10::and_and(cond1, cond2, cond3);
452        assert_eq!(&script.to_string(), "CSV(123) && CSV(456) && CSV(789)");
453    }
454
455    #[test]
456    fn test_as_single_sig() {
457        assert_eq!(
458            WalletScriptV10::single_sig(PublicKey::default()).as_single_sig(),
459            Some(PublicKey::default())
460        );
461        assert!(WalletScriptV10::single(WalletConditionV10::Csv(100))
462            .as_single_sig()
463            .is_none());
464
465        let cond1 = WalletConditionV10::Csv(123);
466        let cond2 = WalletConditionV10::Csv(456);
467        assert!(WalletScriptV10::and(cond1, cond2).as_single_sig().is_none());
468    }
469
470    #[test]
471    fn test_is_single_sig() {
472        assert!(WalletScriptV10::single_sig(PublicKey::default()).is_single_sig());
473        assert!(!WalletScriptV10::single(WalletConditionV10::Csv(100)).is_single_sig());
474
475        let cond1 = WalletConditionV10::Csv(123);
476        let cond2 = WalletConditionV10::Csv(456);
477        assert!(!WalletScriptV10::and(cond1, cond2).is_single_sig());
478    }
479}