diff --git a/.env.example b/.env.example index 9381ce47e..6dbafd718 100644 --- a/.env.example +++ b/.env.example @@ -7,8 +7,9 @@ INFURA_KEY= # Account used by default in all kakarot_scripts # Prefixed accounts {NETWORK}_ will take precedeance over the default one -MADARA_ACCOUNT_ADDRESS=0x3 -MADARA_PRIVATE_KEY=0x00c1cf1490de1352865301bb8705143f3ef938f97fdf892f1090dcb5ac7bcd1d + +MADARA_ACCOUNT_ADDRESS=0x055be462e718c4166d656d11f89e341115b8bc82389c3762a10eade04fcb225d +MADARA_PRIVATE_KEY=0x077e56c6dc32d40a67f6f7e6625c8dc5e570abe49c0a24e9202e4ae906abcc07 SHARINGAN_ACCOUNT_ADDRESS= SHARINGAN_PRIVATE_KEY= diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7b4083bb4..114387d94 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -107,7 +107,7 @@ jobs: tests-end-to-end: runs-on: ubuntu-latest env: - STARKNET_NETWORK: katana + STARKNET_NETWORK: madara PROTOCOL_BUFFERS_PYTHON_IMPLEMENTATION: python needs: paths-filter if: @@ -119,11 +119,11 @@ jobs: needs.paths-filter.outputs.rust == 'true' steps: - uses: actions/checkout@v4 - - name: Extract Katana Version - id: extract_katana_version + - name: Extract Madara Version + id: extract_madara_version run: | - KATANA_VERSION=$(grep -oP '^KATANA_VERSION = \K.*' Makefile) - echo "katana_version=$KATANA_VERSION" >> "$GITHUB_OUTPUT" + MADARA_VERSION=$(grep -oP '^MADARA_VERSION = \K.*' Makefile) + echo "madara_version=$MADARA_VERSION" >> "$GITHUB_OUTPUT" - uses: astral-sh/setup-uv@v2 with: enable-cache: true @@ -142,13 +142,13 @@ jobs: with: path: ~/.cargo/bin key: katana-${{ steps.extract_katana_version.outputs.katana_version }} - - name: Install Katana + - name: Install Madara if: steps.cached-katana.outputs.cache-hit != 'true' - run: make install-katana + run: make install-madara - name: Run tests run: | cp .env.example .env - make run-nodes & make test-end-to-end + make test-end-to-end forge-test: runs-on: ubuntu-latest diff --git a/.madara/deploy_core_contract.py b/.madara/deploy_core_contract.py new file mode 100644 index 000000000..6735b00f6 --- /dev/null +++ b/.madara/deploy_core_contract.py @@ -0,0 +1,13 @@ +from kakarot_scripts.utils.l1 import ( + deploy_on_l1, + get_l1_addresses, + dump_l1_addresses +) + +contract = deploy_on_l1( + "starknet", + "StarknetMessagingLocal", +) +l1_addresses = get_l1_addresses() +l1_addresses.update({"StarknetMessagingLocal": {"address": contract.address}}) +dump_l1_addresses(l1_addresses) diff --git a/Makefile b/Makefile index 5e21574ab..a9124617b 100644 --- a/Makefile +++ b/Makefile @@ -10,7 +10,7 @@ endif KKRT_SSJ_RELEASE_ID = 176384150 # Kakarot SSJ artifacts for precompiles. KKRT_SSJ_BUILD_ARTIFACT_URL = $(shell curl -L https://api.github.com/repos/kkrt-labs/kakarot-ssj/releases/${KKRT_SSJ_RELEASE_ID} | jq -r '.assets[0].browser_download_url') -KATANA_VERSION = v1.0.0-alpha.14 +MADARA_VERSION = d188aa91 ROOT_DIR:=$(shell dirname $(realpath $(lastword $(MAKEFILE_LIST)))) BUILD_DIR = build @@ -45,7 +45,7 @@ test: deploy test-unit: build-sol uv run pytest tests/src -m "not NoCI" -n logical --seed 42 -test-end-to-end: deploy +test-end-to-end: madara-test-deploy uv run pytest tests/end_to_end --seed 42 format: @@ -67,16 +67,16 @@ build-sol: git submodule update --init --recursive forge build --names --force -install-katana: - cargo install --git https://github.com/dojoengine/dojo --locked --tag "${KATANA_VERSION}" katana +install-madara: + cargo install --git https://github.com/madara-alliance/madara.git --locked --rev "${MADARA_VERSION}" -run-katana: - katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee --seed 0 - -run-anvil: - anvil --block-base-fee-per-gas 1 +madara-test-deploy: run-nodes deploy + @echo "Deploying Starknet Core contract and restarting Madara..." + uv run python .madara/deploy_core_contract.py + pkill madara + sleep 1 + madara --devnet --l1-endpoint=http://localhost:8545 --chain-config-override eth_core_contract_address=0x5FbDB2315678afecb367f032d93F642f64180aa3 & run-nodes: - @echo "Starting Anvil and Katana in messaging mode" - @anvil --block-base-fee-per-gas 1 & - @katana --chain-id test --validate-max-steps 6000000 --invoke-max-steps 14000000 --eth-gas-price 0 --strk-gas-price 0 --disable-fee --messaging .katana/messaging_config.json --seed 0 + anvil --block-base-fee-per-gas 1 & + madara --devnet & diff --git a/kakarot_scripts/constants.py b/kakarot_scripts/constants.py index c633029b1..37305e021 100644 --- a/kakarot_scripts/constants.py +++ b/kakarot_scripts/constants.py @@ -125,8 +125,8 @@ class NetworkType(Enum): "rpc_url": os.getenv("MADARA_RPC_URL", "http://127.0.0.1:9944"), "l1_rpc_url": "http://127.0.0.1:8545", "type": NetworkType.DEV, - "check_interval": 6, - "max_wait": 30, + "check_interval": 0.01, + "max_wait": 3, }, "sharingan": { "name": "sharingan", diff --git a/kakarot_scripts/utils/starknet.py b/kakarot_scripts/utils/starknet.py index cbd97b270..05d9d5275 100644 --- a/kakarot_scripts/utils/starknet.py +++ b/kakarot_scripts/utils/starknet.py @@ -129,12 +129,12 @@ async def get_starknet_account( message = str(err) if ( "Client failed with code 40: Contract error." in message - or "Client failed with code 40: Requested entry point was not found." - in message + or "Client failed with code 40: Requested entry point was not found." in message or "Invalid message selector." in message or "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT" in message or ("code 40" in message and "not found in contract" in message) or "{'error': 'Invalid message selector'}" in message + or ("Entry point" in message and "not found in contract" in message) ): continue else: diff --git a/solidity_contracts/src/starknet/IStarknetMessaging.sol b/solidity_contracts/src/starknet/IStarknetMessaging.sol index facb03ec4..3320e8e4e 100644 --- a/solidity_contracts/src/starknet/IStarknetMessaging.sol +++ b/solidity_contracts/src/starknet/IStarknetMessaging.sol @@ -1,5 +1,5 @@ /* - Copyright 2019-2022 StarkWare Industries Ltd. + Copyright 2019-2024 StarkWare Industries Ltd. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ and limitations under the License. */ // SPDX-License-Identifier: Apache-2.0. -pragma solidity ^0.8.0; +pragma solidity ^0.8.24; import "./IStarknetMessagingEvents.sol"; @@ -24,9 +24,15 @@ interface IStarknetMessaging is IStarknetMessagingEvents { */ function getMaxL1MsgFee() external pure returns (uint256); + /** + Returns `msg_fee + 1` if there is a pending message associated with the given 'msgHash', + otherwise, returns 0. + */ + function l1ToL2Messages(bytes32 msgHash) external view returns (uint256); + /** Sends a message to an L2 contract. - This function is payable, the paid amount is the message fee. + This function is payable, the payed amount is the message fee. Returns the hash of the message and the nonce of the message. */ diff --git a/solidity_contracts/src/starknet/IStarknetMessagingEvents.sol b/solidity_contracts/src/starknet/IStarknetMessagingEvents.sol index 11937727b..fdc26f278 100644 --- a/solidity_contracts/src/starknet/IStarknetMessagingEvents.sol +++ b/solidity_contracts/src/starknet/IStarknetMessagingEvents.sol @@ -1,5 +1,5 @@ /* - Copyright 2019-2022 StarkWare Industries Ltd. + Copyright 2019-2024 StarkWare Industries Ltd. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ and limitations under the License. */ // SPDX-License-Identifier: Apache-2.0. -pragma solidity ^0.8.0; +pragma solidity ^0.8.24; interface IStarknetMessagingEvents { // This event needs to be compatible with the one defined in Output.sol. diff --git a/solidity_contracts/src/starknet/Output.sol b/solidity_contracts/src/starknet/Output.sol new file mode 100644 index 000000000..5b6e9944e --- /dev/null +++ b/solidity_contracts/src/starknet/Output.sol @@ -0,0 +1,175 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessagingEvents.sol"; + +library CommitmentTreeUpdateOutput { + /** + Returns the previous commitment tree root. + */ + function getPrevRoot(uint256[] calldata commitmentTreeUpdateData) + internal + pure + returns (uint256) + { + return commitmentTreeUpdateData[0]; + } + + /** + Returns the new commitment tree root. + */ + function getNewRoot(uint256[] calldata commitmentTreeUpdateData) + internal + pure + returns (uint256) + { + return commitmentTreeUpdateData[1]; + } +} + +library StarknetOutput { + uint256 internal constant MERKLE_UPDATE_OFFSET = 0; + uint256 internal constant BLOCK_NUMBER_OFFSET = 2; + uint256 internal constant BLOCK_HASH_OFFSET = 3; + uint256 internal constant CONFIG_HASH_OFFSET = 4; + uint256 internal constant USE_KZG_DA_OFFSET = 5; + uint256 internal constant HEADER_SIZE = 6; + uint256 internal constant KZG_SEGMENT_SIZE = 5; + + uint256 constant MESSAGE_TO_L1_FROM_ADDRESS_OFFSET = 0; + uint256 constant MESSAGE_TO_L1_TO_ADDRESS_OFFSET = 1; + uint256 constant MESSAGE_TO_L1_PAYLOAD_SIZE_OFFSET = 2; + uint256 constant MESSAGE_TO_L1_PREFIX_SIZE = 3; + + uint256 constant MESSAGE_TO_L2_FROM_ADDRESS_OFFSET = 0; + uint256 constant MESSAGE_TO_L2_TO_ADDRESS_OFFSET = 1; + uint256 constant MESSAGE_TO_L2_NONCE_OFFSET = 2; + uint256 constant MESSAGE_TO_L2_SELECTOR_OFFSET = 3; + uint256 constant MESSAGE_TO_L2_PAYLOAD_SIZE_OFFSET = 4; + uint256 constant MESSAGE_TO_L2_PREFIX_SIZE = 5; + + /** + Returns the offset of the messages segment in the output_data. + */ + function messageSegmentOffset(uint256 use_kzg_da) internal pure returns (uint256) { + return HEADER_SIZE + (use_kzg_da == 1 ? KZG_SEGMENT_SIZE : 0); + } + + /** + Returns a slice of the 'output_data' with the commitment tree update information. + */ + function getMerkleUpdate(uint256[] calldata output_data) + internal + pure + returns (uint256[] calldata) + { + return output_data[MERKLE_UPDATE_OFFSET:MERKLE_UPDATE_OFFSET + 2]; + } + + /** + Processes a message segment from the program output. + The format of a message segment is the length of the messages in words followed + by the concatenation of all the messages. + + The 'messages' mapping is updated according to the messages and the direction ('isL2ToL1'). + */ + function processMessages( + bool isL2ToL1, + uint256[] calldata programOutputSlice, + mapping(bytes32 => uint256) storage messages + ) internal returns (uint256) { + uint256 messageSegmentSize = programOutputSlice[0]; + require(messageSegmentSize < 2**30, "INVALID_MESSAGE_SEGMENT_SIZE"); + + uint256 offset = 1; + uint256 messageSegmentEnd = offset + messageSegmentSize; + + uint256 payloadSizeOffset = ( + isL2ToL1 ? MESSAGE_TO_L1_PAYLOAD_SIZE_OFFSET : MESSAGE_TO_L2_PAYLOAD_SIZE_OFFSET + ); + + uint256 totalMsgFees = 0; + while (offset < messageSegmentEnd) { + uint256 payloadLengthOffset = offset + payloadSizeOffset; + require(payloadLengthOffset < programOutputSlice.length, "MESSAGE_TOO_SHORT"); + + uint256 payloadLength = programOutputSlice[payloadLengthOffset]; + require(payloadLength < 2**30, "INVALID_PAYLOAD_LENGTH"); + + uint256 endOffset = payloadLengthOffset + 1 + payloadLength; + require(endOffset <= programOutputSlice.length, "TRUNCATED_MESSAGE_PAYLOAD"); + + if (isL2ToL1) { + bytes32 messageHash = keccak256( + abi.encodePacked(programOutputSlice[offset:endOffset]) + ); + + emit IStarknetMessagingEvents.LogMessageToL1( + // from= + programOutputSlice[offset + MESSAGE_TO_L1_FROM_ADDRESS_OFFSET], + // to= + address(uint160(programOutputSlice[offset + MESSAGE_TO_L1_TO_ADDRESS_OFFSET])), + // payload= + (uint256[])(programOutputSlice[offset + MESSAGE_TO_L1_PREFIX_SIZE:endOffset]) + ); + messages[messageHash] += 1; + } else { + { + bytes32 messageHash = keccak256( + abi.encodePacked(programOutputSlice[offset:endOffset]) + ); + + uint256 msgFeePlusOne = messages[messageHash]; + require(msgFeePlusOne > 0, "INVALID_MESSAGE_TO_CONSUME"); + totalMsgFees += msgFeePlusOne - 1; + messages[messageHash] = 0; + } + + uint256 nonce = programOutputSlice[offset + MESSAGE_TO_L2_NONCE_OFFSET]; + uint256[] memory messageSlice = (uint256[])( + programOutputSlice[offset + MESSAGE_TO_L2_PREFIX_SIZE:endOffset] + ); + emit IStarknetMessagingEvents.ConsumedMessageToL2( + // from= + address( + uint160(programOutputSlice[offset + MESSAGE_TO_L2_FROM_ADDRESS_OFFSET]) + ), + // to= + programOutputSlice[offset + MESSAGE_TO_L2_TO_ADDRESS_OFFSET], + // selector= + programOutputSlice[offset + MESSAGE_TO_L2_SELECTOR_OFFSET], + // payload= + messageSlice, + // nonce = + nonce + ); + } + + offset = endOffset; + } + require(offset == messageSegmentEnd, "INVALID_MESSAGE_SEGMENT_SIZE"); + + if (totalMsgFees > 0) { + // NOLINTNEXTLINE: low-level-calls. + (bool success, ) = msg.sender.call{value: totalMsgFees}(""); + require(success, "ETH_TRANSFER_FAILED"); + } + + return offset; + } +} diff --git a/solidity_contracts/src/starknet/Starknet.sol b/solidity_contracts/src/starknet/Starknet.sol new file mode 100644 index 000000000..5b18a1cf6 --- /dev/null +++ b/solidity_contracts/src/starknet/Starknet.sol @@ -0,0 +1,366 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +import "./Output.sol"; +import "./StarknetGovernance.sol"; +import "./StarknetMessaging.sol"; +import "./StarknetOperator.sol"; +import "./StarknetState.sol"; +import "./starkware/solidity/components/GovernedFinalizable.sol"; +import "./starkware/solidity/components/OnchainDataFactTreeEncoder.sol"; +import "./starkware/solidity/interfaces/ContractInitializer.sol"; +import "./starkware/solidity/interfaces/Identity.sol"; +import "./starkware/solidity/interfaces/IFactRegistry.sol"; +import "./starkware/solidity/interfaces/ProxySupport.sol"; +import "./starkware/solidity/libraries/NamedStorage8.sol"; + +contract Starknet is + Identity, + StarknetMessaging, + StarknetGovernance, + GovernedFinalizable, + StarknetOperator, + ContractInitializer, + ProxySupport +{ + using StarknetState for StarknetState.State; + + // Indicates a change of the Starknet config hash. + event ConfigHashChanged( + address indexed changedBy, + uint256 oldConfigHash, + uint256 newConfigHash + ); + + // Logs the new state following a state update. + event LogStateUpdate(uint256 globalRoot, int256 blockNumber, uint256 blockHash); + + // Logs a stateTransitionFact that was used to update the state. + event LogStateTransitionFact(bytes32 stateTransitionFact); + + // Indicates a change of the Starknet OS program hash. + event ProgramHashChanged( + address indexed changedBy, + uint256 oldProgramHash, + uint256 newProgramHash + ); + + // Random storage slot tags. + string internal constant PROGRAM_HASH_TAG = "STARKNET_1.0_INIT_PROGRAM_HASH_UINT"; + string internal constant VERIFIER_ADDRESS_TAG = "STARKNET_1.0_INIT_VERIFIER_ADDRESS"; + string internal constant STATE_STRUCT_TAG = "STARKNET_1.0_INIT_STARKNET_STATE_STRUCT"; + + // The hash of the StarkNet config. + string internal constant CONFIG_HASH_TAG = "STARKNET_1.0_STARKNET_CONFIG_HASH"; + + // EIP-4844 constants. + address internal constant POINT_EVALUATION_PRECOMPILE_ADDRESS = address(0x0A); + // The precompile expected output: + // Web3.keccak(FIELD_ELEMENTS_PER_BLOB.to_bytes(32, "big") + BLS_PRIME.to_bytes(32, "big")). + bytes32 internal constant POINT_EVALUATION_PRECOMPILE_OUTPUT = + 0xb2157d3a40131b14c4c675335465dffde802f0ce5218ad012284d7f275d1b37c; + uint256 internal constant PROOF_BYTES_LENGTH = 48; + bytes1 internal constant VERSIONED_HASH_VERSION_KZG = bytes1(0x01); + + function setProgramHash(uint256 newProgramHash) external notFinalized onlyGovernance { + emit ProgramHashChanged(msg.sender, programHash(), newProgramHash); + programHash(newProgramHash); + } + + function setConfigHash(uint256 newConfigHash) external notFinalized onlyGovernance { + emit ConfigHashChanged(msg.sender, configHash(), newConfigHash); + configHash(newConfigHash); + } + + function setMessageCancellationDelay(uint256 delayInSeconds) + external + notFinalized + onlyGovernance + { + messageCancellationDelay(delayInSeconds); + } + + // State variable "programHash" read-access function. + function programHash() public view returns (uint256) { + return NamedStorage.getUintValue(PROGRAM_HASH_TAG); + } + + // State variable "programHash" write-access function. + function programHash(uint256 value) internal { + NamedStorage.setUintValue(PROGRAM_HASH_TAG, value); + } + + // State variable "verifier" access function. + function verifier() internal view returns (address) { + return NamedStorage.getAddressValue(VERIFIER_ADDRESS_TAG); + } + + // State variable "configHash" write-access function. + function configHash(uint256 value) internal { + NamedStorage.setUintValue(CONFIG_HASH_TAG, value); + } + + // State variable "configHash" read-access function. + function configHash() public view returns (uint256) { + return NamedStorage.getUintValue(CONFIG_HASH_TAG); + } + + function setVerifierAddress(address value) internal { + NamedStorage.setAddressValueOnce(VERIFIER_ADDRESS_TAG, value); + } + + // State variable "state" access function. + function state() internal pure returns (StarknetState.State storage stateStruct) { + bytes32 location = keccak256(abi.encodePacked(STATE_STRUCT_TAG)); + assembly { + stateStruct.slot := location + } + } + + function isInitialized() internal view override returns (bool) { + return programHash() != 0; + } + + function numOfSubContracts() internal pure override returns (uint256) { + return 0; + } + + function validateInitData(bytes calldata data) internal view override { + require(data.length == 6 * 32, "ILLEGAL_INIT_DATA_SIZE"); + uint256 programHash_ = abi.decode(data[:32], (uint256)); + require(programHash_ != 0, "BAD_INITIALIZATION"); + } + + function processSubContractAddresses(bytes calldata subContractAddresses) internal override {} + + function initializeContractState(bytes calldata data) internal override { + ( + uint256 programHash_, + address verifier_, + uint256 configHash_, + StarknetState.State memory initialState + ) = abi.decode(data, (uint256, address, uint256, StarknetState.State)); + + programHash(programHash_); + setVerifierAddress(verifier_); + state().copy(initialState); + configHash(configHash_); + messageCancellationDelay(5 days); + } + + /** + Verifies p(z) = y given z, y, a commitment to p in the kzgSegment, and a KZG proof. + The verification is done by calling Ethereum's point evaluation precompile. + */ + function verifyKzgProof(uint256[] calldata kzgSegment, bytes calldata kzgProof) internal { + require(kzgSegment.length == StarknetOutput.KZG_SEGMENT_SIZE, "INVALID_KZG_SEGMENT_SIZE"); + require(kzgProof.length == PROOF_BYTES_LENGTH, "INVALID_KZG_PROOF_SIZE"); +// +// bytes32 blobHash = blobhash( +// // blobIndex= +// 0 +// ); +// require(blobHash[0] == VERSIONED_HASH_VERSION_KZG, "UNEXPECTED_BLOB_HASH_VERSION"); + + bytes memory kzgCommitment; + bytes32 y; + { + uint256 kzgCommitmentLow = kzgSegment[0]; + uint256 kzgCommitmentHigh = kzgSegment[1]; + uint256 yLow = kzgSegment[3]; + uint256 yHigh = kzgSegment[4]; + + require(kzgCommitmentLow <= type(uint192).max, "INVALID_KZG_COMMITMENT"); + require(kzgCommitmentHigh <= type(uint192).max, "INVALID_KZG_COMMITMENT"); + require(yLow <= type(uint128).max, "INVALID_Y_VALUE"); + require(yHigh <= type(uint128).max, "INVALID_Y_VALUE"); + + kzgCommitment = abi.encodePacked(uint192(kzgCommitmentHigh), uint192(kzgCommitmentLow)); + y = bytes32((yHigh << 128) + yLow); + } + bytes32 z = bytes32(kzgSegment[2]); + +// (bool ok, bytes memory precompile_output) = POINT_EVALUATION_PRECOMPILE_ADDRESS.staticcall( +// abi.encodePacked(blobHash, z, y, kzgCommitment, kzgProof) +// ); + +// require(ok, "POINT_EVALUATION_PRECOMPILE_CALL_FAILED"); +// require( +// keccak256(precompile_output) == POINT_EVALUATION_PRECOMPILE_OUTPUT, +// "UNEXPECTED_POINT_EVALUATION_PRECOMPILE_OUTPUT" +// ); + } + + /** + Performs the actual state update of Starknet, based on a proof of the Starknet OS that the + state transition is valid. + + Arguments: + programOutput - The main part of the StarkNet OS program output. + stateTransitionFact - An encoding of the 'programOutput' (including on-chain data, if + available). + */ + function updateStateInternal(uint256[] calldata programOutput, bytes32 stateTransitionFact) + internal + { + // Validate config hash. + require( + programOutput[StarknetOutput.CONFIG_HASH_OFFSET] == configHash(), + "INVALID_CONFIG_HASH" + ); + + bytes32 sharpFact = keccak256(abi.encode(programHash(), stateTransitionFact)); + require(IFactRegistry(verifier()).isValid(sharpFact), "NO_STATE_TRANSITION_PROOF"); + emit LogStateTransitionFact(stateTransitionFact); + + // Perform state update. + state().update(programOutput); + + // Process the messages after updating the state. + // This is safer, as there is a call to transfer the fees during + // the processing of the L1 -> L2 messages. + + // Process L2 -> L1 messages. + uint256 outputOffset = StarknetOutput.messageSegmentOffset( + programOutput[StarknetOutput.USE_KZG_DA_OFFSET] + ); + outputOffset += StarknetOutput.processMessages( + // isL2ToL1= + true, + programOutput[outputOffset:], + l2ToL1Messages() + ); + + // Process L1 -> L2 messages. + outputOffset += StarknetOutput.processMessages( + // isL2ToL1= + false, + programOutput[outputOffset:], + l1ToL2Messages() + ); + require(outputOffset == programOutput.length, "STARKNET_OUTPUT_TOO_LONG"); + // Note that processing L1 -> L2 messages does an external call, and it shouldn't be + // followed by storage changes. + + StarknetState.State storage state_ = state(); + emit LogStateUpdate(state_.globalRoot, state_.blockNumber, state_.blockHash); + } + + /** + Returns a string that identifies the contract. + */ + function identify() external pure override returns (string memory) { + return "StarkWare_Starknet_2024_8"; + } + + /** + Returns the current state root. + */ + function stateRoot() external view returns (uint256) { + return state().globalRoot; + } + + /** + Returns the current block number. + */ + function stateBlockNumber() external view returns (int256) { + return state().blockNumber; + } + + /** + Returns the current block hash. + */ + function stateBlockHash() external view returns (uint256) { + return state().blockHash; + } + + /** + Updates the state of the Starknet, based on a proof of the Starknet OS that the state + transition is valid. Data availability is provided on-chain. + + Arguments: + programOutput - The main part of the StarkNet OS program output. + data_availability_fact - An encoding of the on-chain data associated + with the 'programOutput'. + */ + function updateState( + uint256[] calldata programOutput, + uint256 onchainDataHash, + uint256 onchainDataSize + ) external onlyOperator { + // We protect against re-entrancy attacks by reading the block number at the beginning + // and validating that we have the expected block number at the end. + int256 initialBlockNumber = state().blockNumber; + + // Validate program output. + require(programOutput.length > StarknetOutput.HEADER_SIZE, "STARKNET_OUTPUT_TOO_SHORT"); + + // Validate KZG DA flag. + require(programOutput[StarknetOutput.USE_KZG_DA_OFFSET] == 0, "UNEXPECTED_KZG_DA_FLAG"); + + bytes32 stateTransitionFact = OnchainDataFactTreeEncoder.encodeFactWithOnchainData( + programOutput, + OnchainDataFactTreeEncoder.DataAvailabilityFact(onchainDataHash, onchainDataSize) + ); + updateStateInternal(programOutput, stateTransitionFact); + // Note that updateStateInternal does an external call, and it shouldn't be followed by + // storage changes. + + // Re-entrancy protection (see above). + require(state().blockNumber == initialBlockNumber + 1, "INVALID_FINAL_BLOCK_NUMBER"); + } + + /** + Updates the state of the StarkNet, based on a proof of the StarkNet OS that the state + transition is valid. Data availability is committed with KZG and provided in a blob. + + Arguments: + programOutput - The main part of the StarkNet OS program output. + kzgProof - a KZG proof which is validated together with the StarkNet OS data commitment + given in 'programOutput'. + */ + function updateStateKzgDA(uint256[] calldata programOutput, bytes calldata kzgProof) + external + onlyOperator + { + // We protect against re-entrancy attacks by reading the block number at the beginning + // and validating that we have the expected block number at the end. + int256 initialBlockNumber = state().blockNumber; + + // Validate program output. + require( + programOutput.length > StarknetOutput.HEADER_SIZE + StarknetOutput.KZG_SEGMENT_SIZE, + "STARKNET_OUTPUT_TOO_SHORT" + ); + + // Verify the KZG Proof. + require(programOutput[StarknetOutput.USE_KZG_DA_OFFSET] == 1, "UNEXPECTED_KZG_DA_FLAG"); + verifyKzgProof( + programOutput[StarknetOutput.HEADER_SIZE:][:StarknetOutput.KZG_SEGMENT_SIZE], + kzgProof + ); + + bytes32 stateTransitionFact = OnchainDataFactTreeEncoder.hashMainPublicInput(programOutput); + updateStateInternal(programOutput, stateTransitionFact); + // Note that updateStateInternal does an external call, and it shouldn't be followed by + // storage changes. + + // Re-entrancy protection (see above). + require(state().blockNumber == initialBlockNumber + 1, "INVALID_FINAL_BLOCK_NUMBER"); + } +} diff --git a/solidity_contracts/src/starknet/StarknetGovernance.sol b/solidity_contracts/src/starknet/StarknetGovernance.sol new file mode 100644 index 000000000..86864b246 --- /dev/null +++ b/solidity_contracts/src/starknet/StarknetGovernance.sol @@ -0,0 +1,53 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./starkware/solidity/components/Governance.sol"; + +contract StarknetGovernance is Governance { + string constant STARKNET_GOVERNANCE_INFO_TAG = "STARKNET_1.0_GOVERNANCE_INFO"; + + /* + Returns the GovernanceInfoStruct associated with the governance tag. + */ + function getGovernanceInfo() internal view override returns (GovernanceInfoStruct storage gub) { + bytes32 location = keccak256(abi.encodePacked(STARKNET_GOVERNANCE_INFO_TAG)); + assembly { + gub.slot := location + } + } + + function starknetIsGovernor(address user) external view returns (bool) { + return _isGovernor(user); + } + + function starknetNominateNewGovernor(address newGovernor) external { + _nominateNewGovernor(newGovernor); + } + + function starknetRemoveGovernor(address governorForRemoval) external { + _removeGovernor(governorForRemoval); + } + + function starknetAcceptGovernance() external { + _acceptGovernance(); + } + + function starknetCancelNomination() external { + _cancelNomination(); + } +} diff --git a/solidity_contracts/src/starknet/StarknetMessaging.sol b/solidity_contracts/src/starknet/StarknetMessaging.sol index 69cb691e6..d4c07fef8 100644 --- a/solidity_contracts/src/starknet/StarknetMessaging.sol +++ b/solidity_contracts/src/starknet/StarknetMessaging.sol @@ -1,5 +1,5 @@ /* - Copyright 2019-2022 StarkWare Industries Ltd. + Copyright 2019-2024 StarkWare Industries Ltd. Licensed under the Apache License, Version 2.0 (the "License"). You may not use this file except in compliance with the License. @@ -17,13 +17,12 @@ pragma solidity ^0.8.0; import "./IStarknetMessaging.sol"; -import "./NamedStorage.sol"; +import "./starkware/solidity/libraries/NamedStorage8.sol"; /** Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by removing them from a different pipe. A deriving contract can handle the former pipe and add items to the latter pipe while interacting with L2. - Adapted from https://github.com/starkware-libs/cairo-lang/blob/master/src/starkware/starknet/solidity/StarknetMessaging.sol */ contract StarknetMessaging is IStarknetMessaging { /* @@ -52,7 +51,7 @@ contract StarknetMessaging is IStarknetMessaging { Returns the msg_fee + 1 for the message with the given 'msgHash', or 0 if no message with such a hash is pending. */ - function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) { + function l1ToL2Messages(bytes32 msgHash) external view override returns (uint256) { return l1ToL2Messages()[msgHash]; } diff --git a/solidity_contracts/src/starknet/StarknetMessagingLocal.sol b/solidity_contracts/src/starknet/StarknetMessagingLocal.sol index 1e5718f92..257114c19 100644 --- a/solidity_contracts/src/starknet/StarknetMessagingLocal.sol +++ b/solidity_contracts/src/starknet/StarknetMessagingLocal.sol @@ -1,7 +1,7 @@ // SPDX-License-Identifier: Apache-2.0. pragma solidity ^0.8.0; -import "../starknet/StarknetMessaging.sol"; +import "../starknet/Starknet.sol"; /** @notice Interface related to local messaging for Starknet. @@ -28,7 +28,7 @@ interface IStarknetMessagingLocal { DISCLAIMER: The purpose of this contract is for local development only. */ -contract StarknetMessagingLocal is StarknetMessaging, IStarknetMessagingLocal { +contract StarknetMessagingLocal is Starknet, IStarknetMessagingLocal { /** @notice Hashes were added. diff --git a/solidity_contracts/src/starknet/StarknetOperator.sol b/solidity_contracts/src/starknet/StarknetOperator.sol new file mode 100644 index 000000000..c6319d81f --- /dev/null +++ b/solidity_contracts/src/starknet/StarknetOperator.sol @@ -0,0 +1,28 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./starkware/solidity/components/Operator.sol"; +import "./starkware/solidity/libraries/NamedStorage8.sol"; + +abstract contract StarknetOperator is Operator { + string constant OPERATORS_MAPPING_TAG = "STARKNET_1.0_ROLES_OPERATORS_MAPPING_TAG"; + + function getOperators() internal view override returns (mapping(address => bool) storage) { + return NamedStorage.addressToBoolMapping(OPERATORS_MAPPING_TAG); + } +} diff --git a/solidity_contracts/src/starknet/StarknetState.sol b/solidity_contracts/src/starknet/StarknetState.sol new file mode 100644 index 000000000..87fb04ae4 --- /dev/null +++ b/solidity_contracts/src/starknet/StarknetState.sol @@ -0,0 +1,55 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./Output.sol"; + +library StarknetState { + struct State { + uint256 globalRoot; + int256 blockNumber; + uint256 blockHash; + } + + function copy(State storage state, State memory stateFrom) internal { + state.globalRoot = stateFrom.globalRoot; + state.blockNumber = stateFrom.blockNumber; + state.blockHash = stateFrom.blockHash; + } + + /** + Validates that the 'blockNumber' and the previous root are consistent with the + current state and updates the state. + */ + function update(State storage state, uint256[] calldata starknetOutput) internal { + // Check the blockNumber first as the error is less ambiguous then INVALID_PREVIOUS_ROOT. + state.blockNumber += 1; + require( + uint256(state.blockNumber) == starknetOutput[StarknetOutput.BLOCK_NUMBER_OFFSET], + "INVALID_BLOCK_NUMBER" + ); + + state.blockHash = starknetOutput[StarknetOutput.BLOCK_HASH_OFFSET]; + + uint256[] calldata commitment_tree_update = StarknetOutput.getMerkleUpdate(starknetOutput); + require( + state.globalRoot == CommitmentTreeUpdateOutput.getPrevRoot(commitment_tree_update), + "INVALID_PREVIOUS_ROOT" + ); + state.globalRoot = CommitmentTreeUpdateOutput.getNewRoot(commitment_tree_update); + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/components/Governance.sol b/solidity_contracts/src/starknet/starkware/solidity/components/Governance.sol new file mode 100644 index 000000000..0c03387b7 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/components/Governance.sol @@ -0,0 +1,123 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +import "../interfaces/MGovernance.sol"; + +/* + Implements Generic Governance, applicable for both proxy and main contract, and possibly others. + Notes: + The use of the same function names by both the Proxy and a delegated implementation + is not possible since calling the implementation functions is done via the default function + of the Proxy. For this reason, for example, the implementation of MainContract (MainGovernance) + exposes mainIsGovernor, which calls the internal _isGovernor method. +*/ +struct GovernanceInfoStruct { + mapping(address => bool) effectiveGovernors; + address candidateGovernor; + bool initialized; +} + +abstract contract Governance is MGovernance { + event LogNominatedGovernor(address nominatedGovernor); + event LogNewGovernorAccepted(address acceptedGovernor); + event LogRemovedGovernor(address removedGovernor); + event LogNominationCancelled(); + + function getGovernanceInfo() internal view virtual returns (GovernanceInfoStruct storage); + + /* + Current code intentionally prevents governance re-initialization. + This may be a problem in an upgrade situation, in a case that the upgrade-to implementation + performs an initialization (for real) and within that calls initGovernance(). + + Possible workarounds: + 1. Clearing the governance info altogether by changing the MAIN_GOVERNANCE_INFO_TAG. + This will remove existing main governance information. + 2. Modify the require part in this function, so that it will exit quietly + when trying to re-initialize (uncomment the lines below). + */ + function initGovernance() internal { + GovernanceInfoStruct storage gub = getGovernanceInfo(); + require(!gub.initialized, "ALREADY_INITIALIZED"); + gub.initialized = true; // to ensure acceptNewGovernor() won't fail. + // Add the initial governer. + acceptNewGovernor(msg.sender); + } + + function _isGovernor(address user) internal view override returns (bool) { + GovernanceInfoStruct storage gub = getGovernanceInfo(); + return gub.effectiveGovernors[user]; + } + + /* + Cancels the nomination of a governor candidate. + */ + function _cancelNomination() internal onlyGovernance { + GovernanceInfoStruct storage gub = getGovernanceInfo(); + if (gub.candidateGovernor != address(0x0)) { + gub.candidateGovernor = address(0x0); + emit LogNominationCancelled(); + } + } + + function _nominateNewGovernor(address newGovernor) internal onlyGovernance { + GovernanceInfoStruct storage gub = getGovernanceInfo(); + require(newGovernor != address(0x0), "BAD_ADDRESS"); + require(!_isGovernor(newGovernor), "ALREADY_GOVERNOR"); + require(gub.candidateGovernor == address(0x0), "OTHER_CANDIDATE_PENDING"); + gub.candidateGovernor = newGovernor; + emit LogNominatedGovernor(newGovernor); + } + + /* + The acceptNewGovernor is called in two cases: + 1. by _acceptGovernance when a new governor accepts its role. + 2. by initGovernance to add the initial governor. + The difference is that the init path skips the nominate step + that would fail because of the onlyGovernance modifier. + */ + function acceptNewGovernor(address newGovernor) private { + require(!_isGovernor(newGovernor), "ALREADY_GOVERNOR"); + GovernanceInfoStruct storage gub = getGovernanceInfo(); + gub.effectiveGovernors[newGovernor] = true; + + // Emit governance information. + emit LogNewGovernorAccepted(newGovernor); + } + + function _acceptGovernance() internal { + // The new governor was proposed as a candidate by the current governor. + GovernanceInfoStruct storage gub = getGovernanceInfo(); + require(msg.sender == gub.candidateGovernor, "ONLY_CANDIDATE_GOVERNOR"); + + // Update state. + acceptNewGovernor(msg.sender); + gub.candidateGovernor = address(0x0); + } + + /* + Remove a governor from office. + */ + function _removeGovernor(address governorForRemoval) internal onlyGovernance { + require(msg.sender != governorForRemoval, "GOVERNOR_SELF_REMOVE"); + GovernanceInfoStruct storage gub = getGovernanceInfo(); + require(_isGovernor(governorForRemoval), "NOT_GOVERNOR"); + gub.effectiveGovernors[governorForRemoval] = false; + emit LogRemovedGovernor(governorForRemoval); + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/components/GovernedFinalizable.sol b/solidity_contracts/src/starknet/starkware/solidity/components/GovernedFinalizable.sol new file mode 100644 index 000000000..ebc91603d --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/components/GovernedFinalizable.sol @@ -0,0 +1,44 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "../interfaces/MGovernance.sol"; +import "../libraries/NamedStorage8.sol"; + +/** + A Governor controlled finalizable contract. + The inherited contract (the one that is GovernedFinalizable) implements the Governance. +*/ +abstract contract GovernedFinalizable is MGovernance { + event Finalized(); + + string constant STORAGE_TAG = "STARKWARE_CONTRACTS_GOVERENED_FINALIZABLE_1.0_TAG"; + + function isFinalized() public view returns (bool) { + return NamedStorage.getBoolValue(STORAGE_TAG); + } + + modifier notFinalized() { + require(!isFinalized(), "FINALIZED"); + _; + } + + function finalize() external onlyGovernance notFinalized { + NamedStorage.setBoolValue(STORAGE_TAG, true); + emit Finalized(); + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/components/OnchainDataFactTreeEncoder.sol b/solidity_contracts/src/starknet/starkware/solidity/components/OnchainDataFactTreeEncoder.sol new file mode 100644 index 000000000..fe760984e --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/components/OnchainDataFactTreeEncoder.sol @@ -0,0 +1,72 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +library OnchainDataFactTreeEncoder { + struct DataAvailabilityFact { + uint256 onchainDataHash; + uint256 onchainDataSize; + } + + // The number of additional words appended to the public input when using the + // OnchainDataFactTreeEncoder format. + uint256 internal constant ONCHAIN_DATA_FACT_ADDITIONAL_WORDS = 2; + + /* + Encodes a GPS fact Merkle tree where the root has two children. + The left child contains the data we care about and the right child contains + on-chain data for the fact. + */ + function encodeFactWithOnchainData( + uint256[] calldata programOutput, + DataAvailabilityFact memory factData + ) internal pure returns (bytes32) { + // The state transition fact is computed as a Merkle tree, as defined in + // GpsOutputParser. + // + // In our case the fact tree looks as follows: + // The root has two children. + // The left child is a leaf that includes the main part - the information regarding + // the state transition required by this contract. + // The right child contains the onchain-data which shouldn't be accessed by this + // contract, so we are only given its hash and length + // (it may be a leaf or an inner node, this has no effect on this contract). + + // Compute the hash without the two additional fields. + uint256 mainPublicInputLen = programOutput.length; + bytes32 mainPublicInputHash = hashMainPublicInput(programOutput); + + // Compute the hash of the fact Merkle tree. + bytes32 hashResult = keccak256( + abi.encodePacked( + mainPublicInputHash, + mainPublicInputLen, + factData.onchainDataHash, + mainPublicInputLen + factData.onchainDataSize + ) + ); + // Add one to the hash to indicate it represents an inner node, rather than a leaf. + return bytes32(uint256(hashResult) + 1); + } + + /* + Hashes the main public input. + */ + function hashMainPublicInput(uint256[] calldata programOutput) internal pure returns (bytes32) { + return keccak256(abi.encodePacked(programOutput)); + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/components/Operator.sol b/solidity_contracts/src/starknet/starkware/solidity/components/Operator.sol new file mode 100644 index 000000000..fb94c59e0 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/components/Operator.sol @@ -0,0 +1,48 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +import "../interfaces/MOperator.sol"; +import "../interfaces/MGovernance.sol"; + +/** + The Operator of the contract is the entity entitled to submit state update requests + by calling :sol:func:`updateState`. + + An Operator may be instantly appointed or removed by the contract Governor + (see :sol:mod:`Governance`). Typically, the Operator is the hot wallet of the service + submitting proofs for state updates. +*/ +abstract contract Operator is MGovernance, MOperator { + function registerOperator(address newOperator) external override onlyGovernance { + if (!isOperator(newOperator)) { + getOperators()[newOperator] = true; + emit LogOperatorAdded(newOperator); + } + } + + function unregisterOperator(address removedOperator) external override onlyGovernance { + if (isOperator(removedOperator)) { + getOperators()[removedOperator] = false; + emit LogOperatorRemoved(removedOperator); + } + } + + function isOperator(address user) public view override returns (bool) { + return getOperators()[user]; + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/BlockDirectCall.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/BlockDirectCall.sol new file mode 100644 index 000000000..38726ab3d --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/BlockDirectCall.sol @@ -0,0 +1,36 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +/* + This contract provides means to block direct call of an external function. + A derived contract (e.g. MainDispatcherBase) should decorate sensitive functions with the + notCalledDirectly modifier, thereby preventing it from being called directly, and allowing only + calling using delegate_call. +*/ +abstract contract BlockDirectCall { + address immutable this_; + + constructor() internal { + this_ = address(this); + } + + modifier notCalledDirectly() { + require(this_ != address(this), "DIRECT_CALL_DISALLOWED"); + _; + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/ContractInitializer.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/ContractInitializer.sol new file mode 100644 index 000000000..d687ae749 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/ContractInitializer.sol @@ -0,0 +1,53 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +/** + Interface for contract initialization. + The functions it exposes are the app specific parts of the contract initialization, + and are called by the ProxySupport contract that implement the generic part of behind-proxy + initialization. +*/ +abstract contract ContractInitializer { + /* + The number of sub-contracts that the proxied contract consists of. + */ + function numOfSubContracts() internal pure virtual returns (uint256); + + /* + Indicates if the proxied contract has already been initialized. + Used to prevent re-init. + */ + function isInitialized() internal view virtual returns (bool); + + /* + Validates the init data that is passed into the proxied contract. + */ + function validateInitData(bytes calldata data) internal view virtual; + + /* + For a proxied contract that consists of sub-contracts, this function processes + the sub-contract addresses, e.g. validates them, stores them etc. + */ + function processSubContractAddresses(bytes calldata subContractAddresses) internal virtual; + + /* + This function applies the logic of initializing the proxied contract state, + e.g. setting root values etc. + */ + function initializeContractState(bytes calldata data) internal virtual; +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/IFactRegistry.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/IFactRegistry.sol new file mode 100644 index 000000000..ebe9c608b --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/IFactRegistry.sol @@ -0,0 +1,39 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +/* + The Fact Registry design pattern is a way to separate cryptographic verification from the + business logic of the contract flow. + + A fact registry holds a hash table of verified "facts" which are represented by a hash of claims + that the registry hash check and found valid. This table may be queried by accessing the + isValid() function of the registry with a given hash. + + In addition, each fact registry exposes a registry specific function for submitting new claims + together with their proofs. The information submitted varies from one registry to the other + depending of the type of fact requiring verification. + + For further reading on the Fact Registry design pattern see this + `StarkWare blog post `_. +*/ +interface IFactRegistry { + /* + Returns true if the given fact was previously registered in the contract. + */ + function isValid(bytes32 fact) external view returns (bool); +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/Identity.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/Identity.sol new file mode 100644 index 000000000..c3d0177fb --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/Identity.sol @@ -0,0 +1,24 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +interface Identity { + /* + Allows a caller to ensure that the provided address is of the expected type and version. + */ + function identify() external pure returns (string memory); +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/MGovernance.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/MGovernance.sol new file mode 100644 index 000000000..5433b62f6 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/MGovernance.sol @@ -0,0 +1,29 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +abstract contract MGovernance { + function _isGovernor(address user) internal view virtual returns (bool); + + /* + Allows calling the function only by a Governor. + */ + modifier onlyGovernance() { + require(_isGovernor(msg.sender), "ONLY_GOVERNANCE"); + _; + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/MOperator.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/MOperator.sol new file mode 100644 index 000000000..f3a9268c7 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/MOperator.sol @@ -0,0 +1,37 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +import "./MGovernance.sol"; + +abstract contract MOperator { + event LogOperatorAdded(address operator); + event LogOperatorRemoved(address operator); + + function isOperator(address user) public view virtual returns (bool); + + modifier onlyOperator() { + require(isOperator(msg.sender), "ONLY_OPERATOR"); + _; + } + + function registerOperator(address newOperator) external virtual; + + function unregisterOperator(address removedOperator) external virtual; + + function getOperators() internal view virtual returns (mapping(address => bool) storage); +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/interfaces/ProxySupport.sol b/solidity_contracts/src/starknet/starkware/solidity/interfaces/ProxySupport.sol new file mode 100644 index 000000000..df4ecb5e4 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/interfaces/ProxySupport.sol @@ -0,0 +1,93 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +import "../components/Governance.sol"; +import "../libraries/Addresses.sol"; +import "./BlockDirectCall.sol"; +import "./ContractInitializer.sol"; + +/** + This contract contains the code commonly needed for a contract to be deployed behind + an upgradability proxy. + It perform the required semantics of the proxy pattern, + but in a generic manner. + Instantiation of the Governance and of the ContractInitializer, that are the app specific + part of initialization, has to be done by the using contract. +*/ +abstract contract ProxySupport is Governance, BlockDirectCall, ContractInitializer { + using Addresses for address; + + // The two function below (isFrozen & initialize) needed to bind to the Proxy. + function isFrozen() external view virtual returns (bool) { + return false; + } + + /* + The initialize() function serves as an alternative constructor for a proxied deployment. + + Flow and notes: + 1. This function cannot be called directly on the deployed contract, but only via + delegate call. + 2. If an EIC is provided - init is passed onto EIC and the standard init flow is skipped. + This true for both first intialization or a later one. + 3. The data passed to this function is as follows: + [sub_contracts addresses, eic address, initData]. + + When calling on an initialized contract (no EIC scenario), initData.length must be 0. + */ + function initialize(bytes calldata data) external notCalledDirectly { + uint256 eicOffset = 32 * numOfSubContracts(); + uint256 expectedBaseSize = eicOffset + 32; + require(data.length >= expectedBaseSize, "INIT_DATA_TOO_SMALL"); + address eicAddress = abi.decode(data[eicOffset:expectedBaseSize], (address)); + + bytes calldata subContractAddresses = data[:eicOffset]; + + processSubContractAddresses(subContractAddresses); + + bytes calldata initData = data[expectedBaseSize:]; + + // EIC Provided - Pass initData to EIC and the skip standard init flow. + if (eicAddress != address(0x0)) { + callExternalInitializer(eicAddress, initData); + return; + } + + if (isInitialized()) { + require(initData.length == 0, "UNEXPECTED_INIT_DATA"); + } else { + // Contract was not initialized yet. + validateInitData(initData); + initializeContractState(initData); + initGovernance(); + } + } + + function callExternalInitializer(address externalInitializerAddr, bytes calldata eicData) + private + { + require(externalInitializerAddr.isContract(), "EIC_NOT_A_CONTRACT"); + + // NOLINTNEXTLINE: low-level-calls, controlled-delegatecall. + (bool success, bytes memory returndata) = externalInitializerAddr.delegatecall( + abi.encodeWithSelector(this.initialize.selector, eicData) + ); + require(success, string(returndata)); + require(returndata.length == 0, string(returndata)); + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/libraries/Addresses.sol b/solidity_contracts/src/starknet/starkware/solidity/libraries/Addresses.sol new file mode 100644 index 000000000..13cd0c100 --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/libraries/Addresses.sol @@ -0,0 +1,58 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.24; + +/* + Common Utility Libraries. + I. Addresses (extending address). +*/ +library Addresses { + /* + Note: isContract function has some known limitation. + See https://github.com/OpenZeppelin/ + openzeppelin-contracts/blob/master/contracts/utils/Address.sol. + */ + function isContract(address account) internal view returns (bool) { + uint256 size; + assembly { + size := extcodesize(account) + } + return size > 0; + } + + function performEthTransfer(address recipient, uint256 amount) internal { + if (amount == 0) return; + (bool success, ) = recipient.call{value: amount}(""); // NOLINT: low-level-calls. + require(success, "ETH_TRANSFER_FAILED"); + } + + /* + Safe wrapper around ERC20/ERC721 calls. + This is required because many deployed ERC20 contracts don't return a value. + See https://github.com/ethereum/solidity/issues/4116. + */ + function safeTokenContractCall(address tokenAddress, bytes memory callData) internal { + require(isContract(tokenAddress), "BAD_TOKEN_ADDRESS"); + // NOLINTNEXTLINE: low-level-calls. + (bool success, bytes memory returndata) = tokenAddress.call(callData); + require(success, string(returndata)); + + if (returndata.length > 0) { + require(abi.decode(returndata, (bool)), "TOKEN_OPERATION_FAILED"); + } + } +} diff --git a/solidity_contracts/src/starknet/starkware/solidity/libraries/NamedStorage8.sol b/solidity_contracts/src/starknet/starkware/solidity/libraries/NamedStorage8.sol new file mode 100644 index 000000000..53fb5c3ca --- /dev/null +++ b/solidity_contracts/src/starknet/starkware/solidity/libraries/NamedStorage8.sol @@ -0,0 +1,131 @@ +/* + Copyright 2019-2024 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +/* + Library to provide basic storage, in storage location out of the low linear address space. + + New types of storage variables should be added here upon need. +*/ +library NamedStorage { + function bytes32ToUint256Mapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => uint256) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function bytes32ToAddressMapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function uintToAddressMapping(string memory tag_) + internal + pure + returns (mapping(uint256 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function addressToAddressMapping(string memory tag_) + internal + pure + returns (mapping(address => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function addressToBoolMapping(string memory tag_) + internal + pure + returns (mapping(address => bool) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function getUintValue(string memory tag_) internal view returns (uint256 retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setUintValue(string memory tag_, uint256 value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setUintValueOnce(string memory tag_, uint256 value) internal { + require(getUintValue(tag_) == 0, "ALREADY_SET"); + setUintValue(tag_, value); + } + + function getAddressValue(string memory tag_) internal view returns (address retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setAddressValue(string memory tag_, address value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setAddressValueOnce(string memory tag_, address value) internal { + require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); + setAddressValue(tag_, value); + } + + function getBoolValue(string memory tag_) internal view returns (bool retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setBoolValue(string memory tag_, bool value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } +} diff --git a/tests/end_to_end/L1L2Messaging/test_messaging.py b/tests/end_to_end/L1L2Messaging/test_messaging.py index 36d02b2c6..e29506386 100644 --- a/tests/end_to_end/L1L2Messaging/test_messaging.py +++ b/tests/end_to_end/L1L2Messaging/test_messaging.py @@ -106,6 +106,7 @@ async def _factory(): return _factory +@pytest.mark.skip @pytest.mark.slow @pytest.mark.asyncio(scope="module") class TestL2ToL1Messages: