-
Notifications
You must be signed in to change notification settings - Fork 10
FM-184: Proposal interpreter #185
Changes from all commits
659f21c
857f405
f08f5d7
257b921
22e3719
a36603e
e2f7d83
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -3,3 +3,4 @@ | |
mod application; | ||
|
||
pub use application::{AbciResult, Application, ApplicationService}; | ||
pub mod util; |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
// Copyright 2022-2023 Protocol Labs | ||
// SPDX-License-Identifier: Apache-2.0, MIT | ||
|
||
/// Take the first transactions until the first one that would exceed the maximum limit. | ||
/// | ||
/// The function does not skip or reorder transaction even if a later one would stay within the limit. | ||
pub fn take_until_max_size<T: AsRef<[u8]>>(txs: Vec<T>, max_tx_bytes: usize) -> Vec<T> { | ||
let mut size: usize = 0; | ||
let mut out = Vec::new(); | ||
for tx in txs { | ||
let bz: &[u8] = tx.as_ref(); | ||
if size.saturating_add(bz.len()) > max_tx_bytes { | ||
break; | ||
} | ||
size += bz.len(); | ||
out.push(tx); | ||
} | ||
out | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,6 +7,7 @@ use std::sync::{Arc, Mutex}; | |
use anyhow::{anyhow, Context, Result}; | ||
use async_trait::async_trait; | ||
use cid::Cid; | ||
use fendermint_abci::util::take_until_max_size; | ||
use fendermint_abci::{AbciResult, Application}; | ||
use fendermint_storage::{ | ||
Codec, Encode, KVCollection, KVRead, KVReadable, KVStore, KVWritable, KVWrite, | ||
|
@@ -22,7 +23,7 @@ use fendermint_vm_interpreter::fvm::state::{ | |
use fendermint_vm_interpreter::fvm::{FvmApplyRet, FvmGenesisOutput}; | ||
use fendermint_vm_interpreter::signed::InvalidSignature; | ||
use fendermint_vm_interpreter::{ | ||
CheckInterpreter, ExecInterpreter, GenesisInterpreter, QueryInterpreter, | ||
CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, | ||
}; | ||
use fvm::engine::MultiEngine; | ||
use fvm_ipld_blockstore::Blockstore; | ||
|
@@ -309,6 +310,15 @@ where | |
S::Namespace: Sync + Send, | ||
DB: KVWritable<S> + KVReadable<S> + Clone + Send + Sync + 'static, | ||
SS: Blockstore + Clone + Send + Sync + 'static, | ||
I: GenesisInterpreter< | ||
State = FvmGenesisState<SS>, | ||
Genesis = Vec<u8>, | ||
Output = FvmGenesisOutput, | ||
>, | ||
I: ProposalInterpreter< | ||
State = (), // TODO | ||
Message = Vec<u8>, | ||
>, | ||
I: ExecInterpreter< | ||
State = FvmExecState<SS>, | ||
Message = Vec<u8>, | ||
|
@@ -326,11 +336,6 @@ where | |
Query = BytesMessageQuery, | ||
Output = BytesMessageQueryRet, | ||
>, | ||
I: GenesisInterpreter< | ||
State = FvmGenesisState<SS>, | ||
Genesis = Vec<u8>, | ||
Output = FvmGenesisOutput, | ||
>, | ||
{ | ||
/// Provide information about the ABCI application. | ||
async fn info(&self, _request: request::Info) -> AbciResult<response::Info> { | ||
|
@@ -490,6 +495,45 @@ where | |
Ok(response) | ||
} | ||
|
||
/// Amend which transactions to put into the next block proposal. | ||
async fn prepare_proposal( | ||
&self, | ||
request: request::PrepareProposal, | ||
) -> AbciResult<response::PrepareProposal> { | ||
let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect(); | ||
|
||
let txs = self | ||
.interpreter | ||
.prepare((), txs) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Why do we need to pass txs as a parameter to There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We might not be modifying them now, but the interface itself doesn't need to know that, and prepare is about doing whatever you like with the list of transactions in the mempool, so passing it along seems to make sense. I have not implemented any of the following but passing the transactions could be useful for:
So you are right, at the moment all we do is add more, but not passing along would break this abstraction IMO. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Oh yeah, gas limit as well, Tendermint has no idea about it. |
||
.await | ||
.context("failed to prepare proposal")?; | ||
|
||
let txs = txs.into_iter().map(bytes::Bytes::from).collect(); | ||
let txs = 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 commentThe reason will be displayed to describe this comment to others. Learn more. Same here for the gas limit check or should we only check as part of There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. You are right that it must be part of both prepare and process otherwise you risk freaking out CometBFT by rejecting your own proposal, which is a big no-no. Again it would not be here as this is purely based on sizes and cannot access gas. This logic is here because it is mandatory, not something application specific like the gas, which must be delegated to interpreters. |
||
|
||
Ok(response::PrepareProposal { txs }) | ||
} | ||
|
||
/// Inspect a proposal and decide whether to vote on it. | ||
async fn process_proposal( | ||
&self, | ||
request: request::ProcessProposal, | ||
) -> AbciResult<response::ProcessProposal> { | ||
let txs = request.txs.into_iter().map(|tx| tx.to_vec()).collect(); | ||
|
||
let accept = self | ||
.interpreter | ||
.process((), txs) | ||
.await | ||
.context("failed to process proposal")?; | ||
|
||
if accept { | ||
Ok(response::ProcessProposal::Accept) | ||
} else { | ||
Ok(response::ProcessProposal::Reject) | ||
} | ||
} | ||
|
||
/// Signals the beginning of a new block, prior to any `DeliverTx` calls. | ||
async fn begin_block(&self, request: request::BeginBlock) -> AbciResult<response::BeginBlock> { | ||
let db = self.state_store_clone(); | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6,7 +6,9 @@ use fendermint_abci::ApplicationService; | |
use fendermint_app::{App, AppStore}; | ||
use fendermint_rocksdb::{blockstore::NamespaceBlockstore, namespaces, RocksDb, RocksDbConfig}; | ||
use fendermint_vm_interpreter::{ | ||
bytes::BytesMessageInterpreter, chain::ChainMessageInterpreter, fvm::FvmMessageInterpreter, | ||
bytes::{BytesMessageInterpreter, ProposalPrepareMode}, | ||
chain::ChainMessageInterpreter, | ||
fvm::FvmMessageInterpreter, | ||
signed::SignedMessageInterpreter, | ||
}; | ||
use tracing::info; | ||
|
@@ -27,7 +29,8 @@ async fn run(settings: Settings) -> anyhow::Result<()> { | |
); | ||
let interpreter = SignedMessageInterpreter::new(interpreter); | ||
let interpreter = ChainMessageInterpreter::new(interpreter); | ||
let interpreter = BytesMessageInterpreter::new(interpreter); | ||
let interpreter = | ||
BytesMessageInterpreter::new(interpreter, ProposalPrepareMode::AppendOnly, false); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. What is the rationale for not rejecting a malformed proposal here (if any, as maybe is just a placeholder value for now)? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I included the considerations in the code as comments where this flag is used. The reason I thought we might not want to reject them for now is because
If all our nodes are not honest and some are proposing faulty transactions then this would be a stupid way to attack instead of just not proposing any block to cause a timeout. If it happened, our only chance to weed out such actors is to admit the transaction and penalize the miner. If that happened due to a bug, we'd risk consensus failure, but at least we'd see what's happening, and by coding the penalty mechanism we'd discourage anyone from intentionally doing this, and to implement these checks in So, overall, I thought that a malformed TX here is most likely a bug that is easiest to detect by letting it through. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. That makes sense! Assuming our implementation honest saving ourselves from nasty bugs is the way to go, thanks! |
||
|
||
let ns = Namespaces::default(); | ||
let db = open_db(&settings, &ns).context("error opening DB")?; | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,7 +7,7 @@ use fendermint_vm_message::{chain::ChainMessage, signed::SignedMessage}; | |
|
||
use crate::{ | ||
signed::{SignedMessageApplyRet, SignedMessageCheckRet}, | ||
CheckInterpreter, ExecInterpreter, GenesisInterpreter, QueryInterpreter, | ||
CheckInterpreter, ExecInterpreter, GenesisInterpreter, ProposalInterpreter, QueryInterpreter, | ||
}; | ||
|
||
/// A message a user is not supposed to send. | ||
|
@@ -34,6 +34,40 @@ impl<I> ChainMessageInterpreter<I> { | |
} | ||
} | ||
|
||
#[async_trait] | ||
impl<I> ProposalInterpreter for ChainMessageInterpreter<I> | ||
where | ||
I: Sync + Send, | ||
{ | ||
// TODO: The state can include the IPLD Resolver mempool, for example by using STM | ||
// to implement a shared memory space. | ||
type State = (); | ||
type Message = ChainMessage; | ||
|
||
/// Check whether there are any "ready" messages in the IPLD resolution mempool which can be appended to the proposal. | ||
/// | ||
/// We could also use this to select the most profitable user transactions, within the gas limit. We can also take into | ||
/// account the transactions which are part of top-down or bottom-up checkpoints, to stay within gas limits. | ||
async fn prepare( | ||
&self, | ||
_state: Self::State, | ||
msgs: Vec<Self::Message>, | ||
) -> anyhow::Result<Vec<Self::Message>> { | ||
// For now this is just a placeholder. | ||
Ok(msgs) | ||
} | ||
|
||
/// Perform finality checks on top-down transactions and availability checks on bottom-up transactions. | ||
async fn process( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. maybe we can call it There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yeah I know what you mean, I based all names in all the interfaces on what they are called in ABCI but without their prefix they are a bit confusing, like Should we just use the full names, like There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Created https://github.com/consensus-shipyard/fendermint/issues/207 I think it would be good to tackle the naming separately after all the PRs are merged to reduce the rebasing churn, and to treat all of them in a uniform way. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. My 2-cents arriving late to the party: I think interpreters are a really powerful abstraction, and having them well documented can take us a long way. I agree, let's come back to naming once everything is in place so we can figure out one that is clear and general enough if this was to be ported to some other code base. |
||
&self, | ||
_state: Self::State, | ||
_msgs: Vec<Self::Message>, | ||
) -> anyhow::Result<bool> { | ||
// For now this is just a placeholder. | ||
Ok(true) | ||
} | ||
} | ||
|
||
#[async_trait] | ||
impl<I> ExecInterpreter for ChainMessageInterpreter<I> | ||
where | ||
|
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 only check here if we reach the size limit of the Tendermint block, right? Should we add also here a
TODO
to remember not to exhaust the gas limit of a block?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.
The gas aspect is definitely ignored at the moment, thanks for the reminder. I created an issue to track it: https://github.com/consensus-shipyard/fendermint/issues/208
The check would not be here because at this size we don't know what messages we are dealing with, but once they are parsed into
ChainMessage
we can look at the gas limits.