From 6d0dcd6212fb755961f4f8112c5365c46ed38e7e Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 20 Feb 2025 14:19:50 -0500 Subject: [PATCH 1/4] add Int128 proto type --- crates/astria-core/src/connect/types.rs | 10 +- crates/astria-core/src/connect/utils.rs | 12 +- .../src/generated/astria.primitive.v1.rs | 28 ++++- .../generated/astria.primitive.v1.serde.rs | 114 ++++++++++++++++++ .../src/generated/astria.sequencerblock.v1.rs | 2 +- crates/astria-core/src/primitive/v1/i128.rs | 53 ++++++++ crates/astria-core/src/primitive/v1/mod.rs | 1 + crates/astria-core/src/protocol/genesis/v1.rs | 2 +- .../astria/primitive/v1/types.proto | 18 ++- .../astria/sequencerblock/v1/block.proto | 2 +- 10 files changed, 226 insertions(+), 16 deletions(-) create mode 100644 crates/astria-core/src/primitive/v1/i128.rs diff --git a/crates/astria-core/src/connect/types.rs b/crates/astria-core/src/connect/types.rs index 13a110fc53..25881d3bbc 100644 --- a/crates/astria-core/src/connect/types.rs +++ b/crates/astria-core/src/connect/types.rs @@ -14,16 +14,16 @@ pub mod v2 { use crate::generated::connect::types::v2 as raw; #[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] - pub struct Price(u128); + pub struct Price(i128); impl Price { #[must_use] - pub const fn new(value: u128) -> Self { + pub const fn new(value: i128) -> Self { Self(value) } #[must_use] - pub fn get(self) -> u128 { + pub fn get(self) -> i128 { self.0 } } @@ -33,7 +33,7 @@ pub mod v2 { self.get().checked_add(rhs.get()).map(Self) } - pub fn checked_div(self, rhs: u128) -> Option { + pub fn checked_div(self, rhs: i128) -> Option { self.get().checked_div(rhs).map(Self) } } @@ -73,7 +73,7 @@ pub mod v2 { let be_bytes = <[u8; 16]>::try_from(&*input).map_err(|_| Self::Error { input, })?; - Ok(Price::new(u128::from_be_bytes(be_bytes))) + Ok(Price::new(i128::from_be_bytes(be_bytes))) } } diff --git a/crates/astria-core/src/connect/utils.rs b/crates/astria-core/src/connect/utils.rs index b4c2232294..3293ad404b 100644 --- a/crates/astria-core/src/connect/utils.rs +++ b/crates/astria-core/src/connect/utils.rs @@ -177,7 +177,7 @@ mod test { } } - fn oracle_vote_extension>(prices: I) -> OracleVoteExtension { + fn oracle_vote_extension>(prices: I) -> OracleVoteExtension { OracleVoteExtension { prices: prices .into_iter() @@ -221,7 +221,7 @@ mod test { #[test] fn should_calculate_median() { - fn prices>(prices: I) -> Vec { + fn prices>(prices: I) -> Vec { prices.into_iter().map(Price::new).collect() } @@ -245,14 +245,14 @@ mod test { // Should handle large values in a set with odd number of entries. assert_eq!( - u128::MAX, - median(prices([u128::MAX, u128::MAX, 1])).unwrap().get() + i128::MAX, + median(prices([i128::MAX, i128::MAX, 1])).unwrap().get() ); // Should handle large values in a set with even number of entries. assert_eq!( - u128::MAX - 1, - median(prices([u128::MAX, u128::MAX, u128::MAX - 1, u128::MAX - 1])) + i128::MAX - 1, + median(prices([i128::MAX, i128::MAX, i128::MAX - 1, i128::MAX - 1])) .unwrap() .get() ); diff --git a/crates/astria-core/src/generated/astria.primitive.v1.rs b/crates/astria-core/src/generated/astria.primitive.v1.rs index f8b4736ad8..256d475729 100644 --- a/crates/astria-core/src/generated/astria.primitive.v1.rs +++ b/crates/astria-core/src/generated/astria.primitive.v1.rs @@ -1,4 +1,4 @@ -/// A 128 bit unsigned integer encoded in protobuf., +/// A 128 bit unsigned integer encoded in protobuf. /// /// Protobuf does not support integers larger than 64 bits, /// so this message encodes a u128 by splitting it into its @@ -25,6 +25,32 @@ impl ::prost::Name for Uint128 { ::prost::alloc::format!("astria.primitive.v1.{}", Self::NAME) } } +/// A 128 bit signed integer encoded in protobuf. +/// +/// Protobuf does not support integers larger than 64 bits, +/// so this message encodes a i128 by splitting it into its +/// upper 64 and lower 64 bits, each encoded as a uint64. +/// +/// A native i128 x can then be constructed by casting both +/// integers to i128, left shifting hi by 64 positions and +/// adding lo: +/// +/// x = (hi as i128) << 64 + (lo as i128) +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct Int128 { + #[prost(uint64, tag = "1")] + pub lo: u64, + #[prost(uint64, tag = "2")] + pub hi: u64, +} +impl ::prost::Name for Int128 { + const NAME: &'static str = "Int128"; + const PACKAGE: &'static str = "astria.primitive.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.primitive.v1.{}", Self::NAME) + } +} /// A proof for a tree of the given size containing the audit path from a leaf to the root. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/astria-core/src/generated/astria.primitive.v1.serde.rs b/crates/astria-core/src/generated/astria.primitive.v1.serde.rs index 01dedf1d90..0124f61fcb 100644 --- a/crates/astria-core/src/generated/astria.primitive.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.primitive.v1.serde.rs @@ -201,6 +201,120 @@ impl<'de> serde::Deserialize<'de> for Denom { deserializer.deserialize_struct("astria.primitive.v1.Denom", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for Int128 { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.lo != 0 { + len += 1; + } + if self.hi != 0 { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.primitive.v1.Int128", len)?; + if self.lo != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("lo", ToString::to_string(&self.lo).as_str())?; + } + if self.hi != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("hi", ToString::to_string(&self.hi).as_str())?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for Int128 { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "lo", + "hi", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Lo, + Hi, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "lo" => Ok(GeneratedField::Lo), + "hi" => Ok(GeneratedField::Hi), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = Int128; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.primitive.v1.Int128") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut lo__ = None; + let mut hi__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Lo => { + if lo__.is_some() { + return Err(serde::de::Error::duplicate_field("lo")); + } + lo__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Hi => { + if hi__.is_some() { + return Err(serde::de::Error::duplicate_field("hi")); + } + hi__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + } + } + Ok(Int128 { + lo: lo__.unwrap_or_default(), + hi: hi__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.primitive.v1.Int128", FIELDS, GeneratedVisitor) + } +} impl serde::Serialize for Proof { #[allow(deprecated)] fn serialize(&self, serializer: S) -> std::result::Result diff --git a/crates/astria-core/src/generated/astria.sequencerblock.v1.rs b/crates/astria-core/src/generated/astria.sequencerblock.v1.rs index 82f3f8e0ab..a754a83d27 100644 --- a/crates/astria-core/src/generated/astria.sequencerblock.v1.rs +++ b/crates/astria-core/src/generated/astria.sequencerblock.v1.rs @@ -263,7 +263,7 @@ pub struct Price { super::super::super::connect::types::v2::CurrencyPair, >, #[prost(message, optional, tag = "2")] - pub price: ::core::option::Option, + pub price: ::core::option::Option, #[prost(uint64, tag = "3")] pub decimals: u64, } diff --git a/crates/astria-core/src/primitive/v1/i128.rs b/crates/astria-core/src/primitive/v1/i128.rs new file mode 100644 index 0000000000..dbc83d99a4 --- /dev/null +++ b/crates/astria-core/src/primitive/v1/i128.rs @@ -0,0 +1,53 @@ +//! Transformations of compiled protobuf types to other types. + +use crate::generated::astria::primitive::v1::Int128; +impl From for Int128 { + fn from(primitive: i128) -> Self { + let [h0, h1, h2, h3, h4, h5, h6, h7, l0, l1, l2, l3, l4, l5, l6, l7] = + primitive.to_be_bytes(); + let lo = u64::from_be_bytes([l0, l1, l2, l3, l4, l5, l6, l7]); + let hi = u64::from_be_bytes([h0, h1, h2, h3, h4, h5, h6, h7]); + Self { + lo, + hi, + } + } +} + +impl From for i128 { + fn from(pb: Int128) -> i128 { + let [l0, l1, l2, l3, l4, l5, l6, l7] = pb.lo.to_be_bytes(); + let [h0, h1, h2, h3, h4, h5, h6, h7] = pb.hi.to_be_bytes(); + i128::from_be_bytes([ + h0, h1, h2, h3, h4, h5, h6, h7, l0, l1, l2, l3, l4, l5, l6, l7, + ]) + } +} + +impl<'a> From<&'a i128> for Int128 { + fn from(primitive: &'a i128) -> Self { + (*primitive).into() + } +} + +#[cfg(test)] +mod tests { + use super::Int128; + + #[track_caller] + fn i128_roundtrip_check(expected: i128) { + let pb: Int128 = expected.into(); + let actual: i128 = pb.into(); + assert_eq!(expected, actual); + } + #[test] + fn i128_roundtrips_work() { + i128_roundtrip_check(0i128); + i128_roundtrip_check(1i128); + i128_roundtrip_check(i128::from(u64::MAX)); + i128_roundtrip_check(i128::from(u64::MAX) + 1i128); + i128_roundtrip_check(1i128 << 127); + i128_roundtrip_check((1i128 << 127) + (1i128 << 63)); + i128_roundtrip_check(i128::MAX); + } +} diff --git a/crates/astria-core/src/primitive/v1/mod.rs b/crates/astria-core/src/primitive/v1/mod.rs index fe2279960f..4b88769949 100644 --- a/crates/astria-core/src/primitive/v1/mod.rs +++ b/crates/astria-core/src/primitive/v1/mod.rs @@ -1,4 +1,5 @@ pub mod asset; +pub mod i128; pub mod u128; pub use astria_core_address::{ diff --git a/crates/astria-core/src/protocol/genesis/v1.rs b/crates/astria-core/src/protocol/genesis/v1.rs index 1992e712ea..9f74ecf653 100644 --- a/crates/astria-core/src/protocol/genesis/v1.rs +++ b/crates/astria-core/src/protocol/genesis/v1.rs @@ -1147,7 +1147,7 @@ mod tests { id: CurrencyPairId::new(1), nonce: CurrencyPairNonce::new(0), currency_pair_price: Some(QuotePrice { - price: Price::new(3_138_872_234_u128), + price: Price::new(3_138_872_234_i128), block_height: 0, block_timestamp: pbjson_types::Timestamp { seconds: 1_720_122_395, diff --git a/proto/primitives/astria/primitive/v1/types.proto b/proto/primitives/astria/primitive/v1/types.proto index 0d22856965..46fff598a8 100644 --- a/proto/primitives/astria/primitive/v1/types.proto +++ b/proto/primitives/astria/primitive/v1/types.proto @@ -2,7 +2,7 @@ syntax = "proto3"; package astria.primitive.v1; -// A 128 bit unsigned integer encoded in protobuf., +// A 128 bit unsigned integer encoded in protobuf. // // Protobuf does not support integers larger than 64 bits, // so this message encodes a u128 by splitting it into its @@ -18,6 +18,22 @@ message Uint128 { uint64 hi = 2; } +// A 128 bit signed integer encoded in protobuf. +// +// Protobuf does not support integers larger than 64 bits, +// so this message encodes a i128 by splitting it into its +// upper 64 and lower 64 bits, each encoded as a uint64. +// +// A native i128 x can then be constructed by casting both +// integers to i128, left shifting hi by 64 positions and +// adding lo: +// +// x = (hi as i128) << 64 + (lo as i128) +message Int128 { + uint64 lo = 1; + uint64 hi = 2; +} + // A proof for a tree of the given size containing the audit path from a leaf to the root. message Proof { // A sequence of 32 byte hashes used to reconstruct a Merkle Tree Hash. diff --git a/proto/sequencerblockapis/astria/sequencerblock/v1/block.proto b/proto/sequencerblockapis/astria/sequencerblock/v1/block.proto index 16e195017f..73cbf4a36b 100644 --- a/proto/sequencerblockapis/astria/sequencerblock/v1/block.proto +++ b/proto/sequencerblockapis/astria/sequencerblock/v1/block.proto @@ -157,6 +157,6 @@ message OracleData { message Price { connect.types.v2.CurrencyPair currency_pair = 1; - astria.primitive.v1.Uint128 price = 2; + astria.primitive.v1.Int128 price = 2; uint64 decimals = 3; } From db259d64323a020e6da937086ac847d67b57abea Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 20 Feb 2025 14:23:47 -0500 Subject: [PATCH 2/4] update sequencer to handle i128 price --- crates/astria-sequencer-utils/src/blob_parser.rs | 2 +- .../src/connect/oracle/storage/values/currency_pair_state.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/astria-sequencer-utils/src/blob_parser.rs b/crates/astria-sequencer-utils/src/blob_parser.rs index 547671249f..8fa4bb3a42 100644 --- a/crates/astria-sequencer-utils/src/blob_parser.rs +++ b/crates/astria-sequencer-utils/src/blob_parser.rs @@ -570,7 +570,7 @@ impl Display for PrintableDeposit { #[derive(Serialize, Debug)] struct PrintableOracleData { - prices: Vec<(String, u128, u64)>, + prices: Vec<(String, i128, u64)>, } impl TryFrom<&RawOracleData> for PrintableOracleData { diff --git a/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs b/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs index 4f84a654d4..a26a78db50 100644 --- a/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs +++ b/crates/astria-sequencer/src/connect/oracle/storage/values/currency_pair_state.rs @@ -50,7 +50,7 @@ impl From for DomainTimestamp { #[derive(Debug, BorshSerialize, BorshDeserialize)] struct QuotePrice { - price: u128, + price: i128, block_timestamp: Timestamp, block_height: u64, } From af3bd753bd9f01c65b8192bd7ffe983d7a6d9e0d Mon Sep 17 00:00:00 2001 From: elizabeth Date: Thu, 20 Feb 2025 14:29:14 -0500 Subject: [PATCH 3/4] add test --- crates/astria-core/src/connect/types.rs | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/crates/astria-core/src/connect/types.rs b/crates/astria-core/src/connect/types.rs index 25881d3bbc..894b662684 100644 --- a/crates/astria-core/src/connect/types.rs +++ b/crates/astria-core/src/connect/types.rs @@ -448,5 +448,11 @@ pub mod v2 { "ETH /USD".parse::().unwrap_err(); "ETH/ USD".parse::().unwrap_err(); } + + #[test] + fn can_parse_negative_price() { + let price = "-1".parse::().unwrap(); + assert_eq!(price.get(), -1); + } } } From f89e1eebd5a2060788e087653352ee9ce22091a0 Mon Sep 17 00:00:00 2001 From: elizabeth Date: Mon, 24 Feb 2025 13:47:26 -0500 Subject: [PATCH 4/4] address comments --- crates/astria-core/src/connect/types.rs | 2 +- crates/astria-core/src/primitive/v1/i128.rs | 5 +++++ crates/astria-sequencer/src/app/tests_app/mod.rs | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/crates/astria-core/src/connect/types.rs b/crates/astria-core/src/connect/types.rs index 894b662684..7544524ffd 100644 --- a/crates/astria-core/src/connect/types.rs +++ b/crates/astria-core/src/connect/types.rs @@ -452,7 +452,7 @@ pub mod v2 { #[test] fn can_parse_negative_price() { let price = "-1".parse::().unwrap(); - assert_eq!(price.get(), -1); + assert_eq!(price, Price::new(-1)); } } } diff --git a/crates/astria-core/src/primitive/v1/i128.rs b/crates/astria-core/src/primitive/v1/i128.rs index dbc83d99a4..26c8b7e555 100644 --- a/crates/astria-core/src/primitive/v1/i128.rs +++ b/crates/astria-core/src/primitive/v1/i128.rs @@ -40,6 +40,7 @@ mod tests { let actual: i128 = pb.into(); assert_eq!(expected, actual); } + #[test] fn i128_roundtrips_work() { i128_roundtrip_check(0i128); @@ -49,5 +50,9 @@ mod tests { i128_roundtrip_check(1i128 << 127); i128_roundtrip_check((1i128 << 127) + (1i128 << 63)); i128_roundtrip_check(i128::MAX); + i128_roundtrip_check(i128::MIN); + i128_roundtrip_check(-1i128); + i128_roundtrip_check(-i128::from(u64::MAX)); + i128_roundtrip_check(-i128::from(u64::MAX) - 1i128); } } diff --git a/crates/astria-sequencer/src/app/tests_app/mod.rs b/crates/astria-sequencer/src/app/tests_app/mod.rs index fcbe5dae5f..a2b7c3ba53 100644 --- a/crates/astria-sequencer/src/app/tests_app/mod.rs +++ b/crates/astria-sequencer/src/app/tests_app/mod.rs @@ -982,7 +982,7 @@ async fn app_oracle_price_update_events_in_finalize_block() { app.commit(storage.clone()).await; let mut prices = std::collections::BTreeMap::new(); - let price = Price::new(10000u128); + let price = Price::new(10000i128); let price_bytes = price.get().to_be_bytes().to_vec(); let id_to_currency_pair = indexmap::indexmap! { id => CurrencyPairInfo{