Skip to main content

tycho_executor/
config.rs

1use ahash::{HashMap, HashSet, HashSetExt};
2use anyhow::Result;
3use tycho_types::error::Error;
4use tycho_types::models::{
5    BlockchainConfig, BlockchainConfigParams, BurningConfig, CurrencyCollection, GasLimitsPrices,
6    GlobalVersion, MsgForwardPrices, SizeLimitsConfig, StdAddr, StorageInfo, StoragePrices,
7    WorkchainDescription,
8};
9use tycho_types::num::Tokens;
10use tycho_types::prelude::*;
11use tycho_vm::{GasParams, UnpackedConfig};
12
13use crate::util::shift_ceil_price;
14
15/// Parsed [`BlockchainConfigParams`].
16pub struct ParsedConfig {
17    pub blackhole_addr: Option<HashBytes>,
18    pub mc_gas_prices: GasLimitsPrices,
19    pub gas_prices: GasLimitsPrices,
20    pub mc_fwd_prices: MsgForwardPrices,
21    pub fwd_prices: MsgForwardPrices,
22    pub size_limits: SizeLimitsConfig,
23    pub storage_prices: Vec<StoragePrices>,
24    pub global_id: i32,
25    pub global: GlobalVersion,
26    pub workchains: HashMap<i32, WorkchainDescription>,
27    pub special_accounts: HashSet<HashBytes>,
28    pub authority_marks: Option<ParsedAuthorityMarksConfig>,
29    pub raw: BlockchainConfig,
30    pub unpacked: UnpackedConfig,
31}
32
33impl ParsedConfig {
34    pub const DEFAULT_SIZE_LIMITS_CONFIG: SizeLimitsConfig = SizeLimitsConfig {
35        max_msg_bits: 1 << 21,
36        max_msg_cells: 1 << 13,
37        max_library_cells: 1000,
38        max_vm_data_depth: 512,
39        max_ext_msg_size: 65535,
40        max_ext_msg_depth: 512,
41        max_acc_state_cells: 1 << 16,
42        max_acc_state_bits: (1 << 16) * 1023,
43        max_acc_public_libraries: 256,
44        defer_out_queue_size_limit: 256,
45    };
46
47    // TODO: Pass `global_id` here as well? For now we assume that
48    //       `params` will contain a global id entry (`ConfigParam19`).
49    // TODO: Return error if storage prices `utime_since` is not properly sorted.
50    pub fn parse(config: BlockchainConfig, now: u32) -> Result<Self, Error> {
51        thread_local! {
52            static SIZE_LIMITS: Cell =
53                CellBuilder::build_from(ParsedConfig::DEFAULT_SIZE_LIMITS_CONFIG).unwrap();
54        }
55
56        let dict = config.params.as_dict();
57
58        let burning = dict.get(5).and_then(|cell| match cell {
59            Some(cell) => cell.parse::<BurningConfig>(),
60            None => Ok(BurningConfig::default()),
61        })?;
62
63        let Some(mc_gas_prices_raw) = dict.get(20)? else {
64            return Err(Error::CellUnderflow);
65        };
66        let Some(gas_prices_raw) = dict.get(21)? else {
67            return Err(Error::CellUnderflow);
68        };
69
70        let Some(mc_fwd_prices_raw) = dict.get(24)? else {
71            return Err(Error::CellUnderflow);
72        };
73        let Some(fwd_prices_raw) = dict.get(25)? else {
74            return Err(Error::CellUnderflow);
75        };
76
77        let ParsedStoragePrices {
78            latest_storage_prices,
79            storage_prices,
80        } = parse_storage_prices(&config.params, now)?;
81
82        let workchains_dict = config.params.get_workchains()?;
83        let mut workchains = HashMap::<i32, WorkchainDescription>::default();
84        for entry in workchains_dict.iter() {
85            let (workchain, desc) = entry?;
86            workchains.insert(workchain, desc);
87        }
88
89        let global_id_raw = dict.get(19)?;
90        let global = config.params.get_global_version()?;
91
92        let size_limits_raw = dict
93            .get(43)?
94            .unwrap_or_else(|| SIZE_LIMITS.with(Cell::clone));
95
96        let mut special_accounts = HashSet::default();
97        for addr in config.params.get_fundamental_addresses()?.keys() {
98            special_accounts.insert(addr?);
99        }
100
101        let authority_marks = match config.params.get_authority_marks_config() {
102            Ok(params_raw) => {
103                let mut authority_accounts = HashSet::<HashBytes>::new();
104                for addr in params_raw.authority_addresses.keys() {
105                    authority_accounts.insert(addr?);
106                }
107                Some(ParsedAuthorityMarksConfig {
108                    authority_accounts,
109                    black_mark_id: params_raw.black_mark_id,
110                    white_mark_id: params_raw.white_mark_id,
111                })
112            }
113            _ => None,
114        };
115
116        Ok(Self {
117            blackhole_addr: burning.blackhole_addr,
118            mc_gas_prices: mc_gas_prices_raw.parse::<GasLimitsPrices>()?,
119            gas_prices: gas_prices_raw.parse::<GasLimitsPrices>()?,
120            mc_fwd_prices: mc_fwd_prices_raw.parse::<MsgForwardPrices>()?,
121            fwd_prices: fwd_prices_raw.parse::<MsgForwardPrices>()?,
122            size_limits: size_limits_raw.parse::<SizeLimitsConfig>()?,
123            storage_prices,
124            global_id: match &global_id_raw {
125                None => 0, // Return error?
126                Some(param) => param.parse::<i32>()?,
127            },
128            global,
129            workchains,
130            special_accounts,
131            authority_marks,
132            raw: config,
133            unpacked: UnpackedConfig {
134                latest_storage_prices,
135                global_id: global_id_raw,
136                mc_gas_prices: Some(mc_gas_prices_raw),
137                gas_prices: Some(gas_prices_raw),
138                mc_fwd_prices: Some(mc_fwd_prices_raw),
139                fwd_prices: Some(fwd_prices_raw),
140                size_limits_config: Some(size_limits_raw),
141            },
142        })
143    }
144
145    pub fn update_storage_prices(&mut self, now: u32) -> Result<(), Error> {
146        let ParsedStoragePrices {
147            latest_storage_prices,
148            storage_prices,
149        } = parse_storage_prices(&self.raw.params, now)?;
150
151        self.storage_prices = storage_prices;
152        self.unpacked.latest_storage_prices = latest_storage_prices;
153        Ok(())
154    }
155
156    pub fn is_blackhole(&self, addr: &StdAddr) -> bool {
157        match &self.blackhole_addr {
158            Some(blackhole_addr) => addr.is_masterchain() && addr.address == *blackhole_addr,
159            None => false,
160        }
161    }
162
163    pub fn is_special(&self, addr: &StdAddr) -> bool {
164        addr.is_masterchain()
165            && (self.special_accounts.contains(&addr.address) || addr.address == self.raw.address)
166    }
167
168    pub fn fwd_prices(&self, is_masterchain: bool) -> &MsgForwardPrices {
169        if is_masterchain {
170            &self.mc_fwd_prices
171        } else {
172            &self.fwd_prices
173        }
174    }
175
176    pub fn gas_prices(&self, is_masterchain: bool) -> &GasLimitsPrices {
177        if is_masterchain {
178            &self.mc_gas_prices
179        } else {
180            &self.gas_prices
181        }
182    }
183
184    /// Computes fees of storing `storage_stat.used` bits and refs
185    /// since `storage_stat.last_paid` and up until `now`.
186    ///
187    /// NOTE: These fees don't include `due_payment`.
188    pub fn compute_storage_fees(
189        &self,
190        storage_stat: &StorageInfo,
191        now: u32,
192        is_special: bool,
193        is_masterchain: bool,
194    ) -> Tokens {
195        // No fees in following cases:
196        // - Time has not moved forward since the last transaction;
197        // - Account was just created (last_paid: 0);
198        // - Special accounts;
199        // - No storage prices.
200        if now <= storage_stat.last_paid || storage_stat.last_paid == 0 || is_special {
201            return Tokens::ZERO;
202        }
203
204        let Some(oldest_prices) = self.storage_prices.first() else {
205            // No storage prices.
206            return Tokens::ZERO;
207        };
208        if now <= oldest_prices.utime_since {
209            // No storage prices (being active for long enought time).
210            return Tokens::ZERO;
211        }
212
213        let get_prices = if is_masterchain {
214            |prices: &StoragePrices| (prices.mc_bit_price_ps, prices.mc_cell_price_ps)
215        } else {
216            |prices: &StoragePrices| (prices.bit_price_ps, prices.cell_price_ps)
217        };
218
219        let mut total = 0u128;
220
221        // Sum fees for all segments (starting from the most recent).
222        let mut upto = now;
223        for prices in self.storage_prices.iter().rev() {
224            if prices.utime_since > upto {
225                continue;
226            }
227
228            // Compute for how long the segment was active
229            let delta = upto - std::cmp::max(prices.utime_since, storage_stat.last_paid);
230
231            // Sum fees
232            let (bit_price, cell_price) = get_prices(prices);
233            let fee = (bit_price as u128 * storage_stat.used.bits.into_inner() as u128)
234                .saturating_add(cell_price as u128 * storage_stat.used.cells.into_inner() as u128)
235                .saturating_mul(delta as u128);
236            total = total.saturating_add(fee);
237
238            // Stop on the first outdated segment.
239            upto = prices.utime_since;
240            if upto <= storage_stat.last_paid {
241                break;
242            }
243        }
244
245        // Convert from fixed point int.
246        Tokens::new(shift_ceil_price(total))
247    }
248
249    /// Computes gas credit and limits bought for the provided balances.
250    pub fn compute_gas_params(
251        &self,
252        account_balance: &Tokens,
253        msg_balance_remaining: &Tokens,
254        is_special: bool,
255        is_masterchain: bool,
256        is_tx_ordinary: bool,
257        is_in_msg_external: bool,
258    ) -> GasParams {
259        let prices = self.gas_prices(is_masterchain);
260
261        let gas_max = if is_special {
262            prices.special_gas_limit
263        } else {
264            gas_bought_for(prices, account_balance)
265        };
266
267        let gas_limit = if !is_tx_ordinary || is_special {
268            // May use all gas that can be bought using remaining balance.
269            gas_max
270        } else {
271            // Use only gas bought using remaining message balance.
272            // If the message is "accepted" by the smart contract,
273            // the gas limit will be set to `gas_max`.
274            std::cmp::min(gas_bought_for(prices, msg_balance_remaining), gas_max)
275        };
276
277        let gas_credit = if is_tx_ordinary && is_in_msg_external {
278            // External messages carry no balance,
279            // give them some credit to check whether they are accepted.
280            std::cmp::min(prices.gas_credit, gas_max)
281        } else {
282            0
283        };
284
285        GasParams {
286            max: gas_max,
287            limit: gas_limit,
288            credit: gas_credit,
289            price: prices.gas_price,
290        }
291    }
292}
293
294fn parse_storage_prices(
295    config: &BlockchainConfigParams,
296    now: u32,
297) -> Result<ParsedStoragePrices, Error> {
298    let storage_prices_dict = RawDict::<32>::from(config.as_dict().get(18)?);
299    let mut storage_prices = Vec::new();
300    let mut latest_storage_prices = None;
301    for value in storage_prices_dict.values_owned() {
302        let value = value?;
303        let prices = StoragePrices::load_from(&mut value.0.apply_allow_exotic(&value.1))?;
304        if prices.utime_since <= now {
305            latest_storage_prices = Some(value);
306        }
307
308        storage_prices.push(prices);
309    }
310
311    Ok(ParsedStoragePrices {
312        latest_storage_prices,
313        storage_prices,
314    })
315}
316
317struct ParsedStoragePrices {
318    latest_storage_prices: Option<CellSliceParts>,
319    storage_prices: Vec<StoragePrices>,
320}
321
322fn gas_bought_for(prices: &GasLimitsPrices, balance: &Tokens) -> u64 {
323    let balance = balance.into_inner();
324    if balance == 0 || balance < prices.flat_gas_price as u128 {
325        return 0;
326    }
327
328    let max_gas_threshold = if prices.gas_limit > prices.flat_gas_limit {
329        shift_ceil_price(
330            (prices.gas_price as u128) * (prices.gas_limit - prices.flat_gas_limit) as u128,
331        )
332        .saturating_add(prices.flat_gas_price as u128)
333    } else {
334        prices.flat_gas_price as u128
335    };
336
337    if balance >= max_gas_threshold || prices.gas_price == 0 {
338        return prices.gas_limit;
339    }
340
341    let mut res = ((balance - prices.flat_gas_price as u128) << 16) / (prices.gas_price as u128);
342    res = res.saturating_add(prices.flat_gas_limit as u128);
343
344    res.try_into().unwrap_or(u64::MAX)
345}
346
347#[derive(Debug, Clone)]
348pub struct ParsedAuthorityMarksConfig {
349    pub authority_accounts: HashSet<HashBytes>,
350    pub black_mark_id: u32,
351    pub white_mark_id: u32,
352}
353
354impl ParsedAuthorityMarksConfig {
355    pub fn is_authority(&self, addr: &StdAddr) -> bool {
356        addr.is_masterchain() && self.authority_accounts.contains(&addr.address)
357    }
358
359    /// Returns whether the account was suspended by authority marks.
360    pub fn is_suspended(&self, balance: &CurrencyCollection) -> Result<bool, Error> {
361        let cc = balance.other.as_dict();
362        let Some(black_marks) = cc.get(self.black_mark_id)? else {
363            // No black marks means definitely not suspended.
364            return Ok(false);
365        };
366        let white_marks = cc.get(self.white_mark_id)?.unwrap_or_default();
367
368        Ok(black_marks > white_marks)
369    }
370
371    /// Returns whether the specified balance contained non-zero amounts
372    /// of either black or white marks.
373    pub fn has_authority_marks_in(&self, balance: &CurrencyCollection) -> Result<bool, Error> {
374        let cc = balance.other.as_dict();
375
376        // TODO: Can we just check the existance of currencies here?
377        for mark_id in [self.black_mark_id, self.white_mark_id] {
378            if matches!(cc.get(mark_id)?, Some(x) if !x.is_zero()) {
379                return Ok(true);
380            }
381        }
382        Ok(false)
383    }
384
385    /// Removes authority mark currencies from the specified balance.
386    /// Returns whether the balance has changed.
387    pub fn remove_authority_marks_in(
388        &self,
389        balance: &mut CurrencyCollection,
390    ) -> Result<bool, Error> {
391        let mut changed = false;
392
393        let cc = balance.other.as_dict_mut();
394        for mark_id in [self.black_mark_id, self.white_mark_id] {
395            cc.remove_raw(mark_id)?;
396            changed = true;
397        }
398
399        Ok(changed)
400    }
401}