solana_budget_program/
budget_expr.rs

1//! The `budget_expr` module provides a domain-specific language for payment plans. Users create BudgetExpr objects that
2//! are given to an interpreter. The interpreter listens for `Witness` transactions,
3//! which it uses to reduce the payment plan. When the budget is reduced to a
4//! `Payment`, the payment is executed.
5
6use chrono::prelude::*;
7use serde_derive::{Deserialize, Serialize};
8use solana_sdk::hash::Hash;
9use solana_sdk::pubkey::Pubkey;
10
11/// The types of events a payment plan can process.
12#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
13pub enum Witness {
14    /// The current time.
15    Timestamp(DateTime<Utc>),
16
17    /// A signature from Pubkey.
18    Signature,
19
20    /// Account snapshot.
21    AccountData(Hash, Pubkey),
22}
23
24/// Some amount of lamports that should be sent to the `to` `Pubkey`.
25#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
26pub struct Payment {
27    /// Amount to be paid.
28    pub lamports: u64,
29
30    /// The `Pubkey` that `lamports` should be paid to.
31    pub to: Pubkey,
32}
33
34/// The account constraints a Condition would wait on.
35/// Note: ideally this would be function that accepts an Account and returns
36/// a bool, but we don't have a way to pass functions over the wire. To simulate
37/// higher order programming, create your own program that includes an instruction
38/// that sets account data to a boolean. Pass that account key and program_id here.
39#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
40pub struct AccountConstraints {
41    /// The account holder.
42    pub key: Pubkey,
43
44    /// The program id that must own the account at `key`.
45    pub program_id: Pubkey,
46
47    /// The hash of the data in the account at `key`.
48    pub data_hash: Hash,
49}
50
51/// A data type representing a `Witness` that the payment plan is waiting on.
52#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
53pub enum Condition {
54    /// Wait for a `Timestamp` `Witness` at or after the given `DateTime`.
55    Timestamp(DateTime<Utc>, Pubkey),
56
57    /// Wait for a `Signature` `Witness` from `Pubkey`.
58    Signature(Pubkey),
59
60    /// Wait for the account with the given constraints.
61    AccountData(AccountConstraints),
62}
63
64impl Condition {
65    /// Return true if the given Witness satisfies this Condition.
66    pub fn is_satisfied(&self, witness: &Witness, from: &Pubkey) -> bool {
67        match (self, witness) {
68            (Condition::Signature(pubkey), Witness::Signature) => pubkey == from,
69            (Condition::Timestamp(dt, pubkey), Witness::Timestamp(last_time)) => {
70                pubkey == from && dt <= last_time
71            }
72            (
73                Condition::AccountData(constraints),
74                Witness::AccountData(actual_hash, program_id),
75            ) => {
76                constraints.program_id == *program_id
77                    && constraints.key == *from
78                    && constraints.data_hash == *actual_hash
79            }
80            _ => false,
81        }
82    }
83}
84
85/// A data type representing a payment plan.
86#[repr(C)]
87#[derive(Serialize, Deserialize, Debug, PartialEq, Eq, Clone)]
88pub enum BudgetExpr {
89    /// Make a payment.
90    Pay(Payment),
91
92    /// Make a payment after some condition.
93    After(Condition, Box<BudgetExpr>),
94
95    /// Either make a payment after one condition or a different payment after another
96    /// condition, which ever condition is satisfied first.
97    Or((Condition, Box<BudgetExpr>), (Condition, Box<BudgetExpr>)),
98
99    /// Make a payment after both of two conditions are satisfied
100    And(Condition, Condition, Box<BudgetExpr>),
101}
102
103impl BudgetExpr {
104    /// Create the simplest budget - one that pays `lamports` to Pubkey.
105    pub fn new_payment(lamports: u64, to: &Pubkey) -> Self {
106        BudgetExpr::Pay(Payment { lamports, to: *to })
107    }
108
109    /// Create a budget that pays `lamports` to `to` after being witnessed by `from`.
110    pub fn new_authorized_payment(from: &Pubkey, lamports: u64, to: &Pubkey) -> Self {
111        BudgetExpr::After(
112            Condition::Signature(*from),
113            Box::new(Self::new_payment(lamports, to)),
114        )
115    }
116
117    /// Create a budget that pays `lamports` to `to` after witnessing account data in `account_pubkey` with the given hash.
118    pub fn new_payment_when_account_data(
119        account_pubkey: &Pubkey,
120        account_program_id: &Pubkey,
121        account_hash: Hash,
122        lamports: u64,
123        to: &Pubkey,
124    ) -> Self {
125        BudgetExpr::After(
126            Condition::AccountData(AccountConstraints {
127                key: *account_pubkey,
128                program_id: *account_program_id,
129                data_hash: account_hash,
130            }),
131            Box::new(Self::new_payment(lamports, to)),
132        )
133    }
134
135    /// Create a budget that pays `lamports` to `to` after being witnessed by `witness` unless
136    /// canceled with a signature from `from`.
137    pub fn new_cancelable_authorized_payment(
138        witness: &Pubkey,
139        lamports: u64,
140        to: &Pubkey,
141        from: Option<Pubkey>,
142    ) -> Self {
143        if from.is_none() {
144            return Self::new_authorized_payment(witness, lamports, to);
145        }
146        let from = from.unwrap();
147        BudgetExpr::Or(
148            (
149                Condition::Signature(*witness),
150                Box::new(BudgetExpr::new_payment(lamports, to)),
151            ),
152            (
153                Condition::Signature(from),
154                Box::new(BudgetExpr::new_payment(lamports, &from)),
155            ),
156        )
157    }
158
159    /// Create a budget that pays lamports` to `to` after being witnessed by 2x `from`s
160    pub fn new_2_2_multisig_payment(
161        from0: &Pubkey,
162        from1: &Pubkey,
163        lamports: u64,
164        to: &Pubkey,
165    ) -> Self {
166        BudgetExpr::And(
167            Condition::Signature(*from0),
168            Condition::Signature(*from1),
169            Box::new(Self::new_payment(lamports, to)),
170        )
171    }
172
173    /// Create a budget that pays `lamports` to `to` after the given DateTime signed
174    /// by `dt_pubkey`.
175    pub fn new_future_payment(
176        dt: DateTime<Utc>,
177        dt_pubkey: &Pubkey,
178        lamports: u64,
179        to: &Pubkey,
180    ) -> Self {
181        BudgetExpr::After(
182            Condition::Timestamp(dt, *dt_pubkey),
183            Box::new(Self::new_payment(lamports, to)),
184        )
185    }
186
187    /// Create a budget that pays `lamports` to `to` after the given DateTime
188    /// signed by `dt_pubkey` unless canceled by `from`.
189    pub fn new_cancelable_future_payment(
190        dt: DateTime<Utc>,
191        dt_pubkey: &Pubkey,
192        lamports: u64,
193        to: &Pubkey,
194        from: Option<Pubkey>,
195    ) -> Self {
196        if from.is_none() {
197            return Self::new_future_payment(dt, dt_pubkey, lamports, to);
198        }
199        let from = from.unwrap();
200        BudgetExpr::Or(
201            (
202                Condition::Timestamp(dt, *dt_pubkey),
203                Box::new(Self::new_payment(lamports, to)),
204            ),
205            (
206                Condition::Signature(from),
207                Box::new(Self::new_payment(lamports, &from)),
208            ),
209        )
210    }
211
212    /// Return Payment if the budget requires no additional Witnesses.
213    pub fn final_payment(&self) -> Option<Payment> {
214        match self {
215            BudgetExpr::Pay(payment) => Some(payment.clone()),
216            _ => None,
217        }
218    }
219
220    /// Return true if the budget spends exactly `spendable_lamports`.
221    pub fn verify(&self, spendable_lamports: u64) -> bool {
222        match self {
223            BudgetExpr::Pay(payment) => payment.lamports == spendable_lamports,
224            BudgetExpr::After(_, sub_expr) | BudgetExpr::And(_, _, sub_expr) => {
225                sub_expr.verify(spendable_lamports)
226            }
227            BudgetExpr::Or(a, b) => {
228                a.1.verify(spendable_lamports) && b.1.verify(spendable_lamports)
229            }
230        }
231    }
232
233    /// Apply a witness to the budget to see if the budget can be reduced.
234    /// If so, modify the budget in-place.
235    pub fn apply_witness(&mut self, witness: &Witness, from: &Pubkey) {
236        let new_expr = match self {
237            BudgetExpr::After(cond, sub_expr) if cond.is_satisfied(witness, from) => {
238                Some(sub_expr.clone())
239            }
240            BudgetExpr::Or((cond, sub_expr), _) if cond.is_satisfied(witness, from) => {
241                Some(sub_expr.clone())
242            }
243            BudgetExpr::Or(_, (cond, sub_expr)) if cond.is_satisfied(witness, from) => {
244                Some(sub_expr.clone())
245            }
246            BudgetExpr::And(cond0, cond1, sub_expr) => {
247                if cond0.is_satisfied(witness, from) {
248                    Some(Box::new(BudgetExpr::After(cond1.clone(), sub_expr.clone())))
249                } else if cond1.is_satisfied(witness, from) {
250                    Some(Box::new(BudgetExpr::After(cond0.clone(), sub_expr.clone())))
251                } else {
252                    None
253                }
254            }
255            _ => None,
256        };
257        if let Some(expr) = new_expr {
258            *self = *expr;
259        }
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    #[test]
268    fn test_signature_satisfied() {
269        let from = Pubkey::default();
270        assert!(Condition::Signature(from).is_satisfied(&Witness::Signature, &from));
271    }
272
273    #[test]
274    fn test_timestamp_satisfied() {
275        let dt1 = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
276        let dt2 = Utc.ymd(2014, 11, 14).and_hms(10, 9, 8);
277        let from = Pubkey::default();
278        assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt1), &from));
279        assert!(Condition::Timestamp(dt1, from).is_satisfied(&Witness::Timestamp(dt2), &from));
280        assert!(!Condition::Timestamp(dt2, from).is_satisfied(&Witness::Timestamp(dt1), &from));
281    }
282
283    #[test]
284    fn test_verify() {
285        let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
286        let from = Pubkey::default();
287        let to = Pubkey::default();
288        assert!(BudgetExpr::new_payment(42, &to).verify(42));
289        assert!(BudgetExpr::new_authorized_payment(&from, 42, &to).verify(42));
290        assert!(BudgetExpr::new_future_payment(dt, &from, 42, &to).verify(42));
291        assert!(
292            BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from)).verify(42)
293        );
294    }
295
296    #[test]
297    fn test_authorized_payment() {
298        let from = Pubkey::default();
299        let to = Pubkey::default();
300
301        let mut expr = BudgetExpr::new_authorized_payment(&from, 42, &to);
302        expr.apply_witness(&Witness::Signature, &from);
303        assert_eq!(expr, BudgetExpr::new_payment(42, &to));
304    }
305
306    #[test]
307    fn test_future_payment() {
308        let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
309        let from = solana_sdk::pubkey::new_rand();
310        let to = solana_sdk::pubkey::new_rand();
311
312        let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
313        expr.apply_witness(&Witness::Timestamp(dt), &from);
314        assert_eq!(expr, BudgetExpr::new_payment(42, &to));
315    }
316
317    #[test]
318    fn test_unauthorized_future_payment() {
319        // Ensure timestamp will only be acknowledged if it came from the
320        // whitelisted public key.
321        let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
322        let from = solana_sdk::pubkey::new_rand();
323        let to = solana_sdk::pubkey::new_rand();
324
325        let mut expr = BudgetExpr::new_future_payment(dt, &from, 42, &to);
326        let orig_expr = expr.clone();
327        expr.apply_witness(&Witness::Timestamp(dt), &to); // <-- Attack!
328        assert_eq!(expr, orig_expr);
329    }
330
331    #[test]
332    fn test_cancelable_future_payment() {
333        let dt = Utc.ymd(2014, 11, 14).and_hms(8, 9, 10);
334        let from = Pubkey::default();
335        let to = Pubkey::default();
336
337        let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
338        expr.apply_witness(&Witness::Timestamp(dt), &from);
339        assert_eq!(expr, BudgetExpr::new_payment(42, &to));
340
341        let mut expr = BudgetExpr::new_cancelable_future_payment(dt, &from, 42, &to, Some(from));
342        expr.apply_witness(&Witness::Signature, &from);
343        assert_eq!(expr, BudgetExpr::new_payment(42, &from));
344    }
345    #[test]
346    fn test_2_2_multisig_payment() {
347        let from0 = solana_sdk::pubkey::new_rand();
348        let from1 = solana_sdk::pubkey::new_rand();
349        let to = Pubkey::default();
350
351        let mut expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
352        expr.apply_witness(&Witness::Signature, &from0);
353        assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
354    }
355
356    #[test]
357    fn test_multisig_after_sig() {
358        let from0 = solana_sdk::pubkey::new_rand();
359        let from1 = solana_sdk::pubkey::new_rand();
360        let from2 = solana_sdk::pubkey::new_rand();
361        let to = Pubkey::default();
362
363        let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
364        let mut expr = BudgetExpr::After(Condition::Signature(from2), Box::new(expr));
365
366        expr.apply_witness(&Witness::Signature, &from2);
367        expr.apply_witness(&Witness::Signature, &from0);
368        assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
369    }
370
371    #[test]
372    fn test_multisig_after_ts() {
373        let from0 = solana_sdk::pubkey::new_rand();
374        let from1 = solana_sdk::pubkey::new_rand();
375        let dt = Utc.ymd(2014, 11, 11).and_hms(7, 7, 7);
376        let to = Pubkey::default();
377
378        let expr = BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to);
379        let mut expr = BudgetExpr::After(Condition::Timestamp(dt, from0), Box::new(expr));
380
381        expr.apply_witness(&Witness::Timestamp(dt), &from0);
382        assert_eq!(
383            expr,
384            BudgetExpr::new_2_2_multisig_payment(&from0, &from1, 42, &to)
385        );
386
387        expr.apply_witness(&Witness::Signature, &from0);
388        assert_eq!(expr, BudgetExpr::new_authorized_payment(&from1, 42, &to));
389    }
390}