Conversation
51e74e4 to
6dd5f53
Compare
There was a problem hiding this comment.
@PhilippGackstatter if you could throw an eye on the process here to ensure I'm checking the correct things.
The state itself is checked in the mempool, so here we really just want to ensure that the batch and its transactions are valid and the reference block is correct iiuc.
There was a problem hiding this comment.
Looks good to me. I left another comment re batch expiration, but your call where/if to do that.
| &mut self, | ||
| txs: &[Arc<AuthenticatedTransaction>], | ||
| ) -> Result<BlockNumber, MempoolSubmissionError> { | ||
| assert!(!txs.is_empty(), "Cannot have a batch with no transactions"); |
There was a problem hiding this comment.
Just checking we want to crash here instead of return error
There was a problem hiding this comment.
I assumed that one cannot build a ProvenBatch without one, so this would indicate an internal bug somewhere. But maybe that's a poor assumption.
| } | ||
|
|
||
| pub fn select_batch(&mut self, budget: BatchBudget) -> Option<SelectedBatch> { | ||
| self.select_user_batch().or_else(|| self.select_conventional_batch(budget)) |
There was a problem hiding this comment.
Might want some doc comments to make it clear that budget is intended to only relevant for conventional batches.
There was a problem hiding this comment.
Also are we OK with user batches always taking priority over conventional here?
There was a problem hiding this comment.
Also wondering if we need to prevent user batches of size 1 (or some other limit). Unsure if that is relevant to this PR just a general thought.
There was a problem hiding this comment.
Also are we OK with user batches always taking priority over conventional here?
I'm unsure, but at the moment it doesn't matter much. If its a concern we can make it random -- I was thinking maybe that's best.
Also wondering if we need to prevent user batches of size 1 (or some other limit). Unsure if that is relevant to this PR just a general thought.
Good question. I'm unsure 😬 I wonder if that makes some user loop more difficult i.e. they always submit user batches, but sometimes they don't have many transactions to bundle..
Probably we would want some limit even in the future? cc @bobbinth
There was a problem hiding this comment.
Also are we OK with user batches always taking priority over conventional here?
Would having fees solve this?
There was a problem hiding this comment.
It would give us a strategy to use, so yes fees solve this imo. Though it will be potentially complex to implement.
There was a problem hiding this comment.
I think it is OK to have batches of 1 transactions as people are unlikely to to do them on purpose (they would first need to prove TXs and then prove batches - so, it is non-negligible cost). The fees would also address this as some of the tx-related fees would get amortized over large batches.
Regarding batch selection strategy - I think this is fine for now, but it'll need to change once we have fees so that we prioritize based on some value metric. But that's a whole other discussion.
And agreed that adding some doc comments would be nice.
igamigo
left a comment
There was a problem hiding this comment.
LGTM! I left some mostly minor, non-blocking comments. Not sure if you tested already but jsut in case, I'm going to run the client integration tests and report back.
| } | ||
|
|
||
| pub fn select_batch(&mut self, budget: BatchBudget) -> Option<SelectedBatch> { | ||
| self.select_user_batch().or_else(|| self.select_conventional_batch(budget)) |
There was a problem hiding this comment.
Also are we OK with user batches always taking priority over conventional here?
Would having fees solve this?
| .expect("bi-directional mapping should be coherent"); | ||
|
|
||
| for tx in txs { | ||
| let Some(tx) = self.inner.selection_candidates().get(&tx).copied() else { |
There was a problem hiding this comment.
Maybe this calls for a get_selection_candidate()? So that you avoid re-allocating the map every iteration through selection_candidates()
There was a problem hiding this comment.
I'll refactor this in a follow-up PR.
681eb87 to
a474113
Compare
| // Verify batch transaction proofs. | ||
| // | ||
| // Need to do this because ProvenBatch has no real kernel yet, so we can only | ||
| // really check that the calculated proof matches the one given in the request. | ||
| let expected_proof = LocalBatchProver::new(MIN_PROOF_SECURITY_LEVEL) | ||
| .prove(proposed_batch.clone()) | ||
| .map_err(|err| { | ||
| Status::invalid_argument(err.as_report_context("proposed block proof failed")) | ||
| })?; | ||
|
|
||
| if expected_proof != proven_batch { | ||
| return Err(Status::invalid_argument("batch proof did not match proposed batch")); | ||
| } |
There was a problem hiding this comment.
Is this the idea? I'm unsure on how else to align the proof with the batch.. unless I also compare headers and then just assume?
Can a proof differ based on some other variables e.g. time, rng? Or is this safe to do.
There was a problem hiding this comment.
That seems fine and sufficient. The "proof" (which is not a cryptographic one now anyway) should be deterministic based on the input (the proposed batch), since it's really just destructuring the proposed batch, verifying transaction proofs and constructing the proven batch.
1e09f16 to
e528296
Compare
| } | ||
|
|
There was a problem hiding this comment.
Is the batch expiration deliberately not checked? This would also be a pretty cheap check, assuming the latest block is easily retrievable here. E.g. if the batch expiration is already older than the latest block, we should be able to discard it directly.
There was a problem hiding this comment.
I think this is something we should add in a follow-up PR, potentially together with keeping a cache of most recent block headers that I mentioned in another comment.
There was a problem hiding this comment.
Looks good to me. I left another comment re batch expiration, but your call where/if to do that.
Co-authored-by: Philipp Gackstatter <PhilippGackstatter@users.noreply.github.com>
bobbinth
left a comment
There was a problem hiding this comment.
Looks good! Thank you! I left some comments inline - but I think these can be addressed in follow-up PRs. So, I'll merge this as is.
| // The inputs used for the transaction proof. | ||
| // | ||
| // Encoded using [miden_protocol::transaction::TransactionInputs::to_bytes]. | ||
| optional bytes transaction_inputs = 2; |
There was a problem hiding this comment.
I would maybe expand on the comment here to explain why we need to provide transaction_inputs.
| // The batch contents of the given proof. | ||
| // | ||
| // Encoded using [miden_protocol::batch::ProposedBatch::to_bytes]. | ||
| optional bytes proposed_batch = 2; | ||
| // The transaction inputs for each transaction in the batch. | ||
| // | ||
| // Must match the transaction ordering in the batch. | ||
| // | ||
| // Encoded using [miden_protocol::transaction::TransactionInputs::to_bytes]. | ||
| repeated bytes transaction_inputs = 3; |
There was a problem hiding this comment.
Similar comment as above: would be great to expand the comments to explain how these fields are used.
Separately, I do think eventually this should be a single field that is either a separate structured message or a single serialized struct that contains all the data that we need to re-execute the batch.
| // We only revert transactions which are _not_ included in batches. | ||
| let mut to_revert = self.inner.expired(chain_tip); | ||
| to_revert.retain(|batch| !self.inner.is_selected(batch)); |
There was a problem hiding this comment.
Is the comment here correct? Should it be "We only revert batches which are not included in blocks"?
| // Verify the reference header matches the canonical chain. | ||
| let reference_header = self | ||
| .get_block_header_by_number(Request::new(proto::rpc::BlockHeaderByNumberRequest { | ||
| block_num: expected_proof.reference_block_num().as_u32().into(), | ||
| include_mmr_proof: false.into(), | ||
| })) | ||
| .await? | ||
| .into_inner() | ||
| .block_header | ||
| .map(BlockHeader::try_from) | ||
| .transpose()? | ||
| .ok_or_else(|| { | ||
| Status::invalid_argument(format!( | ||
| "unknown reference block {}", | ||
| expected_proof.reference_block_num() | ||
| )) | ||
| })?; |
There was a problem hiding this comment.
We don't do a similar check for transactions - i.e., we don't check if the transaction's reference block is actually in the chain (this is done by the block producer). So, if we do want to keep it here, probably makes to add a similar check for transactions.
But also, going to the store from here introduces non-trivial amount of work. So, maybe we should cache some number of recent block headers (e.g,. 1 million) in the RPC so that for most transactions/batches, we don't need to go to the store.
Separately, I wonder if we should keep some kind of an LRU cache for invalid batches/transactions. The attack vector here is that someone could generate a single proven tx/batch that references some invalid block, and then submit it many times. This way, they attacker pays the cost of generating the proof only once, but the node would be forced to verify it many times (maybe thousands) which may also include going to the store etc.
Let's create issues for these.
| let expected_proof = LocalBatchProver::new(MIN_PROOF_SECURITY_LEVEL) | ||
| .prove(proposed_batch.clone()) | ||
| .map_err(|err| { | ||
| Status::invalid_argument(err.as_report_context("proposed block proof failed")) | ||
| })?; |
There was a problem hiding this comment.
Should we create an issue for this so that we come back and implement it properly once batch kernel proofs are done?
| } | ||
|
|
||
| pub fn select_batch(&mut self, budget: BatchBudget) -> Option<SelectedBatch> { | ||
| self.select_user_batch().or_else(|| self.select_conventional_batch(budget)) |
There was a problem hiding this comment.
I think it is OK to have batches of 1 transactions as people are unlikely to to do them on purpose (they would first need to prove TXs and then prove batches - so, it is non-negligible cost). The fees would also address this as some of the tx-related fees would get amortized over large batches.
Regarding batch selection strategy - I think this is fine for now, but it'll need to change once we have fees so that we prioritize based on some value metric. But that's a whole other discussion.
And agreed that adding some doc comments would be nice.
| // Rollback this batch selection since it cannot complete. | ||
| for tx in selected.txs.into_iter().rev() { | ||
| self.inner.deselect(tx.id()); | ||
| } |
There was a problem hiding this comment.
Would be good to explain why a batch may not be able to complete here (maybe we can do this once we break this code out into a separate function).
| None | ||
| } | ||
|
|
||
| fn select_conventional_batch(&mut self, mut budget: BatchBudget) -> Option<SelectedBatch> { |
There was a problem hiding this comment.
nit: maybe select_internal_batch()?
| // We only revert transactions which are _not_ included in batches. | ||
| let mut to_revert = self.inner.expired(chain_tip); | ||
| to_revert.retain(|tx| !self.inner.is_selected(tx)); |
There was a problem hiding this comment.
Is the comment still correct here? Wouldn't this revert transactions in user-defined batches (and the whole batches themselves)?
| if let Some(batch) = self.txs_user_batch.remove(&tx.id()) { | ||
| if let Some(batch) = self.user_batch_txs.remove(&batch) { | ||
| to_revert.extend(batch); | ||
| } |
There was a problem hiding this comment.
nit: using batch in both Some() expressions here is a bit confusing. I'd maybe use batch_id in the outer one, and batch_txs in the inner one.
This PR is the third and final part of the mempool refactoring PR stack. Part 1 (#1820) performs the broad mempool refactoring to simplify this PR. Builds on part 2 (#1832).
Batch submissions must include their transaction inputs since we currently require this for the validator to verify them before inclusion in a block. This PR abuses this by treating the batch as a set of normal transactions at the mempool level. This simplifies the mempool implementation, which is currently built around a DAG of transactions - so having to insert a batch directly would be more complex. This will need to change once we stop requiring transaction inputs as part of the validator; but it won't be too bad.
The way this is implemented here, is that the transaction DAG tracks user batches and ensures that when a batch is selected, that transactions from user batches are not mixed with conventional transactions. That is,
select_batchoutputs either a user batch, or a conventional batch.Effectively, the transaction DAG internally ensures that the user batch's transactions remain coherent even though the batch has been deconstructed into individual transactions. The benefit is that this doesn't require any major structural changes to the mempool. The rest of the mempool then treats the user batch as per normal.
Closes #1112 and closes #1859