tycho_executor/
config.rs

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