├── .cargo └── config.toml ├── .clippy.toml ├── .github └── workflows │ └── ci.yml ├── .gitignore ├── Cargo.toml ├── LICENSE ├── README.md ├── rustfmt.toml ├── scripts └── rust_fmt.sh └── src ├── block.rs ├── block_hash.rs ├── block_hash ├── block_hash_calculator.rs ├── block_hash_calculator_test.rs ├── event_commitment.rs ├── event_commitment_test.rs ├── receipt_commitment.rs ├── receipt_commitment_test.rs ├── state_diff_hash.rs ├── state_diff_hash_test.rs ├── test_utils.rs ├── transaction_commitment.rs └── transaction_commitment_test.rs ├── block_test.rs ├── core.rs ├── core_test.rs ├── crypto.rs ├── crypto ├── crypto_test.rs ├── patricia_hash.rs ├── patricia_hash_test.rs └── utils.rs ├── data_availability.rs ├── deprecated_contract_class.rs ├── hash.rs ├── internal_transaction.rs ├── lib.rs ├── rpc_transaction.rs ├── rpc_transaction_test.rs ├── serde_utils.rs ├── serde_utils_test.rs ├── state.rs ├── state_test.rs ├── transaction.rs ├── transaction_hash.rs └── type_utils.rs /.cargo/config.toml: -------------------------------------------------------------------------------- 1 | [build] 2 | rustflags = [ 3 | "-Dwarnings", 4 | "-Dfuture-incompatible", 5 | "-Dnonstandard-style", 6 | "-Drust-2018-idioms", 7 | "-Dunused", 8 | ] 9 | -------------------------------------------------------------------------------- /.clippy.toml: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: 8 | - opened 9 | - reopened 10 | - synchronize 11 | - auto_merge_enabled 12 | branches: [main] 13 | merge_group: 14 | types: [checks_requested] 15 | 16 | jobs: 17 | rustfmt: 18 | runs-on: ubuntu-latest 19 | steps: 20 | - uses: actions/checkout@v3 21 | - uses: dtolnay/rust-toolchain@master 22 | with: 23 | components: rustfmt 24 | toolchain: nightly-2023-07-05 25 | - uses: Swatinem/rust-cache@v2 26 | - run: scripts/rust_fmt.sh --check 27 | 28 | clippy: 29 | runs-on: ubuntu-latest 30 | steps: 31 | - uses: actions/checkout@v3 32 | - name: Rust Toolchain Setup 33 | run: rustup show 34 | - uses: Swatinem/rust-cache@v2 35 | - name: Check format 36 | run: cargo clippy --all-targets --all-features 37 | 38 | run-tests: 39 | runs-on: ubuntu-latest 40 | steps: 41 | - uses: actions/checkout@v3 42 | - name: Rust Toolchain Setup 43 | run: rustup show 44 | - uses: Swatinem/rust-cache@v2 45 | - run: pip install cairo-lang; cargo test -- --include-ignored 46 | 47 | udeps: 48 | runs-on: ubuntu-latest 49 | steps: 50 | - uses: actions/checkout@v3 51 | - uses: dtolnay/rust-toolchain@master 52 | name: "Rust Toolchain Setup" 53 | with: 54 | toolchain: nightly-2023-07-05 55 | - uses: Swatinem/rust-cache@v2 56 | - name: "Download and run cargo-udeps" 57 | run: | 58 | wget -O - -c https://github.com/est31/cargo-udeps/releases/download/v0.1.35/cargo-udeps-v0.1.35-x86_64-unknown-linux-gnu.tar.gz | tar -xz 59 | cargo-udeps-*/cargo-udeps udeps 60 | env: 61 | RUSTUP_TOOLCHAIN: nightly-2023-07-05 62 | 63 | doc: 64 | runs-on: ubuntu-latest 65 | env: 66 | RUSTDOCFLAGS: -D warnings 67 | steps: 68 | - uses: actions/checkout@v2 69 | - name: Rust Toolchain Setup 70 | run: rustup show 71 | - uses: Swatinem/rust-cache@v2 72 | - run: cargo doc --document-private-items --no-deps 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | /data 2 | /logs 3 | /target 4 | /Cargo.lock 5 | *.DS_Store 6 | -------------------------------------------------------------------------------- /Cargo.toml: -------------------------------------------------------------------------------- 1 | [package] 2 | name = "starknet_api" 3 | version = "0.13.0-rc.0" 4 | edition = "2021" 5 | repository = "https://github.com/starkware-libs/starknet-api" 6 | license = "Apache-2.0" 7 | license-file = "LICENSE" 8 | description = "Starknet Rust types related to computation and execution." 9 | 10 | [features] 11 | testing = [] 12 | 13 | [dependencies] 14 | bitvec = "1.0.1" 15 | cairo-lang-starknet-classes = "2.7.0-dev.0" 16 | derive_more = "0.99.17" 17 | hex = "0.4.3" 18 | indexmap = { version = "2.1.0", features = ["serde"] } 19 | itertools = "0.12.1" 20 | once_cell = "1.17.1" 21 | primitive-types = { version = "0.12.1", features = ["serde"] } 22 | serde = { version = "1.0.130", features = ["derive", "rc"] } 23 | serde_json = "1.0.81" 24 | sha3 = "0.10.8" 25 | starknet-crypto = "0.5.1" 26 | starknet-types-core = { version = "0.1.4", features = ["hash"] } 27 | strum = "0.24.1" 28 | strum_macros = "0.24.3" 29 | thiserror = "1.0.31" 30 | 31 | [dev-dependencies] 32 | assert_matches = "1.5.0" 33 | rstest = "0.17.0" 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [yyyy] [name of copyright owner] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # starknet-api 2 | 3 | ## About 4 | 5 | `starknet-api` contains general type definitions in Rust for starknet. 6 | 7 | ## License 8 | 9 | This project is licensed under the **Apache 2.0 license**. 10 | See [LICENSE](LICENSE) for more information. -------------------------------------------------------------------------------- /rustfmt.toml: -------------------------------------------------------------------------------- 1 | edition = "2021" 2 | newline_style = "Unix" 3 | use_field_init_shorthand = true 4 | use_small_heuristics = "Max" 5 | use_try_shorthand = true 6 | max_width = 100 7 | 8 | # Unstable features below 9 | unstable_features = true 10 | version = "Two" 11 | comment_width = 100 12 | format_code_in_doc_comments = true 13 | format_macro_bodies = true 14 | format_macro_matchers = true 15 | format_strings = true 16 | imports_granularity = "Module" 17 | group_imports = "StdExternalCrate" 18 | normalize_comments = true 19 | normalize_doc_attributes = true 20 | wrap_comments = true 21 | 22 | # To use these settings in vscode, add the following line to your settings: 23 | # "rust-analyzer.rustfmt.overrideCommand": [ 24 | # "rustup", 25 | # "run", 26 | # "nightly-2022-07-27", 27 | # "--", 28 | # "rustfmt", 29 | # "--edition", 30 | # "2018", 31 | # "--" 32 | # ] 33 | # and run "rustup toolchain install nightly-2022-07-27". 34 | -------------------------------------------------------------------------------- /scripts/rust_fmt.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | cargo +nightly-2023-07-05 fmt --all -- "$@" 4 | -------------------------------------------------------------------------------- /src/block.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[path = "block_test.rs"] 3 | mod block_test; 4 | 5 | use std::fmt::Display; 6 | 7 | use derive_more::Display; 8 | use serde::{Deserialize, Serialize}; 9 | use starknet_types_core::hash::{Poseidon, StarkHash as CoreStarkHash}; 10 | 11 | use crate::core::{ 12 | EventCommitment, GlobalRoot, ReceiptCommitment, SequencerContractAddress, SequencerPublicKey, 13 | StateDiffCommitment, TransactionCommitment, 14 | }; 15 | use crate::crypto::utils::{verify_message_hash_signature, CryptoError, Signature}; 16 | use crate::data_availability::L1DataAvailabilityMode; 17 | use crate::hash::StarkHash; 18 | use crate::serde_utils::{BytesAsHex, PrefixedBytesAsHex}; 19 | use crate::transaction::{Transaction, TransactionHash, TransactionOutput}; 20 | 21 | /// A block. 22 | #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] 23 | pub struct Block { 24 | // TODO: Consider renaming to BlockWithCommitments, for the header use BlockHeaderWithoutHash 25 | // instead of BlockHeader, and add BlockHeaderCommitments and BlockHash fields. 26 | pub header: BlockHeader, 27 | pub body: BlockBody, 28 | } 29 | 30 | /// A version of the Starknet protocol used when creating a block. 31 | #[derive(Clone, Debug, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 32 | pub struct StarknetVersion(pub String); 33 | 34 | impl Default for StarknetVersion { 35 | fn default() -> Self { 36 | Self("0.0.0".to_string()) 37 | } 38 | } 39 | 40 | impl Display for StarknetVersion { 41 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 42 | write!(f, "{}", self.0) 43 | } 44 | } 45 | 46 | /// The header of a [Block](`crate::block::Block`). 47 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 48 | pub struct BlockHeader { 49 | // TODO: Consider removing the block hash from the header (note it can be computed from 50 | // the rest of the fields. 51 | pub block_hash: BlockHash, 52 | pub parent_hash: BlockHash, 53 | pub block_number: BlockNumber, 54 | pub l1_gas_price: GasPricePerToken, 55 | pub l1_data_gas_price: GasPricePerToken, 56 | pub state_root: GlobalRoot, 57 | pub sequencer: SequencerContractAddress, 58 | pub timestamp: BlockTimestamp, 59 | pub l1_da_mode: L1DataAvailabilityMode, 60 | // The optional fields below are not included in older versions of the block. 61 | // Currently they are not included in any RPC spec, so we skip their serialization. 62 | // TODO: Once all environments support these fields, remove the Option (make sure to 63 | // update/resync any storage is missing the data). 64 | #[serde(skip_serializing)] 65 | pub state_diff_commitment: Option, 66 | #[serde(skip_serializing)] 67 | pub state_diff_length: Option, 68 | #[serde(skip_serializing)] 69 | pub transaction_commitment: Option, 70 | #[serde(skip_serializing)] 71 | pub event_commitment: Option, 72 | #[serde(skip_serializing)] 73 | pub n_transactions: usize, 74 | #[serde(skip_serializing)] 75 | pub n_events: usize, 76 | #[serde(skip_serializing)] 77 | pub receipt_commitment: Option, 78 | pub starknet_version: StarknetVersion, 79 | } 80 | 81 | /// The header of a [Block](`crate::block::Block`) without hashing. 82 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 83 | pub struct BlockHeaderWithoutHash { 84 | pub parent_hash: BlockHash, 85 | pub block_number: BlockNumber, 86 | pub l1_gas_price: GasPricePerToken, 87 | pub l1_data_gas_price: GasPricePerToken, 88 | pub state_root: GlobalRoot, 89 | pub sequencer: SequencerContractAddress, 90 | pub timestamp: BlockTimestamp, 91 | pub l1_da_mode: L1DataAvailabilityMode, 92 | pub starknet_version: StarknetVersion, 93 | } 94 | 95 | /// The [transactions](`crate::transaction::Transaction`) and their 96 | /// [outputs](`crate::transaction::TransactionOutput`) in a [block](`crate::block::Block`). 97 | #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] 98 | pub struct BlockBody { 99 | pub transactions: Vec, 100 | pub transaction_outputs: Vec, 101 | pub transaction_hashes: Vec, 102 | } 103 | 104 | /// The status of a [Block](`crate::block::Block`). 105 | #[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 106 | pub enum BlockStatus { 107 | /// A pending block; i.e., a block that is yet to be closed. 108 | #[serde(rename = "PENDING")] 109 | Pending, 110 | /// A block that was created on L2. 111 | #[serde(rename = "ACCEPTED_ON_L2")] 112 | AcceptedOnL2, 113 | /// A block that was accepted on L1. 114 | #[serde(rename = "ACCEPTED_ON_L1")] 115 | AcceptedOnL1, 116 | /// A block rejected on L1. 117 | #[serde(rename = "REJECTED")] 118 | Rejected, 119 | } 120 | 121 | /// The hash of a [Block](`crate::block::Block`). 122 | #[derive( 123 | Debug, 124 | Default, 125 | Copy, 126 | Clone, 127 | Eq, 128 | PartialEq, 129 | Hash, 130 | Deserialize, 131 | Serialize, 132 | PartialOrd, 133 | Ord, 134 | Display, 135 | )] 136 | pub struct BlockHash(pub StarkHash); 137 | 138 | /// The number of a [Block](`crate::block::Block`). 139 | #[derive( 140 | Debug, 141 | Default, 142 | Copy, 143 | Display, 144 | Clone, 145 | Eq, 146 | PartialEq, 147 | Hash, 148 | Deserialize, 149 | Serialize, 150 | PartialOrd, 151 | Ord, 152 | )] 153 | pub struct BlockNumber(pub u64); 154 | 155 | impl BlockNumber { 156 | /// Returns the next block number, without checking if it's in range. 157 | pub fn unchecked_next(&self) -> BlockNumber { 158 | BlockNumber(self.0 + 1) 159 | } 160 | 161 | /// Returns the next block number, or None if the next block number is out of range. 162 | pub fn next(&self) -> Option { 163 | Some(Self(self.0.checked_add(1)?)) 164 | } 165 | 166 | /// Returns the previous block number, or None if the previous block number is out of range. 167 | pub fn prev(&self) -> Option { 168 | match self.0 { 169 | 0 => None, 170 | i => Some(BlockNumber(i - 1)), 171 | } 172 | } 173 | 174 | /// Returns an iterator over the block numbers from self to up_to (exclusive). 175 | pub fn iter_up_to(&self, up_to: Self) -> impl Iterator { 176 | let range = self.0..up_to.0; 177 | range.map(Self) 178 | } 179 | } 180 | 181 | // TODO(yair): Consider moving GasPricePerToken and GasPrice to core. 182 | /// The gas price per token. 183 | #[derive( 184 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 185 | )] 186 | pub struct GasPricePerToken { 187 | pub price_in_fri: GasPrice, 188 | pub price_in_wei: GasPrice, 189 | } 190 | 191 | /// The gas price at a [Block](`crate::block::Block`). 192 | #[derive( 193 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 194 | )] 195 | #[serde(from = "PrefixedBytesAsHex<16_usize>", into = "PrefixedBytesAsHex<16_usize>")] 196 | pub struct GasPrice(pub u128); 197 | 198 | impl From> for GasPrice { 199 | fn from(val: PrefixedBytesAsHex<16_usize>) -> Self { 200 | GasPrice(u128::from_be_bytes(val.0)) 201 | } 202 | } 203 | 204 | impl From for PrefixedBytesAsHex<16_usize> { 205 | fn from(val: GasPrice) -> Self { 206 | BytesAsHex(val.0.to_be_bytes()) 207 | } 208 | } 209 | 210 | /// The timestamp of a [Block](`crate::block::Block`). 211 | #[derive( 212 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 213 | )] 214 | pub struct BlockTimestamp(pub u64); 215 | 216 | /// The signature of a [Block](`crate::block::Block`), signed by the sequencer. The signed message 217 | /// is defined as poseidon_hash(block_hash, state_diff_commitment). 218 | #[derive( 219 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 220 | )] 221 | pub struct BlockSignature(pub Signature); 222 | 223 | /// The error type returned from the block verification functions. 224 | #[derive(thiserror::Error, Clone, Debug)] 225 | pub enum BlockVerificationError { 226 | #[error("Failed to verify the signature of block {block_hash}. Error: {error}")] 227 | BlockSignatureVerificationFailed { block_hash: BlockHash, error: CryptoError }, 228 | } 229 | 230 | /// Verifies that the the block header was signed by the expected sequencer. 231 | pub fn verify_block_signature( 232 | sequencer_pub_key: &SequencerPublicKey, 233 | signature: &BlockSignature, 234 | state_diff_commitment: &GlobalRoot, 235 | block_hash: &BlockHash, 236 | ) -> Result { 237 | let message_hash = Poseidon::hash_array(&[block_hash.0, state_diff_commitment.0]); 238 | verify_message_hash_signature(&message_hash, &signature.0, &sequencer_pub_key.0).map_err( 239 | |err| BlockVerificationError::BlockSignatureVerificationFailed { 240 | block_hash: *block_hash, 241 | error: err, 242 | }, 243 | ) 244 | } 245 | -------------------------------------------------------------------------------- /src/block_hash.rs: -------------------------------------------------------------------------------- 1 | pub mod block_hash_calculator; 2 | pub mod event_commitment; 3 | pub mod receipt_commitment; 4 | pub mod state_diff_hash; 5 | pub mod transaction_commitment; 6 | 7 | #[cfg(test)] 8 | pub mod test_utils; 9 | -------------------------------------------------------------------------------- /src/block_hash/block_hash_calculator.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use serde::{Deserialize, Serialize}; 3 | use starknet_types_core::felt::Felt; 4 | use starknet_types_core::hash::Poseidon; 5 | 6 | use super::event_commitment::{calculate_event_commitment, EventLeafElement}; 7 | use super::receipt_commitment::{calculate_receipt_commitment, ReceiptElement}; 8 | use super::state_diff_hash::calculate_state_diff_hash; 9 | use super::transaction_commitment::{calculate_transaction_commitment, TransactionLeafElement}; 10 | use crate::block::{BlockHash, BlockHeaderWithoutHash}; 11 | use crate::core::{EventCommitment, ReceiptCommitment, StateDiffCommitment, TransactionCommitment}; 12 | use crate::crypto::utils::HashChain; 13 | use crate::data_availability::L1DataAvailabilityMode; 14 | use crate::state::ThinStateDiff; 15 | use crate::transaction::{ 16 | Event, Fee, GasVector, MessageToL1, TransactionExecutionStatus, TransactionHash, 17 | TransactionSignature, 18 | }; 19 | use crate::transaction_hash::ascii_as_felt; 20 | 21 | #[cfg(test)] 22 | #[path = "block_hash_calculator_test.rs"] 23 | mod block_hash_calculator_test; 24 | 25 | static STARKNET_BLOCK_HASH0: Lazy = Lazy::new(|| { 26 | ascii_as_felt("STARKNET_BLOCK_HASH0").expect("ascii_as_felt failed for 'STARKNET_BLOCK_HASH0'") 27 | }); 28 | 29 | /// The common fields of transaction output types. 30 | #[derive(Clone, Debug, Deserialize, PartialEq, Eq)] 31 | pub struct TransactionOutputForHash { 32 | pub actual_fee: Fee, 33 | pub events: Vec, 34 | pub execution_status: TransactionExecutionStatus, 35 | pub gas_consumed: GasVector, 36 | pub messages_sent: Vec, 37 | } 38 | 39 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq)] 40 | pub struct TransactionHashingData { 41 | pub transaction_signature: Option, 42 | pub transaction_output: TransactionOutputForHash, 43 | pub transaction_hash: TransactionHash, 44 | } 45 | 46 | /// Commitments of a block. 47 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 48 | pub struct BlockHeaderCommitments { 49 | pub transaction_commitment: TransactionCommitment, 50 | pub event_commitment: EventCommitment, 51 | pub receipt_commitment: ReceiptCommitment, 52 | pub state_diff_commitment: StateDiffCommitment, 53 | pub concatenated_counts: Felt, 54 | } 55 | 56 | /// Poseidon ( 57 | /// “STARKNET_BLOCK_HASH0”, block_number, global_state_root, sequencer_address, 58 | /// block_timestamp, concat_counts, state_diff_hash, transaction_commitment, 59 | /// event_commitment, receipt_commitment, gas_price_wei, gas_price_fri, 60 | /// data_gas_price_wei, data_gas_price_fri, starknet_version, 0, parent_block_hash 61 | /// ). 62 | pub fn calculate_block_hash( 63 | header: BlockHeaderWithoutHash, 64 | block_commitments: BlockHeaderCommitments, 65 | ) -> BlockHash { 66 | BlockHash( 67 | HashChain::new() 68 | .chain(&STARKNET_BLOCK_HASH0) 69 | .chain(&header.block_number.0.into()) 70 | .chain(&header.state_root.0) 71 | .chain(&header.sequencer.0) 72 | .chain(&header.timestamp.0.into()) 73 | .chain(&block_commitments.concatenated_counts) 74 | .chain(&block_commitments.state_diff_commitment.0.0) 75 | .chain(&block_commitments.transaction_commitment.0) 76 | .chain(&block_commitments.event_commitment.0) 77 | .chain(&block_commitments.receipt_commitment.0) 78 | .chain(&header.l1_gas_price.price_in_wei.0.into()) 79 | .chain(&header.l1_gas_price.price_in_fri.0.into()) 80 | .chain(&header.l1_data_gas_price.price_in_wei.0.into()) 81 | .chain(&header.l1_data_gas_price.price_in_fri.0.into()) 82 | .chain(&ascii_as_felt(&header.starknet_version.0).expect("Expect ASCII version")) 83 | .chain(&Felt::ZERO) 84 | .chain(&header.parent_hash.0) 85 | .get_poseidon_hash(), 86 | ) 87 | } 88 | 89 | /// Calculates the commitments of the transactions data for the block hash. 90 | pub fn calculate_block_commitments( 91 | transactions_data: &[TransactionHashingData], 92 | state_diff: &ThinStateDiff, 93 | l1_da_mode: L1DataAvailabilityMode, 94 | ) -> BlockHeaderCommitments { 95 | let transaction_leaf_elements: Vec = 96 | transactions_data.iter().map(TransactionLeafElement::from).collect(); 97 | let transaction_commitment = 98 | calculate_transaction_commitment::(&transaction_leaf_elements); 99 | 100 | let event_leaf_elements: Vec = transactions_data 101 | .iter() 102 | .flat_map(|transaction_data| { 103 | transaction_data.transaction_output.events.iter().map(|event| EventLeafElement { 104 | event: event.clone(), 105 | transaction_hash: transaction_data.transaction_hash, 106 | }) 107 | }) 108 | .collect(); 109 | let event_commitment = calculate_event_commitment::(&event_leaf_elements); 110 | 111 | let receipt_elements: Vec = 112 | transactions_data.iter().map(ReceiptElement::from).collect(); 113 | let receipt_commitment = calculate_receipt_commitment::(&receipt_elements); 114 | let state_diff_commitment = calculate_state_diff_hash(state_diff); 115 | let concatenated_counts = concat_counts( 116 | transactions_data.len(), 117 | event_leaf_elements.len(), 118 | state_diff.len(), 119 | l1_da_mode, 120 | ); 121 | BlockHeaderCommitments { 122 | transaction_commitment, 123 | event_commitment, 124 | receipt_commitment, 125 | state_diff_commitment, 126 | concatenated_counts, 127 | } 128 | } 129 | 130 | // A single felt: [ 131 | // transaction_count (64 bits) | event_count (64 bits) | state_diff_length (64 bits) 132 | // | L1 data availability mode: 0 for calldata, 1 for blob (1 bit) | 0 ... 133 | // ]. 134 | fn concat_counts( 135 | transaction_count: usize, 136 | event_count: usize, 137 | state_diff_length: usize, 138 | l1_data_availability_mode: L1DataAvailabilityMode, 139 | ) -> Felt { 140 | let l1_data_availability_byte: u8 = match l1_data_availability_mode { 141 | L1DataAvailabilityMode::Calldata => 0, 142 | L1DataAvailabilityMode::Blob => 0b10000000, 143 | }; 144 | let concat_bytes = [ 145 | to_64_bits(transaction_count).as_slice(), 146 | to_64_bits(event_count).as_slice(), 147 | to_64_bits(state_diff_length).as_slice(), 148 | &[l1_data_availability_byte], 149 | &[0_u8; 7], // zero padding 150 | ] 151 | .concat(); 152 | Felt::from_bytes_be_slice(concat_bytes.as_slice()) 153 | } 154 | 155 | fn to_64_bits(num: usize) -> [u8; 8] { 156 | let sized_transaction_count: u64 = num.try_into().expect("Expect usize is at most 8 bytes"); 157 | sized_transaction_count.to_be_bytes() 158 | } 159 | -------------------------------------------------------------------------------- /src/block_hash/block_hash_calculator_test.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | 3 | use super::concat_counts; 4 | use crate::block::{ 5 | BlockHash, BlockHeaderWithoutHash, BlockNumber, BlockTimestamp, GasPrice, GasPricePerToken, 6 | StarknetVersion, 7 | }; 8 | use crate::block_hash::block_hash_calculator::{ 9 | calculate_block_commitments, calculate_block_hash, BlockHeaderCommitments, 10 | TransactionHashingData, 11 | }; 12 | use crate::block_hash::test_utils::{get_state_diff, get_transaction_output}; 13 | use crate::core::{ 14 | ContractAddress, EventCommitment, GlobalRoot, PatriciaKey, ReceiptCommitment, 15 | SequencerContractAddress, StateDiffCommitment, TransactionCommitment, 16 | }; 17 | use crate::data_availability::L1DataAvailabilityMode; 18 | use crate::felt; 19 | use crate::hash::PoseidonHash; 20 | use crate::transaction::{TransactionHash, TransactionSignature}; 21 | 22 | /// Macro to test if changing any field in the header or commitments 23 | /// results a change in the block hash. 24 | /// The macro clones the original header and commitments, modifies each specified field, 25 | /// and asserts that the block hash changes as a result. 26 | macro_rules! test_hash_changes { 27 | ($header:expr, $commitments:expr, header_fields => { $($header_field:ident),* }, commitments_fields => { $($commitments_field:ident),* }) => { 28 | { 29 | let original_hash = calculate_block_hash($header.clone(), $commitments.clone()); 30 | 31 | $( 32 | // Test changing the field in the header. 33 | let mut modified_header = $header.clone(); 34 | modified_header.$header_field = Default::default(); 35 | let new_hash = calculate_block_hash(modified_header, $commitments.clone()); 36 | assert_ne!(original_hash, new_hash, concat!("Hash should change when ", stringify!($header_field), " is modified")); 37 | )* 38 | 39 | $( 40 | // Test changing the field in the commitments. 41 | let mut modified_commitments = $commitments.clone(); 42 | modified_commitments.$commitments_field = Default::default(); 43 | let new_hash = calculate_block_hash($header.clone(), modified_commitments); 44 | assert_ne!(original_hash, new_hash, concat!("Hash should change when ", stringify!($commitments_field), " is modified")); 45 | )* 46 | } 47 | }; 48 | } 49 | 50 | #[test] 51 | fn test_block_hash_regression() { 52 | let block_header = BlockHeaderWithoutHash { 53 | block_number: BlockNumber(1_u64), 54 | state_root: GlobalRoot(Felt::from(2_u8)), 55 | sequencer: SequencerContractAddress(ContractAddress(PatriciaKey::from(3_u8))), 56 | timestamp: BlockTimestamp(4), 57 | l1_da_mode: L1DataAvailabilityMode::Blob, 58 | l1_gas_price: GasPricePerToken { price_in_fri: GasPrice(6), price_in_wei: GasPrice(7) }, 59 | l1_data_gas_price: GasPricePerToken { 60 | price_in_fri: GasPrice(10), 61 | price_in_wei: GasPrice(9), 62 | }, 63 | starknet_version: StarknetVersion("10".to_owned()), 64 | parent_hash: BlockHash(Felt::from(11_u8)), 65 | }; 66 | let transactions_data = vec![TransactionHashingData { 67 | transaction_signature: Some(TransactionSignature(vec![Felt::TWO, Felt::THREE])), 68 | transaction_output: get_transaction_output(), 69 | transaction_hash: TransactionHash(Felt::ONE), 70 | }]; 71 | 72 | let state_diff = get_state_diff(); 73 | let block_commitments = 74 | calculate_block_commitments(&transactions_data, &state_diff, block_header.l1_da_mode); 75 | 76 | let expected_hash = felt!("0x061e4998d51a248f1d0288d7e17f6287757b0e5e6c5e1e58ddf740616e312134"); 77 | 78 | assert_eq!(BlockHash(expected_hash), calculate_block_hash(block_header, block_commitments),); 79 | } 80 | 81 | #[test] 82 | fn concat_counts_test() { 83 | let concated = concat_counts(4, 3, 2, L1DataAvailabilityMode::Blob); 84 | let expected_felt = felt!("0x0000000000000004000000000000000300000000000000028000000000000000"); 85 | assert_eq!(concated, expected_felt) 86 | } 87 | 88 | /// Test that if one of the input to block hash changes, the hash changes. 89 | #[test] 90 | fn change_field_of_hash_input() { 91 | let header = BlockHeaderWithoutHash { 92 | parent_hash: BlockHash(Felt::ONE), 93 | block_number: BlockNumber(1), 94 | l1_gas_price: GasPricePerToken { price_in_fri: GasPrice(1), price_in_wei: GasPrice(1) }, 95 | l1_data_gas_price: GasPricePerToken { 96 | price_in_fri: GasPrice(1), 97 | price_in_wei: GasPrice(1), 98 | }, 99 | state_root: GlobalRoot(Felt::ONE), 100 | sequencer: SequencerContractAddress(ContractAddress::from(1_u128)), 101 | timestamp: BlockTimestamp(1), 102 | l1_da_mode: L1DataAvailabilityMode::Blob, 103 | starknet_version: StarknetVersion("0.1.0".to_string()), 104 | }; 105 | 106 | let block_commitments = BlockHeaderCommitments { 107 | transaction_commitment: TransactionCommitment(Felt::ONE), 108 | event_commitment: EventCommitment(Felt::ONE), 109 | receipt_commitment: ReceiptCommitment(Felt::ONE), 110 | state_diff_commitment: StateDiffCommitment(PoseidonHash(Felt::ONE)), 111 | concatenated_counts: Felt::ONE, 112 | }; 113 | 114 | // Test that changing any of the fields in the header or the commitments changes the hash. 115 | test_hash_changes!( 116 | header, 117 | block_commitments, 118 | header_fields => { 119 | parent_hash, 120 | block_number, 121 | l1_gas_price, 122 | l1_data_gas_price, 123 | state_root, 124 | sequencer, 125 | timestamp, 126 | starknet_version 127 | }, 128 | commitments_fields => { 129 | transaction_commitment, 130 | event_commitment, 131 | receipt_commitment, 132 | state_diff_commitment, 133 | concatenated_counts 134 | } 135 | ); 136 | // TODO(Aviv, 10/06/2024): add tests that changes the first hash input, and the const zero. 137 | } 138 | -------------------------------------------------------------------------------- /src/block_hash/event_commitment.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::StarkHash; 3 | 4 | use crate::core::EventCommitment; 5 | use crate::crypto::patricia_hash::calculate_root; 6 | use crate::crypto::utils::HashChain; 7 | use crate::transaction::{Event, TransactionHash}; 8 | 9 | #[cfg(test)] 10 | #[path = "event_commitment_test.rs"] 11 | mod event_commitment_test; 12 | 13 | /// The elements used to calculate a leaf in the transactions Patricia tree. 14 | #[derive(Clone)] 15 | pub struct EventLeafElement { 16 | pub(crate) event: Event, 17 | pub(crate) transaction_hash: TransactionHash, 18 | } 19 | 20 | /// Returns the root of a Patricia tree where each leaf is an event hash. 21 | pub fn calculate_event_commitment( 22 | event_leaf_elements: &[EventLeafElement], 23 | ) -> EventCommitment { 24 | let event_leaves = event_leaf_elements.iter().map(calculate_event_hash).collect(); 25 | EventCommitment(calculate_root::(event_leaves)) 26 | } 27 | 28 | // Poseidon( 29 | // from_address, transaction_hash, 30 | // num_keys, key0, key1, ..., 31 | // num_contents, content0, content1, ... 32 | // ). 33 | fn calculate_event_hash(event_leaf_element: &EventLeafElement) -> Felt { 34 | let keys = &event_leaf_element.event.content.keys.iter().map(|k| k.0).collect::>(); 35 | let data = &event_leaf_element.event.content.data.0; 36 | HashChain::new() 37 | .chain(event_leaf_element.event.from_address.0.key()) 38 | .chain(&event_leaf_element.transaction_hash.0) 39 | .chain_size_and_elements(keys) 40 | .chain_size_and_elements(data) 41 | .get_poseidon_hash() 42 | } 43 | -------------------------------------------------------------------------------- /src/block_hash/event_commitment_test.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::Poseidon; 3 | 4 | use super::{calculate_event_commitment, calculate_event_hash, EventLeafElement}; 5 | use crate::core::{ContractAddress, EventCommitment, PatriciaKey}; 6 | use crate::transaction::{Event, EventContent, EventData, EventKey, TransactionHash}; 7 | use crate::{contract_address, felt, patricia_key}; 8 | 9 | #[test] 10 | fn test_event_commitment_regression() { 11 | let event_leaf_elements = 12 | [get_event_leaf_element(0), get_event_leaf_element(1), get_event_leaf_element(2)]; 13 | 14 | let expected_root = felt!("0x069bb140ddbbeb01d81c7201ecfb933031306e45dab9c77ff9f9ba3cd4c2b9c3"); 15 | 16 | assert_eq!( 17 | EventCommitment(expected_root), 18 | calculate_event_commitment::(&event_leaf_elements), 19 | ); 20 | } 21 | 22 | #[test] 23 | fn test_event_hash_regression() { 24 | let event_leaf_element = get_event_leaf_element(2); 25 | 26 | let expected_hash = felt!("0x367807f532742a4dcbe2d8a47b974b22dd7496faa75edc64a3a5fdb6709057"); 27 | 28 | assert_eq!(expected_hash, calculate_event_hash(&event_leaf_element)); 29 | } 30 | 31 | fn get_event_leaf_element(seed: u8) -> EventLeafElement { 32 | EventLeafElement { 33 | event: Event { 34 | from_address: contract_address!(format!("{:x}", seed + 8).as_str()), 35 | content: EventContent { 36 | keys: [seed, seed + 1].iter().map(|key| EventKey(Felt::from(*key))).collect(), 37 | data: EventData( 38 | [seed + 2, seed + 3, seed + 4].into_iter().map(Felt::from).collect(), 39 | ), 40 | }, 41 | }, 42 | transaction_hash: TransactionHash(felt!("0x1234")), 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/block_hash/receipt_commitment.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::StarkHash; 3 | 4 | use super::block_hash_calculator::{TransactionHashingData, TransactionOutputForHash}; 5 | use crate::core::ReceiptCommitment; 6 | use crate::crypto::patricia_hash::calculate_root; 7 | use crate::crypto::utils::HashChain; 8 | use crate::hash::starknet_keccak_hash; 9 | use crate::transaction::{GasVector, MessageToL1, TransactionExecutionStatus, TransactionHash}; 10 | 11 | #[cfg(test)] 12 | #[path = "receipt_commitment_test.rs"] 13 | mod receipt_commitment_test; 14 | 15 | // The elements used to calculate a leaf in the transactions Patricia tree. 16 | #[derive(Clone)] 17 | pub struct ReceiptElement { 18 | pub transaction_hash: TransactionHash, 19 | pub transaction_output: TransactionOutputForHash, 20 | } 21 | 22 | impl From<&TransactionHashingData> for ReceiptElement { 23 | fn from(transaction_data: &TransactionHashingData) -> Self { 24 | Self { 25 | transaction_hash: transaction_data.transaction_hash, 26 | transaction_output: transaction_data.transaction_output.clone(), 27 | } 28 | } 29 | } 30 | 31 | /// Returns the root of a Patricia tree where each leaf is a receipt hash. 32 | pub fn calculate_receipt_commitment( 33 | receipt_elements: &[ReceiptElement], 34 | ) -> ReceiptCommitment { 35 | ReceiptCommitment(calculate_root::( 36 | receipt_elements.iter().map(calculate_receipt_hash).collect(), 37 | )) 38 | } 39 | 40 | // Poseidon( 41 | // transaction hash, amount of fee paid, hash of messages sent, revert reason, 42 | // execution resources 43 | // ). 44 | fn calculate_receipt_hash(receipt_element: &ReceiptElement) -> Felt { 45 | let hash_chain = HashChain::new() 46 | .chain(&receipt_element.transaction_hash) 47 | .chain(&receipt_element.transaction_output.actual_fee.0.into()) 48 | .chain(&calculate_messages_sent_hash(&receipt_element.transaction_output.messages_sent)) 49 | .chain(&get_revert_reason_hash(&receipt_element.transaction_output.execution_status)); 50 | chain_gas_consumed(hash_chain, &receipt_element.transaction_output.gas_consumed) 51 | .get_poseidon_hash() 52 | } 53 | 54 | // Poseidon( 55 | // num_messages_sent, 56 | // from_address_0, to_address_0, payload_length_0, payload_0, 57 | // from_address_1, to_address_1, payload_length_1, payload_1, ... 58 | // ). 59 | fn calculate_messages_sent_hash(messages_sent: &Vec) -> Felt { 60 | let mut messages_hash_chain = HashChain::new().chain(&messages_sent.len().into()); 61 | for message_sent in messages_sent { 62 | messages_hash_chain = messages_hash_chain 63 | .chain(&message_sent.from_address) 64 | .chain(&message_sent.to_address.into()) 65 | .chain_size_and_elements(&message_sent.payload.0); 66 | } 67 | messages_hash_chain.get_poseidon_hash() 68 | } 69 | 70 | // Returns starknet-keccak of the revert reason ASCII string, or 0 if the transaction succeeded. 71 | fn get_revert_reason_hash(execution_status: &TransactionExecutionStatus) -> Felt { 72 | match execution_status { 73 | TransactionExecutionStatus::Succeeded => Felt::ZERO, 74 | TransactionExecutionStatus::Reverted(reason) => { 75 | starknet_keccak_hash(reason.revert_reason.as_bytes()) 76 | } 77 | } 78 | } 79 | 80 | // Chains: 81 | // L2 gas consumed (In the current RPC: always 0), 82 | // L1 gas consumed (In the current RPC: 83 | // L1 gas consumed for calldata + L1 gas consumed for steps and builtins. 84 | // L1 data gas consumed (In the current RPC: L1 data gas consumed for blob). 85 | fn chain_gas_consumed(hash_chain: HashChain, gas_consumed: &GasVector) -> HashChain { 86 | hash_chain 87 | .chain(&Felt::ZERO) // L2 gas consumed 88 | .chain(&gas_consumed.l1_gas.into()) 89 | .chain(&gas_consumed.l1_data_gas.into()) 90 | } 91 | -------------------------------------------------------------------------------- /src/block_hash/receipt_commitment_test.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::Poseidon; 3 | 4 | use super::calculate_messages_sent_hash; 5 | use crate::block_hash::receipt_commitment::{ 6 | calculate_receipt_commitment, calculate_receipt_hash, get_revert_reason_hash, ReceiptElement, 7 | }; 8 | use crate::block_hash::test_utils::{generate_message_to_l1, get_transaction_output}; 9 | use crate::core::ReceiptCommitment; 10 | use crate::felt; 11 | use crate::transaction::{ 12 | RevertedTransactionExecutionStatus, TransactionExecutionStatus, TransactionHash, 13 | }; 14 | 15 | #[test] 16 | fn test_receipt_hash_regression() { 17 | let transaction_receipt = ReceiptElement { 18 | transaction_hash: TransactionHash(Felt::from(1234_u16)), 19 | transaction_output: get_transaction_output(), 20 | }; 21 | 22 | let expected_hash = felt!("0x6276abf21e7c68b2eecfdc8a845b11b44401901f5f040efe10c60d625049646"); 23 | assert_eq!(calculate_receipt_hash(&transaction_receipt), expected_hash); 24 | 25 | let expected_root = ReceiptCommitment(felt!( 26 | "0x31963cb891ebb825e83514deb748c89b6967b5368cbc48a9b56193a1464ca87" 27 | )); 28 | assert_eq!(calculate_receipt_commitment::(&[transaction_receipt]), expected_root); 29 | } 30 | 31 | #[test] 32 | fn test_messages_sent_regression() { 33 | let messages_sent = vec![generate_message_to_l1(0), generate_message_to_l1(1)]; 34 | let messages_hash = calculate_messages_sent_hash(&messages_sent); 35 | let expected_hash = felt!("0x00c89474a9007dc060aed76caf8b30b927cfea1ebce2d134b943b8d7121004e4"); 36 | assert_eq!(messages_hash, expected_hash); 37 | } 38 | 39 | #[test] 40 | fn test_revert_reason_hash_regression() { 41 | let execution_succeeded = TransactionExecutionStatus::Succeeded; 42 | assert_eq!(get_revert_reason_hash(&execution_succeeded), Felt::ZERO); 43 | let execution_reverted = 44 | TransactionExecutionStatus::Reverted(RevertedTransactionExecutionStatus { 45 | revert_reason: "ABC".to_string(), 46 | }); 47 | let expected_hash = felt!("0x01629b9dda060bb30c7908346f6af189c16773fa148d3366701fbaa35d54f3c8"); 48 | assert_eq!(get_revert_reason_hash(&execution_reverted), expected_hash); 49 | } 50 | -------------------------------------------------------------------------------- /src/block_hash/state_diff_hash.rs: -------------------------------------------------------------------------------- 1 | use indexmap::IndexMap; 2 | use once_cell::sync::Lazy; 3 | use starknet_types_core::felt::Felt; 4 | 5 | use crate::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, StateDiffCommitment}; 6 | use crate::crypto::utils::HashChain; 7 | use crate::hash::PoseidonHash; 8 | use crate::state::{StorageKey, ThinStateDiff}; 9 | use crate::transaction_hash::ascii_as_felt; 10 | 11 | #[cfg(test)] 12 | #[path = "state_diff_hash_test.rs"] 13 | mod state_diff_hash_test; 14 | 15 | static STARKNET_STATE_DIFF0: Lazy = Lazy::new(|| { 16 | ascii_as_felt("STARKNET_STATE_DIFF0").expect("ascii_as_felt failed for 'STARKNET_STATE_DIFF0'") 17 | }); 18 | 19 | /// Poseidon( 20 | /// "STARKNET_STATE_DIFF0", deployed_contracts, declared_classes, deprecated_declared_classes, 21 | /// 1, 0, storage_diffs, nonces 22 | /// ). 23 | pub fn calculate_state_diff_hash(state_diff: &ThinStateDiff) -> StateDiffCommitment { 24 | let mut hash_chain = HashChain::new(); 25 | hash_chain = hash_chain.chain(&STARKNET_STATE_DIFF0); 26 | hash_chain = chain_updated_contracts( 27 | &state_diff.deployed_contracts, 28 | &state_diff.replaced_classes, 29 | hash_chain, 30 | ); 31 | hash_chain = chain_declared_classes(&state_diff.declared_classes, hash_chain); 32 | hash_chain = 33 | chain_deprecated_declared_classes(&state_diff.deprecated_declared_classes, hash_chain); 34 | hash_chain = hash_chain.chain(&Felt::ONE) // placeholder. 35 | .chain(&Felt::ZERO); // placeholder. 36 | hash_chain = chain_storage_diffs(&state_diff.storage_diffs, hash_chain); 37 | hash_chain = chain_nonces(&state_diff.nonces, hash_chain); 38 | StateDiffCommitment(PoseidonHash(hash_chain.get_poseidon_hash())) 39 | } 40 | 41 | // Chains: [number_of_updated_contracts, address_0, class_hash_0, address_1, class_hash_1, ...]. 42 | // The updated contracts includes deployed contracts and replaced classes. 43 | fn chain_updated_contracts( 44 | deployed_contracts: &IndexMap, 45 | replaced_classes: &IndexMap, 46 | mut hash_chain: HashChain, 47 | ) -> HashChain { 48 | let updated_contracts = deployed_contracts.iter().chain(replaced_classes.iter()); 49 | hash_chain = hash_chain.chain(&(deployed_contracts.len() + replaced_classes.len()).into()); 50 | for (address, class_hash) in sorted_index_map(&updated_contracts.collect()) { 51 | hash_chain = hash_chain.chain(&address.0).chain(class_hash); 52 | } 53 | hash_chain 54 | } 55 | 56 | // Chains: [number_of_declared_classes, 57 | // class_hash_0, compiled_class_hash_0, class_hash_1, compiled_class_hash_1, ...]. 58 | fn chain_declared_classes( 59 | declared_classes: &IndexMap, 60 | mut hash_chain: HashChain, 61 | ) -> HashChain { 62 | hash_chain = hash_chain.chain(&declared_classes.len().into()); 63 | for (class_hash, compiled_class_hash) in sorted_index_map(declared_classes) { 64 | hash_chain = hash_chain.chain(&class_hash).chain(&compiled_class_hash.0) 65 | } 66 | hash_chain 67 | } 68 | 69 | // Chains: [number_of_old_declared_classes, class_hash_0, class_hash_1, ...]. 70 | fn chain_deprecated_declared_classes( 71 | deprecated_declared_classes: &[ClassHash], 72 | hash_chain: HashChain, 73 | ) -> HashChain { 74 | let mut sorted_deprecated_declared_classes = deprecated_declared_classes.to_vec(); 75 | sorted_deprecated_declared_classes.sort_unstable(); 76 | hash_chain 77 | .chain(&sorted_deprecated_declared_classes.len().into()) 78 | .chain_iter(sorted_deprecated_declared_classes.iter().map(|class_hash| &class_hash.0)) 79 | } 80 | 81 | // Chains: [number_of_updated_contracts, 82 | // contract_address_0, number_of_updates_in_contract_0, key_0, value0, key1, value1, ..., 83 | // contract_address_1, number_of_updates_in_contract_1, key_0, value0, key1, value1, ..., 84 | // ] 85 | fn chain_storage_diffs( 86 | storage_diffs: &IndexMap>, 87 | hash_chain: HashChain, 88 | ) -> HashChain { 89 | let mut n_updated_contracts = 0_u64; 90 | let mut storage_diffs_chain = HashChain::new(); 91 | for (contract_address, key_value_map) in sorted_index_map(storage_diffs) { 92 | if key_value_map.is_empty() { 93 | // Filter out a contract with empty storage maps. 94 | continue; 95 | } 96 | n_updated_contracts += 1; 97 | storage_diffs_chain = storage_diffs_chain.chain(&contract_address); 98 | storage_diffs_chain = storage_diffs_chain.chain(&key_value_map.len().into()); 99 | for (key, value) in sorted_index_map(&key_value_map) { 100 | storage_diffs_chain = storage_diffs_chain.chain(&key).chain(&value); 101 | } 102 | } 103 | hash_chain.chain(&n_updated_contracts.into()).extend(storage_diffs_chain) 104 | } 105 | 106 | // Chains: [number_of_updated_contracts nonces, 107 | // contract_address_0, nonce_0, contract_address_1, nonce_1, ..., 108 | // ] 109 | fn chain_nonces(nonces: &IndexMap, mut hash_chain: HashChain) -> HashChain { 110 | hash_chain = hash_chain.chain(&nonces.len().into()); 111 | for (contract_address, nonce) in sorted_index_map(nonces) { 112 | hash_chain = hash_chain.chain(&contract_address); 113 | hash_chain = hash_chain.chain(&nonce); 114 | } 115 | hash_chain 116 | } 117 | 118 | // Returns a clone of the map, sorted by keys. 119 | fn sorted_index_map(map: &IndexMap) -> IndexMap { 120 | let mut sorted_map = map.clone(); 121 | sorted_map.sort_unstable_keys(); 122 | sorted_map 123 | } 124 | -------------------------------------------------------------------------------- /src/block_hash/state_diff_hash_test.rs: -------------------------------------------------------------------------------- 1 | use indexmap::indexmap; 2 | 3 | use crate::block_hash::state_diff_hash::{ 4 | calculate_state_diff_hash, chain_declared_classes, chain_deprecated_declared_classes, 5 | chain_nonces, chain_storage_diffs, chain_updated_contracts, 6 | }; 7 | use crate::block_hash::test_utils::get_state_diff; 8 | use crate::core::{ClassHash, CompiledClassHash, Nonce, StateDiffCommitment}; 9 | use crate::crypto::utils::HashChain; 10 | use crate::felt; 11 | use crate::hash::PoseidonHash; 12 | 13 | #[test] 14 | fn test_state_diff_hash_regression() { 15 | let state_diff = get_state_diff(); 16 | 17 | let expected_hash = StateDiffCommitment(PoseidonHash(felt!( 18 | "0x0281f5966e49ad7dad9323826d53d1d27c0c4e6ebe5525e2e2fbca549bfa0a67" 19 | ))); 20 | 21 | assert_eq!(expected_hash, calculate_state_diff_hash(&state_diff)); 22 | } 23 | 24 | #[test] 25 | fn test_sorting_deployed_contracts() { 26 | let deployed_contracts_0 = indexmap! { 27 | 0u64.into() => ClassHash(3u64.into()), 28 | 1u64.into() => ClassHash(2u64.into()), 29 | }; 30 | let replaced_classes_0 = indexmap! { 31 | 2u64.into() => ClassHash(1u64.into()), 32 | }; 33 | let deployed_contracts_1 = indexmap! { 34 | 2u64.into() => ClassHash(1u64.into()), 35 | 0u64.into() => ClassHash(3u64.into()), 36 | }; 37 | let replaced_classes_1 = indexmap! { 38 | 1u64.into() => ClassHash(2u64.into()), 39 | }; 40 | assert_eq!( 41 | chain_updated_contracts(&deployed_contracts_0, &replaced_classes_0, HashChain::new()) 42 | .get_poseidon_hash(), 43 | chain_updated_contracts(&deployed_contracts_1, &replaced_classes_1, HashChain::new()) 44 | .get_poseidon_hash(), 45 | ); 46 | } 47 | 48 | #[test] 49 | fn test_sorting_declared_classes() { 50 | let declared_classes_0 = indexmap! { 51 | ClassHash(0u64.into()) => CompiledClassHash(3u64.into()), 52 | ClassHash(1u64.into()) => CompiledClassHash(2u64.into()), 53 | }; 54 | let declared_classes_1 = indexmap! { 55 | ClassHash(1u64.into()) => CompiledClassHash(2u64.into()), 56 | ClassHash(0u64.into()) => CompiledClassHash(3u64.into()), 57 | }; 58 | assert_eq!( 59 | chain_declared_classes(&declared_classes_0, HashChain::new()).get_poseidon_hash(), 60 | chain_declared_classes(&declared_classes_1, HashChain::new()).get_poseidon_hash(), 61 | ); 62 | } 63 | 64 | #[test] 65 | fn test_sorting_deprecated_declared_classes() { 66 | let deprecated_declared_classes_0 = vec![ClassHash(0u64.into()), ClassHash(1u64.into())]; 67 | let deprecated_declared_classes_1 = vec![ClassHash(1u64.into()), ClassHash(0u64.into())]; 68 | assert_eq!( 69 | chain_deprecated_declared_classes(&deprecated_declared_classes_0, HashChain::new()) 70 | .get_poseidon_hash(), 71 | chain_deprecated_declared_classes(&deprecated_declared_classes_1, HashChain::new()) 72 | .get_poseidon_hash(), 73 | ); 74 | } 75 | 76 | #[test] 77 | fn test_sorting_storage_diffs() { 78 | let storage_diffs_0 = indexmap! { 79 | 0u64.into() => indexmap! { 80 | 1u64.into() => 2u64.into(), 81 | 3u64.into() => 4u64.into(), 82 | }, 83 | 5u64.into() => indexmap! { 84 | 6u64.into() => 7u64.into(), 85 | }, 86 | }; 87 | let storage_diffs_1 = indexmap! { 88 | 5u64.into() => indexmap! { 89 | 6u64.into() => 7u64.into(), 90 | }, 91 | 0u64.into() => indexmap! { 92 | 3u64.into() => 4u64.into(), 93 | 1u64.into() => 2u64.into(), 94 | }, 95 | }; 96 | assert_eq!( 97 | chain_storage_diffs(&storage_diffs_0, HashChain::new()).get_poseidon_hash(), 98 | chain_storage_diffs(&storage_diffs_1, HashChain::new()).get_poseidon_hash(), 99 | ); 100 | } 101 | 102 | #[test] 103 | fn test_empty_storage_diffs() { 104 | let storage_diffs_0 = indexmap! { 105 | 0u64.into() => indexmap! { 106 | 1u64.into() => 2u64.into(), 107 | }, 108 | 3u64.into() => indexmap! { 109 | }, 110 | }; 111 | let storage_diffs_1 = indexmap! { 112 | 0u64.into() => indexmap! { 113 | 1u64.into() => 2u64.into(), 114 | }, 115 | }; 116 | assert_eq!( 117 | chain_storage_diffs(&storage_diffs_0, HashChain::new()).get_poseidon_hash(), 118 | chain_storage_diffs(&storage_diffs_1, HashChain::new()).get_poseidon_hash(), 119 | ); 120 | } 121 | 122 | #[test] 123 | fn test_sorting_nonces() { 124 | let nonces_0 = indexmap! { 125 | 0u64.into() => Nonce(3u64.into()), 126 | 1u64.into() => Nonce(2u64.into()), 127 | }; 128 | let nonces_1 = indexmap! { 129 | 1u64.into() => Nonce(2u64.into()), 130 | 0u64.into() => Nonce(3u64.into()), 131 | }; 132 | assert_eq!( 133 | chain_nonces(&nonces_0, HashChain::new()).get_poseidon_hash(), 134 | chain_nonces(&nonces_1, HashChain::new()).get_poseidon_hash(), 135 | ); 136 | } 137 | -------------------------------------------------------------------------------- /src/block_hash/test_utils.rs: -------------------------------------------------------------------------------- 1 | use indexmap::indexmap; 2 | use primitive_types::H160; 3 | use starknet_types_core::felt::Felt; 4 | 5 | use super::block_hash_calculator::TransactionOutputForHash; 6 | use crate::core::{ClassHash, CompiledClassHash, ContractAddress, EthAddress, Nonce}; 7 | use crate::state::ThinStateDiff; 8 | use crate::transaction::{ 9 | Fee, GasVector, L2ToL1Payload, MessageToL1, RevertedTransactionExecutionStatus, 10 | TransactionExecutionStatus, 11 | }; 12 | 13 | pub(crate) fn get_transaction_output() -> TransactionOutputForHash { 14 | let execution_status = 15 | TransactionExecutionStatus::Reverted(RevertedTransactionExecutionStatus { 16 | revert_reason: "aborted".to_string(), 17 | }); 18 | TransactionOutputForHash { 19 | actual_fee: Fee(99804), 20 | messages_sent: vec![generate_message_to_l1(34), generate_message_to_l1(56)], 21 | events: vec![], 22 | execution_status, 23 | gas_consumed: GasVector { l1_gas: 16580, l1_data_gas: 32 }, 24 | } 25 | } 26 | 27 | pub(crate) fn generate_message_to_l1(seed: u64) -> MessageToL1 { 28 | MessageToL1 { 29 | from_address: ContractAddress::from(seed), 30 | to_address: EthAddress(H160::from_low_u64_be(seed + 1)), 31 | payload: L2ToL1Payload(vec![Felt::from(seed + 2), Felt::from(seed + 3)]), 32 | } 33 | } 34 | 35 | pub(crate) fn get_state_diff() -> ThinStateDiff { 36 | ThinStateDiff { 37 | deployed_contracts: indexmap! { 38 | 0u64.into() => ClassHash(1u64.into()), 39 | 2u64.into() => ClassHash(3u64.into()), 40 | }, 41 | storage_diffs: indexmap! { 42 | 4u64.into() => indexmap! { 43 | 5u64.into() => 6u64.into(), 44 | 7u64.into() => 8u64.into(), 45 | }, 46 | 9u64.into() => indexmap! { 47 | 10u64.into() => 11u64.into(), 48 | }, 49 | }, 50 | declared_classes: indexmap! { 51 | ClassHash(12u64.into()) => CompiledClassHash(13u64.into()), 52 | ClassHash(14u64.into()) => CompiledClassHash(15u64.into()), 53 | }, 54 | deprecated_declared_classes: vec![ClassHash(16u64.into())], 55 | nonces: indexmap! { 56 | 17u64.into() => Nonce(18u64.into()), 57 | }, 58 | replaced_classes: indexmap! { 59 | 19u64.into() => ClassHash(20u64.into()), 60 | }, 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/block_hash/transaction_commitment.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::StarkHash as CoreStarkHash; 3 | 4 | use super::block_hash_calculator::TransactionHashingData; 5 | use crate::core::TransactionCommitment; 6 | use crate::crypto::patricia_hash::calculate_root; 7 | use crate::crypto::utils::HashChain; 8 | use crate::transaction::{TransactionHash, TransactionSignature}; 9 | 10 | #[cfg(test)] 11 | #[path = "transaction_commitment_test.rs"] 12 | mod transaction_commitment_test; 13 | 14 | /// The elements used to calculate a leaf in the transactions Patricia tree. 15 | #[derive(Clone)] 16 | pub struct TransactionLeafElement { 17 | pub(crate) transaction_hash: TransactionHash, 18 | pub(crate) transaction_signature: Option, 19 | } 20 | 21 | impl From<&TransactionHashingData> for TransactionLeafElement { 22 | fn from(transaction_data: &TransactionHashingData) -> Self { 23 | Self { 24 | transaction_hash: transaction_data.transaction_hash, 25 | transaction_signature: transaction_data.transaction_signature.clone(), 26 | } 27 | } 28 | } 29 | 30 | /// Returns the root of a Patricia tree where each leaf is 31 | /// H(transaction_hash, transaction_signature). 32 | /// The leaf of a transaction types without a signature field is: H(transaction_hash, 0). 33 | pub fn calculate_transaction_commitment( 34 | transaction_leaf_elements: &[TransactionLeafElement], 35 | ) -> TransactionCommitment { 36 | let transaction_leaves = 37 | transaction_leaf_elements.iter().map(calculate_transaction_leaf).collect(); 38 | TransactionCommitment(calculate_root::(transaction_leaves)) 39 | } 40 | 41 | fn calculate_transaction_leaf(transaction_leaf_elements: &TransactionLeafElement) -> Felt { 42 | HashChain::new() 43 | .chain(&transaction_leaf_elements.transaction_hash.0) 44 | .chain_iter( 45 | transaction_leaf_elements 46 | .transaction_signature 47 | .as_ref() 48 | .unwrap_or(&TransactionSignature(vec![Felt::ZERO])) 49 | .0 50 | .iter(), 51 | ) 52 | .get_poseidon_hash() 53 | } 54 | -------------------------------------------------------------------------------- /src/block_hash/transaction_commitment_test.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::Poseidon; 3 | 4 | use super::TransactionLeafElement; 5 | use crate::block_hash::transaction_commitment::{ 6 | calculate_transaction_commitment, calculate_transaction_leaf, 7 | }; 8 | use crate::core::TransactionCommitment; 9 | use crate::felt; 10 | use crate::transaction::{TransactionHash, TransactionSignature}; 11 | 12 | #[test] 13 | fn test_transaction_leaf_regression() { 14 | let transaction_leaf_elements = get_transaction_leaf_element(); 15 | let expected_leaf = felt!("0x2f0d8840bcf3bc629598d8a6cc80cb7c0d9e52d93dab244bbf9cd0dca0ad082"); 16 | 17 | assert_eq!(expected_leaf, calculate_transaction_leaf(&transaction_leaf_elements)); 18 | } 19 | 20 | #[test] 21 | fn test_transaction_leaf_without_signature_regression() { 22 | let transaction_leaf_elements = TransactionLeafElement { 23 | transaction_hash: TransactionHash(Felt::ONE), 24 | transaction_signature: None, 25 | }; 26 | let expected_leaf = felt!("0x00a93bf5e58b9378d093aa86ddc2f61a3295a1d1e665bd0ef3384dd07b30e033"); 27 | 28 | assert_eq!(expected_leaf, calculate_transaction_leaf(&transaction_leaf_elements)); 29 | } 30 | 31 | #[test] 32 | fn test_transaction_commitment_regression() { 33 | let transaction_leaf_elements = get_transaction_leaf_element(); 34 | let expected_root = felt!("0x0282b635972328bd1cfa86496fe920d20bd9440cd78ee8dc90ae2b383d664dcf"); 35 | 36 | assert_eq!( 37 | TransactionCommitment(expected_root), 38 | calculate_transaction_commitment::(&[ 39 | transaction_leaf_elements.clone(), 40 | transaction_leaf_elements 41 | ]) 42 | ); 43 | } 44 | 45 | fn get_transaction_leaf_element() -> TransactionLeafElement { 46 | let transaction_hash = TransactionHash(Felt::ONE); 47 | let transaction_signature = TransactionSignature(vec![Felt::TWO, Felt::THREE]); 48 | TransactionLeafElement { transaction_hash, transaction_signature: Some(transaction_signature) } 49 | } 50 | -------------------------------------------------------------------------------- /src/block_test.rs: -------------------------------------------------------------------------------- 1 | use super::verify_block_signature; 2 | use crate::block::{BlockHash, BlockNumber, BlockSignature}; 3 | use crate::core::{GlobalRoot, SequencerPublicKey}; 4 | use crate::crypto::utils::{PublicKey, Signature}; 5 | use crate::felt; 6 | 7 | #[test] 8 | fn test_block_number_iteration() { 9 | let start: u64 = 3; 10 | let up_until: u64 = 10; 11 | 12 | let mut expected = vec![]; 13 | for i in start..up_until { 14 | expected.push(BlockNumber(i)); 15 | } 16 | 17 | let start_block_number = BlockNumber(start); 18 | let up_until_block_number = BlockNumber(up_until); 19 | 20 | let mut from_iter: Vec<_> = vec![]; 21 | for i in start_block_number.iter_up_to(up_until_block_number) { 22 | from_iter.push(i); 23 | } 24 | 25 | assert_eq!(expected, from_iter); 26 | } 27 | 28 | #[test] 29 | fn block_signature_verification() { 30 | // Values taken from Mainnet. 31 | let block_hash = 32 | BlockHash(felt!("0x7d5db04c5ca2aea828180dc441afb1580e3cee7547a3567ced3aa5bb8b273c0")); 33 | let state_commitment = 34 | GlobalRoot(felt!("0x64689c12248e1110af4b3af0e2b43cd51ad13e8855f10e37669e2a4baf919c6")); 35 | let signature = BlockSignature(Signature { 36 | r: felt!("0x1b382bbfd693011c9b7692bc932b23ed9c288deb27c8e75772e172abbe5950c"), 37 | s: felt!("0xbe4438085057e1a7c704a0da3b30f7b8340fe3d24c86772abfd24aa597e42"), 38 | }); 39 | let sequencer_pub_key = SequencerPublicKey(PublicKey(felt!( 40 | "0x48253ff2c3bed7af18bde0b611b083b39445959102d4947c51c4db6aa4f4e58" 41 | ))); 42 | 43 | assert!( 44 | verify_block_signature(&sequencer_pub_key, &signature, &state_commitment, &block_hash) 45 | .unwrap() 46 | ); 47 | } 48 | -------------------------------------------------------------------------------- /src/core.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[path = "core_test.rs"] 3 | mod core_test; 4 | 5 | use core::fmt::Display; 6 | use std::fmt::Debug; 7 | 8 | use derive_more::Display; 9 | use once_cell::sync::Lazy; 10 | use primitive_types::H160; 11 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 12 | use starknet_types_core::felt::{Felt, NonZeroFelt}; 13 | use starknet_types_core::hash::{Pedersen, StarkHash as CoreStarkHash}; 14 | 15 | use crate::crypto::utils::PublicKey; 16 | use crate::hash::{PoseidonHash, StarkHash}; 17 | use crate::serde_utils::{BytesAsHex, PrefixedBytesAsHex}; 18 | use crate::transaction::{Calldata, ContractAddressSalt}; 19 | use crate::{impl_from_through_intermediate, StarknetApiError}; 20 | 21 | /// A chain id. 22 | #[derive(Clone, Debug, Eq, PartialEq, Hash, PartialOrd, Ord)] 23 | pub enum ChainId { 24 | Mainnet, 25 | Sepolia, 26 | IntegrationSepolia, 27 | Other(String), 28 | } 29 | 30 | impl Serialize for ChainId { 31 | fn serialize(&self, serializer: S) -> Result 32 | where 33 | S: Serializer, 34 | { 35 | serializer.serialize_str(&self.to_string()) 36 | } 37 | } 38 | 39 | impl<'de> Deserialize<'de> for ChainId { 40 | fn deserialize(deserializer: D) -> Result 41 | where 42 | D: Deserializer<'de>, 43 | { 44 | let s = String::deserialize(deserializer)?; 45 | Ok(ChainId::from(s)) 46 | } 47 | } 48 | impl From for ChainId { 49 | fn from(s: String) -> Self { 50 | match s.as_ref() { 51 | "SN_MAIN" => ChainId::Mainnet, 52 | "SN_SEPOLIA" => ChainId::Sepolia, 53 | "SN_INTEGRATION_SEPOLIA" => ChainId::IntegrationSepolia, 54 | other => ChainId::Other(other.to_owned()), 55 | } 56 | } 57 | } 58 | impl Display for ChainId { 59 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 60 | match self { 61 | ChainId::Mainnet => write!(f, "SN_MAIN"), 62 | ChainId::Sepolia => write!(f, "SN_SEPOLIA"), 63 | ChainId::IntegrationSepolia => write!(f, "SN_INTEGRATION_SEPOLIA"), 64 | ChainId::Other(ref s) => write!(f, "{}", s), 65 | } 66 | } 67 | } 68 | 69 | impl ChainId { 70 | pub fn as_hex(&self) -> String { 71 | format!("0x{}", hex::encode(self.to_string())) 72 | } 73 | } 74 | 75 | /// The address of a contract, used for example in [StateDiff](`crate::state::StateDiff`), 76 | /// [DeclareTransaction](`crate::transaction::DeclareTransaction`), and 77 | /// [BlockHeader](`crate::block::BlockHeader`). 78 | 79 | // The block hash table is stored in address 0x1, 80 | // this is a special address that is not used for contracts. 81 | pub const BLOCK_HASH_TABLE_ADDRESS: ContractAddress = ContractAddress(PatriciaKey(StarkHash::ONE)); 82 | #[derive( 83 | Debug, 84 | Default, 85 | Copy, 86 | Clone, 87 | Display, 88 | Eq, 89 | PartialEq, 90 | Hash, 91 | Deserialize, 92 | Serialize, 93 | PartialOrd, 94 | Ord, 95 | derive_more::Deref, 96 | )] 97 | pub struct ContractAddress(pub PatriciaKey); 98 | 99 | impl From for Felt { 100 | fn from(contract_address: ContractAddress) -> Felt { 101 | **contract_address 102 | } 103 | } 104 | 105 | impl From for ContractAddress { 106 | fn from(val: u128) -> Self { 107 | ContractAddress(PatriciaKey::from(val)) 108 | } 109 | } 110 | 111 | impl_from_through_intermediate!(u128, ContractAddress, u8, u16, u32, u64); 112 | 113 | /// The maximal size of storage var. 114 | pub const MAX_STORAGE_ITEM_SIZE: u16 = 256; 115 | /// The prefix used in the calculation of a contract address. 116 | pub const CONTRACT_ADDRESS_PREFIX: &str = "STARKNET_CONTRACT_ADDRESS"; 117 | /// The size of the contract address domain. 118 | pub const CONTRACT_ADDRESS_DOMAIN_SIZE: Felt = Felt::from_hex_unchecked(PATRICIA_KEY_UPPER_BOUND); 119 | /// The address upper bound; it is defined to be congruent with the storage var address upper bound. 120 | pub static L2_ADDRESS_UPPER_BOUND: Lazy = Lazy::new(|| { 121 | NonZeroFelt::try_from(CONTRACT_ADDRESS_DOMAIN_SIZE - Felt::from(MAX_STORAGE_ITEM_SIZE)).unwrap() 122 | }); 123 | 124 | impl TryFrom for ContractAddress { 125 | type Error = StarknetApiError; 126 | fn try_from(hash: StarkHash) -> Result { 127 | Ok(Self(PatriciaKey::try_from(hash)?)) 128 | } 129 | } 130 | 131 | // TODO: Add a hash_function as a parameter 132 | pub fn calculate_contract_address( 133 | salt: ContractAddressSalt, 134 | class_hash: ClassHash, 135 | constructor_calldata: &Calldata, 136 | deployer_address: ContractAddress, 137 | ) -> Result { 138 | let constructor_calldata_hash = Pedersen::hash_array(&constructor_calldata.0); 139 | let contract_address_prefix = format!("0x{}", hex::encode(CONTRACT_ADDRESS_PREFIX)); 140 | let address = Pedersen::hash_array(&[ 141 | Felt::from_hex(contract_address_prefix.as_str()).map_err(|_| { 142 | StarknetApiError::OutOfRange { string: contract_address_prefix.clone() } 143 | })?, 144 | *deployer_address.0.key(), 145 | salt.0, 146 | class_hash.0, 147 | constructor_calldata_hash, 148 | ]); 149 | let (_, address) = address.div_rem(&L2_ADDRESS_UPPER_BOUND); 150 | 151 | ContractAddress::try_from(address) 152 | } 153 | 154 | /// The hash of a ContractClass. 155 | #[derive( 156 | Debug, 157 | Default, 158 | Copy, 159 | Clone, 160 | Eq, 161 | PartialEq, 162 | Hash, 163 | Deserialize, 164 | Serialize, 165 | PartialOrd, 166 | Ord, 167 | Display, 168 | derive_more::Deref, 169 | )] 170 | pub struct ClassHash(pub StarkHash); 171 | 172 | /// The hash of a compiled ContractClass. 173 | #[derive( 174 | Debug, 175 | Default, 176 | Copy, 177 | Clone, 178 | Eq, 179 | PartialEq, 180 | Hash, 181 | Deserialize, 182 | Serialize, 183 | PartialOrd, 184 | Ord, 185 | Display, 186 | )] 187 | pub struct CompiledClassHash(pub StarkHash); 188 | 189 | /// A general type for nonces. 190 | #[derive( 191 | Debug, 192 | Default, 193 | Copy, 194 | Clone, 195 | Eq, 196 | PartialEq, 197 | Hash, 198 | Deserialize, 199 | Serialize, 200 | PartialOrd, 201 | Ord, 202 | derive_more::Deref, 203 | )] 204 | pub struct Nonce(pub Felt); 205 | 206 | impl Nonce { 207 | pub fn try_increment(&self) -> Result { 208 | // Check if an overflow occurred during increment. 209 | let incremented = self.0 + Felt::ONE; 210 | if incremented == Felt::ZERO { 211 | return Err(StarknetApiError::OutOfRange { string: format!("{:?}", self) }); 212 | } 213 | Ok(Self(incremented)) 214 | } 215 | } 216 | 217 | /// The selector of an [EntryPoint](`crate::deprecated_contract_class::EntryPoint`). 218 | #[derive( 219 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 220 | )] 221 | pub struct EntryPointSelector(pub StarkHash); 222 | 223 | /// The root of the global state at a [Block](`crate::block::Block`) 224 | /// and [StateUpdate](`crate::state::StateUpdate`). 225 | #[derive( 226 | Debug, 227 | Copy, 228 | Clone, 229 | Default, 230 | Eq, 231 | PartialEq, 232 | Hash, 233 | Deserialize, 234 | Serialize, 235 | PartialOrd, 236 | Ord, 237 | Display, 238 | )] 239 | pub struct GlobalRoot(pub StarkHash); 240 | 241 | /// The commitment on the transactions in a [Block](`crate::block::Block`). 242 | #[derive( 243 | Debug, 244 | Copy, 245 | Clone, 246 | Default, 247 | Eq, 248 | PartialEq, 249 | Hash, 250 | Deserialize, 251 | Serialize, 252 | PartialOrd, 253 | Ord, 254 | Display, 255 | )] 256 | pub struct TransactionCommitment(pub StarkHash); 257 | 258 | /// The commitment on the events in a [Block](`crate::block::Block`). 259 | #[derive( 260 | Debug, 261 | Copy, 262 | Clone, 263 | Default, 264 | Eq, 265 | PartialEq, 266 | Hash, 267 | Deserialize, 268 | Serialize, 269 | PartialOrd, 270 | Ord, 271 | Display, 272 | )] 273 | pub struct EventCommitment(pub StarkHash); 274 | 275 | /// The commitment on the receipts in a [Block](`crate::block::Block`). 276 | #[derive( 277 | Debug, 278 | Copy, 279 | Clone, 280 | Default, 281 | Eq, 282 | PartialEq, 283 | Hash, 284 | Deserialize, 285 | Serialize, 286 | PartialOrd, 287 | Ord, 288 | Display, 289 | )] 290 | pub struct ReceiptCommitment(pub StarkHash); 291 | 292 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 293 | pub struct StateDiffCommitment(pub PoseidonHash); 294 | 295 | /// A key for nodes of a Patricia tree. 296 | // Invariant: key is in range. 297 | #[derive( 298 | Copy, 299 | Clone, 300 | Display, 301 | Eq, 302 | PartialEq, 303 | Default, 304 | Hash, 305 | Deserialize, 306 | Serialize, 307 | PartialOrd, 308 | Ord, 309 | derive_more:: Deref, 310 | )] 311 | #[display(fmt = "{}", "_0.to_fixed_hex_string()")] 312 | pub struct PatriciaKey(StarkHash); 313 | 314 | // 2**251 315 | pub const PATRICIA_KEY_UPPER_BOUND: &str = 316 | "0x800000000000000000000000000000000000000000000000000000000000000"; 317 | 318 | impl PatriciaKey { 319 | pub fn key(&self) -> &StarkHash { 320 | &self.0 321 | } 322 | } 323 | 324 | impl From for PatriciaKey { 325 | fn from(val: u128) -> Self { 326 | PatriciaKey::try_from(Felt::from(val)).expect("Failed to convert u128 to PatriciaKey.") 327 | } 328 | } 329 | 330 | impl_from_through_intermediate!(u128, PatriciaKey, u8, u16, u32, u64); 331 | 332 | impl TryFrom for PatriciaKey { 333 | type Error = StarknetApiError; 334 | 335 | fn try_from(value: StarkHash) -> Result { 336 | if value < CONTRACT_ADDRESS_DOMAIN_SIZE { 337 | return Ok(PatriciaKey(value)); 338 | } 339 | Err(StarknetApiError::OutOfRange { string: format!("[0x0, {PATRICIA_KEY_UPPER_BOUND})") }) 340 | } 341 | } 342 | 343 | impl Debug for PatriciaKey { 344 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 345 | f.debug_tuple("PatriciaKey").field(&self.0).finish() 346 | } 347 | } 348 | 349 | /// A utility macro to create a [`PatriciaKey`] from a hex string / unsigned integer representation. 350 | #[cfg(any(feature = "testing", test))] 351 | #[macro_export] 352 | macro_rules! patricia_key { 353 | ($s:expr) => { 354 | PatriciaKey::try_from(felt!($s)).unwrap() 355 | }; 356 | } 357 | 358 | /// A utility macro to create a [`ClassHash`] from a hex string / unsigned integer representation. 359 | #[cfg(any(feature = "testing", test))] 360 | #[macro_export] 361 | macro_rules! class_hash { 362 | ($s:expr) => { 363 | ClassHash(felt!($s)) 364 | }; 365 | } 366 | 367 | /// A utility macro to create a [`ContractAddress`] from a hex string / unsigned integer 368 | /// representation. 369 | #[cfg(any(feature = "testing", test))] 370 | #[macro_export] 371 | macro_rules! contract_address { 372 | ($s:expr) => { 373 | ContractAddress(patricia_key!($s)) 374 | }; 375 | } 376 | 377 | /// An Ethereum address. 378 | #[derive( 379 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 380 | )] 381 | #[serde(try_from = "PrefixedBytesAsHex<20_usize>", into = "PrefixedBytesAsHex<20_usize>")] 382 | pub struct EthAddress(pub H160); 383 | 384 | impl TryFrom for EthAddress { 385 | type Error = StarknetApiError; 386 | fn try_from(felt: Felt) -> Result { 387 | const COMPLIMENT_OF_H160: usize = std::mem::size_of::() - H160::len_bytes(); 388 | 389 | let bytes = felt.to_bytes_be(); 390 | let (rest, h160_bytes) = bytes.split_at(COMPLIMENT_OF_H160); 391 | if rest != [0u8; COMPLIMENT_OF_H160] { 392 | return Err(StarknetApiError::OutOfRange { string: felt.to_string() }); 393 | } 394 | 395 | Ok(EthAddress(H160::from_slice(h160_bytes))) 396 | } 397 | } 398 | 399 | impl From for Felt { 400 | fn from(value: EthAddress) -> Self { 401 | Felt::from_bytes_be_slice(value.0.as_bytes()) 402 | } 403 | } 404 | 405 | impl TryFrom> for EthAddress { 406 | type Error = StarknetApiError; 407 | fn try_from(val: PrefixedBytesAsHex<20_usize>) -> Result { 408 | Ok(EthAddress(H160::from_slice(&val.0))) 409 | } 410 | } 411 | 412 | impl From for PrefixedBytesAsHex<20_usize> { 413 | fn from(felt: EthAddress) -> Self { 414 | BytesAsHex(felt.0.to_fixed_bytes()) 415 | } 416 | } 417 | 418 | /// A public key of a sequencer. 419 | #[derive( 420 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 421 | )] 422 | pub struct SequencerPublicKey(pub PublicKey); 423 | 424 | #[derive( 425 | Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 426 | )] 427 | pub struct SequencerContractAddress(pub ContractAddress); 428 | -------------------------------------------------------------------------------- /src/core_test.rs: -------------------------------------------------------------------------------- 1 | use assert_matches::assert_matches; 2 | use starknet_types_core::felt::Felt; 3 | use starknet_types_core::hash::{Pedersen, StarkHash as CoreStarkHash}; 4 | 5 | use crate::core::{ 6 | calculate_contract_address, ClassHash, ContractAddress, EthAddress, Nonce, PatriciaKey, 7 | StarknetApiError, CONTRACT_ADDRESS_PREFIX, L2_ADDRESS_UPPER_BOUND, 8 | }; 9 | use crate::hash::StarkHash; 10 | use crate::transaction::{Calldata, ContractAddressSalt}; 11 | use crate::{class_hash, felt, patricia_key}; 12 | 13 | #[test] 14 | fn patricia_key_valid() { 15 | let hash = felt!("0x123"); 16 | let patricia_key = PatriciaKey::try_from(hash).unwrap(); 17 | assert_eq!(patricia_key.0, hash); 18 | } 19 | 20 | #[test] 21 | fn patricia_key_out_of_range() { 22 | // 2**251 23 | let hash = felt!("0x800000000000000000000000000000000000000000000000000000000000000"); 24 | let err = PatriciaKey::try_from(hash); 25 | assert_matches!(err, Err(StarknetApiError::OutOfRange { string: _err_str })); 26 | } 27 | 28 | #[test] 29 | fn patricia_key_macro() { 30 | assert_eq!( 31 | patricia_key!("0x123"), 32 | PatriciaKey::try_from(StarkHash::from_bytes_be(&[ 33 | 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 34 | 0, 0x1, 0x23 35 | ])) 36 | .unwrap() 37 | ); 38 | } 39 | 40 | #[test] 41 | fn test_calculate_contract_address() { 42 | let salt = ContractAddressSalt(Felt::from(1337_u16)); 43 | let class_hash = class_hash!("0x110"); 44 | let deployer_address = ContractAddress::default(); 45 | let constructor_calldata = 46 | Calldata(vec![Felt::from(60_u16), Felt::from(70_u16), Felt::MAX].into()); 47 | 48 | let actual_address = 49 | calculate_contract_address(salt, class_hash, &constructor_calldata, deployer_address) 50 | .unwrap(); 51 | 52 | let constructor_calldata_hash = Pedersen::hash_array(&constructor_calldata.0); 53 | let address = Pedersen::hash_array(&[ 54 | Felt::from_hex_unchecked(format!("0x{}", hex::encode(CONTRACT_ADDRESS_PREFIX)).as_str()), 55 | *deployer_address.0.key(), 56 | salt.0, 57 | class_hash.0, 58 | constructor_calldata_hash, 59 | ]); 60 | let (_, mod_address) = address.div_rem(&L2_ADDRESS_UPPER_BOUND); 61 | let expected_address = ContractAddress::try_from(mod_address).unwrap(); 62 | 63 | assert_eq!(actual_address, expected_address); 64 | } 65 | 66 | #[test] 67 | fn eth_address_serde() { 68 | let eth_address = EthAddress::try_from(felt!("0x001")).unwrap(); 69 | let serialized = serde_json::to_string(ð_address).unwrap(); 70 | assert_eq!(serialized, r#""0x1""#); 71 | 72 | let restored = serde_json::from_str::(&serialized).unwrap(); 73 | assert_eq!(restored, eth_address); 74 | } 75 | 76 | #[test] 77 | fn nonce_overflow() { 78 | // Increment on this value should overflow back to 0. 79 | let max_nonce = Nonce(Felt::MAX); 80 | 81 | let overflowed_nonce = max_nonce.try_increment(); 82 | assert_matches!(overflowed_nonce, Err(StarknetApiError::OutOfRange { string: _err_str })); 83 | } 84 | 85 | #[test] 86 | fn test_patricia_key_display() { 87 | assert_eq!(format!("{}", patricia_key!(7_u8)), String::from("0x") + &"0".repeat(63) + "7"); 88 | } 89 | 90 | #[test] 91 | fn test_contract_address_display() { 92 | assert_eq!( 93 | format!("{}", ContractAddress(patricia_key!(16_u8))), 94 | String::from("0x") + &"0".repeat(62) + "10" 95 | ); 96 | } 97 | -------------------------------------------------------------------------------- /src/crypto.rs: -------------------------------------------------------------------------------- 1 | pub mod patricia_hash; 2 | pub mod utils; 3 | -------------------------------------------------------------------------------- /src/crypto/crypto_test.rs: -------------------------------------------------------------------------------- 1 | // Unittest for verify_message_signature 2 | 3 | use starknet_types_core::hash::{Poseidon, StarkHash}; 4 | 5 | use crate::crypto::utils::{verify_message_hash_signature, PublicKey, Signature}; 6 | use crate::felt; 7 | 8 | #[test] 9 | fn signature_verification() { 10 | // The signed message of block 4256. 11 | let message_hash = Poseidon::hash_array(&[ 12 | felt!("0x7d5db04c5ca2aea828180dc441afb1580e3cee7547a3567ced3aa5bb8b273c0"), 13 | felt!("0x64689c12248e1110af4b3af0e2b43cd51ad13e8855f10e37669e2a4baf919c6"), 14 | ]); 15 | // The signature of the message. 16 | let signature = Signature { 17 | r: felt!("0x1b382bbfd693011c9b7692bc932b23ed9c288deb27c8e75772e172abbe5950c"), 18 | s: felt!("0xbe4438085057e1a7c704a0da3b30f7b8340fe3d24c86772abfd24aa597e42"), 19 | }; 20 | // The public key of the sequencer. 21 | let public_key = 22 | PublicKey(felt!("0x48253ff2c3bed7af18bde0b611b083b39445959102d4947c51c4db6aa4f4e58")); 23 | 24 | let result = verify_message_hash_signature(&message_hash, &signature, &public_key).unwrap(); 25 | assert!(result); 26 | } 27 | -------------------------------------------------------------------------------- /src/crypto/patricia_hash.rs: -------------------------------------------------------------------------------- 1 | //! Patricia hash tree implementation. 2 | //! 3 | //! Supports root hash calculation for Stark felt values, keyed by consecutive 64 bits numbers, 4 | //! starting from 0. 5 | //! 6 | //! Each edge is marked with one or more bits. 7 | //! The key of a node is the concatenation of the edges' marks in the path from the root to this 8 | //! node. 9 | //! The input keys are in the leaves, and each leaf is an input key. 10 | //! 11 | //! The edges coming out of an internal node with a key `K` are: 12 | //! - If there are input keys that start with 'K0...' and 'K1...', then two edges come out, marked 13 | //! with '0' and '1' bits. 14 | //! - Otherwise, a single edge mark with 'Z' is coming out. 'Z' is the longest string, such that all 15 | //! the input keys that start with 'K...' start with 'KZ...' as well. Note, the order of the input 16 | //! keys in this implementation forces 'Z' to be a zeros string. 17 | //! 18 | //! Hash of a node depends on the number of edges coming out of it: 19 | //! - A leaf: The hash is the input value of its key. 20 | //! - A single edge: hash(child_hash, edge_mark) + edge_length. 21 | //! - '0' and '1' edges: hash(zero_child_hash, one_child_hash). 22 | 23 | #[cfg(test)] 24 | #[path = "patricia_hash_test.rs"] 25 | mod patricia_hash_test; 26 | 27 | use bitvec::prelude::{BitArray, Msb0}; 28 | use starknet_types_core::felt::Felt; 29 | use starknet_types_core::hash::StarkHash as CoreStarkHash; 30 | 31 | const TREE_HEIGHT: u8 = 64; 32 | type BitPath = BitArray<[u8; 8], Msb0>; 33 | 34 | // An entry in a Patricia tree. 35 | #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] 36 | struct Entry { 37 | key: BitPath, 38 | value: Felt, 39 | } 40 | 41 | // A sub-tree is defined by a sub-sequence of leaves with a common ancestor at the specified height, 42 | // with no other leaves under it besides these. 43 | #[derive(Debug)] 44 | struct SubTree<'a> { 45 | leaves: &'a [Entry], 46 | // Levels from the root. 47 | height: u8, 48 | } 49 | 50 | enum SubTreeSplitting { 51 | // Number of '0' bits that all the keys start with. 52 | CommonZerosPrefix(u8), 53 | // The index of the first key that starts with a '1' bit. 54 | PartitionPoint(usize), 55 | } 56 | 57 | /// Calculates Patricia hash root on the given values. 58 | /// The values are keyed by consecutive numbers, starting from 0. 59 | pub fn calculate_root(values: Vec) -> Felt { 60 | if values.is_empty() { 61 | return Felt::ZERO; 62 | } 63 | let leaves: Vec = values 64 | .into_iter() 65 | .zip(0u64..) 66 | .map(|(felt, idx)| Entry { key: idx.to_be_bytes().into(), value: felt }) 67 | .collect(); 68 | get_hash::(SubTree { leaves: &leaves[..], height: 0_u8 }) 69 | } 70 | 71 | // Recursive hash calculation. There are 3 cases: 72 | // - Leaf: The sub tree height is maximal. It should contain exactly one entry. 73 | // - Edge: All the keys start with a longest common ('0's) prefix. NOTE: We assume that the keys are 74 | // a continuous range, and hence the case of '1's in the longest common prefix is impossible. 75 | // - Binary: Some keys start with '0' bit and some start with '1' bit. 76 | fn get_hash(sub_tree: SubTree<'_>) -> Felt { 77 | if sub_tree.height == TREE_HEIGHT { 78 | return sub_tree.leaves.first().expect("a leaf should not be empty").value; 79 | } 80 | match get_splitting(&sub_tree) { 81 | SubTreeSplitting::CommonZerosPrefix(n_zeros) => get_edge_hash::(sub_tree, n_zeros), 82 | SubTreeSplitting::PartitionPoint(partition_point) => { 83 | get_binary_hash::(sub_tree, partition_point) 84 | } 85 | } 86 | } 87 | 88 | // Hash on a '0's sequence with the bottom sub tree. 89 | fn get_edge_hash(sub_tree: SubTree<'_>, n_zeros: u8) -> Felt { 90 | let child_hash = 91 | get_hash::(SubTree { leaves: sub_tree.leaves, height: sub_tree.height + n_zeros }); 92 | let child_and_path_hash = H::hash(&child_hash, &Felt::ZERO); 93 | child_and_path_hash + Felt::from(n_zeros) 94 | } 95 | 96 | // Hash on both sides: starts with '0' bit and starts with '1' bit. 97 | // Assumes: 0 < partition point < sub_tree.len(). 98 | fn get_binary_hash(sub_tree: SubTree<'_>, partition_point: usize) -> Felt { 99 | let zero_hash = get_hash::(SubTree { 100 | leaves: &sub_tree.leaves[..partition_point], 101 | height: sub_tree.height + 1, 102 | }); 103 | let one_hash = get_hash::(SubTree { 104 | leaves: &sub_tree.leaves[partition_point..], 105 | height: sub_tree.height + 1, 106 | }); 107 | H::hash(&zero_hash, &one_hash) 108 | } 109 | 110 | // Returns the manner the keys of a subtree are splitting: some keys start with '1' or all keys 111 | // start with '0'. 112 | fn get_splitting(sub_tree: &SubTree<'_>) -> SubTreeSplitting { 113 | let mut height = sub_tree.height; 114 | 115 | let first_one_bit_index = 116 | sub_tree.leaves.partition_point(|entry| !entry.key[usize::from(height)]); 117 | if first_one_bit_index < sub_tree.leaves.len() { 118 | return SubTreeSplitting::PartitionPoint(first_one_bit_index); 119 | } 120 | 121 | height += 1; 122 | let mut n_zeros = 1; 123 | 124 | while height < TREE_HEIGHT { 125 | if sub_tree.leaves.last().expect("sub tree should not be empty").key[usize::from(height)] { 126 | break; 127 | } 128 | n_zeros += 1; 129 | height += 1; 130 | } 131 | SubTreeSplitting::CommonZerosPrefix(n_zeros) 132 | } 133 | -------------------------------------------------------------------------------- /src/crypto/patricia_hash_test.rs: -------------------------------------------------------------------------------- 1 | use starknet_types_core::felt::Felt; 2 | use starknet_types_core::hash::Poseidon; 3 | 4 | use super::calculate_root; 5 | use crate::felt; 6 | 7 | #[test] 8 | fn test_patricia_regression() { 9 | let root = 10 | calculate_root::(vec![Felt::from(1_u8), Felt::from(2_u8), Felt::from(3_u8)]); 11 | let expected_root = felt!("0x3b5cc7f1292eb3847c3f902d048a7e5dc7702d1c191ccd17c2d33f797e6fc32"); 12 | assert_eq!(root, expected_root); 13 | } 14 | 15 | #[test] 16 | fn test_edge_patricia_regression() { 17 | let root = calculate_root::(vec![Felt::from(1_u8)]); 18 | let expected_root = felt!("0x7752582c54a42fe0fa35c40f07293bb7d8efe90e21d8d2c06a7db52d7d9b7e1"); 19 | assert_eq!(root, expected_root); 20 | } 21 | 22 | #[test] 23 | fn test_binary_patricia_regression() { 24 | let root = calculate_root::(vec![Felt::from(1_u8), Felt::from(2_u8)]); 25 | let expected_root = felt!("0x1c1ba983ee0a0de87d87d67ea3cbee7023aa65f6b7bcf71259f122ea3af80bf"); 26 | assert_eq!(root, expected_root); 27 | } 28 | -------------------------------------------------------------------------------- /src/crypto/utils.rs: -------------------------------------------------------------------------------- 1 | //! Cryptographic utilities. 2 | //! This module provides cryptographic utilities. 3 | #[cfg(test)] 4 | #[path = "crypto_test.rs"] 5 | #[allow(clippy::explicit_auto_deref)] 6 | mod crypto_test; 7 | 8 | use std::fmt; 9 | use std::fmt::LowerHex; 10 | 11 | use serde::{Deserialize, Serialize}; 12 | use starknet_types_core::felt::Felt; 13 | use starknet_types_core::hash::{Pedersen, Poseidon, StarkHash as CoreStarkHash}; 14 | 15 | use crate::hash::StarkHash; 16 | 17 | /// An error that can occur during cryptographic operations. 18 | 19 | #[derive(thiserror::Error, Clone, Debug)] 20 | pub enum CryptoError { 21 | #[error("Invalid public key {0:#x}.")] 22 | InvalidPublicKey(PublicKey), 23 | #[error("Invalid message hash {0:#x}.")] 24 | InvalidMessageHash(Felt), 25 | #[error("Invalid r {0}.")] 26 | InvalidR(Felt), 27 | #[error("Invalid s {0}.")] 28 | InvalidS(Felt), 29 | } 30 | 31 | /// A public key. 32 | #[derive( 33 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 34 | )] 35 | pub struct PublicKey(pub Felt); 36 | 37 | impl LowerHex for PublicKey { 38 | fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { 39 | fmt::LowerHex::fmt(&self.0, f) 40 | } 41 | } 42 | 43 | /// A signature. 44 | #[derive( 45 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 46 | )] 47 | pub struct Signature { 48 | pub r: Felt, 49 | pub s: Felt, 50 | } 51 | 52 | fn to_field_element(felt: &Felt) -> starknet_crypto::FieldElement { 53 | starknet_crypto::FieldElement::from_mont(felt.to_raw_reversed()) 54 | } 55 | 56 | /// Verifies the authenticity of a signed message hash given the public key of the signer. 57 | pub fn verify_message_hash_signature( 58 | message_hash: &Felt, 59 | signature: &Signature, 60 | public_key: &PublicKey, 61 | ) -> Result { 62 | starknet_crypto::verify( 63 | &to_field_element(&public_key.0), 64 | &to_field_element(message_hash), 65 | &to_field_element(&signature.r), 66 | &to_field_element(&signature.s), 67 | ) 68 | .map_err(|err| match err { 69 | starknet_crypto::VerifyError::InvalidPublicKey => { 70 | CryptoError::InvalidPublicKey(*public_key) 71 | } 72 | starknet_crypto::VerifyError::InvalidMessageHash => { 73 | CryptoError::InvalidMessageHash(*message_hash) 74 | } 75 | starknet_crypto::VerifyError::InvalidR => CryptoError::InvalidR(signature.r), 76 | starknet_crypto::VerifyError::InvalidS => CryptoError::InvalidS(signature.s), 77 | }) 78 | } 79 | 80 | // Collect elements for applying hash chain. 81 | pub(crate) struct HashChain { 82 | elements: Vec, 83 | } 84 | 85 | impl HashChain { 86 | pub fn new() -> HashChain { 87 | HashChain { elements: Vec::new() } 88 | } 89 | 90 | // Chains a felt to the hash chain. 91 | pub fn chain(mut self, felt: &Felt) -> Self { 92 | self.elements.push(*felt); 93 | self 94 | } 95 | 96 | // Chains the result of a function to the hash chain. 97 | pub fn chain_if_fn Option>(self, f: F) -> Self { 98 | match f() { 99 | Some(felt) => self.chain(&felt), 100 | None => self, 101 | } 102 | } 103 | 104 | // Chains many felts to the hash chain. 105 | pub fn chain_iter<'a>(self, felts: impl Iterator) -> Self { 106 | felts.fold(self, |current, felt| current.chain(felt)) 107 | } 108 | 109 | // Chains the number of felts followed by the felts themselves to the hash chain. 110 | pub fn chain_size_and_elements(self, felts: &[Felt]) -> Self { 111 | self.chain(&felts.len().into()).chain_iter(felts.iter()) 112 | } 113 | 114 | // Chains a chain of felts to the hash chain. 115 | pub fn extend(mut self, chain: HashChain) -> Self { 116 | self.elements.extend(chain.elements); 117 | self 118 | } 119 | 120 | // Returns the pedersen hash of the chained felts, hashed with the length of the chain. 121 | pub fn get_pedersen_hash(&self) -> StarkHash { 122 | Pedersen::hash_array(self.elements.as_slice()) 123 | } 124 | 125 | // Returns the poseidon hash of the chained felts. 126 | pub fn get_poseidon_hash(&self) -> StarkHash { 127 | Poseidon::hash_array(self.elements.as_slice()) 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/data_availability.rs: -------------------------------------------------------------------------------- 1 | use serde::{Deserialize, Serialize}; 2 | use starknet_types_core::felt::Felt; 3 | 4 | use crate::StarknetApiError; 5 | 6 | #[derive(Clone, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 7 | #[serde(try_from = "Deserializer")] 8 | pub enum DataAvailabilityMode { 9 | L1 = 0, 10 | L2 = 1, 11 | } 12 | 13 | /// Deserialize a `DataAvailabilityMode` from a given `Deserializer`. 14 | /// 15 | /// This implementation supports deserializing the `DataAvailabilityMode` enum from both numerical 16 | /// and textual representations. 17 | #[derive(Deserialize)] 18 | #[serde(untagged)] 19 | enum Deserializer { 20 | Num(u8), 21 | Text(String), 22 | } 23 | 24 | impl TryFrom for DataAvailabilityMode { 25 | type Error = StarknetApiError; 26 | 27 | fn try_from(value: Deserializer) -> Result { 28 | match value { 29 | Deserializer::Num(0_u8) => Ok(DataAvailabilityMode::L1), 30 | Deserializer::Num(1_u8) => Ok(DataAvailabilityMode::L2), 31 | Deserializer::Text(text) if &text == "L1" => Ok(DataAvailabilityMode::L1), 32 | Deserializer::Text(text) if &text == "L2" => Ok(DataAvailabilityMode::L2), 33 | _ => Err(StarknetApiError::OutOfRange { 34 | string: "Data availability must be either 'L1' or '0' for L1, or 'L2' or '1' for \ 35 | L2." 36 | .to_string(), 37 | }), 38 | } 39 | } 40 | } 41 | 42 | impl TryFrom for DataAvailabilityMode { 43 | type Error = StarknetApiError; 44 | 45 | fn try_from(felt: Felt) -> Result { 46 | if felt == Felt::ZERO { 47 | return Ok(DataAvailabilityMode::L1); 48 | } 49 | if felt == Felt::ONE { 50 | return Ok(DataAvailabilityMode::L2); 51 | } 52 | Err(StarknetApiError::OutOfRange { 53 | string: format!("Invalid data availability mode: {felt}."), 54 | }) 55 | } 56 | } 57 | 58 | impl From for Felt { 59 | fn from(data_availability_mode: DataAvailabilityMode) -> Felt { 60 | match data_availability_mode { 61 | DataAvailabilityMode::L1 => Felt::ZERO, 62 | DataAvailabilityMode::L2 => Felt::ONE, 63 | } 64 | } 65 | } 66 | 67 | #[derive( 68 | Clone, Default, Copy, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, 69 | )] 70 | #[serde(rename_all = "SCREAMING_SNAKE_CASE")] 71 | pub enum L1DataAvailabilityMode { 72 | #[default] 73 | Calldata, 74 | Blob, 75 | } 76 | -------------------------------------------------------------------------------- /src/deprecated_contract_class.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | use std::num::ParseIntError; 3 | 4 | use cairo_lang_starknet_classes::casm_contract_class::CasmContractEntryPoint; 5 | use itertools::Itertools; 6 | use serde::de::Error as DeserializationError; 7 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 8 | use serde_json::Value; 9 | 10 | use crate::core::EntryPointSelector; 11 | use crate::hash::StarkHash; 12 | use crate::serde_utils::deserialize_optional_contract_class_abi_entry_vector; 13 | use crate::StarknetApiError; 14 | 15 | /// A deprecated contract class. 16 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 17 | pub struct ContractClass { 18 | // Starknet does not verify the abi. If we can't parse it, we set it to None. 19 | #[serde(default, deserialize_with = "deserialize_optional_contract_class_abi_entry_vector")] 20 | pub abi: Option>, 21 | pub program: Program, 22 | /// The selector of each entry point is a unique identifier in the program. 23 | // TODO: Consider changing to IndexMap, since this is used for computing the 24 | // class hash. 25 | pub entry_points_by_type: HashMap>, 26 | } 27 | 28 | /// A [ContractClass](`crate::deprecated_contract_class::ContractClass`) abi entry. 29 | // Using untagged so the serialization will be sorted by the keys (the default behavior of Serde for 30 | // untagged enums). We care about the order of the fields in the serialization because it affects 31 | // the class hash calculation. 32 | #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] 33 | #[serde(deny_unknown_fields, untagged)] 34 | pub enum ContractClassAbiEntry { 35 | Constructor(FunctionAbiEntry), 36 | Event(EventAbiEntry), 37 | Function(FunctionAbiEntry), 38 | L1Handler(FunctionAbiEntry), 39 | Struct(StructAbiEntry), 40 | } 41 | 42 | /// An event abi entry. 43 | // The members of the struct are sorted lexicographically for correct hash computation. 44 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 45 | pub struct EventAbiEntry { 46 | pub data: Vec, 47 | pub keys: Vec, 48 | pub name: String, 49 | pub r#type: EventType, 50 | } 51 | 52 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 53 | #[serde(rename_all = "snake_case")] 54 | pub enum EventType { 55 | #[default] 56 | Event, 57 | } 58 | 59 | /// A function abi entry. 60 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 61 | pub struct FunctionAbiEntry { 62 | pub inputs: Vec, 63 | pub name: String, 64 | pub outputs: Vec, 65 | #[serde(rename = "stateMutability", default, skip_serializing_if = "Option::is_none")] 66 | pub state_mutability: Option, 67 | pub r#type: TYPE, 68 | } 69 | 70 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 71 | #[serde(rename_all = "snake_case")] 72 | pub enum FunctionType { 73 | #[default] 74 | Function, 75 | } 76 | 77 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 78 | #[serde(rename_all = "snake_case")] 79 | pub enum ConstructorType { 80 | #[default] 81 | Constructor, 82 | } 83 | 84 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 85 | #[serde(rename_all = "snake_case")] 86 | pub enum L1HandlerType { 87 | #[default] 88 | L1Handler, 89 | } 90 | 91 | /// A function state mutability. 92 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 93 | pub enum FunctionStateMutability { 94 | #[serde(rename = "view")] 95 | #[default] 96 | View, 97 | } 98 | 99 | /// A struct abi entry. 100 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 101 | pub struct StructAbiEntry { 102 | pub members: Vec, 103 | pub name: String, 104 | pub size: usize, 105 | pub r#type: StructType, 106 | } 107 | 108 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 109 | #[serde(rename_all = "snake_case")] 110 | pub enum StructType { 111 | #[default] 112 | Struct, 113 | } 114 | 115 | /// A struct member for [StructAbiEntry](`crate::deprecated_contract_class::StructAbiEntry`). 116 | // The members of the struct are sorted lexicographically for correct hash computation. 117 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 118 | pub struct StructMember { 119 | pub name: String, 120 | pub offset: usize, 121 | pub r#type: String, 122 | } 123 | 124 | /// A program corresponding to a [ContractClass](`crate::deprecated_contract_class::ContractClass`). 125 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 126 | pub struct Program { 127 | #[serde(default)] 128 | pub attributes: serde_json::Value, 129 | pub builtins: serde_json::Value, 130 | #[serde(default)] 131 | pub compiler_version: serde_json::Value, 132 | pub data: serde_json::Value, 133 | #[serde(default)] 134 | pub debug_info: serde_json::Value, 135 | #[serde(serialize_with = "serialize_hints_sorted")] 136 | pub hints: serde_json::Value, 137 | pub identifiers: serde_json::Value, 138 | pub main_scope: serde_json::Value, 139 | pub prime: serde_json::Value, 140 | pub reference_manager: serde_json::Value, 141 | } 142 | 143 | // Serialize hints as a sorted mapping for correct hash computation. 144 | fn serialize_hints_sorted(hints: &serde_json::Value, serializer: S) -> Result 145 | where 146 | S: Serializer, 147 | { 148 | if hints.is_null() { 149 | return serializer.serialize_none(); 150 | } 151 | let hints_map = 152 | hints.as_object().ok_or(serde::ser::Error::custom("Hints are not a mapping."))?; 153 | serializer.collect_map( 154 | hints_map 155 | .iter() 156 | // Parse the keys as integers and sort them. 157 | .map(|(k, v)| Ok((k.parse::()?, v))) 158 | .collect::, ParseIntError>>() 159 | .map_err(serde::ser::Error::custom)? 160 | .iter() 161 | .sorted_by_key(|(k, _v)| *k) 162 | // Convert the keys back to strings. 163 | .map(|(k, v)| (k.to_string(), v)), 164 | ) 165 | } 166 | 167 | /// An entry point type of a [ContractClass](`crate::deprecated_contract_class::ContractClass`). 168 | #[derive( 169 | Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 170 | )] 171 | #[serde(deny_unknown_fields)] 172 | pub enum EntryPointType { 173 | /// A constructor entry point. 174 | #[serde(rename = "CONSTRUCTOR")] 175 | Constructor, 176 | /// An external4 entry point. 177 | #[serde(rename = "EXTERNAL")] 178 | #[default] 179 | External, 180 | /// An L1 handler entry point. 181 | #[serde(rename = "L1_HANDLER")] 182 | L1Handler, 183 | } 184 | 185 | /// An entry point of a [ContractClass](`crate::deprecated_contract_class::ContractClass`). 186 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 187 | pub struct EntryPoint { 188 | pub selector: EntryPointSelector, 189 | pub offset: EntryPointOffset, 190 | } 191 | 192 | impl TryFrom for EntryPoint { 193 | type Error = StarknetApiError; 194 | 195 | fn try_from(value: CasmContractEntryPoint) -> Result { 196 | Ok(EntryPoint { 197 | selector: EntryPointSelector(StarkHash::from(value.selector)), 198 | offset: EntryPointOffset(value.offset), 199 | }) 200 | } 201 | } 202 | 203 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 204 | pub struct TypedParameter { 205 | pub name: String, 206 | pub r#type: String, 207 | } 208 | 209 | /// The offset of an [EntryPoint](`crate::deprecated_contract_class::EntryPoint`). 210 | #[derive( 211 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 212 | )] 213 | pub struct EntryPointOffset( 214 | #[serde(deserialize_with = "number_or_string", serialize_with = "usize_to_hex")] pub usize, 215 | ); 216 | impl TryFrom for EntryPointOffset { 217 | type Error = StarknetApiError; 218 | 219 | fn try_from(value: String) -> Result { 220 | Ok(Self(hex_string_try_into_usize(&value)?)) 221 | } 222 | } 223 | 224 | pub fn number_or_string<'de, D: Deserializer<'de>>(deserializer: D) -> Result { 225 | let usize_value = match Value::deserialize(deserializer)? { 226 | Value::Number(number) => { 227 | number.as_u64().ok_or(DeserializationError::custom("Cannot cast number to usize."))? 228 | as usize 229 | } 230 | Value::String(s) => hex_string_try_into_usize(&s).map_err(DeserializationError::custom)?, 231 | _ => return Err(DeserializationError::custom("Cannot cast value into usize.")), 232 | }; 233 | Ok(usize_value) 234 | } 235 | 236 | fn hex_string_try_into_usize(hex_string: &str) -> Result { 237 | usize::from_str_radix(hex_string.trim_start_matches("0x"), 16) 238 | } 239 | 240 | fn usize_to_hex(value: &usize, s: S) -> Result 241 | where 242 | S: Serializer, 243 | { 244 | s.serialize_str(format!("{:#x}", value).as_str()) 245 | } 246 | -------------------------------------------------------------------------------- /src/hash.rs: -------------------------------------------------------------------------------- 1 | use std::fmt::Debug; 2 | 3 | use serde::{Deserialize, Serialize}; 4 | use sha3::{Digest, Keccak256}; 5 | use starknet_types_core::felt::Felt; 6 | 7 | pub type StarkHash = Felt; 8 | 9 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 10 | pub struct PoseidonHash(pub Felt); 11 | 12 | /// Computes the first 250 bits of the Keccak256 hash, in order to fit into a field element. 13 | pub fn starknet_keccak_hash(input: &[u8]) -> Felt { 14 | let mut keccak = Keccak256::default(); 15 | keccak.update(input); 16 | let mut hashed_bytes: [u8; 32] = keccak.finalize().into(); 17 | hashed_bytes[0] &= 0b00000011_u8; // Discard the six MSBs. 18 | Felt::from_bytes_be(&hashed_bytes) 19 | } 20 | 21 | #[cfg(any(feature = "testing", test))] 22 | pub struct FeltConverter; 23 | 24 | #[cfg(any(feature = "testing", test))] 25 | pub trait TryIntoFelt { 26 | fn to_felt_unchecked(v: V) -> Felt; 27 | } 28 | 29 | macro_rules! impl_try_into_felt { 30 | ($type:ty) => { 31 | #[cfg(any(feature = "testing", test))] 32 | impl TryIntoFelt<$type> for FeltConverter { 33 | fn to_felt_unchecked(v: $type) -> Felt { 34 | Felt::from(v) 35 | } 36 | } 37 | }; 38 | } 39 | 40 | impl_try_into_felt!(u128); 41 | impl_try_into_felt!(u64); 42 | impl_try_into_felt!(u32); 43 | impl_try_into_felt!(u16); 44 | impl_try_into_felt!(u8); 45 | 46 | #[cfg(any(feature = "testing", test))] 47 | impl TryIntoFelt<&str> for FeltConverter { 48 | fn to_felt_unchecked(v: &str) -> Felt { 49 | Felt::from_hex_unchecked(v) 50 | } 51 | } 52 | 53 | /// A utility macro to create a [`starknet_types_core::felt::Felt`] from an intergert or a hex 54 | /// string representation. 55 | #[cfg(any(feature = "testing", test))] 56 | #[macro_export] 57 | macro_rules! felt { 58 | ($s:expr) => { 59 | <$crate::hash::FeltConverter as $crate::hash::TryIntoFelt<_>>::to_felt_unchecked($s) 60 | }; 61 | } 62 | -------------------------------------------------------------------------------- /src/internal_transaction.rs: -------------------------------------------------------------------------------- 1 | use crate::core::{ContractAddress, Nonce}; 2 | use crate::state::ContractClass; 3 | use crate::transaction::{ 4 | DeclareTransaction, DeployAccountTransaction, InvokeTransaction, Tip, TransactionHash, 5 | }; 6 | 7 | /// Represents a paid Starknet transaction. 8 | #[derive(Clone, Debug, Eq, PartialEq)] 9 | pub enum InternalTransaction { 10 | Declare(InternalDeclareTransaction), 11 | DeployAccount(InternalDeployAccountTransaction), 12 | Invoke(InternalInvokeTransaction), 13 | } 14 | 15 | impl InternalTransaction { 16 | pub fn contract_address(&self) -> ContractAddress { 17 | match self { 18 | InternalTransaction::Declare(tx_data) => tx_data.tx.sender_address(), 19 | InternalTransaction::DeployAccount(tx_data) => tx_data.contract_address, 20 | InternalTransaction::Invoke(tx_data) => tx_data.tx.sender_address(), 21 | } 22 | } 23 | 24 | pub fn nonce(&self) -> Nonce { 25 | match self { 26 | InternalTransaction::Declare(tx_data) => tx_data.tx.nonce(), 27 | InternalTransaction::DeployAccount(tx_data) => tx_data.tx.nonce(), 28 | InternalTransaction::Invoke(tx_data) => tx_data.tx.nonce(), 29 | } 30 | } 31 | 32 | pub fn tx_hash(&self) -> TransactionHash { 33 | match self { 34 | InternalTransaction::Declare(tx_data) => tx_data.tx_hash, 35 | InternalTransaction::DeployAccount(tx_data) => tx_data.tx_hash, 36 | InternalTransaction::Invoke(tx_data) => tx_data.tx_hash, 37 | } 38 | } 39 | 40 | pub fn tip(&self) -> Option { 41 | match self { 42 | InternalTransaction::Declare(declare_tx) => match &declare_tx.tx { 43 | DeclareTransaction::V3(tx_v3) => Some(tx_v3.tip), 44 | _ => None, 45 | }, 46 | InternalTransaction::DeployAccount(deploy_account_tx) => match &deploy_account_tx.tx { 47 | DeployAccountTransaction::V3(tx_v3) => Some(tx_v3.tip), 48 | _ => None, 49 | }, 50 | InternalTransaction::Invoke(invoke_tx) => match &invoke_tx.tx { 51 | InvokeTransaction::V3(tx_v3) => Some(tx_v3.tip), 52 | _ => None, 53 | }, 54 | } 55 | } 56 | } 57 | 58 | // TODO(Mohammad): Add constructor for all the transaction's structs. 59 | #[derive(Clone, Debug, Eq, PartialEq)] 60 | pub struct InternalDeclareTransaction { 61 | pub tx: DeclareTransaction, 62 | pub tx_hash: TransactionHash, 63 | // Indicates the presence of the only_query bit in the version. 64 | pub only_query: bool, 65 | pub class_info: ClassInfo, 66 | } 67 | 68 | #[derive(Clone, Debug, Eq, PartialEq)] 69 | pub struct InternalDeployAccountTransaction { 70 | pub tx: DeployAccountTransaction, 71 | pub tx_hash: TransactionHash, 72 | pub contract_address: ContractAddress, 73 | // Indicates the presence of the only_query bit in the version. 74 | pub only_query: bool, 75 | } 76 | 77 | #[derive(Clone, Debug, Eq, PartialEq)] 78 | pub struct InternalInvokeTransaction { 79 | pub tx: InvokeTransaction, 80 | pub tx_hash: TransactionHash, 81 | // Indicates the presence of the only_query bit in the version. 82 | pub only_query: bool, 83 | } 84 | 85 | #[derive(Clone, Debug, Eq, PartialEq)] 86 | pub struct ClassInfo { 87 | pub contract_class: ContractClass, 88 | pub sierra_program_length: usize, 89 | pub abi_length: usize, 90 | } 91 | -------------------------------------------------------------------------------- /src/lib.rs: -------------------------------------------------------------------------------- 1 | //! Representations of canonical [`Starknet`] components. 2 | //! 3 | //! [`Starknet`]: https://starknet.io/ 4 | 5 | pub mod block; 6 | pub mod block_hash; 7 | pub mod core; 8 | pub mod crypto; 9 | pub mod data_availability; 10 | pub mod deprecated_contract_class; 11 | pub mod hash; 12 | pub mod internal_transaction; 13 | pub mod rpc_transaction; 14 | pub mod serde_utils; 15 | pub mod state; 16 | pub mod transaction; 17 | pub mod transaction_hash; 18 | pub mod type_utils; 19 | 20 | use std::num::ParseIntError; 21 | 22 | use serde_utils::InnerDeserializationError; 23 | 24 | /// The error type returned by StarknetApi. 25 | // Note: if you need `Eq` see InnerDeserializationError's docstring. 26 | #[derive(thiserror::Error, Clone, Debug, PartialEq)] 27 | pub enum StarknetApiError { 28 | /// Error in the inner deserialization of the node. 29 | #[error(transparent)] 30 | InnerDeserialization(#[from] InnerDeserializationError), 31 | #[error("Out of range {string}.")] 32 | /// An error for when a value is out of range. 33 | OutOfRange { string: String }, 34 | /// Error when serializing into number. 35 | #[error(transparent)] 36 | ParseIntError(#[from] ParseIntError), 37 | /// Missing resource type / duplicated resource type. 38 | #[error("Missing resource type / duplicated resource type; got {0}.")] 39 | InvalidResourceMappingInitializer(String), 40 | } 41 | -------------------------------------------------------------------------------- /src/rpc_transaction.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[path = "rpc_transaction_test.rs"] 3 | mod rpc_transaction_test; 4 | 5 | use std::collections::BTreeMap; 6 | 7 | use serde::{Deserialize, Serialize}; 8 | use starknet_types_core::felt::Felt; 9 | 10 | use crate::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce}; 11 | use crate::data_availability::DataAvailabilityMode; 12 | use crate::state::EntryPoint; 13 | use crate::transaction::{ 14 | AccountDeploymentData, Calldata, ContractAddressSalt, PaymasterData, Resource, ResourceBounds, 15 | Tip, TransactionSignature, 16 | }; 17 | 18 | /// Transactions that are ready to be broadcasted to the network through RPC and are not included in 19 | /// a block. 20 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 21 | #[serde(tag = "type")] 22 | #[serde(deny_unknown_fields)] 23 | pub enum RpcTransaction { 24 | #[serde(rename = "DECLARE")] 25 | Declare(RpcDeclareTransaction), 26 | #[serde(rename = "DEPLOY_ACCOUNT")] 27 | DeployAccount(RpcDeployAccountTransaction), 28 | #[serde(rename = "INVOKE")] 29 | Invoke(RpcInvokeTransaction), 30 | } 31 | 32 | macro_rules! implement_ref_getters { 33 | ($(($member_name:ident, $member_type:ty)), *) => { 34 | $(pub fn $member_name(&self) -> &$member_type { 35 | match self { 36 | RpcTransaction::Declare( 37 | RpcDeclareTransaction::V3(tx) 38 | ) => &tx.$member_name, 39 | RpcTransaction::DeployAccount( 40 | RpcDeployAccountTransaction::V3(tx) 41 | ) => &tx.$member_name, 42 | RpcTransaction::Invoke( 43 | RpcInvokeTransaction::V3(tx) 44 | ) => &tx.$member_name 45 | } 46 | })* 47 | }; 48 | } 49 | 50 | impl RpcTransaction { 51 | implement_ref_getters!( 52 | (nonce, Nonce), 53 | (resource_bounds, ResourceBoundsMapping), 54 | (signature, TransactionSignature), 55 | (tip, Tip) 56 | ); 57 | } 58 | 59 | /// A RPC declare transaction. 60 | /// 61 | /// This transaction is equivalent to the component DECLARE_TXN in the 62 | /// [`Starknet specs`] with a contract class (DECLARE_TXN allows having 63 | /// either a contract class or a class hash). 64 | /// 65 | /// [`Starknet specs`]: https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json 66 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 67 | #[serde(tag = "version")] 68 | pub enum RpcDeclareTransaction { 69 | #[serde(rename = "0x3")] 70 | V3(RpcDeclareTransactionV3), 71 | } 72 | 73 | /// A RPC deploy account transaction. 74 | /// 75 | /// This transaction is equivalent to the component DEPLOY_ACCOUNT_TXN in the 76 | /// [`Starknet specs`]. 77 | /// 78 | /// [`Starknet specs`]: https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json 79 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 80 | #[serde(tag = "version")] 81 | pub enum RpcDeployAccountTransaction { 82 | #[serde(rename = "0x3")] 83 | V3(RpcDeployAccountTransactionV3), 84 | } 85 | 86 | /// A RPC invoke transaction. 87 | /// 88 | /// This transaction is equivalent to the component INVOKE_TXN in the 89 | /// [`Starknet specs`]. 90 | /// 91 | /// [`Starknet specs`]: https://github.com/starkware-libs/starknet-specs/blob/master/api/starknet_api_openrpc.json 92 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 93 | #[serde(tag = "version")] 94 | pub enum RpcInvokeTransaction { 95 | #[serde(rename = "0x3")] 96 | V3(RpcInvokeTransactionV3), 97 | } 98 | 99 | /// A declare transaction of a Cairo-v1 contract class that can be added to Starknet through the 100 | /// RPC. 101 | #[derive(Clone, Debug, Deserialize, Eq, PartialEq, Serialize)] 102 | pub struct RpcDeclareTransactionV3 { 103 | // TODO: Check with Shahak why we need to keep the DeclareType. 104 | // pub r#type: DeclareType, 105 | pub sender_address: ContractAddress, 106 | pub compiled_class_hash: CompiledClassHash, 107 | pub signature: TransactionSignature, 108 | pub nonce: Nonce, 109 | pub contract_class: ContractClass, 110 | pub resource_bounds: ResourceBoundsMapping, 111 | pub tip: Tip, 112 | pub paymaster_data: PaymasterData, 113 | pub account_deployment_data: AccountDeploymentData, 114 | pub nonce_data_availability_mode: DataAvailabilityMode, 115 | pub fee_data_availability_mode: DataAvailabilityMode, 116 | } 117 | 118 | /// A deploy account transaction that can be added to Starknet through the RPC. 119 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 120 | pub struct RpcDeployAccountTransactionV3 { 121 | pub signature: TransactionSignature, 122 | pub nonce: Nonce, 123 | pub class_hash: ClassHash, 124 | pub contract_address_salt: ContractAddressSalt, 125 | pub constructor_calldata: Calldata, 126 | pub resource_bounds: ResourceBoundsMapping, 127 | pub tip: Tip, 128 | pub paymaster_data: PaymasterData, 129 | pub nonce_data_availability_mode: DataAvailabilityMode, 130 | pub fee_data_availability_mode: DataAvailabilityMode, 131 | } 132 | 133 | /// An invoke account transaction that can be added to Starknet through the RPC. 134 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 135 | pub struct RpcInvokeTransactionV3 { 136 | pub sender_address: ContractAddress, 137 | pub calldata: Calldata, 138 | pub signature: TransactionSignature, 139 | pub nonce: Nonce, 140 | pub resource_bounds: ResourceBoundsMapping, 141 | pub tip: Tip, 142 | pub paymaster_data: PaymasterData, 143 | pub account_deployment_data: AccountDeploymentData, 144 | pub nonce_data_availability_mode: DataAvailabilityMode, 145 | pub fee_data_availability_mode: DataAvailabilityMode, 146 | } 147 | 148 | // The contract class in SN_API state doesn't have `contract_class_version`, not following the spec. 149 | #[derive(Clone, Debug, Default, Deserialize, Eq, PartialEq, Serialize)] 150 | pub struct ContractClass { 151 | pub sierra_program: Vec, 152 | pub contract_class_version: String, 153 | pub entry_points_by_type: EntryPointByType, 154 | pub abi: String, 155 | } 156 | 157 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 158 | pub struct EntryPointByType { 159 | #[serde(rename = "CONSTRUCTOR")] 160 | pub constructor: Vec, 161 | #[serde(rename = "EXTERNAL")] 162 | pub external: Vec, 163 | #[serde(rename = "L1_HANDLER")] 164 | pub l1handler: Vec, 165 | } 166 | 167 | // The serialization of the struct in transaction is in capital letters, not following the spec. 168 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 169 | pub struct ResourceBoundsMapping { 170 | pub l1_gas: ResourceBounds, 171 | pub l2_gas: ResourceBounds, 172 | } 173 | 174 | impl From for crate::transaction::ResourceBoundsMapping { 175 | fn from(mapping: ResourceBoundsMapping) -> crate::transaction::ResourceBoundsMapping { 176 | let map = 177 | BTreeMap::from([(Resource::L1Gas, mapping.l1_gas), (Resource::L2Gas, mapping.l2_gas)]); 178 | crate::transaction::ResourceBoundsMapping(map) 179 | } 180 | } 181 | -------------------------------------------------------------------------------- /src/rpc_transaction_test.rs: -------------------------------------------------------------------------------- 1 | use std::sync::Arc; 2 | 3 | use rstest::rstest; 4 | use starknet_types_core::felt::Felt; 5 | 6 | use crate::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; 7 | use crate::rpc_transaction::{ 8 | ContractClass, DataAvailabilityMode, ResourceBoundsMapping, RpcDeclareTransaction, 9 | RpcDeclareTransactionV3, RpcDeployAccountTransaction, RpcDeployAccountTransactionV3, 10 | RpcInvokeTransaction, RpcInvokeTransactionV3, RpcTransaction, 11 | }; 12 | use crate::transaction::{ 13 | AccountDeploymentData, Calldata, ContractAddressSalt, PaymasterData, ResourceBounds, Tip, 14 | TransactionSignature, 15 | }; 16 | use crate::{contract_address, felt, patricia_key}; 17 | 18 | fn create_resource_bounds_for_testing() -> ResourceBoundsMapping { 19 | ResourceBoundsMapping { 20 | l1_gas: ResourceBounds { max_amount: 100, max_price_per_unit: 12 }, 21 | l2_gas: ResourceBounds { max_amount: 58, max_price_per_unit: 31 }, 22 | } 23 | } 24 | 25 | fn create_declare_v3() -> RpcDeclareTransaction { 26 | RpcDeclareTransaction::V3(RpcDeclareTransactionV3 { 27 | contract_class: ContractClass::default(), 28 | resource_bounds: create_resource_bounds_for_testing(), 29 | tip: Tip(1), 30 | signature: TransactionSignature(vec![Felt::ONE, Felt::TWO]), 31 | nonce: Nonce(Felt::ONE), 32 | compiled_class_hash: CompiledClassHash(Felt::TWO), 33 | sender_address: contract_address!("0x3"), 34 | nonce_data_availability_mode: DataAvailabilityMode::L1, 35 | fee_data_availability_mode: DataAvailabilityMode::L2, 36 | paymaster_data: PaymasterData(vec![Felt::ZERO]), 37 | account_deployment_data: AccountDeploymentData(vec![Felt::THREE]), 38 | }) 39 | } 40 | 41 | fn create_deploy_account_v3() -> RpcDeployAccountTransaction { 42 | RpcDeployAccountTransaction::V3(RpcDeployAccountTransactionV3 { 43 | resource_bounds: create_resource_bounds_for_testing(), 44 | tip: Tip::default(), 45 | contract_address_salt: ContractAddressSalt(felt!("0x23")), 46 | class_hash: ClassHash(Felt::TWO), 47 | constructor_calldata: Calldata(Arc::new(vec![Felt::ZERO])), 48 | nonce: Nonce(felt!("0x60")), 49 | signature: TransactionSignature(vec![Felt::TWO]), 50 | nonce_data_availability_mode: DataAvailabilityMode::L2, 51 | fee_data_availability_mode: DataAvailabilityMode::L1, 52 | paymaster_data: PaymasterData(vec![Felt::TWO, Felt::ZERO]), 53 | }) 54 | } 55 | 56 | fn create_invoke_v3() -> RpcInvokeTransaction { 57 | RpcInvokeTransaction::V3(RpcInvokeTransactionV3 { 58 | resource_bounds: create_resource_bounds_for_testing(), 59 | tip: Tip(50), 60 | calldata: Calldata(Arc::new(vec![felt!("0x2000"), felt!("0x1000")])), 61 | sender_address: contract_address!("0x53"), 62 | nonce: Nonce(felt!("0x32")), 63 | signature: TransactionSignature::default(), 64 | nonce_data_availability_mode: DataAvailabilityMode::L1, 65 | fee_data_availability_mode: DataAvailabilityMode::L1, 66 | paymaster_data: PaymasterData(vec![Felt::TWO, Felt::ZERO]), 67 | account_deployment_data: AccountDeploymentData(vec![felt!("0x87")]), 68 | }) 69 | } 70 | 71 | // We are testing the `RpcTransaction` serialization. Passing non-default values. 72 | #[rstest] 73 | #[case(RpcTransaction::Declare(create_declare_v3()))] 74 | #[case(RpcTransaction::DeployAccount(create_deploy_account_v3()))] 75 | #[case(RpcTransaction::Invoke(create_invoke_v3()))] 76 | fn test_rpc_transactions(#[case] tx: RpcTransaction) { 77 | let serialized = serde_json::to_string(&tx).unwrap(); 78 | let deserialized: RpcTransaction = serde_json::from_str(&serialized).unwrap(); 79 | assert_eq!(tx, deserialized); 80 | } 81 | -------------------------------------------------------------------------------- /src/serde_utils.rs: -------------------------------------------------------------------------------- 1 | //! Utilities for serialising/deserialising hexadecimal values. 2 | #[cfg(test)] 3 | #[path = "serde_utils_test.rs"] 4 | mod serde_utils_test; 5 | 6 | use serde::de::{Deserialize, Visitor}; 7 | use serde::ser::{Serialize, SerializeTuple}; 8 | use serde::Deserializer; 9 | 10 | use crate::deprecated_contract_class::ContractClassAbiEntry; 11 | 12 | /// A [BytesAsHex](`crate::serde_utils::BytesAsHex`) prefixed with '0x'. 13 | pub type PrefixedBytesAsHex = BytesAsHex; 14 | 15 | /// A byte array that serializes as a hex string. 16 | /// 17 | /// The `PREFIXED` generic type symbolize whether a string representation of the hex value should be 18 | /// prefixed by `0x` or not. 19 | #[derive(Debug, Eq, PartialEq)] 20 | pub struct BytesAsHex(pub(crate) [u8; N]); 21 | 22 | impl<'de, const N: usize, const PREFIXED: bool> Deserialize<'de> for BytesAsHex { 23 | fn deserialize(deserializer: D) -> Result 24 | where 25 | D: serde::Deserializer<'de>, 26 | { 27 | struct ByteArrayVisitor; 28 | impl<'de, const N: usize, const PREFIXED: bool> Visitor<'de> for ByteArrayVisitor { 29 | type Value = BytesAsHex; 30 | 31 | fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 32 | formatter.write_str("a byte array") 33 | } 34 | 35 | fn visit_seq(self, mut seq: A) -> Result 36 | where 37 | A: serde::de::SeqAccess<'de>, 38 | { 39 | let mut res = [0u8; N]; 40 | let mut i = 0; 41 | while let Some(value) = seq.next_element()? { 42 | res[i] = value; 43 | i += 1; 44 | } 45 | Ok(BytesAsHex(res)) 46 | } 47 | } 48 | 49 | if deserializer.is_human_readable() { 50 | let s = String::deserialize(deserializer)?; 51 | bytes_from_hex_str::(s.as_str()) 52 | .map_err(serde::de::Error::custom) 53 | .map(BytesAsHex) 54 | } else { 55 | deserializer.deserialize_tuple(N, ByteArrayVisitor) 56 | } 57 | } 58 | } 59 | 60 | impl Serialize for BytesAsHex { 61 | fn serialize(&self, serializer: S) -> Result 62 | where 63 | S: serde::Serializer, 64 | { 65 | if serializer.is_human_readable() { 66 | let hex_str = hex_str_from_bytes::(self.0); 67 | serializer.serialize_str(&hex_str) 68 | } else { 69 | let mut seq = serializer.serialize_tuple(N)?; 70 | for element in &self.0[..] { 71 | seq.serialize_element(element)?; 72 | } 73 | seq.end() 74 | } 75 | } 76 | } 77 | 78 | /// The error type returned by the inner deserialization. 79 | // If you need `eq`, add `impl Eq for InnerDeserializationError {}` and read warning below. 80 | // 81 | // For some reason `hex` (now unmaintained for > 3 years) didn't implement `Eq`, even though 82 | // there's no reason not too, so we can't use `derive(Eq)` unfortunately. 83 | // Note that adding the impl is risky, because if at some point `hex` decide to add non-Eq 84 | // things to the error, then combined with the manual `impl Eq` this will create very nasty bugs. 85 | // So, for prudence, we'll hold off on adding `Eq` until we have a good reason to. 86 | // Existing (ignored) issue on this: https://github.com/KokaKiwi/rust-hex/issues/76. 87 | #[derive(thiserror::Error, Clone, Debug, PartialEq)] 88 | pub enum InnerDeserializationError { 89 | /// Error parsing the hex string. 90 | #[error(transparent)] 91 | FromHex(#[from] hex::FromHexError), 92 | /// Missing 0x prefix in the hex string. 93 | #[error("Missing prefix 0x in {hex_str}")] 94 | MissingPrefix { hex_str: String }, 95 | /// Unexpected input byte count. 96 | #[error("Bad input - expected #bytes: {expected_byte_count}, string found: {string_found}.")] 97 | BadInput { expected_byte_count: usize, string_found: String }, 98 | } 99 | 100 | /// Deserializes a Hex decoded as string to a byte array. 101 | pub fn bytes_from_hex_str( 102 | hex_str: &str, 103 | ) -> Result<[u8; N], InnerDeserializationError> { 104 | let hex_str = if PREFIXED { 105 | hex_str 106 | .strip_prefix("0x") 107 | .ok_or(InnerDeserializationError::MissingPrefix { hex_str: hex_str.into() })? 108 | } else { 109 | hex_str 110 | }; 111 | 112 | // Make sure string is not too long. 113 | if hex_str.len() > 2 * N { 114 | let mut err_str = "0x".to_owned(); 115 | err_str.push_str(hex_str); 116 | return Err(InnerDeserializationError::BadInput { 117 | expected_byte_count: N, 118 | string_found: err_str, 119 | }); 120 | } 121 | 122 | // Pad if needed. 123 | let to_add = 2 * N - hex_str.len(); 124 | let padded_str = vec!["0"; to_add].join("") + hex_str; 125 | 126 | Ok(hex::decode(padded_str)?.try_into().expect("Unexpected length of deserialized hex bytes.")) 127 | } 128 | 129 | /// Encodes a byte array to a string. 130 | pub fn hex_str_from_bytes(bytes: [u8; N]) -> String { 131 | let hex_str = hex::encode(bytes); 132 | let mut hex_str = hex_str.trim_start_matches('0'); 133 | hex_str = if hex_str.is_empty() { "0" } else { hex_str }; 134 | if PREFIXED { format!("0x{hex_str}") } else { hex_str.to_string() } 135 | } 136 | 137 | pub fn deserialize_optional_contract_class_abi_entry_vector<'de, D>( 138 | deserializer: D, 139 | ) -> Result>, D::Error> 140 | where 141 | D: Deserializer<'de>, 142 | { 143 | // Deserialize the field as an `Option>` 144 | let result: Result>, _> = Option::deserialize(deserializer); 145 | 146 | // If the field contains junk or an invalid value, return `None`. 147 | match result { 148 | Ok(value) => Ok(value), 149 | Err(_) => Ok(None), 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/serde_utils_test.rs: -------------------------------------------------------------------------------- 1 | use assert_matches::assert_matches; 2 | use serde::Deserialize; 3 | 4 | use crate::deprecated_contract_class::{ 5 | ConstructorType, ContractClassAbiEntry, FunctionAbiEntry, TypedParameter, 6 | }; 7 | use crate::serde_utils::{ 8 | bytes_from_hex_str, deserialize_optional_contract_class_abi_entry_vector, hex_str_from_bytes, 9 | BytesAsHex, InnerDeserializationError, 10 | }; 11 | 12 | #[test] 13 | fn hex_str_from_bytes_scenarios() { 14 | // even length. 15 | assert_eq!(hex_str_from_bytes::<1, true>([106]), "0x6a"); 16 | 17 | // odd length. 18 | assert_eq!(hex_str_from_bytes::<1, true>([6]), "0x6"); 19 | 20 | // Remove padding. 21 | assert_eq!(hex_str_from_bytes::<2, true>([0, 6]), "0x6"); 22 | 23 | // Non-prefixed. 24 | assert_eq!(hex_str_from_bytes::<2, false>([13, 162]), "da2"); 25 | } 26 | 27 | #[test] 28 | fn hex_str_from_bytes_zero() { 29 | // Prefixed. 30 | assert_eq!(hex_str_from_bytes::<3, true>([0, 0, 0]), "0x0"); 31 | 32 | // Non-prefixed. 33 | assert_eq!(hex_str_from_bytes::<2, false>([0, 0]), "0"); 34 | } 35 | 36 | #[test] 37 | fn bytes_from_hex_str_scenarios() { 38 | // even length. 39 | let hex_str = "0x6a"; 40 | let res = bytes_from_hex_str::<1, true>(hex_str).unwrap(); 41 | assert_eq!(res, [106]); 42 | 43 | // odd length. 44 | let hex_str = "0x6"; 45 | let res = bytes_from_hex_str::<1, true>(hex_str).unwrap(); 46 | assert_eq!(res, [6]); 47 | 48 | // No prefix. 49 | let hex_str = "6"; 50 | let res = bytes_from_hex_str::<1, false>(hex_str).unwrap(); 51 | assert_eq!(res, [6]); 52 | } 53 | 54 | #[test] 55 | fn bytes_from_hex_str_padding() { 56 | // even length. 57 | let hex_str = "0xda2b"; 58 | let res = bytes_from_hex_str::<4, true>(hex_str).unwrap(); 59 | assert_eq!(res, [0, 0, 218, 43]); 60 | 61 | // odd length. 62 | let hex_str = "0xda2"; 63 | let res = bytes_from_hex_str::<4, true>(hex_str).unwrap(); 64 | assert_eq!(res, [0, 0, 13, 162]); 65 | } 66 | 67 | #[test] 68 | fn bytes_from_hex_str_errors() { 69 | // Short buffer. 70 | let hex_str = "0xda2b"; 71 | let err = bytes_from_hex_str::<1, true>(hex_str); 72 | assert_matches!(err, Err(InnerDeserializationError::BadInput { expected_byte_count: 1, .. })); 73 | 74 | // Invalid hex char. 75 | let err = bytes_from_hex_str::<1, false>("1z"); 76 | assert_matches!( 77 | err, 78 | Err(InnerDeserializationError::FromHex(hex::FromHexError::InvalidHexCharacter { 79 | c: 'z', 80 | index: 1 81 | })) 82 | ); 83 | 84 | // Missing prefix. 85 | let err = bytes_from_hex_str::<2, true>("11"); 86 | assert_matches!(err, Err(InnerDeserializationError::MissingPrefix { .. })); 87 | 88 | // Unneeded prefix. 89 | let err = bytes_from_hex_str::<2, false>("0x11"); 90 | assert_matches!( 91 | err, 92 | Err(InnerDeserializationError::FromHex(hex::FromHexError::InvalidHexCharacter { 93 | c: 'x', 94 | index: 1 95 | })) 96 | ); 97 | } 98 | 99 | #[test] 100 | fn hex_as_bytes_serde_prefixed() { 101 | let hex_as_bytes = BytesAsHex::<3, true>([1, 2, 3]); 102 | assert_eq!( 103 | hex_as_bytes, 104 | serde_json::from_str(&serde_json::to_string(&hex_as_bytes).unwrap()).unwrap() 105 | ); 106 | } 107 | 108 | #[test] 109 | fn hex_as_bytes_serde_not_prefixed() { 110 | let hex_as_bytes = BytesAsHex::<3, false>([1, 2, 3]); 111 | assert_eq!( 112 | hex_as_bytes, 113 | serde_json::from_str(&serde_json::to_string(&hex_as_bytes).unwrap()).unwrap() 114 | ); 115 | } 116 | 117 | #[derive(Deserialize, PartialEq, Eq, Debug)] 118 | struct DummyContractClass { 119 | #[serde(default, deserialize_with = "deserialize_optional_contract_class_abi_entry_vector")] 120 | pub abi: Option>, 121 | } 122 | 123 | #[test] 124 | fn deserialize_valid_optional_contract_class_abi_entry_vector() { 125 | let json = r#" 126 | { 127 | "abi": 128 | [ 129 | { 130 | "inputs": [ 131 | { 132 | "name": "implementation", 133 | "type": "felt" 134 | } 135 | ], 136 | "name": "constructor", 137 | "outputs": [], 138 | "type": "constructor" 139 | } 140 | ] 141 | } 142 | "#; 143 | let res: DummyContractClass = serde_json::from_str(json).unwrap(); 144 | assert_eq!( 145 | res, 146 | DummyContractClass { 147 | abi: Some(vec![ContractClassAbiEntry::Constructor(FunctionAbiEntry::< 148 | ConstructorType, 149 | > { 150 | name: "constructor".to_string(), 151 | inputs: vec![TypedParameter { 152 | name: "implementation".to_string(), 153 | r#type: "felt".to_string(), 154 | }], 155 | outputs: vec![], 156 | state_mutability: None, 157 | r#type: ConstructorType::Constructor, 158 | })]) 159 | } 160 | ); 161 | } 162 | 163 | #[test] 164 | fn deserialize_optional_contract_class_abi_entry_vector_junk() { 165 | let json = r#" 166 | { 167 | "abi": "Junk" 168 | } 169 | "#; 170 | let res: DummyContractClass = serde_json::from_str(json).unwrap(); 171 | assert_eq!(res, DummyContractClass { abi: None }); 172 | } 173 | 174 | #[test] 175 | fn deserialize_optional_contract_class_abi_entry_vector_none() { 176 | let json = r#" 177 | { 178 | } 179 | "#; 180 | let res: DummyContractClass = serde_json::from_str(json).unwrap(); 181 | assert_eq!(res, DummyContractClass { abi: None }); 182 | } 183 | -------------------------------------------------------------------------------- /src/state.rs: -------------------------------------------------------------------------------- 1 | #[cfg(test)] 2 | #[path = "state_test.rs"] 3 | mod state_test; 4 | 5 | use std::collections::HashMap; 6 | use std::fmt::Debug; 7 | 8 | use indexmap::IndexMap; 9 | use serde::{Deserialize, Serialize}; 10 | use starknet_types_core::felt::Felt; 11 | 12 | use crate::block::{BlockHash, BlockNumber}; 13 | use crate::core::{ 14 | ClassHash, CompiledClassHash, ContractAddress, EntryPointSelector, GlobalRoot, Nonce, 15 | PatriciaKey, 16 | }; 17 | use crate::deprecated_contract_class::ContractClass as DeprecatedContractClass; 18 | use crate::hash::StarkHash; 19 | use crate::{impl_from_through_intermediate, StarknetApiError}; 20 | 21 | pub type DeclaredClasses = IndexMap; 22 | pub type DeprecatedDeclaredClasses = IndexMap; 23 | 24 | /// The differences between two states before and after a block with hash block_hash 25 | /// and their respective roots. 26 | #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] 27 | pub struct StateUpdate { 28 | pub block_hash: BlockHash, 29 | pub new_root: GlobalRoot, 30 | pub old_root: GlobalRoot, 31 | pub state_diff: StateDiff, 32 | } 33 | 34 | /// The differences between two states. 35 | // Invariant: Addresses are strictly increasing. 36 | // Invariant: Class hashes of declared_classes and deprecated_declared_classes are exclusive. 37 | // TODO(yair): Enforce this invariant. 38 | #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] 39 | pub struct StateDiff { 40 | pub deployed_contracts: IndexMap, 41 | pub storage_diffs: IndexMap>, 42 | pub declared_classes: IndexMap, 43 | pub deprecated_declared_classes: IndexMap, 44 | pub nonces: IndexMap, 45 | pub replaced_classes: IndexMap, 46 | } 47 | 48 | // Invariant: Addresses are strictly increasing. 49 | // The invariant is enforced as [`ThinStateDiff`] is created only from [`starknet_api`][`StateDiff`] 50 | // where the addresses are strictly increasing. 51 | #[derive(Debug, Default, Clone, Eq, PartialEq, Deserialize, Serialize)] 52 | pub struct ThinStateDiff { 53 | pub deployed_contracts: IndexMap, 54 | pub storage_diffs: IndexMap>, 55 | pub declared_classes: IndexMap, 56 | pub deprecated_declared_classes: Vec, 57 | pub nonces: IndexMap, 58 | pub replaced_classes: IndexMap, 59 | } 60 | 61 | impl ThinStateDiff { 62 | // Returns also the declared classes without cloning them. 63 | pub fn from_state_diff(diff: StateDiff) -> (Self, DeclaredClasses, DeprecatedDeclaredClasses) { 64 | ( 65 | Self { 66 | deployed_contracts: diff.deployed_contracts, 67 | storage_diffs: diff.storage_diffs, 68 | declared_classes: diff 69 | .declared_classes 70 | .iter() 71 | .map(|(class_hash, (compiled_hash, _class))| (*class_hash, *compiled_hash)) 72 | .collect(), 73 | deprecated_declared_classes: diff 74 | .deprecated_declared_classes 75 | .keys() 76 | .copied() 77 | .collect(), 78 | nonces: diff.nonces, 79 | replaced_classes: diff.replaced_classes, 80 | }, 81 | diff.declared_classes 82 | .into_iter() 83 | .map(|(class_hash, (_compiled_class_hash, class))| (class_hash, class)) 84 | .collect(), 85 | diff.deprecated_declared_classes, 86 | ) 87 | } 88 | 89 | /// This has the same value as `state_diff_length` in the corresponding `BlockHeader`. 90 | pub fn len(&self) -> usize { 91 | let mut result = 0usize; 92 | result += self.deployed_contracts.len(); 93 | result += self.declared_classes.len(); 94 | result += self.deprecated_declared_classes.len(); 95 | result += self.nonces.len(); 96 | result += self.replaced_classes.len(); 97 | 98 | for (_contract_address, storage_diffs) in &self.storage_diffs { 99 | result += storage_diffs.len(); 100 | } 101 | result 102 | } 103 | 104 | pub fn is_empty(&self) -> bool { 105 | self.deployed_contracts.is_empty() 106 | && self.declared_classes.is_empty() 107 | && self.deprecated_declared_classes.is_empty() 108 | && self.nonces.is_empty() 109 | && self.replaced_classes.is_empty() 110 | && self 111 | .storage_diffs 112 | .iter() 113 | .all(|(_contract_address, storage_diffs)| storage_diffs.is_empty()) 114 | } 115 | } 116 | 117 | impl From for ThinStateDiff { 118 | fn from(diff: StateDiff) -> Self { 119 | Self::from_state_diff(diff).0 120 | } 121 | } 122 | 123 | /// The sequential numbering of the states between blocks. 124 | // Example: 125 | // States: S0 S1 S2 126 | // Blocks B0-> B1-> 127 | #[derive( 128 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 129 | )] 130 | pub struct StateNumber(pub BlockNumber); 131 | 132 | impl StateNumber { 133 | /// The state at the beginning of the block. 134 | pub fn right_before_block(block_number: BlockNumber) -> StateNumber { 135 | StateNumber(block_number) 136 | } 137 | 138 | /// The state at the end of the block, or None if it's is out of range. 139 | pub fn right_after_block(block_number: BlockNumber) -> Option { 140 | Some(StateNumber(block_number.next()?)) 141 | } 142 | 143 | /// The state at the end of the block, without checking if it's in range. 144 | pub fn unchecked_right_after_block(block_number: BlockNumber) -> StateNumber { 145 | StateNumber(block_number.unchecked_next()) 146 | } 147 | 148 | pub fn is_before(&self, block_number: BlockNumber) -> bool { 149 | self.0 <= block_number 150 | } 151 | 152 | pub fn is_after(&self, block_number: BlockNumber) -> bool { 153 | !self.is_before(block_number) 154 | } 155 | 156 | pub fn block_after(&self) -> BlockNumber { 157 | self.0 158 | } 159 | } 160 | 161 | /// A storage key in a contract. 162 | #[derive( 163 | Debug, 164 | Default, 165 | Clone, 166 | Copy, 167 | Eq, 168 | PartialEq, 169 | Hash, 170 | Deserialize, 171 | Serialize, 172 | PartialOrd, 173 | Ord, 174 | derive_more::Deref, 175 | )] 176 | pub struct StorageKey(pub PatriciaKey); 177 | 178 | impl From for Felt { 179 | fn from(storage_key: StorageKey) -> Felt { 180 | **storage_key 181 | } 182 | } 183 | 184 | impl TryFrom for StorageKey { 185 | type Error = StarknetApiError; 186 | 187 | fn try_from(val: StarkHash) -> Result { 188 | Ok(Self(PatriciaKey::try_from(val)?)) 189 | } 190 | } 191 | 192 | impl From for StorageKey { 193 | fn from(val: u128) -> Self { 194 | StorageKey(PatriciaKey::from(val)) 195 | } 196 | } 197 | 198 | impl_from_through_intermediate!(u128, StorageKey, u8, u16, u32, u64); 199 | 200 | /// A contract class. 201 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 202 | pub struct ContractClass { 203 | pub sierra_program: Vec, 204 | pub entry_points_by_type: HashMap>, 205 | pub abi: String, 206 | } 207 | 208 | #[derive( 209 | Debug, Default, Clone, Copy, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 210 | )] 211 | #[serde(deny_unknown_fields)] 212 | pub enum EntryPointType { 213 | /// A constructor entry point. 214 | #[serde(rename = "CONSTRUCTOR")] 215 | Constructor, 216 | /// An external entry point. 217 | #[serde(rename = "EXTERNAL")] 218 | #[default] 219 | External, 220 | /// An L1 handler entry point. 221 | #[serde(rename = "L1_HANDLER")] 222 | L1Handler, 223 | } 224 | 225 | /// An entry point of a [ContractClass](`crate::state::ContractClass`). 226 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 227 | pub struct EntryPoint { 228 | pub function_idx: FunctionIndex, 229 | pub selector: EntryPointSelector, 230 | } 231 | 232 | #[derive( 233 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 234 | )] 235 | pub struct FunctionIndex(pub usize); 236 | -------------------------------------------------------------------------------- /src/state_test.rs: -------------------------------------------------------------------------------- 1 | use std::collections::HashMap; 2 | 3 | use indexmap::{indexmap, IndexMap}; 4 | use serde_json::json; 5 | 6 | use super::ThinStateDiff; 7 | use crate::core::{ClassHash, CompiledClassHash, Nonce}; 8 | use crate::deprecated_contract_class::EntryPointOffset; 9 | 10 | #[test] 11 | fn entry_point_offset_from_json_str() { 12 | let data = r#" 13 | { 14 | "offset_1": 2 , 15 | "offset_2": "0x7b" 16 | }"#; 17 | let offsets: HashMap = serde_json::from_str(data).unwrap(); 18 | 19 | assert_eq!(EntryPointOffset(2), offsets["offset_1"]); 20 | assert_eq!(EntryPointOffset(123), offsets["offset_2"]); 21 | } 22 | 23 | #[test] 24 | fn entry_point_offset_into_json_str() { 25 | let offset = EntryPointOffset(123); 26 | assert_eq!(json!(offset), json!(format!("{:#x}", offset.0))); 27 | } 28 | 29 | #[test] 30 | fn thin_state_diff_len() { 31 | let state_diff = ThinStateDiff { 32 | deployed_contracts: indexmap! { 33 | 0u64.into() => ClassHash(4u64.into()), 34 | }, 35 | storage_diffs: indexmap! { 36 | 0u64.into() => indexmap! { 37 | 0u64.into() => 0u64.into(), 38 | 1u64.into() => 1u64.into(), 39 | }, 40 | 1u64.into() => indexmap! { 41 | 0u64.into() => 0u64.into(), 42 | }, 43 | }, 44 | declared_classes: indexmap! { 45 | ClassHash(4u64.into()) => CompiledClassHash(9u64.into()), 46 | ClassHash(5u64.into()) => CompiledClassHash(10u64.into()), 47 | }, 48 | deprecated_declared_classes: vec![ 49 | ClassHash(6u64.into()), 50 | ClassHash(7u64.into()), 51 | ClassHash(8u64.into()), 52 | ], 53 | nonces: indexmap! { 54 | 0u64.into() => Nonce(1u64.into()), 55 | 1u64.into() => Nonce(1u64.into()), 56 | }, 57 | replaced_classes: indexmap! { 58 | 2u64.into() => ClassHash(4u64.into()), 59 | 3u64.into() => ClassHash(5u64.into()), 60 | }, 61 | }; 62 | assert_eq!(state_diff.len(), 13); 63 | } 64 | 65 | #[test] 66 | fn thin_state_diff_is_empty() { 67 | assert!(ThinStateDiff::default().is_empty()); 68 | assert!( 69 | ThinStateDiff { 70 | storage_diffs: indexmap! { Default::default() => IndexMap::new() }, 71 | ..Default::default() 72 | } 73 | .is_empty() 74 | ); 75 | 76 | assert!( 77 | !ThinStateDiff { 78 | deployed_contracts: indexmap! { Default::default() => Default::default() }, 79 | ..Default::default() 80 | } 81 | .is_empty() 82 | ); 83 | assert!( 84 | !ThinStateDiff { 85 | storage_diffs: indexmap! { Default::default() => indexmap! { Default::default() => Default::default() } }, 86 | ..Default::default() 87 | } 88 | .is_empty() 89 | ); 90 | assert!( 91 | !ThinStateDiff { 92 | declared_classes: indexmap! { Default::default() => Default::default() }, 93 | ..Default::default() 94 | } 95 | .is_empty() 96 | ); 97 | assert!( 98 | !ThinStateDiff { 99 | deprecated_declared_classes: vec![Default::default()], 100 | ..Default::default() 101 | } 102 | .is_empty() 103 | ); 104 | assert!( 105 | !ThinStateDiff { 106 | nonces: indexmap! { Default::default() => Default::default() }, 107 | ..Default::default() 108 | } 109 | .is_empty() 110 | ); 111 | assert!( 112 | !ThinStateDiff { 113 | replaced_classes: indexmap! { Default::default() => Default::default() }, 114 | ..Default::default() 115 | } 116 | .is_empty() 117 | ); 118 | } 119 | -------------------------------------------------------------------------------- /src/transaction.rs: -------------------------------------------------------------------------------- 1 | use std::collections::{BTreeMap, HashMap, HashSet}; 2 | use std::fmt::Display; 3 | use std::sync::Arc; 4 | 5 | use derive_more::{Display, From}; 6 | use serde::{Deserialize, Deserializer, Serialize, Serializer}; 7 | use starknet_types_core::felt::Felt; 8 | use strum::IntoEnumIterator; 9 | use strum_macros::EnumIter; 10 | 11 | use crate::block::{BlockHash, BlockNumber}; 12 | use crate::core::{ 13 | ChainId, ClassHash, CompiledClassHash, ContractAddress, EntryPointSelector, EthAddress, Nonce, 14 | }; 15 | use crate::data_availability::DataAvailabilityMode; 16 | use crate::hash::StarkHash; 17 | use crate::serde_utils::PrefixedBytesAsHex; 18 | use crate::transaction_hash::{ 19 | get_declare_transaction_v0_hash, get_declare_transaction_v1_hash, 20 | get_declare_transaction_v2_hash, get_declare_transaction_v3_hash, 21 | get_deploy_account_transaction_v1_hash, get_deploy_account_transaction_v3_hash, 22 | get_deploy_transaction_hash, get_invoke_transaction_v0_hash, get_invoke_transaction_v1_hash, 23 | get_invoke_transaction_v3_hash, get_l1_handler_transaction_hash, 24 | }; 25 | use crate::StarknetApiError; 26 | 27 | pub trait TransactionHasher { 28 | fn calculate_transaction_hash( 29 | &self, 30 | chain_id: &ChainId, 31 | transaction_version: &TransactionVersion, 32 | ) -> Result; 33 | } 34 | 35 | /// A transaction. 36 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 37 | pub enum Transaction { 38 | /// A declare transaction. 39 | Declare(DeclareTransaction), 40 | /// A deploy transaction. 41 | Deploy(DeployTransaction), 42 | /// A deploy account transaction. 43 | DeployAccount(DeployAccountTransaction), 44 | /// An invoke transaction. 45 | Invoke(InvokeTransaction), 46 | /// An L1 handler transaction. 47 | L1Handler(L1HandlerTransaction), 48 | } 49 | 50 | impl Transaction { 51 | pub fn version(&self) -> TransactionVersion { 52 | match self { 53 | Transaction::Declare(tx) => tx.version(), 54 | Transaction::Deploy(tx) => tx.version, 55 | Transaction::DeployAccount(tx) => tx.version(), 56 | Transaction::Invoke(tx) => tx.version(), 57 | Transaction::L1Handler(tx) => tx.version, 58 | } 59 | } 60 | } 61 | 62 | impl TransactionHasher for Transaction { 63 | fn calculate_transaction_hash( 64 | &self, 65 | chain_id: &ChainId, 66 | transaction_version: &TransactionVersion, 67 | ) -> Result { 68 | match self { 69 | Transaction::Declare(tx) => { 70 | tx.calculate_transaction_hash(chain_id, transaction_version) 71 | } 72 | Transaction::Deploy(tx) => tx.calculate_transaction_hash(chain_id, transaction_version), 73 | Transaction::DeployAccount(tx) => { 74 | tx.calculate_transaction_hash(chain_id, transaction_version) 75 | } 76 | Transaction::Invoke(tx) => tx.calculate_transaction_hash(chain_id, transaction_version), 77 | Transaction::L1Handler(tx) => { 78 | tx.calculate_transaction_hash(chain_id, transaction_version) 79 | } 80 | } 81 | } 82 | } 83 | 84 | /// A transaction output. 85 | #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] 86 | pub enum TransactionOutput { 87 | /// A declare transaction output. 88 | Declare(DeclareTransactionOutput), 89 | /// A deploy transaction output. 90 | Deploy(DeployTransactionOutput), 91 | /// A deploy account transaction output. 92 | DeployAccount(DeployAccountTransactionOutput), 93 | /// An invoke transaction output. 94 | Invoke(InvokeTransactionOutput), 95 | /// An L1 handler transaction output. 96 | L1Handler(L1HandlerTransactionOutput), 97 | } 98 | 99 | impl TransactionOutput { 100 | pub fn actual_fee(&self) -> Fee { 101 | match self { 102 | TransactionOutput::Declare(output) => output.actual_fee, 103 | TransactionOutput::Deploy(output) => output.actual_fee, 104 | TransactionOutput::DeployAccount(output) => output.actual_fee, 105 | TransactionOutput::Invoke(output) => output.actual_fee, 106 | TransactionOutput::L1Handler(output) => output.actual_fee, 107 | } 108 | } 109 | 110 | pub fn events(&self) -> &[Event] { 111 | match self { 112 | TransactionOutput::Declare(output) => &output.events, 113 | TransactionOutput::Deploy(output) => &output.events, 114 | TransactionOutput::DeployAccount(output) => &output.events, 115 | TransactionOutput::Invoke(output) => &output.events, 116 | TransactionOutput::L1Handler(output) => &output.events, 117 | } 118 | } 119 | 120 | pub fn execution_status(&self) -> &TransactionExecutionStatus { 121 | match self { 122 | TransactionOutput::Declare(output) => &output.execution_status, 123 | TransactionOutput::Deploy(output) => &output.execution_status, 124 | TransactionOutput::DeployAccount(output) => &output.execution_status, 125 | TransactionOutput::Invoke(output) => &output.execution_status, 126 | TransactionOutput::L1Handler(output) => &output.execution_status, 127 | } 128 | } 129 | 130 | pub fn execution_resources(&self) -> &ExecutionResources { 131 | match self { 132 | TransactionOutput::Declare(output) => &output.execution_resources, 133 | TransactionOutput::Deploy(output) => &output.execution_resources, 134 | TransactionOutput::DeployAccount(output) => &output.execution_resources, 135 | TransactionOutput::Invoke(output) => &output.execution_resources, 136 | TransactionOutput::L1Handler(output) => &output.execution_resources, 137 | } 138 | } 139 | 140 | pub fn messages_sent(&self) -> &Vec { 141 | match self { 142 | TransactionOutput::Declare(output) => &output.messages_sent, 143 | TransactionOutput::Deploy(output) => &output.messages_sent, 144 | TransactionOutput::DeployAccount(output) => &output.messages_sent, 145 | TransactionOutput::Invoke(output) => &output.messages_sent, 146 | TransactionOutput::L1Handler(output) => &output.messages_sent, 147 | } 148 | } 149 | } 150 | 151 | /// A declare V0 or V1 transaction (same schema but different version). 152 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 153 | pub struct DeclareTransactionV0V1 { 154 | pub max_fee: Fee, 155 | pub signature: TransactionSignature, 156 | pub nonce: Nonce, 157 | pub class_hash: ClassHash, 158 | pub sender_address: ContractAddress, 159 | } 160 | 161 | impl TransactionHasher for DeclareTransactionV0V1 { 162 | fn calculate_transaction_hash( 163 | &self, 164 | chain_id: &ChainId, 165 | transaction_version: &TransactionVersion, 166 | ) -> Result { 167 | if *transaction_version == TransactionVersion::ZERO { 168 | return get_declare_transaction_v0_hash(self, chain_id, transaction_version); 169 | } 170 | if *transaction_version == TransactionVersion::ONE { 171 | return get_declare_transaction_v1_hash(self, chain_id, transaction_version); 172 | } 173 | panic!("Illegal transaction version."); 174 | } 175 | } 176 | 177 | /// A declare V2 transaction. 178 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 179 | pub struct DeclareTransactionV2 { 180 | pub max_fee: Fee, 181 | pub signature: TransactionSignature, 182 | pub nonce: Nonce, 183 | pub class_hash: ClassHash, 184 | pub compiled_class_hash: CompiledClassHash, 185 | pub sender_address: ContractAddress, 186 | } 187 | 188 | impl TransactionHasher for DeclareTransactionV2 { 189 | fn calculate_transaction_hash( 190 | &self, 191 | chain_id: &ChainId, 192 | transaction_version: &TransactionVersion, 193 | ) -> Result { 194 | get_declare_transaction_v2_hash(self, chain_id, transaction_version) 195 | } 196 | } 197 | 198 | /// A declare V3 transaction. 199 | #[derive(Clone, Debug, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 200 | pub struct DeclareTransactionV3 { 201 | pub resource_bounds: ResourceBoundsMapping, 202 | pub tip: Tip, 203 | pub signature: TransactionSignature, 204 | pub nonce: Nonce, 205 | pub class_hash: ClassHash, 206 | pub compiled_class_hash: CompiledClassHash, 207 | pub sender_address: ContractAddress, 208 | pub nonce_data_availability_mode: DataAvailabilityMode, 209 | pub fee_data_availability_mode: DataAvailabilityMode, 210 | pub paymaster_data: PaymasterData, 211 | pub account_deployment_data: AccountDeploymentData, 212 | } 213 | 214 | impl TransactionHasher for DeclareTransactionV3 { 215 | fn calculate_transaction_hash( 216 | &self, 217 | chain_id: &ChainId, 218 | transaction_version: &TransactionVersion, 219 | ) -> Result { 220 | get_declare_transaction_v3_hash(self, chain_id, transaction_version) 221 | } 222 | } 223 | 224 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 225 | pub enum DeclareTransaction { 226 | V0(DeclareTransactionV0V1), 227 | V1(DeclareTransactionV0V1), 228 | V2(DeclareTransactionV2), 229 | V3(DeclareTransactionV3), 230 | } 231 | 232 | macro_rules! implement_declare_tx_getters { 233 | ($(($field:ident, $field_type:ty)),*) => { 234 | $(pub fn $field(&self) -> $field_type { 235 | match self { 236 | Self::V0(tx) => tx.$field.clone(), 237 | Self::V1(tx) => tx.$field.clone(), 238 | Self::V2(tx) => tx.$field.clone(), 239 | Self::V3(tx) => tx.$field.clone(), 240 | } 241 | })* 242 | }; 243 | } 244 | 245 | impl DeclareTransaction { 246 | implement_declare_tx_getters!( 247 | (class_hash, ClassHash), 248 | (nonce, Nonce), 249 | (sender_address, ContractAddress), 250 | (signature, TransactionSignature) 251 | ); 252 | 253 | pub fn version(&self) -> TransactionVersion { 254 | match self { 255 | DeclareTransaction::V0(_) => TransactionVersion::ZERO, 256 | DeclareTransaction::V1(_) => TransactionVersion::ONE, 257 | DeclareTransaction::V2(_) => TransactionVersion::TWO, 258 | DeclareTransaction::V3(_) => TransactionVersion::THREE, 259 | } 260 | } 261 | } 262 | 263 | impl TransactionHasher for DeclareTransaction { 264 | fn calculate_transaction_hash( 265 | &self, 266 | chain_id: &ChainId, 267 | transaction_version: &TransactionVersion, 268 | ) -> Result { 269 | match self { 270 | DeclareTransaction::V0(tx) => { 271 | tx.calculate_transaction_hash(chain_id, transaction_version) 272 | } 273 | DeclareTransaction::V1(tx) => { 274 | tx.calculate_transaction_hash(chain_id, transaction_version) 275 | } 276 | DeclareTransaction::V2(tx) => { 277 | tx.calculate_transaction_hash(chain_id, transaction_version) 278 | } 279 | DeclareTransaction::V3(tx) => { 280 | tx.calculate_transaction_hash(chain_id, transaction_version) 281 | } 282 | } 283 | } 284 | } 285 | 286 | /// A deploy account V1 transaction. 287 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 288 | pub struct DeployAccountTransactionV1 { 289 | pub max_fee: Fee, 290 | pub signature: TransactionSignature, 291 | pub nonce: Nonce, 292 | pub class_hash: ClassHash, 293 | pub contract_address_salt: ContractAddressSalt, 294 | pub constructor_calldata: Calldata, 295 | } 296 | 297 | impl TransactionHasher for DeployAccountTransactionV1 { 298 | fn calculate_transaction_hash( 299 | &self, 300 | chain_id: &ChainId, 301 | transaction_version: &TransactionVersion, 302 | ) -> Result { 303 | get_deploy_account_transaction_v1_hash(self, chain_id, transaction_version) 304 | } 305 | } 306 | 307 | /// A deploy account V3 transaction. 308 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 309 | pub struct DeployAccountTransactionV3 { 310 | pub resource_bounds: ResourceBoundsMapping, 311 | pub tip: Tip, 312 | pub signature: TransactionSignature, 313 | pub nonce: Nonce, 314 | pub class_hash: ClassHash, 315 | pub contract_address_salt: ContractAddressSalt, 316 | pub constructor_calldata: Calldata, 317 | pub nonce_data_availability_mode: DataAvailabilityMode, 318 | pub fee_data_availability_mode: DataAvailabilityMode, 319 | pub paymaster_data: PaymasterData, 320 | } 321 | 322 | impl TransactionHasher for DeployAccountTransactionV3 { 323 | fn calculate_transaction_hash( 324 | &self, 325 | chain_id: &ChainId, 326 | transaction_version: &TransactionVersion, 327 | ) -> Result { 328 | get_deploy_account_transaction_v3_hash(self, chain_id, transaction_version) 329 | } 330 | } 331 | 332 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, From)] 333 | pub enum DeployAccountTransaction { 334 | V1(DeployAccountTransactionV1), 335 | V3(DeployAccountTransactionV3), 336 | } 337 | 338 | macro_rules! implement_deploy_account_tx_getters { 339 | ($(($field:ident, $field_type:ty)),*) => { 340 | $( 341 | pub fn $field(&self) -> $field_type { 342 | match self { 343 | Self::V1(tx) => tx.$field.clone(), 344 | Self::V3(tx) => tx.$field.clone(), 345 | } 346 | } 347 | )* 348 | }; 349 | } 350 | 351 | impl DeployAccountTransaction { 352 | implement_deploy_account_tx_getters!( 353 | (class_hash, ClassHash), 354 | (constructor_calldata, Calldata), 355 | (contract_address_salt, ContractAddressSalt), 356 | (nonce, Nonce), 357 | (signature, TransactionSignature) 358 | ); 359 | 360 | pub fn version(&self) -> TransactionVersion { 361 | match self { 362 | DeployAccountTransaction::V1(_) => TransactionVersion::ONE, 363 | DeployAccountTransaction::V3(_) => TransactionVersion::THREE, 364 | } 365 | } 366 | } 367 | 368 | impl TransactionHasher for DeployAccountTransaction { 369 | fn calculate_transaction_hash( 370 | &self, 371 | chain_id: &ChainId, 372 | transaction_version: &TransactionVersion, 373 | ) -> Result { 374 | match self { 375 | DeployAccountTransaction::V1(tx) => { 376 | tx.calculate_transaction_hash(chain_id, transaction_version) 377 | } 378 | DeployAccountTransaction::V3(tx) => { 379 | tx.calculate_transaction_hash(chain_id, transaction_version) 380 | } 381 | } 382 | } 383 | } 384 | 385 | /// A deploy transaction. 386 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 387 | pub struct DeployTransaction { 388 | pub version: TransactionVersion, 389 | pub class_hash: ClassHash, 390 | pub contract_address_salt: ContractAddressSalt, 391 | pub constructor_calldata: Calldata, 392 | } 393 | 394 | impl TransactionHasher for DeployTransaction { 395 | fn calculate_transaction_hash( 396 | &self, 397 | chain_id: &ChainId, 398 | transaction_version: &TransactionVersion, 399 | ) -> Result { 400 | get_deploy_transaction_hash(self, chain_id, transaction_version) 401 | } 402 | } 403 | 404 | /// An invoke V0 transaction. 405 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 406 | pub struct InvokeTransactionV0 { 407 | pub max_fee: Fee, 408 | pub signature: TransactionSignature, 409 | pub contract_address: ContractAddress, 410 | pub entry_point_selector: EntryPointSelector, 411 | pub calldata: Calldata, 412 | } 413 | 414 | impl TransactionHasher for InvokeTransactionV0 { 415 | fn calculate_transaction_hash( 416 | &self, 417 | chain_id: &ChainId, 418 | transaction_version: &TransactionVersion, 419 | ) -> Result { 420 | get_invoke_transaction_v0_hash(self, chain_id, transaction_version) 421 | } 422 | } 423 | 424 | /// An invoke V1 transaction. 425 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 426 | pub struct InvokeTransactionV1 { 427 | pub max_fee: Fee, 428 | pub signature: TransactionSignature, 429 | pub nonce: Nonce, 430 | pub sender_address: ContractAddress, 431 | pub calldata: Calldata, 432 | } 433 | 434 | impl TransactionHasher for InvokeTransactionV1 { 435 | fn calculate_transaction_hash( 436 | &self, 437 | chain_id: &ChainId, 438 | transaction_version: &TransactionVersion, 439 | ) -> Result { 440 | get_invoke_transaction_v1_hash(self, chain_id, transaction_version) 441 | } 442 | } 443 | 444 | /// An invoke V3 transaction. 445 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 446 | pub struct InvokeTransactionV3 { 447 | pub resource_bounds: ResourceBoundsMapping, 448 | pub tip: Tip, 449 | pub signature: TransactionSignature, 450 | pub nonce: Nonce, 451 | pub sender_address: ContractAddress, 452 | pub calldata: Calldata, 453 | pub nonce_data_availability_mode: DataAvailabilityMode, 454 | pub fee_data_availability_mode: DataAvailabilityMode, 455 | pub paymaster_data: PaymasterData, 456 | pub account_deployment_data: AccountDeploymentData, 457 | } 458 | 459 | impl TransactionHasher for InvokeTransactionV3 { 460 | fn calculate_transaction_hash( 461 | &self, 462 | chain_id: &ChainId, 463 | transaction_version: &TransactionVersion, 464 | ) -> Result { 465 | get_invoke_transaction_v3_hash(self, chain_id, transaction_version) 466 | } 467 | } 468 | 469 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, From)] 470 | pub enum InvokeTransaction { 471 | V0(InvokeTransactionV0), 472 | V1(InvokeTransactionV1), 473 | V3(InvokeTransactionV3), 474 | } 475 | 476 | macro_rules! implement_invoke_tx_getters { 477 | ($(($field:ident, $field_type:ty)),*) => { 478 | $(pub fn $field(&self) -> $field_type { 479 | match self { 480 | Self::V0(tx) => tx.$field.clone(), 481 | Self::V1(tx) => tx.$field.clone(), 482 | Self::V3(tx) => tx.$field.clone(), 483 | } 484 | })* 485 | }; 486 | } 487 | 488 | impl InvokeTransaction { 489 | implement_invoke_tx_getters!((calldata, Calldata), (signature, TransactionSignature)); 490 | 491 | pub fn nonce(&self) -> Nonce { 492 | match self { 493 | Self::V0(_) => Nonce::default(), 494 | Self::V1(tx) => tx.nonce, 495 | Self::V3(tx) => tx.nonce, 496 | } 497 | } 498 | 499 | pub fn sender_address(&self) -> ContractAddress { 500 | match self { 501 | Self::V0(tx) => tx.contract_address, 502 | Self::V1(tx) => tx.sender_address, 503 | Self::V3(tx) => tx.sender_address, 504 | } 505 | } 506 | 507 | pub fn version(&self) -> TransactionVersion { 508 | match self { 509 | InvokeTransaction::V0(_) => TransactionVersion::ZERO, 510 | InvokeTransaction::V1(_) => TransactionVersion::ONE, 511 | InvokeTransaction::V3(_) => TransactionVersion::THREE, 512 | } 513 | } 514 | } 515 | 516 | impl TransactionHasher for InvokeTransaction { 517 | fn calculate_transaction_hash( 518 | &self, 519 | chain_id: &ChainId, 520 | transaction_version: &TransactionVersion, 521 | ) -> Result { 522 | match self { 523 | InvokeTransaction::V0(tx) => { 524 | tx.calculate_transaction_hash(chain_id, transaction_version) 525 | } 526 | InvokeTransaction::V1(tx) => { 527 | tx.calculate_transaction_hash(chain_id, transaction_version) 528 | } 529 | InvokeTransaction::V3(tx) => { 530 | tx.calculate_transaction_hash(chain_id, transaction_version) 531 | } 532 | } 533 | } 534 | } 535 | 536 | /// An L1 handler transaction. 537 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 538 | pub struct L1HandlerTransaction { 539 | pub version: TransactionVersion, 540 | pub nonce: Nonce, 541 | pub contract_address: ContractAddress, 542 | pub entry_point_selector: EntryPointSelector, 543 | pub calldata: Calldata, 544 | } 545 | 546 | impl TransactionHasher for L1HandlerTransaction { 547 | fn calculate_transaction_hash( 548 | &self, 549 | chain_id: &ChainId, 550 | transaction_version: &TransactionVersion, 551 | ) -> Result { 552 | get_l1_handler_transaction_hash(self, chain_id, transaction_version) 553 | } 554 | } 555 | 556 | /// A declare transaction output. 557 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 558 | pub struct DeclareTransactionOutput { 559 | pub actual_fee: Fee, 560 | pub messages_sent: Vec, 561 | pub events: Vec, 562 | #[serde(flatten)] 563 | pub execution_status: TransactionExecutionStatus, 564 | pub execution_resources: ExecutionResources, 565 | } 566 | 567 | /// A deploy-account transaction output. 568 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 569 | pub struct DeployAccountTransactionOutput { 570 | pub actual_fee: Fee, 571 | pub messages_sent: Vec, 572 | pub events: Vec, 573 | pub contract_address: ContractAddress, 574 | #[serde(flatten)] 575 | pub execution_status: TransactionExecutionStatus, 576 | pub execution_resources: ExecutionResources, 577 | } 578 | 579 | /// A deploy transaction output. 580 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 581 | pub struct DeployTransactionOutput { 582 | pub actual_fee: Fee, 583 | pub messages_sent: Vec, 584 | pub events: Vec, 585 | pub contract_address: ContractAddress, 586 | #[serde(flatten)] 587 | pub execution_status: TransactionExecutionStatus, 588 | pub execution_resources: ExecutionResources, 589 | } 590 | 591 | /// An invoke transaction output. 592 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 593 | pub struct InvokeTransactionOutput { 594 | pub actual_fee: Fee, 595 | pub messages_sent: Vec, 596 | pub events: Vec, 597 | #[serde(flatten)] 598 | pub execution_status: TransactionExecutionStatus, 599 | pub execution_resources: ExecutionResources, 600 | } 601 | 602 | /// An L1 handler transaction output. 603 | #[derive(Debug, Clone, Default, Eq, PartialEq, Deserialize, Serialize)] 604 | pub struct L1HandlerTransactionOutput { 605 | pub actual_fee: Fee, 606 | pub messages_sent: Vec, 607 | pub events: Vec, 608 | #[serde(flatten)] 609 | pub execution_status: TransactionExecutionStatus, 610 | pub execution_resources: ExecutionResources, 611 | } 612 | 613 | /// A transaction receipt. 614 | #[derive(Debug, Clone, Eq, PartialEq, Deserialize, Serialize)] 615 | pub struct TransactionReceipt { 616 | pub transaction_hash: TransactionHash, 617 | pub block_hash: BlockHash, 618 | pub block_number: BlockNumber, 619 | #[serde(flatten)] 620 | pub output: TransactionOutput, 621 | } 622 | 623 | /// Transaction execution status. 624 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, Default)] 625 | #[serde(tag = "execution_status")] 626 | pub enum TransactionExecutionStatus { 627 | #[serde(rename = "SUCCEEDED")] 628 | #[default] 629 | // Succeeded is the default variant because old versions of Starknet don't have an execution 630 | // status and every transaction is considered succeeded 631 | Succeeded, 632 | #[serde(rename = "REVERTED")] 633 | Reverted(RevertedTransactionExecutionStatus), 634 | } 635 | 636 | /// A reverted transaction execution status. 637 | #[derive(Debug, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 638 | pub struct RevertedTransactionExecutionStatus { 639 | // TODO: Validate it's an ASCII string. 640 | pub revert_reason: String, 641 | } 642 | 643 | /// A fee. 644 | #[derive( 645 | Debug, 646 | Copy, 647 | Clone, 648 | Default, 649 | Display, 650 | Eq, 651 | PartialEq, 652 | Hash, 653 | Deserialize, 654 | Serialize, 655 | PartialOrd, 656 | Ord, 657 | derive_more::Deref, 658 | )] 659 | #[serde(from = "PrefixedBytesAsHex<16_usize>", into = "PrefixedBytesAsHex<16_usize>")] 660 | pub struct Fee(pub u128); 661 | 662 | impl From> for Fee { 663 | fn from(value: PrefixedBytesAsHex<16_usize>) -> Self { 664 | Self(u128::from_be_bytes(value.0)) 665 | } 666 | } 667 | 668 | impl From for PrefixedBytesAsHex<16_usize> { 669 | fn from(fee: Fee) -> Self { 670 | Self(fee.0.to_be_bytes()) 671 | } 672 | } 673 | 674 | impl From for Felt { 675 | fn from(fee: Fee) -> Self { 676 | Self::from(fee.0) 677 | } 678 | } 679 | 680 | /// The hash of a [Transaction](`crate::transaction::Transaction`). 681 | #[derive( 682 | Debug, 683 | Default, 684 | Copy, 685 | Clone, 686 | Eq, 687 | PartialEq, 688 | Hash, 689 | Deserialize, 690 | Serialize, 691 | PartialOrd, 692 | Ord, 693 | derive_more::Deref, 694 | )] 695 | pub struct TransactionHash(pub StarkHash); 696 | 697 | impl Display for TransactionHash { 698 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { 699 | write!(f, "{}", self.0) 700 | } 701 | } 702 | 703 | /// A contract address salt. 704 | #[derive( 705 | Debug, Copy, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 706 | )] 707 | pub struct ContractAddressSalt(pub StarkHash); 708 | 709 | /// A transaction signature. 710 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 711 | pub struct TransactionSignature(pub Vec); 712 | 713 | /// A transaction version. 714 | #[derive( 715 | Debug, 716 | Copy, 717 | Clone, 718 | Default, 719 | Eq, 720 | PartialEq, 721 | Hash, 722 | Deserialize, 723 | Serialize, 724 | PartialOrd, 725 | Ord, 726 | derive_more::Deref, 727 | )] 728 | pub struct TransactionVersion(pub Felt); 729 | 730 | impl TransactionVersion { 731 | /// [TransactionVersion] constant that's equal to 0. 732 | pub const ZERO: Self = { Self(Felt::ZERO) }; 733 | 734 | /// [TransactionVersion] constant that's equal to 1. 735 | pub const ONE: Self = { Self(Felt::ONE) }; 736 | 737 | /// [TransactionVersion] constant that's equal to 2. 738 | pub const TWO: Self = { Self(Felt::TWO) }; 739 | 740 | /// [TransactionVersion] constant that's equal to 3. 741 | pub const THREE: Self = { Self(Felt::THREE) }; 742 | } 743 | 744 | /// The calldata of a transaction. 745 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 746 | pub struct Calldata(pub Arc>); 747 | 748 | #[macro_export] 749 | macro_rules! calldata { 750 | ( $( $x:expr ),* ) => { 751 | Calldata(vec![$($x),*].into()) 752 | }; 753 | } 754 | 755 | /// An L1 to L2 message. 756 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 757 | pub struct MessageToL2 { 758 | pub from_address: EthAddress, 759 | pub payload: L1ToL2Payload, 760 | } 761 | 762 | /// An L2 to L1 message. 763 | #[derive(Debug, Default, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 764 | pub struct MessageToL1 { 765 | pub from_address: ContractAddress, 766 | pub to_address: EthAddress, 767 | pub payload: L2ToL1Payload, 768 | } 769 | 770 | /// The payload of [`MessageToL2`]. 771 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 772 | pub struct L1ToL2Payload(pub Vec); 773 | 774 | /// The payload of [`MessageToL1`]. 775 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 776 | pub struct L2ToL1Payload(pub Vec); 777 | 778 | /// An event. 779 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 780 | pub struct Event { 781 | // TODO: Add a TransactionHash element to this struct, and then remove EventLeafElements. 782 | pub from_address: ContractAddress, 783 | #[serde(flatten)] 784 | pub content: EventContent, 785 | } 786 | 787 | /// An event content. 788 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 789 | pub struct EventContent { 790 | pub keys: Vec, 791 | pub data: EventData, 792 | } 793 | 794 | /// An event key. 795 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 796 | pub struct EventKey(pub Felt); 797 | 798 | /// An event data. 799 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 800 | pub struct EventData(pub Vec); 801 | 802 | /// The index of a transaction in [BlockBody](`crate::block::BlockBody`). 803 | #[derive( 804 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 805 | )] 806 | pub struct TransactionOffsetInBlock(pub usize); 807 | 808 | /// The index of an event in [TransactionOutput](`crate::transaction::TransactionOutput`). 809 | #[derive( 810 | Debug, Default, Copy, Clone, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord, 811 | )] 812 | pub struct EventIndexInTransactionOutput(pub usize); 813 | 814 | /// Transaction fee tip. 815 | #[derive( 816 | Clone, 817 | Copy, 818 | Debug, 819 | Default, 820 | Deserialize, 821 | Eq, 822 | Hash, 823 | Ord, 824 | PartialEq, 825 | PartialOrd, 826 | Serialize, 827 | derive_more::Deref, 828 | )] 829 | #[serde(from = "PrefixedBytesAsHex<8_usize>", into = "PrefixedBytesAsHex<8_usize>")] 830 | pub struct Tip(pub u64); 831 | 832 | impl From> for Tip { 833 | fn from(value: PrefixedBytesAsHex<8_usize>) -> Self { 834 | Self(u64::from_be_bytes(value.0)) 835 | } 836 | } 837 | 838 | impl From for PrefixedBytesAsHex<8_usize> { 839 | fn from(tip: Tip) -> Self { 840 | Self(tip.0.to_be_bytes()) 841 | } 842 | } 843 | 844 | impl From for Felt { 845 | fn from(tip: Tip) -> Self { 846 | Self::from(tip.0) 847 | } 848 | } 849 | 850 | /// Execution resource. 851 | #[derive( 852 | Clone, Copy, Debug, Deserialize, EnumIter, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, 853 | )] 854 | pub enum Resource { 855 | #[serde(rename = "L1_GAS")] 856 | L1Gas, 857 | #[serde(rename = "L2_GAS")] 858 | L2Gas, 859 | } 860 | 861 | /// Fee bounds for an execution resource. 862 | /// TODO(Yael): add types ResourceAmount and ResourcePrice and use them instead of u64 and u128. 863 | #[derive( 864 | Clone, Copy, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize, 865 | )] 866 | pub struct ResourceBounds { 867 | // Specifies the maximum amount of each resource allowed for usage during the execution. 868 | #[serde(serialize_with = "u64_to_hex", deserialize_with = "hex_to_u64")] 869 | pub max_amount: u64, 870 | 871 | // Specifies the maximum price the user is willing to pay for each resource unit. 872 | #[serde(serialize_with = "u128_to_hex", deserialize_with = "hex_to_u128")] 873 | pub max_price_per_unit: u128, 874 | } 875 | 876 | fn u64_to_hex(value: &u64, serializer: S) -> Result 877 | where 878 | S: Serializer, 879 | { 880 | serializer.serialize_str(&format!("0x{:x}", value)) 881 | } 882 | 883 | fn hex_to_u64<'de, D>(deserializer: D) -> Result 884 | where 885 | D: Deserializer<'de>, 886 | { 887 | let s: String = Deserialize::deserialize(deserializer)?; 888 | u64::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) 889 | } 890 | 891 | fn u128_to_hex(value: &u128, serializer: S) -> Result 892 | where 893 | S: Serializer, 894 | { 895 | serializer.serialize_str(&format!("0x{:x}", value)) 896 | } 897 | 898 | fn hex_to_u128<'de, D>(deserializer: D) -> Result 899 | where 900 | D: Deserializer<'de>, 901 | { 902 | let s: String = Deserialize::deserialize(deserializer)?; 903 | u128::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) 904 | } 905 | 906 | /// A mapping from execution resources to their corresponding fee bounds.. 907 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 908 | pub struct ResourceBoundsMapping(pub BTreeMap); 909 | 910 | impl TryFrom> for ResourceBoundsMapping { 911 | type Error = StarknetApiError; 912 | fn try_from( 913 | resource_resource_bounds_pairs: Vec<(Resource, ResourceBounds)>, 914 | ) -> Result { 915 | let n_variants = Resource::iter().count(); 916 | let unique_resources: HashSet = 917 | HashSet::from_iter(resource_resource_bounds_pairs.iter().map(|(k, _)| *k)); 918 | if unique_resources.len() != n_variants 919 | || resource_resource_bounds_pairs.len() != n_variants 920 | { 921 | Err(StarknetApiError::InvalidResourceMappingInitializer(format!( 922 | "{:?}", 923 | resource_resource_bounds_pairs 924 | ))) 925 | } else { 926 | Ok(Self(resource_resource_bounds_pairs.into_iter().collect::>())) 927 | } 928 | } 929 | } 930 | 931 | /// Paymaster-related data. 932 | #[derive(Clone, Debug, Default, Deserialize, Eq, Hash, Ord, PartialEq, PartialOrd, Serialize)] 933 | pub struct PaymasterData(pub Vec); 934 | 935 | /// If nonempty, will contain the required data for deploying and initializing an account contract: 936 | /// its class hash, address salt and constructor calldata. 937 | #[derive(Debug, Clone, Default, Eq, PartialEq, Hash, Deserialize, Serialize, PartialOrd, Ord)] 938 | pub struct AccountDeploymentData(pub Vec); 939 | 940 | #[derive(Debug, Default, Deserialize, Serialize, Clone, Eq, PartialEq)] 941 | pub struct GasVector { 942 | pub l1_gas: u64, 943 | pub l1_data_gas: u64, 944 | } 945 | 946 | /// The execution resources used by a transaction. 947 | #[derive(Debug, Default, Deserialize, Serialize, Clone, Eq, PartialEq)] 948 | pub struct ExecutionResources { 949 | pub steps: u64, 950 | pub builtin_instance_counter: HashMap, 951 | pub memory_holes: u64, 952 | pub da_gas_consumed: GasVector, 953 | pub gas_consumed: GasVector, 954 | } 955 | 956 | #[derive(Clone, Debug, Deserialize, EnumIter, Eq, Hash, PartialEq, Serialize)] 957 | pub enum Builtin { 958 | #[serde(rename = "range_check_builtin_applications")] 959 | RangeCheck, 960 | #[serde(rename = "pedersen_builtin_applications")] 961 | Pedersen, 962 | #[serde(rename = "poseidon_builtin_applications")] 963 | Poseidon, 964 | #[serde(rename = "ec_op_builtin_applications")] 965 | EcOp, 966 | #[serde(rename = "ecdsa_builtin_applications")] 967 | Ecdsa, 968 | #[serde(rename = "bitwise_builtin_applications")] 969 | Bitwise, 970 | #[serde(rename = "keccak_builtin_applications")] 971 | Keccak, 972 | #[serde(rename = "segment_arena_builtin")] 973 | SegmentArena, 974 | #[serde(rename = "add_mod_builtin")] 975 | AddMod, 976 | #[serde(rename = "mul_mod_builtin")] 977 | MulMod, 978 | #[serde(rename = "range_check96_builtin")] 979 | RangeCheck96, 980 | } 981 | 982 | const RANGE_CHACK_BUILTIN_NAME: &str = "range_check"; 983 | const PEDERSEN_BUILTIN_NAME: &str = "pedersen"; 984 | const POSEIDON_BUILTIN_NAME: &str = "poseidon"; 985 | const EC_OP_BUILTIN_NAME: &str = "ec_op"; 986 | const ECDSA_BUILTIN_NAME: &str = "ecdsa"; 987 | const BITWISE_BUILTIN_NAME: &str = "bitwise"; 988 | const KECCAK_BUILTIN_NAME: &str = "keccak"; 989 | const SEGMENT_ARENA_BUILTIN_NAME: &str = "segment_arena"; 990 | const ADD_MOD_BUILTIN_NAME: &str = "add_mod"; 991 | const MUL_MOD_BUILTIN_NAME: &str = "mul_mod"; 992 | const RANGE_CHECK96_BUILTIN_NAME: &str = "range_check96"; 993 | 994 | impl Builtin { 995 | pub fn name(&self) -> &'static str { 996 | match self { 997 | Builtin::RangeCheck => RANGE_CHACK_BUILTIN_NAME, 998 | Builtin::Pedersen => PEDERSEN_BUILTIN_NAME, 999 | Builtin::Poseidon => POSEIDON_BUILTIN_NAME, 1000 | Builtin::EcOp => EC_OP_BUILTIN_NAME, 1001 | Builtin::Ecdsa => ECDSA_BUILTIN_NAME, 1002 | Builtin::Bitwise => BITWISE_BUILTIN_NAME, 1003 | Builtin::Keccak => KECCAK_BUILTIN_NAME, 1004 | Builtin::SegmentArena => SEGMENT_ARENA_BUILTIN_NAME, 1005 | Builtin::AddMod => ADD_MOD_BUILTIN_NAME, 1006 | Builtin::MulMod => MUL_MOD_BUILTIN_NAME, 1007 | Builtin::RangeCheck96 => RANGE_CHECK96_BUILTIN_NAME, 1008 | } 1009 | } 1010 | } 1011 | -------------------------------------------------------------------------------- /src/transaction_hash.rs: -------------------------------------------------------------------------------- 1 | use once_cell::sync::Lazy; 2 | use starknet_types_core::felt::Felt; 3 | 4 | use crate::block::BlockNumber; 5 | use crate::core::{calculate_contract_address, ChainId, ContractAddress}; 6 | use crate::crypto::utils::HashChain; 7 | use crate::data_availability::DataAvailabilityMode; 8 | use crate::transaction::{ 9 | DeclareTransaction, DeclareTransactionV0V1, DeclareTransactionV2, DeclareTransactionV3, 10 | DeployAccountTransaction, DeployAccountTransactionV1, DeployAccountTransactionV3, 11 | DeployTransaction, InvokeTransaction, InvokeTransactionV0, InvokeTransactionV1, 12 | InvokeTransactionV3, L1HandlerTransaction, Resource, ResourceBounds, ResourceBoundsMapping, 13 | Tip, Transaction, TransactionHash, TransactionVersion, 14 | }; 15 | use crate::StarknetApiError; 16 | 17 | type ResourceName = [u8; 7]; 18 | 19 | const DATA_AVAILABILITY_MODE_BITS: usize = 32; 20 | const L1_GAS: &ResourceName = b"\0L1_GAS"; 21 | const L2_GAS: &ResourceName = b"\0L2_GAS"; 22 | 23 | static DECLARE: Lazy = 24 | Lazy::new(|| ascii_as_felt("declare").expect("ascii_as_felt failed for 'declare'")); 25 | static DEPLOY: Lazy = 26 | Lazy::new(|| ascii_as_felt("deploy").expect("ascii_as_felt failed for 'deploy'")); 27 | static DEPLOY_ACCOUNT: Lazy = Lazy::new(|| { 28 | ascii_as_felt("deploy_account").expect("ascii_as_felt failed for 'deploy_account'") 29 | }); 30 | static INVOKE: Lazy = 31 | Lazy::new(|| ascii_as_felt("invoke").expect("ascii_as_felt failed for 'invoke'")); 32 | static L1_HANDLER: Lazy = 33 | Lazy::new(|| ascii_as_felt("l1_handler").expect("ascii_as_felt failed for 'l1_handler'")); 34 | const CONSTRUCTOR_ENTRY_POINT_SELECTOR: Felt = 35 | Felt::from_hex_unchecked("0x28ffe4ff0f226a9107253e17a904099aa4f63a02a5621de0576e5aa71bc5194"); 36 | 37 | /// Calculates hash of a Starknet transaction. 38 | pub fn get_transaction_hash( 39 | transaction: &Transaction, 40 | chain_id: &ChainId, 41 | transaction_version: &TransactionVersion, 42 | ) -> Result { 43 | match transaction { 44 | Transaction::Declare(declare) => match declare { 45 | DeclareTransaction::V0(declare_v0) => { 46 | get_declare_transaction_v0_hash(declare_v0, chain_id, transaction_version) 47 | } 48 | DeclareTransaction::V1(declare_v1) => { 49 | get_declare_transaction_v1_hash(declare_v1, chain_id, transaction_version) 50 | } 51 | DeclareTransaction::V2(declare_v2) => { 52 | get_declare_transaction_v2_hash(declare_v2, chain_id, transaction_version) 53 | } 54 | DeclareTransaction::V3(declare_v3) => { 55 | get_declare_transaction_v3_hash(declare_v3, chain_id, transaction_version) 56 | } 57 | }, 58 | Transaction::Deploy(deploy) => { 59 | get_deploy_transaction_hash(deploy, chain_id, transaction_version) 60 | } 61 | Transaction::DeployAccount(deploy_account) => match deploy_account { 62 | DeployAccountTransaction::V1(deploy_account_v1) => { 63 | get_deploy_account_transaction_v1_hash( 64 | deploy_account_v1, 65 | chain_id, 66 | transaction_version, 67 | ) 68 | } 69 | DeployAccountTransaction::V3(deploy_account_v3) => { 70 | get_deploy_account_transaction_v3_hash( 71 | deploy_account_v3, 72 | chain_id, 73 | transaction_version, 74 | ) 75 | } 76 | }, 77 | Transaction::Invoke(invoke) => match invoke { 78 | InvokeTransaction::V0(invoke_v0) => { 79 | get_invoke_transaction_v0_hash(invoke_v0, chain_id, transaction_version) 80 | } 81 | InvokeTransaction::V1(invoke_v1) => { 82 | get_invoke_transaction_v1_hash(invoke_v1, chain_id, transaction_version) 83 | } 84 | InvokeTransaction::V3(invoke_v3) => { 85 | get_invoke_transaction_v3_hash(invoke_v3, chain_id, transaction_version) 86 | } 87 | }, 88 | Transaction::L1Handler(l1_handler) => { 89 | get_l1_handler_transaction_hash(l1_handler, chain_id, transaction_version) 90 | } 91 | } 92 | } 93 | 94 | // On mainnet, from this block number onwards, there are no deprecated transactions, 95 | // enabling us to validate against a single hash calculation. 96 | const MAINNET_TRANSACTION_HASH_WITH_VERSION: BlockNumber = BlockNumber(1470); 97 | 98 | // Calculates a list of deprecated hashes for a transaction. 99 | fn get_deprecated_transaction_hashes( 100 | chain_id: &ChainId, 101 | block_number: &BlockNumber, 102 | transaction: &Transaction, 103 | transaction_version: &TransactionVersion, 104 | ) -> Result, StarknetApiError> { 105 | Ok(if chain_id == &ChainId::Mainnet && block_number > &MAINNET_TRANSACTION_HASH_WITH_VERSION { 106 | vec![] 107 | } else { 108 | match transaction { 109 | Transaction::Declare(_) => vec![], 110 | Transaction::Deploy(deploy) => { 111 | vec![get_deprecated_deploy_transaction_hash(deploy, chain_id, transaction_version)?] 112 | } 113 | Transaction::DeployAccount(_) => vec![], 114 | Transaction::Invoke(invoke) => match invoke { 115 | InvokeTransaction::V0(invoke_v0) => { 116 | vec![get_deprecated_invoke_transaction_v0_hash( 117 | invoke_v0, 118 | chain_id, 119 | transaction_version, 120 | )?] 121 | } 122 | InvokeTransaction::V1(_) | InvokeTransaction::V3(_) => vec![], 123 | }, 124 | Transaction::L1Handler(l1_handler) => get_deprecated_l1_handler_transaction_hashes( 125 | l1_handler, 126 | chain_id, 127 | transaction_version, 128 | )?, 129 | } 130 | }) 131 | } 132 | 133 | /// Validates the hash of a starknet transaction. 134 | /// For transactions on testnet or those with a low block_number, we validate the 135 | /// transaction hash against all potential historical hash computations. For recent 136 | /// transactions on mainnet, the hash is validated by calculating the precise hash 137 | /// based on the transaction version. 138 | pub fn validate_transaction_hash( 139 | transaction: &Transaction, 140 | block_number: &BlockNumber, 141 | chain_id: &ChainId, 142 | expected_hash: TransactionHash, 143 | transaction_version: &TransactionVersion, 144 | ) -> Result { 145 | let mut possible_hashes = get_deprecated_transaction_hashes( 146 | chain_id, 147 | block_number, 148 | transaction, 149 | transaction_version, 150 | )?; 151 | possible_hashes.push(get_transaction_hash(transaction, chain_id, transaction_version)?); 152 | Ok(possible_hashes.contains(&expected_hash)) 153 | } 154 | 155 | // TODO: should be part of core::Felt 156 | pub(crate) fn ascii_as_felt(ascii_str: &str) -> Result { 157 | Felt::from_hex(hex::encode(ascii_str).as_str()) 158 | .map_err(|_| StarknetApiError::OutOfRange { string: ascii_str.to_string() }) 159 | } 160 | 161 | // An implementation of the SNIP: https://github.com/EvyatarO/SNIPs/blob/snip-8/SNIPS/snip-8.md 162 | fn get_tip_resource_bounds_hash( 163 | resource_bounds_mapping: &ResourceBoundsMapping, 164 | tip: &Tip, 165 | ) -> Result { 166 | let l1_resource_bounds = 167 | resource_bounds_mapping.0.get(&Resource::L1Gas).expect("Missing l1 resource"); 168 | let l1_resource = get_concat_resource(l1_resource_bounds, L1_GAS)?; 169 | 170 | let l2_resource_bounds = 171 | resource_bounds_mapping.0.get(&Resource::L2Gas).expect("Missing l2 resource"); 172 | let l2_resource = get_concat_resource(l2_resource_bounds, L2_GAS)?; 173 | 174 | Ok(HashChain::new() 175 | .chain(&tip.0.into()) 176 | .chain(&l1_resource) 177 | .chain(&l2_resource) 178 | .get_poseidon_hash()) 179 | } 180 | 181 | // Receives resource_bounds and resource_name and returns: 182 | // [0 | resource_name (56 bit) | max_amount (64 bit) | max_price_per_unit (128 bit)]. 183 | // An implementation of the SNIP: https://github.com/EvyatarO/SNIPs/blob/snip-8/SNIPS/snip-8.md. 184 | fn get_concat_resource( 185 | resource_bounds: &ResourceBounds, 186 | resource_name: &ResourceName, 187 | ) -> Result { 188 | let max_amount = resource_bounds.max_amount.to_be_bytes(); 189 | let max_price = resource_bounds.max_price_per_unit.to_be_bytes(); 190 | let concat_bytes = 191 | [[0_u8].as_slice(), resource_name.as_slice(), max_amount.as_slice(), max_price.as_slice()] 192 | .concat(); 193 | Ok(Felt::from_bytes_be(&concat_bytes.try_into().expect("Expect 32 bytes"))) 194 | } 195 | 196 | // Receives nonce_mode and fee_mode and returns: 197 | // [0...0 (192 bit) | nonce_mode (32 bit) | fee_mode (32 bit)]. 198 | // An implementation of the SNIP: https://github.com/EvyatarO/SNIPs/blob/snip-8/SNIPS/snip-8.md. 199 | fn concat_data_availability_mode( 200 | nonce_mode: &DataAvailabilityMode, 201 | fee_mode: &DataAvailabilityMode, 202 | ) -> Felt { 203 | (data_availability_mode_index(fee_mode) 204 | + (data_availability_mode_index(nonce_mode) << DATA_AVAILABILITY_MODE_BITS)) 205 | .into() 206 | } 207 | 208 | fn data_availability_mode_index(mode: &DataAvailabilityMode) -> u64 { 209 | match mode { 210 | DataAvailabilityMode::L1 => 0, 211 | DataAvailabilityMode::L2 => 1, 212 | } 213 | } 214 | 215 | pub(crate) fn get_deploy_transaction_hash( 216 | transaction: &DeployTransaction, 217 | chain_id: &ChainId, 218 | transaction_version: &TransactionVersion, 219 | ) -> Result { 220 | get_common_deploy_transaction_hash(transaction, chain_id, false, transaction_version) 221 | } 222 | 223 | fn get_deprecated_deploy_transaction_hash( 224 | transaction: &DeployTransaction, 225 | chain_id: &ChainId, 226 | transaction_version: &TransactionVersion, 227 | ) -> Result { 228 | get_common_deploy_transaction_hash(transaction, chain_id, true, transaction_version) 229 | } 230 | 231 | fn get_common_deploy_transaction_hash( 232 | transaction: &DeployTransaction, 233 | chain_id: &ChainId, 234 | is_deprecated: bool, 235 | transaction_version: &TransactionVersion, 236 | ) -> Result { 237 | let contract_address = calculate_contract_address( 238 | transaction.contract_address_salt, 239 | transaction.class_hash, 240 | &transaction.constructor_calldata, 241 | ContractAddress::from(0_u8), 242 | )?; 243 | 244 | Ok(TransactionHash( 245 | HashChain::new() 246 | .chain(&DEPLOY) 247 | .chain_if_fn(|| { 248 | if !is_deprecated { 249 | Some(transaction_version.0) 250 | } else { 251 | None 252 | } 253 | }) 254 | .chain(contract_address.0.key()) 255 | .chain(&CONSTRUCTOR_ENTRY_POINT_SELECTOR) 256 | .chain( 257 | &HashChain::new() 258 | .chain_iter(transaction.constructor_calldata.0.iter()) 259 | .get_pedersen_hash(), 260 | ) 261 | // No fee in deploy transaction. 262 | .chain_if_fn(|| { 263 | if !is_deprecated { 264 | Some(Felt::ZERO) 265 | } else { 266 | None 267 | } 268 | }) 269 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 270 | .get_pedersen_hash(), 271 | )) 272 | } 273 | 274 | pub(crate) fn get_invoke_transaction_v0_hash( 275 | transaction: &InvokeTransactionV0, 276 | chain_id: &ChainId, 277 | transaction_version: &TransactionVersion, 278 | ) -> Result { 279 | get_common_invoke_transaction_v0_hash(transaction, chain_id, false, transaction_version) 280 | } 281 | 282 | fn get_deprecated_invoke_transaction_v0_hash( 283 | transaction: &InvokeTransactionV0, 284 | chain_id: &ChainId, 285 | transaction_version: &TransactionVersion, 286 | ) -> Result { 287 | get_common_invoke_transaction_v0_hash(transaction, chain_id, true, transaction_version) 288 | } 289 | 290 | fn get_common_invoke_transaction_v0_hash( 291 | transaction: &InvokeTransactionV0, 292 | chain_id: &ChainId, 293 | is_deprecated: bool, 294 | transaction_version: &TransactionVersion, 295 | ) -> Result { 296 | Ok(TransactionHash( 297 | HashChain::new() 298 | .chain(&INVOKE) 299 | .chain_if_fn(|| if !is_deprecated { Some(transaction_version.0) } else { None }) 300 | .chain(transaction.contract_address.0.key()) 301 | .chain(&transaction.entry_point_selector.0) 302 | .chain(&HashChain::new().chain_iter(transaction.calldata.0.iter()).get_pedersen_hash()) 303 | .chain_if_fn(|| if !is_deprecated { Some(transaction.max_fee.0.into()) } else { None }) 304 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 305 | .get_pedersen_hash(), 306 | )) 307 | } 308 | 309 | pub(crate) fn get_invoke_transaction_v1_hash( 310 | transaction: &InvokeTransactionV1, 311 | chain_id: &ChainId, 312 | transaction_version: &TransactionVersion, 313 | ) -> Result { 314 | Ok(TransactionHash( 315 | HashChain::new() 316 | .chain(&INVOKE) 317 | .chain(&transaction_version.0) 318 | .chain(transaction.sender_address.0.key()) 319 | .chain(&Felt::ZERO) // No entry point selector in invoke transaction. 320 | .chain(&HashChain::new().chain_iter(transaction.calldata.0.iter()).get_pedersen_hash()) 321 | .chain(&transaction.max_fee.0.into()) 322 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 323 | .chain(&transaction.nonce.0) 324 | .get_pedersen_hash(), 325 | )) 326 | } 327 | 328 | pub(crate) fn get_invoke_transaction_v3_hash( 329 | transaction: &InvokeTransactionV3, 330 | chain_id: &ChainId, 331 | transaction_version: &TransactionVersion, 332 | ) -> Result { 333 | let tip_resource_bounds_hash = 334 | get_tip_resource_bounds_hash(&transaction.resource_bounds, &transaction.tip)?; 335 | let paymaster_data_hash = 336 | HashChain::new().chain_iter(transaction.paymaster_data.0.iter()).get_poseidon_hash(); 337 | let data_availability_mode = concat_data_availability_mode( 338 | &transaction.nonce_data_availability_mode, 339 | &transaction.fee_data_availability_mode, 340 | ); 341 | let account_deployment_data_hash = HashChain::new() 342 | .chain_iter(transaction.account_deployment_data.0.iter()) 343 | .get_poseidon_hash(); 344 | let calldata_hash = 345 | HashChain::new().chain_iter(transaction.calldata.0.iter()).get_poseidon_hash(); 346 | 347 | Ok(TransactionHash( 348 | HashChain::new() 349 | .chain(&INVOKE) 350 | .chain(&transaction_version.0) 351 | .chain(transaction.sender_address.0.key()) 352 | .chain(&tip_resource_bounds_hash) 353 | .chain(&paymaster_data_hash) 354 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 355 | .chain(&transaction.nonce.0) 356 | .chain(&data_availability_mode) 357 | .chain(&account_deployment_data_hash) 358 | .chain(&calldata_hash) 359 | .get_poseidon_hash(), 360 | )) 361 | } 362 | 363 | #[derive(PartialEq, PartialOrd)] 364 | enum L1HandlerVersions { 365 | AsInvoke, 366 | V0Deprecated, 367 | V0, 368 | } 369 | 370 | pub(crate) fn get_l1_handler_transaction_hash( 371 | transaction: &L1HandlerTransaction, 372 | chain_id: &ChainId, 373 | transaction_version: &TransactionVersion, 374 | ) -> Result { 375 | get_common_l1_handler_transaction_hash( 376 | transaction, 377 | chain_id, 378 | L1HandlerVersions::V0, 379 | transaction_version, 380 | ) 381 | } 382 | 383 | fn get_deprecated_l1_handler_transaction_hashes( 384 | transaction: &L1HandlerTransaction, 385 | chain_id: &ChainId, 386 | transaction_version: &TransactionVersion, 387 | ) -> Result, StarknetApiError> { 388 | Ok(vec![ 389 | get_common_l1_handler_transaction_hash( 390 | transaction, 391 | chain_id, 392 | L1HandlerVersions::AsInvoke, 393 | transaction_version, 394 | )?, 395 | get_common_l1_handler_transaction_hash( 396 | transaction, 397 | chain_id, 398 | L1HandlerVersions::V0Deprecated, 399 | transaction_version, 400 | )?, 401 | ]) 402 | } 403 | 404 | fn get_common_l1_handler_transaction_hash( 405 | transaction: &L1HandlerTransaction, 406 | chain_id: &ChainId, 407 | version: L1HandlerVersions, 408 | transaction_version: &TransactionVersion, 409 | ) -> Result { 410 | Ok(TransactionHash( 411 | HashChain::new() 412 | .chain_if_fn(|| { 413 | if version == L1HandlerVersions::AsInvoke { 414 | Some(*INVOKE) 415 | } else { 416 | Some(*L1_HANDLER) 417 | } 418 | }) 419 | .chain_if_fn(|| { 420 | if version > L1HandlerVersions::V0Deprecated { 421 | Some(transaction_version.0) 422 | } else { 423 | None 424 | } 425 | }) 426 | .chain(transaction.contract_address.0.key()) 427 | .chain(&transaction.entry_point_selector.0) 428 | .chain(&HashChain::new().chain_iter(transaction.calldata.0.iter()).get_pedersen_hash()) 429 | // No fee in l1 handler transaction. 430 | .chain_if_fn(|| { 431 | if version > L1HandlerVersions::V0Deprecated { 432 | Some(Felt::ZERO) 433 | } else { 434 | None 435 | } 436 | }) 437 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 438 | .chain_if_fn(|| { 439 | if version > L1HandlerVersions::AsInvoke { 440 | Some(transaction.nonce.0) 441 | } else { 442 | None 443 | } 444 | }) 445 | .get_pedersen_hash(), 446 | )) 447 | } 448 | 449 | pub(crate) fn get_declare_transaction_v0_hash( 450 | transaction: &DeclareTransactionV0V1, 451 | chain_id: &ChainId, 452 | transaction_version: &TransactionVersion, 453 | ) -> Result { 454 | Ok(TransactionHash( 455 | HashChain::new() 456 | .chain(&DECLARE) 457 | .chain(&transaction_version.0) 458 | .chain(transaction.sender_address.0.key()) 459 | .chain(&Felt::ZERO) // No entry point selector in declare transaction. 460 | .chain(&HashChain::new().get_pedersen_hash()) 461 | .chain(&transaction.max_fee.0.into()) 462 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 463 | .chain(&transaction.class_hash.0) 464 | .get_pedersen_hash(), 465 | )) 466 | } 467 | 468 | pub(crate) fn get_declare_transaction_v1_hash( 469 | transaction: &DeclareTransactionV0V1, 470 | chain_id: &ChainId, 471 | transaction_version: &TransactionVersion, 472 | ) -> Result { 473 | Ok(TransactionHash( 474 | HashChain::new() 475 | .chain(&DECLARE) 476 | .chain(&transaction_version.0) 477 | .chain(transaction.sender_address.0.key()) 478 | .chain(&Felt::ZERO) // No entry point selector in declare transaction. 479 | .chain(&HashChain::new().chain(&transaction.class_hash.0).get_pedersen_hash()) 480 | .chain(&transaction.max_fee.0.into()) 481 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 482 | .chain(&transaction.nonce.0) 483 | .get_pedersen_hash(), 484 | )) 485 | } 486 | 487 | pub(crate) fn get_declare_transaction_v2_hash( 488 | transaction: &DeclareTransactionV2, 489 | chain_id: &ChainId, 490 | transaction_version: &TransactionVersion, 491 | ) -> Result { 492 | Ok(TransactionHash( 493 | HashChain::new() 494 | .chain(&DECLARE) 495 | .chain(&transaction_version.0) 496 | .chain(transaction.sender_address.0.key()) 497 | .chain(&Felt::ZERO) // No entry point selector in declare transaction. 498 | .chain(&HashChain::new().chain(&transaction.class_hash.0).get_pedersen_hash()) 499 | .chain(&transaction.max_fee.0.into()) 500 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 501 | .chain(&transaction.nonce.0) 502 | .chain(&transaction.compiled_class_hash.0) 503 | .get_pedersen_hash(), 504 | )) 505 | } 506 | 507 | pub(crate) fn get_declare_transaction_v3_hash( 508 | transaction: &DeclareTransactionV3, 509 | chain_id: &ChainId, 510 | transaction_version: &TransactionVersion, 511 | ) -> Result { 512 | let tip_resource_bounds_hash = 513 | get_tip_resource_bounds_hash(&transaction.resource_bounds, &transaction.tip)?; 514 | let paymaster_data_hash = 515 | HashChain::new().chain_iter(transaction.paymaster_data.0.iter()).get_poseidon_hash(); 516 | let data_availability_mode = concat_data_availability_mode( 517 | &transaction.nonce_data_availability_mode, 518 | &transaction.fee_data_availability_mode, 519 | ); 520 | let account_deployment_data_hash = HashChain::new() 521 | .chain_iter(transaction.account_deployment_data.0.iter()) 522 | .get_poseidon_hash(); 523 | 524 | Ok(TransactionHash( 525 | HashChain::new() 526 | .chain(&DECLARE) 527 | .chain(&transaction_version.0) 528 | .chain(transaction.sender_address.0.key()) 529 | .chain(&tip_resource_bounds_hash) 530 | .chain(&paymaster_data_hash) 531 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 532 | .chain(&transaction.nonce.0) 533 | .chain(&data_availability_mode) 534 | .chain(&account_deployment_data_hash) 535 | .chain(&transaction.class_hash.0) 536 | .chain(&transaction.compiled_class_hash.0) 537 | .get_poseidon_hash(), 538 | )) 539 | } 540 | 541 | pub(crate) fn get_deploy_account_transaction_v1_hash( 542 | transaction: &DeployAccountTransactionV1, 543 | chain_id: &ChainId, 544 | transaction_version: &TransactionVersion, 545 | ) -> Result { 546 | let calldata_hash = HashChain::new() 547 | .chain(&transaction.class_hash.0) 548 | .chain(&transaction.contract_address_salt.0) 549 | .chain_iter(transaction.constructor_calldata.0.iter()) 550 | .get_pedersen_hash(); 551 | 552 | let contract_address = calculate_contract_address( 553 | transaction.contract_address_salt, 554 | transaction.class_hash, 555 | &transaction.constructor_calldata, 556 | ContractAddress::from(0_u8), 557 | )?; 558 | 559 | Ok(TransactionHash( 560 | HashChain::new() 561 | .chain(&DEPLOY_ACCOUNT) 562 | .chain(&transaction_version.0) 563 | .chain(contract_address.0.key()) 564 | .chain(&Felt::ZERO) // No entry point selector in deploy account transaction. 565 | .chain(&calldata_hash) 566 | .chain(&transaction.max_fee.0.into()) 567 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 568 | .chain(&transaction.nonce.0) 569 | .get_pedersen_hash(), 570 | )) 571 | } 572 | 573 | pub(crate) fn get_deploy_account_transaction_v3_hash( 574 | transaction: &DeployAccountTransactionV3, 575 | chain_id: &ChainId, 576 | transaction_version: &TransactionVersion, 577 | ) -> Result { 578 | let contract_address = calculate_contract_address( 579 | transaction.contract_address_salt, 580 | transaction.class_hash, 581 | &transaction.constructor_calldata, 582 | ContractAddress::from(0_u8), 583 | )?; 584 | let tip_resource_bounds_hash = 585 | get_tip_resource_bounds_hash(&transaction.resource_bounds, &transaction.tip)?; 586 | let paymaster_data_hash = 587 | HashChain::new().chain_iter(transaction.paymaster_data.0.iter()).get_poseidon_hash(); 588 | let data_availability_mode = concat_data_availability_mode( 589 | &transaction.nonce_data_availability_mode, 590 | &transaction.fee_data_availability_mode, 591 | ); 592 | let constructor_calldata_hash = 593 | HashChain::new().chain_iter(transaction.constructor_calldata.0.iter()).get_poseidon_hash(); 594 | 595 | Ok(TransactionHash( 596 | HashChain::new() 597 | .chain(&DEPLOY_ACCOUNT) 598 | .chain(&transaction_version.0) 599 | .chain(contract_address.0.key()) 600 | .chain(&tip_resource_bounds_hash) 601 | .chain(&paymaster_data_hash) 602 | .chain(&ascii_as_felt(chain_id.to_string().as_str())?) 603 | .chain(&data_availability_mode) 604 | .chain(&transaction.nonce.0) 605 | .chain(&constructor_calldata_hash) 606 | .chain(&transaction.class_hash.0) 607 | .chain(&transaction.contract_address_salt.0) 608 | .get_poseidon_hash(), 609 | )) 610 | } 611 | -------------------------------------------------------------------------------- /src/type_utils.rs: -------------------------------------------------------------------------------- 1 | /// Implements `From for top_type` for all bottom_types. Assumes: 2 | /// - `From for top_type` is implemented. 3 | /// - `From for intermediate_type` is implemented, for all bottom_types. 4 | #[macro_export] 5 | macro_rules! impl_from_through_intermediate { 6 | ($intermediate_type: ty, $top_type:ty, $($bottom_type:ty),+) => { 7 | $( 8 | impl From<$bottom_type> for $top_type { 9 | fn from(x: $bottom_type) -> Self { 10 | Self::from(<$intermediate_type>::from(x)) 11 | } 12 | } 13 | )+ 14 | }; 15 | } 16 | --------------------------------------------------------------------------------