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

chore(sequencer)!: change Deposit byte length calculation #1507

Open
wants to merge 11 commits into
base: main
Choose a base branch
from

Conversation

ethanoroshiba
Copy link
Contributor

@ethanoroshiba ethanoroshiba commented Sep 16, 2024

Summary

Changed Deposit byte length calculation to use constant base value plus the lengths ofasset and destination_chain_address.

Background

The get_deposit_byte_length() function uses prost::message::encoded_len() to calculate the on-wire size of raw deposit events for fee calculation. Due to protobuf using variable length integers, however, this would mean that fees for BridgeLock actions would increase with larger deposit amounts or higher action indices, which should not be the case. This PR aims to address this issue and provide more fair fee calculation.

Offline discussion resulted in the decision to calculate a "fair" base length and then add the lengths of asset and destination_chain_address

Changes

  • Calculate get_deposit_byte_length() based on a constant base length plus the lengths of asset and destination_chain_address. The base length is determined by a unit test with "reasonable" values for all fields except for asset and destination_chain_address.

Testing

  • Added unit test to ensure calculated byte length doesn't change for any other changes than to asset and destination_chain_address.
  • Added snapshot test to ensure the method for calculating byte length is not changed.

Breaking Changelist

  • Changing the byte length calculation changes fees for BridgeLock actions, breaking the app hash.

Related Issues

closes #1503

@github-actions github-actions bot added the sequencer pertaining to the astria-sequencer crate label Sep 16, 2024
@ethanoroshiba ethanoroshiba marked this pull request as ready for review September 16, 2024 16:29
Copy link
Collaborator

@noot noot left a comment

Choose a reason for hiding this comment

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

codewise this looks fine, arguably values that encode to a smaller length should be cheaper, as the fee should be proportional to what's written to DA. however this will return a larger value rather than smaller, which seems okay

Copy link
Member

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

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

Blocking this because the bridge address construction cannot assume a fixed prefix.

@SuperFluffy
Copy link
Member

Suggestions for unblocking this while we redesign our fee handling.

  1. Make a "base case" of a deposit with destination_chain_address and asset empty and an arbitrary prefix set (take the prefix as the max number of bytes permitted in a bech32m encoding, which would be 83 ASCII chars)
  2. define an constant from the Message::encoded_len of this base case scenario
  3. define the fee as the constant from 2. and add the lengths of asset and destination_chain_address to that

Support 2. with a unit test, and provide a few unit tests to support the implementation's claim that the calculated fee/byte length be never smaller than the actual proto len.

@ethanoroshiba
Copy link
Contributor Author

Should we also change get_deposit_byte_length() to be called something more reflective of its true functionality?

Copy link
Member

@SuperFluffy SuperFluffy left a comment

Choose a reason for hiding this comment

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

Should we also change get_deposit_byte_length() to be called something more reflective of its true functionality?

Yeah, we should make give this a more meaningful name in its context. It's extremely rescriptive of what it's doing right now but doesn't give extra meaning.

crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
pub(crate) fn calculate_base_deposit_fee(deposit: &Deposit) -> Option<u128> {
let variable_length = deposit
.asset
.to_string()
Copy link
Member

Choose a reason for hiding this comment

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

Can you extend Deposit to provide a function like display_len() that calculates this using its formatted representation? Also provide tests in astria-core to test that.

We can avoid this allocation.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Done in c46fa8a, let me know if this implementation works for you!

crates/astria-sequencer/src/bridge/bridge_lock_action.rs Outdated Show resolved Hide resolved
@@ -691,4 +709,18 @@ mod tests {

assert_eq!(None, denom.pop_leading_port_and_channel());
}

#[test]
fn display_len_ok() {
Copy link
Member

@SuperFluffy SuperFluffy Sep 20, 2024

Choose a reason for hiding this comment

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

Provide better test names:

Suggested change
fn display_len_ok() {
fn display_len_returns_expected_number() {

Yeah, thats what I had in mind. But in addition provide more exhaustive tests for some other scenarios. Use #[track_caller] to define the actual assertion and call it repeatedly from a test, similar to what's done here:

#[track_caller]
fn assert_wrong_address_bytes(bad_account: &[u8]) {
let error = Address::<Bech32m>::builder()
.slice(bad_account)
.prefix(ASTRIA_ADDRESS_PREFIX)
.try_build()
.expect_err(
"converting from an incorrectly sized byte slice succeeded where it should have \
failed",
);
let AddressError(AddressErrorKind::IncorrectAddressLength {
received,
}) = error
else {
panic!("expected AddressErrorKind::IncorrectAddressLength, got {error:?}");
};
assert_eq!(bad_account.len(), received);
}
#[test]
fn account_of_incorrect_length_gives_error() {
assert_wrong_address_bytes(&[42; 0]);
assert_wrong_address_bytes(&[42; 19]);
assert_wrong_address_bytes(&[42; 21]);
assert_wrong_address_bytes(&[42; 100]);
}

Here is some background reading on this: https://matklad.github.io/2021/05/31/how-to-test.html#Test-Driven-Design-Ossification


#[test]
fn display_len_ok() {
let trace_denom = Denom::from("a/long/path/to/denom".parse::<TracePrefixed>().unwrap());
Copy link
Member

Choose a reason for hiding this comment

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

Parse directly into a Denom. This is unnecessarily noisy.

fn display_len_ok() {
let trace_denom = Denom::from("a/long/path/to/denom".parse::<TracePrefixed>().unwrap());
assert_eq!(
trace_denom.to_string().len(),
Copy link
Member

Choose a reason for hiding this comment

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

Use the length of the input string (expressing it with the track_caller check I suggested above will make that neater).


/// Calculates the length of the display formatted [Denom] without allocating a String.
#[must_use]
pub fn display_len(&self) -> Option<usize> {
Copy link
Member

@SuperFluffy SuperFluffy Sep 20, 2024

Choose a reason for hiding this comment

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

In general we use checked addition for doing math on externally provided data (specifically to calculate balances), but in this case returning usize is more than acceptable.

On any currently available machine you will not blow through 2^64-1 bytes of RAM of allocated strings data.

let mut len: usize = 0;
for segment in &trace.trace.inner {
len = len
.checked_add(segment.port.len())
Copy link
Member

@SuperFluffy SuperFluffy Sep 20, 2024

Choose a reason for hiding this comment

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

As mentioned above: checked addition is unnecessary.

#[must_use]
pub fn display_len(&self) -> Option<usize> {
match self {
Denom::TracePrefixed(trace) => {
Copy link
Member

Choose a reason for hiding this comment

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

Define these on the trace-prefixed and ibc-prefixed types. Then delegate to them in this enum.

@@ -99,7 +103,10 @@ impl ActionHandler for BridgeLockAction {
.await
.wrap_err("failed to get byte cost multiplier")?;
let fee = byte_cost_multiplier
.saturating_mul(get_deposit_byte_len(&deposit))
.saturating_mul(
calculate_base_deposit_fee(&deposit)
Copy link
Member

Choose a reason for hiding this comment

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

Because we are doing saturating arithmetic here I would just keep doing that. Instead of returning early just replace it with with u128::MAX

@@ -126,7 +133,10 @@ impl ActionHandler for BridgeLockAction {
.get_bridge_lock_byte_cost_multiplier()
.await
.wrap_err("failed to get byte cost multiplier")?;
let fee = byte_cost_multiplier.saturating_mul(get_deposit_byte_len(&deposit));
let fee = byte_cost_multiplier.saturating_mul(
calculate_base_deposit_fee(&deposit)
Copy link
Member

Choose a reason for hiding this comment

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

As above, replace with u128::MAX in this context.

/// is to allow for more flexibility in overall fees (we have more flexibility multiplying by a
/// lower number, and if we want fees to be higher we can just raise the multiplier).
#[test]
fn get_base_deposit_fee() {
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
fn get_base_deposit_fee() {
fn calculate_base_deposit_fee() {

state
.put_account_balance(from_address, &asset, 100 + expected_deposit_fee)
.unwrap();
bridge_lock.check_and_execute(&mut state).await.unwrap();
}

#[test]
fn test_calculate_base_deposit_fee() {
Copy link
Member

Choose a reason for hiding this comment

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

You are in a test so don't call the test a test:

Suggested change
fn test_calculate_base_deposit_fee() {
fn calculated_fee_matches_expected_value() {

I'd also refactor the test to use #[track_caller]. See my comment for testing display_len in astria_core.

source_action_index: 0,
};
let calculated_len_2 = calculate_base_deposit_fee(&deposit).unwrap();
assert!(calculated_len_2 >= calculated_len_1);
Copy link
Member

Choose a reason for hiding this comment

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

Try to avoid assert!. If this fails the reader will not know what exactly failed. It's better to use assert_eq!. IMO that's also fine here because all of the values are deterministic.

@@ -34,6 +34,10 @@ use crate::{
utils::create_deposit_event,
};

// The base byte length of a deposit, as determined by the unit test
Copy link
Member

Choose a reason for hiding this comment

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

Make this a doc comment, i.e. start with ///

@@ -34,6 +34,10 @@ use crate::{
utils::create_deposit_event,
};

// The base byte length of a deposit, as determined by the unit test
// `get_base_deposit_byte_length()` below.
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
// `get_base_deposit_byte_length()` below.
/// `tests::get_base_deposit_byte_length()` below.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
sequencer pertaining to the astria-sequencer crate
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Investigate usage of fixed length integers for Deposit fields
3 participants