Skip to content

Commit 8a5dc79

Browse files
kariyclaude
andauthored
fix(ops): pick CASM hash algorithm from settlement chain version (#76)
Starknet v0.14.1 switched the canonical `compiled_class_hash` from Poseidon to Blake2s and rejects declares using the old algorithm. `saya-ops core-contract declare*` was hardcoding `use_blake2s: true` in `prepare_class*`, which would silently produce the wrong hash on any pre-0.14.1 settlement chain. --- Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 1e5f5ef commit 8a5dc79

1 file changed

Lines changed: 58 additions & 5 deletions

File tree

bin/ops/src/core_contract/utils.rs

Lines changed: 58 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,12 +8,15 @@ use anyhow::Result;
88
use cairo_lang_starknet_classes::casm_contract_class::CasmContractClass;
99
use cairo_lang_starknet_classes::contract_class::ContractClass;
1010
use dojo_utils::{Declarer, Deployer, Invoker, LabeledClass, TransactionResult, TxnConfig};
11-
use starknet::accounts::{Account, SingleOwnerAccount};
11+
use starknet::accounts::{Account, ConnectedAccount, SingleOwnerAccount};
1212
use starknet::core::crypto::compute_hash_on_elements;
13-
use starknet::core::types::{contract::SierraClass, Call, Felt, FlattenedSierraClass};
13+
use starknet::core::types::{
14+
contract::SierraClass, BlockId, BlockTag, Call, Felt, FlattenedSierraClass,
15+
MaybePreConfirmedBlockWithTxHashes,
16+
};
1417
use starknet::macros::{selector, short_string};
1518
use starknet::providers::jsonrpc::HttpTransport;
16-
use starknet::providers::JsonRpcClient;
19+
use starknet::providers::{JsonRpcClient, Provider};
1720
use starknet::signers::LocalWallet;
1821
use starknet_api::contract_class::compiled_class_hash::{HashVersion, HashableCompiledClass};
1922
use std::{fs, path::Path};
@@ -32,8 +35,9 @@ pub async fn declare_contract(
3235
) -> Result<(Felt, TransactionResult)> {
3336
let txn_config = TxnConfig::default();
3437

38+
let use_blake2s = chain_uses_blake2s_casm_hash(account.provider()).await?;
3539
let mut declarer = Declarer::new(account, txn_config);
36-
let class = prepare_class(contract_path, true)?;
40+
let class = prepare_class(contract_path, use_blake2s)?;
3741
let labeled = LabeledClass {
3842
label: class.label.clone(),
3943
casm_class_hash: class.casm_class_hash,
@@ -66,8 +70,9 @@ pub async fn declare_contract_from_bytes(
6670
) -> Result<(Felt, TransactionResult)> {
6771
let txn_config = TxnConfig::default();
6872

73+
let use_blake2s = chain_uses_blake2s_casm_hash(account.provider()).await?;
6974
let mut declarer = Declarer::new(account, txn_config);
70-
let class = prepare_class_from_bytes(contract_bytes, true, contract_name.to_string())?;
75+
let class = prepare_class_from_bytes(contract_bytes, use_blake2s, contract_name.to_string())?;
7176
let labeled = LabeledClass {
7277
label: class.label.clone(),
7378
casm_class_hash: class.casm_class_hash,
@@ -292,6 +297,36 @@ fn casm_class_hash_from_bytes(data: &[u8], use_blake2s: bool) -> Result<Felt> {
292297
Ok(Felt::from_bytes_be(&hash.0.to_bytes_be()))
293298
}
294299

300+
/// Returns whether the settlement chain expects the Blake2s-based compiled
301+
/// class hash for `declare` transactions.
302+
///
303+
/// Starknet v0.14.1 switched the canonical compiled class hash from Poseidon
304+
/// to Blake2s and rejects declares using the old algorithm; earlier versions
305+
/// still expect Poseidon. The decision is sourced from the chain's
306+
/// `starknet_version` on the latest block header so it auto-adapts when the
307+
/// settlement chain upgrades.
308+
async fn chain_uses_blake2s_casm_hash<P: Provider>(provider: &P) -> Result<bool> {
309+
let block = provider
310+
.get_block_with_tx_hashes(BlockId::Tag(BlockTag::Latest))
311+
.await?;
312+
let version_str = match block {
313+
MaybePreConfirmedBlockWithTxHashes::Block(b) => b.starknet_version,
314+
MaybePreConfirmedBlockWithTxHashes::PreConfirmedBlock(b) => b.starknet_version,
315+
};
316+
Ok(version_at_least_v0_14_1(&version_str))
317+
}
318+
319+
/// Parses `MAJOR.MINOR.PATCH[.BUILD]` and returns true if it is at least
320+
/// `0.14.1`. Strings that don't have at least three numeric components fall
321+
/// back to `false` so the caller uses the legacy Poseidon hash.
322+
fn version_at_least_v0_14_1(version: &str) -> bool {
323+
let parts: Vec<u64> = version.split('.').map(|s| s.parse().unwrap_or(0)).collect();
324+
match parts.as_slice() {
325+
[major, minor, patch, ..] => (*major, *minor, *patch) >= (0, 14, 1),
326+
_ => false,
327+
}
328+
}
329+
295330
#[cfg(test)]
296331
mod test {
297332
use super::*;
@@ -319,4 +354,22 @@ mod test {
319354

320355
assert_eq!(computed, expected);
321356
}
357+
358+
#[test]
359+
fn version_comparison_picks_blake2s_for_0_14_1_and_above() {
360+
assert!(version_at_least_v0_14_1("0.14.1"));
361+
assert!(version_at_least_v0_14_1("0.14.2"));
362+
assert!(version_at_least_v0_14_1("0.15.0"));
363+
assert!(version_at_least_v0_14_1("1.0.0"));
364+
assert!(version_at_least_v0_14_1("0.14.1.0"));
365+
}
366+
367+
#[test]
368+
fn version_comparison_picks_poseidon_for_pre_0_14_1() {
369+
assert!(!version_at_least_v0_14_1("0.13.4"));
370+
assert!(!version_at_least_v0_14_1("0.14.0"));
371+
assert!(!version_at_least_v0_14_1("0.13.2"));
372+
assert!(!version_at_least_v0_14_1(""));
373+
assert!(!version_at_least_v0_14_1("not-a-version"));
374+
}
322375
}

0 commit comments

Comments
 (0)