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
15pub 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 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, 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 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 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 return Tokens::ZERO;
207 };
208 if now <= oldest_prices.utime_since {
209 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 let mut upto = now;
223 for prices in self.storage_prices.iter().rev() {
224 if prices.utime_since > upto {
225 continue;
226 }
227
228 let delta = upto - std::cmp::max(prices.utime_since, storage_stat.last_paid);
230
231 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 upto = prices.utime_since;
240 if upto <= storage_stat.last_paid {
241 break;
242 }
243 }
244
245 Tokens::new(shift_ceil_price(total))
247 }
248
249 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 gas_max
270 } else {
271 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 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 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 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 pub fn has_authority_marks_in(&self, balance: &CurrencyCollection) -> Result<bool, Error> {
374 let cc = balance.other.as_dict();
375
376 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 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}