From 8bc5714f2ae85e53be3081d7463f730df8280336 Mon Sep 17 00:00:00 2001 From: Dan Brownstein Date: Thu, 7 Sep 2023 14:22:14 +0300 Subject: [PATCH] feat: calculate transaction hash --- Cargo.toml | 5 +- resources/deprecated_transaction_hash.json | 101 ++++++ resources/transaction_hash.json | 139 ++++++++ src/{transaction.rs => transaction/mod.rs} | 2 + src/transaction/transaction_hash.rs | 366 +++++++++++++++++++++ src/transaction/transaction_hash_test.rs | 96 ++++++ 6 files changed, 707 insertions(+), 2 deletions(-) create mode 100644 resources/deprecated_transaction_hash.json create mode 100644 resources/transaction_hash.json rename src/{transaction.rs => transaction/mod.rs} (99%) create mode 100644 src/transaction/transaction_hash.rs create mode 100644 src/transaction/transaction_hash_test.rs diff --git a/Cargo.toml b/Cargo.toml index 52dd3a5c..d69fd1ab 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,10 +15,11 @@ cairo-lang-starknet = "2.1.0-rc0" derive_more = "0.99.17" hex = "0.4.3" indexmap = { version = "1.9.2", features = ["serde"] } -once_cell = "1.17.1" +once_cell = "1.18.0" primitive-types = { version = "0.12.1", features = ["serde"] } serde = { version = "1.0.130", features = ["derive", "rc"] } -serde_json = "1.0.81" +serde_json = { version = "1.0.81", features = ["arbitrary_precision"]} +sha3 = "0.10.8" starknet-crypto = "0.5.1" thiserror = "1.0.31" diff --git a/resources/deprecated_transaction_hash.json b/resources/deprecated_transaction_hash.json new file mode 100644 index 00000000..7751b313 --- /dev/null +++ b/resources/deprecated_transaction_hash.json @@ -0,0 +1,101 @@ +[ + { + "transaction": { + "Deploy": { + "version": "0x0", + "class_hash": "0x10455c752b86932ce552f2b0fe81a880746649b9aee7e0d842bf3f52378f9f8", + "contract_address_salt": "0x7284a0367fdd636434f76da25532785690d5f27db40ba38b0cfcbc89a472507", + "constructor_calldata": [ + "0x635b73abaa9efff71570cb08f3e5014424788470c3b972b952368fb3fc27cc3", + "0x7e92479a573a24241ee6f3e4ade742ff37bae4a60bacef5be1caaff5e7e04f3" + ] + } + }, + "transaction_hash": "0x45c61314be4da85f0e13df53d18062e002c04803218f08061e4b274d4b38537", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Invoke": { + "V0": { + "max_fee": "0x0", + "signature": [], + "entry_point_selector": "0x218f305395474a84a39307fa5297be118fe17bf65e27ac5e2de6617baa44c64", + "contract_address": "0x2adb4393384c09f049c06bc0070b7a2f72c9cbdcbe841fa7e109a520466cd66", + "calldata": [ + "0x2f40faa63fdd5871415b2dcfb1a5e3e1ca06435b3dda6e2ba9df3f726fd3251", + "0x2" + ] + } + } + }, + "transaction_hash": "0x1822471b7751cbaf98a5cce0003181af95d588e38c958739213af59f389fdc5", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Invoke": { + "V0": { + "max_fee": "0x15f95fc1ea3", + "signature": [ + "0x6371bbc217e89142e93f03d667f6a6c0ecfcecaf0fe5b497a2c09b1490ee6ac", + "0x3f37ec9f2f198c397366bc04d2acb4e3282387d1d5933b1b6febd00911b4d13" + ], + "entry_point_selector": "0x15d40a3d6ca2ac30f4031e42be28da9b056fef9bb7357ac5e85627ee876e5ad", + "contract_address": "0x17239d35be9e3a622b01677fff06c05ea7d926b94f864e59188d1a7eca00b1f", + "calldata": [ + "0x1", + "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7", + "0x83afd3f4caedc6eebf44246fe54e38c95e3179a5ec9ea81740eca5b482d12e", + "0x0", + "0x3", + "0x3", + "0x48457b86ae319cdb795b330b53a26141804d2d79", + "0x71afd498d0000", + "0x0", + "0x99c119b69f9e27f9d0d617fc36afd66bdf6e2ddddcf82411a74a71e671071" + ] + } + } + }, + "transaction_hash": "0x394a595572ee0de6ccf79328c24f9e79979c6cae6a9dd2d413b88ee4a1f32e1", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "L1Handler": { + "version": "0x0", + "contract_address": "0x45c2f280c6d96a1e1ff740fd38eb6caab833db833ff03a23fbc10fc746025f8", + "entry_point_selector": "0x3d7e9aabaee46b2e84062eb3bca33a0c08e1142b329c095d47ec3d6af1adbc6", + "nonce": "0x1784", + "calldata": [ + "0xd9cedf07afdd92ef2733b919637744d5e166492e", + "0x54ae80a9c33ee3b65961d8a97d29fd9cc5d23e2d4efa521462f6cf00f6fd4b2", + "0x2e695b94dc82ae61d665d9f546029f379ffa8bac", + "0x53c9f44836ad00b25c65b360c111bdf2d32115faf2f705d84f1acf69f244775", + "0x434d" + ] + } + }, + "transaction_hash": "0x5772cdd88ca51effeeeff8fcdcd9635c90226bd56ed6b5b6b0e3a318c0a2e9a", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "L1Handler": { + "version": "0x0", + "contract_address": "0x58b43819bb12aba8ab3fb2e997523e507399a3f48a1e2aa20a5fb7734a0449f", + "entry_point_selector": "0xe3f5e9e1456ffa52a3fbc7e8c296631d4cc2120c0be1e2829301c0d8fa026b", + "nonce": "0x0", + "calldata": [ + "0x5474c49483aa09993090979ade8101ebb4cdce4a", + "0xabf8dd8438d1c21e83a8b5e9c1f9b58aaf3ed360", + "0x2", + "0x4c04fac82913f01a8f01f6e15ff7e834ff2d9a9a1d8e9adffc7bd45692f4f9a" + ] + } + }, + "transaction_hash": "0x5d50b7020f7cf8033fd7d913e489f47edf74fbf3c8ada85be512c7baa6a2eab", + "chain_id": "SN_MAIN" + } +] diff --git a/resources/transaction_hash.json b/resources/transaction_hash.json new file mode 100644 index 00000000..be5dc282 --- /dev/null +++ b/resources/transaction_hash.json @@ -0,0 +1,139 @@ +[ + { + "transaction": { + "DeployAccount": { + "max_fee": "0x81ca6d7a5d", + "version": "0x1", + "signature": [ + "0x787e77d333665c7c830a8257e36270410f038fb278e8a468d5c89ec09f2c361", + "0x4310e1aad5c88a32439bf5b64b0b4114c2a42c8468aa97f374a0738d7733a49" + ], + "nonce": "0x0", + "class_hash": "0x1fac3074c9d5282f0acc5c69a4781a1c711efea5e73c550c5d9fb253cf7fd3d", + "contract_address_salt": "0x548987f6a88ad1a506c639f4da4571dfabcfa7fa2abc5da30d3c3c1af71b5d9", + "constructor_calldata": [ + "0x3d03f1e51b6be7edf29c4fa772cf4294259a8449ed32e76ee2169479b06afdd" + ] + } + }, + "transaction_hash": "0xd74a7310e8739a8586b851b928bcc32c955009e3a49057e986d0f2c0a06f16", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Deploy": { + "version": "0x0", + "class_hash": "0x25ec026985a3bf9d0cc1fe17326b245dfdc3ff89b8fde106542a3ea56c5a918", + "contract_address_salt": "0x535c361a7ec21e5b863763b7c2f81ed395c6c23e6001720a1bcac9f4e0b145d", + "constructor_calldata": [ + "0x3e327de1c40540b98d05cbcb13552008e36f0ec8d61d46956d2f9752c294328", + "0x79dc0da7c54b95f10aa182ad0a46400db63156920adb65eca2654c0945a463", + "0x2", + "0x535c361a7ec21e5b863763b7c2f81ed395c6c23e6001720a1bcac9f4e0b145d", + "0x0" + ] + } + }, + "transaction_hash": "0x39017e29a3e0f7f01478ef718096ba4529eabe7e554614f26ff4d0000e2c248", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Invoke": { + "V1": { + "max_fee": "0x174876e8000", + "signature": [ + "0xdd6e2f69ef2c90df1913d93a1a26fb232555a7cc72a279b790a4d869c2db2d", + "0x7f8c1ad6e33514733b8a947e25fbede07e572ace6a85a07e88f42cf531dd1c8" + ], + "nonce": "0x0", + "sender_address": "0x70e1e23753c9dbfb01f1a49dc0a37fedeadf57620792e33ab00049e501816a4", + "calldata": [ + "0x1", + "0x70e1e23753c9dbfb01f1a49dc0a37fedeadf57620792e33ab00049e501816a4", + "0x2730079d734ee55315f4f141eaed376bddd8c2133523d223a344c5604e0f7f8", + "0x0", + "0x4", + "0x4", + "0x1e48f20c157f50d7b135363d3684785e2e05cc7cccb7ddca3f55a7601328c81", + "0x46440f630201391aa9e59608828f559ae01b18d344c711111b66da7ecc69520", + "0x0", + "0x0" + ] + } + } + }, + "transaction_hash": "0x2a50202d4c6936c70f3c604c0f8059f17eab30329b662436b02069a7dd27cc9", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "L1Handler": { + "version": "0x0", + "contract_address": "0x73314940630fd6dcda0d772d4c972c4e0a9946bef9dabf4ef84eda8ef542b82", + "entry_point_selector": "0x2d757788a8d8d6f21d1cd40bce38a8222d70654214e96ff95d8086e684fbee5", + "nonce": "0x43d9a", + "calldata": [ + "0xc3511006c04ef1d78af4c8e0e74ec18a6e64ff9e", + "0x32f2c435695ab22bb83199ec49bed6c1b14db4d1bdefb8a04c3d821509ceced", + "0x16345785d8a0000", + "0x0" + ] + } + }, + "transaction_hash": "0xa5eeb0d17fe3262c2b7b95bd934c5a5ee7243b0e7584a8d2a3fbb9bd456241", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Declare": { + "V0": { + "max_fee": "0x0", + "signature": [], + "nonce": "0x0", + "class_hash": "0x70ffba803149d9cfe1762b5099d15ec129de0e998d1cbd2a29c2abca40c2ce1", + "sender_address": "0x1" + } + } + }, + "transaction_hash": "0x5a5e819664c7550d60d9e4bce8da3478a133602b81e4d0bfdbbc4bb30125a96", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Declare": { + "V1": { + "max_fee": "0x174876e8000", + "signature": [ + "0x6b2c99aac0e375ecca77d5056be39c00328e45c541045d89aeb7458acaffbdb", + "0x732e2b1604487609fdabd9be05ea0e2d828f50bca1e5dd86ca0beabf4670866" + ], + "nonce": "0x5", + "class_hash": "0x4da612e187f8ea1508ca96c726e1da9891bc480f97a157df25568f9dc8ea1dc", + "sender_address": "0x92e02752e4309de75f8f07f4a9efca32649ba83d798f13897714b1a5acd93d" + } + } + }, + "transaction_hash": "0x64b91efce5648a65b950d3aa560b4e265eedd9b49b232a85170510c4c77e9bc", + "chain_id": "SN_GOERLI" + }, + { + "transaction": { + "Declare": { + "V2": { + "max_fee": "0xb7efe7b25236", + "signature": [ + "0x2a1abe101f6651801e5faf2adab17feb726d440fb19a0f1d75e3ea7786b9459", + "0x2fda52f14100da5d87ddffdbc2b48981947285bb549a7760d4f472ce06dba5e" + ], + "nonce": "0x17", + "class_hash": "0x702a9e80c74a214caf0e77326180e72ba3bd3f53dbd5519ede339eb3ae9eed4", + "compiled_class_hash": "0x1be58ebac8e3bc17ae4b7de633ed9c78f2d6dd1acd49c12985bcd5002bdb824", + "sender_address": "0x349d9bdae264d4d613f34a7bcb68e287417e4cd3651e1ff7a89fd75ff04b662" + } + } + }, + "transaction_hash": "0x29410c3ab8d8a7d593e915f3051b9c71e48626cf688c7c8c526f400648cf24c", + "chain_id": "SN_GOERLI" + } +] diff --git a/src/transaction.rs b/src/transaction/mod.rs similarity index 99% rename from src/transaction.rs rename to src/transaction/mod.rs index 5026ebb3..c6ca6ce2 100644 --- a/src/transaction.rs +++ b/src/transaction/mod.rs @@ -1,3 +1,5 @@ +pub mod transaction_hash; + use std::fmt::Display; use std::sync::Arc; diff --git a/src/transaction/transaction_hash.rs b/src/transaction/transaction_hash.rs new file mode 100644 index 00000000..093158ca --- /dev/null +++ b/src/transaction/transaction_hash.rs @@ -0,0 +1,366 @@ +#[cfg(test)] +#[path = "transaction_hash_test.rs"] +mod transaction_hash_test; + +use once_cell::sync::Lazy; +use starknet_crypto::{pedersen_hash, FieldElement}; + +use crate::core::{calculate_contract_address, ChainId, ContractAddress}; +use crate::hash::{StarkFelt, StarkHash}; +use crate::transaction::{ + DeclareTransaction, DeclareTransactionV0V1, DeclareTransactionV2, DeployAccountTransaction, + DeployTransaction, InvokeTransaction, InvokeTransactionV0, InvokeTransactionV1, + L1HandlerTransaction, Transaction, TransactionHash, +}; +use crate::StarknetApiError; + +static DECLARE: Lazy = + Lazy::new(|| ascii_as_felt("declare").expect("should be a valid ascii")); +static DEPLOY: Lazy = + Lazy::new(|| ascii_as_felt("deploy").expect("should be a valid ascii")); +static DEPLOY_ACCOUNT: Lazy = + Lazy::new(|| ascii_as_felt("deploy_account").expect("should be a valid ascii")); +static INVOKE: Lazy = + Lazy::new(|| ascii_as_felt("invoke").expect("should be a valid ascii")); +static L1_HANDLER: Lazy = + Lazy::new(|| ascii_as_felt("l1_handler").expect("should be a valid ascii")); +// The first 250 bits of the Keccak256 hash on "constructor". +static CONSTRUCTOR_ENTRY_POINT_SELECTOR: Lazy = Lazy::new(|| { + StarkFelt::try_from("0x28ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194") + .expect("should be a valid felt") +}); +static TWO: Lazy = Lazy::new(|| StarkFelt::from(2_u8)); + +/// Calculates hash of a Starknet transaction. +/// See transaction types [documentation](https://docs.starknet.io/documentation/architecture_and_concepts/Network_Architecture/transactions) for more details. +pub fn get_transaction_hash( + transaction: &Transaction, + chain_id: &ChainId, +) -> Result { + match transaction { + Transaction::Declare(declare) => match declare { + DeclareTransaction::V0(declare_v0) => { + get_declare_transaction_v0_hash(declare_v0, chain_id) + } + DeclareTransaction::V1(declare_v1) => { + get_declare_transaction_v1_hash(declare_v1, chain_id) + } + DeclareTransaction::V2(declare_v2) => { + get_declare_transaction_v2_hash(declare_v2, chain_id) + } + }, + Transaction::Deploy(deploy) => get_deploy_transaction_hash(deploy, chain_id), + Transaction::DeployAccount(deploy_account) => { + get_deploy_account_transaction_hash(deploy_account, chain_id) + } + Transaction::Invoke(invoke) => match invoke { + InvokeTransaction::V0(invoke_v0) => get_invoke_transaction_v0_hash(invoke_v0, chain_id), + InvokeTransaction::V1(invoke_v1) => get_invoke_transaction_v1_hash(invoke_v1, chain_id), + }, + Transaction::L1Handler(l1_handler) => get_l1_handler_transaction_hash(l1_handler, chain_id), + } +} + +/// Validates hash of a starknet transaction. +/// +/// # NOTE +/// A hash is considered valid if it is the result of one of the hash functions that were ever used +/// in Starknet. +pub fn validate_transaction_hash( + transaction: &Transaction, + chain_id: &ChainId, + expected_hash: TransactionHash, +) -> Result { + let mut possible_hashes = match transaction { + Transaction::Declare(_) => vec![], + Transaction::Deploy(deploy) => { + vec![get_deprecated_deploy_transaction_hash(deploy, chain_id)?] + } + Transaction::DeployAccount(_) => vec![], + Transaction::Invoke(invoke) => match invoke { + InvokeTransaction::V0(invoke_v0) => { + vec![get_deprecated_invoke_transaction_v0_hash(invoke_v0, chain_id)?] + } + InvokeTransaction::V1(_) => vec![], + }, + Transaction::L1Handler(l1_handler) => { + get_deprecated_l1_handler_transaction_hashes(l1_handler, chain_id)? + } + }; + possible_hashes.push(get_transaction_hash(transaction, chain_id)?); + Ok(possible_hashes.contains(&expected_hash)) +} + +// Represents an intermediate calculation of Pedersen hash chain. +struct PedersenHashChain { + current_hash: FieldElement, + length: u128, +} + +impl PedersenHashChain { + pub fn new() -> PedersenHashChain { + PedersenHashChain { current_hash: FieldElement::ZERO, length: 0 } + } + + // Chains a felt to the hash chain. + pub fn chain(self, felt: &StarkFelt) -> Self { + let new_hash = pedersen_hash(&self.current_hash, &FieldElement::from(*felt)); + Self { current_hash: new_hash, length: self.length + 1 } + } + + // Chains a felt to the hash chain if a condition is true. + pub fn chain_if(self, felt: &StarkFelt, condition: bool) -> Self { + if condition { self.chain(felt) } else { self } + } + + // Chains felt_if to the hash chain if a condition is true, otherwise chains felt_else. + pub fn chain_if_else( + self, + felt_if: &StarkFelt, + felt_else: &StarkFelt, + condition: bool, + ) -> Self { + if condition { self.chain(felt_if) } else { self.chain(felt_else) } + } + + // Chains many felts to the hash chain. + pub fn chain_iter<'a>(self, felts: impl Iterator) -> Self { + felts.fold(self, |current, felt| current.chain(felt)) + } + + // Returns the hash of the chained felts, hashed with the length of the chain. + pub fn get_hash(&self) -> StarkHash { + let final_hash = pedersen_hash(&self.current_hash, &FieldElement::from(self.length)); + StarkHash::from(final_hash) + } +} + +fn ascii_as_felt(ascii_str: &str) -> Result { + StarkFelt::try_from(hex::encode(ascii_str).as_str()) +} + +fn get_deploy_account_transaction_hash( + transaction: &DeployAccountTransaction, + chain_id: &ChainId, +) -> Result { + let calldata_hash = PedersenHashChain::new() + .chain(&transaction.class_hash.0) + .chain(&transaction.contract_address_salt.0) + .chain_iter(transaction.constructor_calldata.0.iter()) + .get_hash(); + + let contract_address = calculate_contract_address( + transaction.contract_address_salt, + transaction.class_hash, + &transaction.constructor_calldata, + ContractAddress::from(0_u8), + )?; + + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&DEPLOY_ACCOUNT) + .chain(&transaction.version.0) + .chain(contract_address.0.key()) + .chain(&StarkFelt::ZERO) // No entry point selector in deploy account transaction. + .chain(&calldata_hash) + .chain(&transaction.max_fee.0.into()) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain(&transaction.nonce.0) + .get_hash(), + )) +} + +fn get_deploy_transaction_hash( + transaction: &DeployTransaction, + chain_id: &ChainId, +) -> Result { + get_common_deploy_transaction_hash(transaction, chain_id, false) +} + +fn get_deprecated_deploy_transaction_hash( + transaction: &DeployTransaction, + chain_id: &ChainId, +) -> Result { + get_common_deploy_transaction_hash(transaction, chain_id, true) +} + +fn get_common_deploy_transaction_hash( + transaction: &DeployTransaction, + chain_id: &ChainId, + is_deprecated: bool, +) -> Result { + let contract_address = calculate_contract_address( + transaction.contract_address_salt, + transaction.class_hash, + &transaction.constructor_calldata, + ContractAddress::from(0_u8), + )?; + + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&DEPLOY) + .chain_if(&transaction.version.0, !is_deprecated) + .chain(contract_address.0.key()) + .chain(&CONSTRUCTOR_ENTRY_POINT_SELECTOR) + .chain( + &PedersenHashChain::new() + .chain_iter(transaction.constructor_calldata.0.iter()) + .get_hash(), + ) + .chain_if(&StarkFelt::ZERO, !is_deprecated) // No fee in deploy transaction. + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .get_hash(), + )) +} + +fn get_invoke_transaction_v0_hash( + transaction: &InvokeTransactionV0, + chain_id: &ChainId, +) -> Result { + get_common_invoke_transaction_v0_hash(transaction, chain_id, false) +} + +fn get_deprecated_invoke_transaction_v0_hash( + transaction: &InvokeTransactionV0, + chain_id: &ChainId, +) -> Result { + get_common_invoke_transaction_v0_hash(transaction, chain_id, true) +} + +fn get_common_invoke_transaction_v0_hash( + transaction: &InvokeTransactionV0, + chain_id: &ChainId, + is_deprecated: bool, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&INVOKE) + .chain_if(&StarkFelt::ZERO, !is_deprecated) // Version + .chain(transaction.contract_address.0.key()) + .chain(&transaction.entry_point_selector.0) + .chain(&PedersenHashChain::new().chain_iter(transaction.calldata.0.iter()).get_hash()) + .chain_if(&transaction.max_fee.0.into(), !is_deprecated) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .get_hash(), + )) +} + +fn get_invoke_transaction_v1_hash( + transaction: &InvokeTransactionV1, + chain_id: &ChainId, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&INVOKE) + .chain(&StarkFelt::ONE) // Version + .chain(transaction.sender_address.0.key()) + .chain(&StarkFelt::ZERO) // No entry point selector in invoke transaction. + .chain(&PedersenHashChain::new().chain_iter(transaction.calldata.0.iter()).get_hash()) + .chain(&transaction.max_fee.0.into()) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain(&transaction.nonce.0) + .get_hash(), + )) +} + +#[derive(PartialEq, PartialOrd)] +enum L1HandlerVersions { + AsInvoke, + V0Deprecated, + V0, +} + +fn get_l1_handler_transaction_hash( + transaction: &L1HandlerTransaction, + chain_id: &ChainId, +) -> Result { + get_common_l1_handler_transaction_hash(transaction, chain_id, L1HandlerVersions::V0) +} + +fn get_deprecated_l1_handler_transaction_hashes( + transaction: &L1HandlerTransaction, + chain_id: &ChainId, +) -> Result, StarknetApiError> { + Ok(vec![ + get_common_l1_handler_transaction_hash(transaction, chain_id, L1HandlerVersions::AsInvoke)?, + get_common_l1_handler_transaction_hash( + transaction, + chain_id, + L1HandlerVersions::V0Deprecated, + )?, + ]) +} + +fn get_common_l1_handler_transaction_hash( + transaction: &L1HandlerTransaction, + chain_id: &ChainId, + version: L1HandlerVersions, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain_if_else(&INVOKE, &L1_HANDLER, version == L1HandlerVersions::AsInvoke) + .chain_if(&transaction.version.0, version > L1HandlerVersions::V0Deprecated) + .chain(transaction.contract_address.0.key()) + .chain(&transaction.entry_point_selector.0) + .chain(&PedersenHashChain::new().chain_iter(transaction.calldata.0.iter()).get_hash()) + .chain_if(&StarkFelt::ZERO, version > L1HandlerVersions::V0Deprecated) // No fee in l1 handler transaction. + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain_if(&transaction.nonce.0, version > L1HandlerVersions::AsInvoke) + .get_hash(), + )) +} + +fn get_declare_transaction_v0_hash( + transaction: &DeclareTransactionV0V1, + chain_id: &ChainId, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&DECLARE) + .chain(&StarkFelt::ZERO) // Version + .chain(transaction.sender_address.0.key()) + .chain(&StarkFelt::ZERO ) // No entry point selector in declare transaction. + .chain(&PedersenHashChain::new().get_hash()) + .chain(&transaction.max_fee.0.into()) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain(&transaction.class_hash.0) + .get_hash(), + )) +} + +fn get_declare_transaction_v1_hash( + transaction: &DeclareTransactionV0V1, + chain_id: &ChainId, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&DECLARE) + .chain(&StarkFelt::ONE) // Version + .chain(transaction.sender_address.0.key()) + .chain(&StarkFelt::ZERO) // No entry point selector in declare transaction. + .chain(&PedersenHashChain::new().chain(&transaction.class_hash.0).get_hash()) + .chain(&transaction.max_fee.0.into()) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain(&transaction.nonce.0) + .get_hash(), + )) +} + +fn get_declare_transaction_v2_hash( + transaction: &DeclareTransactionV2, + chain_id: &ChainId, +) -> Result { + Ok(TransactionHash( + PedersenHashChain::new() + .chain(&DECLARE) + .chain(&TWO) // Version + .chain(transaction.sender_address.0.key()) + .chain(&StarkFelt::ZERO) // No entry point selector in declare transaction. + .chain(&PedersenHashChain::new().chain(&transaction.class_hash.0).get_hash()) + .chain(&transaction.max_fee.0.into()) + .chain(&ascii_as_felt(chain_id.0.as_str())?) + .chain(&transaction.nonce.0) + .chain(&transaction.compiled_class_hash.0) + .get_hash(), + )) +} diff --git a/src/transaction/transaction_hash_test.rs b/src/transaction/transaction_hash_test.rs new file mode 100644 index 00000000..84c99ad1 --- /dev/null +++ b/src/transaction/transaction_hash_test.rs @@ -0,0 +1,96 @@ +use std::env; +use std::fs::read_to_string; +use std::path::Path; + +use serde::{Deserialize, Serialize}; +use sha3::{Digest, Keccak256}; + +use super::{ascii_as_felt, get_transaction_hash, CONSTRUCTOR_ENTRY_POINT_SELECTOR}; +use crate::core::ChainId; +use crate::hash::StarkFelt; +use crate::transaction::transaction_hash::validate_transaction_hash; +use crate::transaction::{Transaction, TransactionHash}; + +#[test] +fn test_ascii_as_felt() { + let sn_main_id = ChainId("SN_MAIN".to_owned()); + let sn_main_felt = ascii_as_felt(sn_main_id.0.as_str()).unwrap(); + // This is the result of the Python snippet from the Chain-Id documentation. + let expected_sn_main = StarkFelt::from(23448594291968334_u128); + assert_eq!(sn_main_felt, expected_sn_main); +} + +#[test] +fn test_constructor_selector() { + let mut keccak = Keccak256::default(); + keccak.update(b"constructor"); + let mut constructor_bytes: [u8; 32] = keccak.finalize().into(); + constructor_bytes[0] &= 0b00000011_u8; // Discard the six MSBs. + let constructor_felt = StarkFelt::new(constructor_bytes).unwrap(); + assert_eq!(constructor_felt, *CONSTRUCTOR_ENTRY_POINT_SELECTOR); +} + +#[derive(Deserialize, Serialize)] +struct TransactionTestData { + transaction: Transaction, + transaction_hash: TransactionHash, + chain_id: ChainId, +} + +#[test] +fn test_transaction_hash() { + // The details were taken from Starknet Goerli. You can found the transactions by hash in: + // https://alpha4.starknet.io/feeder_gateway/get_transaction?transactionHash= + let transactions_test_data_vec: Vec = + serde_json::from_value(read_json_file("transaction_hash.json")).unwrap(); + + for transaction_test_data in transactions_test_data_vec { + assert!( + validate_transaction_hash( + &transaction_test_data.transaction, + &transaction_test_data.chain_id, + transaction_test_data.transaction_hash + ) + .unwrap() + ); + let actual_transaction_hash = get_transaction_hash( + &transaction_test_data.transaction, + &transaction_test_data.chain_id, + ) + .unwrap(); + assert_eq!( + actual_transaction_hash, transaction_test_data.transaction_hash, + "expected_transaction_hash: {:?}", + transaction_test_data.transaction_hash + ); + } +} + +#[test] +fn test_deprecated_transaction_hash() { + // The details were taken from Starknet Goerli. You can found the transactions by hash in: + // https://alpha4.starknet.io/feeder_gateway/get_transaction?transactionHash= + let transaction_test_data_vec: Vec = + serde_json::from_value(read_json_file("deprecated_transaction_hash.json")).unwrap(); + + for transaction_test_data in transaction_test_data_vec { + assert!( + validate_transaction_hash( + &transaction_test_data.transaction, + &transaction_test_data.chain_id, + transaction_test_data.transaction_hash + ) + .unwrap(), + "expected_transaction_hash: {:?}", + transaction_test_data.transaction_hash + ); + } +} + +pub fn read_json_file(path_in_resource_dir: &str) -> serde_json::Value { + let path = Path::new(&env::var("CARGO_MANIFEST_DIR").unwrap()) + .join("resources") + .join(path_in_resource_dir); + let json_str = read_to_string(path.to_str().unwrap()).unwrap(); + serde_json::from_str(&json_str).unwrap() +}