casper_types/transaction/
pricing_mode.rs

1use alloc::vec::Vec;
2use core::fmt::{self, Display, Formatter};
3
4#[cfg(feature = "datasize")]
5use datasize::DataSize;
6#[cfg(any(feature = "testing", test))]
7use rand::Rng;
8#[cfg(feature = "json-schema")]
9use schemars::JsonSchema;
10use serde::{Deserialize, Serialize};
11
12#[cfg(doc)]
13use super::Transaction;
14use super::{
15    serialization::CalltableSerializationEnvelope, InvalidTransaction, InvalidTransactionV1,
16    TransactionEntryPoint,
17};
18#[cfg(any(feature = "testing", test))]
19use crate::testing::TestRng;
20use crate::{
21    bytesrepr::{
22        Error::{self, Formatting},
23        FromBytes, ToBytes,
24    },
25    transaction::serialization::CalltableSerializationEnvelopeBuilder,
26    Digest,
27};
28#[cfg(any(feature = "std", test))]
29use crate::{Chainspec, Gas, Motes};
30
31/// The pricing mode of a [`Transaction`].
32#[derive(Clone, Ord, PartialOrd, Eq, PartialEq, Hash, Serialize, Deserialize, Debug)]
33#[cfg_attr(feature = "datasize", derive(DataSize))]
34#[cfg_attr(
35    feature = "json-schema",
36    derive(JsonSchema),
37    schemars(description = "Pricing mode of a Transaction.")
38)]
39#[serde(deny_unknown_fields)]
40pub enum PricingMode {
41    /// The original payment model, where the creator of the transaction
42    /// specifies how much they will pay, at what gas price.
43    PaymentLimited {
44        /// User-specified payment amount.
45        payment_amount: u64,
46        /// User-specified gas_price tolerance (minimum 1).
47        /// This is interpreted to mean "do not include this transaction in a block
48        /// if the current gas price is greater than this number"
49        gas_price_tolerance: u8,
50        /// Standard payment.
51        standard_payment: bool,
52    },
53    /// The cost of the transaction is determined by the cost table, per the
54    /// transaction category.
55    Fixed {
56        /// User-specified additional computation factor (minimum 0). If "0" is provided,
57        ///  no additional logic is applied to the computation limit. Each value above "0"
58        ///  tells the node that it needs to treat the transaction as if it uses more gas
59        ///  than it's serialized size indicates. Each "1" will increase the "wasm lane"
60        ///  size bucket for this transaction by 1. So if the size of the transaction
61        ///  indicates bucket "0" and "additional_computation_factor = 2", the transaction
62        ///  will be treated as a "2".
63        additional_computation_factor: u8,
64        /// User-specified gas_price tolerance (minimum 1).
65        /// This is interpreted to mean "do not include this transaction in a block
66        /// if the current gas price is greater than this number"
67        gas_price_tolerance: u8,
68    },
69    /// The payment for this transaction was previously paid, as proven by
70    /// the receipt hash (this is for future use, not currently implemented).
71    Prepaid {
72        /// Pre-paid receipt.
73        receipt: Digest,
74    },
75}
76
77impl PricingMode {
78    /// Returns a random `PricingMode.
79    #[cfg(any(feature = "testing", test))]
80    pub fn random(rng: &mut TestRng) -> Self {
81        match rng.gen_range(0..=2) {
82            0 => PricingMode::PaymentLimited {
83                payment_amount: rng.gen(),
84                gas_price_tolerance: 1,
85                standard_payment: true,
86            },
87            1 => PricingMode::Fixed {
88                gas_price_tolerance: rng.gen(),
89                additional_computation_factor: 1,
90            },
91            2 => PricingMode::Prepaid { receipt: rng.gen() },
92            _ => unreachable!(),
93        }
94    }
95
96    /// Returns standard payment flag, if it is a `PaymentLimited` variant.
97    pub fn is_standard_payment(&self) -> bool {
98        match self {
99            PricingMode::PaymentLimited {
100                standard_payment, ..
101            } => *standard_payment,
102            PricingMode::Fixed { .. } => true,
103            PricingMode::Prepaid { .. } => true,
104        }
105    }
106
107    fn serialized_field_lengths(&self) -> Vec<usize> {
108        match self {
109            PricingMode::PaymentLimited {
110                payment_amount,
111                gas_price_tolerance,
112                standard_payment,
113            } => {
114                vec![
115                    crate::bytesrepr::U8_SERIALIZED_LENGTH,
116                    payment_amount.serialized_length(),
117                    gas_price_tolerance.serialized_length(),
118                    standard_payment.serialized_length(),
119                ]
120            }
121            PricingMode::Fixed {
122                gas_price_tolerance,
123                additional_computation_factor,
124            } => {
125                vec![
126                    crate::bytesrepr::U8_SERIALIZED_LENGTH,
127                    gas_price_tolerance.serialized_length(),
128                    additional_computation_factor.serialized_length(),
129                ]
130            }
131            PricingMode::Prepaid { receipt } => {
132                vec![
133                    crate::bytesrepr::U8_SERIALIZED_LENGTH,
134                    receipt.serialized_length(),
135                ]
136            }
137        }
138    }
139
140    #[cfg(any(feature = "std", test))]
141    /// Returns the gas limit.
142    pub fn gas_limit(&self, chainspec: &Chainspec, lane_id: u8) -> Result<Gas, PricingModeError> {
143        let gas = match self {
144            PricingMode::PaymentLimited { payment_amount, .. } => Gas::new(*payment_amount),
145            PricingMode::Fixed { .. } => {
146                //The lane_id should already include additional_computation_factor in case of wasm
147                Gas::new(chainspec.get_max_gas_limit_by_category(lane_id))
148            }
149            PricingMode::Prepaid { receipt } => {
150                return Err(PricingModeError::InvalidPricingMode {
151                    price_mode: PricingMode::Prepaid { receipt: *receipt },
152                });
153            }
154        };
155        Ok(gas)
156    }
157
158    #[cfg(any(feature = "std", test))]
159    /// Returns gas cost.
160    pub fn gas_cost(
161        &self,
162        chainspec: &Chainspec,
163        lane_id: u8,
164        gas_price: u8,
165    ) -> Result<Motes, PricingModeError> {
166        let gas_limit = self.gas_limit(chainspec, lane_id)?;
167        let motes = match self {
168            PricingMode::PaymentLimited { payment_amount, .. } => {
169                Motes::from_gas(Gas::from(*payment_amount), gas_price)
170                    .ok_or(PricingModeError::UnableToCalculateGasCost)?
171            }
172            PricingMode::Fixed { .. } => Motes::from_gas(gas_limit, gas_price)
173                .ok_or(PricingModeError::UnableToCalculateGasCost)?,
174            PricingMode::Prepaid { .. } => {
175                Motes::zero() // prepaid
176            }
177        };
178        Ok(motes)
179    }
180
181    /// Returns gas cost.
182    pub fn additional_computation_factor(&self) -> u8 {
183        match self {
184            PricingMode::PaymentLimited { .. } => 0,
185            PricingMode::Fixed {
186                additional_computation_factor,
187                ..
188            } => *additional_computation_factor,
189            PricingMode::Prepaid { .. } => 0,
190        }
191    }
192}
193
194// This impl is provided due to a completeness test that we
195// have in binary-port. It checks if all variants of this
196// error have corresponding binary port error codes
197#[cfg(any(feature = "testing", test))]
198impl Default for PricingMode {
199    fn default() -> Self {
200        Self::PaymentLimited {
201            payment_amount: 1,
202            gas_price_tolerance: 1,
203            standard_payment: true,
204        }
205    }
206}
207
208///Errors that can occur when calling PricingMode functions
209#[derive(Debug)]
210pub enum PricingModeError {
211    /// The entry point for this transaction target cannot be `call`.
212    EntryPointCannotBeCall,
213    /// The entry point for this transaction target cannot be `TransactionEntryPoint::Custom`.
214    EntryPointCannotBeCustom {
215        /// The invalid entry point.
216        entry_point: TransactionEntryPoint,
217    },
218    /// Invalid combination of pricing handling and pricing mode.
219    InvalidPricingMode {
220        /// The pricing mode as specified by the transaction.
221        price_mode: PricingMode,
222    },
223    /// Unable to calculate gas cost.
224    UnableToCalculateGasCost,
225    /// Unexpected entry point.
226    UnexpectedEntryPoint {
227        entry_point: TransactionEntryPoint,
228        lane_id: u8,
229    },
230}
231
232impl From<PricingModeError> for InvalidTransaction {
233    fn from(err: PricingModeError) -> Self {
234        InvalidTransaction::V1(err.into())
235    }
236}
237
238impl From<PricingModeError> for InvalidTransactionV1 {
239    fn from(err: PricingModeError) -> Self {
240        match err {
241            PricingModeError::EntryPointCannotBeCall => {
242                InvalidTransactionV1::EntryPointCannotBeCall
243            }
244            PricingModeError::EntryPointCannotBeCustom { entry_point } => {
245                InvalidTransactionV1::EntryPointCannotBeCustom { entry_point }
246            }
247            PricingModeError::InvalidPricingMode { price_mode } => {
248                InvalidTransactionV1::InvalidPricingMode { price_mode }
249            }
250            PricingModeError::UnableToCalculateGasCost => {
251                InvalidTransactionV1::UnableToCalculateGasCost
252            }
253            PricingModeError::UnexpectedEntryPoint {
254                entry_point,
255                lane_id,
256            } => InvalidTransactionV1::UnexpectedEntryPoint {
257                entry_point,
258                lane_id,
259            },
260        }
261    }
262}
263const TAG_FIELD_INDEX: u16 = 0;
264
265const PAYMENT_LIMITED_VARIANT_TAG: u8 = 0;
266const PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX: u16 = 1;
267const PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX: u16 = 2;
268const PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX: u16 = 3;
269
270const FIXED_VARIANT_TAG: u8 = 1;
271const FIXED_GAS_PRICE_TOLERANCE_INDEX: u16 = 1;
272const FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX: u16 = 2;
273
274const RESERVED_VARIANT_TAG: u8 = 2;
275const RESERVED_RECEIPT_INDEX: u16 = 1;
276
277impl ToBytes for PricingMode {
278    fn to_bytes(&self) -> Result<Vec<u8>, Error> {
279        match self {
280            PricingMode::PaymentLimited {
281                payment_amount,
282                gas_price_tolerance,
283                standard_payment,
284            } => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
285                .add_field(TAG_FIELD_INDEX, &PAYMENT_LIMITED_VARIANT_TAG)?
286                .add_field(PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX, &payment_amount)?
287                .add_field(
288                    PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX,
289                    &gas_price_tolerance,
290                )?
291                .add_field(PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX, &standard_payment)?
292                .binary_payload_bytes(),
293            PricingMode::Fixed {
294                gas_price_tolerance,
295                additional_computation_factor,
296            } => CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
297                .add_field(TAG_FIELD_INDEX, &FIXED_VARIANT_TAG)?
298                .add_field(FIXED_GAS_PRICE_TOLERANCE_INDEX, &gas_price_tolerance)?
299                .add_field(
300                    FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX,
301                    &additional_computation_factor,
302                )?
303                .binary_payload_bytes(),
304            PricingMode::Prepaid { receipt } => {
305                CalltableSerializationEnvelopeBuilder::new(self.serialized_field_lengths())?
306                    .add_field(TAG_FIELD_INDEX, &RESERVED_VARIANT_TAG)?
307                    .add_field(RESERVED_RECEIPT_INDEX, &receipt)?
308                    .binary_payload_bytes()
309            }
310        }
311    }
312    fn serialized_length(&self) -> usize {
313        CalltableSerializationEnvelope::estimate_size(self.serialized_field_lengths())
314    }
315}
316
317impl FromBytes for PricingMode {
318    fn from_bytes(bytes: &[u8]) -> Result<(PricingMode, &[u8]), Error> {
319        let (binary_payload, remainder) = CalltableSerializationEnvelope::from_bytes(4, bytes)?;
320        let window = binary_payload.start_consuming()?.ok_or(Formatting)?;
321        window.verify_index(TAG_FIELD_INDEX)?;
322        let (tag, window) = window.deserialize_and_maybe_next::<u8>()?;
323        let to_ret = match tag {
324            PAYMENT_LIMITED_VARIANT_TAG => {
325                let window = window.ok_or(Formatting)?;
326                window.verify_index(PAYMENT_LIMITED_PAYMENT_AMOUNT_INDEX)?;
327                let (payment_amount, window) = window.deserialize_and_maybe_next::<u64>()?;
328                let window = window.ok_or(Formatting)?;
329                window.verify_index(PAYMENT_LIMITED_GAS_PRICE_TOLERANCE_INDEX)?;
330                let (gas_price_tolerance, window) = window.deserialize_and_maybe_next::<u8>()?;
331                let window = window.ok_or(Formatting)?;
332                window.verify_index(PAYMENT_LIMITED_STANDARD_PAYMENT_INDEX)?;
333                let (standard_payment, window) = window.deserialize_and_maybe_next::<bool>()?;
334                if window.is_some() {
335                    return Err(Formatting);
336                }
337                Ok(PricingMode::PaymentLimited {
338                    payment_amount,
339                    gas_price_tolerance,
340                    standard_payment,
341                })
342            }
343            FIXED_VARIANT_TAG => {
344                let window = window.ok_or(Formatting)?;
345                window.verify_index(FIXED_GAS_PRICE_TOLERANCE_INDEX)?;
346                let (gas_price_tolerance, window) = window.deserialize_and_maybe_next::<u8>()?;
347                let window = window.ok_or(Formatting)?;
348                window.verify_index(FIXED_ADDITIONAL_COMPUTATION_FACTOR_INDEX)?;
349                let (additional_computation_factor, window) =
350                    window.deserialize_and_maybe_next::<u8>()?;
351                if window.is_some() {
352                    return Err(Formatting);
353                }
354                Ok(PricingMode::Fixed {
355                    gas_price_tolerance,
356                    additional_computation_factor,
357                })
358            }
359            RESERVED_VARIANT_TAG => {
360                let window = window.ok_or(Formatting)?;
361                window.verify_index(RESERVED_RECEIPT_INDEX)?;
362                let (receipt, window) = window.deserialize_and_maybe_next::<Digest>()?;
363                if window.is_some() {
364                    return Err(Formatting);
365                }
366                Ok(PricingMode::Prepaid { receipt })
367            }
368            _ => Err(Formatting),
369        };
370        to_ret.map(|endpoint| (endpoint, remainder))
371    }
372}
373
374impl Display for PricingMode {
375    fn fmt(&self, formatter: &mut Formatter) -> fmt::Result {
376        match self {
377            PricingMode::PaymentLimited {
378                payment_amount,
379                gas_price_tolerance: gas_price,
380                standard_payment,
381            } => {
382                write!(
383                    formatter,
384                    "payment amount {}, gas price multiplier {} standard_payment {}",
385                    payment_amount, gas_price, standard_payment
386                )
387            }
388            PricingMode::Prepaid { receipt } => write!(formatter, "prepaid: {}", receipt),
389            PricingMode::Fixed {
390                gas_price_tolerance,
391                additional_computation_factor,
392            } => write!(
393                formatter,
394                "fixed pricing {} {}",
395                gas_price_tolerance, additional_computation_factor
396            ),
397        }
398    }
399}
400
401#[cfg(test)]
402mod tests {
403    use super::*;
404    use crate::bytesrepr;
405
406    #[test]
407    fn test_to_bytes_and_from_bytes() {
408        bytesrepr::test_serialization_roundtrip(&PricingMode::PaymentLimited {
409            payment_amount: 100,
410            gas_price_tolerance: 1,
411            standard_payment: true,
412        });
413        bytesrepr::test_serialization_roundtrip(&PricingMode::Fixed {
414            gas_price_tolerance: 2,
415            additional_computation_factor: 1,
416        });
417        bytesrepr::test_serialization_roundtrip(&PricingMode::Prepaid {
418            receipt: Digest::hash(b"prepaid"),
419        });
420    }
421
422    use crate::gens::pricing_mode_arb;
423    use proptest::prelude::*;
424    proptest! {
425        #[test]
426        fn generative_bytesrepr_roundtrip(val in pricing_mode_arb()) {
427            bytesrepr::test_serialization_roundtrip(&val);
428        }
429    }
430}