-
Notifications
You must be signed in to change notification settings - Fork 2.8k
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
Use bytes from unrecorded_blocks
rather from the block from DA
#2252
Conversation
let expected = self.da_recorded_block_height.saturating_add(1); | ||
if height != expected { | ||
Err(Error::SkippedDABlock { | ||
expected: self.da_recorded_block_height.saturating_add(1), | ||
got: height, | ||
}) | ||
} else { | ||
let block_bytes = self |
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.
Maybe we want to remove all blocks until height
?
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'm wondering, is it possible in a sunny-day scenario that we'll have some "lost", unrecorded blocks that will never be consumed by da_block_update()
? I think yes, because update_da_record_data()
gets an arbitrary set of blocks and we don't guarantee what are the heights of those blocks.
Maybe SkippedL2Block
and SkippedDABlock
errors protects us from this.
Anyway, leaving this comment for consideration as we might think about protecting unrecorded_blocks
from growing indefinitely in case of some unexpected flow.
Edit:
I think that at some point we might need to take care about the size of unrecorded_blocks
. It may happen that the user of the algorithm will populate the set by calling update_l2_block_data()
, but will never call into update_da_record_data()
to clear it. Maybe this is enforced on a higher level.
cc: @MitchTurner
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.
Maybe we want to remove all blocks until height?
That might be more performant (and kinda what we were doing before). We'd want to do that before we called da_block_update
and pair each block_bytes
with the corresponding RecordedBlock
.
I honestly don't know the performance of remove
for HashMap
. get
is O(1), but obviously doesn't mut
so remove
probably does re-scaling and other heap garbage. The most performant would be a VecDeque
and split_off
probably? The order is now an issue then. I was going to say we don't need to include the height
(just the bytes
) if we trust the order, but just in case we probably should and throw an error if it doesn't match the recorded_block
it is paired with.
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.
Anyway, leaving this comment for consideration as we might think about protecting unrecorded_blocks from growing indefinitely in case of some unexpected flow.
Yeah. It's directly relevant to what we're talking about. We definitely make assumption about the order.
Talking to the rollup team guys, it sounds like they might not want to guarantee order in the long run, if that's the case then the HashMap
approach might be the best. We could even get rid of SkippedL2Block
I think... except we've lost info on the best cost_for_byte
for L2 blocks that aren't in order.
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 meant: Block committer submits blocks in the bundles. So bundle can have several blocks inside. When you receive the notification from the block commuter about DA submission, you can have unrecorded_blocks = vec![block_height-5, block_height-4, ... block_height]
. Then instead of removing only one entry, you need to remove all entries up to block_height
.
If that is something that we want, then BTreeMap
is better=)
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 are currently removing all in the bundle. We are just iterating over the recorded blocks and removing one at a time:
pub fn update_da_record_data(
&mut self,
blocks: &[RecordedBlock],
) -> Result<(), Error> {
for block in blocks {
self.da_block_update(block.height, block.block_cost)?;
}
self.recalculate_projected_cost();
self.normalize_rewards_and_costs();
Ok(())
}
What I was saying is we could get them all first in some efficient way:
pub fn update_da_record_data(
&mut self,
blocks: &[RecordedBlock],
) -> Result<(), Error> {
let bytes = self.remove_bytes_for_recorded_blocks(&blocks)?;
for (block, bytes) in blocks.iter().zip(bytes) {
self.da_block_update(block.height, block.block_cost, bytes)?;
}
self.recalculate_projected_cost();
self.normalize_rewards_and_costs();
Ok(())
}
Yeah maybe BTreeMap
makes the most sense for the ordered case.
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.
RecordedBlock
data is linked to each other. It just has the latest block height
cc @rymnc Since he wrote down the interface that we agreed with Rollup team. Maybe I remember it incorrectly)
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.
Yes. I was part of those conversations and we've discussed what is required for the algo.
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 is what I have -
struct Bundle {
sequence_number: u64,
blocks_range: Range<BlockHeight>,
// The DA block height of the last transaciton in the bundle.
da_block_height: DaBlockHeight,
// Total cost of all bundles for the whole history.
total_cost: u256,
// Total size of all bundles for the whole history.
total_size: u256,
}
trait CommitterAPI {
fn get_n_last_bundle(&self, number: u64) -> Result<Bundle>;
// Range is based on `sequence_number`
fn get_bundles_by_range(&self, range: Range<u64>) -> Result<Vec<Bundle>>;
}
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.
Right. Okay. So in that case we can just sum all the block bytes for that range and do a single cost calculation. Sounds like maybe that's what you were suggesting, @xgreenx . I was still under the impression that more processing would happen on the service side, but this actually works fine.
let expected = self.da_recorded_block_height.saturating_add(1); | ||
if height != expected { | ||
Err(Error::SkippedDABlock { | ||
expected: self.da_recorded_block_height.saturating_add(1), | ||
got: height, | ||
}) | ||
} else { | ||
let block_bytes = self |
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'm wondering, is it possible in a sunny-day scenario that we'll have some "lost", unrecorded blocks that will never be consumed by da_block_update()
? I think yes, because update_da_record_data()
gets an arbitrary set of blocks and we don't guarantee what are the heights of those blocks.
Maybe SkippedL2Block
and SkippedDABlock
errors protects us from this.
Anyway, leaving this comment for consideration as we might think about protecting unrecorded_blocks
from growing indefinitely in case of some unexpected flow.
Edit:
I think that at some point we might need to take care about the size of unrecorded_blocks
. It may happen that the user of the algorithm will populate the set by calling update_l2_block_data()
, but will never call into update_da_record_data()
to clear it. Maybe this is enforced on a higher level.
cc: @MitchTurner
self.latest_known_total_da_cost_excess = new_block_cost; | ||
self.latest_da_cost_per_byte = new_cost_per_byte; | ||
Ok(()) | ||
} | ||
} | ||
|
||
fn drain_l2_block_bytes_for_range( |
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.
So, we can consider improving performance here still.
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.
Switched to BTreeMap
and pop_first
.
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.
LGTM 👍
Left two small nits.
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.
LGTM 👍
One possible issue for consideration: #2252 (comment)
This could be a non-issue or a real deal that we might consider handling as an follow-up issue.
if !height_range.is_empty() { | ||
self.da_block_update(height_range, range_cost)?; | ||
self.recalculate_projected_cost(); | ||
// self.normalize_rewards_and_costs(); |
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 commented-out on purpose?
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.
Yes. I added the TODO with the related issue.
#[derive(Debug, Clone)] | ||
pub struct RecordedBlock { | ||
pub height: u32, | ||
// pub block_bytes: u64, |
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.
Do we want to keep this?
@rafal-ch It's possible that we get into a situation that this is never shrunk. I'm not sure what the correct way to handle it is. I don't think it should be the worry of the gas price algorithm though. If the committer isn't responding for days or weeks, then the issue is going to need to be addressed anyway. We can always modify the algorithm if we recognize a problem in production. |
It could be as simple as having a hardcoded or configured upper limit on unrecorded blocks and reject to accept more.
True. But yeah, since it's unlikely and of rather low impact we can move forward as is and observe how it behaves in production. |
@@ -104,8 +104,7 @@ impl Simulator { | |||
capacity: u64, | |||
max_block_bytes: u64, | |||
fullness_and_bytes: Vec<(u64, u64)>, | |||
// blocks: Enumerate<Zip<Iter<(u64, u64)>, Iter<Option<Vec<RecordedBlock>>>>>, | |||
blocks: impl Iterator<Item = (usize, ((u64, u64), &'a Option<Vec<RecordedBlock>>))>, | |||
blocks: impl Iterator<Item = (usize, ((u64, u64), &'a Option<(Range<u32>, u128)>))>, |
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.
nit: this type seems complicated enough that a named type should make it more readable
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.
Done.
if let Some((range, cost)) = da_block { | ||
for height in range.to_owned() { | ||
updater | ||
.update_da_record_data(height..(height + 1), *cost) |
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.
.update_da_record_data(height..(height + 1), *cost) | |
.update_da_record_data(height..=height, *cost) |
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.
Unfortunately, RangeInclusive
isn't the same type as Range
, I went back and forth on whether to do a generic for the few traits I needed or just do Range
and I figured this was less noise.
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.
Willing to reconsider if generics seems better.
@@ -0,0 +1,354 @@ | |||
use crate::{ |
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.
Do we need changes from this file? Looks like @rymnc already migrated them and V1 will define own updater/service?
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.
Oh. Thanks. Good catch. I thought I deleted it in the merge but maybe I skipped it.
## Version v0.37.0 ### Added - [1609](#1609): Add DA compression support. Compressed blocks are stored in the offchain database when blocks are produced, and can be fetched using the GraphQL API. - [2290](#2290): Added a new CLI argument `--graphql-max-directives`. The default value is `10`. - [2195](#2195): Added enforcement of the limit on the size of the L2 transactions per block according to the `block_transaction_size_limit` parameter. - [2131](#2131): Add flow in TxPool in order to ask to newly connected peers to share their transaction pool - [2182](#2151): Limit number of transactions that can be fetched via TxSource::next - [2189](#2151): Select next DA height to never include more than u16::MAX -1 transactions from L1. - [2162](#2162): Pool structure with dependencies, etc.. for the next transaction pool module. Also adds insertion/verification process in PoolV2 and tests refactoring - [2265](#2265): Integrate Block Committer API for DA Block Costs. - [2280](#2280): Allow comma separated relayer addresses in cli - [2299](#2299): Support blobs in the predicates. - [2300](#2300): Added new function to `fuel-core-client` for checking whether a blob exists. ### Changed #### Breaking - [2299](#2299): Anyone who wants to participate in the transaction broadcasting via p2p must upgrade to support new predicates on the TxPool level. - [2299](#2299): Upgraded `fuel-vm` to `0.58.0`. More information in the [release](https://github.com/FuelLabs/fuel-vm/releases/tag/v0.58.0). - [2276](#2276): Changed how complexity for blocks is calculated. The default complexity now is 80_000. All queries that somehow touch the block header now are more expensive. - [2290](#2290): Added a new GraphQL limit on number of `directives`. The default value is `10`. - [2206](#2206): Use timestamp of last block when dry running transactions. - [2153](#2153): Updated default gas costs for the local testnet configuration to match `fuel-core 0.35.0`. ## What's Changed * fix: use core-test.fuellabs.net for dnsaddr resolution by @rymnc in #2214 * Removed state transition bytecode from the local testnet by @xgreenx in #2215 * Send whole transaction pool upon subscription to gossip by @AurelienFT in #2131 * Update default gas costs based on 0.35.0 benchmarks by @xgreenx in #2153 * feat: Use timestamp of last block when dry running transactions by @netrome in #2206 * fix(dnsaddr_resolution): use fqdn separator to prevent suffixing by dns resolvers by @rymnc in #2222 * TransactionSource: specify maximum number of transactions to be fetched by @acerone85 in #2182 * Implement worst case scenario for price algorithm v1 by @rafal-ch in #2219 * chore(gas_price_service): define port for L2 data by @rymnc in #2224 * Block producer selects da height to never exceed u64::MAX - 1 transactions from L1 by @acerone85 in #2189 * Weekly `cargo update` by @github-actions in #2236 * Use fees to calculate DA reward and avoid issues with Gwei/Wei conversions by @MitchTurner in #2229 * Protect against passing `i128::MIN` to `abs()` which causes overflow by @rafal-ch in #2241 * Acquire `da_finalization_period` from the command line by @rafal-ch in #2240 * Executor: test Tx_count limit with incorrect tx source by @acerone85 in #2242 * Minor updates to docs + a few typos fixed by @rafal-ch in #2250 * chore(gas_price_service): move algorithm_updater to fuel-core-gas-price-service by @rymnc in #2246 * Use single heavy input in the `transaction_throughput.rs` benchmarks by @xgreenx in #2205 * Enforce the block size limit by @rafal-ch in #2195 * feat: build ARM and AMD in parallel by @mchristopher in #2130 * Weekly `cargo update` by @github-actions in #2268 * chore(gas_price_service): split into v0 and v1 and squash FuelGasPriceUpdater type into GasPriceService by @rymnc in #2256 * feat(gas_price_service): update block committer da source with established contract by @rymnc in #2265 * Use bytes from `unrecorded_blocks` rather from the block from DA by @MitchTurner in #2252 * TxPool v2 General architecture by @AurelienFT in #2162 * Add value delimiter and tests args by @AurelienFT in #2280 * fix(da_block_costs): remove Arc<Mutex<>> on shared_state and expose channel by @rymnc in #2278 * fix(combined_database): syncing auxiliary databases on startup with custom behaviour by @rymnc in #2272 * fix: Manually encode Authorization header for eventsource_client by @Br1ght0ne in #2284 * Address `async-graphql` vulnerability by @MitchTurner in #2290 * Update the WASM compatibility tests for `0.36` release by @rafal-ch in #2271 * DA compression by @Dentosal in #1609 * Use different port for every version compatibility test by @rafal-ch in #2301 * Fix block query complexity by @xgreenx in #2297 * Support blobs in predicates by @Voxelot in #2299 **Full Changelog**: v0.36.0...v0.37.0
Linked Issues/PRs
Closes #2244
Description
Essentially, we would be getting the compressed bytes from the committer, and we already have access to the true bytes in the
unrecorded_blocks
, so this PR changes the algo to use the values on thoseunrecorded_blocks
Uncovered some problems with the simulator in the mean time and created a followup issue for dealing with that.
Checklist
Before requesting review
Followup Issue
#2264