-
Notifications
You must be signed in to change notification settings - Fork 34
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
feat(node): prepare + process + end block abci gas handling #1122
base: gas-market-genesis
Are you sure you want to change the base?
Conversation
let mut msgs_with_gas_limit = signed_msgs_with_gas_limit(msgs)?; | ||
|
||
// sort by gas limit descending | ||
msgs_with_gas_limit.sort_by(|a, b| b.1.cmp(&a.1)); |
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.
probably we dont need this or handle if block gas limit is hit.
fendermint/app/src/app.rs
Outdated
let (state_params, block_height) = | ||
self.state_params_at_height(request.height.value().into())?; | ||
let state = FvmExecState::new( | ||
self.state_store_clone(), | ||
self.multi_engine.as_ref(), | ||
block_height as ChainEpoch, | ||
state_params, | ||
) | ||
.context("error creating new state")?; | ||
|
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.
Is it not possible to reuse new_read_only_exec_state()
?
Don't we have the guarantee that this runs always against the latest committed state, therefore we could simply use that state, instead of loading it from the height that's passed in?
// TODO: This seems leaky placed here, should be done in `interpreter`, that's where it's ipc | ||
// TODO: aware, here might have filtered more important messages. | ||
let (txs, size) = take_until_max_size(txs, request.max_tx_bytes.try_into().unwrap()); |
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.
Could you elaborate on this? Is the claim here that if we're hitting the limit, we might end up pruning IPC system messages? (e.g. checkpoints, top-down finality)? Would you rather pass the block byte limit to the interpreter, so it can prioritise IPC system messages? Generally speaking, that makes sense to me. The added benefit of that refactor is that we'd be making message packing consensus-engine-agnostic.
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.
For example, if you checkout
ipc/fendermint/vm/interpreter/src/chain.rs
Line 166 in f4c6748
msgs.push(ChainMessage::Ipc(IpcMessage::TopDownExec(ParentFinality { |
ipc
critical messages are appended to the end of the list. If there are quite some messages, the ipc
messages might be filtered out as messages are selected from the start.
Since we are introduce some form of message selection, I was looking at some way to put all the selection logic in one place instead of split into different parts of the codebase. I think passing the block limit into the interpreter works for me, at least we have message selection in one place, easier to debug, onboard and maintain.
fendermint/app/src/app.rs
Outdated
let (state_params, block_height) = | ||
self.state_params_at_height(request.height.value().into())?; | ||
let state = FvmExecState::new( | ||
self.state_store_clone(), | ||
self.multi_engine.as_ref(), | ||
block_height as ChainEpoch, | ||
state_params, | ||
) | ||
.context("error creating new state")?; | ||
|
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.
Same as above. If this runs on the committed state, we can just use the helper?
let gas_limit = inner.message.gas_limit; | ||
Ok((ChainMessage::Signed(inner), gas_limit)) | ||
} | ||
ChainMessage::Ipc(_) => Err(anyhow!("should not have ipc messages in user proposals")), |
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.
Is this error elevated to a panic?
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.
This depends on how caller handle error. From the implementation, it will cause panic. ipc
message should never be proposed, if proposed then it means the validator is doing suspicious stuff. We can give a warn instead and continue processing. Not sure if anyone will pick up in logs/monitoring.
/// Performs message selection: | ||
/// - Order by gas limit in descending order | ||
/// - Make sure total gas limit does not exceed the `total_gas_limit` parameter | ||
fn messages_selection( |
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.
From what I gather, CometBFT should always be staying under the block gas limit when selecting messages. We are informing gas_wanted
in CheckTx, and now CometBFT will know about the block_gas_limit
. Is this intended to be an extra filter just in case CometBFT messed up?
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.
Note that we can also set a priority in CheckTx, so we could prioritise checkpoint operations, deposits, withdrawals, staking, unstaking, etc. simply by informing CometBFT of a higher priority.
Generally I think we're duplicating work here, since we can get CometBFT to handle much of this for us (even though that makes us less consensus-agnostic for the time being).
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.
Yeah, I also have this impression that cometbft will handle it, but I could not fully deduce that in https://docs.cometbft.com/v0.37/spec/abci/abci++_app_requirements#gas. So I added it anyways. I can remove it for now.
However I doubt CheckTx
with priority from cometbft will work. The checkpoint operations and etc are derived completely from fendermint
, they are not coming from cometbft. We have to manually put them at the head of the msgs
.
type Constant = SetConstants; | ||
|
||
fn get_constants(&self) -> anyhow::Result<Self::Constant> { | ||
todo!() | ||
} | ||
|
||
fn set_constants(&mut self, constants: Self::Constant) { | ||
self.constant_update = Some(constants); | ||
} | ||
|
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.
Are GetConstants
and SetConstants
ever used by the App? These are policy-specific methods for the user to invoke off band.
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.
Yep, they are not used by the App
and called by system upgrade, maybe.
I added these two for "reference" implementation I guess. The SetConstants
is also related to how state
could detect potential gas market changes. I'm thinking of holding the changes in memory first, as reflected in the option field, then in end_block
performs the commit in interpreter. Then send a summary of the changed to App
which then can be propagated to cometbft. If we dont control SetConstants
, we might lost the change or have to query the gas market over and over to detect changes.
fn commit_constants<E: Executor>( | ||
&self, | ||
executor: &mut E, | ||
block_height: ChainEpoch, | ||
) -> anyhow::Result<()> { | ||
let Some(ref constants) = self.constant_update else { | ||
return Ok(()); | ||
}; | ||
|
||
let msg = FvmMessage { | ||
from: system::SYSTEM_ACTOR_ADDR, | ||
to: GAS_MARKET_ACTOR_ADDR, | ||
sequence: block_height as u64, | ||
// exclude this from gas restriction | ||
gas_limit: i64::MAX as u64, | ||
method_num: fendermint_actor_gas_market::Method::SetConstants as u64, | ||
params: fvm_ipld_encoding::RawBytes::serialize(constants)?, | ||
value: Default::default(), | ||
version: Default::default(), | ||
gas_fee_cap: Default::default(), | ||
gas_premium: Default::default(), | ||
}; | ||
self.call_fvm(msg, executor)?; | ||
|
||
Ok(()) | ||
} |
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 don't need to handle constant changes at all, these happen entirely as standard chain messages, right? Or that's what I thought?
The only thing we need to be careful with is not changing constants in the middle of block execution. So I think we'll need a message selection / prepare proposal / process proposal rule to enforce messages directed to the gas market actor to always be at the top of the block.
Does that make sense?
So given a data structure:
struct MessageOrderRule {
from: Option<Address>,
to: Option<Address>,
precedence: i32,
}
We could have a lazy static:
lazy_static! {
pub static ref MESSAGE_ORDER_POLICY: Vec<MessageOrderRule> = vec![
MessageOrderRule{from: None, to: Some(Address::from_id(GAS_MARKET_ID), precedence: 1000},
}
And then we'd reoder messages during selection to satisfy the policy?
Actually, we could have this policy be a trait with methods to sort (prepare proposal) and validate order (process proposal).
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.
I think a trait is better, more general and flexible. Just holding a vec of MessageOrderRule
traits implementations.
@@ -96,13 +152,18 @@ impl ActorGasMarket { | |||
gas_premium: Default::default(), | |||
}; | |||
|
|||
self.call_fvm(msg, executor)?; |
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.
self.call_fvm(msg, executor)?; | |
self.apply_implicit_message(msg, executor)?; |
if let Some(err) = apply_ret.failure_info { | ||
anyhow::bail!("failed to update EIP1559 gas state: {}", err) | ||
} else { | ||
Ok(()) | ||
Ok(apply_ret) | ||
} | ||
} |
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.
Can be simplified (GitHub won't let me make a suggestion):
apply_ret.failure_info
.map_or(Ok(apply_ret), |err| anyhow::bail!("failed to update EIP1559 gas state: {}", err))
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.
I tried, seems the above does not compile due to partial move because map_or
takes self which consumes apply_ret.failure_info
and Ok(apply_ret) is partially moved. I tried the same with as_ref
, but have the same issue. It works if I do apply_ret.failure_info.take
, but perhaps not desired as it changes the failure info.
/// The constant parameters that determines the readings of gas market, such as block gas limit. | ||
type Constant; | ||
|
||
#[allow(dead_code)] | ||
fn get_constants(&self) -> anyhow::Result<Self::Constant>; | ||
|
||
/// Update the constants of the gas market. If the gas market is actor based, then it's recommended | ||
/// to flush at EndBlock. | ||
#[allow(dead_code)] | ||
fn set_constants(&mut self, constants: Self::Constant); |
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 don't need these.
This PR add gas handling to
prepare
,process
andend_block
abci calls. Some key changes:prepare
: Prepare the proposal with gas limit check. From the doc, cometbft will be able to ensure the block_gas_limit is enforce (that's what I inferred from the doc, somehow not 100% clearly), but still added as a sanity check.process
: Same asprepare
, checks the block gas limit.SetConstants
into the gas market trait. Currently they are not used, addedallow(dead_code)
for future usage. The idea isSetConstants
will buffer the update and in end block will commit the changes. Before eachend_block
finishes, it will check if there areSetConstants
called and if there is, it will fetch latest consensus params from cometbft, update accordingly and push to cometbft.Update:
App
so that interpreters are more tendermint agnostic.ValidatorTracker
andClient
toApp
so that communication with cometbft can be made to query consensus parameters.tendermint::account::Id
fromFvmExecState
withPublicKey
. Theid
is the first 20 bytes of public key hashed, which is not useful for deriving fvm addresses. Converting toPublicKey
will make the downstream process much more helpful.MessageSelector
trait to handle more generic message selection.