From 5fd972322e11280229d9ca4b0a3ed916ecc1c1f1 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 27 Apr 2026 16:28:42 -0500 Subject: [PATCH 01/16] feat(rpc): bind TEE attestations to versioned environment config hash MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each `tee_generateQuote` response now carries a `katana_tee_config_hash` — a Pedersen-array hash of `[KatanaTeeConfig1, chain_id, fee_token]` mirroring the existing `compute_starknet_os_config_hash` shape — bound into both halves of SEV-SNP `report_data`: report_data[0..32] = Poseidon(KATANA_TEE_REPORT_VERSION, KATANA_TEE_{APPCHAIN,SHARDING}_MODE, ...transition fields..., katana_tee_config_hash) report_data[32..64] = katana_tee_config_hash (raw) The hash is precomputed at `TeeApi::new` from the chain spec — no caller-supplied input. This rejects attestations from a different appchain configuration when they reach the on-chain verifier (Piltover settlement), and removes the caller-misconfiguration class where the wrong hash failed far from its cause. Wire-format changes (v1-only — legacy zero-config reports are no longer emitted): - `tee_generateQuote(prev, block, Option)` -> `tee_generateQuote(prev, block)` - `--katana-tee-config-hash` CLI flag removed from `katana rpc tee generate-quote` - Legacy 7-field appchain / 8-field sharding Poseidon shapes deleted - Test docs no longer reference downstream consumers (saya-tee, katana_tee_client) Tests: 9 -> 10 (10/10 pass). New `generate_quote_precomputed_config_hash_binding` asserts the configured hash appears in both halves of `report_data` and round-trips through the response. Co-Authored-By: Claude Opus 4.7 (1M context) --- bin/katana/src/cli/rpc/tee.rs | 3 +- crates/node/sequencer/src/lib.rs | 25 ++- crates/rpc/rpc-api/src/tee.rs | 62 ++++-- crates/rpc/rpc-server/src/tee.rs | 68 +++++-- crates/rpc/rpc-server/tests/tee.rs | 311 ++++++++++++++++++++++++----- 5 files changed, 378 insertions(+), 91 deletions(-) diff --git a/bin/katana/src/cli/rpc/tee.rs b/bin/katana/src/cli/rpc/tee.rs index f7862f28c..f61eee718 100644 --- a/bin/katana/src/cli/rpc/tee.rs +++ b/bin/katana/src/cli/rpc/tee.rs @@ -44,7 +44,8 @@ impl TeeCommands { pub async fn execute(self, client: &Client) -> Result<()> { match self { TeeCommands::GenerateQuote(args) => { - let result = client.tee_generate_quote(args.prev_block_id, args.block_id).await?; + let result = + client.tee_generate_quote(args.prev_block_id, args.block_id).await?; println!("{}", colored_json::to_colored_json_auto(&result)?); } diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index e95fb0e89..0c75910d1 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -51,6 +51,7 @@ use katana_rpc_server::middleware::cartridge::{ControllerDeploymentLayer, VrfLay use katana_rpc_server::middleware::cors::Cors; use katana_rpc_server::middleware::logger::RpcLoggerLayer; use katana_rpc_server::middleware::metrics::RpcServerMetricsLayer; +use katana_rpc_api::tee::compute_katana_tee_config_hash; use katana_rpc_server::node::NodeApi; use katana_rpc_server::paymaster::PaymasterProxy; use katana_rpc_server::starknet::{RpcCache, StarknetApi, StarknetApiConfig}; @@ -394,12 +395,32 @@ where } #[cfg(feature = "tee-mock")] TeeProviderType::Mock => Arc::new(katana_tee::MockProvider::new()), + // The `Mock` variant on `TeeProviderType` is gated on + // `feature = "tee-mock"` in `katana-tee`, but cargo features + // unify across the workspace — so the variant can be visible + // here even when this crate's own `tee-mock` feature is off. + #[allow(unreachable_patterns)] + _ => anyhow::bail!("Mock TEE provider requires the 'tee-mock' feature"), }; - let api = TeeApi::new(provider.clone(), tee_provider, tee_config.fork_block_number); + let katana_tee_config_hash = compute_katana_tee_config_hash( + backend.chain_spec.id().into(), + backend.chain_spec.fee_contracts().strk.into(), + ); + let api = TeeApi::new( + provider.clone(), + tee_provider, + tee_config.fork_block_number, + katana_tee_config_hash, + ); rpc_modules.merge(TeeApiServer::into_rpc(api))?; - info!(target: "node", provider = ?tee_config.provider_type, "TEE API enabled"); + info!( + target: "node", + provider = ?tee_config.provider_type, + %katana_tee_config_hash, + "TEE API enabled" + ); } // --- build rpc middleware diff --git a/crates/rpc/rpc-api/src/tee.rs b/crates/rpc/rpc-api/src/tee.rs index ab95ff71f..9beb11c66 100644 --- a/crates/rpc/rpc-api/src/tee.rs +++ b/crates/rpc/rpc-api/src/tee.rs @@ -1,18 +1,47 @@ use jsonrpsee::core::RpcResult; use jsonrpsee::proc_macros::rpc; use katana_primitives::block::{BlockHash, BlockNumber}; +use katana_primitives::cairo::ShortString; +use katana_primitives::hash::{Pedersen, StarkHash}; use katana_primitives::Felt; use katana_rpc_types::trie::Nodes; use serde::{Deserialize, Serialize}; +/// Version tag for the Katana TEE environment config hash. +pub const KATANA_TEE_CONFIG_VERSION: ShortString = ShortString::from_ascii("KatanaTeeConfig1"); + +/// Version tag for the v1 Katana TEE report-data schema. +pub const KATANA_TEE_REPORT_VERSION: ShortString = ShortString::from_ascii("KatanaTeeReport1"); + +/// Mode tag for appchain settlement attestations. +pub const KATANA_TEE_APPCHAIN_MODE: ShortString = ShortString::from_ascii("KatanaTeeAppchain"); + +/// Mode tag for fork/sharding attestations. +pub const KATANA_TEE_SHARDING_MODE: ShortString = ShortString::from_ascii("KatanaTeeSharding"); + +/// Computes the Starknet-OS-style config hash bound into v1 TEE report data. +/// +/// This mirrors the Starknet OS pattern of hashing a version tag plus stable +/// environment fields. The non-legacy fee token address is the STRK fee token +/// address for the appchain/Katana network. +pub fn compute_katana_tee_config_hash( + appchain_chain_id_or_katana_chain_id: Felt, + fee_token_address: Felt, +) -> Felt { + Pedersen::hash_array(&[ + KATANA_TEE_CONFIG_VERSION.into(), + appchain_chain_id_or_katana_chain_id, + fee_token_address, + ]) +} + /// Serializes an `Option` as a `Felt`-encoded hex string, mapping /// `None` to `Felt::MAX` (the genesis "no previous block" sentinel that /// matches Piltover's `AppchainState` initial value). Deserializes the inverse: /// `Felt::MAX` becomes `None`, anything else becomes `Some(felt as u64)`. /// -/// The non-optional `block_number` field uses `serde_utils::serialize_as_hex` + -/// `serde_utils::deserialize_u64` directly. We can't reuse `serde_utils::{serialize_opt_as_hex, -/// deserialize_opt_u64}` here because those map `None ↔ null`, not `None ↔ Felt::MAX`. +/// The wire format is `Felt`, not JSON `null`, so the field can be hashed into +/// the on-chain commitment without a separate sentinel encoding step. mod prev_block_number_serde { use katana_primitives::block::BlockNumber; use katana_primitives::Felt; @@ -90,16 +119,14 @@ pub struct TeeQuoteResponse { /// The number of the previous block. Serialized as a `Felt`-encoded hex /// string (with `Felt::MAX` representing the genesis "no previous block" - /// case) so the JSON wire format matches what - /// `katana_tee_client::TeeQuoteResponse` (used by `saya-tee`) expects. - /// See [`prev_block_number_serde`]. + /// case) so the field can be hashed into the on-chain commitment without + /// a separate sentinel encoding step. See [`prev_block_number_serde`]. #[serde(with = "prev_block_number_serde")] pub prev_block_number: Option, /// The number of the attested block. Serialized as a `0x`-prefixed hex - /// string so the JSON wire format matches `Felt`'s default serde - /// representation, which is what `katana_tee_client::TeeQuoteResponse` - /// (typed as `Felt` upstream) expects. + /// string matching `Felt`'s default serde representation, so the field + /// can be hashed as a `Felt` on both sides of the wire. #[serde( serialize_with = "serde_utils::serialize_as_hex", deserialize_with = "serde_utils::deserialize_u64" @@ -112,7 +139,7 @@ pub struct TeeQuoteResponse { pub fork_block_number: Option, /// Merkle root of all events in the attested block. - /// Included in report_data: Poseidon(state_root, block_hash, fork_block, events_commitment). + /// Included in fork/sharding report_data. pub events_commitment: Felt, /// Poseidon commitment over all L1<->L2 messages from prev_block+1 to block_number. @@ -121,6 +148,11 @@ pub struct TeeQuoteResponse { /// commitment is `Poseidon` over the individual message hashes in that range. pub messages_commitment: Felt, + /// Versioned Katana TEE environment config hash attested in `report_data`. + /// + /// Always non-zero in v1 — precomputed by the node from its chain spec. + pub katana_tee_config_hash: Felt, + /// All L2→L1 messages emitted in the attested block range. pub l2_to_l1_messages: Vec, @@ -173,11 +205,13 @@ pub struct EventProofResponse { pub trait TeeApi { /// Generate a TEE attestation quote for the requested block state. /// - /// The quote includes a commitment to the requested block's state root - /// and block hash, allowing verifiers to cryptographically verify - /// that the state was attested from within a trusted execution environment. + /// The quote commits to the block's transition fields and to a versioned + /// `katana_tee_config_hash` that the node precomputes from its chain spec + /// at startup. Verifiers can recompute the same hash from on-chain config + /// and reject attestations bound to a different environment. /// - /// `prev_block_id` is optional and included in the response for transition-style flows. + /// `prev_block_id` is optional and included in the response for + /// transition-style flows; `None` is the genesis case. #[method(name = "generateQuote")] async fn generate_quote( &self, diff --git a/crates/rpc/rpc-server/src/tee.rs b/crates/rpc/rpc-server/src/tee.rs index 3c43f4f0b..522d48123 100644 --- a/crates/rpc/rpc-server/src/tee.rs +++ b/crates/rpc/rpc-server/src/tee.rs @@ -13,6 +13,7 @@ use katana_provider::ProviderFactory; use katana_rpc_api::error::tee::TeeApiError; use katana_rpc_api::tee::{ EventProofResponse, TeeApiServer, TeeL1ToL2Message, TeeL2ToL1Message, TeeQuoteResponse, + KATANA_TEE_APPCHAIN_MODE, KATANA_TEE_REPORT_VERSION, KATANA_TEE_SHARDING_MODE, }; use katana_tee::TeeProvider; use starknet_types_core::hash::{Poseidon, StarkHash}; @@ -31,6 +32,9 @@ where /// The block number Katana forked from (if running in fork mode). /// Included in report_data so SP1 can prove fork freshness. fork_block_number: Option, + /// Versioned environment config hash precomputed from the chain spec at + /// construction time. Bound into every attestation's `report_data`. + katana_tee_config_hash: Felt, } impl TeeApi @@ -42,14 +46,16 @@ where provider_factory: PF, tee_provider: Arc, fork_block_number: Option, + katana_tee_config_hash: Felt, ) -> Self { info!( target: "rpc::tee", provider_type = tee_provider.provider_type(), ?fork_block_number, + %katana_tee_config_hash, "TEE API initialized" ); - Self { provider_factory, tee_provider, fork_block_number } + Self { provider_factory, tee_provider, fork_block_number, katana_tee_config_hash } } } @@ -70,10 +76,13 @@ where prev_block: Option, block: BlockNumber, ) -> RpcResult { + let katana_tee_config_hash = self.katana_tee_config_hash; + debug!( target: "rpc::tee", ?prev_block, block, + %katana_tee_config_hash, "Generating TEE attestation quote" ); @@ -127,6 +136,7 @@ where block.into(), fork_block.into(), events_commitment, + katana_tee_config_hash, ); let quote = self @@ -140,6 +150,7 @@ where block_number = block, %prev_block_hash, %block_hash, + %katana_tee_config_hash, quote_size = quote.len(), "Generated TEE attestation quote" ); @@ -154,6 +165,7 @@ where block_number: block, fork_block_number: self.fork_block_number, events_commitment, + katana_tee_config_hash, l1_to_l2_messages: Vec::new(), l2_to_l1_messages: Vec::new(), messages_commitment: Felt::ZERO, @@ -241,6 +253,7 @@ where prev_block_id, block.into(), messages_commitment, + katana_tee_config_hash, ); let quote = self @@ -254,6 +267,7 @@ where block_number = block, %prev_block_hash, %block_hash, + %katana_tee_config_hash, quote_size = quote.len(), l2_to_l1_count = l2_to_l1_messages.len(), l1_to_l2_count = l1_to_l2_messages.len(), @@ -270,6 +284,7 @@ where block_number: block, fork_block_number: self.fork_block_number, events_commitment, + katana_tee_config_hash, l1_to_l2_messages, l2_to_l1_messages, messages_commitment, @@ -384,10 +399,12 @@ where } } -/// Compute the 64-byte report data for attestation. +/// Compute the 64-byte report data for fork/sharding attestation (v1 schema). /// /// ```text -/// Poseidon( +/// commitment = Poseidon( +/// KATANA_TEE_REPORT_VERSION, +/// KATANA_TEE_SHARDING_MODE, /// prev_state_root, /// state_root, /// prev_block_hash, @@ -396,8 +413,9 @@ where /// block_number, /// fork_block_number, /// events_commitment, +/// katana_tee_config_hash, /// ) -/// report_data = commitment_bytes_be ++ [0u8; 32] // 64 bytes total +/// report_data = commitment_bytes_be ++ katana_tee_config_hash_bytes_be /// ``` #[allow(clippy::too_many_arguments)] fn compute_report_data_sharding( @@ -409,9 +427,11 @@ fn compute_report_data_sharding( block_number: Felt, fork_block_number: Felt, events_commitment: Felt, + katana_tee_config_hash: Felt, ) -> [u8; 64] { - // Compute Poseidon hash of state_root and block_hash let commitment = Poseidon::hash_array(&[ + KATANA_TEE_REPORT_VERSION.into(), + KATANA_TEE_SHARDING_MODE.into(), prev_state_root, state_root, prev_block_hash, @@ -420,15 +440,10 @@ fn compute_report_data_sharding( block_number, fork_block_number, events_commitment, + katana_tee_config_hash, ]); - // Convert Felt to bytes (32 bytes) and pad to 64 bytes - let commitment_bytes = commitment.to_bytes_be(); - - let mut report_data = [0u8; 64]; - // Place the 32-byte hash in the first half - report_data[..32].copy_from_slice(&commitment_bytes); - // Second half remains zeros + let report_data = encode_report_data(commitment, katana_tee_config_hash); debug!( target: "rpc::tee", @@ -436,6 +451,7 @@ fn compute_report_data_sharding( %block_hash, ?fork_block_number, %events_commitment, + %katana_tee_config_hash, %commitment, "Computed report data for attestation" ); @@ -443,19 +459,22 @@ fn compute_report_data_sharding( report_data } -/// Compute the 64-byte report data for attestation. +/// Compute the 64-byte report data for appchain attestation (v1 schema). /// /// ```text -/// Poseidon( +/// commitment = Poseidon( +/// KATANA_TEE_REPORT_VERSION, +/// KATANA_TEE_APPCHAIN_MODE, /// prev_state_root, /// state_root, /// prev_block_hash, /// block_hash, /// prev_block_number, /// block_number, -/// messages_commitment +/// messages_commitment, +/// katana_tee_config_hash, /// ) -/// report_data = commitment_bytes_be ++ [0u8; 32] // 64 bytes total +/// report_data = commitment_bytes_be ++ katana_tee_config_hash_bytes_be /// ``` fn compute_report_data_appchain( prev_state_root: Felt, @@ -465,8 +484,11 @@ fn compute_report_data_appchain( prev_block_number: Felt, block_number: Felt, messages_commitment: Felt, + katana_tee_config_hash: Felt, ) -> [u8; 64] { let commitment = Poseidon::hash_array(&[ + KATANA_TEE_REPORT_VERSION.into(), + KATANA_TEE_APPCHAIN_MODE.into(), prev_state_root, state_root, prev_block_hash, @@ -474,20 +496,26 @@ fn compute_report_data_appchain( prev_block_number, block_number, messages_commitment, + katana_tee_config_hash, ]); - let commitment_bytes = commitment.to_bytes_be(); - - let mut report_data = [0u8; 64]; - report_data[..32].copy_from_slice(&commitment_bytes); + let report_data = encode_report_data(commitment, katana_tee_config_hash); debug!( target: "rpc::tee", %state_root, %block_hash, + %katana_tee_config_hash, %commitment, "Computed report data for attestation" ); report_data } + +fn encode_report_data(commitment: Felt, katana_tee_config_hash: Felt) -> [u8; 64] { + let mut report_data = [0u8; 64]; + report_data[..32].copy_from_slice(&commitment.to_bytes_be()); + report_data[32..].copy_from_slice(&katana_tee_config_hash.to_bytes_be()); + report_data +} diff --git a/crates/rpc/rpc-server/tests/tee.rs b/crates/rpc/rpc-server/tests/tee.rs index 4ac4d8b23..f5ac916e7 100644 --- a/crates/rpc/rpc-server/tests/tee.rs +++ b/crates/rpc/rpc-server/tests/tee.rs @@ -13,6 +13,10 @@ use std::sync::Arc; use assert_matches::assert_matches; +use jsonrpsee::core::client::ClientT; +use jsonrpsee::http_client::HttpClientBuilder; +use jsonrpsee::rpc_params; +use jsonrpsee::server::ServerBuilder; use katana_primitives::block::{Block, BlockNumber, FinalityStatus, Header, SealedBlockWithStatus}; use katana_primitives::fee::FeeInfo; use katana_primitives::hash::{Poseidon, StarkHash}; @@ -23,7 +27,10 @@ use katana_primitives::transaction::{InvokeTx, InvokeTxV1, L1HandlerTx, Tx, TxWi use katana_primitives::{address, felt, ContractAddress, Felt, B256}; use katana_provider::api::block::BlockWriter; use katana_provider::{DbProviderFactory, MutableProvider, ProviderFactory}; -use katana_rpc_api::tee::{TeeApiServer, TeeL1ToL2Message, TeeL2ToL1Message}; +use katana_rpc_api::tee::{ + compute_katana_tee_config_hash, TeeApiServer, TeeL1ToL2Message, TeeL2ToL1Message, + KATANA_TEE_APPCHAIN_MODE, KATANA_TEE_REPORT_VERSION, KATANA_TEE_SHARDING_MODE, +}; use katana_rpc_server::tee::TeeApi; use katana_tee::MockProvider; @@ -33,7 +40,12 @@ fn mock_api( factory: DbProviderFactory, fork_block_number: Option, ) -> TeeApi { - TeeApi::new(factory, Arc::new(MockProvider::new()), fork_block_number) + TeeApi::new( + factory, + Arc::new(MockProvider::new()), + fork_block_number, + sample_katana_tee_config_hash(), + ) } fn make_block( @@ -67,18 +79,54 @@ fn extract_report_data(quote_hex: &str) -> [u8; 64] { out } -fn appchain_report_data(fields: [Felt; 7]) -> [u8; 64] { +fn appchain_report_data_v1(fields: [Felt; 7], katana_tee_config_hash: Felt) -> [u8; 64] { + let commitment = Poseidon::hash_array(&[ + KATANA_TEE_REPORT_VERSION.into(), + KATANA_TEE_APPCHAIN_MODE.into(), + fields[0], + fields[1], + fields[2], + fields[3], + fields[4], + fields[5], + fields[6], + katana_tee_config_hash, + ]); + let mut out = [0u8; 64]; - out[..32].copy_from_slice(&Poseidon::hash_array(&fields).to_bytes_be()); + out[..32].copy_from_slice(&commitment.to_bytes_be()); + out[32..].copy_from_slice(&katana_tee_config_hash.to_bytes_be()); out } -fn sharding_report_data(fields: [Felt; 8]) -> [u8; 64] { +fn sharding_report_data_v1(fields: [Felt; 8], katana_tee_config_hash: Felt) -> [u8; 64] { + let commitment = Poseidon::hash_array(&[ + KATANA_TEE_REPORT_VERSION.into(), + KATANA_TEE_SHARDING_MODE.into(), + fields[0], + fields[1], + fields[2], + fields[3], + fields[4], + fields[5], + fields[6], + fields[7], + katana_tee_config_hash, + ]); + let mut out = [0u8; 64]; - out[..32].copy_from_slice(&Poseidon::hash_array(&fields).to_bytes_be()); + out[..32].copy_from_slice(&commitment.to_bytes_be()); + out[32..].copy_from_slice(&katana_tee_config_hash.to_bytes_be()); out } +fn sample_katana_tee_config_hash() -> Felt { + compute_katana_tee_config_hash( + felt!("0x4b4154414e41"), // "KATANA" + felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), + ) +} + fn empty_messages_commitment() -> Felt { Poseidon::hash_array(&[Poseidon::hash_array(&[]), Poseidon::hash_array(&[])]) } @@ -138,9 +186,9 @@ fn l1_handler_tx( // ---------- tests ---------- /// Genesis path: `prev_block = None` folds to the `(Felt::MAX, ZERO, ZERO)` sentinel -/// and the appchain `report_data` formula is used. Asserts the full response shape and -/// that the mock quote's embedded `report_data` matches the formula applied to the -/// inserted block's hash/roots. +/// and the v1 appchain `report_data` formula is used. Asserts the full response shape +/// and that the mock quote's embedded `report_data` matches the formula applied to +/// the inserted block's hash/roots. #[tokio::test] async fn generate_quote_genesis_appchain() { let factory = DbProviderFactory::new_in_memory(); @@ -169,23 +217,86 @@ async fn generate_quote_genesis_appchain() { assert!(resp.l1_to_l2_messages.is_empty()); assert!(resp.l2_to_l1_messages.is_empty()); assert_eq!(resp.messages_commitment, empty_messages_commitment()); + assert_eq!(resp.katana_tee_config_hash, sample_katana_tee_config_hash()); + + let expected_report_data = appchain_report_data_v1( + [ + Felt::ZERO, // prev_state_root + state_root, + Felt::ZERO, // prev_block_hash + block_hash, + Felt::MAX, // prev_block_id (genesis sentinel) + Felt::ZERO, // block + empty_messages_commitment(), + ], + sample_katana_tee_config_hash(), + ); + assert_eq!(extract_report_data(&resp.quote), expected_report_data); +} - let expected_report_data = appchain_report_data([ - Felt::ZERO, // prev_state_root - state_root, - Felt::ZERO, // prev_block_hash - block_hash, - Felt::MAX, // prev_block_id (genesis sentinel) - Felt::ZERO, // block - empty_messages_commitment(), - ]); +/// `tee_generateQuote` accepts exactly two positional parameters over JSON-RPC +/// (`prev_block_id`, `block_id`). The response always carries the node's +/// precomputed `katana_tee_config_hash` regardless of caller input. +#[tokio::test] +async fn generate_quote_rpc_wire_shape() { + let factory = DbProviderFactory::new_in_memory(); + insert(&factory, make_block(0, felt!("0xa0"), Felt::ZERO, Felt::ZERO, Vec::new()), Vec::new()); + + let server = ServerBuilder::default().build("127.0.0.1:0").await.unwrap(); + let addr = server.local_addr().unwrap(); + let handle = server.start(mock_api(factory, None).into_rpc()); + + let client = HttpClientBuilder::default().build(format!("http://{addr}")).unwrap(); + let resp: katana_rpc_api::tee::TeeQuoteResponse = client + .request("tee_generateQuote", rpc_params!(Option::::None, 0u64)) + .await + .expect("two-param request"); + + assert_eq!(resp.katana_tee_config_hash, sample_katana_tee_config_hash()); + handle.stop().expect("stop server"); +} + +/// The v1 appchain `report_data` first half is domain/mode/transition/config-bound +/// and the second half exposes the config hash directly. +#[tokio::test] +async fn generate_quote_appchain_non_zero_config_hash() { + let factory = DbProviderFactory::new_in_memory(); + + let block_hash = felt!("0xb10c"); + let state_root = felt!("0x5747"); + let events_commitment = felt!("0xe0"); + + insert( + &factory, + make_block(0, block_hash, state_root, events_commitment, Vec::new()), + Vec::new(), + ); + + let katana_tee_config_hash = sample_katana_tee_config_hash(); + let api = mock_api(factory, None); + let resp = api.generate_quote(None, 0).await.expect("generate_quote"); + + assert_eq!(resp.katana_tee_config_hash, katana_tee_config_hash); + + let expected_report_data = appchain_report_data_v1( + [ + Felt::ZERO, // prev_state_root + state_root, + Felt::ZERO, // prev_block_hash + block_hash, + Felt::MAX, // prev_block_id (genesis sentinel) + Felt::ZERO, // block + empty_messages_commitment(), + ], + katana_tee_config_hash, + ); assert_eq!(extract_report_data(&resp.quote), expected_report_data); } -/// Fork / sharding path: `fork_block_number = Some` selects the 8-field `report_data` -/// that includes `events_commitment + fork_block_number`, *and* short-circuits message -/// aggregation — even though block 1's receipt carries a `messages_sent` entry, the -/// response has empty message vectors and `messages_commitment = Felt::ZERO`. +/// Fork / sharding path: `fork_block_number = Some` selects the v1 sharding +/// `report_data` formula and short-circuits message aggregation — even though +/// block 1's receipt carries a `messages_sent` entry, the response has empty +/// message vectors and `messages_commitment = Felt::ZERO`. #[tokio::test] async fn generate_quote_fork_sharding_mode() { let factory = DbProviderFactory::new_in_memory(); @@ -227,17 +338,61 @@ async fn generate_quote_fork_sharding_mode() { assert!(resp.l1_to_l2_messages.is_empty()); assert!(resp.l2_to_l1_messages.is_empty()); assert_eq!(resp.messages_commitment, Felt::ZERO); + assert_eq!(resp.katana_tee_config_hash, sample_katana_tee_config_hash()); + + let expected_report_data = sharding_report_data_v1( + [ + sr0, + sr1, + h0, + h1, + Felt::ZERO, // prev_block_id = Felt::from(0) + Felt::ONE, // block = 1 + Felt::from(fork_block), + ec1, + ], + sample_katana_tee_config_hash(), + ); + assert_eq!(extract_report_data(&resp.quote), expected_report_data); +} - let expected_report_data = sharding_report_data([ - sr0, - sr1, - h0, - h1, - Felt::ZERO, // prev_block_id = Felt::from(0) - Felt::ONE, // block = 1 - Felt::from(fork_block), - ec1, - ]); +/// Sharding mode round-trips the precomputed config hash into both halves of +/// `report_data` while preserving the fork-mode behavior of suppressing message +/// aggregation. +#[tokio::test] +async fn generate_quote_sharding_non_zero_config_hash() { + let factory = DbProviderFactory::new_in_memory(); + + let h0 = felt!("0xa0"); + let sr0 = felt!("0xa1"); + insert(&factory, make_block(0, h0, sr0, felt!("0xe0"), Vec::new()), Vec::new()); + + let h1 = felt!("0xb0"); + let sr1 = felt!("0xb1"); + let ec1 = felt!("0xe1"); + insert(&factory, make_block(1, h1, sr1, ec1, Vec::new()), Vec::new()); + + let fork_block = 42u64; + let katana_tee_config_hash = sample_katana_tee_config_hash(); + let api = mock_api(factory, Some(fork_block)); + let resp = api.generate_quote(Some(0), 1).await.expect("generate_quote"); + + assert_eq!(resp.katana_tee_config_hash, katana_tee_config_hash); + assert_eq!(resp.messages_commitment, Felt::ZERO); + + let expected_report_data = sharding_report_data_v1( + [ + sr0, + sr1, + h0, + h1, + Felt::ZERO, // prev_block_id = Felt::from(0) + Felt::ONE, // block = 1 + Felt::from(fork_block), + ec1, + ], + katana_tee_config_hash, + ); assert_eq!(extract_report_data(&resp.quote), expected_report_data); } @@ -309,15 +464,18 @@ async fn generate_quote_l2_to_l1_message_aggregation() { assert_eq!(resp.state_root, sr2); assert_eq!(resp.events_commitment, ec2); - let expected_report_data = appchain_report_data([ - sr0, - sr2, - h0, - h2, - Felt::ZERO, // prev_block_id = Felt::from(0) - Felt::from(2u64), - expected_commitment, - ]); + let expected_report_data = appchain_report_data_v1( + [ + sr0, + sr2, + h0, + h2, + Felt::ZERO, // prev_block_id = Felt::from(0) + Felt::from(2u64), + expected_commitment, + ], + sample_katana_tee_config_hash(), + ); assert_eq!(extract_report_data(&resp.quote), expected_report_data); } @@ -375,15 +533,18 @@ async fn generate_quote_l1_to_l2_message_aggregation() { // Verify the same commitment was bound into the report_data fed to the TEE provider — // guards against a bug where the response field and the attested value diverge. - let expected_report_data = appchain_report_data([ - Felt::ZERO, // prev_state_root - sr0, - Felt::ZERO, // prev_block_hash - h0, - Felt::MAX, // prev_block_id (genesis sentinel) - Felt::ZERO, // block - expected_commitment, - ]); + let expected_report_data = appchain_report_data_v1( + [ + Felt::ZERO, // prev_state_root + sr0, + Felt::ZERO, // prev_block_hash + h0, + Felt::MAX, // prev_block_id (genesis sentinel) + Felt::ZERO, // block + expected_commitment, + ], + sample_katana_tee_config_hash(), + ); assert_eq!(extract_report_data(&resp.quote), expected_report_data); } @@ -407,9 +568,9 @@ async fn generate_quote_missing_prev_block_errors() { } /// Wire-format contract for `prev_block_number`: custom serde maps `None ↔ Felt::MAX` -/// (not `None ↔ null`) so the JSON is compatible with `katana_tee_client::TeeQuoteResponse` -/// on the saya-tee side. Validates both branches *and* that round-trip restores the -/// original `Option` value. +/// (not `None ↔ null`) so the field can be hashed into the on-chain commitment as a +/// `Felt` without a separate sentinel encoding step. Validates both branches *and* +/// that round-trip restores the original `Option` value. #[tokio::test] async fn generate_quote_prev_block_number_wire_format() { let factory = DbProviderFactory::new_in_memory(); @@ -428,7 +589,7 @@ async fn generate_quote_prev_block_number_wire_format() { ); assert!( !json_none["prevBlockNumber"].is_null(), - "None must NOT serialize to JSON null (breaks saya-tee compatibility)" + "None must NOT serialize to JSON null" ); let round_trip_none: katana_rpc_api::tee::TeeQuoteResponse = serde_json::from_value(json_none).expect("deserialize"); @@ -447,3 +608,45 @@ async fn generate_quote_prev_block_number_wire_format() { serde_json::from_value(json_some).expect("deserialize"); assert_eq!(round_trip_some.prev_block_number, Some(0)); } + +/// The TeeApi-stored config hash is bound into both halves of `report_data` *and* +/// echoed back in the response. Guards against a refactor regressing one side or +/// the other (response field and attested value must agree). +#[tokio::test] +async fn generate_quote_precomputed_config_hash_binding() { + let factory = DbProviderFactory::new_in_memory(); + insert(&factory, make_block(0, felt!("0xa0"), felt!("0xa1"), Felt::ZERO, Vec::new()), Vec::new()); + + // Use a distinct hash from sample_katana_tee_config_hash() to prove the binding + // tracks whatever the node was constructed with, not a hard-coded constant. + let custom_hash = compute_katana_tee_config_hash( + felt!("0x534e5f5345504f4c4941"), // "SN_SEPOLIA" + felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), + ); + let api = TeeApi::new(factory, Arc::new(MockProvider::new()), None, custom_hash); + let resp = api.generate_quote(None, 0).await.expect("generate_quote"); + + // 1. Response carries the configured hash. + assert_eq!(resp.katana_tee_config_hash, custom_hash); + + // 2. Second half of report_data decodes back to the same Felt. + let report_data = extract_report_data(&resp.quote); + let mut second_half = [0u8; 32]; + second_half.copy_from_slice(&report_data[32..]); + assert_eq!(Felt::from_bytes_be(&second_half), custom_hash); + + // 3. First half is the v1 commitment that includes the same hash as a Poseidon input. + let expected = appchain_report_data_v1( + [ + Felt::ZERO, + felt!("0xa1"), + Felt::ZERO, + felt!("0xa0"), + Felt::MAX, + Felt::ZERO, + empty_messages_commitment(), + ], + custom_hash, + ); + assert_eq!(report_data, expected); +} From 5038ae6ff10e1236dd98f5305cf87e1fa3479cbf Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 27 Apr 2026 22:45:11 -0500 Subject: [PATCH 02/16] style: rustfmt --all --- bin/katana/src/cli/rpc/tee.rs | 3 +-- crates/node/sequencer/src/lib.rs | 3 +-- crates/rpc/rpc-server/tests/tee.rs | 11 ++++++----- 3 files changed, 8 insertions(+), 9 deletions(-) diff --git a/bin/katana/src/cli/rpc/tee.rs b/bin/katana/src/cli/rpc/tee.rs index f61eee718..f7862f28c 100644 --- a/bin/katana/src/cli/rpc/tee.rs +++ b/bin/katana/src/cli/rpc/tee.rs @@ -44,8 +44,7 @@ impl TeeCommands { pub async fn execute(self, client: &Client) -> Result<()> { match self { TeeCommands::GenerateQuote(args) => { - let result = - client.tee_generate_quote(args.prev_block_id, args.block_id).await?; + let result = client.tee_generate_quote(args.prev_block_id, args.block_id).await?; println!("{}", colored_json::to_colored_json_auto(&result)?); } diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index 0c75910d1..2ced20060 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -44,14 +44,13 @@ use katana_rpc_api::paymaster::PaymasterApiServer; use katana_rpc_api::starknet::StarknetApiServer; #[cfg(feature = "explorer")] use katana_rpc_api::starknet_ext::StarknetApiExtServer; -use katana_rpc_api::tee::TeeApiServer; +use katana_rpc_api::tee::{compute_katana_tee_config_hash, TeeApiServer}; use katana_rpc_server::cartridge::{CartridgeApi, CartridgeConfig}; use katana_rpc_server::dev::DevApi; use katana_rpc_server::middleware::cartridge::{ControllerDeploymentLayer, VrfLayer}; use katana_rpc_server::middleware::cors::Cors; use katana_rpc_server::middleware::logger::RpcLoggerLayer; use katana_rpc_server::middleware::metrics::RpcServerMetricsLayer; -use katana_rpc_api::tee::compute_katana_tee_config_hash; use katana_rpc_server::node::NodeApi; use katana_rpc_server::paymaster::PaymasterProxy; use katana_rpc_server::starknet::{RpcCache, StarknetApi, StarknetApiConfig}; diff --git a/crates/rpc/rpc-server/tests/tee.rs b/crates/rpc/rpc-server/tests/tee.rs index f5ac916e7..830375ded 100644 --- a/crates/rpc/rpc-server/tests/tee.rs +++ b/crates/rpc/rpc-server/tests/tee.rs @@ -587,10 +587,7 @@ async fn generate_quote_prev_block_number_wire_format() { serde_json::to_value(Felt::MAX).unwrap(), "None must serialize to Felt::MAX, not null" ); - assert!( - !json_none["prevBlockNumber"].is_null(), - "None must NOT serialize to JSON null" - ); + assert!(!json_none["prevBlockNumber"].is_null(), "None must NOT serialize to JSON null"); let round_trip_none: katana_rpc_api::tee::TeeQuoteResponse = serde_json::from_value(json_none).expect("deserialize"); assert_matches!(round_trip_none.prev_block_number, None); @@ -615,7 +612,11 @@ async fn generate_quote_prev_block_number_wire_format() { #[tokio::test] async fn generate_quote_precomputed_config_hash_binding() { let factory = DbProviderFactory::new_in_memory(); - insert(&factory, make_block(0, felt!("0xa0"), felt!("0xa1"), Felt::ZERO, Vec::new()), Vec::new()); + insert( + &factory, + make_block(0, felt!("0xa0"), felt!("0xa1"), Felt::ZERO, Vec::new()), + Vec::new(), + ); // Use a distinct hash from sample_katana_tee_config_hash() to prove the binding // tracks whatever the node was constructed with, not a hard-coded constant. From 2b8b7d298e2ddd3f9cef63766e98f32d4c904d12 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Mon, 27 Apr 2026 22:59:52 -0500 Subject: [PATCH 03/16] fix(clippy): allow too_many_arguments on compute_report_data_appchain The v1 schema bumped this function from 7 args to 8 (added `katana_tee_config_hash`), tripping `clippy::too_many_arguments`. The sharding sibling already carries the same allow attribute. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/rpc/rpc-server/src/tee.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rpc/rpc-server/src/tee.rs b/crates/rpc/rpc-server/src/tee.rs index 522d48123..551b9df70 100644 --- a/crates/rpc/rpc-server/src/tee.rs +++ b/crates/rpc/rpc-server/src/tee.rs @@ -476,6 +476,7 @@ fn compute_report_data_sharding( /// ) /// report_data = commitment_bytes_be ++ katana_tee_config_hash_bytes_be /// ``` +#[allow(clippy::too_many_arguments)] fn compute_report_data_appchain( prev_state_root: Felt, state_root: Felt, From fecca16157fdb19bb65ded98b6d3f1f52192b7b5 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 28 Apr 2026 00:01:48 -0500 Subject: [PATCH 04/16] refactor(rpc): compute katana_tee_config_hash inside TeeApi::new MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `TeeApi::new` now accepts the raw chain-spec inputs (`chain_id`, `fee_token_address`) and derives the versioned config hash internally, encapsulating the derivation behind a single constructor. This swaps the off-by-default-precomputed pattern for the right one: TeeApi owns its own environment binding. Tests pass raw inputs (the same values they used to feed into `compute_katana_tee_config_hash` themselves); callers only need to know the chain spec, not the hash construction. The wire format is unchanged — same Pedersen-array hash, same `KatanaTeeConfig1` tag, same dual-binding in both halves of `report_data`. Piltover's verification path is identical. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/node/sequencer/src/lib.rs | 16 +++------ crates/rpc/rpc-server/src/tee.rs | 16 +++++++-- crates/rpc/rpc-server/tests/tee.rs | 52 +++++++++++++++++------------- 3 files changed, 46 insertions(+), 38 deletions(-) diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index 2ced20060..7e032a65e 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -44,7 +44,7 @@ use katana_rpc_api::paymaster::PaymasterApiServer; use katana_rpc_api::starknet::StarknetApiServer; #[cfg(feature = "explorer")] use katana_rpc_api::starknet_ext::StarknetApiExtServer; -use katana_rpc_api::tee::{compute_katana_tee_config_hash, TeeApiServer}; +use katana_rpc_api::tee::TeeApiServer; use katana_rpc_server::cartridge::{CartridgeApi, CartridgeConfig}; use katana_rpc_server::dev::DevApi; use katana_rpc_server::middleware::cartridge::{ControllerDeploymentLayer, VrfLayer}; @@ -402,24 +402,16 @@ where _ => anyhow::bail!("Mock TEE provider requires the 'tee-mock' feature"), }; - let katana_tee_config_hash = compute_katana_tee_config_hash( - backend.chain_spec.id().into(), - backend.chain_spec.fee_contracts().strk.into(), - ); let api = TeeApi::new( provider.clone(), tee_provider, tee_config.fork_block_number, - katana_tee_config_hash, + backend.chain_spec.id().into(), + backend.chain_spec.fee_contracts().strk, ); rpc_modules.merge(TeeApiServer::into_rpc(api))?; - info!( - target: "node", - provider = ?tee_config.provider_type, - %katana_tee_config_hash, - "TEE API enabled" - ); + info!(target: "node", provider = ?tee_config.provider_type, "TEE API enabled"); } // --- build rpc middleware diff --git a/crates/rpc/rpc-server/src/tee.rs b/crates/rpc/rpc-server/src/tee.rs index 551b9df70..046e3fa0a 100644 --- a/crates/rpc/rpc-server/src/tee.rs +++ b/crates/rpc/rpc-server/src/tee.rs @@ -4,6 +4,7 @@ use std::sync::Arc; use jsonrpsee::core::{async_trait, RpcResult}; use katana_primitives::block::{BlockHashOrNumber, BlockNumber}; +use katana_primitives::contract::ContractAddress; use katana_primitives::receipt::Receipt; use katana_primitives::transaction::Tx; use katana_primitives::Felt; @@ -12,8 +13,9 @@ use katana_provider::api::transaction::{ReceiptProvider, TransactionProvider}; use katana_provider::ProviderFactory; use katana_rpc_api::error::tee::TeeApiError; use katana_rpc_api::tee::{ - EventProofResponse, TeeApiServer, TeeL1ToL2Message, TeeL2ToL1Message, TeeQuoteResponse, - KATANA_TEE_APPCHAIN_MODE, KATANA_TEE_REPORT_VERSION, KATANA_TEE_SHARDING_MODE, + compute_katana_tee_config_hash, EventProofResponse, TeeApiServer, TeeL1ToL2Message, + TeeL2ToL1Message, TeeQuoteResponse, KATANA_TEE_APPCHAIN_MODE, KATANA_TEE_REPORT_VERSION, + KATANA_TEE_SHARDING_MODE, }; use katana_tee::TeeProvider; use starknet_types_core::hash::{Poseidon, StarkHash}; @@ -42,16 +44,24 @@ where PF: ProviderFactory, { /// Create a new TEE API instance. + /// + /// The versioned environment config hash is derived from the chain spec + /// at construction time — `pedersen_array([KATANA_TEE_CONFIG_VERSION, chain_id, + /// fee_token_address])` — and bound into every attestation's `report_data`. pub fn new( provider_factory: PF, tee_provider: Arc, fork_block_number: Option, - katana_tee_config_hash: Felt, + chain_id: Felt, + fee_token_address: ContractAddress, ) -> Self { + let katana_tee_config_hash = + compute_katana_tee_config_hash(chain_id, fee_token_address.into()); info!( target: "rpc::tee", provider_type = tee_provider.provider_type(), ?fork_block_number, + %chain_id, %katana_tee_config_hash, "TEE API initialized" ); diff --git a/crates/rpc/rpc-server/tests/tee.rs b/crates/rpc/rpc-server/tests/tee.rs index 830375ded..fc4d9ae2b 100644 --- a/crates/rpc/rpc-server/tests/tee.rs +++ b/crates/rpc/rpc-server/tests/tee.rs @@ -40,12 +40,8 @@ fn mock_api( factory: DbProviderFactory, fork_block_number: Option, ) -> TeeApi { - TeeApi::new( - factory, - Arc::new(MockProvider::new()), - fork_block_number, - sample_katana_tee_config_hash(), - ) + let (chain_id, fee_token) = sample_chain_spec(); + TeeApi::new(factory, Arc::new(MockProvider::new()), fork_block_number, chain_id, fee_token) } fn make_block( @@ -120,13 +116,21 @@ fn sharding_report_data_v1(fields: [Felt; 8], katana_tee_config_hash: Felt) -> [ out } -fn sample_katana_tee_config_hash() -> Felt { - compute_katana_tee_config_hash( +/// Sample chain spec inputs that flow through `TeeApi::new` to derive the +/// expected `katana_tee_config_hash`. Tests use these to construct a `TeeApi` +/// and to recompute the expected hash for assertions. +fn sample_chain_spec() -> (Felt, ContractAddress) { + ( felt!("0x4b4154414e41"), // "KATANA" - felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), + felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d").into(), ) } +fn sample_katana_tee_config_hash() -> Felt { + let (chain_id, fee_token) = sample_chain_spec(); + compute_katana_tee_config_hash(chain_id, fee_token.into()) +} + fn empty_messages_commitment() -> Felt { Poseidon::hash_array(&[Poseidon::hash_array(&[]), Poseidon::hash_array(&[])]) } @@ -606,9 +610,10 @@ async fn generate_quote_prev_block_number_wire_format() { assert_eq!(round_trip_some.prev_block_number, Some(0)); } -/// The TeeApi-stored config hash is bound into both halves of `report_data` *and* -/// echoed back in the response. Guards against a refactor regressing one side or -/// the other (response field and attested value must agree). +/// `TeeApi::new` derives the config hash from chain spec inputs (`chain_id`, +/// `fee_token_address`) and binds it into both halves of `report_data` and +/// the response. Guards against a refactor regressing the derivation or any +/// of the bind sites. #[tokio::test] async fn generate_quote_precomputed_config_hash_binding() { let factory = DbProviderFactory::new_in_memory(); @@ -618,23 +623,24 @@ async fn generate_quote_precomputed_config_hash_binding() { Vec::new(), ); - // Use a distinct hash from sample_katana_tee_config_hash() to prove the binding - // tracks whatever the node was constructed with, not a hard-coded constant. - let custom_hash = compute_katana_tee_config_hash( - felt!("0x534e5f5345504f4c4941"), // "SN_SEPOLIA" - felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), - ); - let api = TeeApi::new(factory, Arc::new(MockProvider::new()), None, custom_hash); + // Use distinct chain-spec inputs from `sample_chain_spec` to prove the + // hash tracks construction-time values, not a hard-coded constant. + let chain_id = felt!("0x534e5f5345504f4c4941"); // "SN_SEPOLIA" + let fee_token: ContractAddress = + felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d").into(); + let expected_hash = compute_katana_tee_config_hash(chain_id, fee_token.into()); + + let api = TeeApi::new(factory, Arc::new(MockProvider::new()), None, chain_id, fee_token); let resp = api.generate_quote(None, 0).await.expect("generate_quote"); - // 1. Response carries the configured hash. - assert_eq!(resp.katana_tee_config_hash, custom_hash); + // 1. Response carries the derived hash. + assert_eq!(resp.katana_tee_config_hash, expected_hash); // 2. Second half of report_data decodes back to the same Felt. let report_data = extract_report_data(&resp.quote); let mut second_half = [0u8; 32]; second_half.copy_from_slice(&report_data[32..]); - assert_eq!(Felt::from_bytes_be(&second_half), custom_hash); + assert_eq!(Felt::from_bytes_be(&second_half), expected_hash); // 3. First half is the v1 commitment that includes the same hash as a Poseidon input. let expected = appchain_report_data_v1( @@ -647,7 +653,7 @@ async fn generate_quote_precomputed_config_hash_binding() { Felt::ZERO, empty_messages_commitment(), ], - custom_hash, + expected_hash, ); assert_eq!(report_data, expected); } From 13b8628ff37780f6f0ce550568e560c1bc2bae9e Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 28 Apr 2026 00:09:25 -0500 Subject: [PATCH 05/16] refactor(rpc): TeeApi::new takes &ChainSpec, extracts fields internally Replaces the `(chain_id, fee_token_address)` pair on `TeeApi::new` with a single `chain_spec: &ChainSpec` argument. Field extraction (`chain_spec.id()` and `chain_spec.fee_contracts().strk`) and config-hash derivation happen inside the constructor. Why: the caller no longer needs to know which fields the v1 schema reads to compute the hash. If a future schema version reads more chain-spec fields, the signature stays the same. The wire format is unchanged. Tests build a minimal `ChainSpec` by cloning `dev::DEV` and overriding the two fields they care about. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/node/sequencer/src/lib.rs | 3 +-- crates/rpc/rpc-server/src/tee.rs | 15 ++++++++------- crates/rpc/rpc-server/tests/tee.rs | 31 ++++++++++++++++++++---------- 3 files changed, 30 insertions(+), 19 deletions(-) diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index 7e032a65e..cb69b8765 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -406,8 +406,7 @@ where provider.clone(), tee_provider, tee_config.fork_block_number, - backend.chain_spec.id().into(), - backend.chain_spec.fee_contracts().strk, + &backend.chain_spec, ); rpc_modules.merge(TeeApiServer::into_rpc(api))?; diff --git a/crates/rpc/rpc-server/src/tee.rs b/crates/rpc/rpc-server/src/tee.rs index 046e3fa0a..2a75ace5b 100644 --- a/crates/rpc/rpc-server/src/tee.rs +++ b/crates/rpc/rpc-server/src/tee.rs @@ -3,8 +3,8 @@ use std::sync::Arc; use jsonrpsee::core::{async_trait, RpcResult}; +use katana_chain_spec::ChainSpec; use katana_primitives::block::{BlockHashOrNumber, BlockNumber}; -use katana_primitives::contract::ContractAddress; use katana_primitives::receipt::Receipt; use katana_primitives::transaction::Tx; use katana_primitives::Felt; @@ -46,17 +46,18 @@ where /// Create a new TEE API instance. /// /// The versioned environment config hash is derived from the chain spec - /// at construction time — `pedersen_array([KATANA_TEE_CONFIG_VERSION, chain_id, - /// fee_token_address])` — and bound into every attestation's `report_data`. + /// at construction time — `pedersen_array([KATANA_TEE_CONFIG_VERSION, + /// chain_id, fee_token_address])` — and bound into every attestation's + /// `report_data`. pub fn new( provider_factory: PF, tee_provider: Arc, fork_block_number: Option, - chain_id: Felt, - fee_token_address: ContractAddress, + chain_spec: &ChainSpec, ) -> Self { - let katana_tee_config_hash = - compute_katana_tee_config_hash(chain_id, fee_token_address.into()); + let chain_id: Felt = chain_spec.id().into(); + let fee_token: Felt = chain_spec.fee_contracts().strk.into(); + let katana_tee_config_hash = compute_katana_tee_config_hash(chain_id, fee_token); info!( target: "rpc::tee", provider_type = tee_provider.provider_type(), diff --git a/crates/rpc/rpc-server/tests/tee.rs b/crates/rpc/rpc-server/tests/tee.rs index fc4d9ae2b..d1d4183e7 100644 --- a/crates/rpc/rpc-server/tests/tee.rs +++ b/crates/rpc/rpc-server/tests/tee.rs @@ -17,6 +17,7 @@ use jsonrpsee::core::client::ClientT; use jsonrpsee::http_client::HttpClientBuilder; use jsonrpsee::rpc_params; use jsonrpsee::server::ServerBuilder; +use katana_chain_spec::ChainSpec; use katana_primitives::block::{Block, BlockNumber, FinalityStatus, Header, SealedBlockWithStatus}; use katana_primitives::fee::FeeInfo; use katana_primitives::hash::{Poseidon, StarkHash}; @@ -40,8 +41,8 @@ fn mock_api( factory: DbProviderFactory, fork_block_number: Option, ) -> TeeApi { - let (chain_id, fee_token) = sample_chain_spec(); - TeeApi::new(factory, Arc::new(MockProvider::new()), fork_block_number, chain_id, fee_token) + let chain_spec = sample_chain_spec(); + TeeApi::new(factory, Arc::new(MockProvider::new()), fork_block_number, &chain_spec) } fn make_block( @@ -116,19 +117,28 @@ fn sharding_report_data_v1(fields: [Felt; 8], katana_tee_config_hash: Felt) -> [ out } -/// Sample chain spec inputs that flow through `TeeApi::new` to derive the -/// expected `katana_tee_config_hash`. Tests use these to construct a `TeeApi` -/// and to recompute the expected hash for assertions. -fn sample_chain_spec() -> (Felt, ContractAddress) { - ( +/// Build a minimal `ChainSpec` for tests by cloning the dev default and +/// overriding only the two fields `TeeApi::new` reads to derive the config +/// hash (chain id + STRK fee token address). +fn build_chain_spec(chain_id: Felt, fee_token: ContractAddress) -> ChainSpec { + let mut spec = katana_chain_spec::dev::DEV.clone(); + spec.id = chain_id.into(); + spec.fee_contracts.strk = fee_token; + ChainSpec::Dev(spec) +} + +fn sample_chain_spec() -> ChainSpec { + build_chain_spec( felt!("0x4b4154414e41"), // "KATANA" felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d").into(), ) } fn sample_katana_tee_config_hash() -> Felt { - let (chain_id, fee_token) = sample_chain_spec(); - compute_katana_tee_config_hash(chain_id, fee_token.into()) + compute_katana_tee_config_hash( + felt!("0x4b4154414e41"), + felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d"), + ) } fn empty_messages_commitment() -> Felt { @@ -629,8 +639,9 @@ async fn generate_quote_precomputed_config_hash_binding() { let fee_token: ContractAddress = felt!("0x4718f5a0fc34cc1af16a1cdee98ffb20c31f5cd61d6ab07201858f4287c938d").into(); let expected_hash = compute_katana_tee_config_hash(chain_id, fee_token.into()); + let chain_spec = build_chain_spec(chain_id, fee_token); - let api = TeeApi::new(factory, Arc::new(MockProvider::new()), None, chain_id, fee_token); + let api = TeeApi::new(factory, Arc::new(MockProvider::new()), None, &chain_spec); let resp = api.generate_quote(None, 0).await.expect("generate_quote"); // 1. Response carries the derived hash. From 303d9cbf4276bf91596d99bdbbcbaa6ff58eac76 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 28 Apr 2026 14:19:33 -0500 Subject: [PATCH 06/16] chore(deps): bump piltover submodule to feat/tee-persistent (ebb714b) Picks up the merged PR #16 on cartridge-gg/piltover, which: - splits `ProgramInfo` into `StarknetOs` / `KatanaTee` enum variants - enforces a runtime variant invariant in `validate_input` so a TEE attestation cannot settle against a contract configured for SNOS, and vice versa The Cairo `katana_tee_config_hash` value Piltover stores for the TEE variant is the same `KatanaTeeConfig1`-tagged Pedersen-array hash this PR's `TeeApi` already produces, so the wire format stays unchanged. Compiled `Appchain` / `MockAmdTeeRegistry` artifacts get regenerated by `crates/contracts/build.rs`; no hard-coded class hash to update. Co-Authored-By: Claude Opus 4.7 (1M context) --- crates/contracts/contracts/piltover | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/contracts/contracts/piltover b/crates/contracts/contracts/piltover index 37e487f70..ebb714b3a 160000 --- a/crates/contracts/contracts/piltover +++ b/crates/contracts/contracts/piltover @@ -1 +1 @@ -Subproject commit 37e487f70a5e9198d9b1f12e0040696d5eb3b866 +Subproject commit ebb714b3a0e63da8088ea4f371bcca2a1a3f74f0 From 3e2ee7ff1e26a37acaebd52e62f6783f67b6750d Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Tue, 28 Apr 2026 14:57:54 -0500 Subject: [PATCH 07/16] feat(init): set ProgramInfo variant per proof type in `katana init rollup` Piltover's `ProgramInfo` is now an enum (StarknetOs vs KatanaTee variants), and the on-chain `validate_input` panics on cross-mode submission. The init flow needs to commit to the right variant at deploy time. `deploy_settlement_contract` and `check_program_info` now take a `tee: bool`. On `--tee`, init writes `ProgramInfo::KatanaTee(KatanaTeeProgramInfo { katana_tee_config_hash })` using the same `compute_katana_tee_config_hash` helper `TeeApi::new` calls, so the deployment-time hash and the runtime attestation hash agree byte-for-byte. Otherwise, init writes the existing `ProgramInfo::StarknetOs(...)` with the four SNOS hashes. Verification adds two new errors: `InvalidKatanaTeeConfigHash` for hash mismatch within a variant, and `InvalidProgramInfoVariant` for cross-mode mismatch (StarknetOs config but operator passed `--tee`, or vice versa). The default fee token address is lifted to a const so deployment and verification share the same value. Cargo.toml: repoints `piltover` from `kariy/piltover#feat/rpc0.9` to `cartridge-gg/piltover#feat/tee-persistent` so the Rust types match the submodule (single source of truth). Co-Authored-By: Claude Opus 4.7 (1M context) --- Cargo.lock | 147 ++++++++++++++++++--- Cargo.toml | 2 +- bin/katana/Cargo.toml | 1 + bin/katana/src/cli/init/deployment.rs | 177 +++++++++++++++++--------- bin/katana/src/cli/init/mod.rs | 2 + bin/katana/src/cli/init/prompt.rs | 59 +++++---- 6 files changed, 281 insertions(+), 107 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 03d0d271a..496c53985 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2124,11 +2124,11 @@ source = "git+https://github.com/cartridge-gg/cainome?rev=7d60de1#7d60de1780374b dependencies = [ "anyhow", "async-trait", - "cainome-cairo-serde", - "cainome-cairo-serde-derive", - "cainome-parser", - "cainome-rs", - "cainome-rs-macro", + "cainome-cairo-serde 0.3.0", + "cainome-cairo-serde-derive 0.1.0 (git+https://github.com/cartridge-gg/cainome?rev=7d60de1)", + "cainome-parser 0.5.1", + "cainome-rs 0.4.0", + "cainome-rs-macro 0.4.0", "camino", "clap", "clap_complete", @@ -2143,6 +2143,33 @@ dependencies = [ "url", ] +[[package]] +name = "cainome" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f296568cda6f1f4934270a321151f9f51afa737450f67ef13a8534f2bd9afdac" +dependencies = [ + "anyhow", + "async-trait", + "cainome-cairo-serde 0.4.1", + "cainome-cairo-serde-derive 0.1.0 (registry+https://github.com/rust-lang/crates.io-index)", + "cainome-parser 0.6.0", + "cainome-rs 0.5.1", + "cainome-rs-macro 0.5.1", + "camino", + "clap", + "clap_complete", + "convert_case 0.8.0", + "serde", + "serde_json", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "starknet-types-core 0.2.3", + "thiserror 2.0.12", + "tracing", + "tracing-subscriber", + "url", +] + [[package]] name = "cainome-cairo-serde" version = "0.3.0" @@ -2155,6 +2182,31 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "cainome-cairo-serde" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95ae2d4c21db23c7730a85187c2e9d73fe00c123171839185fb13f31550f3240" +dependencies = [ + "num-bigint", + "serde", + "serde_with", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "thiserror 2.0.12", +] + +[[package]] +name = "cainome-cairo-serde-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d272424141f0ced49ca5f40bc4b756235ee6e7c9cf6ab01f7ef5ac010f5f8864" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", + "unzip-n", +] + [[package]] name = "cainome-cairo-serde-derive" version = "0.1.0" @@ -2179,14 +2231,47 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "cainome-parser" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ffdbc6abfba9c7e91beb87fe7561f097d9f7bbda45c6ff74be3a9ff3f1a0124" +dependencies = [ + "convert_case 0.8.0", + "quote", + "serde_json", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 2.0.104", + "thiserror 2.0.12", +] + [[package]] name = "cainome-rs" version = "0.4.0" source = "git+https://github.com/cartridge-gg/cainome?rev=7d60de1#7d60de1780374b3644677f55cee053596e99f461" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", + "cainome-cairo-serde 0.3.0", + "cainome-parser 0.5.1", + "camino", + "prettyplease", + "proc-macro2", + "quote", + "serde_json", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 2.0.104", + "thiserror 2.0.12", +] + +[[package]] +name = "cainome-rs" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "93aa5c33e66b58ff6723d3e43e60fe163ac20719ea89fce921096e654b15bcd6" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.4.1", + "cainome-parser 0.6.0", "camino", "prettyplease", "proc-macro2", @@ -2203,9 +2288,28 @@ version = "0.4.0" source = "git+https://github.com/cartridge-gg/cainome?rev=7d60de1#7d60de1780374b3644677f55cee053596e99f461" dependencies = [ "anyhow", - "cainome-cairo-serde", - "cainome-parser", - "cainome-rs", + "cainome-cairo-serde 0.3.0", + "cainome-parser 0.5.1", + "cainome-rs 0.4.0", + "proc-macro-error", + "proc-macro2", + "quote", + "serde_json", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", + "syn 2.0.104", + "thiserror 2.0.12", +] + +[[package]] +name = "cainome-rs-macro" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d679cd9a88b9d412ed4ed30264b80f1bec6c1aa9656e238f06be560ddebb652" +dependencies = [ + "anyhow", + "cainome-cairo-serde 0.4.1", + "cainome-parser 0.6.0", + "cainome-rs 0.5.1", "proc-macro-error", "proc-macro2", "quote", @@ -6088,7 +6192,7 @@ dependencies = [ "assert_matches", "async-trait", "byte-unit", - "cainome", + "cainome 0.9.1", "cairo-lang-starknet-classes", "clap", "clap_complete", @@ -6107,6 +6211,7 @@ dependencies = [ "katana-genesis", "katana-primitives", "katana-provider", + "katana-rpc-api", "katana-rpc-types", "katana-starknet", "katana-utils", @@ -6682,7 +6787,7 @@ dependencies = [ name = "katana-paymaster" version = "1.7.0" dependencies = [ - "cainome", + "cainome 0.9.1", "http 1.3.1", "jsonrpsee 0.26.0", "katana-contracts", @@ -6756,8 +6861,8 @@ dependencies = [ "arbitrary", "assert_matches", "blockifier", - "cainome", - "cainome-cairo-serde", + "cainome 0.9.1", + "cainome-cairo-serde 0.3.0", "cairo-lang-sierra", "cairo-lang-starknet-classes", "cairo-lang-utils", @@ -6876,7 +6981,7 @@ dependencies = [ "assert_matches", "auto_impl", "axum 0.7.9", - "cainome", + "cainome 0.9.1", "cairo-lang-starknet-classes", "cartridge", "hex", @@ -6939,8 +7044,8 @@ version = "1.7.0" dependencies = [ "arbitrary", "assert_matches", - "cainome", - "cainome-cairo-serde", + "cainome 0.9.1", + "cainome-cairo-serde 0.3.0", "cairo-lang-starknet-classes", "cairo-lang-utils", "derive_more 0.99.20", @@ -8675,9 +8780,9 @@ checksum = "5be167a7af36ee22fe3115051bc51f6e6c7054c9348e28deb4f49bd6f705a315" [[package]] name = "piltover" version = "0.1.0" -source = "git+https://github.com/kariy/piltover.git?branch=feat%2Frpc0.9#fda4f1668344021d30c506070ef54a790f4cc5cb" +source = "git+https://github.com/cartridge-gg/piltover.git?branch=feat%2Ftee-persistent#ebb714b3a0e63da8088ea4f371bcca2a1a3f74f0" dependencies = [ - "cainome", + "cainome 0.10.1", "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", ] @@ -10235,7 +10340,7 @@ name = "saya-tee-e2e-test" version = "1.7.0" dependencies = [ "anyhow", - "cainome", + "cainome 0.9.1", "hex", "katana-chain-spec", "katana-genesis", @@ -12848,7 +12953,7 @@ name = "vrf-e2e-test" version = "1.7.0" dependencies = [ "axum 0.7.9", - "cainome", + "cainome 0.9.1", "cartridge", "katana-cli", "katana-contracts", diff --git a/Cargo.toml b/Cargo.toml index c0e840c45..0bb85c428 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -257,7 +257,7 @@ starknet_api = { git = "https://github.com/dojoengine/sequencer", rev = "d2591bb cainome = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1", features = [ "abigen-rs" ] } cainome-cairo-serde = { git = "https://github.com/cartridge-gg/cainome", rev = "7d60de1" } -piltover = { git = "https://github.com/kariy/piltover.git", branch = "feat/rpc0.9" } +piltover = { git = "https://github.com/cartridge-gg/piltover.git", branch = "feat/tee-persistent" } # paymaster paymaster-rpc = { git = "https://github.com/cartridge-gg/paymaster", rev = "d89b5d2" } # branch = main diff --git a/bin/katana/Cargo.toml b/bin/katana/Cargo.toml index 6ea9423b2..1326b10a7 100644 --- a/bin/katana/Cargo.toml +++ b/bin/katana/Cargo.toml @@ -15,6 +15,7 @@ katana-db = { workspace = true, features = [ "arbitrary" ] } katana-genesis.workspace = true katana-primitives.workspace = true katana-provider.workspace = true +katana-rpc-api.workspace = true katana-rpc-types.workspace = true katana-starknet.workspace = true katana-utils.workspace = true diff --git a/bin/katana/src/cli/init/deployment.rs b/bin/katana/src/cli/init/deployment.rs index 98caeef90..08eef30a4 100644 --- a/bin/katana/src/cli/init/deployment.rs +++ b/bin/katana/src/cli/init/deployment.rs @@ -5,10 +5,14 @@ use katana_primitives::block::{BlockHash, BlockNumber}; use katana_primitives::cairo::ShortString; use katana_primitives::class::ContractClass; use katana_primitives::{felt, ContractAddress, Felt}; +use katana_rpc_api::tee::compute_katana_tee_config_hash; use katana_rpc_types::class::Class; use katana_starknet::rpc::StarknetRpcClient as StarknetClient; use katana_utils::{TxWaiter, TxWaitingError}; -use piltover::{AppchainContract, AppchainContractReader, ProgramInfo}; +use piltover::{ + AppchainContract, AppchainContractReader, KatanaTeeProgramInfo, ProgramInfo, + StarknetOsProgramInfo, +}; use spinoff::{spinners, Color, Spinner}; use starknet::accounts::{Account, AccountError, ConnectedAccount, SingleOwnerAccount}; use starknet::contract::{ContractFactory, UdcSelector}; @@ -58,6 +62,16 @@ const LAYOUT_BRIDGE_PROGRAM_HASH: Felt = const BOOTLOADER_PROGRAM_HASH: Felt = felt!("0x5ab580b04e3532b6b18f81cfa654a05e29dd8e2352d88df1e765a84072db07"); +/// Default STRK fee-token address for chains generated by `katana init`. +/// +/// Both the StarknetOS config-hash (`StarknetOsConfig3`-tagged) and the Katana +/// TEE config-hash (`KatanaTeeConfig1`-tagged) bind this address, so the +/// deployment-time hash and the runtime hash agree. +/// +/// TODO: thread the fee token through the rollup config rather than hard-coding it here. +const DEFAULT_FEE_TOKEN_ADDRESS: Felt = + felt!("0x2e7442625bab778683501c0eadbc1ea17b3535da040a12ac7d281066e915eea"); + #[derive(Debug)] pub struct DeploymentOutcome { /// The address of the deployed settlement contract. @@ -72,10 +86,15 @@ pub struct DeploymentOutcome { /// `fact_registry` is the address written into Piltover via `set_facts_registry(...)`. In ZK mode /// this is the Herodotus Atlantic integrity contract; in TEE mode it is the `IAMDTeeRegistry` /// contract that verifies SP1 Groth16 proofs. +/// +/// `tee` selects the `ProgramInfo` variant the contract is configured for. After the variant is +/// committed at construction time, Piltover's `validate_input` panics on cross-mode submission, so +/// this flag must match the kind of `update_state` input the operator intends to settle with. pub async fn deploy_settlement_contract( mut account: SettlementInitializerAccount, chain_id: Felt, fact_registry: Felt, + tee: bool, ) -> Result { // This is important! Otherwise all the estimate fees after a transaction will be executed // against invalid state. @@ -177,27 +196,11 @@ pub async fn deploy_settlement_contract( let deployed_appchain_contract = request.deployed_address(); let appchain = AppchainContract::new(deployed_appchain_contract, &account); - // Compute the chain's config hash - let snos_config_hash = compute_config_hash( - chain_id, - // NOTE: - // - // This is the default fee token contract address of chains generated using `katana - // init`. We shouldn't hardcode this and need to handle this more - // elegantly. - felt!("0x2e7442625bab778683501c0eadbc1ea17b3535da040a12ac7d281066e915eea"), - ); - // 1. Program Info sp.update_text("Setting program info..."); - let program_info = ProgramInfo { - snos_config_hash, - snos_program_hash: SNOS_PROGRAM_HASH, - bootloader_program_hash: BOOTLOADER_PROGRAM_HASH, - layout_bridge_program_hash: LAYOUT_BRIDGE_PROGRAM_HASH, - }; + let program_info = build_program_info(tee, chain_id); let res = appchain .set_program_info(&program_info) @@ -236,6 +239,7 @@ pub async fn deploy_settlement_contract( deployed_appchain_contract.into(), account.provider(), fact_registry, + tee, ) .await?; @@ -259,31 +263,25 @@ pub async fn deploy_settlement_contract( /// Checks that the settlement contract is correctly configured. /// -/// The values checked are:- -/// * Program info (config hash, and StarknetOS program hash) -/// * Fact registry contract address (compared against `expected_fact_registry`) -/// * Layout bridge program hash +/// The values checked depend on the selected settlement mode (`tee`): +/// * StarknetOs mode — bootloader / SNOS-program / layout-bridge program hashes and the +/// `StarknetOsConfig3`-tagged config hash. +/// * KatanaTee mode — the `KatanaTeeConfig1`-tagged environment config hash. +/// +/// In both modes, the fact-registry address is compared against `expected_fact_registry`. /// -/// `expected_fact_registry` is the Herodotus Atlantic address in ZK mode and the -/// `IAMDTeeRegistry` address in TEE mode. +/// `expected_fact_registry` is the Herodotus Atlantic address in StarknetOs mode and the +/// `IAMDTeeRegistry` address in TEE mode. `tee` must match the variant the contract was +/// constructed with; cross-mode lookup is reported as `InvalidProgramInfoVariant`. pub async fn check_program_info( chain_id: Felt, appchain_address: ContractAddress, provider: &SettlementChainProvider, expected_fact_registry: Felt, + tee: bool, ) -> Result<(), ContractInitError> { let appchain = AppchainContractReader::new(appchain_address.into(), provider); - // Compute the chain's config hash - let config_hash = compute_config_hash( - chain_id, - // NOTE: - // - // This is the default fee token contract address of chains generated using `katana init`. - // We shouldn't hardcode this and need to handle this more elegantly. - felt!("0x2e7442625bab778683501c0eadbc1ea17b3535da040a12ac7d281066e915eea"), - ); - // Assert that the values are correctly set let (program_info_res, facts_registry_res) = tokio::join!(appchain.get_program_info().call(), appchain.get_facts_registry().call()); @@ -291,32 +289,60 @@ pub async fn check_program_info( let actual_program_info = program_info_res.map_err(|e| ContractInitError::Other(anyhow!(e)))?; let facts_registry = facts_registry_res.map_err(|e| ContractInitError::Other(anyhow!(e)))?; - if actual_program_info.layout_bridge_program_hash != LAYOUT_BRIDGE_PROGRAM_HASH { - return Err(ContractInitError::InvalidLayoutBridgeProgramHash { - actual: actual_program_info.layout_bridge_program_hash, - expected: LAYOUT_BRIDGE_PROGRAM_HASH, - }); - } + match (tee, actual_program_info) { + (false, ProgramInfo::StarknetOs(info)) => { + let config_hash = compute_config_hash(chain_id, DEFAULT_FEE_TOKEN_ADDRESS); - if actual_program_info.bootloader_program_hash != BOOTLOADER_PROGRAM_HASH { - return Err(ContractInitError::InvalidBootloaderProgramHash { - actual: actual_program_info.bootloader_program_hash, - expected: BOOTLOADER_PROGRAM_HASH, - }); - } + if info.layout_bridge_program_hash != LAYOUT_BRIDGE_PROGRAM_HASH { + return Err(ContractInitError::InvalidLayoutBridgeProgramHash { + actual: info.layout_bridge_program_hash, + expected: LAYOUT_BRIDGE_PROGRAM_HASH, + }); + } - if actual_program_info.snos_program_hash != SNOS_PROGRAM_HASH { - return Err(ContractInitError::InvalidSnosProgramHash { - actual: actual_program_info.snos_program_hash, - expected: SNOS_PROGRAM_HASH, - }); - } + if info.bootloader_program_hash != BOOTLOADER_PROGRAM_HASH { + return Err(ContractInitError::InvalidBootloaderProgramHash { + actual: info.bootloader_program_hash, + expected: BOOTLOADER_PROGRAM_HASH, + }); + } - if actual_program_info.snos_config_hash != config_hash { - return Err(ContractInitError::InvalidConfigHash { - actual: actual_program_info.snos_config_hash, - expected: config_hash, - }); + if info.snos_program_hash != SNOS_PROGRAM_HASH { + return Err(ContractInitError::InvalidSnosProgramHash { + actual: info.snos_program_hash, + expected: SNOS_PROGRAM_HASH, + }); + } + + if info.snos_config_hash != config_hash { + return Err(ContractInitError::InvalidConfigHash { + actual: info.snos_config_hash, + expected: config_hash, + }); + } + } + (true, ProgramInfo::KatanaTee(info)) => { + let expected = compute_katana_tee_config_hash(chain_id, DEFAULT_FEE_TOKEN_ADDRESS); + + if info.katana_tee_config_hash != expected { + return Err(ContractInitError::InvalidKatanaTeeConfigHash { + actual: info.katana_tee_config_hash, + expected, + }); + } + } + (false, ProgramInfo::KatanaTee(_)) => { + return Err(ContractInitError::InvalidProgramInfoVariant { + expected: "StarknetOs", + actual: "KatanaTee", + }); + } + (true, ProgramInfo::StarknetOs(_)) => { + return Err(ContractInitError::InvalidProgramInfoVariant { + expected: "KatanaTee", + actual: "StarknetOs", + }); + } } if facts_registry != expected_fact_registry.into() { @@ -329,6 +355,31 @@ pub async fn check_program_info( Ok(()) } +/// Builds the `ProgramInfo` enum variant the deployment will commit to. +/// +/// `StarknetOs` carries the bootloader / SNOS-program / layout-bridge program hashes and the +/// `StarknetOsConfig3`-tagged config hash. `KatanaTee` carries the `KatanaTeeConfig1`-tagged +/// environment hash that the off-chain Katana node binds into SEV-SNP `report_data`. The two +/// hashes are domain-separated by their version tag, so they are never byte-equal even for the +/// same `(chain_id, fee_token)`. +fn build_program_info(tee: bool, chain_id: Felt) -> ProgramInfo { + if tee { + ProgramInfo::KatanaTee(KatanaTeeProgramInfo { + katana_tee_config_hash: compute_katana_tee_config_hash( + chain_id, + DEFAULT_FEE_TOKEN_ADDRESS, + ), + }) + } else { + ProgramInfo::StarknetOs(StarknetOsProgramInfo { + snos_config_hash: compute_config_hash(chain_id, DEFAULT_FEE_TOKEN_ADDRESS), + snos_program_hash: SNOS_PROGRAM_HASH, + bootloader_program_hash: BOOTLOADER_PROGRAM_HASH, + layout_bridge_program_hash: LAYOUT_BRIDGE_PROGRAM_HASH, + }) + } +} + /// Error that can happen during the initialization of the core contract. #[derive(Error, Debug)] pub enum ContractInitError { @@ -364,6 +415,18 @@ pub enum ContractInitError { )] InvalidConfigHash { expected: Felt, actual: Felt }, + #[error( + "invalid program info: katana tee config hash mismatch - expected {expected:#x}, got \ + {actual:#x}" + )] + InvalidKatanaTeeConfigHash { expected: Felt, actual: Felt }, + + #[error( + "invalid program info: settlement-mode mismatch - expected {expected} variant, got \ + {actual}" + )] + InvalidProgramInfoVariant { expected: &'static str, actual: &'static str }, + #[error("invalid program state: fact registry mismatch - expected {expected:}, got {actual}")] InvalidFactRegistry { expected: Felt, actual: Felt }, diff --git a/bin/katana/src/cli/init/mod.rs b/bin/katana/src/cli/init/mod.rs index dc053f4b5..7770b939c 100644 --- a/bin/katana/src/cli/init/mod.rs +++ b/bin/katana/src/cli/init/mod.rs @@ -349,6 +349,7 @@ impl RollupArgs { contract, &settlement_provider, effective_fact_registry, + self.tee, ) .await .with_context(|| "settlement contract validation failed.".to_string()) @@ -378,6 +379,7 @@ impl RollupArgs { account, chain_id, effective_fact_registry, + self.tee, ) .await .with_context(|| "failed to deploy settlement contract".to_string()) diff --git a/bin/katana/src/cli/init/prompt.rs b/bin/katana/src/cli/init/prompt.rs index 201234032..5ed94fa54 100644 --- a/bin/katana/src/cli/init/prompt.rs +++ b/bin/katana/src/cli/init/prompt.rs @@ -163,41 +163,44 @@ pub async fn prompt_rollup() -> Result { // TEE mode: point Piltover's fact-registry at the IAMDTeeRegistry. // ZK mode: use the provider's default (Herodotus Atlantic for mainnet/sepolia, // or the address the user entered for a custom chain). + let tee = tee_registry_address.is_some(); let fact_registry = tee_registry_address.map(|a| *a).unwrap_or_else(|| settlement_provider.fact_registry()); // 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()? { - deployment::deploy_settlement_contract(account, chain_id.into(), fact_registry).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()? + { + deployment::deploy_settlement_contract(account, chain_id.into(), fact_registry, tee).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. - deployment::check_program_info( - chain_id.into(), - address, - &settlement_provider, - fact_registry, - ) - .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()?; + // Check that the settlement contract has been initialized with the correct program + // info. + deployment::check_program_info( + chain_id.into(), + address, + &settlement_provider, + fact_registry, + tee, + ) + .await + .context( + "Invalid settlement contract. The contract might have been configured incorrectly.", + )?; - DeploymentOutcome { contract_address: address, block_number } - }; + 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 } + }; let slot_paymasters = prompt_slot_paymasters()?; From b1e8fbdbcaf32281424444265b8ffb156760c550 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 00:20:29 -0500 Subject: [PATCH 08/16] test(saya-tee): bump Saya rev to 0072383 + configure Piltover KatanaTee variant MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit dojoengine/saya@0072383 (`feat/tee` post-PR-#72) ships the new `ProgramInfo` enum (`StarknetOs` / `KatanaTee`) and plumbs `katana_tee_config_hash` through the TEE pipeline. The Saya `saya-ops setup-program` subcommand still emits only the `StarknetOs` variant, which would cause Piltover's runtime `validate_input` to panic with "mode: tee needs KatanaTee cfg" when saya-tee submits a `TeeInput` later in the test. Replace `saya.setup_program(...)` in `bootstrap_l2` with a direct multicall (`set_program_info(ProgramInfo::KatanaTee(...))` + `set_facts_registry`) using the prefunded L2 dev account. The `katana_tee_config_hash` is computed via the same `katana_rpc_api::tee::compute_katana_tee_config_hash` that the L3's `tee_generateQuote` uses, so the on-chain assertion that the attested hash equals the stored hash holds. CI bump: `.github/workflows/test.yml` checkout pin `5a3b8c9` → `0072383`. Source comments updated to match. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 2 +- Cargo.lock | 1 + tests/saya-tee/Cargo.toml | 1 + tests/saya-tee/src/bootstrap.rs | 127 +++++++++++++++++++++++++------- tests/saya-tee/src/saya.rs | 2 +- 5 files changed, 106 insertions(+), 27 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 4c3d33d42..0d269eca0 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -637,7 +637,7 @@ jobs: - name: Clone Saya repository run: | git clone https://github.com/dojoengine/saya /tmp/saya - git -C /tmp/saya checkout 5a3b8c9 + git -C /tmp/saya checkout 0072383 - uses: software-mansion/setup-scarb@v1 with: diff --git a/Cargo.lock b/Cargo.lock index 496c53985..297ca0352 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10346,6 +10346,7 @@ dependencies = [ "katana-genesis", "katana-messaging", "katana-primitives", + "katana-rpc-api", "katana-sequencer-node", "katana-tee", "katana-utils", diff --git a/tests/saya-tee/Cargo.toml b/tests/saya-tee/Cargo.toml index 9b4749e0d..742c42afe 100644 --- a/tests/saya-tee/Cargo.toml +++ b/tests/saya-tee/Cargo.toml @@ -15,6 +15,7 @@ katana-chain-spec.workspace = true katana-genesis.workspace = true katana-messaging.workspace = true katana-primitives.workspace = true +katana-rpc-api.workspace = true katana-sequencer-node = { workspace = true, features = ["tee-mock"] } katana-tee = { workspace = true, features = ["tee-mock"] } katana-utils = { workspace = true, features = ["node"] } diff --git a/tests/saya-tee/src/bootstrap.rs b/tests/saya-tee/src/bootstrap.rs index 192a84348..290252170 100644 --- a/tests/saya-tee/src/bootstrap.rs +++ b/tests/saya-tee/src/bootstrap.rs @@ -1,14 +1,19 @@ //! L2 contract deployment via `saya-ops`. //! //! Shells out to the `saya-ops` binary (built from -//! `dojoengine/saya@5a3b8c9`) to declare and deploy: +//! `dojoengine/saya@0072383` — `feat/tee` post-PR-#72, with `ProgramInfo` +//! enum + `katana_tee_config_hash` plumbing) to declare and deploy: //! //! 1. The `mock_amd_tee_registry` contract — a permissive `IAMDTeeRegistry` mock from //! `cartridge-gg/piltover` (added in piltover#15), vendored into saya at //! `contracts/tee_registry_mock.json` and embedded in the `saya-ops` binary. //! 2. The Piltover core contract. -//! 3. `setup-program` against Piltover, pointing the `fact_registry_address` at the deployed mock -//! TEE registry so its on-chain `verify_sp1_proof` becomes a passthrough. +//! +//! After deployment we configure the Piltover for TEE settlement directly +//! via cainome calls (`set_program_info(KatanaTee variant)` + `set_facts_registry`) +//! rather than going through `saya-ops setup-program`, which only emits the +//! `StarknetOs` variant and would cause the on-chain `validate_input` to +//! panic with cross-mode mismatch when saya-tee later submits `TeeInput`. //! //! `saya-ops` is resolved via `SAYA_OPS_BIN` env var or `$PATH`. Address //! parsing scrapes the `info!`-logged "X address: Felt(0x…)" lines from @@ -17,17 +22,32 @@ use std::path::PathBuf; use std::process::{Command, Stdio}; +use std::time::Duration; use anyhow::{anyhow, Context, Result}; +use katana_chain_spec::rollup::DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS; +use katana_rpc_api::tee::compute_katana_tee_config_hash; +use starknet::accounts::{Account, ExecutionEncoding, SingleOwnerAccount}; +use starknet::core::types::{BlockId, BlockTag, Call}; +use starknet::macros::{selector, short_string}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{JsonRpcClient, Provider}; +use starknet::signers::{LocalWallet, SigningKey}; use starknet_types_core::felt::Felt; use crate::nodes::L2InProcess; -const CHAIN_ID_SHORT_STRING: &str = "katana_e2e"; -const FACT_REGISTRY_SALT: &str = "0x53fac7"; const PILTOVER_SALT: &str = "0x9117f0"; const TEE_REGISTRY_SALT: &str = "0x7ee"; +/// Cairo enum variant index for `ProgramInfo::KatanaTee`. `StarknetOs` is index 0. +const KATANA_TEE_VARIANT_INDEX: Felt = Felt::ONE; + +/// L3 chain id felt. Mirrors `nodes::spawn_l3`'s `ChainId::parse("KATANA")`. +/// Used to compute the `katana_tee_config_hash` that L3's `tee_generateQuote` +/// will bind into `report_data` and that on-chain Piltover asserts against. +const L3_CHAIN_ID: Felt = short_string!("KATANA"); + /// Runs the full L2 bootstrap sequence. pub async fn bootstrap_l2(l2: &L2InProcess) -> Result { let l2_url = l2.url(); @@ -53,8 +73,15 @@ pub async fn bootstrap_l2(l2: &L2InProcess) -> Result { let piltover_address = saya.deploy_core_contract()?; println!(" piltover_address={}", hex(&piltover_address)); - println!("Configuring Piltover with mock TEE registry as fact_registry_address"); - saya.setup_program(piltover_address, tee_registry_address)?; + println!("Configuring Piltover for KatanaTee settlement + mock TEE registry as fact_registry"); + configure_piltover_for_tee( + l2, + piltover_address, + tee_registry_address, + account_address, + account_private_key, + ) + .await?; Ok(BootstrapResult { piltover_address, @@ -64,6 +91,73 @@ pub async fn bootstrap_l2(l2: &L2InProcess) -> Result { }) } +/// Configures the freshly-deployed Piltover for TEE settlement, bypassing +/// `saya-ops setup-program` (which only emits the `StarknetOs` `ProgramInfo` +/// variant). Sets the `KatanaTee` variant whose `katana_tee_config_hash` +/// matches what L3's `tee_generateQuote` will compute, so the on-chain +/// `validate_input` assertion `tee_input.katana_tee_config_hash == +/// KatanaTeeProgramInfo.katana_tee_config_hash` holds at settlement time. +/// +/// Bundles `set_program_info` + `set_facts_registry` in one multicall. +async fn configure_piltover_for_tee( + l2: &L2InProcess, + piltover_address: Felt, + tee_registry_address: Felt, + account_address: Felt, + account_private_key: Felt, +) -> Result<()> { + let provider = l2.provider(); + let l2_chain_id = + provider.chain_id().await.context("failed to fetch L2 chain id for account setup")?; + + let signer = LocalWallet::from_signing_key(SigningKey::from_secret_scalar(account_private_key)); + let mut account = SingleOwnerAccount::new( + provider.clone(), + signer, + account_address, + l2_chain_id, + ExecutionEncoding::New, + ); + account.set_block_id(BlockId::Tag(BlockTag::PreConfirmed)); + + let katana_tee_config_hash = + compute_katana_tee_config_hash(L3_CHAIN_ID, DEFAULT_APPCHAIN_FEE_TOKEN_ADDRESS.into()); + + // ProgramInfo::KatanaTee(KatanaTeeProgramInfo { katana_tee_config_hash }) + // serializes as [variant_index=1, katana_tee_config_hash]. + let set_program_info = Call { + to: piltover_address, + selector: selector!("set_program_info"), + calldata: vec![KATANA_TEE_VARIANT_INDEX, katana_tee_config_hash], + }; + let set_facts_registry = Call { + to: piltover_address, + selector: selector!("set_facts_registry"), + calldata: vec![tee_registry_address], + }; + + let result = account + .execute_v3(vec![set_program_info, set_facts_registry]) + .send() + .await + .context("set_program_info + set_facts_registry multicall failed")?; + + wait_for_tx(&provider, result.transaction_hash).await +} + +async fn wait_for_tx(provider: &JsonRpcClient, tx_hash: Felt) -> Result<()> { + let deadline = std::time::Instant::now() + Duration::from_secs(30); + loop { + match provider.get_transaction_receipt(tx_hash).await { + Ok(_) => return Ok(()), + Err(_) if std::time::Instant::now() < deadline => { + tokio::time::sleep(Duration::from_millis(200)).await; + } + Err(e) => return Err(anyhow!("tx {tx_hash:#x} not accepted: {e}")), + } + } +} + #[derive(Debug, Clone)] pub struct BootstrapResult { /// The address of the deployed Piltover core contract on the settlement layer. @@ -112,23 +206,6 @@ impl SayaOps { parse_address("Core contract address", &output) } - fn setup_program(&self, core_contract: Felt, fact_registry: Felt) -> Result<()> { - let mut cmd = self.base_command()?; - cmd.args([ - "core-contract", - "setup-program", - "--core-contract-address", - &hex(&core_contract), - "--fact-registry-address", - &hex(&fact_registry), - "--chain-id", - CHAIN_ID_SHORT_STRING, - ]); - let _ = FACT_REGISTRY_SALT; // currently unused — left for future fact-registry-mock variant - run(cmd, "core-contract setup-program")?; - Ok(()) - } - fn base_command(&self) -> Result { let bin = resolve_saya_ops_bin()?; let mut cmd = Command::new(bin); @@ -204,7 +281,7 @@ fn resolve_saya_ops_bin() -> Result { } Err(anyhow!( "`saya-ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ - dojoengine/saya@5a3b8c9 with `cargo install --path bin/ops`." + dojoengine/saya@0072383 with `cargo install --path bin/ops`." )) } diff --git a/tests/saya-tee/src/saya.rs b/tests/saya-tee/src/saya.rs index dd38a13d2..8681f5768 100644 --- a/tests/saya-tee/src/saya.rs +++ b/tests/saya-tee/src/saya.rs @@ -7,7 +7,7 @@ //! Build instructions: //! //! ```sh -//! cd dojoengine/saya # rev: 5a3b8c9 +//! cd dojoengine/saya # rev: 0072383 //! cd bin/persistent-tee && cargo install --path . //! ``` //! From a071884cbb0dfae6b482f1d54467fc71fd42e544 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 13:58:54 -0500 Subject: [PATCH 09/16] test(saya-tee): rename binary lookup from `saya-ops` to `ops` Saya renamed the `bin/ops` package from `saya-ops` to `ops` between revs `5a3b8c9` and `0072383`, so `cargo install --bin saya-ops` fails on the new rev with `no bin target named saya-ops, available: ops`. Switch the workflow's `cargo install` to `--bin ops` and update the test's PATH lookup in `resolve_saya_ops_bin()` to look for `ops`. The `SAYA_OPS_BIN` env var still works as an explicit override. The `saya-tee` binary (`bin/persistent-tee` package) keeps its name; that install step is unchanged. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 2 +- tests/saya-tee/src/bootstrap.rs | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 0d269eca0..e2014d481 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -644,7 +644,7 @@ jobs: scarb-version: "2.13.1" - name: Install saya-ops - run: cargo install --locked --path /tmp/saya/bin/ops --bin saya-ops + run: cargo install --locked --path /tmp/saya/bin/ops --bin ops - name: Install saya-tee run: cargo install --locked --path /tmp/saya/bin/persistent-tee --bin saya-tee diff --git a/tests/saya-tee/src/bootstrap.rs b/tests/saya-tee/src/bootstrap.rs index 290252170..32b607613 100644 --- a/tests/saya-tee/src/bootstrap.rs +++ b/tests/saya-tee/src/bootstrap.rs @@ -269,18 +269,18 @@ fn resolve_saya_ops_bin() -> Result { if let Ok(path) = std::env::var("SAYA_OPS_BIN") { return Ok(PathBuf::from(path)); } - // Use `which` from the `which` crate if available; fall back to a manual - // PATH search to keep the dep set minimal. + // The bin target is `ops` (saya `bin/ops` package, renamed from `saya-ops` + // in dojoengine/saya@0072383). Manual PATH search keeps the dep set minimal. if let Ok(path) = std::env::var("PATH") { for dir in std::env::split_paths(&path) { - let candidate = dir.join("saya-ops"); + let candidate = dir.join("ops"); if candidate.is_file() { return Ok(candidate); } } } Err(anyhow!( - "`saya-ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ + "`ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ dojoengine/saya@0072383 with `cargo install --path bin/ops`." )) } From 66703367ef8401ee9e935a7e0f4cd25510ee7110 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 14:21:04 -0500 Subject: [PATCH 10/16] =?UTF-8?q?test(saya-tee):=20rewrite=20ssh://=20?= =?UTF-8?q?=E2=86=92=20https://=20for=20katana-tee=20deps=20in=20CI?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit saya@feat/tee@0072383 pins `cartridge-gg/katana-tee.git` via `ssh://git@github.com/...` URLs in `bin/persistent-tee/Cargo.toml`. CI doesn't have an SSH deploy key loaded for that repo, so `cargo install --path bin/persistent-tee` fails to clone the dep. `cartridge-gg/katana-tee` is public, so we can fetch it anonymously over HTTPS. Add `git config --global url."https://github.com/".insteadOf "ssh://git@github.com/"` before the cargo install steps so cargo fetch resolves SSH URLs as HTTPS. (saya@main uses HTTPS URLs directly per dojoengine/saya#73; this workaround is only needed while the saya rev is on `feat/tee`.) Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index e2014d481..060897a87 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -639,6 +639,14 @@ jobs: git clone https://github.com/dojoengine/saya /tmp/saya git -C /tmp/saya checkout 0072383 + # saya@feat/tee pins `cartridge-gg/katana-tee.git` via `ssh://git@github.com/...` + # in `bin/persistent-tee/Cargo.toml`. CI doesn't have an SSH deploy key + # loaded, but the repo is public — rewrite SSH GitHub URLs to HTTPS so the + # cargo fetch resolves anonymously. (`saya@main` uses HTTPS directly; this + # workaround is only needed while the rev is on `feat/tee`.) + - name: Rewrite ssh:// GitHub URLs to https:// + run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" + - uses: software-mansion/setup-scarb@v1 with: scarb-version: "2.13.1" From 798b93fc03657670d0be2f45dcb14a8a9a937b99 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 14:25:23 -0500 Subject: [PATCH 11/16] test(saya-tee): bump Saya rev to 0a34e57 + drop ssh-rewrite workaround dojoengine/saya@0a34e57 (`feat/tee` HEAD) switches the katana-tee deps in `bin/persistent-tee/Cargo.toml` from `ssh://git@github.com/...` to `https://github.com/...`. The CI runner can clone over HTTPS without needing an SSH deploy key, so the previous workaround (`git config --global url.https://...insteadOf ssh://...`) is no longer needed. Source comments updated to point at the new rev. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 10 +--------- tests/saya-tee/src/bootstrap.rs | 6 +++--- tests/saya-tee/src/saya.rs | 2 +- 3 files changed, 5 insertions(+), 13 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 060897a87..1f2497cfa 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -637,15 +637,7 @@ jobs: - name: Clone Saya repository run: | git clone https://github.com/dojoengine/saya /tmp/saya - git -C /tmp/saya checkout 0072383 - - # saya@feat/tee pins `cartridge-gg/katana-tee.git` via `ssh://git@github.com/...` - # in `bin/persistent-tee/Cargo.toml`. CI doesn't have an SSH deploy key - # loaded, but the repo is public — rewrite SSH GitHub URLs to HTTPS so the - # cargo fetch resolves anonymously. (`saya@main` uses HTTPS directly; this - # workaround is only needed while the rev is on `feat/tee`.) - - name: Rewrite ssh:// GitHub URLs to https:// - run: git config --global url."https://github.com/".insteadOf "ssh://git@github.com/" + git -C /tmp/saya checkout 0a34e57 - uses: software-mansion/setup-scarb@v1 with: diff --git a/tests/saya-tee/src/bootstrap.rs b/tests/saya-tee/src/bootstrap.rs index 32b607613..c6d3a372b 100644 --- a/tests/saya-tee/src/bootstrap.rs +++ b/tests/saya-tee/src/bootstrap.rs @@ -1,7 +1,7 @@ //! L2 contract deployment via `saya-ops`. //! //! Shells out to the `saya-ops` binary (built from -//! `dojoengine/saya@0072383` — `feat/tee` post-PR-#72, with `ProgramInfo` +//! `dojoengine/saya@0a34e57` — `feat/tee` post-PR-#72, with `ProgramInfo` //! enum + `katana_tee_config_hash` plumbing) to declare and deploy: //! //! 1. The `mock_amd_tee_registry` contract — a permissive `IAMDTeeRegistry` mock from @@ -270,7 +270,7 @@ fn resolve_saya_ops_bin() -> Result { return Ok(PathBuf::from(path)); } // The bin target is `ops` (saya `bin/ops` package, renamed from `saya-ops` - // in dojoengine/saya@0072383). Manual PATH search keeps the dep set minimal. + // in dojoengine/saya@0a34e57). Manual PATH search keeps the dep set minimal. if let Ok(path) = std::env::var("PATH") { for dir in std::env::split_paths(&path) { let candidate = dir.join("ops"); @@ -281,7 +281,7 @@ fn resolve_saya_ops_bin() -> Result { } Err(anyhow!( "`ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ - dojoengine/saya@0072383 with `cargo install --path bin/ops`." + dojoengine/saya@0a34e57 with `cargo install --path bin/ops`." )) } diff --git a/tests/saya-tee/src/saya.rs b/tests/saya-tee/src/saya.rs index 8681f5768..351e641fe 100644 --- a/tests/saya-tee/src/saya.rs +++ b/tests/saya-tee/src/saya.rs @@ -7,7 +7,7 @@ //! Build instructions: //! //! ```sh -//! cd dojoengine/saya # rev: 0072383 +//! cd dojoengine/saya # rev: 0a34e57 //! cd bin/persistent-tee && cargo install --path . //! ``` //! From 486be7bfb1393cd8f110f7a79c2545ad99e0bffa Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 14:27:40 -0500 Subject: [PATCH 12/16] test(saya-tee): retarget Saya rev pin to main HEAD (5ff9948) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Saya's `main` already has both the ABI alignment (#73) and the ssh→https URL switch baked in, no need to track a `feat/tee` commit. Repoint `cargo install` and source comments at `5ff9948`, the current `main` HEAD. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 2 +- tests/saya-tee/src/bootstrap.rs | 6 +++--- tests/saya-tee/src/saya.rs | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 1f2497cfa..65b9fd23f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -637,7 +637,7 @@ jobs: - name: Clone Saya repository run: | git clone https://github.com/dojoengine/saya /tmp/saya - git -C /tmp/saya checkout 0a34e57 + git -C /tmp/saya checkout 5ff9948 - uses: software-mansion/setup-scarb@v1 with: diff --git a/tests/saya-tee/src/bootstrap.rs b/tests/saya-tee/src/bootstrap.rs index c6d3a372b..27d53c1c3 100644 --- a/tests/saya-tee/src/bootstrap.rs +++ b/tests/saya-tee/src/bootstrap.rs @@ -1,7 +1,7 @@ //! L2 contract deployment via `saya-ops`. //! //! Shells out to the `saya-ops` binary (built from -//! `dojoengine/saya@0a34e57` — `feat/tee` post-PR-#72, with `ProgramInfo` +//! `dojoengine/saya@5ff9948` — `main` post-PR-#73, with `ProgramInfo` //! enum + `katana_tee_config_hash` plumbing) to declare and deploy: //! //! 1. The `mock_amd_tee_registry` contract — a permissive `IAMDTeeRegistry` mock from @@ -270,7 +270,7 @@ fn resolve_saya_ops_bin() -> Result { return Ok(PathBuf::from(path)); } // The bin target is `ops` (saya `bin/ops` package, renamed from `saya-ops` - // in dojoengine/saya@0a34e57). Manual PATH search keeps the dep set minimal. + // in dojoengine/saya@5ff9948). Manual PATH search keeps the dep set minimal. if let Ok(path) = std::env::var("PATH") { for dir in std::env::split_paths(&path) { let candidate = dir.join("ops"); @@ -281,7 +281,7 @@ fn resolve_saya_ops_bin() -> Result { } Err(anyhow!( "`ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ - dojoengine/saya@0a34e57 with `cargo install --path bin/ops`." + dojoengine/saya@5ff9948 with `cargo install --path bin/ops`." )) } diff --git a/tests/saya-tee/src/saya.rs b/tests/saya-tee/src/saya.rs index 351e641fe..b67ee6189 100644 --- a/tests/saya-tee/src/saya.rs +++ b/tests/saya-tee/src/saya.rs @@ -7,7 +7,7 @@ //! Build instructions: //! //! ```sh -//! cd dojoengine/saya # rev: 0a34e57 +//! cd dojoengine/saya # rev: 5ff9948 //! cd bin/persistent-tee && cargo install --path . //! ``` //! From 340f8803d30924804bea6abf2b23be83f5447607 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 14:47:26 -0500 Subject: [PATCH 13/16] fix --- .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 65b9fd23f..2be500e7f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -644,7 +644,7 @@ jobs: scarb-version: "2.13.1" - name: Install saya-ops - run: cargo install --locked --path /tmp/saya/bin/ops --bin ops + run: cargo install --locked --path /tmp/saya/bin/ops --bin saya-ops - name: Install saya-tee run: cargo install --locked --path /tmp/saya/bin/persistent-tee --bin saya-tee From 8af823accd43c50e935a5156ff1ffbcb8e3a2418 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 14:47:44 -0500 Subject: [PATCH 14/16] remove timeout for now --- .github/workflows/test.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 2be500e7f..ef87deeb5 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -608,7 +608,6 @@ jobs: saya-tee-e2e: needs: [detect-changes, fmt, clippy, generate-test-artifacts] runs-on: ubuntu-latest-32-cores - timeout-minutes: 45 if: | needs.detect-changes.outputs.broader-rust == 'true' && (github.event_name == 'push' || (github.event_name == 'pull_request' && github.event.pull_request.draft == false)) From 5f85491ac527589fa178d1ca2982152ae363a7c4 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Wed, 29 Apr 2026 16:34:02 -0500 Subject: [PATCH 15/16] fix binary name --- tests/saya-tee/src/bootstrap.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/saya-tee/src/bootstrap.rs b/tests/saya-tee/src/bootstrap.rs index 27d53c1c3..f0006adb0 100644 --- a/tests/saya-tee/src/bootstrap.rs +++ b/tests/saya-tee/src/bootstrap.rs @@ -273,15 +273,15 @@ fn resolve_saya_ops_bin() -> Result { // in dojoengine/saya@5ff9948). Manual PATH search keeps the dep set minimal. if let Ok(path) = std::env::var("PATH") { for dir in std::env::split_paths(&path) { - let candidate = dir.join("ops"); + let candidate = dir.join("saya-ops"); if candidate.is_file() { return Ok(candidate); } } } Err(anyhow!( - "`ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ - dojoengine/saya@5ff9948 with `cargo install --path bin/ops`." + "`saya-ops` binary not found. Set SAYA_OPS_BIN env var or add it to $PATH. Build from \ + dojoengine/saya@5ff9948 with `cargo install --path bin/saya-ops`." )) } From 0ba7edacb16d89604c2c9d8edcd235fa46efa8f1 Mon Sep 17 00:00:00 2001 From: Ammar Arif Date: Sun, 3 May 2026 22:23:46 -0500 Subject: [PATCH 16/16] ci(saya-tee-e2e): bump saya pin to 17c0ee0 for v1 report_data fix `dojoengine/saya#74` updated `saya-tee --mock-prove` to emit the v1 report_data layout this PR enforces. Without that bump the e2e job reverts at fee estimation with `tee: config hash half mismatch`. Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/test.yml | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ef87deeb5..cebb1a9e2 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -286,7 +286,15 @@ jobs: run: ./scripts/clippy.sh test: - needs: [detect-changes, fmt, clippy, generate-test-artifacts, build-katana-binary-ubuntu, build-katana-binary-macos] + needs: + [ + detect-changes, + fmt, + clippy, + generate-test-artifacts, + build-katana-binary-ubuntu, + build-katana-binary-macos, + ] runs-on: ${{ matrix.runner }} timeout-minutes: ${{ matrix.os == 'macos' && 60 || 30 }} strategy: @@ -636,7 +644,7 @@ jobs: - name: Clone Saya repository run: | git clone https://github.com/dojoengine/saya /tmp/saya - git -C /tmp/saya checkout 5ff9948 + git -C /tmp/saya checkout 17c0ee0 - uses: software-mansion/setup-scarb@v1 with: