From a97fc71f70c92388e9c85a33c41430d8a359d204 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Thu, 18 Sep 2025 20:00:23 -0400 Subject: [PATCH 1/9] feat(primitives): full legacy resource bounds support (#274) ## 'Legacy' Transaction V3 When V3 transaction was first introduced in RPC [v0.6.0], the `resource_bounds` field consists of only two fields; `l1_gas` and `l2_gas`. These values are included in the hash computation for the V3 transaction. But RPC [v0.8.0], a new field is added to the mapping, `l1_data_gas`. The addition of the new field means there are now two variations of the V3 transaction - with or without `l1_data_gas`. Depending on the presence of the field, the transaction hash is computed differently. Transaction V3 w/o `l1_data_gas` (legacy) hash: ``` h( "prefix", version, sender_address, h(tip, l1_gas_bounds, l2_gas_bounds), h(paymaster_data), chain_id, nonce, data_availability_modes, h(account_deployment_data), h(calldata) ) ``` Transaction V3 w/ `l1_data_gas` hash: ``` h( "prefix", version, sender_address, h(tip, l1_gas_bounds, l2_gas_bounds, l1_data_gas_bounds), h(paymaster_data), chain_id, nonce, data_availability_modes, h(account_deployment_data), h(calldata) ) ``` V3 transaction without the `l1_data_gas` is referred to as 'legacy' V3. ## Previous Refactors When pull request [#141] was made, I didn't take into account the possibility that a transaction with 'legacy' resource bounds would have a nonzero `l2_gas`. Hence why in that pull request, the `ResourceBoundsMapping::L1Gas` wraps a `ResourceBounds` struct - intended only for the `l1_gas` field. Currently, when computing the hash for transaction that uses the legacy resource bounds mapping, Katana naively assumes that the `l2_gas` bounds is 0. While this is true for most part, because some Starknet client libraries (eg `starknet-rs`) hardcoded the `l2_gas` to 0, but this doesn't fully negate the possibility that a transaction could be submitted with non-zero `l2_gas`. Even though the `l2_gas` wasn't actually used for execution, it is still used to compute the hash for the transaction. Preserving all the bounds is important if we want to make sure the hashes of transactions stored in the Katana database are reproducible! Honestly, if we only consider the transaction version that Katana supports now - 0.14.0 compatible transaction where all 3 bounds must be present - then this change really doesn't do anything at all. Transactions using legacy resource bounds will be outright rejected by the RPC server, and we don't have to worry about ensuring their hashes reproducibility. This change only matters once we have Katana running as a full node, syncing from Starknet mainnet/sepolia where transactions with only `l1_gas` and `l2_gas` resource bounds exist. As such, it is important that all details of a transaction is preserved correctly. ## Database Compatibility This change isn't compatible with the current database format. As such a database version bump is required! [v0.6.0]: https://github.com/starkware-libs/starknet-specs/blob/49665932a97f8fdef7ac5869755d2858c5e3a687/api/starknet_api_openrpc.json#L3714 [v0.8.0]: https://github.com/starkware-libs/starknet-specs/blob/b4f81445c79b2a8b2b09ff5bb2b7eddca78a32de/api/starknet_api_openrpc.json#L3494-L3514 [#141]: https://github.com/dojoengine/katana/pull/141 --------- Co-authored-by: Claude --- .../src/implementation/blockifier/utils.rs | 6 +- .../feeder-gateway/src/types/transaction.rs | 91 ++--- crates/primitives/src/fee.rs | 385 +++++++++++++++++- crates/primitives/src/transaction.rs | 14 +- crates/rpc/rpc-types/src/broadcasted.rs | 187 +-------- crates/rpc/rpc-types/src/transaction.rs | 98 +---- crates/rpc/rpc-types/tests/transaction.rs | 77 ++-- .../db/src/models/versioned/transaction/v6.rs | 75 +--- 8 files changed, 513 insertions(+), 420 deletions(-) diff --git a/crates/executor/src/implementation/blockifier/utils.rs b/crates/executor/src/implementation/blockifier/utils.rs index de7a4b391..dde58960d 100644 --- a/crates/executor/src/implementation/blockifier/utils.rs +++ b/crates/executor/src/implementation/blockifier/utils.rs @@ -588,8 +588,8 @@ fn to_api_resource_bounds(resource_bounds: fee::ResourceBoundsMapping) -> ValidR } fee::ResourceBoundsMapping::L1Gas(bounds) => ValidResourceBounds::L1Gas(ResourceBounds { - max_amount: bounds.max_amount.into(), - max_price_per_unit: bounds.max_price_per_unit.into(), + max_amount: bounds.l1_gas.max_amount.into(), + max_price_per_unit: bounds.l1_gas.max_price_per_unit.into(), }), } } @@ -723,7 +723,7 @@ pub fn is_zero_resource_bounds(resource_bounds: &ResourceBoundsMapping) -> bool } ResourceBoundsMapping::L1Gas(bounds) => { - (bounds.max_amount as u128 * bounds.max_price_per_unit) == 0 + (bounds.l1_gas.max_amount as u128 * bounds.l1_gas.max_price_per_unit) == 0 } } } diff --git a/crates/feeder-gateway/src/types/transaction.rs b/crates/feeder-gateway/src/types/transaction.rs index 77dcad1e6..49046ce19 100644 --- a/crates/feeder-gateway/src/types/transaction.rs +++ b/crates/feeder-gateway/src/types/transaction.rs @@ -1,6 +1,9 @@ use katana_primitives::class::{ClassHash, CompiledClassHash}; use katana_primitives::contract::Nonce; -use katana_primitives::fee::{AllResourceBoundsMapping, Tip}; +use katana_primitives::fee::{ + AllResourceBoundsMapping, L1GasResourceBoundsMapping, ResourceBounds, ResourceBoundsMapping, + Tip, +}; use katana_primitives::transaction::{ DeclareTx as PrimitiveDeclareTx, DeclareTxV0 as PrimitiveDeclareTxV0, DeclareTxV1 as PrimitiveDeclareTxV1, DeclareTxV2 as PrimitiveDeclareTxV2, @@ -12,7 +15,7 @@ use katana_primitives::transaction::{ TxWithHash, }; use katana_primitives::{ContractAddress, Felt}; -use serde::Deserialize; +use serde::{Deserialize, Deserializer}; #[derive(Debug, PartialEq, Eq, Deserialize)] pub struct ConfirmedTransaction { @@ -70,29 +73,6 @@ impl<'de> Deserialize<'de> for DataAvailabilityMode { } } -// Same reason as `DataAvailabilityMode` above, this struct is also defined because the serde -// implementation of its primitive counterpart is different. -#[derive(Debug, Default, PartialEq, Eq, Deserialize)] -pub struct ResourceBounds { - #[serde(deserialize_with = "serde_utils::deserialize_u64")] - pub max_amount: u64, - #[serde(deserialize_with = "serde_utils::deserialize_u128")] - pub max_price_per_unit: u128, -} - -#[derive(Debug, PartialEq, Eq, Deserialize)] -pub struct ResourceBoundsMapping { - #[serde(rename = "L1_GAS")] - pub l1_gas: ResourceBounds, - - #[serde(rename = "L2_GAS")] - pub l2_gas: ResourceBounds, - /// Marked as optional because prior to 0.13.4, L1 data gas is not a required field in the - /// resource bounds. - #[serde(rename = "L1_DATA_GAS")] - pub l1_data_gas: Option, -} - /// Invoke transaction enum with version-specific variants #[derive(Debug, PartialEq, Eq, Deserialize)] #[serde(tag = "version")] @@ -116,6 +96,7 @@ pub struct InvokeTxV3 { pub nonce: Nonce, pub calldata: Vec, pub signature: Vec, + #[serde(deserialize_with = "deserialize_resource_bounds_mapping")] pub resource_bounds: ResourceBoundsMapping, pub tip: Tip, pub paymaster_data: Vec, @@ -152,6 +133,7 @@ pub struct DeclareTxV3 { pub signature: Vec, pub class_hash: ClassHash, pub compiled_class_hash: CompiledClassHash, + #[serde(deserialize_with = "deserialize_resource_bounds_mapping")] pub resource_bounds: ResourceBoundsMapping, pub tip: Tip, pub paymaster_data: Vec, @@ -191,6 +173,7 @@ pub struct DeployAccountTxV3 { pub contract_address: Option, pub contract_address_salt: Felt, pub constructor_calldata: Vec, + #[serde(deserialize_with = "deserialize_resource_bounds_mapping")] pub resource_bounds: ResourceBoundsMapping, pub tip: Tip, pub paymaster_data: Vec, @@ -270,7 +253,7 @@ impl TryFrom for PrimitiveInvokeTx { calldata: tx.calldata, signature: tx.signature, sender_address: tx.sender_address, - resource_bounds: tx.resource_bounds.into(), + resource_bounds: tx.resource_bounds, account_deployment_data: tx.account_deployment_data, fee_data_availability_mode: tx.fee_data_availability_mode.into(), nonce_data_availability_mode: tx.nonce_data_availability_mode.into(), @@ -315,7 +298,7 @@ impl TryFrom for PrimitiveDeclareTx { signature: tx.signature, class_hash: tx.class_hash, compiled_class_hash: tx.compiled_class_hash, - resource_bounds: tx.resource_bounds.into(), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), paymaster_data: tx.paymaster_data, account_deployment_data: tx.account_deployment_data, @@ -352,7 +335,7 @@ impl TryFrom for PrimitiveDeployAccountTx { contract_address: tx.contract_address.unwrap_or_default(), contract_address_salt: tx.contract_address_salt, constructor_calldata: tx.constructor_calldata, - resource_bounds: tx.resource_bounds.into(), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), paymaster_data: tx.paymaster_data, nonce_data_availability_mode: tx.nonce_data_availability_mode.into(), @@ -387,28 +370,34 @@ impl From for katana_primitives::da::DataAvailabilityMode } } -impl From for katana_primitives::fee::ResourceBoundsMapping { - fn from(bounds: ResourceBoundsMapping) -> Self { - if let Some(l1_data_gas) = bounds.l1_data_gas { - Self::All(AllResourceBoundsMapping { - l1_gas: katana_primitives::fee::ResourceBounds { - max_amount: bounds.l1_gas.max_amount, - max_price_per_unit: bounds.l1_gas.max_price_per_unit, - }, - l2_gas: katana_primitives::fee::ResourceBounds { - max_amount: bounds.l2_gas.max_amount, - max_price_per_unit: bounds.l2_gas.max_price_per_unit, - }, - l1_data_gas: katana_primitives::fee::ResourceBounds { - max_amount: l1_data_gas.max_amount, - max_price_per_unit: l1_data_gas.max_price_per_unit, - }, - }) - } else { - Self::L1Gas(katana_primitives::fee::ResourceBounds { - max_amount: bounds.l1_gas.max_amount, - max_price_per_unit: bounds.l1_gas.max_price_per_unit, - }) - } +fn deserialize_resource_bounds_mapping<'de, D>( + deserializer: D, +) -> Result +where + D: Deserializer<'de>, +{ + #[derive(Deserialize)] + struct FeederGatewayResourceBounds { + #[serde(rename = "L1_GAS")] + l1_gas: ResourceBounds, + #[serde(rename = "L2_GAS")] + l2_gas: ResourceBounds, + #[serde(rename = "L1_DATA_GAS")] + l1_data_gas: Option, + } + + let bounds = FeederGatewayResourceBounds::deserialize(deserializer)?; + + if let Some(l1_data_gas) = bounds.l1_data_gas { + Ok(ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: bounds.l1_gas, + l2_gas: bounds.l2_gas, + l1_data_gas, + })) + } else { + Ok(ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { + l1_gas: bounds.l1_gas, + l2_gas: bounds.l2_gas, + })) } } diff --git a/crates/primitives/src/fee.rs b/crates/primitives/src/fee.rs index 9ae91b1c1..2be066ada 100644 --- a/crates/primitives/src/fee.rs +++ b/crates/primitives/src/fee.rs @@ -1,6 +1,5 @@ #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct ResourceBounds { /// The max amount of the resource that can be used in the tx pub max_amount: u64, @@ -12,23 +11,33 @@ impl ResourceBounds { pub const ZERO: Self = Self { max_amount: 0, max_price_per_unit: 0 }; } -// Aliased to match the feeder gateway API #[derive(Debug, Clone, PartialEq, Eq, Default)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct AllResourceBoundsMapping { /// L1 gas bounds - covers L2→L1 messages sent by the transaction - #[serde(alias = "L1_GAS")] pub l1_gas: ResourceBounds, /// L2 gas bounds - covers L2 resources including computation, tx payload, event emission, code /// size, etc. Units: 1 Cairo step = 100 L2 gas - #[serde(alias = "L2_GAS")] pub l2_gas: ResourceBounds, /// L1 data gas (blob gas) bounds - covers the cost of submitting state diffs as blobs on L1 - #[serde(alias = "L1_DATA_GAS")] pub l1_data_gas: ResourceBounds, } +#[derive(Debug, Clone, PartialEq, Eq, Default)] +#[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] +pub struct L1GasResourceBoundsMapping { + /// L1 gas bounds - covers L2→L1 messages sent by the transaction + pub l1_gas: ResourceBounds, + + /// L2 gas bounds - covers L2 resources including computation, tx payload, event emission, code + /// size, etc. Units: 1 Cairo step = 100 L2 gas. + /// + /// Pre 0.13.3. this field is signed but never used. + pub l2_gas: ResourceBounds, +} + /// Transaction resource bounds. /// /// ## NOTE @@ -40,7 +49,6 @@ pub struct AllResourceBoundsMapping { /// For further details, refer to [Starknet v0.13.4 pre-release notes](https://community.starknet.io/t/starknet-v0-13-4-pre-release-notes/115257). #[derive(Debug, Clone, PartialEq, Eq)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] -#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ResourceBoundsMapping { /// Legacy bounds; only L1 gas bounds specified (backward compatibility). /// @@ -49,7 +57,7 @@ pub enum ResourceBoundsMapping { /// ommitted from this variant and is assumed to be zero during transaction hash computation. /// /// Supported in Starknet v0.13.4 but rejected in v0.14.0+. - L1Gas(ResourceBounds), + L1Gas(L1GasResourceBoundsMapping), /// All three resource bounds specified: L1 gas, L2 gas, and L1 data gas. /// @@ -88,6 +96,7 @@ pub struct FeeInfo { /// Units in which the fee is given pub unit: PriceUnit, } + /// Transaction tip amount. #[derive(Debug, Default, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)] #[cfg_attr(feature = "arbitrary", derive(::arbitrary::Arbitrary))] @@ -149,6 +158,252 @@ impl<'de> serde::Deserialize<'de> for Tip { } } +#[cfg(feature = "serde")] +impl serde::Serialize for ResourceBounds { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + if serializer.is_human_readable() { + let mut state = serializer.serialize_struct("ResourceBounds", 2)?; + state.serialize_field("max_amount", &format!("{:#x}", self.max_amount))?; + state.serialize_field( + "max_price_per_unit", + &format!("{:#x}", self.max_price_per_unit), + )?; + state.end() + } else { + let mut state = serializer.serialize_struct("ResourceBounds", 2)?; + state.serialize_field("max_amount", &self.max_amount)?; + state.serialize_field("max_price_per_unit", &self.max_price_per_unit)?; + state.end() + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ResourceBounds { + fn deserialize>(deserializer: D) -> Result { + use std::fmt; + + use serde::de::{self, MapAccess, Visitor}; + use serde_json::Value; + + if deserializer.is_human_readable() { + struct __Visitor; + + impl<'de> Visitor<'de> for __Visitor { + type Value = ResourceBounds; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("struct ResourceBounds") + } + + fn visit_map>(self, mut map: A) -> Result { + let mut max_amount = None; + let mut max_price_per_unit = None; + + while let Some(key) = map.next_key::()? { + match key.as_str() { + "max_amount" => { + if max_amount.is_some() { + return Err(de::Error::duplicate_field("max_amount")); + } + let value: Value = map.next_value()?; + max_amount = Some(match value { + Value::String(s) => { + if let Some(hex) = s.strip_prefix("0x") { + u64::from_str_radix(hex, 16) + .map_err(de::Error::custom)? + } else { + s.parse().map_err(de::Error::custom)? + } + } + Value::Number(n) => n + .as_u64() + .ok_or_else(|| de::Error::custom("invalid u64"))?, + _ => { + return Err(de::Error::custom( + "expected 0x-prefix hex string or number for \ + max_amount", + )) + } + }); + } + + "max_price_per_unit" => { + if max_price_per_unit.is_some() { + return Err(de::Error::duplicate_field("max_price_per_unit")); + } + + let value: Value = map.next_value()?; + max_price_per_unit = Some(match value { + Value::String(s) => { + if let Some(hex) = s.strip_prefix("0x") { + u128::from_str_radix(hex, 16) + .map_err(de::Error::custom)? + } else { + s.parse().map_err(de::Error::custom)? + } + } + Value::Number(n) => { + if let Some(u) = n.as_u64() { + u as u128 + } else { + return Err(de::Error::custom("invalid u128")); + } + } + _ => { + return Err(de::Error::custom( + "expected 0x-prefix hex string or number for \ + max_price_per_unit", + )) + } + }); + } + + _ => { + let _ = map.next_value::()?; + } + } + } + + let max_amount = + max_amount.ok_or_else(|| de::Error::missing_field("max_amount"))?; + let max_price_per_unit = max_price_per_unit + .ok_or_else(|| de::Error::missing_field("max_price_per_unit"))?; + + Ok(ResourceBounds { max_amount, max_price_per_unit }) + } + } + + deserializer.deserialize_struct( + "ResourceBounds", + &["max_amount", "max_price_per_unit"], + __Visitor, + ) + } else { + #[derive(serde::Deserialize)] + struct ResourceBoundsBinary { + max_amount: u64, + max_price_per_unit: u128, + } + + let binary = ResourceBoundsBinary::deserialize(deserializer)?; + Ok(ResourceBounds { + max_amount: binary.max_amount, + max_price_per_unit: binary.max_price_per_unit, + }) + } + } +} + +#[cfg(feature = "serde")] +impl serde::Serialize for ResourceBoundsMapping { + fn serialize(&self, serializer: S) -> Result { + use serde::ser::SerializeStruct; + + // For human readable formats (primarily targetting JSON), serialize as a flat object. + if serializer.is_human_readable() { + match self { + ResourceBoundsMapping::L1Gas(bounds) => { + let mut state = serializer.serialize_struct("ResourceBoundsMapping", 2)?; + state.serialize_field("l2_gas", &bounds.l2_gas)?; + state.serialize_field("l1_gas", &bounds.l1_gas)?; + state.end() + } + ResourceBoundsMapping::All(bounds) => { + let mut state = serializer.serialize_struct("ResourceBoundsMapping", 3)?; + state.serialize_field("l2_gas", &bounds.l2_gas)?; + state.serialize_field("l1_gas", &bounds.l1_gas)?; + state.serialize_field("l1_data_gas", &bounds.l1_data_gas)?; + state.end() + } + } + } + // For binary formats, use explicit enum tagging: + // + // * ResourceBoundsMapping::L1Gas = 0 + // * ResourceBoundsMapping::All = 1 + else { + match self { + ResourceBoundsMapping::L1Gas(v) => { + serializer.serialize_newtype_variant("ResourceBoundsMapping", 0, "L1Gas", v) + } + ResourceBoundsMapping::All(v) => { + serializer.serialize_newtype_variant("ResourceBoundsMapping", 1, "All", v) + } + } + } + } +} + +#[cfg(feature = "serde")] +impl<'de> serde::Deserialize<'de> for ResourceBoundsMapping { + fn deserialize>(deserializer: D) -> Result { + use std::fmt; + + use serde::de::{self, EnumAccess, VariantAccess, Visitor}; + + if deserializer.is_human_readable() { + // For JSON: deserialize from unified object format + #[derive(serde::Deserialize)] + struct UnifiedResourceBounds { + l1_gas: ResourceBounds, + l2_gas: ResourceBounds, + l1_data_gas: Option, + } + + let unified = UnifiedResourceBounds::deserialize(deserializer)?; + + // If l1_data_gas is present, it's the All variant + if let Some(l1_data_gas) = unified.l1_data_gas { + Ok(ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: unified.l1_gas, + l2_gas: unified.l2_gas, + l1_data_gas, + })) + } else { + // Otherwise it's the L1Gas variant + Ok(ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { + l1_gas: unified.l1_gas, + l2_gas: unified.l2_gas, + })) + } + } + // For binary formats, use standard enum deserialization (when derived using + // #[derive(Deserialize)]) + else { + struct __Visitor; + + impl<'de> Visitor<'de> for __Visitor { + type Value = ResourceBoundsMapping; + + fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result { + formatter.write_str("ResourceBoundsMapping enum") + } + + fn visit_enum>(self, data: A) -> Result { + let (variant_idx, variant) = data.variant::()?; + + match variant_idx { + 0 => { + let value = variant.newtype_variant::()?; + Ok(ResourceBoundsMapping::L1Gas(value)) + } + 1 => { + let value = variant.newtype_variant::()?; + Ok(ResourceBoundsMapping::All(value)) + } + _ => Err(de::Error::custom("invalid variant index; expected 0 or 1")), + } + } + } + + deserializer.deserialize_enum("ResourceBoundsMapping", &["L1Gas", "All"], __Visitor) + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -192,4 +447,120 @@ mod tests { let deserialized: Tip = serde_json::from_str(&serialized).unwrap(); assert_eq!(original, deserialized); } + + #[cfg(feature = "serde")] + #[test] + fn resource_bounds_mapping_json_serde() { + use serde_json::json; + + // ------------------------------------------- + // Legacy resource bounds mapping + + let json = json!({ + "l2_gas": { + "max_amount": "0x7d0", + "max_price_per_unit": "0xc8" + }, + "l1_gas": { + "max_amount": "0x3e8", + "max_price_per_unit": "0x64" + } + }); + + let bounds = ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 1000, max_price_per_unit: 100 }, + l2_gas: ResourceBounds { max_amount: 2000, max_price_per_unit: 200 }, + }); + + let serialized = serde_json::to_value(&bounds).unwrap(); + similar_asserts::assert_eq!(json, serialized); + + let deserialized: ResourceBoundsMapping = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, bounds); + + // ------------------------------------------- + // All resource bounds mapping + + let json = json!({ + "l2_gas": { + "max_amount": "0x7d0", + "max_price_per_unit": "0xc8" + }, + "l1_gas": { + "max_amount": "0x3e8", + "max_price_per_unit": "0x64" + }, + "l1_data_gas": { + "max_amount": "0xbb8", + "max_price_per_unit": "0x12c" + } + }); + + let bounds = ResourceBoundsMapping::All(AllResourceBoundsMapping { + l2_gas: ResourceBounds { max_amount: 2000, max_price_per_unit: 200 }, + l1_gas: ResourceBounds { max_amount: 1000, max_price_per_unit: 100 }, + l1_data_gas: ResourceBounds { max_amount: 3000, max_price_per_unit: 300 }, + }); + + let serialized = serde_json::to_value(&bounds).unwrap(); + similar_asserts::assert_eq!(json, serialized); + + let deserialized: ResourceBoundsMapping = serde_json::from_value(json).unwrap(); + assert_eq!(deserialized, bounds); + } + + #[cfg(feature = "serde")] + #[test] + fn resource_bounds_mapping_binary_serde() { + // Test L1Gas variant binary serialization (using postcard) + let l1_gas_mapping = ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 1000, max_price_per_unit: 100 }, + l2_gas: ResourceBounds { max_amount: 2000, max_price_per_unit: 200 }, + }); + + let binary = postcard::to_stdvec(&l1_gas_mapping).unwrap(); + let deserialized: ResourceBoundsMapping = postcard::from_bytes(&binary).unwrap(); + assert_eq!(deserialized, l1_gas_mapping); + + // Test All variant binary serialization + let all_mapping = ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 1000, max_price_per_unit: 100 }, + l2_gas: ResourceBounds { max_amount: 2000, max_price_per_unit: 200 }, + l1_data_gas: ResourceBounds { max_amount: 3000, max_price_per_unit: 300 }, + }); + + let binary = postcard::to_stdvec(&all_mapping).unwrap(); + let deserialized: ResourceBoundsMapping = postcard::from_bytes(&binary).unwrap(); + assert_eq!(deserialized, all_mapping); + + // Ensure binary format is different from JSON (uses enum tags) + // Binary should be more compact than JSON + let json_size = serde_json::to_string(&all_mapping).unwrap().len(); + assert!(binary.len() < json_size); + } + + #[cfg(feature = "serde")] + #[test] + fn resource_bounds_mapping_cross_format() { + // Test that the same data structure can be serialized/deserialized + // in both JSON and binary formats independently + let mapping = ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 5000, max_price_per_unit: 500 }, + l2_gas: ResourceBounds { max_amount: 6000, max_price_per_unit: 600 }, + l1_data_gas: ResourceBounds { max_amount: 7000, max_price_per_unit: 700 }, + }); + + // Serialize to JSON, deserialize, and verify + let json = serde_json::to_string(&mapping).unwrap(); + let from_json: ResourceBoundsMapping = serde_json::from_str(&json).unwrap(); + assert_eq!(from_json, mapping); + + // Serialize to binary, deserialize, and verify + let binary = postcard::to_stdvec(&mapping).unwrap(); + let from_binary: ResourceBoundsMapping = postcard::from_bytes(&binary).unwrap(); + assert_eq!(from_binary, mapping); + + // Verify that JSON and binary deserializations produce the same result + assert_eq!(from_json, from_binary); + } } diff --git a/crates/primitives/src/transaction.rs b/crates/primitives/src/transaction.rs index 7b67a8e50..30b082487 100644 --- a/crates/primitives/src/transaction.rs +++ b/crates/primitives/src/transaction.rs @@ -6,7 +6,7 @@ use crate::chain::ChainId; use crate::class::{ClassHash, CompiledClassHash, ContractClass}; use crate::contract::{ContractAddress, Nonce}; use crate::da::DataAvailabilityMode; -use crate::fee::{ResourceBounds, ResourceBoundsMapping}; +use crate::fee::ResourceBoundsMapping; use crate::utils::transaction::{ compute_declare_v0_tx_hash, compute_declare_v1_tx_hash, compute_declare_v2_tx_hash, compute_declare_v3_tx_hash, compute_deploy_account_v1_tx_hash, @@ -338,8 +338,8 @@ impl InvokeTx { Felt::from(tx.sender_address), &tx.calldata, tx.tip, - bounds, - &ResourceBounds::ZERO, + &bounds.l1_gas, + &bounds.l2_gas, None, &tx.paymaster_data, tx.chain_id.into(), @@ -533,8 +533,8 @@ impl DeclareTx { tx.class_hash, tx.compiled_class_hash, tx.tip, - bounds, - &ResourceBounds::ZERO, + &bounds.l1_gas, + &bounds.l2_gas, None, &tx.paymaster_data, tx.chain_id.into(), @@ -702,8 +702,8 @@ impl DeployAccountTx { tx.class_hash, tx.contract_address_salt, tx.tip, - bounds, - &ResourceBounds::ZERO, + &bounds.l1_gas, + &bounds.l2_gas, None, &tx.paymaster_data, tx.chain_id.into(), diff --git a/crates/rpc/rpc-types/src/broadcasted.rs b/crates/rpc/rpc-types/src/broadcasted.rs index 3ea917bec..412ff7128 100644 --- a/crates/rpc/rpc-types/src/broadcasted.rs +++ b/crates/rpc/rpc-types/src/broadcasted.rs @@ -6,17 +6,14 @@ use katana_primitives::class::{ }; use katana_primitives::contract::Nonce; use katana_primitives::da::DataAvailabilityMode; -use katana_primitives::fee::{ - AllResourceBoundsMapping, ResourceBounds, ResourceBoundsMapping, Tip, -}; +use katana_primitives::fee::{ResourceBoundsMapping, Tip}; use katana_primitives::transaction::{ DeclareTx, DeclareTxV3, DeclareTxWithClass, DeployAccountTx, DeployAccountTxV3, InvokeTx, InvokeTxV3, TxHash, TxType, }; use katana_primitives::utils::get_contract_address; use katana_primitives::{ContractAddress, Felt}; -use serde::{de, Deserialize, Deserializer, Serialize, Serializer}; -use serde_utils::{deserialize_u128, deserialize_u64, serialize_as_hex}; +use serde::{de, Deserialize, Deserializer, Serialize}; use crate::class::SierraClass; @@ -61,10 +58,6 @@ pub struct UntypedBroadcastedTx { pub signature: Vec, pub tip: Tip, pub paymaster_data: Vec, - #[serde( - serialize_with = "serialize_resource_bounds_mapping", - deserialize_with = "deserialize_resource_bounds_mapping" - )] pub resource_bounds: ResourceBoundsMapping, pub nonce_data_availability_mode: DataAvailabilityMode, pub fee_data_availability_mode: DataAvailabilityMode, @@ -601,109 +594,6 @@ impl<'de> Deserialize<'de> for BroadcastedDeployAccountTx { } } -#[derive(Debug, Serialize, Deserialize)] -#[serde(untagged)] -enum RpcResourceBoundsMapping { - Current { - #[serde( - serialize_with = "serialize_resource_bounds", - deserialize_with = "deserialize_resource_bounds" - )] - l1_gas: ResourceBounds, - - #[serde( - serialize_with = "serialize_resource_bounds", - deserialize_with = "deserialize_resource_bounds" - )] - l2_gas: ResourceBounds, - - #[serde( - serialize_with = "serialize_resource_bounds", - deserialize_with = "deserialize_resource_bounds" - )] - l1_data_gas: ResourceBounds, - }, - - Legacy { - #[serde( - serialize_with = "serialize_resource_bounds", - deserialize_with = "deserialize_resource_bounds" - )] - l1_gas: ResourceBounds, - - #[serde( - serialize_with = "serialize_resource_bounds", - deserialize_with = "deserialize_resource_bounds" - )] - l2_gas: ResourceBounds, - }, -} - -#[derive(Serialize, Deserialize)] -struct RpcResourceBounds { - #[serde(serialize_with = "serialize_as_hex", deserialize_with = "deserialize_u64")] - max_amount: u64, - #[serde(serialize_with = "serialize_as_hex", deserialize_with = "deserialize_u128")] - max_price_per_unit: u128, -} - -fn serialize_resource_bounds_mapping( - resource_bounds: &ResourceBoundsMapping, - serializer: S, -) -> Result { - let rpc_mapping = match resource_bounds { - ResourceBoundsMapping::L1Gas(l1_gas) => RpcResourceBoundsMapping::Legacy { - l1_gas: l1_gas.clone(), - l2_gas: ResourceBounds::default(), - }, - - ResourceBoundsMapping::All(AllResourceBoundsMapping { l1_gas, l2_gas, l1_data_gas }) => { - RpcResourceBoundsMapping::Current { - l1_gas: l1_gas.clone(), - l2_gas: l2_gas.clone(), - l1_data_gas: l1_data_gas.clone(), - } - } - }; - - rpc_mapping.serialize(serializer) -} - -fn deserialize_resource_bounds_mapping<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result { - let rpc_mapping = RpcResourceBoundsMapping::deserialize(deserializer)?; - - match rpc_mapping { - RpcResourceBoundsMapping::Legacy { l1_gas, .. } => Ok(ResourceBoundsMapping::L1Gas(l1_gas)), - RpcResourceBoundsMapping::Current { l1_gas, l2_gas, l1_data_gas } => { - Ok(ResourceBoundsMapping::All(AllResourceBoundsMapping { l1_gas, l2_gas, l1_data_gas })) - } - } -} - -fn serialize_resource_bounds( - resource_bounds: &ResourceBounds, - serializer: S, -) -> Result { - let helper = RpcResourceBounds { - max_amount: resource_bounds.max_amount, - max_price_per_unit: resource_bounds.max_price_per_unit, - }; - - helper.serialize(serializer) -} - -fn deserialize_resource_bounds<'de, D: Deserializer<'de>>( - deserializer: D, -) -> Result { - let helper = RpcResourceBounds::deserialize(deserializer)?; - Ok(ResourceBounds { - max_amount: helper.max_amount, - max_price_per_unit: helper.max_price_per_unit, - }) -} - #[cfg(test)] mod tests { use assert_matches::assert_matches; @@ -715,64 +605,9 @@ mod tests { use super::*; use crate::broadcasted::{ AddDeclareTransactionResponse, AddDeployAccountTransactionResponse, - AddInvokeTransactionResponse, BroadcastedTx, RpcResourceBoundsMapping, - UntypedBroadcastedTxError, + AddInvokeTransactionResponse, BroadcastedTx, UntypedBroadcastedTxError, }; - #[test] - fn legacy_rpc_resource_bounds_serde() { - let json = json!({ - "l1_gas": { - "max_amount": "0x1", - "max_price_per_unit": "0x1" - }, - "l2_gas": { - "max_amount": "0x0", - "max_price_per_unit": "0x0" - } - }); - - let resource_bounds: RpcResourceBoundsMapping = serde_json::from_value(json).unwrap(); - - assert_matches!(resource_bounds, RpcResourceBoundsMapping::Legacy { l1_gas, l2_gas } => { - assert_eq!(l1_gas.max_amount, 1); - assert_eq!(l1_gas.max_price_per_unit, 1); - assert_eq!(l2_gas.max_amount, 0); - assert_eq!(l2_gas.max_price_per_unit, 0); - }); - } - - #[test] - fn rpc_resource_bounds_serde() { - let json = json!({ - "l2_gas": { - "max_amount": "0xa", - "max_price_per_unit": "0xb" - }, - "l1_gas": { - "max_amount": "0x1", - "max_price_per_unit": "0x2" - }, - "l1_data_gas": { - "max_amount": "0xabc", - "max_price_per_unit": "0x1337" - } - }); - - let resource_bounds: RpcResourceBoundsMapping = serde_json::from_value(json).unwrap(); - - assert_matches!(resource_bounds, RpcResourceBoundsMapping::Current { l2_gas, l1_gas, l1_data_gas } => { - assert_eq!(l2_gas.max_amount, 0xa); - assert_eq!(l2_gas.max_price_per_unit, 0xb); - - assert_eq!(l1_gas.max_amount, 0x1); - assert_eq!(l1_gas.max_price_per_unit, 0x2); - - assert_eq!(l1_data_gas.max_amount, 0xabc); - assert_eq!(l1_data_gas.max_price_per_unit, 0x1337); - }); - } - #[test] fn untyped_invoke_tx_serde() { let json = json!({ @@ -940,8 +775,10 @@ mod tests { assert_eq!(untyped.fee_data_availability_mode, DataAvailabilityMode::L1); assert_eq!(untyped.nonce_data_availability_mode, DataAvailabilityMode::L1); assert_matches!(&untyped.resource_bounds, ResourceBoundsMapping::L1Gas(bounds) => { - assert_eq!(bounds.max_amount, 0x100); - assert_eq!(bounds.max_price_per_unit, 0x200); + assert_eq!(bounds.l1_gas.max_amount, 0x100); + assert_eq!(bounds.l1_gas.max_price_per_unit, 0x200); + assert_eq!(bounds.l2_gas.max_amount, 0x0); + assert_eq!(bounds.l2_gas.max_price_per_unit, 0x0); }); // Tx specific fields @@ -996,8 +833,8 @@ mod tests { "max_price_per_unit": "0x200" }, "l2_gas": { - "max_amount": "0x0", - "max_price_per_unit": "0x0" + "max_amount": "0x123", + "max_price_per_unit": "0x1337" } }, "nonce_data_availability_mode": "L1", @@ -1021,8 +858,10 @@ mod tests { assert_eq!(untyped.fee_data_availability_mode, DataAvailabilityMode::L1); assert_eq!(untyped.nonce_data_availability_mode, DataAvailabilityMode::L1); assert_matches!(&untyped.resource_bounds, ResourceBoundsMapping::L1Gas(bounds) => { - assert_eq!(bounds.max_amount, 0x100); - assert_eq!(bounds.max_price_per_unit, 0x200); + assert_eq!(bounds.l1_gas.max_amount, 0x100); + assert_eq!(bounds.l1_gas.max_price_per_unit, 0x200); + assert_eq!(bounds.l2_gas.max_amount, 0x123); + assert_eq!(bounds.l2_gas.max_price_per_unit, 0x1337); }); // Tx specific fields diff --git a/crates/rpc/rpc-types/src/transaction.rs b/crates/rpc/rpc-types/src/transaction.rs index 91d6ebde2..dfb9d5e1b 100644 --- a/crates/rpc/rpc-types/src/transaction.rs +++ b/crates/rpc/rpc-types/src/transaction.rs @@ -2,11 +2,10 @@ use katana_primitives::class::{ClassHash, CompiledClassHash}; use katana_primitives::contract::Nonce; use katana_primitives::da::DataAvailabilityMode; use katana_primitives::execution::EntryPointSelector; -use katana_primitives::fee::{AllResourceBoundsMapping, Tip}; +use katana_primitives::fee::{ResourceBoundsMapping, Tip}; use katana_primitives::transaction::TxHash; use katana_primitives::{transaction as primitives, ContractAddress, Felt}; use serde::{Deserialize, Serialize}; -use starknet::core::types::ResourceBoundsMapping; use crate::ExecutionResult; @@ -357,7 +356,7 @@ impl From for RpcTx { fee_data_availability_mode: tx.fee_data_availability_mode, nonce_data_availability_mode: tx.nonce_data_availability_mode, paymaster_data: tx.paymaster_data, - resource_bounds: to_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), })), }, @@ -397,7 +396,7 @@ impl From for RpcTx { fee_data_availability_mode: tx.fee_data_availability_mode, nonce_data_availability_mode: tx.nonce_data_availability_mode, paymaster_data: tx.paymaster_data, - resource_bounds: to_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), }), }), @@ -432,7 +431,7 @@ impl From for RpcTx { fee_data_availability_mode: tx.fee_data_availability_mode, nonce_data_availability_mode: tx.nonce_data_availability_mode, paymaster_data: tx.paymaster_data, - resource_bounds: to_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), }) } @@ -448,89 +447,6 @@ impl From for RpcTx { } } -fn to_rpc_resource_bounds( - bounds: katana_primitives::fee::ResourceBoundsMapping, -) -> starknet::core::types::ResourceBoundsMapping { - match bounds { - katana_primitives::fee::ResourceBoundsMapping::All(all_bounds) => { - starknet::core::types::ResourceBoundsMapping { - l1_gas: starknet::core::types::ResourceBounds { - max_amount: all_bounds.l1_gas.max_amount, - max_price_per_unit: all_bounds.l1_gas.max_price_per_unit, - }, - l2_gas: starknet::core::types::ResourceBounds { - max_amount: all_bounds.l2_gas.max_amount, - max_price_per_unit: all_bounds.l2_gas.max_price_per_unit, - }, - l1_data_gas: starknet::core::types::ResourceBounds { - max_amount: all_bounds.l1_data_gas.max_amount, - max_price_per_unit: all_bounds.l1_data_gas.max_price_per_unit, - }, - } - } - // The `l1_data_gas` bounds should actually be ommitted but because `starknet-rs` doesn't - // support older RPC spec, we default to zero. This aren't technically accurate so should - // find an alternative or completely remove legacy support. But we need to support in order - // to maintain backward compatibility from older database version. - katana_primitives::fee::ResourceBoundsMapping::L1Gas(l1_gas_bounds) => { - starknet::core::types::ResourceBoundsMapping { - l1_gas: starknet::core::types::ResourceBounds { - max_amount: l1_gas_bounds.max_amount, - max_price_per_unit: l1_gas_bounds.max_price_per_unit, - }, - l2_gas: starknet::core::types::ResourceBounds { - max_amount: 0, - max_price_per_unit: 0, - }, - l1_data_gas: starknet::core::types::ResourceBounds { - max_amount: 0, - max_price_per_unit: 0, - }, - } - } - } -} - -fn from_rpc_resource_bounds( - bounds: starknet::core::types::ResourceBoundsMapping, -) -> katana_primitives::fee::ResourceBoundsMapping { - // If l2_gas and l1_data_gas are zero, treat it as L1Gas only (legacy support) - // - // this is technically an incorrect way to do this because the l2_gas and l1_data_gas can - // technically still be zero even if we're using non-legacy resource bounds (ie all bounds are - // set). the only way to do this is to use a different type/variant to represent a legacy - // resource bounds mapping. ideally we could just use the ResourceBoundsMapping from - // katana-primitives, but the L1Gas (ie legacy) has been incorrectly defined (lack of l2_gas - // field) and we can't simply add it because it'll break the type serialization format. - if bounds.l2_gas.max_amount == 0 - && bounds.l2_gas.max_price_per_unit == 0 - && bounds.l1_data_gas.max_amount == 0 - && bounds.l1_data_gas.max_price_per_unit == 0 - { - katana_primitives::fee::ResourceBoundsMapping::L1Gas( - katana_primitives::fee::ResourceBounds { - max_amount: bounds.l1_gas.max_amount, - max_price_per_unit: bounds.l1_gas.max_price_per_unit, - }, - ) - } else { - katana_primitives::fee::ResourceBoundsMapping::All(AllResourceBoundsMapping { - l1_gas: katana_primitives::fee::ResourceBounds { - max_amount: bounds.l1_gas.max_amount, - max_price_per_unit: bounds.l1_gas.max_price_per_unit, - }, - l2_gas: katana_primitives::fee::ResourceBounds { - max_amount: bounds.l2_gas.max_amount, - max_price_per_unit: bounds.l2_gas.max_price_per_unit, - }, - l1_data_gas: katana_primitives::fee::ResourceBounds { - max_amount: bounds.l1_data_gas.max_amount, - max_price_per_unit: bounds.l1_data_gas.max_price_per_unit, - }, - }) - } -} - impl From for primitives::TxWithHash { fn from(value: RpcTxWithHash) -> Self { primitives::TxWithHash { @@ -567,7 +483,7 @@ impl From for primitives::Tx { nonce: tx.nonce, calldata: tx.calldata, signature: tx.signature, - resource_bounds: from_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), paymaster_data: tx.paymaster_data, account_deployment_data: tx.account_deployment_data, @@ -611,7 +527,7 @@ impl From for primitives::Tx { signature: tx.signature, class_hash: tx.class_hash, compiled_class_hash: tx.compiled_class_hash, - resource_bounds: from_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), paymaster_data: tx.paymaster_data, account_deployment_data: tx.account_deployment_data, @@ -654,7 +570,7 @@ impl From for primitives::Tx { contract_address: Default::default(), contract_address_salt: tx.contract_address_salt, constructor_calldata: tx.constructor_calldata, - resource_bounds: from_rpc_resource_bounds(tx.resource_bounds), + resource_bounds: tx.resource_bounds, tip: tx.tip.into(), paymaster_data: tx.paymaster_data, nonce_data_availability_mode: tx.nonce_data_availability_mode, diff --git a/crates/rpc/rpc-types/tests/transaction.rs b/crates/rpc/rpc-types/tests/transaction.rs index 8f697181c..cda2b9c81 100644 --- a/crates/rpc/rpc-types/tests/transaction.rs +++ b/crates/rpc/rpc-types/tests/transaction.rs @@ -1,6 +1,9 @@ use assert_matches::assert_matches; use katana_primitives::da::DataAvailabilityMode; -use katana_primitives::fee::{ResourceBoundsMapping, Tip}; +use katana_primitives::fee::{ + AllResourceBoundsMapping, L1GasResourceBoundsMapping, ResourceBounds, ResourceBoundsMapping, + Tip, +}; use katana_primitives::{address, felt, transaction as primitives, ContractAddress}; use katana_rpc_types::transaction::{ RpcDeclareTx, RpcDeployAccountTx, RpcInvokeTx, RpcTx, RpcTxWithHash, @@ -10,7 +13,6 @@ use katana_rpc_types::{ RpcDeployAccountTxV3, RpcDeployTx, RpcInvokeTxV0, RpcInvokeTxV1, RpcInvokeTxV3, RpcL1HandlerTx, }; use serde_json::Value; -use starknet::core::types::ResourceBounds as RpcResourceBounds; mod fixtures; @@ -48,9 +50,11 @@ fn invoke_transaction() { assert_eq!(tx.account_deployment_data, vec![]); assert_eq!(tx.tip, Tip::new(0x5f5e100)); - assert_eq!(tx.resource_bounds.l1_data_gas, RpcResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l1_gas, RpcResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l2_gas, RpcResourceBounds { max_amount: 0x5f5e100, max_price_per_unit: 0xba43b7400 }); + assert_matches!(&tx.resource_bounds, ResourceBoundsMapping::All(bounds) => { + assert_eq!(bounds.l1_data_gas, ResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l1_gas, ResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l2_gas, ResourceBounds { max_amount: 0x5f5e100, max_price_per_unit: 0xba43b7400 }); + }); }); let serialized = serde_json::to_value(&tx).unwrap(); @@ -87,9 +91,12 @@ fn declare_transaction() { assert_eq!(tx.paymaster_data, vec![]); assert_eq!(tx.tip, Tip::new(0x0)); - assert_eq!(tx.resource_bounds.l1_data_gas, RpcResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l1_gas, RpcResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l2_gas, RpcResourceBounds { max_amount: 0x6c76900, max_price_per_unit: 0xba43b7400 }); + assert_matches!(&tx.resource_bounds, ResourceBoundsMapping::All(bounds) => { + assert_eq!(bounds.l1_data_gas, ResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l1_gas, ResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l2_gas, ResourceBounds { max_amount: 0x6c76900, max_price_per_unit: 0xba43b7400 }); + }); + }); let serialized = serde_json::to_value(&tx).unwrap(); @@ -128,9 +135,11 @@ fn deploy_account_transaction() { assert_eq!(tx.paymaster_data, vec![]); assert_eq!(tx.tip, Tip::new(0x5f5e100)); - assert_eq!(tx.resource_bounds.l1_data_gas, RpcResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l1_gas, RpcResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); - assert_eq!(tx.resource_bounds.l2_gas, RpcResourceBounds { max_amount: 0x5f5e100, max_price_per_unit: 0xba43b7400 }); + assert_matches!(&tx.resource_bounds, ResourceBoundsMapping::All(bounds) => { + assert_eq!(bounds.l1_data_gas, ResourceBounds { max_amount: 0x2710, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l1_gas, ResourceBounds { max_amount: 0x249f0, max_price_per_unit: 0x8d79883d20000 }); + assert_eq!(bounds.l2_gas, ResourceBounds { max_amount: 0x5f5e100, max_price_per_unit: 0xba43b7400 }); + }); }); let serialized = serde_json::to_value(&tx).unwrap(); @@ -182,11 +191,11 @@ fn rpc_to_primitives_invoke_v3() { calldata: vec![felt!("0x1"), felt!("0x2"), felt!("0x3")], signature: vec![felt!("0xabc"), felt!("0xdef")], nonce: felt!("0x5"), - resource_bounds: starknet::core::types::ResourceBoundsMapping { - l1_gas: RpcResourceBounds { max_amount: 0x1000, max_price_per_unit: 0x100 }, - l2_gas: RpcResourceBounds { max_amount: 0x2000, max_price_per_unit: 0x200 }, - l1_data_gas: RpcResourceBounds { max_amount: 0x3000, max_price_per_unit: 0x300 }, - }, + resource_bounds: ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0x1000, max_price_per_unit: 0x100 }, + l2_gas: ResourceBounds { max_amount: 0x2000, max_price_per_unit: 0x200 }, + l1_data_gas: ResourceBounds { max_amount: 0x3000, max_price_per_unit: 0x300 }, + }), tip: Tip::new(0x50), paymaster_data: vec![felt!("0x999")], account_deployment_data: vec![felt!("0x888"), felt!("0x777")], @@ -296,11 +305,11 @@ fn rpc_to_primitives_declare_v3() { signature: vec![felt!("0x444"), felt!("0x555")], nonce: felt!("0x20"), class_hash: felt!("0x666777"), - resource_bounds: starknet::core::types::ResourceBoundsMapping { - l1_gas: RpcResourceBounds { max_amount: 0x100, max_price_per_unit: 0x10 }, - l2_gas: RpcResourceBounds { max_amount: 0x200, max_price_per_unit: 0x20 }, - l1_data_gas: RpcResourceBounds { max_amount: 0x300, max_price_per_unit: 0x30 }, - }, + resource_bounds: ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0x100, max_price_per_unit: 0x10 }, + l2_gas: ResourceBounds { max_amount: 0x200, max_price_per_unit: 0x20 }, + l1_data_gas: ResourceBounds { max_amount: 0x300, max_price_per_unit: 0x30 }, + }), tip: Tip::new(0x99), paymaster_data: vec![felt!("0xfff")], account_deployment_data: vec![felt!("0xeee")], @@ -441,11 +450,11 @@ fn rpc_to_primitives_deploy_account_v3() { contract_address_salt: felt!("0xccc"), constructor_calldata: vec![felt!("0xddd"), felt!("0xeee")], class_hash: felt!("0xfff111"), - resource_bounds: starknet::core::types::ResourceBoundsMapping { - l1_gas: RpcResourceBounds { max_amount: 0x400, max_price_per_unit: 0x40 }, - l2_gas: RpcResourceBounds { max_amount: 0x500, max_price_per_unit: 0x50 }, - l1_data_gas: RpcResourceBounds { max_amount: 0x600, max_price_per_unit: 0x60 }, - }, + resource_bounds: ResourceBoundsMapping::All(AllResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0x400, max_price_per_unit: 0x40 }, + l2_gas: ResourceBounds { max_amount: 0x500, max_price_per_unit: 0x50 }, + l1_data_gas: ResourceBounds { max_amount: 0x600, max_price_per_unit: 0x60 }, + }), tip: Tip::new(0x88), paymaster_data: vec![felt!("0x222333")], nonce_data_availability_mode: DataAvailabilityMode::L1, @@ -575,7 +584,6 @@ fn rpc_to_primitives_deploy() { } #[test] -#[ignore = "we don't have proper support for legacy resource bounds on both RPC and primitives"] fn rpc_to_primitives_resource_bounds_l1_only() { // Test the case where only L1 gas bounds are set (legacy support) let rpc_tx = RpcTxWithHash { @@ -585,11 +593,10 @@ fn rpc_to_primitives_resource_bounds_l1_only() { calldata: vec![], signature: vec![], nonce: felt!("0x1"), - resource_bounds: starknet::core::types::ResourceBoundsMapping { - l1_gas: RpcResourceBounds { max_amount: 0x1000, max_price_per_unit: 0x100 }, - l2_gas: RpcResourceBounds { max_amount: 0, max_price_per_unit: 0 }, - l1_data_gas: RpcResourceBounds { max_amount: 0, max_price_per_unit: 0 }, - }, + resource_bounds: ResourceBoundsMapping::L1Gas(L1GasResourceBoundsMapping { + l1_gas: ResourceBounds { max_amount: 0x1000, max_price_per_unit: 0x100 }, + l2_gas: ResourceBounds { max_amount: 0x99, max_price_per_unit: 0x88 }, + }), tip: Tip::new(0), paymaster_data: vec![], account_deployment_data: vec![], @@ -603,8 +610,10 @@ fn rpc_to_primitives_resource_bounds_l1_only() { assert_matches!(&primitives_tx.transaction, primitives::Tx::Invoke(primitives::InvokeTx::V3(tx)) => { // When l2_gas and l1_data_gas are zero, it should be converted to L1Gas variant assert_matches!(&tx.resource_bounds, ResourceBoundsMapping::L1Gas(bounds) => { - assert_eq!(bounds.max_amount, 0x1000); - assert_eq!(bounds.max_price_per_unit, 0x100); + assert_eq!(bounds.l1_gas.max_amount, 0x1000); + assert_eq!(bounds.l1_gas.max_price_per_unit, 0x100); + assert_eq!(bounds.l2_gas.max_amount, 0x99); + assert_eq!(bounds.l2_gas.max_price_per_unit, 0x88); }); }); diff --git a/crates/storage/db/src/models/versioned/transaction/v6.rs b/crates/storage/db/src/models/versioned/transaction/v6.rs index f615cb16a..814c7025a 100644 --- a/crates/storage/db/src/models/versioned/transaction/v6.rs +++ b/crates/storage/db/src/models/versioned/transaction/v6.rs @@ -2,15 +2,25 @@ //! has been defined in database version 6. Modifying the order will break compatibility with the //! version. -use katana_primitives::{chain, class, contract, da, fee, transaction, Felt}; +use katana_primitives::fee::{self}; +use katana_primitives::{chain, class, contract, da, transaction, Felt}; use serde::{Deserialize, Serialize}; +#[repr(u8)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[cfg_attr(test, derive(::arbitrary::Arbitrary))] +pub enum Tx { + Invoke(InvokeTx) = 0, + Declare(DeclareTx), + L1Handler(transaction::L1HandlerTx), + DeployAccount(DeployAccountTx), + Deploy(transaction::DeployTx), +} + #[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] #[cfg_attr(test, derive(::arbitrary::Arbitrary))] pub struct ResourceBoundsMapping { - #[serde(alias = "L1_GAS")] pub l1_gas: fee::ResourceBounds, - #[serde(alias = "L2_GAS")] pub l2_gas: fee::ResourceBounds, } @@ -91,54 +101,9 @@ pub enum DeployAccountTx { V3(DeployAccountTxV3), } -#[repr(u8)] -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[cfg_attr(test, derive(::arbitrary::Arbitrary))] -pub enum Tx { - Invoke(InvokeTx) = 0, - Declare(DeclareTx), - L1Handler(transaction::L1HandlerTx), - DeployAccount(DeployAccountTx), - Deploy(transaction::DeployTx), -} - -impl Tx { - pub fn version(&self) -> Felt { - match self { - Tx::Invoke(tx) => match tx { - InvokeTx::V0(_) => Felt::ZERO, - InvokeTx::V1(_) => Felt::ONE, - InvokeTx::V3(_) => Felt::THREE, - }, - Tx::Declare(tx) => match tx { - DeclareTx::V0(_) => Felt::ZERO, - DeclareTx::V1(_) => Felt::ONE, - DeclareTx::V2(_) => Felt::TWO, - DeclareTx::V3(_) => Felt::THREE, - }, - Tx::L1Handler(tx) => tx.version, - Tx::DeployAccount(tx) => match tx { - DeployAccountTx::V1(_) => Felt::ONE, - DeployAccountTx::V3(_) => Felt::THREE, - }, - Tx::Deploy(tx) => tx.version, - } - } - - pub fn r#type(&self) -> transaction::TxType { - match self { - Self::Invoke(_) => transaction::TxType::Invoke, - Self::Deploy(_) => transaction::TxType::Deploy, - Self::Declare(_) => transaction::TxType::Declare, - Self::L1Handler(_) => transaction::TxType::L1Handler, - Self::DeployAccount(_) => transaction::TxType::DeployAccount, - } - } -} - impl From for fee::ResourceBoundsMapping { fn from(v6: ResourceBoundsMapping) -> Self { - Self::L1Gas(v6.l1_gas) + Self::L1Gas(fee::L1GasResourceBoundsMapping { l1_gas: v6.l1_gas, l2_gas: v6.l2_gas }) } } @@ -272,8 +237,10 @@ mod tests { match converted { fee::ResourceBoundsMapping::L1Gas(bounds) => { - assert_eq!(bounds.max_amount, 1000); - assert_eq!(bounds.max_price_per_unit, 100); + assert_eq!(bounds.l1_gas.max_amount, 1000); + assert_eq!(bounds.l1_gas.max_price_per_unit, 100); + assert_eq!(bounds.l2_gas.max_amount, 2000); + assert_eq!(bounds.l2_gas.max_price_per_unit, 200); } fee::ResourceBoundsMapping::All(..) => panic!("wrong variant"), } @@ -306,8 +273,10 @@ mod tests { match converted.resource_bounds { fee::ResourceBoundsMapping::L1Gas(bounds) => { - assert_eq!(bounds.max_amount, 1000); - assert_eq!(bounds.max_price_per_unit, 100); + assert_eq!(bounds.l1_gas.max_amount, 1000); + assert_eq!(bounds.l1_gas.max_price_per_unit, 100); + assert_eq!(bounds.l2_gas.max_amount, 2000); + assert_eq!(bounds.l2_gas.max_price_per_unit, 200); } fee::ResourceBoundsMapping::All(..) => panic!("wrong variant"), } From d584d4224f75f3648d55d1e4cec773ed638967ff Mon Sep 17 00:00:00 2001 From: "blacksmith-sh[bot]" <157653362+blacksmith-sh[bot]@users.noreply.github.com> Date: Mon, 22 Sep 2025 08:34:36 +0800 Subject: [PATCH 2/9] .github/workflows: Migrate workflows to Blacksmith runners (#276) Switch from GitHub-hosted runners to [Blacksmith] runners. This is an initial test migration. The change may be reverted if Blacksmith runners do not provide the expected benefits. [Blacksmith]: https://www.blacksmith.sh/ --- Co-authored-by: blacksmith-sh[bot] <157653362+blacksmith-sh[bot]@users.noreply.github.com> Co-authored-by: Ammar Arif --- .github/workflows/bench.yml | 2 +- .github/workflows/build-and-push-docker.yml | 18 +++++++--------- .github/workflows/claude.yml | 4 ++-- .github/workflows/dockerfile-build-test.yml | 10 ++++----- .github/workflows/generate-db-dispatch.yml | 2 +- .github/workflows/release-dispatch.yml | 2 +- .github/workflows/release.yml | 24 ++++++++++----------- .github/workflows/test.yml | 16 +++++++------- .github/workflows/weekly-report.yml | 12 ++++++++--- 9 files changed, 46 insertions(+), 44 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 1d195be1f..d9afdaabb 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -15,7 +15,7 @@ permissions: jobs: bench: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest steps: diff --git a/.github/workflows/build-and-push-docker.yml b/.github/workflows/build-and-push-docker.yml index 97208b8ce..57efd77bf 100644 --- a/.github/workflows/build-and-push-docker.yml +++ b/.github/workflows/build-and-push-docker.yml @@ -28,15 +28,15 @@ jobs: # |--------------------|---------------------------| # setup: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: tag_name: ${{ steps.docker_tag.outputs.tag_name }} steps: - name: Checkout repository uses: actions/checkout@v2 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + - name: Setup Blacksmith Builder + uses: useblacksmith/setup-docker-builder@v1 - name: Set Docker tag id: docker_tag @@ -49,7 +49,7 @@ jobs: fi build-and-push-amd64: - runs-on: ubuntu-latest-8-cores + runs-on: blacksmith-8vcpu-ubuntu-2404 needs: setup steps: - name: Checkout repository @@ -69,7 +69,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image for amd64 - uses: docker/build-push-action@v6 + uses: useblacksmith/build-push-action@v2 with: context: . push: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }} @@ -79,10 +79,9 @@ jobs: RUST_VERSION=${{ env.RUST_VERSION }} CLIPPY_VERSION=${{ env.CLIPPY_VERSION }} platforms: linux/amd64 - cache-from: type=registry,ref=ghcr.io/${{ github.repository }}-dev:latest-amd64 build-and-push-arm64: - runs-on: ubuntu-latest-8-cores-arm64 + runs-on: blacksmith-8vcpu-ubuntu-2404-arm needs: setup steps: - name: Checkout repository @@ -102,7 +101,7 @@ jobs: password: ${{ secrets.GITHUB_TOKEN }} - name: Build and push Docker image for arm64 - uses: docker/build-push-action@v6 + uses: useblacksmith/build-push-action@v2 with: context: . push: ${{ (github.event_name == 'push' && github.ref == 'refs/heads/main') || github.event_name == 'workflow_dispatch' }} @@ -112,10 +111,9 @@ jobs: RUST_VERSION=${{ env.RUST_VERSION }} CLIPPY_VERSION=${{ env.CLIPPY_VERSION }} platforms: linux/arm64 - cache-from: type=registry,ref=ghcr.io/${{ github.repository }}-dev:latest-arm64 create-multiplatform-manifest: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: [setup, build-and-push-amd64, build-and-push-arm64] steps: - name: Login to GitHub Container Registry diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 267a6090b..b370603b2 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -18,7 +18,7 @@ jobs: (github.event_name == 'pull_request_review' && contains(github.event.review.body, '@claude') && contains(vars.ALLOWED_CLAUDE_USERS, github.event.review.user.login)) || (github.event_name == 'issues' && (contains(github.event.issue.body, '@claude') || contains(github.event.issue.title, '@claude')) && contains(vars.ALLOWED_CLAUDE_USERS, github.event.issue.user.login))) - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: write pull-requests: write @@ -66,7 +66,7 @@ jobs: github.event.comment && github.event.comment.body == '/claude-review' - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 permissions: contents: write pull-requests: write diff --git a/.github/workflows/dockerfile-build-test.yml b/.github/workflows/dockerfile-build-test.yml index 184493f2b..abdd6ef5f 100644 --- a/.github/workflows/dockerfile-build-test.yml +++ b/.github/workflows/dockerfile-build-test.yml @@ -11,7 +11,7 @@ env: jobs: build-dev-image: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 strategy: matrix: platform: [linux/amd64] @@ -19,11 +19,11 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v3 + - name: Setup Blacksmith Builder + uses: useblacksmith/setup-docker-builder@v1 - name: Test Docker build for ${{ matrix.platform }} - uses: docker/build-push-action@v5 + uses: useblacksmith/build-push-action@v2 with: push: false file: .github/Dockerfile @@ -31,5 +31,3 @@ jobs: build-args: | RUST_VERSION=${{ env.RUST_VERSION }} CLIPPY_VERSION=${{ env.CLIPPY_VERSION }} - cache-from: type=gha - cache-to: type=gha,mode=max diff --git a/.github/workflows/generate-db-dispatch.yml b/.github/workflows/generate-db-dispatch.yml index 085eef18d..66da9ea1d 100644 --- a/.github/workflows/generate-db-dispatch.yml +++ b/.github/workflows/generate-db-dispatch.yml @@ -4,7 +4,7 @@ on: jobs: generate-database: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest diff --git a/.github/workflows/release-dispatch.yml b/.github/workflows/release-dispatch.yml index 5100cc8f9..01f83c1e6 100644 --- a/.github/workflows/release-dispatch.yml +++ b/.github/workflows/release-dispatch.yml @@ -26,7 +26,7 @@ jobs: permissions: pull-requests: write contents: write - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest env: diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index a4c498d8d..ce9b0c0bd 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -20,7 +20,7 @@ jobs: prepare: # The prepare-release branch names comes from the release-dispatch.yml workflow. if: (github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'prepare-release') || github.event_name == 'workflow_dispatch' - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 outputs: tag_name: ${{ steps.release_info.outputs.tag_name }} steps: @@ -41,7 +41,7 @@ jobs: fi build-contracts: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: prepare container: image: ghcr.io/dojoengine/katana-dev:latest @@ -76,23 +76,23 @@ jobs: # The arch is either 386, arm64 or amd64 # The svm target platform to use for the binary https://github.com/roynalnaruto/svm-rs/blob/84cbe0ac705becabdc13168bae28a45ad2299749/svm-builds/build.rs#L4-L24 # Added native_build dimension to control build type - - os: ubuntu-latest-8-cores + - os: blacksmith-8vcpu-ubuntu-2404 platform: linux target: x86_64-unknown-linux-gnu arch: amd64 native_build: true - - os: ubuntu-latest-8-cores + - os: blacksmith-8vcpu-ubuntu-2404 platform: linux target: x86_64-unknown-linux-gnu arch: amd64 native_build: false - - os: ubuntu-latest-8-cores-arm64 + - os: blacksmith-8vcpu-ubuntu-2404-arm platform: linux target: aarch64-unknown-linux-gnu arch: arm64 svm_target_platform: linux-aarch64 native_build: true - - os: ubuntu-latest-8-cores-arm64 + - os: blacksmith-8vcpu-ubuntu-2404-arm platform: linux target: aarch64-unknown-linux-gnu arch: arm64 @@ -277,7 +277,7 @@ jobs: retention-days: 1 create-draft-release: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 needs: [prepare, release] container: image: ghcr.io/dojoengine/katana-dev:latest @@ -301,7 +301,7 @@ jobs: - run: gh release create ${{ steps.version_info.outputs.version }} ./artifacts/* --generate-notes --draft docker-build-and-push: - runs-on: ubuntu-latest-8-cores + runs-on: blacksmith-8vcpu-ubuntu-2404 needs: [prepare, release] steps: @@ -315,8 +315,8 @@ jobs: path: artifacts/linux merge-multiple: true - - name: Set up Docker Buildx - uses: docker/setup-buildx-action@v1 + - name: Setup Blacksmith Builder + uses: useblacksmith/setup-docker-builder@v1 - name: Login to GitHub Container Registry uses: docker/login-action@v1 @@ -327,7 +327,7 @@ jobs: - name: Build and push docker image if: ${{ contains(needs.prepare.outputs.tag_name, 'preview') }} - uses: docker/build-push-action@v3 + uses: useblacksmith/build-push-action@v2 with: push: true tags: ghcr.io/${{ github.repository }}:${{ needs.prepare.outputs.tag_name }} @@ -337,7 +337,7 @@ jobs: - name: Build and push docker image if: ${{ !contains(needs.prepare.outputs.tag_name, 'preview') }} - uses: docker/build-push-action@v3 + uses: useblacksmith/build-push-action@v2 with: push: true tags: ghcr.io/${{ github.repository }}:latest,ghcr.io/${{ github.repository }}:${{ needs.prepare.outputs.tag_name }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index a22be2289..e6fe0a3c0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -33,7 +33,7 @@ env: jobs: fmt: - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest @@ -43,7 +43,7 @@ jobs: generate-test-artifacts: needs: [fmt] - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest @@ -89,7 +89,7 @@ jobs: build-katana-binary: needs: [fmt, clippy, generate-test-artifacts] - runs-on: ubuntu-latest-32-cores + runs-on: blacksmith-32vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest @@ -128,7 +128,7 @@ jobs: clippy: needs: [generate-test-artifacts] - runs-on: ubuntu-latest-4-cores + runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest @@ -158,7 +158,7 @@ jobs: test: needs: [fmt, clippy, generate-test-artifacts, build-katana-binary] - runs-on: ubuntu-latest-32-cores + runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 30 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: @@ -216,7 +216,7 @@ jobs: snos-integration-test: needs: [fmt, clippy] - runs-on: ubuntu-latest-32-cores + runs-on: blacksmith-32vcpu-ubuntu-2404 timeout-minutes: 30 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: @@ -253,7 +253,7 @@ jobs: explorer-reverse-proxy: needs: [fmt, clippy, build-katana-binary] - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest @@ -285,7 +285,7 @@ jobs: dojo-integration-test: needs: [fmt, clippy, build-katana-binary] - runs-on: ubuntu-latest-32-cores + runs-on: blacksmith-32vcpu-ubuntu-2404 if: github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false) container: image: ghcr.io/dojoengine/katana-dev:latest diff --git a/.github/workflows/weekly-report.yml b/.github/workflows/weekly-report.yml index d5fce260e..d469911ab 100644 --- a/.github/workflows/weekly-report.yml +++ b/.github/workflows/weekly-report.yml @@ -14,7 +14,7 @@ on: jobs: current-main-size: name: Current main branch binary size - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest outputs: @@ -33,6 +33,9 @@ jobs: with: key: weekly-binary-size-main + - name: Make contracts + run: make contracts + - name: Get binary size (main) id: binary-size run: | @@ -45,7 +48,7 @@ jobs: latest-release-size: name: Latest release binary size - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest outputs: @@ -64,6 +67,9 @@ jobs: with: key: weekly-binary-size-release + - name: Make contracts + run: make contracts + - name: Get latest release and binary size id: binary-size run: | @@ -85,7 +91,7 @@ jobs: generate-report: name: Generate binary size report needs: [current-main-size, latest-release-size] - runs-on: ubuntu-latest + runs-on: blacksmith-4vcpu-ubuntu-2404 container: image: ghcr.io/dojoengine/katana-dev:latest From 291c572173f32f2fcf9c6ce7dceb5fc57329224a Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 09:41:52 -0400 Subject: [PATCH 3/9] fix(contracts): prevent unnecessary build script triggers (#281) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Fixes build script triggering on every `cargo build` even when contract files haven't changed ## Problem The `katana-contracts` build script was being triggered unnecessarily on every `cargo build` command, even when no files in the `crates/contracts/contracts` directory had changed. ## Root Causes * The build script was watching the entire `contracts/` directory with `cargo:rerun-if-changed=contracts/`, which is unreliable in Cargo * `scarb build` updates `Scarb.lock` file timestamp even when content doesn't change ## Solution Instead of watching the entire `contracts/` directory, the script watches the nested directories instead to avoid including `Scarb.lock` file in the watch list. 🤖 Generated with [Claude Code](https://claude.ai/code) --------- Co-authored-by: Claude --- crates/contracts/Cargo.toml | 1 + crates/contracts/build.rs | 12 ++++++++++-- 2 files changed, 11 insertions(+), 2 deletions(-) diff --git a/crates/contracts/Cargo.toml b/crates/contracts/Cargo.toml index 01e06f5af..e9279b3d6 100644 --- a/crates/contracts/Cargo.toml +++ b/crates/contracts/Cargo.toml @@ -4,6 +4,7 @@ license.workspace = true name = "katana-contracts" repository.workspace = true version.workspace = true +build = "build.rs" [dependencies] katana-contracts-macro = { path = "macro" } diff --git a/crates/contracts/build.rs b/crates/contracts/build.rs index f083a3125..12eb90338 100644 --- a/crates/contracts/build.rs +++ b/crates/contracts/build.rs @@ -3,7 +3,15 @@ use std::process::Command; use std::{env, fs}; fn main() { - println!("cargo:rerun-if-changed=contracts/"); + // Track specific source directories and files that should trigger a rebuild + // Important: We don't track Scarb.lock as scarb will update it on every `scarb build` + println!("cargo:rerun-if-changed=contracts/Scarb.toml"); + println!("cargo:rerun-if-changed=contracts/account"); + println!("cargo:rerun-if-changed=contracts/legacy"); + println!("cargo:rerun-if-changed=contracts/messaging"); + println!("cargo:rerun-if-changed=contracts/test-contracts"); + println!("cargo:rerun-if-changed=contracts/vrf"); + println!("cargo:rerun-if-changed=build.rs"); let contracts_dir = Path::new("contracts"); let target_dir = contracts_dir.join("target/dev"); @@ -21,7 +29,7 @@ fn main() { return; } - // Only build if we're not in a docs build or if explicitly requested + // Only build if we're not in a docs build if env::var("DOCS_RS").is_ok() { return; } From c1fd9f003cf36cde9d8cd0145b2d6c47d90d0c9e Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 10:15:51 -0400 Subject: [PATCH 4/9] feat(bin): restructure `init` command with explicit rollup/sovereign modes (#280) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Refactor `katana init` command to use explicit subcommands for rollup and sovereign chain modes. Now running `katana init` requires you to select the mode as a subcommand: `katana init rollup` or `katana init sovereign`. One of the reason why we made this change is because the CLI argument configurations will always require you to provide the arguments instead of falling back to prompting. The expected behaviour when you literally just run `katana init` is for it to prompt the arguments like so: Screenshot 2025-09-23 at 10 03 49 AM But due to how the CLI arguments is being configured, we instead get an error for not providing the arguments using flags: Screenshot 2025-09-23 at 10 04
05 AM This is mainly because of the `#[arg(required_unless_present = "sovereign")]` tag we're using that forces us to provide the options if `--sovereign` is not present. https://github.com/dojoengine/katana/blob/d584d4224f75f3648d55d1e4cec773ed638967ff/bin/katana/src/cli/init/mod.rs#L44-L48 Afaik it doesn't seem to be possible to 'fix' this using `clap` derive macro. Even if it does, I believe separating the mode into separate commands feels like a better UX. ## Comparison with current behaviour - Current: `katana init --id my-chain --settlement-chain sepolia ...` - New: `katana init rollup --id my-chain --settlement-chain sepolia ...` --- bin/katana/src/cli/init/deployment.rs | 6 +- bin/katana/src/cli/init/mod.rs | 374 ++++++++++++++++++-------- bin/katana/src/cli/init/prompt.rs | 86 +++--- bin/katana/src/cli/mod.rs | 2 +- bin/katana/src/main.rs | 2 +- 5 files changed, 320 insertions(+), 150 deletions(-) diff --git a/bin/katana/src/cli/init/deployment.rs b/bin/katana/src/cli/init/deployment.rs index 1c249f8ce..d8b4a07e0 100644 --- a/bin/katana/src/cli/init/deployment.rs +++ b/bin/katana/src/cli/init/deployment.rs @@ -234,7 +234,7 @@ pub async fn deploy_settlement_contract( // FINAL CHECKS // ----------------------------------------------------------------------- - check_program_info(chain_id, deployed_appchain_contract, account.provider()).await?; + check_program_info(chain_id, deployed_appchain_contract.into(), account.provider()).await?; Ok(DeploymentOutcome { block_number: deployment_block, @@ -262,10 +262,10 @@ pub async fn deploy_settlement_contract( /// * Layout bridge program hash pub async fn check_program_info( chain_id: Felt, - appchain_address: Felt, + appchain_address: ContractAddress, provider: &SettlementChainProvider, ) -> Result<(), ContractInitError> { - let appchain = AppchainContractReader::new(appchain_address, provider); + let appchain = AppchainContractReader::new(appchain_address.into(), provider); // Compute the chain's config hash let config_hash = compute_config_hash( diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs index 3b6a087cd..3a863fac0 100644 --- a/bin/katana/src/cli/init/mod.rs +++ b/bin/katana/src/cli/init/mod.rs @@ -1,9 +1,66 @@ +//! Chain initialization commands for Katana. +//! +//! This module provides functionality to initialize new blockchain networks with Katana, +//! supporting both rollup and [sovereign] chain configurations. Currently, Katana only supports +//! deploying the rollup chain on top of the Starknet blockchain. +//! +//! # Overview +//! +//! The `init` command supports two distinct initialization modes: +//! +//! ## Rollup Mode (`katana init rollup`) +//! +//! Initializes a rollup chain that settles on an existing blockchain (right now only Starknet). +//! +//! **Interactive Usage:** +//! +//! ```bash +//! // Prompts for all required information when no flags are provided. +//! katana init rollup +//! ``` +//! +//! **Explicit Usage:** +//! +//! ```bash +//! katana init rollup \ +//! --id my-rollup \ +//! --settlement-chain sepolia \ +//! --settlement-account-address 0x123... \ +//! --settlement-account-private-key 0x456... +//! ``` +//! +//! ## Sovereign Mode (`katana init sovereign`) +//! +//! Initializes a sovereign chain that operates independently without settlement on another +//! blockchain. State updates and proofs are published to a Data Availability layer only. +//! +//! **Interactive Usage:** +//! +//! ```bash +//! // Prompts for all required information when no flags are provided. +//! katana init sovereign +//! ``` +//! +//! **Explicit Usage:** +//! +//! ```bash +//! katana init sovereign --id my-sovereign-chain +//! ``` +//! +//! # configuration output +//! +//! both modes generate chain specification files that can be used to start katana nodes: +//! - local configuration: `~/.config/katana/chains/` +//! - custom path: use `--output-path` to specify a directory +//! +//! [sovereign]: https://celestia.org/learn/intermediates/sovereign-rollups-an-introduction/ + use std::path::PathBuf; use std::str::FromStr; use anyhow::Context; use clap::builder::NonEmptyStringValueParser; -use clap::Args; +use clap::{Args, Subcommand}; use deployment::DeploymentOutcome; use katana_chain_spec::rollup::{ChainConfigDir, FeeContract}; use katana_chain_spec::{rollup, SettlementLayer}; @@ -27,42 +84,80 @@ mod settlement; mod slot; #[derive(Debug, Args)] -pub struct InitArgs { +pub struct InitCommand { + #[command(subcommand)] + pub mode: InitMode, +} + +/// initialization mode selection for different chain types. +#[derive(Debug, Subcommand)] +pub enum InitMode { + #[command(about = "Initialize a rollup chain")] + Rollup(Box), + + #[command(hide = true)] + #[command(about = "Initialize a sovereign chain")] + Sovereign(SovereignArgs), +} + +/// Configuration arguments for rollup chain initialization. +/// +/// Rollup chains settle their state and proofs on an existing Layer 1 blockchain. +/// This requires settlement layer connectivity, account management, and contract deployment. +/// +/// ## Interactivity +/// +/// If no arguments are provided, the command will prompt interactively for them. +#[derive(Debug, Args)] +pub struct RollupArgs { /// The id of the new chain to be initialized. /// /// An empty `Id` is not a allowed, since the chain id must be /// a valid ASCII string. #[arg(long)] #[arg(value_parser = NonEmptyStringValueParser::new())] + #[arg(requires_all = ["settlement_chain", "settlement_account", "settlement_account_private_key"])] id: Option, - /// The settlement chain to be used, where the core contract is deployed. - /// - /// If a custom settlement chain is provided, setting a custom facts registry is required using - /// the `--settlement-facts-registry` option. Otherwise, setting a custom facts registry - /// with a known chain is a no-op. - #[arg(long = "settlement-chain")] - #[arg(required_unless_present = "sovereign")] - #[arg(requires_all = ["id", "settlement_account", "settlement_account_private_key"])] + #[arg( + long = "settlement-chain", + help = "The settlement chain to be used, where the core contract is deployed." + )] + #[arg(long_help = "The settlement chain to be used, where the core contract is deployed. + +Possible values: + - `mainnet`, `sn_mainnet`: Starknet mainnet + - `sepolia`, `sn_sepolia`: Starknet sepolia")] + #[cfg_attr( + feature = "init-custom-settlement-chain", + arg(long_help = "The settlement chain to be used, where the core contract is deployed. + +Possible values: + - `mainnet`, `sn_mainnet`: Starknet mainnet + - `sepolia`, `sn_sepolia`: Starknet sepolia + - : Custom settlement chain URL (requires --settlement-facts-registry) + +If a custom settlement chain is provided, setting a custom facts registry is required using +the `--settlement-facts-registry` option. Otherwise, setting a custom facts registry +with a known chain is a no-op for now.") + )] + #[arg(requires = "id")] settlement_chain: Option, - /// The address of the settlement account to be used to configure the core contract. #[arg(long = "settlement-account-address")] - #[arg(required_unless_present = "sovereign")] - #[arg(requires_all = ["id", "settlement_chain", "settlement_account_private_key"])] + #[arg(requires = "id")] settlement_account: Option, /// The private key of the settlement account to be used to configure the core contract. #[arg(long = "settlement-account-private-key")] - #[arg(required_unless_present = "sovereign")] - #[arg(requires_all = ["id", "settlement_chain", "settlement_account"])] + #[arg(requires = "id")] settlement_account_private_key: Option, /// The address of the settlement contract. /// If not provided, the contract will be deployed on the settlement chain using the provided /// settlement account. #[arg(long = "settlement-contract")] - #[arg(requires_all = ["id", "settlement_chain", "settlement_account", "settlement_account_private_key", "settlement_contract_deployed_block"])] + #[arg(requires_all = ["id", "settlement_contract_deployed_block"])] settlement_contract: Option, /// The block number of the settlement contract deployment. @@ -76,18 +171,26 @@ pub struct InitArgs { /// /// Required if a custom settlement chain is specified. #[arg(long = "settlement-facts-registry")] - #[arg(requires_all = ["id", "settlement_chain", "settlement_account"])] - pub settlement_facts_registry_contract: Option, + settlement_facts_registry_contract: Option, - /// Initialize a sovereign chain with no settlement layer, by only publishing the state updates - /// and proofs on a Data Availability Layer. By using this flag, no settlement option is - /// required. + /// Specify the path of the directory where the configuration files will be stored at. #[arg(long)] - #[arg(help = "Initialize a sovereign chain with no settlement layer, by only publishing the \ - state updates and proofs on a Data Availability Layer.")] - #[arg(requires_all = ["id"])] - #[arg(conflicts_with_all = ["settlement_chain", "settlement_account", "settlement_account_private_key", "settlement_contract"])] - sovereign: bool, + output_path: Option, + + #[cfg(feature = "init-slot")] + #[command(flatten)] + slot: slot::SlotArgs, +} + +#[derive(Debug, Args)] +pub struct SovereignArgs { + /// The id of the new chain to be initialized. + /// + /// An empty `Id` is not a allowed, since the chain id must be + /// a valid ASCII string. + #[arg(long)] + #[arg(value_parser = NonEmptyStringValueParser::new())] + id: Option, /// Specify the path of the directory where the configuration files will be stored at. #[arg(long)] @@ -98,36 +201,45 @@ pub struct InitArgs { slot: slot::SlotArgs, } -impl InitArgs { - // TODO: - // - deploy bridge contract +impl InitCommand { + /// Executes the initialization command based on the selected mode. + /// + /// Dispatches to the appropriate initialization logic for either rollup or sovereign chains. + pub(crate) async fn execute(self) -> anyhow::Result<()> { + match self.mode { + InitMode::Rollup(args) => args.execute().await, + InitMode::Sovereign(args) => args.execute().await, + } + } +} + +impl RollupArgs { + /// Executes rollup chain initialization with settlement layer integration. + /// + /// # Interactive Behavior + /// + /// Falls back to interactive prompts when no CLI flags are provided. pub(crate) async fn execute(self) -> anyhow::Result<()> { let output = if let Some(output) = self.configure_from_args().await { output? } else { - prompt::prompt().await? + prompt::prompt_rollup().await? }; - let settlement = match &output { - AnyOutcome::Persistent(persistent) => SettlementLayer::Starknet { - account: persistent.account, - rpc_url: persistent.rpc_url.clone(), - id: ChainId::parse(&persistent.settlement_id)?, - block: persistent.deployment_outcome.block_number, - core_contract: persistent.deployment_outcome.contract_address, - }, - AnyOutcome::Sovereign(_) => SettlementLayer::Sovereign {}, + let settlement = SettlementLayer::Starknet { + account: output.account, + rpc_url: output.rpc_url.clone(), + id: ChainId::parse(&output.settlement_id)?, + block: output.deployment_outcome.block_number, + core_contract: output.deployment_outcome.contract_address, }; - let id = ChainId::parse(output.id())?; + let id = ChainId::parse(&output.id)?; #[cfg_attr(not(feature = "init-slot"), allow(unused_mut))] let mut genesis = generate_genesis(); #[cfg(feature = "init-slot")] - slot::add_paymasters_to_genesis( - &mut genesis, - &output.slot_paymasters().unwrap_or_default(), - ); + slot::add_paymasters_to_genesis(&mut genesis, &output.slot_paymasters.unwrap_or_default()); // At the moment, the fee token is limited to a predefined token. let fee_contract = FeeContract::default(); @@ -145,23 +257,20 @@ impl InitArgs { Ok(()) } - async fn configure_from_args(&self) -> Option> { + async fn configure_from_args(&self) -> Option> { if let Some(id) = self.id.clone() { - if self.sovereign { - return Some(Ok(AnyOutcome::Sovereign(SovereignOutcome { - id, - #[cfg(feature = "init-slot")] - slot_paymasters: self.slot.paymaster_accounts.clone(), - }))); - } - - // These args are all required if at least one of them are specified (incl chain id) and - // `clap` has already handled that for us, so it's safe to unwrap here. - let settlement_chain = self.settlement_chain.clone().expect("must present"); - let settlement_account_address = self.settlement_account.expect("must present"); - let settlement_private_key = self.settlement_account_private_key.expect("must present"); + // Check if all required settlement args are provided + let Some(settlement_chain) = self.settlement_chain.clone() else { + return None; // Fall back to prompting + }; + let Some(settlement_account_address) = self.settlement_account else { + return None; // Fall back to prompting + }; + let Some(settlement_private_key) = self.settlement_account_private_key else { + return None; // Fall back to prompting + }; - let settlement_provider = match settlement_chain { + let settlement_provider = match &settlement_chain { SettlementChain::Mainnet => { let mut provider = SettlementChainProvider::sn_mainnet(); if let Some(fact_registry) = self.settlement_facts_registry_contract { @@ -185,18 +294,32 @@ impl InitArgs { chain" ))); }; - SettlementChainProvider::new(url, *fact_registry) + SettlementChainProvider::new(url.clone(), *fact_registry) } }; - let l1_chain_id = settlement_provider.chain_id().await.unwrap(); + let l1_chain_id = match settlement_provider.chain_id().await.with_context(|| { + format!("failed to get chain id for settlement layer `{settlement_chain}`") + }) { + Ok(id) => id, + Err(err) => return Some(Err(err)), + }; - let chain_id = cairo_short_string_to_felt(&id).unwrap(); + let chain_id = match cairo_short_string_to_felt(&id) + .with_context(|| format!("invalid chain id: {id}")) + { + Ok(id) => id, + Err(err) => return Some(Err(err)), + }; let deployment_outcome = if let Some(contract) = self.settlement_contract { - deployment::check_program_info(chain_id, contract.into(), &settlement_provider) + match deployment::check_program_info(chain_id, contract, &settlement_provider) .await - .unwrap(); + .with_context(|| "settlement contract validation failed.".to_string()) + { + Ok(..) => (), + Err(err) => return Some(Err(err)), + }; DeploymentOutcome { contract_address: contract, @@ -215,10 +338,16 @@ impl InitArgs { ExecutionEncoding::New, ); - deployment::deploy_settlement_contract(account, chain_id).await.unwrap() + match deployment::deploy_settlement_contract(account, chain_id) + .await + .with_context(|| "failed to deploy settlement contract".to_string()) + { + Ok(id) => id, + Err(err) => return Some(Err(err)), + } }; - Some(Ok(AnyOutcome::Persistent(PersistentOutcome { + Some(Ok(PersistentOutcome { id, deployment_outcome, rpc_url: settlement_provider.url().clone(), @@ -226,34 +355,52 @@ impl InitArgs { settlement_id: parse_cairo_short_string(&l1_chain_id).unwrap(), #[cfg(feature = "init-slot")] slot_paymasters: self.slot.paymaster_accounts.clone(), - }))) + })) } else { None } } } -/// The outcome of the initialization process. -#[derive(Debug)] -enum AnyOutcome { - Persistent(PersistentOutcome), - Sovereign(SovereignOutcome), -} +impl SovereignArgs { + /// Executes sovereign chain initialization. + pub(crate) async fn execute(self) -> anyhow::Result<()> { + let output = if let Some(output) = self.configure_from_args() { + output + } else { + prompt::prompt_sovereign().await? + }; + + let settlement = SettlementLayer::Sovereign {}; + let id = ChainId::parse(&output.id)?; -impl AnyOutcome { - pub fn id(&self) -> &str { - match self { - AnyOutcome::Persistent(persistent) => &persistent.id, - AnyOutcome::Sovereign(sovereign) => &sovereign.id, + #[cfg_attr(not(feature = "init-slot"), allow(unused_mut))] + let mut genesis = generate_genesis(); + #[cfg(feature = "init-slot")] + slot::add_paymasters_to_genesis(&mut genesis, &output.slot_paymasters.unwrap_or_default()); + + // At the moment, the fee token is limited to a predefined token. + let fee_contract = FeeContract::default(); + let chain_spec = rollup::ChainSpec { id, genesis, settlement, fee_contract }; + + if let Some(path) = self.output_path { + let dir = ChainConfigDir::create(path)?; + rollup::write(&dir, &chain_spec).context("failed to write chain spec file")?; + } else { + // Write to the local chain config directory by default if user + // doesn't specify the output path + rollup::write_local(&chain_spec).context("failed to write chain spec file")?; } + + Ok(()) } - #[cfg(feature = "init-slot")] - pub fn slot_paymasters(&self) -> Option> { - match self { - AnyOutcome::Persistent(persistent) => persistent.slot_paymasters.clone(), - AnyOutcome::Sovereign(sovereign) => sovereign.slot_paymasters.clone(), - } + fn configure_from_args(&self) -> Option { + self.id.clone().map(|id| SovereignOutcome { + id, + #[cfg(feature = "init-slot")] + slot_paymasters: self.slot.paymaster_accounts.clone(), + }) } } @@ -302,6 +449,7 @@ struct SettlementChainTryFromStrError { id: String, } +/// Supported settlement chain options for rollup initialization. #[derive(Debug, Clone, strum_macros::Display, PartialEq, Eq)] enum SettlementChain { Mainnet, @@ -312,6 +460,7 @@ enum SettlementChain { impl std::str::FromStr for SettlementChain { type Err = SettlementChainTryFromStrError; + fn from_str(s: &str) -> Result::Err> { let id = s.to_lowercase(); if &id == "sepolia" || &id == "sn_sepolia" { @@ -333,6 +482,7 @@ impl std::str::FromStr for SettlementChain { impl TryFrom<&str> for SettlementChain { type Error = SettlementChainTryFromStrError; + fn try_from(s: &str) -> Result>::Error> { SettlementChain::from_str(s) } @@ -381,7 +531,7 @@ mod tests { #[derive(Parser)] struct Cli { #[command(flatten)] - args: InitArgs, + args: InitCommand, } // This should fail with the expected error message:- @@ -392,7 +542,7 @@ mod tests { // --settlement-account-address // --settlement-account-private-key // ``` - match Cli::try_parse_from(["init", "--id", "bruh"]) { + match Cli::try_parse_from(["init", "rollup", "--id", "bruh"]) { Ok(..) => panic!("Expected parsing to fail with missing required arguments"), Err(err) => { if let ContextValue::Strings(values) = err.get(ContextKind::InvalidArg).unwrap() { @@ -417,10 +567,14 @@ mod tests { #[derive(Parser)] struct Cli { #[command(flatten)] - args: InitArgs, + args: InitCommand, } - Cli::parse_from(["init", "--id", "bruh", "--sovereign"]); + let result = Cli::parse_from(["init", "sovereign", "--id", "bruh"]); + + assert_matches!(result.args.mode, InitMode::Sovereign(config) => { + assert_eq!(config.id, Some("bruh".to_string())); + }); } #[test] @@ -428,12 +582,13 @@ mod tests { #[derive(Parser)] struct Cli { #[command(flatten)] - args: InitArgs, + args: InitCommand, } let custom_settlement_fact_registry = "0x1234567890123456789012345678901234567890"; let result = Cli::parse_from([ "init", + "rollup", "--id", "wot", "--settlement-chain", @@ -445,10 +600,13 @@ mod tests { "--settlement-facts-registry", custom_settlement_fact_registry, ]); - assert_eq!( - result.args.settlement_facts_registry_contract, - Some(ContractAddress::from_str(custom_settlement_fact_registry).unwrap()) - ); + + assert_matches!(result.args.mode, InitMode::Rollup(config) => { + assert_eq!( + config.settlement_facts_registry_contract, + Some(ContractAddress::from_str(custom_settlement_fact_registry).unwrap()) + ); + }); } #[test] @@ -456,7 +614,7 @@ mod tests { #[derive(Parser)] struct Cli { #[command(flatten)] - args: InitArgs, + args: InitCommand, } // This should fail with the expected error message:- @@ -469,6 +627,7 @@ mod tests { // ``` match Cli::try_parse_from([ "init", + "rollup", "--id", "wot", "--settlement-facts-registry", @@ -499,11 +658,12 @@ mod tests { #[derive(Parser)] struct Cli { #[command(flatten)] - args: InitArgs, + args: InitCommand, } let result = Cli::parse_from([ "init", + "rollup", "--id", "wot", "--settlement-chain", @@ -513,16 +673,18 @@ mod tests { "--settlement-account-private-key", "0x1234567890123456789012345678901234567890", ]); - assert_eq!(result.args.settlement_facts_registry_contract, None); - - let configure_result = result.args.configure_from_args().await; - assert!(configure_result.is_some()); - let configure_result = configure_result.unwrap(); - assert!(configure_result.is_err()); - assert_eq!( - configure_result.unwrap_err().to_string(), - "Specifying the facts registry contract (using `--settlement-facts-registry`) is \ - required when settling on a custom chain" - ); + assert_matches!(result.args.mode, InitMode::Rollup(config) => { + assert_eq!(config.settlement_facts_registry_contract, None); + + let configure_result = config.configure_from_args().await; + assert!(configure_result.is_some()); + let configure_result = configure_result.unwrap(); + assert!(configure_result.is_err()); + assert_eq!( + configure_result.unwrap_err().to_string(), + "Specifying the facts registry contract (using `--settlement-facts-registry`) is \ + required when settling on a custom chain" + ); + }); } } diff --git a/bin/katana/src/cli/init/prompt.rs b/bin/katana/src/cli/init/prompt.rs index d6cbd76b6..6d4f60cc8 100644 --- a/bin/katana/src/cli/init/prompt.rs +++ b/bin/katana/src/cli/init/prompt.rs @@ -14,12 +14,12 @@ use starknet::providers::Provider; use starknet::signers::{LocalWallet, SigningKey}; use tokio::runtime::Handle; -use super::{deployment, AnyOutcome, PersistentOutcome, SovereignOutcome}; +use super::{deployment, PersistentOutcome, SovereignOutcome}; use crate::cli::init::deployment::DeploymentOutcome; use crate::cli::init::settlement::SettlementChainProvider; use crate::cli::init::slot::{self, PaymasterAccountArgs}; -pub async fn prompt() -> Result { +pub async fn prompt_rollup() -> Result { let chain_id = CustomType::::new("Id") .with_help_message("This will be the id of your rollup chain.") // checks that the input is a valid ascii string. @@ -37,7 +37,6 @@ pub async fn prompt() -> Result { enum SettlementChainOpt { Mainnet, Sepolia, - Sovereign, #[cfg(feature = "init-custom-settlement-chain")] Custom, } @@ -49,7 +48,6 @@ pub async fn prompt() -> Result { let network_opts = vec![ SettlementChainOpt::Mainnet, SettlementChainOpt::Sepolia, - SettlementChainOpt::Sovereign, #[cfg(feature = "init-custom-settlement-chain")] SettlementChainOpt::Custom, ]; @@ -61,16 +59,6 @@ pub async fn prompt() -> Result { let settlement_provider = match network_type { SettlementChainOpt::Mainnet => SettlementChainProvider::sn_mainnet(), SettlementChainOpt::Sepolia => SettlementChainProvider::sn_sepolia(), - - SettlementChainOpt::Sovereign => { - let slot_paymasters = prompt_slot_paymasters()?; - return Ok(AnyOutcome::Sovereign(SovereignOutcome { - id: chain_id, - #[cfg(feature = "init-slot")] - slot_paymasters, - })); - } - // Useful for testing the program flow without having to run it against actual network. #[cfg(feature = "init-custom-settlement-chain")] SettlementChainOpt::Custom => { @@ -140,38 +128,35 @@ pub async fn prompt() -> Result { // The core settlement contract on L1c. // Prompt the user whether to deploy the settlement contract or not. - let deployment_outcome = if Confirm::new("Deploy settlement contract?") - .with_default(true) - .prompt()? - { - let chain_id = cairo_short_string_to_felt(&chain_id)?; - deployment::deploy_settlement_contract(account, chain_id).await? - } - // If denied, prompt the user for an already deployed contract. - else { - let address = CustomType::::new("Settlement contract") - .with_parser(contract_exist_parser) - .prompt()?; + let deployment_outcome = + if Confirm::new("Deploy settlement contract?").with_default(true).prompt()? { + let chain_id = cairo_short_string_to_felt(&chain_id)?; + deployment::deploy_settlement_contract(account, chain_id).await? + } + // If denied, prompt the user for an already deployed contract. + else { + let address = CustomType::::new("Settlement contract") + .with_parser(contract_exist_parser) + .prompt()?; - // Check that the settlement contract has been initialized with the correct program - // info. - let chain_id = cairo_short_string_to_felt(&chain_id)?; - deployment::check_program_info(chain_id, address.into(), &settlement_provider) - .await - .context( + // Check that the settlement contract has been initialized with the correct program + // info. + let chain_id = cairo_short_string_to_felt(&chain_id)?; + deployment::check_program_info(chain_id, address, &settlement_provider).await.context( "Invalid settlement contract. The contract might have been configured incorrectly.", )?; - let block_number = CustomType::::new("Settlement contract deployment block") - .with_help_message("The block at which the settlement contract was deployed") - .prompt()?; + let block_number = + CustomType::::new("Settlement contract deployment block") + .with_help_message("The block at which the settlement contract was deployed") + .prompt()?; - DeploymentOutcome { contract_address: address, block_number } - }; + DeploymentOutcome { contract_address: address, block_number } + }; let slot_paymasters = prompt_slot_paymasters()?; - Ok(AnyOutcome::Persistent(PersistentOutcome { + Ok(PersistentOutcome { id: chain_id, deployment_outcome, rpc_url: settlement_provider.url().clone(), @@ -179,7 +164,30 @@ pub async fn prompt() -> Result { settlement_id: parse_cairo_short_string(&l1_chain_id)?, #[cfg(feature = "init-slot")] slot_paymasters, - })) + }) +} + +pub async fn prompt_sovereign() -> Result { + let chain_id = CustomType::::new("Id") + .with_help_message("This will be the id of your sovereign chain.") + // checks that the input is a valid ascii string. + .with_parser(&|input| { + if !input.is_empty() && input.is_ascii() { + Ok(input.to_string()) + } else { + Err(()) + } + }) + .with_error_message("Must be valid ASCII characters") + .prompt()?; + + let slot_paymasters = prompt_slot_paymasters()?; + + Ok(SovereignOutcome { + id: chain_id, + #[cfg(feature = "init-slot")] + slot_paymasters, + }) } fn prompt_slot_paymasters() -> Result>> { diff --git a/bin/katana/src/cli/mod.rs b/bin/katana/src/cli/mod.rs index 7c8949b2f..759a2cef4 100644 --- a/bin/katana/src/cli/mod.rs +++ b/bin/katana/src/cli/mod.rs @@ -46,7 +46,7 @@ impl Cli { #[derive(Debug, Subcommand)] enum Commands { #[command(about = "Initialize chain")] - Init(Box), + Init(Box), #[command(about = "Chain configuration utilities")] Config(config::ConfigArgs), diff --git a/bin/katana/src/main.rs b/bin/katana/src/main.rs index b11b1075f..bb6ee33f1 100644 --- a/bin/katana/src/main.rs +++ b/bin/katana/src/main.rs @@ -2,7 +2,7 @@ use clap::Parser; fn main() { if let Err(err) = katana::cli::Cli::parse().run() { - eprintln!("\x1b[31merror:\x1b[0m {err}"); + eprintln!("\x1b[31merror:\x1b[0m {err:?}"); std::process::exit(1); } } From 648ce4f0f02f2047992a106cdea12338d13955ea Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 15:07:53 -0400 Subject: [PATCH 5/9] chore: update dojo to v1.7.0 and scarb to 2.12.2 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/test.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e6fe0a3c0..263244306 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -321,12 +321,12 @@ jobs: uses: actions/checkout@v3 with: repository: dojoengine/dojo - ref: v1.7.0-alpha.2 + ref: v1.7.0 path: dojo - uses: software-mansion/setup-scarb@v1 with: - scarb-version: "dev-2025-09-05" + scarb-version: "2.12.2" - name: Build and migrate `spawn-and-move` project run: | From d038c09a1832c056cfb247717260a9d71776f979 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 15:15:19 -0400 Subject: [PATCH 6/9] chore: use prebuilt sozo binary from GitHub release MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace building sozo from source with downloading the prebuilt binary from Dojo v1.7.0 GitHub release. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/test.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 263244306..8fe0aae91 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,12 +328,15 @@ jobs: with: scarb-version: "2.12.2" - - name: Build and migrate `spawn-and-move` project + - name: Download and setup Sozo binary run: | - cd dojo - cargo install --path bin/sozo --locked --force + curl -L https://github.com/dojoengine/dojo/releases/download/v1.7.0/dojo_v1.7.0_linux_amd64.tar.gz | tar -xz + chmod +x sozo + sudo mv sozo /usr/local/bin/ - cd examples/spawn-and-move + - name: Build and migrate `spawn-and-move` project + run: | + cd dojo/examples/spawn-and-move sozo build && sozo migrate - name: Output Katana logs on failure From eebcab5447f27ffd5ef12a1d3eef3f9c0acd8527 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 15:41:36 -0400 Subject: [PATCH 7/9] fix: remove sudo from sozo binary setup --- .github/workflows/test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8fe0aae91..990eb6d70 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -332,7 +332,7 @@ jobs: run: | curl -L https://github.com/dojoengine/dojo/releases/download/v1.7.0/dojo_v1.7.0_linux_amd64.tar.gz | tar -xz chmod +x sozo - sudo mv sozo /usr/local/bin/ + mv sozo /usr/local/bin/ - name: Build and migrate `spawn-and-move` project run: | From a64ef069e2d4e5cb94cecab175d2c02f94b893ff Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 16:14:22 -0400 Subject: [PATCH 8/9] revert: build sozo from source due to GLIBC incompatibility MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prebuilt binary requires GLIBC 2.38/2.39 which is not available in the CI container. Added comment explaining why we need to build from source. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/test.yml | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 990eb6d70..b6bca1067 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -328,11 +328,13 @@ jobs: with: scarb-version: "2.12.2" - - name: Download and setup Sozo binary + # Note: We build sozo from source instead of downloading the prebuilt binary + # because the GitHub release artifacts require GLIBC 2.38/2.39 which is not + # available in our CI container (ghcr.io/dojoengine/katana-dev:latest) + - name: Build sozo from source run: | - curl -L https://github.com/dojoengine/dojo/releases/download/v1.7.0/dojo_v1.7.0_linux_amd64.tar.gz | tar -xz - chmod +x sozo - mv sozo /usr/local/bin/ + cd dojo + cargo install --path bin/sozo --locked --force - name: Build and migrate `spawn-and-move` project run: | From ecc54324277cab89b49c8aaaa9237d9219fca854 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 23 Sep 2025 17:48:26 -0400 Subject: [PATCH 9/9] fix: use Rust 1.88 to build sozo from source MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Sozo v1.7.0 requires Rust 1.88+ due to clap dependency API changes. The CI container uses Rust 1.86 which causes compilation errors. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude --- .github/workflows/test.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b6bca1067..789d4cd24 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -331,6 +331,12 @@ jobs: # Note: We build sozo from source instead of downloading the prebuilt binary # because the GitHub release artifacts require GLIBC 2.38/2.39 which is not # available in our CI container (ghcr.io/dojoengine/katana-dev:latest) + # We need to use Rust 1.88+ due to clap dependency requirements in sozo v1.7.0 + - name: Install Rust 1.88 for sozo build + run: | + rustup toolchain install 1.88.0 + rustup default 1.88.0 + - name: Build sozo from source run: | cd dojo