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

feat(gas_price_service_v1): define RunnableTask for GasPriceServiceV1 #2416

Merged
merged 16 commits into from
Nov 14, 2024

Conversation

rymnc
Copy link
Member

@rymnc rymnc commented Oct 31, 2024

Note

Some values for the tests need expert opinion from @MitchTurner. A follow up PR will be created to define the UninitializedTask that wraps over this task.

Linked Issues/PRs

part of #2140, but doesn't close it yet.

Description

  • We define a new RunnableTask, GasPriceServiceV1, which uses the da block cost source in tandem with the l2 block source
  • Tests for the same
  • Casts to and from the v1 algorithm updater
  • we take a direct dependency on v0's metadata so that we can migrate between the two.

Checklist

  • Breaking changes are clearly marked as such in the PR description and changelog
  • New behavior is reflected in tests
  • The specification matches the implemented behavior (link update PR if changes are needed)

Before requesting review

  • I have reviewed the code myself
  • I have created follow-up issues caused by this PR and linked them here

After merging, notify other teams

[Add or remove entries as needed]

@rymnc rymnc self-assigned this Oct 31, 2024
}

#[tokio::test]
async fn run__updates_gas_price_with_da_block_cost_source() {
Copy link
Member Author

Choose a reason for hiding this comment

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

cc: @MitchTurner this test seems to be failing with the following error -

called `Result::unwrap()` on an `Err` value: L2 block expected but not found in unrecorded blocks: 1

which i narrowed down to

fn drain_l2_block_bytes_for_range(
&mut self,
height_range: Range<u32>,
) -> Result<u128, Error> {
let mut total: u128 = 0;
for expected_height in height_range {
let (actual_height, bytes) = self
.unrecorded_blocks
.pop_first()
.ok_or(Error::L2BlockExpectedNotFound(expected_height))?;
if actual_height != expected_height {
return Err(Error::L2BlockExpectedNotFound(expected_height));
}
total = total.saturating_add(bytes as u128);
}
Ok(total)
}
.

This is what happens when the da block cost that is received refers to an L2 block that hasn't been processed by the algorithm yet. that is fine, seems like defined behaviour.

Below, on line 432-434, I define the cost and bytes. Something seems to be wrong in the drain_l2_block_bytes_for_range function

fn drain_l2_block_bytes_for_range(
when I call it with just one item in the range. because the range_bytes it yields after being called in
let range_bytes = self.drain_l2_block_bytes_for_range(height_range)?;
is 0, which doesn't seem too correct.

Copy link
Member Author

Choose a reason for hiding this comment

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

wondering if you have any thoughts about the params I'm using.

Copy link
Member

Choose a reason for hiding this comment

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

I think we discussed this offline someplace, but I'll record it here.

This is what happens when the da block cost that is received refers to an L2 block that hasn't been processed by the algorithm yet. that is fine, seems like defined behaviour.

We should always receive DA recorded blocks after L2 blocks. This is because we can't commit a block to DA until after it is produced on L2.

when I call it with just one item in the range. because the range_bytes it yields after being called in

Good catch. I've changed the behavior anyway in this pr: #2415

Should solve it.

Copy link
Member

Choose a reason for hiding this comment

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

wondering if you have any thoughts about the params I'm using.

With these tests, the values don't exactly matter. We use an assert_ne! and as long as that's true we're not concerned with the details. The problem with this approach is the test writer/reader still needs to know what values would result in a different gas price. This is part of the reason I originally had an abstraction of gas price updater (which we've (you've) removed)

/// The range of blocks that the costs apply to
pub blocks_range: core::ops::Range<u64>,
pub blocks_range: core::ops::Range<u32>,
Copy link
Member

Choose a reason for hiding this comment

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

We'll need to change this to just be a vec with these changes:
#2415

Comment on lines 197 to 207
// arbitrary logic to generate a new value
let mut value = self.value.clone();
if let Some(value) = &mut value {
value.sequence_number = seq_no;
value.blocks_range =
value.blocks_range.end * seq_no..value.blocks_range.end * seq_no + 10;
value.da_block_height = value.da_block_height + (seq_no + 1).into();
value.da_block_height =
value.da_block_height + ((seq_no + 1) as u64).into();
value.total_cost += 1;
value.total_size_bytes += 1;
}
Copy link
Member

Choose a reason for hiding this comment

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

There is a lot of unchecked arithmetic and casting going on here. How do you feel about switching to checked and into/from here?

}

#[tokio::test]
async fn run__updates_gas_price_with_da_block_cost_source() {
Copy link
Member

Choose a reason for hiding this comment

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

I think we discussed this offline someplace, but I'll record it here.

This is what happens when the da block cost that is received refers to an L2 block that hasn't been processed by the algorithm yet. that is fine, seems like defined behaviour.

We should always receive DA recorded blocks after L2 blocks. This is because we can't commit a block to DA until after it is produced on L2.

when I call it with just one item in the range. because the range_bytes it yields after being called in

Good catch. I've changed the behavior anyway in this pr: #2415

Should solve it.

}

#[tokio::test]
async fn run__updates_gas_price_with_da_block_cost_source() {
Copy link
Member

Choose a reason for hiding this comment

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

wondering if you have any thoughts about the params I'm using.

With these tests, the values don't exactly matter. We use an assert_ne! and as long as that's true we're not concerned with the details. The problem with this approach is the test writer/reader still needs to know what values would result in a different gas price. This is part of the reason I originally had an abstraction of gas price updater (which we've (you've) removed)


// when
service.run(&mut watcher).await.unwrap();
l2_block_sender.send(l2_block).await.unwrap();
Copy link
Member

Choose a reason for hiding this comment

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

Is the bug you're seeing the reason we aren't submitting a new DA block here?

Copy link
Member Author

Choose a reason for hiding this comment

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

not seeing the bug anymore :)

Copy link
Member

Choose a reason for hiding this comment

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

I think this test should be "sending" a block to the DA record service, not the l2_block_sender.

Copy link
Member Author

Choose a reason for hiding this comment

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

I still need to trigger update_da_gas_price(), so I've added it in the update_da_record_data fn of the AlgorithmV1 impl in c516ea9

@@ -103,12 +104,14 @@ where
async fn run(&mut self, state_watcher: &mut StateWatcher) -> Result<bool> {
let continue_running;

let tick: BoxFuture<tokio::time::Instant> = Box::pin(self.poll_interval.tick());
Copy link
Member

Choose a reason for hiding this comment

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

Why do we need the BoxFuture? I ran it on my machine without and it compiled.

Copy link
Member Author

Choose a reason for hiding this comment

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

you're right, I had it that way when i was testing something. removed in 53291a5

@@ -313,6 +313,7 @@ impl AlgorithmUpdaterV1 {
if !height_range.is_empty() {
self.da_block_update(height_range, range_cost)?;
self.recalculate_projected_cost();
self.update_da_gas_price();
Copy link
Member

Choose a reason for hiding this comment

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

Ah. Haha. I get it now... I think it makes sense either way but this probably better.

Copy link
Member

Choose a reason for hiding this comment

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

Probably should have some tests in the algo for this.

Copy link
Member Author

Choose a reason for hiding this comment

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

addressed in 8ae983f

pub capped_range_size: u16,
pub decrease_range_size: u16,
pub block_activity_threshold: u8,
pub unrecorded_blocks: Vec<(u32, u64)>,
Copy link
Member

Choose a reason for hiding this comment

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

Hmm. I guess it doesn't hurt. I don't know why we'd need this if it wasn't already in the metadata.

Copy link
Member Author

Choose a reason for hiding this comment

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

need this to bootstrap the AlgorithmV1 with a state that we can test only DA data being sent :)

Copy link
Member

Choose a reason for hiding this comment

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

Hmm. K. Technically you could provide a metadata with it, but that's expensive.

Comment on lines 260 to 273
if let Ok(v1_metadata) = V1Metadata::try_from(updater_metadata.clone()) {
algorithm_updater = v1_algorithm_from_metadata(v1_metadata, config);
} else {
let v0_metadata = V0Metadata::try_from(updater_metadata).map_err(|_| {
crate::common::utils::Error::CouldNotInitUpdater(anyhow::anyhow!(
"Could not convert metadata to V0Metadata"
))
})?;
let v1_metadata = V1Metadata::construct_from_v0_metadata(v0_metadata, config)
.map_err(|err| {
crate::common::utils::Error::CouldNotInitUpdater(anyhow::anyhow!(err))
})?;
algorithm_updater = v1_algorithm_from_metadata(v1_metadata, config);
}
Copy link
Member

@MitchTurner MitchTurner Nov 13, 2024

Choose a reason for hiding this comment

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

nit: I think we should just unify this branch and add a method fn best_v1_metadata(metadata: &Metadata, config: &V1AlgorithmConfig). In both branches we end up calling v1_algorithm_from_metadata, so something like:

Suggested change
if let Ok(v1_metadata) = V1Metadata::try_from(updater_metadata.clone()) {
algorithm_updater = v1_algorithm_from_metadata(v1_metadata, config);
} else {
let v0_metadata = V0Metadata::try_from(updater_metadata).map_err(|_| {
crate::common::utils::Error::CouldNotInitUpdater(anyhow::anyhow!(
"Could not convert metadata to V0Metadata"
))
})?;
let v1_metadata = V1Metadata::construct_from_v0_metadata(v0_metadata, config)
.map_err(|err| {
crate::common::utils::Error::CouldNotInitUpdater(anyhow::anyhow!(err))
})?;
algorithm_updater = v1_algorithm_from_metadata(v1_metadata, config);
}
let v1_metadata = best_v1_metadata(updater_metadata, config)?;
algorithm_updater = v1_algorithm_from_metadata(v1_metadata, config);

Copy link
Member Author

Choose a reason for hiding this comment

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

addressed in 2291207

@rymnc rymnc marked this pull request as ready for review November 13, 2024 15:09
@rymnc rymnc requested a review from a team November 13, 2024 15:09

// then
let new_da_gas_price = updater.new_scaled_da_gas_price;
assert_ne!(old_da_gas_price, new_da_gas_price);
Copy link
Member

Choose a reason for hiding this comment

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

We should be more specific at this level (and not just say it is different) You can copy most of the code from
update_l2_block_data__below_threshold_will_decrease_exec_gas_price, update_l2_block_data__above_threshold_will_increase_exec_gas_price, and update_l2_block_data__even_threshold_will_not_change_exec_gas_price

It feels redundant, but they are different endpoints so they change independently.

AurelienFT
AurelienFT previously approved these changes Nov 14, 2024
Copy link
Contributor

@AurelienFT AurelienFT left a comment

Choose a reason for hiding this comment

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

The code looks clean and understandable from an external POV.

MitchTurner
MitchTurner previously approved these changes Nov 14, 2024
MitchTurner
MitchTurner previously approved these changes Nov 14, 2024
AurelienFT
AurelienFT previously approved these changes Nov 14, 2024
@AurelienFT AurelienFT dismissed stale reviews from MitchTurner and themself via 1e3b3f5 November 14, 2024 09:09
@rymnc rymnc merged commit ba0fa15 into master Nov 14, 2024
38 checks passed
@rymnc rymnc deleted the feat/gas-price-service-v1-impl branch November 14, 2024 10:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants