From 9c15a744d603b3bc35d5bbddd650fd2f86933f8e Mon Sep 17 00:00:00 2001 From: ron-starkware Date: Mon, 31 Aug 2026 14:39:24 +0300 Subject: [PATCH] apollo_starknet_client: read DeployAccount v4 from the feeder gateway The typed feeder-gateway reader bounced version 0x4 with "DeployAccount version is not supported", so sync-from-feeder and blockifier_reexecution could not see a v4 transaction at all. Route 0x4 to DeployAccountTransaction::V4 and extract the shared v3/v4 field validation, which is identical -- v4 is v3's field set, and only the version felt and the address derivation differ. The reexecution reader passes `deployed_contract_address: None`, so this makes `Transaction::from_api`'s version-dispatched derivation load-bearing for v4. The writer objects need no v4 variant: their DeployAccountTransaction enum is `#[serde(untagged)]`, so a variant with v3's exact field set would be indistinguishable on deserialization, and the v3-shaped struct already carries `version` as data. There is also no v4 producer on that path until the JSON-RPC spec adds the broadcasted type. Co-Authored-By: Claude Opus 5 (1M context) --- .../resources/reader/deploy_account_v4.json | 36 ++++++ .../src/reader/objects/transaction.rs | 121 ++++++++++++------ .../src/reader/objects/transaction_test.rs | 57 +++++++++ 3 files changed, 176 insertions(+), 38 deletions(-) create mode 100644 crates/apollo_starknet_client/resources/reader/deploy_account_v4.json diff --git a/crates/apollo_starknet_client/resources/reader/deploy_account_v4.json b/crates/apollo_starknet_client/resources/reader/deploy_account_v4.json new file mode 100644 index 00000000000..5aa83acf970 --- /dev/null +++ b/crates/apollo_starknet_client/resources/reader/deploy_account_v4.json @@ -0,0 +1,36 @@ +{ + "class_hash": "0x1fcd2dcd811e049eaa730b87038a68f39220a04986b43fe287d690da0df01b8", + "constructor_calldata": [ + "0x0", + "0x1", + "0x2", + "0x3", + "0x4", + "0x5", + "0x6", + "0x7", + "0x8", + "0x9" + ], + "contract_address_salt": "0x2", + "fee_data_availability_mode": 0, + "nonce": "0x0", + "nonce_data_availability_mode": 0, + "paymaster_data": [], + "resource_bounds": { + "L1_GAS": { + "max_amount": "0xffffffffffffffff", + "max_price_per_unit": "0xffffffffffffffffffffffffffffffff" + }, + "L2_GAS": { + "max_amount": "0x0", + "max_price_per_unit": "0x0" + } + }, + "sender_address": "0x1b34d819720bd84c89bdfb476bc2c4d0de9a41b766efabd20fa292280e4c6d9", + "signature": [], + "tip": "0x0", + "transaction_hash": "0x60bc493f32b6a2ebc2f6fd67324e72c5104ca26afa9a1f61726caa1094f340b", + "version": "0x4", + "type": "DEPLOY_ACCOUNT" +} diff --git a/crates/apollo_starknet_client/src/reader/objects/transaction.rs b/crates/apollo_starknet_client/src/reader/objects/transaction.rs index 24c0c8d9359..31acf305037 100644 --- a/crates/apollo_starknet_client/src/reader/objects/transaction.rs +++ b/crates/apollo_starknet_client/src/reader/objects/transaction.rs @@ -371,7 +371,7 @@ impl From for starknet_api::transaction::DeployTransaction { /// Feeder Gateway format for deploy account transactions (see [`Transaction`]). /// -/// Supports multiple transaction versions (V1/V3) through optional fields. +/// Supports multiple transaction versions (V1/V3/V4) through optional fields. // TODO(shahak, 01/11/2023): Add serde tests for v3 transactions. #[derive(Debug, Deserialize, Serialize, Clone, Eq, PartialEq)] #[serde(deny_unknown_fields)] @@ -416,6 +416,9 @@ impl TryFrom if deploy_account_tx.version == TransactionVersion::THREE { return Ok(Self::V3(deploy_account_tx.try_into()?)); } + if deploy_account_tx.version == TransactionVersion::FOUR { + return Ok(Self::V4(deploy_account_tx.try_into()?)); + } Err(ReaderClientError::BadTransaction { tx_hash: deploy_account_tx.transaction_hash, msg: format!("DeployAccount version {:?} is not supported.", deploy_account_tx.version), @@ -453,48 +456,90 @@ impl TryFrom fn try_from( deploy_account_tx: IntermediateDeployAccountTransaction, ) -> Result { + deploy_account_v3_shaped_fields(deploy_account_tx) + } +} + +/// V4 has the same field set as V3; only the version felt and the address derivation differ. +impl TryFrom + for starknet_api::transaction::DeployAccountTransactionV4 +{ + type Error = ReaderClientError; + + fn try_from( + deploy_account_tx: IntermediateDeployAccountTransaction, + ) -> Result { + let starknet_api::transaction::DeployAccountTransactionV3 { + resource_bounds, + tip, + signature, + nonce, + class_hash, + contract_address_salt, + constructor_calldata, + nonce_data_availability_mode, + fee_data_availability_mode, + paymaster_data, + } = deploy_account_v3_shaped_fields(deploy_account_tx)?; Ok(Self { - resource_bounds: deploy_account_tx.resource_bounds.ok_or( - ReaderClientError::BadTransaction { - tx_hash: deploy_account_tx.transaction_hash, - msg: "DeployAccount V3 must contain resource_bounds field.".to_string(), - }, - )?, - tip: deploy_account_tx.tip.ok_or(ReaderClientError::BadTransaction { - tx_hash: deploy_account_tx.transaction_hash, - msg: "DeployAccount V3 must contain tip field.".to_string(), - })?, - signature: deploy_account_tx.signature, - nonce: deploy_account_tx.nonce, - class_hash: deploy_account_tx.class_hash, - contract_address_salt: deploy_account_tx.contract_address_salt, - constructor_calldata: deploy_account_tx.constructor_calldata, - nonce_data_availability_mode: deploy_account_tx - .nonce_data_availability_mode - .ok_or(ReaderClientError::BadTransaction { - tx_hash: deploy_account_tx.transaction_hash, - msg: "DeployAccount V3 must contain nonce_data_availability_mode field." - .to_string(), - })? - .into(), - fee_data_availability_mode: deploy_account_tx - .fee_data_availability_mode - .ok_or(ReaderClientError::BadTransaction { - tx_hash: deploy_account_tx.transaction_hash, - msg: "DeployAccount V3 must contain fee_data_availability_mode field." - .to_string(), - })? - .into(), - paymaster_data: deploy_account_tx.paymaster_data.ok_or( - ReaderClientError::BadTransaction { - tx_hash: deploy_account_tx.transaction_hash, - msg: "DeployAccount V3 must contain paymaster_data field.".to_string(), - }, - )?, + resource_bounds, + tip, + signature, + nonce, + class_hash, + contract_address_salt, + constructor_calldata, + nonce_data_availability_mode, + fee_data_availability_mode, + paymaster_data, }) } } +fn deploy_account_v3_shaped_fields( + deploy_account_tx: IntermediateDeployAccountTransaction, +) -> Result { + Ok(starknet_api::transaction::DeployAccountTransactionV3 { + resource_bounds: deploy_account_tx.resource_bounds.ok_or( + ReaderClientError::BadTransaction { + tx_hash: deploy_account_tx.transaction_hash, + msg: "DeployAccount V3/V4 must contain resource_bounds field.".to_string(), + }, + )?, + tip: deploy_account_tx.tip.ok_or(ReaderClientError::BadTransaction { + tx_hash: deploy_account_tx.transaction_hash, + msg: "DeployAccount V3/V4 must contain tip field.".to_string(), + })?, + signature: deploy_account_tx.signature, + nonce: deploy_account_tx.nonce, + class_hash: deploy_account_tx.class_hash, + contract_address_salt: deploy_account_tx.contract_address_salt, + constructor_calldata: deploy_account_tx.constructor_calldata, + nonce_data_availability_mode: deploy_account_tx + .nonce_data_availability_mode + .ok_or(ReaderClientError::BadTransaction { + tx_hash: deploy_account_tx.transaction_hash, + msg: "DeployAccount V3/V4 must contain nonce_data_availability_mode field." + .to_string(), + })? + .into(), + fee_data_availability_mode: deploy_account_tx + .fee_data_availability_mode + .ok_or(ReaderClientError::BadTransaction { + tx_hash: deploy_account_tx.transaction_hash, + msg: "DeployAccount V3/V4 must contain fee_data_availability_mode field." + .to_string(), + })? + .into(), + paymaster_data: deploy_account_tx.paymaster_data.ok_or( + ReaderClientError::BadTransaction { + tx_hash: deploy_account_tx.transaction_hash, + msg: "DeployAccount V3/V4 must contain paymaster_data field.".to_string(), + }, + )?, + }) +} + /// Feeder Gateway format for invoke transactions (see [`Transaction`]). /// /// Supports multiple transaction versions (V0/V1/V3) through optional fields. diff --git a/crates/apollo_starknet_client/src/reader/objects/transaction_test.rs b/crates/apollo_starknet_client/src/reader/objects/transaction_test.rs index 43de9f01d9d..904967c8af3 100644 --- a/crates/apollo_starknet_client/src/reader/objects/transaction_test.rs +++ b/crates/apollo_starknet_client/src/reader/objects/transaction_test.rs @@ -1,4 +1,5 @@ use assert_matches::assert_matches; +use starknet_api::transaction::DeployAccountTransaction; use super::{Transaction, TransactionReceipt}; use crate::test_utils::read_resource::read_resource_file; @@ -57,6 +58,7 @@ fn load_transaction_succeeds() { "reader/declare_v0.json", "reader/declare_v3.json", "reader/deploy_account_v3.json", + "reader/deploy_account_v4.json", ] { let res = serde_json::from_str::(&read_resource_file(file_name)); assert!(res.is_ok(), "filename: {file_name}, error: {res:?}"); @@ -70,6 +72,7 @@ fn load_transaction_unknown_field_fails() { "reader/invoke_v0.json", "reader/declare_v0.json", "reader/deploy_account_v3.json", + "reader/deploy_account_v4.json", ] { let mut json_value: serde_json::Value = serde_json::from_str(&read_resource_file(file_name)).unwrap(); @@ -90,6 +93,7 @@ fn load_transaction_wrong_type_fails() { ("reader/invoke_v0.json", "DECLARE"), ("reader/declare_v0.json", "DEPLOY"), ("reader/deploy_account_v3.json", "INVOKE_FUNCTION"), + ("reader/deploy_account_v4.json", "DECLARE"), ] { let mut json_value: serde_json::Value = serde_json::from_str(&read_resource_file(file_name)).unwrap(); @@ -119,3 +123,56 @@ fn load_transaction_receipt_succeeds() { ); } } + +#[test] +fn deploy_account_v4_converts_to_starknet_api_v4() { + let client_tx: Transaction = + serde_json::from_str(&read_resource_file("reader/deploy_account_v4.json")).unwrap(); + let Transaction::DeployAccount(deploy_account_tx) = client_tx.clone() else { + panic!("Expected a deploy account transaction."); + }; + + let api_tx = DeployAccountTransaction::try_from(deploy_account_tx).unwrap(); + let DeployAccountTransaction::V4(v4_tx) = &api_tx else { + panic!("Expected a V4 deploy account transaction, got {api_tx:?}."); + }; + + // V4 carries the v3 field set verbatim; the version felt is the only difference. + let v3_client_tx: Transaction = + serde_json::from_str(&read_resource_file("reader/deploy_account_v3.json")).unwrap(); + let Transaction::DeployAccount(v3_deploy_account_tx) = v3_client_tx else { + panic!("Expected a deploy account transaction."); + }; + let DeployAccountTransaction::V3(v3_tx) = + DeployAccountTransaction::try_from(v3_deploy_account_tx).unwrap() + else { + panic!("Expected a V3 deploy account transaction."); + }; + assert_eq!(v4_tx.resource_bounds, v3_tx.resource_bounds); + assert_eq!(v4_tx.constructor_calldata, v3_tx.constructor_calldata); + assert_eq!(v4_tx.class_hash, v3_tx.class_hash); + assert_eq!(v4_tx.contract_address_salt, v3_tx.contract_address_salt); + + // Round-trip: the client form serializes back to the same JSON. + let round_tripped: serde_json::Value = serde_json::to_value(&client_tx).unwrap(); + let original: serde_json::Value = + serde_json::from_str(&read_resource_file("reader/deploy_account_v4.json")).unwrap(); + assert_eq!(round_tripped, original); +} + +#[test] +fn version_0x4_is_deploy_account_only() { + for file_name in ["reader/invoke_v3.json", "reader/declare_v3.json"] { + let mut json_value: serde_json::Value = + serde_json::from_str(&read_resource_file(file_name)).unwrap(); + json_value + .as_object_mut() + .unwrap() + .insert("version".to_string(), serde_json::Value::String("0x4".to_string())); + let client_tx: Transaction = serde_json::from_value(json_value).unwrap(); + assert!( + starknet_api::transaction::Transaction::try_from(client_tx).is_err(), + "filename: {file_name}" + ); + } +}