Skip to content
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

Merged
merged 22 commits into from
Oct 2, 2024

Conversation

MitchTurner
Copy link
Member

@MitchTurner MitchTurner commented Sep 25, 2024

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 those unrecorded_blocks

Uncovered some problems with the simulator in the mean time and created a followup issue for dealing with that.

image

Checklist

  • New behavior is reflected in tests

Before requesting review

  • I have reviewed the code myself

Followup Issue

#2264

@MitchTurner MitchTurner added the no changelog Skip the CI check of the changelog modification label Sep 25, 2024
@MitchTurner MitchTurner marked this pull request as ready for review September 25, 2024 14:08
@MitchTurner MitchTurner requested review from rafal-ch and a team September 25, 2024 14:09
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
Copy link
Collaborator

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?

Copy link
Contributor

@rafal-ch rafal-ch Sep 25, 2024

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

Copy link
Member Author

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.

Copy link
Member Author

@MitchTurner MitchTurner Sep 25, 2024

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.

Copy link
Collaborator

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=)

Copy link
Member Author

@MitchTurner MitchTurner Sep 25, 2024

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.

Copy link
Collaborator

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)

Copy link
Member Author

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.

Copy link
Member

@rymnc rymnc Sep 26, 2024

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>>;
}

Copy link
Member Author

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
Copy link
Contributor

@rafal-ch rafal-ch Sep 25, 2024

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(
Copy link
Member Author

@MitchTurner MitchTurner Sep 26, 2024

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.

Copy link
Member Author

@MitchTurner MitchTurner Sep 26, 2024

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.

@MitchTurner MitchTurner self-assigned this Sep 26, 2024
rafal-ch
rafal-ch previously approved these changes Sep 27, 2024
Copy link
Contributor

@rafal-ch rafal-ch left a 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.

@MitchTurner MitchTurner requested a review from a team September 27, 2024 17:02
rafal-ch
rafal-ch previously approved these changes Sep 30, 2024
Copy link
Contributor

@rafal-ch rafal-ch left a 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();
Copy link
Member

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?

Copy link
Member Author

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,
Copy link
Member

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?

@MitchTurner
Copy link
Member Author

This could be a non-issue or a real deal that we might consider handling as an follow-up issue.

@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.

@rafal-ch
Copy link
Contributor

This could be a non-issue or a real deal that we might consider handling as an follow-up issue.

@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.

It could be as simple as having a hardcoded or configured upper limit on unrecorded blocks and reject to accept more.

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.

True.
It could also happen due to a bug or an edge case in wiring up of the function calls, even if the committer is responding.

But yeah, since it's unlikely and of rather low impact we can move forward as is and observe how it behaves in production.

rafal-ch
rafal-ch previously approved these changes Sep 30, 2024
Dentosal
Dentosal previously approved these changes Oct 2, 2024
@@ -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)>))>,
Copy link
Member

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

Copy link
Member Author

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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
.update_da_record_data(height..(height + 1), *cost)
.update_da_record_data(height..=height, *cost)

Copy link
Member Author

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.

Copy link
Member Author

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::{
Copy link
Collaborator

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?

Copy link
Member Author

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.

@MitchTurner MitchTurner enabled auto-merge (squash) October 2, 2024 18:13
@MitchTurner MitchTurner merged commit c039c28 into master Oct 2, 2024
37 of 38 checks passed
@MitchTurner MitchTurner deleted the fix/use-bytes-from-l2-blocks-not-da branch October 2, 2024 18:30
@xgreenx xgreenx mentioned this pull request Oct 5, 2024
xgreenx added a commit that referenced this pull request Oct 5, 2024
## 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
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
no changelog Skip the CI check of the changelog modification
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Use true bytes not compressed bytes for calculated cost_per_byte in algorithm
5 participants