From b18e4e1f0ffc2a98ad86bdc661629f09c3ec7ebd Mon Sep 17 00:00:00 2001 From: ron-starkware Date: Wed, 15 Jul 2026 11:11:09 +0300 Subject: [PATCH] starknet_os: support DeployAccount v4 Blake2 address derivation in the OS --- .../starknet/core/os/constants.cairo | 4 +- .../contract_address/contract_address.cairo | 120 +++++++++++++++++- .../execution/execute_transaction_utils.cairo | 6 +- .../core/os/execution/transaction_impls.cairo | 88 ++++++++++--- .../transaction_hash/transaction_hash.cairo | 5 +- .../src/program_hash.json | 4 +- .../src/virtual_os_test.rs | 4 +- ...blockifier_versioned_constants_0_14_4.json | 10 +- .../0.14.3_0.14.4.txt | 9 +- .../src/transaction/execution_flavors_test.rs | 4 +- .../resources/blob_file_generation | 2 +- .../resources/preconfirmed_block.json | 2 +- crates/starknet_api/src/core.rs | 50 +++++++- .../starknet_os/src/hints/enum_definition.rs | 2 + .../blake2s/blake2s_test.rs | 88 +++++++++++++ .../execution/implementation.rs | 43 +++++-- crates/starknet_os/src/hints/vars.rs | 5 + crates/starknet_os/src/test_utils/coverage.rs | 1 + 18 files changed, 392 insertions(+), 55 deletions(-) diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/constants.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/constants.cairo index 96835943c9d..67083a938e0 100644 --- a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/constants.cairo +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/constants.cairo @@ -66,10 +66,10 @@ const STORED_BLOCK_HASH_BUFFER = 10; // Allowed virtual OS program hashes for client-side proving. const ALLOWED_VIRTUAL_OS_PROGRAM_HASHES_0 = ( - 0x053f6c9fcfd31d27279ff7d7e422b44623550a732b59fe193354a7316a96daa1 + 0x01c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324 ); const ALLOWED_VIRTUAL_OS_PROGRAM_HASHES_1 = ( - 0x01c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324 + 0x00fedbf2b6504b9217026ba2bf9b2a33e8e28ed38e5b0f84eef59075b02174b8 ); const ALLOWED_VIRTUAL_OS_PROGRAM_HASHES_LEN = 2; diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/contract_address/contract_address.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/contract_address/contract_address.cairo index b9ffa884a8d..cc6344839d2 100644 --- a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/contract_address/contract_address.cairo +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/contract_address/contract_address.cairo @@ -1,11 +1,23 @@ from starkware.cairo.common.cairo_builtins import HashBuiltin +from starkware.cairo.common.ec import StarkCurve from starkware.cairo.common.hash_state import ( hash_finalize, hash_init, hash_update_single, hash_update_with_hashchain, ) -from starkware.starknet.common.storage import normalize_address +from starkware.cairo.common.math import assert_le_felt +from starkware.cairo.common.math_cmp import is_le_felt +from starkware.starknet.common.storage import ADDR_BOUND, normalize_address +from starkware.starknet.core.os.hash.hash_state_blake import HashState as BlakeHashState +from starkware.starknet.core.os.hash.hash_state_blake import hash_finalize as hash_finalize_blake +from starkware.starknet.core.os.hash.hash_state_blake import hash_init as hash_init_blake +from starkware.starknet.core.os.hash.hash_state_blake import ( + hash_update_single as hash_update_single_blake, +) +from starkware.starknet.core.os.hash.hash_state_blake import ( + hash_update_with_nested_hash as hash_update_with_nested_hash_blake, +) const CONTRACT_ADDRESS_PREFIX = 'STARKNET_CONTRACT_ADDRESS'; @@ -33,3 +45,109 @@ func get_contract_address{hash_ptr: HashBuiltin*, range_check_ptr}( return (contract_address=contract_address); } + +// Same as `get_contract_address`, but uses Blake2s (the optimized +// `encode_felt252_data_and_calc_blake_hash` encoding) instead of Pedersen. +func get_contract_address_blake{range_check_ptr}( + salt: felt, + class_hash: felt, + constructor_calldata_size: felt, + constructor_calldata: felt*, + deployer_address: felt, +) -> (contract_address: felt) { + let hash_state: BlakeHashState = hash_init_blake(); + with hash_state { + hash_update_single_blake(item=CONTRACT_ADDRESS_PREFIX); + hash_update_single_blake(item=deployer_address); + hash_update_single_blake(item=salt); + hash_update_single_blake(item=class_hash); + hash_update_with_nested_hash_blake( + data_ptr=constructor_calldata, data_length=constructor_calldata_size + ); + } + let contract_address_before_modulo: felt = hash_finalize_blake(hash_state=hash_state); + let (contract_address) = normalize_address(addr=contract_address_before_modulo); + + return (contract_address=contract_address); +} + +// A fixed quadratic non-residue in the STARK field: `t = NON_RESIDUE_WITNESS_FACTOR * s * s` +// proves `t` is a non-residue. +const NON_RESIDUE_WITNESS_FACTOR = 3; +// Addresses below this bound (the field prime minus ADDR_BOUND) have a second lift into the +// field: both `address` and `address + ADDR_BOUND` are below the prime. +const ADDRESS_SECOND_LIFT_BOUND = 0x11000000000000000000000000000000000000000000000101; + +// Returns `x^3 + ALPHA * x + BETA` — a square iff `x` is a STARK-curve x-coordinate, i.e. iff +// some Pedersen hash output equals `x`. +func curve_cubic(x: felt) -> felt { + return x * x * x + StarkCurve.ALPHA * x + StarkCurve.BETA; +} + +// Increments `candidate` (expected ~1 step) until no lift of it is a STARK-curve x-coordinate, +// so no Pedersen derivation can produce the returned address and funded-but-undeployed Blake +// addresses cannot be front-run through the Pedersen deploy paths. +// +// Each skipped candidate is proven reachable with an on-curve square-root witness; the returned +// candidate is proven unreachable with non-residue witnesses (`witness^2 = t / 3`, sound since 3 +// is a non-residue). The hint only supplies witnesses — the escape logic itself is fully +// verified. +// +// Note: the Rust derivation also wraps around ADDR_BOUND and skips addresses < 2. Both cases +// require a Blake output within ~2^-240 of the bound edges, which is cryptographically +// unreachable; hitting one would make the transaction unprovable rather than diverge. +func escape_pedersen_image{range_check_ptr}(candidate: felt) -> (contract_address: felt) { + alloc_locals; + local is_reachable; + local reachable_lift; + local witness; + local second_witness; + %{ EscapePedersenImageWitness %} + assert is_reachable * (is_reachable - 1) = 0; + assert reachable_lift * (reachable_lift - 1) = 0; + + if (is_reachable != 0) { + // The candidate is in the Pedersen image: verify with an on-curve square-root witness + // for one of its lifts, then move to the next candidate. + if (reachable_lift != 0) { + // A second lift exists only below ADDRESS_SECOND_LIFT_BOUND. + assert_le_felt(candidate, ADDRESS_SECOND_LIFT_BOUND - 1); + let t_lift = curve_cubic(x=candidate + ADDR_BOUND); + assert witness * witness = t_lift; + return escape_pedersen_image(candidate=candidate + 1); + } + let t = curve_cubic(x=candidate); + assert witness * witness = t; + return escape_pedersen_image(candidate=candidate + 1); + } + + // The candidate escapes the Pedersen image: every lift of it is off-curve. + let t = curve_cubic(x=candidate); + assert witness * witness = t / NON_RESIDUE_WITNESS_FACTOR; + let candidate_has_second_lift = is_le_felt(candidate, ADDRESS_SECOND_LIFT_BOUND - 1); + if (candidate_has_second_lift != 0) { + let t_lift = curve_cubic(x=candidate + ADDR_BOUND); + assert second_witness * second_witness = t_lift / NON_RESIDUE_WITNESS_FACTOR; + return (contract_address=candidate); + } + return (contract_address=candidate); +} + +// The deploy-account v4 / deploy_v2 contract address derivation: the Blake2s hash of the +// deployment arguments, escaped out of the Pedersen image. +func get_contract_address_blake_escaped{range_check_ptr}( + salt: felt, + class_hash: felt, + constructor_calldata_size: felt, + constructor_calldata: felt*, + deployer_address: felt, +) -> (contract_address: felt) { + let (raw_address) = get_contract_address_blake( + salt=salt, + class_hash=class_hash, + constructor_calldata_size=constructor_calldata_size, + constructor_calldata=constructor_calldata, + deployer_address=deployer_address, + ); + return escape_pedersen_image(candidate=raw_address); +} diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/execute_transaction_utils.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/execute_transaction_utils.cairo index 2a67b8cea97..b1f562cf203 100644 --- a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/execute_transaction_utils.cairo +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/execute_transaction_utils.cairo @@ -35,7 +35,7 @@ func fill_deprecated_tx_info(tx_info: TxInfo*, dst: DeprecatedTxInfo*) { // Verifies that the given (non-deprecated) `TxInfo` object is consistent with its version, in the // sense that deprecated transactions (version < 3) have all new fields set to zero and -// non-deprecated transactions (version = 3) have old fields set to zero. +// non-deprecated transactions (version >= 3) have old fields set to zero. func assert_deprecated_tx_fields_consistency(tx_info: TxInfo*) { tempvar version = tx_info.version; if (version * (version - 1) * (version - 2) == 0) { @@ -50,8 +50,10 @@ func assert_deprecated_tx_fields_consistency(tx_info: TxInfo*) { assert tx_info.account_deployment_data_start = nullptr; assert tx_info.account_deployment_data_end = nullptr; } else { + // Version 4 exists only for deploy-account transactions (Blake2 address derivation); + // the version itself is validated by the per-type transaction-hash functions. with_attr error_message("Invalid transaction version: {version}.") { - assert version = 3; + assert (version - 3) * (version - 4) = 0; } assert tx_info.max_fee = 0; } diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/transaction_impls.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/transaction_impls.cairo index b89cdd2abde..102d72965de 100644 --- a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/transaction_impls.cairo +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/execution/transaction_impls.cairo @@ -34,7 +34,10 @@ from starkware.starknet.core.os.constants import ( VALIDATE_MAX_SIERRA_GAS, VALIDATED, ) -from starkware.starknet.core.os.contract_address.contract_address import get_contract_address +from starkware.starknet.core.os.contract_address.contract_address import ( + get_contract_address, + get_contract_address_blake_escaped, +) from starkware.starknet.core.os.contract_class.contract_class import ( ContractClassComponentHashes, finalize_class_hash, @@ -88,8 +91,9 @@ func compute_max_possible_fee(tx_info: TxInfo*) -> felt { tempvar resource_bounds: ResourceBounds* = tx_info.resource_bounds_start; let n_resource_bounds = (tx_info.resource_bounds_end - resource_bounds) / ResourceBounds.SIZE; - // Only V3 transactions with all resource bounds are supported. - assert tx_info.version = 3; + // Only V3-shaped transactions (V3, and deploy-account V4) with all resource bounds are + // supported. + assert (tx_info.version - 3) * (tx_info.version - 4) = 0; assert n_resource_bounds = 3; tempvar l1_gas_bounds: ResourceBounds = resource_bounds[L1_GAS_INDEX]; @@ -168,7 +172,7 @@ func charge_fee{ // // The account transaction should be passed in the hint variable 'tx'. func get_account_tx_common_fields( - block_context: BlockContext*, tx_hash_prefix: felt, sender_address: felt + block_context: BlockContext*, tx_hash_prefix: felt, sender_address: felt, version: felt ) -> CommonTxFields* { alloc_locals; local resource_bounds: ResourceBounds*; @@ -182,7 +186,7 @@ func get_account_tx_common_fields( %{ LoadTxNonceAccount %} tempvar common_tx_fields = new CommonTxFields( tx_hash_prefix=tx_hash_prefix, - version=3, + version=version, sender_address=sender_address, chain_id=block_context.os_global_context.starknet_os_config.chain_id, nonce=nonce, @@ -272,6 +276,7 @@ func execute_invoke_function_transaction{ block_context=block_context, tx_hash_prefix=INVOKE_HASH_PREFIX, sender_address=sender_address, + version=3, ); local account_deployment_data_size; local account_deployment_data: felt*; @@ -518,32 +523,69 @@ func consume_l1_to_l2_message{outputs: OsCarriedOutputs*}( return (); } +// Derives the deploy-account contract address for the given transaction version: Pedersen for +// v1/v3, Blake2 with the Pedersen-image escape for v4. The deployer address is always zero for +// deploy-account transactions. The Pedersen path advances the pedersen builtin pointer; the +// Blake path leaves it unchanged. +func get_contract_address_for_version{range_check_ptr, builtin_ptrs: BuiltinPointers*}( + tx_version: felt, + salt: felt, + class_hash: felt, + constructor_calldata_size: felt, + constructor_calldata: felt*, +) -> (contract_address: felt) { + alloc_locals; + if (tx_version == 4) { + let (contract_address) = get_contract_address_blake_escaped( + salt=salt, + class_hash=class_hash, + constructor_calldata_size=constructor_calldata_size, + constructor_calldata=constructor_calldata, + deployer_address=0, + ); + return (contract_address=contract_address); + } + + let hash_ptr = builtin_ptrs.selectable.pedersen; + with hash_ptr { + let (contract_address) = get_contract_address( + salt=salt, + class_hash=class_hash, + constructor_calldata_size=constructor_calldata_size, + constructor_calldata=constructor_calldata, + deployer_address=0, + ); + } + update_pedersen_in_builtin_ptrs(pedersen_ptr=hash_ptr); + return (contract_address=contract_address); +} + // Prepares a constructor execution context based on the 'tx' hint variable. // Leaves 'execution_info.tx_info' and 'deprecated_tx_info' empty - should be filled later on. // TODO(Yoni, 1/1/2026): consider removing this function (used only once). func prepare_constructor_execution_context{range_check_ptr, builtin_ptrs: BuiltinPointers*}( block_info: BlockInfo* -) -> (constructor_execution_context: ExecutionContext*, salt: felt) { +) -> (constructor_execution_context: ExecutionContext*, salt: felt, tx_version: felt) { alloc_locals; local contract_address_salt; local class_hash; local constructor_calldata_size; local constructor_calldata: felt*; + local tx_version; %{ PrepareConstructorExecution %} assert_nn_le(constructor_calldata_size, SIERRA_ARRAY_LEN_BOUND - 1); - let hash_ptr = builtin_ptrs.selectable.pedersen; - with hash_ptr { - let (contract_address) = get_contract_address( - salt=contract_address_salt, - class_hash=class_hash, - constructor_calldata_size=constructor_calldata_size, - constructor_calldata=constructor_calldata, - deployer_address=0, - ); - } - update_pedersen_in_builtin_ptrs(pedersen_ptr=hash_ptr); + // The hinted version is bound by the transaction-hash assertion performed by the caller: + // both the version felt and the derived contract address are part of the committed hash + // preimage, so lying about either fails the proof. + let (contract_address) = get_contract_address_for_version( + tx_version=tx_version, + salt=contract_address_salt, + class_hash=class_hash, + constructor_calldata_size=constructor_calldata_size, + constructor_calldata=constructor_calldata, + ); let (tx_info_ptr: TxInfo*) = alloc(); let (deprecated_tx_info_ptr: DeprecatedTxInfo*) = alloc(); @@ -563,7 +605,9 @@ func prepare_constructor_execution_context{range_check_ptr, builtin_ptrs: Builti ); return ( - constructor_execution_context=constructor_execution_context, salt=contract_address_salt + constructor_execution_context=constructor_execution_context, + salt=contract_address_salt, + tx_version=tx_version, ); } @@ -578,7 +622,7 @@ func execute_deploy_account_transaction{ // Calculate address and prepare constructor execution context. let ( - local constructor_execution_context: ExecutionContext*, local salt + local constructor_execution_context: ExecutionContext*, local salt, local tx_version ) = prepare_constructor_execution_context(block_info=block_context.block_info_for_validate); local constructor_execution_info: ExecutionInfo* = constructor_execution_context.execution_info; local sender_address = constructor_execution_info.contract_address; @@ -596,11 +640,14 @@ func execute_deploy_account_transaction{ // Guess transaction fields. // Compute transaction hash and prepare transaction info. - // The version validation is done in `compute_deploy_account_transaction_hash()`. + // The version validation is done in `compute_deploy_account_transaction_hash()`; the hinted + // version also selected the address derivation above, and both are bound by the + // transaction-hash assertion below. let common_tx_fields = get_account_tx_common_fields( block_context=block_context, tx_hash_prefix=DEPLOY_ACCOUNT_HASH_PREFIX, sender_address=sender_address, + version=tx_version, ); let poseidon_ptr = builtin_ptrs.selectable.poseidon; with poseidon_ptr { @@ -717,6 +764,7 @@ func execute_declare_transaction{ block_context=block_context, tx_hash_prefix=DECLARE_HASH_PREFIX, sender_address=sender_address, + version=3, ); let poseidon_ptr = builtin_ptrs.selectable.poseidon; diff --git a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/transaction_hash/transaction_hash.cairo b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/transaction_hash/transaction_hash.cairo index 21a81d73d05..9be2271f42c 100644 --- a/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/transaction_hash/transaction_hash.cairo +++ b/crates/apollo_starknet_os_program/src/cairo/starkware/starknet/core/os/transaction_hash/transaction_hash.cairo @@ -243,8 +243,11 @@ func compute_deploy_account_transaction_hash{range_check_ptr, poseidon_ptr: Pose ) -> felt { alloc_locals; + // v3 and v4 share the same preimage layout; the chained version felt (and the derived + // contract address) distinguish the hashes. v4 derives its address with Blake2 plus the + // Pedersen-image escape. with_attr error_message("Invalid transaction version: {version}.") { - assert common_fields.version = 3; + assert (common_fields.version - 3) * (common_fields.version - 4) = 0; } let hash_state: PoseidonHashState = poseidon_hash_init(); diff --git a/crates/apollo_starknet_os_program/src/program_hash.json b/crates/apollo_starknet_os_program/src/program_hash.json index 9be88e8e791..926366d5486 100644 --- a/crates/apollo_starknet_os_program/src/program_hash.json +++ b/crates/apollo_starknet_os_program/src/program_hash.json @@ -1,6 +1,6 @@ { - "os": "0x6826aaa1594a6718a232f3951f07c42a23628a98028dcb8dffb9169ab940a33", - "virtual_os": "0x1c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324", + "os": "0x75ad601aae1fb3c8b239e68f4f4f33168d9b268dca5551987a5b7e6b3b7c067", + "virtual_os": "0xfedbf2b6504b9217026ba2bf9b2a33e8e28ed38e5b0f84eef59075b02174b8", "aggregator": "0x3e4ce8340259e374200ed856e597a0c0b268d1021119d33573d7c759c5320f9", "aggregator_with_prefix": "0x2526c12112a2d7f57f7ae74af7be1fe1b2766e4c34b0c88ceda16b70ed2d6c2" } \ No newline at end of file diff --git a/crates/apollo_starknet_os_program/src/virtual_os_test.rs b/crates/apollo_starknet_os_program/src/virtual_os_test.rs index 73af3578a55..e4965e1a267 100644 --- a/crates/apollo_starknet_os_program/src/virtual_os_test.rs +++ b/crates/apollo_starknet_os_program/src/virtual_os_test.rs @@ -19,11 +19,11 @@ fn test_virtual_os_swapped_files() { #[test] fn test_program_bytecode_lengths() { expect![[r#" - 16370 + 16564 "#]] .assert_debug_eq(&OS_PROGRAM.data_len()); expect![[r#" - 11426 + 11438 "#]] .assert_debug_eq(&VIRTUAL_OS_PROGRAM.data_len()); } diff --git a/crates/blockifier/resources/blockifier_versioned_constants_0_14_4.json b/crates/blockifier/resources/blockifier_versioned_constants_0_14_4.json index a74ae6c1a33..e8b96366cb3 100644 --- a/crates/blockifier/resources/blockifier_versioned_constants_0_14_4.json +++ b/crates/blockifier/resources/blockifier_versioned_constants_0_14_4.json @@ -127,8 +127,8 @@ "segment_arena_cells": false, "os_constants": { "allowed_virtual_os_program_hashes": [ - "0x53f6c9fcfd31d27279ff7d7e422b44623550a732b59fe193354a7316a96daa1", - "0x1c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324" + "0x1c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324", + "0xfedbf2b6504b9217026ba2bf9b2a33e8e28ed38e5b0f84eef59075b02174b8" ], "allowed_proof_versions": [ "0x50524f4f4631" @@ -503,7 +503,7 @@ }, "execute_txs_inner": { "Declare": { - "n_steps": 3938, + "n_steps": 3946, "n_memory_holes": 0, "builtin_instance_counter": { "range_check_builtin": 92, @@ -513,7 +513,7 @@ }, "DeployAccount": { "constant": { - "n_steps": 4989, + "n_steps": 5017, "n_memory_holes": 0, "builtin_instance_counter": { "range_check_builtin": 113, @@ -535,7 +535,7 @@ }, "InvokeFunction": { "constant": { - "n_steps": 4779, + "n_steps": 4787, "n_memory_holes": 0, "builtin_instance_counter": { "range_check_builtin": 110, diff --git a/crates/blockifier/resources/versioned_constants_diff_regression/0.14.3_0.14.4.txt b/crates/blockifier/resources/versioned_constants_diff_regression/0.14.3_0.14.4.txt index 2e2baceb4df..41c0ac45634 100644 --- a/crates/blockifier/resources/versioned_constants_diff_regression/0.14.3_0.14.4.txt +++ b/crates/blockifier/resources/versioned_constants_diff_regression/0.14.3_0.14.4.txt @@ -1,4 +1,5 @@ -+ /os_constants/allowed_virtual_os_program_hashes/1: "0x1c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324" +~ /os_constants/allowed_virtual_os_program_hashes/0: "0x1c7be3225dfb33359b3ba9ffbe1542b7da27879b6d89f47b967e512463fd324" ++ /os_constants/allowed_virtual_os_program_hashes/1: "0xfedbf2b6504b9217026ba2bf9b2a33e8e28ed38e5b0f84eef59075b02174b8" ~ /os_resources/execute_syscalls/CallContract/n_steps: 901 ~ /os_resources/execute_syscalls/EmitEvent/n_steps: 47 ~ /os_resources/execute_syscalls/GetBlockHash/builtin_instance_counter/range_check_builtin: 3 @@ -27,12 +28,12 @@ - /os_resources/execute_syscalls/SendMessageToL1/n_steps ~ /os_resources/execute_syscalls/Sha256ProcessBlock/n_steps: 1854 ~ /os_resources/execute_txs_inner/Declare/builtin_instance_counter/range_check_builtin: 92 -~ /os_resources/execute_txs_inner/Declare/n_steps: 3938 +~ /os_resources/execute_txs_inner/Declare/n_steps: 3946 ~ /os_resources/execute_txs_inner/DeployAccount/constant/builtin_instance_counter/pedersen_builtin: 10 ~ /os_resources/execute_txs_inner/DeployAccount/constant/builtin_instance_counter/poseidon_builtin: 10 ~ /os_resources/execute_txs_inner/DeployAccount/constant/builtin_instance_counter/range_check_builtin: 113 -~ /os_resources/execute_txs_inner/DeployAccount/constant/n_steps: 4989 +~ /os_resources/execute_txs_inner/DeployAccount/constant/n_steps: 5017 ~ /os_resources/execute_txs_inner/InvokeFunction/constant/builtin_instance_counter/poseidon_builtin: 11 ~ /os_resources/execute_txs_inner/InvokeFunction/constant/builtin_instance_counter/range_check_builtin: 110 -~ /os_resources/execute_txs_inner/InvokeFunction/constant/n_steps: 4779 +~ /os_resources/execute_txs_inner/InvokeFunction/constant/n_steps: 4787 ~ /os_resources/execute_txs_inner/L1Handler/constant/n_steps: 1329 diff --git a/crates/blockifier/src/transaction/execution_flavors_test.rs b/crates/blockifier/src/transaction/execution_flavors_test.rs index bcba0ed7515..ded2962eadb 100644 --- a/crates/blockifier/src/transaction/execution_flavors_test.rs +++ b/crates/blockifier/src/transaction/execution_flavors_test.rs @@ -690,10 +690,10 @@ fn test_simulate_validate_charge_fee_mid_execution( // Second scenario: limit resources via sender bounds. Should revert if and only if step limit // is derived from sender bounds (`charge_fee` mode). - let (gas_bound, fee_bound) = gas_and_fee(6962_u32.into(), validate, &fee_type); + let (gas_bound, fee_bound) = gas_and_fee(6970_u32.into(), validate, &fee_type); // If `charge_fee` is true, execution is limited by sender bounds, so less resources will be // used. Otherwise, execution is limited by block bounds, so more resources will be used. - let (limited_gas_used, limited_fee) = gas_and_fee(8614_u32.into(), validate, &fee_type); + let (limited_gas_used, limited_fee) = gas_and_fee(8622_u32.into(), validate, &fee_type); let (unlimited_gas_used, unlimited_fee) = gas_and_fee( u64_from_usize( get_const_syscall_resources(SyscallSelector::CallContract).n_steps diff --git a/crates/central_systest_blobs/resources/blob_file_generation b/crates/central_systest_blobs/resources/blob_file_generation index 31ff414b74c..8783e305111 100644 --- a/crates/central_systest_blobs/resources/blob_file_generation +++ b/crates/central_systest_blobs/resources/blob_file_generation @@ -1 +1 @@ -48 \ No newline at end of file +53 \ No newline at end of file diff --git a/crates/central_systest_blobs/resources/preconfirmed_block.json b/crates/central_systest_blobs/resources/preconfirmed_block.json index e49c537ead9..fcd599277d2 100644 --- a/crates/central_systest_blobs/resources/preconfirmed_block.json +++ b/crates/central_systest_blobs/resources/preconfirmed_block.json @@ -47,7 +47,7 @@ "l2_gas": 0 }, "n_memory_holes": 0, - "n_steps": 4812, + "n_steps": 4820, "total_gas_consumed": { "l1_data_gas": 0, "l1_gas": 1652, diff --git a/crates/starknet_api/src/core.rs b/crates/starknet_api/src/core.rs index 3f8dbeeaecd..618606438e4 100644 --- a/crates/starknet_api/src/core.rs +++ b/crates/starknet_api/src/core.rs @@ -338,11 +338,53 @@ const STARK_CURVE_BETA: Felt = const ADDRESS_SECOND_LIFT_BOUND: Felt = Felt::from_hex_unchecked("0x11000000000000000000000000000000000000000000000101"); -/// Returns whether `value` is the x-coordinate of a STARK curve point, i.e. whether -/// `value^3 + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA` is a square in the field. -pub fn is_stark_curve_x_coordinate(value: &Felt) -> bool { +/// Returns `value^3 + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA` — a square in the field iff +/// `value` is the x-coordinate of a STARK curve point. +fn stark_curve_cubic(value: &Felt) -> Felt { let value = *value; - (value * value * value + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA).sqrt().is_some() + value * value * value + STARK_CURVE_ALPHA * value + STARK_CURVE_BETA +} + +/// Returns whether `value` is the x-coordinate of a STARK curve point. +pub fn is_stark_curve_x_coordinate(value: &Felt) -> bool { + stark_curve_cubic(value).sqrt().is_some() +} + +/// A witness for one step of the Pedersen-image escape, consumed by the Starknet OS hint: either +/// a square root showing a lift of the candidate is on the STARK curve (the candidate is +/// reachable by Pedersen and is skipped), or square roots of `cubic / 3` for every lift of the +/// candidate (3 is a fixed non-residue, so these prove the candidate escapes). +pub enum PedersenImageEscapeWitness { + Reachable { second_lift: bool, sqrt_witness: Felt }, + Unreachable { first_lift_witness: Felt, second_lift_witness: Option }, +} + +/// Computes the escape-step witness for `candidate` (see [PedersenImageEscapeWitness]). +pub fn pedersen_image_escape_witness(candidate: &Felt) -> PedersenImageEscapeWitness { + let non_residue_sqrt = |cubic: Felt| { + (cubic * Felt::THREE.inverse().expect("3 is invertible")) + .sqrt() + .expect("cubic / 3 must be a square when cubic is a non-residue") + }; + + let cubic = stark_curve_cubic(candidate); + if let Some(sqrt_witness) = cubic.sqrt() { + return PedersenImageEscapeWitness::Reachable { second_lift: false, sqrt_witness }; + } + if *candidate < ADDRESS_SECOND_LIFT_BOUND { + let lift_cubic = stark_curve_cubic(&(candidate + Felt::from(&*L2_ADDRESS_UPPER_BOUND))); + if let Some(sqrt_witness) = lift_cubic.sqrt() { + return PedersenImageEscapeWitness::Reachable { second_lift: true, sqrt_witness }; + } + return PedersenImageEscapeWitness::Unreachable { + first_lift_witness: non_residue_sqrt(cubic), + second_lift_witness: Some(non_residue_sqrt(lift_cubic)), + }; + } + PedersenImageEscapeWitness::Unreachable { + first_lift_witness: non_residue_sqrt(cubic), + second_lift_witness: None, + } } /// Returns whether some Pedersen hash output reduces (mod L2_ADDRESS_UPPER_BOUND) to `address`. diff --git a/crates/starknet_os/src/hints/enum_definition.rs b/crates/starknet_os/src/hints/enum_definition.rs index e8178f2c44b..310991baa1e 100644 --- a/crates/starknet_os/src/hints/enum_definition.rs +++ b/crates/starknet_os/src/hints/enum_definition.rs @@ -88,6 +88,7 @@ use crate::hints::hint_implementation::execution::implementation::{ enter_scope_deprecated_syscall_handler, enter_scope_execute_transactions_inner, enter_scope_syscall_handler, + escape_pedersen_image_witness, exit_call, exit_tx, gen_signature_arg, @@ -500,6 +501,7 @@ define_hint_enum!( (LoadCommonTxFields, load_common_tx_fields), (ExitTx, exit_tx), (PrepareConstructorExecution, prepare_constructor_execution), + (EscapePedersenImageWitness, escape_pedersen_image_witness), (AssertTransactionHash, assert_transaction_hash), (SetStateEntryToAccountContractAddress, set_state_entry_to_account_contract_address), (CheckIsDeprecated, check_is_deprecated), diff --git a/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs b/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs index 08146739cc2..34f5029adfd 100644 --- a/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs +++ b/crates/starknet_os/src/hints/hint_implementation/blake2s/blake2s_test.rs @@ -1,4 +1,5 @@ use std::collections::HashMap; +use std::sync::Arc; use blockifier::execution::casm_hash_estimation::expected::{ BASE_STEPS_FULL_MSG_EXPECT, @@ -18,6 +19,14 @@ use cairo_vm::types::layout_name::LayoutName; use cairo_vm::types::relocatable::MaybeRelocatable; use cairo_vm::vm::runners::cairo_runner::ExecutionResources; use rstest::rstest; +use starknet_api::core::{ + calculate_contract_address, + is_pedersen_reachable_address, + AddressDerivationHash, + ClassHash, + ContractAddress, +}; +use starknet_api::transaction::fields::{Calldata, ContractAddressSalt}; use starknet_types_core::felt::Felt; use starknet_types_core::hash::Blake2Felt252; @@ -205,3 +214,82 @@ fn test_calc_naive_blake_hash(#[case] test_data: Vec) { }; assert_eq!(calc_blake_hash(&test_data), *cairo_hash); } + +/// Asserts the Cairo OS `get_contract_address_blake_escaped` matches the Rust Blake2 +/// contract-address derivation (the Blake hash plus the Pedersen-image escape). This is the +/// parity guarantee for the v4 / deploy_v2 address derivation. The escape-step counts of the +/// frozen-vector cases were verified against an independent python implementation. +#[rstest] +#[case::no_calldata(Felt::from(1234), Felt::from(0x110), vec![], Felt::from(0x1111))] +#[case::zero_escape_steps( + Felt::from(777), + Felt::from(0x4242), + vec![Felt::from(42), Felt::from(1u64 << 63), Felt::from(1337)], + Felt::ZERO +)] +#[case::one_escape_step( + Felt::from(771), + Felt::from(0x4242), + vec![Felt::from(42), Felt::from(1u64 << 63), Felt::from(1337)], + Felt::ZERO +)] +#[case::seven_escape_steps( + Felt::from(774), + Felt::from(0x4242), + vec![Felt::from(42), Felt::from(1u64 << 63), Felt::from(1337)], + Felt::ZERO +)] +fn test_cairo_vs_rust_get_contract_address_blake( + #[case] salt: Felt, + #[case] class_hash: Felt, + #[case] constructor_calldata: Vec, + #[case] deployer_address: Felt, +) { + let runner_config = EntryPointRunnerConfig { + layout: LayoutName::all_cairo, + trace_enabled: false, + verify_secure: false, + proof_mode: false, + add_main_prefix_to_entrypoint: false, + validate_builtins_offset: true, + }; + + let (_, return_values, _) = initialize_and_run_cairo_0_entry_point( + &runner_config, + apollo_starknet_os_program::OS_PROGRAM_BYTES, + "starkware.starknet.core.os.contract_address.contract_address.\ + get_contract_address_blake_escaped", + &[ + EndpointArg::from(salt), + EndpointArg::from(class_hash), + EndpointArg::from(Felt::from(constructor_calldata.len())), + EndpointArg::Pointer(PointerArg::Array( + constructor_calldata.iter().map(|felt| MaybeRelocatable::Int(*felt)).collect(), + )), + EndpointArg::from(deployer_address), + ], + &[ImplicitArg::Builtin(BuiltinName::range_check)], + &[EndpointArg::from(Felt::ZERO)], + HashMap::new(), + None, + ) + .unwrap(); + + let [EndpointArg::Value(ValueArg::Single(MaybeRelocatable::Int(cairo_address)))] = + return_values.as_slice() + else { + panic!("Expected a single felt return value"); + }; + + let rust_address = calculate_contract_address( + ContractAddressSalt(salt), + ClassHash(class_hash), + &Calldata(Arc::new(constructor_calldata)), + ContractAddress::try_from(deployer_address).unwrap(), + AddressDerivationHash::Blake2, + ) + .unwrap(); + + assert_eq!(*cairo_address, *rust_address.0.key()); + assert!(!is_pedersen_reachable_address(cairo_address)); +} diff --git a/crates/starknet_os/src/hints/hint_implementation/execution/implementation.rs b/crates/starknet_os/src/hints/hint_implementation/execution/implementation.rs index f3ab43da5b6..0e1c68d6a9a 100644 --- a/crates/starknet_os/src/hints/hint_implementation/execution/implementation.rs +++ b/crates/starknet_os/src/hints/hint_implementation/execution/implementation.rs @@ -8,7 +8,14 @@ use cairo_vm::any_box; use cairo_vm::hint_processor::hint_processor_utils::felt_to_usize; use cairo_vm::types::relocatable::MaybeRelocatable; use starknet_api::block::BlockNumber; -use starknet_api::core::{ClassHash, ContractAddress, Nonce, PatriciaKey}; +use starknet_api::core::{ + pedersen_image_escape_witness, + ClassHash, + ContractAddress, + Nonce, + PatriciaKey, + PedersenImageEscapeWitness, +}; use starknet_api::executable_transaction::{AccountTransaction, Transaction}; use starknet_api::state::StorageKey; use starknet_api::transaction::fields::ValidResourceBounds; @@ -125,17 +132,15 @@ pub(crate) fn prepare_constructor_execution( ctx.insert_value(Ids::ContractAddressSalt, deploy_account_tx.contract_address_salt().0)?; ctx.insert_value(Ids::ClassHash, deploy_account_tx.class_hash().0)?; + // The version selects the address-derivation hash (v4 = Blake2 with the Pedersen-image + // escape). The hinted value is bound by the transaction-hash assertion: both the version felt + // and the derived address are part of the committed hash preimage. + ctx.insert_value(Ids::TxVersion, deploy_account_tx.version().0)?; let constructor_calldata = match &deploy_account_tx.tx { DeployAccountTransaction::V1(v1_tx) => &v1_tx.constructor_calldata, DeployAccountTransaction::V3(v3_tx) => &v3_tx.constructor_calldata, - // TODO(Ron): support v4 in the OS (Blake2 address derivation) before enabling v4 - // ingestion at the gateway. - DeployAccountTransaction::V4(_) => { - return Err(OsHintError::AssertionFailed { - message: "Deploy account v4 is not yet supported by the OS.".to_string(), - }); - } + DeployAccountTransaction::V4(v4_tx) => &v4_tx.constructor_calldata, }; ctx.insert_value(Ids::ConstructorCalldataSize, constructor_calldata.0.len())?; let constructor_calldata_base = ctx.vm.add_memory_segment(); @@ -146,6 +151,28 @@ pub(crate) fn prepare_constructor_execution( Ok(()) } +pub(crate) fn escape_pedersen_image_witness( + _hint_processor: &mut SnosHintProcessor<'_, S>, + mut ctx: HintContext<'_>, +) -> OsHintResult { + let candidate = ctx.get_integer(Ids::Candidate)?; + match pedersen_image_escape_witness(&candidate) { + PedersenImageEscapeWitness::Reachable { second_lift, sqrt_witness } => { + ctx.insert_value(Ids::IsReachable, Felt::ONE)?; + ctx.insert_value(Ids::ReachableLift, Felt::from(second_lift))?; + ctx.insert_value(Ids::Witness, sqrt_witness)?; + ctx.insert_value(Ids::SecondWitness, Felt::ZERO)?; + } + PedersenImageEscapeWitness::Unreachable { first_lift_witness, second_lift_witness } => { + ctx.insert_value(Ids::IsReachable, Felt::ZERO)?; + ctx.insert_value(Ids::ReachableLift, Felt::ZERO)?; + ctx.insert_value(Ids::Witness, first_lift_witness)?; + ctx.insert_value(Ids::SecondWitness, second_lift_witness.unwrap_or(Felt::ZERO))?; + } + } + Ok(()) +} + pub(crate) fn assert_transaction_hash( hint_processor: &mut SnosHintProcessor<'_, S>, ctx: HintContext<'_>, diff --git a/crates/starknet_os/src/hints/vars.rs b/crates/starknet_os/src/hints/vars.rs index 738a72fb072..9cd62012458 100644 --- a/crates/starknet_os/src/hints/vars.rs +++ b/crates/starknet_os/src/hints/vars.rs @@ -147,6 +147,7 @@ define_string_enum! { (ClassHash), (ClassHashPtr), (CompiledClass), + (Candidate), (CompiledClassFact), (CompiledClassFacts), (CompiledClassHash), @@ -208,6 +209,7 @@ define_string_enum! { (IsNUpdatesSmall), (IsOnCurve), (IsRemainingGasLtInitialBudget), + (IsReachable), (IsReverted), (IsSierraGasMode), (Key), @@ -267,6 +269,7 @@ define_string_enum! { (SnPrivateKeys), (RangeCheck96Ptr, "range_check96_ptr"), (RangeCheckPtr), + (ReachableLift), (RemainingGas), (RemainingGasGtMax), (ResourceBounds), @@ -286,6 +289,7 @@ define_string_enum! { (Selector), (SenderAddress), (SequencerAddress), + (SecondWitness), (Sha256Ptr, "sha256_ptr"), (Sha256PtrEnd, "sha256_ptr_end"), (Sha512PtrEnd, "sha512_ptr_end"), @@ -317,6 +321,7 @@ define_string_enum! { (UpdatePtr), (UseKzgDa), (Value), + (Witness), (Word), } } diff --git a/crates/starknet_os/src/test_utils/coverage.rs b/crates/starknet_os/src/test_utils/coverage.rs index 8295ef28efc..00c14f35f9a 100644 --- a/crates/starknet_os/src/test_utils/coverage.rs +++ b/crates/starknet_os/src/test_utils/coverage.rs @@ -18,6 +18,7 @@ pub fn expect_hint_coverage(unused_hints: &HashSet, test_name: &str) { const UNCOVERED_HINTS: expect_test::Expect = expect_test::expect![[r#" [ "DeprecatedSyscallHint(Deploy)", + "OsHint(EscapePedersenImageWitness)", "OsHint(GetClassHashAndCompiledClassFact)", "StatelessHint(SetApToSegmentHashPoseidon)", ]