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