1use crate::utils::cumulative_percentage_change;
2use std::{
3 cmp::{
4 max,
5 min,
6 },
7 collections::BTreeMap,
8 num::NonZeroU64,
9 ops::{
10 Div,
11 RangeInclusive,
12 },
13};
14
15#[cfg(test)]
16mod tests;
17
18#[derive(Debug, thiserror::Error, PartialEq)]
19pub enum Error {
20 #[error("Skipped L2 block update: expected {expected:?}, got {got:?}")]
21 SkippedL2Block { expected: u32, got: u32 },
22 #[error("Could not calculate cost per byte: {bytes:?} bytes, {cost:?} cost")]
23 CouldNotCalculateCostPerByte { bytes: u128, cost: u128 },
24 #[error("Failed to include L2 block data: {0}")]
25 FailedToIncludeL2BlockData(String),
26 #[error("L2 block expected but not found in unrecorded blocks: {height}")]
27 L2BlockExpectedNotFound { height: u32 },
28 #[error("Could not insert unrecorded block: {0}")]
29 CouldNotInsertUnrecordedBlock(String),
30 #[error("Could not remove unrecorded block: {0}")]
31 CouldNotRemoveUnrecordedBlock(String),
32}
33
34#[derive(Debug, Clone, PartialEq)]
37pub struct AlgorithmV1 {
38 new_exec_price: u64,
40 exec_price_percentage: u64,
42 new_da_gas_price: u64,
44 da_gas_price_percentage: u64,
46 for_height: u32,
48}
49
50impl AlgorithmV1 {
51 pub fn calculate(&self) -> u64 {
52 self.new_exec_price.saturating_add(self.new_da_gas_price)
53 }
54
55 pub fn worst_case(&self, height: u32) -> u64 {
56 let exec = cumulative_percentage_change(
57 self.new_exec_price,
58 self.for_height,
59 self.exec_price_percentage,
60 height,
61 );
62 let da = cumulative_percentage_change(
63 self.new_da_gas_price,
64 self.for_height,
65 self.da_gas_price_percentage,
66 height,
67 );
68 exec.saturating_add(da)
69 }
70}
71
72pub type Height = u32;
73pub type Bytes = u64;
74
75pub trait UnrecordedBlocks {
76 fn insert(&mut self, height: Height, bytes: Bytes) -> Result<(), String>;
77
78 fn remove(&mut self, height: &Height) -> Result<Option<Bytes>, String>;
79}
80
81impl UnrecordedBlocks for BTreeMap<Height, Bytes> {
82 fn insert(&mut self, height: Height, bytes: Bytes) -> Result<(), String> {
83 self.insert(height, bytes);
84 Ok(())
85 }
86
87 fn remove(&mut self, height: &Height) -> Result<Option<Bytes>, String> {
88 let value = self.remove(height);
89 Ok(value)
90 }
91}
92
93#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
131pub struct AlgorithmUpdaterV1 {
132 pub new_scaled_exec_price: u64,
135 pub min_exec_gas_price: u64,
137 pub exec_gas_price_change_percent: u16,
141 pub l2_block_height: u32,
143 pub l2_block_fullness_threshold_percent: ClampedPercentage,
146 pub new_scaled_da_gas_price: u64,
149 pub gas_price_factor: NonZeroU64,
151 pub min_da_gas_price: u64,
153 pub max_da_gas_price: u64,
155 pub max_da_gas_price_change_percent: u16,
158 pub total_da_rewards: u128,
160 pub latest_known_total_da_cost: u128,
162 pub projected_total_da_cost: u128,
165 pub da_p_component: i64,
167 pub da_d_component: i64,
169 pub last_profit: i128,
171 pub second_to_last_profit: i128,
173 pub latest_da_cost_per_byte: u128,
175 pub l2_activity: L2ActivityTracker,
177 pub unrecorded_blocks_bytes: u128,
179}
180
181#[derive(serde::Serialize, serde::Deserialize, Debug, Clone, PartialEq)]
203pub struct L2ActivityTracker {
204 max_activity: u16,
206 capped_activity_threshold: u16,
208 decrease_activity_threshold: u16,
210 chain_activity: u16,
212 block_activity_threshold: ClampedPercentage,
215}
216
217pub enum DAGasPriceSafetyMode {
219 Normal,
221 Capped,
223 AlwaysDecrease,
225}
226
227impl L2ActivityTracker {
228 pub fn new_full(
229 normal_range_size: u16,
230 capped_range_size: u16,
231 decrease_range_size: u16,
232 block_activity_threshold: ClampedPercentage,
233 ) -> Self {
234 let decrease_activity_threshold = decrease_range_size;
235 let capped_activity_threshold =
236 decrease_range_size.saturating_add(capped_range_size);
237 let max_activity = capped_activity_threshold.saturating_add(normal_range_size);
238 let chain_activity = max_activity;
239 Self {
240 max_activity,
241 capped_activity_threshold,
242 decrease_activity_threshold,
243 chain_activity,
244 block_activity_threshold,
245 }
246 }
247
248 pub fn new(
249 normal_range_size: u16,
250 capped_range_size: u16,
251 decrease_range_size: u16,
252 activity: u16,
253 block_activity_threshold: ClampedPercentage,
254 ) -> Self {
255 let mut tracker = Self::new_full(
256 normal_range_size,
257 capped_range_size,
258 decrease_range_size,
259 block_activity_threshold,
260 );
261 tracker.chain_activity = activity.min(tracker.max_activity);
262 tracker
263 }
264
265 pub fn new_always_normal() -> Self {
266 let normal_range_size = 100;
267 let capped_range_size = 0;
268 let decrease_range_size = 0;
269 let percentage = ClampedPercentage::new(0);
270 Self::new(
271 normal_range_size,
272 capped_range_size,
273 decrease_range_size,
274 100,
275 percentage,
276 )
277 }
278
279 pub fn safety_mode(&self) -> DAGasPriceSafetyMode {
280 if self.chain_activity >= self.capped_activity_threshold {
281 DAGasPriceSafetyMode::Normal
282 } else if self.chain_activity >= self.decrease_activity_threshold {
283 DAGasPriceSafetyMode::Capped
284 } else {
285 DAGasPriceSafetyMode::AlwaysDecrease
286 }
287 }
288
289 pub fn update(&mut self, block_usage: ClampedPercentage) {
290 if block_usage < self.block_activity_threshold {
291 tracing::debug!(
292 "Decreasing activity {:?} < {:?}",
293 block_usage,
294 self.block_activity_threshold
295 );
296 self.chain_activity = self.chain_activity.saturating_sub(1);
297 } else {
298 self.chain_activity =
299 self.chain_activity.saturating_add(1).min(self.max_activity);
300 }
301 }
302
303 pub fn current_activity(&self) -> u16 {
304 self.chain_activity
305 }
306
307 pub fn max_activity(&self) -> u16 {
308 self.max_activity
309 }
310
311 pub fn capped_activity_threshold(&self) -> u16 {
312 self.capped_activity_threshold
313 }
314
315 pub fn decrease_activity_threshold(&self) -> u16 {
316 self.decrease_activity_threshold
317 }
318
319 pub fn block_activity_threshold(&self) -> ClampedPercentage {
320 self.block_activity_threshold
321 }
322}
323
324#[derive(
326 serde::Serialize, serde::Deserialize, Debug, Copy, Clone, PartialEq, PartialOrd,
327)]
328pub struct ClampedPercentage {
329 value: u8,
330}
331
332impl ClampedPercentage {
333 pub fn new(maybe_value: u8) -> Self {
334 Self {
335 value: maybe_value.min(100),
336 }
337 }
338}
339
340impl From<u8> for ClampedPercentage {
341 fn from(value: u8) -> Self {
342 Self::new(value)
343 }
344}
345
346impl core::ops::Deref for ClampedPercentage {
347 type Target = u8;
348
349 fn deref(&self) -> &Self::Target {
350 &self.value
351 }
352}
353
354impl AlgorithmUpdaterV1 {
355 pub fn update_da_record_data<U: UnrecordedBlocks>(
356 &mut self,
357 heights: RangeInclusive<u32>,
358 recorded_bytes: u32,
359 recording_cost: u128,
360 unrecorded_blocks: &mut U,
361 ) -> Result<(), Error> {
362 if !heights.is_empty() {
363 self.da_block_update(
364 heights,
365 recorded_bytes as u128,
366 recording_cost,
367 unrecorded_blocks,
368 )?;
369 self.recalculate_projected_cost();
370 self.update_da_gas_price();
371 }
372 Ok(())
373 }
374
375 pub fn update_l2_block_data<U: UnrecordedBlocks>(
376 &mut self,
377 height: u32,
378 used: u64,
379 capacity: NonZeroU64,
380 block_bytes: u64,
381 fee_wei: u128,
382 unrecorded_blocks: &mut U,
383 ) -> Result<(), Error> {
384 let expected = self.l2_block_height.saturating_add(1);
385 if height != expected {
386 Err(Error::SkippedL2Block {
387 expected,
388 got: height,
389 })
390 } else {
391 self.l2_block_height = height;
392
393 self.update_da_rewards(fee_wei);
395 let rewards = self.clamped_rewards_as_i128();
396
397 self.update_projected_da_cost(block_bytes);
399 let projected_total_da_cost = self.clamped_projected_cost_as_i128();
400
401 let last_profit = rewards.saturating_sub(projected_total_da_cost);
403 self.update_last_profit(last_profit);
404
405 self.update_activity(used, capacity);
407
408 self.update_exec_gas_price(used, capacity);
410 self.update_da_gas_price();
411
412 unrecorded_blocks
414 .insert(height, block_bytes)
415 .map_err(Error::CouldNotInsertUnrecordedBlock)?;
416 self.unrecorded_blocks_bytes = self
417 .unrecorded_blocks_bytes
418 .saturating_add(block_bytes as u128);
419 Ok(())
420 }
421 }
422
423 fn update_activity(&mut self, used: u64, capacity: NonZeroU64) {
424 let block_activity = used.saturating_mul(100).div(capacity);
425 let usage = ClampedPercentage::new(block_activity.try_into().unwrap_or(100));
426 self.l2_activity.update(usage);
427 }
428
429 fn update_da_rewards(&mut self, fee_wei: u128) {
430 let block_da_reward = self.da_portion_of_fee(fee_wei);
431 self.total_da_rewards = self.total_da_rewards.saturating_add(block_da_reward);
432 }
433
434 fn update_projected_da_cost(&mut self, block_bytes: u64) {
435 let block_projected_da_cost =
436 (block_bytes as u128).saturating_mul(self.latest_da_cost_per_byte);
437 self.projected_total_da_cost = self
438 .projected_total_da_cost
439 .saturating_add(block_projected_da_cost);
440 }
441
442 fn da_portion_of_fee(&self, fee_wei: u128) -> u128 {
444 let numerator = fee_wei.saturating_mul(self.descaled_da_price() as u128);
446 let denominator = (self.descaled_exec_price() as u128)
447 .saturating_add(self.descaled_da_price() as u128);
448 if denominator == 0 {
449 0
450 } else {
451 numerator.div_ceil(denominator)
452 }
453 }
454
455 fn clamped_projected_cost_as_i128(&self) -> i128 {
456 i128::try_from(self.projected_total_da_cost).unwrap_or(i128::MAX)
457 }
458
459 fn clamped_rewards_as_i128(&self) -> i128 {
460 i128::try_from(self.total_da_rewards).unwrap_or(i128::MAX)
461 }
462
463 fn update_last_profit(&mut self, new_profit: i128) {
464 self.second_to_last_profit = self.last_profit;
465 self.last_profit = new_profit;
466 }
467
468 fn update_exec_gas_price(&mut self, used: u64, capacity: NonZeroU64) {
469 let threshold = *self.l2_block_fullness_threshold_percent as u64;
470 let mut scaled_exec_gas_price = self.new_scaled_exec_price;
471 let fullness_percent = used
472 .saturating_mul(100)
473 .checked_div(capacity.into())
474 .unwrap_or(threshold);
475
476 match fullness_percent.cmp(&threshold) {
477 std::cmp::Ordering::Greater | std::cmp::Ordering::Equal => {
478 let change_amount = self.exec_change(scaled_exec_gas_price);
479 scaled_exec_gas_price =
480 scaled_exec_gas_price.saturating_add(change_amount);
481 }
482 std::cmp::Ordering::Less => {
483 let change_amount = self.exec_change(scaled_exec_gas_price);
484 scaled_exec_gas_price =
485 scaled_exec_gas_price.saturating_sub(change_amount);
486 }
487 }
488 self.new_scaled_exec_price =
489 max(self.min_scaled_exec_gas_price(), scaled_exec_gas_price);
490 }
491
492 fn min_scaled_exec_gas_price(&self) -> u64 {
493 self.min_exec_gas_price
494 .saturating_mul(self.gas_price_factor.into())
495 }
496
497 fn update_da_gas_price(&mut self) {
498 let p = self.p();
499 let d = self.d();
500 let maybe_scaled_da_change = self.da_change(p, d);
501 let scaled_da_change =
502 self.da_change_accounting_for_activity(maybe_scaled_da_change);
503 let maybe_new_scaled_da_gas_price = i128::from(self.new_scaled_da_gas_price)
504 .checked_add(scaled_da_change)
505 .and_then(|x| u64::try_from(x).ok())
506 .unwrap_or_else(|| {
507 if scaled_da_change.is_positive() {
508 u64::MAX
509 } else {
510 0u64
511 }
512 });
513 tracing::debug!("Profit: {}", self.last_profit);
514 tracing::debug!(
515 "DA gas price change: p: {}, d: {}, change: {}, new: {}",
516 p,
517 d,
518 scaled_da_change,
519 maybe_new_scaled_da_gas_price
520 );
521 self.new_scaled_da_gas_price = min(
522 max(
523 self.min_scaled_da_gas_price(),
524 maybe_new_scaled_da_gas_price,
525 ),
526 self.max_scaled_da_gas_price(),
527 );
528 }
529
530 fn da_change_accounting_for_activity(&self, maybe_da_change: i128) -> i128 {
531 if maybe_da_change > 0 {
532 match self.l2_activity.safety_mode() {
533 DAGasPriceSafetyMode::Normal => maybe_da_change,
534 DAGasPriceSafetyMode::Capped => 0,
535 DAGasPriceSafetyMode::AlwaysDecrease => {
536 tracing::info!("Activity is low, decreasing DA gas price");
537 self.max_change().saturating_mul(-1)
538 }
539 }
540 } else {
541 maybe_da_change
542 }
543 }
544
545 fn min_scaled_da_gas_price(&self) -> u64 {
546 self.min_da_gas_price
547 .saturating_mul(self.gas_price_factor.into())
548 }
549
550 fn max_scaled_da_gas_price(&self) -> u64 {
551 max(self.max_da_gas_price, self.min_da_gas_price)
553 .saturating_mul(self.gas_price_factor.into())
554 }
555
556 fn p(&self) -> i128 {
557 let upcast_p = i128::from(self.da_p_component);
558 let checked_p = self.last_profit.checked_div(upcast_p);
559 checked_p.unwrap_or(0).saturating_mul(-1)
561 }
562
563 fn d(&self) -> i128 {
564 let upcast_d = i128::from(self.da_d_component);
565 let slope = self.last_profit.saturating_sub(self.second_to_last_profit);
566 let checked_d = slope.checked_div(upcast_d);
567 checked_d.unwrap_or(0).saturating_mul(-1)
569 }
570
571 fn da_change(&self, p: i128, d: i128) -> i128 {
572 let scaled_pd_change = p
573 .saturating_add(d)
574 .saturating_mul(self.gas_price_factor.get() as i128);
575 let max_change = self.max_change();
576 let clamped_change = scaled_pd_change.saturating_abs().min(max_change);
577 scaled_pd_change.signum().saturating_mul(clamped_change)
578 }
579
580 fn max_change(&self) -> i128 {
582 let upcast_percent = self.max_da_gas_price_change_percent.into();
583 self.new_scaled_da_gas_price
584 .saturating_mul(upcast_percent)
585 .saturating_div(100)
586 .into()
587 }
588
589 fn exec_change(&self, principle: u64) -> u64 {
590 principle
591 .saturating_mul(self.exec_gas_price_change_percent as u64)
592 .saturating_div(100)
593 }
594
595 fn da_block_update<U: UnrecordedBlocks>(
596 &mut self,
597 heights: RangeInclusive<u32>,
598 recorded_bytes: u128,
599 recording_cost: u128,
600 unrecorded_blocks: &mut U,
601 ) -> Result<(), Error> {
602 self.update_unrecorded_block_bytes(heights, unrecorded_blocks)?;
603
604 let new_da_block_cost = self
605 .latest_known_total_da_cost
606 .saturating_add(recording_cost);
607 self.latest_known_total_da_cost = new_da_block_cost;
608
609 let compressed_cost_per_bytes = recording_cost
610 .checked_div(recorded_bytes)
611 .ok_or(Error::CouldNotCalculateCostPerByte {
612 bytes: recorded_bytes,
613 cost: recording_cost,
614 })?;
615
616 self.latest_da_cost_per_byte = compressed_cost_per_bytes;
619 Ok(())
620 }
621
622 fn update_unrecorded_block_bytes<U: UnrecordedBlocks>(
625 &mut self,
626 heights: RangeInclusive<u32>,
627 unrecorded_blocks: &mut U,
628 ) -> Result<(), Error> {
629 let mut total: u128 = 0;
630 for expected_height in heights {
631 let maybe_bytes = unrecorded_blocks
632 .remove(&expected_height)
633 .map_err(Error::CouldNotRemoveUnrecordedBlock)?;
634
635 if let Some(bytes) = maybe_bytes {
636 total = total.saturating_add(bytes as u128);
637 } else {
638 tracing::warn!(
639 "L2 block expected but not found in unrecorded blocks: {}",
640 expected_height,
641 );
642 }
643 }
644 self.unrecorded_blocks_bytes = self.unrecorded_blocks_bytes.saturating_sub(total);
645
646 Ok(())
647 }
648
649 fn recalculate_projected_cost(&mut self) {
650 let projection_portion = self
651 .unrecorded_blocks_bytes
652 .saturating_mul(self.latest_da_cost_per_byte);
653 self.projected_total_da_cost = self
654 .latest_known_total_da_cost
655 .saturating_add(projection_portion);
656 }
657
658 fn descaled_exec_price(&self) -> u64 {
659 self.new_scaled_exec_price.div(self.gas_price_factor)
660 }
661
662 fn descaled_da_price(&self) -> u64 {
663 self.new_scaled_da_gas_price.div(self.gas_price_factor)
664 }
665
666 pub fn algorithm(&self) -> AlgorithmV1 {
667 AlgorithmV1 {
668 new_exec_price: self.descaled_exec_price(),
669 exec_price_percentage: self.exec_gas_price_change_percent as u64,
670 new_da_gas_price: self.descaled_da_price(),
671 da_gas_price_percentage: self.max_da_gas_price_change_percent as u64,
672 for_height: self.l2_block_height,
673 }
674 }
675}