-
Notifications
You must be signed in to change notification settings - Fork 76
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
base: main
Are you sure you want to change the base?
Conversation
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.
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
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.
Blocking this because the bridge address construction cannot assume a fixed prefix.
Suggestions for unblocking this while we redesign our fee handling.
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. |
Should we also change |
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.
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.
pub(crate) fn calculate_base_deposit_fee(deposit: &Deposit) -> Option<u128> { | ||
let variable_length = deposit | ||
.asset | ||
.to_string() |
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.
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.
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 in c46fa8a, let me know if this implementation works for you!
@@ -691,4 +709,18 @@ mod tests { | |||
|
|||
assert_eq!(None, denom.pop_leading_port_and_channel()); | |||
} | |||
|
|||
#[test] | |||
fn display_len_ok() { |
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.
Provide better test names:
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:
astria/crates/astria-core/src/primitive/v1/mod.rs
Lines 771 to 796 in f11f900
#[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()); |
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.
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(), |
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.
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> { |
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.
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()) |
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.
As mentioned above: checked addition is unnecessary.
#[must_use] | ||
pub fn display_len(&self) -> Option<usize> { | ||
match self { | ||
Denom::TracePrefixed(trace) => { |
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.
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) |
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.
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) |
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.
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() { |
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.
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() { |
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.
You are in a test so don't call the test a test:
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); |
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.
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 |
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.
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. |
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.
// `get_base_deposit_byte_length()` below. | |
/// `tests::get_base_deposit_byte_length()` below. |
Summary
Changed
Deposit
byte length calculation to use constant base value plus the lengths ofasset
anddestination_chain_address
.Background
The
get_deposit_byte_length()
function usesprost::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 forBridgeLock
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
anddestination_chain_address
Changes
get_deposit_byte_length()
based on a constant base length plus the lengths ofasset
anddestination_chain_address
. The base length is determined by a unit test with "reasonable" values for all fields except forasset
anddestination_chain_address
.Testing
asset
anddestination_chain_address
.Breaking Changelist
BridgeLock
actions, breaking the app hash.Related Issues
closes #1503