From 92c0df1d86b0368fe740d91d2c7dfba27e4ce09c Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Thu, 14 May 2026 10:37:17 -0500 Subject: [PATCH] fix(ops): pick CASM hash algorithm from settlement chain version Starknet v0.14.1 switched the canonical compiled_class_hash from Poseidon to Blake2s and rejects declares that still use the old hash, mirroring the issue fixed for `katana init rollup` in dojoengine/katana#570. `saya-ops core-contract declare*` hardcoded `use_blake2s: true` in `prepare_class*`, which would have failed against any pre-0.14.1 settlement chain. The declare path now reads the settlement chain's `starknet_version` from the latest block and selects Blake2s for >= 0.14.1, Poseidon otherwise. Verified end-to-end by declaring the Piltover core contract against api.cartridge.gg/x/starknet/sepolia (tx 0x3d0a1eafb039b3f6f81d9df5fe71892a9eb0101f746f685059b902fc410c374, ACCEPTED_ON_L2 / SUCCEEDED). Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/ops/src/core_contract/utils.rs | 63 +++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/bin/ops/src/core_contract/utils.rs b/bin/ops/src/core_contract/utils.rs index 112158f..bf4df26 100644 --- a/bin/ops/src/core_contract/utils.rs +++ b/bin/ops/src/core_contract/utils.rs @@ -8,12 +8,15 @@ use anyhow::Result; use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass; use cairo_lang_starknet_classes::contract_class::ContractClass; use dojo_utils::{Declarer, Deployer, Invoker, LabeledClass, TransactionResult, TxnConfig}; -use starknet::accounts::{Account, SingleOwnerAccount}; +use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount}; use starknet::core::crypto::compute_hash_on_elements; -use starknet::core::types::{contract::SierraClass, Call, Felt, FlattenedSierraClass}; +use starknet::core::types::{ + contract::SierraClass, BlockId, BlockTag, Call, Felt, FlattenedSierraClass, + MaybePreConfirmedBlockWithTxHashes, +}; use starknet::macros::{selector, short_string}; use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; +use starknet::providers::{JsonRpcClient, Provider}; use starknet::signers::LocalWallet; use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass}; use std::{fs, path::Path}; @@ -32,8 +35,9 @@ pub async fn declare_contract( ) -> Result<(Felt, TransactionResult)> { let txn_config = TxnConfig::default(); + let use_blake2s = chain_uses_blake2s_casm_hash(account.provider()).await?; let mut declarer = Declarer::new(account, txn_config); - let class = prepare_class(contract_path, true)?; + let class = prepare_class(contract_path, use_blake2s)?; let labeled = LabeledClass { label: class.label.clone(), casm_class_hash: class.casm_class_hash, @@ -66,8 +70,9 @@ pub async fn declare_contract_from_bytes( ) -> Result<(Felt, TransactionResult)> { let txn_config = TxnConfig::default(); + let use_blake2s = chain_uses_blake2s_casm_hash(account.provider()).await?; let mut declarer = Declarer::new(account, txn_config); - let class = prepare_class_from_bytes(contract_bytes, true, contract_name.to_string())?; + let class = prepare_class_from_bytes(contract_bytes, use_blake2s, contract_name.to_string())?; let labeled = LabeledClass { label: class.label.clone(), casm_class_hash: class.casm_class_hash, @@ -292,6 +297,36 @@ fn casm_class_hash_from_bytes(data: &[u8], use_blake2s: bool) -> Result { Ok(Felt::from_bytes_be(&hash.0.to_bytes_be())) } +/// Returns whether the settlement chain expects the Blake2s-based compiled +/// class hash for `declare` transactions. +/// +/// Starknet v0.14.1 switched the canonical compiled class hash from Poseidon +/// to Blake2s and rejects declares using the old algorithm; earlier versions +/// still expect Poseidon. The decision is sourced from the chain's +/// `starknet_version` on the latest block header so it auto-adapts when the +/// settlement chain upgrades. +async fn chain_uses_blake2s_casm_hash(provider: &P) -> Result { + let block = provider + .get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest)) + .await?; + let version_str = match block { + MaybePreConfirmedBlockWithTxHashes::Block(b) => b.starknet_version, + MaybePreConfirmedBlockWithTxHashes::PreConfirmedBlock(b) => b.starknet_version, + }; + Ok(version_at_least_v0_14_1(&version_str)) +} + +/// Parses `MAJOR.MINOR.PATCH[.BUILD]` and returns true if it is at least +/// `0.14.1`. Strings that don't have at least three numeric components fall +/// back to `false` so the caller uses the legacy Poseidon hash. +fn version_at_least_v0_14_1(version: &str) -> bool { + let parts: Vec = version.split('.').map(|s| s.parse().unwrap_or(0)).collect(); + match parts.as_slice() { + [major, minor, patch, ..] => (*major, *minor, *patch) >= (0, 14, 1), + _ => false, + } +} + #[cfg(test)] mod test { use super::*; @@ -319,4 +354,22 @@ mod test { assert_eq!(computed, expected); } + + #[test] + fn version_comparison_picks_blake2s_for_0_14_1_and_above() { + assert!(version_at_least_v0_14_1("0.14.1")); + assert!(version_at_least_v0_14_1("0.14.2")); + assert!(version_at_least_v0_14_1("0.15.0")); + assert!(version_at_least_v0_14_1("1.0.0")); + assert!(version_at_least_v0_14_1("0.14.1.0")); + } + + #[test] + fn version_comparison_picks_poseidon_for_pre_0_14_1() { + assert!(!version_at_least_v0_14_1("0.13.4")); + assert!(!version_at_least_v0_14_1("0.14.0")); + assert!(!version_at_least_v0_14_1("0.13.2")); + assert!(!version_at_least_v0_14_1("")); + assert!(!version_at_least_v0_14_1("not-a-version")); + } }