diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1.rs index 8d03de6cd..a4019d8dc 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1.rs @@ -223,6 +223,21 @@ impl ::prost::Name for IbcSudoChangeFeeComponents { ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) } } +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValidatorUpdateV2FeeComponents { + #[prost(message, optional, tag = "1")] + pub base: ::core::option::Option, + #[prost(message, optional, tag = "2")] + pub multiplier: ::core::option::Option, +} +impl ::prost::Name for ValidatorUpdateV2FeeComponents { + const NAME: &'static str = "ValidatorUpdateV2FeeComponents"; + const PACKAGE: &'static str = "astria.protocol.fees.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.fees.v1.{}", Self::NAME) + } +} /// Response to a transaction fee ABCI query. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs index 90c71b03f..98c7c7409 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1.serde.rs @@ -1729,3 +1729,111 @@ impl<'de> serde::Deserialize<'de> for ValidatorUpdateFeeComponents { deserializer.deserialize_struct("astria.protocol.fees.v1.ValidatorUpdateFeeComponents", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ValidatorUpdateV2FeeComponents { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.base.is_some() { + len += 1; + } + if self.multiplier.is_some() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.fees.v1.ValidatorUpdateV2FeeComponents", len)?; + if let Some(v) = self.base.as_ref() { + struct_ser.serialize_field("base", v)?; + } + if let Some(v) = self.multiplier.as_ref() { + struct_ser.serialize_field("multiplier", v)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ValidatorUpdateV2FeeComponents { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "base", + "multiplier", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Base, + Multiplier, + } + 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 { + "base" => Ok(GeneratedField::Base), + "multiplier" => Ok(GeneratedField::Multiplier), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ValidatorUpdateV2FeeComponents; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.fees.v1.ValidatorUpdateV2FeeComponents") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut base__ = None; + let mut multiplier__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Base => { + if base__.is_some() { + return Err(serde::de::Error::duplicate_field("base")); + } + base__ = map_.next_value()?; + } + GeneratedField::Multiplier => { + if multiplier__.is_some() { + return Err(serde::de::Error::duplicate_field("multiplier")); + } + multiplier__ = map_.next_value()?; + } + } + } + Ok(ValidatorUpdateV2FeeComponents { + base: base__, + multiplier: multiplier__, + }) + } + } + deserializer.deserialize_struct("astria.protocol.fees.v1.ValidatorUpdateV2FeeComponents", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs index 69ba71896..e98961bbd 100644 --- a/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs +++ b/crates/astria-core/src/generated/astria.protocol.fees.v1alpha1.rs @@ -133,6 +133,7 @@ impl ::prost::Name for IbcRelayFeeComponents { ::prost::alloc::format!("astria.protocol.fees.v1alpha1.{}", Self::NAME) } } +/// Deprecated, use `ValidatorUpdateV2FeeComponents` instead. #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] pub struct ValidatorUpdateFeeComponents { diff --git a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs index 6bd2ba2fd..dff78fd19 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.rs @@ -3,7 +3,7 @@ pub struct Action { #[prost( oneof = "action::Value", - tags = "1, 2, 11, 12, 13, 14, 21, 22, 50, 51, 52, 53, 55, 56" + tags = "1, 2, 11, 12, 13, 14, 21, 22, 50, 52, 53, 54, 55, 56, 51" )] pub value: ::core::option::Option, } @@ -34,18 +34,21 @@ pub mod action { /// POA sudo actions are defined on 50-60 #[prost(message, tag = "50")] SudoAddressChange(super::SudoAddressChange), - #[prost(message, tag = "51")] - ValidatorUpdate( - crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate, - ), #[prost(message, tag = "52")] IbcRelayerChange(super::IbcRelayerChange), #[prost(message, tag = "53")] FeeAssetChange(super::FeeAssetChange), + #[prost(message, tag = "54")] + ValidatorUpdateV2(super::ValidatorUpdateV2), #[prost(message, tag = "55")] FeeChange(super::FeeChange), #[prost(message, tag = "56")] IbcSudoChange(super::IbcSudoChange), + /// Prefer `ValidatorUpdateV2` instead. + #[prost(message, tag = "51")] + ValidatorUpdate( + crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate, + ), } } impl ::prost::Name for Action { @@ -398,11 +401,30 @@ impl ::prost::Name for BridgeSudoChange { } #[allow(clippy::derive_partial_eq_without_eq)] #[derive(Clone, PartialEq, ::prost::Message)] +pub struct ValidatorUpdateV2 { + #[prost(message, optional, tag = "1")] + pub pub_key: ::core::option::Option< + crate::generated::astria_vendored::tendermint::crypto::PublicKey, + >, + #[prost(int64, tag = "2")] + pub power: i64, + #[prost(string, tag = "3")] + pub name: ::prost::alloc::string::String, +} +impl ::prost::Name for ValidatorUpdateV2 { + const NAME: &'static str = "ValidatorUpdateV2"; + const PACKAGE: &'static str = "astria.protocol.transaction.v1"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("astria.protocol.transaction.v1.{}", Self::NAME) + } +} +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] pub struct FeeChange { /// the new fee components values #[prost( oneof = "fee_change::FeeComponents", - tags = "1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14" + tags = "1, 2, 3, 4, 5, 7, 6, 8, 9, 10, 11, 12, 13, 14, 15" )] pub fee_components: ::core::option::Option, } @@ -442,6 +464,8 @@ pub mod fee_change { Transfer(super::super::super::fees::v1::TransferFeeComponents), #[prost(message, tag = "14")] ValidatorUpdate(super::super::super::fees::v1::ValidatorUpdateFeeComponents), + #[prost(message, tag = "15")] + ValidatorUpdateV2(super::super::super::fees::v1::ValidatorUpdateV2FeeComponents), } } impl ::prost::Name for FeeChange { diff --git a/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs b/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs index 4c77666d1..17611bcdf 100644 --- a/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs +++ b/crates/astria-core/src/generated/astria.protocol.transaction.v1.serde.rs @@ -39,21 +39,24 @@ impl serde::Serialize for Action { action::Value::SudoAddressChange(v) => { struct_ser.serialize_field("sudoAddressChange", v)?; } - action::Value::ValidatorUpdate(v) => { - struct_ser.serialize_field("validatorUpdate", v)?; - } action::Value::IbcRelayerChange(v) => { struct_ser.serialize_field("ibcRelayerChange", v)?; } action::Value::FeeAssetChange(v) => { struct_ser.serialize_field("feeAssetChange", v)?; } + action::Value::ValidatorUpdateV2(v) => { + struct_ser.serialize_field("validatorUpdateV2", v)?; + } action::Value::FeeChange(v) => { struct_ser.serialize_field("feeChange", v)?; } action::Value::IbcSudoChange(v) => { struct_ser.serialize_field("ibcSudoChange", v)?; } + action::Value::ValidatorUpdate(v) => { + struct_ser.serialize_field("validatorUpdate", v)?; + } } } struct_ser.end() @@ -82,16 +85,18 @@ impl<'de> serde::Deserialize<'de> for Action { "ics20Withdrawal", "sudo_address_change", "sudoAddressChange", - "validator_update", - "validatorUpdate", "ibc_relayer_change", "ibcRelayerChange", "fee_asset_change", "feeAssetChange", + "validator_update_v2", + "validatorUpdateV2", "fee_change", "feeChange", "ibc_sudo_change", "ibcSudoChange", + "validator_update", + "validatorUpdate", ]; #[allow(clippy::enum_variant_names)] @@ -105,11 +110,12 @@ impl<'de> serde::Deserialize<'de> for Action { Ibc, Ics20Withdrawal, SudoAddressChange, - ValidatorUpdate, IbcRelayerChange, FeeAssetChange, + ValidatorUpdateV2, FeeChange, IbcSudoChange, + ValidatorUpdate, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -140,11 +146,12 @@ impl<'de> serde::Deserialize<'de> for Action { "ibc" => Ok(GeneratedField::Ibc), "ics20Withdrawal" | "ics20_withdrawal" => Ok(GeneratedField::Ics20Withdrawal), "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), - "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), "ibcRelayerChange" | "ibc_relayer_change" => Ok(GeneratedField::IbcRelayerChange), "feeAssetChange" | "fee_asset_change" => Ok(GeneratedField::FeeAssetChange), + "validatorUpdateV2" | "validator_update_v2" => Ok(GeneratedField::ValidatorUpdateV2), "feeChange" | "fee_change" => Ok(GeneratedField::FeeChange), "ibcSudoChange" | "ibc_sudo_change" => Ok(GeneratedField::IbcSudoChange), + "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -228,13 +235,6 @@ impl<'de> serde::Deserialize<'de> for Action { return Err(serde::de::Error::duplicate_field("sudoAddressChange")); } value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::SudoAddressChange) -; - } - GeneratedField::ValidatorUpdate => { - if value__.is_some() { - return Err(serde::de::Error::duplicate_field("validatorUpdate")); - } - value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::ValidatorUpdate) ; } GeneratedField::IbcRelayerChange => { @@ -249,6 +249,13 @@ impl<'de> serde::Deserialize<'de> for Action { return Err(serde::de::Error::duplicate_field("feeAssetChange")); } value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::FeeAssetChange) +; + } + GeneratedField::ValidatorUpdateV2 => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorUpdateV2")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::ValidatorUpdateV2) ; } GeneratedField::FeeChange => { @@ -263,6 +270,13 @@ impl<'de> serde::Deserialize<'de> for Action { return Err(serde::de::Error::duplicate_field("ibcSudoChange")); } value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::IbcSudoChange) +; + } + GeneratedField::ValidatorUpdate => { + if value__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorUpdate")); + } + value__ = map_.next_value::<::std::option::Option<_>>()?.map(action::Value::ValidatorUpdate) ; } } @@ -945,6 +959,9 @@ impl serde::Serialize for FeeChange { fee_change::FeeComponents::ValidatorUpdate(v) => { struct_ser.serialize_field("validatorUpdate", v)?; } + fee_change::FeeComponents::ValidatorUpdateV2(v) => { + struct_ser.serialize_field("validatorUpdateV2", v)?; + } } } struct_ser.end() @@ -984,6 +1001,8 @@ impl<'de> serde::Deserialize<'de> for FeeChange { "transfer", "validator_update", "validatorUpdate", + "validator_update_v2", + "validatorUpdateV2", ]; #[allow(clippy::enum_variant_names)] @@ -1002,6 +1021,7 @@ impl<'de> serde::Deserialize<'de> for FeeChange { SudoAddressChange, Transfer, ValidatorUpdate, + ValidatorUpdateV2, } impl<'de> serde::Deserialize<'de> for GeneratedField { fn deserialize(deserializer: D) -> std::result::Result @@ -1037,6 +1057,7 @@ impl<'de> serde::Deserialize<'de> for FeeChange { "sudoAddressChange" | "sudo_address_change" => Ok(GeneratedField::SudoAddressChange), "transfer" => Ok(GeneratedField::Transfer), "validatorUpdate" | "validator_update" => Ok(GeneratedField::ValidatorUpdate), + "validatorUpdateV2" | "validator_update_v2" => Ok(GeneratedField::ValidatorUpdateV2), _ => Err(serde::de::Error::unknown_field(value, FIELDS)), } } @@ -1155,6 +1176,13 @@ impl<'de> serde::Deserialize<'de> for FeeChange { return Err(serde::de::Error::duplicate_field("validatorUpdate")); } fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::ValidatorUpdate) +; + } + GeneratedField::ValidatorUpdateV2 => { + if fee_components__.is_some() { + return Err(serde::de::Error::duplicate_field("validatorUpdateV2")); + } + fee_components__ = map_.next_value::<::std::option::Option<_>>()?.map(fee_change::FeeComponents::ValidatorUpdateV2) ; } } @@ -2635,3 +2663,132 @@ impl<'de> serde::Deserialize<'de> for Transfer { deserializer.deserialize_struct("astria.protocol.transaction.v1.Transfer", FIELDS, GeneratedVisitor) } } +impl serde::Serialize for ValidatorUpdateV2 { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.pub_key.is_some() { + len += 1; + } + if self.power != 0 { + len += 1; + } + if !self.name.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("astria.protocol.transaction.v1.ValidatorUpdateV2", len)?; + if let Some(v) = self.pub_key.as_ref() { + struct_ser.serialize_field("pubKey", v)?; + } + if self.power != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("power", ToString::to_string(&self.power).as_str())?; + } + if !self.name.is_empty() { + struct_ser.serialize_field("name", &self.name)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for ValidatorUpdateV2 { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "pub_key", + "pubKey", + "power", + "name", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + PubKey, + Power, + Name, + } + 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 { + "pubKey" | "pub_key" => Ok(GeneratedField::PubKey), + "power" => Ok(GeneratedField::Power), + "name" => Ok(GeneratedField::Name), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = ValidatorUpdateV2; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct astria.protocol.transaction.v1.ValidatorUpdateV2") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut pub_key__ = None; + let mut power__ = None; + let mut name__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::PubKey => { + if pub_key__.is_some() { + return Err(serde::de::Error::duplicate_field("pubKey")); + } + pub_key__ = map_.next_value()?; + } + GeneratedField::Power => { + if power__.is_some() { + return Err(serde::de::Error::duplicate_field("power")); + } + power__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Name => { + if name__.is_some() { + return Err(serde::de::Error::duplicate_field("name")); + } + name__ = Some(map_.next_value()?); + } + } + } + Ok(ValidatorUpdateV2 { + pub_key: pub_key__, + power: power__.unwrap_or_default(), + name: name__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("astria.protocol.transaction.v1.ValidatorUpdateV2", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-core/src/protocol/fees/v1.rs b/crates/astria-core/src/protocol/fees/v1.rs index 400a63651..d89a7f09b 100644 --- a/crates/astria-core/src/protocol/fees/v1.rs +++ b/crates/astria-core/src/protocol/fees/v1.rs @@ -81,6 +81,7 @@ impl_protobuf_for_fee_components!( FeeChangeFeeComponents => raw::FeeChangeFeeComponents, SudoAddressChangeFeeComponents => raw::SudoAddressChangeFeeComponents, IbcSudoChangeFeeComponents => raw::IbcSudoChangeFeeComponents, + ValidatorUpdateV2FeeComponents => raw::ValidatorUpdateV2FeeComponents, ); #[derive(Clone, Copy, Debug, PartialEq)] @@ -167,6 +168,12 @@ pub struct IbcSudoChangeFeeComponents { pub multiplier: u128, } +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct ValidatorUpdateV2FeeComponents { + pub base: u128, + pub multiplier: u128, +} + #[derive(Debug, Clone)] pub struct TransactionFeeResponse { pub height: u64, diff --git a/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs b/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs index c14517680..3c7265086 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/group/mod.rs @@ -24,6 +24,7 @@ use super::{ SudoAddressChange, Transfer, ValidatorUpdate, + ValidatorUpdateV2, }; trait BelongsToGroup { @@ -55,6 +56,7 @@ impl_belong_to_group!( (FeeAssetChange, Group::BundleableSudo), (IbcRelay, Group::BundleableGeneral), (IbcSudoChange, Group::UnbundleableSudo), + (ValidatorUpdateV2, Group::BundleableGeneral), ); impl Action { @@ -74,6 +76,7 @@ impl Action { Action::FeeAssetChange(_) => FeeAssetChange::GROUP, Action::Ibc(_) => IbcRelay::GROUP, Action::IbcSudoChange(_) => IbcSudoChange::GROUP, + Action::ValidatorUpdateV2(_) => ValidatorUpdateV2::GROUP, } } } diff --git a/crates/astria-core/src/protocol/transaction/v1/action/group/tests.rs b/crates/astria-core/src/protocol/transaction/v1/action/group/tests.rs index 6eb546ab9..c835c93c3 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/group/tests.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/group/tests.rs @@ -28,6 +28,7 @@ use crate::{ Transfer, TransferFeeComponents, ValidatorUpdate, + ValidatorUpdateV2, }, }; const ASTRIA_ADDRESS_PREFIX: &str = "astria"; @@ -73,6 +74,11 @@ fn try_from_list_of_actions_bundleable_general() { power: 100, verification_key: VerificationKey::try_from([0; 32]).unwrap(), }), + Action::ValidatorUpdateV2(ValidatorUpdateV2 { + power: 100, + verification_key: VerificationKey::try_from([0; 32]).unwrap(), + name: "test_validator".to_string(), + }), Action::Ics20Withdrawal(Ics20Withdrawal { denom: asset.clone(), destination_chain_address: String::new(), diff --git a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs index c4f8320f9..f74f17787 100644 --- a/crates/astria-core/src/protocol/transaction/v1/action/mod.rs +++ b/crates/astria-core/src/protocol/transaction/v1/action/mod.rs @@ -37,12 +37,15 @@ use crate::{ SudoAddressChangeFeeComponents, TransferFeeComponents, ValidatorUpdateFeeComponents, + ValidatorUpdateV2FeeComponents, }, Protobuf, }; pub mod group; +const MAX_VALIDATOR_NAME_LENGTH: usize = 32; + #[derive(Clone, Debug)] #[cfg_attr( feature = "serde", @@ -64,6 +67,7 @@ pub enum Action { BridgeUnlock(BridgeUnlock), BridgeSudoChange(BridgeSudoChange), FeeChange(FeeChange), + ValidatorUpdateV2(ValidatorUpdateV2), } impl Protobuf for Action { @@ -88,6 +92,7 @@ impl Protobuf for Action { Action::BridgeUnlock(act) => Value::BridgeUnlock(act.to_raw()), Action::BridgeSudoChange(act) => Value::BridgeSudoChange(act.to_raw()), Action::FeeChange(act) => Value::FeeChange(act.to_raw()), + Action::ValidatorUpdateV2(act) => Value::ValidatorUpdateV2(act.to_raw()), }; raw::Action { value: Some(kind), @@ -161,6 +166,9 @@ impl Protobuf for Action { Value::FeeChange(act) => { Self::FeeChange(FeeChange::try_from_raw_ref(&act).map_err(Error::fee_change)?) } + Value::ValidatorUpdateV2(act) => Self::ValidatorUpdateV2( + ValidatorUpdateV2::try_from_raw(act).map_err(Error::validator_update)?, + ), }; Ok(action) } @@ -308,6 +316,7 @@ impl ActionName for Action { Action::BridgeUnlock(_) => "BridgeUnlock", Action::BridgeSudoChange(_) => "BridgeSudoChange", Action::FeeChange(_) => "FeeChange", + Action::ValidatorUpdateV2(_) => "ValidatorUpdateV2", } } } @@ -610,6 +619,12 @@ impl ValidatorUpdateError { source, }) } + + fn name_too_long(length: usize) -> Self { + Self(ValidatorUpdateErrorKind::NameTooLong { + length, + }) + } } #[derive(Debug, thiserror::Error)] @@ -622,8 +637,14 @@ enum ValidatorUpdateErrorKind { Secp256k1NotSupported, #[error("bytes stored in the .pub_key field could not be read as an ed25519 verification key")] VerificationKey { source: crate::crypto::Error }, + #[error( + "validator name was {length} characters long, but must be {MAX_VALIDATOR_NAME_LENGTH} at \ + most" + )] + NameTooLong { length: usize }, } +/// **NOTE**: This action is deprecated. Use [`ValidatorUpdateV2`] instead. #[derive(Clone, Debug, PartialEq, Eq)] #[cfg_attr( feature = "serde", @@ -738,6 +759,114 @@ impl TryFrom Result { + use crate::generated::astria_vendored::tendermint::crypto::{ + public_key, + PublicKey, + }; + let Self::Raw { + pub_key, + power, + name, + } = value; + if name.len() > MAX_VALIDATOR_NAME_LENGTH { + return Err(Self::Error::name_too_long(name.len())); + } + let power = power + .try_into() + .map_err(|_| Self::Error::negative_power(power))?; + let verification_key = match pub_key { + None + | Some(PublicKey { + sum: None, + }) => Err(Self::Error::public_key_not_set()), + Some(PublicKey { + sum: Some(public_key::Sum::Secp256k1(..)), + }) => Err(Self::Error::secp256k1_not_supported()), + + Some(PublicKey { + sum: Some(public_key::Sum::Ed25519(bytes)), + }) => crate::crypto::VerificationKey::try_from(&*bytes) + .map_err(Self::Error::verification_key), + }?; + Ok(Self { + power, + verification_key, + name, + }) + } + + /// Create a validator update by verifying a reference to raw protobuf-decoded + /// [`crate::generated::protocol::transaction::v1alpha1::ValidatorUpdateV2`]. + /// + /// # Errors + /// Returns an error if the `.power` field is negative, if `.pub_key` + /// is not set, or if `.pub_key` contains a non-ed25519 variant, or + /// if the ed25519 has invalid bytes (that is, bytes from which an + /// ed25519 public key cannot be constructed). + fn try_from_raw_ref(raw: &Self::Raw) -> Result { + Self::try_from_raw(raw.clone()) + } + + #[must_use] + fn to_raw(&self) -> Self::Raw { + use crate::generated::astria_vendored::tendermint::crypto::{ + public_key, + PublicKey, + }; + let Self { + power, + verification_key, + name, + } = self; + + Self::Raw { + power: (*power).into(), + pub_key: Some(PublicKey { + sum: Some(public_key::Sum::Ed25519( + verification_key.to_bytes().to_vec(), + )), + }), + name: name.clone(), + } + } +} + +impl From for crate::generated::protocol::transaction::v1::ValidatorUpdateV2 { + fn from(value: ValidatorUpdateV2) -> Self { + value.into_raw() + } +} + +impl TryFrom for ValidatorUpdateV2 { + type Error = ValidatorUpdateError; + + fn try_from( + value: crate::generated::protocol::transaction::v1::ValidatorUpdateV2, + ) -> Result { + Self::try_from_raw(value) + } +} + #[derive(Clone, Debug)] pub struct SudoAddressChange { pub new_address: Address, @@ -1960,6 +2089,7 @@ pub enum FeeChange { IbcRelayerChange(IbcRelayerChangeFeeComponents), SudoAddressChange(SudoAddressChangeFeeComponents), IbcSudoChange(IbcSudoChangeFeeComponents), + ValidatorUpdateV2(ValidatorUpdateV2FeeComponents), } impl Protobuf for FeeChange { @@ -2012,6 +2142,9 @@ impl Protobuf for FeeChange { Self::IbcSudoChange(fee_change) => { raw::fee_change::FeeComponents::IbcSudoChange(fee_change.to_raw()) } + Self::ValidatorUpdateV2(fee_change) => { + raw::fee_change::FeeComponents::ValidatorUpdateV2(fee_change.to_raw()) + } }), } } @@ -2072,6 +2205,11 @@ impl Protobuf for FeeChange { Some(raw::fee_change::FeeComponents::IbcSudoChange(fee_change)) => { Self::IbcSudoChange(IbcSudoChangeFeeComponents::try_from_raw_ref(fee_change)?) } + Some(raw::fee_change::FeeComponents::ValidatorUpdateV2(fee_change)) => { + Self::ValidatorUpdateV2(ValidatorUpdateV2FeeComponents::try_from_raw_ref( + fee_change, + )?) + } None => return Err(FeeChangeError::field_unset("fee_components")), }) } diff --git a/crates/astria-sequencer/CHANGELOG.md b/crates/astria-sequencer/CHANGELOG.md index 9e18802d9..a333203a1 100644 --- a/crates/astria-sequencer/CHANGELOG.md +++ b/crates/astria-sequencer/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add validator update action with name [#1679](https://github.com/astriaorg/astria/pull/1679) + ## [1.0.0] - 2024-10-25 ### Changed diff --git a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap index 200653097..cf0be0abb 100644 --- a/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap +++ b/crates/astria-sequencer/src/app/snapshots/astria_sequencer__app__tests_breaking_changes__app_execute_transaction_with_every_action_snapshot.snap @@ -3,36 +3,36 @@ source: crates/astria-sequencer/src/app/tests_breaking_changes.rs expression: app.app_hash.as_bytes() --- [ - 195, - 205, - 225, - 173, - 118, - 201, - 149, - 122, - 173, - 117, - 237, - 146, - 148, - 114, - 152, - 59, - 68, - 60, - 33, - 65, - 41, - 154, - 249, - 85, - 76, - 183, + 5, + 209, 32, - 108, - 175, - 88, - 197, - 63 + 87, + 164, + 134, + 213, + 181, + 114, + 5, + 84, + 18, + 64, + 190, + 208, + 206, + 63, + 169, + 11, + 224, + 123, + 234, + 194, + 158, + 27, + 111, + 75, + 172, + 82, + 226, + 4, + 223 ] diff --git a/crates/astria-sequencer/src/app/tests_breaking_changes.rs b/crates/astria-sequencer/src/app/tests_breaking_changes.rs index 031538a05..769dd3483 100644 --- a/crates/astria-sequencer/src/app/tests_breaking_changes.rs +++ b/crates/astria-sequencer/src/app/tests_breaking_changes.rs @@ -17,6 +17,7 @@ use std::{ use astria_core::{ primitive::v1::RollupId, protocol::{ + fees::v1::ValidatorUpdateV2FeeComponents, genesis::v1::Account, transaction::v1::{ action::{ @@ -28,6 +29,7 @@ use astria_core::{ RollupDataSubmission, Transfer, ValidatorUpdate, + ValidatorUpdateV2, }, Action, TransactionBody, @@ -69,9 +71,11 @@ use crate::{ astria_address, astria_address_from_hex_string, nria, + verification_key, ASTRIA_PREFIX, }, bridge::StateWriteExt as _, + fees::StateWriteExt as _, proposal::commitment::generate_rollup_datas_commitment, }; @@ -198,10 +202,25 @@ async fn app_execute_transaction_with_every_action_snapshot() { .unwrap(); let (mut app, storage) = initialize_app_with_storage(Some(genesis_state), vec![]).await; + let mut state_tx = StateDelta::new(app.state.clone()); + state_tx + .put_validator_update_v2_fees(ValidatorUpdateV2FeeComponents { + base: 0, + multiplier: 0, + }) + .unwrap(); + app.apply(state_tx); + // setup for ValidatorUpdate action let update = ValidatorUpdate { power: 100, - verification_key: crate::benchmark_and_test_utils::verification_key(1), + verification_key: verification_key(1), + }; + + let update_with_name = ValidatorUpdateV2 { + name: "test_validator".to_string(), + power: 100, + verification_key: verification_key(2), }; let rollup_id = RollupId::from_unhashed_bytes(b"testchainid"); @@ -222,6 +241,7 @@ async fn app_execute_transaction_with_every_action_snapshot() { } .into(), Action::ValidatorUpdate(update.clone()), + Action::ValidatorUpdateV2(update_with_name.clone()), ]) .chain_id("test") .try_build() diff --git a/crates/astria-sequencer/src/app/tests_execute_transaction.rs b/crates/astria-sequencer/src/app/tests_execute_transaction.rs index 7f4d99d65..8823dcf00 100644 --- a/crates/astria-sequencer/src/app/tests_execute_transaction.rs +++ b/crates/astria-sequencer/src/app/tests_execute_transaction.rs @@ -10,6 +10,7 @@ use astria_core::{ fees::v1::{ InitBridgeAccountFeeComponents, RollupDataSubmissionFeeComponents, + ValidatorUpdateV2FeeComponents, }, genesis::v1::GenesisAppState, transaction::v1::{ @@ -22,6 +23,7 @@ use astria_core::{ SudoAddressChange, Transfer, ValidatorUpdate, + ValidatorUpdateV2, }, Action, TransactionBody, @@ -1278,3 +1280,59 @@ async fn transaction_execution_records_fee_event() { assert_eq!(event.attributes[2].key, "feeAmount"); assert_eq!(event.attributes[3].key, "positionInTransaction"); } + +#[tokio::test] +async fn app_execute_transaction_validator_update_v2() { + let alice = get_alice_signing_key(); + let alice_address = astria_address(&alice.address_bytes()); + + let mut app = initialize_app(Some(genesis_state()), vec![]).await; + let verification_key = crate::benchmark_and_test_utils::verification_key(1); + + let validator_update_v2_fees = ValidatorUpdateV2FeeComponents { + base: 0, + multiplier: 0, + }; + + let mut state_tx = StateDelta::new(app.state.clone()); + state_tx + .put_validator_update_v2_fees(validator_update_v2_fees) + .unwrap(); + app.apply(state_tx); + + let inner_update = ValidatorUpdate { + power: 100, + verification_key: verification_key.clone(), + }; + let update_with_name = ValidatorUpdateV2 { + power: inner_update.power, + verification_key: inner_update.verification_key.clone(), + name: "test_validator".to_string(), + }; + + let tx = TransactionBody::builder() + .actions(vec![Action::ValidatorUpdateV2(update_with_name.clone())]) + .chain_id("test") + .try_build() + .unwrap(); + + let signed_tx = Arc::new(tx.sign(&alice)); + app.execute_transaction(signed_tx).await.unwrap(); + assert_eq!( + app.state.get_account_nonce(&alice_address).await.unwrap(), + 1 + ); + + let validator_updates = app.state.get_validator_updates().await.unwrap(); + assert_eq!(validator_updates.len(), 1); + assert_eq!( + validator_updates.get(&verification_key), + Some(&inner_update) + ); + let validator_name = app + .state + .get_validator_name(verification_key.address_bytes()) + .await + .unwrap(); + assert_eq!(validator_name, Some(update_with_name.name)); +} diff --git a/crates/astria-sequencer/src/authority/action.rs b/crates/astria-sequencer/src/authority/action.rs index 78d69733e..f2d58ff37 100644 --- a/crates/astria-sequencer/src/authority/action.rs +++ b/crates/astria-sequencer/src/authority/action.rs @@ -2,6 +2,7 @@ use astria_core::protocol::transaction::v1::action::{ IbcSudoChange, SudoAddressChange, ValidatorUpdate, + ValidatorUpdateV2, }; use astria_eyre::eyre::{ bail, @@ -63,7 +64,7 @@ impl ActionHandler for ValidatorUpdate { .get_validator_updates() .await .wrap_err("failed getting validator updates from state")?; - validator_updates.push_update(self.clone()); + validator_updates.insert(self.clone()); state .put_validator_updates(validator_updates) .wrap_err("failed to put validator updates in state")?; @@ -128,3 +129,32 @@ impl ActionHandler for IbcSudoChange { Ok(()) } } + +#[async_trait::async_trait] +impl ActionHandler for ValidatorUpdateV2 { + async fn check_stateless(&self) -> Result<()> { + Ok(()) + } + + async fn check_and_execute(&self, mut state: S) -> Result<()> { + let inner_update = ValidatorUpdate { + verification_key: self.verification_key.clone(), + power: self.power, + }; + inner_update + .check_and_execute(&mut state) + .await + .wrap_err("failed to execute inner validator update")?; + match self.power { + 0 => { + state.remove_validator_name(self.verification_key.address_bytes()); + } + _ => { + state + .put_validator_name(self.verification_key.address_bytes(), self.name.clone()) + .wrap_err("failed to put validator name in state")?; + } + } + Ok(()) + } +} diff --git a/crates/astria-sequencer/src/authority/mod.rs b/crates/astria-sequencer/src/authority/mod.rs index 22d8a0078..2b9805f13 100644 --- a/crates/astria-sequencer/src/authority/mod.rs +++ b/crates/astria-sequencer/src/authority/mod.rs @@ -1,5 +1,6 @@ mod action; pub(crate) mod component; +pub(crate) mod query; mod state_ext; pub(crate) mod storage; @@ -51,7 +52,7 @@ impl ValidatorSet { self.0.get(address.address_bytes()) } - pub(super) fn push_update(&mut self, update: ValidatorUpdate) { + pub(super) fn insert(&mut self, update: ValidatorUpdate) { self.0 .insert(*update.verification_key.address_bytes(), update); } diff --git a/crates/astria-sequencer/src/authority/query.rs b/crates/astria-sequencer/src/authority/query.rs new file mode 100644 index 000000000..19424a1c3 --- /dev/null +++ b/crates/astria-sequencer/src/authority/query.rs @@ -0,0 +1,277 @@ +use astria_core::{ + primitive::v1::Address, + protocol::abci::AbciErrorCode, +}; +use astria_eyre::eyre::Context as _; +use cnidarium::Storage; +use tendermint::abci::{ + request, + response, + Code, +}; + +use crate::{ + accounts::AddressBytes, + authority::state_ext::StateReadExt as _, +}; + +pub(crate) async fn validator_name_request( + storage: Storage, + request: request::Query, + params: Vec<(String, String)>, +) -> response::Query { + let address = match preprocess_request(¶ms) { + Ok(address) => address, + Err(err) => return err, + }; + + let snapshot = storage.latest_snapshot(); + + match snapshot.get_validator_name(address.as_bytes()).await { + Ok(Some(name)) => { + return response::Query { + code: Code::Ok, + key: request.path.clone().into_bytes().into(), + value: name.clone().into_bytes().into(), + ..response::Query::default() + }; + } + Ok(None) => {} + Err(err) => { + return error_query_response( + Some(err), + AbciErrorCode::INTERNAL_ERROR, + "failed to get validator names", + ); + } + }; + + let validator_set = match snapshot.get_validator_set().await { + Ok(validator_set) => validator_set, + Err(err) => { + return error_query_response( + Some(err), + AbciErrorCode::INTERNAL_ERROR, + "failed to get validator set", + ); + } + }; + + if validator_set.get(address.address_bytes()).is_none() { + error_query_response( + None, + AbciErrorCode::VALUE_NOT_FOUND, + "validator address not found in validator set", + ) + } else { + error_query_response( + None, + AbciErrorCode::VALUE_NOT_FOUND, + "validator address exists but does not have a name", + ) + } +} + +fn preprocess_request(params: &[(String, String)]) -> Result { + let Some(address) = params + .iter() + .find_map(|(k, v)| (k == "address").then_some(v)) + else { + return Err(error_query_response( + None, + AbciErrorCode::INVALID_PARAMETER, + "path did not contain address parameter", + )); + }; + let address = address + .parse() + .wrap_err("failed to parse argument as address") + .map_err(|err| response::Query { + code: Code::Err(AbciErrorCode::INVALID_PARAMETER.value()), + info: AbciErrorCode::INVALID_PARAMETER.info(), + log: format!("address could not be constructed from provided parameter: {err:#}"), + ..response::Query::default() + })?; + Ok(address) +} + +fn error_query_response( + err: Option, + code: AbciErrorCode, + msg: &str, +) -> response::Query { + let log = match err { + Some(err) => format!("{msg}: {err:#}"), + None => msg.into(), + }; + response::Query { + code: Code::Err(code.value()), + info: code.info(), + log, + ..response::Query::default() + } +} + +#[cfg(test)] +mod tests { + use std::{ + collections::BTreeMap, + vec, + }; + + use astria_core::protocol::{ + abci::AbciErrorCode, + transaction::v1::action::{ + ValidatorUpdate, + ValidatorUpdateV2, + }, + }; + use cnidarium::StateDelta; + use tendermint::abci::request; + + use crate::{ + authority::{ + query::validator_name_request, + StateWriteExt, + ValidatorSet, + }, + benchmark_and_test_utils::{ + astria_address, + verification_key, + }, + }; + + #[tokio::test] + async fn validator_name_request_works_as_expected() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let verification_key = verification_key(1); + let key_address_bytes = *verification_key.clone().address_bytes(); + let validator_name = "test".to_string(); + + let update_with_name = ValidatorUpdateV2 { + name: validator_name.clone(), + power: 100, + verification_key, + }; + + state + .put_validator_name(&key_address_bytes, update_with_name.name) + .unwrap(); + storage.commit(state).await.unwrap(); + + let query = request::Query { + data: vec![].into(), + path: "path".to_string(), + height: 0u32.into(), + prove: false, + }; + let params = vec![( + "address".to_string(), + astria_address(&key_address_bytes).to_string(), + )]; + + let rsp = validator_name_request(storage.clone(), query, params).await; + assert!(rsp.code.is_ok(), "code: {:?}, log: {}", rsp.code, rsp.log); + assert_eq!(rsp.key, "path".as_bytes()); + assert_eq!(rsp.value, validator_name); + } + + #[tokio::test] + async fn validator_name_request_fails_if_not_in_validator_set() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let verification_key = verification_key(1); + + let query = request::Query { + data: vec![].into(), + path: "path".to_string(), + height: 0u32.into(), + prove: false, + }; + + // Use a different address than the one submitted to the validator set + let params = vec![( + "address".to_string(), + astria_address(&[0u8; 20]).to_string(), + )]; + + let inner_update = ValidatorUpdate { + power: 100, + verification_key, + }; + + let rsp = validator_name_request(storage.clone(), query.clone(), params.clone()).await; + assert_eq!( + rsp.code.value(), + u32::from(AbciErrorCode::INTERNAL_ERROR.value()), + "{}", + rsp.log + ); + let err_msg = "failed to get validator set: validator set not found"; + assert_eq!(rsp.log, err_msg); + + let inner_validator_map = BTreeMap::new(); + let mut validator_set = ValidatorSet::new(inner_validator_map); + assert_eq!(validator_set.len(), 0); + validator_set.insert(inner_update); + state.put_validator_set(validator_set).unwrap(); + storage.commit(state).await.unwrap(); + + let rsp = validator_name_request(storage.clone(), query, params).await; + assert_eq!( + rsp.code.value(), + u32::from(AbciErrorCode::VALUE_NOT_FOUND.value()), + "{}", + rsp.log + ); + let err_msg = "validator address not found in validator set"; + assert_eq!(rsp.log, err_msg); + } + + #[tokio::test] + async fn validator_name_request_fails_if_validator_has_no_name() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let verification_key = verification_key(1); + let key_address_bytes = *verification_key.clone().address_bytes(); + let inner_update = ValidatorUpdate { + power: 100, + verification_key, + }; + + let inner_validator_map = BTreeMap::new(); + let mut validator_set = ValidatorSet::new(inner_validator_map); + assert_eq!(validator_set.len(), 0); + validator_set.insert(inner_update); + state.put_validator_set(validator_set).unwrap(); + storage.commit(state).await.unwrap(); + + let query = request::Query { + data: vec![].into(), + path: "path".to_string(), + height: 0u32.into(), + prove: false, + }; + + let params = vec![( + "address".to_string(), + astria_address(&key_address_bytes).to_string(), + )]; + let rsp = validator_name_request(storage.clone(), query, params).await; + assert_eq!( + rsp.code.value(), + u32::from(AbciErrorCode::VALUE_NOT_FOUND.value()), + "{}", + rsp.log + ); + let err_msg = "validator address exists but does not have a name"; + assert_eq!(rsp.log, err_msg); + } +} diff --git a/crates/astria-sequencer/src/authority/state_ext.rs b/crates/astria-sequencer/src/authority/state_ext.rs index e55b81fcb..e3a783a15 100644 --- a/crates/astria-sequencer/src/authority/state_ext.rs +++ b/crates/astria-sequencer/src/authority/state_ext.rs @@ -28,6 +28,10 @@ use crate::{ storage::StoredValue, }; +fn validator_name(key: &[u8]) -> String { + format!("{}/{}", keys::VALIDATOR_NAMES_PREFIX, hex::encode(key)) +} + #[async_trait] pub(crate) trait StateReadExt: StateRead { #[instrument(skip_all)] @@ -77,6 +81,24 @@ pub(crate) trait StateReadExt: StateRead { .and_then(|value| storage::ValidatorSet::try_from(value).map(ValidatorSet::from)) .wrap_err("invalid validator update bytes") } + + #[instrument(skip_all)] + async fn get_validator_name(&self, validator: &[u8]) -> Result> { + let Some(bytes) = self + .get_raw(&validator_name(validator)) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw validator name from state")? + else { + return Ok(None); + }; + Some( + StoredValue::deserialize(&bytes) + .and_then(|value| storage::ValidatorName::try_from(value).map(String::from)) + .wrap_err("invalid validator name bytes"), + ) + .transpose() + } } impl StateReadExt for T {} @@ -114,6 +136,20 @@ pub(crate) trait StateWriteExt: StateWrite { fn clear_validator_updates(&mut self) { self.nonverifiable_delete(keys::VALIDATOR_UPDATES.into()); } + + #[instrument(skip_all)] + fn remove_validator_name(&mut self, validator: &[u8]) { + self.delete(validator_name(validator)); + } + + #[instrument(skip_all)] + fn put_validator_name(&mut self, validator: &[u8], name: String) -> Result<()> { + let bytes = StoredValue::from(storage::ValidatorName::from(name.as_str())) + .serialize() + .wrap_err("failed to serialize validator names")?; + self.put_raw(validator_name(validator), bytes); + Ok(()) + } } impl StateWriteExt for T {} @@ -418,4 +454,48 @@ mod tests { "validator set apply updates did not behave as expected" ); } + + #[tokio::test] + async fn put_and_get_validator_names() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let validator_address = &[0; ADDRESS_LEN]; + let validator_name = "ethan_was_here".to_string(); + + // returns empty validator names if none in state + assert_eq!( + state.get_validator_name(validator_address).await.unwrap(), + None + ); + + // can write new + state + .put_validator_name(validator_address, validator_name.clone()) + .expect("writing initial validator should not fail"); + assert_eq!( + state + .get_validator_name(validator_address) + .await + .expect("validator name was written and must exist inside the database"), + Some(validator_name), + "stored validator name was not what was expected" + ); + + // can update + let validator_address_2 = &[1; ADDRESS_LEN]; + let validator_name_2 = "ethan_was_here_again".to_string(); + state + .put_validator_name(validator_address_2, validator_name_2.clone()) + .expect("writing update validator set should not fail"); + assert_eq!( + state + .get_validator_name(validator_address_2) + .await + .expect("validator name was written and must exist inside the database"), + Some(validator_name_2), + "stored validator name was not what was expected" + ); + } } diff --git a/crates/astria-sequencer/src/authority/storage/keys.rs b/crates/astria-sequencer/src/authority/storage/keys.rs index 0120a0b42..364b94221 100644 --- a/crates/astria-sequencer/src/authority/storage/keys.rs +++ b/crates/astria-sequencer/src/authority/storage/keys.rs @@ -1,6 +1,7 @@ pub(in crate::authority) const SUDO: &str = "authority/sudo"; pub(in crate::authority) const VALIDATOR_SET: &str = "authority/validator_set"; pub(in crate::authority) const VALIDATOR_UPDATES: &str = "authority/validator_updates"; +pub(in crate::authority) const VALIDATOR_NAMES_PREFIX: &str = "authority/validator_names"; #[cfg(test)] mod tests { @@ -13,6 +14,7 @@ mod tests { insta::assert_snapshot!(SUDO); insta::assert_snapshot!(VALIDATOR_SET); insta::assert_snapshot!(VALIDATOR_UPDATES); + insta::assert_snapshot!(VALIDATOR_NAMES_PREFIX); } #[test] @@ -20,5 +22,6 @@ mod tests { assert!(SUDO.starts_with(COMPONENT_PREFIX)); assert!(VALIDATOR_SET.starts_with(COMPONENT_PREFIX)); assert!(VALIDATOR_UPDATES.starts_with(COMPONENT_PREFIX)); + assert!(VALIDATOR_NAMES_PREFIX.starts_with(COMPONENT_PREFIX)); } } diff --git a/crates/astria-sequencer/src/authority/storage/mod.rs b/crates/astria-sequencer/src/authority/storage/mod.rs index cb52a74a3..98c364a60 100644 --- a/crates/astria-sequencer/src/authority/storage/mod.rs +++ b/crates/astria-sequencer/src/authority/storage/mod.rs @@ -4,5 +4,6 @@ mod values; pub(crate) use values::Value; pub(super) use values::{ AddressBytes, + ValidatorName, ValidatorSet, }; diff --git a/crates/astria-sequencer/src/authority/storage/snapshots/astria_sequencer__authority__storage__keys__tests__keys_should_not_change-4.snap b/crates/astria-sequencer/src/authority/storage/snapshots/astria_sequencer__authority__storage__keys__tests__keys_should_not_change-4.snap new file mode 100644 index 000000000..946721aa2 --- /dev/null +++ b/crates/astria-sequencer/src/authority/storage/snapshots/astria_sequencer__authority__storage__keys__tests__keys_should_not_change-4.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/authority/storage/keys.rs +expression: VALIDATOR_NAMES +--- +authority/validator_names diff --git a/crates/astria-sequencer/src/authority/storage/values.rs b/crates/astria-sequencer/src/authority/storage/values.rs index 1c2aaaa4a..3f3d330d4 100644 --- a/crates/astria-sequencer/src/authority/storage/values.rs +++ b/crates/astria-sequencer/src/authority/storage/values.rs @@ -31,6 +31,7 @@ pub(crate) struct Value<'a>(ValueImpl<'a>); enum ValueImpl<'a> { AddressBytes(AddressBytes<'a>), ValidatorSet(ValidatorSet<'a>), + ValidatorName(ValidatorName<'a>), } #[derive(BorshSerialize, BorshDeserialize)] @@ -153,3 +154,38 @@ impl<'a> TryFrom> for ValidatorSet<'a> { Ok(validator_set) } } + +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::authority) struct ValidatorName<'a>(Cow<'a, str>); + +impl<'a> From<&'a str> for ValidatorName<'a> { + fn from(value: &'a str) -> Self { + ValidatorName(Cow::Borrowed(value)) + } +} + +impl<'a> From> for String { + fn from(value: ValidatorName<'a>) -> Self { + value.0.into_owned() + } +} + +impl<'a> From> for crate::storage::StoredValue<'a> { + fn from(validator_name: ValidatorName<'a>) -> Self { + crate::storage::StoredValue::Authority(Value(ValueImpl::ValidatorName(validator_name))) + } +} + +impl<'a> TryFrom> for ValidatorName<'a> { + type Error = astria_eyre::eyre::Error; + + fn try_from(value: crate::storage::StoredValue<'a>) -> Result { + let crate::storage::StoredValue::Authority(Value(ValueImpl::ValidatorName( + validator_names, + ))) = value + else { + bail!("authority stored value type mismatch: expected validator name, found {value:?}"); + }; + Ok(validator_names) + } +} diff --git a/crates/astria-sequencer/src/fees/action.rs b/crates/astria-sequencer/src/fees/action.rs index 927eea5ec..9909e1b7e 100644 --- a/crates/astria-sequencer/src/fees/action.rs +++ b/crates/astria-sequencer/src/fees/action.rs @@ -84,6 +84,9 @@ impl ActionHandler for FeeChange { Self::IbcSudoChange(fees) => state .put_ibc_sudo_change_fees(*fees) .wrap_err("failed to put ibc sudo change fees"), + Self::ValidatorUpdateV2(fees) => state + .put_validator_update_v2_fees(*fees) + .wrap_err("failed to put validator update (v2) fees"), } } } diff --git a/crates/astria-sequencer/src/fees/mod.rs b/crates/astria-sequencer/src/fees/mod.rs index 2e3547e7f..264e34c6a 100644 --- a/crates/astria-sequencer/src/fees/mod.rs +++ b/crates/astria-sequencer/src/fees/mod.rs @@ -15,6 +15,7 @@ use astria_core::{ SudoAddressChange, Transfer, ValidatorUpdate, + ValidatorUpdateV2, }, }, Protobuf, @@ -326,6 +327,22 @@ impl FeeHandler for IbcRelay { } } +#[async_trait::async_trait] +impl FeeHandler for ValidatorUpdateV2 { + #[instrument(skip_all, err)] + async fn check_and_pay_fees(&self, state: S) -> eyre::Result<()> { + state + .get_validator_update_v2_fees() + .await + .wrap_err("validator update (v2) fees not found, so this action is disabled")?; + Ok(()) + } + + fn variable_component(&self) -> u128 { + 0 + } +} + #[instrument(skip_all, err(level = Level::WARN))] async fn check_and_pay_fees( act: &T, diff --git a/crates/astria-sequencer/src/fees/query.rs b/crates/astria-sequencer/src/fees/query.rs index 634284366..62821db3f 100644 --- a/crates/astria-sequencer/src/fees/query.rs +++ b/crates/astria-sequencer/src/fees/query.rs @@ -229,6 +229,7 @@ pub(crate) async fn get_fees_for_transaction( let bridge_unlock_fees = OnceCell::new(); let bridge_sudo_change_fees = OnceCell::new(); let validator_update_fees = OnceCell::new(); + let validator_update_v2_fees = OnceCell::new(); let sudo_address_change_fees = OnceCell::new(); let ibc_sudo_change_fees = OnceCell::new(); let ibc_relay_fees = OnceCell::new(); @@ -354,6 +355,15 @@ pub(crate) async fn get_fees_for_transaction( "fees not found for `ValidatorUpdate` action, hence it is disabled", )?; } + Action::ValidatorUpdateV2(_) => { + validator_update_v2_fees + .get_or_try_init(|| async { state.get_validator_update_v2_fees().await }) + .await + .wrap_err("failed to get validator update v2 fees")? + .ok_or_eyre( + "fees not found for `ValidatorUpdateV2` action, hence it is disabled", + )?; + } Action::SudoAddressChange(_) => { sudo_address_change_fees .get_or_try_init(|| async { state.get_sudo_address_change_fees().await }) diff --git a/crates/astria-sequencer/src/fees/state_ext.rs b/crates/astria-sequencer/src/fees/state_ext.rs index 06e954e3a..101d92265 100644 --- a/crates/astria-sequencer/src/fees/state_ext.rs +++ b/crates/astria-sequencer/src/fees/state_ext.rs @@ -25,6 +25,7 @@ use astria_core::{ SudoAddressChangeFeeComponents, TransferFeeComponents, ValidatorUpdateFeeComponents, + ValidatorUpdateV2FeeComponents, }, Protobuf, }; @@ -245,6 +246,7 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("invalid fees bytes") } + /// **NOTE**: Deprecated. Use [`ValidatorUpdateV2`] action instead. #[instrument(skip_all)] async fn get_validator_update_fees(&self) -> Result> { let bytes = self @@ -353,6 +355,24 @@ pub(crate) trait StateReadExt: StateRead { .wrap_err("invalid fees bytes") } + #[instrument(skip_all)] + async fn get_validator_update_v2_fees(&self) -> Result> { + let bytes = self + .get_raw(keys::VALIDATOR_UPDATE_V2) + .await + .map_err(anyhow_to_eyre) + .wrap_err("failed reading raw validator update v2 fee components from state")?; + let Some(bytes) = bytes else { + return Ok(None); + }; + StoredValue::deserialize(&bytes) + .and_then(|value| { + storage::ValidatorUpdateV2FeeComponentsStorage::try_from(value) + .map(|fees| Some(ValidatorUpdateV2FeeComponents::from(fees))) + }) + .wrap_err("invalid fees bytes") + } + #[instrument(skip_all)] async fn is_allowed_fee_asset<'a, TAsset>(&self, asset: &'a TAsset) -> Result where @@ -492,6 +512,7 @@ pub(crate) trait StateWriteExt: StateWrite { Ok(()) } + /// **NOTE**: Deprecated. Use [`ValidatorUpdateV2`] action instead. #[instrument(skip_all)] fn put_validator_update_fees(&mut self, fees: ValidatorUpdateFeeComponents) -> Result<()> { let bytes = StoredValue::from(storage::ValidatorUpdateFeeComponentsStorage::from(fees)) @@ -546,6 +567,15 @@ pub(crate) trait StateWriteExt: StateWrite { Ok(()) } + #[instrument(skip_all)] + fn put_validator_update_v2_fees(&mut self, fees: ValidatorUpdateV2FeeComponents) -> Result<()> { + let bytes = StoredValue::from(storage::ValidatorUpdateV2FeeComponentsStorage::from(fees)) + .serialize() + .wrap_err("failed to serialize fees")?; + self.put_raw(keys::VALIDATOR_UPDATE_V2.to_string(), bytes); + Ok(()) + } + #[instrument(skip_all)] fn delete_allowed_fee_asset<'a, TAsset>(&mut self, asset: &'a TAsset) where @@ -906,6 +936,22 @@ mod tests { assert_eq!(retrieved_fee, Some(fee_components)); } + #[tokio::test] + async fn validator_update_v2_fees_round_trip() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let snapshot = storage.latest_snapshot(); + let mut state = StateDelta::new(snapshot); + + let fee_components = ValidatorUpdateV2FeeComponents { + base: 123, + multiplier: 1, + }; + + state.put_validator_update_v2_fees(fee_components).unwrap(); + let retrieved_fee = state.get_validator_update_v2_fees().await.unwrap().unwrap(); + assert_eq!(retrieved_fee, fee_components); + } + #[tokio::test] async fn is_allowed_fee_asset() { let storage = cnidarium::TempStorage::new().await.unwrap(); diff --git a/crates/astria-sequencer/src/fees/storage/keys.rs b/crates/astria-sequencer/src/fees/storage/keys.rs index 946c9fbef..0cc43261b 100644 --- a/crates/astria-sequencer/src/fees/storage/keys.rs +++ b/crates/astria-sequencer/src/fees/storage/keys.rs @@ -17,6 +17,7 @@ pub(in crate::fees) const BRIDGE_UNLOCK: &str = "fees/bridge_unlock"; pub(in crate::fees) const BRIDGE_SUDO_CHANGE: &str = "fees/bridge_sudo_change"; pub(in crate::fees) const IBC_RELAY: &str = "fees/ibc_relay"; pub(in crate::fees) const VALIDATOR_UPDATE: &str = "fees/validator_update"; +pub(in crate::fees) const VALIDATOR_UPDATE_V2: &str = "fees/validator_update_v2"; pub(in crate::fees) const FEE_ASSET_CHANGE: &str = "fees/fee_asset_change"; pub(in crate::fees) const FEE_CHANGE: &str = "fees/fee_change"; pub(in crate::fees) const IBC_RELAYER_CHANGE: &str = "fees/ibc_relayer_change"; @@ -81,6 +82,7 @@ mod tests { assert_snapshot!("sudo_address_change_fees_key", SUDO_ADDRESS_CHANGE); assert_snapshot!("transer_fees_key", TRANSFER); assert_snapshot!("validator_update_fees_key", VALIDATOR_UPDATE); + assert_snapshot!("validator_update_v2_fees_key", VALIDATOR_UPDATE_V2); assert_snapshot!("allowed_asset_key", allowed_asset(&test_asset())); } @@ -95,6 +97,7 @@ mod tests { assert!(BRIDGE_SUDO_CHANGE.starts_with(COMPONENT_PREFIX)); assert!(IBC_RELAY.starts_with(COMPONENT_PREFIX)); assert!(VALIDATOR_UPDATE.starts_with(COMPONENT_PREFIX)); + assert!(VALIDATOR_UPDATE_V2.starts_with(COMPONENT_PREFIX)); assert!(FEE_ASSET_CHANGE.starts_with(COMPONENT_PREFIX)); assert!(FEE_CHANGE.starts_with(COMPONENT_PREFIX)); assert!(IBC_RELAYER_CHANGE.starts_with(COMPONENT_PREFIX)); diff --git a/crates/astria-sequencer/src/fees/storage/mod.rs b/crates/astria-sequencer/src/fees/storage/mod.rs index 2de95a747..0408dfd51 100644 --- a/crates/astria-sequencer/src/fees/storage/mod.rs +++ b/crates/astria-sequencer/src/fees/storage/mod.rs @@ -17,4 +17,5 @@ pub(super) use values::{ SudoAddressChangeFeeComponentsStorage, TransferFeeComponentsStorage, ValidatorUpdateFeeComponentsStorage, + ValidatorUpdateV2FeeComponentsStorage, }; diff --git a/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_v2_fees_key.snap b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_v2_fees_key.snap new file mode 100644 index 000000000..e1b181b43 --- /dev/null +++ b/crates/astria-sequencer/src/fees/storage/snapshots/astria_sequencer__fees__storage__keys__tests__validator_update_v2_fees_key.snap @@ -0,0 +1,5 @@ +--- +source: crates/astria-sequencer/src/fees/storage/keys.rs +expression: VALIDATOR_UPDATE_V2 +--- +fees/validator_update_v2 diff --git a/crates/astria-sequencer/src/fees/storage/values.rs b/crates/astria-sequencer/src/fees/storage/values.rs index c3dcc9fbe..e9e94b5ec 100644 --- a/crates/astria-sequencer/src/fees/storage/values.rs +++ b/crates/astria-sequencer/src/fees/storage/values.rs @@ -13,6 +13,7 @@ use astria_core::protocol::fees::v1::{ SudoAddressChangeFeeComponents, TransferFeeComponents, ValidatorUpdateFeeComponents, + ValidatorUpdateV2FeeComponents, }; use astria_eyre::eyre::bail; use borsh::{ @@ -43,6 +44,7 @@ enum ValueImpl { IbcRelayerChangeFees(IbcRelayerChangeFeeComponentsStorage), IbcSudoChangeFees(IbcSudoChangeFeeComponentsStorage), SudoAddressChangeFees(SudoAddressChangeFeeComponentsStorage), + ValidatorUpdateV2Fees(ValidatorUpdateV2FeeComponentsStorage), } macro_rules! impl_from_for_fee_component{ @@ -171,6 +173,12 @@ pub(in crate::fees) struct SudoAddressChangeFeeComponentsStorage { pub multiplier: u128, } +#[derive(Debug, BorshSerialize, BorshDeserialize)] +pub(in crate::fees) struct ValidatorUpdateV2FeeComponentsStorage { + pub base: u128, + pub multiplier: u128, +} + impl_from_for_fee_component!( TransferFeeComponents => TransferFeeComponentsStorage, RollupDataSubmissionFeeComponents => RollupDataSubmissionFeeComponentsStorage, @@ -186,6 +194,7 @@ impl_from_for_fee_component!( IbcRelayerChangeFeeComponents => IbcRelayerChangeFeeComponentsStorage, IbcSudoChangeFeeComponents => IbcSudoChangeFeeComponentsStorage, SudoAddressChangeFeeComponents => SudoAddressChangeFeeComponentsStorage, + ValidatorUpdateV2FeeComponents => ValidatorUpdateV2FeeComponentsStorage, ); impl_from_for_fee_storage!( @@ -203,4 +212,5 @@ impl_from_for_fee_storage!( IbcRelayerChangeFeeComponentsStorage => IbcRelayerChangeFees, IbcSudoChangeFeeComponentsStorage => IbcSudoChangeFees, SudoAddressChangeFeeComponentsStorage => SudoAddressChangeFees, + ValidatorUpdateV2FeeComponentsStorage => ValidatorUpdateV2Fees, ); diff --git a/crates/astria-sequencer/src/service/info/mod.rs b/crates/astria-sequencer/src/service/info/mod.rs index 23b863aeb..fe0b26a1e 100644 --- a/crates/astria-sequencer/src/service/info/mod.rs +++ b/crates/astria-sequencer/src/service/info/mod.rs @@ -87,6 +87,12 @@ impl Info { crate::bridge::query::bridge_account_info_request, ) .wrap_err("invalid path: `bridge/account_info/:address`")?; + query_router + .insert( + "authority/validator_name/:address", + crate::authority::query::validator_name_request, + ) + .wrap_err("invalid path: `authority/validator_name/:address`")?; Ok(Self { storage, query_router, @@ -183,6 +189,7 @@ mod tests { protocol::{ account::v1::BalanceResponse, asset::v1::DenomResponse, + transaction::v1::action::ValidatorUpdateV2, }, }; use cnidarium::StateDelta; @@ -202,7 +209,12 @@ mod tests { }, app::StateWriteExt as _, assets::StateWriteExt as _, - benchmark_and_test_utils::nria, + authority::StateWriteExt as _, + benchmark_and_test_utils::{ + astria_address, + nria, + verification_key, + }, fees::{ StateReadExt as _, StateWriteExt as _, @@ -384,4 +396,55 @@ mod tests { ); } } + + #[tokio::test] + async fn handle_validator_name_query() { + let storage = cnidarium::TempStorage::new().await.unwrap(); + let mut state = StateDelta::new(storage.latest_snapshot()); + let verification_key = verification_key(1); + let height = 0u32; + let power = 100; + + let validator_update = ValidatorUpdateV2 { + verification_key: verification_key.clone(), + power, + name: "validator_name".to_string(), + }; + + state + .put_validator_name( + verification_key.address_bytes(), + validator_update.name.clone(), + ) + .unwrap(); + + storage.commit(state).await.unwrap(); + + let info_request = InfoRequest::Query(request::Query { + path: format!( + "authority/validator_name/{}", + astria_address(verification_key.address_bytes()) + ), + data: vec![].into(), + height: height.into(), + prove: false, + }); + + let response = { + let storage = (*storage).clone(); + let info_service = Info::new(storage).unwrap(); + info_service + .handle_info_request(info_request) + .await + .unwrap() + }; + let query_response = match response { + InfoResponse::Query(query) => query, + other => panic!("expected InfoResponse::Query, got {other:?}"), + }; + assert!(query_response.code.is_ok()); + + let validator_name_resp = String::from_utf8(query_response.value.to_vec()).unwrap(); + assert_eq!(validator_name_resp, validator_update.name); + } } diff --git a/crates/astria-sequencer/src/transaction/checks.rs b/crates/astria-sequencer/src/transaction/checks.rs index 3e00c7b0f..26d8739df 100644 --- a/crates/astria-sequencer/src/transaction/checks.rs +++ b/crates/astria-sequencer/src/transaction/checks.rs @@ -106,6 +106,7 @@ pub(crate) async fn get_total_transaction_cost( .or_insert(act.amount); } Action::ValidatorUpdate(_) + | Action::ValidatorUpdateV2(_) | Action::SudoAddressChange(_) | Action::IbcSudoChange(_) | Action::RollupDataSubmission(_) diff --git a/crates/astria-sequencer/src/transaction/mod.rs b/crates/astria-sequencer/src/transaction/mod.rs index d430cca11..7171aac7c 100644 --- a/crates/astria-sequencer/src/transaction/mod.rs +++ b/crates/astria-sequencer/src/transaction/mod.rs @@ -91,27 +91,27 @@ impl ActionHandler for Transaction { Action::Transfer(act) => act .check_stateless() .await - .wrap_err("stateless check failed for TransferAction")?, + .wrap_err("stateless check failed for Transfer")?, Action::RollupDataSubmission(act) => act .check_stateless() .await - .wrap_err("stateless check failed for SequenceAction")?, + .wrap_err("stateless check failed for Sequence")?, Action::ValidatorUpdate(act) => act .check_stateless() .await - .wrap_err("stateless check failed for ValidatorUpdateAction")?, + .wrap_err("stateless check failed for ValidatorUpdate")?, Action::SudoAddressChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for SudoAddressChangeAction")?, + .wrap_err("stateless check failed for SudoAddressChange")?, Action::IbcSudoChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for IbcSudoChangeAction")?, + .wrap_err("stateless check failed for IbcSudoChange")?, Action::FeeChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for FeeChangeAction")?, + .wrap_err("stateless check failed for FeeChange")?, Action::Ibc(act) => { let action = act .clone() @@ -120,36 +120,40 @@ impl ActionHandler for Transaction { .check_stateless(()) .await .map_err(anyhow_to_eyre) - .wrap_err("stateless check failed for IbcAction")?; + .wrap_err("stateless check failed for IbcRelay")?; } Action::Ics20Withdrawal(act) => act .check_stateless() .await - .wrap_err("stateless check failed for Ics20WithdrawalAction")?, + .wrap_err("stateless check failed for Ics20Withdrawal")?, Action::IbcRelayerChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for IbcRelayerChangeAction")?, + .wrap_err("stateless check failed for IbcRelayerChange")?, Action::FeeAssetChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for FeeAssetChangeAction")?, + .wrap_err("stateless check failed for FeeAssetChange")?, Action::InitBridgeAccount(act) => act .check_stateless() .await - .wrap_err("stateless check failed for InitBridgeAccountAction")?, + .wrap_err("stateless check failed for InitBridgeAccount")?, Action::BridgeLock(act) => act .check_stateless() .await - .wrap_err("stateless check failed for BridgeLockAction")?, + .wrap_err("stateless check failed for BridgeLock")?, Action::BridgeUnlock(act) => act .check_stateless() .await - .wrap_err("stateless check failed for BridgeUnlockAction")?, + .wrap_err("stateless check failed for BridgeUnlock")?, Action::BridgeSudoChange(act) => act .check_stateless() .await - .wrap_err("stateless check failed for BridgeSudoChangeAction")?, + .wrap_err("stateless check failed for BridgeSudoChange")?, + Action::ValidatorUpdateV2(act) => act + .check_stateless() + .await + .wrap_err("stateless check failed for ValidatorUpdateV2")?, } } Ok(()) @@ -276,6 +280,9 @@ impl ActionHandler for Transaction { Action::BridgeSudoChange(act) => check_execute_and_pay_fees(act, &mut state) .await .wrap_err("failed executing bridge sudo change")?, + Action::ValidatorUpdateV2(act) => check_execute_and_pay_fees(act, &mut state) + .await + .wrap_err("executing validor update (v2)")?, } } diff --git a/proto/protocolapis/astria/protocol/fees/v1/types.proto b/proto/protocolapis/astria/protocol/fees/v1/types.proto index 1d3edc270..bb95ca1fa 100644 --- a/proto/protocolapis/astria/protocol/fees/v1/types.proto +++ b/proto/protocolapis/astria/protocol/fees/v1/types.proto @@ -79,6 +79,11 @@ message IbcSudoChangeFeeComponents { astria.primitive.v1.Uint128 multiplier = 2; } +message ValidatorUpdateV2FeeComponents { + astria.primitive.v1.Uint128 base = 1; + astria.primitive.v1.Uint128 multiplier = 2; +} + // Response to a transaction fee ABCI query. message TransactionFeeResponse { uint64 height = 2; diff --git a/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto b/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto index cac01cb84..9522a88c7 100644 --- a/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto +++ b/proto/protocolapis/astria/protocol/fees/v1alpha1/types.proto @@ -49,6 +49,7 @@ message IbcRelayFeeComponents { astria.primitive.v1.Uint128 multiplier = 2; } +// Deprecated, use `ValidatorUpdateV2FeeComponents` instead. message ValidatorUpdateFeeComponents { astria.primitive.v1.Uint128 base = 1; astria.primitive.v1.Uint128 multiplier = 2; diff --git a/proto/protocolapis/astria/protocol/transaction/v1/action.proto b/proto/protocolapis/astria/protocol/transaction/v1/action.proto index 26cab7579..c5619e7c3 100644 --- a/proto/protocolapis/astria/protocol/transaction/v1/action.proto +++ b/proto/protocolapis/astria/protocol/transaction/v1/action.proto @@ -6,6 +6,7 @@ import "astria/primitive/v1/types.proto"; import "astria/protocol/fees/v1/types.proto"; import "astria_vendored/penumbra/core/component/ibc/v1/ibc.proto"; import "astria_vendored/tendermint/abci/types.proto"; +import "astria_vendored/tendermint/crypto/keys.proto"; message Action { oneof value { @@ -25,11 +26,14 @@ message Action { // POA sudo actions are defined on 50-60 SudoAddressChange sudo_address_change = 50; - astria_vendored.tendermint.abci.ValidatorUpdate validator_update = 51; IbcRelayerChange ibc_relayer_change = 52; FeeAssetChange fee_asset_change = 53; + ValidatorUpdateV2 validator_update_v2 = 54; FeeChange fee_change = 55; IbcSudoChange ibc_sudo_change = 56; + + // Prefer `ValidatorUpdateV2` instead. + astria_vendored.tendermint.abci.ValidatorUpdate validator_update = 51 [deprecated = true]; } } @@ -211,6 +215,12 @@ message BridgeSudoChange { string fee_asset = 4; } +message ValidatorUpdateV2 { + astria_vendored.tendermint.crypto.PublicKey pub_key = 1; + int64 power = 2; + string name = 3; +} + message FeeChange { // the new fee components values oneof fee_components { @@ -227,7 +237,8 @@ message FeeChange { astria.protocol.fees.v1.RollupDataSubmissionFeeComponents rollup_data_submission = 11; astria.protocol.fees.v1.SudoAddressChangeFeeComponents sudo_address_change = 12; astria.protocol.fees.v1.TransferFeeComponents transfer = 13; - astria.protocol.fees.v1.ValidatorUpdateFeeComponents validator_update = 14; + astria.protocol.fees.v1.ValidatorUpdateFeeComponents validator_update = 14 [deprecated = true]; + astria.protocol.fees.v1.ValidatorUpdateV2FeeComponents validator_update_v2 = 15; } } diff --git a/tools/protobuf-compiler/src/main.rs b/tools/protobuf-compiler/src/main.rs index 1034672e9..8e06f6175 100644 --- a/tools/protobuf-compiler/src/main.rs +++ b/tools/protobuf-compiler/src/main.rs @@ -76,6 +76,10 @@ fn main() { ".astria_vendored.tendermint.abci.ValidatorUpdate", "crate::generated::astria_vendored::tendermint::abci::ValidatorUpdate", ) + .extern_path( + ".astria_vendored.tendermint.crypto.PublicKey", + "crate::generated::astria_vendored::tendermint::crypto::PublicKey", + ) .type_attribute(".astria.primitive.v1.Uint128", "#[derive(Copy)]") .type_attribute( ".astria.protocol.genesis.v1.IbcParameters",