mina_tree/scan_state/
fee_excess.rs

1//! Fee excesses associated with transactions or transitions.
2//!
3//! These are represented as a 'left' and 'right' excess, which describe the
4//! unresolved fee excesses in the fee tokens of the first (or leftmost) and
5//! last (or rightmost) transactions in the transition.
6//!
7//! Assumptions:
8//! * Transactions are grouped by their fee token.
9//! * The 'fee transfer' transaction to dispense those fees is part of this
10//!   group.
11//! * The fee excess for each token is 0 across the group.
12//! * No transactions with fees paid in another token are executed while the
13//!   previous fee token's excess is non-zero.
14//!
15//! By maintaining these assumptions, we can ensure that the un-settled fee
16//! excesses can be represented by excesses in (at most) 2 tokens.
17//! Consider, for example, any consecutive subsequence of the transactions
18//!
19//! `..[txn@2][ft@2][txn@3][txn@3][ft@3][txn@4][ft@4][txn@5][txn@5][ft@5][txn@6][ft@6]..`
20//!
21//! where `[txn@i]` and `[ft@i]` are transactions and fee transfers respectively
22//! paid in token i.
23//! The only groups which may have non-zero fee excesses are those which
24//! contain the start and end of the subsequence.
25//!
26//! The code below also defines a canonical representation where fewer
27//! than 2 tokens have non-zero excesses. See the internal function
28//! `FeeExcess::rebalance` below for details and the implementation.
29//!
30//!
31//! Port of the implementation from:
32//! <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/fee_excess.ml#L1>
33
34use ark_ff::{BigInteger, BigInteger256, Zero};
35use mina_curves::pasta::Fp;
36use poseidon::hash::Inputs;
37
38use crate::{
39    proofs::{
40        field::{field, Boolean, FieldWitness},
41        numbers::currency::{CheckedFee, CheckedSigned},
42        witness::Witness,
43    },
44    AppendToInputs, ToInputs, TokenId,
45};
46
47use super::{
48    currency::{Fee, Magnitude, Sgn, Signed},
49    scan_state::transaction_snark::OneOrTwo,
50};
51
52#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct FeeExcess {
54    pub fee_token_l: TokenId,
55    pub fee_excess_l: Signed<Fee>,
56    pub fee_token_r: TokenId,
57    pub fee_excess_r: Signed<Fee>,
58}
59
60#[derive(Debug, Clone)]
61pub struct CheckedFeeExcess<F: FieldWitness> {
62    pub fee_token_l: TokenId,
63    pub fee_excess_l: CheckedSigned<F, CheckedFee<F>>,
64    pub fee_token_r: TokenId,
65    pub fee_excess_r: CheckedSigned<F, CheckedFee<F>>,
66}
67
68impl ToInputs for FeeExcess {
69    /// <https://github.com/MinaProtocol/mina/blob/4e0b324912017c3ff576704ee397ade3d9bda412/src/lib/mina_base/fee_excess.ml#L162>
70    fn to_inputs(&self, inputs: &mut Inputs) {
71        let Self {
72            fee_token_l,
73            fee_excess_l,
74            fee_token_r,
75            fee_excess_r,
76        } = self;
77
78        inputs.append(fee_token_l);
79        inputs.append(fee_excess_l);
80        inputs.append(fee_token_r);
81        inputs.append(fee_excess_r);
82    }
83}
84
85impl FeeExcess {
86    pub fn empty() -> Self {
87        Self {
88            fee_token_l: TokenId::default(),
89            fee_excess_l: Signed::<Fee>::zero(),
90            fee_token_r: TokenId::default(),
91            fee_excess_r: Signed::<Fee>::zero(),
92        }
93    }
94
95    pub fn is_zero(&self) -> bool {
96        let Self {
97            fee_token_l,
98            fee_excess_l,
99            fee_token_r,
100            fee_excess_r,
101        } = self;
102
103        fee_token_l.is_default()
104            && fee_token_r.is_default()
105            && fee_excess_l.is_zero()
106            && fee_excess_r.is_zero()
107    }
108
109    /// <https://github.com/MinaProtocol/mina/blob/e5183ca1dde1c085b4c5d37d1d9987e24c294c32/src/lib/mina_base/fee_excess.ml#L536>
110    pub fn of_one_or_two(excesses: OneOrTwo<(TokenId, Signed<Fee>)>) -> Result<Self, String> {
111        match excesses {
112            OneOrTwo::One((fee_token_l, fee_excess_l)) => Self {
113                fee_token_l,
114                fee_excess_l,
115                fee_token_r: TokenId::default(),
116                fee_excess_r: Signed::<Fee>::zero(),
117            },
118            OneOrTwo::Two(((fee_token_l, fee_excess_l), (fee_token_r, fee_excess_r))) => Self {
119                fee_token_l,
120                fee_excess_l,
121                fee_token_r,
122                fee_excess_r,
123            },
124        }
125        .rebalance()
126    }
127
128    /// <https://github.com/MinaProtocol/mina/blob/e5183ca1dde1c085b4c5d37d1d9987e24c294c32/src/lib/mina_base/fee_excess.ml#L526>
129    pub fn of_single((fee_token_l, fee_excess_l): (TokenId, Signed<Fee>)) -> Self {
130        Self {
131            fee_token_l,
132            fee_excess_l,
133            fee_token_r: TokenId::default(),
134            fee_excess_r: Signed::<Fee>::zero(),
135        }
136    }
137
138    /// 'Rebalance' to a canonical form, where
139    /// - if there is only 1 nonzero excess, it is to the left
140    /// - any zero fee excess has the default token
141    /// - if the fee tokens are the same, the excesses are combined
142    ///
143    /// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/fee_excess.ml#L301>
144    fn rebalance(&self) -> Result<Self, String> {
145        let Self {
146            fee_token_l,
147            fee_excess_l,
148            fee_token_r,
149            fee_excess_r,
150        } = self;
151
152        // Use the same token for both if [fee_excess_l] is zero.
153        let fee_token_l = if fee_excess_l.magnitude.is_zero() {
154            fee_token_r
155        } else {
156            fee_token_l
157        };
158
159        // Rebalancing.
160        let (fee_excess_l, fee_excess_r) = if fee_token_l == fee_token_r {
161            match fee_excess_l.add(fee_excess_r) {
162                Some(fee_excess_l) => (fee_excess_l, Signed::<Fee>::zero()),
163                None => return Err("Error adding fees: overflow".to_string()),
164            }
165        } else {
166            (*fee_excess_l, *fee_excess_r)
167        };
168
169        // Use the default token if the excess is zero.
170        // This allows [verify_complete_merge] to verify a proof without knowledge of
171        // the particular fee tokens used.
172
173        let fee_token_l = if fee_excess_l.magnitude.is_zero() {
174            TokenId::default()
175        } else {
176            fee_token_l.clone()
177        };
178        let fee_token_r = if fee_excess_r.magnitude.is_zero() {
179            TokenId::default()
180        } else {
181            fee_token_r.clone()
182        };
183
184        Ok(Self {
185            fee_token_l,
186            fee_excess_l,
187            fee_token_r,
188            fee_excess_r,
189        })
190    }
191
192    fn rebalance_checked(
193        fee_token_l: TokenId,
194        fee_excess_l: Fp,
195        fee_token_r: TokenId,
196        fee_excess_r: Fp,
197        w: &mut Witness<Fp>,
198    ) -> (TokenId, Fp, TokenId, Fp) {
199        let fee_token_l = {
200            let excess_is_zero = field::equal(Fp::zero(), fee_excess_l, w);
201            w.exists_no_check(match excess_is_zero {
202                Boolean::True => &fee_token_r,
203                Boolean::False => &fee_token_l,
204            })
205        };
206
207        let (fee_excess_l, fee_excess_r) = {
208            let tokens_equal = field::equal(fee_token_l.0, fee_token_r.0, w);
209            let amount_to_move = w.exists_no_check(match tokens_equal {
210                Boolean::True => fee_excess_r,
211                Boolean::False => Fp::zero(),
212            });
213
214            (fee_excess_l + amount_to_move, fee_excess_r + amount_to_move)
215        };
216
217        let fee_token_l = {
218            let excess_is_zero = field::equal(Fp::zero(), fee_excess_l, w);
219            w.exists_no_check(match excess_is_zero {
220                Boolean::True => TokenId::default(),
221                Boolean::False => fee_token_l.clone(),
222            })
223        };
224
225        let fee_token_r = {
226            let excess_is_zero = field::equal(Fp::zero(), fee_excess_r, w);
227            w.exists_no_check(match excess_is_zero {
228                Boolean::True => TokenId::default(),
229                Boolean::False => fee_token_r.clone(),
230            })
231        };
232
233        (fee_token_l, fee_excess_l, fee_token_r, fee_excess_r)
234    }
235
236    /// Combine the fee excesses from two transitions.
237    ///
238    /// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/fee_excess.ml#L380>
239    pub fn combine(
240        Self {
241            fee_token_l: fee_token1_l,
242            fee_excess_l: fee_excess1_l,
243            fee_token_r: fee_token1_r,
244            fee_excess_r: fee_excess1_r,
245        }: &Self,
246        Self {
247            fee_token_l: fee_token2_l,
248            fee_excess_l: fee_excess2_l,
249            fee_token_r: fee_token2_r,
250            fee_excess_r: fee_excess2_r,
251        }: &Self,
252    ) -> Result<Self, String> {
253        // Eliminate fee_excess1_r.
254        // [1l; 1r; 2l; 2r] -> [1l; 2l; 2r]
255        let ((fee_token1_l, fee_excess1_l), (fee_token2_l, fee_excess2_l)) = eliminate_fee_excess(
256            (fee_token1_l, fee_excess1_l),
257            (fee_token1_r, fee_excess1_r),
258            (fee_token2_l, fee_excess2_l),
259        )?;
260
261        // Eliminate fee_excess2_l.
262        // [1l; 2l; 2r] -> [1l; 2r]
263        let ((fee_token1_l, fee_excess1_l), (fee_token2_r, fee_excess2_r)) = eliminate_fee_excess(
264            (fee_token1_l, &fee_excess1_l),
265            (fee_token2_l, &fee_excess2_l),
266            (fee_token2_r, fee_excess2_r),
267        )?;
268
269        Self {
270            fee_token_l: fee_token1_l.clone(),
271            fee_excess_l: fee_excess1_l,
272            fee_token_r: fee_token2_r.clone(),
273            fee_excess_r: fee_excess2_r,
274        }
275        .rebalance()
276    }
277
278    pub fn combine_checked(
279        Self {
280            fee_token_l: fee_token1_l,
281            fee_excess_l: fee_excess1_l,
282            fee_token_r: fee_token1_r,
283            fee_excess_r: fee_excess1_r,
284        }: &Self,
285        Self {
286            fee_token_l: fee_token2_l,
287            fee_excess_l: fee_excess2_l,
288            fee_token_r: fee_token2_r,
289            fee_excess_r: fee_excess2_r,
290        }: &Self,
291        w: &mut Witness<Fp>,
292    ) -> (TokenId, Signed<Fee>, TokenId, Signed<Fee>) {
293        // Represent amounts as field elements.
294        let fee_excess1_l = fee_excess1_l.to_checked::<Fp>().value(w);
295        let fee_excess1_r = fee_excess1_r.to_checked::<Fp>().value(w);
296        let fee_excess2_l = fee_excess2_l.to_checked::<Fp>().value(w);
297        let fee_excess2_r = fee_excess2_r.to_checked::<Fp>().value(w);
298
299        let ((fee_token1_l, fee_excess1_l), (fee_token2_l, fee_excess2_l)) =
300            eliminate_fee_excess_checked(
301                (fee_token1_l, fee_excess1_l),
302                (fee_token1_r, fee_excess1_r),
303                (fee_token2_l, fee_excess2_l),
304                w,
305            );
306
307        let ((fee_token1_l, fee_excess1_l), (fee_token2_r, fee_excess2_r)) =
308            eliminate_fee_excess_checked(
309                (&fee_token1_l, fee_excess1_l),
310                (&fee_token2_l, fee_excess2_l),
311                (fee_token2_r, fee_excess2_r),
312                w,
313            );
314
315        let (fee_token_l, fee_excess_l, fee_token_r, fee_excess_r) =
316            Self::rebalance_checked(fee_token1_l, fee_excess1_l, fee_token2_r, fee_excess2_r, w);
317
318        let convert_to_currency = |excess: Fp| {
319            let bigint: BigInteger256 = excess.into();
320            let is_neg = bigint.get_bit(255 - 1);
321            let sgn = if is_neg { Sgn::Neg } else { Sgn::Pos };
322            let magnitude = Fee::from_u64(bigint.to_64x4()[0]);
323            Signed::create(magnitude, sgn)
324        };
325
326        let fee_excess_l = w.exists(convert_to_currency(fee_excess_l));
327        fee_excess_l.to_checked::<Fp>().value(w); // Made by `Fee.Signed.Checked.to_field_var` call
328
329        let fee_excess_r = w.exists(convert_to_currency(fee_excess_r));
330        fee_excess_r.to_checked::<Fp>().value(w); // Made by `Fee.Signed.Checked.to_field_var` call
331
332        (fee_token_l, fee_excess_l, fee_token_r, fee_excess_r)
333    }
334}
335
336/// Eliminate a fee excess, either by combining it with one to the left/right,
337/// or by checking that it is zero.
338///
339/// <https://github.com/MinaProtocol/mina/blob/2ee6e004ba8c6a0541056076aab22ea162f7eb3a/src/lib/mina_base/fee_excess.ml#L200>
340fn eliminate_fee_excess<'a>(
341    (fee_token_l, fee_excess_l): (&'a TokenId, &'a Signed<Fee>),
342    (fee_token_m, fee_excess_m): (&'a TokenId, &'a Signed<Fee>),
343    (fee_token_r, fee_excess_r): (&'a TokenId, &'a Signed<Fee>),
344) -> Result<((&'a TokenId, Signed<Fee>), (&'a TokenId, Signed<Fee>)), String> {
345    let add_err = |x: &Signed<Fee>, y: &Signed<Fee>| -> Result<Signed<Fee>, String> {
346        x.add(y)
347            .ok_or_else(|| "Error adding fees: Overflow".to_string())
348    };
349
350    if fee_token_l == fee_token_m || fee_excess_l.magnitude.is_zero() {
351        let fee_excess_l = add_err(fee_excess_l, fee_excess_m)?;
352        Ok(((fee_token_m, fee_excess_l), (fee_token_r, *fee_excess_r)))
353    } else if fee_token_r == fee_token_m || fee_excess_r.magnitude.is_zero() {
354        let fee_excess_r = add_err(fee_excess_r, fee_excess_m)?;
355        Ok(((fee_token_l, *fee_excess_l), (fee_token_m, fee_excess_r)))
356    } else if fee_excess_m.magnitude.is_zero() {
357        Ok(((fee_token_l, *fee_excess_l), (fee_token_r, *fee_excess_r)))
358    } else {
359        Err(format!(
360            "Error eliminating fee excess: Excess for token {:?} \
361             {:?} was nonzero",
362            fee_token_m, fee_excess_m
363        ))
364    }
365}
366
367pub fn assert_equal_checked(_t1: &FeeExcess, t2: &FeeExcess, w: &mut Witness<Fp>) {
368    t2.fee_excess_l.to_checked::<Fp>().value(w);
369    t2.fee_excess_r.to_checked::<Fp>().value(w);
370}
371
372fn eliminate_fee_excess_checked<'a>(
373    (fee_token_l, fee_excess_l): (&'a TokenId, Fp),
374    (fee_token_m, fee_excess_m): (&'a TokenId, Fp),
375    (fee_token_r, fee_excess_r): (&'a TokenId, Fp),
376    w: &mut Witness<Fp>,
377) -> ((TokenId, Fp), (TokenId, Fp)) {
378    let mut combine = |fee_token: &TokenId, fee_excess: Fp, fee_excess_m: Fp| {
379        let fee_token_equal = field::equal(fee_token.0, fee_token_m.0, w);
380        let fee_excess_zero = field::equal(Fp::zero(), fee_excess, w);
381
382        let may_move = fee_token_equal.or(&fee_excess_zero, w);
383
384        let fee_token = w.exists_no_check(match fee_excess_zero {
385            Boolean::True => fee_token_m,
386            Boolean::False => fee_token,
387        });
388
389        let fee_excess_to_move = w.exists_no_check(match may_move {
390            Boolean::True => fee_excess_m,
391            Boolean::False => Fp::zero(),
392        });
393
394        (
395            (fee_token.clone(), fee_excess + fee_excess_to_move),
396            fee_excess_m - fee_excess_to_move,
397        )
398    };
399
400    let ((fee_token_l, fee_excess_l), fee_excess_m) =
401        combine(fee_token_l, fee_excess_l, fee_excess_m);
402    let ((fee_token_r, fee_excess_r), _fee_excess_m) =
403        combine(fee_token_r, fee_excess_r, fee_excess_m);
404
405    ((fee_token_l, fee_excess_l), (fee_token_r, fee_excess_r))
406}