-
Notifications
You must be signed in to change notification settings - Fork 47
feat(node): txn prioritization based on gas parameters #1185
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from 3 commits
a63006e
6176a67
17796a0
76b4e8c
ac4ecd2
3146752
63c0cb5
d8711e2
f9ee238
6784475
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -397,6 +397,40 @@ where | |
| _ => Err(anyhow!("invalid app state json")), | ||
| } | ||
| } | ||
|
|
||
| fn create_check_state(&self) -> anyhow::Result<FvmExecState<ReadOnlyBlockstore<SS>>> { | ||
| let db = self.state_store_clone(); | ||
| let state = self.committed_state()?; | ||
|
|
||
| // This would create a partial state, but some client scenarios need the full one. | ||
| // FvmCheckState::new(db, state.state_root(), state.chain_id()) | ||
| // .context("error creating check state")? | ||
|
|
||
| let mut state = FvmExecState::new( | ||
| ReadOnlyBlockstore::new(db), | ||
| self.multi_engine.as_ref(), | ||
| state.block_height.try_into()?, | ||
| state.state_params, | ||
| ) | ||
| .context("error creating check state")?; | ||
|
|
||
| // load txn priority calculator | ||
| let end = state.block_height() as u64; | ||
| let start = end | ||
| .saturating_sub(state.txn_priority_calculator().len() as u64) | ||
| .max(1); | ||
| for h in start..=end { | ||
| let Ok((state_params, _)) = self.state_params_at_height(FvmQueryHeight::Height(h)) | ||
| else { | ||
| continue; | ||
| }; | ||
| state | ||
| .txn_priority_calculator_mut() | ||
| .base_fee_updated(state_params.base_fee); | ||
| } | ||
|
|
||
| Ok(state) | ||
| } | ||
| } | ||
|
|
||
| // NOTE: The `Application` interface doesn't allow failures at the moment. The protobuf | ||
|
|
@@ -561,22 +595,7 @@ where | |
|
|
||
| let state = match guard.take() { | ||
| Some(state) => state, | ||
| None => { | ||
| let db = self.state_store_clone(); | ||
| let state = self.committed_state()?; | ||
|
|
||
| // This would create a partial state, but some client scenarios need the full one. | ||
| // FvmCheckState::new(db, state.state_root(), state.chain_id()) | ||
| // .context("error creating check state")? | ||
|
|
||
| FvmExecState::new( | ||
| ReadOnlyBlockstore::new(db), | ||
| self.multi_engine.as_ref(), | ||
| state.block_height.try_into()?, | ||
| state.state_params, | ||
| ) | ||
| .context("error creating check state")? | ||
| } | ||
| None => self.create_check_state()?, | ||
| }; | ||
|
|
||
| let (state, result) = self | ||
|
|
@@ -589,9 +608,6 @@ where | |
| .await | ||
| .context("error running check")?; | ||
|
|
||
| // Update the check state. | ||
| *guard = Some(state); | ||
|
|
||
| let mut mpool_received_trace = MpoolReceived::default(); | ||
|
|
||
| let response = match result { | ||
|
|
@@ -601,11 +617,18 @@ where | |
| Ok(Err(InvalidSignature(d))) => invalid_check_tx(AppError::InvalidSignature, d), | ||
| Ok(Ok(ret)) => { | ||
| mpool_received_trace.message = Some(Message::from(&ret.message)); | ||
| to_check_tx(ret) | ||
|
|
||
| let priority = state.txn_priority_calculator().priority(&ret.message); | ||
| let mut t = to_check_tx(ret); | ||
| t.priority = priority; | ||
| t | ||
|
||
| } | ||
| }, | ||
| }; | ||
|
|
||
| // Update the check state. | ||
| *guard = Some(state); | ||
|
|
||
| mpool_received_trace.accept = response.code.is_ok(); | ||
| if !mpool_received_trace.accept { | ||
| mpool_received_trace.reason = Some(format!("{:?} - {}", response.code, response.info)); | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -8,6 +8,7 @@ pub mod snapshot; | |
| mod check; | ||
| mod exec; | ||
| mod genesis; | ||
| mod priority; | ||
| mod query; | ||
|
|
||
| use std::sync::Arc; | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,211 @@ | ||
| // Copyright 2022-2024 Protocol Labs | ||
| // SPDX-License-Identifier: Apache-2.0, MIT | ||
|
|
||
| use crate::fvm::FvmMessage; | ||
| use fvm_shared::bigint::BigInt; | ||
| use fvm_shared::econ::TokenAmount; | ||
| use lazy_static::lazy_static; | ||
| use num_traits::{ToPrimitive, Zero}; | ||
| use std::str::FromStr; | ||
|
|
||
| lazy_static! { | ||
| // Max U256 | ||
| static ref MAX_GAS: TokenAmount = TokenAmount::from_atto(BigInt::from_str("115792089237316195423570985008687907853269984665640564039457584007913129639935").unwrap()); | ||
| } | ||
|
|
||
| /// The transaction priority calculator. The priority calculated is used to determine the ordering | ||
| /// in the mempool. | ||
| #[derive(Clone, Debug)] | ||
| pub struct TxnPriorityCalculator { | ||
| /// Ring buffer of base fee history | ||
| base_fee_history: Vec<Option<TokenAmount>>, | ||
|
||
| /// Next slot in the ring buffer | ||
| next_slot: usize, | ||
| } | ||
|
|
||
| impl TxnPriorityCalculator { | ||
| pub fn new(size: usize) -> Self { | ||
| let mut v = Vec::with_capacity(size); | ||
| for _ in 0..size { | ||
| v.push(None); | ||
| } | ||
| Self { | ||
| base_fee_history: v, | ||
| next_slot: 0, | ||
| } | ||
| } | ||
|
|
||
| pub fn len(&self) -> usize { | ||
| self.base_fee_history.len() | ||
| } | ||
|
|
||
| pub fn base_fee_updated(&mut self, base_fee: TokenAmount) { | ||
| self.base_fee_history[self.next_slot] = Some(base_fee); | ||
| self.next_slot = (self.next_slot + 1) % self.base_fee_history.len(); | ||
| } | ||
|
|
||
| pub fn priority(&self, msg: &FvmMessage) -> i64 { | ||
| let base_fee = self.lowest_base_fee(); | ||
|
|
||
| if msg.gas_fee_cap >= base_fee { | ||
| let i = msg.gas_fee_cap.clone() - base_fee + msg.gas_premium.clone(); | ||
| i.atto() | ||
| .min(&BigInt::from(i64::MAX)) | ||
| .to_i64() | ||
| .expect("clipped to i64 max") | ||
| } else { | ||
| 0 | ||
| } | ||
|
||
| } | ||
|
|
||
| fn lowest_base_fee(&self) -> TokenAmount { | ||
| let mut out: Option<TokenAmount> = None; | ||
| for v in &self.base_fee_history { | ||
| let Some(v) = v.as_ref() else { continue }; | ||
|
|
||
| match out { | ||
| Some(min) => out = Some(min.min(v.clone())), | ||
| None => out = Some(v.clone()), | ||
| } | ||
| } | ||
|
|
||
| out.unwrap_or(TokenAmount::zero()) | ||
| } | ||
| } | ||
|
|
||
| #[cfg(test)] | ||
| mod tests { | ||
| use crate::fvm::state::priority::TxnPriorityCalculator; | ||
| use crate::fvm::FvmMessage; | ||
| use fvm_shared::address::Address; | ||
| use fvm_shared::bigint::BigInt; | ||
| use fvm_shared::econ::TokenAmount; | ||
|
|
||
| fn create_msg(fee_cap: TokenAmount, premium: TokenAmount) -> FvmMessage { | ||
| FvmMessage { | ||
| version: 0, | ||
| from: Address::new_id(10), | ||
| to: Address::new_id(12), | ||
| sequence: 0, | ||
| value: Default::default(), | ||
| method_num: 0, | ||
| params: Default::default(), | ||
| gas_limit: 0, | ||
| gas_fee_cap: fee_cap, | ||
| gas_premium: premium, | ||
| } | ||
| } | ||
|
|
||
| #[test] | ||
| fn base_fee_update_works() { | ||
| let mut cal = TxnPriorityCalculator::new(3); | ||
|
|
||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(0)); | ||
|
|
||
| // [10, None, None] | ||
| cal.base_fee_updated(TokenAmount::from_atto(10)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(10)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![Some(TokenAmount::from_atto(10)), None, None] | ||
| ); | ||
|
|
||
| // [10, 20, None] | ||
| cal.base_fee_updated(TokenAmount::from_atto(20)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(10)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(10)), | ||
| Some(TokenAmount::from_atto(20)), | ||
| None | ||
| ] | ||
| ); | ||
|
|
||
| // [10, 20, 5] | ||
| cal.base_fee_updated(TokenAmount::from_atto(5)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(5)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(10)), | ||
| Some(TokenAmount::from_atto(20)), | ||
| Some(TokenAmount::from_atto(5)), | ||
| ] | ||
| ); | ||
|
|
||
| // [6, 20, 5] | ||
| cal.base_fee_updated(TokenAmount::from_atto(6)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(5)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(6)), | ||
| Some(TokenAmount::from_atto(20)), | ||
| Some(TokenAmount::from_atto(5)), | ||
| ] | ||
| ); | ||
|
|
||
| // [6, 100, 5] | ||
| cal.base_fee_updated(TokenAmount::from_atto(100)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(5)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(6)), | ||
| Some(TokenAmount::from_atto(100)), | ||
| Some(TokenAmount::from_atto(5)), | ||
| ] | ||
| ); | ||
|
|
||
| // [6, 100, 10] | ||
| cal.base_fee_updated(TokenAmount::from_atto(10)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(6)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(6)), | ||
| Some(TokenAmount::from_atto(100)), | ||
| Some(TokenAmount::from_atto(10)), | ||
| ] | ||
| ); | ||
|
|
||
| // [10, 100, 10] | ||
| cal.base_fee_updated(TokenAmount::from_atto(10)); | ||
| assert_eq!(cal.lowest_base_fee(), TokenAmount::from_atto(10)); | ||
| assert_eq!( | ||
| cal.base_fee_history, | ||
| vec![ | ||
| Some(TokenAmount::from_atto(10)), | ||
| Some(TokenAmount::from_atto(100)), | ||
| Some(TokenAmount::from_atto(10)), | ||
| ] | ||
| ); | ||
| } | ||
|
|
||
| #[test] | ||
| fn priority_calculation() { | ||
| let mut cal = TxnPriorityCalculator::new(3); | ||
|
|
||
| cal.base_fee_updated(TokenAmount::from_atto(10)); | ||
| cal.base_fee_updated(TokenAmount::from_atto(20)); | ||
| cal.base_fee_updated(TokenAmount::from_atto(30)); | ||
|
|
||
| // lowest base fee is 10 | ||
|
|
||
| let msg = create_msg(TokenAmount::from_atto(1), TokenAmount::from_atto(20)); | ||
| assert_eq!(cal.priority(&msg), 0); | ||
|
|
||
| let msg = create_msg(TokenAmount::from_atto(10), TokenAmount::from_atto(20)); | ||
| assert_eq!(cal.priority(&msg), 20); | ||
|
|
||
raulk marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| let msg = create_msg(TokenAmount::from_atto(15), TokenAmount::from_atto(20)); | ||
| assert_eq!(cal.priority(&msg), 25); | ||
|
|
||
| let msg = create_msg( | ||
| TokenAmount::from_atto(BigInt::from(i64::MAX)), | ||
| TokenAmount::from_atto(BigInt::from(i64::MAX)), | ||
| ); | ||
| assert_eq!(cal.priority(&msg), i64::MAX); | ||
| } | ||
| } | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
We can avoid loading the 5 blocks state params for
check_tx, to do this, we must change thecommitto hold some historicalcheck_stateinstead of purging everything. Not sure if we should do in this PR.