From d1e727c571781c966d1ac1b0699281ca4ccde272 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Fri, 11 Apr 2025 20:32:14 +0200 Subject: [PATCH 1/5] EVM Node: Duplicate Calypso kernel to prepare Calypso2 native execution --- etherlink/kernel_calypso2/.rustfmt.toml | 7 + etherlink/kernel_calypso2/Cargo.lock | 2827 +++++++++ etherlink/kernel_calypso2/Cargo.toml | 100 + etherlink/kernel_calypso2/Makefile | 64 + etherlink/kernel_calypso2/YesEVM.md | 72 + etherlink/kernel_calypso2/ethereum/Cargo.toml | 30 + .../ethereum/src/access_list.rs | 48 + .../kernel_calypso2/ethereum/src/block.rs | 411 ++ .../kernel_calypso2/ethereum/src/eth_gen.rs | 6 + .../kernel_calypso2/ethereum/src/helpers.rs | 21 + etherlink/kernel_calypso2/ethereum/src/lib.rs | 16 + .../ethereum/src/rlp_helpers.rs | 378 ++ .../ethereum/src/transaction.rs | 448 ++ .../kernel_calypso2/ethereum/src/tx_common.rs | 1665 ++++++ .../ethereum/src/tx_signature.rs | 250 + etherlink/kernel_calypso2/ethereum/src/wei.rs | 41 + .../kernel_calypso2/evm_execution/Cargo.toml | 76 + .../kernel_calypso2/evm_execution/src/abi.rs | 91 + .../evm_execution/src/access_record.rs | 38 + .../evm_execution/src/account_storage.rs | 1365 +++++ .../evm_execution/src/code_storage.rs | 248 + .../evm_execution/src/fa_bridge/deposit.rs | 411 ++ .../evm_execution/src/fa_bridge/error.rs | 34 + .../evm_execution/src/fa_bridge/mod.rs | 414 ++ .../evm_execution/src/fa_bridge/test_utils.rs | 440 ++ .../evm_execution/src/fa_bridge/tests.rs | 522 ++ .../src/fa_bridge/ticket_table.rs | 150 + .../evm_execution/src/fa_bridge/withdrawal.rs | 443 ++ .../evm_execution/src/handler.rs | 5094 +++++++++++++++++ .../kernel_calypso2/evm_execution/src/lib.rs | 4037 +++++++++++++ .../evm_execution/src/precompiles/blake2.rs | 325 ++ .../evm_execution/src/precompiles/ecdsa.rs | 350 ++ .../src/precompiles/fa_bridge.rs | 538 ++ .../evm_execution/src/precompiles/hash.rs | 158 + .../evm_execution/src/precompiles/identity.rs | 48 + .../evm_execution/src/precompiles/mod.rs | 392 ++ .../evm_execution/src/precompiles/modexp.rs | 445 ++ .../src/precompiles/reentrancy_guard.rs | 71 + .../evm_execution/src/precompiles/revert.rs | 28 + .../src/precompiles/withdrawal.rs | 949 +++ .../src/precompiles/zero_knowledge.rs | 595 ++ .../evm_execution/src/storage.rs | 207 + .../evm_execution/src/tick_model_opcodes.rs | 1244 ++++ .../evm_execution/src/trace.rs | 513 ++ .../evm_execution/src/transaction.rs | 41 + .../src/transaction_layer_data.rs | 58 + .../evm_execution/src/utilities.rs | 100 + .../evm_execution/src/withdrawal_counter.rs | 87 + .../indexable_storage/Cargo.toml | 19 + .../indexable_storage/src/lib.rs | 216 + etherlink/kernel_calypso2/kernel/Cargo.toml | 83 + etherlink/kernel_calypso2/kernel/build.rs | 18 + etherlink/kernel_calypso2/kernel/src/apply.rs | 1042 ++++ etherlink/kernel_calypso2/kernel/src/block.rs | 1961 +++++++ .../kernel/src/block_in_progress.rs | 715 +++ .../kernel/src/block_storage.rs | 141 + .../kernel_calypso2/kernel/src/blueprint.rs | 99 + .../kernel/src/blueprint_storage.rs | 660 +++ .../kernel_calypso2/kernel/src/bridge.rs | 390 ++ .../kernel/src/configuration.rs | 251 + etherlink/kernel_calypso2/kernel/src/dal.rs | 601 ++ .../kernel/src/dal_slot_import_signal.rs | 371 ++ .../kernel/src/delayed_inbox.rs | 470 ++ etherlink/kernel_calypso2/kernel/src/error.rs | 227 + etherlink/kernel_calypso2/kernel/src/event.rs | 76 + .../kernel/src/evm_node_entrypoint.rs | 30 + .../kernel/src/fallback_upgrade.rs | 50 + etherlink/kernel_calypso2/kernel/src/fees.rs | 585 ++ .../kernel_calypso2/kernel/src/gas_price.rs | 246 + etherlink/kernel_calypso2/kernel/src/inbox.rs | 1477 +++++ etherlink/kernel_calypso2/kernel/src/lib.rs | 1016 ++++ .../kernel_calypso2/kernel/src/linked_list.rs | 984 ++++ .../kernel_calypso2/kernel/src/migration.rs | 292 + .../kernel_calypso2/kernel/src/parsing.rs | 955 +++ .../kernel/src/reveal_storage.rs | 110 + .../kernel/src/sequencer_blueprint.rs | 347 ++ .../kernel_calypso2/kernel/src/simulation.rs | 1031 ++++ .../kernel_calypso2/kernel/src/stage_one.rs | 908 +++ .../kernel_calypso2/kernel/src/storage.rs | 973 ++++ .../kernel_calypso2/kernel/src/tick_model.rs | 182 + .../kernel_calypso2/kernel/src/upgrade.rs | 243 + etherlink/kernel_calypso2/logging/Cargo.toml | 20 + etherlink/kernel_calypso2/logging/src/lib.rs | 65 + etherlink/kernel_calypso2/runtime/Cargo.toml | 19 + .../kernel_calypso2/runtime/src/extensions.rs | 10 + .../runtime/src/internal_runtime.rs | 58 + etherlink/kernel_calypso2/runtime/src/lib.rs | 9 + .../runtime/src/mock_internal.rs | 18 + .../kernel_calypso2/runtime/src/runtime.rs | 343 ++ .../runtime/src/safe_storage.rs | 266 + etherlink/kernel_calypso2/storage/Cargo.toml | 26 + .../kernel_calypso2/storage/src/error.rs | 39 + .../kernel_calypso2/storage/src/helpers.rs | 11 + etherlink/kernel_calypso2/storage/src/lib.rs | 242 + etherlink/lib_wasm_runtime/Cargo.lock | 145 +- etherlink/lib_wasm_runtime/Cargo.toml | 3 + etherlink/lib_wasm_runtime/dune | 1 + etherlink/lib_wasm_runtime/src/host.rs | 11 + etherlink/lib_wasm_runtime/src/runtime/mod.rs | 15 + manifest/product_etherlink.ml | 1 + 100 files changed, 43966 insertions(+), 1 deletion(-) create mode 100644 etherlink/kernel_calypso2/.rustfmt.toml create mode 100644 etherlink/kernel_calypso2/Cargo.lock create mode 100644 etherlink/kernel_calypso2/Cargo.toml create mode 100644 etherlink/kernel_calypso2/Makefile create mode 100644 etherlink/kernel_calypso2/YesEVM.md create mode 100644 etherlink/kernel_calypso2/ethereum/Cargo.toml create mode 100644 etherlink/kernel_calypso2/ethereum/src/access_list.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/block.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/eth_gen.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/helpers.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/lib.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/rlp_helpers.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/transaction.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/tx_common.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/tx_signature.rs create mode 100644 etherlink/kernel_calypso2/ethereum/src/wei.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/Cargo.toml create mode 100644 etherlink/kernel_calypso2/evm_execution/src/abi.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/access_record.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/account_storage.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/code_storage.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/deposit.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/error.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/tests.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/ticket_table.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/fa_bridge/withdrawal.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/handler.rs create mode 100755 etherlink/kernel_calypso2/evm_execution/src/lib.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/blake2.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/ecdsa.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/fa_bridge.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/hash.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/identity.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/mod.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/modexp.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/reentrancy_guard.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/revert.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/withdrawal.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/precompiles/zero_knowledge.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/storage.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/tick_model_opcodes.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/trace.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/transaction.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/transaction_layer_data.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/utilities.rs create mode 100644 etherlink/kernel_calypso2/evm_execution/src/withdrawal_counter.rs create mode 100644 etherlink/kernel_calypso2/indexable_storage/Cargo.toml create mode 100644 etherlink/kernel_calypso2/indexable_storage/src/lib.rs create mode 100644 etherlink/kernel_calypso2/kernel/Cargo.toml create mode 100644 etherlink/kernel_calypso2/kernel/build.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/apply.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/block.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/block_in_progress.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/block_storage.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/blueprint.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/blueprint_storage.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/bridge.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/configuration.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/dal.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/dal_slot_import_signal.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/delayed_inbox.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/error.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/event.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/evm_node_entrypoint.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/fallback_upgrade.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/fees.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/gas_price.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/inbox.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/lib.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/linked_list.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/migration.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/parsing.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/reveal_storage.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/sequencer_blueprint.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/simulation.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/stage_one.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/storage.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/tick_model.rs create mode 100644 etherlink/kernel_calypso2/kernel/src/upgrade.rs create mode 100644 etherlink/kernel_calypso2/logging/Cargo.toml create mode 100644 etherlink/kernel_calypso2/logging/src/lib.rs create mode 100644 etherlink/kernel_calypso2/runtime/Cargo.toml create mode 100644 etherlink/kernel_calypso2/runtime/src/extensions.rs create mode 100644 etherlink/kernel_calypso2/runtime/src/internal_runtime.rs create mode 100644 etherlink/kernel_calypso2/runtime/src/lib.rs create mode 100644 etherlink/kernel_calypso2/runtime/src/mock_internal.rs create mode 100644 etherlink/kernel_calypso2/runtime/src/runtime.rs create mode 100644 etherlink/kernel_calypso2/runtime/src/safe_storage.rs create mode 100644 etherlink/kernel_calypso2/storage/Cargo.toml create mode 100644 etherlink/kernel_calypso2/storage/src/error.rs create mode 100644 etherlink/kernel_calypso2/storage/src/helpers.rs create mode 100644 etherlink/kernel_calypso2/storage/src/lib.rs diff --git a/etherlink/kernel_calypso2/.rustfmt.toml b/etherlink/kernel_calypso2/.rustfmt.toml new file mode 100644 index 000000000000..028d31d515b8 --- /dev/null +++ b/etherlink/kernel_calypso2/.rustfmt.toml @@ -0,0 +1,7 @@ +# SPDX-FileCopyrightText: 2022 TriliTech +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# +# SPDX-License-Identifier: MIT + +max_width=90 +newline_style="Unix" diff --git a/etherlink/kernel_calypso2/Cargo.lock b/etherlink/kernel_calypso2/Cargo.lock new file mode 100644 index 000000000000..ffd7ca536f2f --- /dev/null +++ b/etherlink/kernel_calypso2/Cargo.lock @@ -0,0 +1,2827 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 4 + +[[package]] +name = "adler" +version = "1.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f26201604c87b1e01bd3d98f8d5d9a8fcbb815e8cedb41ffccbeb4bf593a35fe" + +[[package]] +name = "aho-corasick" +version = "1.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2969dcb958b36655471fc61f7e416fa76033bdd4bfed0678d8fee1e2d07a1f0" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-json-abi" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc05b04ac331a9f07e3a4036ef7926e49a8bf84a99a1ccfc7e2ab55a5fcbb372" +dependencies = [ + "alloy-primitives", + "alloy-sol-type-parser", + "serde", +] + +[[package]] +name = "alloy-primitives" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ccb3ead547f4532bc8af961649942f0b9c16ee9226e26caa3f38420651cc0bf4" +dependencies = [ + "bytes", + "cfg-if", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "ruint", + "serde", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b40397ddcdcc266f59f959770f601ce1280e699a91fc1862f29cef91707cd09" +dependencies = [ + "alloy-sol-macro-expander", + "alloy-sol-macro-input", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "alloy-sol-macro-expander" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "867a5469d61480fea08c7333ffeca52d5b621f5ca2e44f271b117ec1fc9a0525" +dependencies = [ + "alloy-json-abi", + "alloy-sol-macro-input", + "const-hex", + "heck 0.5.0", + "indexmap 2.7.1", + "proc-macro-error", + "proc-macro2", + "quote", + "syn 2.0.87", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro-input" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e482dc33a32b6fadbc0f599adea520bd3aaa585c141a80b404d0a3e3fa72528" +dependencies = [ + "alloy-json-abi", + "const-hex", + "dunce", + "heck 0.5.0", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.87", + "syn-solidity", +] + +[[package]] +name = "alloy-sol-type-parser" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cbcba3ca07cf7975f15d871b721fb18031eec8bce51103907f6dcce00b255d98" +dependencies = [ + "serde", + "winnow 0.6.13", +] + +[[package]] +name = "alloy-sol-types" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a91ca40fa20793ae9c3841b83e74569d1cc9af29a2f5237314fd3452d51e38c7" +dependencies = [ + "alloy-json-abi", + "alloy-primitives", + "alloy-sol-macro", + "const-hex", +] + +[[package]] +name = "anyhow" +version = "1.0.69" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "224afbd727c3d6e4b90103ece64b8d1b67fbb1973b1046c2281eed3f3803f800" + +[[package]] +name = "arrayref" +version = "0.3.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4c527152e37cf757a3f78aae5a06fbeefdb07ccc535c980a3208ee3060dd544" + +[[package]] +name = "arrayvec" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8da52d66c7071e2e3fa2a1e5c6d088fec47b593032b254f5e980de8ea54454d6" + +[[package]] +name = "aurora-engine-modexp" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfacad86e9e138fca0670949eb8ed4ffdf73a55bded8887efe0863cd1a3a6f70" +dependencies = [ + "hex", + "num", +] + +[[package]] +name = "auto_impl" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fee3da8ef1276b0bee5dd1c7258010d8fffd31801447323115a25560e1327b89" +dependencies = [ + "proc-macro-error", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "autocfg" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d468802bab17cbc0cc575e9b053f41e72aa36bfa6b7f55e3529ffa43161b97fa" + +[[package]] +name = "base64" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9e1b586273c5702936fe7b7d6896644d8be71e6314cfe09d3167c95f712589e8" + +[[package]] +name = "base64" +version = "0.21.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d297deb1925b89f2ccc13d7635fa0714f12c87adce1c75356b39ca9b7178567" + +[[package]] +name = "bit-set" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1" +dependencies = [ + "bit-vec", +] + +[[package]] +name = "bit-vec" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" + +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + +[[package]] +name = "bitflags" +version = "2.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf" + +[[package]] +name = "bitvec" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" +dependencies = [ + "funty", + "radium", + "tap", + "wyz", +] + +[[package]] +name = "block-buffer" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" +dependencies = [ + "generic-array", +] + +[[package]] +name = "block-buffer" +version = "0.10.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cce20737498f97b993470a6e536b8523f0af7892a4f928cceb1ac5e52ebe7e" +dependencies = [ + "generic-array", +] + +[[package]] +name = "bs58" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "byte-slice-cast" +version = "1.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c3ac9f8b63eca6fd385229b3675f6cc0dc5c8a5c8a54a59d4f52ffd670d87b0c" + +[[package]] +name = "byteorder" +version = "1.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" + +[[package]] +name = "bytes" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" +dependencies = [ + "serde", +] + +[[package]] +name = "cc" +version = "1.2.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "755717a7de9ec452bf7f3f1a3099085deabd7f2962b861dae91ecd7a365903d2" +dependencies = [ + "shlex", +] + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-decoder" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5241cd7938b1b415942e943ea96f615953d500b50347b505b0b507080bad5a6f" + +[[package]] +name = "const-hex" +version = "1.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b0485bab839b018a8f1723fc5391819fea5f8f0f32288ef8a735fd096b6160c" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "proptest", + "serde", +] + +[[package]] +name = "convert_case" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" + +[[package]] +name = "cpufeatures" +version = "0.2.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce420fe07aecd3e67c5f910618fe65e94158f6dcc0adf44e00d69ce2bdfe0fd0" +dependencies = [ + "libc", +] + +[[package]] +name = "crc32fast" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b540bd8bc810d3885c6ea91e2018302f68baba2129ab3e88f32389ee9370880d" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[package]] +name = "crypto-bigint" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8658c15c5d921ddf980f7fe25b1e82f4b7a4083b2c4985fea4922edb8e43e07d" +dependencies = [ + "generic-array", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "crypto-common" +version = "0.1.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" +dependencies = [ + "generic-array", + "typenum", +] + +[[package]] +name = "crypto-mac" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b584a330336237c1eecd3e94266efb216c56ed91225d634cb2991c5f3fd1aeab" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "crypto-mac" +version = "0.11.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1d1a86f49236c215f271d40892d5fc950490551400b02ef360692c29815c714" +dependencies = [ + "generic-array", + "subtle", +] + +[[package]] +name = "cryptoxide" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "382ce8820a5bb815055d3553a610e8cb542b2d767bbacea99038afda96cd760d" + +[[package]] +name = "curve25519-dalek" +version = "4.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97fb8b7c4503de7d6ae7b42ab72a5a59857b4c937ec27a3d4539dba95b5ab2be" +dependencies = [ + "cfg-if", + "cpufeatures", + "curve25519-dalek-derive", + "digest 0.10.6", + "fiat-crypto", + "rustc_version", + "subtle", +] + +[[package]] +name = "curve25519-dalek-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f46882e17999c6cc590af592290432be3bce0428cb0d5f8b6715e4dc7b383eb3" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "der" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "79b71cca7d95d7681a4b3b9cdf63c8dbc3730d0584c2c74e31416d64a90493f4" + +[[package]] +name = "derive_more" +version = "0.99.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4fb810d30a7c1953f91334de7244731fc3f3c10d7fe163338a35b9f640960321" +dependencies = [ + "convert_case", + "proc-macro2", + "quote", + "rustc_version", + "syn 1.0.109", +] + +[[package]] +name = "diff" +version = "0.1.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56254986775e3233ffa9c4d7d3faaf6d36a2c09d30b20687e9f88bc8bafc16c8" + +[[package]] +name = "digest" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" +dependencies = [ + "generic-array", +] + +[[package]] +name = "digest" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8168378f4e5023e7218c89c891c0fd8ecdb5e5e4f18cb78f38cf245dd021e76f" +dependencies = [ + "block-buffer 0.10.3", + "crypto-common", +] + +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + +[[package]] +name = "dirs-sys" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "520f05a5cbd335fae5a99ff7a6ab8627577660ee5cfd6a94a6a929b52ff0321c" +dependencies = [ + "libc", + "option-ext", + "redox_users", + "windows-sys 0.48.0", +] + +[[package]] +name = "dlmalloc" +version = "0.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "203540e710bfadb90e5e29930baf5d10270cec1f43ab34f46f78b147b2de715a" +dependencies = [ + "libc", +] + +[[package]] +name = "dunce" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ce8c6da7551ec6c462cbaf3bfbc75131ebbfa1c944aeaa9dab51ca1c5f0c3b" + +[[package]] +name = "ecdsa" +version = "0.12.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43ee23aa5b4f68c7a092b5c3beb25f50c406adc75e2363634f242f28ab255372" +dependencies = [ + "der", + "elliptic-curve", + "hmac 0.11.0", + "signature 1.3.2", +] + +[[package]] +name = "ed25519" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5fb04eee5d9d907f29e80ee6b0e78f7e2c82342c63e3580d8c4f69d9d5aad963" +dependencies = [ + "signature 2.1.0", +] + +[[package]] +name = "ed25519-dalek" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f628eaec48bfd21b865dc2950cfa014450c01d2fa2b69a86c2fd5844ec523c0" +dependencies = [ + "curve25519-dalek", + "ed25519", + "sha2 0.10.6", + "subtle", +] + +[[package]] +name = "elliptic-curve" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "83e5c176479da93a0983f0a6fdc3c1b8e7d5be0d7fe3fe05a99f15b96582b9a8" +dependencies = [ + "crypto-bigint", + "ff", + "generic-array", + "group", + "rand_core 0.6.4", + "subtle", + "zeroize", +] + +[[package]] +name = "equivalent" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" + +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno" +version = "0.3.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a258e46cdc063eb8519c00b9fc845fc47bcfca4130e2f08e88665ceda8474245" +dependencies = [ + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + +[[package]] +name = "ethbloom" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" +dependencies = [ + "crunchy", + "fixed-hash", + "impl-codec", + "impl-rlp", + "scale-info", + "tiny-keccak", +] + +[[package]] +name = "ethereum" +version = "0.14.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a89fb87a9e103f71b903b80b670200b54cc67a07578f070681f1fffb7396fb7" +dependencies = [ + "bytes", + "ethereum-types", + "hash-db", + "hash256-std-hasher", + "rlp", + "sha3", + "triehash", +] + +[[package]] +name = "ethereum-types" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" +dependencies = [ + "ethbloom", + "fixed-hash", + "impl-codec", + "impl-rlp", + "primitive-types", + "scale-info", + "uint", +] + +[[package]] +name = "evm" +version = "0.39.1" +dependencies = [ + "auto_impl", + "ethereum", + "evm-core", + "evm-gasometer", + "evm-runtime", + "log", + "primitive-types", + "rlp", + "sha3", +] + +[[package]] +name = "evm-core" +version = "0.39.0" +dependencies = [ + "primitive-types", +] + +[[package]] +name = "evm-execution-calypso2" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "aurora-engine-modexp", + "const-decoder", + "evm", + "hex", + "libsecp256k1", + "num-bigint", + "num-traits", + "pretty_assertions", + "primitive-types", + "proptest", + "rand 0.8.5", + "ripemd", + "rlp", + "sha2 0.10.6", + "sha3", + "substrate-bn", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-indexable-storage-calypso2", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "tezos_crypto_rs", + "tezos_data_encoding", + "tezos_ethereum_calypso2", + "thiserror", +] + +[[package]] +name = "evm-gasometer" +version = "0.39.0" +dependencies = [ + "evm-core", + "evm-runtime", + "primitive-types", +] + +[[package]] +name = "evm-runtime" +version = "0.39.0" +dependencies = [ + "auto_impl", + "evm-core", + "primitive-types", + "sha3", +] + +[[package]] +name = "evm_kernel_calypso2" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "anyhow", + "bytes", + "ethbloom", + "ethereum", + "evm", + "evm-execution-calypso2", + "getrandom", + "hex", + "libsecp256k1", + "num-derive", + "num-traits", + "pretty_assertions", + "primitive-types", + "proptest", + "rlp", + "sha3", + "softfloat", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-indexable-storage-calypso2", + "tezos-smart-rollup", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-entrypoint", + "tezos-smart-rollup-host", + "tezos-smart-rollup-installer-config", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-panic-hook", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "tezos_crypto_rs", + "tezos_data_encoding", + "tezos_ethereum_calypso2", + "thiserror", +] + +[[package]] +name = "fastrand" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e51093e27b0797c359783294ca4f0a911c270184cb10f85783b118614a1501be" +dependencies = [ + "instant", +] + +[[package]] +name = "ff" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d0f40b2dcd8bc322217a5f6559ae5f9e9d1de202a2ecee2e9eafcbece7562a4f" +dependencies = [ + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "fiat-crypto" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "27573eac26f4dd11e2b1916c3fe1baa56407c83c71a773a8ba17ec0bca03b6b7" + +[[package]] +name = "filetime" +version = "0.2.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ee447700ac8aa0b2f2bd7bc4462ad686ba06baa6727ac149a2d6277f0d240fd" +dependencies = [ + "cfg-if", + "libc", + "redox_syscall 0.4.1", + "windows-sys 0.52.0", +] + +[[package]] +name = "fixed-hash" +version = "0.8.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" +dependencies = [ + "byteorder", + "rustc-hex", + "static_assertions", +] + +[[package]] +name = "flate2" +version = "1.0.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "46303f565772937ffe1d394a4fac6f411c6013172fadde9dcdb1e147a086940e" +dependencies = [ + "crc32fast", + "miniz_oxide", +] + +[[package]] +name = "fnv" +version = "1.0.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" + +[[package]] +name = "form_urlencoded" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" +dependencies = [ + "percent-encoding", +] + +[[package]] +name = "funty" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" + +[[package]] +name = "generic-array" +version = "0.14.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bff49e947297f3312447abdca79f45f4738097cc82b06e72054d2223f601f1b9" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "getrandom" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" +dependencies = [ + "cfg-if", + "libc", + "wasi", +] + +[[package]] +name = "group" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1c363a5301b8f153d80747126a04b3c82073b9fe3130571a9d170cacdeaf7912" +dependencies = [ + "ff", + "rand_core 0.6.4", + "subtle", +] + +[[package]] +name = "hash-db" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d23bd4e7b5eda0d0f3a307e8b381fdc8ba9000f26fbe912250c0a4cc3956364a" + +[[package]] +name = "hash256-std-hasher" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92c171d55b98633f4ed3860808f004099b36c1cc29c42cfc53aa8591b21efcf2" +dependencies = [ + "crunchy", +] + +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" + +[[package]] +name = "hashbrown" +version = "0.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" + +[[package]] +name = "heck" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d621efb26863f0e9924c6ac577e8275e5e6b77455db64ffa6c65c904e9e132c" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" + +[[package]] +name = "hermit" +version = "0.7.2" +source = "git+https://github.com/hermit-os/hermit-rs.git?rev=01df75880c9be03fe935c3faabd191082d8916a2#01df75880c9be03fe935c3faabd191082d8916a2" +dependencies = [ + "flate2", + "tar", + "ureq", +] + +[[package]] +name = "hermit-abi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d231dfb89cfffdbc30e7fc41579ed6066ad03abda9e567ccafae602b97ec5024" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" +dependencies = [ + "serde", +] + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hmac" +version = "0.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "126888268dcc288495a26bf004b38c5fdbb31682f992c84ceb046a1f0fe38840" +dependencies = [ + "crypto-mac 0.8.0", + "digest 0.9.0", +] + +[[package]] +name = "hmac" +version = "0.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a2a2320eb7ec0ebe8da8f744d7812d9fc4cb4d09344ac01898dbcb6a20ae69b" +dependencies = [ + "crypto-mac 0.11.1", + "digest 0.9.0", +] + +[[package]] +name = "hmac-drbg" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "17ea0a1394df5b6574da6e0c1ade9e78868c9fb0a4e5ef4428e32da4676b85b1" +dependencies = [ + "digest 0.9.0", + "generic-array", + "hmac 0.8.1", +] + +[[package]] +name = "idna" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "634d9b1461af396cad843f47fdba5597a4f9e6ddd4bfb6ff5d85028c25cb12f6" +dependencies = [ + "unicode-bidi", + "unicode-normalization", +] + +[[package]] +name = "impl-codec" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba6a270039626615617f3f36d15fc827041df3b78c439da2cadfa47455a77f2f" +dependencies = [ + "parity-scale-codec", +] + +[[package]] +name = "impl-rlp" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28220f89297a075ddc7245cd538076ee98b01f2a9c23a53a4f1105d5a322808" +dependencies = [ + "rlp", +] + +[[package]] +name = "impl-trait-for-tuples" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11d7a9f6330b71fea57921c9b61c47ee6e84f72d394754eff6163ae67e7395eb" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "indexmap" +version = "1.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd070e393353796e801d209ad339e89596eb4c8d430d18ede6a1cced8fafbd99" +dependencies = [ + "autocfg", + "hashbrown 0.12.3", +] + +[[package]] +name = "indexmap" +version = "2.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c9c992b02b5b4c94ea26e32fe5bccb7aa7d9f390ab5c1221ff895bc7ea8b652" +dependencies = [ + "equivalent", + "hashbrown 0.15.2", +] + +[[package]] +name = "instant" +version = "0.1.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "io-lifetimes" +version = "1.0.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09270fd4fa1111bc614ed2246c7ef56239a3063d5be0d1ec3b589c505d400aeb" +dependencies = [ + "hermit-abi", + "libc", + "windows-sys 0.45.0", +] + +[[package]] +name = "itoa" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fad582f4b9e86b6caa621cabeb0963332d92eea04729ab12892c2533951e6440" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "lazy_static" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" +dependencies = [ + "spin 0.5.2", +] + +[[package]] +name = "libc" +version = "0.2.155" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c" + +[[package]] +name = "libm" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "348108ab3fba42ec82ff6e9564fc4ca0247bdccdc68dd8af9764bbc79c3c8ffb" + +[[package]] +name = "libredox" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0ff37bd590ca25063e35af745c343cb7a0271906fb7b37e4813e8f79f00268d" +dependencies = [ + "bitflags 2.4.2", + "libc", +] + +[[package]] +name = "libsecp256k1" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95b09eff1b35ed3b33b877ced3a691fc7a481919c7e29c53c906226fcf55e2a1" +dependencies = [ + "arrayref", + "base64 0.13.1", + "digest 0.9.0", + "hmac-drbg", + "libsecp256k1-core", + "libsecp256k1-gen-ecmult", + "libsecp256k1-gen-genmult", + "rand 0.8.5", + "serde", + "sha2 0.9.9", + "typenum", +] + +[[package]] +name = "libsecp256k1-core" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5be9b9bb642d8522a44d533eab56c16c738301965504753b03ad1de3425d5451" +dependencies = [ + "crunchy", + "digest 0.9.0", + "subtle", +] + +[[package]] +name = "libsecp256k1-gen-ecmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3038c808c55c87e8a172643a7d87187fc6c4174468159cb3090659d55bcb4809" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "libsecp256k1-gen-genmult" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8d6ba2cec9eacc40e6e8ccc98931840301f1006e95647ceb2dd5c3aa06f7c" +dependencies = [ + "libsecp256k1-core", +] + +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + +[[package]] +name = "linux-raw-sys" +version = "0.4.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "01cda141df6706de531b6c46c3a33ecca755538219bd484262fa09410c13539c" + +[[package]] +name = "log" +version = "0.4.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "abb12e687cfb44aa40f41fc3978ef76448f9b6038cad6aef4259d3c095a2382e" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "memchr" +version = "2.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" + +[[package]] +name = "minimal-lexical" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" + +[[package]] +name = "miniz_oxide" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" +dependencies = [ + "adler", +] + +[[package]] +name = "nom" +version = "7.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a" +dependencies = [ + "memchr", + "minimal-lexical", +] + +[[package]] +name = "num" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b05180d69e3da0e530ba2a1dae5110317e49e3b7f3d41be227dc5f92e49ee7af" +dependencies = [ + "num-bigint", + "num-complex", + "num-integer", + "num-iter", + "num-rational", + "num-traits", +] + +[[package]] +name = "num-bigint" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608e7659b5c3d7cba262d894801b9ec9d00de989e8a82bd4bef91d08da45cdc0" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", + "serde", +] + +[[package]] +name = "num-complex" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ba157ca0885411de85d6ca030ba7e2a83a28636056c7c699b07c8b6f7383214" +dependencies = [ + "num-traits", +] + +[[package]] +name = "num-derive" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "876a53fff98e03a936a674b29568b0e605f06b29372c2489ff4de23f1949743d" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "num-integer" +version = "0.1.45" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "225d3389fb3509a24c93f5c29eb6bde2586b98d9f016636dff58d7c6f7569cd9" +dependencies = [ + "autocfg", + "num-traits", +] + +[[package]] +name = "num-iter" +version = "0.1.43" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d03e6c028c5dc5cac6e2dec0efda81fc887605bb3d884578bb6d6bf7514e252" +dependencies = [ + "autocfg", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-rational" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0638a1c9d0a3c0914158145bc76cff373a75a627e6ecbfb71cbe6f453a5a19b0" +dependencies = [ + "autocfg", + "num-bigint", + "num-integer", + "num-traits", +] + +[[package]] +name = "num-traits" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "39e3200413f237f41ab11ad6d161bc7239c84dcb631773ccd7de3dfe4b5c267c" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "once_cell" +version = "1.17.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" + +[[package]] +name = "opaque-debug" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" + +[[package]] +name = "option-ext" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "04744f49eae99ab78e0d5c0b603ab218f515ea8cfe5a456d7629ad883a3b6e7d" + +[[package]] +name = "p256" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d053368e1bae4c8a672953397bd1bd7183dde1c72b0b7612a15719173148d186" +dependencies = [ + "ecdsa", + "elliptic-curve", + "sha2 0.9.9", +] + +[[package]] +name = "parity-scale-codec" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "637935964ff85a605d114591d4d2c13c5d1ba2806dae97cea6bf180238a749ac" +dependencies = [ + "arrayvec", + "byte-slice-cast", + "impl-trait-for-tuples", + "parity-scale-codec-derive", +] + +[[package]] +name = "parity-scale-codec-derive" +version = "3.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "86b26a931f824dd4eca30b3e43bb4f31cd5f0d3a403c5f5ff27106b805bfde7b" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "parse-display" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7271152b3c46c07c729698e7a5248e2744466b3446d222c97a0b1315925a97b1" +dependencies = [ + "once_cell", + "parse-display-derive", + "regex", +] + +[[package]] +name = "parse-display-derive" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6a9f3e41b237b77c99c09686481c235964ff5878229412b226c451f3e809f4f" +dependencies = [ + "once_cell", + "proc-macro2", + "quote", + "regex", + "regex-syntax 0.6.28", + "syn 1.0.109", +] + +[[package]] +name = "paste" +version = "1.0.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" + +[[package]] +name = "percent-encoding" +version = "2.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" + +[[package]] +name = "ppv-lite86" +version = "0.2.17" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b40af805b3121feab8a3c29f04d8ad262fa8e0561883e7653e024ae4479e6de" + +[[package]] +name = "pretty_assertions" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af7cee1a6c8a5b9208b3cb1061f10c0cb689087b3d8ce85fb9d2dd7a29b6ba66" +dependencies = [ + "diff", + "yansi", +] + +[[package]] +name = "primitive-types" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f3486ccba82358b11a77516035647c34ba167dfa53312630de83b12bd4f3d66" +dependencies = [ + "fixed-hash", + "impl-codec", + "impl-rlp", + "scale-info", + "uint", +] + +[[package]] +name = "proc-macro-crate" +version = "1.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f4c021e1093a56626774e81216a4ce732a735e5bad4868a03f3ed65ca0c3919" +dependencies = [ + "once_cell", + "toml_edit", +] + +[[package]] +name = "proc-macro-error" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da25490ff9892aab3fcf7c36f08cfb902dd3e71ca0f9f9517bea02a73a5ce38c" +dependencies = [ + "proc-macro-error-attr", + "proc-macro2", + "quote", + "syn 1.0.109", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1be40180e52ecc98ad80b184934baf3d0d29f979574e439af5a55274b35f869" +dependencies = [ + "proc-macro2", + "quote", + "version_check", +] + +[[package]] +name = "proc-macro-error-attr2" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "96de42df36bb9bba5542fe9f1a054b8cc87e172759a1868aa05c1f3acc89dfc5" +dependencies = [ + "proc-macro2", + "quote", +] + +[[package]] +name = "proc-macro-error2" +version = "2.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11ec05c52be0a07b08061f7dd003e7d7092e0472bc731b4af7bb1ef876109802" +dependencies = [ + "proc-macro-error-attr2", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "proc-macro2" +version = "1.0.84" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec96c6a92621310b51366f1e28d05ef11489516e93be030060e5fc12024a49d6" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bit-set", + "bit-vec", + "bitflags 2.4.2", + "lazy_static", + "num-traits", + "rand 0.8.5", + "rand_chacha 0.3.1", + "rand_xorshift", + "regex-syntax 0.8.2", + "rusty-fork", + "tempfile", + "unarray", +] + +[[package]] +name = "quick-error" +version = "1.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1d01941d82fa2ab50be1e79e6714289dd7cde78eba4c074bc5a4374f650dfe0" + +[[package]] +name = "quote" +version = "1.0.36" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa76aaf39101c457836aec0ce2316dbdc3ab723cdda1c6bd4e6ad4208acaca7" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "radium" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" + +[[package]] +name = "rand" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a6b1679d49b24bbfe0c803429aa1874472f50d9b363131f0e89fc356b544d03" +dependencies = [ + "rand_chacha 0.2.2", + "rand_core 0.5.1", + "rand_hc", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "libc", + "rand_chacha 0.3.1", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_chacha" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4c8ed856279c9737206bf725bf36935d8666ead7aa69b52be55af369d193402" +dependencies = [ + "ppv-lite86", + "rand_core 0.5.1", +] + +[[package]] +name = "rand_chacha" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" +dependencies = [ + "ppv-lite86", + "rand_core 0.6.4", +] + +[[package]] +name = "rand_core" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "90bde5296fc891b0cef12a6d03ddccc162ce7b2aff54160af9338f8d40df6d19" + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" +dependencies = [ + "getrandom", +] + +[[package]] +name = "rand_hc" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ca3129af7b92a17112d59ad498c6f81eaf463253766b90396d39ea7a39d6613c" +dependencies = [ + "rand_core 0.5.1", +] + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core 0.6.4", +] + +[[package]] +name = "redox_syscall" +version = "0.2.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fb5a58c1855b4b6819d59012155603f0b22ad30cad752600aadfcb695265519a" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_syscall" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4722d768eff46b75989dd134e5c353f0d6296e5aaa3132e776cbdb56be7731aa" +dependencies = [ + "bitflags 1.3.2", +] + +[[package]] +name = "redox_users" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd283d9651eeda4b2a83a43c1c91b266c40fd76ecd39a50a8c630ae69dc72891" +dependencies = [ + "getrandom", + "libredox", + "thiserror", +] + +[[package]] +name = "regex" +version = "1.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "380b951a9c5e80ddfd6136919eef32310721aa4aacd4889a8d39124b026ab343" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-automata" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f804c7828047e88b2d32e2d7fe5a105da8ee3264f01902f796c8e067dc2483f" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax 0.8.2", +] + +[[package]] +name = "regex-syntax" +version = "0.6.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" + +[[package]] +name = "regex-syntax" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c08c74e62047bb2de4ff487b251e4a92e24f48745648451635cec7d591162d9f" + +[[package]] +name = "ring" +version = "0.17.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9babe80d5c16becf6594aa32ad2be8fe08498e7ae60b77de8df700e67f191d7e" +dependencies = [ + "cc", + "getrandom", + "libc", + "spin 0.9.8", + "untrusted", + "windows-sys 0.48.0", +] + +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.6", +] + +[[package]] +name = "rlp" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" +dependencies = [ + "bytes", + "rlp-derive", + "rustc-hex", +] + +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "ruint" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" +dependencies = [ + "proptest", + "rand 0.8.5", + "ruint-macro", + "serde", + "valuable", + "zeroize", +] + +[[package]] +name = "ruint-macro" +version = "1.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" + +[[package]] +name = "rustc-hex" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3e75f6a532d0fd9f7f13144f392b6ad56a32696bfcd9c78f797f16bbb6f072d6" + +[[package]] +name = "rustc_version" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bfa0f585226d2e68097d4f95d113b15b83a82e819ab25717ec0590d9584ef366" +dependencies = [ + "semver", +] + +[[package]] +name = "rustix" +version = "0.36.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "db4165c9963ab29e422d6c26fbc1d37f15bace6b2810221f9d925023480fcf0e" +dependencies = [ + "bitflags 1.3.2", + "errno 0.2.8", + "io-lifetimes", + "libc", + "linux-raw-sys 0.1.4", + "windows-sys 0.45.0", +] + +[[package]] +name = "rustix" +version = "0.38.28" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72e572a5e8ca657d7366229cdde4bd14c4eb5499a9573d4d366fe1b599daa316" +dependencies = [ + "bitflags 2.4.2", + "errno 0.3.8", + "libc", + "linux-raw-sys 0.4.13", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls" +version = "0.21.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f56a14d1f48b391359b22f731fd4bd7e43c97f3c50eee276f3aa09c94784d3e" +dependencies = [ + "log", + "ring", + "rustls-webpki", + "sct", +] + +[[package]] +name = "rustls-webpki" +version = "0.101.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b6275d1ee7a1cd780b64aca7726599a1dbc893b1e64144529e55c3c2f745765" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "rustversion" +version = "1.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e819f2bc632f285be6d7cd36e25940d45b2391dd6d9b939e79de557f7014248" + +[[package]] +name = "rusty-fork" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb3dcc6e454c328bb824492db107ab7c0ae8fcffe4ad210136ef014458c1bc4f" +dependencies = [ + "fnv", + "quick-error", + "tempfile", + "wait-timeout", +] + +[[package]] +name = "ryu" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7b4b9743ed687d4b4bcedf9ff5eaa7398495ae14e61cba0a295704edbc7decde" + +[[package]] +name = "scale-info" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61471dff9096de1d8b2319efed7162081e96793f5ebb147e50db10d50d648a4d" +dependencies = [ + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "219580e803a66b3f05761fd06f1f879a872444e49ce23f73694d26e5a954c7e6" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "sct" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da046153aa2352493d6cb7da4b6e5c0c057d8a1d0a9aa8560baffdd945acd414" +dependencies = [ + "ring", + "untrusted", +] + +[[package]] +name = "semver" +version = "1.0.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "836fa6a3e1e547f9a2c4040802ec865b5d85f4014efe00555d7090a3dcaa1090" + +[[package]] +name = "serde" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7253ab4de971e72fb7be983802300c30b5a7f0c2e56fab8abfc6a214307c0094" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.203" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "500cbc0ebeb6f46627f50f3f5811ccf6bf00643be300b4c3eabc0ef55dc5b5ba" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "serde_json" +version = "1.0.115" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +dependencies = [ + "itoa", + "ryu", + "serde", +] + +[[package]] +name = "serde_yaml" +version = "0.9.25" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a49e178e4452f45cb61d0cd8cebc1b0fafd3e41929e996cef79aa3aca91f574" +dependencies = [ + "indexmap 2.7.1", + "itoa", + "ryu", + "serde", + "unsafe-libyaml", +] + +[[package]] +name = "sha2" +version = "0.9.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" +dependencies = [ + "block-buffer 0.9.0", + "cfg-if", + "cpufeatures", + "digest 0.9.0", + "opaque-debug", +] + +[[package]] +name = "sha2" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82e6b795fe2e3b1e845bafcb27aa35405c4d47cdfc92af5fc8d3002f76cebdc0" +dependencies = [ + "cfg-if", + "cpufeatures", + "digest 0.10.6", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest 0.10.6", + "keccak", +] + +[[package]] +name = "shellexpand" +version = "3.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "da03fa3b94cc19e3ebfc88c4229c49d8f08cdbd1228870a45f0ffdf84988e14b" +dependencies = [ + "dirs", +] + +[[package]] +name = "shlex" +version = "1.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" + +[[package]] +name = "signature" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f2807892cfa58e081aa1f1111391c7a0649d4fa127a4ffbe34bcbfb35a1171a4" +dependencies = [ + "digest 0.9.0", + "rand_core 0.6.4", +] + +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" + +[[package]] +name = "softfloat" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "359a2aa2309eed307acc397678f7fbb43989db6ee417a11752248fc98451e696" + +[[package]] +name = "spin" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e63cff320ae2c57904679ba7cb63280a3dc4613885beafb148ee7bf9aa9042d" + +[[package]] +name = "spin" +version = "0.9.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6980e8d7511241f8acf4aebddbb1ff938df5eebe98691418c4468d0b72a96a67" + +[[package]] +name = "static_assertions" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" + +[[package]] +name = "strum" +version = "0.20.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7318c509b5ba57f18533982607f24070a55d353e90d4cae30c467cdb2ad5ac5c" + +[[package]] +name = "strum_macros" +version = "0.20.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee8bc6b87a5112aeeab1f4a9f7ab634fe6cbefc4850006df31267f4cfb9e3149" +dependencies = [ + "heck 0.3.3", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "substrate-bn" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b5bbfa79abbae15dd642ea8176a21a635ff3c00059961d1ea27ad04e5b441c" +dependencies = [ + "byteorder", + "crunchy", + "lazy_static", + "rand 0.8.5", + "rustc-hex", +] + +[[package]] +name = "subtle" +version = "2.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601" + +[[package]] +name = "syn" +version = "1.0.109" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn" +version = "2.0.87" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25aa4ce346d03a6dcd68dd8b4010bcb74e54e62c90c573f394c46eae99aba32d" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "0.7.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c837dc8852cb7074e46b444afb81783140dab12c58867b49fb3898fbafedf7ea" +dependencies = [ + "paste", + "proc-macro2", + "quote", + "syn 2.0.87", +] + +[[package]] +name = "tap" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" + +[[package]] +name = "tar" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b16afcea1f22891c49a00c751c7b63b2233284064f11a200fc624137c51e2ddb" +dependencies = [ + "filetime", + "libc", + "xattr", +] + +[[package]] +name = "tempfile" +version = "3.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" +dependencies = [ + "cfg-if", + "fastrand", + "redox_syscall 0.2.16", + "rustix 0.36.11", + "windows-sys 0.42.0", +] + +[[package]] +name = "tezos-evm-logging-calypso2" +version = "0.1.0" +dependencies = [ + "num-derive", + "num-traits", + "tezos-smart-rollup-debug", +] + +[[package]] +name = "tezos-evm-runtime-calypso2" +version = "0.1.0" +dependencies = [ + "sha3", + "tezos-evm-logging-calypso2", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", +] + +[[package]] +name = "tezos-indexable-storage-calypso2" +version = "0.1.0" +dependencies = [ + "rlp", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "thiserror", +] + +[[package]] +name = "tezos-smart-rollup" +version = "0.2.2" +dependencies = [ + "hermit", + "hex", + "tezos-smart-rollup-build-utils", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-entrypoint", + "tezos-smart-rollup-host", + "tezos-smart-rollup-macros", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-storage", + "tezos_crypto_rs", + "tezos_data_encoding", +] + +[[package]] +name = "tezos-smart-rollup-build-utils" +version = "0.2.2" +dependencies = [ + "proc-macro2", + "quote", + "rustc_version", +] + +[[package]] +name = "tezos-smart-rollup-constants" +version = "0.2.2" + +[[package]] +name = "tezos-smart-rollup-core" +version = "0.2.2" +dependencies = [ + "tezos-smart-rollup-build-utils", + "tezos-smart-rollup-constants", +] + +[[package]] +name = "tezos-smart-rollup-debug" +version = "0.2.2" +dependencies = [ + "tezos-smart-rollup-core", + "tezos-smart-rollup-host", +] + +[[package]] +name = "tezos-smart-rollup-encoding" +version = "0.2.2" +dependencies = [ + "hex", + "nom", + "num-bigint", + "num-traits", + "paste", + "regex", + "tezos-smart-rollup-core", + "tezos-smart-rollup-host", + "tezos_crypto_rs", + "tezos_data_encoding", + "thiserror", + "time", +] + +[[package]] +name = "tezos-smart-rollup-entrypoint" +version = "0.2.2" +dependencies = [ + "cfg-if", + "dlmalloc", + "tezos-smart-rollup-build-utils", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-host", + "tezos-smart-rollup-panic-hook", +] + +[[package]] +name = "tezos-smart-rollup-host" +version = "0.2.2" +dependencies = [ + "tezos-smart-rollup-build-utils", + "tezos-smart-rollup-core", + "tezos_crypto_rs", + "tezos_data_encoding", + "thiserror", +] + +[[package]] +name = "tezos-smart-rollup-installer-config" +version = "0.2.2" +dependencies = [ + "hex", + "nom", + "serde", + "serde_yaml", + "tezos-smart-rollup-core", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos_crypto_rs", + "tezos_data_encoding", + "thiserror", +] + +[[package]] +name = "tezos-smart-rollup-macros" +version = "0.2.2" +dependencies = [ + "proc-macro-error2", + "quote", + "shellexpand", + "syn 2.0.87", + "tezos-smart-rollup-build-utils", +] + +[[package]] +name = "tezos-smart-rollup-mock" +version = "0.2.2" +dependencies = [ + "hex", + "tezos-smart-rollup-core", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos_crypto_rs", + "tezos_data_encoding", +] + +[[package]] +name = "tezos-smart-rollup-panic-hook" +version = "0.2.2" +dependencies = [ + "rustversion", + "tezos-smart-rollup-build-utils", + "tezos-smart-rollup-core", +] + +[[package]] +name = "tezos-smart-rollup-storage" +version = "0.2.2" +dependencies = [ + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "thiserror", +] + +[[package]] +name = "tezos-storage-calypso2" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "primitive-types", + "rlp", + "sha3", + "tezos-evm-runtime-calypso2", + "tezos-smart-rollup-host", + "tezos-smart-rollup-storage", + "tezos_crypto_rs", + "tezos_ethereum_calypso2", + "thiserror", +] + +[[package]] +name = "tezos_crypto_rs" +version = "0.6.0" +dependencies = [ + "anyhow", + "bs58", + "byteorder", + "cryptoxide", + "ed25519-dalek", + "hex", + "libsecp256k1", + "nom", + "num-bigint", + "num-traits", + "p256", + "rand 0.7.3", + "serde", + "strum", + "strum_macros", + "tezos_data_encoding", + "thiserror", + "zeroize", +] + +[[package]] +name = "tezos_data_encoding" +version = "0.6.0" +dependencies = [ + "bit-vec", + "bitvec", + "hex", + "lazy_static", + "nom", + "num-bigint", + "num-traits", + "serde", + "tezos_data_encoding_derive", + "thiserror", +] + +[[package]] +name = "tezos_data_encoding_derive" +version = "0.6.0" +dependencies = [ + "lazy_static", + "once_cell", + "parse-display", + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "tezos_ethereum_calypso2" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "ethbloom", + "ethereum", + "hex", + "libsecp256k1", + "primitive-types", + "rlp", + "sha3", + "tezos-smart-rollup-encoding", + "tezos_crypto_rs", + "thiserror", +] + +[[package]] +name = "thiserror" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +dependencies = [ + "thiserror-impl", +] + +[[package]] +name = "thiserror-impl" +version = "1.0.38" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "time" +version = "0.3.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0cbfecb4d19b5ea75bb31ad904eb5b9fa13f21079c3b92017ebdf4999a5890" +dependencies = [ + "itoa", + "serde", + "time-core", + "time-macros", +] + +[[package]] +name = "time-core" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2e153e1f1acaef8acc537e68b44906d2db6436e2b35ac2c6b42640fff91f00fd" + +[[package]] +name = "time-macros" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fd80a657e71da814b8e5d60d3374fc6d35045062245d80224748ae522dd76f36" +dependencies = [ + "time-core", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "tinyvec" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50" +dependencies = [ + "tinyvec_macros", +] + +[[package]] +name = "tinyvec_macros" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" + +[[package]] +name = "toml_datetime" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3ab8ed2edee10b50132aed5f331333428b011c99402b5a534154ed15746f9622" + +[[package]] +name = "toml_edit" +version = "0.19.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "239410c8609e8125456927e6707163a3b1fdb40561e4b803bc041f466ccfdc13" +dependencies = [ + "indexmap 1.9.3", + "toml_datetime", + "winnow 0.4.1", +] + +[[package]] +name = "triehash" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1631b201eb031b563d2e85ca18ec8092508e262a3196ce9bd10a67ec87b9f5c" +dependencies = [ + "hash-db", + "rlp", +] + +[[package]] +name = "typenum" +version = "1.16.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "497961ef93d974e23eb6f433eb5fe1b7930b659f06d12dec6fc44a8f554c0bba" + +[[package]] +name = "uint" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "76f64bba2c53b04fcab63c01a7d7427eadc821e3bc48c34dc9ba29c501164b52" +dependencies = [ + "byteorder", + "crunchy", + "hex", + "static_assertions", +] + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-bidi" +version = "0.3.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75" + +[[package]] +name = "unicode-ident" +version = "1.0.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "84a22b9f218b40614adcb3f4ff08b703773ad44fa9423e4e0d346d5db86e4ebc" + +[[package]] +name = "unicode-normalization" +version = "0.1.22" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921" +dependencies = [ + "tinyvec", +] + +[[package]] +name = "unicode-segmentation" +version = "1.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36" + +[[package]] +name = "unsafe-libyaml" +version = "0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f28467d3e1d3c6586d8f25fa243f544f5800fec42d97032474e17222c2b75cfa" + +[[package]] +name = "untrusted" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" + +[[package]] +name = "ureq" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8cdd25c339e200129fe4de81451814e5228c9b771d57378817d6117cc2b3f97" +dependencies = [ + "base64 0.21.7", + "flate2", + "log", + "once_cell", + "rustls", + "rustls-webpki", + "url", + "webpki-roots", +] + +[[package]] +name = "url" +version = "2.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633" +dependencies = [ + "form_urlencoded", + "idna", + "percent-encoding", +] + +[[package]] +name = "valuable" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d" + +[[package]] +name = "version_check" +version = "0.9.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" + +[[package]] +name = "wait-timeout" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f200f5b12eb75f8c1ed65abd4b2db8a6e1b138a20de009dacee265a2498f3f6" +dependencies = [ + "libc", +] + +[[package]] +name = "wasi" +version = "0.11.0+wasi-snapshot-preview1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" + +[[package]] +name = "webpki-roots" +version = "0.25.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" + +[[package]] +name = "winapi" +version = "0.3.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" +dependencies = [ + "winapi-i686-pc-windows-gnu", + "winapi-x86_64-pc-windows-gnu", +] + +[[package]] +name = "winapi-i686-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" + +[[package]] +name = "winapi-x86_64-pc-windows-gnu" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "windows-sys" +version = "0.42.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + +[[package]] +name = "windows-sys" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" +dependencies = [ + "windows-targets 0.52.0", +] + +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", +] + +[[package]] +name = "windows-targets" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a18201040b24831fbb9e4eb208f8892e1f50a37feb53cc7ff887feb8f50e7cd" +dependencies = [ + "windows_aarch64_gnullvm 0.52.0", + "windows_aarch64_msvc 0.52.0", + "windows_i686_gnu 0.52.0", + "windows_i686_msvc 0.52.0", + "windows_x86_64_gnu 0.52.0", + "windows_x86_64_gnullvm 0.52.0", + "windows_x86_64_msvc 0.52.0", +] + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cb7764e35d4db8a7921e09562a0304bf2f93e0a51bfccee0bd0bb0b666b015ea" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + +[[package]] +name = "windows_aarch64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbaa0368d4f1d2aaefc55b6fcfee13f41544ddf36801e793edbbfd7d7df075ef" + +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + +[[package]] +name = "windows_i686_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a28637cb1fa3560a16915793afb20081aba2c92ee8af57b4d5f28e4b3e7df313" + +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + +[[package]] +name = "windows_i686_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ffe5e8e31046ce6230cc7215707b816e339ff4d4d67c65dffa206fd0f7aa7b9a" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + +[[package]] +name = "windows_x86_64_gnu" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6fa32db2bc4a2f5abeacf2b69f7992cd09dca97498da74a151a3132c26befd" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a657e1e9d3f514745a572a6846d3c7aa7dbe1658c056ed9c3344c4109a6949e" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + +[[package]] +name = "windows_x86_64_msvc" +version = "0.52.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dff9641d1cd4be8d1a070daf9e3773c5f67e78b4d9d42263020c057706765c04" + +[[package]] +name = "winnow" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae8970b36c66498d8ff1d66685dc86b91b29db0c7739899012f63a63814b4b28" +dependencies = [ + "memchr", +] + +[[package]] +name = "winnow" +version = "0.6.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59b5e5f6c299a3c7890b876a2a587f3115162487e704907d9b6cd29473052ba1" + +[[package]] +name = "wyz" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" +dependencies = [ + "tap", +] + +[[package]] +name = "xattr" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "914566e6413e7fa959cc394fb30e563ba80f3541fbd40816d4c05a0fc3f2a0f1" +dependencies = [ + "libc", + "linux-raw-sys 0.4.13", + "rustix 0.38.28", +] + +[[package]] +name = "yansi" +version = "0.5.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09041cd90cf85f7f8b2df60c646f853b7f535ce68f85244eb6731cf89fa498ec" + +[[package]] +name = "zeroize" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2a0956f1ba7c7909bfb66c2e9e4124ab6f6482560f6628b5aaeba39207c9aad9" diff --git a/etherlink/kernel_calypso2/Cargo.toml b/etherlink/kernel_calypso2/Cargo.toml new file mode 100644 index 000000000000..224dbd63916c --- /dev/null +++ b/etherlink/kernel_calypso2/Cargo.toml @@ -0,0 +1,100 @@ +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# SPDX-FileCopyrightText: 2023 Marigold +# SPDX-FileCopyrightText: 2023-2025 Functori +# SPDX-FileCopyrightText: 2023 PK Lab +# SPDX-FileCopyrightText: 2024 TriliTech +# +# SPDX-License-Identifier: MIT + +[workspace] +resolver = "2" + +members = [ + "ethereum", + "kernel", + "evm_execution", + "indexable_storage", + "logging", + "storage", + "runtime", +] + +[workspace.dependencies] + +# error handling +thiserror = "1.0" +anyhow = "1.0" + +# types +primitive-types = { version = "0.12.1", default-features = false } +num-bigint = { version = "0.4", default-features = false } +num-traits = "0.2.8" +num-derive = "0.3" +ethereum = { version = "0.14.0", default-features = false } +ethbloom = { version = "0.13.0", default-features = false, features = ["rlp"] } +softfloat = "1.0.0" +bytes = "^1" + +# serialization +hex = "0.4" +hex-literal = "0.4.1" +tezos_data_encoding = { version = "0.6", path = "../../sdk/rust/encoding" } +const-decoder = { version = "0.3.0" } +rlp = "0.5.2" + +# ethereum VM +evm = { path = "../sputnikvm", default-features = false } +aurora-engine-modexp = { version = "1.0", default-features = false } +bn = { package = "substrate-bn", version = "0.6", default-features = false } + +# crypto stuff +sha2 = { version = "0.10.6", default-features = false } +sha3 = { version = "0.10.6", default-features = false } +ripemd = { version = "0.1.3", default-features = false } +# TODO (SDK-45): bump to `0.6.1` from crates.io as soon as it is released. +tezos_crypto_rs = { version = "0.6", path = "../../sdk/rust/crypto", default-features = false } +libsecp256k1 = { version = "0.7", default-features = false, features = [ + "static-context", + "hmac", +] } + +# kernel crates +tezos_ethereum = { package = "tezos_ethereum_calypso2", path = "./ethereum" } +evm-execution = { package = "evm-execution-calypso2", path = "./evm_execution" } +tezos-evm-logging = { package = "tezos-evm-logging-calypso2", path = "./logging" } +tezos-evm-runtime = { package = "tezos-evm-runtime-calypso2", path = "./runtime" } +tezos-indexable-storage = { package = "tezos-indexable-storage-calypso2", path = "./indexable_storage" } +tezos-storage = { package = "tezos-storage-calypso2", path = "./storage" } + +# SDK +# we disable BLS, because we don’t need it and it is a roadblock for the native execution +tezos-smart-rollup = { path = "../../src/kernel_sdk/sdk", default-features = false, features = ["std", "crypto", "dlmalloc", "panic-hook", "data-encoding", "storage", "testing"] } +tezos-smart-rollup-core = { path = "../../src/kernel_sdk/core" } +tezos-smart-rollup-host = { path = "../../src/kernel_sdk/host" } +tezos-smart-rollup-panic-hook = { path = "../../src/kernel_sdk/panic-hook" } +tezos-smart-rollup-entrypoint = { path = "../../src/kernel_sdk/entrypoint" } +tezos-smart-rollup-debug = { path = "../../src/kernel_sdk/debug" } +tezos-smart-rollup-encoding = { path = "../../src/kernel_sdk/encoding", default-features = false, features = [ + "alloc", + "tezos-encoding", + "crypto", +] } +tezos-smart-rollup-installer-config = { path = "../../src/kernel_sdk/installer-config" } +tezos-smart-rollup-mock = { path = "../../src/kernel_sdk/mock" } +tezos-smart-rollup-storage = { path = "../../src/kernel_sdk/storage" } + +# property based testing +rand = { version = "0.8" } +proptest = { version = "1.0" } +pretty_assertions = { version = "1.4.0" } + +# alloy +alloy-sol-types = { version = "0.7.7", default-features = false, features = [ + "json", +] } +alloy-primitives = { version = "0.7.7", default-features = false } + +[profile.release] +# Will apply heavy LTO which attempts to perform optimizations across all crates +# within the dependency graph. +lto = true diff --git a/etherlink/kernel_calypso2/Makefile b/etherlink/kernel_calypso2/Makefile new file mode 100644 index 000000000000..98fbfe1fcd2a --- /dev/null +++ b/etherlink/kernel_calypso2/Makefile @@ -0,0 +1,64 @@ +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# SPDX-FileCopyrightText: 2023 TriliTech +# SPDX-FileCopyrightText: 2023-2024 Functori +# +# SPDX-License-Identifier: MIT + +.PHONY: all +all: build test check + +.PHONY: build +build: +ifdef EVM_KERNEL_FEATURES + $(eval FEATURES := --features ${EVM_KERNEL_FEATURES}) +endif +ifdef EXCLUDE_MEMBER + $(eval EXCLUDE := --workspace --exclude ${EXCLUDE_MEMBER}) +else +# By default evm-evaluation is excluded as it's an isolated component +# of the EVM workspace. + $(eval EXCLUDE := --workspace --exclude evm-evaluation) +endif + @cargo build --target wasm32-unknown-unknown --release ${EXCLUDE} ${FEATURES} + +.PHONY: build-evm-execution +build-evm-execution: + @cargo build --target wasm32-unknown-unknown --release --package evm-execution + +.PHONY: build-evm-evaluation +build-evm-evaluation: +ifdef EVM_EVALUATION_FEATURES + @cargo build --features ${EVM_EVALUATION_FEATURES} --release --package evm-evaluation +else + @cargo build --release --package evm-evaluation +endif + +.PHONY: build-deps +build-deps: + # 'rustup show' will install the toolchain in addition to showing + # toolchain information. + @rustup show active-toolchain 2> /dev/null + @rustup target add wasm32-unknown-unknown + +.PHONY: build-dev-deps +build-dev-deps: build-deps + @rustup component add rustfmt clippy + +.PHONY: test +test: +# Setting RUST_MIN_STACK is needed otherwise some tests will panic by reaching +# Rust's max stack size. + @RUST_MIN_STACK=104857600 RUST_TEST_THREADS=1 cargo test --features testing ${TESTNAME} + +.PHONY: check +check: + @cargo update --workspace --locked + @cargo clippy --all-targets --features testing -- --deny warnings + +.PHONY: check-all +check-all: check + @cargo fmt --check + +.PHONY: clean +clean: + @cargo clean diff --git a/etherlink/kernel_calypso2/YesEVM.md b/etherlink/kernel_calypso2/YesEVM.md new file mode 100644 index 000000000000..4c01a43973fb --- /dev/null +++ b/etherlink/kernel_calypso2/YesEVM.md @@ -0,0 +1,72 @@ +# YesEVM + +This document explains how you can run a proper manual upgrade/migration test starting from the **exact** state of the EVM rollup at the wanted level with the help of **Yes-node/wallet**. + +## Preliminary steps + +- Retrieve a full snapshot of the desired network and run a regular `octez-node` on it. + +- Run a rollup node for the desired EVM rollup (with the correct preimages): + +``` +> octez-smart-rollup-node init operator config for with operators --data-dir ~/.evm-test/rollup-node +> octez-smart-rollup-node run observer for with operators --data-dir ~/.evm-test/rollup-node --rpc-port 8932 +``` + +To ensure everything works properly, both the node and the rollup node will need to be on the same L1 level: + +- Stop the node. Run it back with `[--connections 0]` to put it in a waiting mode. Get the L1 level with the `octez-client` and then stop the node. The rollup won't receive new blocks and wait at the same level, and can now be safely stopped. + +- Create a snapshot of the node at the retrieved level. + +> octez-node snapshot export --block --data-dir ~/evm-test/ghostnet-node + +- Get a "snapshot" of the EVM rollup (basically all its data directory) and store it somewhere. + +## Setup and use a yes-node/wallet + +Fork the chain to make the tests in our deviated chain. + +- Setup the yes-node/wallet on ghostnet: + +``` +# TODO: remove the following line. Should hopefully be merged to master soon, in the meantime: +> git checkout -B yes-evm +> git cherry-pick julien@yes-wallet-multi-network +> make +> ./scripts/patch-yes_node.sh +> cp octez-client octez-client-ok # octez-client compiled with the yes patches won't be able to actually inject transactions +> make octez-node octez-client +``` + +``` +> octez-node config init --data-dir ~/.yes-node --synchronisation-threshold 0 --connections 0 --rpc-addr localhost:8732 --network +``` + +``` +> octez-node snapshot import --data-dir ~/.yes-node +``` + +``` +Retrieve the list of bakers, here is an example for Ghostnet: + +> curl https://api.ghostnet.tzkt.io/v1/delegates\?limit\=5000 | jq 'map(select ((.alias?) and .active) |{ "alias" : .alias, "address" : .address , "publicKey" : .publicKey})' > aliases.json +``` + +``` +> dune exec devtools/yes_wallet/yes_wallet.exe -- create from context ~/.yes-node in ~/.yes-wallet --active-bakers-only --aliases aliases.json --network +``` + +``` +> octez-node run --data-dir ~/.yes-node +``` + +To push the chain forward: + +``` +> octez-client -d ~/.yes-wallet bake for --minimal-timestamp +``` + +Run a new EVM rollup node with the exported "snapshot" of the EVM rollup. Don't use the binary compiled with the __yes__ patches, as they will break the signature and the batcher of the rollup node won't be able to send operations. + +Once this is done this network acts as a sandbox with the same values as the initial network you used. This means you can safely test whatever you want from your forked chain/EVM rollup (injecting transactions, applying upgrades to check they don't break the rollup, etc) while benefit from the tooling (indexers, wallets, etc). diff --git a/etherlink/kernel_calypso2/ethereum/Cargo.toml b/etherlink/kernel_calypso2/ethereum/Cargo.toml new file mode 100644 index 000000000000..b9d28fbc834b --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/Cargo.toml @@ -0,0 +1,30 @@ +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# SPDX-FileCopyrightText: 2023 Marigold +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos_ethereum_calypso2" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] + +thiserror.workspace = true +anyhow.workspace = true + +primitive-types.workspace = true +ethereum.workspace = true +ethbloom.workspace = true + +rlp.workspace = true +hex.workspace = true + +bytes.workspace = true + +sha3.workspace = true +tezos_crypto_rs.workspace = true +libsecp256k1.workspace = true + +tezos-smart-rollup-encoding.workspace = true diff --git a/etherlink/kernel_calypso2/ethereum/src/access_list.rs b/etherlink/kernel_calypso2/ethereum/src/access_list.rs new file mode 100644 index 000000000000..79c72e28edbe --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/access_list.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// +// SPDX-License-Identifier: MIT + +use primitive_types::{H160, H256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; + +use crate::rlp_helpers::{decode_field, decode_list, next}; + +/// Access list item used to specify addresses +/// which are being accessed during a contract invocation. +/// For more information see ``. +#[derive(Clone, Debug, PartialEq, Eq)] +pub struct AccessListItem { + /// Address of the contract invoked during execution + pub address: H160, + /// Keys in the contract's storage accessed during contract execution + pub storage_keys: Vec, +} + +impl Encodable for AccessListItem { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(2); + s.append(&self.address); + s.append_list(&self.storage_keys); + } +} + +impl Decodable for AccessListItem { + fn decode(rlp: &Rlp) -> Result { + if !rlp.is_list() { + Err(DecoderError::RlpExpectedToBeList) + } else { + let mut it = rlp.iter(); + let address: H160 = decode_field(&next(&mut it)?, "address")?; + let storage_keys: Vec = decode_list(&next(&mut it)?, "storage_keys")?; + if it.next().is_some() { + return Err(DecoderError::RlpIncorrectListLen); + } + Ok(Self { + address, + storage_keys, + }) + } + } +} + +pub type AccessList = Vec; diff --git a/etherlink/kernel_calypso2/ethereum/src/block.rs b/etherlink/kernel_calypso2/ethereum/src/block.rs new file mode 100644 index 000000000000..a315867b2551 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/block.rs @@ -0,0 +1,411 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::eth_gen::OwnedHash; +use crate::helpers::{bytes_of_u256, hex_of_option}; +use crate::rlp_helpers::{ + append_option_explicit, append_timestamp, append_u256_le, append_u64_le, + decode_field, decode_field_u256_le, decode_field_u64_le, decode_option_explicit, + decode_timestamp, decode_transaction_hash_list, next, VersionedEncoding, +}; +use crate::transaction::TransactionHash; +use ethbloom::Bloom; +use primitive_types::{H160, H256, U256}; +use rlp::{DecoderError, Rlp, RlpStream}; +use sha3::{Digest, Keccak256}; +use tezos_smart_rollup_encoding::timestamp::Timestamp; + +/// Container for fee calculation. +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub struct BlockFees { + minimum_base_fee_per_gas: U256, + base_fee_per_gas: U256, + da_fee_per_byte: U256, +} + +impl BlockFees { + /// Setup fee information for the current block + pub const fn new( + minimum_base_fee_per_gas: U256, + base_fee_per_gas: U256, + da_fee_per_byte: U256, + ) -> Self { + Self { + minimum_base_fee_per_gas, + base_fee_per_gas, + da_fee_per_byte, + } + } + + /// The base fee per gas for doing a transaction within the current block. + #[inline(always)] + pub const fn base_fee_per_gas(&self) -> U256 { + self.base_fee_per_gas + } + + /// The minimum base fee per gas + #[inline(always)] + pub const fn minimum_base_fee_per_gas(&self) -> U256 { + self.minimum_base_fee_per_gas + } + + /// The da fee per byte charged per transaction. + #[inline(always)] + pub const fn da_fee_per_byte(&self) -> U256 { + self.da_fee_per_byte + } +} + +/// All data for an Ethereum block. +/// +/// This data does not change for the duration of the block. All values are +/// updated when the block is finalized and may change for the next block. +pub struct BlockConstants { + /// The number of the current block + pub number: U256, + /// Who is the beneficiary of the current block + pub coinbase: H160, + /// Unix date/time of the current block - when was the previous block finished + pub timestamp: U256, + /// Mining difficulty of the current block. This relates to PoW, and we can set + /// Gas limit for the current block. + pub gas_limit: u64, + /// Basis of fee calculation when performing transactions in the current block. + pub block_fees: BlockFees, + /// Identifier for the chain. Normally this would identify the chain (Ethereum + /// main net, or some other net). We can use it to identify rollup EVM kernel. + pub chain_id: U256, + /// A random number depending on previous block + /// NB: this field is not relevant for Etherlink but is required to enable other + /// relevant test from the Ethereum test suit + pub prevrandao: Option, +} + +impl BlockConstants { + /// Return the first block of the chain (genisis). + /// TODO find suitable values for gas_limit et.c. + /// To be done in . + pub fn first_block( + timestamp: U256, + chain_id: U256, + block_fees: BlockFees, + gas_limit: u64, + coinbase: H160, + ) -> Self { + Self { + number: U256::zero(), + coinbase, + timestamp, + gas_limit, + block_fees, + chain_id, + prevrandao: None, + } + } + + #[inline(always)] + pub const fn base_fee_per_gas(&self) -> U256 { + self.block_fees.base_fee_per_gas + } +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct L2Block { + // This choice of a L2 block representation is totally + // arbitrarily based on what is an Ethereum block and is + // subject to change. + // Optional types are used for currently unused fields, + // which will be populated in the future. This makes the + // data representation future proof, as it won't need + // to be migrated once the fields are used. + pub number: U256, + pub hash: H256, + pub parent_hash: H256, + pub logs_bloom: Bloom, + pub transactions_root: OwnedHash, + pub state_root: OwnedHash, + pub receipts_root: OwnedHash, + pub miner: Option, + pub extra_data: Option, + pub gas_limit: u64, + pub gas_used: U256, + pub timestamp: Timestamp, + pub transactions: Vec, + pub base_fee_per_gas: U256, + pub mix_hash: H256, +} + +impl L2Block { + #[allow(clippy::too_many_arguments)] + pub fn new( + number: U256, + transactions: Vec, + timestamp: Timestamp, + parent_hash: H256, + logs_bloom: Bloom, + transactions_root: OwnedHash, + state_root: OwnedHash, + receipts_root: OwnedHash, + gas_used: U256, + block_constants: &BlockConstants, + base_fee_per_gas: U256, + ) -> Self { + let hash = Self::hash( + parent_hash, + &state_root, + &transactions_root, + &receipts_root, + &logs_bloom, + &Some(block_constants.coinbase), + number, + block_constants.gas_limit, + gas_used, + timestamp, + &None, + ); + L2Block { + number, + hash, + parent_hash, + timestamp, + transactions, + logs_bloom, + transactions_root, + state_root, + receipts_root, + gas_used, + gas_limit: block_constants.gas_limit, + extra_data: None, + miner: Some(block_constants.coinbase), + base_fee_per_gas, + mix_hash: H256::zero(), + } + } + + #[allow(clippy::too_many_arguments)] + fn hash( + parent_hash: H256, + state_root: &OwnedHash, + transactions_root: &OwnedHash, + receipts_root: &OwnedHash, + logs_bloom: &Bloom, + miner: &Option, + number: U256, + gas_limit: u64, + gas_used: U256, + timestamp: Timestamp, + extra_data: &Option, + ) -> H256 { + let header = [ + hex::encode(parent_hash), + hex::encode(state_root), + hex::encode(transactions_root), + hex::encode(receipts_root), + hex::encode(logs_bloom), + hex_of_option(miner), + hex::encode(bytes_of_u256(number)), + hex::encode(gas_limit.to_le_bytes()), + hex::encode(gas_used.to_string()), + hex::encode(timestamp.to_string()), + hex_of_option(extra_data), + ]; + let bytes: Vec = rlp::encode_list::(&header).into(); + H256(Keccak256::digest(bytes).into()) + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let first = *bytes.first().ok_or(DecoderError::Custom("Empty bytes"))?; + if first == 0x01 { + let decoder = Rlp::new(&bytes[1..]); + Self::rlp_decode_v1(&decoder) + } else { + let decoder = Rlp::new(bytes); + Self::rlp_decode_v0(&decoder) + } + } + + fn rlp_decode_v0(decoder: &Rlp) -> Result { + if decoder.is_list() { + if Ok(13) == decoder.item_count() { + let mut it = decoder.iter(); + let number: U256 = decode_field_u256_le(&next(&mut it)?, "number")?; + let hash: H256 = decode_field(&next(&mut it)?, "hash")?; + let parent_hash: H256 = decode_field(&next(&mut it)?, "parent_hash")?; + let logs_bloom: Bloom = decode_field(&next(&mut it)?, "logs_bloom")?; + let transactions_root: OwnedHash = + decode_field(&next(&mut it)?, "transactions_root")?; + let state_root: OwnedHash = decode_field(&next(&mut it)?, "state_root")?; + let receipts_root: OwnedHash = + decode_field(&next(&mut it)?, "receipts_root")?; + let miner: Option = + decode_option_explicit(&next(&mut it)?, "miner", decode_field)?; + let extra_data: Option = + decode_option_explicit(&next(&mut it)?, "extra_data", decode_field)?; + let gas_limit = decode_field_u64_le(&next(&mut it)?, "gas_limit")?; + let transactions: Vec = + decode_transaction_hash_list(&next(&mut it)?, "transactions")?; + let gas_used: U256 = decode_field_u256_le(&next(&mut it)?, "gas_used")?; + let timestamp = decode_timestamp(&next(&mut it)?)?; + Ok(L2Block { + number, + hash, + parent_hash, + logs_bloom, + transactions_root, + state_root, + receipts_root, + miner, + extra_data, + gas_limit, + gas_used, + timestamp, + transactions, + base_fee_per_gas: U256::from(1000000000), + mix_hash: H256::zero(), + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + fn rlp_decode_v1(decoder: &Rlp) -> Result { + if decoder.is_list() { + if Ok(15) == decoder.item_count() { + let mut it = decoder.iter(); + let number: U256 = decode_field_u256_le(&next(&mut it)?, "number")?; + let hash: H256 = decode_field(&next(&mut it)?, "hash")?; + let parent_hash: H256 = decode_field(&next(&mut it)?, "parent_hash")?; + let logs_bloom: Bloom = decode_field(&next(&mut it)?, "logs_bloom")?; + let transactions_root: OwnedHash = + decode_field(&next(&mut it)?, "transactions_root")?; + let state_root: OwnedHash = decode_field(&next(&mut it)?, "state_root")?; + let receipts_root: OwnedHash = + decode_field(&next(&mut it)?, "receipts_root")?; + let miner: Option = + decode_option_explicit(&next(&mut it)?, "miner", decode_field)?; + let extra_data: Option = + decode_option_explicit(&next(&mut it)?, "extra_data", decode_field)?; + let gas_limit = decode_field_u64_le(&next(&mut it)?, "gas_limit")?; + let transactions: Vec = + decode_transaction_hash_list(&next(&mut it)?, "transactions")?; + let gas_used: U256 = decode_field_u256_le(&next(&mut it)?, "gas_used")?; + let timestamp = decode_timestamp(&next(&mut it)?)?; + let base_fee_per_gas: U256 = + decode_field_u256_le(&next(&mut it)?, "base_fee_per_gas")?; + let mix_hash: H256 = decode_field(&next(&mut it)?, "mix_hash")?; + Ok(L2Block { + number, + hash, + parent_hash, + logs_bloom, + transactions_root, + state_root, + receipts_root, + miner, + extra_data, + gas_limit, + gas_used, + timestamp, + transactions, + base_fee_per_gas, + mix_hash, + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + fn rlp_encode(self: &L2Block, s: &mut RlpStream) { + s.begin_list(15); + append_u256_le(s, &self.number); + s.append(&self.hash); + s.append(&self.parent_hash); + s.append(&self.logs_bloom); + s.append(&self.transactions_root); + s.append(&self.state_root); + s.append(&self.receipts_root); + append_option_explicit(s, &self.miner, RlpStream::append); + append_option_explicit(s, &self.extra_data, RlpStream::append); + append_u64_le(s, &self.gas_limit); + let transactions_bytes: Vec> = + self.transactions.iter().map(|x| x.to_vec()).collect(); + s.append_list::, _>(&transactions_bytes); + append_u256_le(s, &self.gas_used); + append_timestamp(s, self.timestamp); + append_u256_le(s, &self.base_fee_per_gas); + s.append(&self.mix_hash); + } +} + +impl VersionedEncoding for L2Block { + const VERSION: u8 = 1; + + fn unversionned_encode(&self) -> bytes::BytesMut { + let mut s = RlpStream::new(); + self.rlp_encode(&mut s); + s.out() + } + + fn unversionned_decode(decoder: &Rlp) -> Result { + Self::rlp_decode_v1(decoder) + } +} + +#[cfg(test)] +mod tests { + + use super::L2Block; + use crate::eth_gen::OwnedHash; + use crate::rlp_helpers::VersionedEncoding; + use crate::transaction::TRANSACTION_HASH_SIZE; + use crate::Bloom; + use primitive_types::{H256, U256}; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + + fn block_encoding_roundtrip(v: L2Block) { + let bytes = v.to_bytes(); + let v2 = L2Block::from_bytes(&bytes).expect("L2Block should be decodable"); + assert_eq!(v, v2, "Roundtrip failed on {:?}", v) + } + + const DUMMY_HASH: &str = "00000000000000000000000000000000"; + + pub fn dummy_hash() -> OwnedHash { + DUMMY_HASH.into() + } + fn dummy_block(tx_length: usize) -> L2Block { + L2Block { + number: U256::from(42), + hash: H256::from([3u8; 32]), + parent_hash: H256::from([2u8; 32]), + timestamp: Timestamp::from(10i64), + transactions: vec![[0u8; TRANSACTION_HASH_SIZE]; tx_length], + logs_bloom: Bloom::default(), + transactions_root: dummy_hash(), + state_root: dummy_hash(), + receipts_root: dummy_hash(), + miner: None, + extra_data: None, + gas_limit: 1 << 50, + gas_used: U256::zero(), + base_fee_per_gas: U256::zero(), + mix_hash: H256::default(), + } + } + + #[test] + fn roundtrip_rlp() { + for tx_length in 0..3 { + let v: L2Block = dummy_block(tx_length); + block_encoding_roundtrip(v); + } + } +} diff --git a/etherlink/kernel_calypso2/ethereum/src/eth_gen.rs b/etherlink/kernel_calypso2/ethereum/src/eth_gen.rs new file mode 100644 index 000000000000..3495d7af563d --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/eth_gen.rs @@ -0,0 +1,6 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +pub type OwnedHash = Vec; +pub type Hash<'a> = &'a Vec; diff --git a/etherlink/kernel_calypso2/ethereum/src/helpers.rs b/etherlink/kernel_calypso2/ethereum/src/helpers.rs new file mode 100644 index 000000000000..0a9f34ee41a6 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/helpers.rs @@ -0,0 +1,21 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use primitive_types::U256; + +pub fn hex_of_option(o: &Option) -> String +where + T: AsRef<[u8]>, +{ + match o { + Some(v) => hex::encode(v), + None => hex::encode(""), + } +} + +pub fn bytes_of_u256(v: U256) -> Vec { + let mut bytes = Into::<[u8; 32]>::into(v); + v.to_little_endian(&mut bytes); + bytes.to_vec() +} diff --git a/etherlink/kernel_calypso2/ethereum/src/lib.rs b/etherlink/kernel_calypso2/ethereum/src/lib.rs new file mode 100644 index 000000000000..8dafd07e6d60 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/lib.rs @@ -0,0 +1,16 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +pub mod access_list; +pub mod block; +pub mod eth_gen; +pub mod helpers; +pub mod rlp_helpers; +pub mod transaction; +pub mod tx_common; +pub mod tx_signature; +pub mod wei; + +pub use ethbloom::{Bloom, Input}; +pub use ethereum::Log; diff --git a/etherlink/kernel_calypso2/ethereum/src/rlp_helpers.rs b/etherlink/kernel_calypso2/ethereum/src/rlp_helpers.rs new file mode 100644 index 000000000000..1fa23d430c78 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/rlp_helpers.rs @@ -0,0 +1,378 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +//! Module containing helper functions for RLP encoding/decoding. + +use crate::transaction::{ + TransactionHash, TransactionStatus, TransactionType, TRANSACTION_HASH_SIZE, +}; +use primitive_types::{H256, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpIterator, RlpStream}; +use tezos_smart_rollup_encoding::{public_key::PublicKey, timestamp::Timestamp}; + +pub fn next<'a>(decoder: &mut RlpIterator<'a, '_>) -> Result, DecoderError> { + decoder.next().ok_or(DecoderError::RlpIncorrectListLen) +} + +pub fn check_list(decoder: &Rlp<'_>, length: usize) -> Result<(), DecoderError> { + if !decoder.is_list() { + Err(DecoderError::RlpExpectedToBeList) + } else if decoder.item_count() != Ok(length) { + Err(DecoderError::RlpIncorrectListLen) + } else { + Ok(()) + } +} + +pub fn check_is_list(decoder: &rlp::Rlp) -> Result<(), DecoderError> { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + Ok(()) +} + +pub fn decode_field( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result { + let custom_err = |_: DecoderError| (DecoderError::Custom(field_name)); + decoder.as_val().map_err(custom_err) +} + +pub fn decode_option( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result, DecoderError> { + decode_option_explicit(decoder, field_name, decode_field) +} + +// Combinator for decoding an optional field using an explicit +// decoding function, instead of the implemented in the Decodable trait. +pub fn decode_option_explicit( + decoder: &Rlp<'_>, + field_name: &'static str, + dec_field: Dec, +) -> Result, DecoderError> +where + Dec: Fn(&Rlp<'_>, &'static str) -> Result, +{ + if decoder.is_empty() { + Ok(None) + } else { + let inner: T = dec_field(decoder, field_name)?; + Ok(Some(inner)) + } +} + +pub fn decode_list( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result, DecoderError> { + let custom_err = |_: DecoderError| (DecoderError::Custom(field_name)); + decoder.as_list().map_err(custom_err) +} + +pub fn decode_array( + item: rlp::Rlp<'_>, + size: usize, + vec: &mut [u8], +) -> Result<(), DecoderError> { + let list = item.data()?; + if list.len() != size { + return Err(DecoderError::RlpIncorrectListLen); + } + vec.copy_from_slice(list); + Ok(()) +} + +pub fn append_option<'a, T: Encodable>( + stream: &'a mut RlpStream, + data: &Option, +) -> &'a mut RlpStream { + append_option_explicit(stream, data, |s, v| s.append(v)) +} + +// Combinator for encoding an optional value using an explicit +// encoding function, instead of the implemented in the Encodable trait. +pub fn append_option_explicit<'a, T, Enc>( + stream: &'a mut RlpStream, + data: &Option, + encoder: Enc, +) -> &'a mut RlpStream +where + Enc: Fn(&'a mut RlpStream, &T) -> &'a mut RlpStream, +{ + if let Some(value) = data { + encoder(stream, value) + } else { + stream.append_empty_data() + } +} + +pub fn append_vec<'a>(stream: &'a mut RlpStream, data: &Vec) -> &'a mut RlpStream { + if data.is_empty() { + stream.append_empty_data() + } else { + stream.append_iter((*data).clone()) + } +} + +/// Append H256 compressed must be used for signatures only. The signatures +/// in transaction are meant to be compressed, we should switch them to +/// U256 to make this explicit. +pub fn append_compressed_h256(s: &mut rlp::RlpStream, h256: H256) -> &mut RlpStream { + s.append(&U256::from_big_endian(h256.as_bytes())) +} + +/// Decode H256 compressed must be used for signatures only. +pub fn decode_compressed_h256(decoder: &Rlp<'_>) -> Result { + let length = decoder.data()?.len(); + if length == 32 { + Ok(H256::from_slice(decoder.data()?)) + } else if length < 32 && length > 0 { + // there were missing 0 that encoding deleted + let missing = 32 - length; + let mut full = [0u8; 32]; + full[missing..].copy_from_slice(decoder.data()?); + Ok(H256::from(full)) + } else if decoder.data()?.is_empty() { + // considering the case empty allows to decode unsigned transactions + Ok(H256::zero()) + } else { + Err(DecoderError::RlpInvalidLength) + } +} + +pub fn decode_tx_hash(item: rlp::Rlp<'_>) -> Result { + let list = item.data()?; + if list.len() != TRANSACTION_HASH_SIZE { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut tx = [0u8; TRANSACTION_HASH_SIZE]; + tx.copy_from_slice(list); + Ok(tx) +} + +pub fn decode_field_u256_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result { + let bytes: Vec = decode_field(decoder, field_name)?; + Ok(U256::from_little_endian(&bytes)) +} + +pub fn append_u256_le<'a>(stream: &'a mut RlpStream, v: &U256) -> &'a mut RlpStream { + let mut bytes = Into::<[u8; 32]>::into(*v); + v.to_little_endian(&mut bytes); + stream.append(&bytes.to_vec()) +} + +pub fn decode_field_u64_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result { + let bytes: Vec = decode_field(decoder, field_name)?; + let bytes_array: [u8; 8] = bytes.try_into().map_err(|_| { + DecoderError::Custom("Invalid conversion from vector of bytes to bytes.") + })?; + Ok(u64::from_le_bytes(bytes_array)) +} + +pub fn append_u64_le<'a>(stream: &'a mut RlpStream, v: &u64) -> &'a mut RlpStream { + stream.append(&v.to_le_bytes().to_vec()) +} + +pub fn decode_field_u32_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result { + let bytes: Vec = decode_field(decoder, field_name)?; + let bytes_array: [u8; 4] = bytes.try_into().map_err(|_| { + DecoderError::Custom("Invalid conversion from vector of bytes to bytes.") + })?; + Ok(u32::from_le_bytes(bytes_array)) +} + +pub fn append_u32_le<'a>(stream: &'a mut RlpStream, v: &u32) -> &'a mut RlpStream { + stream.append(&v.to_le_bytes().to_vec()) +} + +pub fn decode_field_u16_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result { + let bytes: Vec = decode_field(decoder, field_name)?; + let bytes_array: [u8; 2] = bytes + .try_into() + .map_err(|_| DecoderError::Custom("Field is not 2 bytes"))?; + Ok(u16::from_le_bytes(bytes_array)) +} + +pub fn append_u16_le<'a>(stream: &'a mut RlpStream, v: &u16) -> &'a mut RlpStream { + stream.append(&v.to_le_bytes().to_vec()) +} + +pub fn decode_transaction_hash( + decoder: &Rlp<'_>, +) -> Result { + let hash: H256 = decode_field(decoder, "transaction_hash")?; + Ok(hash.into()) +} + +pub fn decode_transaction_hash_list( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result, DecoderError> { + let custom_err = |_: DecoderError| (DecoderError::Custom(field_name)); + decoder + .iter() + .map(|rlp| rlp.as_val::().map(|h| h.into())) + .collect::, DecoderError>>() + .map_err(custom_err) +} +pub fn decode_transaction_type( + decoder: &Rlp<'_>, +) -> Result { + let tag: u8 = decode_field(decoder, "transaction_type")?; + TransactionType::try_from(&tag) + .map_err(|_| (DecoderError::Custom("Transaction type cannot be decoded"))) +} + +pub fn decode_transaction_status( + decoder: &Rlp<'_>, +) -> Result { + let tag: u8 = decode_field(decoder, "transaction_status")?; + TransactionStatus::try_from(&tag) + .map_err(|_| (DecoderError::Custom("Transaction status cannot be decoded"))) +} + +pub trait FromRlpBytes: Decodable { + fn from_rlp_bytes(bytes: &[u8]) -> Result; +} + +impl FromRlpBytes for T { + fn from_rlp_bytes(bytes: &[u8]) -> Result { + let decoder = Rlp::new(bytes); + Self::decode(&decoder) + } +} + +pub fn append_timestamp(stream: &mut RlpStream, timestamp: Timestamp) { + stream.append(×tamp.i64().to_le_bytes().to_vec()); +} + +pub fn decode_timestamp(decoder: &Rlp<'_>) -> Result { + let bytes: Vec = decode_field(decoder, "timestamp")?; + + let timestamp = i64::from_le_bytes(bytes.try_into().map_err(|_| { + DecoderError::Custom( + "Invalid conversion from timestamp vector of bytes to bytes.", + ) + })?) + .into(); + Ok(timestamp) +} + +/// Hardcoding the option RLP encoding, usable for types where we cannot +/// redefine their trait as they're defined in an external crate. +pub fn append_option_canonical<'a, T, Enc>( + stream: &'a mut rlp::RlpStream, + v: &Option, + append: Enc, +) where + Enc: Fn(&'a mut RlpStream, &T) -> &'a mut RlpStream, +{ + match v { + None => { + stream.begin_list(0); + } + Some(value) => { + stream.begin_list(1); + append(stream, value); + } + } +} + +/// Hardcoding the option RLP encoding for u64 in little endian. This is +/// unfortunately necessary as we cannot redefine the u64 encoding. +pub fn append_option_u64_le(v: &Option, stream: &mut rlp::RlpStream) { + append_option_canonical(stream, v, append_u64_le) +} + +/// See [append_option_canonical] +pub fn decode_option_canonical( + decoder: &Rlp<'_>, + field_name: &'static str, + dec_field: Dec, +) -> Result, DecoderError> +where + Dec: Fn(&Rlp<'_>, &'static str) -> Result, +{ + let items = decoder.item_count()?; + match items { + 1 => { + let mut it = decoder.iter(); + Ok(Some(dec_field(&next(&mut it)?, field_name)?)) + } + 0 => Ok(None), + _ => Err(DecoderError::RlpIncorrectListLen), + } +} + +/// See [append_option_u64_le] +pub fn decode_option_u64_le( + decoder: &Rlp<'_>, + field_name: &'static str, +) -> Result, DecoderError> { + decode_option_canonical(decoder, field_name, decode_field_u64_le) +} + +pub fn append_public_key(stream: &mut RlpStream, public_key: &PublicKey) { + let pk_b58 = PublicKey::to_b58check(public_key); + let pk_bytes = String::as_bytes(&pk_b58); + stream.append(&pk_bytes.to_vec()); +} + +pub fn decode_public_key(decoder: &Rlp<'_>) -> Result { + let bytes: Vec = decode_field(decoder, "public_key")?; + + let pk_b58 = String::from_utf8(bytes.to_vec()).map_err(|_| { + DecoderError::Custom( + "Invalid conversion from timestamp vector of bytes to bytes.", + ) + })?; + let pk = PublicKey::from_b58check(&pk_b58).map_err(|_| { + DecoderError::Custom( + "Invalid conversion from timestamp vector of bytes to bytes.", + ) + })?; + Ok(pk) +} + +pub trait VersionedEncoding: std::marker::Sized { + const VERSION: u8; + fn unversionned_encode(&self) -> bytes::BytesMut; + fn unversionned_decode(decoder: &Rlp) -> Result; + + /// from_bytes is never used outside of tests. If needed, it needs to be + /// implemented explicitly outside of the trait. + #[cfg(test)] + fn from_bytes(vec: &[u8]) -> Result { + let tag = vec[0]; + if tag == Self::VERSION { + let decoder = Rlp::new(&vec[1..]); + Self::unversionned_decode(&decoder) + } else { + Err(DecoderError::Custom("Decoding on unknown version")) + } + } + + fn to_bytes(&self) -> Vec { + let mut bytes: Vec = self.unversionned_encode().into(); + bytes.insert(0, Self::VERSION); + bytes + } +} diff --git a/etherlink/kernel_calypso2/ethereum/src/transaction.rs b/etherlink/kernel_calypso2/ethereum/src/transaction.rs new file mode 100644 index 000000000000..e288c77e8cd2 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/transaction.rs @@ -0,0 +1,448 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::{ + rlp_helpers::*, + tx_signature::{rlp_append_opt, rlp_decode_opt, TxSignature}, +}; +use ethbloom::{Bloom, Input}; +use ethereum::Log; +use primitive_types::{H160, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; + +pub const TRANSACTION_HASH_SIZE: usize = 32; +pub type TransactionHash = [u8; TRANSACTION_HASH_SIZE]; + +#[derive(Debug, PartialEq, Clone, Copy, Eq)] +pub enum TransactionType { + Legacy, + Eip2930, + Eip1559, +} + +#[derive(PartialEq, Debug, Clone, Copy)] +pub enum TransactionStatus { + Success, + Failure, +} + +pub enum TransactionDecodingError { + InvalidEncoding, + InvalidVectorLength, +} + +impl TryFrom<&u8> for TransactionStatus { + type Error = TransactionDecodingError; + + fn try_from(v: &u8) -> Result { + if *v == 0 { + Ok(Self::Failure) + } else if *v == 1 { + Ok(Self::Success) + } else { + Err(TransactionDecodingError::InvalidEncoding) + } + } +} + +impl From for u8 { + fn from(v: TransactionStatus) -> Self { + match v { + TransactionStatus::Failure => 0u8, + TransactionStatus::Success => 1u8, + } + } +} + +impl TryFrom<&u8> for TransactionType { + type Error = TransactionDecodingError; + + fn try_from(v: &u8) -> Result { + if *v == 0 { + Ok(Self::Legacy) + } else if *v == 1 { + Ok(Self::Eip2930) + } else if *v == 2 { + Ok(Self::Eip1559) + } else { + Err(TransactionDecodingError::InvalidEncoding) + } + } +} + +impl From for u8 { + fn from(v: TransactionType) -> Self { + match v { + TransactionType::Legacy => 0u8, + TransactionType::Eip2930 => 1u8, + TransactionType::Eip1559 => 2u8, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct IndexedLog { + pub log: Log, + /// Position of the log within a block. + pub index: u64, +} + +impl Encodable for IndexedLog { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(2); + stream.append(&self.log); + append_u64_le(stream, &self.index); + } +} + +impl Decodable for IndexedLog { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(2) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut it = decoder.iter(); + let log: Log = decode_field(&next(&mut it)?, "log")?; + let index: u64 = decode_field_u64_le(&next(&mut it)?, "index")?; + Ok(Self { log, index }) + } +} + +/// Transaction receipt, see https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionreceipt +#[derive(Debug, PartialEq, Clone)] +pub struct TransactionReceipt { + /// Hash of the transaction. + pub hash: TransactionHash, + /// Integer of the transactions index position in the block. + pub index: u32, + /// Block number where this transaction was in. + pub block_number: U256, + /// Address of the sender. + pub from: H160, + /// Address of the receiver. null when its a contract creation transaction. + pub to: Option, + /// The total amount of gas used when this transaction was executed in the block + pub cumulative_gas_used: U256, + /// The sum of the base fee and tip paid per unit of gas. + pub effective_gas_price: U256, + /// The amount of gas used by this specific transaction alone. + pub gas_used: U256, + /// The contract address created, if the transaction was a contract creation, otherwise null. + pub contract_address: Option, + /// The logs emitted during contract execution + pub logs: Vec, + /// The bloom filter corresponding to the logs. + /// It basically contains all addresses and topics from log objects. + pub logs_bloom: Bloom, + pub type_: TransactionType, + /// Transaction status + pub status: TransactionStatus, +} + +impl TransactionReceipt { + // DO NOT RENAME: function name is used during benchmark + // Never inlined when the kernel is compiled for benchmarks, to ensure the + // function is visible in the profiling results. + #[cfg_attr(feature = "benchmark", inline(never))] + pub fn logs_to_bloom(logs: &[IndexedLog]) -> Bloom { + let mut bloom = Bloom::default(); + // According to + // https://github.com/ethereum/go-ethereum/blob/41ee96fdfee5924004e8fbf9bbc8aef783893917/core/types/bloom9.go#L119 + for log in logs { + bloom.accrue(Input::Raw(log.log.address.as_bytes())); + for topic in log.log.topics.iter() { + bloom.accrue(Input::Raw(topic.as_bytes())); + } + } + bloom + } +} + +impl Decodable for TransactionReceipt { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(13) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let hash: TransactionHash = decode_transaction_hash(&next(&mut it)?)?; + let index: u32 = decode_field(&next(&mut it)?, "index")?; + let block_number: U256 = decode_field_u256_le(&next(&mut it)?, "block_number")?; + let from: H160 = decode_field(&next(&mut it)?, "from")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let cumulative_gas_used: U256 = + decode_field_u256_le(&next(&mut it)?, "cumulative_gas_used")?; + let effective_gas_price: U256 = + decode_field_u256_le(&next(&mut it)?, "effective_gas_price")?; + let gas_used: U256 = decode_field_u256_le(&next(&mut it)?, "gas_used")?; + let contract_address: Option = + decode_option(&next(&mut it)?, "contract_address")?; + let logs = decode_list(&next(&mut it)?, "logs")?; + let logs_bloom = decode_field(&next(&mut it)?, "logs_bloom")?; + let type_: TransactionType = decode_transaction_type(&next(&mut it)?)?; + let status: TransactionStatus = decode_transaction_status(&next(&mut it)?)?; + Ok(Self { + hash, + index, + block_number, + from, + to, + cumulative_gas_used, + effective_gas_price, + gas_used, + contract_address, + logs, + logs_bloom, + type_, + status, + }) + } +} + +impl Encodable for TransactionReceipt { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(13); + stream.append(&self.hash.to_vec()); + stream.append(&self.index); + append_u256_le(stream, &self.block_number); + stream.append(&self.from); + match &self.to { + Some(to) => stream.append(to), + None => stream.append_empty_data(), + }; + append_u256_le(stream, &self.cumulative_gas_used); + append_u256_le(stream, &self.effective_gas_price); + append_u256_le(stream, &self.gas_used); + match &self.contract_address { + Some(address) => stream.append(address), + None => stream.append_empty_data(), + }; + stream.append_list(&self.logs); + stream.append(&self.logs_bloom); + stream.append::(&self.type_.into()); + stream.append::(&self.status.into()); + } +} + +impl TryFrom<&[u8]> for TransactionReceipt { + type Error = DecoderError; + + fn try_from(bytes: &[u8]) -> Result { + Self::from_rlp_bytes(bytes) + } +} + +/// Transaction object, https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_gettransactionbyhash +/// There a lot of redundancy between a transaction object and a transaction +/// receipt. In fact, transaction objects should not be stored in the kernel +/// but rather in the EVM node. Duplicating the code instead of sharing fields +/// is intentional to facilitate the associated code to the EVM node. +/// TODO: https://gitlab.com/tezos/tezos/-/issues/5695 +#[derive(Debug, PartialEq, Clone)] +pub struct TransactionObject { + /// Block number where this transaction was in. null when its pending. + pub block_number: U256, + /// Address of the sender. + pub from: H160, + /// The amount of gas used by this specific transaction alone. + pub gas_used: U256, + /// The amount of gas price provided by the sender in Wei. + pub gas_price: U256, + /// Hash of the transaction. + pub hash: TransactionHash, + /// The data send along with the transaction. + pub input: Vec, + /// The number of transactions made by the sender prior to this one. + pub nonce: u64, + /// Address of the receiver. null when its a contract creation transaction. + pub to: Option, + /// Integer of the transactions index position in the block. + pub index: u32, + /// Value transferred in Wei. + pub value: U256, + /// ECDSA signature + pub signature: Option, +} + +impl Decodable for TransactionObject { + fn decode(decoder: &Rlp<'_>) -> Result { + if decoder.is_list() { + if Ok(13) == decoder.item_count() { + let mut it = decoder.iter(); + let block_number: U256 = + decode_field_u256_le(&next(&mut it)?, "block_number")?; + let from: H160 = decode_field(&next(&mut it)?, "from")?; + let gas_used: U256 = decode_field_u256_le(&next(&mut it)?, "gas_used")?; + let gas_price: U256 = decode_field_u256_le(&next(&mut it)?, "gas_price")?; + let hash: TransactionHash = decode_transaction_hash(&next(&mut it)?)?; + let input: Vec = decode_field(&next(&mut it)?, "input")?; + let nonce: u64 = decode_field_u64_le(&next(&mut it)?, "nonce")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let index: u32 = decode_field(&next(&mut it)?, "index")?; + let value: U256 = decode_field_u256_le(&next(&mut it)?, "value")?; + let signature = rlp_decode_opt(&mut it)?; + Ok(Self { + block_number, + from, + gas_used, + gas_price, + hash, + input, + nonce, + to, + index, + value, + signature, + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } +} + +impl Encodable for TransactionObject { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(13); + append_u256_le(stream, &self.block_number); + stream.append(&self.from); + append_u256_le(stream, &self.gas_used); + append_u256_le(stream, &self.gas_price); + stream.append(&self.hash.to_vec()); + stream.append(&self.input); + append_u64_le(stream, &self.nonce); + match &self.to { + Some(to) => stream.append(to), + None => stream.append_empty_data(), + }; + stream.append(&self.index); + append_u256_le(stream, &self.value); + rlp_append_opt(&self.signature, stream); + } +} + +#[cfg(test)] +mod test { + use super::*; + use primitive_types::H256; + + fn address_of_str(s: &str) -> H160 { + let data = &hex::decode(s).unwrap(); + H160::from_slice(data) + } + + fn receipt_encoding_roundtrip(v: TransactionReceipt) { + let bytes = v.rlp_bytes(); + let v2 = TransactionReceipt::from_rlp_bytes(&bytes) + .expect("Transaction receipt should be decodable"); + assert_eq!(v, v2, "Roundtrip failed on {:?}", v) + } + + fn tx_receipt(logs: Vec) -> TransactionReceipt { + TransactionReceipt { + hash: [0; TRANSACTION_HASH_SIZE], + index: 15u32, + block_number: U256::from(42), + from: address_of_str("3535353535353535353535353535353535353535"), + to: Some(address_of_str("3635353535353535353535353535353535353536")), + cumulative_gas_used: U256::from(1252345235), + effective_gas_price: U256::from(47457345), + gas_used: U256::from(474573452), + contract_address: Some(address_of_str( + "4335353535353535353535353535353535353543", + )), + type_: TransactionType::Legacy, + logs_bloom: TransactionReceipt::logs_to_bloom(&logs), + logs, + status: TransactionStatus::Success, + } + } + + #[test] + fn test_receipt_encoding_rountrip() { + let address = address_of_str("ef2d6d194084c2de36e0dabfce45d046b37d1106"); + let topic = H256::from_slice( + &hex::decode( + "02c69be41d0b7e40352fc85be1cd65eb03d40ef8427a0ca4596b1ead9a00e9fc", + ) + .expect("Valid hex"), + ); + let logs = vec![IndexedLog { + log: Log { + address, + topics: vec![topic], + data: vec![0, 1, 2, 3], + }, + index: 0, + }]; + + let v = tx_receipt(logs); + receipt_encoding_roundtrip(v.clone()); + + let v1 = TransactionReceipt { + to: None, + ..v.clone() + }; + receipt_encoding_roundtrip(v1); + + let v2 = TransactionReceipt { + to: None, + contract_address: None, + ..v + }; + receipt_encoding_roundtrip(v2); + } + + fn object_encoding_roundtrip(v: TransactionObject) { + let bytes = v.rlp_bytes(); + let v2 = TransactionObject::from_rlp_bytes(&bytes) + .expect("Transaction object should be decodable"); + assert_eq!(v, v2, "Roundtrip failed on {:?}", v) + } + + #[test] + fn test_object_encoding_rountrip() { + let v = TransactionObject { + block_number: U256::from(532532), + from: address_of_str("3535353535353535353535353535353535353535"), + gas_used: U256::from(32523), + gas_price: U256::from(100432432), + hash: [5; TRANSACTION_HASH_SIZE], + input: vec![], + nonce: 8888, + to: Some(address_of_str("3635353535353535353535353535353535353536")), + index: 15u32, + value: U256::from(0), + signature: Some( + TxSignature::new( + U256::from(1337), + H256::from_low_u64_be(1), + H256::from_low_u64_be(2), + ) + .unwrap(), + ), + }; + object_encoding_roundtrip(v.clone()); + + let v1 = TransactionObject { + to: None, + ..v.clone() + }; + object_encoding_roundtrip(v1); + + let v2 = TransactionObject { + to: None, + input: [15; 564].to_vec(), + ..v + }; + object_encoding_roundtrip(v2); + } +} diff --git a/etherlink/kernel_calypso2/ethereum/src/tx_common.rs b/etherlink/kernel_calypso2/ethereum/src/tx_common.rs new file mode 100644 index 000000000000..8cd3f24e57cb --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/tx_common.rs @@ -0,0 +1,1665 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +//! Signature functions for Ethereum compatibility +//! +//! We need to sign and write Ethereum specific values such +//! as addresses and values. + +use std::array::TryFromSliceError; + +use libsecp256k1::{recover, Message, RecoveryId, Signature}; +use primitive_types::{H160, H256, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpIterator, RlpStream}; +use sha3::{Digest, Keccak256}; +use thiserror::Error; + +use crate::{ + access_list::AccessList, + rlp_helpers::{ + append_compressed_h256, append_option, append_vec, decode_compressed_h256, + decode_field, decode_list, decode_option, next, + }, + transaction::TransactionType, + tx_signature::{TxSigError, TxSignature}, +}; + +#[derive(Error, Debug, PartialEq)] +pub enum SigError { + #[error("Error decoding RLP encoded byte array: {0}")] + DecoderError(#[from] DecoderError), + + #[error("Error extracting a slice")] + SlicingError, + + #[error("Signature error: {0}")] + TxSigError(TxSigError), + + #[error("Transaction doesn't have a signature")] + UnsignedTransactionError, +} + +impl From for SigError { + fn from(_: TryFromSliceError) -> Self { + Self::SlicingError + } +} + +impl From for SigError { + fn from(e: TxSigError) -> Self { + SigError::TxSigError(e) + } +} + +/// Data common for all kind of Ethereum transactions +/// (transfers, contract creation and contract invocation). +/// All transaction versions (Legacy, EIP-2930 and EIP-1559) +/// are parsed to this common type. +/// This type is common for both signed and unsigned transactions as well. +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct EthereumTransactionCommon { + pub type_: TransactionType, + /// the id of the chain + /// see `` for values + /// None if the signature doesn't contain the chain id (pre EIP-155). + pub chain_id: Option, + /// A scalar value equal to the number of transactions sent by the sender + pub nonce: u64, + + /// Normally, this would be a fee paid per gas in addition to base fee per gas. + /// This would incentivise miners to include the transaction. + /// More details see here https://eips.ethereum.org/EIPS/eip-1559#abstract + /// + /// We choose to ignore this, however, as we actually do not implement eip-1559 + /// mechanism exactly. The sequencer is compensated via the data availability fee. + /// + /// We keep this field purely for compatibility with existing ethereum tooling. + max_priority_fee_per_gas: U256, + /// Maximum amount of fee to be paid per gas. + /// Thus, as a transaction might be included in the block + /// with higher base_fee_per_gas then one + /// at the moment of the creation of tx, + /// this value is protection for user that + /// they won't pay for a tx more than they wanted to pay. + /// Given this cap, effective priority fee will be equal to + /// min(max_priority_fee_per_gas, max_fee_per_gas - base_fee_per_gas). + /// More details see here https://eips.ethereum.org/EIPS/eip-1559#abstract + pub max_fee_per_gas: U256, + + /// The maximum amount of gas that the user is willing to pay. + /// + /// *NB* this is inclusive of any additional fees that are paid, prior to execution: + /// - data availability fee + gas_limit: u64, + /// The 160-bit address of the message call’s recipient + /// or, for a contract creation transaction + pub to: Option, + /// A scalar value equal to the number of Wei to + /// be transferred to the message call’s recipient or, + /// in the case of contract creation, as an endowment + /// to the newly created account + pub value: U256, + /// the transaction data. In principle this can be large + pub data: Vec, + /// Access list specifies a list of addresses and storage keys, + /// which are going to be accessed during transaction execution. + /// For more information see https://eips.ethereum.org/EIPS/eip-2930 + pub access_list: AccessList, + /// If transaction is unsigned then this field is None + /// See encoding details in + pub signature: Option, +} + +impl EthereumTransactionCommon { + #[allow(clippy::too_many_arguments)] + pub fn new( + type_: TransactionType, + chain_id: Option, + nonce: u64, + max_priority_fee_per_gas: U256, + max_fee_per_gas: U256, + gas_limit: u64, + to: Option, + value: U256, + data: Vec, + access_list: AccessList, + signature: Option, + ) -> Self { + Self { + type_, + chain_id, + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + data, + access_list, + signature, + } + } + + // This decoding function encapsulates logic of decoding (v, r, s). + // There might be 3 possible cases: + // - unsigned NON-legacy: (0, 0, 0) + // - signed NON-legacy: (v, r, s), r > 0 && s > 0 + // - unsigned legacy: none of coordinates present + fn rlp_decode_vrs( + it: &mut RlpIterator, + ) -> Result, DecoderError> { + let v = it.next(); + let r = it.next(); + let s = it.next(); + match (v, r, s) { + // It might be either signed NON-legacy tx case or usigned legacy with all zeros + (Some(v), Some(r), Some(s)) => { + let v: U256 = decode_field(&v, "v")?; + let r: H256 = decode_compressed_h256(&r)?; + let s: H256 = decode_compressed_h256(&s)?; + Ok(Some((v, r, s))) + } + // It might be unsigned NON-legacy tx case + (None, None, None) => Ok(None), + // Malformed signature: none of the cases is applicable + _ => Err(DecoderError::Custom( + "Invalid transaction encoding: neither signed nor unsigned tx", + )), + } + } + + // RLP decoding of legacy tx + fn rlp_decode_legacy_tx(decoder: &Rlp) -> Result { + // If we don't have 9 elements, then list has incorrect length + if decoder.item_count() != Ok(9) { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let nonce: u64 = decode_field(&next(&mut it)?, "nonce")?; + let gas_price: U256 = decode_field(&next(&mut it)?, "gas_price")?; + let gas_limit: u64 = decode_field(&next(&mut it)?, "gas_limit")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let value: U256 = decode_field(&next(&mut it)?, "value")?; + let data: Vec = decode_field(&next(&mut it)?, "data")?; + + // Decode v, r, s of signatures + let vrs = Self::rlp_decode_vrs(&mut it)?; + // In case of legacy tx: both signed and unsigned tx has 9 elements, + // hence all v, r, and s should be presented + let (v, r, s) = + vrs.ok_or( + DecoderError::Custom("Invalid legacy tx encoding: legacy tx has to have 9 elements in the RLP list"))?; + let is_unsigned = r == H256::zero() && s == H256::zero(); + + // Derive chain_id from v of the signature + let chain_id = if is_unsigned { + // in a rlp encoded unsigned eip-155 transaction, v is used to store the chainid + Ok(Some(v)) + } else { + // in a rlp encoded signed eip-155 transaction, v is {0,1} + CHAIN_ID * 2 + 35 + // v > 36 is because we support only chain_id which is strictly greater than 0 + if v > U256::from(36) { + Ok(Some((v - 35) / 2)) + // signatures pre EIP-155 don't encode the chain id in the parity of + // the signature, as such `v` is either 27 or 28. + } else if v == U256::from(27) || v == U256::from(28) { + Ok(None) + } else { + Err(DecoderError::Custom( + "v has to be greater than 36 for a signed EIP-155 transaction", + )) + } + }?; + + // Set signature + let signature = if is_unsigned { + None + } else { + Some( + TxSignature::new(v, r, s) + .map_err(|_| DecoderError::Custom("Invalid signature"))?, + ) + }; + Ok(EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id, + nonce, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + // default value for access_list + access_list: vec![], + signature, + }) + } + + // RLP decoding of EIP-2930 tx + fn rlp_decode_eip2930_tx(decoder: &Rlp) -> Result { + // It's either: + // - 8 fields fields for an unsigned tx + // - 11 fields for a signed tx + if decoder.item_count() != Ok(8) && decoder.item_count() != Ok(11) { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let chain_id: U256 = decode_field(&next(&mut it)?, "chain_id")?; + let nonce: u64 = decode_field(&next(&mut it)?, "nonce")?; + let gas_price: U256 = decode_field(&next(&mut it)?, "gas_price")?; + let gas_limit: u64 = decode_field(&next(&mut it)?, "gas_limit")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let value: U256 = decode_field(&next(&mut it)?, "value")?; + let data: Vec = decode_field(&next(&mut it)?, "data")?; + let access_list = decode_list(&next(&mut it)?, "access_list")?; + // Decode a signature if it exists + let vrs = Self::rlp_decode_vrs(&mut it)?; + let signature = match vrs { + Some((v, r, s)) => TxSignature::new(v, r, s) + .map(Option::Some) + .map_err(|_| DecoderError::Custom("Invalid signature")), + None => Ok(None), + }?; + + Ok(EthereumTransactionCommon { + type_: TransactionType::Eip2930, + chain_id: Some(chain_id), + nonce, + max_fee_per_gas: gas_price, + max_priority_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + access_list, + signature, + }) + } + + fn rlp_decode_eip1559_tx(decoder: &Rlp) -> Result { + // It's either: + // - 9 fields fields for an unsigned tx + // - 12 fields for a signed tx + if decoder.item_count() != Ok(9) && decoder.item_count() != Ok(12) { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let chain_id: U256 = decode_field(&next(&mut it)?, "chain_id")?; + let nonce: u64 = decode_field(&next(&mut it)?, "nonce")?; + let max_priority_fee_per_gas = + decode_field(&next(&mut it)?, "max_priority_fee_per_gas")?; + let max_fee_per_gas = decode_field(&next(&mut it)?, "max_fee_per_gas")?; + let gas_limit: u64 = decode_field(&next(&mut it)?, "gas_limit")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let value: U256 = decode_field(&next(&mut it)?, "value")?; + let data: Vec = decode_field(&next(&mut it)?, "data")?; + let access_list: AccessList = decode_list(&next(&mut it)?, "access_list")?; + + let vrs = Self::rlp_decode_vrs(&mut it)?; + let signature = match vrs { + Some((v, r, s)) => TxSignature::new(v, r, s) + .map(Option::Some) + .map_err(|_| DecoderError::Custom("Invalid signature")), + None => Ok(None), + }?; + Ok(EthereumTransactionCommon { + type_: TransactionType::Eip1559, + chain_id: Some(chain_id), + nonce, + max_priority_fee_per_gas, + max_fee_per_gas, + gas_limit, + to, + value, + data, + access_list, + signature, + }) + } + + fn from_rlp_any( + decoder: &Rlp, + tx_version: TransactionType, + ) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + let tx = match tx_version { + TransactionType::Legacy => Self::rlp_decode_legacy_tx(decoder), + TransactionType::Eip2930 => Self::rlp_decode_eip2930_tx(decoder), + TransactionType::Eip1559 => Self::rlp_decode_eip1559_tx(decoder), + }?; + Ok(tx) + } + + /// Encodes a transaction as before EIP-155, i.e. without the chain_id + fn rlp_encode_legacy_pre_eip155(&self, stream: &mut RlpStream) { + if self.signature.is_some() { + // If there is a signature, there will be 9 fields + stream.begin_list(9); + } else { + // Otherwise, there won't be signature + stream.begin_list(6); + } + + stream.append(&self.nonce); + // self.max_fee_per_gas has to be equal to gas_price + stream.append(&self.max_fee_per_gas); + stream.append(&self.gas_limit); + append_option(stream, &self.to); + stream.append(&self.value); + append_vec(stream, &self.data); + if let Some(sig) = &self.signature { + sig.rlp_append(stream) + } + } + + fn rlp_encode_legacy_post_eip155(&self, chain_id: U256, stream: &mut RlpStream) { + stream.begin_list(9); + stream.append(&self.nonce); + // self.max_fee_per_gas has to be equal to gas_price + stream.append(&self.max_fee_per_gas); + stream.append(&self.gas_limit); + append_option(stream, &self.to); + stream.append(&self.value); + append_vec(stream, &self.data); + match &self.signature { + None => { + // In case of unsigned legacy tx we have to append chain_id as v component of (v, r, s) + stream.append(&chain_id); + append_compressed_h256(stream, H256::zero()); + append_compressed_h256(stream, H256::zero()); + } + Some(sig) => sig.rlp_append(stream), + } + } + fn rlp_encode_legacy_tx(&self, stream: &mut RlpStream) { + match self.chain_id { + Some(chain_id) => self.rlp_encode_legacy_post_eip155(chain_id, stream), + None => self.rlp_encode_legacy_pre_eip155(stream), + } + } + + fn rlp_encode_eip2930_tx(&self, stream: &mut RlpStream) { + if self.signature.is_some() { + // If there is a signature, there will be 11 fields + stream.begin_list(11); + } else { + // Otherwise, there won't be signature + stream.begin_list(8); + } + // In that case the chain id is mandatory, as such this unwrapping is safe + stream.append(&self.chain_id.unwrap()); + stream.append(&self.nonce); + // self.max_fee_per_gas has to be equal to gas_price + stream.append(&self.max_fee_per_gas); + stream.append(&self.gas_limit); + append_option(stream, &self.to); + stream.append(&self.value); + append_vec(stream, &self.data); + stream.append_list(&self.access_list); + + match &self.signature { + Some(sig) => sig.rlp_append(stream), + // If tx is NOT legacy and unsigned: DON'T append anything like (0, 0, 0) + None => (), + } + } + + fn rlp_encode_eip1559_tx(&self, stream: &mut RlpStream) { + if self.signature.is_some() { + // If there is a signature, there will be 12 fields + stream.begin_list(12); + } else { + // Otherwise, there won't be signature + stream.begin_list(9); + } + + // In that case the chain id is mandatory, as such this unwrapping is safe + stream.append(&self.chain_id.unwrap()); + stream.append(&self.nonce); + stream.append(&self.max_priority_fee_per_gas); + stream.append(&self.max_fee_per_gas); + stream.append(&self.gas_limit); + append_option(stream, &self.to); + stream.append(&self.value); + append_vec(stream, &self.data); + stream.append_list(&self.access_list); + + match &self.signature { + Some(sig) => sig.rlp_append(stream), + // If tx is NOT legacy and unsigned: DON'T append anything like (0, 0, 0) + None => (), + } + } + + fn to_rlp_any(self: &EthereumTransactionCommon, stream: &mut RlpStream) { + match &self.type_ { + TransactionType::Legacy => self.rlp_encode_legacy_tx(stream), + TransactionType::Eip2930 => self.rlp_encode_eip2930_tx(stream), + TransactionType::Eip1559 => self.rlp_encode_eip1559_tx(stream), + } + } + + /// Extracts the Keccak encoding of a message from an EthereumTransactionCommon + #[cfg_attr(feature = "benchmark", inline(never))] + fn message(&self) -> Message { + let to_sign = EthereumTransactionCommon { + signature: None, + ..self.clone() + }; + + let hash: [u8; 32] = Keccak256::digest(to_sign.to_bytes()).into(); + Message::parse(&hash) + } + + /// Extracts the signature from an EthereumTransactionCommon + pub fn signature(&self) -> Result<(Signature, RecoveryId), SigError> { + let tx_signature = self + .signature + .as_ref() + .ok_or(SigError::UnsignedTransactionError)?; + match self.type_ { + TransactionType::Legacy => tx_signature + .signature_legacy(self.chain_id) + .map_err(SigError::TxSigError), + _ => tx_signature.signature().map_err(SigError::TxSigError), + } + } + + /// Find the caller address from r and s of the common data + /// for an Ethereum transaction, ie, what address is associated + /// with the signature of the message. + // TODO + // DO NOT RENAME: function name is used during benchmark + // Never inlined when the kernel is compiled for benchmarks, to ensure the + // function is visible in the profiling results. + #[cfg_attr(feature = "benchmark", inline(never))] + pub fn caller(&self) -> Result { + let mes = self.message(); + let (sig, ri) = self.signature()?; + let pk = recover(&mes, &sig, &ri).map_err(TxSigError::ECDSAError)?; + let serialised = &pk.serialize()[1..]; + let kec = Keccak256::digest(serialised); + let value: [u8; 20] = kec.as_slice()[12..].try_into()?; + + Ok(value.into()) + } + + /// Produce a signed EthereumTransactionCommon. If the initial one was signed + /// you should get the same thing. + pub fn sign_transaction(&self, string_sk: String) -> Result { + let mes = self.message(); + let signature = match self.type_ { + TransactionType::Legacy => { + TxSignature::sign_legacy(&mes, string_sk, self.chain_id)? + } + _ => TxSignature::sign(&mes, string_sk)?, + }; + + Ok(EthereumTransactionCommon { + signature: Some(signature), + ..self.clone() + }) + } + + // Unserialize Ethereum tx of arbitrary version from raw bytes. + // This is a separate method of the tx type + // but not rlp::Decodable instance because after legacy + // version a tx encoding, strictly speaking, is not RLP list anymore, + // rather opaque sequence of bytes. + pub fn from_bytes(bytes: &[u8]) -> Result { + let first = *bytes.first().ok_or(DecoderError::Custom("Empty bytes"))?; + if first == 0x01 { + let decoder = Rlp::new(&bytes[1..]); + Self::from_rlp_any(&decoder, TransactionType::Eip2930) + } else if first == 0x02 { + let decoder = Rlp::new(&bytes[1..]); + Self::from_rlp_any(&decoder, TransactionType::Eip1559) + } else { + let decoder = Rlp::new(bytes); + Self::from_rlp_any(&decoder, TransactionType::Legacy) + } + } + + /// Unserialize an hex string + pub fn from_hex(e: String) -> Result { + let tx = + hex::decode(e).or(Err(DecoderError::Custom("Couldn't parse hex value")))?; + Self::from_bytes(&tx) + } + + // Serialize Ethereum tx of arbitrary version to raw bytes. + // This is a separate method of the tx type + // but not rlp::Encodable instance because after legacy + // version a tx encoding, strictly speaking, is not RLP list anymore, + // rather opaque sequence of bytes. + pub fn to_bytes(&self) -> Vec { + let mut stream = RlpStream::new(); + self.to_rlp_any(&mut stream); + let mut rlp_enc = stream.out().to_vec(); + match self.type_ { + TransactionType::Legacy => rlp_enc, + TransactionType::Eip2930 | TransactionType::Eip1559 => { + let tag = From::from(self.type_); + rlp_enc.insert(0, tag); + rlp_enc + } + } + } + + /// Returns the total gas limit for this transaction, including execution and fees. + #[inline(always)] + pub const fn gas_limit_with_fees(&self) -> u64 { + self.gas_limit + } +} + +impl From for EthereumTransactionCommon { + /// Decode a transaction in hex format. Unsafe, to be used only in tests : panics when fails + fn from(e: String) -> Self { + EthereumTransactionCommon::from_hex(e).unwrap() + } +} + +impl TryFrom<&[u8]> for EthereumTransactionCommon { + type Error = DecoderError; + + fn try_from(bytes: &[u8]) -> Result { + Self::from_bytes(bytes) + } +} + +#[allow(clippy::from_over_into)] +impl Into> for EthereumTransactionCommon { + fn into(self) -> Vec { + self.to_bytes() + } +} + +// Produces address from a secret key +// Used in tests only +pub fn string_to_sk_and_address_unsafe( + s: String, +) -> (libsecp256k1::SecretKey, primitive_types::H160) { + use libsecp256k1::PublicKey; + use libsecp256k1::SecretKey; + + let mut data: [u8; 32] = [0u8; 32]; + hex::decode_to_slice(s, &mut data).unwrap(); + let sk = SecretKey::parse(&data).unwrap(); + let pk = PublicKey::from_secret_key(&sk); + let serialised = &pk.serialize()[1..]; + let kec = Keccak256::digest(serialised); + let mut value: [u8; 20] = [0u8; 20]; + value.copy_from_slice(&kec[12..]); + (sk, value.into()) +} + +impl Encodable for EthereumTransactionCommon { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let eth_bytes = self.to_bytes(); + stream.encoder().encode_value(ð_bytes) + } +} + +impl Decodable for EthereumTransactionCommon { + fn decode(decoder: &rlp::Rlp) -> Result { + let bytes: Vec = decoder.as_val()?; + EthereumTransactionCommon::from_bytes(&bytes) + } +} + +// cargo test ethereum::signatures::test --features testing +#[cfg(test)] +mod test { + + use std::{ops::Neg, str::FromStr}; + + use libsecp256k1::curve::Scalar; + + use crate::access_list::AccessListItem; + + use crate::tx_signature::{h256_to_scalar, TxSignature}; + + use super::*; + fn address_from_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + + // utility function to just build a standard correct transaction + // extracted from example in EIP 155 standard + // https://github.com/ethereum/EIPs/blob/master/EIPS/eip-155.md + // signing data 0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080 + // private key : 0x4646464646464646464646464646464646464646464646464646464646464646 + // corresponding address 0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f + // signed tx : 0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83 + fn basic_eip155_transaction() -> EthereumTransactionCommon { + EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 9, + max_fee_per_gas: U256::from(20000000000u64), + max_priority_fee_per_gas: U256::from(20000000000u64), + gas_limit: 21000u64, + to: address_from_str("3535353535353535353535353535353535353535"), + value: U256::from(1000000000000000000u64), + data: vec![], + access_list: vec![], + signature: Some(TxSignature::new_unsafe( + 37, + string_to_h256_unsafe( + "28EF61340BD939BC2195FE537567866003E1A15D3C71FF63E1590620AA636276", + ), + string_to_h256_unsafe( + "67CBE9D8997F761AECB703304B3800CCF555C9F3DC64214B297FB1966A3B6D83", + ), + )), + } + } + + fn basic_eip155_transaction_unsigned() -> EthereumTransactionCommon { + EthereumTransactionCommon { + signature: None, + ..basic_eip155_transaction() + } + } + + fn eip2930_tx() -> EthereumTransactionCommon { + EthereumTransactionCommon { + type_: TransactionType::Eip2930, + chain_id: Some(U256::from(1900)), + nonce: 34, + max_fee_per_gas: U256::from(1000000000u64), + max_priority_fee_per_gas: U256::from(1000000000u64), + gas_limit: 100000, + to: address_from_str("09616C3d61b3331fc4109a9E41a8BDB7d9776609"), + value: U256::from(0x5af3107a4000_u64), + data: hex::decode("616263646566").unwrap(), + access_list: vec![AccessListItem { + address: address_from_str("0000000000000000000000000000000000000001") + .unwrap(), + storage_keys: vec![H256::from_str( + "0100000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap()], + }], + signature: Some(TxSignature::new_unsafe( + 0, + string_to_h256_unsafe( + "ea38506c4afe4bb402e030877fbe1011fa1da47aabcf215db8da8fee5d3af086", + ), + string_to_h256_unsafe( + "51e9af653b8eb98e74e894a766cf88904dbdb10b0bc1fbd12f18f661fa2797a4", + ), + )), + } + } + + fn eip1559_tx() -> EthereumTransactionCommon { + EthereumTransactionCommon { + type_: TransactionType::Eip1559, + chain_id: Some( U256::one()), + nonce: 4142, + max_priority_fee_per_gas: U256::from(1000000000), + max_fee_per_gas: U256::from(28750000000_i64), + gas_limit: 37262, + to: address_from_str("9f8f72aa9304c8b593d555f12ef6589cc3a579a2"), + value: U256::from(0), + data: hex::decode("a9059cbb000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43000000000000000000000000000000000000000000000000f020482e89b73c14").unwrap(), + access_list: vec![], + signature: Some(TxSignature::new_unsafe( + 0, + string_to_h256_unsafe( + "f876cca0898a15f2eaf17ee8a0ac33ec7933b17db612bd5aaf5925774da84fad", + ), + string_to_h256_unsafe( + "5c7310fa69b650d583fa5a66fbcfd720e96d9eacadc18632595cb6423e2f613f", + ), + )), + } + } + + fn h256_to_string(e: H256) -> String { + format!("{:x}", e) + } + + /// used in test to decode a string and get the size of the decoded input, + /// before determining the H256 value + fn decode_compressed_h256_helper(str: &str) -> (Result, usize) { + let hash = hex::decode(str).unwrap(); + let decoder = Rlp::new(&hash); + let decoded = decode_compressed_h256(&decoder); + assert!(decoded.is_ok(), "hash should be decoded ok"); + let length = decoder.data().unwrap().len(); + (decoded, length) + } + + #[test] + fn test_decode_compressed_h256_l0() { + // rlp encoding of empty is the byte 80 + let (decoded, length) = decode_compressed_h256_helper("80"); + assert_eq!(0, length); + assert_eq!( + H256::zero(), + decoded.unwrap(), + "empty hash should be decoded as 0x0...0" + ); + } + + #[test] + fn test_decode_h256_l32() { + // rlp encoding of hex string of 32 bytes + let (decoded, length) = decode_compressed_h256_helper( + "a03232323232323232323232323232323232323232323232323232323232323232", + ); + assert_eq!(32, length); + assert_eq!( + "3232323232323232323232323232323232323232323232323232323232323232", + h256_to_string(decoded.unwrap()), + "32 hash should be decoded as 0x32...32" + ); + } + + #[test] + fn test_decode_h256_l31() { + // rlp encoding of hex string of 31 bytes + let (decoded, length) = decode_compressed_h256_helper( + "9f31313131313131313131313131313131313131313131313131313131313131", + ); + assert_eq!(31, length); + assert_eq!( + "0031313131313131313131313131313131313131313131313131313131313131", + h256_to_string(decoded.unwrap()), + "31 hash should be decoded as 0x0031..31" + ); + } + + #[test] + fn test_caller_classic() { + // setup + let (_sk, address_from_sk) = string_to_sk_and_address_unsafe( + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string(), + ); + let encoded = + "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(); + + let expected_address = + address_from_str("9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F").unwrap(); + + // act + let transaction = EthereumTransactionCommon::from_hex(encoded).unwrap(); + let address = transaction.caller().unwrap(); + + // assert + assert_eq!(expected_address, address); + assert_eq!(expected_address, address_from_sk) + } + + #[test] + fn test_decoding_eip_155_example_unsigned() { + // setup + let expected_transaction = basic_eip155_transaction_unsigned(); + let signing_data = "ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080"; + + // act + let tx = hex::decode(signing_data).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + assert!(decoded.is_ok(), "testing the decoding went ok"); + + // assert + let decoded_transaction = decoded.unwrap(); + assert_eq!(expected_transaction, decoded_transaction) + } + + #[test] + fn test_decoding_leading0_signature() { + // decoding of a transaction where r or s had some leading 0, which where deleted + let signed_tx = "f888018506fc23ac00831000009412f142944da31ab85458787aaecaf5e34128619d80a40b7d796e0000000000000000000000000000000000000000000000000000000000000000269f75b1bc94b868a5a047470eae6008602e414d1471c2bbd14b37ffe56b1a85c9a001d9d58bb23af2090742aab9824c916fdc021a91f3e8d36571a5fc55547bc596"; + + // act + let tx = hex::decode(signed_tx).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert!(decoded.is_ok(), "testing the decoding went ok"); + } + + #[test] + fn test_encoding_eip155_unsigned() { + // setup + let expected_transaction = basic_eip155_transaction_unsigned(); + let signing_data = "ec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080"; + + // act + let encoded = expected_transaction.to_bytes(); + + // assert + assert_eq!(signing_data, hex::encode(encoded)); + } + + pub fn string_to_h256_unsafe(s: &str) -> H256 { + let mut v: [u8; 32] = [0; 32]; + hex::decode_to_slice(s, &mut v).expect("Could not parse to 256 hex value."); + H256::from(v) + } + + fn basic_create() -> EthereumTransactionCommon { + // transaction "without to field" + // private key : 0x4646464646464646464646464646464646464646464646464646464646464646 + // corresponding address 0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f + // signed tx : 0xf8572e8506c50218ba8304312280843b9aca0082ffff26a0e9637495be4c216a833ef390b1f6798917c8a102ab165c5085cced7ca1f2eb3aa057854e7044a8fee7bccb6a2c32c4229dd9cbacad74350789e0ce75bf40b6f713 + let nonce = 46; + let gas_price = U256::from(29075052730u64); + let gas_limit = 274722u64; + let to = None; + let value = U256::from(1000000000u64); + let data: Vec = hex::decode("ffff").unwrap(); + let chain_id = Some(U256::one()); + let r = string_to_h256_unsafe( + "e9637495be4c216a833ef390b1f6798917c8a102ab165c5085cced7ca1f2eb3a", + ); + let s = string_to_h256_unsafe( + "57854e7044a8fee7bccb6a2c32c4229dd9cbacad74350789e0ce75bf40b6f713", + ); + EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id, + nonce, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + access_list: vec![], + signature: Some(TxSignature::new_unsafe(38, r, s)), + } + } + + #[test] + fn test_encoding_create() { + // setup + let transaction = basic_create(); + let expected_encoded = "f8572e8506c50218ba8304312280843b9aca0082ffff26a0e9637495be4c216a833ef390b1f6798917c8a102ab165c5085cced7ca1f2eb3aa057854e7044a8fee7bccb6a2c32c4229dd9cbacad74350789e0ce75bf40b6f713"; + + // act + let encoded = transaction.to_bytes(); + + // assert + assert_eq!(expected_encoded, hex::encode(encoded)); + } + + #[test] + fn test_decoding_create() { + // setup + let expected_transaction = basic_create(); + let signed_tx = "f8572e8506c50218ba8304312280843b9aca0082ffff26a0e9637495be4c216a833ef390b1f6798917c8a102ab165c5085cced7ca1f2eb3aa057854e7044a8fee7bccb6a2c32c4229dd9cbacad74350789e0ce75bf40b6f713"; + + // act + let tx = hex::decode(signed_tx).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert!(decoded.is_ok()); + assert_eq!(expected_transaction, decoded.unwrap()); + } + + #[test] + fn test_encoding_eip155_signed() { + // setup + let expected_transaction = basic_eip155_transaction(); + let signed_tx = "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"; + + // act + let encoded = expected_transaction.to_bytes(); + + // assert + assert_eq!(signed_tx, hex::encode(encoded)); + } + + #[test] + fn test_decoding_arbitrary_signed() { + // arbitrary transaction with data + //setup + let nonce = 0; + let gas_price = U256::from(40000000000u64); + let gas_limit = 21000u64; + let to = address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"); + let value = U256::from(5000000000000000u64); + let data: Vec = hex::decode("deace8f5000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000041bca408a6b4029b42883aeb2c25087cab76cb58000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000002357a49c7d75f600000000000000000000000000000000000000000000000000000000640b5549000000000000000000000000710bda329b2a6224e4b44833de30f38e7f81d5640000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let r = string_to_h256_unsafe( + "25dd6c973368c45ddfc17f5148e3f468a2e3f2c51920cbe9556a64942b0ab2eb", + ); + let s = string_to_h256_unsafe( + "31da07ce40c24b0a01f46fb2abc028b5ccd70dbd1cb330725323edc49a2a9558", + ); + let expected_transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + access_list: vec![], + signature: Some(TxSignature::new_unsafe(37, r, s)), + }; + let signed_data = "f90150808509502f900082520894423163e58aabec5daa3dd1130b759d24bef0f6ea8711c37937e08000b8e4deace8f5000000000000000000000000000000000000000000000000000000000000a4b100000000000000000000000041bca408a6b4029b42883aeb2c25087cab76cb58000000000000000000000000000000000000000000000000002386f26fc10000000000000000000000000000000000000000000000000000002357a49c7d75f600000000000000000000000000000000000000000000000000000000640b5549000000000000000000000000710bda329b2a6224e4b44833de30f38e7f81d564000000000000000000000000000000000000000000000000000000000000000025a025dd6c973368c45ddfc17f5148e3f468a2e3f2c51920cbe9556a64942b0ab2eba031da07ce40c24b0a01f46fb2abc028b5ccd70dbd1cb330725323edc49a2a9558"; + + // act + let tx = hex::decode(signed_data).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert_eq!(Ok(expected_transaction), decoded) + } + + #[test] + fn test_decoding_eip_155_example_signed() { + // setup + let expected_transaction = basic_eip155_transaction(); + let signed_data = "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83"; + + // act + let tx = hex::decode(signed_data).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert!(decoded.is_ok(), "testing the decoding went ok"); + let decoded_transaction = decoded.unwrap(); + assert_eq!(expected_transaction, decoded_transaction) + } + + #[test] + fn test_decoding_uniswap_call_signed() { + // inspired by 0xf598016f51e0544187088ddd50fd37818fd268a0363a17281576425f3ee334cb + // private key dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701 + // corresponding address 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + // to 0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b + // data: 0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000 + // tx: 0xf903732e8506c50218ba8304312294ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b880a8db2d41b89b009b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000025a0c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45aa05721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06 + + //setup + let nonce = 46; + let gas_price = U256::from(29075052730u64); + let gas_limit = 274722u64; + let to = address_from_str("ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b"); + let value = U256::from(760460536160301065u64); // /!\ > 2^53 -1 + let data: Vec = hex::decode("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let r = string_to_h256_unsafe( + "c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45a", + ); + let s = string_to_h256_unsafe( + "5721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06", + ); + let expected_transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + access_list: vec![], + signature: Some(TxSignature::new_unsafe(37, r, s)), + }; + + // act + let signed_data = "f903732e8506c50218ba8304312294ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b880a8db2d41b89b009b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000025a0c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45aa05721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06"; + let tx = hex::decode(signed_data).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert_eq!(Ok(expected_transaction), decoded); + } + + #[test] + fn test_encoding_uniswap_call_signed() { + // inspired by 0xf598016f51e0544187088ddd50fd37818fd268a0363a17281576425f3ee334cb + // private key dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701 + // corresponding address 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + // to 0xef1c6e67703c7bd7107eed8303fbe6ec2554bf6b + // data: 0x3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000 + // tx: 0xf903732e8506c50218ba8304312294ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b880a8db2d41b89b009b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000025a0c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45aa05721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06 + + //setup + let nonce = 46; + let gas_price = U256::from(29075052730u64); + let gas_limit = 274722u64; + let to = address_from_str("ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b"); + let value = U256::from(760460536160301065u64); // /!\ > 2^53 -1 + let data: Vec = hex::decode("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let r = string_to_h256_unsafe( + "c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45a", + ); + let s = string_to_h256_unsafe( + "5721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06", + ); + let expected_transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit, + to, + value, + data, + access_list: vec![], + signature: Some(TxSignature::new_unsafe(37, r, s)), + }; + let signed_data = "f903732e8506c50218ba8304312294ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b880a8db2d41b89b009b903043593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c600000000000000000000000000000000000000000000000000000000000000400000000000000000000000000000000000000000000000000000000000000001000000000000000000000000000000000000000000000000000000000000000025a0c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45aa05721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06"; + + // act + let encoded = expected_transaction.to_bytes(); + + // assert + assert_eq!(signed_data, hex::encode(encoded)); + } + + #[test] + fn test_decoding_ethereum_js() { + // private key 0xcb9db6b5878db2fa20586e23b7f7b51c22a7c6ed0530daafc2615b116f170cd3 + // from 0xd9e5c94a12f78a96640757ac97ba0c257e8aa262 + // "nonce": 1, + // "gasPrice": 30000000000, + // "gasLimit": "0x100000", + // "to": "0x4e1b2c985d729ae6e05ef7974013eeb48f394449", + // "value": 1000000000, + // "data": "", + // "chainId": 1, + // v: 38 + // r: bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad + // s: 6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + // tx: 0xf869018506fc23ac0083100000944e1b2c985d729ae6e05ef7974013eeb48f394449843b9aca008026a0bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ada06053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + let gas_price = U256::from(30000000000u64); + let expected_transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 1, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 1048576u64, + to: address_from_str("4e1b2c985d729ae6e05ef7974013eeb48f394449"), + value: U256::from(1000000000u64), + data: vec![], + access_list: vec![], + signature: Some(TxSignature::new_unsafe( + 38, + string_to_h256_unsafe( + "bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad", + ), + string_to_h256_unsafe( + "6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f", + ), + )), + }; + let signed_data = "f869018506fc23ac0083100000944e1b2c985d729ae6e05ef7974013eeb48f394449843b9aca008026a0bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ada06053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f"; + + // act + let tx = hex::decode(signed_data).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx); + + // assert + assert_eq!(Ok(expected_transaction), decoded); + } + + #[test] + fn test_caller_ethereum_js() { + // private key 0xcb9db6b5878db2fa20586e23b7f7b51c22a7c6ed0530daafc2615b116f170cd3 + // from 0xd9e5c94a12f78a96640757ac97ba0c257e8aa262 + // "nonce": 1, + // "gasPrice": 30000000000, + // "gasLimit": "0x100000", + // "to": "0x4e1b2c985d729ae6e05ef7974013eeb48f394449", + // "value": 1000000000, + // "data": "", + // "chainId": 1, + // r: bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad + // s: 6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + // tx: 0xf869018506fc23ac0083100000944e1b2c985d729ae6e05ef7974013eeb48f394449843b9aca008026a0bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ada06053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + + let gas_price = U256::from(30000000000u64); + let transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 1, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 1048576u64, + to: address_from_str("4e1b2c985d729ae6e05ef7974013eeb48f394449"), + value: U256::from(1000000000u64), + data: vec![], + access_list: vec![], + signature: Some(TxSignature::new_unsafe( + 38, + string_to_h256_unsafe( + "bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad", + ), + string_to_h256_unsafe( + "6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f", + ), + )), + }; + + // assert + assert_eq!( + Ok(address_from_str("d9e5c94a12f78a96640757ac97ba0c257e8aa262").unwrap()), + transaction.caller(), + "test field from" + ) + } + + #[test] + fn test_signature_ethereum_js() { + // private key 0xcb9db6b5878db2fa20586e23b7f7b51c22a7c6ed0530daafc2615b116f170cd3 + // from 0xd9e5c94a12f78a96640757ac97ba0c257e8aa262 + // "nonce": 1, + // "gasPrice": 30000000000, + // "gasLimit": "0x100000", + // "to": "0x4e1b2c985d729ae6e05ef7974013eeb48f394449", + // "value": 1000000000, + // "data": "", + // "chainId": 1, + // r: bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad + // s: 6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + // tx: 0xf869018506fc23ac0083100000944e1b2c985d729ae6e05ef7974013eeb48f394449843b9aca008026a0bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ada06053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f + + let gas_price = U256::from(30000000000u64); + // setup + let transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 1, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 1048576u64, + to: address_from_str("4e1b2c985d729ae6e05ef7974013eeb48f394449"), + value: U256::from(1000000000u64), + data: vec![], + access_list: vec![], + signature: None, + }; + + // act + let signature = transaction + .sign_transaction( + "cb9db6b5878db2fa20586e23b7f7b51c22a7c6ed0530daafc2615b116f170cd3" + .to_string(), + ) + .unwrap() + .signature + .unwrap(); + + // assert + let r = string_to_h256_unsafe( + "bb03310570362eef497a09dd6e4ef42f56374965cfb09cc4e055a22a2eeac7ad", + ); + let s = string_to_h256_unsafe( + "6053c1bd83abb30c109801844709202208736d598649afe2a53f024b61b3383f", + ); + + assert_eq!(U256::from(38), signature.v(), "checking v"); + assert_eq!(&r, signature.r(), "checking r"); + assert_eq!(&s, signature.s(), "checking s"); + } + + #[test] + fn test_caller_classic_with_chain_id() { + let sk = "9bfc9fbe6296c8fef8eb8d6ce2ed5f772a011898c6cabe32d35e7c3e419efb1b" + .to_string(); + let (_sk, address) = string_to_sk_and_address_unsafe(sk.clone()); + // Check that the derived address is the expected one. + let expected_address = + address_from_str("6471A723296395CF1Dcc568941AFFd7A390f94CE").unwrap(); + assert_eq!(expected_address, address); + + // Check that the derived sender address is the expected one. + let encoded = "f86d80843b9aca00825208940b52d4d3be5d18a7ab5e4476a2f5382bbf2b38d888016345785d8a000080820a95a0d9ef1298c18c88604e3f08e14907a17dfa81b1dc6b37948abe189d8db5cb8a43a06fc7040a71d71d3cb74bd05ead7046b10668ad255da60391c017eea31555f156".to_string(); + let transaction = EthereumTransactionCommon::from_hex(encoded).unwrap(); + let address = transaction.caller().unwrap(); + assert_eq!(expected_address, address); + + // Check that signing the signed transaction returns the same transaction. + let signed_transaction = transaction.sign_transaction(sk); + assert_eq!(transaction, signed_transaction.unwrap()) + } + + #[test] + fn test_caller_eip155_example() { + let transaction = basic_eip155_transaction(); + assert_eq!( + address_from_str("9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f").unwrap(), + transaction.caller().unwrap() + ) + } + + #[test] + fn test_caller_eip155_example_fail_eip2() { + // this test checks that EIP2 part (2) is implemented + // https://eips.ethereum.org/EIPS/eip-2 + // ie, All transaction signatures whose s-value is greater + // than secp256k1n/2 are now considered invalid + + let transaction = basic_eip155_transaction(); + let signature = transaction.signature.unwrap(); + // flip s + let s: &H256 = signature.s(); + let s1: [u8; 32] = (*s).into(); + let mut scalar = Scalar([0; 8]); + let _ = scalar.set_b32(&s1); + let flipped_scalar = scalar.neg(); + let flipped_s = H256::from_slice(&flipped_scalar.b32()); + + // flip v + let flipped_v = if signature.v() == U256::from(37) { + 38 + } else { + 37 + }; + + let flipped_transaction = EthereumTransactionCommon { + signature: Some(TxSignature::new_unsafe( + flipped_v, + *signature.r(), + flipped_s, + )), + ..transaction + }; + + // as v and s are flipped, the signature is a correct ECDSA signature + // and the caller should be the same, if EIP2 is not implemented + // but with EIP2 s should be too big, and the transaction should be rejected + assert_eq!( + Err(SigError::TxSigError(TxSigError::ECDSAError( + libsecp256k1::Error::InvalidSignature + ))), + flipped_transaction.caller() + ) + } + + #[test] + fn test_caller_uniswap_inspired() { + // inspired by 0xf598016f51e0544187088ddd50fd37818fd268a0363a17281576425f3ee334cb + + // setup + let data: Vec = hex::decode("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + + let gas_price = U256::from(29075052730u64); + let transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 46, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 274722u64, + to: address_from_str("ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b"), + value: U256::from(760460536160301065u64), + data, + access_list: vec![], + signature: Some(TxSignature::new_unsafe( + 37, + string_to_h256_unsafe( + "c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45a", + ), + string_to_h256_unsafe( + "5721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06", + ), + )), + }; + + // check + assert_eq!( + Ok(address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap()), + transaction.caller(), + "checking caller" + ) + } + + #[test] + fn test_message_uniswap_inspired() { + // inspired by 0xf598016f51e0544187088ddd50fd37818fd268a0363a17281576425f3ee334cb + + // setup + let data: Vec = hex::decode("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let gas_price = U256::from(29075052730u64); + let transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 46, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 274722u64, + to: address_from_str("ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b"), + value: U256::from(760460536160301065u64), + data, + access_list: vec![], + signature: None, + }; + + // check + assert_eq!( + Message::parse_slice( + &hex::decode( + "f1099d98570e86be48efa3ba9d3df6531d0069b1f9d7590329ba3791d97a37f1" + ) + .unwrap() + ) + .unwrap(), + transaction.message(), + "checking message hash" + ); + } + + #[test] + fn test_signature_uniswap_inspired() { + // inspired by 0xf598016f51e0544187088ddd50fd37818fd268a0363a17281576425f3ee334cb + // private key dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701 + let data: Vec = hex::decode("3593564c000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000a00000000000000000000000000000000000000000000000000000000064023c1700000000000000000000000000000000000000000000000000000000000000030b090c00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000003000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000000c000000000000000000000000000000000000000000000000000000000000001e0000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000a8db2d41b89b009000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000002ab0c205a56c1e000000000000000000000000000000000000000000000000000000a8db2d41b89b00900000000000000000000000000000000000000000000000000000000000000a000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000002000000000000000000000000c02aaa39b223fe8d0a0e5c4f27ead9083c756cc20000000000000000000000009eb6299e4bb6669e42cb295a254c8492f67ae2c6000000000000000000000000000000000000000000000000000000000000004000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000").unwrap(); + let gas_price = U256::from(29075052730u64); + let transaction = EthereumTransactionCommon { + type_: TransactionType::Legacy, + chain_id: Some(U256::one()), + nonce: 46, + max_priority_fee_per_gas: gas_price, + max_fee_per_gas: gas_price, + gas_limit: 274722u64, + to: address_from_str("ef1c6e67703c7bd7107eed8303fbe6ec2554bf6b"), + value: U256::from(760460536160301065u64), + data, + access_list: vec![], + signature: None, + }; + + // act + let signature = transaction + .sign_transaction( + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701" + .to_string(), + ) + .unwrap() + .signature + .unwrap(); + + // assert + let v = U256::from(37); + let r = string_to_h256_unsafe( + "c78be9ab81c622c08f7098eefc250935365fb794dfd94aec0fea16c32adec45a", + ); + let s = string_to_h256_unsafe( + "5721614264d8490c6866f110c1594151bbcc4fac43758adae644db6bc3314d06", + ); + + assert_eq!(v, signature.v(), "checking v"); + assert_eq!(&r, signature.r(), "checking r"); + assert_eq!(&s, signature.s(), "checking s"); + } + + #[test] + fn test_signature_eip155_example() { + // example directly lifted from eip155 description + // signing data 0xec098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a764000080018080 + // private key : 0x4646464646464646464646464646464646464646464646464646464646464646 + // corresponding address 0x9d8a62f656a8d1615c1294fd71e9cfb3e4855a4f + // signed tx : 0xf86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83 let nonce = U256::from(9); + + // setup + let transaction = basic_eip155_transaction_unsigned(); + let expected_signed = basic_eip155_transaction(); + + // act + let signed = transaction.sign_transaction( + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string(), + ); + + // assert + assert_eq!(Ok(expected_signed), signed, "checking signed transaction") + } + + #[test] + fn test_rlp_decode_succeeds_without_chain_id() { + // This transaction is signed but its v doesn't equal to CHAIN_ID * 2 + 35 + {0, 1} + // but equal to 27/28 as in "old" (before https://eips.ethereum.org/EIPS/eip-155) + // six fields encoding + let malformed_tx = "f86c0a8502540be400825208944bbeeb066ed09b7aed07bf39eee0460dfa261520880de0b6b3a7640000801ca0f3ae52c1ef3300f44df0bcfd1341c232ed6134672b16e35699ae3f5fe2493379a023d23d2955a239dd6f61c4e8b2678d174356ff424eac53da53e17706c43ef871".to_string(); + let e = EthereumTransactionCommon::from_hex(malformed_tx); + assert!(e.is_ok()); + } + + #[test] + fn test_rlp_decode_encode_with_valid_chain_id() { + let wellformed_tx = + "f86a8302ae2a7b82f618948e998a00253cb1747679ac25e69a8d870b52d8898802c68af0bb140000802da0cd2d976eb691dc16a397462c828975f0b836e1b448ecb8f00d9765cf5032cecca066247d13fc2b65fd70a2931b5897fff4b3079e9587e69ac8a0036c99eb5ea927".to_string(); + let e = EthereumTransactionCommon::from_hex(wellformed_tx.clone()).unwrap(); + let encoded = e.to_bytes(); + assert_eq!(hex::encode(encoded), wellformed_tx); + } + + #[test] + fn test_roundtrip_pre_eip_155() { + // decoding of a transaction that is not eip 155, ie v = 28 / 27 + // initial transaction: + // { + // "nonce": "0x0", + // "gasPrice": "0x10000000000", + // "gasLimit": "0x25000", + // "value": "0x0", + // "data": "0x608060405234801561001057600080fd5b50602a600081905550610150806100286000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212204d6c1853cec27824f5dbf8bcd0994714258d22fc0e0dc8a2460d87c70e3e57a564736f6c63430008120033", + // "chainId": 0 + // } + // private key: 0xe75f4c63daecfbb5be03f65940257f5b15e440e6cf26faa126ce68741d5d0f78 + // caller address: 0x3dbeca6e9a6f0677e3c7b5946fc8adbb1b071e0a + + // setup + let signed_tx = "f901cc8086010000000000830250008080b90178608060405234801561001057600080fd5b50602a600081905550610150806100286000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212204d6c1853cec27824f5dbf8bcd0994714258d22fc0e0dc8a2460d87c70e3e57a564736f6c634300081200331ca06d851632958801b6919ba534b4b1feb1bdfaabd0d42890bce200a11ac735d58da0219b058d7169d7a4839c5cdd555b0820b545797365287a81ba409419912de7b1"; + // act + let tx = hex::decode(signed_tx).unwrap(); + let decoded = EthereumTransactionCommon::from_bytes(&tx).unwrap(); + let encoded = EthereumTransactionCommon::to_bytes(&decoded); + + // sanity check + assert_eq!(hex::encode(encoded), signed_tx); + } + + #[test] + fn test_signature_unsigned_fails_gracefully() { + let transaction = basic_eip155_transaction_unsigned(); + + // check signature fails gracefully + assert!( + transaction.signature().is_err(), + "testing signature for unsigned fails" + ); + } + + #[test] + fn test_impossible_create_invalid_sig() { + let basic = basic_eip155_transaction(); + let signature = basic.signature.unwrap(); + assert!(TxSignature::new(U256::from(38), H256::zero(), *signature.s()).is_err()); + assert!(TxSignature::new(U256::from(38), *signature.r(), H256::zero()).is_err()); + } + + #[test] + fn test_signature_invalid_parity_fails_gracefully() { + let basic = basic_eip155_transaction(); + let signature = basic.signature.unwrap(); + // most data is not relevant here, the point is to test failure mode of signature verification + let transaction = EthereumTransactionCommon { + signature: Some(TxSignature::new_unsafe( + 150, + signature.r().to_owned(), + signature.s().to_owned(), + )), + chain_id: Some(U256::one()), + ..basic + }; + + // check signature fails gracefully + assert!( + transaction.signature().is_err(), + "testing signature checking fails gracefully" + ); + } + + #[test] + fn test_signature_invalid_chain_id_fails_gracefully() { + // most data is not relevant here, the point is to test failure mode of signature verification + let transaction = EthereumTransactionCommon { + chain_id: Some(U256::max_value()), // chain_id will overflow parity computation + ..basic_eip155_transaction() + }; + + // check signature fails gracefully + assert!( + transaction.signature().is_err(), + "testing signature checking fails gracefully" + ); + } + + #[test] + fn test_sign_invalid_chain_id_fails_gracefully() { + // most data is not relevant here, the point is to test failure mode of signature verification + let transaction = EthereumTransactionCommon { + chain_id: Some(U256::max_value()), + ..basic_eip155_transaction_unsigned() + }; + + // check signature fails gracefully + assert!( + transaction + .sign_transaction( + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string() + ) + .is_err(), + "testing signature fails gracefully" + ); + } + + #[test] + fn test_eip2930_signed_enc_dec() { + let signed_tx = "01f8ad82076c22843b9aca00830186a09409616c3d61b3331fc4109a9e41a8bdb7d9776609865af3107a400086616263646566f838f7940000000000000000000000000000000000000001e1a0010000000000000000000000000000000000000000000000000000000000000080a0ea38506c4afe4bb402e030877fbe1011fa1da47aabcf215db8da8fee5d3af086a051e9af653b8eb98e74e894a766cf88904dbdb10b0bc1fbd12f18f661fa2797a4".to_string(); + let parsed = EthereumTransactionCommon::from_hex(signed_tx.clone()).unwrap(); + let expected = eip2930_tx(); + assert_eq!(expected, parsed); + + assert_eq!(signed_tx, hex::encode(parsed.to_bytes())); + } + + #[test] + fn test_eip2930_unsigned_enc_dec() { + let unsigned_tx = EthereumTransactionCommon { + signature: None, + ..eip2930_tx() + }; + let tx_encoding = "01f86a82076c22843b9aca00830186a09409616c3d61b3331fc4109a9e41a8bdb7d9776609865af3107a400086616263646566f838f7940000000000000000000000000000000000000001e1a00100000000000000000000000000000000000000000000000000000000000000"; + assert_eq!(tx_encoding, hex::encode(unsigned_tx.to_bytes())); + assert_eq!( + unsigned_tx, + EthereumTransactionCommon::from_bytes(&hex::decode(tx_encoding).unwrap()) + .unwrap() + ); + } + + #[test] + fn test_different_tx_signature_and_sign() { + // Test that signature() and sign_transaction() work as expected + // for both legacy and non-legacy txs. + + fn sign_signature_roundtrip( + tx: EthereumTransactionCommon, + string_sk: String, + expected_r: &str, + expected_s: &str, + parity: u8, + ) { + let expected_signature = ( + Signature { + r: h256_to_scalar(string_to_h256_unsafe(expected_r)), + s: h256_to_scalar(string_to_h256_unsafe(expected_s)), + }, + RecoveryId::parse(parity).unwrap(), + ); + + // Check that parity recovered correctly + assert_eq!(Ok(expected_signature), tx.signature()); + + // Check that signature computed correctly + assert_eq!( + Ok(expected_signature), + tx.sign_transaction(string_sk).unwrap().signature() + ); + } + + // EIP-155 tx + let legacy_tx = basic_eip155_transaction(); + sign_signature_roundtrip( + legacy_tx, + "4646464646464646464646464646464646464646464646464646464646464646".to_owned(), + "28EF61340BD939BC2195FE537567866003E1A15D3C71FF63E1590620AA636276", + "67CBE9D8997F761AECB703304B3800CCF555C9F3DC64214B297FB1966A3B6D83", + 0, + ); + + // EIP-2930 tx + let eip2930 = eip2930_tx(); + sign_signature_roundtrip( + eip2930, + "4c0883a69102937d6231471b5dbb6204fe5129617082792ae468d01a3f362318".to_owned(), + "ea38506c4afe4bb402e030877fbe1011fa1da47aabcf215db8da8fee5d3af086", + "51e9af653b8eb98e74e894a766cf88904dbdb10b0bc1fbd12f18f661fa2797a4", + 0, + ); + } + + #[test] + fn test_eip1559_enc_dec() { + let signed_tx = "02f8b20182102e843b9aca008506b1a22f8082918e949f8f72aa9304c8b593d555f12ef6589cc3a579a280b844a9059cbb000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43000000000000000000000000000000000000000000000000f020482e89b73c14c080a0f876cca0898a15f2eaf17ee8a0ac33ec7933b17db612bd5aaf5925774da84fada05c7310fa69b650d583fa5a66fbcfd720e96d9eacadc18632595cb6423e2f613f".to_string(); + let parsed = EthereumTransactionCommon::from_hex(signed_tx.clone()).unwrap(); + let expected = eip1559_tx(); + assert_eq!(expected, parsed); + + assert_eq!(signed_tx, hex::encode(parsed.to_bytes())) + } + + #[test] + fn test_eip1559_unsigned_enc_dec() { + let unsigned_tx = EthereumTransactionCommon { + signature: None, + ..eip1559_tx() + }; + let tx_encoding = "02f86f0182102e843b9aca008506b1a22f8082918e949f8f72aa9304c8b593d555f12ef6589cc3a579a280b844a9059cbb000000000000000000000000a9d1e08c7793af67e9d92fe308d5697fb81d3e43000000000000000000000000000000000000000000000000f020482e89b73c14c0"; + assert_eq!(tx_encoding, hex::encode(unsigned_tx.to_bytes())); + assert_eq!( + unsigned_tx, + EthereumTransactionCommon::from_bytes(&hex::decode(tx_encoding).unwrap()) + .unwrap() + ); + } + + #[test] + fn test_hash_exact() { + let tx_encoded = "f86480843b9aca00825cd6946ce4d79d4e77402e1ef3417fdda433aa744c6e1c0180820a959f3df55056959e51b66a515312fd0b851d629f562cb1e1b30d29fc909acb8450a02edac87401057bbe3a6255a253db01e5c5c6b9a597bfdab07d4ceefe08c03af9"; + let tx_decoded = + EthereumTransactionCommon::from_bytes(&hex::decode(tx_encoded).unwrap()) + .unwrap(); + let tx_reencoded = hex::encode(tx_decoded.to_bytes()); + assert_eq!(tx_encoded, tx_reencoded); + } +} diff --git a/etherlink/kernel_calypso2/ethereum/src/tx_signature.rs b/etherlink/kernel_calypso2/ethereum/src/tx_signature.rs new file mode 100644 index 000000000000..20f1608dac39 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/tx_signature.rs @@ -0,0 +1,250 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// +// SPDX-License-Identifier: MIT + +use hex::FromHexError; +use libsecp256k1::{ + curve::Scalar, sign, Error, Message, RecoveryId, SecretKey, Signature, +}; +use primitive_types::{H256, U256}; +use rlp::{DecoderError, Encodable, RlpIterator}; +use thiserror::Error; + +use crate::rlp_helpers::{ + append_compressed_h256, decode_compressed_h256, decode_field, next, +}; + +/// Represents a **valid** Ethereum signature +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct TxSignature { + v: U256, + r: H256, + s: H256, +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum ParityError { + #[error("Couldn't reconstruct V from chain_id: {0}")] + ChainId(U256), + + #[error("Couldn't reconstruct parity from V: {0}")] + V(U256), +} + +#[derive(Error, Debug, PartialEq, Clone)] +pub enum TxSigError { + #[error("Error reading a hex string: {0}")] + HexError(#[from] FromHexError), + + #[error("Error manipulating ECDSA key: {0}")] + ECDSAError(libsecp256k1::Error), + + #[error("Error recomputing parity of signature: {0}")] + Parity(ParityError), + + #[error("Invalid R and S: {0} {1}")] + InvalidRS(H256, H256), +} + +impl From for TxSigError { + fn from(e: libsecp256k1::Error) -> Self { + Self::ECDSAError(e) + } +} + +pub fn h256_to_scalar(h: H256) -> Scalar { + let h1: [u8; 32] = h.into(); + let mut h = Scalar([0; 8]); + let _ = h.set_b32(&h1); + h +} + +impl TxSignature { + pub fn v(&self) -> U256 { + self.v + } + + pub fn r(&self) -> &H256 { + &self.r + } + + pub fn s(&self) -> &H256 { + &self.s + } + + pub fn new(v: U256, r: H256, s: H256) -> Result { + if r != H256::zero() && s != H256::zero() { + Ok(TxSignature { v, r, s }) + } else { + Err(TxSigError::InvalidRS(r, s)) + } + } + + /// Computes the parity for a transaction in the Ethereum "legacy" format, + /// ie a transaction that encodes the `chain_id` in the value `v`, this + /// should not be used for EIP-1559 or EIP-2930 transaction for example. + /// The boolean correspond to parity `0` or `1`. + fn legacy_compute_parity(&self, chain_id: Option) -> Result { + let err = TxSigError::Parity(ParityError::V(self.v)); + let parity = match chain_id { + Some(chain_id) => { + let chain_id_encoding = chain_id + .checked_mul(U256::from(2)) + .ok_or_else(|| err.clone())? + .checked_add(U256::from(35)) + .ok_or_else(|| err.clone())?; + self.v.checked_sub(chain_id_encoding) + } + None => self.v.checked_sub(U256::from(27)), + }; + match parity { + Some(p) if p < U256::from(2) => Ok(p == U256::one()), + _ => Err(err), + } + } + + /// Validate that signatures conforms EIP-2 + /// and that is possible to restore parity from `chain_id` and `v` + pub fn signature_legacy( + &self, + chain_id: Option, + ) -> Result<(Signature, RecoveryId), TxSigError> { + let r = h256_to_scalar(self.r.to_owned()); + let s = h256_to_scalar(self.s.to_owned()); + if s.is_high() { + // if s > secp256k1n / 2 the signature is invalid + // cf EIP2 (part 2) https://eips.ethereum.org/EIPS/eip-2 + Err(TxSigError::ECDSAError( + libsecp256k1::Error::InvalidSignature, + )) + } else { + // recompute parity from v and chain_id + let parity = self.legacy_compute_parity(chain_id)?; + let ri = RecoveryId::parse(parity as u8)?; + Ok((Signature { r, s }, ri)) + } + } + + /// Validate that signatures conforms EIP-2 + pub fn signature(&self) -> Result<(Signature, RecoveryId), TxSigError> { + let r = h256_to_scalar(self.r.to_owned()); + let s = h256_to_scalar(self.s.to_owned()); + if s.is_high() { + // if s > secp256k1n / 2 the signature is invalid + // cf EIP2 (part 2) https://eips.ethereum.org/EIPS/eip-2 + Err(TxSigError::ECDSAError( + libsecp256k1::Error::InvalidSignature, + )) + } else if self.v < U256::from(4) { + let ri = RecoveryId::parse(self.v().as_u32() as u8)?; + Ok((Signature { r, s }, ri)) + } else { + Err(TxSigError::ECDSAError(Error::InvalidRecoveryId)) + } + } + + /// compute v from parity and chain_id + fn compute_v(chain_id: Option, parity: u8) -> Option { + match chain_id { + Some(chain_id) => { + if chain_id == U256::zero() { + // we don't support transactions with unpresented chain_id + None + } else { + let chain_id_encoding = chain_id + .checked_mul(U256::from(2))? + .checked_add(U256::from(35))?; + U256::from(parity).checked_add(chain_id_encoding) + } + } + None => U256::from(parity).checked_add(U256::from(27)), + } + } + + pub fn sign_secp256k1( + msg: &Message, + string_sk: String, + ) -> Result<(Signature, RecoveryId), TxSigError> { + let hex: &[u8] = &hex::decode(string_sk)?; + let sk = SecretKey::parse_slice(hex)?; + Ok(sign(msg, &sk)) + } + + /// This function creates a signature for a legacy tx. + /// Legacy tx has a tricky approach for signature creation, where + /// `v` field of a signature carry information about `chain_id`. + /// For more information see https://eips.ethereum.org/EIPS/eip-155 + pub fn sign_legacy( + msg: &Message, + string_sk: String, + chain_id: Option, + ) -> Result { + let (sig, recovery_id) = Self::sign_secp256k1(msg, string_sk)?; + let parity: u8 = recovery_id.into(); + let v = Self::compute_v(chain_id, parity).ok_or_else(|| { + TxSigError::Parity(ParityError::ChainId( + chain_id.unwrap_or_else(|| U256::from(0)), + )) + })?; + + let (r, s) = (H256::from(sig.r.b32()), H256::from(sig.s.b32())); + Ok(TxSignature { v, r, s }) + } + + pub fn sign(msg: &Message, string_sk: String) -> Result { + let (sig, recovery_id) = Self::sign_secp256k1(msg, string_sk)?; + let parity: u8 = recovery_id.into(); + + let (r, s) = (H256::from(sig.r.b32()), H256::from(sig.s.b32())); + Ok(TxSignature { + v: U256::from(parity), + r, + s, + }) + } + + #[cfg(test)] + pub fn new_unsafe(v: u64, r: H256, s: H256) -> TxSignature { + TxSignature::new(U256::from(v), r, s).expect("Signature data should be valid") + } +} + +impl Encodable for TxSignature { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.append(&self.v); + append_compressed_h256(stream, self.r); + append_compressed_h256(stream, self.s); + } +} + +// Encode None as (0, 0, 0) +// This encoding is used for transaction object, +// which expects to have a signature. +// However, for deposits there are no signature so we have to mock it. +pub fn rlp_append_opt(sig: &Option, stream: &mut rlp::RlpStream) { + match sig { + Some(sig) => Encodable::rlp_append(sig, stream), + None => { + stream.append(&U256::zero()); + stream.append(&H256::zero()); + stream.append(&H256::zero()); + } + } +} + +// Decode (0, 0, 0) as None +// See comment for rlp_append_opt above. +pub fn rlp_decode_opt( + it: &mut RlpIterator<'_, '_>, +) -> Result, DecoderError> { + let v: U256 = decode_field(&next(it)?, "v")?; + let r: H256 = decode_compressed_h256(&next(it)?)?; + let s: H256 = decode_compressed_h256(&next(it)?)?; + if r == H256::zero() && s == H256::zero() && v == U256::zero() { + Ok(None) + } else { + let sig = TxSignature::new(v, r, s) + .map_err(|_| DecoderError::Custom("Invalid signature"))?; + Ok(Some(sig)) + } +} diff --git a/etherlink/kernel_calypso2/ethereum/src/wei.rs b/etherlink/kernel_calypso2/ethereum/src/wei.rs new file mode 100644 index 000000000000..55ed9939f613 --- /dev/null +++ b/etherlink/kernel_calypso2/ethereum/src/wei.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 PK Lab +// SPDX-FileCopyrightText: 2025 Functori +// +// SPDX-License-Identifier: MIT + +use primitive_types::U256; + +pub type Wei = U256; + +pub const ETH_AS_WEI: u64 = 1_000_000_000_000_000_000; + +pub fn from_eth(eth: u64) -> Wei { + Wei::from(eth) * Wei::from(ETH_AS_WEI) +} + +pub fn eth_from_mutez(mutez: u64) -> Wei { + // Mutez is 10^6, Wei is 10^18 + U256::from(mutez) * U256::exp10(12) +} + +pub enum ErrorMutezFromWei { + AmountTooLarge, + NonNullRemainder, +} + +pub fn mutez_from_wei(wei: Wei) -> Result { + // Wei is 10^18, Mutez is 10^6 + let amount: U256 = wei / U256::exp10(12); + // Check that remainder is 0 to make sure we don't lose Wei when + // rounding to mutez. + let remainder: U256 = wei % U256::exp10(12); + + if !remainder.is_zero() { + Err(ErrorMutezFromWei::NonNullRemainder) + } else if amount >= U256::from(u64::MAX) { + Err(ErrorMutezFromWei::AmountTooLarge) + } else { + Ok(amount.as_u64()) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/Cargo.toml b/etherlink/kernel_calypso2/evm_execution/Cargo.toml new file mode 100644 index 000000000000..279beed68ef8 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/Cargo.toml @@ -0,0 +1,76 @@ +# SPDX-FileCopyrightText: 2023 TriliTech +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# SPDX-FileCopyrightText: 2023 Marigold +# SPDX-FileCopyrightText: 2023, 2025 Functori +# +# SPDX-License-Identifier: MIT + +[package] +name = "evm-execution-calypso2" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] + +thiserror.workspace = true + +num-bigint.workspace = true +num-traits.workspace = true +primitive-types.workspace = true +alloy-sol-types.workspace = true +alloy-primitives.workspace = true + +hex.workspace = true +rlp.workspace = true +const-decoder.workspace = true + +tezos_crypto_rs.workspace = true +sha2.workspace = true +sha3.workspace = true +ripemd.workspace = true +libsecp256k1.workspace = true + +evm.workspace = true +aurora-engine-modexp.workspace = true +bn.workspace = true + +tezos_ethereum.workspace = true +tezos-evm-logging.workspace = true +tezos-evm-runtime.workspace = true +tezos-indexable-storage.workspace = true +tezos-storage.workspace = true + +tezos-smart-rollup-core.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-debug.workspace = true +tezos-smart-rollup-encoding.workspace = true +tezos-smart-rollup-storage.workspace = true +tezos_data_encoding.workspace = true + +# Adding these to 'dev_dependencies' causes the rand feature in crypto to be enabled +# on wasm builds, when building the entire workspace. +rand = { workspace = true, optional = true } +proptest = { workspace = true, optional = true } + +# Enabled when testing feature is on +tezos-smart-rollup-mock = { workspace = true, optional = true } + +[dev-dependencies] +pretty_assertions.workspace = true + +tezos-smart-rollup-mock.workspace = true + +[features] +default = ["evm_execution"] +testing = ["rand", "proptest", "dep:tezos-smart-rollup-mock"] +fa_bridge_testing = ["dep:tezos-smart-rollup-mock"] +evm_execution = [] +debug = ["tezos-evm-logging/debug"] +# the `benchmark` and `benchmark-opcodes` feature flags instrument the kernel for profiling +benchmark = ["tezos-evm-logging/benchmark"] +benchmark-opcodes = ["benchmark"] +benchmark-full = ["debug", "benchmark", "benchmark-opcodes"] diff --git a/etherlink/kernel_calypso2/evm_execution/src/abi.rs b/etherlink/kernel_calypso2/evm_execution/src/abi.rs new file mode 100644 index 000000000000..bf07883367ba --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/abi.rs @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 TriliTech +// +// SPDX-License-Identifier: MIT + +//! ABI utility functions +//! +//! This implements the bare minimum for handling contract call +//! parameters encoded using Solidity ABI standard. See the documentation +//! at +//! [Contract ABI specification](https://docs.soliditylang.org/en/develop/abi-spec.html) + +// TODO: https://gitlab.com/tezos/tezos/-/issues/7722 +// This whole file is hack-ish. +// In the long term we should get rid of this file completely and rely on a proper +// implementation for abi parameters (for instance see cast's implementation to +// depend on the same crates). + +use primitive_types::{H160, U256}; + +/// All arguments in ABI encoding are padded to 32 bytes +/// https://docs.soliditylang.org/en/develop/abi-spec.html#formal-specification-of-the-encoding +pub const ABI_H160_LEFT_PADDING: [u8; 12] = [0u8; 12]; +pub const ABI_U32_LEFT_PADDING: [u8; 28] = [0u8; 28]; +pub const ABI_B22_RIGHT_PADDING: [u8; 10] = [0u8; 10]; + +/// Get a single 32 bytes/256 bit parameter from a contract call, input data buffer +pub fn u256_parameter(input_data: &[u8], parameter_number: usize) -> Option { + let location = parameter_number * 32; + input_data + .get(location..location + 32) + .map(|bytes| bytes.into()) +} + +/// Get the bytes of a dynamic parameter from a contract call, input data buffer +pub fn bytes_parameter(input_data: &[u8], parameter_number: usize) -> Option<&[u8]> { + let location: usize = u256_parameter(input_data, parameter_number)? + .try_into() + .ok()?; + let length: usize = U256::from(input_data.get(location..location + 32)?) + .try_into() + .ok()?; + input_data.get(location + 32..location + 32 + length) +} + +/// Get a string parameter from a contract call, input data buffer +pub fn string_parameter(input_data: &[u8], parameter_number: usize) -> Option<&str> { + core::str::from_utf8(bytes_parameter(input_data, parameter_number)?).ok() +} + +/// Get an address parameter from the input data buffer +pub fn h160_parameter(input_data: &[u8], parameter_number: usize) -> Option { + let location = parameter_number * 32; + if location + 32 <= input_data.len() { + // Check leading zeroes + if input_data[location..location + 12] + .iter() + .all(|x| *x == 0u8) + { + let mut parameter = [0u8; 20]; + parameter.copy_from_slice(&input_data[location + 12..location + 32]); + Some(H160(parameter)) + } else { + None + } + } else { + None + } +} + +/// Get a fixed N bytes parameter from the input data buffer, where 0 < N < 32 +pub fn fixed_bytes_parameter( + input_data: &[u8], + parameter_number: usize, +) -> Option<[u8; N]> { + let location = parameter_number * 32; + if location + 32 <= input_data.len() { + // Check trailing zeroes + if input_data[location + N..location + 32] + .iter() + .all(|x| *x == 0u8) + { + let mut parameter = [0u8; N]; + parameter.copy_from_slice(&input_data[location..location + N]); + Some(parameter) + } else { + None + } + } else { + None + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/access_record.rs b/etherlink/kernel_calypso2/evm_execution/src/access_record.rs new file mode 100644 index 000000000000..50a6942f9650 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/access_record.rs @@ -0,0 +1,38 @@ +use alloc::collections::btree_set::BTreeSet; +use primitive_types::{H160, H256}; + +#[derive(Eq, Clone, PartialOrd, PartialEq, Ord, Debug)] +struct AddressIndex(H160, H256); + +#[derive(Clone, Debug)] +pub struct AccessRecord { + accessed_storage_keys: BTreeSet, + accessed_addresses: BTreeSet, +} + +impl AccessRecord { + pub fn default() -> Self { + AccessRecord { + accessed_storage_keys: BTreeSet::new(), + accessed_addresses: BTreeSet::new(), + } + } + + pub fn insert_storage(&mut self, address: H160, index: H256) { + self.accessed_storage_keys + .insert(AddressIndex(address, index)); + } + + pub fn contains_storage(&self, address: H160, index: H256) -> bool { + self.accessed_storage_keys + .contains(&AddressIndex(address, index)) + } + + pub fn insert_address(&mut self, address: H160) { + self.accessed_addresses.insert(address); + } + + pub fn contains_address(&self, address: H160) -> bool { + self.accessed_addresses.contains(&address) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/account_storage.rs b/etherlink/kernel_calypso2/evm_execution/src/account_storage.rs new file mode 100644 index 000000000000..edd710855a0c --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/account_storage.rs @@ -0,0 +1,1365 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2023 Functori +// +// SPDX-License-Identifier: MIT + +//! Ethereum account state and storage + +use const_decoder::Decoder; +use host::path::{concat, OwnedPath, Path, RefPath}; +use host::runtime::{RuntimeError, ValueType}; +use primitive_types::{H160, H256, U256}; +use rlp::DecoderError; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_storage::storage::Storage; +use tezos_storage::{ + error::Error as GenStorageError, read_u256_le_default, read_u64_le_default, +}; +use tezos_storage::{path_from_h256, read_h256_be_default, read_h256_be_opt, WORD_SIZE}; +use thiserror::Error; + +use crate::{code_storage, DurableStorageError}; + +/// All errors that may happen as result of using the Ethereum account +/// interface. +#[derive(Error, Eq, PartialEq, Clone, Debug)] +pub enum AccountStorageError { + /// Some error happened while using durable storage, either from an invalid + /// path or a runtime error. + #[error("Durable storage error: {0:?}")] + DurableStorageError(#[from] DurableStorageError), + /// Some error occurred while using the transaction storage + /// API. + #[error("Transaction storage API error: {0:?}")] + StorageError(tezos_smart_rollup_storage::StorageError), + #[error("Failed to decode: {0}")] + RlpDecoderError(DecoderError), + #[error("Storage error: error while reading a value (incorrect size). Expected {expected} but got {actual}")] + InvalidLoadValue { expected: usize, actual: usize }, + /// Some account balance became greater than what can be + /// stored in an unsigned 256 bit integer. + #[error("Account balance overflow")] + BalanceOverflow, + /// Technically, the Ethereum account nonce can overflow if + /// an account does an incredible number of transactions. + #[error("Nonce overflow")] + NonceOverflow, + #[error("Account code already initialised")] + AccountCodeAlreadySet, +} + +impl From for AccountStorageError { + fn from(error: tezos_smart_rollup_storage::StorageError) -> Self { + AccountStorageError::StorageError(error) + } +} + +impl From for AccountStorageError { + fn from(error: host::path::PathError) -> Self { + AccountStorageError::DurableStorageError(DurableStorageError::from(error)) + } +} + +impl From for AccountStorageError { + fn from(error: host::runtime::RuntimeError) -> Self { + AccountStorageError::DurableStorageError(DurableStorageError::from(error)) + } +} + +impl From for AccountStorageError { + fn from(e: GenStorageError) -> Self { + match e { + GenStorageError::Path(e) => { + AccountStorageError::DurableStorageError(DurableStorageError::from(e)) + } + GenStorageError::Runtime(e) => { + AccountStorageError::DurableStorageError(DurableStorageError::from(e)) + } + GenStorageError::Storage(e) => AccountStorageError::StorageError(e), + GenStorageError::RlpDecoderError(e) => { + AccountStorageError::RlpDecoderError(e) + } + GenStorageError::InvalidLoadValue { expected, actual } => { + AccountStorageError::InvalidLoadValue { expected, actual } + } + } + } +} + +/// When an Ethereum contract acts on storage, it spends gas. The gas cost of +/// operations that affect storage depends both on what was in storage already, +/// and the new value written to storage. +#[derive(Eq, PartialEq, Debug)] +pub struct StorageEffect { + /// Indicates whether the original value before a storage update + /// was the default value. + pub from_default: bool, + /// Indicates whether the new value after storage update is the + /// default value. + pub to_default: bool, +} + +/// An Ethereum account +/// +/// This struct defines the storage interface for interacting with Ethereum accounts +/// in durable storage. The values kept in storage correspond to the values in section +/// 4.1 of the Ethereum Yellow Paper. Also, contract code and contract permanent data +/// storage are accessed through this API. The durable storage for an account includes: +/// - The **nonce** of the account. A scalar value equal to the number of transactions +/// send from this address or (in the case of contract accounts) the number of contract +/// creations done by this contract. +/// - The **balance** of the account. A scalar value equal to the number of Wei held by +/// the account. +/// - The **code hash** of any contract code associated with the account. +/// - The **code**, ie, the opcodes, of any contract associated with the account. +/// +/// An account is considered _empty_ (according to EIP-161) iff +/// `balance == nonce == code == 0x`. +/// +/// The Ethereum Yellow Paper also lists the **storageRoot** as a field associated with +/// an account. We don't currently require it, and so it's omitted. +#[derive(Debug, PartialEq)] +pub struct EthereumAccount { + path: OwnedPath, +} + +impl From for EthereumAccount { + fn from(path: OwnedPath) -> Self { + Self { path } + } +} + +/// Path where Ethereum accounts are stored +pub const EVM_ACCOUNTS_PATH: RefPath = + RefPath::assert_from(b"/evm/world_state/eth_accounts"); + +/// Path where an account nonce is stored. This should be prefixed with the path to +/// where the account is stored for the world state or for the current transaction. +const NONCE_PATH: RefPath = RefPath::assert_from(b"/nonce"); + +/// Path where an account balance, ether held, is stored. This should be prefixed with the path to +/// where the account is stored for the world state or for the current transaction. +const BALANCE_PATH: RefPath = RefPath::assert_from(b"/balance"); + +/// "Internal" accounts - accounts with contract code have a contract code hash. +/// This value is computed when the code is stored and kept for future queries. This +/// path should be prefixed with the path to +/// where the account is stored for the world state or for the current transaction. +const CODE_HASH_PATH: RefPath = RefPath::assert_from(b"/code.hash"); + +/// "Internal" accounts - accounts with contract code, have their code stored here. +/// This +/// path should be prefixed with the path to +/// where the account is stored for the world state or for the current transaction. +const CODE_PATH: RefPath = RefPath::assert_from(b"/code"); + +/// The contracts of "internal" accounts have their own storage area. The account +/// location prefixed to this path gives the root path (prefix) to where such storage +/// values are kept. Each index in durable storage gives one complete path to one +/// such 256 bit integer value in storage. +const STORAGE_ROOT_PATH: RefPath = RefPath::assert_from(b"/storage"); + +/// If a contract tries to read a value from storage and it has previously not written +/// anything to this location or if it wrote the default value, then it gets this +/// value back. +const STORAGE_DEFAULT_VALUE: H256 = H256::zero(); + +/// Default balance value for an account. +const BALANCE_DEFAULT_VALUE: U256 = U256::zero(); + +/// Default nonce value for an account. +const NONCE_DEFAULT_VALUE: u64 = 0; + +/// Nonce is a u64 so its size is 8 bytes +const NONCE_ENCODING_SIZE: usize = 8_usize; + +/// An account with no code - an "external" account, or an unused account has the zero +/// hash as code hash. +const CODE_HASH_BYTES: [u8; WORD_SIZE] = Decoder::Hex + .decode(b"c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470"); + +/// The default hash for when there is no code - the hash of the empty string. +pub const CODE_HASH_DEFAULT: H256 = H256(CODE_HASH_BYTES); + +/// Turn an Ethereum address - a H160 - into a valid path +pub fn account_path(address: &H160) -> Result { + let path_string = alloc::format!("/{}", hex::encode(address.to_fixed_bytes())); + OwnedPath::try_from(path_string).map_err(DurableStorageError::from) +} + +#[derive(Clone, Copy)] +pub enum StorageValue { + Hit(H256), + Default, +} + +impl StorageValue { + pub fn h256(self) -> H256 { + match self { + StorageValue::Hit(v) => v, + StorageValue::Default => STORAGE_DEFAULT_VALUE, + } + } +} + +impl EthereumAccount { + pub fn from_address(address: &H160) -> Result { + let path = concat(&EVM_ACCOUNTS_PATH, &account_path(address)?)?; + Ok(path.into()) + } + + /// Get the **nonce** for the Ethereum account. Default value is zero, so an account will + /// _always_ have this **nonce**. + pub fn nonce(&self, host: &impl Runtime) -> Result { + let path = concat(&self.path, &NONCE_PATH)?; + read_u64_le_default(host, &path, NONCE_DEFAULT_VALUE) + .map_err(AccountStorageError::from) + } + + /// Increment the **nonce** by one. It is technically possible for this operation to overflow, + /// but in practice this will not happen for a very long time. The nonce is a 256 bit unsigned + /// integer. + pub fn increment_nonce( + &mut self, + host: &mut impl Runtime, + ) -> Result<(), AccountStorageError> { + let path = concat(&self.path, &NONCE_PATH)?; + + let old_value = self.nonce(host)?; + + let new_value = old_value + .checked_add(1) + .ok_or(AccountStorageError::NonceOverflow)?; + + let new_value_bytes: [u8; NONCE_ENCODING_SIZE] = new_value.to_le_bytes(); + + host.store_write_all(&path, &new_value_bytes) + .map_err(AccountStorageError::from) + } + + pub fn decrement_nonce( + &mut self, + host: &mut impl Runtime, + ) -> Result<(), AccountStorageError> { + let path = concat(&self.path, &NONCE_PATH)?; + + let old_value = self.nonce(host)?; + + let new_value = old_value.checked_sub(1).unwrap_or_default(); + + let new_value_bytes: [u8; NONCE_ENCODING_SIZE] = new_value.to_le_bytes(); + + host.store_write(&path, &new_value_bytes, 0) + .map_err(AccountStorageError::from) + } + + pub fn set_nonce( + &mut self, + host: &mut impl Runtime, + nonce: u64, + ) -> Result<(), AccountStorageError> { + let path = concat(&self.path, &NONCE_PATH)?; + + let value_bytes: [u8; NONCE_ENCODING_SIZE] = nonce.to_le_bytes(); + + host.store_write_all(&path, &value_bytes) + .map_err(AccountStorageError::from) + } + + /// Get the **balance** of an account in Wei held by the account. + pub fn balance(&self, host: &impl Runtime) -> Result { + let path = concat(&self.path, &BALANCE_PATH)?; + read_u256_le_default(host, &path, BALANCE_DEFAULT_VALUE) + .map_err(AccountStorageError::from) + } + + /// Add an amount in Wei to the balance of an account. In theory, this can overflow if the + /// final amount exceeds the range of a a 256 bit unsigned integer. + pub fn balance_add( + &mut self, + host: &mut impl Runtime, + amount: U256, + ) -> Result<(), AccountStorageError> { + let path = concat(&self.path, &BALANCE_PATH)?; + + let value = self.balance(host)?; + + if let Some(new_value) = value.checked_add(amount) { + let mut new_value_bytes: [u8; WORD_SIZE] = [0; WORD_SIZE]; + new_value.to_little_endian(&mut new_value_bytes); + + host.store_write_all(&path, &new_value_bytes) + .map_err(AccountStorageError::from) + } else { + Err(AccountStorageError::BalanceOverflow) + } + } + + /// Remove an amount in Wei from the balance of an account. If the account doesn't hold + /// enough funds, this will underflow, in which case the account is unaffected, but the + /// function call will return `Ok(false)`. In case the removal went without underflow, + /// ie the account held enough funds, the function returns `Ok(true)`. + pub fn balance_remove( + &mut self, + host: &mut impl Runtime, + amount: U256, + ) -> Result { + let path = concat(&self.path, &BALANCE_PATH)?; + + let value = self.balance(host)?; + + if let Some(new_value) = value.checked_sub(amount) { + let mut new_value_bytes: [u8; WORD_SIZE] = [0; WORD_SIZE]; + new_value.to_little_endian(&mut new_value_bytes); + + host.store_write_all(&path, &new_value_bytes) + .map_err(AccountStorageError::from) + .map(|_| true) + } else { + Ok(false) + } + } + + /// Set the balance of an account to an amount in Wei + pub fn set_balance( + &mut self, + host: &mut impl Runtime, + new_balance: U256, + ) -> Result<(), AccountStorageError> { + let path = concat(&self.path, &BALANCE_PATH)?; + + let mut new_balance_bytes: [u8; WORD_SIZE] = [0; WORD_SIZE]; + new_balance.to_little_endian(&mut new_balance_bytes); + + host.store_write_all(&path, &new_balance_bytes) + .map_err(AccountStorageError::from) + } + + /// Get the path to a custom account state section, can be used by precompiles + pub fn custom_path( + &self, + suffix: &impl Path, + ) -> Result { + concat(&self.path, suffix).map_err(AccountStorageError::from) + } + + /// Get the path to an index in durable storage for an account. + fn storage_path(&self, index: &H256) -> Result { + let storage_path = concat(&self.path, &STORAGE_ROOT_PATH)?; + let index_path = path_from_h256(index)?; + concat(&storage_path, &index_path).map_err(AccountStorageError::from) + } + + /// Clear the entire storage in durable storage for an account. + pub fn clear_storage( + &self, + host: &mut impl Runtime, + ) -> Result<(), AccountStorageError> { + let storage_path = concat(&self.path, &STORAGE_ROOT_PATH)?; + if host.store_has(&storage_path)?.is_some() { + host.store_delete(&storage_path) + .map_err(AccountStorageError::from)? + }; + Ok(()) + } + + /// Get the value stored in contract permanent storage at a given index for an account. + pub fn get_storage( + &self, + host: &impl Runtime, + index: &H256, + ) -> Result { + let path = self.storage_path(index)?; + read_h256_be_default(host, &path, STORAGE_DEFAULT_VALUE) + .map_err(AccountStorageError::from) + } + + pub fn read_storage( + &self, + host: &impl Runtime, + index: &H256, + ) -> Result { + let path = self.storage_path(index)?; + let res = read_h256_be_opt(host, &path).map_err(AccountStorageError::from)?; + + match res { + Some(v) => Ok(StorageValue::Hit(v)), + None => Ok(StorageValue::Default), + } + } + + /// Set the value associated with an index in durable storage. The result depends on the + /// values being stored. It tracks whether the update went from default to non-default, + /// non-default to default, et.c. This is for the purpose of calculating gas cost. + pub fn set_storage_checked( + &mut self, + host: &mut impl Runtime, + index: &H256, + value: &H256, + ) -> Result { + let path = self.storage_path(index)?; + + let old_value = self.get_storage(host, index)?; + + let from_default = old_value == STORAGE_DEFAULT_VALUE; + let to_default = *value == STORAGE_DEFAULT_VALUE; + + if !from_default && to_default { + host.store_delete(&path)?; + } + + if !to_default { + let value_bytes = value.to_fixed_bytes(); + + host.store_write_all(&path, &value_bytes)?; + } + + Ok(StorageEffect { + from_default, + to_default, + }) + } + + /// Set the value associated with an index in durable storage. The result depends on the + /// values being stored. This function does no tracking for the purpose of gas cost. + pub fn set_storage( + &mut self, + host: &mut impl Runtime, + index: &H256, + value: &H256, + ) -> Result<(), AccountStorageError> { + let path = self.storage_path(index)?; + + let value_bytes = value.to_fixed_bytes(); + + host.store_write_all(&path, &value_bytes) + .map_err(AccountStorageError::from) + } + + /// Find whether the account has any code associated with it. + pub fn code_exists(&self, host: &impl Runtime) -> Result { + let path = concat(&self.path, &CODE_HASH_PATH)?; + + match host.store_has(&path) { + Ok(Some(ValueType::Value | ValueType::ValueWithSubtree)) => Ok(true), + Ok(Some(ValueType::Subtree) | None) => Ok(false), + Err(err) => Err(err.into()), + } + } + + /// Get the contract code associated with a contract. A contract can have zero length + /// contract code associated with it - this is the same for "external" and un-used + /// accounts. First check if code is located in this storage, if that fails try to + /// retrieve it within the code_storage module. + // There is a possibility here to migrate lazily all code in order + // to have all legacy contract uses the new code storage. + pub fn code(&self, host: &impl Runtime) -> Result, AccountStorageError> { + let path = concat(&self.path, &CODE_PATH)?; + + // If we ever do the lazy migration of code for account + // (i.e. moving code from account to code_storage) then this + // would be dead code. + match host.store_read_all(&path) { + Ok(bytes) => Ok(bytes), + Err(RuntimeError::PathNotFound) => { + let code_hash = self.code_hash(host)?; + code_storage::CodeStorage::get_code(host, &code_hash).map_err(Into::into) + } + Err(err) => Err(AccountStorageError::from(err)), + } + } + + /// Get the hash of the code associated with an account. This value is computed and + /// stored when the code of a contract is set. + pub fn code_hash(&self, host: &impl Runtime) -> Result { + let path = concat(&self.path, &CODE_HASH_PATH)?; + read_h256_be_default(host, &path, CODE_HASH_DEFAULT) + .map_err(AccountStorageError::from) + } + + /// Get the size of a contract in number of bytes used for opcodes. This value is + /// computed and stored when the code of a contract is set. + pub fn code_size(&self, host: &impl Runtime) -> Result { + let path = concat(&self.path, &CODE_PATH)?; + // If we ever do the lazy migration of code for account + // (i.e. moving code from account to code_storage) then this + // would be dead code. + if host.store_has(&path)? == Some(ValueType::Value) { + let size = host.store_value_size(&path)?; + Ok(size.into()) + } else { + let code_hash = self.code_hash(host)?; + code_storage::CodeStorage::code_size(host, &code_hash).map_err(Into::into) + } + } + + /// Set the code associated with an account. This stores the code and also computes + /// hash and size and stores those values as well. No check for validity of contract + /// code is done. Contract code is validated through execution (contract calls), and + /// not before. + pub fn set_code( + &mut self, + host: &mut impl Runtime, + code: &[u8], + ) -> Result<(), AccountStorageError> { + if self.code_exists(host)? { + Err(AccountStorageError::AccountCodeAlreadySet) + } else { + let code_hash = code_storage::CodeStorage::add(host, code)?; + let code_hash_bytes: [u8; WORD_SIZE] = code_hash.into(); + let code_hash_path = concat(&self.path, &CODE_HASH_PATH)?; + host.store_write_all(&code_hash_path, &code_hash_bytes) + .map_err(AccountStorageError::from) + } + } + + /// Delete all code associated with a contract. Also sets code length and size accordingly + pub fn delete_code( + &mut self, + host: &mut impl Runtime, + ) -> Result<(), AccountStorageError> { + let code_hash_path = concat(&self.path, &CODE_HASH_PATH)?; + + if let Some(ValueType::Value | ValueType::ValueWithSubtree) = + host.store_has(&code_hash_path)? + { + host.store_delete(&code_hash_path)? + } + + let code_path = concat(&self.path, &CODE_PATH)?; + + if let Some(ValueType::Value | ValueType::ValueWithSubtree) = + host.store_has(&code_path)? + { + host.store_delete(&code_path)? + } else { + let code_hash = self.code_hash(host)?; + code_storage::CodeStorage::delete(host, &code_hash)? + } + + Ok(()) + } +} + +/// The type of the storage API for accessing the Ethereum World State. +pub type EthereumAccountStorage = Storage; + +/// Get the storage API for accessing the Ethereum World State and do transactions +/// on it. +pub fn init_account_storage() -> Result { + Storage::::init(&EVM_ACCOUNTS_PATH) + .map_err(AccountStorageError::from) +} + +#[cfg(test)] +mod test { + use super::*; + use host::path::RefPath; + use primitive_types::U256; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; // Used for + use tezos_storage::helpers::bytes_hash; + use tezos_storage::write_u256_le; + + #[test] + fn test_account_nonce_update() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + assert_eq!(a1.nonce(&host).expect("Could not get nonce for account"), 0); + + a1.increment_nonce(&mut host) + .expect("Could not increment nonce"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!(a1.nonce(&host).expect("Could nnt get nonce for account"), 1); + } + + #[test] + fn test_zero_account_balance_for_new_accounts() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + a1.increment_nonce(&mut host) + .expect("Could not increment nonce"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.balance(&host) + .expect("Could not get balance for account"), + U256::zero() + ); + } + + #[test] + fn test_account_balance_add() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let v1: U256 = 17_u32.into(); + let v2: U256 = 119_u32.into(); + let v3: U256 = v1 + v2; + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + a1.balance_add(&mut host, v1) + .expect("Could not add first value to balance"); + a1.balance_add(&mut host, v2) + .expect("Could not add second value to balance"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.balance(&host) + .expect("Could not get balance for account"), + v3 + ); + } + + #[test] + fn test_account_balance_sub() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let v1: U256 = 170_u32.into(); + let v2: U256 = 19_u32.into(); + let v3: U256 = v1 - v2; + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + a1.balance_add(&mut host, v1) + .expect("Could not add first value to balance"); + a1.balance_remove(&mut host, v2) + .expect("Could not add second value to balance"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.balance(&host) + .expect("Could not get balance for account"), + v3 + ); + } + + #[test] + fn test_account_balance_underflow() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let v1: U256 = 17_u32.into(); + let v2: U256 = 190_u32.into(); + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + a1.balance_add(&mut host, v1) + .expect("Could not add first value to balance"); + assert_eq!(a1.balance_remove(&mut host, v2), Ok(false),); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.balance(&host) + .expect("Could not get balance for account"), + v1 + ); + } + + #[test] + fn test_account_storage_zero_default() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let addr: H256 = H256::from_low_u64_be(17_u64); + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + assert_eq!( + a1.get_storage(&host, &addr) + .expect("Could not read storage for account"), + H256::zero() + ); + } + + #[test] + fn test_account_storage_update() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let addr: H256 = H256::from_low_u64_be(17_u64); + let v: H256 = H256::from_low_u64_be(190_u64); + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + a1.set_storage(&mut host, &addr, &v) + .expect("Could not update account storage"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.get_storage(&host, &addr) + .expect("Could not read storage for account"), + v + ); + } + + #[test] + fn test_account_storage_update_checked() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/dfkjd"); + + let addr: H256 = H256::from_low_u64_be(17_u64); + let v1: H256 = H256::from_low_u64_be(191_u64); + let v2: H256 = H256::from_low_u64_be(192_u64); + let v3: H256 = H256::zero(); + + // Act - create an account with no funds + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists"); + + assert_eq!( + a1.set_storage_checked(&mut host, &addr, &v1) + .expect("Could not update account storage"), + StorageEffect { + from_default: true, + to_default: false + } + ); + assert_eq!( + a1.set_storage_checked(&mut host, &addr, &v2) + .expect("Could not update account storage"), + StorageEffect { + from_default: false, + to_default: false + } + ); + assert_eq!( + a1.set_storage_checked(&mut host, &addr, &v3) + .expect("Could not update account storage"), + StorageEffect { + from_default: false, + to_default: true + } + ); + assert_eq!( + a1.set_storage_checked(&mut host, &addr, &v3) + .expect("Could not update account storage"), + StorageEffect { + from_default: true, + to_default: true + } + ); + assert_eq!( + a1.set_storage_checked(&mut host, &addr, &v1) + .expect("Could not update account storage"), + StorageEffect { + from_default: true, + to_default: false + } + ); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account from storage") + .expect("Account does not exist"); + + assert_eq!( + a1.get_storage(&host, &addr) + .expect("Could not read storage for account"), + v1 + ); + } + + #[test] + fn test_account_code_storage_initial_code_is_zero() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + + // Act - make sure there is an account + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + assert_eq!(a1.nonce(&host).expect("Could not get nonce for account"), 0); + + a1.increment_nonce(&mut host) + .expect("Could not increment nonce"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + Vec::::new() + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + U256::zero() + ); + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + CODE_HASH_DEFAULT + ); + } + + fn test_account_code_storage_write_code_aux(sample_code: Vec) { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + let sample_code_hash: H256 = bytes_hash(&sample_code); + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a1.set_code(&mut host, &sample_code) + .expect("Could not write code to account"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + sample_code + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + sample_code.len().into() + ); + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + sample_code_hash + ); + } + + #[test] + fn test_account_code_storage_write_code() { + let sample_code: Vec = (0..100).collect(); + test_account_code_storage_write_code_aux(sample_code) + } + + #[test] + fn test_account_code_storage_write_big_code() { + let sample_code: Vec = vec![1; 10000]; + test_account_code_storage_write_code_aux(sample_code) + } + + #[test] + fn test_account_code_storage_cant_be_overwritten() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + let sample_code1: Vec = (0..100).collect(); + let sample_code1_hash: H256 = bytes_hash(&sample_code1); + let sample_code2: Vec = (0..50).map(|x| 50 - x).collect(); + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a1.set_code(&mut host, &sample_code1) + .expect("Could not write code to account"); + a1.set_code(&mut host, &sample_code2) + .expect_err("Account storage code can't be overwritten"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + sample_code1 + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + sample_code1.len().into() + ); + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + sample_code1_hash + ); + } + + #[test] + fn test_account_code_storage_delete_code() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + let sample_code: Vec = (0..100).collect(); + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a1.increment_nonce(&mut host) + .expect("Could not increment nonce"); + + a1.set_code(&mut host, &sample_code) + .expect("Could not write code to account"); + + a1.delete_code(&mut host) + .expect("Could not delete code for contract"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + CODE_HASH_DEFAULT + ); + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + Vec::::new() + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + U256::zero() + ); + } + + #[test] + fn test_empty_contract_hash_matches_default() { + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + let sample_code: Vec = vec![]; + let sample_code_hash: H256 = CODE_HASH_DEFAULT; + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a1.set_code(&mut host, &sample_code) + .expect("Could not write code to account"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + sample_code + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + sample_code.len().into() + ); + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + sample_code_hash + ); + } + + #[test] + fn test_read_u256_le_default_le() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/value"); + assert_eq!( + read_u256_le_default(&host, &path, U256::from(128)).unwrap(), + U256::from(128) + ); + + host.store_write_all(&path, &[1u8; 20]).unwrap(); + assert_eq!( + read_u256_le_default(&host, &path, U256::zero()).unwrap(), + U256::zero() + ); + + host.store_write_all( + &path, + &hex::decode( + "ff00000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + ) + .unwrap(); + assert_eq!( + read_u256_le_default(&host, &path, U256::zero()).unwrap(), + U256::from(255) + ); + } + + #[test] + fn test_write_u256_le_le() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/value"); + + write_u256_le(&mut host, &path, U256::from(255)).unwrap(); + assert_eq!( + hex::encode(host.store_read_all(&path).unwrap()), + "ff00000000000000000000000000000000000000000000000000000000000000" + ); + } + + #[test] + fn test_account_code_storage_can_still_be_addressed_if_exists() { + let sample_code: Vec = (0..100).collect(); + let sample_code_hash: H256 = bytes_hash(&sample_code); + + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/asdf"); + + // Act + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + let code_hash_bytes: [u8; WORD_SIZE] = sample_code_hash.into(); + let code_hash_path = + concat(&a1.path, &CODE_HASH_PATH).expect("Could not get code hash path"); + + host.store_write_all(&code_hash_path, &code_hash_bytes) + .expect("Could not write code hash into code hash path"); + + let code_path = concat(&a1.path, &CODE_PATH).expect("Could not get code path"); + + host.store_write_all(&code_path, &sample_code) + .expect("Could not write code into code path"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + // Assert + let a1 = storage + .get(&host, &a1_path) + .expect("Could not get account") + .expect("Account does not exist"); + + let code_path = concat(&a1.path, &CODE_PATH).expect("Could not get code path"); + + let code = host + .store_read_all(&code_path) + .expect("Could not read code from code path"); + + assert_eq!(code, sample_code); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + sample_code + ); + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + sample_code.len().into() + ); + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + sample_code_hash + ); + } + + #[test] + fn test_code_is_deleted() { + let sample_code: Vec = (0..100).collect(); + let sample_code_hash: H256 = bytes_hash(&sample_code); + + let mut host = MockKernelHost::default(); + let mut storage = + init_account_storage().expect("Could not create EVM accounts storage API"); + + let a1_path = RefPath::assert_from(b"/account1"); + + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a1 = storage + .create_new(&mut host, &a1_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a1.set_code(&mut host, &sample_code) + .expect("Could not write code to account"); + + storage + .commit_transaction(&mut host) + .expect("Could not commit transaction"); + + let a2_path = RefPath::assert_from(b"/account2"); + + storage + .begin_transaction(&mut host) + .expect("Could not begin transaction"); + + let mut a2 = storage + .create_new(&mut host, &a2_path) + .expect("Could not create new account") + .expect("Account already exists in storage"); + + a2.set_code(&mut host, &sample_code) + .expect("Could not write code to account"); + + a2.delete_code(&mut host) + .expect("Could not delete code for contract"); + + assert_eq!( + a2.code_hash(&host) + .expect("Could not get code hash for account"), + CODE_HASH_DEFAULT + ); + + assert_eq!( + a2.code(&host).expect("Could not get code for account"), + Vec::::new() + ); + assert_eq!( + a2.code_size(&host) + .expect("Could not get code size for account"), + U256::zero() + ); + + assert_eq!( + a1.code(&host).expect("Could not get code for account"), + sample_code + ); + + assert_eq!( + a1.code_size(&host) + .expect("Could not get code size for account"), + sample_code.len().into() + ); + + assert_eq!( + a1.code_hash(&host) + .expect("Could not get code hash for account"), + sample_code_hash + ); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/code_storage.rs b/etherlink/kernel_calypso2/evm_execution/src/code_storage.rs new file mode 100644 index 000000000000..6474eb67edd4 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/code_storage.rs @@ -0,0 +1,248 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2023 Functori +// +// SPDX-License-Identifier: MIT + +//! Ethereum code storage + +use host::path::{concat, OwnedPath, RefPath}; +use primitive_types::{H256, U256}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_storage::helpers::bytes_hash; +use tezos_storage::{error::Error as GenStorageError, read_u64_le, write_u64_le}; + +/// Path where Ethereum account's code are stored +const EVM_CODES_PATH: RefPath = RefPath::assert_from(b"/evm/world_state/eth_codes"); + +/// Path to the number of accounts to use a particular code +const REFERENCE_PATH: RefPath = RefPath::assert_from(b"/ref_count"); + +/// Path to the code +const CODE_PATH: RefPath = RefPath::assert_from(b"/code"); + +fn code_hash_path(code_hash: &H256) -> Result { + let code_hash_hex = hex::encode(code_hash); + let code_hash_path_string = format!("/{}", code_hash_hex); + OwnedPath::try_from(code_hash_path_string).map_err(Into::into) +} + +#[derive(Debug, PartialEq)] +pub struct CodeStorage { + path: OwnedPath, +} + +impl From for CodeStorage { + fn from(path: OwnedPath) -> Self { + Self { path } + } +} + +impl CodeStorage { + fn new(code_hash: &H256) -> Result { + let code_hash_path = code_hash_path(code_hash)?; + let path = concat(&EVM_CODES_PATH, &code_hash_path)?; + Ok(Self { path }) + } + + fn exists(&self, host: &impl Runtime) -> Result { + let store_has_code = host.store_has(&self.path)?; + Ok(store_has_code.is_some()) + } + + fn get_ref_count(&self, host: &mut impl Runtime) -> Result { + let reference_path = concat(&self.path, &REFERENCE_PATH)?; + read_u64_le(host, &reference_path).or(Ok(0u64)) + } + + fn set_ref_count( + &self, + host: &mut impl Runtime, + number_ref: u64, + ) -> Result<(), GenStorageError> { + let reference_path = concat(&self.path, &REFERENCE_PATH)?; + write_u64_le(host, &reference_path, number_ref)?; + Ok(()) + } + + fn increment_code_usage( + &self, + host: &mut impl Runtime, + ) -> Result<(), GenStorageError> { + let number_reference = self.get_ref_count(host)?; + let number_reference = number_reference.saturating_add(1u64); + self.set_ref_count(host, number_reference) + } + + fn decrement_code_usage( + &self, + host: &mut impl Runtime, + ) -> Result { + let number_reference = self.get_ref_count(host)?; + let number_reference = number_reference.saturating_sub(1u64); + self.set_ref_count(host, number_reference)?; + Ok(number_reference) + } + + pub fn add( + host: &mut impl Runtime, + bytecode: &[u8], + ) -> Result { + let code_hash: H256 = bytes_hash(bytecode); + let code = Self::new(&code_hash)?; + if !code.exists(host)? { + let code_path = concat(&code.path, &CODE_PATH)?; + host.store_write_all(&code_path, bytecode)?; + }; + code.increment_code_usage(host)?; + Ok(code_hash) + } + + pub fn delete( + host: &mut impl Runtime, + code_hash: &H256, + ) -> Result<(), GenStorageError> { + let code = Self::new(code_hash)?; + if code.exists(host)? { + let number_reference = code.decrement_code_usage(host)?; + // This was the last smart contract using this code + if number_reference == 0 { + host.store_delete(&code.path)?; + }; + }; + Ok(()) + } + + pub fn get_code( + host: &impl Runtime, + code_hash: &H256, + ) -> Result, GenStorageError> { + let code = Self::new(code_hash)?; + if code.exists(host)? { + let code1_path = concat(&code.path, &CODE_PATH)?; + host.store_read_all(&code1_path).map_err(Into::into) + } else { + Ok(vec![]) + } + } + + pub fn code_size( + host: &impl Runtime, + code_hash: &H256, + ) -> Result { + let code = Self::new(code_hash)?; + if code.exists(host)? { + let code_path = concat(&code.path, &CODE_PATH)?; + host.store_value_size(&code_path) + .map(U256::from) + .map_err(Into::into) + } else { + Ok(U256::zero()) + } + } +} + +#[cfg(test)] +mod test { + use crate::account_storage; + use tezos_evm_runtime::runtime::MockKernelHost; + + use super::*; + + #[test] + fn test_empty_contract_hash_matches_default() { + let mut host = MockKernelHost::default(); + let empty_code: Vec = vec![]; + let empty_code_hash: H256 = account_storage::CODE_HASH_DEFAULT; + + let found_code_hash = CodeStorage::add(&mut host, &empty_code) + .expect("Could not create code storage"); + + assert_eq!(found_code_hash, empty_code_hash); + } + + #[test] + fn test_get_code_matches_given() { + let mut host = MockKernelHost::default(); + let code: Vec = (0..100).collect(); + let code_hash = + CodeStorage::add(&mut host, &code).expect("Could not create code storage"); + let found_code = CodeStorage::get_code(&host, &code_hash).expect(""); + assert_eq!(found_code, code); + } + + #[test] + fn test_code_ref_is_incremented() { + let mut host = MockKernelHost::default(); + let code: Vec = (0..100).collect(); + let code_hash = + CodeStorage::add(&mut host, &code).expect("Could not create code storage"); + let code_storage = + CodeStorage::new(&code_hash).expect("Could not find code storage"); + let ref_path = concat(&code_storage.path, &REFERENCE_PATH).unwrap(); + + let ref_count = read_u64_le(&host, &ref_path).expect("reference count not found"); + assert_eq!(ref_count, 1u64); + + let second_code_hash = + CodeStorage::add(&mut host, &code).expect("Could not create code storage"); + + assert_eq!(second_code_hash, code_hash); + + let ref_count = read_u64_le(&host, &ref_path).expect("reference count not found"); + assert_eq!(ref_count, 2u64); + + let () = CodeStorage::delete(&mut host, &code_hash) + .expect("Could not delete code storage"); + + let ref_count = read_u64_le(&host, &ref_path).expect("reference count not found"); + assert_eq!(ref_count, 1u64); + } + + #[test] + fn test_code_is_deleted() { + let mut host = MockKernelHost::default(); + let code_hash: H256 = account_storage::CODE_HASH_DEFAULT; + + let code_storage = + CodeStorage::new(&code_hash).expect("Could not find code storage"); + + let exists = code_storage + .exists(&host) + .expect("Could not check contract exists"); + + assert!(!exists, "code storage should not exists"); + + let code: Vec = vec![]; + let _code_hash = + CodeStorage::add(&mut host, &code).expect("Could not create code storage"); + + let exists = code_storage + .exists(&host) + .expect("Could not check contract exists"); + assert!(exists, "code storage should exists"); + + let _code_hash = + CodeStorage::add(&mut host, &code).expect("Could not create code storage"); + + let exists = code_storage + .exists(&host) + .expect("Could not check contract exists"); + assert!(exists, "code storage should exists"); + + let () = CodeStorage::delete(&mut host, &code_hash) + .expect("Could not delete code storage"); + + let exists = code_storage + .exists(&host) + .expect("Could not check contract exists"); + assert!(exists, "code storage should exists"); + + let () = CodeStorage::delete(&mut host, &code_hash) + .expect("Could not delete code storage"); + + let exists = code_storage + .exists(&host) + .expect("Could not check contract exists"); + assert!(!exists, "code storage should not exists"); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/deposit.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/deposit.rs new file mode 100644 index 000000000000..548d133a5056 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/deposit.rs @@ -0,0 +1,411 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA token deposit. +//! +//! Represents a ticket transfer from L1 to L2 that has: +//! * Arbitrary ticketer (excluding whitelisted native token) +//! * Standard ticket content (FA2.1 compatible) +//! * Additional routing info parameter (raw bytes) +//! +//! It has several implicit constraints: +//! * Total token supply must fit into U256 +//! (losses are possible otherwise) +//! * Routing info must contain valid receiver address, user has access to +//! (otherwise funds are forever lost) +//! +//! User can optionally specify an address of a valid existing proxy contract, +//! which is typically an ERC wrapper contract. Any runtime errors related +//! to that contract will be handled and user will still be able to withdraw +//! funds. +//! +//! Given the permissionless nature of the bridge (anyone can bridge any token), +//! these constraints can only be checked on the client side. +//! +//! A special deposit event is emitted upon the successful transfer +//! (regardless of the inner proxy contract call result) and can be used for +//! indexing. + +use primitive_types::{H160, H256, U256}; +use rlp::{Encodable, RlpDecodable, RlpEncodable}; +use sha3::{Digest, Keccak256}; +use tezos_data_encoding::enc::BinWriter; +use tezos_ethereum::Log; +use tezos_smart_rollup_encoding::michelson::{ticket::FA2_1Ticket, MichelsonBytes}; + +use crate::{ + abi::{ABI_H160_LEFT_PADDING, ABI_U32_LEFT_PADDING}, + utilities::{bigint_to_u256, keccak256_hash}, +}; + +use super::error::FaBridgeError; + +/// Keccak256 of deposit(address,uint256,uint256), first 4 bytes +/// This is function selector: https://docs.soliditylang.org/en/latest/abi-spec.html#function-selector +pub const FA_PROXY_DEPOSIT_METHOD_ID: &[u8; 4] = b"\x0e\xfe\x6a\x8b"; + +/// Keccak256 of Deposit(uint256,address,address,uint256,uint256,uint256) +/// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events +pub const FA_DEPOSIT_EVENT_TOPIC: &[u8; 32] = b"\ + \x7e\xe7\xa1\xde\x9c\x18\xce\x69\x5c\x95\xb8\xb1\x9f\xbd\xf2\x6c\ + \xce\x35\x44\xe3\xca\x9e\x08\xc9\xf4\x87\x77\x67\x83\xd7\x59\x9f"; + +/// Overapproximation for the typical FA ticket payload (ticketer address and content) +const TICKET_PAYLOAD_SIZE_HINT: usize = 200; + +/// Deposit structure parsed from the inbox message +#[derive(Debug, PartialEq, Clone, RlpEncodable, RlpDecodable)] +pub struct FaDeposit { + /// Original ticket transfer amount + pub amount: U256, + /// Final deposit receiver address on L2 + pub receiver: H160, + /// Optional proxy contract address on L2 (ERC wrapper) + pub proxy: Option, + /// Digest of the pair (ticketer address + ticket content) + pub ticket_hash: H256, + /// Inbox level containing the original deposit message + pub inbox_level: u32, + /// Inbox message id (can be used for tracking and as nonce) + pub inbox_msg_id: u32, +} + +impl FaDeposit { + /// Tries to parse FA deposit given encoded parameters + pub fn try_parse( + ticket: FA2_1Ticket, + routing_info: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + ) -> Result<(Self, Option), FaBridgeError> { + let amount = bigint_to_u256(ticket.amount())?; + let (receiver, proxy, chain_id) = parse_l2_routing_info(routing_info)?; + let ticket_hash = ticket_hash(&ticket)?; + + Ok(( + FaDeposit { + amount, + receiver, + proxy, + ticket_hash, + inbox_level, + inbox_msg_id, + }, + chain_id, + )) + } + + /// Returns calldata for the proxy (ERC wrapper) contract. + /// + /// Signature: deposit(address,uint256,uint256) + pub fn calldata(&self) -> Vec { + let mut call_data = Vec::with_capacity(100); + call_data.extend_from_slice(FA_PROXY_DEPOSIT_METHOD_ID); + + call_data.extend_from_slice(&ABI_H160_LEFT_PADDING); + call_data.extend_from_slice(&self.receiver.0); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data.extend_from_slice(self.ticket_hash.as_bytes()); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data + } + + /// Returns log structure for an implicit deposit event. + /// + /// This event is added to the outer transaction receipt, + /// so that we can index successful deposits and update status. + /// Ticket owner can be either proxy contract or receiver + /// (if proxy is not specified or proxy call failed). + /// + /// Signature: Deposit(uint256,address,address,uint256,uint256,uint256) + pub fn event_log(&self, ticket_owner: &H160) -> Log { + let mut data = Vec::with_capacity(5 * 32); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(&ticket_owner.0); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(&self.receiver.0); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_level.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_msg_id.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + Log { + // Emitted by the "system" contract + address: H160::zero(), + // Event ID (non-anonymous) and indexed fields + topics: vec![H256(*FA_DEPOSIT_EVENT_TOPIC), self.ticket_hash], + // Non-indexed fields + data, + } + } + + /// Returns unique deposit digest that can be used as hash for the + /// pseudo transaction. + pub fn hash(&self, seed: &[u8]) -> H256 { + let mut hasher = Keccak256::new(); + hasher.update(&self.rlp_bytes()); + hasher.update(seed); + H256(hasher.finalize().into()) + } + + /// Formats FA deposit structure for logging purposes. + pub fn display(&self) -> String { + format!( + "FA deposit {} of {} for {} via {:?}", + self.amount, self.ticket_hash, self.receiver, self.proxy + ) + } +} + +const RECEIVER_LENGTH: usize = std::mem::size_of::(); + +const RECEIVER_AND_PROXY_LENGTH: usize = RECEIVER_LENGTH + std::mem::size_of::(); + +const RECEIVER_PROXY_AND_CHAIN_ID_LENGTH: usize = + RECEIVER_AND_PROXY_LENGTH + std::mem::size_of::(); + +/// Split routing info (raw bytes passed along with the ticket) into receiver and optional proxy address and chain id. +fn parse_l2_routing_info( + routing_info: MichelsonBytes, +) -> Result<(H160, Option, Option), FaBridgeError> { + let routing_info_len = routing_info.0.len(); + if routing_info_len == RECEIVER_LENGTH { + Ok((H160::from_slice(&routing_info.0), None, None)) + } else if routing_info_len == RECEIVER_AND_PROXY_LENGTH { + Ok(( + H160::from_slice(&routing_info.0[..RECEIVER_LENGTH]), + Some(H160::from_slice(&routing_info.0[RECEIVER_LENGTH..])), + None, + )) + } else if routing_info_len == RECEIVER_PROXY_AND_CHAIN_ID_LENGTH { + Ok(( + H160::from_slice(&routing_info.0[..RECEIVER_LENGTH]), + Some(H160::from_slice( + &routing_info.0[RECEIVER_LENGTH..RECEIVER_AND_PROXY_LENGTH], + )), + Some(U256::from_little_endian( + &routing_info.0[RECEIVER_AND_PROXY_LENGTH..], + )), + )) + } else { + Err(FaBridgeError::InvalidRoutingInfo("invalid length")) + } +} + +/// Calculate unique ticket hash out of the ticket identifier (ticketer address and content). +/// +/// Computed as Keccak256(ticketer || content) where +/// * ticketer: contract is in its forged form [ 0x01 | 20 bytes | 0x00 ] +/// * content: Micheline expression is in its forged form, legacy optimized mode +/// +/// Solidity equivalent: uint256(keccak256(abi.encodePacked(ticketer, content))); +pub fn ticket_hash(ticket: &FA2_1Ticket) -> Result { + let mut payload = Vec::with_capacity(TICKET_PAYLOAD_SIZE_HINT); + ticket.creator().0.bin_write(&mut payload)?; + ticket.contents().bin_write(&mut payload)?; + Ok(keccak256_hash(&payload)) +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::fa_bridge::test_utils::create_fa_ticket; + use num_bigint::BigInt; + use sha3::{Digest, Keccak256}; + + use super::*; + + #[test] + fn fa_deposit_parsing_success_no_chain_id() { + let ticket = + create_fa_ticket("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT", 1, &[0u8], 2.into()); + let routing_info = MichelsonBytes([[1u8; 20], [0u8; 20]].concat().to_vec()); + let (deposit, chain_id) = + FaDeposit::try_parse(ticket, routing_info, 1, 0).expect("Failed to parse"); + + pretty_assertions::assert_eq!( + deposit, + FaDeposit { + amount: 2.into(), + proxy: Some(H160([0u8; 20])), + receiver: H160([1u8; 20]), + inbox_level: 1, + inbox_msg_id: 0, + ticket_hash: H256::from_str( + "e0027297584c9e4162c872e072f1cc75b527023f9c0eda44ad4c732762b0b897" + ) + .unwrap(), + } + ); + pretty_assertions::assert_eq!(chain_id, None) + } + + #[test] + fn fa_deposit_parsing_success() { + let ticket = + create_fa_ticket("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT", 1, &[0u8], 2.into()); + let mut routing_info = vec![]; + routing_info.extend([1u8; 20]); + routing_info.extend([0u8; 20]); + routing_info.extend([1u8]); + routing_info.extend([0u8; 31]); + let routing_info = MichelsonBytes(routing_info); + let (deposit, chain_id) = + FaDeposit::try_parse(ticket, routing_info, 1, 0).expect("Failed to parse"); + + pretty_assertions::assert_eq!( + deposit, + FaDeposit { + amount: 2.into(), + proxy: Some(H160([0u8; 20])), + receiver: H160([1u8; 20]), + inbox_level: 1, + inbox_msg_id: 0, + ticket_hash: H256::from_str( + "e0027297584c9e4162c872e072f1cc75b527023f9c0eda44ad4c732762b0b897", + ) + .unwrap(), + } + ); + pretty_assertions::assert_eq!(chain_id, Some(U256::one())) + } + + #[test] + fn fa_deposit_parsing_error_amount_overflow() { + let ticket = create_fa_ticket( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + 1, + &[0u8], + BigInt::from_bytes_be(num_bigint::Sign::Plus, &[1u8; 64]), + ); + let routing_info = MichelsonBytes([0u8; 40].to_vec()); + + let res = FaDeposit::try_parse(ticket, routing_info, 1, 0); + + match res { + Err(FaBridgeError::PrimitiveType(_)) => (), + _ => panic!("Expected overflow error"), + } + } + + #[test] + fn fa_deposit_parsing_error_invalid_routing() { + let ticket = + create_fa_ticket("KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", 1, &[0u8], 1.into()); + let routing_info = MichelsonBytes([0u8; 10].to_vec()); + + let res = FaDeposit::try_parse(ticket, routing_info, 1, 0); + + match res { + Err(FaBridgeError::InvalidRoutingInfo(_)) => (), + _ => panic!("Expected routing error"), + } + } + + #[test] + fn fa_deposit_routing_does_not_contain_proxy() { + let ticket = + create_fa_ticket("KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", 1, &[0u8], 1.into()); + let routing_info = MichelsonBytes([0u8; 20].to_vec()); + + let (res, _chain_id) = FaDeposit::try_parse(ticket, routing_info, 1, 0).unwrap(); + assert_eq!(res.receiver, H160::zero()); + assert!(res.proxy.is_none()); + } + + #[test] + fn fa_deposit_erc_calldata_consistent() { + // Use this data to ensure consistency with the ERC wrapper contract + let deposit = FaDeposit { + amount: 1.into(), + proxy: Some(H160([2u8; 20])), + inbox_level: 3, + inbox_msg_id: 0, + receiver: H160([4u8; 20]), + ticket_hash: H256::from_str( + "12fb6647075cb9289e40af5560ce27a462ec2e49046b98298cdb41c9f128fb89", + ) + .unwrap(), + }; + + assert_eq!( + FA_PROXY_DEPOSIT_METHOD_ID.to_vec(), + Keccak256::digest(b"deposit(address,uint256,uint256)").to_vec()[..4] + ); + + pretty_assertions::assert_eq!( + hex::encode(deposit.calldata()), + "0efe6a8b\ + 0000000000000000000000000404040404040404040404040404040404040404\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 12fb6647075cb9289e40af5560ce27a462ec2e49046b98298cdb41c9f128fb89" + ); + } + + #[test] + fn fa_deposit_event_log_consistent() { + // Use this data to ensure consistency with the ERC wrapper contract + let deposit = FaDeposit { + amount: 1.into(), + proxy: Some(H160([2u8; 20])), + inbox_level: 3, + inbox_msg_id: 43775, + receiver: H160([4u8; 20]), + ticket_hash: H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(), + }; + let event_log = deposit.event_log(&deposit.proxy.unwrap()); + + pretty_assertions::assert_eq!( + hex::encode(&event_log.data), + "0000000000000000000000000202020202020202020202020202020202020202\ + 0000000000000000000000000404040404040404040404040404040404040404\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000003\ + 000000000000000000000000000000000000000000000000000000000000aaff" + ); + + assert_eq!( + FA_DEPOSIT_EVENT_TOPIC.to_vec(), + Keccak256::digest( + b"Deposit(uint256,address,address,uint256,uint256,uint256)" + ) + .to_vec() + ); + assert!(event_log.address.is_zero()); + } + + #[test] + fn ticket_payload_size_overapproximation() { + let ticket = create_fa_ticket( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + 1000000, + b"{\"contract_address\":\"KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5\",\"token_type\":\"FA2.0\",\"token_id\":\"1000000\",\"decimals\":\"12\",\"symbol\":\"USDQ\"}", + BigInt::from_bytes_be(num_bigint::Sign::Plus, &[1u8; 64]), + ); + let mut payload = Vec::new(); + ticket.creator().0.bin_write(&mut payload).unwrap(); + ticket.contents().bin_write(&mut payload).unwrap(); + assert!(payload.len() < TICKET_PAYLOAD_SIZE_HINT); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/error.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/error.rs new file mode 100644 index 000000000000..2c5a11c10538 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/error.rs @@ -0,0 +1,34 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA bridge specific errors. + +use tezos_data_encoding::enc::BinError; + +#[derive(Debug, thiserror::Error)] +pub enum FaBridgeError { + #[error("Binary codec error: {0}")] + BinaryCodec(#[from] BinError), + + #[error("Primitive type error: {0:?}")] + PrimitiveType(primitive_types::Error), + + #[error("Invalid routing info")] + InvalidRoutingInfo(&'static str), + + #[error("Ticket parsing error: {0}")] + TicketConstructError(&'static str), + + #[error("Entrypoint pasing error")] + EntrypointParseError, + + #[error("Abi decode error: {0}")] + AbiDecodeError(&'static str), +} + +impl From for FaBridgeError { + fn from(value: primitive_types::Error) -> Self { + Self::PrimitiveType(value) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs new file mode 100644 index 000000000000..2c31dc896318 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs @@ -0,0 +1,414 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA token bridge. +//! +//! A permissionless transport protocol, that enables ticket transfers +//! from L1 to L2 and back, supporting two destination types: +//! 1. Simple address, which can be both externally owner account, +//! or a smart contract wallet (that supports tickets) +//! 2. Proxy contract, exposing standard methods for deposits (on L2) +//! and withdrawals (on L1); must handle both ticket and +//! routing info that carries the final receiver address. +//! +//! FA bridge maintains the global ticket table, which is a ledger +//! tracking internal ticket ownerships on Etherlink side. +//! +//! FA bridge consists of two main parts: +//! * The one responsible for deposit handling: integrates with the +//! inbox handling flow, results in a pseudo transaction from +//! Zero account. +//! * The one responsible for withdrawal handling: implemented as +//! as precompiled contract, which can be invoked both by EOA +//! or another smart contract. +//! +//! It should be noted that FA withdrawal precompile DOES NOT post any +//! messages to the outbox since it cannot know if the outer transaction +//! fails or succeeds. +//! +//! All the state updates (ticket table, outbox message counter) are done +//! using the transactional Eth account storage, so that they are discarded +//! in case of a revert/failure. + +use std::borrow::Cow; + +use deposit::FaDeposit; +use evm::{Config, ExitReason}; +use primitive_types::{H160, U256}; +use tezos_ethereum::block::BlockConstants; +use tezos_evm_logging::{ + log, + Level::{Debug, Info}, +}; +use tezos_evm_runtime::runtime::Runtime; +use ticket_table::TicketTable; +use withdrawal::FaWithdrawal; + +use crate::{ + account_storage::EthereumAccountStorage, + handler::{CreateOutcome, EvmHandler, ExecutionOutcome, Withdrawal}, + precompiles::{PrecompileBTreeMap, PrecompileOutcome, SYSTEM_ACCOUNT_ADDRESS}, + trace::TracerInput, + transaction::TransactionContext, + withdrawal_counter::WithdrawalCounter, + EthereumError, +}; + +pub mod deposit; +pub mod error; +pub mod ticket_table; +pub mod withdrawal; + +#[cfg(test)] +mod tests; + +#[cfg(any(test, feature = "fa_bridge_testing"))] +pub mod test_utils; + +/// Gas limit for calling "deposit" method of the proxy contract call. +/// Since we cannot control a particular destination, +/// we need to make sure there's no DoS attack vector. +/// +/// Current value reflects roughly the fees paid on L1 for initiating +/// the deposit. Since only smart contracts can mint tickets and send +/// internal inbox messages, the lower bound for a single deposit is +/// approximately 0.0005ęś©; assuming current price per L2 gas of 1 Gwei +/// the equivalent amount of gas is 0.0005 * 10^18 / 10^9 = 500_000 +/// +/// Multiplying by two to enable more involved proxy contract implementations. +/// +/// /!\ Note that if the EVM gas changes over future upgrades, we might break +/// compatibility with contract relying on this gas limit. If the EVM consumes +/// more gas in the future, we need to increase this gas limit as well. /!\ +pub const FA_DEPOSIT_PROXY_GAS_LIMIT: u64 = 1_000_000; + +/// Overapproximation of the amount of ticks for updating +/// the global ticket table and emitting deposit event. +/// +/// It does not include the ticks consumed by the ERC proxy execution +/// as it is accounted independently by the EVM hander. +/// +/// Obtained by running the `bench_fa_deposit` script and examining the +/// `run_transaction_ticks` for the maximum value. +/// The final ticks amount has +50% safe reserve. +pub const FA_DEPOSIT_EXECUTE_TICKS: u64 = 2_250_000; + +/// Overapproximation of the amount of ticks for parsing FA deposit. +/// Also includes hashing costs. +/// +/// Obtained by running the `bench_fa_deposit` and examining both +/// `hashing_ticks` and `signature_verification_ticks` (parsing). +/// The final value is maximum total plus +50% reserve. +/// +/// NOTE that we have a hard cap because of the maximum inbox message size limitation. +/// If it is lifted at some point in the future, we need to reflect that. +pub const TICKS_PER_FA_DEPOSIT_PARSING: u64 = 3_500_000; + +macro_rules! create_outcome_error { + ($($arg:tt)*) => { + (evm::ExitReason::Error(evm::ExitError::Other( + std::borrow::Cow::from(format!($($arg)*)) + )), None, vec![]) + }; +} + +/// Executes FA deposit. +/// +/// From the EVM perspective this is a "system contract" call, +/// that tries to perform an internal invocation of the proxy +/// contract, and emits an additional deposit event. +/// +/// This method can only be called by the kernel, not by any +/// other contract. Therefore we assume there is no open +/// account storage transaction, and we can open one. +#[allow(clippy::too_many_arguments)] +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn execute_fa_deposit<'a, Host: Runtime>( + host: &'a mut Host, + block: &'a BlockConstants, + evm_account_storage: &'a mut EthereumAccountStorage, + precompiles: &'a PrecompileBTreeMap, + config: Config, + caller: H160, + deposit: &FaDeposit, + allocated_ticks: u64, + tracer_input: Option, + gas_limit: u64, +) -> Result { + log!(host, Info, "Going to execute a {}", deposit.display()); + + let mut handler = EvmHandler::<'_, Host>::new( + host, + evm_account_storage, + caller, + block, + &config, + precompiles, + allocated_ticks, + block.base_fee_per_gas(), + // Warm-cold access only used for evaluation (for checking EVM compatibility), but not in production + false, + tracer_input, + ); + + handler.begin_initial_transaction(false, Some(gas_limit))?; + + // It's ok if internal proxy call fails, we will update the ticket table anyways. + let ticket_owner = if let Some(proxy) = deposit.proxy { + let (exit_reason, _, _) = + inner_execute_proxy(&mut handler, caller, proxy, deposit.calldata())?; + // If proxy contract call succeeded, proxy becomes the owner, + // otherwise we fall back and set the receiver as the owner instead. + if exit_reason.is_succeed() { + proxy + } else { + log!( + handler.borrow_host(), + Info, + "FA deposit: proxy call failed w/ {:?}", + exit_reason + ); + deposit.receiver + } + } else { + // Proxy contract is not specified + deposit.receiver + }; + + // Deposit execution might fail because of the balance overflow + // so we need to rollback the entire transaction in that case. + let deposit_res = inner_execute_deposit(&mut handler, ticket_owner, deposit); + + let mut outcome = handler.end_initial_transaction(deposit_res)?; + + // Adjust resource consumption to account for the outer transaction + outcome.gas_used += config.gas_transaction_call; + outcome.estimated_ticks_used += FA_DEPOSIT_EXECUTE_TICKS; + + Ok(outcome) +} + +/// Execute the FA withdrawal within an execution layer. It aborts as soon as possible +/// on errors and the caller is responsible of cleaning up intermediate layer in +/// case of errors (i.e. using `end_inter_transaction`). It also can return the +/// withdrawal if it was succesful. +fn execute_layered_fa_withdrawal( + handler: &mut EvmHandler, + caller: H160, + withdrawal: FaWithdrawal, +) -> Result<(ExitReason, Option), EthereumError> { + let (mut exit_status, _, _) = inner_execute_withdrawal(handler, &withdrawal)?; + + // Withdrawal execution might fail because of non sufficient balance + // so we need to rollback the entire transaction in that case. + if exit_status.is_succeed() { + // In most cases sender is user's EOA and ticket owner is ERC wrapper contract + if withdrawal.ticket_owner != withdrawal.sender { + // If the proxy call fails we need to rollback the entire transaction + (exit_status, _, _) = inner_execute_proxy( + handler, + caller, + withdrawal.ticket_owner, + withdrawal.calldata(), + )?; + } + Ok((exit_status, Some(withdrawal.into_outbox_message()))) + } else { + Ok((exit_status, None)) + } +} + +/// Executes FA withdrawal. +/// +/// From the EVM perspective this is a precompile contract +/// call, that can be potentially an internal invocation from +/// another smart contract. +/// +/// We assume there is an open account storage transaction. +pub fn execute_fa_withdrawal( + handler: &mut EvmHandler, + caller: H160, + withdrawal: FaWithdrawal, +) -> Result { + log!( + handler.borrow_host(), + Info, + "Going to execute a {}", + withdrawal.display() + ); + + if handler.can_begin_inter_transaction_call_stack() { + // Create a new transaction layer with 63/64 of the remaining gas. + let gas_limit = handler.nested_call_gas_limit(None); + + if let Err(err) = handler.record_cost(gas_limit.unwrap_or_default()) { + log!( + handler.borrow_host(), + Debug, + "Not enough gas for create. Required at least: {:?} ({:?})", + gas_limit, + err + ); + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(evm::ExitError::OutOfGas), + withdrawals: vec![], + output: vec![], + // Precompile and inner proxy calls have already registered their costs + estimated_ticks: 0, + }); + } + + handler.begin_inter_transaction(false, gas_limit)?; + + // Execute the withdrawal in the transaction layer and clean it based + // on the result. + let (end_inter_transaction_result, withdrawals) = + match execute_layered_fa_withdrawal(handler, caller, withdrawal) { + Ok((exit_status, withdrawal_opt)) => { + let withdrawals = match withdrawal_opt { + Some(withdrawal) => vec![withdrawal], + None => vec![], + }; + ( + handler.end_inter_transaction::(Ok(( + exit_status, + None, + vec![], + ))), + withdrawals, + ) + } + Err(err) => ( + handler.end_inter_transaction::(Err(err)), + vec![], + ), + }; + // Transforms the transaction clean up result into a Sputnik call exit + // type. + match end_inter_transaction_result { + evm::Capture::Exit((exit_status, _, _)) => { + Ok(PrecompileOutcome { + exit_status, + withdrawals, + output: vec![], + // Precompile and inner proxy calls have already registered their costs + estimated_ticks: 0, + }) + } + evm::Capture::Trap(err) => Err(err), + } + } else { + Ok(PrecompileOutcome { + exit_status: ExitReason::Error(evm::ExitError::CallTooDeep), + withdrawals: vec![], + output: vec![], + // Precompile and inner proxy calls have already registered their costs + estimated_ticks: 0, + }) + } +} + +/// Updates ticket table according to the deposit and actual ticket owner. +/// Assuming there is an open account storage transaction. +fn inner_execute_deposit( + handler: &mut EvmHandler, + ticket_owner: H160, + deposit: &FaDeposit, +) -> Result { + // Updating the ticket table in accordance with the ownership. + let mut system = handler.get_or_create_account(SYSTEM_ACCOUNT_ADDRESS)?; + + if system.ticket_balance_add( + handler.borrow_host(), + &deposit.ticket_hash, + &ticket_owner, + deposit.amount, + )? { + handler + .add_log(deposit.event_log(&ticket_owner)) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + Ok(( + ExitReason::Succeed(evm::ExitSucceed::Returned), + None, + vec![], + )) + } else { + Ok(create_outcome_error!( + "Ticket table balance overflow: {} at {}", + deposit.ticket_hash, + ticket_owner + )) + } +} + +/// Updates ticket ledger and outbox counter according to the withdrawal. +/// Assuming there is an open account storage transaction. +fn inner_execute_withdrawal( + handler: &mut EvmHandler, + withdrawal: &FaWithdrawal, +) -> Result { + // Updating the ticket table in accordance with the ownership. + let mut system = handler.get_or_create_account(SYSTEM_ACCOUNT_ADDRESS)?; + + if system.ticket_balance_remove( + handler.borrow_host(), + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + withdrawal.amount, + )? { + let withdrawal_id = + system.withdrawal_counter_get_and_increment(handler.borrow_host())?; + + handler + .add_log(withdrawal.event_log(withdrawal_id)) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + + Ok((ExitReason::Succeed(evm::ExitSucceed::Stopped), None, vec![])) + } else { + Ok(create_outcome_error!( + "Insufficient ticket balance: {} of {} at {}", + withdrawal.amount, + withdrawal.ticket_hash, + withdrawal.ticket_owner + )) + } +} + +/// Invokes proxy (ERC wrapper) contract from within a deposit or +/// withdrawal handling function. +/// Assuming there is an open account storage transaction. +fn inner_execute_proxy( + handler: &mut EvmHandler, + caller: H160, + proxy: H160, + input: Vec, +) -> Result { + // We need to check that the proxy contract exists and has code, + // because otherwise the inner call will succeed although without + // any effect. + // + // Of course, we cannot protect from cases where proxy contract + // executes without errors, but does not actually update the ledger. + // At very least we can protect from typos and other mistakes. + if let Some(account) = handler.get_account(proxy)? { + if let Ok(true) = account.code_exists(handler.borrow_host()) { + handler.execute_call( + proxy, + None, + input, + TransactionContext::new(caller, proxy, U256::zero()), + ) + } else { + Ok(create_outcome_error!( + "Proxy contract does not have code: {}", + proxy + )) + } + } else { + Ok(create_outcome_error!( + "Proxy contract does not exist: {}", + proxy + )) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs new file mode 100644 index 000000000000..38d874928511 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs @@ -0,0 +1,440 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +pub use alloy_sol_types::SolCall; +use alloy_sol_types::{sol, SolConstructor}; +use crypto::hash::ContractKt1Hash; +use evm::Config; +use num_bigint::BigInt; +use primitive_types::{H160, H256, U256}; +use tezos_data_encoding::enc::BinWriter; +use tezos_ethereum::{ + block::{BlockConstants, BlockFees}, + Log, +}; +use tezos_evm_runtime::runtime::MockKernelHost; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::{ + contract::Contract, + michelson::{ + ticket::FA2_1Ticket, MichelsonBytes, MichelsonNat, MichelsonOption, MichelsonPair, + }, +}; +use tezos_storage::read_u256_le_default; + +use crate::{ + account_storage::{account_path, EthereumAccountStorage}, + handler::{EvmHandler, ExecutionOutcome}, + precompiles::{ + self, precompile_set, FA_BRIDGE_PRECOMPILE_ADDRESS, SYSTEM_ACCOUNT_ADDRESS, + }, + run_transaction, + utilities::keccak256_hash, + withdrawal_counter::WITHDRAWAL_COUNTER_PATH, +}; + +use super::{ + deposit::{ticket_hash, FaDeposit}, + execute_fa_deposit, execute_fa_withdrawal, + ticket_table::{ticket_balance_path, TicketTable}, + withdrawal::FaWithdrawal, +}; + +sol!( + token_wrapper, + "tests/contracts/artifacts/MockFaBridgeWrapper.abi" +); +sol!( + kernel_wrapper, + "tests/contracts/artifacts/MockFaBridgePrecompile.abi" +); +sol!( + reentrancy_tester, + "tests/contracts/artifacts/ReentrancyTester.abi" +); + +const MOCK_WRAPPER_BYTECODE: &[u8] = + include_bytes!("../../tests/contracts/artifacts/MockFaBridgeWrapper.bytecode"); + +const REENTRANCY_TESTER_BYTECODE: &[u8] = + include_bytes!("../../tests/contracts/artifacts/ReentrancyTester.bytecode"); + +/// Create a smart contract in the storage with the mocked token code +pub fn deploy_mock_wrapper( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + ticket: &FA2_1Ticket, + caller: &H160, + flag: u32, +) -> ExecutionOutcome { + let code = MOCK_WRAPPER_BYTECODE.to_vec(); + let (ticketer, content) = ticket_id(ticket); + let calldata = token_wrapper::constructorCall::new(( + ticketer.into(), + content.into(), + caller.0.into(), + convert_u256(&U256::from(flag)), + )); + + let block = dummy_block_constants(); + let precompiles = precompile_set::(false); + + set_balance(host, evm_account_storage, caller, U256::from(1_000_000)); + run_transaction( + host, + &block, + evm_account_storage, + &precompiles, + Config::shanghai(), + None, + *caller, + [code, calldata.abi_encode()].concat(), + Some(300_000), + U256::one(), + U256::zero(), + false, + 1_000_000_000, + false, + false, + None, + ) + .expect("Failed to deploy") + .unwrap() +} + +/// Create a smart contract in the storage with the mocked token code +pub fn deploy_reentrancy_tester( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + ticket: &FA2_1Ticket, + caller: &H160, + withdrawal_amount: U256, + withdrawal_count: U256, +) -> ExecutionOutcome { + let code = REENTRANCY_TESTER_BYTECODE.to_vec(); + let (ticketer, content) = ticket_id(ticket); + let dummy_routing_info = [vec![0u8; 22], vec![1u8], vec![0u8; 21]].concat(); + let calldata = reentrancy_tester::constructorCall::new(( + convert_h160(&FA_BRIDGE_PRECOMPILE_ADDRESS), + dummy_routing_info.into(), + convert_u256(&withdrawal_amount), + ticketer.into(), + content.into(), + convert_u256(&withdrawal_count), + )); + + let block = dummy_block_constants(); + let precompiles = precompile_set::(false); + + set_balance(host, evm_account_storage, caller, U256::from(1_000_000)); + run_transaction( + host, + &block, + evm_account_storage, + &precompiles, + Config::shanghai(), + None, + *caller, + [code, calldata.abi_encode()].concat(), + Some(1_000_000), + U256::one(), + U256::zero(), + false, + 1_000_000_000, + false, + false, + None, + ) + .expect("Failed to deploy") + .unwrap() +} + +/// Execute FA deposit +pub fn run_fa_deposit( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + deposit: &FaDeposit, + caller: &H160, + gas_limit: u64, + enable_fa_withdrawals: bool, +) -> ExecutionOutcome { + let block = dummy_block_constants(); + let precompiles = precompile_set::(enable_fa_withdrawals); + + execute_fa_deposit( + host, + &block, + evm_account_storage, + &precompiles, + Config::shanghai(), + *caller, + deposit, + 100_000_000_000, + None, + gas_limit, + ) + .expect("Failed to execute deposit") +} + +/// Create FA deposit given ticket and proxy address (optional) +pub fn dummy_fa_deposit(ticket: FA2_1Ticket, proxy: Option) -> FaDeposit { + FaDeposit { + ticket_hash: ticket_hash(&ticket).expect("Failed to calc ticket hash"), + proxy, + amount: 42.into(), + receiver: H160([4u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } +} + +/// Get value of a specific slot in the proxy contract storage +/// It is used to determine if the said contract was called +/// +/// See MockFaBridgeWrapper.sol where the flag is set: +/// +/// function setFlag(uint256 value) internal { +/// bytes32 slot = keccak256(abi.encodePacked("FLAG_TAG")); +/// assembly { +/// sstore(slot, value) +/// } +/// } +pub fn get_storage_flag( + host: &MockKernelHost, + evm_account_storage: &EthereumAccountStorage, + proxy: H160, +) -> u32 { + let proxy_account = evm_account_storage + .get(host, &account_path(&proxy).unwrap()) + .unwrap() + .unwrap(); + + let flag = proxy_account + .get_storage(host, &keccak256_hash(b"FLAG_TAG")) + .unwrap(); + U256::from_big_endian(&flag.0).as_u32() +} + +/// Block constants for testing +pub fn dummy_block_constants() -> BlockConstants { + let block_fees = BlockFees::new( + U256::from(21000), + U256::from(21000), + U256::from(2_000_000_000_000u64), + ); + let gas_limit = 1u64; + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + gas_limit, + H160::zero(), + ) +} + +/// Provision ticket balance for a specified account +pub fn ticket_balance_add( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + ticket_hash: &H256, + address: &H160, + balance: U256, +) -> bool { + let mut system = evm_account_storage + .get_or_create(host, &account_path(&SYSTEM_ACCOUNT_ADDRESS).unwrap()) + .unwrap(); + system + .ticket_balance_add(host, ticket_hash, address, balance) + .unwrap() +} + +/// Get ticket balance for a specified account +pub fn ticket_balance_get( + host: &impl Runtime, + evm_account_storage: &EthereumAccountStorage, + ticket_hash: &H256, + address: &H160, +) -> U256 { + let system = evm_account_storage + .get(host, &account_path(&SYSTEM_ACCOUNT_ADDRESS).unwrap()) + .unwrap() + .unwrap(); + + let path = system + .custom_path(&ticket_balance_path(ticket_hash, address).unwrap()) + .unwrap(); + read_u256_le_default(host, &path, U256::zero()).unwrap() +} + +/// Get next withdrawal counter value +pub fn withdrawal_counter_next( + host: &impl Runtime, + evm_account_storage: &EthereumAccountStorage, +) -> Option { + let system = evm_account_storage + .get_or_create(host, &account_path(&SYSTEM_ACCOUNT_ADDRESS).unwrap()) + .unwrap(); + + let path = system.custom_path(&WITHDRAWAL_COUNTER_PATH).unwrap(); + match host.store_read_all(&path) { + Ok(bytes) => Some(U256::from_little_endian(&bytes)), + _ => None, + } +} + +/// Provision TEZ balance for a specified account +pub fn set_balance( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, +) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } +} + +/// Create ticket with dummy creator and content +pub fn dummy_ticket() -> FA2_1Ticket { + use tezos_crypto_rs::hash::HashTrait; + + let ticketer = ContractKt1Hash::try_from_bytes(&[1u8; 20]).unwrap(); + FA2_1Ticket::new( + Contract::from_b58check(&ticketer.to_base58_check()).unwrap(), + MichelsonPair(0.into(), MichelsonOption(None)), + 1i32, + ) + .expect("Failed to construct ticket") +} + +/// Return ticket creator and content in forged form +pub fn ticket_id(ticket: &FA2_1Ticket) -> ([u8; 22], Vec) { + let mut ticketer = Vec::new(); + ticket.creator().0.bin_write(&mut ticketer).unwrap(); + + let mut content = Vec::new(); + ticket.contents().bin_write(&mut content).unwrap(); + + (ticketer.try_into().unwrap(), content) +} + +/// Convert U256 to the alloy primitive type +pub fn convert_u256(value: &U256) -> alloy_primitives::U256 { + alloy_primitives::U256::from_limbs(value.0) +} + +/// Convert H160 to the alloy primitive type +pub fn convert_h160(value: &H160) -> alloy_primitives::Address { + alloy_primitives::Address::from_slice(&value.0) +} + +/// Convert EVM Log to the alloy primitive type +pub fn convert_log(log: &Log) -> alloy_primitives::LogData { + alloy_primitives::LogData::new_unchecked( + log.topics.iter().map(|x| x.0.into()).collect(), + log.data.clone().into(), + ) +} + +/// Create block constants +pub fn dummy_first_block() -> BlockConstants { + let block_fees = BlockFees::new( + U256::from(21000), + U256::from(21000), + U256::from(2_000_000_000_000u64), + ); + let gas_limit = 1u64; + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + gas_limit, + H160::zero(), + ) +} + +/// Create FA withdrawal given ticket and sender/owner addresses +pub fn dummy_fa_withdrawal( + ticket: FA2_1Ticket, + sender: H160, + ticket_owner: H160, +) -> FaWithdrawal { + FaWithdrawal { + sender, + receiver: Contract::from_b58check("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU") + .unwrap(), + proxy: Contract::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT").unwrap(), + amount: 42.into(), + ticket_hash: ticket_hash(&ticket).expect("Failed to calc ticket hash"), + ticket, + ticket_owner, + } +} + +/// Execute FA withdrawal directly without going through the precompile +pub fn fa_bridge_precompile_call_withdraw( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + withdrawal: FaWithdrawal, + caller: H160, +) -> ExecutionOutcome { + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let config = Config::shanghai(); + + let mut handler = EvmHandler::new( + host, + evm_account_storage, + caller, + &block, + &config, + &precompiles, + 1_000_000_000, + U256::from(21000), + false, + None, + ); + + handler + .begin_initial_transaction(false, Some(30_000_000)) + .unwrap(); + + let res = execute_fa_withdrawal(&mut handler, caller, withdrawal); + + let execution_result = match res { + Ok(mut outcome) => { + handler.add_withdrawals(&mut outcome.withdrawals).unwrap(); + Ok((outcome.exit_status, None, vec![])) + } + Err(err) => Err(err), + }; + + handler.end_initial_transaction(execution_result).unwrap() +} + +pub fn create_fa_ticket( + ticketer: &str, + token_id: u64, + metadata: &[u8], + amount: BigInt, +) -> FA2_1Ticket { + let creator = + Contract::Originated(ContractKt1Hash::from_base58_check(ticketer).unwrap()); + let contents = MichelsonPair( + MichelsonNat::new(BigInt::from(token_id).into()).unwrap(), + MichelsonOption(Some(MichelsonBytes(metadata.to_vec()))), + ); + FA2_1Ticket::new(creator, contents, amount).unwrap() +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/tests.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/tests.rs new file mode 100644 index 000000000000..1b4a723c09e7 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/tests.rs @@ -0,0 +1,522 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +use alloy_primitives::FixedBytes; +use alloy_sol_types::SolEvent; +use evm::ExitError; +use primitive_types::{H160, U256}; +use tezos_evm_runtime::runtime::MockKernelHost; + +use crate::{ + account_storage::{account_path, init_account_storage}, + fa_bridge::{ + deposit::ticket_hash, + test_utils::{ + convert_h160, convert_log, convert_u256, deploy_mock_wrapper, + deploy_reentrancy_tester, dummy_fa_deposit, dummy_fa_withdrawal, + dummy_ticket, fa_bridge_precompile_call_withdraw, get_storage_flag, + kernel_wrapper, run_fa_deposit, ticket_balance_add, ticket_balance_get, + token_wrapper, withdrawal_counter_next, + }, + FA_DEPOSIT_PROXY_GAS_LIMIT, + }, + handler::ExecutionResult, +}; + +#[test] +fn fa_deposit_reached_wrapper_contract() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_mock_wrapper( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + 0, + ) + .new_address() + .unwrap(); + + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + FA_DEPOSIT_PROXY_GAS_LIMIT, + false, + ); + assert!(res.is_success()); + assert_eq!(2, res.logs.len()); + + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(deposit.amount.as_u32(), flag); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + U256::zero() + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &proxy + ), + deposit.amount + ); + + let mint_event = + token_wrapper::Mint::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Mint event"); + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[1]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!(mint_event.amount, convert_u256(&deposit.amount)); + assert_eq!(mint_event.receiver, convert_h160(&deposit.receiver)); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!( + deposit_event.ticketOwner, + convert_h160(&deposit.proxy.unwrap()) + ); // ticket owner is now wrapper contract + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_refused_due_non_existing_contract() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + let deposit = dummy_fa_deposit(ticket, Some(H160([1u8; 20]))); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + FA_DEPOSIT_PROXY_GAS_LIMIT, + false, + ); + assert_eq!(1, res.logs.len()); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + deposit.amount + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::zero() + ); + + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!(deposit_event.ticketOwner, convert_h160(&deposit.receiver)); // ticket owner is deposit receiver + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_refused_non_compatible_interface() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let proxy = H160([1u8; 20]); + let ticket = dummy_ticket(); + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + + // Making it look as a smart contract + let mut account = evm_account_storage + .get_or_create(&mock_runtime, &account_path(&proxy).unwrap()) + .unwrap(); + account.set_code(&mut mock_runtime, &[255u8; 1024]).unwrap(); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + FA_DEPOSIT_PROXY_GAS_LIMIT, + false, + ); + assert_eq!(1, res.logs.len()); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + deposit.amount + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::zero() + ); + + let deposit_event = + kernel_wrapper::Deposit::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Deposit event"); + + assert_eq!( + deposit_event.ticketHash, + convert_u256(&U256::from(deposit.ticket_hash.as_bytes())) + ); + assert_eq!(deposit_event.ticketOwner, convert_h160(&deposit.receiver)); // ticket owner is deposit receiver + assert_eq!(deposit_event.receiver, convert_h160(&deposit.receiver)); + assert_eq!(deposit_event.amount, convert_u256(&deposit.amount)); + assert_eq!( + deposit_event.inboxLevel, + convert_u256(&U256::from(deposit.inbox_level)) + ); + assert_eq!( + deposit_event.inboxMsgId, + convert_u256(&U256::from(deposit.inbox_msg_id)) + ); +} + +#[test] +fn fa_deposit_proxy_state_reverted_if_ticket_balance_overflows() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_mock_wrapper( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + 100500, + ) + .new_address() + .unwrap(); + + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &deposit.ticket_hash, + &proxy, + U256::MAX, + ); + + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + FA_DEPOSIT_PROXY_GAS_LIMIT, + false, + ); + assert!(!res.is_success()); + assert!(res.logs.is_empty()); + + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(100500, flag); + + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.receiver + ), + U256::zero() + ); + assert_eq!( + ticket_balance_get( + &mock_runtime, + &evm_account_storage, + &deposit.ticket_hash, + &deposit.proxy.unwrap() + ), + U256::MAX + ); +} + +#[test] +fn fa_withdrawal_executed_via_l2_proxy_contract() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_mock_wrapper( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + 0, + ) + .new_address() + .unwrap(); + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &proxy, + withdrawal.amount, + ); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(res.is_success()); + assert!(!res.withdrawals.is_empty()); + assert_eq!(2, res.logs.len()); + + // Re-create withdrawal struct + let withdrawal = dummy_fa_withdrawal(dummy_ticket(), sender, proxy); + + // Ensure proxy contract state changed + let flag = get_storage_flag(&mock_runtime, &evm_account_storage, proxy); + assert_eq!(withdrawal.amount.as_u32(), flag); + + // Ensure ticket balance reduced to zero (if not then will overflow) + assert!(ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + U256::MAX, + )); + + // Ensure events are emitted correctly + let withdrawal_event = + kernel_wrapper::Withdrawal::decode_log_data(&convert_log(&res.logs[0]), true) + .expect("Failed to parse Withdrawal event"); + + let burn_event = + token_wrapper::Burn::decode_log_data(&convert_log(&res.logs[1]), true) + .expect("Failed to parse Burn event"); + + assert_eq!( + withdrawal_event.ticketHash, + alloy_primitives::U256::from_be_bytes(withdrawal.ticket_hash.0) + ); + assert_eq!(withdrawal_event.sender, convert_h160(&sender)); + assert_eq!( + withdrawal_event.ticketOwner, + convert_h160(&withdrawal.ticket_owner) + ); + assert_eq!(withdrawal_event.receiver, FixedBytes::new([0u8; 22])); + assert_eq!(withdrawal_event.amount, convert_u256(&withdrawal.amount)); + assert_eq!(withdrawal_event.withdrawalId, convert_u256(&U256::from(0))); + + assert_eq!(burn_event.sender, convert_h160(&withdrawal.sender)); + assert_eq!(burn_event.amount, convert_u256(&withdrawal.amount)); + + // Ensure withdrawal counter is incremented + assert_eq!( + Some(U256::one()), + withdrawal_counter_next(&mock_runtime, &evm_account_storage) + ); +} + +#[test] +fn fa_withdrawal_fails_due_to_faulty_l2_proxy() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + let proxy = H160::from_low_u64_be(2); // non-existing contract + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &proxy, + withdrawal.amount, + ); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(!res.is_success()); + assert!(res.withdrawals.is_empty()); + assert!(res.logs.is_empty()); + + // Re-create withdrawal struct + let withdrawal = dummy_fa_withdrawal(dummy_ticket(), sender, proxy); + + // Ensure ticket balance is non-zero (should overflow) + assert!(!ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &withdrawal.ticket_hash, + &withdrawal.ticket_owner, + U256::MAX, + )); + + // Ensure withdrawal counter is reverted + assert_eq!( + None, + withdrawal_counter_next(&mock_runtime, &evm_account_storage) + ); + assert!( + matches!(res.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("Proxy contract does not exist")) + ); +} + +#[test] +fn fa_withdrawal_fails_due_to_insufficient_balance() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let sender = H160::from_low_u64_be(1); + let caller = H160::zero(); + let ticket = dummy_ticket(); + let proxy = H160::from_low_u64_be(2); // non-existing contract + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + let res = fa_bridge_precompile_call_withdraw( + &mut mock_runtime, + &mut evm_account_storage, + withdrawal, + caller, + ); + assert!(!res.is_success()); + assert!(res.withdrawals.is_empty()); + assert!(res.logs.is_empty()); + + // Ensure withdrawal counter is not updated (returned before incrementing the nonce) + assert_eq!( + None, + withdrawal_counter_next(&mock_runtime, &evm_account_storage) + ); + assert!( + matches!(res.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("Insufficient ticket balance")) + ); +} + +#[test] +fn fa_deposit_cannot_call_fa_withdrawal_precompile() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::zero(); + let ticket = dummy_ticket(); + + let proxy = deploy_reentrancy_tester( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &caller, + U256::one(), + U256::one(), + ) + .new_address() + .expect("Failed to deploy reentrancy tester"); + + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &ticket_hash(&ticket).unwrap(), + &proxy, + U256::one(), + ); + + let deposit = dummy_fa_deposit(ticket, Some(proxy)); + + // First let's show that it's possible to withdraw from the inner call + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + 100_000_000, + true, + ); + assert!(res.is_success()); + assert!(!res.withdrawals.is_empty()); + + // Now let's do the same but without enabling the withdrawal precompile + let res = run_fa_deposit( + &mut mock_runtime, + &mut evm_account_storage, + &deposit, + &caller, + 100_000_000, + false, + ); + assert!(res.is_success()); + assert!(res.withdrawals.is_empty()); +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/ticket_table.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/ticket_table.rs new file mode 100644 index 000000000000..119b9fa7886f --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/ticket_table.rs @@ -0,0 +1,150 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! Global ticket table. +//! +//! Maintains a ledger that tracks ownership of deposited tickets. +//! Any EVM account can be ticket owner, whether it's EOA or smart contract. + +use crate::account_storage::{account_path, AccountStorageError, EthereumAccount}; +use primitive_types::{H160, H256, U256}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_host::path::{concat, OwnedPath, RefPath}; +use tezos_storage::{path_from_h256, read_u256_le_default, write_u256_le}; + +/// Path where global ticket table is stored +const TICKET_TABLE_PATH: RefPath = RefPath::assert_from(b"/ticket_table"); + +pub trait TicketTable { + /// Increases ticket balance + fn ticket_balance_add( + &mut self, + host: &mut impl Runtime, + ticket_hash: &H256, + address: &H160, + amount: U256, + ) -> Result; + + /// Decreases ticket balance + fn ticket_balance_remove( + &mut self, + host: &mut impl Runtime, + ticket_hash: &H256, + address: &H160, + amount: U256, + ) -> Result; +} + +pub(crate) fn ticket_balance_path( + ticket_hash: &H256, + address: &H160, +) -> Result { + let suffix = concat(&path_from_h256(ticket_hash)?, &account_path(address)?)?; + concat(&TICKET_TABLE_PATH, &suffix).map_err(Into::into) +} + +impl TicketTable for EthereumAccount { + fn ticket_balance_add( + &mut self, + host: &mut impl Runtime, + ticket_hash: &H256, + owner: &H160, + amount: U256, + ) -> Result { + let path = self.custom_path(&ticket_balance_path(ticket_hash, owner)?)?; + let balance = read_u256_le_default(host, &path, U256::zero())?; + + if let Some(new_balance) = balance.checked_add(amount) { + write_u256_le(host, &path, new_balance)?; + Ok(true) + } else { + Ok(false) + } + } + + fn ticket_balance_remove( + &mut self, + host: &mut impl Runtime, + ticket_hash: &H256, + owner: &H160, + amount: U256, + ) -> Result { + let path = self.custom_path(&ticket_balance_path(ticket_hash, owner)?)?; + let balance = read_u256_le_default(host, &path, U256::zero())?; + + if let Some(new_balance) = balance.checked_sub(amount) { + write_u256_le(host, &path, new_balance)?; + Ok(true) + } else { + Ok(false) + } + } +} + +#[cfg(test)] +mod tests { + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_host::path::RefPath; + use tezos_storage::read_u256_le_default; + + use crate::precompiles::SYSTEM_ACCOUNT_ADDRESS; + + use super::*; + + #[test] + fn ticket_table_balance_add_succeeds() { + let mut host = MockKernelHost::default(); + + let mut account = EthereumAccount::from_address(&SYSTEM_ACCOUNT_ADDRESS).unwrap(); + + let ticket_hash: H256 = H256([1u8; 32]); + let address = H160([2u8; 20]); + + account + .ticket_balance_add(&mut host, &ticket_hash, &address, 42.into()) + .unwrap(); + account + .ticket_balance_add(&mut host, &ticket_hash, &address, 42.into()) + .unwrap(); + + let path = b"\ + /evm/world_state/eth_accounts/0000000000000000000000000000000000000000/ticket_table\ + /0101010101010101010101010101010101010101010101010101010101010101\ + /0202020202020202020202020202020202020202"; + let balance = + read_u256_le_default(&host, &RefPath::assert_from(path), U256::zero()) + .unwrap(); + + assert_eq!(U256::from(84), balance); + } + + #[test] + fn ticket_table_balance_add_overflows() { + let mut host = MockKernelHost::default(); + + let mut account = EthereumAccount::from_address(&SYSTEM_ACCOUNT_ADDRESS).unwrap(); + + let ticket_hash: H256 = H256([1u8; 32]); + let address = H160([2u8; 20]); + + account + .ticket_balance_add(&mut host, &ticket_hash, &address, U256::MAX) + .unwrap(); + + let res = account + .ticket_balance_add(&mut host, &ticket_hash, &address, U256::MAX) + .unwrap(); + assert!(!res); + + let path = b"\ + /evm/world_state/eth_accounts/0000000000000000000000000000000000000000/ticket_table\ + /0101010101010101010101010101010101010101010101010101010101010101\ + /0202020202020202020202020202020202020202"; + let balance = + read_u256_le_default(&host, &RefPath::assert_from(path), U256::zero()) + .unwrap(); + + assert_eq!(U256::MAX, balance); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/withdrawal.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/withdrawal.rs new file mode 100644 index 000000000000..41665dbeafe7 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/withdrawal.rs @@ -0,0 +1,443 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA token withdrawal. +//! +//! Represents a ticket transfer from L2 to L1 where: +//! * The content is of standard type (FA2.1 compatible) +//! * Destination is either an implicit account, +//! or a smart contract implementing standard "withdraw" method +//! +//! It has several implicit constraints: +//! * Routing info must contain valid receiver address, user has access to +//! (otherwise funds are forever lost) +//! * If a smart contract address is specified as target, it must be +//! correct and expose a standard "withdraw" method, otherwise funds +//! will be lost. +//! +//! Unlike FA deposits, we cannot handle runtime errors on Tezos L1 +//! (at least in the current implementation). +//! +//! It should also be noted that in order to complete the withdrawal on L1 +//! one must obtain the outbox message proof and explicitly submit it to Tezos L1. +//! +//! A special withdrawal event is emitted upon successful withdrawal request, +//! which can be used both for indexing and for retrieving information necessary +//! to generate outbox message proof (outbox level + message id). + +use num_bigint::BigInt; +use primitive_types::{H160, H256, U256}; +use tezos_data_encoding::{enc::BinWriter, nom::NomReader}; +use tezos_ethereum::Log; +use tezos_smart_rollup_encoding::{ + contract::Contract, + entrypoint::Entrypoint, + michelson::{ + ticket::FA2_1Ticket, MichelsonBytes, MichelsonContract, MichelsonNat, + MichelsonOption, MichelsonPair, + }, + outbox::{OutboxMessage, OutboxMessageTransaction}, +}; + +use crate::{ + abi::{self, ABI_B22_RIGHT_PADDING, ABI_H160_LEFT_PADDING}, + handler::Withdrawal, + precompiles::FA_BRIDGE_PRECOMPILE_ADDRESS, + utilities::keccak256_hash, +}; + +use super::error::FaBridgeError; + +/// Keccak256 of withdraw(address,uint256,uint256), first 4 bytes +pub const WITHDRAW_METHOD_ID: &[u8; 4] = b"\xb5\xc5\xf6\x72"; + +/// Keccak256 of Withdrawal(uint256,address,address,bytes22,bytes22,uint256,uint256) +pub const WITHDRAW_EVENT_TOPIC: &[u8; 32] = b"\ + \xab\x68\x45\x0c\x9e\x54\x6f\x60\x62\xa8\x61\xee\xbf\x8e\xc5\xbb\ + \xd4\x1b\x44\x25\xe2\x6b\x20\x19\x9c\x91\x22\x7c\x7f\x90\x38\xca"; + +/// L1 proxy contract entrypoint that will be invoked by the outbox message +/// execution. +pub const WITHDRAW_ENTRYPOINT: &str = "withdraw"; + +/// Withdrawal structure parsed from the precompile calldata +#[derive(Debug, PartialEq)] +pub struct FaWithdrawal { + /// Account that invoked the precompile (assuming the sender) + pub sender: H160, + /// Contract (either implicit or originated) that will receive tokens or tickets + pub receiver: Contract, + /// Proxy contract on L1 that can handle ticket + receiver address (mandatory for now) + pub proxy: Contract, + /// Ticket transfer amount + pub amount: U256, + /// FA2.1 compatible ticket, constructed from the input + pub ticket: FA2_1Ticket, + /// Etherlink compatible ticket digest + pub ticket_hash: H256, + /// Actual ticket owner in global table (can be either sender or an ERC wrapper) + pub ticket_owner: H160, +} + +impl FaWithdrawal { + /// Tries to parse withdrawal structure from the precompile call data, + /// method id excluded (first 4 bytes). + /// + /// withdraw( + /// address ticketOwner, + /// bytes memory routingInfo, + /// uint256 amount, + /// bytes22 ticketer, + /// bytes memory content + /// ) + pub fn try_parse(input_data: &[u8], sender: H160) -> Result { + let ticket_owner = abi::h160_parameter(input_data, 0) + .ok_or(FaBridgeError::AbiDecodeError("ticket_owner"))?; + let routing_info = abi::bytes_parameter(input_data, 1) + .ok_or(FaBridgeError::AbiDecodeError("routing_info"))?; + let amount = abi::u256_parameter(input_data, 2) + .ok_or(FaBridgeError::AbiDecodeError("amount"))?; + let ticketer: [u8; 22] = abi::fixed_bytes_parameter(input_data, 3) + .ok_or(FaBridgeError::AbiDecodeError("ticketer"))?; + let content = abi::bytes_parameter(input_data, 4) + .ok_or(FaBridgeError::AbiDecodeError("content"))?; + + let ticket_hash = ticket_hash_from_raw_parts(&ticketer, content); + let (receiver, proxy) = parse_l1_routing_info(routing_info)?; + let ticket = construct_ticket(ticketer, content, amount)?; + + Ok(Self { + sender, + receiver, + proxy, + amount, + ticket, + ticket_hash, + ticket_owner, + }) + } + + /// Returns calldata for the proxy (ERC wrapper) contract. + /// + /// Signature: withdraw(address,uint256,uint256) + pub fn calldata(&self) -> Vec { + let mut call_data = Vec::with_capacity(100); + call_data.extend_from_slice(WITHDRAW_METHOD_ID); + + call_data.extend_from_slice(&ABI_H160_LEFT_PADDING); + call_data.extend_from_slice(self.sender.as_bytes()); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data.extend_from_slice(self.ticket_hash.as_bytes()); + debug_assert!((call_data.len() - 4) % 32 == 0); + + call_data + } + + /// Returns log structure for an implicit withdrawal event. + /// This event is added to the outer transaction receipt, + /// so that we can index successful withdrawal requests. + /// + /// It also contains unique withdrawal identifier. + /// + /// Signature: Withdrawal(uint256,address,address,bytes22,bytes22,uint256,uint256) + pub fn event_log(&self, withdrawal_id: U256) -> Log { + let mut data = Vec::with_capacity(7 * 32); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(self.sender.as_bytes()); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(self.ticket_owner.as_bytes()); + debug_assert!(data.len() % 32 == 0); + + // It is safe to unwrap, underlying implementation never fails (always returns Ok(())) + self.receiver.bin_write(&mut data).unwrap(); + data.extend_from_slice(&ABI_B22_RIGHT_PADDING); + debug_assert!(data.len() % 32 == 0); + + self.proxy.bin_write(&mut data).unwrap(); + data.extend_from_slice(&ABI_B22_RIGHT_PADDING); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(withdrawal_id)); + debug_assert!(data.len() % 32 == 0); + + Log { + address: FA_BRIDGE_PRECOMPILE_ADDRESS, + topics: vec![H256(*WITHDRAW_EVENT_TOPIC), self.ticket_hash], + data, + } + } + + // Converts FA withdrawal to an outbox message with a predefined Michelson type + pub fn into_outbox_message(self) -> Withdrawal { + let message = OutboxMessageTransaction { + // Destination is always proxy contract (until sr -> tz ticket transfers are enabled) + destination: self.proxy, + // Constant entrypoint name parsing won't fail + entrypoint: Entrypoint::try_from(WITHDRAW_ENTRYPOINT.to_string()).unwrap(), + // L1 proxy accepts ticket and the final receiver address + parameters: MichelsonPair(MichelsonContract(self.receiver), self.ticket), + }; + crate::handler::Withdrawal::Standard(OutboxMessage::AtomicTransactionBatch( + vec![message].into(), + )) + } + + /// Formats FA withdrawal structure for logging purposes. + pub fn display(&self) -> String { + format!( + "FA withdrawal {} of {} from {} via {:?}", + self.amount, self.ticket_hash, self.sender, self.ticket_owner + ) + } +} + +/// Split routing info (raw bytes passed along with the ticket) into receiver and proxy addresses. +fn parse_l1_routing_info( + routing_info: &[u8], +) -> Result<(Contract, Contract), FaBridgeError> { + let (rest, receiver) = Contract::nom_read(routing_info) + .map_err(|_| FaBridgeError::InvalidRoutingInfo("receiver"))?; + + let (rest, proxy) = Contract::nom_read(rest) + .map_err(|_| FaBridgeError::InvalidRoutingInfo("proxy"))?; + + if let Contract::Implicit(_) = proxy { + return Err(FaBridgeError::InvalidRoutingInfo("implicit proxy")); + } + + if !rest.is_empty() { + return Err(FaBridgeError::InvalidRoutingInfo("trailing bytes")); + } + + Ok((receiver, proxy)) +} + +/// Construct FA2.1 ticket given its parts in raw format +fn construct_ticket( + ticketer: [u8; 22], + content: &[u8], + amount: U256, +) -> Result { + let (_, creator) = Contract::nom_read(&ticketer) + .map_err(|_| FaBridgeError::TicketConstructError("creator"))?; + let (_, contents) = + MichelsonPair::>::nom_read(content) + .map_err(|_| FaBridgeError::TicketConstructError("contents"))?; + let amount = + BigInt::from_bytes_be(num_bigint::Sign::Plus, &Into::<[u8; 32]>::into(amount)); + + FA2_1Ticket::new(creator, contents, amount) + .map_err(|_| FaBridgeError::TicketConstructError("amount")) +} + +/// Calculate unique ticket hash out of the ticket identifier (ticketer address and content). +/// +/// Computed as Keccak256(ticketer || content) where +/// * ticketer: contract is in its forged form [ 0x01 | 20 bytes | 0x00 ] +/// * content: Micheline expression is in its forged form, legacy optimized mode +/// +/// Solidity equivalent: uint256(keccak256(abi.encodePacked(ticketer, content))); +fn ticket_hash_from_raw_parts(ticketer: &[u8], content: &[u8]) -> H256 { + let mut bytes = Vec::with_capacity(ticketer.len() + content.len()); + bytes.extend_from_slice(ticketer); + bytes.extend_from_slice(content); + keccak256_hash(&bytes) +} + +#[cfg(test)] +mod tests { + use alloy_sol_types::{SolCall, SolEvent}; + use primitive_types::{H160, U256}; + use tezos_data_encoding::enc::BinWriter; + use tezos_smart_rollup_encoding::contract::Contract; + + use crate::{ + fa_bridge::{ + deposit::ticket_hash, + test_utils::{ + convert_h160, convert_log, convert_u256, dummy_ticket, kernel_wrapper, + ticket_id, token_wrapper, + }, + withdrawal::WITHDRAW_EVENT_TOPIC, + }, + utilities::bigint_to_u256, + }; + + use super::{ticket_hash_from_raw_parts, FaWithdrawal}; + + fn dummy_fa_withdrawal() -> FaWithdrawal { + let ticket = dummy_ticket(); + let sender = H160([1u8; 20]); + let ticket_owner = H160([2u8; 20]); + let ticket_hash = ticket_hash(&ticket).expect("Failed to calc ticket hash"); + let amount = bigint_to_u256(ticket.amount()).unwrap(); + FaWithdrawal { + sender, + receiver: Contract::from_b58check("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU") + .unwrap(), + proxy: Contract::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(), + amount, + ticket_hash, + ticket, + ticket_owner, + } + } + + #[test] + fn fa_withdrawal_parsing_success() { + let ticket_owner = H160([1u8; 20]); + let receiver = [ + [0u8; 22].to_vec(), + vec![0x01], + [0u8; 20].to_vec(), + vec![0x00], + ] + .concat(); + + let ticket = dummy_ticket(); + let (ticketer, content) = ticket_id(&ticket); + let ticket_hash = ticket_hash(&ticket).unwrap(); + + let amount = bigint_to_u256(ticket.amount()).unwrap(); + + let input = kernel_wrapper::withdrawCall::new(( + convert_h160(&ticket_owner), + receiver.into(), + convert_u256(&amount), + ticketer.into(), + content.into(), + )) + .abi_encode(); + let sender = H160::zero(); + + let withdrawal = FaWithdrawal::try_parse(&input[4..], sender).unwrap(); + + pretty_assertions::assert_eq!( + withdrawal, + FaWithdrawal { + amount, + sender, + receiver: Contract::from_b58check("tz1Ke2h7sDdakHJQh8WX4Z372du1KChsksyU") + .unwrap(), + proxy: Contract::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(), + ticket, + ticket_hash, + ticket_owner + } + ); + } + + #[test] + fn fa_withdrawal_verify_calldata_encoding() { + let withdrawal = dummy_fa_withdrawal(); + + let actual = withdrawal.calldata(); + + let expected = token_wrapper::withdrawCall::new(( + convert_h160(&withdrawal.sender), + convert_u256(&withdrawal.amount), + alloy_primitives::U256::from_be_slice(&withdrawal.ticket_hash.0), + )) + .abi_encode(); + + pretty_assertions::assert_eq!(expected, actual); + } + + #[test] + fn fa_withdrawal_verify_eventlog_encoding() { + let withdrawal = dummy_fa_withdrawal(); + + let log = withdrawal.event_log(U256::one()); + + let withdrawal_event = + kernel_wrapper::Withdrawal::decode_log_data(&convert_log(&log), true) + .expect("Failed to parse Withdrawal event"); + + let ticket_hash_topic = + alloy_primitives::U256::from_be_slice(&withdrawal.ticket_hash.0); + + assert_eq!(withdrawal_event.topics().0 .0, *WITHDRAW_EVENT_TOPIC); + assert_eq!(withdrawal_event.topics().1, ticket_hash_topic); + + assert_eq!(withdrawal_event.ticketHash, ticket_hash_topic); + assert_eq!(withdrawal_event.sender, convert_h160(&withdrawal.sender)); + assert_eq!( + withdrawal_event.ticketOwner, + convert_h160(&withdrawal.ticket_owner) + ); + assert_eq!( + withdrawal_event.receiver, + alloy_primitives::FixedBytes::<22>::repeat_byte(0) + ); + assert_eq!( + withdrawal_event.proxy, + alloy_primitives::FixedBytes::<22>::from_slice(&[ + 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0 + ]) + ); + assert_eq!(withdrawal_event.amount, convert_u256(&withdrawal.amount)); + assert_eq!( + withdrawal_event.withdrawalId, + alloy_primitives::U256::from(1) + ); + } + + #[test] + fn fa_withdrawal_verify_message_to_originated_contract_encoding() { + let withdrawal = dummy_fa_withdrawal(); + + let outbox_message = withdrawal.into_outbox_message(); + + let mut encoded_message = Vec::new(); + outbox_message.bin_write(&mut encoded_message).unwrap(); + + // 0x00 byte prefix + // forged array + // [ + // forged parameters — pair(receiver, ticket) + // forged destination — originated address + // forged entrypoint (array) — "withdraw" + // ] + // octez-codec decode 017-PtNairob.smart_rollup.outbox.message from + let expected = hex::decode( + "\ + 000000006607070a000000160000000000000000000000000000000000000000\ + 000007070a000000160101010101010101010101010101010101010101010007\ + 0707070000030600010100000000000000000000000000000000000000000000\ + 0000087769746864726177", + ) + .unwrap(); + + pretty_assertions::assert_eq!(expected, encoded_message); + } + + #[test] + fn check_ticket_hash_equality() { + let ticket = dummy_ticket(); + + let mut ticketer = Vec::new(); + ticket.creator().0.bin_write(&mut ticketer).unwrap(); + assert_eq!(ticketer.len(), 22); + + let mut content = Vec::new(); + ticket.contents().bin_write(&mut content).unwrap(); + assert_eq!(content.len(), 6); + + let actual = ticket_hash_from_raw_parts(&ticketer, &content); + let expected = ticket_hash(&ticket).unwrap(); + assert_eq!(expected, actual); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/handler.rs b/etherlink/kernel_calypso2/evm_execution/src/handler.rs new file mode 100644 index 000000000000..81fa4be8e4d1 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/handler.rs @@ -0,0 +1,5094 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023-2025 Functori +// SPDX-FileCopyrightText: 2023-2024 PK Lab +// +// SPDX-License-Identifier: MIT + +//! Handle details of EVM runtime +//! +//! The interface between SputnikVM and the kernel. This includes interface +//! to storage, account balances, block constants, _and transaction state_. + +use crate::access_record::AccessRecord; +use crate::account_storage::{ + account_path, AccountStorageError, EthereumAccount, EthereumAccountStorage, + StorageValue, CODE_HASH_DEFAULT, +}; +use crate::precompiles::reentrancy_guard::ReentrancyGuard; +use crate::precompiles::{FA_BRIDGE_PRECOMPILE_ADDRESS, WITHDRAWAL_ADDRESS}; +use crate::storage::blocks::{get_block_hash, BLOCKS_STORED}; +use crate::storage::tracer; +use crate::tick_model_opcodes; +use crate::trace::{ + CallTrace, CallTracerConfig, CallTracerInput, StorageMapItem, StructLog, + StructLoggerInput, TracerInput, +}; +use crate::transaction::TransactionContext; +use crate::transaction_layer_data::TransactionLayerData; +use crate::utilities::create_address_legacy; +use crate::EthereumError; +use crate::PrecompileSet; +use crate::TracerInput::{CallTracer, StructLogger}; +use alloc::borrow::Cow; +use alloc::rc::Rc; +use core::convert::Infallible; +use evm::executor::stack::Log; +use evm::gasometer::{GasCost, MemoryCost}; +use evm::{ + CallScheme, Capture, Config, Context, CreateScheme, ExitError, ExitFatal, ExitReason, + ExitRevert, ExitSucceed, Handler, Opcode, Resolve, Stack, Transfer, +}; +use primitive_types::{H160, H256, U256}; +use sha3::{Digest, Keccak256}; +use std::cmp::min; +use std::collections::HashMap; +use std::fmt::Debug; +use tezos_data_encoding::enc::{BinResult, BinWriter}; +use tezos_ethereum::block::BlockConstants; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; +use tezos_smart_rollup_encoding::michelson::{ + MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonPair, MichelsonTimestamp, +}; +use tezos_smart_rollup_encoding::outbox::OutboxMessage; +use tezos_smart_rollup_storage::StorageError; + +/// Withdrawal interface of the ticketer contract +pub type RouterInterface = MichelsonPair; + +/// Interface of the default entrypoint of the fast withdrawal contract. +/// +/// The parameters corresponds to (from left to right w.r.t. `MichelsonPair`): +/// * withdrawal_id +/// * ticket +/// * timestamp +/// * withdrawer's address +/// * generic payload +/// * l2 caller's address +pub type FastWithdrawalInterface = MichelsonPair< + MichelsonNat, + MichelsonPair< + FA2_1Ticket, + MichelsonPair< + MichelsonTimestamp, + MichelsonPair< + MichelsonContract, + MichelsonPair, + >, + >, + >, +>; + +/// Outbox messages that implements the different withdrawal interfaces, +/// ready to be encoded and posted. +#[derive(Debug, PartialEq, Eq)] +pub enum Withdrawal { + Standard(OutboxMessage), + Fast(OutboxMessage), +} + +impl BinWriter for Withdrawal { + fn bin_write(&self, output: &mut Vec) -> BinResult { + match self { + Withdrawal::Standard(outbox_message_full) => { + outbox_message_full.bin_write(output) + } + Withdrawal::Fast(outbox_message_full) => { + outbox_message_full.bin_write(output) + } + } + } +} + +#[derive(Debug, Eq, PartialEq)] +pub enum ExecutionResult { + TransferSucceeded, + ContractDeployed(H160, Vec), + CallSucceeded(ExitSucceed, Vec), + CallReverted(Vec), + Error(ExitError), + FatalError(ExitFatal), + OutOfTicks, +} + +impl ExecutionResult { + pub const fn is_success(&self) -> bool { + matches!( + self, + ExecutionResult::TransferSucceeded + | ExecutionResult::ContractDeployed(_, _) + | ExecutionResult::CallSucceeded(_, _) + ) + } + pub fn output(&self) -> Option<&[u8]> { + match self { + ExecutionResult::ContractDeployed(_, output) + | ExecutionResult::CallSucceeded(_, output) + | ExecutionResult::CallReverted(output) => Some(output.as_slice()), + _ => None, + } + } + pub const fn new_address(&self) -> Option { + match self { + ExecutionResult::ContractDeployed(new_address, _) => Some(*new_address), + _ => None, + } + } +} + +/// Outcome of making the [EvmHandler] run an Ethereum transaction +/// +/// Be it contract -call, -create or simple transfer, the handler will update the world +/// state in durable storage _and_ produce a summary of the outcome that will be needed +/// for creating a transaction receipt. +#[derive(Debug, Eq, PartialEq)] +pub struct ExecutionOutcome { + /// How much gas was used for processing an entire transaction. + pub gas_used: u64, + /// Logs generated by the transaction. + pub logs: Vec, + /// Result of the execution + pub result: ExecutionResult, + /// Withdrawals generated by the transaction. This field will be empty if the + /// transaction fails (or if the transaction doesn't produce any withdrawals). + pub withdrawals: Vec, + /// Number of estimated ticks used at the end of the contract call + pub estimated_ticks_used: u64, +} + +impl ExecutionOutcome { + pub const fn is_success(&self) -> bool { + self.result.is_success() + } + pub fn output(&self) -> Option<&[u8]> { + self.result.output() + } + pub const fn new_address(&self) -> Option { + self.result.new_address() + } +} + +/// The result of calling a contract as expected by the SputnikVM EVM implementation. +/// First part of the tuple tells Sputnik how the execution went (success or failure +/// and in what way). Second part tells Sputnik the return data if any. +type CallOutcome = (ExitReason, Vec); + +// Will be used to check precondition before executing a call or a create +pub enum Precondition { + PassPrecondition, + PreconditionErr(ExitReason), + EthereumErr(EthereumError), +} + +/// The result of creating a contract as expected by the SputnikVM EVM implementation. +/// First part of the triple is the execution outcome - same as for normal contract +/// execution. Second part is the address of the newly created contract, if one was +/// created. Last part is the return value, which is required by Sputnik, but it is +/// always an empty vector when this type is used for create outcome. +/// +/// Beware that this type is sometimes used as outcome of a _call_. This is simply to +/// be able to use the `end_xxx_transaction` functions for both contract -create and +/// -call. In this case, the last element of the triple can be non-empty, and the +/// address will be `None`. +pub(crate) type CreateOutcome = (ExitReason, Option, Vec); + +/// Wrap ethereum errors in the SputnikVM errors +/// +/// This function wraps critical errors that indicate something is wrong +/// with the kernel or rollup node into errors that can be passed on to +/// SputnikVM execution. This is needed if an error occurs in a callback +/// called by SputnikVM. +fn ethereum_error_to_exit_reason(exit_reason: &EthereumError) -> ExitReason { + match exit_reason { + EthereumError::EthereumAccountError(AccountStorageError::NonceOverflow) => { + ExitReason::Error(ExitError::MaxNonce) + } + _ => ExitReason::Fatal(ExitFatal::Other(Cow::from(format!("{:?}", exit_reason)))), + } +} + +fn ethereum_error_to_execution_result(error: &EthereumError) -> ExecutionResult { + match error { + EthereumError::EthereumAccountError(AccountStorageError::NonceOverflow) => { + ExecutionResult::Error(ExitError::MaxNonce) + } + _ => ExecutionResult::FatalError(ExitFatal::Other(Cow::from(format!( + "{:?}", + error + )))), + } +} + +pub enum TransferExitReason { + Returned, + OutOfFund, +} + +#[cfg(feature = "benchmark-opcodes")] +mod benchmarks { + + use super::*; + + /// These values encodes the result of an evaluation step of the virtual + /// machine. They can be used to filter some data that can be seen as non + /// conclusive or irrelevant for the ticks model, or simply for data + /// analysis. + const STEP_CONTINUE: u8 = 0; + const SUCCEED_STOP: u8 = 1; + const SUCCEED_RETURN: u8 = 2; + const SUCCEED_SUICIDE: u8 = 3; + const EXIT_ERROR: u8 = 4; + const EXIT_REVERT: u8 = 5; + const EXIT_FATAL: u8 = 6; + const TRAP: u8 = 7; + + #[inline(always)] + fn step_exit_reason(capture: &Result<(), Capture>) -> u8 { + match capture { + Ok(()) => STEP_CONTINUE, + Err(Capture::Exit(ExitReason::Succeed(ExitSucceed::Stopped))) => SUCCEED_STOP, + Err(Capture::Exit(ExitReason::Succeed(ExitSucceed::Returned))) => { + SUCCEED_RETURN + } + Err(Capture::Exit(ExitReason::Succeed(ExitSucceed::Suicided))) => { + SUCCEED_SUICIDE + } + Err(Capture::Exit(ExitReason::Error(_))) => EXIT_ERROR, + Err(Capture::Exit(ExitReason::Revert(_))) => EXIT_REVERT, + Err(Capture::Exit(ExitReason::Fatal(_))) => EXIT_FATAL, + Err(Capture::Trap(_)) => TRAP, + } + } + + // About the two `static mut` below and their usage + // + // Low key optimisation to avoid the formatting: we know that the data are + // always 1 byte for the opcode, 8 bytes pour the gas (u64), 1 byte for the + // step exit reason. The messages are preallocated and updated with the + // correct values each time they are called. It avoid using the formatting. + // The overhead of formatting is significative. + + // The start section for the opcodes expects a single byte which is the + // current opcode. + static mut START_OPCODE_SECTION_MSG: [u8; 35] = + *b"__wasm_debugger__::start_section(\0)"; + + // The start section for the precompiles expects the address of the + // contract (20 bytes) and the size of the data (4 bytes). + static mut START_PRECOMPILE_SECTION_MSG: [u8; 58] = + *b"__wasm_debugger__::start_section(\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0\0)"; + + #[inline(always)] + pub fn start_opcode_section(host: &mut Host, opcode: &Opcode) { + unsafe { + START_OPCODE_SECTION_MSG[33] = opcode.as_u8(); + host.write_debug(core::str::from_utf8_unchecked(&START_OPCODE_SECTION_MSG)); + } + } + + #[inline(always)] + pub fn start_precompile_section( + host: &mut Host, + address: H160, + input: &Vec, + ) { + unsafe { + START_PRECOMPILE_SECTION_MSG[33..53].copy_from_slice(address.as_bytes()); + START_PRECOMPILE_SECTION_MSG[53..57] + .copy_from_slice(&input.len().to_be_bytes()); + host.write_debug(core::str::from_utf8_unchecked( + &START_PRECOMPILE_SECTION_MSG, + )); + } + } + + // The value of the ending sections are: + // - 8 bytes for the gas, in little endian + // - 1 byte that describes the continuation of the evaluation: it either + // continues to the next opcode (`STEP_CONTINUE`) or stops for a given + // reason, this reason being encoded in a byte. These values are described + // at the beginning of the `benchmarks` module. + static mut END_OPCODE_SECTION_MSG: [u8; 41] = + *b"__wasm_debugger__::end_section(\0\0\0\0\0\0\0\0\0)"; + + static mut END_PRECOMPILE_SECTION_MSG: [u8; 32] = + *b"__wasm_debugger__::end_section()"; + + #[inline(always)] + pub fn end_opcode_section( + host: &mut Host, + gas: u64, + step_result: &Result<(), Capture>, + ) { + unsafe { + END_OPCODE_SECTION_MSG[31..39].copy_from_slice(&gas.to_le_bytes()); + END_OPCODE_SECTION_MSG[39] = step_exit_reason(step_result); + host.write_debug(core::str::from_utf8_unchecked(&END_OPCODE_SECTION_MSG)); + } + } + + #[inline(always)] + pub fn end_precompile_section(host: &mut Host) { + unsafe { + host.write_debug(core::str::from_utf8_unchecked(&END_PRECOMPILE_SECTION_MSG)); + } + } +} + +#[derive(Eq, Hash, PartialEq, Clone, Copy, Debug)] +pub struct StorageKey { + pub address: H160, + pub index: H256, +} + +#[derive(Clone, Copy)] +pub enum CacheStorageValue { + Read(StorageValue), + Write(H256), +} + +impl CacheStorageValue { + pub fn h256(&self) -> H256 { + match self { + CacheStorageValue::Read(storage_value) => storage_value.h256(), + CacheStorageValue::Write(storage_value) => *storage_value, + } + } +} + +/// The layer cache is associating a storage slot which is an +/// address and an index (StorageKey) to a value (CacheStorageValue). +pub type LayerCache = HashMap; + +/// The storage cache is associating at each layer (usize) its +/// own cache (LayerCache). For each slot that is modified or +/// read during a call it will be added to the cache in its own +/// layer. +// NB: The cache is implicitly bounded in memory thanks to the +// gas limit, because at most we can do 300_000 different SSTORE +// which costs 100 gas (300_000 Ă— 100 = 30M gas). +// In memory it means we take at most: +// 300_000 Ă— 32B = 9_600_000B = 9.6MB +pub type StorageCache = HashMap; + +/// The implementation of the SputnikVM [Handler] trait +pub struct EvmHandler<'a, Host: Runtime> { + /// The host + pub host: &'a mut Host, + /// The ethereum accounts storage + evm_account_storage: &'a mut EthereumAccountStorage, + /// The original caller initiating the toplevel transaction + origin: H160, + /// The constants for the current block + pub block: &'a BlockConstants, + /// The precompiled functions + precompiles: &'a dyn PrecompileSet, + /// The configuration, eg, London or Frontier for execution + config: &'a Config, + /// The contexts associated with transaction(s) currently in + /// progress + transaction_data: Vec>, + /// Estimated number of ticks remaining for the current run + pub ticks_allocated: u64, + /// Estimated ticks spent for the execution of the current transaction, + /// according to the ticks per gas per opcode model + pub estimated_ticks_used: u64, + /// The effective gas price of the current transaction + effective_gas_price: U256, + /// Whether warm/cold storage and address access is enabled + /// If not, all access are considered warm + pub enable_warm_cold_access: bool, + /// Tracer configuration for debugging. + tracer: Option, + /// Storage cache during a given execution. + /// NB: See `StorageCache`'s documentation for more information. + storage_cache: StorageCache, + /// The original storage cache has its own cache because its unrelated + /// to the VM, its used by an internal mechanism of Sputnik to quickly + /// access storage slots before any transaction happens. + /// See: `fn original_storage`. + original_storage_cache: LayerCache, + /// Reentrancy guard prevents circular calls to impure precompiles + reentrancy_guard: ReentrancyGuard, +} + +impl<'a, Host: Runtime> EvmHandler<'a, Host> { + /// Create a new handler to suit a new, initial EVM call context + #[allow(clippy::too_many_arguments)] + pub fn new( + host: &'a mut Host, + evm_account_storage: &'a mut EthereumAccountStorage, + origin: H160, + block: &'a BlockConstants, + config: &'a Config, + precompiles: &'a dyn PrecompileSet, + ticks_allocated: u64, + effective_gas_price: U256, + enable_warm_cold_access: bool, + tracer: Option, + ) -> Self { + Self { + host, + evm_account_storage, + origin, + block, + config, + precompiles, + transaction_data: vec![], + ticks_allocated, + estimated_ticks_used: 0, + effective_gas_price, + enable_warm_cold_access, + tracer, + storage_cache: HashMap::with_capacity(10), + original_storage_cache: HashMap::with_capacity(10), + reentrancy_guard: ReentrancyGuard::new(vec![ + WITHDRAWAL_ADDRESS, + FA_BRIDGE_PRECOMPILE_ADDRESS, + ]), + } + } + + /// Get the total amount of gas used for the duration of the current + /// transaction. + pub fn gas_used(&self) -> u64 { + self.transaction_data + .last() + .map(|layer| { + layer + .gasometer + .as_ref() + .map(|g| g.total_used_gas()) + .unwrap_or_default() + }) + .unwrap_or_default() + } + + /// Get the amount of gas still available for the current transaction. + pub fn gas_remaining(&self) -> u64 { + self.transaction_data + .last() + .map(|layer| { + layer + .gasometer + .as_ref() + .map(|g| g.gas()) + .unwrap_or_default() + }) + .unwrap_or_default() + } + + /// Record the cost of a static-cost opcode + pub fn record_cost(&mut self, cost: u64) -> Result<(), ExitError> { + let Some(layer) = self.transaction_data.last_mut() else { + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); + }; + + layer + .gasometer + .as_mut() + .map(|gasometer| gasometer.record_cost(cost)) + .unwrap_or(Ok(())) + } + + /// Record code deposit. Pay per byte for a CREATE operation + pub fn record_deposit(&mut self, len: usize) -> Result<(), ExitError> { + let Some(layer) = self.transaction_data.last_mut() else { + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); + }; + + layer + .gasometer + .as_mut() + .map(|gasometer| gasometer.record_deposit(len)) + .unwrap_or(Ok(())) + } + + /// Record the cost of a dynamic-cost opcode + fn record_dynamic_cost( + &mut self, + cost: GasCost, + memory_cost: Option, + ) -> Result<(), ExitError> { + let Some(layer) = self.transaction_data.last_mut() else { + return Err(ExitError::Other(Cow::from( + "Recording cost, but there is no transaction in progress", + ))); + }; + + layer + .gasometer + .as_mut() + .map(|gasometer| gasometer.record_dynamic_cost(cost, memory_cost)) + .unwrap_or(Ok(())) + } + + /// Record the refund of a contract call. This differs from a storage + /// operation refund in that the refunded gas can be used again by the + /// same transaction. Function name reflects the SputnikVM name used to + /// implement this functionality. + fn record_stipend(&mut self, stipend: u64) -> Result<(), EthereumError> { + let Some(layer) = self.transaction_data.last_mut() else { + return Err(EthereumError::InconsistentTransactionStack( + self.transaction_data.len(), + false, + false, + )); + }; + + layer + .gasometer + .as_mut() + .map(|gasometer| gasometer.record_stipend(stipend)) + .unwrap_or(Ok(())) + .map_err(|_| { + EthereumError::InconsistentState(Cow::from( + "Recording a stipend returned an error", + )) + }) + } + + fn get_word_size(&self, init_code: &[u8]) -> u64 { + // ceil(len(init_code) / 32) + (init_code.len() as u64 + 31) / 32 + } + + fn record_init_code_cost(&mut self, init_code: &[u8]) -> Result<(), ExitError> { + // As per EIP-3860: + // > We define init_code_cost to equal INITCODE_WORD_COST * get_word_size(init_code). + // where INITCODE_WORD_COST is 2. + let init_code_cost = 2 * self.get_word_size(init_code); + self.record_cost(init_code_cost) + } + + /// Mark a location in durable storage as _hot_ for the purpose of calculating + /// cost of SSTORE and SLOAD. Return type chosen for compatibility with the + /// SputnikVM functions that need to call this function. + fn mark_storage_as_hot( + &mut self, + address: H160, + index: H256, + ) -> Result<(), ExitError> { + if !self.enable_warm_cold_access { + return Ok(()); + } + + match self.transaction_data.last_mut() { + Some(layer) => { + layer.accessed_storage_keys.insert_storage(address, index); + Ok(()) + } + None => Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for mark_storage_as_hot", + ))), + } + } + + /// Mark an address as _hot_ for the purpose of calculating + /// cost of *CALL, BALANCE, EXT* and SELFDESTRUCT. Return type chosen for compatibility with the + /// SputnikVM functions that need to call this function. + fn mark_address_as_hot(&mut self, address: H160) -> Result<(), ExitError> { + if !self.enable_warm_cold_access { + return Ok(()); + } + + match self.transaction_data.last_mut() { + Some(layer) => { + layer.accessed_storage_keys.insert_address(address); + Ok(()) + } + None => Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for mark_address_as_hot", + ))), + } + } + + /// Check if some location in durable storage is hot + fn is_storage_hot(&self, address: H160, index: H256) -> Result { + let Some(layer) = self.transaction_data.last() else { + return Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for is_storage_hot", + ))); + }; + + Ok(layer.accessed_storage_keys.contains_storage(address, index)) + } + + /// Check if address is hot + fn is_address_hot(&self, address: H160) -> Result { + let Some(layer) = self.transaction_data.last() else { + return Err(ExitError::Other(Cow::from( + "Invalid transaction data stack for is_address_hot", + ))); + }; + + Ok(layer.accessed_storage_keys.contains_address(address)) + } + + /// Check if an address has either a nonzero nonce, or a nonzero code length, i.e., if the address exists. + fn is_colliding(&mut self, address: H160) -> Result { + let Some(account) = self.get_account(address)? else { + return Ok(false); + }; + + let has_code = account + .code_size(self.borrow_host()) + .map(|s| s != U256::zero())?; + let non_zero_nonce = account.nonce(self.borrow_host()).map(|s| s != 0)?; + + Ok(has_code || non_zero_nonce) + } + + /// Returns true if there is a static transaction in progress, otherwise + /// return false. + fn is_static(&self) -> bool { + self.transaction_data + .last() + .map(|data| data.is_static) + .unwrap_or(false) + } + + /// Record the base fee part of the transaction cost. We need the SputnikVM + /// error code in case this goes wrong, so that's what we return. + fn record_base_gas_cost( + &mut self, + is_create: bool, + data: &[u8], + ) -> Result<(), ExitError> { + let base_cost = if is_create { + self.config.gas_transaction_create + } else { + self.config.gas_transaction_call + }; + + let data_cost: u64 = data + .iter() + .map(|datum| { + if *datum == 0_u8 { + self.config.gas_transaction_zero_data + } else { + self.config.gas_transaction_non_zero_data + } + }) + .sum(); + + self.record_cost(base_cost + data_cost) + } + + /// Add withdrawals to the current transaction layer + pub fn add_withdrawals( + &mut self, + withdrawals: &mut Vec, + ) -> Result<(), EthereumError> { + match self.transaction_data.last_mut() { + Some(layer) => { + layer.withdrawals.try_reserve_exact(withdrawals.len())?; + layer.withdrawals.append(withdrawals); + + Ok(()) + } + None => Err(EthereumError::InconsistentTransactionStack(0, false, false)), + } + } + + /// Add log to the current transaction layer + pub(crate) fn add_log(&mut self, log: Log) -> Result<(), ExitError> { + if let Some(top_data) = self.transaction_data.last_mut() { + top_data.logs.push(log); + Ok(()) + } else { + Err(ExitError::Other(Cow::from("No transaction data for log"))) + } + } + + /// Have the caller account pay for gas. Returns `Ok(true)` if the payment + /// went through; returns `Ok(false)` if `caller` doesn't have the funds. + /// Return `Err(...)` in case something is at fault with durable storage or + /// runtime. + pub fn pre_pay_transactions( + &mut self, + caller: H160, + gas_limit: Option, + effective_gas_price: U256, + ) -> Result { + let Some(gas_limit) = gas_limit else { + return Ok(true); + }; + + let amount = U256::from(gas_limit) + .checked_mul(effective_gas_price) + .ok_or(EthereumError::GasPaymentOverflow)?; + + log!( + self.host, + Debug, + "{caller:?} pays {amount:?} for transaction" + ); + + self.get_or_create_account(caller)? + .balance_remove(self.host, amount) + .map_err(EthereumError::from) + } + + /// Repay unused gas + pub fn repay_gas( + &mut self, + caller: H160, + unused_gas: Option, + effective_gas_price: U256, + ) -> Result<(), EthereumError> { + let Some(unused_gas) = unused_gas else { + return Ok(()); + }; + + let amount = U256::from(unused_gas) + .checked_mul(effective_gas_price) + .ok_or(EthereumError::GasPaymentOverflow)?; + + log!( + self.host, + Debug, + "{caller:?} refunded {amount:?} for transaction" + ); + + self.get_or_create_account(caller)? + .balance_add(self.host, amount) + .map_err(EthereumError::from) + } + + /// Account for the estimated ticks spent during the execution of the given opcode + pub fn account_for_ticks( + &mut self, + opcode: &Opcode, + gas: u64, + ) -> Result<(), EthereumError> { + self.estimated_ticks_used += tick_model_opcodes::ticks(opcode, gas); + if self.estimated_ticks_used > self.ticks_allocated { + Err(EthereumError::OutOfTicks) + } else { + Ok(()) + } + } + + /// Clear the entire storage located at [address]. + pub fn clear_storage(&mut self, address: H160) -> Result<(), EthereumError> { + if let Some(account) = self.get_account(address)? { + account.clear_storage(self.host)? + }; + Ok(()) + } + + /// Prepare trace info if needed + fn prepare_trace( + &mut self, + runtime: &evm::Runtime, + opcode: &Option, + ) -> Option { + if let ( + Some(StructLogger(StructLoggerInput { + transaction_hash: _, + config, + })), + Some(opcode), + ) = (&self.tracer, opcode) + { + let opcode = opcode.as_u8(); + let pc: u64 = runtime + .machine() + .trace_position() + .ok()? + .try_into() + .unwrap_or_default(); + let gas = self.gas_remaining(); + let depth: u16 = self.transaction_data.len().try_into().unwrap_or_default(); + let stack = (!config.disable_stack) + .then(|| runtime.machine().stack().data().to_owned()); + let return_data = config + .enable_return_data + .then(|| runtime.machine().return_value()); + let memory = config + .enable_memory + .then(|| runtime.machine().memory().data()[..].to_vec()); + let storage = (!config.disable_storage).then(|| { + let mut flat_storage = vec![]; + self.storage_cache + .clone() + .into_iter() + .flat_map(|(_, layer_cache)| layer_cache) + .for_each(|(StorageKey { address, index }, value)| { + flat_storage.push(StorageMapItem { + address, + index, + value: value.h256(), + }); + }); + flat_storage + }); + + Some(StructLog::prepare( + pc, + opcode, + gas, + depth, + stack, + return_data, + memory, + storage, + )) + } else { + None + } + } + + fn complete_and_store_trace( + &mut self, + trace: Option, + gas_cost: u64, + step_result: &Result<(), Capture>>>, + ) -> Result<(), EthereumError> { + if let ( + Some(StructLogger(StructLoggerInput { + transaction_hash, + config: _, + })), + Some(struct_log), + ) = (self.tracer, trace) + { + // TODO: https://gitlab.com/tezos/tezos/-/issues/7437 + // For error, find the appropriate value to return for tracing. + // The following value is kind of a placeholder. + let error = if let Err(Capture::Exit(reason)) = &step_result { + match &reason { + ExitReason::Error(exit) => { + Some(format!("{:?}", exit).as_bytes().to_vec()) + } + ExitReason::Fatal(exit) => { + Some(format!("{:?}", exit).as_bytes().to_vec()) + } + _ => None, + } + } else { + None + }; + tracer::store_struct_log( + self.host, + struct_log.finish(gas_cost, error), + &transaction_hash, + )?; + } + Ok(()) + } + + /// Execute a SputnikVM run with this handler + /// + // DO NOT RENAME: function name is used during benchmark + // Never inlined when the kernel is compiled for benchmarks, to ensure the + // function is visible in the profiling results. + #[cfg_attr(feature = "benchmark", inline(never))] + fn execute( + &mut self, + runtime: &mut evm::Runtime, + ) -> Result { + loop { + // This decomposition allows both benchmarking the ticks per gas + // consumption of opcode and implement the tick model at the opcode + // level. At the end of each step if the kernel takes more than the + // allocated ticks the transaction is marked as failed. + let opcode = runtime.machine().inspect().map(|p| p.0); + let trace = self.prepare_trace(runtime, &opcode); + + #[cfg(feature = "benchmark-opcodes")] + if let Some(opcode) = opcode { + benchmarks::start_opcode_section(self.host, &opcode); + } + + // For now, these variables capturing the gas one will be marked + // unused without benchmarking, but they will be used during the + // tick accounting. + #[cfg_attr(not(feature = "benchmark-opcodes"), allow(unused_variables))] + let gas_before = self.gas_used(); + + let step_result = runtime.step(self); + + #[cfg_attr(not(feature = "benchmark-opcodes"), allow(unused_variables))] + let gas_after = self.gas_used(); + + let gas_cost = gas_after - gas_before; + + if let Some(opcode) = opcode { + self.account_for_ticks(&opcode, gas_cost)?; + #[cfg(feature = "benchmark-opcodes")] + benchmarks::end_opcode_section(self.host, gas_cost, &step_result); + }; + + self.complete_and_store_trace(trace, gas_cost, &step_result)?; + + match step_result { + Ok(()) => (), + Err(Capture::Exit(reason)) => { + return Ok(reason); + } + Err(Capture::Trap(_)) => { + return Err(EthereumError::InternalTrapError); + } + } + } + } + + fn create_address( + &mut self, + scheme: CreateScheme, + ) -> Result { + match scheme { + CreateScheme::Create2 { + caller, + code_hash, + salt, + } => { + let mut hasher = Keccak256::new(); + hasher.update([0xff]); + hasher.update(caller); + hasher.update(salt); + hasher.update(code_hash); + Ok(H256::from_slice(hasher.finalize().as_slice()).into()) + } + CreateScheme::Legacy { caller } => { + let nonce = self.get_nonce(caller)?; + Ok(create_address_legacy(&caller, &nonce)) + } + CreateScheme::Fixed(address) => Ok(address), + } + } + + /// Execute a transfer between two accounts + /// + /// In case the transfer succeeds, the function returns + /// `Ok(ExitReason::Succeed(ExitSucceed::Returned))`. In case the + /// transaction fails, but execution doesn't encounter non-contract or + /// -account errors, it returns `Ok(ExitReason::Error(err))`, where `err` + /// indicates what went wrong (insufficient balance, etc.). In case of + /// critical errors in the rollup node or kernel, an `Err(err)` is returned, + /// where `err` indicates what went wrong, eg, a storage error. + fn execute_transfer( + &mut self, + from: H160, + to: H160, + value: U256, + ) -> Result { + log!( + self.host, + Debug, + "Executing a transfer from {} to {} of {}", + from, + to, + value + ); + + if value == U256::zero() { + // Nothing to transfer so succeeds by default + Ok(TransferExitReason::Returned) + } else if let Some(mut from_account) = self.get_account(from)? { + let mut to_account = self.get_or_create_account(to)?; + + if from_account.balance_remove(self.host, value)? { + to_account + .balance_add(self.host, value) + .map_err(EthereumError::from)?; + Ok(TransferExitReason::Returned) + } else { + log!( + self.host, + Debug, + "Failed transfer due to insufficient funds, value: {:?}, from: {:?}, to: {:?}", + value, + from_account, + to + ); + + Ok(TransferExitReason::OutOfFund) + } + } else { + log!(self.host, Debug, "'from' account {:?} is empty", from); + // Accounts of zero balance by default, so this must be + // an underflow. + Ok(TransferExitReason::OutOfFund) + } + } + + // Stack depth is the number of internal call that happened in the EVM + // It's just the number of transaction minus the initial one + // NB: Different from `stack_depth` in `src/kernel_sdk/storage/src/storage.rs` + fn stack_depth(&self) -> usize { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + number_of_tx_layer.checked_sub(1).unwrap_or_default() + } + + fn has_enough_fund(&self, from: H160, value: &U256) -> Result { + if value.is_zero() { + Ok(true) + } else if let Some(from_account) = self.get_account(from)? { + let balance = from_account + .balance(self.host) + .map_err(EthereumError::from)?; + let enough_balance = &balance >= value; + if !enough_balance { + log!( + self.host, + Debug, + "Insufficient funds, balance of {:?} is {:?} but needs at least {:?}", + from, + balance, + value + ); + } + Ok(enough_balance) + } else { + log!(self.host, Debug, "'from' account {:?} is empty", from); + // Accounts of zero balance by default, so this must be + // an underflow. + Ok(false) + } + } + + fn end_create( + &mut self, + runtime: evm::Runtime, + creation_result: Result, + address: H160, + ) -> Result { + match creation_result { + Ok(sub_context_result @ ExitReason::Succeed(ExitSucceed::Suicided)) => { + Ok((sub_context_result, Some(address), vec![])) + } + Ok(sub_context_result @ ExitReason::Succeed(_)) => { + let code_out = runtime.machine().return_value(); + + if code_out.first() == Some(&0xef) { + // EIP-3541: see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3541.md + return Ok(( + ExitReason::Error(ExitError::InvalidCode(Opcode(0xef))), + None, + vec![], + )); + } + + // We check that the maximum allowed code size as specified by EIP-170 can not + // be reached. + if let Some(create_contract_limit) = self.config.create_contract_limit { + if code_out.len() > create_contract_limit { + // EIP-170: see https://github.com/ethereum/EIPs/blob/master/EIPS/eip-170.md + return Ok(( + ExitReason::Error(ExitError::CreateContractLimit), + None, + vec![], + )); + } + } + + if let Err(err) = self.record_deposit(code_out.len()) { + return Ok((ExitReason::Error(err), None, vec![])); + } + + self.set_contract_code(address, &code_out)?; + + Ok((sub_context_result, Some(address), code_out)) + } + Ok(sub_context_result @ ExitReason::Revert(_)) => { + // EIP-140: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-140.md + // In case of a REVERT in a context of a CREATE, CREATE2, the error message + // is available in the returndata buffer + Ok((sub_context_result, None, runtime.machine().return_value())) + } + // Since the creation fails, return address 0 (`None` in our case) (https://www.evm.codes/#f0?fork=shanghai) + Ok(create_err @ ExitReason::Error(_)) => Ok((create_err, None, vec![])), + Ok(create_err @ ExitReason::Fatal(_)) => Ok((create_err, None, vec![])), + Err(err) => Err(err), + } + } + + pub fn can_begin_inter_transaction_call_stack(&self) -> bool { + self.stack_depth() < self.config.call_stack_limit + } + + // Sub function to determine if the `caller` can create a new internal transaction. + // According to the Ethereum yellow paper (p.37) for `CALL`, `CREATE`, ... instructions that + // creates a new substate, it must always check that there are no `OutOfFund` or `CallTooDeep` + fn can_begin_inter_transaction(&self, caller: H160, value: &U256) -> Precondition { + // This check SHOULD be called outside of `begin_inter` and `end_inter`, this way + // we can reproduce the exact same check on the stack from the Ethereum yellow paper (p.37). + match ( + self.has_enough_fund(caller, value), + self.can_begin_inter_transaction_call_stack(), + ) { + (Ok(true), true) => Precondition::PassPrecondition, + (Ok(false), _) => { + Precondition::PreconditionErr(ExitReason::Error(ExitError::OutOfFund)) + } + (Ok(_), false) => { + Precondition::PreconditionErr(ExitReason::Error(ExitError::CallTooDeep)) + } + (Err(err), _) => Precondition::EthereumErr(err), + } + } + + // Sub function to handle the collision part, we pinpoint two ways of colliding: + // - the contract already exists + // - it's been marked as `deleted` within the same transaction + fn contract_will_collide(&mut self, address: H160) -> Precondition { + if self.deleted(address) { + // The contract has been deleted, so the address is empty. + // We are trying to re-create the same contract that was deleted at + // the same transaction level: this is not allowed. + // TODO/NB: https://gitlab.com/tezos/tezos/-/issues/6783 + // This behaviour is appropriate to <=Shanghai configuration. + // In the upcoming Cancun fork, the semantic of this behaviour will change. + Precondition::PreconditionErr(ExitReason::Error(ExitError::CreateCollision)) + } else { + // TODO: https://gitlab.com/tezos/tezos/-/issues/6716 + // Create collision and failed transfers should use up all the gas + match self.is_colliding(address) { + Ok(false) => Precondition::PassPrecondition, + Ok(true) => { + log!( + self.host, + Debug, + "Failed to create contract at {:?}. Address is non-empty", + address + ); + Precondition::PreconditionErr(ExitReason::Error( + ExitError::CreateCollision, + )) + } + Err(collide_err) => Precondition::EthereumErr(collide_err), + } + } + } + + /// Create a contract + /// + /// Performs the actual contract creation for both transactions initiated + /// by external accounts and contract creation initiated through contract + /// execution. + /// + /// In the specific case where this function is called via the CREATE opcode, + /// it needs to bump the nonce. If it's a transaction initated by external + /// accounts, the nonce must be bumped by the caller. + fn execute_create( + &mut self, + caller: H160, + value: U256, + initial_code: Vec, + address: H160, + ) -> Result { + log!(self.host, Debug, "Executing a contract create"); + + // We check that the maximum allowed init code size as specified by EIP-3860 + // can not be reached. + if let Some(max_initcode_size) = self.config.max_initcode_size { + if initial_code.len() > max_initcode_size { + return Ok(( + ExitReason::Error(ExitError::CreateContractLimit), + None, + vec![], + )); + } + } + + if let Err(err) = self.record_init_code_cost(&initial_code) { + log!( + self.host, + Debug, + "{:?}: Not enough gas for create. Cannot record init code cost.", + err + ); + + return Ok((ExitReason::Error(ExitError::OutOfGas), None, vec![])); + } + + let context = Context { + address, + caller, + apparent_value: value, + }; + + let mut runtime = evm::Runtime::new( + Rc::new(initial_code), + Rc::new(Vec::new()), + context, + self.config.stack_limit, + self.config.memory_limit, + ); + + // Execute if there is no collision + let creation_result = match self.contract_will_collide(address) { + Precondition::PassPrecondition => { + match self.execute_transfer(caller, address, value) { + Ok(TransferExitReason::Returned) => { + match self.increment_nonce(address) { + Ok(()) => { + self.clear_storage(address)?; + self.execute(&mut runtime) + } + Err(eth_err) => Err(eth_err), + } + } + Ok(TransferExitReason::OutOfFund) => { + Ok(ExitReason::Error(ExitError::OutOfFund)) + } + Err(err) => Err(err), + } + } + Precondition::PreconditionErr(collision) => Ok(collision), + Precondition::EthereumErr(err) => Err(err), + }; + + self.end_create(runtime, creation_result, address) + } + + /// Call a contract + /// + /// Perform the actual contract execution - works both for executing an + /// Ethereum transaction as initiated by an external account or as aresult + /// of any of the -CALL instructions. + /// + /// The outcome is encoded as a SputnikVM _Create_ outcome for easy transaction + /// handling. The new address "field" in the triple is always `None`. + #[allow(clippy::too_many_arguments)] + pub(crate) fn execute_call( + &mut self, + address: H160, + transfer: Option, + input: Vec, + transaction_context: TransactionContext, + ) -> Result { + let stack_depth = self.stack_depth(); + log!( + self.host, + Debug, + "Executing contract call on contract {} at depth: {}", + address, + stack_depth + ); + + if let Some(ref transfer) = transfer { + match self.execute_transfer( + transfer.source, + transfer.target, + transfer.value, + )? { + TransferExitReason::OutOfFund => { + return Ok((ExitReason::Error(ExitError::OutOfFund), None, vec![])) + } + TransferExitReason::Returned => (), // Otherwise result is ok and we do nothing and continue + } + } + #[cfg(feature = "benchmark-opcodes")] + benchmarks::start_precompile_section(self.host, address, &input); + + if let Err(err) = self.reentrancy_guard.begin_precompile_call(&address) { + return Ok(( + ExitReason::Fatal(evm::ExitFatal::CallErrorAsFatal(err)), + None, + vec![], + )); + } + + let precompile_execution_result = self.precompiles.execute( + self, + address, + &input, + &transaction_context.context, + self.is_static(), + transfer, + ); + + self.reentrancy_guard.end_precompile_call(); + + #[cfg(feature = "benchmark-opcodes")] + benchmarks::end_precompile_section(self.host); + + if let Some(precompile_result) = precompile_execution_result { + match precompile_result { + Ok(mut outcome) => { + self.add_withdrawals(&mut outcome.withdrawals)?; + self.estimated_ticks_used += outcome.estimated_ticks; + Ok((outcome.exit_status, None, outcome.output)) + } + Err(err) => Err(err), + } + } else { + let code = self.code(address); + + let mut runtime = evm::Runtime::new( + Rc::new(code), + Rc::new(input), + transaction_context.context, + self.config.stack_limit, + self.config.memory_limit, + ); + + let result = self.execute(&mut runtime)?; + + return Ok((result, None, runtime.machine().return_value())); + } + } + + /// Perform a contract call transaction + pub fn call_contract( + &mut self, + caller: H160, + callee: H160, + value: Option, + input: Vec, + gas_limit: Option, + is_static: bool, + ) -> Result { + self.increment_nonce(caller)?; + self.begin_initial_transaction(is_static, gas_limit)?; + + if self.mark_address_as_hot(caller).is_err() { + return Err(EthereumError::InconsistentState(Cow::from( + "Failed to mark caller address as hot", + ))); + } + + if self.mark_address_as_hot(callee).is_err() { + return Err(EthereumError::InconsistentState(Cow::from( + "Failed to mark callee address as hot", + ))); + } + + if let Err(err) = self.record_base_gas_cost(false, &input) { + return self.end_initial_transaction(Ok(( + ExitReason::Error(err), + None, + vec![], + ))); + } + + let result = self.execute_call( + callee, + value.map(|value| Transfer { + source: caller, + target: callee, + value, + }), + input, + TransactionContext::new(caller, callee, value.unwrap_or_default()), + ); + + self.end_initial_transaction(result) + } + + /// Perform a create-contract transaction + pub fn create_contract( + &mut self, + caller: H160, + value: Option, + input: Vec, + gas_limit: Option, + ) -> Result { + let default_create_scheme = CreateScheme::Legacy { caller }; + let address = self.create_address(default_create_scheme)?; + self.increment_nonce(caller)?; + + self.begin_initial_transaction(false, gas_limit)?; + + if self.mark_address_as_hot(caller).is_err() { + return Err(EthereumError::InconsistentState(Cow::from( + "Failed to mark caller address as hot", + ))); + } + + if let Err(err) = self.record_base_gas_cost(true, &input) { + return self.end_initial_transaction(Ok(( + ExitReason::Error(err), + None, + vec![], + ))); + } + + if self.mark_address_as_hot(address).is_err() { + return Err(EthereumError::InconsistentState(Cow::from( + "Failed to mark callee address as hot", + ))); + } + + let result = + self.execute_create(caller, value.unwrap_or_default(), input, address); + + self.end_initial_transaction(result) + } + + pub(crate) fn get_or_create_account( + &self, + address: H160, + ) -> Result { + self.evm_account_storage + .get_or_create( + self.host, + &account_path(&address).map_err(AccountStorageError::from)?, + ) + .map_err(EthereumError::from) + } + + pub(crate) fn get_account( + &self, + address: H160, + ) -> Result, StorageError> { + // Note: if we get an error we cannot report this to SputnikVM as the return types + // for functions that use _this_ function don't support errors. Rather than do + // error handling in all those functions (and those we'll write in the future), we + // do the error handling here. + if let Ok(path) = account_path(&address) { + self.evm_account_storage.get(self.host, &path) + } else { + log!( + self.host, + Debug, + "Failed to get account path for EVM handler get_account" + ); + Ok(None) + } + } + + fn get_original_account( + &self, + address: H160, + ) -> Result, StorageError> { + // Note, there is no way to recover from an error when creating the + // account path. At this point we are being called from SputnikVM and + // it does not allow for this to fail, so we just return None. + if let Ok(path) = account_path(&address) { + self.evm_account_storage.get_original(self.host, &path) + } else { + log!( + self.host, + Debug, + "Failed to get account path for EVM handler get_original_account" + ); + Ok(None) + } + } + + pub fn increment_nonce(&mut self, address: H160) -> Result<(), EthereumError> { + match account_path(&address) { + Ok(path) => { + let mut account = + self.evm_account_storage.get_or_create(self.host, &path)?; + account + .increment_nonce(self.host) + .map_err(EthereumError::from) + } + Err(err) => { + log!( + self.host, + Debug, + "Failed to increment nonce for account {:?}", + address + ); + Err(EthereumError::from(AccountStorageError::from(err))) + } + } + } + + pub fn decrement_nonce(&mut self, address: H160) -> Result<(), EthereumError> { + match account_path(&address) { + Ok(path) => { + let mut account = + self.evm_account_storage.get_or_create(self.host, &path)?; + account + .decrement_nonce(self.host) + .map_err(EthereumError::from) + } + Err(err) => { + log!( + self.host, + Debug, + "Failed to decrement nonce for account {:?}", + address + ); + Err(EthereumError::from(AccountStorageError::from(err))) + } + } + } + + fn set_contract_code( + &mut self, + address: H160, + code: &[u8], + ) -> Result<(), EthereumError> { + self.get_or_create_account(address)? + .set_code(self.host, code) + .map_err(EthereumError::from) + } + + fn get_nonce(&self, address: H160) -> Result { + self.get_account(address)? + .map(|account| account.nonce(self.host)) + .unwrap_or(Ok(0)) + } + + fn reset_balance(&mut self, address: H160) -> Result<(), AccountStorageError> { + match self.get_account(address)? { + Some(mut account) => account.set_balance(self.host, U256::zero()), + None => Err(AccountStorageError::StorageError( + StorageError::InvalidAccountsPath, + )), + } + } + + /// Completely delete an account including nonce, code, and data. This is for + /// contract selfdestruct completion, ie, when contract selfdestructs takes final + /// effect. + fn delete_contract(&mut self, address: H160) -> Result<(), EthereumError> { + log!(self.host, Debug, "Deleting contract at {:?}", address); + + self.evm_account_storage + .delete( + self.host, + &account_path(&address).map_err(AccountStorageError::from)?, + ) + .map_err(EthereumError::from) + } + + /// Borrow a reference to the host - needed for eg precompiled contracts + pub fn borrow_host(&mut self) -> &'_ mut Host { + self.host + } + + /// Begin the first transaction layer + /// + /// This requires that no other transaction is in progress. If there is a + /// transaction in progress, then the function returns an error to report + /// this. + pub(crate) fn begin_initial_transaction( + &mut self, + is_static: bool, + gas_limit: Option, + ) -> Result<(), EthereumError> { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + log!(self.host, Debug, "Begin initial transaction"); + + if number_of_tx_layer > 0 { + log!( + self.host, + Debug, + "Initial transaction when there is already {} transaction", + number_of_tx_layer + ); + + return Err(EthereumError::InconsistentTransactionStack( + number_of_tx_layer, + true, + true, + )); + } + + self.transaction_data.push(TransactionLayerData::new( + self.is_static() || is_static, + gas_limit, + self.config, + AccessRecord::default(), + )); + + self.evm_account_storage + .begin_transaction(self.host) + .map_err(EthereumError::from) + } + + /// Final commit of initial transaction + /// + /// This requires that only one transaction is in progress. Since we should + /// never end in a state with a transaction in progress after we are done + /// executing, such state is the sort of thing that may cause panic. + fn commit_initial_transaction( + &mut self, + result: ExecutionResult, + ) -> Result { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + log!(self.host, Debug, "Committing initial transaction"); + + if number_of_tx_layer != 1 { + log!( + self.host, + Debug, + "Committing initial transaction, but there are {:?} transactions", + number_of_tx_layer + ); + + return Err(EthereumError::InconsistentTransactionStack( + number_of_tx_layer, + true, + false, + )); + } + + if number_of_tx_layer != self.transaction_data.len() { + return Err(EthereumError::InconsistentTransactionData( + number_of_tx_layer, + self.transaction_data.len(), + )); + } + + let gas_used = self.gas_used(); + + if let Some(last_layer) = self.transaction_data.pop() { + for address in last_layer.deleted_contracts.iter() { + if self.delete_contract(*address).is_err() { + log!( + self.host, + Debug, + "Failed to remove deleted address {:?}", + address + ); + } + } + + commit_storage_cache(self, number_of_tx_layer); + + self.evm_account_storage + .commit_transaction(self.host) + .map_err(EthereumError::from)?; + + Ok(ExecutionOutcome { + gas_used, + logs: last_layer.logs, + result, + withdrawals: last_layer.withdrawals, + estimated_ticks_used: self.estimated_ticks_used, + }) + } else { + Err(EthereumError::InconsistentState(Cow::from( + "The transaction data stack is empty when committing the initial transaction", + ))) + } + } + + /// Rollback of initial transaction + /// + /// This requires that only one transaction is in progress. Since we should + /// never end in a state with a transaction in progress after we are done + /// executing, such state is the sort of thing that may cause panic. + fn rollback_initial_transaction( + &mut self, + result: ExecutionResult, + ) -> Result { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + log!(self.host, Debug, "Rolling back the initial transaction"); + + if number_of_tx_layer != 1 { + log!( + self.host, + Debug, + "Rolling back initial transaction, but there are {:?} in progress", + number_of_tx_layer + ); + + return Err(EthereumError::InconsistentTransactionStack( + number_of_tx_layer, + true, + false, + )); + } + + if number_of_tx_layer != self.transaction_data.len() { + return Err(EthereumError::InconsistentTransactionData( + number_of_tx_layer, + self.transaction_data.len(), + )); + } + + let gas_used = self.gas_used(); + + self.evm_account_storage + .rollback_transaction(self.host) + .map_err(EthereumError::from)?; + + let _ = self.transaction_data.pop(); + self.storage_cache.clear(); + self.original_storage_cache.clear(); + + Ok(ExecutionOutcome { + gas_used, + logs: vec![], + result, + withdrawals: vec![], + estimated_ticks_used: self.estimated_ticks_used, + }) + } + + fn flush_storage_cache(&mut self) -> Result<(), EthereumError> { + let storage_cache_size = self.storage_cache.len(); + if storage_cache_size > 1 { + log!( + self.host, + Fatal, + "The storage cache size is {storage_cache_size} when \ + flushing. It is inconsistent with the transaction stack. \ + The EVM is most likely broken." + ); + return Err(EthereumError::InconsistentTransactionStack( + storage_cache_size, + false, + false, + )); + } + + if let Some(cache) = self.storage_cache.get(&0) { + for (StorageKey { address, index }, value) in cache.iter() { + if let CacheStorageValue::Write(value) = value { + let mut account = self.get_or_create_account(*address)?; + account.set_storage(self.host, index, value)?; + } + } + } + Ok(()) + } + + /// End the initial transaction with either a commit or a rollback. The + /// outcome depends on the execution result given. + pub(crate) fn end_initial_transaction( + &mut self, + execution_result: Result, + ) -> Result { + match execution_result { + Ok((ExitReason::Succeed(r), new_address, result)) => { + log!( + self.host, + Debug, + "The initial transaction ended with success: {:?}", + r + ); + + let commit_result = self.commit_initial_transaction( + if let Some(new_address) = new_address { + ExecutionResult::ContractDeployed(new_address, result) + } else { + ExecutionResult::CallSucceeded(r, result) + }, + ); + + // We flush the storage's cache into the durable storage + // by actually writing in it. + self.flush_storage_cache()?; + + commit_result + } + Ok((ExitReason::Revert(ExitRevert::Reverted), _, result)) => { + self.rollback_initial_transaction(ExecutionResult::CallReverted(result)) + } + Ok((ExitReason::Error(error), _, _)) => { + log!( + self.host, + Debug, + "The initial transaction ended with an error: {:?}", + error + ); + + self.rollback_initial_transaction(ExecutionResult::Error(error)) + } + Ok((ExitReason::Fatal(ExitFatal::Other(cow_str)), _, _)) => { + self.rollback_initial_transaction(ExecutionResult::FatalError( + ExitFatal::Other(cow_str.clone()), + ))?; + Err(EthereumError::WrappedError(cow_str)) + } + Ok((ExitReason::Fatal(fatal_error), _, _)) => { + log!( + self.host, + Debug, + "The initial transaction ended with a fatal error: {:?}", + fatal_error + ); + + self.rollback_initial_transaction(ExecutionResult::FatalError( + fatal_error, + )) + } + Err(EthereumError::OutOfTicks) => { + log!( + self.host, + Debug, + "The initial transaction exhausted the allocated ticks." + ); + + self.rollback_initial_transaction(ExecutionResult::OutOfTicks) + } + Err(err) => { + log!( + self.host, + Debug, + "The initial transaction ended with an Ethereum error: {:?}", + err + ); + + self.rollback_initial_transaction(ethereum_error_to_execution_result( + &err, + ))?; + Err(err) + } + } + } + + /// Begin an intermediate transaction + pub fn begin_inter_transaction( + &mut self, + is_static: bool, + gas_limit: Option, + ) -> Result<(), EthereumError> { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + log!( + self.host, + Debug, + "Begin transaction from {} at transaction depth: {}", + self.origin(), + self.stack_depth() + ); + + if number_of_tx_layer == 0 { + return Err(EthereumError::InconsistentTransactionStack(0, false, true)); + } + + let Some(current_top) = self.transaction_data.last() else { + return Err(EthereumError::InconsistentTransactionStack(0, false, true)); + }; + + let accessed_storage_keys = current_top.accessed_storage_keys.clone(); + + self.transaction_data.push(TransactionLayerData::new( + self.is_static() || is_static, + gas_limit, + self.config, + accessed_storage_keys, + )); + + self.evm_account_storage + .begin_transaction(self.host) + .map_err(EthereumError::from) + } + + /// Commit an intermediate transaction + fn commit_inter_transaction(&mut self) -> Result<(), EthereumError> { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + + if number_of_tx_layer < 2 { + return Err(EthereumError::InconsistentTransactionStack( + number_of_tx_layer, + false, + false, + )); + } + + log!( + self.host, + Debug, + "Commit transaction at transaction depth: {}", + self.stack_depth() + ); + + let gas_remaining = self.gas_remaining(); + + commit_storage_cache(self, number_of_tx_layer); + + self.evm_account_storage + .commit_transaction(self.host) + .map_err(EthereumError::from)?; + + if let Some(mut committed_data) = self.transaction_data.pop() { + if let Some(top_layer) = self.transaction_data.last_mut() { + top_layer + .logs + .try_reserve_exact(committed_data.logs.len())?; + top_layer.logs.append(&mut committed_data.logs); + + top_layer + .withdrawals + .try_reserve_exact(committed_data.withdrawals.len())?; + top_layer + .withdrawals + .append(&mut committed_data.withdrawals); + + top_layer + .deleted_contracts + .extend(committed_data.deleted_contracts); + top_layer.accessed_storage_keys = committed_data.accessed_storage_keys; + + self.record_stipend(gas_remaining)?; + + Ok(()) + } else { + Err(EthereumError::InconsistentState(Cow::from( + "The transaction data stack is empty", + ))) + } + } else { + Err(EthereumError::InconsistentState(Cow::from( + "The transaction data stack is empty at commit", + ))) + } + } + + /// Rollback an intermediate transaction + fn rollback_inter_transaction( + &mut self, + refund_gas: bool, + ) -> Result<(), EthereumError> { + let number_of_tx_layer = self.evm_account_storage.stack_depth(); + + if number_of_tx_layer < 2 { + return Err(EthereumError::InconsistentTransactionStack( + number_of_tx_layer, + false, + false, + )); + } + + log!( + self.host, + Debug, + "Rollback transaction at transaction depth: {}", + self.stack_depth() + ); + + if refund_gas { + let gas_remaining = self.gas_remaining(); + let _ = self.transaction_data.pop(); + self.record_stipend(gas_remaining)?; + } else { + let _ = self.transaction_data.pop(); + } + + self.storage_cache.remove(&number_of_tx_layer); + + self.evm_account_storage + .rollback_transaction(self.host) + .map_err(EthereumError::from) + } + + fn rollback_inter_transaction_side_effect( + handler: &mut EvmHandler<'_, Host>, + execution_result: CreateOutcome, + refund_gas: bool, + ) -> Capture { + if let Err(err) = handler.rollback_inter_transaction(refund_gas) { + log!( + handler.host, + Debug, + "Rolling back reverted transaction caused an error: {:?}", + err + ); + + Capture::Exit((ethereum_error_to_exit_reason(&err), None, vec![])) + } else { + Capture::Exit(execution_result) + } + } + + /// End a transaction based on an execution result from a call to + /// [execute]. This can be either a rollback or a commit depending + /// on whether the execution was successful or not. + /// + /// This function applies _only_ to intermediate transactions. Calling + /// it with only the initial transaction in progress is an error. + pub fn end_inter_transaction( + &mut self, + execution_result: Result, + ) -> Capture { + match execution_result { + Ok((ref exit_reason, _, _)) => match exit_reason { + ExitReason::Succeed(_) => { + log!( + self.host, + Debug, + "Intermediate transaction ended with: {:?}", + exit_reason + ); + + if let Err(err) = self.commit_inter_transaction() { + log!( + self.host, + Debug, + "Committing intermediate transaction caused an error: {:?}", + err + ); + + Capture::Exit((ethereum_error_to_exit_reason(&err), None, vec![])) + } else { + Capture::Exit(execution_result.unwrap()) // safe unwrap + } + } + ExitReason::Revert(_) => { + log!( + self.host, + Debug, + "Intermediate transaction reverted with: {:?}", + exit_reason + ); + + Self::rollback_inter_transaction_side_effect( + self, + execution_result.unwrap(), // safe unwrap + true, + ) + } + ExitReason::Error(_) => { + log!( + self.host, + Debug, + "Intermediate transaction produced the following error: {:?}", + exit_reason + ); + + Self::rollback_inter_transaction_side_effect( + self, + execution_result.unwrap(), // safe unwrap + false, + ) + } + ExitReason::Fatal(ExitFatal::CallErrorAsFatal( + ExitError::CreateContractLimit, + )) => { + // For more context for why we need this specific case and behaviour + // look out for [MAX_INIT_CODE_SIZE_RETURN_HACK] in this file. + + let create_contract_limit = + ExitReason::Error(ExitError::CreateContractLimit); + let execution_result = execution_result.unwrap(); // safe unwrap + + log!( + self.host, + Debug, + "Intermediate transaction produced the following error: {:?}", + create_contract_limit + ); + + Self::rollback_inter_transaction_side_effect( + self, + ( + create_contract_limit, + execution_result.1, + execution_result.2, + ), + false, + ) + } + ExitReason::Fatal(_) => { + log!( + self.host, + Debug, + "Intermediate transaction produced the following fatal error: {:?}", + exit_reason + ); + + Self::rollback_inter_transaction_side_effect( + self, + execution_result.unwrap(), // safe unwrap + false, + ) + } + }, + Err(EthereumError::PrecompileFailed(failure_reason)) => { + // We need this case, otherwise the failure will be considered as fatal + // when it shouldn't. + + log!( + self.host, + Debug, + "Intermediate precompiled call ended with failure: {:?}", + failure_reason + ); + + if let Err(err) = self.rollback_inter_transaction(false) { + log!( + self.host, + Debug, + "Rolling back reverted transaction caused an error: {:?}", + err + ); + } + + Capture::Exit(( + ExitReason::Error(ExitError::Other(Cow::Owned(format!( + "{:?}", + EthereumError::PrecompileFailed(failure_reason) + )))), + None, + vec![], + )) + } + Err(err) => { + log!( + self.host, + Debug, + "Intermediate transaction ended in error: {:?}", + err + ); + + if let Err(err) = self.rollback_inter_transaction(false) { + log!( + self.host, + Debug, + "Rolling back reverted transaction caused an error: {:?}", + err + ); + } + + Capture::Exit((ethereum_error_to_exit_reason(&err), None, vec![])) + } + } + } + + pub fn nested_call_gas_limit(&mut self, target_gas: Option) -> Option { + // Part of EIP-150: https://github.com/ethereum/EIPs/blob/master/EIPS/eip-150.md + let gas_remaining = self.gas_remaining(); + let max_gas_limit = if self.config.call_l64_after_gas { + gas_remaining - gas_remaining / 64 + } else { + gas_remaining + }; + if let Some(gas) = target_gas { + Some(min(gas, max_gas_limit)) + } else { + Some(max_gas_limit) + } + } + + fn output_for_inter_create(&self, reason: &ExitReason, output: Vec) -> Vec { + match reason { + ExitReason::Error(_) | ExitReason::Revert(_) | ExitReason::Fatal(_) => output, + ExitReason::Succeed(_) => vec![], + } + } + + /// Test helper used to demonstrate that reentrancy guard is actually the exit reason. + #[cfg(test)] + pub(crate) fn disable_reentrancy_guard(&mut self) { + self.reentrancy_guard.disable(); + } +} + +fn update_cache( + handler: &mut EvmHandler<'_, Host>, + address: H160, + index: H256, + value: CacheStorageValue, + layer: usize, +) { + if let Some(cache) = handler.storage_cache.get_mut(&layer) { + cache.insert(StorageKey { address, index }, value); + } else { + let mut cache = HashMap::new(); + cache.insert(StorageKey { address, index }, value); + handler.storage_cache.insert(layer, cache); + } +} + +fn find_storage_key( + handler: &mut EvmHandler<'_, Host>, + address: H160, + index: H256, + current_layer: usize, +) -> Option { + for layer in (0..=current_layer).rev() { + if let Some(cache) = handler.storage_cache.get(&layer) { + if let Some(value) = cache.get(&StorageKey { address, index }) { + return Some(value.h256()); + } + } + } + None +} + +/// Committing the storage cache means that the current layer changes +/// are propagated to the previous one and the current layer is popped. +fn commit_storage_cache( + handler: &mut EvmHandler<'_, Host>, + current_layer: usize, +) { + let commit_layer = current_layer - 1; + if let Some(cache) = handler.storage_cache.remove(¤t_layer) { + if let Some(prev_layer_cache) = handler.storage_cache.get_mut(&commit_layer) { + prev_layer_cache.extend(cache); + } else { + handler.storage_cache.insert(commit_layer, cache); + } + } +} + +fn cached_storage_access( + handler: &mut EvmHandler<'_, Host>, + address: H160, + index: H256, + layer: usize, +) -> H256 { + if let Some(value) = find_storage_key(handler, address, index, layer) { + value + } else { + let value = handler + .get_account(address) + .ok() + .flatten() + .and_then(|a| a.read_storage(handler.host, &index).ok()); + + // This condition will help avoiding unecessary write access + // in the durable storage at the end of the transaction. + if let Some(value) = value { + update_cache( + handler, + address, + index, + CacheStorageValue::Read(value), + layer, + ); + } + + value.map(|x| x.h256()).unwrap_or_default() + } +} + +#[allow(unused_variables)] +impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { + type CreateInterrupt = Infallible; + type CreateFeedback = Infallible; + type CallInterrupt = Infallible; + type CallFeedback = Infallible; + + fn balance(&self, address: H160) -> U256 { + self.get_account(address) + .ok() + .flatten() + .and_then(|a| a.balance(self.host).ok()) + .unwrap_or_default() + } + + fn code_size(&self, address: H160) -> U256 { + self.get_account(address) + .ok() + .flatten() + .and_then(|a| a.code_size(self.host).ok()) + .unwrap_or_default() + } + + // Hash of the chosen account's code, the empty hash (CODE_HASH_DEFAULT) if the account has no code, + // or 0 if the account does not exist or has been destroyed. + fn code_hash(&self, address: H160) -> H256 { + if !self.exists(address) { + return H256::zero(); + } + + self.get_account(address) + .ok() + .flatten() + .and_then(|a| a.code_hash(self.host).ok()) + .unwrap_or(CODE_HASH_DEFAULT) + } + + fn code(&self, address: H160) -> Vec { + self.get_account(address) + .ok() + .flatten() + .and_then(|a| a.code(self.host).ok()) + .unwrap_or_default() + } + + fn storage(&mut self, address: H160, index: H256) -> H256 { + let layer = self.evm_account_storage.stack_depth(); + cached_storage_access(self, address, index, layer) + } + + fn original_storage(&mut self, address: H160, index: H256) -> H256 { + let key = StorageKey { address, index }; + if let Some(value) = self.original_storage_cache.get(&key) { + value.h256() + } else { + let value = self + .get_original_account(address) + .ok() + .flatten() + .and_then(|a| a.get_storage(self.host, &index).ok()) + .unwrap_or_default(); + + self.original_storage_cache + .insert(key, CacheStorageValue::Read(StorageValue::Hit(value))); + + value + } + } + + fn gas_left(&self) -> U256 { + self.gas_remaining().into() + } + + fn gas_price(&self) -> U256 { + self.effective_gas_price + } + + fn origin(&self) -> H160 { + self.origin + } + + fn block_hash(&self, number: U256) -> H256 { + // return 0 when block number not in valid range + // Ref. https://www.evm.codes/#40?fork=shanghai (opcode 0x40) + + match self.block.number.checked_sub(number) { + Some(block_diff) + if block_diff <= U256::from(BLOCKS_STORED) + && block_diff != U256::zero() => + { + get_block_hash(self.host, number).unwrap_or_default() + } + _ => H256::zero(), + } + } + + fn block_number(&self) -> U256 { + self.block.number + } + + fn block_coinbase(&self) -> H160 { + self.block.coinbase + } + + fn block_timestamp(&self) -> U256 { + self.block.timestamp + } + + fn block_difficulty(&self) -> U256 { + // There's no difficulty in the blocks + // A default value is returned here + U256::zero() + } + + fn block_gas_limit(&self) -> U256 { + self.block.gas_limit.into() + } + + fn block_base_fee_per_gas(&self) -> U256 { + self.block.base_fee_per_gas() + } + + fn block_randomness(&self) -> Option { + self.block.prevrandao // Always None + } + + fn chain_id(&self) -> U256 { + self.block.chain_id + } + + fn exists(&self, address: H160) -> bool { + self.code_size(address) > U256::zero() + || self.get_nonce(address).unwrap_or_default() > 0 + || self.balance(address) > U256::zero() + } + + fn deleted(&self, address: H160) -> bool { + for data in &self.transaction_data { + if data.deleted_contracts.contains(&address) { + return true; + } + } + + false + } + + fn is_cold(&mut self, address: H160, index: Option) -> Result { + if !self.enable_warm_cold_access { + return Ok(false); + } + + match index { + Some(index) => { + let is_cold = self.is_storage_hot(address, index).map(|x| !x); + if let Ok(true) = is_cold { + self.mark_storage_as_hot(address, index)?; + } + is_cold + } + None => { + if self.precompiles.is_precompile(address) { + Ok(false) + } else { + let is_cold = self.is_address_hot(address).map(|x| !x); + if let Ok(true) = is_cold { + self.mark_address_as_hot(address)?; + } + is_cold + } + } + } + } + + fn set_storage( + &mut self, + address: H160, + index: H256, + value: H256, + ) -> Result<(), ExitError> { + let layer = self.evm_account_storage.stack_depth(); + update_cache(self, address, index, CacheStorageValue::Write(value), layer); + Ok(()) + } + + fn log( + &mut self, + address: H160, + topics: Vec, + data: Vec, + ) -> Result<(), ExitError> { + self.add_log(Log { + address, + topics, + data, + }) + } + + fn mark_delete(&mut self, address: H160, target: H160) -> Result<(), ExitError> { + let new_deletion = match self.transaction_data.last_mut() { + Some(top_layer) => Ok(top_layer.deleted_contracts.insert(address)), + None => Err(ExitError::Other(Cow::from( + "No transaction data for delete", + ))), + }?; + if new_deletion && address == target { + self.reset_balance(address).map_err(|_| { + ExitError::Other(Cow::from( + "Could not reset balance when deleting contract", + )) + }) + } else if new_deletion { + let balance = self.balance(address); + + self.execute_transfer(address, target, balance) + .map_err(|_| { + ExitError::Other(Cow::from( + "Could not execute transfer on contract delete", + )) + })?; + Ok(()) + } else { + log!(self.host, Debug, "Contract already marked to delete"); + Ok(()) + } + } + + fn create( + &mut self, + caller: H160, + scheme: CreateScheme, + value: U256, + init_code: Vec, + target_gas: Option, + ) -> Capture { + match self.can_begin_inter_transaction(caller, &value) { + Precondition::PassPrecondition => { + // We check that the maximum allowed init code size as specified by EIP-3860 + // can not be reached. + if let Some(max_initcode_size) = self.config.max_initcode_size { + if init_code.len() > max_initcode_size { + // [MAX_INIT_CODE_SIZE_RETURN_HACK] + // The normal behavior stated by https://www.evm.codes/#f0?fork=shanghai + // would be to return a simple error. + // « Error cases: [..] + // * size is greater than the chain's maximum initcode size (since Shanghai fork) » + // + // Unfortunately there is a bug in [evm-runtime-0.39.0] where the `finish_create` + // function will always consider error/revert/succed as a "Ok(()) => Control::Continue" + // flow which makes it that we can not rollback anything as it should in this case. + // The hack-ish way to be able to capture that error and rollback as it should is + // to consider this error as fatal and then catch it in `end_inter_transaction`, + // rollback what needs to be and then transform the outputed fatal error to a simple + // `ExitReason::Error(ExitError::CreateContractLimit)`. + + return Capture::Exit(( + ExitReason::Fatal(ExitFatal::CallErrorAsFatal( + ExitError::CreateContractLimit, + )), + None, + vec![], + )); + } + } + + // The contract address is created before the increment of the nonce + // to generate a correct address when the scheme is `Legacy`. + let contract_address = match self.create_address(scheme) { + Ok(address) => address, + Err(err) => { + return Capture::Exit(( + ethereum_error_to_exit_reason(&err.into()), + None, + vec![], + )); + } + }; + + // This `mark_address_as_hot` must be before the `begin_inter_transaction` + // so the address will still be hot even if the creation fails + if self.mark_address_as_hot(contract_address).is_err() { + let err = EthereumError::InconsistentState(Cow::from( + "Failed to mark callee address as hot", + )); + return Capture::Exit(( + ethereum_error_to_exit_reason(&err), + None, + vec![], + )); + } + + // The nonce of the caller is incremented before the internal tx + // Even if the internal transaction rollback the nonce will not + if let Err(err) = self.increment_nonce(caller) { + log!( + self.host, + Debug, + "Failed to increment nonce of {:?}", + caller + ); + + return Capture::Exit(( + ethereum_error_to_exit_reason(&err), + None, + vec![], + )); + } + + let gas_limit = self.nested_call_gas_limit(target_gas); + + if let Err(err) = self.record_cost(gas_limit.unwrap_or_default()) { + log!( + self.host, + Debug, + "Not enough gas for create. Required at least: {:?}", + gas_limit + ); + + return Capture::Exit(( + ExitReason::Error(ExitError::OutOfGas), + None, + vec![], + )); + } + + match self.begin_inter_transaction(false, gas_limit) { + Ok(()) => { + let gas_before = self.gas_used(); + let result = self.execute_create( + caller, + value, + init_code.clone(), + contract_address, + ); + let gas_after = self.gas_used(); + + match self.end_inter_transaction(result) { + Capture::Exit((reason, address, output)) => { + // TRACING + if let Some(CallTracer(CallTracerInput { + transaction_hash, + config: + CallTracerConfig { + with_logs, + only_top_call: false, + }, + })) = self.tracer + { + let mut call_trace = CallTrace::new_minimal_trace( + if let CreateScheme::Create2 { .. } = scheme { + "CREATE2" + } else { + "CREATE" + } + .into(), + caller, + value, + gas_after - gas_before, + init_code, + // We need to make the distinction between the initial call (depth 0) + // and the other subcalls + (self.stack_depth() + 1) + .try_into() + .unwrap_or_default(), + ); + + call_trace.add_to(address); + call_trace.add_output(Some(output.to_owned())); + call_trace.add_gas(target_gas); + + // TODO: https://gitlab.com/tezos/tezos/-/issues/7437 + // For errors and revert reasons, find the appropriate values + // to return for tracing. The following values are kind of placeholders. + match &reason { + ExitReason::Error(e) => call_trace + .add_error(Some(format!("{:?}", e).into())), + ExitReason::Revert(r) => call_trace + .add_error(Some(format!("{:?}", r).into())), + ExitReason::Fatal(f) => call_trace + .add_error(Some(format!("{:?}", f).into())), + ExitReason::Succeed(_) => (), + }; + + if with_logs { + call_trace.add_logs( + self.transaction_data + .last() + .map(|tx_layer| tx_layer.logs.clone()), + ) + } + + let _ = tracer::store_call_trace( + self.host, + call_trace, + &transaction_hash, + ); + } + let output = + self.output_for_inter_create(&reason, output); + Capture::Exit((reason, address, output)) + } + Capture::Trap(x) => Capture::Trap(x), + } + } + Err(err) => { + log!( + self.host, + Debug, + "Intermediate transaction failed, reason: {:?}", + err + ); + + Capture::Exit((ethereum_error_to_exit_reason(&err), None, vec![])) + } + } + } + Precondition::PreconditionErr(exit_reason) => { + Capture::Exit((exit_reason, None, vec![])) + } + Precondition::EthereumErr(err) => { + Capture::Exit((ethereum_error_to_exit_reason(&err), None, vec![])) + } + } + } + + fn call( + &mut self, + code_address: H160, + transfer: Option, + input: Vec, + target_gas: Option, + call_scheme: CallScheme, + context: Context, + ) -> Capture { + let transaction_context = TransactionContext::from_context(context); + let caller = transaction_context.context.caller; + + // Retrieve value from `Transfer` struct to check if caller has enough balance + let value = match transfer { + None => U256::zero(), + Some(Transfer { value, .. }) => value, + }; + + match self.can_begin_inter_transaction(caller, &value) { + Precondition::PassPrecondition => { + let mut gas_limit = self.nested_call_gas_limit(target_gas); + + if let Err(err) = self.record_cost(gas_limit.unwrap_or_default()) { + log!( + self.host, + Debug, + "Not enough gas for call. Required at least: {:?}", + gas_limit + ); + + return Capture::Exit(( + ExitReason::Error(ExitError::OutOfGas), + vec![], + )); + } + + // For call with transfer value > 0, a stipend is added to the gaslimit. + // see yellowpaper, appendix H, opcode CALL (0xf1) and CALLCODE (Oxf2) + // Note that for other CALL* opcodes sputnik will not add a transfer at all. + if value > U256::zero() { + gas_limit = + gas_limit.map(|v| v.saturating_add(self.config.call_stipend)); + } + + if let Err(err) = self.begin_inter_transaction( + call_scheme == CallScheme::StaticCall, + gas_limit, + ) { + return Capture::Exit((ethereum_error_to_exit_reason(&err), vec![])); + } + + let address = transaction_context.context.address; + + let gas_before = self.gas_used(); + let result = self.execute_call( + code_address, + transfer, + input.clone(), + transaction_context.clone(), + ); + let gas_after = self.gas_used(); + + match self.end_inter_transaction(result) { + Capture::Exit((reason, _, output)) => { + log!(self.host, Debug, "Call ended with reason: {:?}", reason); + + // TRACING + if let Some(CallTracer(CallTracerInput { + transaction_hash, + config: + CallTracerConfig { + with_logs, + only_top_call: false, + }, + })) = self.tracer + { + let (type_, from) = match call_scheme { + CallScheme::Call => ("CALL", caller), + CallScheme::StaticCall => ("STATICCALL", caller), + CallScheme::DelegateCall => { + // FIXME: #7738 this only point to parent call + // address if it was not a DELEGATECALL or + // CALLCODE itself + ("DELEGATECALL", transaction_context.context.address) + } + CallScheme::CallCode => { + // FIXME: #7738 this only point to parent call + // address if it was not a DELEGATECALL or + // CALLCODE itself + ("CALLCODE", transaction_context.context.address) + } + }; + let mut call_trace = CallTrace::new_minimal_trace( + type_.into(), + from, + value, + gas_after - gas_before, + input, + // We need to make the distinction between the initial call (depth 0) + // and the other subcalls + (self.stack_depth() + 1).try_into().unwrap_or_default(), + ); + + // for the trace we want the contract address to always be the "to" + // field, not necessarily the address used in the transition context + // which may be something else (eg DELEGATECALL) + call_trace.add_to(Some(code_address)); + + call_trace.add_gas(target_gas); + call_trace.add_output(Some(output.to_owned())); + + // TODO: https://gitlab.com/tezos/tezos/-/issues/7437 + // For errors and revert reasons, find the appropriate values + // to return for tracing. The following values are kind of placeholders. + match &reason { + ExitReason::Succeed(_) => (), + ExitReason::Error(e) => { + call_trace.add_error(Some(format!("{:?}", e).into())) + } + ExitReason::Revert(r) => { + call_trace.add_error(Some(format!("{:?}", r).into())) + } + ExitReason::Fatal(f) => { + call_trace.add_error(Some(format!("{:?}", f).into())) + } + }; + + if with_logs { + call_trace.add_logs( + self.transaction_data + .last() + .map(|tx_layer| tx_layer.logs.clone()), + ) + } + + let _ = tracer::store_call_trace( + self.host, + call_trace, + &transaction_hash, + ); + } + + Capture::Exit((reason, output)) + } + Capture::Trap(x) => Capture::Trap(x), + } + } + Precondition::PreconditionErr(exit_reason) => { + if value > U256::zero() { + if let Err(err) = self.record_stipend(self.config.call_stipend) { + return Capture::Exit(( + ethereum_error_to_exit_reason(&err), + vec![], + )); + } + } + Capture::Exit((exit_reason, vec![])) + } + Precondition::EthereumErr(err) => { + Capture::Exit((ethereum_error_to_exit_reason(&err), vec![])) + } + } + } + + fn pre_validate( + &mut self, + context: &Context, + opcode: Opcode, + stack: &Stack, + ) -> Result<(), ExitError> { + if let Some(cost) = evm::gasometer::static_opcode_cost(opcode) { + self.record_cost(cost) + } else { + let (cost, _target, memory_cost) = evm::gasometer::dynamic_opcode_cost( + context.address, + opcode, + stack, + self.is_static(), + self.config, + self, + )?; + + self.record_dynamic_cost(cost, memory_cost) + } + } + + fn transient_storage(&self, _address: H160, _index: H256) -> H256 { + panic!("Not available on calypso") + } + + fn blob_hash(&self, _index: H256) -> H256 { + panic!("Not available on calypso") + } + + fn block_blob_base_fee(&self) -> U256 { + panic!("Not available on calypso") + } + + fn set_transient_storage( + &mut self, + _address: H160, + _index: H256, + _value: H256, + ) -> Result<(), ExitError> { + panic!("Not available on calypso") + } +} + +#[cfg(test)] +mod test { + use super::*; + use crate::account_storage::init_account_storage; + use crate::precompiles; + use evm::Config; + use pretty_assertions::assert_eq; + use primitive_types::{H160, H256}; + use std::cmp::Ordering; + use std::str::FromStr; + use std::vec; + use tezos_ethereum::block::BlockFees; + use tezos_evm_runtime::runtime::MockKernelHost; + + const DUMMY_ALLOCATED_TICKS: u64 = 1_000_000_000; + + fn set_code( + handler: &mut EvmHandler<'_, MockKernelHost>, + address: &H160, + code: Vec, + ) { + let mut account = handler.get_or_create_account(*address).unwrap(); + account.delete_code(handler.borrow_host()).unwrap(); //first clean code if it exists. + account.set_code(handler.borrow_host(), &code).unwrap(); + } + + fn set_nonce( + handler: &mut EvmHandler<'_, MockKernelHost>, + address: &H160, + nonce: u64, + ) { + let mut account = handler.get_or_create_account(*address).unwrap(); + account.set_nonce(handler.borrow_host(), nonce).unwrap() + } + + fn get_balance(handler: &mut EvmHandler<'_, MockKernelHost>, address: &H160) -> U256 { + let account = handler.get_or_create_account(*address).unwrap(); + account.balance(handler.borrow_host()).unwrap() + } + + fn set_balance( + handler: &mut EvmHandler<'_, MockKernelHost>, + address: &H160, + new_balance: U256, + ) { + let mut account = handler.get_or_create_account(*address).unwrap(); + let old_balance = account.balance(handler.borrow_host()).unwrap(); + match old_balance.cmp(&new_balance) { + Ordering::Greater => { + // we require that fund removal goes fine + assert!( + account + .balance_remove(handler.borrow_host(), old_balance - new_balance) + .unwrap(), + "Could not set balance of account" + ) + } + Ordering::Less => account + .balance_add(handler.borrow_host(), new_balance - old_balance) + .unwrap(), + Ordering::Equal => (), + } + } + + fn get_durable_slot( + handler: &mut EvmHandler<'_, MockKernelHost>, + address: &H160, + index: &H256, + ) -> H256 { + let layer = handler.evm_account_storage.stack_depth(); + cached_storage_access(handler, *address, *index, layer) + } + + fn dummy_first_block() -> BlockConstants { + let block_fees = BlockFees::new( + U256::one(), + U256::from(12345), + U256::from(2_000_000_000_000u64), + ); + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ) + } + + #[test] + fn legacy_create_to_correct_address() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let gas_price = U256::from(21000); + + // This is a randomly generated address. It has been used for testing legacy address + // generation with zero nonce using Ethereum. To replicate (with new address): + // - generate a fresh Ethereum account (on Rinkeby or other test net) + // - make sure it has eth (transfer from faucet) + // - check nonce is zero (or bump nonce accordingly below) + // - create a new contract. Any contract will do. + // - check address of new contract - it is `expected_result` below. + let caller: H160 = + H160::from_str("9bbfed6889322e016e0a02ee459d306fc19545d8").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let result = handler + .create_address(CreateScheme::Legacy { caller }) + .unwrap_or_default(); + + let expected_result: H160 = + H160::from_str("43a61f3f4c73ea0d444c5c1c1a8544067a86219b").unwrap(); + + assert_eq!(result, expected_result); + } + + #[test] + fn create2_to_correct_address() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller: H160 = + H160::from_str("9bbfed6889322e016e0a02ee459d306fc19545d8").unwrap(); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let code_hash: H256 = CODE_HASH_DEFAULT; + let salt: H256 = H256::zero(); + + let result = handler + .create_address(CreateScheme::Create2 { + caller, + code_hash, + salt, + }) + .unwrap_or_default(); + + let expected_result: H160 = + H160::from_str("0687a12da0ffa0a64a28c9512512b8ae8870b7ea").unwrap(); + + assert_eq!(result, expected_result); + } + + #[test] + fn create2_to_correct_address_nonzero_salt() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let gas_price = U256::from(21000); + + let caller: H160 = + H160::from_str("9bbfed6889322e016e0a02ee459d306fc19545d8").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let code_hash: H256 = CODE_HASH_DEFAULT; + let salt: H256 = H256::from_str( + "0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(); + + let result = handler + .create_address(CreateScheme::Create2 { + caller, + code_hash, + salt, + }) + .unwrap_or_default(); + + let expected_result: H160 = + H160::from_str("dbd0b036a125995a83d0ab020656a8355abac612").unwrap(); + + assert_eq!(result, expected_result); + } + + #[test] + fn origin_instruction_returns_origin_address() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(28349_u64); + + // We use an origin distinct from caller for testing purposes + let origin = H160::from_low_u64_be(117_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + origin, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(213_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + let code: Vec = vec![ + Opcode::ORIGIN.as_u8(), // Push the 32(!) byte origin on to stack (this is "the value") + Opcode::PUSH1.as_u8(), // Push a zero valued word onto stack (this is "the address") + 0_u8, + Opcode::MSTORE.as_u8(), // Store "the value" at "the address" + Opcode::PUSH1.as_u8(), // Push value 2 onto stack - this is "number of bytes" + 32_u8, + Opcode::PUSH1.as_u8(), // Push value 0 onto stack - this is "the address" again + 0_u8, + Opcode::RETURN.as_u8(), // Return "number of bytes" at "the address" in the RETURNBUFFER + ]; + + set_code(&mut handler, &address, code); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = ( + ExitReason::Succeed(ExitSucceed::Returned), + None, + H256::from(origin).0.to_vec(), + ); + assert_eq!(result, expected_result); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_call_produces_correct_output() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(28349_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(213_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + let code: Vec = vec![ + Opcode::PUSH32.as_u8(), // Push a 32 byte word onto stack (this is "the value") + 0xFF_u8, + 1, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + 0, + Opcode::PUSH1.as_u8(), // Push a zero valued word onto stack (this is "the address") + 0_u8, + Opcode::MSTORE.as_u8(), // Store "the value" at "the address" + Opcode::PUSH1.as_u8(), // Push value 2 onto stack - this is "number of bytes" + 2_u8, + Opcode::PUSH1.as_u8(), // Push value 0 onto stack - this is "the address" again + 0_u8, + Opcode::RETURN.as_u8(), // Return "number of bytes" at "the address" in the RETURNBUFFER + ]; + + set_code(&mut handler, &address, code); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = ( + ExitReason::Succeed(ExitSucceed::Returned), + None, + vec![0xFF_u8, 0x01_u8], + ); + assert_eq!(result, expected_result); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_call_fails_beyond_max_stack_depth() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(2340); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let input_value = U256::from(2026_u32); + let mut input = [0_u8; 32]; + input_value.to_big_endian(&mut input); + + let address = H160::from_low_u64_be(118); + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + let code: Vec = vec![ + // get input data, subtract one and prepare as argument to nested call + Opcode::PUSH1.as_u8(), + 1, + Opcode::PUSH1.as_u8(), + 0, // call data offset + Opcode::CALLDATALOAD.as_u8(), + Opcode::SUB.as_u8(), + // check if result is zero - if so, skip to return + Opcode::DUP1.as_u8(), + Opcode::ISZERO.as_u8(), + Opcode::PUSH1.as_u8(), + 28_u8, // to JPMDEST + Opcode::JUMPI.as_u8(), + // store result in memory to use as call argument + Opcode::PUSH1.as_u8(), + 0, + Opcode::MSTORE.as_u8(), + // set call parameters + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 32_u8, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 0, // value + Opcode::ADDRESS.as_u8(), // address + Opcode::PUSH1.as_u8(), + 0, // gas + Opcode::CALL.as_u8(), // call self + // when we get here we are done + Opcode::JUMPDEST.as_u8(), + Opcode::PUSH1.as_u8(), + 0, // return data size + Opcode::PUSH1.as_u8(), + 0, // return data offset + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = + handler.execute_call(address, transfer, input.to_vec(), transaction_context); + + match result { + Ok(result) => { + let expected_result = + (ExitReason::Succeed(ExitSucceed::Returned), None, vec![]); + assert_eq!(result, expected_result); + } + Err(err) => { + panic!( + "Expected call to fail because of call depth, but got {:?}", + err + ); + } + } + } + + #[test] + fn contract_call_succeeds_at_maximum_stack_depth() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(8213); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let input_value = U256::from(1025_u32); // transaction depth for contract below is callarg - 1 + let mut input = [0_u8; 32]; + input_value.to_big_endian(&mut input); + + let address = H160::from_low_u64_be(12389); + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + let code: Vec = vec![ + // get input data, subtract one and prepare as argument to nested call + Opcode::PUSH1.as_u8(), + 1, + Opcode::PUSH1.as_u8(), + 0, // call data offset + Opcode::CALLDATALOAD.as_u8(), + Opcode::SUB.as_u8(), + // check if result is zero - if so, skip to return + Opcode::DUP1.as_u8(), + Opcode::ISZERO.as_u8(), + Opcode::PUSH1.as_u8(), + 28_u8, // to JPMDEST + Opcode::JUMPI.as_u8(), + // store result in memory to use as call argument + Opcode::PUSH1.as_u8(), + 0, + Opcode::MSTORE.as_u8(), + // set call parameters + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 32_u8, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 0, // value + Opcode::ADDRESS.as_u8(), // address + Opcode::PUSH1.as_u8(), + 0, // gas + Opcode::CALL.as_u8(), // call self + // when we get here we are done + Opcode::JUMPDEST.as_u8(), + Opcode::PUSH1.as_u8(), + 0, // return data size + Opcode::PUSH1.as_u8(), + 0, // return data offset + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = + handler.execute_call(address, transfer, input.to_vec(), transaction_context); + + match result { + Ok(result) => { + let expected_result = + (ExitReason::Succeed(ExitSucceed::Returned), None, vec![]); + assert_eq!(result, expected_result); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_can_use_durable_storage() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(444); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(312); + let input: Vec = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 46_u8, + Opcode::PUSH1.as_u8(), + 0_u8, + Opcode::SSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0_u8, + Opcode::SLOAD.as_u8(), + Opcode::PUSH1.as_u8(), + 1_u8, + Opcode::SLOAD.as_u8(), + Opcode::PUSH1.as_u8(), + 0, + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = + (ExitReason::Succeed(ExitSucceed::Returned), None, vec![]); + assert_eq!(result, expected_result); + let expected_in_storage = H256::from_str( + "000000000000000000000000000000000000000000000000000000000000002e", + ) + .unwrap(); + assert_eq!( + get_durable_slot(&mut handler, &address, &H256::zero()), + expected_in_storage + ); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_create_can_use_durable_storage() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(117); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let value = U256::zero(); + let create_scheme = CreateScheme::Legacy { caller }; + let init_code: Vec = hex::decode("608060405234801561001057600080fd5b50602a600081905550610150806100286000396000f3fe608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212204d6c1853cec27824f5dbf8bcd0994714258d22fc0e0dc8a2460d87c70e3e57a564736f6c63430008120033").unwrap(); + + let expected_address = handler.create_address(create_scheme).unwrap_or_default(); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_create(caller, value, init_code, expected_address); + + match result { + Ok(result) => { + let expected_result = ( + ExitReason::Succeed(ExitSucceed::Returned), + Some(expected_address), + hex::decode("608060405234801561001057600080fd5b50600436106100365760003560e01c80632e64cec11461003b5780636057361d14610059575b600080fd5b610043610075565b60405161005091906100a1565b60405180910390f35b610073600480360381019061006e91906100ed565b61007e565b005b60008054905090565b8060008190555050565b6000819050919050565b61009b81610088565b82525050565b60006020820190506100b66000830184610092565b92915050565b600080fd5b6100ca81610088565b81146100d557600080fd5b50565b6000813590506100e7816100c1565b92915050565b600060208284031215610103576101026100bc565b5b6000610111848285016100d8565b9150509291505056fea26469706673582212204d6c1853cec27824f5dbf8bcd0994714258d22fc0e0dc8a2460d87c70e3e57a564736f6c63430008120033").unwrap(), + ); + assert_eq!(result, expected_result); + assert_eq!( + get_durable_slot(&mut handler, &expected_address, &H256::zero()), + H256::from_str( + "000000000000000000000000000000000000000000000000000000000000002a" + ) + .unwrap() + ); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_create_has_return_when_revert() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(117); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let value = U256::zero(); + let create_scheme = CreateScheme::Legacy { caller }; + + // The code of the contract revert with 0x18 (equivalent to 24) + let initial_code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0x18, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::MSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x20, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::REVERT.as_u8(), + ]; + + let contract_address = handler.create_address(create_scheme).unwrap_or_default(); + handler.begin_initial_transaction(false, None).unwrap(); + + let result = + handler.execute_create(caller, value, initial_code, contract_address); + + match result { + Ok(result) => { + // Expecting to revert with 0x18 in the return vector + let expected_result = ( + ExitReason::Revert(ExitRevert::Reverted), + None, + vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 24, + ], + ); + assert_eq!(result, expected_result); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_call_does_transfer() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(118); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(117); + let input = vec![0_u8]; + let transaction_context = + TransactionContext::new(caller, address, U256::from(50_u32)); + let transfer: Option = Some(Transfer { + source: caller, + target: address, + value: U256::from(100_u32), + }); + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(101_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = + (ExitReason::Succeed(ExitSucceed::Returned), None, vec![]); + assert_eq!(result, expected_result); + assert_eq!(get_balance(&mut handler, &address), U256::from(100_u32)); + assert_eq!(get_balance(&mut handler, &caller), U256::from(1_u32)); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Expected Ok, but got {:?}", err); + } + } + } + + #[test] + fn contract_call_fails_when_insufficient_funds_for_transfer() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = Some(Transfer { + source: caller, + target: address, + value: U256::from(100_u32), + }); + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = + (ExitReason::Error(ExitError::OutOfFund), None, vec![]); + assert_eq!(result, expected_result); + assert_eq!(get_balance(&mut handler, &caller), U256::from(99_u32)); + assert_eq!(get_balance(&mut handler, &address), U256::zero()); + assert_eq!(handler.gas_used(), 0); + } + Err(err) => { + panic!("Unexpected error: {:?}", err); + } + } + } + + #[test] + fn revert_can_return_a_value() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + + let code: Vec = vec![ + Opcode::PUSH8.as_u8(), // push value of return data + 0, + 1, + 2, + 3, + 4, + 5, + 6, + 7, + Opcode::PUSH1.as_u8(), // push address of return data + 0, + Opcode::MSTORE.as_u8(), // store return data in memory + Opcode::PUSH1.as_u8(), // push size of return data + 8, + Opcode::PUSH1.as_u8(), // push offset in memory of return data + 24, + Opcode::REVERT.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + match result { + Ok(result) => { + let expected_result = ( + ExitReason::Revert(ExitRevert::Reverted), + None, + vec![0, 1, 2, 3, 4, 5, 6, 7], + ); + assert_eq!(expected_result, result); + } + Err(err) => { + panic!("Unexpected error: {:?}", err); + } + } + } + + #[test] + fn return_hash_of_zero_for_unavailable_block() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let hash_of_unavailable_block = handler.block_hash(U256::zero()); + assert_eq!(H256::zero(), hash_of_unavailable_block) + } + + #[test] + fn transactions_fails_if_not_enough_allocated_ticks() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::london(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + 10_000, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + handler + .begin_initial_transaction(false, Some(30000)) + .unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + assert_eq!( + Err(EthereumError::OutOfTicks), + result, + "Contract call was expected to fail and run out of ticks: \n" + ); + } + + #[test] + fn store_after_offset_1024() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), // push value 0xff + 0xff, + Opcode::PUSH2.as_u8(), // push offset 0x401 == 1025 + 0x04, + 0x01, + Opcode::MSTORE8.as_u8(), // Store 0xff at offset 1025 in memory + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + assert_eq!( + Ok((ExitReason::Succeed(ExitSucceed::Stopped), None, vec![])), + result, + "Writing at offset 1025 in the memory doesn't work" + ) + } + + #[test] + fn dont_crash_on_blockhash_instruction() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + let input = vec![0_u8]; + let transaction_context = TransactionContext::new(caller, address, U256::zero()); + let transfer: Option = None; + + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), // push value 0x1 + 0x1, + Opcode::PUSH1.as_u8(), // push value 0x0 + 0x0, + Opcode::MSTORE.as_u8(), // a 1 at location 0 + Opcode::PUSH4.as_u8(), // push value 0xffffffff + 0xff, + 0xff, + 0xff, + 0xff, + Opcode::BLOCKHASH.as_u8(), + Opcode::PUSH1.as_u8(), // push value 0x0 + 0x0, + Opcode::MSTORE.as_u8(), // store blockhash at location 0x0 + Opcode::PUSH1.as_u8(), // push 32 + 32, + Opcode::PUSH1.as_u8(), // push 0x0 + 0x0, + Opcode::RETURN.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call(address, transfer, input, transaction_context); + + assert_eq!( + Ok(( + ExitReason::Succeed(ExitSucceed::Returned), + None, + vec![0; 32], + )), + result, + ) + } + + #[test] + fn prevent_collision_create2_selfdestruct() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + + let config = Config::shanghai(); + + let caller_address: [u8; 20] = + hex::decode("a94f5374fce5edbc8e2a8697c15331677e6ebf0b") + .unwrap() + .try_into() + .unwrap(); + let caller = H160::from(caller_address); + let target_address: [u8; 20] = + hex::decode("ec2c6832d00680ece8ff9254f81fdab0a5a2ac50") + .unwrap() + .try_into() + .unwrap(); + let target_address = H160::from(target_address); + + let transaction_context = + TransactionContext::new(caller, target_address, U256::zero()); + + let gas_price = U256::from(21000); + + // { (CALL 50000 0xec2c6832d00680ece8ff9254f81fdab0a5a2ac50 0 0 0 0 0) (MSTORE 0 0x6460016001556000526005601bf3) (CREATE2 0 18 14 0) } + let input = hex::decode("6000600060006000600073e2b35478fdd26477cc576dd906e6277761246a3c61c350f1506000600060006000f500").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + // { (SELFDESTRUCT 0x10) } + let code = hex::decode("6010ff").unwrap(); + + set_code(&mut handler, &target_address, code); + set_balance(&mut handler, &caller, U256::from(1000000000000000000u64)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = + handler.execute_call(target_address, None, input, transaction_context); + + // This assertion will change with the upcoming Dencun config/fork + // See: https://eips.ethereum.org/EIPS/eip-6780 + assert_eq!( + Ok((ExitReason::Succeed(ExitSucceed::Suicided), None, vec![],)), + result, + ); + + let code = handler.code(target_address); + let balance = handler.balance(target_address); + + assert_eq!(code, vec![96, 16, 255]); + assert_eq!(balance, U256::zero()); + } + + #[test] + fn create_contract_with_insufficient_funds() { + //Init + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + U256::one(), + false, + None, + ); + + set_balance(&mut handler, &caller, U256::from(10000)); + + let scheme = CreateScheme::Legacy { caller }; + let code = hex::decode("600c60005566602060406000f060205260076039f3").unwrap(); + + let contract_address = handler.create_address(scheme).unwrap_or_default(); + + handler + .begin_initial_transaction(false, Some(10000)) + .unwrap(); + + let result = + handler.execute_create(caller, U256::from(100000), code, contract_address); + + assert_eq!( + result.unwrap(), + (ExitReason::Error(ExitError::OutOfFund), None, vec![]) + ); + } + + #[test] + fn inter_call_with_non_zero_transfer_value_gets_call_stipend() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let address_1 = H160::from_low_u64_be(210_u64); + let address_2 = H160::from_low_u64_be(211_u64); + let input = vec![0_u8]; + let transaction_context = + TransactionContext::new(caller, address_1, U256::zero()); + let transfer: Option = None; + + let code_1: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 0, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 100, // non-zero value + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::PUSH1.as_u8(), + 0, // gas + Opcode::CALL.as_u8(), // call should suceed and return 1 + Opcode::PUSH1.as_u8(), + 0, + Opcode::MSTORE.as_u8(), // store 1 to Memory[0:32] + Opcode::PUSH1.as_u8(), + 1, + Opcode::PUSH1.as_u8(), + 31, + Opcode::RETURN.as_u8(), // return byte that contains the 1 + ]; + + let code_2: Vec = vec![Opcode::TIMESTAMP.as_u8()]; + + set_code(&mut handler, &address_1, code_1); + set_code(&mut handler, &address_2, code_2); + + set_balance(&mut handler, &caller, U256::from(1000_u32)); + set_balance(&mut handler, &address_1, U256::from(1000_u32)); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = + handler.execute_call(address_1, transfer, input, transaction_context); + + assert_eq!( + Ok((ExitReason::Succeed(ExitSucceed::Returned), None, vec![1],)), + result, + ) + } + + #[test] + fn code_hash_of_zero_for_non_existing_address() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let hash = handler.code_hash(H160::from_low_u64_le(1)); + + assert_eq!(H256::zero(), hash) + } + + #[test] + fn create_contract_with_selfdestruct_init_code() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + let withdrawal_contract = + H160::from_str("2adc25665018aa1fe0e6bc666dac8fc2697ff9ba").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + U256::one(), + false, + None, + ); + + set_balance(&mut handler, &caller, U256::from(1000000000)); + + let code = hex::decode("732adc25665018aa1fe0e6bc666dac8fc2697ff9baff00").unwrap(); // transfer balance to 0x2adc25665018aa1fe0e6bc666dac8fc2697ff9ba and selfdestruct + + let result = handler + .create_contract(caller, Some(U256::one()), code, None) + .unwrap(); + + let suicided_contract = result.new_address().unwrap(); + + assert!(matches!( + result.result, + ExecutionResult::ContractDeployed(_, _) + )); + assert_eq!(get_balance(&mut handler, &withdrawal_contract), U256::one()); + assert_eq!(get_balance(&mut handler, &caller), U256::from(999999999)); + assert!(!handler.exists(suicided_contract)); + } + + #[test] + fn contract_that_selfdestruct_not_deleted_within_same_transaction() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + false, + None, + ); + + let address_1 = H160::from_low_u64_be(210_u64); + let address_2 = H160::from_low_u64_be(211_u64); + let input = vec![0_u8]; + let transaction_context = + TransactionContext::new(caller, address_1, U256::zero()); + let transfer: Option = None; + + let code_1: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 0, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 0, // value + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::PUSH2.as_u8(), + 100, + 0, // gas + Opcode::CALL.as_u8(), // call should cause address 2 to selfdestruct + Opcode::POP.as_u8(), // pop return value off the stack + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::EXTCODESIZE.as_u8(), + Opcode::PUSH1.as_u8(), + 0, + Opcode::MSTORE.as_u8(), // store 1 to Memory[0:32] + Opcode::PUSH1.as_u8(), + 1, + Opcode::PUSH1.as_u8(), + 31, + Opcode::RETURN.as_u8(), // return codesize of the contract that selfdestruct + ]; + + let code_2: Vec = vec![Opcode::PUSH1.as_u8(), 0, Opcode::SUICIDE.as_u8()]; + + set_code(&mut handler, &address_1, code_1); + set_code(&mut handler, &address_2, code_2); + + set_balance(&mut handler, &caller, U256::from(1000_u32)); + set_balance(&mut handler, &address_1, U256::from(1000_u32)); + + handler + .begin_initial_transaction(false, Some(1000000)) + .unwrap(); + + let result = + handler.execute_call(address_1, transfer, input, transaction_context); + + assert_eq!( + Ok((ExitReason::Succeed(ExitSucceed::Returned), None, vec![3],)), + result, + ) + } + + #[test] + fn contract_that_selfdestruct_can_be_called_again_in_same_transaction() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + false, + None, + ); + + let address_1 = H160::from_low_u64_be(210_u64); + let address_2 = H160::from_low_u64_be(211_u64); + let input = vec![0_u8]; + let transaction_context = + TransactionContext::new(caller, address_1, U256::zero()); + let transfer: Option = None; + + let code_1: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 0, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 0, // value + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::PUSH2.as_u8(), + 100, + 0, // gas + Opcode::CALL.as_u8(), // call should cause address 2 to selfdestruct + Opcode::POP.as_u8(), // pop return value off the stack + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 0, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 0, // value + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::PUSH2.as_u8(), + 100, + 0, // gas + Opcode::CALL.as_u8(), // call should cause address 2 to selfdestruct + Opcode::PUSH1.as_u8(), + 0, + Opcode::MSTORE.as_u8(), // store result to Memory[0:1] + Opcode::PUSH1.as_u8(), + 1, + Opcode::PUSH1.as_u8(), + 31, + Opcode::RETURN.as_u8(), // return result of second call + ]; + + let code_2: Vec = vec![Opcode::PUSH1.as_u8(), 0, Opcode::SUICIDE.as_u8()]; + + set_code(&mut handler, &address_1, code_1); + set_code(&mut handler, &address_2, code_2); + + set_balance(&mut handler, &caller, U256::from(1000_u32)); + set_balance(&mut handler, &address_1, U256::from(1000_u32)); + + handler + .begin_initial_transaction(false, Some(1000000)) + .unwrap(); + + let result = + handler.execute_call(address_1, transfer, input, transaction_context); + + // The transaction should consume more than twice the cost of a selfdestruct + assert!(handler.gas_used() > 10000); + + // The second call succeeded + assert_eq!( + Ok((ExitReason::Succeed(ExitSucceed::Returned), None, vec![1],)), + result, + ) + } + + #[test] + fn contract_selfdestruct_itself_has_no_balance_left() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let caller = H160::from_str("095e7baea6a6c7c4c2dfeb977efac326af552d87").unwrap(); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + false, + None, + ); + + let target_destruct = + H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let contract = H160::from_low_u64_be(523_u64); + + // Contract selfdestruct itself and then tries to selfdestruct to target_destruct. + // The second selfdestruct is ignored and the first remove the balance of contract + let code = hex::decode("60003560085730ff5b600080808080305af15073a94f5374fce5edbc8e2a8697c15331677e6ebf0bff").unwrap(); + + set_code(&mut handler, &contract, code); + + set_balance(&mut handler, &contract, U256::from(100000_u32)); + + let result = handler.call_contract( + caller, + contract, + None, + vec![0xff], + Some(100_000), + false, + ); + + match result { + Ok(exec_out) if exec_out.is_success() => { + assert_eq!( + get_balance(&mut handler, &contract), + U256::zero(), + "Contract balance is not 0" + ); + + assert_eq!( + get_balance(&mut handler, &target_destruct), + U256::zero(), + "target_destruct balance is not 0" + ); + } + _ => panic!("Execution failed"), + } + } + + // According EIP-2929, the created address should still be hot even if the creation fails + #[test] + fn address_still_marked_as_hot_after_creation_fails() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let gas_price = U256::from(21000); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + true, + None, + ); + + let contrac_addr = + H160::from_str("095e7baea6a6c7c4c2dfeb977efac326af552d87").unwrap(); + let expected_address = handler + .create_address(CreateScheme::Legacy { + caller: contrac_addr, + }) + .unwrap_or_default(); + + // Tries to CREATE a contract (that will revert) + let contract_code = vec![ + Opcode::PUSH5.as_u8(), + 0x60, + 0x00, + 0x60, + 0x00, + 0xfd, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::MSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x05, + Opcode::PUSH1.as_u8(), + 0x1b, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::CREATE.as_u8(), + ]; + set_code(&mut handler, &contrac_addr, contract_code); + let input = vec![0_u8]; + let transaction_context = + TransactionContext::new(caller, contrac_addr, U256::zero()); + let transfer: Option = None; + + handler + .begin_initial_transaction(false, Some(1000000)) + .unwrap(); + + let _ = handler.execute_call(contrac_addr, transfer, input, transaction_context); + + let exist = handler.is_colliding(expected_address).unwrap(); + + assert!(!exist, "Expected address should not exist"); + + // After the `execute_call` expected address should be marked as hot + let is_hot = handler.is_address_hot(expected_address).unwrap(); + + assert!(is_hot, "Expected address is cold where it should be hot"); + } + + #[test] + fn precompile_failure_are_not_fatal() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let mut handler = EvmHandler::new( + &mut host, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + U256::from(21000), + false, + None, + ); + + handler + .begin_initial_transaction(false, Some(150000)) + .unwrap(); + + handler + .begin_inter_transaction(false, Some(150000)) + .unwrap(); + + let ecmul = H160::from_low_u64_be(7u64); + + // ecmul -> point not on curve fail + let failing_input = hex::decode( + "\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 0f00000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let transaction_context = TransactionContext::new(caller, ecmul, U256::zero()); + + let result = + handler.execute_call(ecmul, None, failing_input, transaction_context); + + let inter_result: Capture = + handler.end_inter_transaction(result); + + // Internal result is not a Fatal case anymore. + match inter_result { + Capture::Exit((exit_reason, _, _)) => match exit_reason { + ExitReason::Error(_) => (), + e => panic!("The exit reason should be an error but got {:?}.", e), + }, + Capture::Trap(_) => panic!("The internal result shouldn't be a trap case."), + } + } + + #[test] + fn inner_create_costs_gas() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 1000, + gas_price, + false, + None, + ); + + let address = H160::from_low_u64_be(210_u64); + + // Create an account with 0 wei and 4 FF as code + // PUSH13 0x63FFFFFFFF6000526004601CF3 + // PUSH1 0 + // MSTORE + // PUSH1 13 + // PUSH1 19 + // PUSH1 0 + // CREATE + // STOP + + let code: Vec = vec![ + Opcode::PUSH13.as_u8(), // 3 gas + 0x63, // 3 gas + 0xff, + 0xff, + 0xff, + 0xff, + 0x60, // 3 gas + 0x00, + 0x52, // 6 gas + 0x60, // 3 gas + 0x04, + 0x60, // 3 gas + 0x1c, + 0xf3, + Opcode::PUSH1.as_u8(), // 3 gas + 0, + Opcode::MSTORE.as_u8(), // 6 gas + Opcode::PUSH1.as_u8(), // 3 gas + 13, + Opcode::PUSH1.as_u8(), // 3 gas + 19, + Opcode::PUSH1.as_u8(), // 3 gas + 0, + Opcode::CREATE.as_u8(), // 32000 gas + 800 gas (code_deposit_cost) + 2 gas (init_code_cost) + Opcode::STOP.as_u8(), + ]; + + set_code(&mut handler, &address, code); + set_balance(&mut handler, &caller, U256::from(99_u32)); + + let result = + handler.call_contract(caller, address, None, vec![], Some(1000000), false); + + assert_eq!( + Ok(ExecutionOutcome { + gas_used: 53841, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 40549406 + }), + result, + ) + } + + #[test] + fn exceed_max_create_init_code_size_fail_with_error() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let initial_code = [1; 49153]; // MAX_INIT_CODE_SIZE + 1 + + let scheme = CreateScheme::Legacy { caller }; + let address = handler.create_address(scheme).unwrap_or_default(); + + handler + .begin_initial_transaction(false, Some(150000)) + .unwrap(); + + let result = handler + .execute_create(caller, U256::zero(), initial_code.to_vec(), address) + .unwrap(); + + assert_eq!(result.0, ExitReason::Error(ExitError::CreateContractLimit)); + } + + #[test] + fn create_fails_with_max_nonce() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + U256::one(), + false, + None, + ); + + set_balance(&mut handler, &caller, U256::from(1000000000)); + set_nonce(&mut handler, &caller, u64::MAX); + + let init_code = hex::decode("60006000fd").unwrap(); // Just revert + + let capture = handler.create( + caller, + CreateScheme::Legacy { caller }, + U256::zero(), + init_code, + None, + ); + + match capture { + Capture::Exit((ExitReason::Error(ExitError::MaxNonce), ..)) => (), + _ => panic!("Create doesn't fail with Error MaxNonce"), + } + } + + #[test] + fn record_call_stipend_when_balance_not_enough_for_inner_call() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(523_u64); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 1000, + U256::from(21000), + false, + None, + ); + + let address1 = H160::from_low_u64_be(210_u64); + let address2 = H160::from_low_u64_be(211_u64); + + let code1: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0, // return size + Opcode::PUSH1.as_u8(), + 0, // return offset + Opcode::PUSH1.as_u8(), + 0, // arg size + Opcode::PUSH1.as_u8(), + 0, // arg offset + Opcode::PUSH1.as_u8(), + 100, // value + Opcode::PUSH1.as_u8(), + 211, // address + Opcode::PUSH1.as_u8(), + 0, // gas + Opcode::CALL.as_u8(), // Call should fail due to insufficient balance, leaving a zero on the stack + Opcode::PUSH1.as_u8(), + 0, // memory index for mstore + Opcode::MSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 1, // size of return + Opcode::PUSH1.as_u8(), + 31, // memory index for return + Opcode::RETURN.as_u8(), + ]; + + let code2: Vec = vec![Opcode::STOP.as_u8()]; + + set_code(&mut handler, &address1, code1); + set_code(&mut handler, &address2, code2); + set_balance(&mut handler, &caller, U256::from(100_u32)); + + let result = handler + .call_contract( + caller, + address1, + Some(U256::from(99_u32)), + vec![], + Some(1000000), + false, + ) + .unwrap(); + + // Gas cost: 21000(BASE) + 10 * 3(PUSH1) + 3(MSTORE) + 3(Memory expansion) + 100(Call) + 9000(Positive value cost) - 2300(Call Stipend) + assert_eq!(result.gas_used, 27836); + + assert_eq!( + result.result, + ExecutionResult::CallSucceeded(ExitSucceed::Returned, vec![0]) + ); + } + + // eip-161 + #[test] + fn eip161_gas_consumption_rules_for_suicide() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(111_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + false, + None, + ); + + // SUICIDE would charge 25,000 gas when the destination is non-existent, + // now the charge SHALL only be levied if the operation transfers more than zero value + // and the destination account is dead. + + let cases = [ + (100_u32, 100_u8, 51003_u64), // transfer > 0 && non-existent destination + (100, 100, 26003), // transfer > 0 && touched destination + (0, 101, 26003), // transfer == 0 && non-existent destination + (0, 101, 26003), // transfer == 0 && touched destination + ]; + + for (balance, destination, expected_gas) in cases { + let address = H160::from_low_u64_be(110_u64); + set_balance(&mut handler, &address, U256::from(balance)); + set_code( + &mut handler, + &address, + vec![ + Opcode::PUSH1.as_u8(), // 3 gas + destination, + Opcode::SUICIDE.as_u8(), // 5000(BASE) + (25000 gas if transfer > 0 && destination is dead) + ], + ); + + let result = handler + .call_contract( + caller, + address, + Some(U256::zero()), + vec![], + Some(1000000), + false, + ) + .unwrap(); + + assert_eq!(result.gas_used, expected_gas); + + assert_eq!( + result.result, + ExecutionResult::CallSucceeded(ExitSucceed::Suicided, vec![]) + ); + + // At the end of the transaction, any account touched by the execution of that transaction + // which is now empty SHALL instead become non-existent (i.e. deleted). + assert!(!handler.exists(address)); + } + } + + #[test] + fn eip161_gas_consumption_rules_for_call() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_account_storage().unwrap(); + let config = Config::shanghai(); + let caller = H160::from_low_u64_be(111_u64); + + let gas_price = U256::from(21000); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS * 10000, + gas_price, + false, + None, + ); + + // CALL would charge 25,000 gas when the destination is non-existent, + // now the charge SHALL only be levied if the operation transfers more than zero value + // and the destination account is dead. + + let address = H160::from_low_u64_be(110_u64); + + let cases = [ + (100_u32, 10_u8, 100_u8, 52821_u64), // transfer > 0 && non-existent destination + (100, 10, 100, 27821), // transfer > 0 && touched destination + (100, 0, 101, 21121), // transfer == 0 && non-existent destination + (100, 0, 101, 21121), // transger == 0 && touched destination + (0, 10, 102, 52821), // unsufficient balance && transfer > 0 && non-existent destination + (0, 10, 100, 27821), // unsufficient balance && transfer > 0 && touched destination + ]; + + for (balance, value, destination, expected_gas) in cases { + set_balance(&mut handler, &address, U256::from(balance)); + set_code( + &mut handler, + &address, + vec![ + Opcode::PUSH1.as_u8(), // 3 gas + 0, // return size + Opcode::PUSH1.as_u8(), // 3 gas + 0, // return offset + Opcode::PUSH1.as_u8(), // 3 gas + 0, // arg size + Opcode::PUSH1.as_u8(), // 3 gas + 0, // arg offset + Opcode::PUSH1.as_u8(), // 3 gas + value, // non-zero value + Opcode::PUSH1.as_u8(), // 3 gas + destination, // address + Opcode::PUSH1.as_u8(), // 3 gas + 0, + // 100(BASE) + 9000(non-zero value cost) + // + (25000 gas if transfer > 0 && destination is dead) - 2300(call stipend) + Opcode::CALL.as_u8(), + ], + ); + + let result = handler + .call_contract( + caller, + address, + Some(U256::zero()), + vec![], + Some(1000000), + false, + ) + .unwrap(); + + assert_eq!(result.gas_used, expected_gas); + + assert_eq!( + result.result, + ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]) + ); + } + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/lib.rs b/etherlink/kernel_calypso2/evm_execution/src/lib.rs new file mode 100755 index 000000000000..c72670a1d812 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/lib.rs @@ -0,0 +1,4037 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023-2025 Functori +// +// SPDX-License-Identifier: MIT + +//! Types and functions for Ethereum compatibility +//! +//! We need to read and write Ethereum specific values such +//! as addresses and values. +use crate::trace::TracerInput::{CallTracer, StructLogger}; +use account_storage::{AccountStorageError, EthereumAccountStorage}; +use alloc::borrow::Cow; +use alloc::collections::TryReserveError; +use crypto::hash::{ContractKt1Hash, HashTrait}; +use evm::executor::stack::PrecompileFailure; +use handler::{EvmHandler, ExecutionOutcome, ExecutionResult}; +use host::path::RefPath; +use primitive_types::{H160, H256, U256}; +use tezos_ethereum::block::BlockConstants; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_storage::StorageError; +use thiserror::Error; + +mod access_record; + +pub mod abi; +pub mod account_storage; +pub mod code_storage; +pub mod fa_bridge; +pub mod handler; +pub mod precompiles; +pub mod storage; +pub mod tick_model_opcodes; +pub mod trace; +pub mod transaction; +pub mod transaction_layer_data; +pub mod utilities; +pub mod withdrawal_counter; + +pub use evm::Config; + +extern crate alloc; +extern crate tezos_crypto_rs as crypto; +extern crate tezos_smart_rollup_debug as debug; +extern crate tezos_smart_rollup_host as host; + +use precompiles::PrecompileSet; +use trace::{ + CallTrace, CallTracerConfig, CallTracerInput, StructLoggerInput, TracerInput, +}; + +use crate::storage::tracer; + +#[derive(Error, Clone, Copy, Debug, Eq, PartialEq)] +pub enum DurableStorageError { + /// Some runtime error happened while using durable storage + #[error("Runtime error: {0:?}")] + RuntimeError(#[from] host::runtime::RuntimeError), + /// Some error happened while constructing the path to some + /// resource. + #[error("Path error: {0:?}")] + PathError(#[from] host::path::PathError), +} + +/// Errors when processing Ethereum transactions +/// +/// What could possibly go wrong? Some of these are place holders for now. +/// When we call an address without code, this should be treated as a simple +/// transfer for instance. +#[derive(Error, Debug, Eq, PartialEq)] +pub enum EthereumError { + /// An ethereum error happened inside a callback and we had to print it to + /// a string so we could wrap it in a `ExitFatal` error. We have lost the + /// exact variant, but we can retain the message. + #[error("Wrapped Ethereum error: {0}")] + WrappedError(Cow<'static, str>), + /// Calling a precompiled failed (implies there was a precompiled contract + /// at the call address. + #[error("Precompile call failed")] + PrecompileFailed(PrecompileFailure), + /// The SputnikVM runtime returned a Trap. This should be impossible. + #[error("Internal SputnikVM trap")] + InternalTrapError, + /// Something went wrong when using the durable storage for transactions + #[error("Error when using durable storage for transactions: {0}")] + EthereumStorageError(#[from] StorageError), + /// Something went wrong with an account in durable storage during a transaction + #[error("Error with an account in durable storage: {0}")] + EthereumAccountError(#[from] AccountStorageError), + /// A contract call transferred too much gas to sub-context or contract + /// call itself got too much gas + #[error("Gas limit overflow: {0}")] + GasLimitOverflow(U256), + /// The transaction stack has an unexpected size. + /// - First element tells the stack size at time of error. + /// - Second element telss if the error happened when we were expecting to deal + /// beginning or end of the initial transaction. + /// - The last element tells if the error happended at the beginning of a transaction + /// (if false, this happened at the commit or rollback of the transaction). + #[error( + "Inconsistent transaction stack: depth is {0}, is_initial is {1}, is_begin is {2}" + )] + InconsistentTransactionStack(usize, bool, bool), + /// The transaction data stack has an unexpected size. It should be the same size as + /// the transaction stack, but it isn't. + /// - first argument is transaction depth, + /// - second argument is the transaction data size. + #[error( + "Inconsistent transaction data size: transaction depth is {0}, transaction info depth {1}" + )] + InconsistentTransactionData(usize, usize), + /// Memory allocation error. Could not expand the capacity of a vector. + #[error("Vector expand error: {0}")] + VectorExpandError(#[from] TryReserveError), + /// The execution state is inconsistent in some way. This can only happen as a + /// result of a bug in the EvmHandler. + #[error("Inconsistent EvmHandler state: {0}")] + InconsistentState(Cow<'static, str>), + /// The execution failed because it spent more ticks than the one currently + /// available for the current run. + #[error("The transaction took more ticks than expected")] + OutOfTicks, + /// gas_limit * gas_price > u64::max + #[error("Gas payment overflowed u64::max")] + GasPaymentOverflow, + /// Converting non-execution fees to gas overflowed u64::max + #[error("Gas for fees overflowed u64::max in conversion")] + FeesToGasOverflow, + /// Underflow of gas limit when subtracting gas for fees + #[error("Insufficient gas to cover the non-execution fees")] + GasToFeesUnderflow, + #[error("Error while trying to trace a transaction: {0}")] + Tracer(#[from] tracer::Error), +} + +fn trace_outcome( + handler: EvmHandler, + is_success: bool, + output: Option<&[u8]>, + gas_used: u64, + transaction_hash: Option, +) -> Result<(), EthereumError> { + tracer::store_trace_failed(handler.host, is_success, &transaction_hash)?; + tracer::store_trace_gas(handler.host, gas_used, &transaction_hash)?; + if let Some(return_value) = output { + tracer::store_return_value(handler.host, return_value, &transaction_hash)?; + }; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn call_trace_outcome( + handler: EvmHandler, + base_call_type: &str, + call_data: Vec, + caller: H160, + address: Option, + value: Option, + gas_limit: Option, + with_logs: bool, + result: &ExecutionOutcome, + transaction_hash: Option, +) -> Result<(), EthereumError> { + let mut call_trace = CallTrace::new_minimal_trace( + base_call_type.into(), + caller, + value.unwrap_or_default(), + result.gas_used, + call_data, + 0, // Initial call, we start at depth 0. + ); + + if base_call_type != "CREATE" { + call_trace.add_to(address); + } else { + call_trace.add_to(result.new_address()); + } + + call_trace.add_gas(gas_limit); + call_trace.add_output(result.output().as_ref().map(|res| res.to_vec())); + + if with_logs { + call_trace.add_logs(Some(result.logs.clone())) + } + match result.result { + ExecutionResult::CallReverted(ref error) => { + call_trace.add_error(Some(error.clone())) + } + ExecutionResult::Error(ref exit_error) => { + call_trace.add_error(Some(format!("{:?}", exit_error).into())) + } + ExecutionResult::FatalError(ref fatal_error) => { + call_trace.add_error(Some(format!("{:?}", fatal_error).into())) + } + ExecutionResult::OutOfTicks => call_trace.add_error(Some("OutOfTicks".into())), + ExecutionResult::TransferSucceeded + | ExecutionResult::CallSucceeded(_, _) + | ExecutionResult::ContractDeployed(_, _) => (), + }; + + tracer::store_call_trace(handler.host, call_trace, &transaction_hash)?; + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn call_trace_error( + handler: EvmHandler, + base_call_type: &str, + call_data: Vec, + caller: H160, + address: Option, + value: Option, + gas_limit: Option, + e: &EthereumError, + transaction_hash: Option, +) -> Result<(), EthereumError> { + let mut call_trace = CallTrace::new_minimal_trace( + base_call_type.into(), + caller, + value.unwrap_or_default(), + 0, + call_data, + 0, // Initial call, we start at depth 0. + ); + + call_trace.add_to(address); + call_trace.add_gas(gas_limit); + call_trace.add_error(Some(format!("{:?}", e).into())); + + tracer::store_call_trace(handler.host, call_trace, &transaction_hash)?; + Ok(()) +} + +/// Execute an Ethereum Transaction +/// +/// The function returns `Err` only if something is wrong with the kernel and/or the +/// rollup node. If the transaction ends by executing STOP, RETURN or SUICIDE, then this is +/// a _success_ (by Ethereum definition). Note that a REVERT instruction _can_ return +/// data even though it will mean rollback of the transaction effect. This is also true +/// for sub-transactions, ie, REVERT can _always_ return data. +/// +/// If the gas limit is given as `None` (there is no gas limit), then there will be no +/// accounting for gas usage at all. So the gas usage in the return value will be zero. +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +#[allow(clippy::too_many_arguments)] +pub fn run_transaction<'a, Host>( + host: &'a mut Host, + block: &'a BlockConstants, + evm_account_storage: &'a mut EthereumAccountStorage, + precompiles: &'a precompiles::PrecompileBTreeMap, + config: Config, + address: Option, + caller: H160, + call_data: Vec, + gas_limit: Option, + effective_gas_price: U256, + value: U256, + pay_for_gas: bool, + allocated_ticks: u64, + retriable: bool, + enable_warm_cold_access: bool, + tracer: Option, +) -> Result, EthereumError> +where + Host: Runtime, +{ + fn do_refund(outcome: &handler::ExecutionOutcome, pay_for_gas: bool) -> bool { + match outcome.result { + ExecutionResult::CallReverted(_) | ExecutionResult::OutOfTicks => pay_for_gas, + _ => pay_for_gas && outcome.is_success(), + } + } + + log!(host, Debug, "Going to run an Ethereum transaction\n - from address: {}\n - to address: {:?}", caller, address); + + let mut handler = handler::EvmHandler::<'_, Host>::new( + host, + evm_account_storage, + caller, + block, + &config, + precompiles, + allocated_ticks, + effective_gas_price, + enable_warm_cold_access, + tracer, + ); + + let call_data_for_tracing = if tracer.is_some() { + Some(call_data.clone()) + } else { + None + }; + + if (!pay_for_gas) + || handler.pre_pay_transactions(caller, gas_limit, effective_gas_price)? + { + let (result, base_call_type) = if let Some(address) = address { + ( + handler.call_contract( + caller, + address, + Some(value), + call_data, + gas_limit, + false, + ), + "CALL", + ) + } else { + // This is a create-contract transaction + ( + handler.create_contract(caller, Some(value), call_data, gas_limit), + "CREATE", + ) + }; + + match result { + Ok(result) => { + if result.result == ExecutionResult::OutOfTicks && retriable { + // The nonce must be incremented before the execution. Details here: https://gitlab.com/tezos/tezos/-/merge_requests/11998. + // In the EVM logic, the nonce is never decremented + // But with the ticks model, if the execution raises an 'out of ticks' error and if the transaction is 'retriable', the nonce must not be incremented + // But we can only know that after the execution, which is why we must decrement the nonce here + handler.decrement_nonce(caller)?; + } + + if do_refund(&result, pay_for_gas) { + // In case of `OutOfTicks` and the transaction can be + // retried, the gas is entirely refunded as it will be + // repaid in the next attempt + if result.result == ExecutionResult::OutOfTicks && retriable { + log!( + handler.borrow_host(), + Debug, + "The transaction exhausted the ticks of the \ + current reboot and is retriable: refunding all the gas." + ); + handler.repay_gas(caller, gas_limit, effective_gas_price)?; + } else { + let unused_gas = gas_limit.map(|gl| gl - result.gas_used); + handler.repay_gas(caller, unused_gas, effective_gas_price)?; + } + } + + if let Some(call_data) = call_data_for_tracing { + if let Some(StructLogger(StructLoggerInput { + transaction_hash, + .. + })) = tracer + { + trace_outcome( + handler, + result.is_success(), + result.output(), + result.gas_used, + transaction_hash, + )? + } else if let Some(CallTracer(CallTracerInput { + transaction_hash, + config: CallTracerConfig { with_logs, .. }, + })) = tracer + { + call_trace_outcome( + handler, + base_call_type, + call_data, + caller, + address, + Some(value), + gas_limit, + with_logs, + &result, + transaction_hash, + )?; + } + } + + Ok(Some(result)) + } + Err(e) => { + if let Some(call_data) = call_data_for_tracing { + if let Some(StructLogger(StructLoggerInput { + transaction_hash, + .. + })) = tracer + { + trace_outcome(handler, false, None, 0, transaction_hash)? + } else if let Some(CallTracer(CallTracerInput { + transaction_hash, + .. + })) = tracer + { + call_trace_error( + handler, + base_call_type, + call_data, + caller, + address, + Some(value), + gas_limit, + &e, + transaction_hash, + )?; + } + } + Err(e) + } + } + } else { + // caller was unable to pay for the gas limit + if pay_for_gas { + log!(host, Info, "Caller was unable to pre-pay the transaction") + }; + Ok(None) + } +} +pub const NATIVE_TOKEN_TICKETER_PATH: RefPath = + RefPath::assert_from(b"/evm/world_state/ticketer"); + +/// Reads the ticketer address set by the installer, if any, encoded in b58. +pub fn read_ticketer(host: &impl Runtime) -> Option { + let ticketer = host.store_read_all(&NATIVE_TOKEN_TICKETER_PATH).ok()?; + + let kt1_b58 = String::from_utf8(ticketer.to_vec()).ok()?; + ContractKt1Hash::from_b58check(&kt1_b58).ok() +} + +// Path to the fast withdrawals feature flag. If there is nothing at this +// path, fast withdrawals are not used. +pub const ENABLE_FAST_WITHDRAWAL: RefPath = + RefPath::assert_from(b"/evm/world_state/feature_flags/enable_fast_withdrawal"); + +pub fn fast_withdrawals_enabled(host: &Host) -> bool { + host.store_read_all(&ENABLE_FAST_WITHDRAWAL).is_ok() +} + +#[cfg(test)] +mod test { + use crate::account_storage::EthereumAccount; + + use super::*; + use account_storage::{ + account_path, init_account_storage as init_evm_account_storage, + EthereumAccountStorage, + }; + use evm::executor::stack::Log; + use evm::{ExitError, ExitSucceed, Opcode}; + use handler::ExecutionOutcome; + use primitive_types::{H160, H256}; + use std::str::FromStr; + use std::vec; + use tezos_ethereum::block::BlockFees; + use tezos_ethereum::tx_common::EthereumTransactionCommon; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_evm_runtime::runtime::Runtime; + + // The compiled initialization code for the Ethereum demo contract given + // as an example in kernel_evm/solidity_examples/storage.sol + const STORAGE_CONTRACT_INITIALIZATION: &str = "608060405234801561001057600080fd5b5061017f806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80634e70b1dc1461004657806360fe47b1146100645780636d4ce63c14610080575b600080fd5b61004e61009e565b60405161005b91906100d0565b60405180910390f35b61007e6004803603810190610079919061011c565b6100a4565b005b6100886100ae565b60405161009591906100d0565b60405180910390f35b60005481565b8060008190555050565b60008054905090565b6000819050919050565b6100ca816100b7565b82525050565b60006020820190506100e560008301846100c1565b92915050565b600080fd5b6100f9816100b7565b811461010457600080fd5b50565b600081359050610116816100f0565b92915050565b600060208284031215610132576101316100eb565b5b600061014084828501610107565b9150509291505056fea2646970667358221220ec57e49a647342208a1f5c9b1f2049bf1a27f02e19940819f38929bf67670a5964736f6c63430008120033"; + // call: num + const STORAGE_CONTRACT_CALL_NUM: &str = "4e70b1dc"; + // call: set(42) + const STORAGE_CONTRACT_CALL_SET42: &str = + "60fe47b1000000000000000000000000000000000000000000000000000000000000002a"; + + const CONFIG: Config = Config { + // The current implementation doesn't support Shanghai call + // stack limit of 256. We need to set a lower limit until we + // have switched to a head-based recursive calls. + call_stack_limit: 256, + ..Config::shanghai() + }; + + // The compiled initialization code for the Ethereum demo contract given + // as an example in kernel_evm/solidity_examples/erc20tok.sol + const ERC20_CONTRACT_INITIALISATION: &str = "60806040526040518060400160405280601381526020017f536f6c6964697479206279204578616d706c6500000000000000000000000000815250600390816200004a91906200033c565b506040518060400160405280600781526020017f534f4c4259455800000000000000000000000000000000000000000000000000815250600490816200009191906200033c565b506012600560006101000a81548160ff021916908360ff160217905550348015620000bb57600080fd5b5062000423565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200014457607f821691505b6020821081036200015a5762000159620000fc565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620001c47fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000185565b620001d0868362000185565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200021d620002176200021184620001e8565b620001f2565b620001e8565b9050919050565b6000819050919050565b6200023983620001fc565b62000251620002488262000224565b84845462000192565b825550505050565b600090565b6200026862000259565b620002758184846200022e565b505050565b5b818110156200029d57620002916000826200025e565b6001810190506200027b565b5050565b601f821115620002ec57620002b68162000160565b620002c18462000175565b81016020851015620002d1578190505b620002e9620002e08562000175565b8301826200027a565b50505b505050565b600082821c905092915050565b60006200031160001984600802620002f1565b1980831691505092915050565b60006200032c8383620002fe565b9150826002028217905092915050565b6200034782620000c2565b67ffffffffffffffff811115620003635762000362620000cd565b5b6200036f82546200012b565b6200037c828285620002a1565b600060209050601f831160018114620003b457600084156200039f578287015190505b620003ab85826200031e565b8655506200041b565b601f198416620003c48662000160565b60005b82811015620003ee57848901518255600182019150602085019450602081019050620003c7565b868310156200040e57848901516200040a601f891682620002fe565b8355505b6001600288020188555050505b505050505050565b610d6a80620004336000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461016857806370a082311461018457806395d89b41146101b4578063a0712d68146101d2578063a9059cbb146101ee578063dd62ed3e1461021e576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b661024e565b6040516100c391906109be565b60405180910390f35b6100e660048036038101906100e19190610a79565b6102dc565b6040516100f39190610ad4565b60405180910390f35b6101046103ce565b6040516101119190610afe565b60405180910390f35b610134600480360381019061012f9190610b19565b6103d4565b6040516101419190610ad4565b60405180910390f35b610152610585565b60405161015f9190610b88565b60405180910390f35b610182600480360381019061017d9190610ba3565b610598565b005b61019e60048036038101906101999190610bd0565b61066f565b6040516101ab9190610afe565b60405180910390f35b6101bc610687565b6040516101c991906109be565b60405180910390f35b6101ec60048036038101906101e79190610ba3565b610715565b005b61020860048036038101906102039190610a79565b6107ec565b6040516102159190610ad4565b60405180910390f35b61023860048036038101906102339190610bfd565b610909565b6040516102459190610afe565b60405180910390f35b6003805461025b90610c6c565b80601f016020809104026020016040519081016040528092919081815260200182805461028790610c6c565b80156102d45780601f106102a9576101008083540402835291602001916102d4565b820191906000526020600020905b8154815290600101906020018083116102b757829003601f168201915b505050505081565b600081600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516103bc9190610afe565b60405180910390a36001905092915050565b60005481565b600081600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104629190610ccc565b9250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104b89190610ccc565b9250508190555081600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461050e9190610d00565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516105729190610afe565b60405180910390a3600190509392505050565b600560009054906101000a900460ff1681565b80600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546105e79190610ccc565b92505081905550806000808282546105ff9190610ccc565b92505081905550600073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106649190610afe565b60405180910390a350565b60016020528060005260406000206000915090505481565b6004805461069490610c6c565b80601f01602080910402602001604051908101604052809291908181526020018280546106c090610c6c565b801561070d5780601f106106e25761010080835404028352916020019161070d565b820191906000526020600020905b8154815290600101906020018083116106f057829003601f168201915b505050505081565b80600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546107649190610d00565b925050819055508060008082825461077c9190610d00565b925050819055503373ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516107e19190610afe565b60405180910390a350565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461083d9190610ccc565b9250508190555081600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546108939190610d00565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516108f79190610afe565b60405180910390a36001905092915050565b6002602052816000526040600020602052806000526040600020600091509150505481565b600081519050919050565b600082825260208201905092915050565b60005b8381101561096857808201518184015260208101905061094d565b60008484015250505050565b6000601f19601f8301169050919050565b60006109908261092e565b61099a8185610939565b93506109aa81856020860161094a565b6109b381610974565b840191505092915050565b600060208201905081810360008301526109d88184610985565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610a10826109e5565b9050919050565b610a2081610a05565b8114610a2b57600080fd5b50565b600081359050610a3d81610a17565b92915050565b6000819050919050565b610a5681610a43565b8114610a6157600080fd5b50565b600081359050610a7381610a4d565b92915050565b60008060408385031215610a9057610a8f6109e0565b5b6000610a9e85828601610a2e565b9250506020610aaf85828601610a64565b9150509250929050565b60008115159050919050565b610ace81610ab9565b82525050565b6000602082019050610ae96000830184610ac5565b92915050565b610af881610a43565b82525050565b6000602082019050610b136000830184610aef565b92915050565b600080600060608486031215610b3257610b316109e0565b5b6000610b4086828701610a2e565b9350506020610b5186828701610a2e565b9250506040610b6286828701610a64565b9150509250925092565b600060ff82169050919050565b610b8281610b6c565b82525050565b6000602082019050610b9d6000830184610b79565b92915050565b600060208284031215610bb957610bb86109e0565b5b6000610bc784828501610a64565b91505092915050565b600060208284031215610be657610be56109e0565b5b6000610bf484828501610a2e565b91505092915050565b60008060408385031215610c1457610c136109e0565b5b6000610c2285828601610a2e565b9250506020610c3385828601610a2e565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610c8457607f821691505b602082108103610c9757610c96610c3d565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610cd782610a43565b9150610ce283610a43565b9250828203905081811115610cfa57610cf9610c9d565b5b92915050565b6000610d0b82610a43565b9150610d1683610a43565b9250828201905080821115610d2e57610d2d610c9d565b5b9291505056fea26469706673582212207b919b45bc1fe90bc3f638a6d1e91b799fb6d2f27c0f6ebaa634fc5258371a1a64736f6c63430008120033"; + + const DUMMY_ALLOCATED_TICKS: u64 = 1_000_000_000; + + fn set_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } + } + + fn set_storage( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + index: &H256, + value: &H256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.set_storage(host, index, value).unwrap(); + } + + fn get_storage( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + index: &H256, + ) -> H256 { + let account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.get_storage(host, index).unwrap() + } + + fn get_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + ) -> U256 { + let account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.balance(host).unwrap() + } + + // Simple utility function to set some code for an account + fn set_account_code( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + code: &[u8], + ) { + let path = account_path(address).unwrap(); + let mut account = evm_account_storage.get_or_create(host, &path).unwrap(); + account.set_code(host, code).unwrap(); + } + + fn get_code( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + ) -> Vec { + let account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.code(host).unwrap() + } + + fn bump_nonce( + host: &mut impl Runtime, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + ) { + let path = account_path(address).unwrap(); + let mut account = evm_account_storage.get_or_create(host, &path).unwrap(); + account.increment_nonce(host).unwrap(); + } + + fn get_nonce( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + ) -> u64 { + let account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.nonce(host).unwrap() + } + + fn dummy_first_block() -> BlockConstants { + let block_fees = BlockFees::new( + U256::one(), + U256::from(12345), + U256::from(2_000_000_000_000u64), + ); + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ) + } + + #[test] + fn transfer_without_sufficient_funds_fails() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = H160::from_low_u64_be(234213); + let caller = H160::from_low_u64_be(985493); + let call_data: Vec = vec![]; + let transaction_value = U256::from(100_u32); + let config = Config::shanghai(); + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(22099), + ); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &callee, + U256::from(2), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(callee), + caller, + call_data, + Some(22000), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: config.gas_transaction_call, + logs: vec![], + result: ExecutionResult::Error(ExitError::OutOfFund), + withdrawals: vec![], + estimated_ticks_used: 0, + })); + + assert_eq!(expected_result, result); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + U256::from(99) + ); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &callee), + U256::from(2) + ); + } + + #[test] + fn transfer_funds_with_sufficient_balance() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = H160::from_low_u64_be(82193); + let caller = H160::from_low_u64_be(1234); + let call_data: Vec = vec![]; + let transaction_value = U256::from(100_u32); + let config = Config::shanghai(); + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &callee, + U256::from(3), + ); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(21101), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(callee), + caller, + call_data, + Some(21000), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: config.gas_transaction_call, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 0, + })); + + assert_eq!(expected_result, result); + + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &callee), + U256::from(103) + ); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + U256::from(1) + ); + } + + #[test] + fn create_contract_fails_with_insufficient_funds() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(328794); + let transaction_value = U256::from(100_u32); + let call_data: Vec = hex::decode(STORAGE_CONTRACT_INITIALIZATION).unwrap(); + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(10), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + None, + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: 0, + logs: vec![], + result: ExecutionResult::Error(ExitError::OutOfFund), + withdrawals: vec![], + estimated_ticks_used: 0, + })); + + assert_eq!(expected_result, result); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + U256::from(10) + ); + } + + #[test] + fn create_contract_succeeds_with_valid_initialization() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + let call_data: Vec = hex::decode(STORAGE_CONTRACT_INITIALIZATION).unwrap(); + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(1_000_000), + ); + // gas limit was estimated using Remix on Shanghai network (256,842) + // plus a safety margin for gas accounting discrepancies + let gas_limit = 300_000; + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let new_address = + Some(H160::from_str("907823e0a92f94355968feb2cbf0fbb594fe3214").unwrap()); + + assert!(result.is_ok(), "execution should have succeeded"); + let result = result.unwrap(); + assert!( + result.is_some(), + "execution should have produced some outcome" + ); + let result = result.unwrap(); + assert!(result.is_success(), "transaction should have succeeded"); + assert_eq!( + new_address, + result.new_address(), + "Contract addess not its expected value" + ); + + // test of a call + let call_data2 = hex::decode(STORAGE_CONTRACT_CALL_NUM).unwrap(); + let result2 = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + new_address, + caller, + call_data2, + Some(31000), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + assert!(result2.is_ok(), "execution should have succeeded"); + let result = result2.unwrap(); + assert!( + result.is_some(), + "execution should have produced some outcome" + ); + let result = result.unwrap(); + assert!(result.is_success(), "transaction should have succeeded"); + assert!( + result.output().is_some(), + "Call should have returned a value" + ); + let value = U256::from_little_endian(result.output().unwrap()); + assert_eq!(U256::zero(), value, "unexpected result value"); + + let call_data_set = hex::decode(STORAGE_CONTRACT_CALL_SET42).unwrap(); + let result3 = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + new_address, + caller, + call_data_set, + Some(100000), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + assert!(result3.is_ok(), "execution should have succeeded"); + let result = result3.unwrap(); + assert!( + result.is_some(), + "execution should have produced some outcome" + ); + let result = result.unwrap(); + assert!(result.is_success(), "transaction should have succeeded"); + assert!( + result.output().is_some(), + "Call should have returned a value" + ); + let value = U256::from_big_endian(result.output().unwrap()); + assert_eq!(U256::zero(), value, "unexpected result value"); + + let result2 = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + new_address, + caller, + hex::decode(STORAGE_CONTRACT_CALL_NUM).unwrap(), + Some(31000), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + assert!(result2.is_ok(), "execution should have succeeded"); + let result = result2.unwrap(); + assert!( + result.is_some(), + "execution should have produced some outcome" + ); + let result = result.unwrap(); + assert!(result.is_success(), "transaction should have succeeded"); + assert!( + result.output().is_some(), + "Call should have returned a value" + ); + let value = U256::from_big_endian(result.output().unwrap()); + assert_eq!(U256::from(42), value, "unexpected result value"); + } + + #[test] + fn create_contract_erc20_succeeds() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + let call_data: Vec = hex::decode(ERC20_CONTRACT_INITIALISATION).unwrap(); + + // gas_limit estimated using remix on shanghai network (1,631,430) + // plus a 50% margin for gas accounting discrepancies + let gas_limit = 2_400_000; + let gas_price = U256::from(21000); + + // the test is not to check that account can prepay, + // so we can choose the balance depending on set gas limit + let balance = gas_price.saturating_mul(gas_limit.into()); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + balance, + ); + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + assert!(result.is_ok()); + let result = result.unwrap(); + assert!(result.is_some()); + let result = result.unwrap(); + assert!(result.is_success()); + assert_eq!( + Some(H160::from_str("907823e0a92f94355968feb2cbf0fbb594fe3214").unwrap()), + result.new_address() + ); + } + + #[test] + fn create_contract_fails_when_initialization_fails() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(2893); + let transaction_value = U256::from(100_u32); + // Some EVM instructions. They are all valid, but the last one is opcode + // 0xFE, which is the designated INVALID opcode, so running this code + // snippet must fail. + let call_data: Vec = hex::decode("602e600055600054600154fe").unwrap(); + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(100), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + None, + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: 0, + logs: vec![], + result: ExecutionResult::Error(ExitError::DesignatedInvalid), + withdrawals: vec![], + estimated_ticks_used: 1187236, + })); + + assert_eq!(expected_result, result); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + U256::from(100) + ); + } + + #[test] + fn call_non_existing_contract() { + // Arange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(100000), + ); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(22000), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000; // base cost + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 0, + })); + + assert_eq!(expected_result, result); + } + + #[test] + //this is based on https://eips.ethereum.org/EIPS/eip-155 + fn test_signatures() { + let (sk, _address) = tezos_ethereum::tx_common::string_to_sk_and_address_unsafe( + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string(), + ); + let m: [u8; 32] = hex::decode( + "daf5a779ae972f972197303d7b574746c7ef83eadac0f2791ad23db92e4c8e53", + ) + .unwrap() + .try_into() + .unwrap(); + let mes = libsecp256k1::Message::parse(&m); + let (s, _ri) = libsecp256k1::sign(&mes, &sk); + assert_eq!( + hex::encode(s.r.b32()), + "28ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276" + .to_string() + ); + assert_eq!( + hex::encode(s.s.b32()), + "67cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83" + ) + } + + #[test] + fn test_signature_to_address() { + let test_list = [ + ( + "4f3edf983ac636a65a842ce7c78d9aa706d3b113bce9c46f30d7d21715b23b1d", + "90F8bf6A479f320ead074411a4B0e7944Ea8c9C1", + ), + ( + "DC38EE117CAE37750EB1ECC5CFD3DE8E85963B481B93E732C5D0CB66EE6B0C9D", + "c5ed5d9b9c957be2baa01c16310aa4d1f8bc8e6f", + ), + ( + "80b28170e7c2cb2145c052d622ced9de477abcb287e0d23f07263cc30a260534", + "D0a2dBb5e6F757fd2066a7664f413CAAC504BC95", + ), + ]; + test_list.iter().fold((), |_, (s, ea)| { + let (_, a) = + tezos_ethereum::tx_common::string_to_sk_and_address_unsafe(s.to_string()); + let value: [u8; 20] = hex::decode(ea).unwrap().try_into().unwrap(); + let ea = value.into(); + assert_eq!(a, ea); + }) + } + + #[test] + fn test_caller_classic() { + let (_sk, address_from_sk) = + tezos_ethereum::tx_common::string_to_sk_and_address_unsafe( + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string(), + ); + let encoded = + "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(); + let transaction = EthereumTransactionCommon::from_hex(encoded).unwrap(); + let address = transaction.caller().unwrap(); + let expected_address_string: [u8; 20] = + hex::decode("9d8A62f656a8d1615C1294fd71e9CFb3E4855A4F") + .unwrap() + .try_into() + .unwrap(); + let expected_address: H160 = expected_address_string.into(); + assert_eq!(expected_address, address); + assert_eq!(expected_address, address_from_sk) + } + + #[test] + fn test_signed_classic_transaction() { + let string_sk = + "4646464646464646464646464646464646464646464646464646464646464646" + .to_string(); + let encoded = + "f86c098504a817c800825208943535353535353535353535353535353535353535880de0b6b3a76400008025a028ef61340bd939bc2195fe537567866003e1a15d3c71ff63e1590620aa636276a067cbe9d8997f761aecb703304b3800ccf555c9f3dc64214b297fb1966a3b6d83".to_string(); + let expected_transaction = EthereumTransactionCommon::from_hex(encoded).unwrap(); + + let transaction = expected_transaction.sign_transaction(string_sk).unwrap(); + assert_eq!(expected_transaction, transaction) + } + + #[test] + fn call_simple_return_contract() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let code = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::RETURN.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + let all_the_gas = 25_000; + let gas_price = U256::from(1); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 6; // execution cost + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Returned, vec![]), + withdrawals: vec![], + estimated_ticks_used: 64163, + })); + + assert_eq!(expected_result, result); + } + + #[test] + fn call_simple_revert_contract() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(1); + let code = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::REVERT.as_u8(), + ]; + let init_balance = 22_000; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + init_balance.into(), + ); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(init_balance), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 2 * 3; // execution cost (only push) + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallReverted(vec![]), + withdrawals: vec![], + estimated_ticks_used: 63539, + })); + + assert_eq!(expected_result, result); + + // Some gas is returned to the send after the transaction is reverted + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + gas_price.saturating_mul((init_balance - expected_gas).into()) + ) + } + + #[test] + fn call_contract_with_invalid_opcode() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(21000); + let code = vec![ + Opcode::INVALID.as_u8(), + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::REVERT.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + None, + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: 0, + logs: vec![], + result: ExecutionResult::Error(ExitError::DesignatedInvalid), + withdrawals: vec![], + estimated_ticks_used: 52333, + })); + + assert_eq!(expected_result, result); + } + + #[test] + fn no_transfer_when_contract_call_fails() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let caller = H160::from_low_u64_be(118_u64); + let gas_price = U256::from(21000); + + let address = H160::from_low_u64_be(117_u64); + let code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::PUSH1.as_u8(), + 0u8, + Opcode::INVALID.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &address, &code); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(101_u32), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(address), + caller, + vec![], + None, + gas_price, + U256::from(100), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: 0, + logs: vec![], + result: ExecutionResult::Error(ExitError::DesignatedInvalid), + withdrawals: vec![], + estimated_ticks_used: 63539, + })); + + assert_eq!(expected_result, result); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + U256::from(101_u32) + ); + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &address), + U256::zero() + ); + } + + #[test] + fn call_precompiled_contract() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let target = H160::from_low_u64_be(4u64); // identity contract + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let caller = H160::from_low_u64_be(118u64); + let data = [1u8; 32]; // Need some data to make it a contract call + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 22006.into(), + ); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + data.to_vec(), + Some(22001), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 18 // execution cost + + 32 * CONFIG.gas_transaction_non_zero_data; // transaction data cost + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Returned, vec![1u8; 32]), + withdrawals: vec![], + estimated_ticks_used: 42_000 + 35 * 32, + })); + + assert_eq!(expected_result, result); + } + + #[test] + fn call_ecrecover() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + // example from https://www.evm.codes/precompiled?fork=shanghai + let data_str = "456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3000000000000000000000000000000000000000000000000000000000000001c9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac80388256084f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada"; + let data = hex::decode(data_str) + .expect("Data should have been decoded from hex to bytes"); + // targes precompiled 0x01: ecrecover + let target = H160::from_low_u64_be(1u64); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(21000); + let gas_limit = 35000; + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * gas_limit + U256::one(), + ); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + data.to_vec(), + Some(gas_limit), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Assert + // ecrecover pad address with 0 to be encoded over 32 bytes + let expected_address = + "0000000000000000000000007156526fbd7a3c72969b54f64e42c10fbb768c8a"; + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: 25676, + logs: vec![], + result: ExecutionResult::CallSucceeded( + ExitSucceed::Returned, + hex::decode(expected_address).unwrap(), + ), + withdrawals: vec![], + estimated_ticks_used: 30_000_000, + })); + + assert_eq!(expected_result, result); + } + + #[test] + fn create_and_call_contract() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(21000); + + let code = vec![ + // Create a contract that creates an exception if first word of calldata is 0 + Opcode::PUSH17.as_u8(), + 0x67, + 0x60, + 0x00, + 0x35, + 0x60, + 0x07, + 0x57, + 0xFE, + 0x5B, + 0x60, + 0x00, + 0x52, + 0x60, + 0x08, + 0x60, + 0x18, + 0xF3, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::MSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x17, + Opcode::PUSH1.as_u8(), + 0x15, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::CREATE.as_u8(), + // Call with no parameters, return 0 + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::DUP6.as_u8(), + Opcode::PUSH2.as_u8(), + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), + // Call with non 0 calldata, returns success + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x32, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::PUSH1.as_u8(), + 0x0, + Opcode::DUP7.as_u8(), + Opcode::PUSH2.as_u8(), + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + None, + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Assert + // let expected_result = Ok(()); + // assert_eq!(result, expected_result); + assert!(result.is_ok()); + } + + #[test] + fn static_calls_cannot_update_storage() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117_u64); + let caller = H160::from_low_u64_be(118_u64); + let static_call_target = H160::from_low_u64_be(200_u64); + let all_the_gas = 2_000_000_u64; + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // contract that stores something in durable storage + let static_call_code = vec![ + Opcode::PUSH2.as_u8(), + 0xFF, + 0xFF, + Opcode::PUSH1.as_u8(), + 0, + Opcode::SSTORE.as_u8(), + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &static_call_target, + &static_call_code, + ); + + // contract that does static call to contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push address + 200, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::STATICCALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 65535 // staticcall allocated gas + + 6 * 3 // cost for push + + 100; // cost for staticcall + + // Since we execute an invalid instruction (for a static call that is) we spend + // _all_ the gas allocated to the call (so 0xFFFF or 65535) + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 532678536, + })); + + // assert that call succeeds + assert_eq!(result, expected_result); + } + + #[test] + fn static_calls_fail_when_logging() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117_u64); + let caller = H160::from_low_u64_be(118_u64); + let static_call_target = H160::from_low_u64_be(200_u64); + let all_the_gas = 2_000_000_u64; + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // contract that does logging + let static_call_code = vec![ + Opcode::PUSH1.as_u8(), // push size + 1, + Opcode::PUSH1.as_u8(), // push address + 0x1, + Opcode::LOG0.as_u8(), // write a zero to log + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &static_call_target, + &static_call_code, + ); + + // contract that does static call to contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push address + 200, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::STATICCALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 65535 // staticcall allocated gas + + 6 * 3 // cost for push + + 100; // cost for staticcall + + // Since we execute an invalid instruction (for a static call that is), we + // expect to spend _all_ the gas. + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + // No logs were produced + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 532095791, + })); + + // assert that call succeeds + assert_eq!(result, expected_result); + } + + #[test] + fn logs_get_written_to_output() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117_u64); + let caller = H160::from_low_u64_be(118_u64); + let call_target = H160::from_low_u64_be(200_u64); + let all_the_gas = 2_000_000_u64; + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // contract that does logging + let contract_that_logs = vec![ + Opcode::PUSH8.as_u8(), // push some value + 1, + 2, + 3, + 4, + 5, + 6, + 7, + 8, + Opcode::PUSH1.as_u8(), // push memory address + 0, + Opcode::MSTORE.as_u8(), // store value to memory address + Opcode::PUSH1.as_u8(), // push some topic + 42, + Opcode::PUSH1.as_u8(), // push size + 8, + Opcode::PUSH1.as_u8(), // push address + 24, + Opcode::LOG1.as_u8(), // write a zero to log + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &call_target, + &contract_that_logs, + ); + + // contract that calls contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push log record size + 1, + Opcode::PUSH1.as_u8(), // push memory address of log data + 1, + Opcode::LOG0.as_u8(), // write something to the log + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push value + 0, + Opcode::PUSH1.as_u8(), // push address + 200, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + 1_000_000_000, + false, + false, + None, + ); + + let log_record1 = Log { + address: target, + topics: vec![], + data: vec![0], + }; + + let log_record2 = Log { + address: call_target, + topics: vec![H256::from_low_u64_be(42)], + data: vec![1, 2, 3, 4, 5, 6, 7, 8], + }; + + let expected_gas = 21000 // base cost + + 1348; // execution cost (taken at face value from tests) + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![log_record1, log_record2], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 48062519, + })); + + assert_eq!(result, expected_result); + } + + #[test] + fn no_logs_when_contract_reverts() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117_u64); + let caller = H160::from_low_u64_be(118_u64); + let static_call_target = H160::from_low_u64_be(200_u64); + let all_the_gas = 2_000_000_u64; + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // contract that does logging + let static_call_code = vec![ + Opcode::PUSH1.as_u8(), // push size + 1, + Opcode::PUSH1.as_u8(), // push address + 0x1, + Opcode::LOG0.as_u8(), // write a zero to log + Opcode::PUSH1.as_u8(), // size of return data + 0, + Opcode::PUSH1.as_u8(), // offset of return data + 0, + Opcode::REVERT.as_u8(), + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &static_call_target, + &static_call_code, + ); + + // contract that does call to contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push log record size + 1, + Opcode::PUSH1.as_u8(), // push memory address of log data + 1, + Opcode::LOG0.as_u8(), // write something to the log + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push value + 0, + Opcode::PUSH1.as_u8(), // push address + 200, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let log_record1 = Log { + address: target, + topics: vec![], + data: vec![0], + }; + + let expected_gas = 21000 // base cost + + 911; // execution cost (taken at face value from tests) + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![log_record1], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 47793126, + })); + + assert_eq!(result, expected_result); + } + + #[test] + fn contract_selfdestruct_deletes_contract() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(42_u64); + let caller = H160::from_low_u64_be(115_u64); + let selfdestructing_contract = H160::from_low_u64_be(100_u64); + let all_the_gas = 1_000_000_u64; + let gas_price = U256::from(1); + + // This contract selfdestructs and gives its funds to `caller` + let selfdestructing_code = vec![ + Opcode::PUSH1.as_u8(), // push address of beneficiary + 115, + Opcode::SUICIDE.as_u8(), // this also stops execution + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &selfdestructing_contract, + &selfdestructing_code, + ); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &selfdestructing_contract, + 1_000_000.into(), + ); + + // contract that does call to contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push value + 0, + Opcode::PUSH1.as_u8(), // push address + 100, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + let expected_gas = 21000 // base cost + + 5124; // execution gas cost (taken at face value from tests) + let expected_result = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + withdrawals: vec![], + estimated_ticks_used: 50578554, + }; + + assert_eq!(result.unwrap().unwrap(), expected_result); + + assert_eq!( + evm_account_storage + .get( + &mock_runtime, + &account_path(&selfdestructing_contract).unwrap() + ) + .unwrap(), + None + ); + + let funds_total = 1_000_000 + all_the_gas - expected_result.gas_used; + + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + funds_total.into() + ); + } + + #[test] + fn selfdestruct_is_ignored_when_call_fails() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(42_u64); + let caller = H160::from_low_u64_be(115_u64); + let selfdestructing_contract = H160::from_low_u64_be(100_u64); + let all_the_gas = 1_000_000_u64; + let gas_price = U256::from(1); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + all_the_gas.into(), + ); + + // This contract selfdestructs and gives its funds to `caller` + let selfdestructing_code = vec![ + Opcode::PUSH1.as_u8(), // push address of beneficiary + 115, + Opcode::SUICIDE.as_u8(), // this also stops execution + ]; + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &selfdestructing_contract, + &selfdestructing_code, + ); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &selfdestructing_contract, + 1_000_000.into(), + ); + + bump_nonce( + &mut mock_runtime, + &mut evm_account_storage, + &selfdestructing_contract, + ); + + // contract that does call to contract above + let code = vec![ + Opcode::PUSH1.as_u8(), // push return data size + 0, + Opcode::PUSH1.as_u8(), // push return data offset + 0, + Opcode::PUSH1.as_u8(), // push arg size + 0, + Opcode::PUSH1.as_u8(), // push arg offset + 0, + Opcode::PUSH1.as_u8(), // push value + 0, + Opcode::PUSH1.as_u8(), // push address + 100, + Opcode::PUSH2.as_u8(), // push gas + 0xFF, + 0xFF, + Opcode::CALL.as_u8(), // call the contract that selfdestructs + Opcode::INVALID.as_u8(), // fail the entire transaction + ]; + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + 10_000_000_000, + false, + false, + None, + ); + + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: all_the_gas, + logs: vec![], + result: ExecutionResult::Error(ExitError::InvalidCode(Opcode::INVALID)), + withdrawals: vec![], + estimated_ticks_used: 50630887, + })); + + assert_eq!(result, expected_result); + + let account = evm_account_storage + .get_or_create( + &mock_runtime, + &account_path(&selfdestructing_contract).unwrap(), + ) + .unwrap(); + + assert_eq!( + account.balance(&mock_runtime).unwrap(), + U256::from(1_000_000) + ); + assert_eq!(account.code(&mock_runtime).unwrap(), selfdestructing_code); + assert_eq!(account.nonce(&mock_runtime).unwrap(), 1); + + assert_eq!( + get_balance(&mut mock_runtime, &mut evm_account_storage, &caller), + 0.into() + ); + } + + #[test] + fn test_chain_id() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let chain_id = U256::from(42); + let mut chain_id_bytes = [0u8; 32]; + chain_id.to_big_endian(&mut chain_id_bytes); + let block_fees = BlockFees::new( + U256::zero(), + U256::from(54321), + U256::from(2_000_000_000_000u64), + ); + let block = BlockConstants::first_block( + U256::zero(), + chain_id, + block_fees, + u64::MAX, + H160::zero(), + ); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(12345); + let code = vec![ + Opcode::CHAINID.as_u8(), // cost 2 + Opcode::PUSH1.as_u8(), // push ost, cost 3 + 0, + Opcode::MSTORE.as_u8(), // cost 3, memory expansion cost 3 + Opcode::PUSH1.as_u8(), // push len, cost 3 + 32, + Opcode::PUSH1.as_u8(), // push ost, cost 3 + 0, + Opcode::RETURN.as_u8(), // cost 0 + ]; + + // value not relevant to test, must be big enough + let all_the_gas = 25_000_u64; + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * all_the_gas, + ); + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 17; // execution cost + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded( + ExitSucceed::Returned, + chain_id_bytes.into(), + ), + withdrawals: vec![], + estimated_ticks_used: 166052, + })); + assert_eq!(result, expected_result); + } + + #[test] + fn test_base_fee_per_gas() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let base_fee_per_gas = U256::from(23000); + let mut base_fee_per_gas_bytes = [0u8; 32]; + base_fee_per_gas.to_big_endian(&mut base_fee_per_gas_bytes); + let block_fees = BlockFees::new( + U256::zero(), + base_fee_per_gas, + U256::from(2_000_000_000_000u64), + ); + let block = BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + let code = vec![ + Opcode::BASEFEE.as_u8(), // cost 2 + Opcode::PUSH1.as_u8(), // push ost, cost 3 + 0, + Opcode::MSTORE.as_u8(), // cost 3, memory expansion cost 3 + Opcode::PUSH1.as_u8(), // push len, cost 3 + 32, + Opcode::PUSH1.as_u8(), // push ost, cost 3 + 0, + Opcode::RETURN.as_u8(), + ]; + + // value not relevant to test, just needs to be big enough + let all_the_gas = 25_000_u64; + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * all_the_gas, + ); + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_gas = 21000 // base cost + + 17; // execution cost + + // Assert + let expected_result = Ok(Some(ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded( + ExitSucceed::Returned, + base_fee_per_gas_bytes.into(), + ), + withdrawals: vec![], + estimated_ticks_used: 166070, + })); + assert_eq!(result, expected_result); + } + + /// [unwrap_outcome!(result, expect_success)] tries to unwrap a value of type + /// `Result, ...>` and check the outcome status + /// according to optional argument [expect_success] (default value true) + macro_rules! unwrap_outcome { + ($result:expr, $expect_success:expr) => {{ + assert!($result.is_ok(), "Couldn't unwrap, Result was Err"); + let tmp = $result.as_ref().unwrap(); + assert!(tmp.is_some(), "Couldn't unwrap, Option was None"); + let tmp = tmp.as_ref().unwrap(); + assert_eq!( + tmp.is_success(), + $expect_success, + "outcome field 'is_success' should be {}", + $expect_success + ); + tmp + }}; + ($result:expr) => {{ + unwrap_outcome!($result, true) + }}; + } + + #[test] + fn evm_should_fail_gracefully_when_balance_overflow_occurs() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let caller = H160::from_low_u64_be(523); + let target = H160::from_low_u64_be(210); + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + U256::from(200), + ); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &target, + U256::max_value(), + ); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + vec![], + None, + gas_price, + U256::from(100), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let expected_result = Err(EthereumError::EthereumAccountError( + AccountStorageError::BalanceOverflow, + )); + assert_eq!(expected_result, result); + } + + #[test] + fn create_contract_gas_cost() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + // example stolen from https://www.rareskills.io/post/smart-contract-creation-cost + let data_str = "6080604052603f8060116000396000f3fe6080604052600080fdfea2646970667358221220c5cad0aa1e64e2ca6a6cdf28a25255a8ebbf3cdd5ea0b8e4129a3c83c4fbb72a64736f6c63430008070033"; + let call_data: Vec = hex::decode(data_str).unwrap(); + + // not testing gas_limit, should be big enough + let gas_limit = 2_400_000; + let gas_price = U256::from(21000); + + // the test is not to check that account can prepay, + // so we can choose the balance depending on set gas limit + let balance = gas_price.saturating_mul(gas_limit.into()); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + balance, + ); + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let result = unwrap_outcome!(result); + + // gas calculation + let expected_gas = 21000 // base cost + + 32000 // create base cost + + 1220 // transaction data cost + + 12600 // code deposit cost + + 42 // init cost + + 6; // extra cost (EIP-3860) + + assert_eq!(expected_gas, result.gas_used); + } + + #[test] + fn create_contract_fail_gas_cost() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + // data should result in failed contract creation + let create_data: Vec = vec![0x01; 32]; + + // not testing gas_limit, should be big enough + let gas_limit = 2_400_000; + let gas_price = U256::from(21000); + + // the test is not to check that account can prepay, + // so we can choose the balance depending on set gas limit + let balance = gas_price.saturating_mul(gas_limit.into()); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + balance, + ); + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + create_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let result = unwrap_outcome!(&result, false); + + // gas calculation + let expected_gas = 21000 // base cost + + 32000 // create cost + + 32 * CONFIG.gas_transaction_non_zero_data // transaction data cost + + 3 // init cost + + 2; // extra cost (EIP-3860) + + assert_eq!(expected_gas, result.gas_used); + } + + #[test] + fn test_transaction_data_cost() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let base_fee_per_gas = U256::from(23000); + let block_fees = BlockFees::new( + U256::one(), + base_fee_per_gas, + U256::from(2_000_000_000_000u64), + ); + let block = BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + + // zero byte data + let data = [0u8; 32]; + + // zero cost contract + let code = vec![Opcode::STOP.as_u8()]; + + // value not relevant to test, just needs to be big enough + let all_the_gas = 25_000_u64; + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * all_the_gas, + ); + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + data.to_vec(), + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Assert + let result = unwrap_outcome!(&result); + + let expected_gas = 21000 // base cost + + 32 * CONFIG.gas_transaction_zero_data; // transaction data cost + + assert_eq!(expected_gas, result.gas_used); + } + + #[test] + fn test_transaction_data_cost_non_zero() { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let base_fee_per_gas = U256::from(23000); + let block_fees = BlockFees::new( + U256::one(), + base_fee_per_gas, + U256::from(2_000_000_000_000u64), + ); + let block = BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let target = H160::from_low_u64_be(117u64); + let caller = H160::from_low_u64_be(118u64); + + // no zero byte data + let data = [127u8; 32]; + + // zero cost contract + let code = vec![Opcode::STOP.as_u8()]; + + // value not relevant to test, just needs to be big enough + let all_the_gas = 25_000_u64; + let gas_price = U256::from(21000); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * all_the_gas, + ); + + set_account_code(&mut mock_runtime, &mut evm_account_storage, &target, &code); + + // Act + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(target), + caller, + data.to_vec(), + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Assert + let result = unwrap_outcome!(&result); + + let expected_gas = 21000 // base cost + + 32 * CONFIG.gas_transaction_non_zero_data; // transaction data cost: should be zero_byte cost * size + + assert_eq!(expected_gas, result.gas_used); + } + + fn first_block() -> BlockConstants { + let base_fee_per_gas = U256::from(23000); + let block_fees = BlockFees::new( + base_fee_per_gas, + base_fee_per_gas, + U256::from(2_000_000_000_000u64), + ); + BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ) + } + + fn deploy( + data: Vec, + all_the_gas: u64, + ) -> Result, EthereumError> { + // Arrange + let mut mock_runtime = MockKernelHost::default(); + let block = first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let caller = H160::from_low_u64_be(118u64); + let gas_price = U256::from(1356); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + gas_price * all_the_gas, + ); + + // Act + + run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + data.to_vec(), + Some(all_the_gas), + gas_price, + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ) + } + + fn test_eip_3541(data: Vec) { + // value not relevant to test, just needs to be big enough + let all_the_gas = 65_000_u64; + + // Act + let result = deploy(data, all_the_gas); + + // Assert + let result = unwrap_outcome!(&result, false); + assert_eq!( + ExecutionResult::Error(ExitError::InvalidCode(Opcode(0xef))), + result.result + ); + } + + #[test] + fn test_eip_3541_errors() { + // Arrange: see test cases + // in https://github.com/ethereum/EIPs/blob/master/EIPS/eip-3541.md + test_eip_3541(hex::decode("60ef60005360016000f3").unwrap()); + test_eip_3541(hex::decode("60ef60005360026000f3").unwrap()); + test_eip_3541(hex::decode("60ef60005360036000f3").unwrap()); + test_eip_3541(hex::decode("60ef60005360206000f3").unwrap()); + } + + #[test] + fn test_eip_3541_noerror() { + // value not relevant to test, just needs to be big enough + let all_the_gas = 65_000_u64; + let data = hex::decode("60fe60005360016000f3").unwrap(); + // Act + let result = deploy(data, all_the_gas); + + // Assert + let result = unwrap_outcome!(&result); + assert!(matches!( + result.result, + ExecutionResult::ContractDeployed(_, _) + )); + } + + // Test case from https://eips.ethereum.org/EIPS/eip-684 + #[test] + fn test_create_to_address_with_code_returns_error() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = + H160::from_str("0xd0bBEc6D2c628b7e2E6D5556daA14a5181b604C5").unwrap(); + + let target_address = + H160::from_str("0x7658771dc6Af74a3d2F8499D349FF9c1a0DF8826").unwrap(); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 10000000.into(), + ); + + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &target_address, + hex::decode("B0B0FACE") + .expect("Failed to decode contract code") + .as_slice(), + ); + + // call_data is the input to deploy the following contract. + // The content of the contract does not matter since address of created contract depends on address and nonce of caller + // contract Empty { + // function run() public{ + // } + // } + let call_data = hex::decode( + "6080604052348015600e575f80fd5b50606a80601a5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063c040622614602a575b5f80fd5b60306032565b005b56fea264697066735822122033200c2933dd0930ac60a7727e0a3e56f8967f6f76afb4ecf2459651419983ab64736f6c63430008170033", + ) + .expect("Failed to decode call data"); + + let gas_limit = 300_000; + let gas_price = U256::from(1); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + call_data, + Some(gas_limit), + gas_price, + U256::zero(), + true, + 10_000_000_000, + false, + false, + None, + ); + + let result = unwrap_outcome!(&result, false); + match &result.result { + ExecutionResult::Error(ExitError::CreateCollision) => (), + result => panic!( + "ExecutionResult: {:?}. Expect ExecutionResult::Error(ExitError::CreateCollision)", + result + ), + } + } + + // Caller nonce is bumped at each contract creation + #[test] + fn test_caller_nonce_after_create() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = + H160::from_str("0xd0bBEc6D2c628b7e2E6D5556daA14a5181b604C5").unwrap(); + + let contract_address = + H160::from_str("0x7658771dc6af74a3d2f8499d349ff9c1a0df8826").unwrap(); + + let sub_contract = + H160::from_str("0x6697a027694475d9f64c97566698c24cff8f17e7").unwrap(); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 10000000.into(), + ); + + // init_code is the input to deploy the following contract. + // This contract tries to create a contract at the sub_contract, the creation within the creation will fail + // But the creation will success + // PUSH5 0x6001600155 + // PUSH1 0 + // MSTORE + // PUSH1 5 + // PUSH1 27 + // PUSH1 0 + // CREATE + let init_code = hex::decode("6460016001556000526005601b6000f0") + .expect("Failed to decode call data"); + + let gas_limit = 300_000; + let gas_price = U256::from(1); + + // Test contract nonce before creation + let contract = EthereumAccount::from_address(&contract_address).unwrap(); + let original_contract_nonce = contract.nonce(&mock_runtime).unwrap_or_default(); + + assert_eq!(0, original_contract_nonce); + + // Test the nonce of the sub contract that will be created within contract creation + let sub_contract_account = EthereumAccount::from_address(&sub_contract).unwrap(); + let original_sub_nonce = sub_contract_account + .nonce(&mock_runtime) + .unwrap_or_default(); + + assert_eq!(0, original_sub_nonce); + + let result_init = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + init_code, + Some(gas_limit), + gas_price, + U256::zero(), + true, + 10_000_000_000, + false, + false, + None, + ); + + let result_init = unwrap_outcome!(&result_init, true); + + match &result_init.result { + ExecutionResult::ContractDeployed(_, _) => { + let contract = EthereumAccount::from_address(&contract_address).unwrap(); + let caller_nonce = contract.nonce(&mock_runtime).unwrap_or_default(); + // Check that even if the contract fails to create another contract (due to a collision), + // the nonce of contract_address is still bumped + assert_eq!(caller_nonce, 2); + + // The sub contract has been created at 0xd807115ef18e7e9b8e54188b4e9ef514277a0740 and + // its nonce is incremented to 1 + let sub_contract_created = + EthereumAccount::from_address(&sub_contract).unwrap(); + let sub_contract_nonce = sub_contract_created + .nonce(&mock_runtime) + .unwrap_or_default(); + assert_eq!(sub_contract_nonce, 1); + } + result => panic!( + "ExecutionResult: {:?}. Expect ExecutionResult::ContractDeployed(_)", + result + ), + } + } + + // This test is the same as test_caller_nonce_after_create but with a collision + // The nonce of the caller 'contract' should still be incremented + #[test] + fn test_caller_nonce_after_create_collision() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = + H160::from_str("0xd0bBEc6D2c628b7e2E6D5556daA14a5181b604C5").unwrap(); + + let sub_contract = + H160::from_str("0xd807115ef18e7e9b8e54188b4e9ef514277a0740").unwrap(); + + let contract_address = + H160::from_str("0x7658771dc6af74a3d2f8499d349ff9c1a0df8826").unwrap(); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 10000000.into(), + ); + + // Set the code of sub_contract to set up a collision + set_account_code( + &mut mock_runtime, + &mut evm_account_storage, + &sub_contract, + hex::decode("B0B0FACE") + .expect("Failed to decode contract code") + .as_slice(), + ); + + // init_code is the input to deploy the following contract. + // This contract tries to create a contract at the sub_contract, the creation within the creation will fail + // But the creation will success + // PUSH5 0x6001600155 + // PUSH1 0 + // MSTORE + // PUSH1 5 + // PUSH1 27 + // PUSH1 0 + // CREATE + let init_code = hex::decode("6460016001556000526005601b6000f0") + .expect("Failed to decode call data"); + + let gas_limit = 300_000; + let gas_price = U256::from(1); + + // Test contract nonce before creation + let contract = EthereumAccount::from_address(&contract_address).unwrap(); + let original_contract_nonce = contract.nonce(&mock_runtime).unwrap_or_default(); + + assert_eq!(0, original_contract_nonce); + + // Test the nonce of the sub contract that will be created within contract creation + let sub_contract_account = EthereumAccount::from_address(&sub_contract).unwrap(); + let original_sub_nonce = sub_contract_account + .nonce(&mock_runtime) + .unwrap_or_default(); + + assert_eq!(0, original_sub_nonce); + + let result_init = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + init_code, + Some(gas_limit), + gas_price, + U256::zero(), + true, + 10_000_000_000, + false, + false, + None, + ); + + let result_init = unwrap_outcome!(&result_init, true); + + match &result_init.result { + ExecutionResult::ContractDeployed(_, _) => { + // Check that even if the contract fails to create another contract (due to a collision), + // the nonce of contract_address is still bumped + let contract = EthereumAccount::from_address(&contract_address).unwrap(); + let caller_nonce = contract.nonce(&mock_runtime).unwrap_or_default(); + assert_eq!(caller_nonce, 2); + + // The sub contract has a collision, so its nonce should not change + let sub_contract_created = + EthereumAccount::from_address(&sub_contract).unwrap(); + let sub_contract_nonce = sub_contract_created + .nonce(&mock_runtime) + .unwrap_or_default(); + assert_eq!(sub_contract_nonce, original_sub_nonce); + } + result => panic!( + "ExecutionResult: {:?}. Expect ExecutionResult::ContractDeployed(_)", + result + ), + } + } + + // Test case from https://eips.ethereum.org/EIPS/eip-684 with modification + #[test] + fn test_create_to_address_with_non_zero_nonce_returns_error() { + let mut mock_runtime = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = + H160::from_str("0xd0bBEc6D2c628b7e2E6D5556daA14a5181b604C5").unwrap(); + + let target_address = + H160::from_str("0x7658771dc6Af74a3d2F8499D349FF9c1a0DF8826").unwrap(); + + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 10000000.into(), + ); + + bump_nonce(&mut mock_runtime, &mut evm_account_storage, &target_address); + + // call_data is the input to deploy the following contract. + // The content of the contract does not matter since address of created contract depends on address and nonce of caller + // contract Empty { + // function run() public{ + // } + // } + let call_data = hex::decode( + "6080604052348015600e575f80fd5b50606a80601a5f395ff3fe6080604052348015600e575f80fd5b50600436106026575f3560e01c8063c040622614602a575b5f80fd5b60306032565b005b56fea264697066735822122033200c2933dd0930ac60a7727e0a3e56f8967f6f76afb4ecf2459651419983ab64736f6c63430008170033", + ) + .expect("Failed to decode call data"); + + let gas_limit = 300_000; + let gas_price = U256::from(1); + + let result = run_transaction( + &mut mock_runtime, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + call_data, + Some(gas_limit), + gas_price, + U256::zero(), + true, + 10_000_000_000, + false, + false, + None, + ); + + let result = unwrap_outcome!(&result, false); + match &result.result { + ExecutionResult::Error(ExitError::CreateCollision) => (), + result => panic!( + "ExecutionResult: {:?}. Expect ExecutionResult::Error(ExitError::CreateCollision)", + result + ), + } + } + + #[test] + fn created_contract_start_at_nonce_one() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + let data_str = "6101aa6064526000600060206000600073b000000000000000000000000000000000000000620493e0f1506000600060006000733000000000000000000000000000000000000000620493e0f450600060006020600073b000000000000000000000000000000000000000620493e0f45060006000600060006000733000000000000000000000000000000000000000620493e0f25060006000600060006000732000000000000000000000000000000000000000620927c0f100"; + let call_data: Vec = hex::decode(data_str).unwrap(); + + let gas_limit = 2_400_000; + let gas_price = U256::from(21000); + let balance = gas_price.saturating_mul(gas_limit.into()); + + set_balance(&mut host, &mut evm_account_storage, &caller, balance); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let result = unwrap_outcome!(result); + let address = result.new_address().unwrap(); + let smart_contract = EthereumAccount::from_address(&address).unwrap(); + + assert_eq!(smart_contract.nonce(&host).unwrap(), 1) + } + + #[test] + fn call_contract_create_contract_with_insufficient_funds() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let callee = H160::from_str("095e7baea6a6c7c4c2dfeb977efac326af552d87").unwrap(); + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + set_balance( + &mut host, + &mut evm_account_storage, + &caller, + U256::from(1000000000), + ); + + set_balance( + &mut host, + &mut evm_account_storage, + &callee, + U256::from(10000), + ); + + let code = hex::decode( + "74600c60005566602060406000f060205260076039f36000526015600b620186a0f060005500", + ) + .unwrap(); + set_account_code(&mut host, &mut evm_account_storage, &callee, &code); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(callee), + caller, + vec![], + Some(20000000), + U256::one(), + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + let path = account_path(&caller).unwrap(); + let account = evm_account_storage.get_or_create(&host, &path).unwrap(); + let caller_nonce = account.nonce(&host).unwrap(); + + let path = account_path(&callee).unwrap(); + let account = evm_account_storage.get_or_create(&host, &path).unwrap(); + let callee_nonce = account.nonce(&host).unwrap(); + + assert_eq!( + ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + result.unwrap().unwrap().result, + ); + assert_eq!(callee_nonce, 0); + assert_eq!(caller_nonce, 1); + } + + #[test] + fn nested_create_check_nonce_start_at_one() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + // 6295...bf8f is the address that caller will create + let callee = H160::from_str("6295ee1b4f6dd65047762f924ecd367c17eabf8f").unwrap(); + + // a426...2701 is the address that callee will create if its nonce is at 0 + let should_not_create: H160 = + H160::from_str("0xa42676447b7cedfa5fde894d1d3df24aab362701").unwrap(); + + // 64e2...bba6 is the address that callee will create if its nonce is at 1 + let should_create = + H160::from_str("0x64e2ebd6405af8cb348aec519084d3fff42ebba6").unwrap(); + + set_balance( + &mut host, + &mut evm_account_storage, + &caller, + U256::from(1000000000), + ); + + set_balance( + &mut host, + &mut evm_account_storage, + &callee, + U256::from(10000), + ); + + // This code creates a contract that stores 0x12 in the slot 0 of the storage + let code = vec![ + Opcode::PUSH5.as_u8(), + 0x60, + 0x12, + 0x60, + 0x00, + 0x55, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::MSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x05, + Opcode::PUSH1.as_u8(), + 0x1b, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::CREATE.as_u8(), + ]; + + // We create a contract that creates a contract + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + code, + Some(20000000), + U256::one(), + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // Get info on contract that should not be created + let path = account_path(&should_not_create).unwrap(); + let account = evm_account_storage.get_or_create(&host, &path).unwrap(); + let nonce_of_should_not_create = account.nonce(&host).unwrap(); + let storage_of_should_not_create = account + .get_storage(&host, &H256::zero()) + .unwrap_or_default(); + + // Get info on contract that should be created + let path = account_path(&should_create).unwrap(); + let account = evm_account_storage.get_or_create(&host, &path).unwrap(); + let nonce_of_should_create = account.nonce(&host).unwrap(); + let storage_of_should_create = account + .get_storage(&host, &H256::zero()) + .unwrap_or_default(); + + let exec_result = unwrap_outcome!(result, true); + assert!(matches!( + exec_result.result, + ExecutionResult::ContractDeployed(_, _) + )); + assert_eq!( + nonce_of_should_not_create, 0, + "Nonce of the contract that should not be created is not 0" + ); + assert_eq!( + storage_of_should_not_create, + H256::zero(), + "Storage of the contract that should not be created is not 0" + ); + assert_eq!( + nonce_of_should_create, 1, + "Nonce of the contract that should be created is not 1" + ); + assert_eq!( + storage_of_should_create, + H256::from_low_u64_be(0x12), + "Storage of the contract that should be created is not 0x12" + ); + } + + fn out_of_tick_scenario( + retriable: bool, + ) -> ( + MockKernelHost, + Result, EthereumError>, + EthereumAccount, + U256, + u64, + ) { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + let call_data: Vec = hex::decode(ERC20_CONTRACT_INITIALISATION).unwrap(); + + // gas_limit estimated using remix on shanghai network (1,631,430) + // plus a 50% margin for gas accounting discrepancies + let gas_limit = 2_400_000; + let gas_price = U256::from(21000); + + // the test is not to check that account can prepay, + // so we can choose the balance depending on set gas limit + let initial_balance = gas_price + .saturating_mul(gas_limit.into()) + .saturating_mul(U256::from(2)); + set_balance( + &mut host, + &mut evm_account_storage, + &caller, + initial_balance, + ); + + let path = account_path(&caller).unwrap(); + let account = evm_account_storage.get_or_create(&host, &path).unwrap(); + let initial_caller_nonce = account.nonce(&host).unwrap(); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + true, + 10_000, + retriable, + false, + None, + ); + + (host, result, account, initial_balance, initial_caller_nonce) + } + + #[test] + fn transaction_has_no_impact_if_retriable() { + let (host, result, caller, initial_caller_balance, initial_caller_nonce) = + out_of_tick_scenario(true); + let caller_balance = caller.balance(&host).unwrap(); + let caller_nonce = caller.nonce(&host).unwrap(); + + assert_eq!( + ExecutionResult::OutOfTicks, + result.unwrap().unwrap().result, + "Contract creation was expected to fail and run out of ticks: \n" + ); + assert_eq!( + initial_caller_balance, caller_balance, + "Balance shouldn't have changed as gas should have been repaid" + ); + assert_eq!( + initial_caller_nonce, caller_nonce, + "Nonce shouldn't have changed" + ) + } + + #[test] + fn non_retriable_transaction_pays_for_exhausted_ticks() { + let (host, result, caller, initial_caller_balance, initial_caller_nonce) = + out_of_tick_scenario(false); + let caller_balance = caller.balance(&host).unwrap(); + let caller_nonce = caller.nonce(&host).unwrap(); + + assert_eq!( + ExecutionResult::OutOfTicks, + result.unwrap().unwrap().result, + "Contract creation was expected to fail and run out of ticks: \n" + ); + assert_ne!( + initial_caller_balance, caller_balance, + "Gas was not deducted from the caller account" + ); + assert_eq!( + initial_caller_nonce + 1, + caller_nonce, + "Nonce should have been incremented" + ) + } + + // If runned locally, use: + // RUST_MIN_STACK= cargo test -p evm-kernel --features testing + // with set to 104857600 or something similar in size. + // Multiple recursive call and stopping when storage 0 is equal to 1024 (will succeed) + #[test] + #[ignore] + fn multiple_call_all_the_way_to_1024() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let contract_address = + H160::from_str("7335dfb20cdcd40881235a54d61cb1152d771f4d").unwrap(); + + // This contract calls itself until its storage slot 0 is 1024 (reach the limit of the stack but not reverting calltoodeep) + let contract_code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::SLOAD.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::PUSH2.as_u8(), + 0x04, // 0x0400 = 1024 + 0x00, + Opcode::EQ.as_u8(), + Opcode::PUSH1.as_u8(), + 0x22, + Opcode::JUMPI.as_u8(), + Opcode::PUSH1.as_u8(), + 0x01, + Opcode::ADD.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::SSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::ADDRESS.as_u8(), + Opcode::GAS.as_u8(), + Opcode::CALL.as_u8(), + Opcode::PUSH1.as_u8(), + 0x22, + Opcode::JUMPI.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::REVERT.as_u8(), + Opcode::JUMPDEST.as_u8(), + Opcode::STOP.as_u8(), + ]; + + set_account_code( + &mut host, + &mut evm_account_storage, + &contract_address, + &contract_code, + ); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(contract_address), + caller, + vec![], + // gas limit comes from GeneralStateTests/stRevertTest/LoopCallsDepthThenRevert3.json + Some(9214364837600034817), + U256::one(), + U256::zero(), + false, + u64::MAX, + false, + false, + None, + ); + + unwrap_outcome!(result, true); + } + + // If runned locally, use: + // RUST_MIN_STACK= cargo test -p evm-kernel --features testing + // with set to 104857600 or something similar in size. + // This test is the same as `multiple_call_all_the_way_to_1024` but instead of stopping + // at 1024 we stop at 1025 (this should fail) + #[test] + fn multiple_call_fails_right_after_1024() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let contract_address = + H160::from_str("7335dfb20cdcd40881235a54d61cb1152d771f4d").unwrap(); + + // This contract calls itself until its storage slot 0 is 1025 (reach the limit of the stack and revert) + let contract_code: Vec = vec![ + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::SLOAD.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::PUSH2.as_u8(), + 0x04, // 0x0401 = 1025 + 0x01, + Opcode::EQ.as_u8(), + Opcode::PUSH1.as_u8(), + 0x22, + Opcode::JUMPI.as_u8(), + Opcode::PUSH1.as_u8(), + 0x01, + Opcode::ADD.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::SSTORE.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::DUP1.as_u8(), + Opcode::ADDRESS.as_u8(), + Opcode::GAS.as_u8(), + Opcode::CALL.as_u8(), + Opcode::PUSH1.as_u8(), + 0x22, + Opcode::JUMPI.as_u8(), + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::PUSH1.as_u8(), + 0x00, + Opcode::REVERT.as_u8(), + Opcode::JUMPDEST.as_u8(), + Opcode::STOP.as_u8(), + ]; + + set_account_code( + &mut host, + &mut evm_account_storage, + &contract_address, + &contract_code, + ); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(contract_address), + caller, + vec![], + Some(9214364837600034817), // gas limit comes from GeneralStateTests/stRevertTest/LoopCallsDepthThenRevert3.json + U256::one(), + U256::zero(), + false, + u64::MAX, + false, + false, + None, + ); + + unwrap_outcome!(result, false); + } + + // If runned locally, use: + // RUST_MIN_STACK= cargo test -p evm-kernel --features testing + // with set to 104857600 or something similar in size. + #[test] + #[ignore] + fn call_too_deep_not_revert() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let internal_address = + H160::from_str("6cb631a210b05eab95b1f0b0cc83be18789bc479").unwrap(); + + let code = hex::decode("3060025560206000600039602060006000f000").unwrap(); // Creates an infinity of contract + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + code, + Some(u64::MAX), + U256::one(), + U256::zero(), + false, + u64::MAX, + false, + false, + None, + ); + + let internal_address_nonce = + get_nonce(&mut host, &mut evm_account_storage, &internal_address); + let caller_nonce = get_nonce(&mut host, &mut evm_account_storage, &caller); + + assert!(matches!( + result.unwrap().unwrap().result, + ExecutionResult::ContractDeployed(_, _) + )); + + assert_eq!(caller_nonce, 1); + assert_eq!(internal_address_nonce, 2); + } + + #[test] + fn exceed_max_create_init_code_size_fails() { + // Test is taken from: + // ethereum/tests/GeneralStateTests/Shanghai/stEIP3860-limitmeterinitcode/createInitCodeSizeLimit.json + // with the second data which is invalid (size wise) + // The test was a bit tweaked so that the called contract transfer 1 WEI to the called contract. + + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let address_1 = + H160::from_str("000000000000000000000000000000000000c0de").unwrap(); + let address_2 = + H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + let address_3 = + H160::from_str("bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb").unwrap(); + + bump_nonce(&mut host, &mut evm_account_storage, &address_1); + bump_nonce(&mut host, &mut evm_account_storage, &address_2); + bump_nonce(&mut host, &mut evm_account_storage, &address_3); + + set_balance( + &mut host, + &mut evm_account_storage, + &address_2, + U256::from(200000000), + ); + + set_balance(&mut host, &mut evm_account_storage, &address_3, U256::one()); + + let code_1 = hex::decode( + "69600a80600080396000f360b01b6000908152355a90600080f0905a9003600a5560005500", + ) + .unwrap(); + let code_3 = + hex::decode("600035600052600080366000600161c0de62989680f16000556001805500") + .unwrap(); + + set_account_code(&mut host, &mut evm_account_storage, &address_1, &code_1); + set_account_code(&mut host, &mut evm_account_storage, &address_3, &code_3); + + // Invalid initcode size = 49153 bytes (max allowed is 49152) + let call_data = hex::decode( + "000000000000000000000000000000000000000000000000000000000000c001", + ) + .unwrap(); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(address_3), + address_2, + call_data, + Some(15000000), + U256::from(10), + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS * 100, + false, + false, + None, + ) + .unwrap() + .unwrap(); + + // The internal call should have failed but not the initial one. + // Second contract tried to transfer one WEI to the second contract but + // the changes have been rollbacked since there was a CreateContractLimit + // error. + + let balance_1 = get_balance(&mut host, &mut evm_account_storage, &address_1); + let balance_3 = get_balance(&mut host, &mut evm_account_storage, &address_3); + + assert_eq!(balance_1, U256::zero()); + assert_eq!(balance_3, U256::one()); + + // With call data: 0x000000000000000000000000000000000000000000000000000000000000c001 + // 0x5f6baaeb5b7c97725f84d1569c4abc85135f4716 should be the generated address, + // we check that it does not exist. + + let address_unknown = + H160::from_str("5f6baaeb5b7c97725f84d1569c4abc85135f4716").unwrap(); + + let balance_unknwown = + get_balance(&mut host, &mut evm_account_storage, &address_unknown); + let nonce_unknown = + get_nonce(&mut host, &mut evm_account_storage, &address_unknown); + let code_unknown = + get_code(&mut host, &mut evm_account_storage, &address_unknown); + + assert_eq!(balance_unknwown, U256::zero()); + assert_eq!(nonce_unknown, 0); + assert!(code_unknown.is_empty()); + + // The initial call succeeds + assert_eq!( + ExecutionResult::CallSucceeded(ExitSucceed::Stopped, vec![]), + result.result, + ) + } + + #[test] + fn storage_is_cleared_before_contract_creation() { + // Test is taken from: + // ethereum/tests/GeneralStateTests/Shanghai/stSStoreTest/InitCollision.json + // with the second data. + + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let address_1 = + H160::from_str("6295ee1b4f6dd65047762f924ecd367c17eabf8f").unwrap(); + let address_2 = + H160::from_str("7b9f5332c245e5c60923427eeb34e5adfba6470e").unwrap(); + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let one = H256::from_low_u64_be(0x01); + + set_balance( + &mut host, + &mut evm_account_storage, + &caller, + U256::from(1000000000000u64), + ); + + set_storage(&mut host, &mut evm_account_storage, &address_1, &one, &one); + set_storage(&mut host, &mut evm_account_storage, &address_2, &one, &one); + + // { (seq (CREATE2 0 0 (lll (seq (SSTORE 1 0) (SSTORE 1 1) ) 0) 0) (STOP) ) } + let call_data = + hex::decode("6000600b80601360003960006000f5500000fe6000600155600160015500") + .unwrap(); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + None, + caller, + call_data, + Some(200000), + U256::from(10), + U256::zero(), + true, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ) + .unwrap() + .unwrap(); + + let nonce_1 = get_nonce(&mut host, &mut evm_account_storage, &address_1); + let nonce_2 = get_nonce(&mut host, &mut evm_account_storage, &address_2); + + // Nonce is at 2, because the contract is originated and start at 1 (EIP-161) + // and it internally creates an other contract in its initialisation code, so + // the nonce is 2. + assert_eq!(nonce_1, 2); + assert_eq!(nonce_2, 1); + + let storage_1 = + get_storage(&mut host, &mut evm_account_storage, &address_1, &one); + let storage_2 = + get_storage(&mut host, &mut evm_account_storage, &address_2, &one); + + // storage was set but is cleared on contract creation + assert_eq!(storage_1, H256::zero()); + // storage was set during initialisation code when CREATE2 is called + assert_eq!(storage_2, one); + + // The initial call succeeds + assert!(matches!( + result.result, + ExecutionResult::ContractDeployed(_, _) + )) + } + + #[test] + fn nonce_bump_before_tx() { + let mut host = MockKernelHost::default(); + let block = dummy_first_block(); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + + let caller = H160::from_str("a94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + let callee = H160::from_str("b94f5374fce5edbc8e2a8697c15331677e6ebf0b").unwrap(); + + let code = hex::decode("323f60005260206000f3").unwrap(); // RETURN (EXTCODEHASH (ORIGIN)) + + set_account_code(&mut host, &mut evm_account_storage, &callee, &code); + + let result = run_transaction( + &mut host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + Some(callee), + caller, + vec![], + None, + U256::one(), + U256::zero(), + false, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + + // The origin address is empty but when you start a transaction the nonce is bump + // so the EXTCODEHASH return the following value (https://eips.ethereum.org/EIPS/eip-1052). + // If the nonce isn't bump before the transaction the return value is 0. + + let return_expected = hex::decode( + "c5d2460186f7233c927e7db2dcc703c0e500b653ca82273b7bfad8045d85a470", + ) + .unwrap(); + + assert_eq!( + *result.unwrap().unwrap().result.output().unwrap(), + return_expected + ); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/blake2.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/blake2.rs new file mode 100644 index 000000000000..7dc032392128 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/blake2.rs @@ -0,0 +1,325 @@ +// SputnikVM - Apache 2.0 LICENSE - https://github.com/rust-ethereum/evm/blob/master/LICENSE +// +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::fail_if_too_much; +use crate::precompiles::call_precompile_with_gas_draining; +use crate::precompiles::tick_model; +use crate::{handler::EvmHandler, precompiles::PrecompileOutcome, EthereumError}; +use alloc::borrow::Cow; +use evm::{executor::stack::PrecompileFailure, ExitError}; +use evm::{Context, ExitReason, ExitSucceed, Transfer}; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::{Debug, Info}; +use tezos_evm_runtime::runtime::Runtime; + +/// The precomputed values for BLAKE2b [from the spec](https://tools.ietf.org/html/rfc7693#section-2.7) +/// There are 10 16-byte arrays - one for each round +/// the entries are calculated from the sigma constants. +const SIGMA: [[usize; 16]; 10] = [ + [0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15], + [14, 10, 4, 8, 9, 15, 13, 6, 1, 12, 0, 2, 11, 7, 5, 3], + [11, 8, 12, 0, 5, 2, 15, 13, 10, 14, 3, 6, 7, 1, 9, 4], + [7, 9, 3, 1, 13, 12, 11, 14, 2, 6, 5, 10, 4, 0, 15, 8], + [9, 0, 5, 7, 2, 4, 10, 15, 14, 1, 11, 12, 6, 8, 3, 13], + [2, 12, 6, 10, 0, 11, 8, 3, 4, 13, 7, 5, 15, 14, 1, 9], + [12, 5, 1, 15, 14, 13, 4, 10, 0, 7, 6, 3, 9, 2, 8, 11], + [13, 11, 7, 14, 12, 1, 3, 9, 5, 0, 15, 4, 8, 6, 2, 10], + [6, 15, 14, 9, 11, 3, 0, 8, 12, 2, 13, 7, 1, 4, 10, 5], + [10, 2, 8, 4, 7, 6, 1, 5, 15, 11, 9, 14, 3, 12, 13, 0], +]; + +/// IV is the initialization vector for BLAKE2b. See https://tools.ietf.org/html/rfc7693#section-2.6 +/// for details. +const IV: [u64; 8] = [ + 0x6a09e667f3bcc908, + 0xbb67ae8584caa73b, + 0x3c6ef372fe94f82b, + 0xa54ff53a5f1d36f1, + 0x510e527fade682d1, + 0x9b05688c2b3e6c1f, + 0x1f83d9abfb41bd6b, + 0x5be0cd19137e2179, +]; + +#[inline(always)] +/// The G mixing function. See https://tools.ietf.org/html/rfc7693#section-3.1 +fn g(v: &mut [u64], a: usize, b: usize, c: usize, d: usize, x: u64, y: u64) { + v[a] = v[a].wrapping_add(v[b]).wrapping_add(x); + v[d] = (v[d] ^ v[a]).rotate_right(32); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(24); + v[a] = v[a].wrapping_add(v[b]).wrapping_add(y); + v[d] = (v[d] ^ v[a]).rotate_right(16); + v[c] = v[c].wrapping_add(v[d]); + v[b] = (v[b] ^ v[c]).rotate_right(63); +} + +/// The Blake2 compression function F. See https://tools.ietf.org/html/rfc7693#section-3.2 +/// Takes as an argument the state vector `h`, message block vector `m`, offset counter `t`, final +/// block indicator flag `f`, and number of rounds `rounds`. The state vector provided as the first +/// parameter is modified by the function. +pub fn compress(h: &mut [u64; 8], m: [u64; 16], t: [u64; 2], f: bool, rounds: usize) { + let mut v = [0u64; 16]; + v[..h.len()].copy_from_slice(h); // First half from state. + v[h.len()..].copy_from_slice(&IV); // Second half from IV. + + v[12] ^= t[0]; + v[13] ^= t[1]; + + if f { + v[14] = !v[14] // Invert all bits if the last-block-flag is set. + } + for i in 0..rounds { + // Message word selection permutation for this round. + let s = &SIGMA[i % 10]; + g(&mut v, 0, 4, 8, 12, m[s[0]], m[s[1]]); + g(&mut v, 1, 5, 9, 13, m[s[2]], m[s[3]]); + g(&mut v, 2, 6, 10, 14, m[s[4]], m[s[5]]); + g(&mut v, 3, 7, 11, 15, m[s[6]], m[s[7]]); + + g(&mut v, 0, 5, 10, 15, m[s[8]], m[s[9]]); + g(&mut v, 1, 6, 11, 12, m[s[10]], m[s[11]]); + g(&mut v, 2, 7, 8, 13, m[s[12]], m[s[13]]); + g(&mut v, 3, 4, 9, 14, m[s[14]], m[s[15]]); + } + + for i in 0..8 { + h[i] ^= v[i] ^ v[i + 8]; + } +} + +trait Decodable { + fn decode_from_le_slice(&mut self, source: &[u8]); +} + +impl Decodable for [u64; N] { + fn decode_from_le_slice(&mut self, src: &[u8]) { + let mut word_buf = [0_u8; 8]; + for (i, word) in self.iter_mut().enumerate() { + word_buf.copy_from_slice(&src[i * 8..(i + 1) * 8]); + *word = u64::from_le_bytes(word_buf); + } + } +} + +fn blake2f_output_for_wrong_input() -> EthereumError { + EthereumError::PrecompileFailed(PrecompileFailure::Error { + exit_status: ExitError::Other(Cow::from("Wrong input for blake2f precompile")), + }) +} + +fn blake2f_precompile_without_gas_draining( + handler: &mut EvmHandler, + input: &[u8], +) -> Result { + log!(handler.borrow_host(), Debug, "Calling blake2f precompile"); + + // The precompile requires 6 inputs tightly encoded, taking exactly 213 bytes + if input.len() != 213 { + return Err(blake2f_output_for_wrong_input()); + } + + // the number of rounds - 32-bit unsigned big-endian word + let mut rounds_buf = [0_u8; 4]; + rounds_buf.copy_from_slice(&input[0..4]); + let rounds: u32 = u32::from_be_bytes(rounds_buf); + + // check that enough resources to execute (gas / ticks) are available + let estimated_ticks = + fail_if_too_much!(tick_model::ticks_of_blake2f(rounds), handler); + let cost = rounds as u64; // static_gas + dynamic_gas + if let Err(err) = handler.record_cost(cost) { + log!( + handler.borrow_host(), + Info, + "Couldn't record the cost of blake2f {:?}", + err + ); + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + log!( + handler.borrow_host(), + Debug, + "Input is {:?}", + hex::encode(input) + ); + + // parse inputs + // the state vector - 8 unsigned 64-bit little-endian words + let mut h = [0_u64; 8]; + h.decode_from_le_slice(&input[4..68]); + + // the message block vector - 16 unsigned 64-bit little-endian words + let mut m = [0_u64; 16]; + m.decode_from_le_slice(&input[68..196]); + + // offset counters - 2 unsigned 64-bit little-endian words + let mut t = [0_u64; 2]; + t.decode_from_le_slice(&input[196..212]); + + // the final block indicator flag - 8-bit word (true if 1 or false if 0) + let f = match input[212] { + 1 => true, + 0 => false, + _ => return Err(blake2f_output_for_wrong_input()), + }; + + compress(&mut h, m, t, f, rounds as usize); + + let mut output = [0_u8; 64]; + for (i, state_word) in h.iter().enumerate() { + output[i * 8..(i + 1) * 8].copy_from_slice(&state_word.to_le_bytes()); + } + log!( + handler.borrow_host(), + Debug, + "Output is {:?}", + hex::encode(output) + ); + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +pub fn blake2f_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + call_precompile_with_gas_draining( + handler, + input, + blake2f_precompile_without_gas_draining, + ) +} + +#[cfg(test)] +mod tests { + use primitive_types::H160; + + use crate::precompiles::test_helpers::execute_precompiled; + + #[test] + fn test_blake2f_invalid_empty() { + let input = [0; 0]; + + // act + let result = execute_precompiled( + H160::from_low_u64_be(9), + &input, + None, + Some(25000), + true, + ); + + // assert + // expected outcome is Err + + assert!(result.is_err()); + } + + #[test] + fn test_blake2f_invalid_flag() { + let input = hex::decode( + "0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbab\ + d9831f79217e1319cde05b616263000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000300000000000000000000000000000002" + ).unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(9), + &input, + None, + Some(25000), + true, + ); + + assert!(result.is_err()); + } + + struct Blake2fTest { + input: &'static str, + expected: &'static str, + } + + const BLAKE2F_TESTS: [Blake2fTest; 4] = [ + Blake2fTest { + input: "\ + 0000000048c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbab\ + d9831f79217e1319cde05b616263000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "\ + 08c9bcf367e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d282e6ad7f520e511f6c3e2b8c68059b9442be0454267ce079\ + 217e1319cde05b" + }, + Blake2fTest { + input: "\ + 0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbab\ + d9831f79217e1319cde05b616263000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "\ + ba80a53f981c4d0d6a2797b69f12f6e94c212f14685ac4b74b12bb6fdbffa2d17d87c5392aab792dc252d5de4533cc9518d38aa8dbf1925ab9\ + 2386edd4009923" + }, + Blake2fTest { + input: "\ + 0000000c48c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbab\ + d9831f79217e1319cde05b616263000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000300000000000000000000000000000000", + expected: "\ + 75ab69d3190a562c51aef8d88f1c2775876944407270c42c9844252c26d2875298743e7f6d5ea2f2d3e8d226039cd31b4e426ac4f2d3d666a6\ + 10c2116fde4735" + }, + Blake2fTest { + input: "\ + 0000000148c9bdf267e6096a3ba7ca8485ae67bb2bf894fe72f36e3cf1361d5f3af54fa5d182e6ad7f520e511f6c3e2b8c68059b6bbd41fbab\ + d9831f79217e1319cde05b616263000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000300000000000000000000000000000001", + expected: "\ + b63a380cb2897d521994a85234ee2c181b5f844d2c624c002677e9703449d2fba551b3a8333bcdf5f2f7e08993d53923de3d64fcc68c034e71\ + 7b9293fed7a421" + } + ]; + + #[test] + fn test_blake2f_input_spec() { + for test in BLAKE2F_TESTS.iter() { + let input = hex::decode(test.input).unwrap(); + let result = execute_precompiled( + H160::from_low_u64_be(9), + &input, + None, + Some(25000), + true, + ); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + println!("{}", outcome.gas_used); + assert!(outcome.is_success()); + + let expected = hex::decode(test.expected).unwrap(); + + assert_eq!(Some(expected.as_slice()), outcome.output()); + } + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/ecdsa.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/ecdsa.rs new file mode 100644 index 000000000000..93b505082685 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/ecdsa.rs @@ -0,0 +1,350 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::fail_if_too_much; +use crate::precompiles::tick_model; +use crate::{handler::EvmHandler, precompiles::PrecompileOutcome, EthereumError}; +use evm::{Context, Transfer}; +use evm::{ExitReason, ExitSucceed}; +use libsecp256k1::{recover, Message, RecoveryId, Signature}; +use sha2::Digest; +use sha3::Keccak256; +use std::cmp::min; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::{Debug, Info}; +use tezos_evm_runtime::runtime::Runtime; + +macro_rules! unwrap_ecrecover { + ($expr : expr) => { + match $expr { + Ok(x) => x, + Err(_) => return Ok(erec_output_for_wrong_input()), + } + }; +} + +fn erec_output_for_wrong_input() -> PrecompileOutcome { + PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: vec![], + withdrawals: vec![], + estimated_ticks: 0, + } +} + +fn erec_parse_inputs(input: &[u8]) -> ([u8; 32], [u8; 32], [u8; 64]) { + // input is padded with 0 on the right + let mut clean_input: [u8; 128] = [0; 128]; + // and truncated if too large + let input_size = min(128, input.len()); + clean_input[..input_size].copy_from_slice(&input[..input_size]); + + // extract values + let mut hash = [0; 32]; + let mut v_array = [0; 32]; + let mut rs_array = [0; 64]; + hash.copy_from_slice(&clean_input[0..32]); + v_array.copy_from_slice(&clean_input[32..64]); + rs_array.copy_from_slice(&clean_input[64..128]); + (hash, v_array, rs_array) +} + +// Implementation of 0x01 ECDSA recover +pub fn ecrecover_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + log!(handler.borrow_host(), Debug, "Calling ecrecover precompile"); + + // check that enough resources to execute (gas / ticks) are available + let estimated_ticks = fail_if_too_much!(tick_model::ticks_of_ecrecover(), handler); + let cost = 3000; + if let Err(err) = handler.record_cost(cost) { + log!( + handler.borrow_host(), + Info, + "Couldn't record the cost of ecrecover {:?}", + err + ); + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + log!( + handler.borrow_host(), + Debug, + "Input is {:?}", + hex::encode(input) + ); + + // parse inputs + let (hash, v_array, rs_array) = erec_parse_inputs(input); + let v_raw = v_array[31]; + + if !(v_array[0..31] == [0u8; 31] && matches!(v_raw, 27 | 28)) { + return Ok(erec_output_for_wrong_input()); + } + + // `parse_standard` will check for potential overflows + let sig = unwrap_ecrecover!(Signature::parse_standard(&rs_array)); + let ri = unwrap_ecrecover!(RecoveryId::parse(v_raw - 27)); + + // check signature + let pubk = unwrap_ecrecover!(recover(&Message::parse(&hash), &sig, &ri)); + let mut hash = Keccak256::digest(&pubk.serialize()[1..]); + hash[..12].fill(0); + + log!( + handler.borrow_host(), + Debug, + "Output is {:?}", + hex::encode(hash) + ); + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: hash.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +#[cfg(test)] +mod tests { + use primitive_types::H160; + + use crate::precompiles::{ + ecdsa::erec_parse_inputs, test_helpers::execute_precompiled, + }; + + #[test] + fn test_ercover_parse_input_padding() { + let (h, v, rs) = erec_parse_inputs(&[1u8]); + assert_eq!(1, h[0]); + assert_eq!([0; 31], h[1..]); + assert_eq!(0, v[31]); + assert_eq!([0; 64], rs); + } + + #[test] + fn test_ercover_parse_input_order() { + let input = [[1; 32], [2; 32], [3; 32], [3; 32]].join(&[0u8; 0][..]); + let (h, v, rs) = erec_parse_inputs(&input); + assert_eq!([1; 32], h); + assert_eq!(2, v[31]); + assert_eq!([3; 64], rs); + } + + #[test] + fn test_ercover_parse_input_ignore_right_padding() { + let input = [[1; 32], [2; 32], [3; 32], [3; 32], [4; 32]].join(&[0u8; 0][..]); + let (h, v, rs) = erec_parse_inputs(&input); + assert_eq!([1; 32], h); + assert_eq!(2, v[31]); + assert_eq!([3; 64], rs); + } + + #[test] + fn test_ecrecover_invalid_empty() { + // act + let input: [u8; 0] = [0; 0]; + let result = execute_precompiled( + H160::from_low_u64_be(1), + &input, + None, + Some(25000), + true, + ); + + // assert + // expected outcome is OK and empty output + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(Some(&[] as &[u8]), outcome.output()); + } + + #[test] + fn test_ecrecover_invalid_zero() { + // act + let input: [u8; 128] = [0; 128]; + let result = execute_precompiled( + H160::from_low_u64_be(1), + &input, + None, + Some(25000), + true, + ); + + // assert + // expected outcome is OK but empty output + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(Some(&[] as &[u8]), outcome.output()); + } + + #[test] + fn test_ercover_parse_input_real() { + let (hash, v, rs) = input_legacy(); + let input: [u8; 128] = assemble_input(hash, v, rs); + let (ho, vo, rso) = erec_parse_inputs(&input); + assert_eq!(hex::decode(hash).unwrap(), ho); + assert_eq!(27, vo[31]); + assert_eq!(hex::decode(rs).unwrap(), rso); + } + + #[test] + fn test_ercover_parse_input_spec() { + let (hash, v, rs) = input_spec(); + let input: [u8; 128] = assemble_input(hash, v, rs); + let (ho, vo, rso) = erec_parse_inputs(&input); + assert_eq!(hex::decode(hash).unwrap(), ho); + assert_eq!(28, vo[31]); + assert_eq!(hex::decode(rs).unwrap(), rso); + } + + fn assemble_input(h: &str, v: &str, rs: &str) -> [u8; 128] { + let mut data_str = "".to_owned(); + data_str.push_str(h); + data_str.push_str(v); + data_str.push_str(rs); + let data = hex::decode(data_str).unwrap(); + let mut input: [u8; 128] = [0; 128]; + input.copy_from_slice(&data); + input + } + + fn input_legacy() -> (&'static str, &'static str, &'static str) { + // Obtain by signing a transaction tx_legacy.json (even though it doesn't need to be) + // address: 0xf0affc80a5f69f4a9a3ee01a640873b6ba53e539 + // privateKey: 0x84e147b8bc36d99cc6b1676318a0635d8febc9f02897b0563ad27358589ee502 + // publicKey: 0x08a4681ba8c520aaab2308957d401ffded69155b358246596846f87c0728e76f618f9772f16687ed5a2854234b037b71e4c3bc92cad78e575fb12c8df8b8dae5 + // node etherlink/kernel_evm/benchmarks/scripts/sign_tx.js $(pwd)/src/kernel_evm/benchmarks/scripts/transactions_example/tx_legacy.json 0x84e147b8bc36d99cc6b1676318a0635d8febc9f02897b0563ad27358589ee502 + let hash = "3c74ed8cf6d9695ac4de8e5dda38ac3719b3f42e913e0109344a5fcbd1ff8562"; + let rs = "b17daf010e907d83f0235467faac96f346c4cc46600477d1b5f543ced8c986b770221fd3c40e0cbaef013e9bb62cf8adc70c77a5c313954c03897f3f08f90726"; + // v = 27 -> 1b, is encoded as 32 bytes + let v = "000000000000000000000000000000000000000000000000000000000000001b"; + (hash, v, rs) + } + + fn input_spec() -> (&'static str, &'static str, &'static str) { + // taken from https://www.evm.codes/precompiled?fork=shanghai + let hash = "456e9aea5e197a1f1af7a3e85a3212fa4049a3ba34c2289b4c860fc0b0c64ef3"; + let rs = "9242685bf161793cc25603c231bc2f568eb630ea16aa137d2664ac80388256084f8ae3bd7535248d0bd448298cc2e2071e56992d0774dc340c368ae950852ada"; + // v = 28 -> 1c, is encoded as 32 bytes + let v = "000000000000000000000000000000000000000000000000000000000000001c"; + (hash, v, rs) + } + + #[test] + fn test_ecrecover_input_real() { + // setup + let (hash, v, rs) = input_legacy(); + let input: [u8; 128] = assemble_input(hash, v, rs); + let mut expected_address: Vec = + hex::decode("f0affc80a5f69f4a9a3ee01a640873b6ba53e539").unwrap(); + let mut expected_output = [0u8; 12].to_vec(); + expected_output.append(&mut expected_address); + + // act + let result = execute_precompiled( + H160::from_low_u64_be(1), + &input, + None, + Some(35000), + true, + ); + + // assert + // expected outcome is OK and address over 32 bytes + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!( + hex::encode(expected_output), + hex::encode(outcome.output().unwrap()) + ); + } + + #[test] + fn test_ecrecover_corrupted_data() { + // Input was taken from the official ethereum test-suite at: + // GeneralStateTests/stPrecompiledContracts2/CALLCODEEcrecoverV_prefixedf0.json + let corrupted_input = hex::decode("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000f01c73b1693892219d736caba55bdb67216e485557ea6b6af75f37096c9aa6a5a75feeb940b1d03b21e36b0e47e79769f095fe2ab855bd91e3a38756b7d75a9c4549").unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(1), + &corrupted_input, + None, + Some(35000), + true, + ); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.output().unwrap().is_empty()); + } + + #[test] + fn test_ecrecover_signature_overflow() { + // Input was taken from the official ethereum test-suite at: + // GeneralStateTests/stPrecompiledContracts2/CallEcrecover_Overflow.json + let input_overflow = hex::decode("18c547e4f7b0f325ad1e56f57e26c745b09a3e503d86e00e5255ff7f715d3d1c000000000000000000000000000000000000000000000000000000000000001c48b55bfa915ac795c431978d8a6a992b628d557da5ff759b307d495a36649353fffffffffffffffffffffffffffffffebaaedce6af48a03bbfd25e8cd0364142").unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(1), + &input_overflow, + None, + Some(35000), + true, + ); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.output().unwrap().is_empty()); + } + + #[test] + fn test_ecrecover_input_spec() { + let (hash, v, rs) = input_spec(); + let input: [u8; 128] = assemble_input(hash, v, rs); + + let mut expected_address: Vec = + hex::decode("7156526fbd7a3c72969b54f64e42c10fbb768c8a").unwrap(); + let mut expected_output = [0u8; 12].to_vec(); + expected_output.append(&mut expected_address); + + // act + let result = execute_precompiled( + H160::from_low_u64_be(1), + &input, + None, + Some(35000), + true, + ); + + // assert + // expected outcome is OK and address over 32 bytes + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!( + hex::encode(expected_output), + hex::encode(outcome.output().unwrap()) + ); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/fa_bridge.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/fa_bridge.rs new file mode 100644 index 000000000000..91f613ff8166 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/fa_bridge.rs @@ -0,0 +1,538 @@ +// SPDX-FileCopyrightText: 2023 PK Lab +// +// SPDX-License-Identifier: MIT + +//! FA bridge precompiled contract. +//! +//! Provides users with EVM interface for: +//! * Submitting ticket withdrawal requests +//! +//! This is a stateful precompile: +//! * Alters ticket table (changes balance) +//! * Increments outbox counter + +use evm::{Context, Handler, Transfer}; +use primitive_types::{H160, U256}; +use tezos_evm_runtime::runtime::Runtime; + +use crate::{ + fa_bridge::{execute_fa_withdrawal, withdrawal::FaWithdrawal}, + fail_if_too_much, + handler::EvmHandler, + EthereumError, +}; + +use super::{PrecompileOutcome, FA_BRIDGE_PRECOMPILE_ADDRESS}; + +/// Overapproximation of the amount of ticks for parsing +/// FA withdrawal from calldata, checking transfer value, +/// and executing the FA withdrawal, excluding the ticks consumed by +/// the inner proxy call. +/// +/// Linear regression parameters are obtained by running `bench_fa_withdrawal` +/// script, evaluating `run_transaction_ticks` against `data_size`. +/// Intercept is extended with +50% safe reserve. +pub const FA_WITHDRAWAL_PRECOMPILE_TICKS_INTERCEPT: u64 = 4_000_000; +pub const FA_WITHDRAWAL_PRECOMPILE_TICKS_SLOPE: u64 = 700; + +/// Added ("artificial") cost of doing FA withdrawal not including actual gas +/// spent for executing the withdrawal (and inner proxy contract call). +/// +/// This is roughly the implied costs of executing the outbox message on L1 +/// as a spam prevention mechanism (outbox queue clogging). +/// In particular it prevents cases when a big number of withdrawals is batched +/// together in a single transaction which exploits the system. +/// +/// An execution of a single outbox message carrying a FA withdrawal +/// costs around 0.0025ęś© on L1; the equivalent amount of gas units on L2 is: +/// +/// 0.0025 * 10^18 / GAS_PRICE +/// +/// Multiplying the numerator by 2 for a safe reserve and this is our cost in Wei. +pub const FA_WITHDRAWAL_PRECOMPILE_ADDED_COST: u64 = 5_000_000_000_000_000; + +/// Hard cap for the added gas cost (0.5 of the maximum gas limit per transaction). +/// If gas price drops the gas amount rises, but we don't want it to hit the transaction +/// gas limit. +pub const FA_WITHDRAWAL_PRECOMPILE_MAX_ADDED_CAS_COST: u64 = 15_000_000; + +/// Calculate precompile gas cost given the estimated amount of ticks and gas price. +fn estimate_gas_cost(estimated_ticks: u64, gas_price: U256) -> u64 { + // Using 1 gas unit ~= 1000 ticks convert ratio + let execution_cost = estimated_ticks / 1000; + let added_cost = U256::from(FA_WITHDRAWAL_PRECOMPILE_MAX_ADDED_CAS_COST) + .min(U256::from(FA_WITHDRAWAL_PRECOMPILE_ADDED_COST) / gas_price); + execution_cost + added_cost.as_u64() +} + +macro_rules! precompile_outcome_error { + ($($arg:tt)*) => { + crate::precompiles::PrecompileOutcome { + exit_status: evm::ExitReason::Error(evm::ExitError::Other( + std::borrow::Cow::from(format!($($arg)*)) + )), + withdrawals: vec![], + output: vec![], + estimated_ticks: 0, + } + }; +} + +/// FA bridge precompile entrypoint. +#[allow(unused)] +pub fn fa_bridge_precompile( + handler: &mut EvmHandler, + input: &[u8], + context: &Context, + is_static: bool, + transfer: Option, +) -> Result { + // We register the cost of the precompile early to prevent cases where inner proxy call + // consumes more ticks than allowed. + let estimated_ticks = FA_WITHDRAWAL_PRECOMPILE_TICKS_SLOPE * (input.len() as u64) + + FA_WITHDRAWAL_PRECOMPILE_TICKS_INTERCEPT; + handler.estimated_ticks_used += fail_if_too_much!(estimated_ticks, handler); + + // We also record gas cost which consists of computation cost (1 gas unit per 1000 ticks) + // and added FA withdrawal cost (spam prevention measure). + let estimated_gas_cost = estimate_gas_cost(estimated_ticks, handler.gas_price()); + if handler.record_cost(estimated_gas_cost).is_err() { + return Ok(precompile_outcome_error!( + "FA withdrawal: gas limit too low" + )); + } + + if is_static { + // It is a STATICCALL that prevents storage modification + // see https://eips.ethereum.org/EIPS/eip-214 + return Ok(precompile_outcome_error!( + "FA withdrawal: static call not allowed" + )); + } + + if context.address != FA_BRIDGE_PRECOMPILE_ADDRESS { + // It is a DELEGATECALL or CALLCODE (deprecated) which can be impersonating + // see https://eips.ethereum.org/EIPS/eip-7 + return Ok(precompile_outcome_error!( + "FA withdrawal: delegate call not allowed" + )); + } + + if transfer + .as_ref() + .map(|t| !t.value.is_zero()) + .unwrap_or(true) + { + return Ok(precompile_outcome_error!( + "FA withdrawal: unexpected value transfer {:?}", + transfer + )); + } + + match input { + // "withdraw"'s selector | 4 first bytes of keccak256("withdraw(address,bytes,uint256,bytes22,bytes)") + [0x80, 0xfc, 0x1f, 0xe3, input_data @ ..] => { + // Withdrawal initiator is the precompile caller. + // NOTE that since we deny delegate calls, it can either be EOA or + // a smart contract that calls the precompile directly (e.g. AA wallet). + match FaWithdrawal::try_parse(input_data, context.caller) { + Ok(withdrawal) => { + // Using Zero account here so that the inner proxy call + // has the same sender as during the FA deposit + // (so that the proxy contract has a single admin). + execute_fa_withdrawal(handler, H160::zero(), withdrawal) + } + Err(err) => Ok(precompile_outcome_error!( + "FA withdrawal: parsing failed w/ `{err}`" + )), + } + } + _ => Ok(precompile_outcome_error!( + "FA withdrawal: unexpected selector" + )), + } +} + +#[cfg(test)] +mod tests { + use std::{borrow::Cow, str::FromStr}; + + use alloy_sol_types::SolCall; + use evm::{Config, ExitError}; + use primitive_types::{H160, U256}; + use tezos_data_encoding::enc::BinWriter; + use tezos_evm_runtime::runtime::MockKernelHost; + + use crate::{ + account_storage::{init_account_storage, EthereumAccountStorage}, + fa_bridge::{ + deposit::ticket_hash, + test_utils::{ + convert_h160, convert_u256, deploy_reentrancy_tester, + dummy_fa_withdrawal, dummy_first_block, dummy_ticket, kernel_wrapper, + set_balance, ticket_balance_add, ticket_id, + }, + }, + handler::{EvmHandler, ExecutionOutcome, ExecutionResult}, + precompiles::{self, FA_BRIDGE_PRECOMPILE_ADDRESS}, + transaction::TransactionContext, + utilities::{bigint_to_u256, keccak256_hash}, + }; + + #[allow(clippy::too_many_arguments)] + fn execute_precompile( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + caller: H160, + value: U256, + input: Vec, + gas_limit: Option, + is_static: bool, + disable_reentrancy_guard: bool, + ) -> ExecutionOutcome { + let block = dummy_first_block(); + let config = Config::shanghai(); + let callee = FA_BRIDGE_PRECOMPILE_ADDRESS; + + let precompiles = precompiles::precompile_set::(true); + + let mut handler = EvmHandler::new( + host, + evm_account_storage, + caller, + &block, + &config, + &precompiles, + 100_000_000_000, + U256::from(21000), + false, + None, + ); + + if disable_reentrancy_guard { + handler.disable_reentrancy_guard(); + } + + handler + .call_contract(caller, callee, Some(value), input, gas_limit, is_static) + .expect("Failed to invoke precompile") + } + + #[test] + fn fa_bridge_precompile_fails_due_to_bad_selector() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + U256::zero(), + vec![0x00, 0x01, 0x02, 0x03], + None, + false, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("unexpected selector")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_low_gas_limit() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + U256::zero(), + vec![0x80, 0xfc, 0x1f, 0xe3], + // Cover only basic cost + Some(21000 + 16 * 4), + false, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("gas limit too low")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_non_zero_value() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &caller, + 1_000_000_000.into(), + ); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + caller, + 1_000_000_000.into(), + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + false, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("unexpected value transfer")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_static_call() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + caller, + U256::zero(), + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + true, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("static call not allowed")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_delegate_call() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = H160::from_low_u64_be(1); + let callee = H160::from_low_u64_be(2); + let block = dummy_first_block(); + let config = Config::shanghai(); + + let precompiles = precompiles::precompile_set::(true); + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + 1_000_000_000, + U256::from(21000), + false, + None, + ); + + handler.begin_initial_transaction(false, None).unwrap(); + + let result = handler.execute_call( + FA_BRIDGE_PRECOMPILE_ADDRESS, + None, + vec![0x80, 0xfc, 0x1f, 0xe3], + TransactionContext::new(caller, callee, U256::zero()), + ); + + let outcome = handler.end_initial_transaction(result).unwrap(); + + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("delegate call not allowed")) + ); + } + + #[test] + fn fa_bridge_precompile_fails_due_to_invalid_input() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + H160::from_low_u64_be(1), + U256::zero(), + vec![0x80, 0xfc, 0x1f, 0xe3], + None, + false, + false, + ); + assert!(!outcome.is_success()); + assert!( + matches!(outcome.result, ExecutionResult::Error(ExitError::Other(err)) if err.contains("parsing failed")) + ); + } + + #[test] + fn fa_bridge_precompile_succeeds_without_l2_proxy_contract() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let ticket_owner = H160::from_low_u64_be(1); + let ticket = dummy_ticket(); + let ticket_hash = ticket_hash(&ticket).unwrap(); + let amount = bigint_to_u256(ticket.amount()).unwrap(); + + // Patch ticket table + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &ticket_hash, + &ticket_owner, + amount, + ); + + let (ticketer, content) = ticket_id(&ticket); + + let routing_info = [ + [0u8; 22].to_vec(), + vec![0x01], + [0u8; 20].to_vec(), + vec![0x00], + ] + .concat(); + + let input = kernel_wrapper::withdrawCall::new(( + convert_h160(&ticket_owner), + routing_info.into(), + convert_u256(&amount), + ticketer.into(), + content.into(), + )) + .abi_encode(); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + ticket_owner, + U256::zero(), + input, + Some(30_000_000), + false, + false, + ); + assert!(outcome.is_success()); + assert_eq!(1, outcome.withdrawals.len()); + assert_eq!(1, outcome.logs.len()); + } + + #[test] + fn fa_bridge_precompile_address() { + assert_eq!( + FA_BRIDGE_PRECOMPILE_ADDRESS, + H160::from_str("ff00000000000000000000000000000000000002").unwrap() + ); + } + + #[test] + fn fa_bridge_precompile_withdraw_method_id() { + let method_hash = + keccak256_hash(b"withdraw(address,bytes,uint256,bytes22,bytes)"); + assert_eq!(method_hash.0[0..4], [0x80, 0xfc, 0x1f, 0xe3]); + } + + #[test] + fn fa_bridge_precompile_cannot_call_itself() { + let mut mock_runtime = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let system = H160::zero(); + let sender = H160::from_low_u64_be(1); + let ticket = dummy_ticket(); + + let proxy = deploy_reentrancy_tester( + &mut mock_runtime, + &mut evm_account_storage, + &ticket, + &system, + U256::from(2), + U256::from(4), + ) + .new_address() + .expect("Failed to deploy reentrancy tester"); + + ticket_balance_add( + &mut mock_runtime, + &mut evm_account_storage, + &ticket_hash(&ticket).unwrap(), + &proxy, + U256::from(100), + ); + + let withdrawal = dummy_fa_withdrawal(ticket, sender, proxy); + + let mut receiver = Vec::new(); + withdrawal.receiver.bin_write(&mut receiver).unwrap(); + + let mut proxy = Vec::new(); + withdrawal.proxy.bin_write(&mut proxy).unwrap(); + + let mut ticketer = Vec::new(); + withdrawal + .ticket + .creator() + .0 + .bin_write(&mut ticketer) + .unwrap(); + + let mut contents = Vec::new(); + withdrawal + .ticket + .contents() + .bin_write(&mut contents) + .unwrap(); + + let input = kernel_wrapper::withdrawCall::new(( + convert_h160(&withdrawal.ticket_owner), + [receiver, proxy].concat().into(), + convert_u256(&withdrawal.amount), + TryInto::<[u8; 22]>::try_into(ticketer).unwrap().into(), + contents.into(), + )); + + let outcome: ExecutionOutcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + sender, + U256::zero(), + input.abi_encode(), + // Note that we set gas limit larger than hard cap for a single transaction: + // that is to overcome the added cost per FA withdrawal which is pretty large + // for the given gas price (up to 15M). + Some(100_000_000), + false, + false, + ); + assert_eq!( + outcome.result, + ExecutionResult::FatalError(evm::ExitFatal::CallErrorAsFatal( + ExitError::Other(Cow::from("Circular calls are not allowed")) + )) + ); + + let outcome = execute_precompile( + &mut mock_runtime, + &mut evm_account_storage, + sender, + U256::zero(), + input.abi_encode(), + Some(100_000_000), + false, + true, + ); + assert!(outcome.is_success()); + assert!(!outcome.withdrawals.is_empty()); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/hash.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/hash.rs new file mode 100644 index 000000000000..c3d25159b422 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/hash.rs @@ -0,0 +1,158 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::fail_if_too_much; +use crate::precompiles::{tick_model, PrecompileOutcome}; +use crate::{handler::EvmHandler, EthereumError}; +use evm::{Context, ExitReason, ExitSucceed, Transfer}; +use ripemd::Ripemd160; +use sha2::{Digest, Sha256}; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Debug; +use tezos_evm_runtime::runtime::Runtime; + +// Implementation of 0x03 precompiled (sha256) +pub fn sha256_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + log!(handler.borrow_host(), Debug, "Calling sha2-256 precompile"); + let estimated_ticks = + fail_if_too_much!(tick_model::ticks_of_sha256(input.len()), handler); + + let size = input.len() as u64; + let data_word_size = (31 + size) / 32; + let cost = 60 + 12 * data_word_size; + + if let Err(err) = handler.record_cost(cost) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + let output = Sha256::digest(input); + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +// Implementation of 0x04 precompiled (ripemd160) +pub fn ripemd160_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + log!( + handler.borrow_host(), + Debug, + "Calling ripemd-160 precompile" + ); + let estimated_ticks = + fail_if_too_much!(tick_model::ticks_of_ripemd160(input.len()), handler); + + let size = input.len() as u64; + let data_word_size = (31 + size) / 32; + let cost = 600 + 120 * data_word_size; + + if let Err(err) = handler.record_cost(cost) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + let hash = Ripemd160::digest(input); + // The 20-byte hash is returned right aligned to 32 bytes + let mut output = [0u8; 32]; + output[12..].clone_from_slice(&hash); + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +#[cfg(test)] +mod tests { + use evm::ExitSucceed; + use primitive_types::H160; + + use crate::{ + handler::{ExecutionOutcome, ExecutionResult}, + precompiles::test_helpers::execute_precompiled, + }; + + #[test] + fn call_sha256() { + // act + let input: &[u8] = &[0xFF]; + let address = H160::from_low_u64_be(2u64); + let result = execute_precompiled(address, input, None, Some(22000), true); + + // assert + let expected_hash = hex::decode( + "a8100ae6aa1940d0b663bb31cd466142ebbdbd5187131b92d93818987832eb89", + ) + .expect("Result should be hex string"); + + let expected_gas = 21000 // base cost + + 72 // sha256 cost + + 16; // transaction data cost + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Returned, expected_hash), + withdrawals: vec![], + estimated_ticks_used: 75_000, + }; + + assert_eq!(Ok(expected), result); + } + + #[test] + fn call_ripemd() { + // act + let input: &[u8] = &[0xFF]; + let address = H160::from_low_u64_be(3u64); + let result = execute_precompiled(address, input, None, Some(22000), true); + + // assert + let expected_hash = hex::decode( + "0000000000000000000000002c0c45d3ecab80fe060e5f1d7057cd2f8de5e557", + ) + .expect("Result should be hex string"); + + let expected_gas = 21000 // base cost + + 600 + 120// ripeMD cost + + 16; // transaction data cost + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallSucceeded(ExitSucceed::Returned, expected_hash), + withdrawals: vec![], + estimated_ticks_used: 70_000, + }; + + assert_eq!(Ok(expected), result); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/identity.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/identity.rs new file mode 100644 index 000000000000..680288ae9f9f --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/identity.rs @@ -0,0 +1,48 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::fail_if_too_much; +use crate::precompiles::tick_model; +use crate::{handler::EvmHandler, precompiles::PrecompileOutcome, EthereumError}; +use evm::{Context, Transfer}; +use evm::{ExitReason, ExitSucceed}; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Debug; +use tezos_evm_runtime::runtime::Runtime; + +// Implementation of 0x02 precompiled (identity) +pub fn identity_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + log!(handler.borrow_host(), Debug, "Calling identity precompile"); + let estimated_ticks = + fail_if_too_much!(tick_model::ticks_of_identity(input.len()), handler); + + let size = input.len() as u64; + let data_word_size = (size + 31) / 32; + let static_gas = 15; + let dynamic_gas = 3 * data_word_size; + let cost = static_gas + dynamic_gas; + + if let Err(err) = handler.record_cost(cost) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: input.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/mod.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/mod.rs new file mode 100644 index 000000000000..f11eeee487fe --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/mod.rs @@ -0,0 +1,392 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +//! Precompiles for the EVM +//! +//! This module defines the set of precompiled function for the +//! EVM interpreter to use instead of calling contracts. +//! Unfortunately, we cannot use the standard `PrecompileSet` +//! provided by SputnikVM, as we require the Host type and object +//! for writing to the log. + +use std::vec; + +mod blake2; +mod ecdsa; +mod fa_bridge; +mod hash; +mod identity; +mod modexp; +pub(crate) mod reentrancy_guard; +mod revert; +mod withdrawal; +mod zero_knowledge; + +use crate::handler::{EvmHandler, Withdrawal}; +use crate::EthereumError; +use alloc::collections::btree_map::BTreeMap; +use blake2::blake2f_precompile; +use ecdsa::ecrecover_precompile; +use evm::{Context, ExitReason, Handler, Transfer}; +use fa_bridge::fa_bridge_precompile; +use hash::{ripemd160_precompile, sha256_precompile}; +use identity::identity_precompile; +use modexp::modexp_precompile; +use primitive_types::H160; +use revert::revert_precompile; +use tezos_evm_runtime::runtime::Runtime; +use withdrawal::withdrawal_precompile; +use zero_knowledge::{ecadd_precompile, ecmul_precompile, ecpairing_precompile}; + +/// FA bridge precompile address +pub const FA_BRIDGE_PRECOMPILE_ADDRESS: H160 = H160([ + 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 2, +]); + +// System (zero) account address, owns ticket table and withdrawal counter +pub const SYSTEM_ACCOUNT_ADDRESS: H160 = H160::zero(); + +/// Outcome of executing a precompiled contract. Covers both successful +/// return, stop and revert and additionally, it covers contract execution +/// failures (malformed input etc.). This is encoded using the `ExitReason` +/// same as with normal contract calls. +#[derive(PartialEq, Debug)] +pub struct PrecompileOutcome { + /// Status after execution. This has the same semantics as with normal + /// contract calls. + pub exit_status: ExitReason, + /// The return value of the call. + pub output: Vec, + /// Any withdrawals produced by the precompiled contract. This encodes + /// withdrawals to Tezos Layer 1. + pub withdrawals: Vec, + /// Number of ticks estimated by the tick model of the precompiled contract. + /// Note that the implementation of the contract is responsible for failing + /// with EthereumError::OutOfTicks if the number of tricks would make the + /// total number of ticks of the Handler go over the allocated number of + /// ticks. + pub estimated_ticks: u64, +} + +/// Type for a single precompiled contract +pub type PrecompileFn = fn( + _: &mut EvmHandler, + _: &[u8], + _: &Context, + _: bool, + _: Option, +) -> Result; + +/// Trait for encapsulating all precompiles +/// +/// This is adapted from SputnikVM trait with same name. It has been +/// modified to take the Host into account, so that precompiles can +/// interact with log and durable storage and the rest of the kernel. +pub trait PrecompileSet { + /// Execute a single contract call to a precompiled contract. Should + /// return None (and have no effect), if there is no precompiled contract + /// at the address given. + #[allow(clippy::too_many_arguments)] + fn execute( + &self, + handler: &mut EvmHandler, + address: H160, + input: &[u8], + context: &Context, + is_static: bool, + transfer: Option, + ) -> Option>; + + /// Check if there is a precompiled contract at the given address. + fn is_precompile(&self, address: H160) -> bool; +} + +/// One implementation for PrecompileSet above. Adapted from SputnikVM. +pub type PrecompileBTreeMap = BTreeMap>; + +impl PrecompileSet for PrecompileBTreeMap { + fn execute( + &self, + handler: &mut EvmHandler, + address: H160, + input: &[u8], + context: &Context, + is_static: bool, + transfer: Option, + ) -> Option> + where + Host: Runtime, + { + self.get(&address) + .map(|precompile| (*precompile)(handler, input, context, is_static, transfer)) + } + + /// Check if the given address is a precompile. Should only be called to + /// perform the check while not executing the precompile afterward, since + /// `execute` already performs a check internally. + fn is_precompile(&self, address: H160) -> bool { + self.contains_key(&address) + } +} + +type PrecompileWithoutGasDrainFn = + fn(_: &mut EvmHandler, _: &[u8]) -> Result; + +pub fn call_precompile_with_gas_draining( + handler: &mut EvmHandler, + input: &[u8], + precompile_contract_without_gas: PrecompileWithoutGasDrainFn, +) -> Result { + match precompile_contract_without_gas(handler, input) { + Ok(precompile_outcome) => Ok(precompile_outcome), + Err(err) => { + if let Err(record_err) = handler.record_cost(handler.gas_left().as_u64()) { + Ok(PrecompileOutcome { + exit_status: ExitReason::Error(record_err), + output: vec![], + withdrawals: vec![], + estimated_ticks: 0, + }) + } else { + Err(err) + } + } + } +} + +// Prefixed by 'ff' to make sure we will not conflict with any +// upcoming Ethereum upgrades. +pub const WITHDRAWAL_ADDRESS: H160 = H160([ + 0xff, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1, +]); + +pub fn evm_precompile_set() -> PrecompileBTreeMap { + BTreeMap::from([ + ( + H160::from_low_u64_be(1u64), + ecrecover_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(2u64), + sha256_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(3u64), + ripemd160_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(4u64), + identity_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(5u64), + modexp_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(6u64), + ecadd_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(7u64), + ecmul_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(8u64), + ecpairing_precompile as PrecompileFn, + ), + ( + H160::from_low_u64_be(9u64), + blake2f_precompile as PrecompileFn, + ), + ]) +} + +/// Factory function for generating the precompileset that the EVM kernel uses. +pub fn precompile_set( + enable_fa_withdrawals: bool, +) -> PrecompileBTreeMap { + let mut precompiles = evm_precompile_set(); + + precompiles.insert( + WITHDRAWAL_ADDRESS, + withdrawal_precompile as PrecompileFn, + ); + + if enable_fa_withdrawals { + precompiles.insert( + FA_BRIDGE_PRECOMPILE_ADDRESS, + fa_bridge_precompile as PrecompileFn, + ); + } + precompiles +} + +pub fn precompile_set_with_revert_withdrawals( + enable_fa_withdrawals: bool, +) -> PrecompileBTreeMap { + let mut precompiles = evm_precompile_set(); + + precompiles.insert(WITHDRAWAL_ADDRESS, revert_precompile as PrecompileFn); + + if enable_fa_withdrawals { + precompiles.insert( + FA_BRIDGE_PRECOMPILE_ADDRESS, + revert_precompile as PrecompileFn, + ); + } + + precompiles +} + +#[macro_export] +macro_rules! fail_if_too_much { + ($estimated_ticks : expr, $handler: expr) => { + if $estimated_ticks + $handler.estimated_ticks_used > $handler.ticks_allocated { + return Err(EthereumError::OutOfTicks); + } else { + $estimated_ticks + } + }; +} + +mod tick_model { + pub fn ticks_of_sha256(data_size: usize) -> u64 { + let size = data_size as u64; + 75_000 + 30_000 * (size.div_euclid(64)) + } + pub fn ticks_of_ripemd160(data_size: usize) -> u64 { + let size = data_size as u64; + 70_000 + 20_000 * (size.div_euclid(64)) + } + pub fn ticks_of_identity(data_size: usize) -> u64 { + let size = data_size as u64; + 42_000 + 35 * size + } + pub fn ticks_of_withdraw() -> u64 { + 880_000 + } + + pub fn ticks_of_ecrecover() -> u64 { + 30_000_000 + } + + pub fn ticks_of_blake2f(rounds: u32) -> u64 { + 1_850_000 + 3_200 * rounds as u64 + } +} + +#[cfg(test)] +mod test_helpers { + use crate::account_storage::account_path; + use crate::account_storage::init_account_storage as init_evm_account_storage; + use crate::account_storage::EthereumAccountStorage; + use crate::handler::EvmHandler; + use crate::handler::ExecutionOutcome; + use crate::EthereumError; + use crate::NATIVE_TOKEN_TICKETER_PATH; + use evm::Config; + use evm::Transfer; + use host::runtime::Runtime; + use primitive_types::{H160, U256}; + use tezos_ethereum::block::BlockConstants; + use tezos_ethereum::block::BlockFees; + use tezos_evm_runtime::runtime::MockKernelHost; + + use super::precompile_set; + const DUMMY_ALLOCATED_TICKS: u64 = 100_000_000; + pub const DUMMY_TICKETER: &str = "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5"; + + pub fn set_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } + } + + pub fn execute_precompiled( + address: H160, + input: &[u8], + transfer: Option, + gas_limit: Option, + is_static: bool, + ) -> Result { + let caller = H160::from_low_u64_be(118u64); + let mut mock_runtime = MockKernelHost::default(); + let block_fees = BlockFees::new( + U256::from(21000), + U256::from(21000), + U256::from(2_000_000_000_000u64), + ); + let block = BlockConstants::first_block( + U256::zero(), + U256::one(), + block_fees, + u64::MAX, + H160::zero(), + ); + let mut evm_account_storage = init_evm_account_storage().unwrap(); + let precompiles = precompile_set::(false); + let config = Config::shanghai(); + let gas_price = U256::from(21000); + + set_ticketer(&mut mock_runtime, DUMMY_TICKETER); + + if let Some(Transfer { source, value, .. }) = transfer { + set_balance( + &mut mock_runtime, + &mut evm_account_storage, + &source, + value + + gas_limit + .map(U256::from) + .unwrap_or_default() + .saturating_mul(gas_price), + ); + } + + let mut handler = EvmHandler::new( + &mut mock_runtime, + &mut evm_account_storage, + caller, + &block, + &config, + &precompiles, + DUMMY_ALLOCATED_TICKS, + gas_price, + false, + None, + ); + + let value = transfer.map(|t| t.value); + + handler.call_contract( + caller, + address, + value, + input.to_vec(), + gas_limit, + is_static, + ) + } + + fn set_ticketer(host: &mut MockKernelHost, address: &str) { + host.store_write(&NATIVE_TOKEN_TICKETER_PATH, address.as_bytes(), 0) + .unwrap(); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/modexp.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/modexp.rs new file mode 100644 index 000000000000..132bafe5163b --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/modexp.rs @@ -0,0 +1,445 @@ +// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2023 draganrakita +// +// SPDX-License-Identifier: MIT + +use std::{ + borrow::Cow, + cmp::{max, min}, +}; + +use crate::{ + handler::EvmHandler, + precompiles::PrecompileOutcome, + utilities::{get_right_padded, get_right_padded_vec, left_padding, left_padding_vec}, + EthereumError, +}; +use aurora_engine_modexp::modexp; +use evm::{Context, ExitError, ExitReason, ExitSucceed, Transfer}; +use primitive_types::U256; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Debug; +use tezos_evm_runtime::runtime::Runtime; + +fn calculate_iteration_count(exp_length: u64, exp_highp: &U256) -> u64 { + let mut iteration_count: u64 = 0; + + if exp_length <= 32 && *exp_highp == U256::zero() { + iteration_count = 0; + } else if exp_length <= 32 { + iteration_count = exp_highp.bits() as u64 - 1; + } else if exp_length > 32 { + iteration_count = (8 * (exp_length - 32)) + max(1, exp_highp.bits() as u64) - 1; + } + + max(iteration_count, 1) +} + +// Calculate gas cost according to EIP 2565: +// https://eips.ethereum.org/EIPS/eip-2565 +fn gas_calc(base_length: u64, exp_length: u64, mod_length: u64, exp_highp: &U256) -> u64 { + fn calculate_multiplication_complexity(base_length: u64, mod_length: u64) -> U256 { + let max_length = max(base_length, mod_length); + let mut words = max_length / 8; + if max_length % 8 > 0 { + words += 1; + } + let words = U256::from(words); + words * words + } + + let multiplication_complexity = + calculate_multiplication_complexity(base_length, mod_length); + let iteration_count = calculate_iteration_count(exp_length, exp_highp); + let gas = (multiplication_complexity * U256::from(iteration_count)) / U256::from(3); + + if gas.0[1] != 0 || gas.0[2] != 0 || gas.0[3] != 0 { + u64::MAX + } else { + max(200, gas.0[0]) + } +} + +fn modexp_mod_overflow_exit(reason: &'static str) -> PrecompileOutcome { + PrecompileOutcome { + exit_status: ExitReason::Error(ExitError::Other(Cow::Borrowed(reason))), + output: vec![], + withdrawals: vec![], + estimated_ticks: 0, + } +} + +// The format of input is: +// +// Where every length is a 32-byte left-padded integer representing the number of bytes +// to be taken up by the next value +const HEADER_LENGTH: usize = 96; + +pub fn modexp_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + log!(handler.borrow_host(), Debug, "Calling modexp precompile"); + + // Extract the header. + let base_len = U256::from_big_endian(&get_right_padded::<32>(input, 0)); + let exp_len = U256::from_big_endian(&get_right_padded::<32>(input, 32)); + let mod_len = U256::from_big_endian(&get_right_padded::<32>(input, 64)); + + let estimated_ticks = tick::model(base_len, exp_len, mod_len); + + // cast base and modulus to usize, it does not make sense to handle larger values + let Ok(base_len) = usize::try_from(base_len) else { + return Ok(modexp_mod_overflow_exit("base length: modexp mod overflow")); + }; + // cast mod length to usize, it does not make sense to handle larger values. + let Ok(mod_len) = usize::try_from(mod_len) else { + return Ok(modexp_mod_overflow_exit("mod length: modexp mod overflow")); + }; + + // Handle a special case when both the base and mod length is zero + if base_len == 0 && mod_len == 0 { + if let Err(err) = handler.record_cost(200) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + return Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + // cast exponent length to usize, it does not make sense to handle larger values. + let Ok(exp_len) = usize::try_from(exp_len) else { + return Ok(modexp_mod_overflow_exit( + "exponent length: modexp mod overflow", + )); + }; + + // Used to extract ADJUSTED_EXPONENT_LENGTH. + let exp_highp_len = min(exp_len, 32); + + // throw away the header data as we already extracted lengths. + let input = if input.len() >= HEADER_LENGTH { + &input[HEADER_LENGTH..] + } else { + // or set input to zero if there is no more data + &[] + }; + + let exp_highp = { + // get right padded bytes so if data.len is less then exp_len we will get right padded zeroes. + let right_padded_highp = get_right_padded::<32>(input, base_len); + // If exp_len is less then 32 bytes get only exp_len bytes and do left padding. + let out = left_padding::<32>(&right_padded_highp[..exp_highp_len]); + U256::from_big_endian(&out) + }; + + // calculate gas spent. + let gas_cost = gas_calc(base_len as u64, exp_len as u64, mod_len as u64, &exp_highp); + + if let Err(err) = handler.record_cost(gas_cost) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + // Padding is needed if the input does not contain all 3 values. + let base = get_right_padded_vec(input, 0, base_len); + let exponent = get_right_padded_vec(input, base_len, exp_len); + let modulus = get_right_padded_vec(input, base_len.saturating_add(exp_len), mod_len); + + // Call the modexp. + let output = modexp(&base, &exponent, &modulus); + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + // left pad the result to modulus length. bytes will always by less or equal to modulus length. + output: left_padding_vec(&output, mod_len), + withdrawals: vec![], + estimated_ticks, + }) +} + +mod tick { + use primitive_types::U256; + + const MIN_LEADING_ZEROS: u32 = 256 - 32; + const TICKS_BASE_COST: u64 = 100_000; + + const ESIZE_FACTOR1: u64 = 241; + const ESIZE_FACTOR2: u64 = 6480; + const ESIZE_FACTOR3: u64 = 114172; + const MSIZE_FACTOR: u64 = 9346; + const CONSTANT_TERM: u64 = 112053; + + pub fn model(bsize: U256, esize: U256, msize: U256) -> u64 { + // If either of bsize, esize or msize are bigger than what can be held in 63 bits, then + // the number of ticks needed to compute modexp is way too high. + if bsize.leading_zeros() < MIN_LEADING_ZEROS { + return TICKS_BASE_COST; + } + if esize.leading_zeros() < MIN_LEADING_ZEROS { + return TICKS_BASE_COST; + } + if msize.leading_zeros() < MIN_LEADING_ZEROS { + return TICKS_BASE_COST; + } + + let esize: u64 = esize.low_u64(); + let msize: u64 = msize.low_u64(); + + let estimated_ticks = ESIZE_FACTOR1 * msize * msize * esize + + ESIZE_FACTOR2 * msize * esize + + ESIZE_FACTOR3 * esize + + MSIZE_FACTOR * msize + + CONSTANT_TERM; + + estimated_ticks.max(TICKS_BASE_COST) + } +} + +#[cfg(test)] +mod tests { + use primitive_types::H160; + + use crate::{ + handler::ExecutionResult, precompiles::test_helpers::execute_precompiled, + }; + + struct ModexpTestCase { + input: &'static str, + expected: &'static str, + _name: &'static str, // Used as a comment and debugging if needed. + } + + const MODEXP_TESTS: [ModexpTestCase; 19] = [ + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000064\ + 0000000000000000000000000000000000000000000000000000000000000064\ + 0000000000000000000000000000000000000000000000000000000000000064\ + 5442ddc2b70f66c1f6d2b296c0a875be7eddd0a80958cbc7425f1899ccf90511\ + a5c318226e48ee23f130b44dc17a691ce66be5da18b85ed7943535b205aa125e\ + 9f59294a00f05155c23e97dac6b3a00b0c63c8411bf815fc183b420b4d9dc5f7\ + 15040d5c60957f52d334b843197adec58c131c907cd96059fc5adce9dda351b5\ + df3d666fcf3eb63c46851c1816e323f2119ebdf5ef35", + expected: "00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000", + _name: "eth_tests_modexp_modsize0_returndatasizeFiller", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 03\ + fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e\ + ffffffffffffffffffffffffffffffffffffffffff2f", + expected: "162ead82cadefaeaf6e9283248fdf2f2845f6396f6f17c4d5a39f820b6f6b5f9", + _name: "eth_tests_create2callPrecompiles_test0_berlin", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 03\ + fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e\ + fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + expected: "0000000000000000000000000000000000000000000000000000000000000001", + _name: "eip198_example_1", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000020\ + fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2e\ + fffffffffffffffffffffffffffffffffffffffffffffffffffffffefffffc2f", + expected: "0000000000000000000000000000000000000000000000000000000000000000", + _name: "eip198_example_2", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000040\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000040\ + e09ad9675465c53a109fac66a445c91b292d2bb2c5268addb30cd82f80fcb003\ + 3ff97c80a5fc6f39193ae969c6ede6710a6b7ac27078a06d90ef1c72e5c85fb5\ + 02fc9e1f6beb81516545975218075ec2af118cd8798df6e08a147c60fd6095ac\ + 2bb02c2908cf4dd7c81f11c289e4bce98f3553768f392a80ce22bf5c4f4a248c\ + 6b", + expected: "60008f1614cc01dcfb6bfb09c625cf90b47d4468db81b5f8b7a39d42f332eab9b2da8f2d95311648a8f243f4bb13cfb3d8f7f2a3c014122ebb3ed41b02783adc", + _name: "nagydani_1_square", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000040\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000040\ + e09ad9675465c53a109fac66a445c91b292d2bb2c5268addb30cd82f80fcb003\ + 3ff97c80a5fc6f39193ae969c6ede6710a6b7ac27078a06d90ef1c72e5c85fb5\ + 03fc9e1f6beb81516545975218075ec2af118cd8798df6e08a147c60fd6095ac\ + 2bb02c2908cf4dd7c81f11c289e4bce98f3553768f392a80ce22bf5c4f4a248c\ + 6b", + expected: "4834a46ba565db27903b1c720c9d593e84e4cbd6ad2e64b31885d944f68cd801f92225a8961c952ddf2797fa4701b330c85c4b363798100b921a1a22a46a7fec", + _name: "nagydani_1_qube" + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000040\ + 0000000000000000000000000000000000000000000000000000000000000003\ + 0000000000000000000000000000000000000000000000000000000000000040\ + e09ad9675465c53a109fac66a445c91b292d2bb2c5268addb30cd82f80fcb003\ + 3ff97c80a5fc6f39193ae969c6ede6710a6b7ac27078a06d90ef1c72e5c85fb5\ + 010001fc9e1f6beb81516545975218075ec2af118cd8798df6e08a147c60fd60\ + 95ac2bb02c2908cf4dd7c81f11c289e4bce98f3553768f392a80ce22bf5c4f4a\ + 248c6b", + expected: "c36d804180c35d4426b57b50c5bfcca5c01856d104564cd513b461d3c8b8409128a5573e416d0ebe38f5f736766d9dc27143e4da981dfa4d67f7dc474cbee6d2", + _name: "nagydani_1_pow0x10001", + }, + ModexpTestCase { + input: "\ + 0000000000000000000000000000000000000000000000000000000000000080\ + 0000000000000000000000000000000000000000000000000000000000000001\ + 0000000000000000000000000000000000000000000000000000000000000080\ + cad7d991a00047dd54d3399b6b0b937c718abddef7917c75b6681f40cc15e2be\ + 0003657d8d4c34167b2f0bbbca0ccaa407c2a6a07d50f1517a8f22979ce12a81\ + dcaf707cc0cebfc0ce2ee84ee7f77c38b9281b9822a8d3de62784c089c9b18dc\ + b9a2a5eecbede90ea788a862a9ddd9d609c2c52972d63e289e28f6a590ffbf51\ + 02e6d893b80aeed5e6e9ce9afa8a5d5675c93a32ac05554cb20e9951b2c140e3\ + ef4e433068cf0fb73bc9f33af1853f64aa27a0028cbf570d7ac9048eae5dc7b2\ + 8c87c31e5810f1e7fa2cda6adf9f1076dbc1ec1238560071e7efc4e9565c49be\ + 9e7656951985860a558a754594115830bcdb421f741408346dd5997bb01c2870\ + 87", + expected: "981dd99c3b113fae3e3eaa9435c0dc96779a23c12a53d1084b4f67b0b053a27560f627b873e3f16ad78f28c94f14b6392def26e4d8896c5e3c984e50fa0b3aa44f1da78b913187c6128baa9340b1e9c9a0fd02cb78885e72576da4a8f7e5a113e173a7a2889fde9d407bd9f06eb05bc8fc7b4229377a32941a02bf4edcc06d70", + _name: "nagydani_2_square", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000080cad7d991a00047dd54d3399b6b0b937c718abddef7917c75b6681f40cc15e2be0003657d8d4c34167b2f0bbbca0ccaa407c2a6a07d50f1517a8f22979ce12a81dcaf707cc0cebfc0ce2ee84ee7f77c38b9281b9822a8d3de62784c089c9b18dcb9a2a5eecbede90ea788a862a9ddd9d609c2c52972d63e289e28f6a590ffbf5103e6d893b80aeed5e6e9ce9afa8a5d5675c93a32ac05554cb20e9951b2c140e3ef4e433068cf0fb73bc9f33af1853f64aa27a0028cbf570d7ac9048eae5dc7b28c87c31e5810f1e7fa2cda6adf9f1076dbc1ec1238560071e7efc4e9565c49be9e7656951985860a558a754594115830bcdb421f741408346dd5997bb01c287087", + expected: "d89ceb68c32da4f6364978d62aaa40d7b09b59ec61eb3c0159c87ec3a91037f7dc6967594e530a69d049b64adfa39c8fa208ea970cfe4b7bcd359d345744405afe1cbf761647e32b3184c7fbe87cee8c6c7ff3b378faba6c68b83b6889cb40f1603ee68c56b4c03d48c595c826c041112dc941878f8c5be828154afd4a16311f", + _name: "nagydani_2_qube", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000008000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000080cad7d991a00047dd54d3399b6b0b937c718abddef7917c75b6681f40cc15e2be0003657d8d4c34167b2f0bbbca0ccaa407c2a6a07d50f1517a8f22979ce12a81dcaf707cc0cebfc0ce2ee84ee7f77c38b9281b9822a8d3de62784c089c9b18dcb9a2a5eecbede90ea788a862a9ddd9d609c2c52972d63e289e28f6a590ffbf51010001e6d893b80aeed5e6e9ce9afa8a5d5675c93a32ac05554cb20e9951b2c140e3ef4e433068cf0fb73bc9f33af1853f64aa27a0028cbf570d7ac9048eae5dc7b28c87c31e5810f1e7fa2cda6adf9f1076dbc1ec1238560071e7efc4e9565c49be9e7656951985860a558a754594115830bcdb421f741408346dd5997bb01c287087", + expected: "ad85e8ef13fd1dd46eae44af8b91ad1ccae5b7a1c92944f92a19f21b0b658139e0cabe9c1f679507c2de354bf2c91ebd965d1e633978a830d517d2f6f8dd5fd58065d58559de7e2334a878f8ec6992d9b9e77430d4764e863d77c0f87beede8f2f7f2ab2e7222f85cc9d98b8467f4bb72e87ef2882423ebdb6daf02dddac6db2", + _name: "nagydani_2_pow0x10001", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100c9130579f243e12451760976261416413742bd7c91d39ae087f46794062b8c239f2a74abf3918605a0e046a7890e049475ba7fbb78f5de6490bd22a710cc04d30088179a919d86c2da62cf37f59d8f258d2310d94c24891be2d7eeafaa32a8cb4b0cfe5f475ed778f45907dc8916a73f03635f233f7a77a00a3ec9ca6761a5bbd558a2318ecd0caa1c5016691523e7e1fa267dd35e70c66e84380bdcf7c0582f540174e572c41f81e93da0b757dff0b0fe23eb03aa19af0bdec3afb474216febaacb8d0381e631802683182b0fe72c28392539850650b70509f54980241dc175191a35d967288b532a7a8223ce2440d010615f70df269501944d4ec16fe4a3cb02d7a85909174757835187cb52e71934e6c07ef43b4c46fc30bbcd0bc72913068267c54a4aabebb493922492820babdeb7dc9b1558fcf7bd82c37c82d3147e455b623ab0efa752fe0b3a67ca6e4d126639e645a0bf417568adbb2a6a4eef62fa1fa29b2a5a43bebea1f82193a7dd98eb483d09bb595af1fa9c97c7f41f5649d976aee3e5e59e2329b43b13bea228d4a93f16ba139ccb511de521ffe747aa2eca664f7c9e33da59075cc335afcd2bf3ae09765f01ab5a7c3e3938ec168b74724b5074247d200d9970382f683d6059b94dbc336603d1dfee714e4b447ac2fa1d99ecb4961da2854e03795ed758220312d101e1e3d87d5313a6d052aebde75110363d", + expected: "affc7507ea6d84751ec6b3f0d7b99dbcc263f33330e450d1b3ff0bc3d0874320bf4edd57debd587306988157958cb3cfd369cc0c9c198706f635c9e0f15d047df5cb44d03e2727f26b083c4ad8485080e1293f171c1ed52aef5993a5815c35108e848c951cf1e334490b4a539a139e57b68f44fee583306f5b85ffa57206b3ee5660458858534e5386b9584af3c7f67806e84c189d695e5eb96e1272d06ec2df5dc5fabc6e94b793718c60c36be0a4d031fc84cd658aa72294b2e16fc240aef70cb9e591248e38bd49c5a554d1afa01f38dab72733092f7555334bbef6c8c430119840492380aa95fa025dcf699f0a39669d812b0c6946b6091e6e235337b6f8", + _name: "nagydani_3_square", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000100c9130579f243e12451760976261416413742bd7c91d39ae087f46794062b8c239f2a74abf3918605a0e046a7890e049475ba7fbb78f5de6490bd22a710cc04d30088179a919d86c2da62cf37f59d8f258d2310d94c24891be2d7eeafaa32a8cb4b0cfe5f475ed778f45907dc8916a73f03635f233f7a77a00a3ec9ca6761a5bbd558a2318ecd0caa1c5016691523e7e1fa267dd35e70c66e84380bdcf7c0582f540174e572c41f81e93da0b757dff0b0fe23eb03aa19af0bdec3afb474216febaacb8d0381e631802683182b0fe72c28392539850650b70509f54980241dc175191a35d967288b532a7a8223ce2440d010615f70df269501944d4ec16fe4a3cb03d7a85909174757835187cb52e71934e6c07ef43b4c46fc30bbcd0bc72913068267c54a4aabebb493922492820babdeb7dc9b1558fcf7bd82c37c82d3147e455b623ab0efa752fe0b3a67ca6e4d126639e645a0bf417568adbb2a6a4eef62fa1fa29b2a5a43bebea1f82193a7dd98eb483d09bb595af1fa9c97c7f41f5649d976aee3e5e59e2329b43b13bea228d4a93f16ba139ccb511de521ffe747aa2eca664f7c9e33da59075cc335afcd2bf3ae09765f01ab5a7c3e3938ec168b74724b5074247d200d9970382f683d6059b94dbc336603d1dfee714e4b447ac2fa1d99ecb4961da2854e03795ed758220312d101e1e3d87d5313a6d052aebde75110363d", + expected: "1b280ecd6a6bf906b806d527c2a831e23b238f89da48449003a88ac3ac7150d6a5e9e6b3be4054c7da11dd1e470ec29a606f5115801b5bf53bc1900271d7c3ff3cd5ed790d1c219a9800437a689f2388ba1a11d68f6a8e5b74e9a3b1fac6ee85fc6afbac599f93c391f5dc82a759e3c6c0ab45ce3f5d25d9b0c1bf94cf701ea6466fc9a478dacc5754e593172b5111eeba88557048bceae401337cd4c1182ad9f700852bc8c99933a193f0b94cf1aedbefc48be3bc93ef5cb276d7c2d5462ac8bb0c8fe8923a1db2afe1c6b90d59c534994a6a633f0ead1d638fdc293486bb634ff2c8ec9e7297c04241a61c37e3ae95b11d53343d4ba2b4cc33d2cfa7eb705e", + _name: "nagydani_3_qube", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000100c9130579f243e12451760976261416413742bd7c91d39ae087f46794062b8c239f2a74abf3918605a0e046a7890e049475ba7fbb78f5de6490bd22a710cc04d30088179a919d86c2da62cf37f59d8f258d2310d94c24891be2d7eeafaa32a8cb4b0cfe5f475ed778f45907dc8916a73f03635f233f7a77a00a3ec9ca6761a5bbd558a2318ecd0caa1c5016691523e7e1fa267dd35e70c66e84380bdcf7c0582f540174e572c41f81e93da0b757dff0b0fe23eb03aa19af0bdec3afb474216febaacb8d0381e631802683182b0fe72c28392539850650b70509f54980241dc175191a35d967288b532a7a8223ce2440d010615f70df269501944d4ec16fe4a3cb010001d7a85909174757835187cb52e71934e6c07ef43b4c46fc30bbcd0bc72913068267c54a4aabebb493922492820babdeb7dc9b1558fcf7bd82c37c82d3147e455b623ab0efa752fe0b3a67ca6e4d126639e645a0bf417568adbb2a6a4eef62fa1fa29b2a5a43bebea1f82193a7dd98eb483d09bb595af1fa9c97c7f41f5649d976aee3e5e59e2329b43b13bea228d4a93f16ba139ccb511de521ffe747aa2eca664f7c9e33da59075cc335afcd2bf3ae09765f01ab5a7c3e3938ec168b74724b5074247d200d9970382f683d6059b94dbc336603d1dfee714e4b447ac2fa1d99ecb4961da2854e03795ed758220312d101e1e3d87d5313a6d052aebde75110363d", + expected: "37843d7c67920b5f177372fa56e2a09117df585f81df8b300fba245b1175f488c99476019857198ed459ed8d9799c377330e49f4180c4bf8e8f66240c64f65ede93d601f957b95b83efdee1e1bfde74169ff77002eaf078c71815a9220c80b2e3b3ff22c2f358111d816ebf83c2999026b6de50bfc711ff68705d2f40b753424aefc9f70f08d908b5a20276ad613b4ab4309a3ea72f0c17ea9df6b3367d44fb3acab11c333909e02e81ea2ed404a712d3ea96bba87461720e2d98723e7acd0520ac1a5212dbedcd8dc0c1abf61d4719e319ff4758a774790b8d463cdfe131d1b2dcfee52d002694e98e720cb6ae7ccea353bc503269ba35f0f63bf8d7b672a76", + _name: "nagydani_3_pow0x10001", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000200db34d0e438249c0ed685c949cc28776a05094e1c48691dc3f2dca5fc3356d2a0663bd376e4712839917eb9a19c670407e2c377a2de385a3ff3b52104f7f1f4e0c7bf7717fb913896693dc5edbb65b760ef1b00e42e9d8f9af17352385e1cd742c9b006c0f669995cb0bb21d28c0aced2892267637b6470d8cee0ab27fc5d42658f6e88240c31d6774aa60a7ebd25cd48b56d0da11209f1928e61005c6eb709f3e8e0aaf8d9b10f7d7e296d772264dc76897ccdddadc91efa91c1903b7232a9e4c3b941917b99a3bc0c26497dedc897c25750af60237aa67934a26a2bc491db3dcc677491944bc1f51d3e5d76b8d846a62db03dedd61ff508f91a56d71028125035c3a44cbb041497c83bf3e4ae2a9613a401cc721c547a2afa3b16a2969933d3626ed6d8a7428648f74122fd3f2a02a20758f7f693892c8fd798b39abac01d18506c45e71432639e9f9505719ee822f62ccbf47f6850f096ff77b5afaf4be7d772025791717dbe5abf9b3f40cff7d7aab6f67e38f62faf510747276e20a42127e7500c444f9ed92baf65ade9e836845e39c4316d9dce5f8e2c8083e2c0acbb95296e05e51aab13b6b8f53f06c9c4276e12b0671133218cc3ea907da3bd9a367096d9202128d14846cc2e20d56fc8473ecb07cecbfb8086919f3971926e7045b853d85a69d026195c70f9f7a823536e2a8f4b3e12e94d9b53a934353451094b8102df3143a0057457d75e8c708b6337a6f5a4fd1a06727acf9fb93e2993c62f3378b37d56c85e7b1e00f0145ebf8e4095bd723166293c60b6ac1252291ef65823c9e040ddad14969b3b340a4ef714db093a587c37766d68b8d6b5016e741587e7e6bf7e763b44f0247e64bae30f994d248bfd20541a333e5b225ef6a61199e301738b1e688f70ec1d7fb892c183c95dc543c3e12adf8a5e8b9ca9d04f9445cced3ab256f29e998e69efaa633a7b60e1db5a867924ccab0a171d9d6e1098dfa15acde9553de599eaa56490c8f411e4985111f3d40bddfc5e301edb01547b01a886550a61158f7e2033c59707789bf7c854181d0c2e2a42a93cf09209747d7082e147eb8544de25c3eb14f2e35559ea0c0f5877f2f3fc92132c0ae9da4e45b2f6c866a224ea6d1f28c05320e287750fbc647368d41116e528014cc1852e5531d53e4af938374daba6cee4baa821ed07117253bb3601ddd00d59a3d7fb2ef1f5a2fbba7c429f0cf9a5b3462410fd833a69118f8be9c559b1000cc608fd877fb43f8e65c2d1302622b944462579056874b387208d90623fcdaf93920ca7a9e4ba64ea208758222ad868501cc2c345e2d3a5ea2a17e5069248138c8a79c0251185d29ee73e5afab5354769142d2bf0cb6712727aa6bf84a6245fcdae66e4938d84d1b9dd09a884818622080ff5f98942fb20acd7e0c916c2d5ea7ce6f7e173315384518f", + expected: "8a5aea5f50dcc03dc7a7a272b5aeebc040554dbc1ffe36753c4fc75f7ed5f6c2cc0de3a922bf96c78bf0643a73025ad21f45a4a5cadd717612c511ab2bff1190fe5f1ae05ba9f8fe3624de1de2a817da6072ddcdb933b50216811dbe6a9ca79d3a3c6b3a476b079fd0d05f04fb154e2dd3e5cb83b148a006f2bcbf0042efb2ae7b916ea81b27aac25c3bf9a8b6d35440062ad8eae34a83f3ffa2cc7b40346b62174a4422584f72f95316f6b2bee9ff232ba9739301c97c99a9ded26c45d72676eb856ad6ecc81d36a6de36d7f9dafafee11baa43a4b0d5e4ecffa7b9b7dcefd58c397dd373e6db4acd2b2c02717712e6289bed7c813b670c4a0c6735aa7f3b0f1ce556eae9fcc94b501b2c8781ba50a8c6220e8246371c3c7359fe4ef9da786ca7d98256754ca4e496be0a9174bedbecb384bdf470779186d6a833f068d2838a88d90ef3ad48ff963b67c39cc5a3ee123baf7bf3125f64e77af7f30e105d72c4b9b5b237ed251e4c122c6d8c1405e736299c3afd6db16a28c6a9cfa68241e53de4cd388271fe534a6a9b0dbea6171d170db1b89858468885d08fecbd54c8e471c3e25d48e97ba450b96d0d87e00ac732aaa0d3ce4309c1064bd8a4c0808a97e0143e43a24cfa847635125cd41c13e0574487963e9d725c01375db99c31da67b4cf65eff555f0c0ac416c727ff8d438ad7c42030551d68c2e7adda0abb1ca7c10", + _name: "nagydani_4_square", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000200db34d0e438249c0ed685c949cc28776a05094e1c48691dc3f2dca5fc3356d2a0663bd376e4712839917eb9a19c670407e2c377a2de385a3ff3b52104f7f1f4e0c7bf7717fb913896693dc5edbb65b760ef1b00e42e9d8f9af17352385e1cd742c9b006c0f669995cb0bb21d28c0aced2892267637b6470d8cee0ab27fc5d42658f6e88240c31d6774aa60a7ebd25cd48b56d0da11209f1928e61005c6eb709f3e8e0aaf8d9b10f7d7e296d772264dc76897ccdddadc91efa91c1903b7232a9e4c3b941917b99a3bc0c26497dedc897c25750af60237aa67934a26a2bc491db3dcc677491944bc1f51d3e5d76b8d846a62db03dedd61ff508f91a56d71028125035c3a44cbb041497c83bf3e4ae2a9613a401cc721c547a2afa3b16a2969933d3626ed6d8a7428648f74122fd3f2a02a20758f7f693892c8fd798b39abac01d18506c45e71432639e9f9505719ee822f62ccbf47f6850f096ff77b5afaf4be7d772025791717dbe5abf9b3f40cff7d7aab6f67e38f62faf510747276e20a42127e7500c444f9ed92baf65ade9e836845e39c4316d9dce5f8e2c8083e2c0acbb95296e05e51aab13b6b8f53f06c9c4276e12b0671133218cc3ea907da3bd9a367096d9202128d14846cc2e20d56fc8473ecb07cecbfb8086919f3971926e7045b853d85a69d026195c70f9f7a823536e2a8f4b3e12e94d9b53a934353451094b8103df3143a0057457d75e8c708b6337a6f5a4fd1a06727acf9fb93e2993c62f3378b37d56c85e7b1e00f0145ebf8e4095bd723166293c60b6ac1252291ef65823c9e040ddad14969b3b340a4ef714db093a587c37766d68b8d6b5016e741587e7e6bf7e763b44f0247e64bae30f994d248bfd20541a333e5b225ef6a61199e301738b1e688f70ec1d7fb892c183c95dc543c3e12adf8a5e8b9ca9d04f9445cced3ab256f29e998e69efaa633a7b60e1db5a867924ccab0a171d9d6e1098dfa15acde9553de599eaa56490c8f411e4985111f3d40bddfc5e301edb01547b01a886550a61158f7e2033c59707789bf7c854181d0c2e2a42a93cf09209747d7082e147eb8544de25c3eb14f2e35559ea0c0f5877f2f3fc92132c0ae9da4e45b2f6c866a224ea6d1f28c05320e287750fbc647368d41116e528014cc1852e5531d53e4af938374daba6cee4baa821ed07117253bb3601ddd00d59a3d7fb2ef1f5a2fbba7c429f0cf9a5b3462410fd833a69118f8be9c559b1000cc608fd877fb43f8e65c2d1302622b944462579056874b387208d90623fcdaf93920ca7a9e4ba64ea208758222ad868501cc2c345e2d3a5ea2a17e5069248138c8a79c0251185d29ee73e5afab5354769142d2bf0cb6712727aa6bf84a6245fcdae66e4938d84d1b9dd09a884818622080ff5f98942fb20acd7e0c916c2d5ea7ce6f7e173315384518f", + expected: "5a2664252aba2d6e19d9600da582cdd1f09d7a890ac48e6b8da15ae7c6ff1856fc67a841ac2314d283ffa3ca81a0ecf7c27d89ef91a5a893297928f5da0245c99645676b481b7e20a566ee6a4f2481942bee191deec5544600bb2441fd0fb19e2ee7d801ad8911c6b7750affec367a4b29a22942c0f5f4744a4e77a8b654da2a82571037099e9c6d930794efe5cdca73c7b6c0844e386bdca8ea01b3d7807146bb81365e2cdc6475f8c23e0ff84463126189dc9789f72bbce2e3d2d114d728a272f1345122de23df54c922ec7a16e5c2a8f84da8871482bd258c20a7c09bbcd64c7a96a51029bbfe848736a6ba7bf9d931a9b7de0bcaf3635034d4958b20ae9ab3a95a147b0421dd5f7ebff46c971010ebfc4adbbe0ad94d5498c853e7142c450d8c71de4b2f84edbf8acd2e16d00c8115b150b1c30e553dbb82635e781379fe2a56360420ff7e9f70cc64c00aba7e26ed13c7c19622865ae07248daced36416080f35f8cc157a857ed70ea4f347f17d1bee80fa038abd6e39b1ba06b97264388b21364f7c56e192d4b62d9b161405f32ab1e2594e86243e56fcf2cb30d21adef15b9940f91af681da24328c883d892670c6aa47940867a81830a82b82716895db810df1b834640abefb7db2092dd92912cb9a735175bc447be40a503cf22dfe565b4ed7a3293ca0dfd63a507430b323ee248ec82e843b673c97ad730728cebc", + _name: "nagydani_4_qube", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000200db34d0e438249c0ed685c949cc28776a05094e1c48691dc3f2dca5fc3356d2a0663bd376e4712839917eb9a19c670407e2c377a2de385a3ff3b52104f7f1f4e0c7bf7717fb913896693dc5edbb65b760ef1b00e42e9d8f9af17352385e1cd742c9b006c0f669995cb0bb21d28c0aced2892267637b6470d8cee0ab27fc5d42658f6e88240c31d6774aa60a7ebd25cd48b56d0da11209f1928e61005c6eb709f3e8e0aaf8d9b10f7d7e296d772264dc76897ccdddadc91efa91c1903b7232a9e4c3b941917b99a3bc0c26497dedc897c25750af60237aa67934a26a2bc491db3dcc677491944bc1f51d3e5d76b8d846a62db03dedd61ff508f91a56d71028125035c3a44cbb041497c83bf3e4ae2a9613a401cc721c547a2afa3b16a2969933d3626ed6d8a7428648f74122fd3f2a02a20758f7f693892c8fd798b39abac01d18506c45e71432639e9f9505719ee822f62ccbf47f6850f096ff77b5afaf4be7d772025791717dbe5abf9b3f40cff7d7aab6f67e38f62faf510747276e20a42127e7500c444f9ed92baf65ade9e836845e39c4316d9dce5f8e2c8083e2c0acbb95296e05e51aab13b6b8f53f06c9c4276e12b0671133218cc3ea907da3bd9a367096d9202128d14846cc2e20d56fc8473ecb07cecbfb8086919f3971926e7045b853d85a69d026195c70f9f7a823536e2a8f4b3e12e94d9b53a934353451094b81010001df3143a0057457d75e8c708b6337a6f5a4fd1a06727acf9fb93e2993c62f3378b37d56c85e7b1e00f0145ebf8e4095bd723166293c60b6ac1252291ef65823c9e040ddad14969b3b340a4ef714db093a587c37766d68b8d6b5016e741587e7e6bf7e763b44f0247e64bae30f994d248bfd20541a333e5b225ef6a61199e301738b1e688f70ec1d7fb892c183c95dc543c3e12adf8a5e8b9ca9d04f9445cced3ab256f29e998e69efaa633a7b60e1db5a867924ccab0a171d9d6e1098dfa15acde9553de599eaa56490c8f411e4985111f3d40bddfc5e301edb01547b01a886550a61158f7e2033c59707789bf7c854181d0c2e2a42a93cf09209747d7082e147eb8544de25c3eb14f2e35559ea0c0f5877f2f3fc92132c0ae9da4e45b2f6c866a224ea6d1f28c05320e287750fbc647368d41116e528014cc1852e5531d53e4af938374daba6cee4baa821ed07117253bb3601ddd00d59a3d7fb2ef1f5a2fbba7c429f0cf9a5b3462410fd833a69118f8be9c559b1000cc608fd877fb43f8e65c2d1302622b944462579056874b387208d90623fcdaf93920ca7a9e4ba64ea208758222ad868501cc2c345e2d3a5ea2a17e5069248138c8a79c0251185d29ee73e5afab5354769142d2bf0cb6712727aa6bf84a6245fcdae66e4938d84d1b9dd09a884818622080ff5f98942fb20acd7e0c916c2d5ea7ce6f7e173315384518f", + expected: "bed8b970c4a34849fc6926b08e40e20b21c15ed68d18f228904878d4370b56322d0da5789da0318768a374758e6375bfe4641fca5285ec7171828922160f48f5ca7efbfee4d5148612c38ad683ae4e3c3a053d2b7c098cf2b34f2cb19146eadd53c86b2d7ccf3d83b2c370bfb840913ee3879b1057a6b4e07e110b6bcd5e958bc71a14798c91d518cc70abee264b0d25a4110962a764b364ac0b0dd1ee8abc8426d775ec0f22b7e47b32576afaf1b5a48f64573ed1c5c29f50ab412188d9685307323d990802b81dacc06c6e05a1e901830ba9fcc67688dc29c5e27bde0a6e845ca925f5454b6fb3747edfaa2a5820838fb759eadf57f7cb5cec57fc213ddd8a4298fa079c3c0f472b07fb15aa6a7f0a3780bd296ff6a62e58ef443870b02260bd4fd2bbc98255674b8e1f1f9f8d33c7170b0ebbea4523b695911abbf26e41885344823bd0587115fdd83b721a4e8457a31c9a84b3d3520a07e0e35df7f48e5a9d534d0ec7feef1ff74de6a11e7f93eab95175b6ce22c68d78a642ad642837897ec11349205d8593ac19300207572c38d29ca5dfa03bc14cdbc32153c80e5cc3e739403d34c75915e49beb43094cc6dcafb3665b305ddec9286934ae66ec6b777ca528728c851318eb0f207b39f1caaf96db6eeead6b55ed08f451939314577d42bcc9f97c0b52d0234f88fd07e4c1d7780fdebc025cfffcb572cb27a8c33963", + _name: "nagydani_4_pow0x10001", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000400c5a1611f8be90071a43db23cc2fe01871cc4c0e8ab5743f6378e4fef77f7f6db0095c0727e20225beb665645403453e325ad5f9aeb9ba99bf3c148f63f9c07cf4fe8847ad5242d6b7d4499f93bd47056ddab8f7dee878fc2314f344dbee2a7c41a5d3db91eff372c730c2fdd3a141a4b61999e36d549b9870cf2f4e632c4d5df5f024f81c028000073a0ed8847cfb0593d36a47142f578f05ccbe28c0c06aeb1b1da027794c48db880278f79ba78ae64eedfea3c07d10e0562668d839749dc95f40467d15cf65b9cfc52c7c4bcef1cda3596dd52631aac942f146c7cebd46065131699ce8385b0db1874336747ee020a5698a3d1a1082665721e769567f579830f9d259cec1a836845109c21cf6b25da572512bf3c42fd4b96e43895589042ab60dd41f497db96aec102087fe784165bb45f942859268fd2ff6c012d9d00c02ba83eace047cc5f7b2c392c2955c58a49f0338d6fc58749c9db2155522ac17914ec216ad87f12e0ee95574613942fa615898c4d9e8a3be68cd6afa4e7a003dedbdf8edfee31162b174f965b20ae752ad89c967b3068b6f722c16b354456ba8e280f987c08e0a52d40a2e8f3a59b94d590aeef01879eb7a90b3ee7d772c839c85519cbeaddc0c193ec4874a463b53fcaea3271d80ebfb39b33489365fc039ae549a17a9ff898eea2f4cb27b8dbee4c17b998438575b2b8d107e4a0d66ba7fca85b41a58a8d51f191a35c856dfbe8aef2b00048a694bbccff832d23c8ca7a7ff0b6c0b3011d00b97c86c0628444d267c951d9e4fb8f83e154b8f74fb51aa16535e498235c5597dac9606ed0be3173a3836baa4e7d756ffe1e2879b415d3846bccd538c05b847785699aefde3e305decb600cd8fb0e7d8de5efc26971a6ad4e6d7a2d91474f1023a0ac4b78dc937da0ce607a45974d2cac1c33a2631ff7fe6144a3b2e5cf98b531a9627dea92c1dc82204d09db0439b6a11dd64b484e1263aa45fd9539b6020b55e3baece3986a8bffc1003406348f5c61265099ed43a766ee4f93f5f9c5abbc32a0fd3ac2b35b87f9ec26037d88275bd7dd0a54474995ee34ed3727f3f97c48db544b1980193a4b76a8a3ddab3591ce527f16d91882e67f0103b5cda53f7da54d489fc4ac08b6ab358a5a04aa9daa16219d50bd672a7cb804ed769d218807544e5993f1c27427104b349906a0b654df0bf69328afd3013fbe430155339c39f236df5557bf92f1ded7ff609a8502f49064ec3d1dbfb6c15d3a4c11a4f8acd12278cbf68acd5709463d12e3338a6eddb8c112f199645e23154a8e60879d2a654e3ed9296aa28f134168619691cd2c6b9e2eba4438381676173fc63c2588a3c5910dc149cf3760f0aa9fa9c3f5faa9162b0bf1aac9dd32b706a60ef53cbdb394b6b40222b5bc80eea82ba8958386672564cae3794f977871ab62337cf02e30049201ec12937e7ce79d0f55d9c810e20acf52212aca1d3888949e0e4830aad88d804161230eb89d4d329cc83570fe257217d2119134048dd2ed167646975fc7d77136919a049ea74cf08ddd2b896890bb24a0ba18094a22baa351bf29ad96c66bbb1a598f2ca391749620e62d61c3561a7d3653ccc8892c7b99baaf76bf836e2991cb06d6bc0514568ff0d1ec8bb4b3d6984f5eaefb17d3ea2893722375d3ddb8e389a8eef7d7d198f8e687d6a513983df906099f9a2d23f4f9dec6f8ef2f11fc0a21fac45353b94e00486f5e17d386af42502d09db33cf0cf28310e049c07e88682aeeb00cb833c5174266e62407a57583f1f88b304b7c6e0c84bbe1c0fd423072d37a5bd0aacf764229e5c7cd02473460ba3645cd8e8ae144065bf02d0dd238593d8e230354f67e0b2f23012c23274f80e3ee31e35e2606a4a3f31d94ab755e6d163cff52cbb36b6d0cc67ffc512aeed1dce4d7a0d70ce82f2baba12e8d514dc92a056f994adfb17b5b9712bd5186f27a2fda1f7039c5df2c8587fdc62f5627580c13234b55be4df3056050e2d1ef3218f0dd66cb05265fe1acfb0989d8213f2c19d1735a7cf3fa65d88dad5af52dc2bba22b7abf46c3bc77b5091baab9e8f0ddc4d5e581037de91a9f8dcbc69309be29cc815cf19a20a7585b8b3073edf51fc9baeb3e509b97fa4ecfd621e0fd57bd61cac1b895c03248ff12bdbc57509250df3517e8a3fe1d776836b34ab352b973d932ef708b14f7418f9eceb1d87667e61e3e758649cb083f01b133d37ab2f5afa96d6c84bcacf4efc3851ad308c1e7d9113624fce29fab460ab9d2a48d92cdb281103a5250ad44cb2ff6e67ac670c02fdafb3e0f1353953d6d7d5646ca1568dea55275a050ec501b7c6250444f7219f1ba7521ba3b93d089727ca5f3bbe0d6c1300b423377004954c5628fdb65770b18ced5c9b23a4a5a6d6ef25fe01b4ce278de0bcc4ed86e28a0a68818ffa40970128cf2c38740e80037984428c1bd5113f40ff47512ee6f4e4d8f9b8e8e1b3040d2928d003bd1c1329dc885302fbce9fa81c23b4dc49c7c82d29b52957847898676c89aa5d32b5b0e1c0d5a2b79a19d67562f407f19425687971a957375879d90c5f57c857136c17106c9ab1b99d80e69c8c954ed386493368884b55c939b8d64d26f643e800c56f90c01079d7c534e3b2b7ae352cefd3016da55f6a85eb803b85e2304915fd2001f77c74e28746293c46e4f5f0fd49cf988aafd0026b8e7a3bab2da5cdce1ea26c2e29ec03f4807fac432662b2d6c060be1c7be0e5489de69d0a6e03a4b9117f9244b34a0f1ecba89884f781c6320412413a00c4980287409a2a78c2cd7e65cecebbe4ec1c28cac4dd95f6998e78fc6f1392384331c9436aa10e10e2bf8ad2c4eafbcf276aa7bae64b74428911b3269c749338b0fc5075ad", + expected: "d61fe4e3f32ac260915b5b03b78a86d11bfc41d973fce5b0cc59035cf8289a8a2e3878ea15fa46565b0d806e2f85b53873ea20ed653869b688adf83f3ef444535bf91598ff7e80f334fb782539b92f39f55310cc4b35349ab7b278346eda9bc37c0d8acd3557fae38197f412f8d9e57ce6a76b7205c23564cab06e5615be7c6f05c3d05ec690cba91da5e89d55b152ff8dd2157dc5458190025cf94b1ad98f7cbe64e9482faba95e6b33844afc640892872b44a9932096508f4a782a4805323808f23e54b6ff9b841dbfa87db3505ae4f687972c18ea0f0d0af89d36c1c2a5b14560c153c3fee406f5cf15cfd1c0bb45d767426d465f2f14c158495069d0c5955a00150707862ecaae30624ebacdd8ac33e4e6aab3ff90b6ba445a84689386b9e945d01823a65874444316e83767290fcff630d2477f49d5d8ffdd200e08ee1274270f86ed14c687895f6caf5ce528bd970c20d2408a9ba66216324c6a011ac4999098362dbd98a038129a2d40c8da6ab88318aa3046cb660327cc44236d9e5d2163bd0959062195c51ed93d0088b6f92051fc99050ece2538749165976233697ab4b610385366e5ce0b02ad6b61c168ecfbedcdf74278a38de340fd7a5fead8e588e294795f9b011e2e60377a89e25c90e145397cdeabc60fd32444a6b7642a611a83c464d8b8976666351b4865c37b02e6dc21dbcdf5f930341707b618cc0f03c3122646b3385c9df9f2ec730eec9d49e7dfc9153b6e6289da8c4f0ebea9ccc1b751948e3bb7171c9e4d57423b0eeeb79095c030cb52677b3f7e0b45c30f645391f3f9c957afa549c4e0b2465b03c67993cd200b1af01035962edbc4c9e89b31c82ac121987d6529dafdeef67a132dc04b6dc68e77f22862040b75e2ceb9ff16da0fca534e6db7bd12fa7b7f51b6c08c1e23dfcdb7acbd2da0b51c87ffbced065a612e9b1c8bba9b7e2d8d7a2f04fcc4aaf355b60d764879a76b5e16762d5f2f55d585d0c8e82df6940960cddfb72c91dfa71f6b4e1c6ca25dfc39a878e998a663c04fe29d5e83b9586d047b4d7ff70a9f0d44f127e7d741685ca75f11629128d916a0ffef4be586a30c4b70389cc746e84ebf177c01ee8a4511cfbb9d1ecf7f7b33c7dd8177896e10bbc82f838dcd6db7ac67de62bf46b6a640fb580c5d1d2708f3862e3d2b645d0d18e49ef088053e3a220adc0e033c2afcfe61c90e32151152eb3caaf746c5e377d541cafc6cbb0cc0fa48b5caf1728f2e1957f5addfc234f1a9d89e40d49356c9172d0561a695fce6dab1d412321bbf407f63766ffd7b6b3d79bcfa07991c5a9709849c1008689e3b47c50d613980bec239fb64185249d055b30375ccb4354d71fe4d05648fbf6c80634dfc3575f2f24abb714c1e4c95e8896763bf4316e954c7ad19e5780ab7a040ca6fb9271f90a8b22ae738daf6cb", + _name: "nagydani_5_square", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000400c5a1611f8be90071a43db23cc2fe01871cc4c0e8ab5743f6378e4fef77f7f6db0095c0727e20225beb665645403453e325ad5f9aeb9ba99bf3c148f63f9c07cf4fe8847ad5242d6b7d4499f93bd47056ddab8f7dee878fc2314f344dbee2a7c41a5d3db91eff372c730c2fdd3a141a4b61999e36d549b9870cf2f4e632c4d5df5f024f81c028000073a0ed8847cfb0593d36a47142f578f05ccbe28c0c06aeb1b1da027794c48db880278f79ba78ae64eedfea3c07d10e0562668d839749dc95f40467d15cf65b9cfc52c7c4bcef1cda3596dd52631aac942f146c7cebd46065131699ce8385b0db1874336747ee020a5698a3d1a1082665721e769567f579830f9d259cec1a836845109c21cf6b25da572512bf3c42fd4b96e43895589042ab60dd41f497db96aec102087fe784165bb45f942859268fd2ff6c012d9d00c02ba83eace047cc5f7b2c392c2955c58a49f0338d6fc58749c9db2155522ac17914ec216ad87f12e0ee95574613942fa615898c4d9e8a3be68cd6afa4e7a003dedbdf8edfee31162b174f965b20ae752ad89c967b3068b6f722c16b354456ba8e280f987c08e0a52d40a2e8f3a59b94d590aeef01879eb7a90b3ee7d772c839c85519cbeaddc0c193ec4874a463b53fcaea3271d80ebfb39b33489365fc039ae549a17a9ff898eea2f4cb27b8dbee4c17b998438575b2b8d107e4a0d66ba7fca85b41a58a8d51f191a35c856dfbe8aef2b00048a694bbccff832d23c8ca7a7ff0b6c0b3011d00b97c86c0628444d267c951d9e4fb8f83e154b8f74fb51aa16535e498235c5597dac9606ed0be3173a3836baa4e7d756ffe1e2879b415d3846bccd538c05b847785699aefde3e305decb600cd8fb0e7d8de5efc26971a6ad4e6d7a2d91474f1023a0ac4b78dc937da0ce607a45974d2cac1c33a2631ff7fe6144a3b2e5cf98b531a9627dea92c1dc82204d09db0439b6a11dd64b484e1263aa45fd9539b6020b55e3baece3986a8bffc1003406348f5c61265099ed43a766ee4f93f5f9c5abbc32a0fd3ac2b35b87f9ec26037d88275bd7dd0a54474995ee34ed3727f3f97c48db544b1980193a4b76a8a3ddab3591ce527f16d91882e67f0103b5cda53f7da54d489fc4ac08b6ab358a5a04aa9daa16219d50bd672a7cb804ed769d218807544e5993f1c27427104b349906a0b654df0bf69328afd3013fbe430155339c39f236df5557bf92f1ded7ff609a8502f49064ec3d1dbfb6c15d3a4c11a4f8acd12278cbf68acd5709463d12e3338a6eddb8c112f199645e23154a8e60879d2a654e3ed9296aa28f134168619691cd2c6b9e2eba4438381676173fc63c2588a3c5910dc149cf3760f0aa9fa9c3f5faa9162b0bf1aac9dd32b706a60ef53cbdb394b6b40222b5bc80eea82ba8958386672564cae3794f977871ab62337cf03e30049201ec12937e7ce79d0f55d9c810e20acf52212aca1d3888949e0e4830aad88d804161230eb89d4d329cc83570fe257217d2119134048dd2ed167646975fc7d77136919a049ea74cf08ddd2b896890bb24a0ba18094a22baa351bf29ad96c66bbb1a598f2ca391749620e62d61c3561a7d3653ccc8892c7b99baaf76bf836e2991cb06d6bc0514568ff0d1ec8bb4b3d6984f5eaefb17d3ea2893722375d3ddb8e389a8eef7d7d198f8e687d6a513983df906099f9a2d23f4f9dec6f8ef2f11fc0a21fac45353b94e00486f5e17d386af42502d09db33cf0cf28310e049c07e88682aeeb00cb833c5174266e62407a57583f1f88b304b7c6e0c84bbe1c0fd423072d37a5bd0aacf764229e5c7cd02473460ba3645cd8e8ae144065bf02d0dd238593d8e230354f67e0b2f23012c23274f80e3ee31e35e2606a4a3f31d94ab755e6d163cff52cbb36b6d0cc67ffc512aeed1dce4d7a0d70ce82f2baba12e8d514dc92a056f994adfb17b5b9712bd5186f27a2fda1f7039c5df2c8587fdc62f5627580c13234b55be4df3056050e2d1ef3218f0dd66cb05265fe1acfb0989d8213f2c19d1735a7cf3fa65d88dad5af52dc2bba22b7abf46c3bc77b5091baab9e8f0ddc4d5e581037de91a9f8dcbc69309be29cc815cf19a20a7585b8b3073edf51fc9baeb3e509b97fa4ecfd621e0fd57bd61cac1b895c03248ff12bdbc57509250df3517e8a3fe1d776836b34ab352b973d932ef708b14f7418f9eceb1d87667e61e3e758649cb083f01b133d37ab2f5afa96d6c84bcacf4efc3851ad308c1e7d9113624fce29fab460ab9d2a48d92cdb281103a5250ad44cb2ff6e67ac670c02fdafb3e0f1353953d6d7d5646ca1568dea55275a050ec501b7c6250444f7219f1ba7521ba3b93d089727ca5f3bbe0d6c1300b423377004954c5628fdb65770b18ced5c9b23a4a5a6d6ef25fe01b4ce278de0bcc4ed86e28a0a68818ffa40970128cf2c38740e80037984428c1bd5113f40ff47512ee6f4e4d8f9b8e8e1b3040d2928d003bd1c1329dc885302fbce9fa81c23b4dc49c7c82d29b52957847898676c89aa5d32b5b0e1c0d5a2b79a19d67562f407f19425687971a957375879d90c5f57c857136c17106c9ab1b99d80e69c8c954ed386493368884b55c939b8d64d26f643e800c56f90c01079d7c534e3b2b7ae352cefd3016da55f6a85eb803b85e2304915fd2001f77c74e28746293c46e4f5f0fd49cf988aafd0026b8e7a3bab2da5cdce1ea26c2e29ec03f4807fac432662b2d6c060be1c7be0e5489de69d0a6e03a4b9117f9244b34a0f1ecba89884f781c6320412413a00c4980287409a2a78c2cd7e65cecebbe4ec1c28cac4dd95f6998e78fc6f1392384331c9436aa10e10e2bf8ad2c4eafbcf276aa7bae64b74428911b3269c749338b0fc5075ad", + expected: "5f9c70ec884926a89461056ad20ac4c30155e817f807e4d3f5bb743d789c83386762435c3627773fa77da5144451f2a8aad8adba88e0b669f5377c5e9bad70e45c86fe952b613f015a9953b8a5de5eaee4566acf98d41e327d93a35bd5cef4607d025e58951167957df4ff9b1627649d3943805472e5e293d3efb687cfd1e503faafeb2840a3e3b3f85d016051a58e1c9498aab72e63b748d834b31eb05d85dcde65e27834e266b85c75cc4ec0135135e0601cb93eeeb6e0010c8ceb65c4c319623c5e573a2c8c9fbbf7df68a930beb412d3f4dfd146175484f45d7afaa0d2e60684af9b34730f7c8438465ad3e1d0c3237336722f2aa51095bd5759f4b8ab4dda111b684aa3dac62a761722e7ae43495b7709933512c81c4e3c9133a51f7ce9f2b51fcec064f65779666960b4e45df3900f54311f5613e8012dd1b8efd359eda31a778264c72aa8bb419d862734d769076bce2810011989a45374e5c5d8729fec21427f0bf397eacbb4220f603cf463a4b0c94efd858ffd9768cd60d6ce68d755e0fbad007ce5c2223d70c7018345a102e4ab3c60a13a9e7794303156d4c2063e919f2153c13961fb324c80b240742f47773a7a8e25b3e3fb19b00ce839346c6eb3c732fbc6b888df0b1fe0a3d07b053a2e9402c267b2d62f794d8a2840526e3ade15ce2264496ccd7519571dfde47f7a4bb16292241c20b2be59f3f8fb4f6383f232d838c5a22d8c95b6834d9d2ca493f5a505ebe8899503b0e8f9b19e6e2dd81c1628b80016d02097e0134de51054c4e7674824d4d758760fc52377d2cad145e259aa2ffaf54139e1a66b1e0c1c191e32ac59474c6b526f5b3ba07d3e5ec286eddf531fcd5292869be58c9f22ef91026159f7cf9d05ef66b4299f4da48cc1635bf2243051d342d378a22c83390553e873713c0454ce5f3234397111ac3fe3207b86f0ed9fc025c81903e1748103692074f83824fda6341be4f95ff00b0a9a208c267e12fa01825054cc0513629bf3dbb56dc5b90d4316f87654a8be18227978ea0a8a522760cad620d0d14fd38920fb7321314062914275a5f99f677145a6979b156bd82ecd36f23f8e1273cc2759ecc0b2c69d94dad5211d1bed939dd87ed9e07b91d49713a6e16ade0a98aea789f04994e318e4ff2c8a188cd8d43aeb52c6daa3bc29b4af50ea82a247c5cd67b573b34cbadcc0a376d3bbd530d50367b42705d870f2e27a8197ef46070528bfe408360faa2ebb8bf76e9f388572842bcb119f4d84ee34ae31f5cc594f23705a49197b181fb78ed1ec99499c690f843a4d0cf2e226d118e9372271054fbabdcc5c92ae9fefaef0589cd0e722eaf30c1703ec4289c7fd81beaa8a455ccee5298e31e2080c10c366a6fcf56f7d13582ad0bcad037c612b710fc595b70fbefaaca23623b60c6c39b11beb8e5843b6b3dac60f", + _name: "nagydani_5_qube", + }, + ModexpTestCase { + input: "000000000000000000000000000000000000000000000000000000000000040000000000000000000000000000000000000000000000000000000000000000030000000000000000000000000000000000000000000000000000000000000400c5a1611f8be90071a43db23cc2fe01871cc4c0e8ab5743f6378e4fef77f7f6db0095c0727e20225beb665645403453e325ad5f9aeb9ba99bf3c148f63f9c07cf4fe8847ad5242d6b7d4499f93bd47056ddab8f7dee878fc2314f344dbee2a7c41a5d3db91eff372c730c2fdd3a141a4b61999e36d549b9870cf2f4e632c4d5df5f024f81c028000073a0ed8847cfb0593d36a47142f578f05ccbe28c0c06aeb1b1da027794c48db880278f79ba78ae64eedfea3c07d10e0562668d839749dc95f40467d15cf65b9cfc52c7c4bcef1cda3596dd52631aac942f146c7cebd46065131699ce8385b0db1874336747ee020a5698a3d1a1082665721e769567f579830f9d259cec1a836845109c21cf6b25da572512bf3c42fd4b96e43895589042ab60dd41f497db96aec102087fe784165bb45f942859268fd2ff6c012d9d00c02ba83eace047cc5f7b2c392c2955c58a49f0338d6fc58749c9db2155522ac17914ec216ad87f12e0ee95574613942fa615898c4d9e8a3be68cd6afa4e7a003dedbdf8edfee31162b174f965b20ae752ad89c967b3068b6f722c16b354456ba8e280f987c08e0a52d40a2e8f3a59b94d590aeef01879eb7a90b3ee7d772c839c85519cbeaddc0c193ec4874a463b53fcaea3271d80ebfb39b33489365fc039ae549a17a9ff898eea2f4cb27b8dbee4c17b998438575b2b8d107e4a0d66ba7fca85b41a58a8d51f191a35c856dfbe8aef2b00048a694bbccff832d23c8ca7a7ff0b6c0b3011d00b97c86c0628444d267c951d9e4fb8f83e154b8f74fb51aa16535e498235c5597dac9606ed0be3173a3836baa4e7d756ffe1e2879b415d3846bccd538c05b847785699aefde3e305decb600cd8fb0e7d8de5efc26971a6ad4e6d7a2d91474f1023a0ac4b78dc937da0ce607a45974d2cac1c33a2631ff7fe6144a3b2e5cf98b531a9627dea92c1dc82204d09db0439b6a11dd64b484e1263aa45fd9539b6020b55e3baece3986a8bffc1003406348f5c61265099ed43a766ee4f93f5f9c5abbc32a0fd3ac2b35b87f9ec26037d88275bd7dd0a54474995ee34ed3727f3f97c48db544b1980193a4b76a8a3ddab3591ce527f16d91882e67f0103b5cda53f7da54d489fc4ac08b6ab358a5a04aa9daa16219d50bd672a7cb804ed769d218807544e5993f1c27427104b349906a0b654df0bf69328afd3013fbe430155339c39f236df5557bf92f1ded7ff609a8502f49064ec3d1dbfb6c15d3a4c11a4f8acd12278cbf68acd5709463d12e3338a6eddb8c112f199645e23154a8e60879d2a654e3ed9296aa28f134168619691cd2c6b9e2eba4438381676173fc63c2588a3c5910dc149cf3760f0aa9fa9c3f5faa9162b0bf1aac9dd32b706a60ef53cbdb394b6b40222b5bc80eea82ba8958386672564cae3794f977871ab62337cf010001e30049201ec12937e7ce79d0f55d9c810e20acf52212aca1d3888949e0e4830aad88d804161230eb89d4d329cc83570fe257217d2119134048dd2ed167646975fc7d77136919a049ea74cf08ddd2b896890bb24a0ba18094a22baa351bf29ad96c66bbb1a598f2ca391749620e62d61c3561a7d3653ccc8892c7b99baaf76bf836e2991cb06d6bc0514568ff0d1ec8bb4b3d6984f5eaefb17d3ea2893722375d3ddb8e389a8eef7d7d198f8e687d6a513983df906099f9a2d23f4f9dec6f8ef2f11fc0a21fac45353b94e00486f5e17d386af42502d09db33cf0cf28310e049c07e88682aeeb00cb833c5174266e62407a57583f1f88b304b7c6e0c84bbe1c0fd423072d37a5bd0aacf764229e5c7cd02473460ba3645cd8e8ae144065bf02d0dd238593d8e230354f67e0b2f23012c23274f80e3ee31e35e2606a4a3f31d94ab755e6d163cff52cbb36b6d0cc67ffc512aeed1dce4d7a0d70ce82f2baba12e8d514dc92a056f994adfb17b5b9712bd5186f27a2fda1f7039c5df2c8587fdc62f5627580c13234b55be4df3056050e2d1ef3218f0dd66cb05265fe1acfb0989d8213f2c19d1735a7cf3fa65d88dad5af52dc2bba22b7abf46c3bc77b5091baab9e8f0ddc4d5e581037de91a9f8dcbc69309be29cc815cf19a20a7585b8b3073edf51fc9baeb3e509b97fa4ecfd621e0fd57bd61cac1b895c03248ff12bdbc57509250df3517e8a3fe1d776836b34ab352b973d932ef708b14f7418f9eceb1d87667e61e3e758649cb083f01b133d37ab2f5afa96d6c84bcacf4efc3851ad308c1e7d9113624fce29fab460ab9d2a48d92cdb281103a5250ad44cb2ff6e67ac670c02fdafb3e0f1353953d6d7d5646ca1568dea55275a050ec501b7c6250444f7219f1ba7521ba3b93d089727ca5f3bbe0d6c1300b423377004954c5628fdb65770b18ced5c9b23a4a5a6d6ef25fe01b4ce278de0bcc4ed86e28a0a68818ffa40970128cf2c38740e80037984428c1bd5113f40ff47512ee6f4e4d8f9b8e8e1b3040d2928d003bd1c1329dc885302fbce9fa81c23b4dc49c7c82d29b52957847898676c89aa5d32b5b0e1c0d5a2b79a19d67562f407f19425687971a957375879d90c5f57c857136c17106c9ab1b99d80e69c8c954ed386493368884b55c939b8d64d26f643e800c56f90c01079d7c534e3b2b7ae352cefd3016da55f6a85eb803b85e2304915fd2001f77c74e28746293c46e4f5f0fd49cf988aafd0026b8e7a3bab2da5cdce1ea26c2e29ec03f4807fac432662b2d6c060be1c7be0e5489de69d0a6e03a4b9117f9244b34a0f1ecba89884f781c6320412413a00c4980287409a2a78c2cd7e65cecebbe4ec1c28cac4dd95f6998e78fc6f1392384331c9436aa10e10e2bf8ad2c4eafbcf276aa7bae64b74428911b3269c749338b0fc5075ad", + expected: "5a0eb2bdf0ac1cae8e586689fa16cd4b07dfdedaec8a110ea1fdb059dd5253231b6132987598dfc6e11f86780428982d50cf68f67ae452622c3b336b537ef3298ca645e8f89ee39a26758206a5a3f6409afc709582f95274b57b71fae5c6b74619ae6f089a5393c5b79235d9caf699d23d88fb873f78379690ad8405e34c19f5257d596580c7a6a7206a3712825afe630c76b31cdb4a23e7f0632e10f14f4e282c81a66451a26f8df2a352b5b9f607a7198449d1b926e27036810368e691a74b91c61afa73d9d3b99453e7c8b50fd4f09c039a2f2feb5c419206694c31b92df1d9586140cb3417b38d0c503c7b508cc2ed12e813a1c795e9829eb39ee78eeaf360a169b491a1d4e419574e712402de9d48d54c1ae5e03739b7156615e8267e1fb0a897f067afd11fb33f6e24182d7aaaaa18fe5bc1982f20d6b871e5a398f0f6f718181d31ec225cfa9a0a70124ed9a70031bdf0c1c7829f708b6e17d50419ef361cf77d99c85f44607186c8d683106b8bd38a49b5d0fb503b397a83388c5678dcfcc737499d84512690701ed621a6f0172aecf037184ddf0f2453e4053024018e5ab2e30d6d5363b56e8b41509317c99042f517247474ab3abc848e00a07f69c254f46f2a05cf6ed84e5cc906a518fdcfdf2c61ce731f24c5264f1a25fc04934dc28aec112134dd523f70115074ca34e3807aa4cb925147f3a0ce152d323bd8c675ace446d0fd1ae30c4b57f0eb2c23884bc18f0964c0114796c5b6d080c3d89175665fbf63a6381a6a9da39ad070b645c8bb1779506da14439a9f5b5d481954764ea114fac688930bc68534d403cff4210673b6a6ff7ae416b7cd41404c3d3f282fcd193b86d0f54d0006c2a503b40d5c3930da980565b8f9630e9493a79d1c03e74e5f93ac8e4dc1a901ec5e3b3e57049124c7b72ea345aa359e782285d9e6a5c144a378111dd02c40855ff9c2be9b48425cb0b2fd62dc8678fd151121cf26a65e917d65d8e0dacfae108eb5508b601fb8ffa370be1f9a8b749a2d12eeab81f41079de87e2d777994fa4d28188c579ad327f9957fb7bdecec5c680844dd43cb57cf87aeb763c003e65011f73f8c63442df39a92b946a6bd968a1c1e4d5fa7d88476a68bd8e20e5b70a99259c7d3f85fb1b65cd2e93972e6264e74ebf289b8b6979b9b68a85cd5b360c1987f87235c3c845d62489e33acf85d53fa3561fe3a3aee18924588d9c6eba4edb7a4d106b31173e42929f6f0c48c80ce6a72d54eca7c0fe870068b7a7c89c63cdda593f5b32d3cb4ea8a32c39f00ab449155757172d66763ed9527019d6de6c9f2416aa6203f4d11c9ebee1e1d3845099e55504446448027212616167eb36035726daa7698b075286f5379cd3e93cb3e0cf4f9cb8d017facbb5550ed32d5ec5400ae57e47e2bf78d1eaeff9480cc765ceff39db500", + _name: "nagydani_5_pow0x10001", + } + ]; + + #[test] + fn test_modexp_inputs() { + for test in MODEXP_TESTS.iter() { + let input = hex::decode(test.input).unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(5), + &input, + None, + Some(100_000_000), + true, + ); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(hex::encode(outcome.output().unwrap()), test.expected); + } + } + + #[test] + fn test_modexp_empty_input() { + let result = + execute_precompiled(H160::from_low_u64_be(5), &[], None, Some(100_000), true); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + + assert_eq!("", hex::encode(outcome.output().unwrap())); + } + + // All the tests for ecAdd, ecMul, ecPrecompile were taken from: + // https://github.com/bluealloy/revm/blob/main/crates/precompile/src/bn128.rs + + #[test] + fn test_modexp_mod_overflow_return_none() { + // Input was taken from the official ethereum test-suite at: + // GeneralStateTests/stPrecompiledContracts2/modexpRandomInput.json + let input_overflow = hex::decode("00000000008000000000000000000000000000000000000000000000000000000000000400000000000000000000000a").unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(5), + &input_overflow, + None, + Some(100_000), + true, + ); + + assert!(result.is_ok()); + let outcome = result.unwrap(); + match outcome.result { + ExecutionResult::Error(_) => (), + _ => panic!("The execution should exit with an error."), + } + assert!(outcome.output().is_none()); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/reentrancy_guard.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/reentrancy_guard.rs new file mode 100644 index 000000000000..40176f7b2542 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/reentrancy_guard.rs @@ -0,0 +1,71 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Reentrancy guard prevents circular impure precompile calls, such as: +//! * FA bridge -> Arbitrary contract -> XTZ bridge +//! * FA bridge -> Arbitrary contract -> FA bridge +//! +//! Although such calls do not immediately introduce a vulnerability, +//! it's still a good practice that can prevent potential issues. +//! +//! NOTE that this does not prevent batched precompile calls. +//! +//! Read more: https://blog.openzeppelin.com/reentrancy-after-istanbul + +use std::borrow::Cow; + +use evm::ExitError; +use primitive_types::H160; + +#[derive(Debug)] +pub struct ReentrancyGuard { + /// List of impure precompiles + stoplist: Vec, + /// Flag is set when kernel encounters the first "impure" precompile + /// in the call stack. + enabled_at: Option, + /// Current call depth (only precompile calls count) + level: u32, +} + +impl ReentrancyGuard { + /// Create new reentrancy guard from a list of impure precompiles. + pub fn new(stoplist: Vec) -> Self { + Self { + stoplist, + enabled_at: None, + level: 0, + } + } + + /// Try to begin a precompile call. + /// + /// If the address belongs to an impure precompile this method AND + /// this is a circular call then this method will fail with an error. + pub fn begin_precompile_call(&mut self, address: &H160) -> Result<(), ExitError> { + if self.stoplist.contains(address) { + if self.enabled_at.is_some() { + return Err(ExitError::Other(Cow::from( + "Circular calls are not allowed", + ))); + } + self.enabled_at = Some(self.level); + } + self.level += 1; + Ok(()) + } + + /// End precompile call. + pub fn end_precompile_call(&mut self) { + self.level -= 1; + if self.enabled_at == Some(self.level) { + self.enabled_at = None; + } + } + + #[cfg(test)] + pub fn disable(&mut self) { + self.stoplist.clear(); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/revert.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/revert.rs new file mode 100644 index 000000000000..4fa410f69d56 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/revert.rs @@ -0,0 +1,28 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +/// Implements a precompiled contract that always revert. It can be useful +/// if we want to replace the implementation of a precompiled contract by +/// a version that always revert, e.g. to prevent reetrancy on withdrawals. +use evm::{Context, Transfer}; +use tezos_evm_runtime::runtime::Runtime; + +use crate::{handler::EvmHandler, EthereumError}; + +use super::PrecompileOutcome; + +pub fn revert_precompile( + _handler: &mut EvmHandler, + _input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + Ok(crate::precompiles::PrecompileOutcome { + exit_status: evm::ExitReason::Revert(evm::ExitRevert::Reverted), + withdrawals: vec![], + output: vec![], + estimated_ticks: 0, + }) +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/withdrawal.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/withdrawal.rs new file mode 100644 index 000000000000..5d93afee3d90 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/withdrawal.rs @@ -0,0 +1,949 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2023-2024 PK Lab +// SPDX-FileCopyrightText: 2024-2025 Functori +// +// SPDX-License-Identifier: MIT + +use std::borrow::Cow; + +use crate::abi::ABI_B22_RIGHT_PADDING; +use crate::abi::ABI_H160_LEFT_PADDING; +use crate::fast_withdrawals_enabled; +use crate::handler::EvmHandler; +use crate::handler::FastWithdrawalInterface; +use crate::handler::RouterInterface; +use crate::handler::Withdrawal; +use crate::precompiles::tick_model; +use crate::precompiles::PrecompileOutcome; +use crate::precompiles::{SYSTEM_ACCOUNT_ADDRESS, WITHDRAWAL_ADDRESS}; +use crate::read_ticketer; +use crate::utilities::alloy::h160_to_alloy; +use crate::utilities::alloy::u256_to_alloy; +use crate::utilities::u256_to_bigint; +use crate::withdrawal_counter::WithdrawalCounter; +use crate::{abi, fail_if_too_much, EthereumError}; +use alloy_primitives::FixedBytes; +use alloy_sol_types::SolEvent; +use crypto::hash::ContractKt1Hash; +use crypto::hash::HashTrait; +use evm::Handler; +use evm::{Context, ExitReason, ExitRevert, ExitSucceed, Transfer}; +use primitive_types::H160; +use primitive_types::H256; +use primitive_types::U256; +use tezos_data_encoding::enc::BinWriter; +use tezos_data_encoding::types::Zarith; +use tezos_ethereum::wei::mutez_from_wei; +use tezos_ethereum::wei::ErrorMutezFromWei; +use tezos_ethereum::Log; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Info; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::contract::Contract; +use tezos_smart_rollup_encoding::entrypoint::Entrypoint; +use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; +use tezos_smart_rollup_encoding::michelson::MichelsonBytes; +use tezos_smart_rollup_encoding::michelson::MichelsonNat; +use tezos_smart_rollup_encoding::michelson::MichelsonTimestamp; +use tezos_smart_rollup_encoding::michelson::{ + MichelsonContract, MichelsonOption, MichelsonPair, +}; +use tezos_smart_rollup_encoding::outbox::{OutboxMessage, OutboxMessageTransaction}; + +/// Added cost of doing a withdrawal. +/// +/// This is roughly the implied costs of executing the outbox message on L1 +/// as a spam prevention mechanism (outbox queue clogging). +/// In particular it prevents cases when a big number of withdrawals is batched +/// together in a single transaction which exploits the system. +/// +/// An execution of a single outbox message carrying a XTZ withdrawal +/// costs around 0.0025ęś© on L1; the equivalent amount of gas units on L2 is: +/// +/// 0.0025 * 10^18 / GAS_PRICE +/// +/// Multiplying the numerator by 2 for a safe reserve and this is our cost in Wei. +const WITHDRAWAL_PRECOMPILE_ADDED_COST: u64 = 5_000_000_000_000_000_000; + +/// Hard cap for the added gas cost (0.5 of the maximum gas limit per transaction). +/// If gas price drops the gas amount rises, but we don't want it to hit the transaction +/// gas limit. +const WITHDRAWAL_PRECOMPILE_MAX_ADDED_CAS_COST: u64 = 15_000_000; + +/// Keccak256 of Withdrawal(uint256,address,bytes22,uint256) +/// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events +pub const WITHDRAWAL_EVENT_TOPIC: [u8; 32] = [ + 45, 90, 215, 147, 24, 31, 91, 107, 215, 39, 192, 194, 22, 70, 30, 1, 158, 207, 228, + 102, 53, 63, 221, 233, 204, 248, 19, 244, 91, 132, 250, 130, +]; + +/// Keccak256 of FastWithdrawal(bytes22,uint256,uint256,uint256,bytes,address) +/// Arguments in this order: l1 target address, withdrawal_id, amount, timestamp +pub const FAST_WITHDRAWAL_EVENT_TOPIC: [u8; 32] = [ + 0x62, 0xe8, 0xe0, 0x1e, 0x31, 0xb8, 0x30, 0x84, 0xb9, 0x7c, 0x32, 0xb1, 0xb1, 0x1a, + 0xd5, 0xa2, 0x4, 0x23, 0x82, 0xf5, 0xf6, 0x5e, 0xe4, 0x10, 0x6a, 0xd4, 0xa0, 0xd0, + 0xf8, 0xb9, 0x94, 0x2c, +]; + +alloy_sol_types::sol! { + event SolFastWithdrawalEvent ( + bytes22 receiver, + uint256 withdrawalId, + uint256 amount, + uint256 timestamp, + bytes payload, + address l2_caller, + ); +} + +alloy_sol_types::sol! { + event SolFastWithdrawalInput ( + string target, + string fast_withdrawal_contract, + bytes payload, + ); +} + +/// Calculate precompile gas cost given the estimated amount of ticks and gas price. +fn estimate_gas_cost(estimated_ticks: u64, gas_price: U256) -> u64 { + // Using 1 gas unit ~= 1000 ticks convert ratio + let execution_cost = estimated_ticks / 1000; + let added_cost = U256::from(WITHDRAWAL_PRECOMPILE_MAX_ADDED_CAS_COST) + .min(U256::from(WITHDRAWAL_PRECOMPILE_ADDED_COST) / gas_price); + execution_cost + added_cost.as_u64() +} + +fn prepare_message( + parameters: RouterInterface, + destination: Contract, + entrypoint: Option<&str>, +) -> Option { + let entrypoint = + Entrypoint::try_from(String::from(entrypoint.unwrap_or("default"))).ok()?; + + let message = OutboxMessageTransaction { + parameters, + entrypoint, + destination, + }; + + let outbox_message = + Withdrawal::Standard(OutboxMessage::AtomicTransactionBatch(vec![message].into())); + Some(outbox_message) +} + +fn prepare_fast_withdraw_message( + parameters: FastWithdrawalInterface, + destination: Contract, + entrypoint: Option<&str>, +) -> Option { + let entrypoint = + Entrypoint::try_from(String::from(entrypoint.unwrap_or("default"))).ok()?; + + let message = OutboxMessageTransaction { + parameters, + entrypoint, + destination, + }; + + let outbox_message = + Withdrawal::Fast(OutboxMessage::AtomicTransactionBatch(vec![message].into())); + Some(outbox_message) +} + +fn revert_withdrawal() -> PrecompileOutcome { + PrecompileOutcome { + exit_status: ExitReason::Revert(ExitRevert::Reverted), + output: vec![], + withdrawals: vec![], + estimated_ticks: tick_model::ticks_of_withdraw(), + } +} + +pub struct WithdrawalBase { + ticketer: ContractKt1Hash, + base_transfer_value: U256, + target: Contract, + withdrawal_id: U256, + ticket: FA2_1Ticket, +} + +pub type BaseWithdrawOutcome = (Option, Option); + +pub fn base_withdrawal_preliminary( + handler: &mut EvmHandler, + address_str: String, + transfer: Transfer, +) -> Result { + let Some(target) = Contract::from_b58check(&address_str).ok() else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: invalid target address string" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let base_transfer_value = transfer.value; + + let amount = match mutez_from_wei(base_transfer_value) { + Ok(amount) => amount, + Err(ErrorMutezFromWei::NonNullRemainder) => { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: rounding would lose wei" + ); + return Ok((None, Some(revert_withdrawal()))); + } + Err(ErrorMutezFromWei::AmountTooLarge) => { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: amount is too large" + ); + return Ok((None, Some(revert_withdrawal()))); + } + }; + + // Burn the withdrawn amount + let mut withdrawal_precompiled = handler.get_or_create_account(WITHDRAWAL_ADDRESS)?; + withdrawal_precompiled.balance_remove(handler.borrow_host(), base_transfer_value)?; + + log!( + handler.borrow_host(), + Info, + "Withdrawal of {} to {:?}", + amount, + target + ); + + let Some(ticketer) = read_ticketer(handler.borrow_host()) else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to read ticketer" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let mut system = handler.get_or_create_account(SYSTEM_ACCOUNT_ADDRESS)?; + let withdrawal_id = + system.withdrawal_counter_get_and_increment(handler.borrow_host())?; + + let Some(ticket) = FA2_1Ticket::new( + Contract::Originated(ticketer.clone()), + MichelsonPair(0.into(), MichelsonOption(None)), + amount, + ) + .ok() else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: ticket amount is invalid" + ); + return Ok((None, Some(revert_withdrawal()))); + }; + + let outcome = WithdrawalBase { + ticketer, + base_transfer_value, + target, + withdrawal_id, + ticket, + }; + + Ok((Some(outcome), None)) +} + +pub fn emit_log_and_return( + handler: &mut EvmHandler, + message: Withdrawal, + estimated_ticks: u64, + withdrawal_event: Log, +) -> Result { + // TODO we need to measure number of ticks and translate this number into + // Ethereum gas units + + let withdrawals = vec![message]; + + handler + .add_log(withdrawal_event) + .map_err(|e| EthereumError::WrappedError(Cow::from(format!("{:?}", e))))?; + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: vec![], + withdrawals, + estimated_ticks, + }) +} + +/// Implementation of Etherlink specific withdrawals precompiled contract. +pub fn withdrawal_precompile( + handler: &mut EvmHandler, + input: &[u8], + context: &Context, + is_static: bool, + transfer: Option, +) -> Result { + let estimated_ticks = fail_if_too_much!(tick_model::ticks_of_withdraw(), handler); + + let estimated_gas_cost = estimate_gas_cost(estimated_ticks, handler.gas_price()); + if let Err(err) = handler.record_cost(estimated_gas_cost) { + log!( + handler.borrow_host(), + Info, + "Couldn't record the cost of withdrawal {:?}", + err + ); + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + let Some(transfer) = transfer else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: no transfer" + ); + return Ok(revert_withdrawal()); + }; + + if transfer.target != WITHDRAWAL_ADDRESS + || context.address != WITHDRAWAL_ADDRESS + || context.caller == WITHDRAWAL_ADDRESS + || is_static + { + return Ok(revert_withdrawal()); + } + + match input { + // "cda4fee2" is the function selector for `withdraw_base58(string)` + [0xcd, 0xa4, 0xfe, 0xe2, input_data @ ..] => { + // Execute base withdrawal preliminary + let Some(address_str) = abi::string_parameter(input_data, 0) else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: unable to get address argument" + ); + return Ok(revert_withdrawal()); + }; + + let (base_withdraw, precompile_outcome) = + base_withdrawal_preliminary(handler, address_str.to_string(), transfer)?; + + if let Some(precompile_outcome) = precompile_outcome { + return Ok(precompile_outcome); + } + + let Some(WithdrawalBase { + ticketer, + base_transfer_value, + target, + withdrawal_id, + ticket, + }) = base_withdraw + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to execute base withdrawal" + ); + return Ok(revert_withdrawal()); + }; + + // Prepare the outbox message + let parameters = MichelsonPair::( + MichelsonContract(target.clone()), + ticket, + ); + + let Some(message) = + prepare_message(parameters, Contract::Originated(ticketer), Some("burn")) + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to encode outbox message" + ); + return Ok(revert_withdrawal()); + }; + + // Emit log and return + let withdrawal_event = event_log( + &base_transfer_value, + &context.caller, + &target, + withdrawal_id, + ); + + emit_log_and_return(handler, message, estimated_ticks, withdrawal_event) + } + // "67a32cd7" is the function selector for `fast_withdraw_base58(string,string,bytes)` + [0x67, 0xa3, 0x2c, 0xd7, input_data @ ..] => { + if !fast_withdrawals_enabled(handler.host) { + let output = "The fast withdrawal feature flag is not enabled, \ + cannot call this entrypoint."; + return Ok(PrecompileOutcome { + exit_status: ExitReason::Revert(ExitRevert::Reverted), + output: output.as_bytes().to_vec(), + withdrawals: vec![], + estimated_ticks: tick_model::ticks_of_withdraw(), + }); + }; + + let Ok((target, fast_withdrawal_contract, payload)) = + SolFastWithdrawalInput::abi_decode_data(input_data, true) + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: unable to get address argument" + ); + return Ok(revert_withdrawal()); + }; + + // Execute base withdrawal preliminary + let (base_withdraw, precompile_outcome) = + base_withdrawal_preliminary(handler, target, transfer)?; + + if let Some(precompile_outcome) = precompile_outcome { + return Ok(precompile_outcome); + } + + let Some(WithdrawalBase { + ticketer: _, + base_transfer_value, + target, + withdrawal_id, + ticket, + }) = base_withdraw + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to execute base withdrawal" + ); + return Ok(revert_withdrawal()); + }; + + // Prepare the outbox message + let Some(fast_withdrawal) = + ContractKt1Hash::from_b58check(&fast_withdrawal_contract).ok() + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to read the fast withdrawal + contract address" + ); + return Ok(revert_withdrawal()); + }; + + let timestamp_u256 = handler.block_timestamp(); + + let Some(withdrawal_id_nat) = + MichelsonNat::new(Zarith(u256_to_bigint(withdrawal_id))) + else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: the withdrawal id is negative" + ); + return Ok(revert_withdrawal()); + }; + + let bytes_payload = MichelsonBytes(payload.to_vec()); + + let caller = MichelsonBytes(context.caller.to_fixed_bytes().to_vec()); + + let timestamp: MichelsonTimestamp = + MichelsonTimestamp(Zarith(u256_to_bigint(timestamp_u256))); + let contract_address = MichelsonContract(target.clone()); + + let parameters = MichelsonPair( + withdrawal_id_nat, + MichelsonPair( + ticket, + MichelsonPair( + timestamp, + MichelsonPair( + contract_address, + MichelsonPair(bytes_payload, caller), + ), + ), + ), + ); + + let Some(message) = prepare_fast_withdraw_message( + parameters, + Contract::Originated(fast_withdrawal), + Some("default"), + ) else { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: failed to encode outbox message" + ); + return Ok(revert_withdrawal()); + }; + + // Emit log and return + let withdrawal_event = event_log_fast_withdrawal( + &withdrawal_id, + &base_transfer_value, + ×tamp_u256, + &target, + payload.to_vec(), + &context.caller, + ); + + emit_log_and_return(handler, message, estimated_ticks, withdrawal_event) + } + // TODO A contract "function" to do withdrawal to byte encoded address + _ => { + log!( + handler.borrow_host(), + Info, + "Withdrawal precompiled contract: invalid function selector" + ); + Ok(revert_withdrawal()) + } + } +} + +/// Construct withdrawal event log from parts: +/// * `amount` - TEZ amount in wei +/// * `sender` - account on L2 +/// * `receiver` - account on L1 +/// * `withdrawal_id` - unique withdrawal ID (incremented on every successful or failed XTZ/FA withdrawal) +fn event_log( + amount: &U256, + sender: &H160, + receiver: &Contract, + withdrawal_id: U256, +) -> Log { + let mut data = Vec::with_capacity(3 * 32); + + data.extend_from_slice(&Into::<[u8; 32]>::into(*amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(&sender.0); + debug_assert!(data.len() % 32 == 0); + + // It is safe to unwrap, underlying implementation never fails (always returns Ok(())) + receiver.bin_write(&mut data).unwrap(); + data.extend_from_slice(&ABI_B22_RIGHT_PADDING); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&Into::<[u8; 32]>::into(withdrawal_id)); + debug_assert!(data.len() % 32 == 0); + + Log { + address: WITHDRAWAL_ADDRESS, + topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], + data, + } +} + +/// Construct fast withdrawal event log from parts: +/// * `withdrawal_id` - unique withdrawal ID (incremented on every successful or failed XTZ/FA withdrawal) +/// * `amount` - TEZ amount in wei +/// * `timestamp` - timestamp in milliseconds since Epoch +/// * `receiver` - account on L1 that initiated the fast withdrawal +/// * `payload` - generic payload +/// * `caller` - L2 caller's address +fn event_log_fast_withdrawal( + withdrawal_id: &U256, + amount: &U256, + timestamp: &U256, + receiver: &Contract, + payload: Vec, + caller: &H160, +) -> Log { + let mut receiver_bytes = vec![]; + // It is safe to unwrap, underlying implementation never fails (always returns Ok(())) + receiver.bin_write(&mut receiver_bytes).unwrap(); + let receiver_bytes: [u8; 22] = receiver_bytes.try_into().unwrap(); + + let event_data = SolFastWithdrawalEvent { + receiver: FixedBytes::<22>::from(&receiver_bytes), + withdrawalId: u256_to_alloy(withdrawal_id).unwrap_or_default(), + amount: u256_to_alloy(amount).unwrap_or_default(), + timestamp: u256_to_alloy(timestamp).unwrap_or_default(), + payload: payload.into(), + l2_caller: h160_to_alloy(caller), + }; + + let data = SolFastWithdrawalEvent::encode_data(&event_data); + + Log { + address: WITHDRAWAL_ADDRESS, + topics: vec![H256(FAST_WITHDRAWAL_EVENT_TOPIC)], + data, + } +} + +#[cfg(test)] +mod tests { + use crate::{ + handler::{ExecutionOutcome, ExecutionResult, Withdrawal}, + precompiles::{ + test_helpers::{execute_precompiled, DUMMY_TICKETER}, + withdrawal::{FAST_WITHDRAWAL_EVENT_TOPIC, WITHDRAWAL_EVENT_TOPIC}, + WITHDRAWAL_ADDRESS, + }, + }; + use alloy_sol_types::SolEvent; + use evm::{ExitSucceed, Transfer}; + use pretty_assertions::assert_eq; + use primitive_types::{H160, H256, U256}; + use sha3::{Digest, Keccak256}; + use std::str::FromStr; + use tezos_ethereum::{wei::eth_from_mutez, Log}; + use tezos_smart_rollup_encoding::contract::Contract; + use tezos_smart_rollup_encoding::michelson::ticket::FA2_1Ticket; + use tezos_smart_rollup_encoding::michelson::{ + MichelsonContract, MichelsonOption, MichelsonPair, + }; + + use super::prepare_message; + + mod events { + alloy_sol_types::sol! { + event Withdrawal ( + uint256 amount, + address sender, + bytes22 receiver, + uint256 withdrawalId, + ); + } + } + + fn make_message(ticketer: &str, target: &str, amount: u64) -> Withdrawal { + let target = Contract::from_b58check(target).unwrap(); + let ticketer = Contract::from_b58check(ticketer).unwrap(); + + let ticket = FA2_1Ticket::new( + ticketer.clone(), + MichelsonPair(0.into(), MichelsonOption(None)), + amount, + ) + .unwrap(); + + let parameters = MichelsonPair::( + MichelsonContract(target), + ticket, + ); + + prepare_message(parameters, ticketer, Some("burn")).unwrap() + } + + #[test] + fn withdrawal_event_signature() { + assert_eq!( + WITHDRAWAL_EVENT_TOPIC.to_vec(), + Keccak256::digest(b"Withdrawal(uint256,address,bytes22,uint256)").to_vec() + ); + } + + #[test] + fn fast_withdrawal_event_signature() { + assert_eq!( + FAST_WITHDRAWAL_EVENT_TOPIC.to_vec(), + Keccak256::digest( + b"FastWithdrawal(bytes22,uint256,uint256,uint256,bytes,address)" + ) + .to_vec() + ); + } + + #[test] + fn withdrawal_event_codec() { + assert_eq!(events::Withdrawal::SIGNATURE_HASH.0, WITHDRAWAL_EVENT_TOPIC); + + let amount = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 9, 24, 78, 114, 160, 0, + ]; + let sender = [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 118, + ]; + let receiver = [ + 0, 0, 66, 236, 118, 95, 39, 0, 19, 78, 158, 14, 254, 137, 208, 51, 142, 46, + 132, 60, 83, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ]; + let withdrawal_id = [1u8; 32]; + + let log = Log { + address: H160::zero(), + topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], + data: [amount, sender, receiver, withdrawal_id].concat(), + }; + + let log_data = alloy_primitives::LogData::new_unchecked( + log.topics.iter().map(|x| x.0.into()).collect(), + log.data.clone().into(), + ); + let event = events::Withdrawal::decode_log_data(&log_data, true).unwrap(); + assert_eq!(event.amount, alloy_primitives::U256::from_be_bytes(amount)); + assert_eq!( + event.sender, + alloy_primitives::Address::from_slice(&sender[12..]) + ); + assert_eq!( + event.receiver, + alloy_primitives::FixedBytes::from_slice(&receiver[..22]) + ); + assert_eq!( + event.withdrawalId, + alloy_primitives::U256::from_be_bytes(withdrawal_id) + ); + } + + #[test] + fn call_withdraw_with_implicit_address() { + // Format of input - generated by eg remix to match withdrawal ABI + // 1. function identifier (_not_ the parameter block) + // 2. location of first parameter (measured from start of parameter block) + // 3. Number of bytes in string argument + // 4. A Layer 1 contract address, hex-encoded + // 5. Zero padding for hex-encoded address + + // cast calldata "withdraw_base58(string)" "tz1RjtZUVeLhADFHDL8UwDZA6vjWWhojpu5w": + let input: &[u8] = &hex::decode( + "cda4fee2\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000024\ + 747a31526a745a5556654c6841444648444c385577445a4136766a5757686f6a70753577\ + 00000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let source = H160::from_low_u64_be(118u64); + let target = WITHDRAWAL_ADDRESS; + let value_mutez = 10; + + let transfer = Some(Transfer { + source, + target, + value: eth_from_mutez(value_mutez), + }); + + let result = + execute_precompiled(target, input, transfer, Some(30_000_000), false); + + let expected_output = vec![]; + let message = make_message( + DUMMY_TICKETER, + "tz1RjtZUVeLhADFHDL8UwDZA6vjWWhojpu5w", + value_mutez, + ); + + let expected_gas = 21000 // base cost, no additional cost for withdrawal + + 1032 // transaction data cost (90 zero bytes + 42 non zero bytes) + + 880 // gas for ticks + + 15_000_000; // cost of calling withdrawal precompiled contract (hard cap because of low gas price) + + let expected_log = Log { + address: WITHDRAWAL_ADDRESS, + topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], + data: [ + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 9, 24, 78, 114, 160, 0, + ], + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 118, + ], + [ + 0, 0, 66, 236, 118, 95, 39, 0, 19, 78, 158, 14, 254, 137, 208, 51, + 142, 46, 132, 60, 83, 220, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + [0u8; 32], + ] + .concat(), + }; + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![expected_log], + result: ExecutionResult::CallSucceeded( + ExitSucceed::Returned, + expected_output, + ), + withdrawals: vec![message], + estimated_ticks_used: 880_000, + }; + + assert_eq!(Ok(expected), result); + } + + #[test] + fn call_withdraw_with_kt1_address() { + // Format of input - generated by eg remix to match withdrawal ABI + // 1. function identifier (_not_ the parameter block) + // 2. location of first parameter (measured from start of parameter block) + // 3. Number of bytes in string argument + // 4. A Layer 1 contract address, hex-encoded + // 5. Zero padding for hex-encoded address + + let input: &[u8] = &hex::decode( + "cda4fee2\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000024\ + 4b54314275455a7462363863315134796a74636b634e6a47454c71577435365879657363\ + 00000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let source = H160::from_low_u64_be(118u64); + let target = H160::from_str("ff00000000000000000000000000000000000001").unwrap(); + let value_mutez = 10; + + let transfer = Some(Transfer { + source, + target, + value: eth_from_mutez(value_mutez), + }); + + let result = + execute_precompiled(target, input, transfer, Some(30_000_000), false); + + let expected_output = vec![]; + let message = make_message( + DUMMY_TICKETER, + "KT1BuEZtb68c1Q4yjtckcNjGELqWt56Xyesc", + value_mutez, + ); + + let expected_gas = 21000 // base cost, no additional cost for withdrawal + + 1032 // transaction data cost (90 zero bytes + 42 non zero bytes) + + 880 // gas for ticks + + 15_000_000; // cost of calling withdrawal precompiled contract (hard cap because of low gas price) + + let expected_log = Log { + address: WITHDRAWAL_ADDRESS, + topics: vec![H256(WITHDRAWAL_EVENT_TOPIC)], + data: [ + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 9, 24, 78, 114, 160, 0, + ], + [ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 118, + ], + [ + 1, 36, 102, 103, 169, 49, 254, 11, 210, 251, 28, 182, 4, 247, 20, 96, + 30, 136, 40, 69, 80, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + ], + [0u8; 32], + ] + .concat(), + }; + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![expected_log], + result: ExecutionResult::CallSucceeded( + ExitSucceed::Returned, + expected_output, + ), + withdrawals: vec![message], + // TODO (#6426): estimate the ticks consumption of precompiled contracts + estimated_ticks_used: 880_000, + }; + + assert_eq!(Ok(expected), result); + } + + #[test] + fn call_withdrawal_fails_without_transfer() { + let input: &[u8] = &hex::decode( + "cda4fee2\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000024\ + 747a31526a745a5556654c6841444648444c385577445a4136766a5757686f6a70753577\ + 00000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + // 1. Fails with no transfer + let target = H160::from_str("ff00000000000000000000000000000000000001").unwrap(); + + let transfer: Option = None; + + let result = + execute_precompiled(target, input, transfer, Some(30_000_000), false); + + let expected_gas = 21000 // base cost, no additional cost for withdrawal + + 1032 // transaction data cost (90 zero bytes + 42 non zero bytes) + + 880 // gas for ticks + + 15_000_000; // cost of calling withdrawal precompiled contract (hard cap because of low gas price) + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallReverted(vec![]), + withdrawals: vec![], + estimated_ticks_used: 880_000, + }; + + assert_eq!(Ok(expected), result); + + // 2. Fails with transfer of 0 amount. + + let source = H160::from_low_u64_be(118u64); + + let transfer: Option = Some(Transfer { + target, + source, + value: U256::zero(), + }); + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallReverted(vec![]), + withdrawals: vec![], + estimated_ticks_used: 880_000, + }; + + let result = + execute_precompiled(target, input, transfer, Some(30_000_000), false); + + assert_eq!(Ok(expected), result); + + // 3. Fails if static is true. + + let source = H160::from_low_u64_be(118u64); + + let transfer: Option = Some(Transfer { + target, + source, + value: eth_from_mutez(13), + }); + + let expected = ExecutionOutcome { + gas_used: expected_gas, + logs: vec![], + result: ExecutionResult::CallReverted(vec![]), + withdrawals: vec![], + estimated_ticks_used: 880_000, + }; + + let result = execute_precompiled(target, input, transfer, Some(30_000_000), true); + + assert_eq!(Ok(expected), result); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/precompiles/zero_knowledge.rs b/etherlink/kernel_calypso2/evm_execution/src/precompiles/zero_knowledge.rs new file mode 100644 index 000000000000..c351d7cb53fa --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/precompiles/zero_knowledge.rs @@ -0,0 +1,595 @@ +// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2023 draganrakita +// +// SPDX-License-Identifier: MIT + +use crate::precompiles::call_precompile_with_gas_draining; +use crate::{handler::EvmHandler, precompiles::PrecompileOutcome, EthereumError}; +use alloc::vec::Vec; +use bn::{FieldError, GroupError}; +use evm::{executor::stack::PrecompileFailure, ExitError, ExitReason, ExitSucceed}; +use evm::{Context, Transfer}; +use primitive_types::U256; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Debug; +use tezos_evm_runtime::runtime::Runtime; + +/// Input length for the add operation. +const ADD_INPUT_LEN: usize = 128; + +/// Input length for the multiplication operation. +const MUL_INPUT_LEN: usize = 128; + +/// Pair element length. +const PAIR_ELEMENT_LEN: usize = 192; + +fn bn128_field_point_not_a_member_error(_: FieldError) -> EthereumError { + EthereumError::PrecompileFailed(PrecompileFailure::Error { + exit_status: ExitError::Other(std::borrow::Cow::Borrowed( + "Bn128FieldPointNotAMember", + )), + }) +} + +fn bn128_affine_g_failed_to_create_error(_: GroupError) -> EthereumError { + EthereumError::PrecompileFailed(PrecompileFailure::Error { + exit_status: ExitError::Other(std::borrow::Cow::Borrowed( + "Bn128AffineGFailedToCreate", + )), + }) +} + +/// Reads the `x` and `y` points from an input at a given position. +fn read_point(input: &[u8], pos: usize) -> Result { + use bn::{AffineG1, Fq, Group, G1}; + + let mut px_buf = [0u8; 32]; + px_buf.copy_from_slice(&input[pos..(pos + 32)]); + let px = Fq::from_slice(&px_buf).map_err(bn128_field_point_not_a_member_error)?; + + let mut py_buf = [0u8; 32]; + py_buf.copy_from_slice(&input[(pos + 32)..(pos + 64)]); + let py = Fq::from_slice(&py_buf).map_err(bn128_field_point_not_a_member_error)?; + + if px == Fq::zero() && py == bn::Fq::zero() { + Ok(G1::zero()) + } else { + AffineG1::new(px, py) + .map(Into::into) + .map_err(bn128_affine_g_failed_to_create_error) + } +} + +fn ecadd_precompile_without_gas_draining( + handler: &mut EvmHandler, + input: &[u8], +) -> Result { + use bn::AffineG1; + log!(handler.borrow_host(), Debug, "Calling ecAdd precompile"); + let estimated_ticks = 1_700_000; + + if let Err(record_err) = handler.record_cost(150) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(record_err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + let mut input = input.to_vec(); + input.resize(ADD_INPUT_LEN, 0); + + let p1 = read_point(&input, 0)?; + let p2 = read_point(&input, 64)?; + + let mut output = [0u8; 64]; + if let Some(sum) = AffineG1::from_jacobian(p1 + p2) { + sum.x() + .into_u256() + .to_big_endian(&mut output[..32]) + .unwrap(); + sum.y() + .into_u256() + .to_big_endian(&mut output[32..]) + .unwrap(); + } + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +pub fn ecadd_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + call_precompile_with_gas_draining( + handler, + input, + ecadd_precompile_without_gas_draining, + ) +} + +fn ecmul_precompile_without_gas_draining( + handler: &mut EvmHandler, + input: &[u8], +) -> Result { + use bn::AffineG1; + log!(handler.borrow_host(), Debug, "Calling ecMul precompile"); + let estimated_ticks = 100_000_000; + + if let Err(record_err) = handler.record_cost(6_000) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(record_err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + let mut input = input.to_vec(); + input.resize(MUL_INPUT_LEN, 0); + + let p = read_point(&input, 0)?; + + let mut fr_buf = [0u8; 32]; + fr_buf.copy_from_slice(&input[64..96]); + // Fr::from_slice can only fail on incorrect length, and this is not a case. + let fr = bn::Fr::from_slice(&fr_buf[..]).unwrap(); + + let mut output = [0u8; 64]; + if let Some(mul) = AffineG1::from_jacobian(p * fr) { + mul.x().to_big_endian(&mut output[..32]).unwrap(); + mul.y().to_big_endian(&mut output[32..]).unwrap(); + } + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +pub fn ecmul_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + call_precompile_with_gas_draining( + handler, + input, + ecmul_precompile_without_gas_draining, + ) +} + +fn ecpairing_precompile_without_gas_draining( + handler: &mut EvmHandler, + input: &[u8], +) -> Result { + log!(handler.borrow_host(), Debug, "Calling ecPairing precompile"); + + let mut estimated_ticks = 70_000; + + if input.len() % PAIR_ELEMENT_LEN == 0 { + estimated_ticks += (input.len() / PAIR_ELEMENT_LEN) as u64 * 1_200_000_000 + } + + let gas_cost = (input.len() / PAIR_ELEMENT_LEN) as u64 * 34_000 /* ISTANBUL_PAIR_PER_POINT */ + 45_000 /* ISTANBUL_PAIR_BASE */; + + if let Err(record_err) = handler.record_cost(gas_cost) { + return Ok(PrecompileOutcome { + exit_status: ExitReason::Error(record_err), + output: vec![], + withdrawals: vec![], + estimated_ticks, + }); + } + + use bn::{AffineG1, AffineG2, Fq, Fq2, Group, Gt, G1, G2}; + + if input.len() % PAIR_ELEMENT_LEN != 0 { + return Err(EthereumError::PrecompileFailed(PrecompileFailure::Error { + exit_status: ExitError::Other(std::borrow::Cow::Borrowed("Bn128PairLength")), + })); + } + + let output = if input.is_empty() { + U256::one() + } else { + let elements = input.len() / PAIR_ELEMENT_LEN; + let mut vals = Vec::with_capacity(elements); + + const PEL: usize = PAIR_ELEMENT_LEN; + + for idx in 0..elements { + let mut buf = [0u8; 32]; + + buf.copy_from_slice(&input[(idx * PEL)..(idx * PEL + 32)]); + let ax = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + buf.copy_from_slice(&input[(idx * PEL + 32)..(idx * PEL + 64)]); + let ay = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + buf.copy_from_slice(&input[(idx * PEL + 64)..(idx * PEL + 96)]); + let bay = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + buf.copy_from_slice(&input[(idx * PEL + 96)..(idx * PEL + 128)]); + let bax = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + buf.copy_from_slice(&input[(idx * PEL + 128)..(idx * PEL + 160)]); + let bby = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + buf.copy_from_slice(&input[(idx * PEL + 160)..(idx * PEL + 192)]); + let bbx = + Fq::from_slice(&buf).map_err(bn128_field_point_not_a_member_error)?; + + let a = { + if ax.is_zero() && ay.is_zero() { + G1::zero() + } else { + G1::from( + AffineG1::new(ax, ay) + .map_err(bn128_affine_g_failed_to_create_error)?, + ) + } + }; + let b = { + let ba = Fq2::new(bax, bay); + let bb = Fq2::new(bbx, bby); + + if ba.is_zero() && bb.is_zero() { + G2::zero() + } else { + G2::from( + AffineG2::new(ba, bb) + .map_err(bn128_affine_g_failed_to_create_error)?, + ) + } + }; + vals.push((a, b)) + } + + let mul = vals + .into_iter() + .fold(Gt::one(), |s, (a, b)| s * bn::pairing(a, b)); + + if mul == Gt::one() { + U256::one() + } else { + U256::zero() + } + }; + + let mut final_output: [u8; 32] = [0; 32]; + U256::to_big_endian(&output, &mut final_output); + + Ok(PrecompileOutcome { + exit_status: ExitReason::Succeed(ExitSucceed::Returned), + output: final_output.to_vec(), + withdrawals: vec![], + estimated_ticks, + }) +} + +pub fn ecpairing_precompile( + handler: &mut EvmHandler, + input: &[u8], + _context: &Context, + _is_static: bool, + _transfer: Option, +) -> Result { + call_precompile_with_gas_draining( + handler, + input, + ecpairing_precompile_without_gas_draining, + ) +} + +#[cfg(test)] +mod tests { + use crate::precompiles::test_helpers::execute_precompiled; + use primitive_types::H160; + + #[test] + fn test_ecadd_precompile() { + let input = hex::decode( + "\ + 18b18acfb4c2c30276db5411368e7185b311dd124691610c5d3b74034e093dc9\ + 063c909c4720840cb5134cb9f59fa749755796819658d32efc0d288198f37266\ + 07c2b7f58a84bd6145f00c9c2bc0bb1a187f20ff2c92963a88019e7c6a014eed\ + 06614e20c147e940f2d70da3f74c9a17df361706a4485c742bd6788478fa17d7", + ) + .unwrap(); + let expected = hex::decode( + "\ + 2243525c5efd4b9c3d3c45ac0ca3fe4dd85e830a4ce6b65fa1eeaee202839703\ + 301d1d33be6da8e509df21cc35964723180eed7532537db9ae5e7d48f195c915", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(6), + &input, + None, + Some(50_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // zero sum test + let input = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + let expected = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(6), + &input, + None, + Some(50_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // no input test + let input = [0u8; 0]; + let expected = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(6), + &input, + None, + Some(50_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // point not on curve fail + let input = hex::decode( + "\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(6), + &input, + None, + Some(50_000), + false, + ); + // ERR_BN128_INVALID_POINT + assert!(result.is_err()); + } + + #[test] + fn test_ecmul_precompile() { + let input = hex::decode( + "\ + 2bd3e6d0f3b142924f5ca7b49ce5b9d54c4703d7ae5648e61d02268b1a0a9fb7\ + 21611ce0a6af85915e2f1d70300909ce2e49dfad4a4619c8390cae66cefdb204\ + 00000000000000000000000000000000000000000000000011138ce750fa15c2", + ) + .unwrap(); + let expected = hex::decode( + "\ + 070a8d6a982153cae4be29d434e8faef8a47b274a053f5a4ee2a6c9c13c31e5c\ + 031b8ce914eba3a9ffb989f9cdd5b0f01943074bf4f0f315690ec3cec6981afc", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(7), + &input, + None, + Some(40_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // zero multiplication test + let input = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0200000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + let expected = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(7), + &input, + None, + Some(40_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // no input test + let input = [0u8; 0]; + let expected = hex::decode( + "\ + 0000000000000000000000000000000000000000000000000000000000000000\ + 0000000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(7), + &input, + None, + Some(40_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // point not on curve fail + let input = hex::decode( + "\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 0f00000000000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(7), + &input, + None, + Some(40_000), + false, + ); + // ERR_BN128_INVALID_POINT + assert!(result.is_err()); + } + + #[test] + fn test_ecpairing_precompile() { + let input = hex::decode( + "\ + 1c76476f4def4bb94541d57ebba1193381ffa7aa76ada664dd31c16024c43f59\ + 3034dd2920f673e204fee2811c678745fc819b55d3e9d294e45c9b03a76aef41\ + 209dd15ebff5d46c4bd888e51a93cf99a7329636c63514396b4a452003a35bf7\ + 04bf11ca01483bfa8b34b43561848d28905960114c8ac04049af4b6315a41678\ + 2bb8324af6cfc93537a2ad1a445cfd0ca2a71acd7ac41fadbf933c2a51be344d\ + 120a2a4cf30c1bf9845f20c6fe39e07ea2cce61f0c9bb048165fe5e4de877550\ + 111e129f1cf1097710d41c4ac70fcdfa5ba2023c6ff1cbeac322de49d1b6df7c\ + 2032c61a830e3c17286de9462bf242fca2883585b93870a73853face6a6bf411\ + 198e9393920d483a7260bfb731fb5d25f1aa493335a9e71297e485b7aef312c2\ + 1800deef121f1e76426a00665e5c4479674322d4f75edadd46debd5cd992f6ed\ + 090689d0585ff075ec9e99ad690c3395bc4b313370b38ef355acdadcd122975b\ + 12c85ea5db8c6deb4aab71808dcb408fe3d1e7690c43d37b4ce6cc0166fa7daa", + ) + .unwrap(); + let expected = hex::decode( + "0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(8), + &input, + None, + Some(260_000), + false, + ); + println!("result {:?}", result); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // no input test + let input = [0u8; 0]; + let expected = hex::decode( + "0000000000000000000000000000000000000000000000000000000000000001", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(8), + &input, + None, + Some(260_000), + false, + ); + assert!(result.is_ok()); + let outcome = result.unwrap(); + assert!(outcome.is_success()); + assert_eq!(*outcome.output().unwrap(), expected); + + // point not on curve fail + let input = hex::decode( + "\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(8), + &input, + None, + Some(260_000), + false, + ); + // ERR_BN128_INVALID_A + assert!(result.is_err()); + + // invalid input length + let input = hex::decode( + "\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 1111111111111111111111111111111111111111111111111111111111111111\ + 111111111111111111111111111111\ + ", + ) + .unwrap(); + + let result = execute_precompiled( + H160::from_low_u64_be(8), + &input, + None, + Some(260_000), + false, + ); + // ERR_BN128_INVALID_LEN + assert!(result.is_err()); + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/storage.rs b/etherlink/kernel_calypso2/evm_execution/src/storage.rs new file mode 100644 index 000000000000..11f5c431bca8 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/storage.rs @@ -0,0 +1,207 @@ +// SPDX-FileCopyrightText: 2022,2024 TriliTech +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +pub mod tracer { + use host::{ + path::{OwnedPath, RefPath}, + runtime::RuntimeError, + }; + + use primitive_types::H256; + use tezos_evm_logging::log; + use tezos_evm_logging::Level::Debug; + use tezos_evm_runtime::runtime::Runtime; + use tezos_indexable_storage::{IndexableStorage, IndexableStorageError}; + use tezos_smart_rollup_host::path::*; + use thiserror::Error; + + use crate::trace::CallTrace; + use crate::trace::StructLog; + + const EVM_TRACE: RefPath = RefPath::assert_from(b"/evm/trace"); + + const GAS: RefPath = RefPath::assert_from(b"/gas"); + const FAILED: RefPath = RefPath::assert_from(b"/failed"); + const RETURN_VALUE: RefPath = RefPath::assert_from(b"/return_value"); + const STRUCT_LOGS: RefPath = RefPath::assert_from(b"/struct_logs"); + + #[derive(Eq, Error, Debug, PartialEq)] + pub enum Error { + #[error("Error from the indexable storage while tracing: {0}")] + IndexableStorageError(#[from] IndexableStorageError), + #[error("Error from runtime while tracing: {0}")] + RuntimeError(#[from] RuntimeError), + #[error("Error from path while tracing: {0}")] + PathError(#[from] PathError), + } + + pub fn trace_tx_path( + hash: &Option, + field: &RefPath, + ) -> Result { + let trace_tx_path = match hash { + None => EVM_TRACE.into(), + Some(hash) => { + let hash = hex::encode(hash); + let raw_tx_path: Vec = format!("/{}", &hash).into(); + let tx_path = OwnedPath::try_from(raw_tx_path)?; + concat(&EVM_TRACE, &tx_path)? + } + }; + concat(&trace_tx_path, field).map_err(Error::PathError) + } + + pub fn store_trace_gas( + host: &mut Host, + gas: u64, + hash: &Option, + ) -> Result<(), Error> { + let path = trace_tx_path(hash, &GAS)?; + host.store_write_all(&path, gas.to_le_bytes().as_slice())?; + Ok(()) + } + + pub fn store_trace_failed( + host: &mut Host, + is_success: bool, + hash: &Option, + ) -> Result<(), Error> { + let path = trace_tx_path(hash, &FAILED)?; + host.store_write_all(&path, &[u8::from(!is_success)])?; + log!(host, Debug, "Store trace info: is_success {}", is_success); + Ok(()) + } + + pub fn store_return_value( + host: &mut Host, + value: &[u8], + hash: &Option, + ) -> Result<(), Error> { + let path = trace_tx_path(hash, &RETURN_VALUE)?; + host.store_write_all(&path, value)?; + log!(host, Debug, "Store trace info: value {:?}", value); + Ok(()) + } + + pub fn store_struct_log( + host: &mut Host, + struct_log: StructLog, + hash: &Option, + ) -> Result<(), Error> { + let logs = rlp::encode(&struct_log); + + let path = trace_tx_path(hash, &STRUCT_LOGS)?; + let struct_logs_storage = IndexableStorage::new_owned_path(path); + + struct_logs_storage.push_value(host, &logs)?; + log!(host, Debug, "Store trace info: logs {:?}", logs); + + Ok(()) + } + + const CALL_TRACE: RefPath = RefPath::assert_from(b"/call_trace"); + + pub fn store_call_trace( + host: &mut Host, + call_trace: CallTrace, + hash: &Option, + ) -> Result<(), Error> { + let encoded_call_trace = rlp::encode(&call_trace); + + let path = trace_tx_path(hash, &CALL_TRACE)?; + let call_trace_storage = IndexableStorage::new_owned_path(path); + + call_trace_storage.push_value(host, &encoded_call_trace)?; + log!(host, Debug, "Store call trace: {:?}", call_trace); + + Ok(()) + } +} + +/// API to interact with blocks storage +pub mod blocks { + use host::path::OwnedPath; + use host::runtime::{Runtime, RuntimeError}; + use primitive_types::{H256, U256}; + use thiserror::Error; + + /// All errors that may happen as result of using this EVM storage interface. + #[derive(Error, Debug, PartialEq)] + pub enum EvmBlockStorageError { + /// Some runtime error happened while using the hosts durable storage. + #[error("Runtime error: {0:?}")] + RuntimeError(host::runtime::RuntimeError), + + /// Some error happened when constructing the path to some resource + /// associated with an account. + #[error("Path error: {0:?}")] + PathError(host::path::PathError), + + /// Passed block number is not sequential + /// comparing to stored current block number + #[error("Non sequential block levels. Current: {0}, new one: {1}")] + NonSequentialBlockLevels(U256, U256), + + /// Some blockhash in storage has wrong number of bytes + #[error("Malformed blockhash. Number of bytes: {0}")] + MalformedBlockHash(usize), + } + + impl From for EvmBlockStorageError { + fn from(error: RuntimeError) -> Self { + EvmBlockStorageError::RuntimeError(error) + } + } + + impl From for EvmBlockStorageError { + fn from(error: host::path::PathError) -> Self { + EvmBlockStorageError::PathError(error) + } + } + + // Ref. https://www.evm.codes/#40?fork=shanghai + // (opcode 0x40: BLOCKHASH) + pub const BLOCKS_STORED: usize = 256; + + /// Get block hash by block number. + pub fn get_block_hash( + host: &impl Runtime, + block_number: U256, + ) -> Result { + let block_path = to_block_hash_path(block_number)?; + let block_hash = host.store_read_all(&block_path)?; + + if block_hash.len() == 32 { + Ok(H256::from_slice(&block_hash)) + } else { + Err(EvmBlockStorageError::MalformedBlockHash(block_hash.len())) + } + } + + fn to_block_hash_path(block_number: U256) -> Result { + let path: Vec = + format!("/evm/world_state/indexes/blocks/{}", block_number).into(); + let owned_path = OwnedPath::try_from(path)?; + Ok(owned_path) + } + + /// Test utilities for block storage + pub mod test_utils { + use crypto::hash::BlockHash; + use std::iter::Map; + use std::ops::RangeFrom; + + type BlockIter = Map, fn(i32) -> BlockHash>; + + /// Helper function for generation infinite iterator of blocks + pub fn blocks_iter() -> BlockIter { + (1_i32..).map(|level| { + let level_bytes: Vec = Vec::from(level.to_be_bytes()); + BlockHash::try_from(level_bytes.repeat(8)) + .expect("Hash expected to be valid") + }) + } + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/tick_model_opcodes.rs b/etherlink/kernel_calypso2/evm_execution/src/tick_model_opcodes.rs new file mode 100644 index 000000000000..67e1b2964401 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/tick_model_opcodes.rs @@ -0,0 +1,1244 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Ticks per gas per Opcode model for the EVM Kernel + +// The values from this file have been autogenerated by a benchmark script. If +// it needs to be updated, please have a look at the script +// `etherlink/kernel_evm/benchmarks/scripts/analysis/opcodes.js`. + +use evm::Opcode; + +// Default ticks per gas value +const DEFAULT_TICKS_PER_GAS: u64 = 10000; + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x00(_gas: u64) -> u64 { + let upperbound = 6089; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x01(_gas: u64) -> u64 { + let upperbound = 12672; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x02(_gas: u64) -> u64 { + let upperbound = 15785; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x03(_gas: u64) -> u64 { + let upperbound = 12814; + let delta = 119; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x04(_gas: u64) -> u64 { + let upperbound = 18196; + let delta = 3430; + upperbound + delta +} + +// Reusing model for 0x04 +#[inline] +fn model_0x05(_gas: u64) -> u64 { + model_0x04(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x06(_gas: u64) -> u64 { + let upperbound = 18198; + let delta = 2375; + upperbound + delta +} + +// SMOO, approximated for MOD +#[inline] +fn model_0x07(_gas: u64) -> u64 { + model_0x06(_gas) +} + +// ADDMOO, model from AUB + MOD +#[inline] +fn model_0x08(_gas: u64) -> u64 { + model_0x01(_gas) + model_0x06(_gas) +} + +// MULMOD, model from MUL + MOD +#[inline] +fn model_0x09(_gas: u64) -> u64 { + model_0x02(_gas) + model_0x06(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x0a(_gas: u64) -> u64 { + let upperbound = 47478; + let delta = 0; + upperbound + delta +} + +// SIGNEXTEND, no data, 10 gas, approximated from EXP that uses 5 gas. +#[inline] +fn model_0x0b(_gas: u64) -> u64 { + model_0x0a(_gas) * 2 +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x10(_gas: u64) -> u64 { + let upperbound = 11811; + let delta = 3; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x11(_gas: u64) -> u64 { + let upperbound = 11814; + let delta = 7; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x12(_gas: u64) -> u64 { + let upperbound = 23967; + let delta = 107; + upperbound + delta +} + +// SGT, approximated for GT +#[inline] +fn model_0x13(_gas: u64) -> u64 { + model_0x11(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x14(_gas: u64) -> u64 { + let upperbound = 16187; + let delta = 4535; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x15(_gas: u64) -> u64 { + let upperbound = 13299; + let delta = 4527; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x16(_gas: u64) -> u64 { + let upperbound = 12491; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x17(_gas: u64) -> u64 { + let upperbound = 12494; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x18(_gas: u64) -> u64 { + let upperbound = 12497; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x19(_gas: u64) -> u64 { + let upperbound = 9219; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x1a(_gas: u64) -> u64 { + let upperbound = 75490; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x1b(_gas: u64) -> u64 { + let upperbound = 36666; + let delta = 499; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x1c(_gas: u64) -> u64 { + let upperbound = 40579; + let delta = 3154; + upperbound + delta +} + +// SAR, approximated BY SHR +#[inline] +fn model_0x1d(_gas: u64) -> u64 { + model_0x1c(_gas) +} + +// Linear regression model with delta +#[inline] +fn model_0x20(gas: u64) -> u64 { + let coef = 3360; + let intercept = 39248; + let delta = 35274; + coef * gas + intercept + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x30(_gas: u64) -> u64 { + let upperbound = 5838; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x31(_gas: u64) -> u64 { + let upperbound = 130222; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x32(_gas: u64) -> u64 { + let upperbound = 5832; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x33(_gas: u64) -> u64 { + let upperbound = 5847; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x34(_gas: u64) -> u64 { + let upperbound = 6520; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x35(_gas: u64) -> u64 { + let upperbound = 81623; + let delta = 332; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x36(_gas: u64) -> u64 { + let upperbound = 6078; + let delta = 0; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0x37(gas: u64) -> u64 { + let coef = 159; + let intercept = 102200; + let delta = 144193; + coef * gas + intercept + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x38(_gas: u64) -> u64 { + let upperbound = 6084; + let delta = 0; + upperbound + delta +} + +// NOTE: This model will need more investigation. +// Linear regression model with delta +#[inline] +fn model_0x39(gas: u64) -> u64 { + let coef = 224; + let intercept = 98507; + let delta = 428497; + coef * gas + intercept + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x3a(_gas: u64) -> u64 { + let upperbound = 6538; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x3b(_gas: u64) -> u64 { + let upperbound = 122188; + let delta = 17401; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0x3c(gas: u64) -> u64 { + let coef = 608; + let intercept = 90487; + let delta = 0; + coef * gas + intercept + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x3d(_gas: u64) -> u64 { + let upperbound = 27390; + let delta = 0; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0x3e(gas: u64) -> u64 { + let coef = 4004; + let intercept = 70083; + let delta = 3987; + coef * gas + intercept + delta +} + +// EXTCODEHASH, approximated by EXTCODESIZE with the same gas value and never +// recomputed. +#[inline] +fn model_0x3f(gas: u64) -> u64 { + model_0x3b(gas) +} + +// BLOCKHASH, approximated from EXTCODESIZE (which cost 100 gas, whereas +// BLOCKHASH costs 20). +#[inline] +fn model_0x40(_gas: u64) -> u64 { + model_0x3b(100) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x41(_gas: u64) -> u64 { + let upperbound = 5895; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x42(_gas: u64) -> u64 { + let upperbound = 6574; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x43(_gas: u64) -> u64 { + let upperbound = 6541; + let delta = 0; + upperbound + delta +} + +// PREVRANDAO, approximated from NUMBER +#[inline] +fn model_0x44(gas: u64) -> u64 { + model_0x43(gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x45(_gas: u64) -> u64 { + let upperbound = 6807; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x46(_gas: u64) -> u64 { + let upperbound = 27187; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x47(_gas: u64) -> u64 { + let upperbound = 130927; + let delta = 3186; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x48(_gas: u64) -> u64 { + let upperbound = 27205; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x50(_gas: u64) -> u64 { + let upperbound = 4582; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x51(_gas: u64) -> u64 { + let upperbound = 49314; + let delta = 57; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x52(_gas: u64) -> u64 { + let upperbound = 63417; + let delta = 5682; + upperbound + delta +} + +// MSTORE8, approximated from MSTORE +#[inline] +fn model_0x53(_gas: u64) -> u64 { + model_0x52(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x54(_gas: u64) -> u64 { + let upperbound = 220163; + let delta = 5167; + upperbound + delta +} + +// NOTE: this model has non constant gas and a linear regression with a negative +// coefficient, but the values seems rather packed under the 650K ticks. +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x55(_gas: u64) -> u64 { + let upperbound = 631174; + let delta = 30657; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x56(_gas: u64) -> u64 { + let upperbound = 8788; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x57(_gas: u64) -> u64 { + let upperbound = 13986; + let delta = 808; + upperbound + delta +} + +// PC, overapproximated from POP: it only a value from the runtime so i +// is completely overapproximated. +#[inline] +fn model_0x58(_gas: u64) -> u64 { + model_0x50(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x59(_gas: u64) -> u64 { + let upperbound = 5959; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x5a(_gas: u64) -> u64 { + let upperbound = 7119; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x5b(_gas: u64) -> u64 { + let upperbound = 4529; + let delta = 0; + upperbound + delta +} + +// PUSH0, overapproximated from PUSH1. +#[inline] +fn model_0x5f(_gas: u64) -> u64 { + model_0x60(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x60(_gas: u64) -> u64 { + let upperbound = 5603; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x61(_gas: u64) -> u64 { + let upperbound = 5706; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x62(_gas: u64) -> u64 { + let upperbound = 5887; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x63(_gas: u64) -> u64 { + let upperbound = 5912; + let delta = 0; + upperbound + delta +} + +// PUSH5, overapproximated from PUSH8. +#[inline] +fn model_0x64(_gas: u64) -> u64 { + model_0x67(_gas) +} + +// PUSH6, overapproximated from PUSH8. +#[inline] +fn model_0x65(_gas: u64) -> u64 { + model_0x67(_gas) +} + +// PUSH7, overapproximated from PUSH8. +#[inline] +fn model_0x66(_gas: u64) -> u64 { + model_0x67(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x67(_gas: u64) -> u64 { + let upperbound = 6294; + let delta = 0; + upperbound + delta +} + +// PUSH9, overapproximated from PUSH10. +#[inline] +fn model_0x68(_gas: u64) -> u64 { + model_0x69(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x69(_gas: u64) -> u64 { + let upperbound = 6566; + let delta = 0; + upperbound + delta +} + +#[inline] +fn model_0x6a(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x6b(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x6c(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x6d(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x6e(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x6f(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x70(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x71(_gas: u64) -> u64 { + model_0x73(_gas) +} + +#[inline] +fn model_0x72(_gas: u64) -> u64 { + model_0x73(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x73(_gas: u64) -> u64 { + let upperbound = 6761; + let delta = 297; + upperbound + delta +} + +#[inline] +fn model_0x74(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x75(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x76(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x77(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x78(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x79(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +#[inline] +fn model_0x7a(_gas: u64) -> u64 { + model_0x7b(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x7b(_gas: u64) -> u64 { + let upperbound = 7081; + let delta = 274; + upperbound + delta +} + +#[inline] +fn model_0x7c(_gas: u64) -> u64 { + model_0x7f(_gas) +} + +#[inline] +fn model_0x7d(_gas: u64) -> u64 { + model_0x7f(_gas) +} + +#[inline] +fn model_0x7e(_gas: u64) -> u64 { + model_0x7f(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x7f(_gas: u64) -> u64 { + let upperbound = 7163; + let delta = 331; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x80(_gas: u64) -> u64 { + let upperbound = 5602; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x81(_gas: u64) -> u64 { + let upperbound = 5611; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x82(_gas: u64) -> u64 { + let upperbound = 5614; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x83(_gas: u64) -> u64 { + let upperbound = 5617; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x84(_gas: u64) -> u64 { + let upperbound = 5620; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x85(_gas: u64) -> u64 { + let upperbound = 5623; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x86(_gas: u64) -> u64 { + let upperbound = 5626; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x87(_gas: u64) -> u64 { + let upperbound = 5629; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x88(_gas: u64) -> u64 { + let upperbound = 5632; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x89(_gas: u64) -> u64 { + let upperbound = 5635; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8a(_gas: u64) -> u64 { + let upperbound = 5638; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8b(_gas: u64) -> u64 { + let upperbound = 5641; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8c(_gas: u64) -> u64 { + let upperbound = 5644; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8d(_gas: u64) -> u64 { + let upperbound = 5647; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8e(_gas: u64) -> u64 { + let upperbound = 5650; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x8f(_gas: u64) -> u64 { + let upperbound = 5653; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x90(_gas: u64) -> u64 { + let upperbound = 5599; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x91(_gas: u64) -> u64 { + let upperbound = 5602; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x92(_gas: u64) -> u64 { + let upperbound = 5605; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x93(_gas: u64) -> u64 { + let upperbound = 5608; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x94(_gas: u64) -> u64 { + let upperbound = 5611; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x95(_gas: u64) -> u64 { + let upperbound = 5614; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x96(_gas: u64) -> u64 { + let upperbound = 5617; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x97(_gas: u64) -> u64 { + let upperbound = 5620; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x98(_gas: u64) -> u64 { + let upperbound = 5623; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x99(_gas: u64) -> u64 { + let upperbound = 5626; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x9a(_gas: u64) -> u64 { + let upperbound = 5629; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x9b(_gas: u64) -> u64 { + let upperbound = 5632; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x9c(_gas: u64) -> u64 { + let upperbound = 5635; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0x9d(_gas: u64) -> u64 { + let upperbound = 5638; + let delta = 0; + upperbound + delta +} + +// Approximated from the other SWAPs, that increase of 3 ticks each time +#[inline] +fn model_0x9e(_gas: u64) -> u64 { + model_0x9d(_gas) + 3 +} + +// Approximated from the other SWAPs, that increase of 3 ticks each time +#[inline] +fn model_0x9f(_gas: u64) -> u64 { + model_0x9e(_gas) + 3 +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xa0(_gas: u64) -> u64 { + let upperbound = 79189; + let delta = 0; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xa1(gas: u64) -> u64 { + let coef = 18; + let intercept = 65445; + let delta = 2453; + coef * gas + intercept + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xa2(gas: u64) -> u64 { + let coef = 27; + let intercept = 45729; + let delta = 111963; + coef * gas + intercept + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xa3(gas: u64) -> u64 { + let coef = 10; + let intercept = 67131; + let delta = 43; + coef * gas + intercept + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xa4(gas: u64) -> u64 { + let coef = 27; + let intercept = 31611; + let delta = 1355; + coef * gas + intercept + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xf0(gas: u64) -> u64 { + let coef = 49; + let intercept = 6049713; + let delta = 32642078; + coef * gas + intercept + delta +} + +// NOTE: CALL could be better optimized potentially. +// Linear regression model with delta +#[inline] +fn model_0xf1(gas: u64) -> u64 { + let coef = 556; + // Manually craft from: + // let intercept = -4086019; + // let delta = 51318604; + coef * gas + 47232585 +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xf2(_gas: u64) -> u64 { + let upperbound = 2313960; + let delta = 0; + upperbound + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xf3(_gas: u64) -> u64 { + let upperbound = 52332; + let delta = 625; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xf4(gas: u64) -> u64 { + let coef = 41; + let intercept = 1937715; + let delta = 593892; + coef * gas + intercept + delta +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xf5(_gas: u64) -> u64 { + let upperbound = 53856587; + let delta = 48758846; + upperbound + delta +} + +// Linear regression model with delta +#[inline] +fn model_0xfa(gas: u64) -> u64 { + let coef = 8105; + // Manually crafted from: + // let intercept = -112933; + // let delta = 1424; + // Set 0 as intercept + delta, otherwise its negactive + coef * gas +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xfd(_gas: u64) -> u64 { + let upperbound = 52333; + let delta = 0; + upperbound + delta +} + +// INVALID opcode, same as REVERT, overapproximated in practice +#[inline] +fn model_0xfe(_gas: u64) -> u64 { + model_0xfd(_gas) +} + +// Upperbound model, gas is ignored but kept to ease the automatic update from the script +#[inline] +fn model_0xff(_gas: u64) -> u64 { + let upperbound = 463774; + let delta = 0; + upperbound + delta +} + +pub fn ticks(opcode: &Opcode, gas: u64) -> u64 { + match opcode.as_u8() { + 0x00 => model_0x00(gas), // constant, no gas accounted + 0x01 => model_0x01(gas), + 0x02 => model_0x02(gas), + 0x03 => model_0x03(gas), + 0x04 => model_0x04(gas), + 0x05 => model_0x05(gas), + 0x06 => model_0x06(gas), + 0x07 => model_0x07(gas), + 0x08 => model_0x08(gas), + 0x09 => model_0x09(gas), + 0x0a => model_0x0a(gas), + 0x0b => model_0x0b(gas), + 0x10 => model_0x10(gas), + 0x11 => model_0x11(gas), + 0x12 => model_0x12(gas), + 0x13 => model_0x13(gas), + 0x14 => model_0x14(gas), + 0x15 => model_0x15(gas), + 0x16 => model_0x16(gas), + 0x17 => model_0x17(gas), + 0x18 => model_0x18(gas), + 0x19 => model_0x19(gas), + 0x1a => model_0x1a(gas), + 0x1b => model_0x1b(gas), + 0x1c => model_0x1c(gas), + 0x1d => model_0x1d(gas), + 0x20 => model_0x20(gas), + 0x30 => model_0x30(gas), + 0x31 => model_0x31(gas), + 0x32 => model_0x32(gas), + 0x33 => model_0x33(gas), + 0x34 => model_0x34(gas), + 0x35 => model_0x35(gas), + 0x36 => model_0x36(gas), + 0x37 => model_0x37(gas), + 0x38 => model_0x38(gas), + 0x39 => model_0x39(gas), + 0x3a => model_0x3a(gas), + 0x3b => model_0x3b(gas), + 0x3c => model_0x3c(gas), + 0x3d => model_0x3d(gas), + 0x3e => model_0x3e(gas), + 0x3f => model_0x3f(gas), + 0x40 => model_0x40(gas), + 0x41 => model_0x41(gas), + 0x42 => model_0x42(gas), + 0x43 => model_0x43(gas), + 0x44 => model_0x44(gas), + 0x45 => model_0x45(gas), + 0x46 => model_0x46(gas), + 0x47 => model_0x47(gas), + 0x48 => model_0x48(gas), + 0x50 => model_0x50(gas), + 0x51 => model_0x51(gas), + 0x52 => model_0x52(gas), + 0x53 => model_0x53(gas), + 0x54 => model_0x54(gas), + 0x55 => model_0x55(gas), + 0x56 => model_0x56(gas), + 0x57 => model_0x57(gas), + 0x58 => model_0x58(gas), + 0x59 => model_0x59(gas), + 0x5a => model_0x5a(gas), + 0x5b => model_0x5b(gas), + 0x5f => model_0x5f(gas), + 0x60 => model_0x60(gas), + 0x61 => model_0x61(gas), + 0x62 => model_0x62(gas), + 0x63 => model_0x63(gas), + 0x64 => model_0x64(gas), + 0x65 => model_0x65(gas), + 0x66 => model_0x66(gas), + 0x67 => model_0x67(gas), + 0x68 => model_0x68(gas), + 0x69 => model_0x69(gas), + 0x6a => model_0x6a(gas), + 0x6b => model_0x6b(gas), + 0x6c => model_0x6c(gas), + 0x6d => model_0x6d(gas), + 0x6e => model_0x6e(gas), + 0x6f => model_0x6f(gas), + 0x70 => model_0x70(gas), + 0x71 => model_0x71(gas), + 0x72 => model_0x72(gas), + 0x73 => model_0x73(gas), + 0x74 => model_0x74(gas), + 0x75 => model_0x75(gas), + 0x76 => model_0x76(gas), + 0x77 => model_0x77(gas), + 0x78 => model_0x78(gas), + 0x79 => model_0x79(gas), + 0x7a => model_0x7a(gas), + 0x7b => model_0x7b(gas), + 0x7c => model_0x7c(gas), + 0x7d => model_0x7d(gas), + 0x7e => model_0x7e(gas), + 0x7f => model_0x7f(gas), + 0x80 => model_0x80(gas), + 0x81 => model_0x81(gas), + 0x82 => model_0x82(gas), + 0x83 => model_0x83(gas), + 0x84 => model_0x84(gas), + 0x85 => model_0x85(gas), + 0x86 => model_0x86(gas), + 0x87 => model_0x87(gas), + 0x88 => model_0x88(gas), + 0x89 => model_0x89(gas), + 0x8a => model_0x8a(gas), + 0x8b => model_0x8b(gas), + 0x8c => model_0x8c(gas), + 0x8d => model_0x8d(gas), + 0x8e => model_0x8e(gas), + 0x8f => model_0x8f(gas), + 0x90 => model_0x90(gas), + 0x91 => model_0x91(gas), + 0x92 => model_0x92(gas), + 0x93 => model_0x93(gas), + 0x94 => model_0x94(gas), + 0x95 => model_0x95(gas), + 0x96 => model_0x96(gas), + 0x97 => model_0x97(gas), + 0x98 => model_0x98(gas), + 0x99 => model_0x99(gas), + 0x9a => model_0x9a(gas), + 0x9b => model_0x9b(gas), + 0x9c => model_0x9c(gas), + 0x9d => model_0x9d(gas), + 0x9e => model_0x9e(gas), + 0x9f => model_0x9f(gas), + 0xa0 => model_0xa0(gas), + 0xa1 => model_0xa1(gas), + 0xa2 => model_0xa2(gas), + 0xa3 => model_0xa3(gas), + 0xa4 => model_0xa4(gas), + 0xf0 => model_0xf0(gas), + 0xf1 => model_0xf1(gas), + 0xf2 => model_0xf2(gas), + 0xf3 => model_0xf3(gas), + 0xf4 => model_0xf4(gas), + 0xf5 => model_0xf5(gas), + 0xfa => model_0xfa(gas), + 0xfd => model_0xfd(gas), + 0xfe => model_0xfe(gas), + 0xff => model_0xff(gas), + _ => DEFAULT_TICKS_PER_GAS * gas, + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/trace.rs b/etherlink/kernel_calypso2/evm_execution/src/trace.rs new file mode 100644 index 000000000000..93221135c3d8 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/trace.rs @@ -0,0 +1,513 @@ +// SPDX-FileCopyrightText: 2024 Functori +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use primitive_types::{H160, H256, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; +#[cfg(test)] +use tezos_ethereum::rlp_helpers::{ + decode_field_u16_le, decode_field_u256_le, decode_field_u64_le, decode_list, + decode_option_canonical, +}; + +use tezos_ethereum::{ + rlp_helpers::{ + append_option_canonical, append_u16_le, append_u256_le, append_u64_le, + check_list, decode_field, decode_option, next, + }, + Log, +}; + +// For the following constant, we add +1 for the transaction hash in the input. +const STRUCT_LOGGER_CONFIG_SIZE: usize = 5; +const CALL_TRACER_CONFIG_SIZE: usize = 3; + +pub const CALL_TRACER_CONFIG_PREFIX: u8 = 0x01; + +#[derive(Debug, Clone, Copy)] +pub struct StructLoggerConfig { + pub enable_memory: bool, + pub enable_return_data: bool, + pub disable_stack: bool, + pub disable_storage: bool, +} + +#[derive(Debug, Clone, Copy)] +pub struct CallTracerConfig { + pub only_top_call: bool, + pub with_logs: bool, +} + +#[derive(Debug, Clone, Copy)] +pub enum TracerConfig { + StructLogger(StructLoggerConfig), + CallTracer(CallTracerConfig), +} + +#[derive(Debug, Clone, Copy)] +pub struct StructLoggerInput { + pub transaction_hash: Option, + pub config: StructLoggerConfig, +} + +#[derive(Debug, Clone, Copy)] +pub struct CallTracerInput { + pub transaction_hash: Option, + pub config: CallTracerConfig, +} + +#[derive(Debug, Clone, Copy)] +pub enum TracerInput { + StructLogger(StructLoggerInput), + CallTracer(CallTracerInput), +} + +impl TracerInput { + pub fn tx_hash(&self) -> Option { + match self { + TracerInput::StructLogger(input) => input.transaction_hash, + TracerInput::CallTracer(input) => input.transaction_hash, + } + } +} + +pub fn get_tracer_configuration( + tx_hash_target: H256, + tracer_input: Option, +) -> Option { + match tracer_input { + Some(tracer_input) => match tracer_input.tx_hash() { + None => { + // If there is no transaction hash, we still provide + // the configuration to trace all transactions + Some(tracer_input) + } + Some(input_hash) => { + // If there is a transaction hash in the input + // we only trace if the current transaction hash + // matches the transaction hash from the input + if input_hash == tx_hash_target { + Some(tracer_input) + } else { + None + } + } + }, + None => None, + } +} + +impl Decodable for StructLoggerInput { + fn decode(decoder: &Rlp) -> Result { + let mut it = decoder.iter(); + check_list(decoder, STRUCT_LOGGER_CONFIG_SIZE)?; + + let transaction_hash = decode_option(&next(&mut it)?, "transaction_hash")?; + let enable_memory = decode_field(&next(&mut it)?, "enable_memory")?; + let enable_return_data = decode_field(&next(&mut it)?, "enable_return_data")?; + let disable_stack = decode_field(&next(&mut it)?, "disable_stack")?; + let disable_storage = decode_field(&next(&mut it)?, "disable_storage")?; + + Ok(StructLoggerInput { + transaction_hash, + config: StructLoggerConfig { + enable_return_data, + enable_memory, + disable_stack, + disable_storage, + }, + }) + } +} + +impl Decodable for CallTracerInput { + fn decode(decoder: &Rlp) -> Result { + let mut it = decoder.iter(); + check_list(decoder, CALL_TRACER_CONFIG_SIZE)?; + + let transaction_hash = decode_option(&next(&mut it)?, "transaction_hash")?; + let only_top_call = decode_field(&next(&mut it)?, "only_top_call")?; + let with_logs = decode_field(&next(&mut it)?, "with_logs")?; + + Ok(CallTracerInput { + transaction_hash, + config: CallTracerConfig { + only_top_call, + with_logs, + }, + }) + } +} + +#[derive(Clone, PartialEq, Debug)] +pub struct StorageMapItem { + pub address: H160, + pub index: H256, + pub value: H256, +} + +impl Encodable for StorageMapItem { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(3); + stream.append(&self.address); + stream.append(&self.index); + stream.append(&self.value); + } +} + +#[cfg(test)] +impl Decodable for StorageMapItem { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(3) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let address: H160 = decode_field(&next(&mut it)?, "address")?; + let index: H256 = decode_field(&next(&mut it)?, "index")?; + let value: H256 = decode_field(&next(&mut it)?, "value")?; + + Ok(Self { + address, + index, + value, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct CallTrace { + pub type_: Vec, + pub from: H160, + pub to: Option, // None if type is CREATE / CREATE2 + pub value: U256, + pub gas: Option, // None if no gas limit was provided + pub gas_used: u64, + pub input: Vec, + pub output: Option>, // this output will also be used in revert reason, if there's any + pub error: Option>, + pub logs: Option>, + pub depth: u16, // will be helpful to reconstruct the tree of call on the node's side +} + +impl CallTrace { + pub fn new_minimal_trace( + type_: Vec, + from: H160, + value: U256, + gas_used: u64, + input: Vec, + depth: u16, + ) -> Self { + Self { + type_, + from, + to: None, + value, + gas: None, + gas_used, + input, + output: None, + error: None, + logs: None, + depth, + } + } + + pub fn add_to(&mut self, to: Option) { + *self = Self { to, ..self.clone() }; + } + + pub fn add_gas(&mut self, gas: Option) { + *self = Self { + gas, + ..self.clone() + }; + } + + pub fn add_output(&mut self, output: Option>) { + *self = Self { + output, + ..self.clone() + }; + } + + pub fn add_error(&mut self, error: Option>) { + *self = Self { + error, + ..self.clone() + }; + } + + pub fn add_logs(&mut self, logs: Option>) { + *self = Self { + logs, + ..self.clone() + }; + } +} + +impl Encodable for CallTrace { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(11); + stream.append(&self.type_); + stream.append(&self.from); + stream.append(&self.to); + append_u256_le(stream, &self.value); + append_option_canonical(stream, &self.gas, append_u64_le); + append_u64_le(stream, &self.gas_used); + stream.append(&self.input); + stream.append(&self.output); + stream.append(&self.error); + append_option_canonical(stream, &self.logs, |s, logs| s.append_list(logs)); + append_u16_le(stream, &self.depth); + } +} + +#[cfg(test)] +impl Decodable for CallTrace { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(11) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let type_ = decode_field(&next(&mut it)?, "type_")?; + let from = decode_field(&next(&mut it)?, "from")?; + let to = decode_field(&next(&mut it)?, "to")?; + let value = decode_field_u256_le(&next(&mut it)?, "value")?; + let gas = decode_option_canonical(&next(&mut it)?, "gas", decode_field_u64_le)?; + let gas_used = decode_field_u64_le(&next(&mut it)?, "gas_used")?; + let input = decode_field(&next(&mut it)?, "input")?; + let output = decode_field(&next(&mut it)?, "output")?; + let error = decode_field(&next(&mut it)?, "error")?; + let logs = decode_option_canonical(&next(&mut it)?, "logs", decode_list)?; + let depth = decode_field_u16_le(&next(&mut it)?, "depth")?; + + Ok(Self { + type_, + from, + to, + value, + gas, + gas_used, + input, + output, + error, + logs, + depth, + }) + } +} + +#[derive(PartialEq, Debug)] +pub struct StructLog { + pub pc: u64, + pub opcode: u8, + pub gas: u64, + pub gas_cost: u64, + pub depth: u16, + pub error: Option>, + pub stack: Option>, + pub return_data: Option>, + pub memory: Option>, + pub storage: Option>, +} + +impl StructLog { + #[allow(clippy::too_many_arguments)] + pub fn prepare( + pc: u64, + opcode: u8, + gas: u64, + depth: u16, + stack: Option>, + return_data: Option>, + memory: Option>, + storage: Option>, + ) -> Self { + StructLog { + pc, + opcode, + gas, + gas_cost: 0, + depth, + error: None, + stack, + return_data, + memory, + storage, + } + } + + pub fn finish(self, gas_cost: u64, error: Option>) -> Self { + StructLog { + gas_cost, + error, + ..self + } + } +} + +impl Encodable for StructLog { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(10); + append_u64_le(stream, &self.pc); + stream.append(&self.opcode); + append_u64_le(stream, &self.gas); + append_u64_le(stream, &self.gas_cost); + append_u16_le(stream, &self.depth); + stream.append(&self.error); + append_option_canonical(stream, &self.stack, |s, l| s.append_list(l)); + stream.append(&self.return_data); + stream.append(&self.memory); + append_option_canonical(stream, &self.storage, |s, l| s.append_list(l)); + } +} + +#[cfg(test)] +impl Decodable for StructLog { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(10) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let pc: u64 = decode_field_u64_le(&next(&mut it)?, "pc")?; + let opcode: u8 = decode_field(&next(&mut it)?, "opcode")?; + let gas: u64 = decode_field_u64_le(&next(&mut it)?, "gas")?; + let gas_cost: u64 = decode_field_u64_le(&next(&mut it)?, "gas")?; + let depth: u16 = decode_field_u16_le(&next(&mut it)?, "depth")?; + let error: Option> = decode_field(&next(&mut it)?, "error")?; + let stack: Option> = + decode_option_canonical(&next(&mut it)?, "stack", decode_list)?; + let return_data: Option> = decode_field(&next(&mut it)?, "return_data")?; + let memory: Option> = decode_field(&next(&mut it)?, "memory")?; + let storage: Option> = + decode_option_canonical(&next(&mut it)?, "storage", decode_list)?; + + Ok(Self { + pc, + opcode, + gas, + gas_cost, + depth, + error, + stack, + return_data, + memory, + storage, + }) + } +} + +#[cfg(test)] +pub mod tests { + use pretty_assertions::assert_eq; + use primitive_types::{H160, H256, U256}; + use tezos_ethereum::Log; + + use super::{CallTrace, StorageMapItem, StructLog}; + + #[test] + fn rlp_encode_decode_call_trace() { + let logs = Log { + address: H160::from([25; 20]), + topics: vec![H256::from([25; 32]), H256::from([13; 32])], + data: vec![0x00, 0x01, 0x02], + }; + + let call_trace = CallTrace { + type_: "CALL".into(), + from: H160::from([25; 20]), + to: Some(H160::from([25; 20])), + value: U256::from(251197), + gas: Some(5000), + gas_used: 5000, + input: vec![0x00, 0x01, 0x02], + output: Some(vec![0x00, 0x01, 0x02]), + error: Some(vec![0x00, 0x01, 0x02]), + logs: Some(vec![logs]), + depth: 2, + }; + + let encoded = rlp::encode(&call_trace); + let decoded: CallTrace = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(call_trace, decoded); + + let call_trace_none = CallTrace { + type_: "CALL".into(), + from: H160::from([25; 20]), + to: None, + value: U256::from(251197), + gas: None, + gas_used: 5000, + input: vec![0x00, 0x01, 0x02], + output: None, + error: None, + logs: None, + depth: 2, + }; + + let encoded = rlp::encode(&call_trace_none); + let decoded: CallTrace = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(call_trace_none, decoded) + } + + #[test] + fn rlp_encode_decode_storage_map_item() { + let storage_map_item = StorageMapItem { + address: H160::from([25; 20]), + index: H256::from([11; 32]), + value: H256::from([97; 32]), + }; + + let encoded = rlp::encode(&storage_map_item); + let decoded: StorageMapItem = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(storage_map_item, decoded) + } + + #[test] + fn rlp_encode_decode_struct_log() { + let storage_map_item = StorageMapItem { + address: H160::from([25; 20]), + index: H256::from([11; 32]), + value: H256::from([97; 32]), + }; + + let struct_log = StructLog { + pc: 25, + opcode: 11, + gas: 97, + gas_cost: 100, + depth: 3, + error: Some(vec![25, 11, 97]), + stack: Some(vec![H256::from([33; 32]), H256::from([35; 32])]), + return_data: Some(vec![25, 11, 97]), + memory: Some(vec![25, 11, 97]), + storage: Some(vec![storage_map_item]), + }; + + let encoded = rlp::encode(&struct_log); + let decoded: StructLog = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + assert_eq!(struct_log, decoded) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/transaction.rs b/etherlink/kernel_calypso2/evm_execution/src/transaction.rs new file mode 100644 index 000000000000..2e21d6e92027 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/transaction.rs @@ -0,0 +1,41 @@ +// SPDX-FileCopyrightText: 2022 TriliTech +// SPDX-FileCopyrightText: 2023 Functori +// +// SPDX-License-Identifier: MIT + +//! Ethereum transaction data + +use evm::Context; +use primitive_types::{H160, U256}; + +/// One transaction level +/// +/// Calling an EVM contract initiates a new transaction unless it is a +/// delegate call. This is a wrapper for the Context object of SputnikVM, +/// but will in time contain additional data related to the rollup node, +/// including a vector of withdrawals that may be generated using +/// a precompiled contract. +#[derive(Clone)] +pub struct TransactionContext { + /// The context for the transaction - caller/callee, et.c. + pub context: Context, +} + +#[allow(unused_variables)] +impl TransactionContext { + /// Create a new transaction context + pub fn new(caller_address: H160, callee_address: H160, apparent_value: U256) -> Self { + Self { + context: Context { + address: callee_address, + caller: caller_address, + apparent_value, + }, + } + } + + /// Create a transaction context from a SputnikVm context + pub fn from_context(context: Context) -> Self { + Self { context } + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/transaction_layer_data.rs b/etherlink/kernel_calypso2/evm_execution/src/transaction_layer_data.rs new file mode 100644 index 000000000000..956892ab3004 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/transaction_layer_data.rs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// SPDX-FileCopyrightText: 2023-2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::access_record::AccessRecord; +use crate::handler::Withdrawal; +use evm::executor::stack::Log; +use evm::gasometer::Gasometer; +use evm::Config; +use primitive_types::H160; +use std::collections::BTreeSet; + +/// Data related to the current transaction layer +pub struct TransactionLayerData<'config> { + /// Gasometer for the current transaction layer. If this value is + /// `None`, then the current transaction has no gas limit and no + /// gas accounting. + pub gasometer: Option>, + /// Whether the current transaction is static or not, ie, if the + /// transaction is allowed to update durable storage. + pub is_static: bool, + /// The log records gathered in this layer of transactions and any + /// committed sub layers. + pub logs: Vec, + /// The addresses of contracts that have been deleted as part of + /// the current transaction. + pub deleted_contracts: BTreeSet, + /// Any withdrawals generated by the current transaction level and + /// successful sub-levels. + pub withdrawals: Vec, + /// Keep track of accessed adresses and storages indices. + /// See EIP-2929 and YP section 6.1 + pub accessed_storage_keys: AccessRecord, +} + +impl<'config> TransactionLayerData<'config> { + /// Create the data associated with one layer of transactions - + /// one Ethereum transaction context. It initially has no log + /// records. If the gas limit is `None`, then there will be no + /// accounting for gas usage throughout the transaction, ie, there + /// will be no gasometer. + pub fn new( + is_static: bool, + gas_limit: Option, + config: &'config Config, + accessed_storage_keys: AccessRecord, + ) -> Self { + TransactionLayerData { + gasometer: gas_limit.map(|gl| Gasometer::new(gl, config)), + is_static, + logs: vec![], + deleted_contracts: BTreeSet::new(), + withdrawals: vec![], + accessed_storage_keys, + } + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/utilities.rs b/etherlink/kernel_calypso2/evm_execution/src/utilities.rs new file mode 100644 index 000000000000..1bb00ab80826 --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/utilities.rs @@ -0,0 +1,100 @@ +// SPDX-FileCopyrightText: 2024-2025 Functori +// SPDX-FileCopyrightText: 2023 draganrakita +// +// SPDX-License-Identifier: MIT + +use core::cmp::min; + +use alloc::vec::Vec; +use num_bigint::{BigInt, Sign}; +use primitive_types::{H160, H256, U256}; +use sha3::{Digest, Keccak256}; + +/// Get an array from the data, if data does not contain `start` to `len` bytes, add right padding with +/// zeroes +#[inline(always)] +pub fn get_right_padded(data: &[u8], offset: usize) -> [u8; S] { + let mut padded = [0; S]; + let start = min(offset, data.len()); + let end = min(start.saturating_add(S), data.len()); + padded[..end - start].copy_from_slice(&data[start..end]); + padded +} + +/// Get a vector of the data, if data does not contain the slice of `start` to `len`, right pad missing +/// part with zeroes +#[inline(always)] +pub fn get_right_padded_vec(data: &[u8], offset: usize, len: usize) -> Vec { + let mut padded = vec![0; len]; + let start = min(offset, data.len()); + let end = min(start.saturating_add(len), data.len()); + padded[..end - start].copy_from_slice(&data[start..end]); + padded +} + +/// Left padding until `len`. If data is more then len, truncate the right most bytes. +#[inline(always)] +pub fn left_padding(data: &[u8]) -> [u8; S] { + let mut padded = [0; S]; + let end = min(S, data.len()); + padded[S - end..].copy_from_slice(&data[..end]); + padded +} + +/// Left padding until `len`. If data is more then len, truncate the right most bytes. +#[inline(always)] +pub fn left_padding_vec(data: &[u8], len: usize) -> Vec { + let mut padded = vec![0; len]; + let end = min(len, data.len()); + padded[len - end..].copy_from_slice(&data[..end]); + padded +} + +pub fn create_address_legacy(caller: &H160, nonce: &u64) -> H160 { + let mut stream = rlp::RlpStream::new_list(2); + stream.append(caller); + stream.append(nonce); + H256::from_slice(Keccak256::digest(&stream.out()).as_slice()).into() +} + +/// Compute Keccak 256 for some bytes +pub fn keccak256_hash(bytes: &[u8]) -> H256 { + H256(Keccak256::digest(bytes).into()) +} + +/// Try to cast BigInt to U256 +pub fn bigint_to_u256(value: &BigInt) -> Result { + let (_, bytes) = value.to_bytes_le(); + if bytes.len() > 32 { + return Err(primitive_types::Error::Overflow); + } + Ok(U256::from_little_endian(&bytes)) +} + +/// Converts a U256 to a BigInt +pub fn u256_to_bigint(value: U256) -> BigInt { + let mut bytes = vec![0u8; 32]; + value.to_big_endian(&mut bytes); + BigInt::from_bytes_be(Sign::Plus, &bytes) +} + +pub fn u256_to_le_bytes(value: U256) -> Vec { + let mut bytes = vec![0u8; 32]; + value.to_little_endian(&mut bytes); + bytes +} + +pub mod alloy { + use super::*; + use alloy_primitives; + + pub fn u256_to_alloy(value: &U256) -> Option { + Some(alloy_primitives::U256::from_le_bytes::<32>( + u256_to_le_bytes(*value).try_into().ok()?, + )) + } + + pub fn h160_to_alloy(value: &H160) -> alloy_primitives::Address { + alloy_primitives::Address::from_slice(&value.to_fixed_bytes()) + } +} diff --git a/etherlink/kernel_calypso2/evm_execution/src/withdrawal_counter.rs b/etherlink/kernel_calypso2/evm_execution/src/withdrawal_counter.rs new file mode 100644 index 000000000000..d40d220b32bb --- /dev/null +++ b/etherlink/kernel_calypso2/evm_execution/src/withdrawal_counter.rs @@ -0,0 +1,87 @@ +// SPDX-FileCopyrightText: 2024 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Withdrawal counter tracks successful XTZ/FA withdrawals, +//! in other words - absolute outbox messages identifiers. +//! It is not related to actual outbox message IDs and indended for offchain +//! usage only (in particular for indexing purposes). +//! +//! Implemented as an EVM account state extension to enable +//! revertable updates (if withdrawal fails the counter won't increment). + +use primitive_types::U256; + +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_host::path::RefPath; +use tezos_storage::{read_u256_le_default, write_u256_le}; + +use crate::account_storage::{AccountStorageError, EthereumAccount}; + +/// Path where withdrawal counter is stored (relative to account) +pub const WITHDRAWAL_COUNTER_PATH: RefPath = RefPath::assert_from(b"/withdrawal_counter"); + +pub trait WithdrawalCounter { + /// Returns current withdrawal ID from the storage (or 0 if it's not initialized) + /// and increments & store the new value (will fail in case of overflow). + fn withdrawal_counter_get_and_increment( + &mut self, + host: &mut impl Runtime, + ) -> Result; +} + +impl WithdrawalCounter for EthereumAccount { + fn withdrawal_counter_get_and_increment( + &mut self, + host: &mut impl Runtime, + ) -> Result { + let path = self.custom_path(&WITHDRAWAL_COUNTER_PATH)?; + let old_value = read_u256_le_default(host, &path, U256::zero())?; + let new_value = old_value + .checked_add(U256::one()) + .ok_or(AccountStorageError::NonceOverflow)?; + + write_u256_le(host, &path, new_value)?; + Ok(old_value) + } +} + +#[cfg(test)] +mod tests { + use primitive_types::U256; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_host::path::RefPath; + use tezos_storage::read_u256_le_default; + + use crate::{account_storage::EthereumAccount, precompiles::SYSTEM_ACCOUNT_ADDRESS}; + + use super::WithdrawalCounter; + + #[test] + fn withdrawal_counter_initializes_and_increments() { + let mut mock_host = MockKernelHost::default(); + + let mut account = EthereumAccount::from_address(&SYSTEM_ACCOUNT_ADDRESS).unwrap(); + let path = b"/evm/world_state/eth_accounts/0000000000000000000000000000000000000000/withdrawal_counter"; + + let id = account + .withdrawal_counter_get_and_increment(&mut mock_host) + .unwrap(); + assert_eq!(U256::zero(), id); + + let next_id = + read_u256_le_default(&mock_host, &RefPath::assert_from(path), U256::zero()) + .unwrap(); + assert_eq!(U256::one(), next_id); + + let id = account + .withdrawal_counter_get_and_increment(&mut mock_host) + .unwrap(); + assert_eq!(U256::one(), id); + + let next_id = + read_u256_le_default(&mock_host, &RefPath::assert_from(path), U256::zero()) + .unwrap(); + assert_eq!(U256::from(2), next_id); + } +} diff --git a/etherlink/kernel_calypso2/indexable_storage/Cargo.toml b/etherlink/kernel_calypso2/indexable_storage/Cargo.toml new file mode 100644 index 000000000000..13ccd45e1ebe --- /dev/null +++ b/etherlink/kernel_calypso2/indexable_storage/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Functori +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos-indexable-storage-calypso2" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +thiserror.workspace = true +rlp.workspace = true +tezos-evm-logging.workspace = true +tezos-evm-runtime.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-mock.workspace = true +tezos-smart-rollup-storage.workspace = true +tezos-storage.workspace = true diff --git a/etherlink/kernel_calypso2/indexable_storage/src/lib.rs b/etherlink/kernel_calypso2/indexable_storage/src/lib.rs new file mode 100644 index 000000000000..383d87337623 --- /dev/null +++ b/etherlink/kernel_calypso2/indexable_storage/src/lib.rs @@ -0,0 +1,216 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use rlp::DecoderError; +use tezos_evm_logging::log; +use tezos_evm_logging::Level::Error; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_host::path::{concat, OwnedPath, PathError, RefPath}; +use tezos_smart_rollup_host::runtime::RuntimeError; +use tezos_smart_rollup_storage::StorageError; +use tezos_storage::{error::Error as GenStorageError, read_u64_le, write_u64_le}; +use thiserror::Error; + +const LENGTH: RefPath = RefPath::assert_from(b"/length"); + +/// An indexable storage is a push-only mapping between increasing integers to +/// bytes. It can serve as a replacement for the combination of the host +/// functions `store_get_nth` and `store_list_size` that are unsafe. +pub struct IndexableStorage { + /// An indexable storage is stored at a given path and consists of: + /// - `/length`: the number of keys + /// - `/` where keys are from `0` to `length - 1` + pub path: OwnedPath, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum IndexableStorageError { + #[error(transparent)] + Path(#[from] PathError), + #[error(transparent)] + Runtime(#[from] RuntimeError), + #[error(transparent)] + Storage(#[from] StorageError), + #[error("Failed to decode: {0}")] + RlpDecoderError(DecoderError), + #[error("Storage error: error while reading a value (incorrect size). Expected {expected} but got {actual}")] + InvalidLoadValue { expected: usize, actual: usize }, + #[error("Storage error: index out of bound")] + IndexOutOfBounds, +} + +impl From for IndexableStorageError { + fn from(e: GenStorageError) -> Self { + match e { + GenStorageError::Path(e) => IndexableStorageError::Path(e), + GenStorageError::Runtime(e) => IndexableStorageError::Runtime(e), + GenStorageError::Storage(e) => IndexableStorageError::Storage(e), + GenStorageError::RlpDecoderError(e) => { + IndexableStorageError::RlpDecoderError(e) + } + GenStorageError::InvalidLoadValue { expected, actual } => { + IndexableStorageError::InvalidLoadValue { expected, actual } + } + } + } +} + +impl IndexableStorage { + pub fn new(path: &RefPath<'_>) -> Result { + Ok(Self { path: path.into() }) + } + + pub fn new_owned_path(path: OwnedPath) -> Self { + Self { path } + } + + fn value_path(&self, index: u64) -> Result { + let index_as_path: Vec = format!("/{}", index).into(); + // The key being an integer value, it will always be valid as a path, + // `assert_from` cannot fail. + let index_subkey = RefPath::assert_from(&index_as_path); + concat(&self.path, &index_subkey) + } + + fn store_index( + &self, + host: &mut Host, + index: u64, + value_repr: &[u8], + ) -> Result<(), IndexableStorageError> { + let key_path = self.value_path(index)?; + host.store_write_all(&key_path, value_repr) + .map_err(IndexableStorageError::from) + } + + fn get_length_and_increment( + &self, + host: &mut Host, + ) -> Result { + let path = concat(&self.path, &LENGTH)?; + let length = read_u64_le(host, &path).unwrap_or(0); + write_u64_le(host, &path, length + 1)?; + Ok(length) + } + + #[allow(dead_code)] + /// `length` returns the number of keys in the storage. If `/length` does + /// not exists, the storage is considered as empty and returns '0'. + pub fn length( + &self, + host: &Host, + ) -> Result { + let path = concat(&self.path, &LENGTH)?; + match read_u64_le(host, &path) { + Ok(l) => Ok(l), + Err( + GenStorageError::Runtime( + RuntimeError::PathNotFound + | RuntimeError::HostErr(tezos_smart_rollup_host::Error::StoreNotAValue) + | RuntimeError::HostErr( + tezos_smart_rollup_host::Error::StoreInvalidAccess, + ), + ), + // An InvalidAccess implies that the path does not exist at all + // in the storage: store_read fails because reading is out of + // bounds since the value has never been allocated before + ) => Ok(0_u64), + Err(e) => { + log!(host, Error, "Error in indexable storage: {}", e); + Err(e.into()) + } + } + } + + #[allow(dead_code)] + /// Same as `get_value`, but doesn't check for bounds. + pub fn unsafe_get_value( + &self, + host: &Host, + index: u64, + ) -> Result, StorageError> { + let key_path = self.value_path(index)?; + host.store_read_all(&key_path).map_err(StorageError::from) + } + + /// Returns the value a the given index. Fails if the index is greater or + /// equal to the length. + #[cfg(debug_assertions)] + pub fn get_value( + &self, + host: &Host, + index: u64, + ) -> Result, IndexableStorageError> { + let length = self.length(host)?; + if index >= length { + return Err(IndexableStorageError::IndexOutOfBounds); + }; + self.unsafe_get_value(host, index) + .map_err(IndexableStorageError::from) + } + + /// Push a value at index `length`, and increments the length. + pub fn push_value( + &self, + host: &mut Host, + value: &[u8], + ) -> Result<(), IndexableStorageError> { + let new_index = self.get_length_and_increment(host)?; + self.store_index(host, new_index, value) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_host::path::RefPath; + + #[test] + fn test_indexable_empty() { + let host = MockKernelHost::default(); + let values = RefPath::assert_from(b"/values"); + let storage = IndexableStorage::new(&values).expect("Path to index is invalid"); + + assert_eq!(storage.length(&host), Ok(0)); + } + + #[test] + fn test_indexing_new_value() { + let mut host = MockKernelHost::default(); + let values = RefPath::assert_from(b"/values"); + let storage = IndexableStorage::new(&values).expect("Path to index is invalid"); + + let value = b"value"; + + storage + .push_value(&mut host, value) + .expect("Value could not be indexed"); + + assert_eq!(storage.length(&host), Ok(1)); + + assert_eq!(storage.get_value(&host, 0), Ok(value.to_vec())) + } + + #[test] + fn test_get_out_of_bounds() { + let mut host = MockKernelHost::default(); + let values = RefPath::assert_from(b"/values"); + let storage = IndexableStorage::new(&values).expect("Path to index is invalid"); + + let value = b"value"; + + storage + .push_value(&mut host, value) + .expect("Value could not be indexed"); + + assert_eq!(storage.length(&host), Ok(1)); + + assert_eq!( + storage.get_value(&host, 1), + Err(IndexableStorageError::IndexOutOfBounds) + ) + } +} diff --git a/etherlink/kernel_calypso2/kernel/Cargo.toml b/etherlink/kernel_calypso2/kernel/Cargo.toml new file mode 100644 index 000000000000..8b21dc5b1cee --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/Cargo.toml @@ -0,0 +1,83 @@ +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# SPDX-FileCopyrightText: 2023-2024 TriliTech +# SPDX-FileCopyrightText: 2023 Functori +# SPDX-FileCopyrightText: 2023 Marigold +# +# SPDX-License-Identifier: MIT + +[package] +name = 'evm_kernel_calypso2' +version = '0.1.0' +edition = '2021' +build = "build.rs" +license = "MIT" + +[lib] +crate-type = ["cdylib", "rlib"] + +[dependencies] +thiserror.workspace = true +anyhow.workspace = true + +primitive-types.workspace = true +num-traits.workspace = true +num-derive.workspace = true +softfloat.workspace = true + +rlp.workspace = true +hex.workspace = true + +bytes.workspace = true + +sha3.workspace = true +libsecp256k1.workspace = true +tezos_crypto_rs.workspace = true + +ethereum.workspace = true +ethbloom.workspace = true + +evm.workspace = true +evm-execution.workspace = true +tezos_ethereum.workspace = true +tezos-evm-logging.workspace = true +tezos-evm-runtime.workspace = true +tezos-indexable-storage.workspace = true +tezos-storage.workspace = true + +tezos-smart-rollup.workspace = true +tezos-smart-rollup-core.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-entrypoint.workspace = true +tezos-smart-rollup-debug.workspace = true +tezos-smart-rollup-encoding.workspace = true +tezos-smart-rollup-installer-config.workspace = true +tezos-smart-rollup-storage.workspace = true + +tezos_data_encoding.workspace = true + +proptest = { workspace = true, optional = true } + +[dev-dependencies] +tezos-smart-rollup-mock.workspace = true +tezos-smart-rollup-panic-hook.workspace = true +proptest.workspace = true + +# Hack: getrandom will use custom implementation if kernel is being built for wasm32-unknown-unknown +# See https://github.com/rust-random/getrandom/blob/a39033a34a0b81c5b15ef1fba28696ab93aac9db/src/custom.rs +# Generally getrandom is not supposed to be used in wasm env, this trick is just to overcome build errors +getrandom = { version = "=0.2.15", features = ["custom"] } + +pretty_assertions.workspace = true +evm-execution = { workspace = true, features = ["fa_bridge_testing"] } +alloy-sol-types.workspace = true +alloy-primitives.workspace = true + +[features] +default = ["panic-hook"] +panic-hook = [] +testing = ["proptest", "debug", "evm-execution/testing"] +debug = ["tezos-evm-logging/debug"] +benchmark = ["tezos-evm-logging/benchmark", "evm-execution/benchmark"] +benchmark-bypass-stage2 = ["benchmark"] +benchmark-opcodes = ["benchmark", "evm-execution/benchmark-opcodes"] +benchmark-full = ["benchmark", "debug", "benchmark-opcodes"] diff --git a/etherlink/kernel_calypso2/kernel/build.rs b/etherlink/kernel_calypso2/kernel/build.rs new file mode 100644 index 000000000000..110c524ff182 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/build.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Functori +// +// SPDX-License-Identifier: MIT + +use std::process::Command; + +fn main() { + println!("cargo:rerun-if-changed=src/*"); + let git_hash = match ( + option_env!("CI_COMMIT_SHA"), + Command::new("git").args(["rev-parse", "HEAD"]).output(), + ) { + (Some(commit), _) => commit.to_string(), + (_, Ok(output)) => String::from_utf8(output.stdout).unwrap(), + (None, Err(_)) => "unknown version".to_string(), + }; + println!("cargo:rustc-env=GIT_HASH={}", git_hash) +} diff --git a/etherlink/kernel_calypso2/kernel/src/apply.rs b/etherlink/kernel_calypso2/kernel/src/apply.rs new file mode 100644 index 000000000000..e7328911d364 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/apply.rs @@ -0,0 +1,1042 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023, 2025 Functori +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023-2024 PK Lab +// +// SPDX-License-Identifier: MIT + +use ethereum::Log; +use evm_execution::account_storage::{EthereumAccount, EthereumAccountStorage}; +use evm_execution::fa_bridge::deposit::FaDeposit; +use evm_execution::fa_bridge::{execute_fa_deposit, FA_DEPOSIT_PROXY_GAS_LIMIT}; +use evm_execution::handler::{ + ExecutionOutcome, ExecutionResult as ExecutionOutcomeResult, FastWithdrawalInterface, + RouterInterface, +}; +use evm_execution::precompiles::{self, PrecompileBTreeMap}; +use evm_execution::run_transaction; +use evm_execution::storage::tracer; +use evm_execution::trace::TracerInput::CallTracer; +use evm_execution::trace::{ + get_tracer_configuration, CallTrace, CallTracerConfig, CallTracerInput, TracerInput, +}; +use primitive_types::{H160, H256, U256}; +use tezos_ethereum::block::BlockConstants; +use tezos_ethereum::transaction::{TransactionHash, TransactionType}; +use tezos_ethereum::tx_common::EthereumTransactionCommon; +use tezos_ethereum::tx_signature::TxSignature; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::outbox::{OutboxMessage, OutboxQueue}; +use tezos_smart_rollup_host::path::{Path, RefPath}; + +use crate::bridge::{execute_deposit, Deposit}; +use crate::error::Error; +use crate::fees::{tx_execution_gas_limit, FeeUpdates}; +use crate::inbox::{Transaction, TransactionContent}; +use crate::CONFIG; + +// This implementation of `Transaction` is used to share the logic of +// transaction receipt and transaction object making. The functions +// `make_receipt_info` and `make_object_info` use these functions to build +// the associated infos. +impl Transaction { + fn to(&self) -> Option { + match &self.content { + TransactionContent::Deposit(Deposit { receiver, .. }) => Some(*receiver), + TransactionContent::FaDeposit(FaDeposit { + receiver, proxy, .. + }) => Some(proxy.unwrap_or(*receiver)), + TransactionContent::Ethereum(transaction) + | TransactionContent::EthereumDelayed(transaction) => transaction.to, + } + } + + fn data(&self) -> Vec { + match &self.content { + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => vec![], + TransactionContent::Ethereum(transaction) + | TransactionContent::EthereumDelayed(transaction) => { + transaction.data.clone() + } + } + } + + fn value(&self) -> U256 { + match &self.content { + TransactionContent::Deposit(Deposit { amount, .. }) => *amount, + &TransactionContent::FaDeposit(_) => U256::zero(), + TransactionContent::Ethereum(transaction) + | TransactionContent::EthereumDelayed(transaction) => transaction.value, + } + } + + fn nonce(&self) -> u64 { + match &self.content { + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => 0, + TransactionContent::Ethereum(transaction) + | TransactionContent::EthereumDelayed(transaction) => transaction.nonce, + } + } + + fn signature(&self) -> Option { + match &self.content { + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => None, + TransactionContent::Ethereum(transaction) + | TransactionContent::EthereumDelayed(transaction) => { + transaction.signature.clone() + } + } + } +} + +pub struct TransactionReceiptInfo { + pub tx_hash: TransactionHash, + pub index: u32, + pub execution_outcome: Option, + pub caller: H160, + pub to: Option, + pub effective_gas_price: U256, + pub type_: TransactionType, + pub overall_gas_used: U256, +} + +/// Details about the original transaction. +/// +/// See +/// for more details. +#[derive(Debug)] +pub struct TransactionObjectInfo { + pub from: H160, + /// Gas provided by the sender + pub gas: U256, + /// Gas price provided by the sender + pub gas_price: U256, + pub hash: TransactionHash, + pub input: Vec, + pub nonce: u64, + pub to: Option, + pub index: u32, + pub value: U256, + pub signature: Option, +} + +#[inline(always)] +#[allow(clippy::too_many_arguments)] +fn make_receipt_info( + tx_hash: TransactionHash, + index: u32, + execution_outcome: Option, + caller: H160, + to: Option, + effective_gas_price: U256, + type_: TransactionType, + overall_gas_used: U256, +) -> TransactionReceiptInfo { + TransactionReceiptInfo { + tx_hash, + index, + execution_outcome, + caller, + to, + effective_gas_price, + type_, + overall_gas_used, + } +} + +#[inline(always)] +fn make_object_info( + transaction: &Transaction, + from: H160, + index: u32, + fee_updates: &FeeUpdates, +) -> Result { + let (gas, gas_price) = match &transaction.content { + TransactionContent::Ethereum(e) | TransactionContent::EthereumDelayed(e) => { + (e.gas_limit_with_fees().into(), e.max_fee_per_gas) + } + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => { + (fee_updates.overall_gas_used, fee_updates.overall_gas_price) + } + }; + + Ok(TransactionObjectInfo { + from, + gas, + gas_price, + hash: transaction.tx_hash, + input: transaction.data(), + nonce: transaction.nonce(), + to: transaction.to(), + index, + value: transaction.value(), + signature: transaction.signature(), + }) +} + +fn account( + host: &mut Host, + caller: H160, + evm_account_storage: &mut EthereumAccountStorage, +) -> Result, Error> { + let caller_account_path = evm_execution::account_storage::account_path(&caller)?; + Ok(evm_account_storage.get(host, &caller_account_path)?) +} + +#[derive(Debug, PartialEq)] +pub enum Validity { + Valid(H160, u64), + InvalidChainId, + InvalidSignature, + InvalidNonce, + InvalidPrePay, + InvalidCode, + InvalidMaxBaseFee, + InvalidNotEnoughGasForFees, + InvalidGasLimitTooHigh, +} + +// TODO: https://gitlab.com/tezos/tezos/-/issues/6812 +// arguably, effective_gas_price should be set on EthereumTransactionCommon +// directly - initialised when constructed. +fn is_valid_ethereum_transaction_common( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + transaction: &EthereumTransactionCommon, + block_constant: &BlockConstants, + effective_gas_price: U256, + is_delayed: bool, +) -> Result { + // Chain id is correct. + if transaction.chain_id.is_some() + && Some(block_constant.chain_id) != transaction.chain_id + { + log!(host, Benchmarking, "Transaction status: ERROR_CHAINID"); + return Ok(Validity::InvalidChainId); + } + + // ensure that the user was willing to at least pay the base fee + if transaction.max_fee_per_gas < block_constant.base_fee_per_gas() { + log!(host, Benchmarking, "Transaction status: ERROR_MAX_BASE_FEE"); + return Ok(Validity::InvalidMaxBaseFee); + } + + // The transaction signature is valid. + let caller = match transaction.caller() { + Ok(caller) => caller, + Err(_err) => { + log!(host, Benchmarking, "Transaction status: ERROR_SIGNATURE."); + // Transaction with undefined caller are ignored, i.e. the caller + // could not be derived from the signature. + return Ok(Validity::InvalidSignature); + } + }; + + let account = account(host, caller, evm_account_storage)?; + + let (nonce, balance, code_exists): (u64, U256, bool) = match account { + None => (0, U256::zero(), false), + Some(account) => ( + account.nonce(host)?, + account.balance(host)?, + account.code_exists(host)?, + ), + }; + + // The transaction nonce is valid. + if nonce != transaction.nonce { + log!(host, Benchmarking, "Transaction status: ERROR_NONCE."); + return Ok(Validity::InvalidNonce); + }; + + // The sender account balance contains at least the cost. + let total_gas_limit = U256::from(transaction.gas_limit_with_fees()); + let cost = total_gas_limit.saturating_mul(effective_gas_price); + // The sender can afford the max gas fee he set, see EIP-1559 + let max_fee = total_gas_limit.saturating_mul(transaction.max_fee_per_gas); + + if balance < cost || balance < max_fee { + log!(host, Benchmarking, "Transaction status: ERROR_PRE_PAY."); + return Ok(Validity::InvalidPrePay); + } + + // The sender does not have code, see EIP3607. + if code_exists { + log!(host, Benchmarking, "Transaction status: ERROR_CODE."); + return Ok(Validity::InvalidCode); + } + + // check that enough gas is provided to cover fees + let Ok(gas_limit) = + tx_execution_gas_limit(transaction, &block_constant.block_fees, is_delayed) + else { + log!(host, Benchmarking, "Transaction status: ERROR_GAS_FEE."); + return Ok(Validity::InvalidNotEnoughGasForFees); + }; + + Ok(Validity::Valid(caller, gas_limit)) +} + +pub struct TransactionResult { + caller: H160, + execution_outcome: Option, + gas_used: U256, + estimated_ticks_used: u64, +} + +/// Technically incorrect: it is possible to do a call without sending any data, +/// however it's done for benchmarking only, and benchmarking doesn't include +/// such a scenario +fn log_transaction_type(host: &Host, to: Option, data: &[u8]) { + if to.is_none() { + log!(host, Benchmarking, "Transaction type: CREATE"); + } else if data.is_empty() { + log!(host, Benchmarking, "Transaction type: TRANSFER"); + } else { + log!(host, Benchmarking, "Transaction type: CALL"); + } +} + +#[allow(clippy::too_many_arguments)] +fn apply_ethereum_transaction_common( + host: &mut Host, + block_constants: &BlockConstants, + precompiles: &PrecompileBTreeMap, + evm_account_storage: &mut EthereumAccountStorage, + transaction: &EthereumTransactionCommon, + allocated_ticks: u64, + retriable: bool, + is_delayed: bool, + tracer_input: Option, +) -> Result, anyhow::Error> { + let effective_gas_price = block_constants.base_fee_per_gas(); + let (caller, gas_limit) = match is_valid_ethereum_transaction_common( + host, + evm_account_storage, + transaction, + block_constants, + effective_gas_price, + is_delayed, + )? { + Validity::Valid(caller, gas_limit) => (caller, gas_limit), + _reason => { + log!(host, Benchmarking, "Transaction type: INVALID"); + return Ok(ExecutionResult::Invalid); + } + }; + + let to = transaction.to; + let call_data = transaction.data.clone(); + log_transaction_type(host, to, &call_data); + let value = transaction.value; + let execution_outcome = match run_transaction( + host, + block_constants, + evm_account_storage, + precompiles, + CONFIG, + to, + caller, + call_data, + Some(gas_limit), + effective_gas_price, + value, + true, + allocated_ticks, + retriable, + false, + tracer_input, + ) { + Ok(outcome) => outcome, + Err(err) => { + return Err(Error::InvalidRunTransaction(err).into()); + } + }; + + let (gas_used, estimated_ticks_used, out_of_ticks) = match &execution_outcome { + Some(execution_outcome) => { + log!( + host, + Benchmarking, + "Transaction status: OK_{}.", + execution_outcome.is_success() + ); + ( + execution_outcome.gas_used.into(), + execution_outcome.estimated_ticks_used, + execution_outcome.result == ExecutionOutcomeResult::OutOfTicks, + ) + } + None => { + log!(host, Benchmarking, "Transaction status: OK_UNKNOWN."); + (U256::zero(), 0, false) + } + }; + + let transaction_result = TransactionResult { + caller, + execution_outcome, + gas_used, + estimated_ticks_used, + }; + + if out_of_ticks && retriable { + Ok(ExecutionResult::Retriable(transaction_result)) + } else { + Ok(ExecutionResult::Valid(transaction_result)) + } +} + +fn trace_deposit( + host: &mut Host, + amount: U256, + receiver: Option, + gas_used: u64, + logs: &[Log], + tracer_input: Option, +) { + if let Some(CallTracer(CallTracerInput { + transaction_hash, + config: CallTracerConfig { with_logs, .. }, + })) = tracer_input + { + let mut call_trace = CallTrace::new_minimal_trace( + "CALL".into(), + H160::zero(), + amount, + gas_used, + vec![], + 0, + ); + + call_trace.add_to(receiver); + + if with_logs { + call_trace.add_logs(Some(logs.to_owned())) + } + + let _ = tracer::store_call_trace(host, call_trace, &transaction_hash); + } +} + +fn apply_deposit( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + deposit: &Deposit, + transaction: &Transaction, + tracer_input: Option, +) -> Result, Error> { + let execution_outcome = execute_deposit(host, evm_account_storage, deposit, CONFIG) + .map_err(Error::InvalidRunTransaction)?; + + trace_deposit( + host, + transaction.value(), + transaction.to(), + execution_outcome.gas_used, + &execution_outcome.logs, + tracer_input, + ); + + Ok(ExecutionResult::Valid(TransactionResult { + caller: H160::zero(), + gas_used: execution_outcome.gas_used.into(), + estimated_ticks_used: execution_outcome.estimated_ticks_used, + execution_outcome: Some(execution_outcome), + })) +} + +#[allow(clippy::too_many_arguments)] +fn apply_fa_deposit( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + fa_deposit: &FaDeposit, + block_constants: &BlockConstants, + allocated_ticks: u64, + transaction: &Transaction, + tracer_input: Option, +) -> Result, Error> { + let caller = H160::zero(); + // Prevent inner calls to XTZ/FA withdrawal precompiles + let precompiles = precompiles::precompile_set_with_revert_withdrawals(true); + let outcome = execute_fa_deposit( + host, + block_constants, + evm_account_storage, + &precompiles, + CONFIG, + caller, + fa_deposit, + allocated_ticks, + tracer_input, + FA_DEPOSIT_PROXY_GAS_LIMIT, + ) + .map_err(Error::InvalidRunTransaction)?; + + log!( + host, + Benchmarking, + "Transaction status: OK_{}.", + outcome.is_success() + ); + + trace_deposit( + host, + transaction.value(), + transaction.to(), + outcome.gas_used, + &outcome.logs, + tracer_input, + ); + + Ok(ExecutionResult::Valid(TransactionResult { + caller, + gas_used: outcome.gas_used.into(), + estimated_ticks_used: outcome.estimated_ticks_used, + execution_outcome: Some(outcome), + })) +} + +pub const WITHDRAWAL_OUTBOX_QUEUE: RefPath = + RefPath::assert_from(b"/evm/world_state/__outbox_queue"); + +pub struct ExecutionInfo { + pub receipt_info: TransactionReceiptInfo, + pub object_info: TransactionObjectInfo, + pub estimated_ticks_used: u64, +} + +pub enum ExecutionResult { + Valid(T), + Invalid, + Retriable(T), +} + +impl From> for ExecutionResult { + fn from(opt: Option) -> ExecutionResult { + match opt { + Some(v) => ExecutionResult::Valid(v), + None => ExecutionResult::Invalid, + } + } +} + +#[allow(clippy::too_many_arguments)] +pub fn handle_transaction_result( + host: &mut Host, + outbox_queue: &OutboxQueue<'_, impl Path>, + block_constants: &BlockConstants, + transaction: &Transaction, + index: u32, + evm_account_storage: &mut EthereumAccountStorage, + transaction_result: TransactionResult, + pay_fees: bool, + sequencer_pool_address: Option, +) -> Result { + let TransactionResult { + caller, + mut execution_outcome, + gas_used, + estimated_ticks_used: ticks_used, + } = transaction_result; + + let to = transaction.to(); + + let fee_updates = transaction + .content + .fee_updates(&block_constants.block_fees, gas_used); + + if let Some(outcome) = &mut execution_outcome { + log!(host, Debug, "Transaction executed, outcome: {:?}", outcome); + log!(host, Benchmarking, "gas_used: {:?}", outcome.gas_used); + log!(host, Benchmarking, "reason: {:?}", outcome.result); + for message in outcome.withdrawals.drain(..) { + match message { + evm_execution::handler::Withdrawal::Standard(message) => { + let outbox_message: OutboxMessage = message; + let len = outbox_queue.queue_message(host, outbox_message)?; + log!(host, Debug, "Length of the outbox queue: {}", len); + } + evm_execution::handler::Withdrawal::Fast(message) => { + let outbox_message: OutboxMessage = message; + let len = outbox_queue.queue_message(host, outbox_message)?; + log!(host, Debug, "Length of the outbox queue: {}", len); + } + } + } + } + + if pay_fees { + fee_updates.apply(host, evm_account_storage, caller, sequencer_pool_address)?; + } + + let object_info = make_object_info(transaction, caller, index, &fee_updates)?; + + let receipt_info = make_receipt_info( + transaction.tx_hash, + index, + execution_outcome, + caller, + to, + fee_updates.overall_gas_price, + transaction.type_(), + fee_updates.overall_gas_used, + ); + + Ok(ExecutionInfo { + receipt_info, + object_info, + estimated_ticks_used: ticks_used, + }) +} + +#[allow(clippy::too_many_arguments)] +pub fn apply_transaction( + host: &mut Host, + outbox_queue: &OutboxQueue<'_, impl Path>, + block_constants: &BlockConstants, + precompiles: &PrecompileBTreeMap, + transaction: &Transaction, + index: u32, + evm_account_storage: &mut EthereumAccountStorage, + allocated_ticks: u64, + retriable: bool, + sequencer_pool_address: Option, + tracer_input: Option, +) -> Result, anyhow::Error> { + let tracer_input = get_tracer_configuration(H256(transaction.tx_hash), tracer_input); + let apply_result = match &transaction.content { + TransactionContent::Ethereum(tx) => apply_ethereum_transaction_common( + host, + block_constants, + precompiles, + evm_account_storage, + tx, + allocated_ticks, + retriable, + false, + tracer_input, + )?, + TransactionContent::EthereumDelayed(tx) => apply_ethereum_transaction_common( + host, + block_constants, + precompiles, + evm_account_storage, + tx, + allocated_ticks, + retriable, + true, + tracer_input, + )?, + TransactionContent::Deposit(deposit) => { + log!(host, Benchmarking, "Transaction type: DEPOSIT"); + apply_deposit( + host, + evm_account_storage, + deposit, + transaction, + tracer_input, + )? + } + TransactionContent::FaDeposit(fa_deposit) => { + log!(host, Benchmarking, "Transaction type: FA_DEPOSIT"); + apply_fa_deposit( + host, + evm_account_storage, + fa_deposit, + block_constants, + allocated_ticks, + transaction, + tracer_input, + )? + } + }; + + match apply_result { + ExecutionResult::Valid(tx_result) => { + let execution_result = handle_transaction_result( + host, + outbox_queue, + block_constants, + transaction, + index, + evm_account_storage, + tx_result, + true, + sequencer_pool_address, + )?; + Ok(ExecutionResult::Valid(execution_result)) + } + // Note that both branch must be differentiated as the fees won't be + // collected yet if the transaction is retriable. + ExecutionResult::Retriable(tx_result) => { + let execution_result = handle_transaction_result( + host, + outbox_queue, + block_constants, + transaction, + index, + evm_account_storage, + tx_result, + false, + sequencer_pool_address, + )?; + Ok(ExecutionResult::Retriable(execution_result)) + } + ExecutionResult::Invalid => Ok(ExecutionResult::Invalid), + } +} + +#[cfg(test)] +mod tests { + + use crate::{apply::Validity, fees::gas_for_fees}; + use evm_execution::account_storage::{account_path, EthereumAccountStorage}; + use primitive_types::{H160, U256}; + use tezos_ethereum::{ + block::{BlockConstants, BlockFees}, + transaction::TransactionType, + tx_common::EthereumTransactionCommon, + }; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + + use super::is_valid_ethereum_transaction_common; + + const CHAIN_ID: u32 = 1337; + + fn mock_block_constants() -> BlockConstants { + let block_fees = BlockFees::new( + U256::from(12345), + U256::from(12345), + U256::from(2_000_000_000_000u64), + ); + BlockConstants::first_block( + U256::from(Timestamp::from(0).as_u64()), + CHAIN_ID.into(), + block_fees, + crate::block::GAS_LIMIT, + H160::zero(), + ) + } + + fn address_from_str(s: &str) -> H160 { + let data = &hex::decode(s).unwrap(); + H160::from_slice(data) + } + + fn set_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } + } + + fn resign(transaction: EthereumTransactionCommon) -> EthereumTransactionCommon { + // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + let private_key = + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701"; + transaction + .sign_transaction(private_key.to_string()) + .expect("Should have been able to sign") + } + + fn valid_tx(gas_limit: u64) -> EthereumTransactionCommon { + let transaction = EthereumTransactionCommon::new( + TransactionType::Eip1559, + Some(CHAIN_ID.into()), + 0, + U256::zero(), + U256::from(21000), + gas_limit, + Some(H160::zero()), + U256::zero(), + vec![], + vec![], + None, + ); + // sign tx + resign(transaction) + } + + fn gas_for_fees_no_data(block_constants: &BlockConstants) -> u64 { + gas_for_fees( + block_constants.block_fees.da_fee_per_byte(), + block_constants.block_fees.minimum_base_fee_per_gas(), + vec![].as_slice(), + vec![].as_slice(), + ) + .expect("should have been able to calculate fees") + } + + #[test] + fn test_tx_is_valid() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let fee_gas = gas_for_fees_no_data(&block_constants); + let balance = U256::from(fee_gas + 21000) * gas_price; + let gas_limit = 21000 + fee_gas; + let transaction = valid_tx(gas_limit); + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::Valid(address, 21000), + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_is_invalid_cannot_prepay() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let fee_gas = gas_for_fees_no_data(&block_constants); + // account doesnt have enough funds for execution + let balance = U256::from(fee_gas) * gas_price; + let gas_limit = 21000 + fee_gas; + let transaction = valid_tx(gas_limit); + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidPrePay, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_is_invalid_signature() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let fee_gas = gas_for_fees_no_data(&block_constants); + let balance = U256::from(fee_gas + 21000) * gas_price; + let gas_limit = 21000 + fee_gas; + let mut transaction = valid_tx(gas_limit); + transaction.signature = None; + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidSignature, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_is_invalid_wrong_nonce() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let fee_gas = gas_for_fees_no_data(&block_constants); + let balance = U256::from(fee_gas + 21000) * gas_price; + let gas_limit = 21000 + fee_gas; + let mut transaction = valid_tx(gas_limit); + transaction.nonce = 42; + transaction = resign(transaction); + + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidNonce, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_is_invalid_wrong_chain_id() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let balance = U256::from(21000) * gas_price; + let mut transaction = valid_tx(1); + transaction.chain_id = Some(U256::from(42)); + transaction = resign(transaction); + + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidChainId, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_is_invalid_max_fee_less_than_base_fee() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let gas_price = U256::from(21000); + let max_gas_price = U256::one(); + // account doesnt have enough funds for execution + let fee_gas = gas_for_fees_no_data(&block_constants); + let gas_limit = 21000 + fee_gas; + let mut transaction = valid_tx(gas_limit); + // set a max base fee too low + transaction.max_fee_per_gas = max_gas_price; + transaction = resign(transaction); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidMaxBaseFee, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + } + + #[test] + fn test_tx_invalid_not_enough_gas_for_fee() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + let block_constants = mock_block_constants(); + + // setup + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let gas_price = U256::from(21000); + let balance = U256::from(21000) * gas_price; + // fund account + set_balance(&mut host, &mut evm_account_storage, &address, balance); + + let gas_limit = 21000; // gas limit is not enough to cover fees + let mut transaction = valid_tx(gas_limit); + transaction.data = vec![1u8]; + transaction = resign(transaction); + + // act + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + false, + ); + assert_eq!( + Validity::InvalidNotEnoughGasForFees, + res.expect("Verification should not have raise an error"), + "Transaction should have been rejected" + ); + + let res = is_valid_ethereum_transaction_common( + &mut host, + &mut evm_account_storage, + &transaction, + &block_constants, + gas_price, + true, + ); + assert!( + matches!( + res.expect("Verification should not have raise an error"), + Validity::Valid(_, _) + ), + "Transaction should have been accepted through delayed inbox" + ); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/block.rs b/etherlink/kernel_calypso2/kernel/src/block.rs new file mode 100644 index 000000000000..97a4a6ad5539 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/block.rs @@ -0,0 +1,1961 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +use crate::apply::{ + apply_transaction, ExecutionInfo, ExecutionResult, Validity, WITHDRAWAL_OUTBOX_QUEUE, +}; +use crate::block_storage; +use crate::blueprint_storage::{drop_blueprint, read_next_blueprint}; +use crate::configuration::ConfigurationMode; +use crate::configuration::Limits; +use crate::delayed_inbox::DelayedInbox; +use crate::error::Error; +use crate::event::Event; +use crate::inbox::Transaction; +use crate::storage; +use crate::upgrade; +use crate::upgrade::KernelUpgrade; +use crate::Configuration; +use crate::{block_in_progress, tick_model}; +use anyhow::Context; +use block_in_progress::BlockInProgress; +use evm_execution::account_storage::{init_account_storage, EthereumAccountStorage}; +use evm_execution::precompiles; +use evm_execution::precompiles::PrecompileBTreeMap; +use evm_execution::trace::TracerInput; +use primitive_types::{H160, H256, U256}; +use tezos_ethereum::transaction::TransactionHash; +use tezos_evm_logging::{log, Level::*, Verbosity}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_evm_runtime::safe_storage::SafeStorage; +use tezos_smart_rollup::outbox::OutboxQueue; +use tezos_smart_rollup_host::path::Path; +use tick_model::estimate_remaining_ticks_for_transaction_execution; + +use tezos_ethereum::block::BlockConstants; + +pub const GENESIS_PARENT_HASH: H256 = H256([0xff; 32]); + +pub const GAS_LIMIT: u64 = 1 << 50; + +/// Struct used to allow the compiler to check that the tick counter value is +/// correctly moved and updated. Copy and Clone should NOT be derived. +struct TickCounter { + c: u64, +} + +impl TickCounter { + pub fn new(c: u64) -> Self { + Self { c } + } + pub fn finalize(consumed_ticks: u64) -> Self { + Self { + c: consumed_ticks + tick_model::constants::FINALIZE_UPPER_BOUND, + } + } +} + +#[derive(PartialEq, Debug)] +pub enum BlockComputationResult { + RebootNeeded, + Finished { + included_delayed_transactions: Vec, + }, +} + +#[derive(PartialEq, Debug)] +pub enum ComputationResult { + RebootNeeded, + Finished, +} + +// A block in progress can either come directly from the storage (when the previous run did not have enough ticks to apply the full block) or from a blueprint +enum BlockInProgressProvenance { + Storage, + Blueprint, +} + +fn on_invalid_transaction( + host: &mut Host, + transaction: &Transaction, + block_in_progress: &mut BlockInProgress, + data_size: u64, +) { + if transaction.is_delayed() { + block_in_progress.register_delayed_transaction(transaction.tx_hash); + } + + block_in_progress.account_for_invalid_transaction(data_size); + log!( + host, + Benchmarking, + "Estimated ticks after tx: {}", + block_in_progress.estimated_ticks_in_run + ); +} + +#[allow(clippy::too_many_arguments)] +fn compute( + host: &mut Host, + outbox_queue: &OutboxQueue<'_, impl Path>, + block_in_progress: &mut BlockInProgress, + block_constants: &BlockConstants, + precompiles: &PrecompileBTreeMap, + evm_account_storage: &mut EthereumAccountStorage, + sequencer_pool_address: Option, + limits: &Limits, + tracer_input: Option, +) -> Result { + log!( + host, + Debug, + "Queue length {}.", + block_in_progress.queue_length() + ); + let mut is_first_transaction = true; + // iteration over all remaining transaction in the block + while block_in_progress.has_tx() { + let transaction = block_in_progress.pop_tx().ok_or(Error::Reboot)?; + let data_size: u64 = transaction.data_size(); + + log!(host, Benchmarking, "Transaction data size: {}", data_size); + // The current number of ticks remaining for the current `kernel_run` is allocated for the transaction. + let allocated_ticks = estimate_remaining_ticks_for_transaction_execution( + limits.maximum_allowed_ticks, + block_in_progress.estimated_ticks_in_run, + data_size, + ); + + let retriable = !is_first_transaction; + if allocated_ticks == 0 { + if retriable { + log!( + host, + Debug, + "There are not enough ticks left to try the\ + transaction, but it will be retried after reboot." + ); + block_in_progress.repush_tx(transaction); + } else { + log!( + host, + Error, + "Discarded a transaction because it couldn't\ + be allocated enough ticks even alone in a kernel run." + ); + } + + return Ok(BlockComputationResult::RebootNeeded); + } + + let execution_gas_limit = + transaction.execution_gas_limit(&block_constants.block_fees)?; + if execution_gas_limit > limits.maximum_gas_limit { + log!( + host, + Debug, + "Reason of invalidity: {:?}", + Validity::InvalidGasLimitTooHigh + ); + log!(host, Benchmarking, "Transaction type: INVALID"); + on_invalid_transaction(host, &transaction, block_in_progress, data_size); + continue; + }; + + // If `apply_transaction` returns `None`, the transaction should be + // ignored, i.e. invalid signature or nonce. + match apply_transaction( + host, + outbox_queue, + block_constants, + precompiles, + &transaction, + block_in_progress.index, + evm_account_storage, + allocated_ticks, + retriable, + sequencer_pool_address, + tracer_input, + )? { + ExecutionResult::Valid(ExecutionInfo { + receipt_info, + object_info, + estimated_ticks_used, + }) => { + if transaction.is_delayed() { + block_in_progress.register_delayed_transaction(transaction.tx_hash); + } + + block_in_progress.register_valid_transaction( + &transaction, + object_info, + receipt_info, + estimated_ticks_used, + host, + )?; + log!( + host, + Benchmarking, + "Estimated ticks after tx: {}", + block_in_progress.estimated_ticks_in_run + ); + } + + ExecutionResult::Retriable(_) => { + // It is the first block processed in this reboot. Additionally, + // it is the first transaction processed from this block in this + // reboot. The tick limit cannot be larger. + log!( + host, + Debug, + "The transaction exhausted the ticks of the \ + current reboot but will be retried." + ); + block_in_progress.repush_tx(transaction); + return Ok(BlockComputationResult::RebootNeeded); + } + ExecutionResult::Invalid => { + on_invalid_transaction(host, &transaction, block_in_progress, data_size) + } + }; + is_first_transaction = false; + } + Ok(BlockComputationResult::Finished { + included_delayed_transactions: block_in_progress.delayed_txs.clone(), + }) +} + +enum BlueprintParsing { + Next(Box), + None, +} + +#[cfg_attr(feature = "benchmark", inline(never))] +fn next_bip_from_blueprints( + host: &mut Host, + current_block_number: U256, + current_block_parent_hash: H256, + tick_counter: &TickCounter, + config: &mut Configuration, + kernel_upgrade: &Option, + minimum_base_fee_per_gas: U256, +) -> Result { + let (blueprint, size) = read_next_blueprint(host, config)?; + log!(host, Benchmarking, "Size of blueprint: {}", size); + match blueprint { + Some(blueprint) => { + if let Some(kernel_upgrade) = kernel_upgrade { + if blueprint.timestamp >= kernel_upgrade.activation_timestamp { + upgrade::upgrade(host, kernel_upgrade.preimage_hash)?; + // We abort the call, as there is no blueprint to execute, + // the kernel will reboot. + return Ok(BlueprintParsing::None); + } + } + let gas_price = crate::gas_price::base_fee_per_gas( + host, + blueprint.timestamp, + minimum_base_fee_per_gas, + ); + + let bip = block_in_progress::BlockInProgress::from_blueprint( + blueprint, + current_block_number, + current_block_parent_hash, + tick_counter.c, + gas_price, + ); + + tezos_evm_logging::log!( + host, + tezos_evm_logging::Level::Debug, + "bip: {bip:?}" + ); + Ok(BlueprintParsing::Next(Box::new(bip))) + } + None => Ok(BlueprintParsing::None), + } +} + +#[allow(clippy::too_many_arguments)] +fn compute_bip( + host: &mut Host, + outbox_queue: &OutboxQueue<'_, impl Path>, + mut block_in_progress: BlockInProgress, + current_block_number: &mut U256, + current_block_parent_hash: &mut H256, + previous_receipts_root: &mut Vec, + previous_transactions_root: &mut Vec, + precompiles: &PrecompileBTreeMap, + evm_account_storage: &mut EthereumAccountStorage, + tick_counter: &mut TickCounter, + sequencer_pool_address: Option, + limits: &Limits, + tracer_input: Option, + chain_id: U256, + minimum_base_fee_per_gas: U256, + da_fee_per_byte: U256, + coinbase: H160, +) -> anyhow::Result { + let constants: BlockConstants = block_in_progress.constants( + chain_id, + minimum_base_fee_per_gas, + da_fee_per_byte, + GAS_LIMIT, + coinbase, + ); + let result = compute( + host, + outbox_queue, + &mut block_in_progress, + &constants, + precompiles, + evm_account_storage, + sequencer_pool_address, + limits, + tracer_input, + )?; + match &result { + BlockComputationResult::RebootNeeded => { + log!(host, Info, "Ask for reboot."); + log!( + host, + Benchmarking, + "Ask for reboot. Estimated ticks: {}", + &block_in_progress.estimated_ticks_in_run + ); + storage::store_block_in_progress(host, &block_in_progress)?; + } + BlockComputationResult::Finished { + included_delayed_transactions: _, + } => { + crate::gas_price::register_block(host, &block_in_progress)?; + *tick_counter = + TickCounter::finalize(block_in_progress.estimated_ticks_in_run); + let new_block = block_in_progress + .finalize_and_store( + host, + &constants, + previous_receipts_root.clone(), + previous_transactions_root.clone(), + ) + .context("Failed to finalize the block in progress")?; + *current_block_number = new_block.number + 1; + *current_block_parent_hash = new_block.hash; + *previous_receipts_root = new_block.receipts_root; + *previous_transactions_root = new_block.transactions_root; + } + } + Ok(result) +} + +fn revert_block( + safe_host: &mut SafeStorage<&mut Host>, + block_in_progress_provenance: &BlockInProgressProvenance, + number: U256, + error: anyhow::Error, +) -> anyhow::Result<()> { + log!( + safe_host, + Error, + "Block{} {} failed with '{:?}'. Reverting.", + match block_in_progress_provenance { + BlockInProgressProvenance::Storage => { + "InProgress" + } + BlockInProgressProvenance::Blueprint => { + "" + } + }, + number, + error + ); + safe_host.revert()?; + drop_blueprint(safe_host.host, number)?; + Ok(()) +} + +fn clean_delayed_transactions( + host: &mut impl Runtime, + delayed_inbox: &mut DelayedInbox, + delayed_txs: Vec, +) -> anyhow::Result<()> { + for hash in delayed_txs { + delayed_inbox.delete(host, hash.into())?; + } + Ok(()) +} + +fn promote_block( + safe_host: &mut SafeStorage<&mut Host>, + outbox_queue: &OutboxQueue<'_, impl Path>, + block_in_progress_provenance: &BlockInProgressProvenance, + number: U256, + config: &mut Configuration, + delayed_txs: Vec, +) -> anyhow::Result<()> { + if let BlockInProgressProvenance::Storage = block_in_progress_provenance { + storage::delete_block_in_progress(safe_host)?; + } + safe_host.promote()?; + safe_host.promote_trace()?; + drop_blueprint(safe_host.host, number)?; + + let hash = block_storage::read_current_hash(safe_host.host)?; + + Event::BlueprintApplied { number, hash }.store(safe_host.host)?; + + let written = outbox_queue.flush_queue(safe_host.host); + // Log to Info only if we flushed messages. + let level = if written > 0 { Info } else { Debug }; + log!( + safe_host, + level, + "Flushed outbox queue messages ({} flushed)", + written + ); + + if let ConfigurationMode::Sequencer { delayed_inbox, .. } = &mut config.mode { + clean_delayed_transactions(safe_host.host, delayed_inbox, delayed_txs)?; + } + + Ok(()) +} + +pub fn produce( + host: &mut Host, + chain_id: U256, + config: &mut Configuration, + sequencer_pool_address: Option, + tracer_input: Option, +) -> Result { + let minimum_base_fee_per_gas = crate::retrieve_minimum_base_fee_per_gas(host)?; + let da_fee_per_byte = crate::retrieve_da_fee(host)?; + + let kernel_upgrade = upgrade::read_kernel_upgrade(host)?; + + // If there's a pool address, the coinbase in block constants and miner + // in blocks is set to the pool address. + let coinbase = sequencer_pool_address.unwrap_or_default(); + + let ( + mut current_block_number, + mut current_block_parent_hash, + mut previous_receipts_root, + mut previous_transactions_root, + ) = match block_storage::read_current(host) { + Ok(block) => ( + block.number + 1, + block.hash, + block.receipts_root, + block.transactions_root, + ), + Err(_) => (U256::zero(), GENESIS_PARENT_HASH, vec![0; 32], vec![0; 32]), + }; + let mut evm_account_storage = + init_account_storage().context("Failed to initialize EVM account storage")?; + let mut tick_counter = TickCounter::new(0u64); + + let mut safe_host = SafeStorage { host }; + let outbox_queue = OutboxQueue::new(&WITHDRAWAL_OUTBOX_QUEUE, u32::MAX)?; + let precompiles = + precompiles::precompile_set::>(config.enable_fa_bridge); + + // Check if there's a BIP in storage to resume its execution + let (processed_blueprint, block_in_progress_provenance, block_in_progress) = + match storage::read_block_in_progress(&safe_host)? { + Some(block_in_progress) => ( + block_in_progress.number, + BlockInProgressProvenance::Storage, + block_in_progress, + ), + None => { + // Using `safe_host.host` allows to escape from the failsafe storage, which is necessary + // because the sequencer pool address is located outside of `/evm/world_state`. + upgrade::possible_sequencer_upgrade(safe_host.host)?; + + // Execute at most one of the stored blueprints + let block_in_progress = match next_bip_from_blueprints( + safe_host.host, + current_block_number, + current_block_parent_hash, + &tick_counter, + config, + &kernel_upgrade, + minimum_base_fee_per_gas, + )? { + BlueprintParsing::Next(bip) => bip, + BlueprintParsing::None => { + log!( + safe_host, + Benchmarking, + "Estimated ticks: {}", + tick_counter.c + ); + return Ok(ComputationResult::Finished); + } + }; + // We are going to execute a new block, we copy the storage to allow + // to revert if the block fails. + safe_host.start()?; + ( + current_block_number, + BlockInProgressProvenance::Blueprint, + *block_in_progress, + ) + } + }; + + match compute_bip( + &mut safe_host, + &outbox_queue, + block_in_progress, + &mut current_block_number, + &mut current_block_parent_hash, + &mut previous_receipts_root, + &mut previous_transactions_root, + &precompiles, + &mut evm_account_storage, + &mut tick_counter, + sequencer_pool_address, + &config.limits, + tracer_input, + chain_id, + minimum_base_fee_per_gas, + da_fee_per_byte, + coinbase, + ) { + Ok(BlockComputationResult::Finished { + included_delayed_transactions, + }) => { + promote_block( + &mut safe_host, + &outbox_queue, + &block_in_progress_provenance, + processed_blueprint, + config, + included_delayed_transactions, + )?; + Ok(ComputationResult::RebootNeeded) + } + Ok(BlockComputationResult::RebootNeeded) => { + // The computation will resume at next reboot, we leave the + // storage untouched. + if let BlockInProgressProvenance::Blueprint = &block_in_progress_provenance { + log!( + safe_host, + Benchmarking, + "Estimated ticks: {}", + tick_counter.c + ) + }; + Ok(ComputationResult::RebootNeeded) + } + Err(err) => { + revert_block( + &mut safe_host, + &block_in_progress_provenance, + processed_blueprint, + err, + )?; + // The block was reverted because it failed. We don't know at + // which point did it fail nor why. We cannot make assumption + // on how many ticks it consumed before failing. Therefore + // the safest solution is to simply reboot after a failure. + if let BlockInProgressProvenance::Blueprint = &block_in_progress_provenance { + log!( + safe_host, + Benchmarking, + "Estimated ticks: {}", + tick_counter.c + ) + }; + Ok(ComputationResult::RebootNeeded) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::block_storage; + use crate::blueprint::Blueprint; + use crate::blueprint_storage::store_inbox_blueprint; + use crate::blueprint_storage::store_inbox_blueprint_by_number; + use crate::fees::DA_FEE_PER_BYTE; + use crate::fees::MINIMUM_BASE_FEE_PER_GAS; + use crate::inbox::Transaction; + use crate::inbox::TransactionContent; + use crate::inbox::TransactionContent::Ethereum; + use crate::inbox::TransactionContent::EthereumDelayed; + use crate::storage::read_block_in_progress; + use crate::storage::read_last_info_per_level_timestamp; + use crate::storage::{read_transaction_receipt, read_transaction_receipt_status}; + use crate::{retrieve_block_fees, retrieve_chain_id}; + use evm_execution::account_storage::{ + account_path, init_account_storage, EthereumAccountStorage, + }; + use primitive_types::{H160, U256}; + use std::str::FromStr; + use tezos_ethereum::block::BlockFees; + use tezos_ethereum::transaction::{ + TransactionHash, TransactionStatus, TransactionType, TRANSACTION_HASH_SIZE, + }; + use tezos_ethereum::tx_common::EthereumTransactionCommon; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_evm_runtime::runtime::Runtime; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; + + fn blueprint(transactions: Vec) -> Blueprint { + Blueprint { + transactions, + timestamp: Timestamp::from(0i64), + } + } + + fn address_from_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + + fn set_balance( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } + } + + fn get_balance( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + ) -> U256 { + let account = evm_account_storage + .get_or_create(host, &account_path(address).unwrap()) + .unwrap(); + account.balance(host).unwrap() + } + + const DUMMY_CHAIN_ID: U256 = U256::one(); + const DUMMY_BASE_FEE_PER_GAS: u64 = MINIMUM_BASE_FEE_PER_GAS; + const DUMMY_DA_FEE: u64 = DA_FEE_PER_BYTE; + + fn dummy_block_fees() -> BlockFees { + BlockFees::new( + U256::from(DUMMY_BASE_FEE_PER_GAS), + U256::from(DUMMY_BASE_FEE_PER_GAS), + U256::from(DUMMY_DA_FEE), + ) + } + + fn dummy_eth_gen_transaction( + nonce: u64, + type_: TransactionType, + ) -> EthereumTransactionCommon { + let chain_id = Some(DUMMY_CHAIN_ID); + let gas_price = U256::from(DUMMY_BASE_FEE_PER_GAS); + let gas_limit = 21000u64; + + let gas_for_fees = crate::fees::gas_for_fees( + DUMMY_DA_FEE.into(), + DUMMY_BASE_FEE_PER_GAS.into(), + &[], + &[], + ) + .unwrap(); + let gas_limit = gas_limit + gas_for_fees; + + let to = address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"); + let value = U256::from(500000000u64); + let data: Vec = vec![]; + EthereumTransactionCommon::new( + type_, + chain_id, + nonce, + gas_price, + gas_price, + gas_limit, + to, + value, + data, + vec![], + None, + ) + } + + // When updating the dummy txs, you can use the `resign` function to get the updated + // signatures (see bottom of test module) + fn dummy_eth_caller() -> H160 { + H160::from_str("f95abdf6ede4c3703e0e9453771fbee8592d31e9").unwrap() + } + + fn sign_transaction(tx: EthereumTransactionCommon) -> EthereumTransactionCommon { + let private_key = + "e922354a3e5902b5ac474f3ff08a79cff43533826b8f451ae2190b65a9d26158"; + + tx.sign_transaction(private_key.to_string()).unwrap() + } + + fn make_dummy_transaction( + nonce: u64, + type_: TransactionType, + ) -> EthereumTransactionCommon { + let tx = dummy_eth_gen_transaction(nonce, type_); + sign_transaction(tx) + } + + fn dummy_eth_transaction_zero() -> EthereumTransactionCommon { + // corresponding caller's address is 0xf95abdf6ede4c3703e0e9453771fbee8592d31e9 + // private key 0xe922354a3e5902b5ac474f3ff08a79cff43533826b8f451ae2190b65a9d26158 + let nonce = 0; + make_dummy_transaction(nonce, TransactionType::Legacy) + } + + fn dummy_eth_transaction_one() -> EthereumTransactionCommon { + // corresponding caller's address is 0xf95abdf6ede4c3703e0e9453771fbee8592d31e9 + // private key 0xe922354a3e5902b5ac474f3ff08a79cff43533826b8f451ae2190b65a9d26158 + let nonce = 1; + make_dummy_transaction(nonce, TransactionType::Legacy) + } + + fn dummy_eth_transaction_deploy_from_nonce_and_pk( + nonce: u64, + private_key: &str, + ) -> EthereumTransactionCommon { + let gas_price = U256::from(DUMMY_BASE_FEE_PER_GAS); + // gas limit was estimated using Remix on Shanghai network (256,842) + // plus a safety margin for gas accounting discrepancies + let gas_limit = 300_000u64; + let value = U256::zero(); + // corresponding contract is kernel_benchmark/scripts/benchmarks/contracts/storage.sol + let data: Vec = hex::decode("608060405234801561001057600080fd5b5061017f806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80634e70b1dc1461004657806360fe47b1146100645780636d4ce63c14610080575b600080fd5b61004e61009e565b60405161005b91906100d0565b60405180910390f35b61007e6004803603810190610079919061011c565b6100a4565b005b6100886100ae565b60405161009591906100d0565b60405180910390f35b60005481565b8060008190555050565b60008054905090565b6000819050919050565b6100ca816100b7565b82525050565b60006020820190506100e560008301846100c1565b92915050565b600080fd5b6100f9816100b7565b811461010457600080fd5b50565b600081359050610116816100f0565b92915050565b600060208284031215610132576101316100eb565b5b600061014084828501610107565b9150509291505056fea2646970667358221220ec57e49a647342208a1f5c9b1f2049bf1a27f02e19940819f38929bf67670a5964736f6c63430008120033").unwrap(); + + let gas_for_fees = + crate::fees::gas_for_fees(DUMMY_DA_FEE.into(), gas_price, &data, &[]) + .unwrap(); + let gas_limit = gas_limit + gas_for_fees; + + let tx = EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Legacy, + Some(DUMMY_CHAIN_ID), + nonce, + gas_price, + gas_price, + gas_limit, + None, + value, + data, + vec![], + None, + ); + + tx.sign_transaction(private_key.to_string()).unwrap() + } + + fn dummy_eth_transaction_deploy() -> EthereumTransactionCommon { + // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + dummy_eth_transaction_deploy_from_nonce_and_pk( + 0, + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701", + ) + } + + fn store_blueprints(host: &mut Host, blueprints: Vec) { + for (i, blueprint) in blueprints.into_iter().enumerate() { + store_inbox_blueprint_by_number(host, blueprint, U256::from(i)) + .expect("Should have stored blueprint"); + } + } + + fn store_block_fees( + host: &mut Host, + block_fees: &BlockFees, + ) -> anyhow::Result<()> { + storage::store_minimum_base_fee_per_gas( + host, + block_fees.minimum_base_fee_per_gas(), + )?; + storage::store_da_fee(host, block_fees.da_fee_per_byte())?; + Ok(()) + } + + fn produce_block_with_several_valid_txs( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + ) { + let tx_hash_0 = [0; TRANSACTION_HASH_SIZE]; + let tx_hash_1 = [1; TRANSACTION_HASH_SIZE]; + + let transactions = vec![ + Transaction { + tx_hash: tx_hash_0, + content: Ethereum(dummy_eth_transaction_zero()), + }, + Transaction { + tx_hash: tx_hash_1, + content: Ethereum(dummy_eth_transaction_one()), + }, + ]; + + store_blueprints(host, vec![blueprint(transactions)]); + + let sender = dummy_eth_caller(); + set_balance( + host, + evm_account_storage, + &sender, + U256::from(10000000000000000000u64), + ); + store_block_fees(host, &dummy_block_fees()).unwrap(); + + produce( + host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + } + + fn assert_current_block_reading_validity(host: &mut Host) { + match block_storage::read_current(host) { + Ok(_) => (), + Err(e) => { + panic!("Block reading failed: {:?}\n", e) + } + } + } + + #[test] + // Test if the invalid transactions are producing receipts + fn test_invalid_transactions_receipt_status() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + + let invalid_tx = Transaction { + tx_hash, + content: Ethereum(dummy_eth_transaction_zero()), + }; + + let transactions: Vec = vec![invalid_tx]; + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let mut evm_account_storage = init_account_storage().unwrap(); + let sender = dummy_eth_caller(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(30000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + assert!( + read_transaction_receipt_status(&mut host, &tx_hash).is_err(), + "Invalid transaction should not have a receipt" + ); + } + + #[test] + // Test if a valid transaction is producing a receipt with a success status + fn test_valid_transactions_receipt_status() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + + let valid_tx = Transaction { + tx_hash, + content: Ethereum(dummy_eth_transaction_zero()), + }; + + let transactions: Vec = vec![valid_tx]; + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let sender = dummy_eth_caller(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(1_000_000_000_000_000_000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + let status = read_transaction_receipt_status(&mut host, &tx_hash) + .expect("Should have found receipt"); + assert_eq!(TransactionStatus::Success, status); + } + + #[test] + // Test if a valid transaction is producing a receipt with a contract address + fn test_valid_transactions_receipt_contract_address() { + let mut host = MockKernelHost::default(); + + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + let tx = dummy_eth_transaction_deploy(); + assert_eq!( + H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(), + tx.caller().unwrap() + ); + let valid_tx = Transaction { + tx_hash, + content: Ethereum(dummy_eth_transaction_deploy()), + }; + + let transactions: Vec = vec![valid_tx]; + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(5000000000000000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + let receipt = read_transaction_receipt(&mut host, &tx_hash) + .expect("should have found receipt"); + assert_eq!(TransactionStatus::Success, receipt.status); + assert_eq!( + H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(), + receipt.from + ); + assert_eq!( + Some(H160::from_str("d9d427235f5746ffd1d5a0d850e77880a94b668f").unwrap()), + receipt.contract_address + ); + } + + #[test] + // Test if several valid transactions can be performed + fn test_several_valid_transactions() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let mut evm_account_storage = init_account_storage().unwrap(); + + produce_block_with_several_valid_txs(&mut host, &mut evm_account_storage); + + let dest_address = + H160::from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea").unwrap(); + let dest_balance = + get_balance(&mut host, &mut evm_account_storage, &dest_address); + + assert_eq!(dest_balance, U256::from(1000000000u64)) + } + + #[test] + // Test if several valid proposals can produce valid blocks + fn test_several_valid_proposals() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let tx_hash_0 = [0; TRANSACTION_HASH_SIZE]; + let tx_hash_1 = [1; TRANSACTION_HASH_SIZE]; + + let transaction_0 = vec![Transaction { + tx_hash: tx_hash_0, + content: Ethereum(dummy_eth_transaction_zero()), + }]; + + let transaction_1 = vec![Transaction { + tx_hash: tx_hash_1, + content: Ethereum(dummy_eth_transaction_one()), + }]; + + store_blueprints( + &mut host, + vec![blueprint(transaction_0), blueprint(transaction_1)], + ); + + let sender = dummy_eth_caller(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(10000000000000000000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + // Produce block for blueprint containing transaction_0 + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + // Produce block for blueprint containing transaction_1 + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + let dest_address = + H160::from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea").unwrap(); + let dest_balance = + get_balance(&mut host, &mut evm_account_storage, &dest_address); + + assert_eq!(dest_balance, U256::from(1000000000u64)) + } + + #[test] + // Test transfers gas consumption consistency + fn test_cumulative_transfers_gas_consumption() { + let mut host = MockKernelHost::default(); + + let base_gas = U256::from(21000); + let dummy_block_fees = dummy_block_fees(); + let gas_for_fees = crate::fees::gas_for_fees( + dummy_block_fees.da_fee_per_byte(), + dummy_block_fees.base_fee_per_gas(), + &[], + &[], + ) + .unwrap(); + + let tx_hash_0 = [0; TRANSACTION_HASH_SIZE]; + let tx_hash_1 = [1; TRANSACTION_HASH_SIZE]; + + let transactions = vec![ + Transaction { + tx_hash: tx_hash_0, + content: Ethereum(dummy_eth_transaction_zero()), + }, + Transaction { + tx_hash: tx_hash_1, + content: Ethereum(dummy_eth_transaction_one()), + }, + ]; + + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let sender = dummy_eth_caller(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(10000000000000000000u64), + ); + store_block_fees(&mut host, &dummy_block_fees).unwrap(); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + let receipt0 = read_transaction_receipt(&mut host, &tx_hash_0) + .expect("should have found receipt"); + let receipt1 = read_transaction_receipt(&mut host, &tx_hash_1) + .expect("should have found receipt"); + + assert_eq!(receipt0.cumulative_gas_used, base_gas + gas_for_fees); + assert_eq!( + receipt1.cumulative_gas_used, + receipt0.cumulative_gas_used + base_gas + gas_for_fees + ); + } + + #[test] + // Test if we're able to read current block (with a filled queue) after + // a block production + fn test_read_storage_current_block_after_block_production_with_filled_queue() { + let mut host = MockKernelHost::default(); + + let mut evm_account_storage = init_account_storage().unwrap(); + + produce_block_with_several_valid_txs(&mut host, &mut evm_account_storage); + + assert_current_block_reading_validity(&mut host); + } + + #[test] + // Test that the same transaction can not be replayed twice + fn test_replay_attack() { + let mut host = MockKernelHost::default(); + + let tx = Transaction { + tx_hash: [0; TRANSACTION_HASH_SIZE], + content: Ethereum(dummy_eth_transaction_zero()), + }; + + let transactions = vec![tx.clone(), tx]; + store_blueprints( + &mut host, + vec![blueprint(transactions.clone()), blueprint(transactions)], + ); + + let sender = dummy_eth_caller(); + let initial_sender_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + initial_sender_balance, + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + let dest_address = + H160::from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea").unwrap(); + let sender_balance = get_balance(&mut host, &mut evm_account_storage, &sender); + let dest_balance = + get_balance(&mut host, &mut evm_account_storage, &dest_address); + + let expected_dest_balance = U256::from(500000000u64); + let expected_gas = 21000; + let da_fee = crate::fees::da_fee(DUMMY_DA_FEE.into(), &[], &[]); + let expected_fees = dummy_block_fees().base_fee_per_gas() * expected_gas + da_fee; + let expected_sender_balance = + initial_sender_balance - expected_dest_balance - expected_fees; + + assert_eq!(dest_balance, expected_dest_balance); + assert_eq!(sender_balance, expected_sender_balance, "sender balance"); + } + + #[test] + fn test_blocks_are_indexed() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let blocks_index = + block_storage::internal_for_tests::init_blocks_index().unwrap(); + + store_blueprints(&mut host, vec![blueprint(vec![])]); + + let number_of_blocks_indexed = blocks_index.length(&host).unwrap(); + let sender = dummy_eth_caller(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(10000000000000000000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + let new_number_of_blocks_indexed = blocks_index.length(&host).unwrap(); + + let current_block_hash = block_storage::read_current(&mut host) + .unwrap() + .hash + .as_bytes() + .to_vec(); + + assert_eq!(number_of_blocks_indexed + 1, new_number_of_blocks_indexed); + + assert_eq!( + Ok(current_block_hash), + blocks_index.get_value(&host, new_number_of_blocks_indexed - 1) + ); + } + + fn first_block(host: &mut MockHost) -> BlockConstants { + let timestamp = + read_last_info_per_level_timestamp(host).unwrap_or(Timestamp::from(0)); + let timestamp = U256::from(timestamp.as_u64()); + let chain_id = retrieve_chain_id(host); + let block_fees = retrieve_block_fees(host); + assert!(chain_id.is_ok(), "chain_id should be defined"); + assert!(block_fees.is_ok(), "block fees should be defined"); + BlockConstants::first_block( + timestamp, + chain_id.unwrap(), + block_fees.unwrap(), + crate::block::GAS_LIMIT, + H160::zero(), + ) + } + + #[test] + fn test_stop_computation() { + // init host + let mut host = MockKernelHost::default(); + + let block_constants = first_block(&mut host); + let precompiles = precompiles::precompile_set(false); + + //provision sender account + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(10000000000000000000u64), + ); + + // tx is valid because correct nonce and account provisionned + let valid_tx = Transaction { + tx_hash: [0; TRANSACTION_HASH_SIZE], + content: TransactionContent::Ethereum(dummy_eth_transaction_zero()), + }; + let transactions = vec![valid_tx].into(); + + // init block in progress + let mut block_in_progress = BlockInProgress::new( + U256::from(1), + transactions, + block_constants.block_fees.base_fee_per_gas(), + ); + // run is almost full wrt ticks + let limits = Limits::default(); + block_in_progress.estimated_ticks_in_run = limits.maximum_allowed_ticks - 1000; + + // act + let result = compute( + &mut host, + &OutboxQueue::new(&WITHDRAWAL_OUTBOX_QUEUE, u32::MAX).unwrap(), + &mut block_in_progress, + &block_constants, + &precompiles, + &mut evm_account_storage, + None, + &limits, + None, + ) + .expect("Should safely ask for a reboot"); + + // assert + + assert_eq!( + result, + BlockComputationResult::RebootNeeded, + "Should have asked for a reboot" + ); + // block in progress should not have registered any gas or ticks + assert_eq!( + block_in_progress.cumulative_gas, + U256::from(0), + "should not have consumed any gas" + ); + assert_eq!( + block_in_progress.estimated_ticks_in_run, + tick_model::constants::MAX_TICKS - 1000, + "should not have consumed any tick" + ); + assert_eq!( + block_in_progress.estimated_ticks_in_block, 0, + "should not have consumed any tick" + ); + + // the transaction should not have been processed + let dest_address = + H160::from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea").unwrap(); + let sender_balance = get_balance(&mut host, &mut evm_account_storage, &sender); + let dest_balance = + get_balance(&mut host, &mut evm_account_storage, &dest_address); + assert_eq!(sender_balance, U256::from(10000000000000000000u64)); + assert_eq!(dest_balance, U256::from(0u64)) + } + + #[test] + fn invalid_transaction_should_bump_nonce() { + let mut host = MockKernelHost::default(); + + let mut evm_account_storage = init_account_storage().unwrap(); + + let caller = + address_from_str("f95abdf6ede4c3703e0e9453771fbee8592d31e9").unwrap(); + + // Get the balance before the transaction, i.e. 0. + let caller_account = evm_account_storage + .get_or_create(&host, &account_path(&caller).unwrap()) + .unwrap(); + let default_nonce = caller_account.nonce(&host).unwrap(); + assert_eq!(default_nonce, 0, "default nonce should be 0"); + + let tx = dummy_eth_transaction_zero(); + // Ensures the caller has enough balance to pay for the fees, but not + // the transaction itself, otherwise the transaction will not even be + // taken into account. + let fees = U256::from(21000) * tx.gas_limit_with_fees(); + set_balance(&mut host, &mut evm_account_storage, &caller, fees); + + // Prepare a invalid transaction, i.e. with not enough funds. + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + let transaction = Transaction { + tx_hash, + content: Ethereum(tx), + }; + store_blueprints(&mut host, vec![blueprint(vec![transaction])]); + + // Apply the transaction + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + assert!( + read_transaction_receipt(&mut host, &tx_hash).is_err(), + "Transaction is invalid, so should not have a receipt" + ); + + // Nonce should not have been bumped + let nonce = caller_account.nonce(&host).unwrap(); + assert_eq!(nonce, default_nonce, "nonce should not have been bumped"); + } + + /// A blueprint that should produce 1 block with an invalid transaction + fn almost_empty_blueprint() -> Blueprint { + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + + // transaction should be invalid + let tx = Transaction { + tx_hash, + content: Ethereum(dummy_eth_transaction_one()), + }; + + let transactions = vec![tx]; + + blueprint(transactions) + } + + fn check_current_block_number(host: &mut Host, nb: usize) { + let current_nb = block_storage::read_current_number(host) + .expect("Should have manage to check block number"); + assert_eq!(current_nb, U256::from(nb), "Incorrect block number"); + } + + #[test] + fn test_first_blocks() { + let mut host = MockKernelHost::default(); + + // first block should be 0 + let blueprint = almost_empty_blueprint(); + store_inbox_blueprint(&mut host, blueprint).expect("Should store a blueprint"); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 0); + + // second block + let blueprint = almost_empty_blueprint(); + store_inbox_blueprint(&mut host, blueprint).expect("Should store a blueprint"); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 1); + + // third block + let blueprint = almost_empty_blueprint(); + store_inbox_blueprint(&mut host, blueprint).expect("Should store a blueprint"); + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("Empty block should have been produced"); + check_current_block_number(&mut host, 2); + } + + fn hash_from_nonce(nonce: u64) -> TransactionHash { + let nonce = u64::to_le_bytes(nonce); + let mut hash = [0; 32]; + hash[..8].copy_from_slice(&nonce); + hash + } + + const CREATE_LOOP_DATA: &str = "608060405234801561001057600080fd5b506101d0806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630b7d796e14610030575b600080fd5b61004a600480360381019061004591906100c2565b61004c565b005b60005b81811015610083576001600080828254610069919061011e565b92505081905550808061007b90610152565b91505061004f565b5050565b600080fd5b6000819050919050565b61009f8161008c565b81146100aa57600080fd5b50565b6000813590506100bc81610096565b92915050565b6000602082840312156100d8576100d7610087565b5b60006100e6848285016100ad565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101298261008c565b91506101348361008c565b925082820190508082111561014c5761014b6100ef565b5b92915050565b600061015d8261008c565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361018f5761018e6100ef565b5b60018201905091905056fea26469706673582212200cd6584173dbec22eba4ce6cc7cc4e702e00e018d340f84fc0ff197faf980ad264736f6c63430008150033"; + + const LOOP_1300: &str = + "0b7d796e0000000000000000000000000000000000000000000000000000000000000514"; + + const LOOP_4600: &str = + "0b7d796e00000000000000000000000000000000000000000000000000000000000011f8"; + + const LOOP_5800: &str = + "0b7d796e00000000000000000000000000000000000000000000000000000000000016a8"; + + const TEST_SK: &str = + "84e147b8bc36d99cc6b1676318a0635d8febc9f02897b0563ad27358589ee502"; + + const TEST_ADDR: &str = "f0affc80a5f69f4a9a3ee01a640873b6ba53e539"; + + fn create_and_sign_transaction( + data: &str, + nonce: u64, + gas_limit: u64, + to: Option, + secret_key: &str, + ) -> EthereumTransactionCommon { + let data = hex::decode(data).unwrap(); + + let gas_for_fees = crate::fees::gas_for_fees( + DUMMY_DA_FEE.into(), + DUMMY_BASE_FEE_PER_GAS.into(), + &data, + &[], + ) + .unwrap(); + + let unsigned_tx = EthereumTransactionCommon::new( + TransactionType::Eip1559, + Some(DUMMY_CHAIN_ID), + nonce, + U256::from(DUMMY_BASE_FEE_PER_GAS), + U256::from(DUMMY_BASE_FEE_PER_GAS), + gas_limit + gas_for_fees, + to, + U256::zero(), + data, + vec![], + None, + ); + unsigned_tx + .sign_transaction(String::from(secret_key)) + .unwrap() + } + + fn wrap_transaction(nonce: u64, tx: EthereumTransactionCommon) -> Transaction { + Transaction { + tx_hash: hash_from_nonce(nonce), + content: TransactionContent::Ethereum(tx), + } + } + + #[test] + fn test_reboot_many_tx_one_proposal() { + // init host + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + // sanity check: no current block + assert!( + block_storage::read_current_number(&host).is_err(), + "Should not have found current block number" + ); + + //provision sender account + let sender = H160::from_str(TEST_ADDR).unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // These transactions are generated with the loop.sol contract, which are: + // - create the contract + // - call `loop(1200)` + // - call `loop(4600)` + let create_transaction = + create_and_sign_transaction(CREATE_LOOP_DATA, 0, 3_000_000, None, TEST_SK); + let loop_addr: H160 = + evm_execution::utilities::create_address_legacy(&sender, &0); + let loop_1200_tx = + create_and_sign_transaction(LOOP_1300, 1, 900_000, Some(loop_addr), TEST_SK); + let loop_4600_tx = create_and_sign_transaction( + LOOP_4600, + 2, + 2_600_000, + Some(loop_addr), + TEST_SK, + ); + + let proposals = vec![ + wrap_transaction(0, create_transaction), + wrap_transaction(1, loop_1200_tx), + wrap_transaction(2, loop_4600_tx), + ]; + + store_blueprints(&mut host, vec![blueprint(proposals)]); + + host.reboot_left().expect("should be some reboot left"); + + // Set the tick limit to 11bn ticks - 2bn, which is the old limit minus the safety margin. + let limits = Limits { + maximum_allowed_ticks: 9_000_000_000, + ..Limits::default() + }; + + let mut configuration = Configuration { + limits, + ..Configuration::default() + }; + + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + let computation_result = + produce(&mut host, DUMMY_CHAIN_ID, &mut configuration, None, None) + .expect("Should have produced"); + + // test no new block + assert!( + block_storage::read_current_number(&host).is_err(), + "Should not have found current block number" + ); + + // test reboot is set + matches!(computation_result, ComputationResult::RebootNeeded); + } + + #[test] + fn test_reboot_many_tx_many_proposal() { + // init host + let mut host = MockKernelHost::default(); + + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + // sanity check: no current block + assert!( + block_storage::read_current_number(&host).is_err(), + "Should not have found current block number" + ); + //provision sender account + let sender = H160::from_str(TEST_ADDR).unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // These transactions are generated with the loop.sol contract, which are: + // - create the contract + // - call `loop(1200)` + // - call `loop(4600)` + let create_transaction = + create_and_sign_transaction(CREATE_LOOP_DATA, 0, 3_000_000, None, TEST_SK); + let loop_addr: H160 = + evm_execution::utilities::create_address_legacy(&sender, &0); + let loop_1200_tx = + create_and_sign_transaction(LOOP_1300, 1, 900_000, Some(loop_addr), TEST_SK); + let loop_4600_tx = create_and_sign_transaction( + LOOP_4600, + 2, + 2_600_000, + Some(loop_addr), + TEST_SK, + ); + + let proposals = vec![ + blueprint(vec![wrap_transaction(0, create_transaction)]), + blueprint(vec![ + wrap_transaction(1, loop_1200_tx), + wrap_transaction(2, loop_4600_tx), + ]), + ]; + + store_blueprints(&mut host, proposals); + + // Set the tick limit to 11bn ticks - 2bn, which is the old limit minus the safety margin. + let limits = Limits { + maximum_allowed_ticks: 9_000_000_000, + ..Limits::default() + }; + + let mut configuration = Configuration { + limits, + ..Configuration::default() + }; + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + let computation_result = + produce(&mut host, DUMMY_CHAIN_ID, &mut configuration, None, None) + .expect("Should have produced"); + // test reboot is set + matches!(computation_result, ComputationResult::RebootNeeded); + + let computation_result = + produce(&mut host, DUMMY_CHAIN_ID, &mut configuration, None, None) + .expect("Should have produced"); + + // test no new block + assert_eq!( + block_storage::read_current_number(&host) + .expect("should have found a block number"), + U256::zero(), + "There should have been one block registered" + ); + + // test reboot is set again + matches!(computation_result, ComputationResult::RebootNeeded); + + // The block is in progress, therefore it is in the safe storage. + let safe_host = SafeStorage { host: &mut host }; + let bip = read_block_in_progress(&safe_host) + .expect("Should be able to read the block in progress") + .expect("The reboot context should have a block in progress"); + + assert!( + bip.queue_length() > 0, + "There should be some transactions left" + ); + + let _next_blueprint = + read_next_blueprint(&mut host, &mut Configuration::default()) + .expect("The next blueprint should be available"); + } + + #[test] + fn test_transaction_pre_eip155() { + // This test injects a presigned transaction defined by + // https://github.com/mds1/multicall#new-deployments, which is a well + // known contract on multiple EVM chains. The transaction has been + // signed without a chain id, so that it can be reproduced on multiple + // networks (and the contract address is the same on any chain). + // + // The purpose of this test is to check the kernel accepts such a + // transaction, with the expected hash and the expected contract + // address. + + // init host + let mut host = MockKernelHost::default(); + + // see + // https://basescan.org/tx/0x07471adfe8f4ec553c1199f495be97fc8be8e0626ae307281c22534460184ed1 + // for example, as the transaction has the same hash on every EVM Chain. + let expected_tx_hash = hex::decode( + "07471adfe8f4ec553c1199f495be97fc8be8e0626ae307281c22534460184ed1", + ) + .unwrap(); + + // Extracted from https://github.com/mds1/multicall#new-deployments + let signed_transaction = hex::decode("f90f538085174876e800830f42408080b90f00608060405234801561001057600080fd5b50610ee0806100206000396000f3fe6080604052600436106100f35760003560e01c80634d2301cc1161008a578063a8b0574e11610059578063a8b0574e1461025a578063bce38bd714610275578063c3077fa914610288578063ee82ac5e1461029b57600080fd5b80634d2301cc146101ec57806372425d9d1461022157806382ad56cb1461023457806386d516e81461024757600080fd5b80633408e470116100c65780633408e47014610191578063399542e9146101a45780633e64a696146101c657806342cbb15c146101d957600080fd5b80630f28c97d146100f8578063174dea711461011a578063252dba421461013a57806327e86d6e1461015b575b600080fd5b34801561010457600080fd5b50425b6040519081526020015b60405180910390f35b61012d610128366004610a85565b6102ba565b6040516101119190610bbe565b61014d610148366004610a85565b6104ef565b604051610111929190610bd8565b34801561016757600080fd5b50437fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff0140610107565b34801561019d57600080fd5b5046610107565b6101b76101b2366004610c60565b610690565b60405161011193929190610cba565b3480156101d257600080fd5b5048610107565b3480156101e557600080fd5b5043610107565b3480156101f857600080fd5b50610107610207366004610ce2565b73ffffffffffffffffffffffffffffffffffffffff163190565b34801561022d57600080fd5b5044610107565b61012d610242366004610a85565b6106ab565b34801561025357600080fd5b5045610107565b34801561026657600080fd5b50604051418152602001610111565b61012d610283366004610c60565b61085a565b6101b7610296366004610a85565b610a1a565b3480156102a757600080fd5b506101076102b6366004610d18565b4090565b60606000828067ffffffffffffffff8111156102d8576102d8610d31565b60405190808252806020026020018201604052801561031e57816020015b6040805180820190915260008152606060208201528152602001906001900390816102f65790505b5092503660005b8281101561047757600085828151811061034157610341610d60565b6020026020010151905087878381811061035d5761035d610d60565b905060200281019061036f9190610d8f565b6040810135958601959093506103886020850185610ce2565b73ffffffffffffffffffffffffffffffffffffffff16816103ac6060870187610dcd565b6040516103ba929190610e32565b60006040518083038185875af1925050503d80600081146103f7576040519150601f19603f3d011682016040523d82523d6000602084013e6103fc565b606091505b50602080850191909152901515808452908501351761046d577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260846000fd5b5050600101610325565b508234146104e6576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601a60248201527f4d756c746963616c6c333a2076616c7565206d69736d6174636800000000000060448201526064015b60405180910390fd5b50505092915050565b436060828067ffffffffffffffff81111561050c5761050c610d31565b60405190808252806020026020018201604052801561053f57816020015b606081526020019060019003908161052a5790505b5091503660005b8281101561068657600087878381811061056257610562610d60565b90506020028101906105749190610e42565b92506105836020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166105a66020850185610dcd565b6040516105b4929190610e32565b6000604051808303816000865af19150503d80600081146105f1576040519150601f19603f3d011682016040523d82523d6000602084013e6105f6565b606091505b5086848151811061060957610609610d60565b602090810291909101015290508061067d576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b50600101610546565b5050509250929050565b43804060606106a086868661085a565b905093509350939050565b6060818067ffffffffffffffff8111156106c7576106c7610d31565b60405190808252806020026020018201604052801561070d57816020015b6040805180820190915260008152606060208201528152602001906001900390816106e55790505b5091503660005b828110156104e657600084828151811061073057610730610d60565b6020026020010151905086868381811061074c5761074c610d60565b905060200281019061075e9190610e76565b925061076d6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff166107906040850185610dcd565b60405161079e929190610e32565b6000604051808303816000865af19150503d80600081146107db576040519150601f19603f3d011682016040523d82523d6000602084013e6107e0565b606091505b506020808401919091529015158083529084013517610851577f08c379a000000000000000000000000000000000000000000000000000000000600052602060045260176024527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060445260646000fd5b50600101610714565b6060818067ffffffffffffffff81111561087657610876610d31565b6040519080825280602002602001820160405280156108bc57816020015b6040805180820190915260008152606060208201528152602001906001900390816108945790505b5091503660005b82811015610a105760008482815181106108df576108df610d60565b602002602001015190508686838181106108fb576108fb610d60565b905060200281019061090d9190610e42565b925061091c6020840184610ce2565b73ffffffffffffffffffffffffffffffffffffffff1661093f6020850185610dcd565b60405161094d929190610e32565b6000604051808303816000865af19150503d806000811461098a576040519150601f19603f3d011682016040523d82523d6000602084013e61098f565b606091505b506020830152151581528715610a07578051610a07576040517f08c379a000000000000000000000000000000000000000000000000000000000815260206004820152601760248201527f4d756c746963616c6c333a2063616c6c206661696c656400000000000000000060448201526064016104dd565b506001016108c3565b5050509392505050565b6000806060610a2b60018686610690565b919790965090945092505050565b60008083601f840112610a4b57600080fd5b50813567ffffffffffffffff811115610a6357600080fd5b6020830191508360208260051b8501011115610a7e57600080fd5b9250929050565b60008060208385031215610a9857600080fd5b823567ffffffffffffffff811115610aaf57600080fd5b610abb85828601610a39565b90969095509350505050565b6000815180845260005b81811015610aed57602081850181015186830182015201610ad1565b81811115610aff576000602083870101525b50601f017fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe0169290920160200192915050565b600082825180855260208086019550808260051b84010181860160005b84811015610bb1578583037fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe001895281518051151584528401516040858501819052610b9d81860183610ac7565b9a86019a9450505090830190600101610b4f565b5090979650505050505050565b602081526000610bd16020830184610b32565b9392505050565b600060408201848352602060408185015281855180845260608601915060608160051b870101935082870160005b82811015610c52577fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa0888703018452610c40868351610ac7565b95509284019290840190600101610c06565b509398975050505050505050565b600080600060408486031215610c7557600080fd5b83358015158114610c8557600080fd5b9250602084013567ffffffffffffffff811115610ca157600080fd5b610cad86828701610a39565b9497909650939450505050565b838152826020820152606060408201526000610cd96060830184610b32565b95945050505050565b600060208284031215610cf457600080fd5b813573ffffffffffffffffffffffffffffffffffffffff81168114610bd157600080fd5b600060208284031215610d2a57600080fd5b5035919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052603260045260246000fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff81833603018112610dc357600080fd5b9190910192915050565b60008083357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffe1843603018112610e0257600080fd5b83018035915067ffffffffffffffff821115610e1d57600080fd5b602001915036819003821315610a7e57600080fd5b8183823760009101908152919050565b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc1833603018112610dc357600080fd5b600082357fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffa1833603018112610dc357600080fdfea2646970667358221220bb2b5c71a328032f97c676ae39a1ec2148d3e5d6f73d95e9b17910152d61f16264736f6c634300080c00331ca0edce47092c0f398cebf3ffc267f05c8e7076e3b89445e0fe50f6332273d4569ba01b0b9d000e19b24c5869b0fc3b22b0d6fa47cd63316875cbbd577d76e6fde086").unwrap(); + + let transaction = EthereumTransactionCommon::from_bytes(&signed_transaction) + .expect("The MultiCall3 transaction shouldn't be unparsable"); + + let mut tx_hash = [0; TRANSACTION_HASH_SIZE]; + tx_hash.copy_from_slice(&expected_tx_hash); + + // *NB*: due to the da fee, this will fail by default - so we inject it through the + // delayed inbox instead, so that it doesn't pay the da fee. + let tx = Transaction { + tx_hash, + content: EthereumDelayed(transaction), + }; + + let transactions: Vec = vec![tx]; + + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let sender = H160::from_str("05f32b3cc3888453ff71b01135b34ff8e41263f2").unwrap(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(1_000_000_000_000_000_000u64), + ); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + // See address at https://www.multicall3.com/ on in the github repository linked above + let expected_created_contract = + H160::from_str("0xcA11bde05977b3631167028862bE2a173976CA11").unwrap(); + + let receipt = read_transaction_receipt(&mut host, &tx_hash) + .expect("Should have found receipt"); + assert_eq!(TransactionStatus::Success, receipt.status); + assert_eq!(Some(expected_created_contract), receipt.contract_address); + } + + #[test] + fn test_non_retriable_transaction_are_marked_as_failed() { + // init host + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + //provision sender account + let sender = H160::from_str(TEST_ADDR).unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // These transactions are generated with the loop.sol contract, which are: + // - create the contract + // - call `loop(1200)` + // - call `loop(5800)` + let create_transaction = + create_and_sign_transaction(CREATE_LOOP_DATA, 0, 3_000_000, None, TEST_SK); + let loop_addr: H160 = + evm_execution::utilities::create_address_legacy(&sender, &0); + let loop_5800_tx = create_and_sign_transaction( + LOOP_5800, + 1, + 4_000_000, + Some(loop_addr), + TEST_SK, + ); + + let proposals_first_reboot = vec![wrap_transaction(0, create_transaction)]; + + store_inbox_blueprint(&mut host, blueprint(proposals_first_reboot)).unwrap(); + + // Set the tick limit to 11bn ticks - 2bn, which is the old limit minus the safety margin. + let limits = Limits { + maximum_allowed_ticks: 9_000_000_000, + ..Limits::default() + }; + + let mut configuration = Configuration { + limits, + ..Configuration::default() + }; + // sanity check: no current block + assert!( + block_storage::read_current_number(&host).is_err(), + "Should not have found current block number" + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + produce(&mut host, DUMMY_CHAIN_ID, &mut configuration, None, None) + .expect("Should have produced"); + + assert!( + block_storage::read_current_number(&host).is_ok(), + "Should have found a block" + ); + + // We start a new proposal, calling produce again simulates a reboot. + + let proposals_second_reboot = vec![wrap_transaction(2, loop_5800_tx)]; + + store_inbox_blueprint(&mut host, blueprint(proposals_second_reboot)).unwrap(); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + produce(&mut host, DUMMY_CHAIN_ID, &mut configuration, None, None) + .expect("Should have produced"); + + let block = + block_storage::read_current(&mut host).expect("Should have found a block"); + let failed_loop_hash = block + .transactions + .first() + .expect("There should have been a transaction"); + let failed_loop_status = + storage::read_transaction_receipt_status(&mut host, failed_loop_hash) + .expect("There should have been a receipt"); + + assert_eq!( + failed_loop_status, + TransactionStatus::Failure, + "The transaction should have failed" + ) + } + + // Comment out `ignore` when resigning the dummy transactions + #[test] + #[ignore = "Only run when re-signing required"] + fn resign() { + println!("Dummy eth transactions"); + // corresponding caller's address is 0xf95abdf6ede4c3703e0e9453771fbee8592d31e9 + let private_key = + "e922354a3e5902b5ac474f3ff08a79cff43533826b8f451ae2190b65a9d26158"; + + let zero = dummy_eth_transaction_zero(); + let one = dummy_eth_transaction_one(); + + let zero_resigned = zero.sign_transaction(private_key.to_string()).unwrap(); + let one_resigned = one.sign_transaction(private_key.to_string()).unwrap(); + + assert_eq!(zero, zero_resigned); + assert_eq!(one, one_resigned); + } + + #[test] + // Test if a valid transaction is producing a receipt with a success status + fn test_type_propagation() { + let mut host = MockKernelHost::default(); + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + let tx_hash_eip1559 = [1; TRANSACTION_HASH_SIZE]; + let tx_hash_eip2930 = [2; TRANSACTION_HASH_SIZE]; + + let valid_tx = Transaction { + tx_hash, + content: Ethereum(make_dummy_transaction(0, TransactionType::Legacy)), + }; + + let valid_tx_eip1559 = Transaction { + tx_hash: tx_hash_eip1559, + content: Ethereum(make_dummy_transaction(1, TransactionType::Eip1559)), + }; + + let valid_tx_eip2930 = Transaction { + tx_hash: tx_hash_eip2930, + content: Ethereum(make_dummy_transaction(2, TransactionType::Eip2930)), + }; + + let transactions: Vec = + vec![valid_tx, valid_tx_eip1559, valid_tx_eip2930]; + store_blueprints(&mut host, vec![blueprint(transactions)]); + + let sender = dummy_eth_caller(); + let mut evm_account_storage = init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + U256::from(1_000_000_000_000_000_000u64), + ); + store_block_fees(&mut host, &dummy_block_fees()).unwrap(); + + produce( + &mut host, + DUMMY_CHAIN_ID, + &mut Configuration::default(), + None, + None, + ) + .expect("The block production failed."); + + let receipt = read_transaction_receipt(&mut host, &tx_hash) + .expect("Should have found receipt"); + assert_eq!(receipt.type_, TransactionType::Legacy); + + let receipt_eip1559 = read_transaction_receipt(&mut host, &tx_hash_eip1559) + .expect("Should have found receipt"); + assert_eq!(receipt_eip1559.type_, TransactionType::Eip1559); + + let receipt_eip2930 = read_transaction_receipt(&mut host, &tx_hash_eip2930) + .expect("Should have found receipt"); + assert_eq!(receipt_eip2930.type_, TransactionType::Eip2930); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/block_in_progress.rs b/etherlink/kernel_calypso2/kernel/src/block_in_progress.rs new file mode 100644 index 000000000000..00452ee4dae2 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/block_in_progress.rs @@ -0,0 +1,715 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +use crate::apply::{TransactionObjectInfo, TransactionReceiptInfo}; +use crate::block_storage; +use crate::error::Error; +use crate::error::TransferError::CumulativeGasUsedOverflow; +use crate::gas_price::base_fee_per_gas; +use crate::inbox::Transaction; +use crate::storage::{self, object_path, receipt_path}; +use crate::tick_model; +use anyhow::Context; +use evm_execution::account_storage::EVM_ACCOUNTS_PATH; +use primitive_types::{H160, H256, U256}; +use rlp::{Decodable, DecoderError, Encodable}; +use std::collections::VecDeque; +use tezos_ethereum::block::{BlockConstants, BlockFees, L2Block}; +use tezos_ethereum::rlp_helpers::*; +use tezos_ethereum::transaction::{ + IndexedLog, TransactionHash, TransactionObject, TransactionReceipt, + TransactionStatus, TRANSACTION_HASH_SIZE, +}; +use tezos_ethereum::Bloom; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::timestamp::Timestamp; +use tezos_smart_rollup_host::path::{concat, RefPath}; + +#[derive(Debug, PartialEq, Clone)] +/// Container for all data needed during block computation +pub struct BlockInProgress { + /// block number + pub number: U256, + /// queue containing the transactions to execute + tx_queue: VecDeque, + /// list of transactions executed without issue + valid_txs: Vec, + pub delayed_txs: Vec, + /// gas accumulator + pub cumulative_gas: U256, + /// index for next transaction + pub index: u32, + /// hash of the parent + pub parent_hash: H256, + /// Cumulative number of ticks used in current kernel run + pub estimated_ticks_in_run: u64, + /// Cumulative number of ticks used in the block + pub estimated_ticks_in_block: u64, + /// logs bloom filter + pub logs_bloom: Bloom, + /// offset for the first log of the next transaction + pub logs_offset: u64, + /// Timestamp + pub timestamp: Timestamp, + /// The base fee, is adjusted before and after the computation of + /// the block + pub base_fee_per_gas: U256, +} + +impl Encodable for BlockInProgress { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let BlockInProgress { + number, + tx_queue, + valid_txs, + delayed_txs, + cumulative_gas, + index, + parent_hash, + estimated_ticks_in_run: _, + estimated_ticks_in_block, + logs_bloom, + logs_offset, + timestamp, + base_fee_per_gas, + } = self; + stream.begin_list(12); + stream.append(number); + append_queue(stream, tx_queue); + append_txs(stream, valid_txs); + append_txs(stream, delayed_txs); + stream.append(cumulative_gas); + stream.append(index); + stream.append(parent_hash); + stream.append(estimated_ticks_in_block); + stream.append(logs_bloom); + stream.append(logs_offset); + append_timestamp(stream, *timestamp); + stream.append(base_fee_per_gas); + } +} + +fn append_queue(stream: &mut rlp::RlpStream, queue: &VecDeque) { + stream.begin_list(queue.len()); + for transaction in queue { + stream.append(transaction); + } +} + +fn append_txs(stream: &mut rlp::RlpStream, valid_txs: &[[u8; TRANSACTION_HASH_SIZE]]) { + stream.begin_list(valid_txs.len()); + valid_txs.iter().for_each(|tx| { + stream.append_iter(*tx); + }) +} + +impl Decodable for BlockInProgress { + fn decode(decoder: &rlp::Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 12 { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let number: U256 = decode_field(&next(&mut it)?, "number")?; + let tx_queue: VecDeque = decode_queue(&next(&mut it)?)?; + let valid_txs: Vec = decode_valid_txs(&next(&mut it)?)?; + let delayed_txs: Vec = decode_valid_txs(&next(&mut it)?)?; + let cumulative_gas: U256 = decode_field(&next(&mut it)?, "cumulative_gas")?; + let index: u32 = decode_field(&next(&mut it)?, "index")?; + let parent_hash: H256 = decode_field(&next(&mut it)?, "parent_hash")?; + let estimated_ticks_in_block: u64 = + decode_field(&next(&mut it)?, "estimated_ticks_in_block")?; + let logs_bloom: Bloom = decode_field(&next(&mut it)?, "logs_bloom")?; + let logs_offset: u64 = decode_field(&next(&mut it)?, "logs_offset")?; + let timestamp = decode_timestamp(&next(&mut it)?)?; + let base_fee_per_gas = decode_field(&next(&mut it)?, "base_fee_per_gas")?; + let bip = Self { + number, + tx_queue, + valid_txs, + delayed_txs, + cumulative_gas, + index, + parent_hash, + estimated_ticks_in_run: 0, + estimated_ticks_in_block, + logs_bloom, + logs_offset, + timestamp, + base_fee_per_gas, + }; + Ok(bip) + } +} + +fn decode_valid_txs( + decoder: &rlp::Rlp<'_>, +) -> Result, DecoderError> { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + let mut valid_txs = Vec::with_capacity(decoder.item_count()?); + for item in decoder.iter() { + let tx = decode_tx_hash(item)?; + valid_txs.push(tx); + } + Ok(valid_txs) +} + +fn decode_queue(decoder: &rlp::Rlp<'_>) -> Result, DecoderError> { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + let mut queue = VecDeque::with_capacity(decoder.item_count()?); + for item in decoder.iter() { + let tx: Transaction = item.as_val()?; + queue.push_back(tx); + } + Ok(queue) +} + +impl BlockInProgress { + pub fn queue_length(&self) -> usize { + self.tx_queue.len() + } + + pub fn new_with_ticks( + number: U256, + parent_hash: H256, + transactions: VecDeque, + estimated_ticks_in_run: u64, + timestamp: Timestamp, + base_fee_per_gas: U256, + ) -> Self { + Self { + number, + tx_queue: transactions, + valid_txs: Vec::new(), + delayed_txs: Vec::new(), + cumulative_gas: U256::zero(), + index: 0, + parent_hash, + estimated_ticks_in_block: 0, + estimated_ticks_in_run, + logs_bloom: Bloom::default(), + logs_offset: 0, + timestamp, + base_fee_per_gas, + } + } + + // constructor of raw structure, used in tests + #[cfg(test)] + pub fn new( + number: U256, + transactions: VecDeque, + base_fee_per_gas: U256, + ) -> BlockInProgress { + Self::new_with_ticks( + number, + H256::zero(), + transactions, + 0u64, + Timestamp::from(0i64), + base_fee_per_gas, + ) + } + + /// Derive `BlockConstants` based on current block in progress. + /// Number and timestamp are taken from `self`. + pub fn constants( + &self, + chain_id: U256, + minimum_base_fee_per_gas: U256, + da_fee_per_byte: U256, + gas_limit: u64, + coinbase: H160, + ) -> BlockConstants { + let timestamp = U256::from(self.timestamp.as_u64()); + let block_fees = BlockFees::new( + minimum_base_fee_per_gas, + self.base_fee_per_gas, + da_fee_per_byte, + ); + BlockConstants { + number: self.number, + coinbase, + timestamp, + gas_limit, + block_fees, + chain_id, + prevrandao: None, + } + } + + pub fn from_blueprint( + blueprint: crate::blueprint::Blueprint, + current_block_number: U256, + parent_hash: H256, + tick_counter: u64, + base_fee_per_gas: U256, + ) -> BlockInProgress { + // blueprint is turn into a ring to allow popping from the front + let ring = blueprint.transactions.into(); + BlockInProgress::new_with_ticks( + current_block_number, + parent_hash, + ring, + tick_counter, + blueprint.timestamp, + base_fee_per_gas, + ) + } + + fn add_gas(&mut self, gas: U256) -> Result<(), Error> { + self.cumulative_gas = self + .cumulative_gas + .checked_add(gas) + .ok_or(Error::Transfer(CumulativeGasUsedOverflow))?; + Ok(()) + } + + fn add_ticks(&mut self, ticks: u64) { + self.estimated_ticks_in_run += ticks; + self.estimated_ticks_in_block += ticks; + } + + pub fn register_delayed_transaction(&mut self, hash: TransactionHash) { + self.delayed_txs.push(hash); + } + + pub fn register_valid_transaction( + &mut self, + transaction: &Transaction, + object_info: TransactionObjectInfo, + receipt_info: TransactionReceiptInfo, + ticks_used: u64, + host: &mut Host, + ) -> Result<(), anyhow::Error> { + // account for gas + let Some(gas_used) = receipt_info + .execution_outcome + .as_ref() + .map(|eo| eo.gas_used) + else { + anyhow::bail!( + "No execution outcome on valid transaction 0x{}", + hex::encode(transaction.tx_hash) + ); + }; + host.add_execution_gas(gas_used); + + self.add_gas(receipt_info.overall_gas_used)?; + + // account for transaction ticks + self.add_ticks(tick_model::ticks_of_valid_transaction( + transaction, + ticks_used, + )); + + // register transaction as done + self.valid_txs.push(transaction.tx_hash); + self.index += 1; + + // make receipt + let receipt = self.make_receipt(receipt_info); + let receipt_bloom_size: u64 = tick_model::bloom_size(&receipt.logs).try_into()?; + log!(host, Benchmarking, "bloom size: {}", receipt_bloom_size); + // extend BIP's logs bloom + self.logs_bloom.accrue_bloom(&receipt.logs_bloom); + + // store info + let receipt_size = storage::store_transaction_receipt(host, &receipt) + .context("Failed to store the receipt")?; + let obj_size = + storage::store_transaction_object(host, &self.make_object(object_info)) + .context("Failed to store the transaction object")?; + + // account for registering ticks + self.add_ticks(tick_model::ticks_of_register( + receipt_size, + obj_size, + receipt_bloom_size, + )); + + Ok(()) + } + + pub fn account_for_invalid_transaction(&mut self, tx_data_size: u64) { + self.add_ticks(tick_model::ticks_of_invalid_transaction(tx_data_size)); + } + + fn safe_store_get_hash( + host: &mut Host, + path: &RefPath, + ) -> Result, anyhow::Error> { + match host.store_get_hash(path) { + Ok(hash) => Ok(hash), + _ => Ok("00000000000000000000000000000000".into()), + } + } + + const RECEIPTS: RefPath<'static> = RefPath::assert_from(b"/receipts"); + const RECEIPTS_PREVIOUS_ROOT: RefPath<'static> = + RefPath::assert_from(b"/receipts/previous_root"); + + fn receipts_root( + &self, + host: &mut impl Runtime, + previous_receipts_root: Vec, + ) -> anyhow::Result> { + if self.valid_txs.is_empty() { + Ok(previous_receipts_root) + } else { + for hash in &self.valid_txs { + let receipt_path = receipt_path(hash)?; + let new_receipt_path = concat(&Self::RECEIPTS, &receipt_path)?; + host.store_copy(&receipt_path, &new_receipt_path)?; + } + host.store_write_all(&Self::RECEIPTS_PREVIOUS_ROOT, &previous_receipts_root)?; + let receipts_root = Self::safe_store_get_hash(host, &Self::RECEIPTS)?; + host.store_delete(&Self::RECEIPTS)?; + Ok(receipts_root) + } + } + + const OBJECTS: RefPath<'static> = RefPath::assert_from(b"/objects"); + const OBJECTS_PREVIOUS_ROOT: RefPath<'static> = + RefPath::assert_from(b"/objects/previous_root"); + + fn transactions_root( + &self, + host: &mut impl Runtime, + previous_transactions_root: Vec, + ) -> anyhow::Result> { + if self.valid_txs.is_empty() { + Ok(previous_transactions_root) + } else { + for hash in &self.valid_txs { + let object_path = object_path(hash)?; + let new_object_path = concat(&Self::OBJECTS, &object_path)?; + host.store_copy(&object_path, &new_object_path)?; + } + host.store_write_all( + &Self::OBJECTS_PREVIOUS_ROOT, + &previous_transactions_root, + )?; + let objects_root = Self::safe_store_get_hash(host, &Self::OBJECTS)?; + host.store_delete(&Self::OBJECTS)?; + Ok(objects_root) + } + } + + #[cfg_attr(feature = "benchmark", inline(never))] + pub fn finalize_and_store( + self, + host: &mut Host, + block_constants: &BlockConstants, + previous_receipts_root: Vec, + previous_transactions_root: Vec, + ) -> Result { + let state_root = Self::safe_store_get_hash(host, &EVM_ACCOUNTS_PATH)?; + let receipts_root = self.receipts_root(host, previous_receipts_root)?; + let transactions_root = + self.transactions_root(host, previous_transactions_root)?; + let base_fee_per_gas = base_fee_per_gas( + host, + self.timestamp, + block_constants.block_fees.minimum_base_fee_per_gas(), + ); + let new_block = L2Block::new( + self.number, + self.valid_txs, + self.timestamp, + self.parent_hash, + self.logs_bloom, + transactions_root, + state_root, + receipts_root, + self.cumulative_gas, + block_constants, + base_fee_per_gas, + ); + block_storage::store_current(host, &new_block) + .context("Failed to store the current block")?; + Ok(new_block) + } + + pub fn pop_tx(&mut self) -> Option { + self.tx_queue.pop_front() + } + + pub fn repush_tx(&mut self, tx: Transaction) { + self.tx_queue.push_front(tx) + } + + pub fn has_tx(&self) -> bool { + !self.tx_queue.is_empty() + } + + pub fn make_receipt( + &mut self, + receipt_info: TransactionReceiptInfo, + ) -> TransactionReceipt { + let TransactionReceiptInfo { + tx_hash: hash, + index, + caller: from, + to, + execution_outcome, + effective_gas_price, + type_, + .. + } = receipt_info; + + let &mut Self { + number: block_number, + cumulative_gas, + logs_offset, + .. + } = self; + + match execution_outcome { + Some(outcome) => { + let is_success = outcome.is_success(); + let contract_address = outcome.new_address(); + let log_iter = outcome.logs.into_iter(); + let logs: Vec = log_iter + .enumerate() + .map(|(i, log)| IndexedLog { + log, + index: i as u64 + logs_offset, + }) + .collect(); + self.logs_offset += logs.len() as u64; + TransactionReceipt { + hash, + index, + block_number, + from, + to, + cumulative_gas_used: cumulative_gas, + effective_gas_price, + gas_used: receipt_info.overall_gas_used, + contract_address, + logs_bloom: TransactionReceipt::logs_to_bloom(&logs), + logs, + type_, + status: if is_success { + TransactionStatus::Success + } else { + TransactionStatus::Failure + }, + } + } + None => TransactionReceipt { + hash, + index, + block_number, + from, + to, + cumulative_gas_used: cumulative_gas, + effective_gas_price, + gas_used: U256::zero(), + contract_address: None, + logs: vec![], + logs_bloom: Bloom::default(), + type_, + status: TransactionStatus::Failure, + }, + } + } + + pub fn make_object(&self, object_info: TransactionObjectInfo) -> TransactionObject { + TransactionObject { + block_number: self.number, + from: object_info.from, + gas_used: object_info.gas, + gas_price: object_info.gas_price, + hash: object_info.hash, + input: object_info.input, + nonce: object_info.nonce, + to: object_info.to, + index: object_info.index, + value: object_info.value, + signature: object_info.signature, + } + } +} + +#[cfg(test)] +mod tests { + + use super::BlockInProgress; + use crate::bridge::Deposit; + use crate::inbox::{Transaction, TransactionContent}; + use primitive_types::{H160, H256, U256}; + use rlp::{Decodable, Encodable, Rlp}; + use tezos_ethereum::{ + transaction::{TransactionType, TRANSACTION_HASH_SIZE}, + tx_common::EthereumTransactionCommon, + tx_signature::TxSignature, + Bloom, + }; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + + fn new_sig_unsafe(v: u64, r: H256, s: H256) -> TxSignature { + TxSignature::new(U256::from(v), r, s).unwrap() + } + + fn dummy_etc(i: u8) -> EthereumTransactionCommon { + EthereumTransactionCommon::new( + TransactionType::Legacy, + Some(U256::from(i)), + u64::from(i), + U256::from(i), + U256::from(i), + i.into(), + None, + U256::from(i), + Vec::new(), + vec![], + Some(new_sig_unsafe( + (36 + i * 2).into(), // need to be consistent with chain_id + H256::from([i; 32]), + H256::from([i; 32]), + )), + ) + } + + fn dummy_tx_eth(i: u8) -> Transaction { + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: TransactionContent::Ethereum(dummy_etc(i)), + } + } + + fn dummy_tx_deposit(i: u8) -> Transaction { + let deposit = Deposit { + amount: U256::from(i), + receiver: H160::from([i; 20]), + inbox_level: 1, + inbox_msg_id: 0, + }; + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: TransactionContent::Deposit(deposit), + } + } + + #[test] + fn test_encode_bip_ethereum() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_eth(1), dummy_tx_eth(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + delayed_txs: vec![], + cumulative_gas: U256::from(3), + index: 4, + parent_hash: H256::from([5; 32]), + estimated_ticks_in_block: 99, + estimated_ticks_in_run: 199, + logs_bloom: Bloom::default(), + logs_offset: 33, + timestamp: Timestamp::from(0i64), + base_fee_per_gas: U256::from(21000u64), + }; + + let encoded = bip.rlp_bytes(); + let expected = "f902622af8e6f871a00101010101010101010101010101010101010101010101010101010101010101f84e01b84bf84901010180018026a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f871a00808080808080808080808080808080808080808080808080808080808080808f84e01b84bf84908080880088034a00808080808080808080808080808080808080808080808080808080808080808a00808080808080808080808080808080808080808080808080808080808080808f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909c00304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000021880000000000000000825208"; + + pretty_assertions::assert_str_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks in the current run are not stored + let fresh_bip = BlockInProgress { + estimated_ticks_in_run: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } + + #[test] + fn test_encode_bip_deposit() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_deposit(1), dummy_tx_deposit(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + delayed_txs: vec![[2; TRANSACTION_HASH_SIZE]], + cumulative_gas: U256::from(3), + index: 4, + parent_hash: H256::from([5; 32]), + estimated_ticks_in_block: 99, + estimated_ticks_in_run: 199, + logs_bloom: Bloom::default(), + logs_offset: 0, + timestamp: Timestamp::from(0i64), + base_fee_per_gas: U256::from(21000u64), + }; + + let encoded = bip.rlp_bytes(); + let expected = "f902192af87cf83ca00101010101010101010101010101010101010101010101010101010101010101da02d8019401010101010101010101010101010101010101010180f83ca00808080808080808080808080808080808080808080808080808080808080808da02d8089408080808080808080808080808080808080808080180f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909e1a002020202020202020202020202020202020202020202020202020202020202020304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000080880000000000000000825208"; + + pretty_assertions::assert_str_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks in the current run are not stored + let fresh_bip = BlockInProgress { + estimated_ticks_in_run: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } + + #[test] + fn test_encode_bip_mixed() { + let bip = BlockInProgress { + number: U256::from(42), + tx_queue: vec![dummy_tx_eth(1), dummy_tx_deposit(8)].into(), + valid_txs: vec![[2; TRANSACTION_HASH_SIZE], [9; TRANSACTION_HASH_SIZE]], + delayed_txs: vec![], + cumulative_gas: U256::from(3), + index: 4, + parent_hash: H256::from([5; 32]), + estimated_ticks_in_block: 99, + estimated_ticks_in_run: 199, + logs_bloom: Bloom::default(), + logs_offset: 4, + timestamp: Timestamp::from(0i64), + base_fee_per_gas: U256::from(21000u64), + }; + + let encoded = bip.rlp_bytes(); + let expected = "f9022d2af8b1f871a00101010101010101010101010101010101010101010101010101010101010101f84e01b84bf84901010180018026a00101010101010101010101010101010101010101010101010101010101010101a00101010101010101010101010101010101010101010101010101010101010101f83ca00808080808080808080808080808080808080808080808080808080808080808da02d8089408080808080808080808080808080808080808080180f842a00202020202020202020202020202020202020202020202020202020202020202a00909090909090909090909090909090909090909090909090909090909090909c00304a0050505050505050505050505050505050505050505050505050505050505050563b901000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000004880000000000000000825208"; + + pretty_assertions::assert_str_eq!(hex::encode(encoded), expected); + + let bytes = hex::decode(expected).expect("Should be valid hex string"); + let decoder = Rlp::new(&bytes); + let decoded = + BlockInProgress::decode(&decoder).expect("Should have decoded data"); + + // the estimated ticks in the run are not stored + let fresh_bip = BlockInProgress { + estimated_ticks_in_run: 0, + ..bip + }; + assert_eq!(decoded, fresh_bip); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/block_storage.rs b/etherlink/kernel_calypso2/kernel/src/block_storage.rs new file mode 100644 index 000000000000..f090c86b40f9 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/block_storage.rs @@ -0,0 +1,141 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use primitive_types::{H256, U256}; +use tezos_ethereum::block::L2Block; +use tezos_ethereum::rlp_helpers::VersionedEncoding; +use tezos_evm_logging::{ + log, + Level::{Debug, Info}, +}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_indexable_storage::IndexableStorage; +use tezos_smart_rollup_host::path::concat; +use tezos_smart_rollup_host::path::OwnedPath; +use tezos_smart_rollup_host::path::RefPath; +use tezos_storage::{read_h256_be, read_u256_le, write_h256_be, write_u256_le}; + +use crate::migration::allow_path_not_found; +use crate::storage::EVM_TRANSACTIONS_OBJECTS; +use crate::storage::EVM_TRANSACTIONS_RECEIPTS; + +mod path { + use super::*; + + pub const PATH: RefPath = RefPath::assert_from(b"/evm/world_state/blocks"); + + pub const CURRENT_NUMBER: RefPath = + RefPath::assert_from(b"/evm/world_state/blocks/current/number"); + pub const CURRENT_HASH: RefPath = + RefPath::assert_from(b"/evm/world_state/blocks/current/hash"); + + pub const INDEXES: RefPath = RefPath::assert_from(b"/evm/world_state/indexes/blocks"); + + /// Path to the block in the storage. The path to the block is + /// indexed by its hash. + pub fn path(hash: H256) -> anyhow::Result { + let hash = hex::encode(hash); + let raw_hash_path: Vec = format!("/{}", &hash).into(); + let hash_path = OwnedPath::try_from(raw_hash_path)?; + Ok(concat(&PATH, &hash_path)?) + } +} + +fn store_current_number(host: &mut impl Runtime, number: U256) -> anyhow::Result<()> { + Ok(write_u256_le(host, &path::CURRENT_NUMBER, number)?) +} + +fn store_current_hash(host: &mut impl Runtime, hash: H256) -> anyhow::Result<()> { + write_h256_be(host, &path::CURRENT_HASH, hash) +} + +fn store_block( + host: &mut impl Runtime, + block: &L2Block, + index_block: bool, +) -> anyhow::Result<()> { + if index_block { + // Index the block, /evm/world_state/indexes/blocks/ points to + // the block hash. + let index = IndexableStorage::new(&path::INDEXES)?; + index.push_value(host, block.hash.as_bytes())?; + } + let path = path::path(block.hash)?; + let bytes = block.to_bytes(); + Ok(host.store_write_all(&path, &bytes)?) +} + +fn store_current_index_or_not( + host: &mut impl Runtime, + block: &L2Block, + index_block: bool, +) -> anyhow::Result<()> { + store_current_number(host, block.number)?; + store_current_hash(host, block.hash)?; + store_block(host, block, index_block)?; + log!( + host, + Info, + "Storing block {} at {} containing {} transaction(s) for {} gas used.", + block.number, + block.timestamp, + block.transactions.len(), + U256::to_string(&block.gas_used) + ); + Ok(()) +} + +pub fn store_current(host: &mut impl Runtime, block: &L2Block) -> anyhow::Result<()> { + store_current_index_or_not(host, block, true) +} + +pub fn restore_current(host: &mut impl Runtime, block: &L2Block) -> anyhow::Result<()> { + store_current_index_or_not(host, block, false) +} + +pub fn read_current_number(host: &impl Runtime) -> anyhow::Result { + Ok(read_u256_le(host, &path::CURRENT_NUMBER)?) +} + +pub fn read_current_hash(host: &impl Runtime) -> anyhow::Result { + read_h256_be(host, &path::CURRENT_HASH) +} + +pub fn read_current(host: &mut impl Runtime) -> anyhow::Result { + let hash = read_current_hash(host)?; + let block_path = path::path(hash)?; + let bytes = &host.store_read_all(&block_path)?; + let block_from_bytes = L2Block::from_bytes(bytes)?; + Ok(block_from_bytes) +} + +pub fn garbage_collect_blocks(host: &mut impl Runtime) -> anyhow::Result<()> { + log!(host, Debug, "Garbage collecting blocks."); + if let Ok(block) = read_current(host) { + // The kernel needs the current block to process the next one. Therefore + // we garbage collect everything but the current block. + host.store_delete(&path::PATH)?; + restore_current(host, &block)?; + // Clean all transactions, they are unused by the kernel. + allow_path_not_found(host.store_delete(&EVM_TRANSACTIONS_OBJECTS))?; + allow_path_not_found(host.store_delete(&EVM_TRANSACTIONS_RECEIPTS))?; + } + Ok(()) +} + +#[cfg(test)] +pub mod internal_for_tests { + use super::*; + + pub fn init_blocks_index() -> anyhow::Result { + Ok(IndexableStorage::new(&path::INDEXES)?) + } + + pub fn store_current_number( + host: &mut impl Runtime, + number: U256, + ) -> anyhow::Result<()> { + super::store_current_number(host, number) + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/blueprint.rs b/etherlink/kernel_calypso2/kernel/src/blueprint.rs new file mode 100644 index 000000000000..4b5fd2a928e4 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/blueprint.rs @@ -0,0 +1,99 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use crate::inbox::Transaction; +use rlp::{Decodable, DecoderError, Encodable}; +use tezos_ethereum::rlp_helpers::{self, append_timestamp, decode_timestamp}; + +use tezos_smart_rollup_encoding::timestamp::Timestamp; + +/// The blueprint of a block is a list of transactions. +#[derive(PartialEq, Debug, Clone)] +pub struct Blueprint { + pub transactions: Vec, + pub timestamp: Timestamp, +} + +impl Encodable for Blueprint { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append_list(&self.transactions); + append_timestamp(stream, self.timestamp); + } +} + +impl Decodable for Blueprint { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let transactions = + rlp_helpers::decode_list(&rlp_helpers::next(&mut it)?, "transactions")?; + let timestamp = decode_timestamp(&rlp_helpers::next(&mut it)?)?; + + Ok(Blueprint { + transactions, + timestamp, + }) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::inbox::TransactionContent::Ethereum; + use primitive_types::{H160, U256}; + use rlp::Rlp; + use tezos_ethereum::{ + transaction::TRANSACTION_HASH_SIZE, tx_common::EthereumTransactionCommon, + }; + + fn address_from_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + fn tx_(i: u64) -> EthereumTransactionCommon { + EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Legacy, + Some(U256::one()), + i, + U256::from(40000000u64), + U256::from(40000000u64), + 21000u64, + address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"), + U256::from(500000000u64), + vec![], + vec![], + None, + ) + } + + fn dummy_transaction(i: u8) -> Transaction { + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: Ethereum(tx_(i.into())), + } + } + + #[test] + fn test_encode_blueprint() { + let proposal = Blueprint { + transactions: vec![dummy_transaction(0), dummy_transaction(1)], + timestamp: Timestamp::from(0i64), + }; + let encoded = proposal.rlp_bytes(); + let decoder = Rlp::new(&encoded); + let decoded = Blueprint::decode(&decoder).expect("Should be decodable"); + assert_eq!(decoded, proposal); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/blueprint_storage.rs b/etherlink/kernel_calypso2/kernel/src/blueprint_storage.rs new file mode 100644 index 000000000000..fc94fdf31f45 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/blueprint_storage.rs @@ -0,0 +1,660 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 TriliTech +// +// SPDX-License-Identifier: MIT + +use crate::block::GENESIS_PARENT_HASH; +use crate::block_storage; +use crate::blueprint::Blueprint; +use crate::configuration::{Configuration, ConfigurationMode}; +use crate::error::{Error, StorageError}; +use crate::inbox::{Transaction, TransactionContent}; +use crate::sequencer_blueprint::{ + BlueprintWithDelayedHashes, UnsignedSequencerBlueprint, +}; +use crate::storage::read_last_info_per_level_timestamp; +use crate::{delayed_inbox, DelayedInbox}; +use primitive_types::U256; +use rlp::{Decodable, DecoderError, Encodable}; +use sha3::{Digest, Keccak256}; +use tezos_ethereum::rlp_helpers; +use tezos_ethereum::tx_common::EthereumTransactionCommon; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::types::Timestamp; +use tezos_smart_rollup_core::MAX_INPUT_MESSAGE_SIZE; +use tezos_smart_rollup_host::path::*; +use tezos_smart_rollup_host::runtime::RuntimeError; +use tezos_storage::{ + error::Error as GenStorageError, read_rlp, store_read_slice, store_rlp, +}; + +pub const EVM_BLUEPRINTS: RefPath = RefPath::assert_from(b"/evm/blueprints"); + +const EVM_BLUEPRINT_NB_CHUNKS: RefPath = RefPath::assert_from(b"/nb_chunks"); + +/// The store representation of a blueprint. +/// It's designed to support storing sequencer blueprints, +/// which can be chunked, and blueprints constructed from +/// inbox messages. Note that the latter are only to be +/// used when the kernel isn't running with a sequencer. +#[derive(PartialEq, Debug, Clone)] +enum StoreBlueprint { + SequencerChunk(Vec), + InboxBlueprint(Blueprint), +} + +const SEQUENCER_CHUNK_TAG: u8 = 0; +const INBOX_BLUEPRINT_TAG: u8 = 1; + +impl Encodable for StoreBlueprint { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match &self { + StoreBlueprint::SequencerChunk(chunk) => { + stream.append(&SEQUENCER_CHUNK_TAG); + stream.append(chunk); + } + StoreBlueprint::InboxBlueprint(blueprint) => { + stream.append(&INBOX_BLUEPRINT_TAG); + stream.append(blueprint); + } + } + } +} + +impl Decodable for StoreBlueprint { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let tag: u8 = decoder.at(0)?.as_val()?; + let rest = decoder.at(1)?; + match tag { + SEQUENCER_CHUNK_TAG => { + let chunk: Vec = rest.as_val()?; + Ok(Self::SequencerChunk(chunk)) + } + INBOX_BLUEPRINT_TAG => { + let blueprint = rlp_helpers::decode_field(&rest, "blueprint")?; + Ok(Self::InboxBlueprint(blueprint)) + } + _ => Err(DecoderError::Custom("Unknown store blueprint tag.")), + } + } +} + +pub fn blueprint_path(number: U256) -> Result { + let number_as_path: Vec = format!("/{}", number).into(); + // The key being an integer value, it will always be valid as a path, + // `assert_from` cannot fail. + let number_subkey = RefPath::assert_from(&number_as_path); + concat(&EVM_BLUEPRINTS, &number_subkey).map_err(StorageError::from) +} + +fn blueprint_chunk_path( + blueprint_path: &OwnedPath, + chunk_index: u16, +) -> Result { + let chunk_index_as_path: Vec = format!("/{}", chunk_index).into(); + let chunk_index_subkey = RefPath::assert_from(&chunk_index_as_path); + concat(blueprint_path, &chunk_index_subkey).map_err(StorageError::from) +} + +fn blueprint_nb_chunks_path( + blueprint_path: &OwnedPath, +) -> Result { + concat(blueprint_path, &EVM_BLUEPRINT_NB_CHUNKS).map_err(StorageError::from) +} + +fn read_blueprint_nb_chunks( + host: &Host, + blueprint_path: &OwnedPath, +) -> Result { + let path = blueprint_nb_chunks_path(blueprint_path)?; + let mut buffer = [0u8; 2]; + store_read_slice(host, &path, &mut buffer, 2)?; + Ok(u16::from_le_bytes(buffer)) +} + +fn store_blueprint_nb_chunks( + host: &mut Host, + blueprint_path: &OwnedPath, + nb_chunks: u16, +) -> Result<(), Error> { + let path = blueprint_nb_chunks_path(blueprint_path)?; + let bytes = nb_chunks.to_le_bytes(); + host.store_write_all(&path, &bytes).map_err(Error::from) +} + +pub fn store_sequencer_blueprint( + host: &mut Host, + blueprint: UnsignedSequencerBlueprint, +) -> Result<(), Error> { + let blueprint_path = blueprint_path(blueprint.number)?; + store_blueprint_nb_chunks(host, &blueprint_path, blueprint.nb_chunks)?; + let blueprint_chunk_path = + blueprint_chunk_path(&blueprint_path, blueprint.chunk_index)?; + let store_blueprint = StoreBlueprint::SequencerChunk(blueprint.chunk); + store_rlp(&store_blueprint, host, &blueprint_chunk_path).map_err(Error::from) +} + +pub fn store_inbox_blueprint_by_number( + host: &mut Host, + blueprint: Blueprint, + number: U256, +) -> Result<(), Error> { + let blueprint_path = blueprint_path(number)?; + store_blueprint_nb_chunks(host, &blueprint_path, 1)?; + let chunk_path = blueprint_chunk_path(&blueprint_path, 0)?; + let store_blueprint = StoreBlueprint::InboxBlueprint(blueprint); + store_rlp(&store_blueprint, host, &chunk_path).map_err(Error::from) +} + +pub fn store_inbox_blueprint( + host: &mut Host, + blueprint: Blueprint, +) -> anyhow::Result<()> { + let number = read_next_blueprint_number(host)?; + Ok(store_inbox_blueprint_by_number(host, blueprint, number)?) +} + +#[inline(always)] +pub fn read_next_blueprint_number(host: &Host) -> anyhow::Result { + match block_storage::read_current_number(host) { + Err(err) => match err.downcast_ref() { + Some(GenStorageError::Runtime(RuntimeError::PathNotFound)) => { + Ok(U256::zero()) + } + _ => Err(err), + }, + Ok(block_number) => Ok(block_number.saturating_add(U256::one())), + } +} + +// Used to store a blueprint made out of forced delayed transactions. +pub fn store_forced_blueprint( + host: &mut Host, + blueprint: Blueprint, + number: U256, +) -> Result<(), Error> { + let blueprint_path = blueprint_path(number)?; + store_blueprint_nb_chunks(host, &blueprint_path, 1)?; + let chunk_path = blueprint_chunk_path(&blueprint_path, 0)?; + let store_blueprint = StoreBlueprint::InboxBlueprint(blueprint); + store_rlp(&store_blueprint, host, &chunk_path).map_err(Error::from) +} + +/// For the tick model we only accept blueprints where cumulative size of chunks +/// less or equal than 512kB. A chunk weights 4kB, then (512 * 1024) / 4096 = +/// 128. +pub const MAXIMUM_NUMBER_OF_CHUNKS: u16 = 128; + +const MAXIMUM_SIZE_OF_BLUEPRINT: usize = + MAXIMUM_NUMBER_OF_CHUNKS as usize * MAX_INPUT_MESSAGE_SIZE; + +const MAXIMUM_SIZE_OF_DELAYED_TRANSACTION: usize = MAX_INPUT_MESSAGE_SIZE; + +/// Possible errors when validating a blueprint +/// Only used for test, as all errors are handled in the same way +#[cfg_attr(feature = "benchmark", allow(dead_code))] +#[derive(Debug, PartialEq)] +pub enum BlueprintValidity { + Valid(Blueprint), + InvalidParentHash, + TimestampFromPast, + TimestampFromFuture, + DecoderError(DecoderError), + DelayedHashMissing(delayed_inbox::Hash), + BlueprintTooLarge, +} + +fn fetch_delayed_txs( + host: &mut Host, + blueprint_with_hashes: BlueprintWithDelayedHashes, + delayed_inbox: &mut DelayedInbox, + current_blueprint_size: usize, +) -> anyhow::Result<(BlueprintValidity, usize)> { + let mut delayed_txs = vec![]; + let mut total_size = current_blueprint_size; + for tx_hash in blueprint_with_hashes.delayed_hashes { + let tx = delayed_inbox.find_transaction(host, tx_hash)?; + // This is overestimated, as the transactions cannot be chunked in the + // delayed bridge. + total_size += MAXIMUM_SIZE_OF_DELAYED_TRANSACTION; + // If the size would overflow the 512KB, reject the blueprint + if MAXIMUM_SIZE_OF_BLUEPRINT < total_size { + return Ok((BlueprintValidity::BlueprintTooLarge, total_size)); + } + match tx { + Some(tx) => delayed_txs.push(tx.0), + None => { + return Ok((BlueprintValidity::DelayedHashMissing(tx_hash), total_size)) + } + } + } + + let transactions_with_hashes = blueprint_with_hashes + .transactions + .into_iter() + .map(|tx_common| { + let tx_hash = Keccak256::digest(&tx_common).into(); + let tx_common = EthereumTransactionCommon::from_bytes(&tx_common)?; + + Ok(Transaction { + tx_hash, + content: TransactionContent::Ethereum(tx_common), + }) + }) + .collect::>>()?; + + delayed_txs.extend(transactions_with_hashes); + Ok(( + BlueprintValidity::Valid(Blueprint { + transactions: delayed_txs, + timestamp: blueprint_with_hashes.timestamp, + }), + total_size, + )) +} + +// Default value is 5 minutes. The rationale for 5 minutes is that we have +// only the timestamp from the predecessor block, and we want the rollup to +// accept blocks even if the chain is impacted by high rounds (e.g. > 10). +// The predecessor block timestamp can be completely off and we do not +// wish to refuse such blueprints. +pub const DEFAULT_MAX_BLUEPRINT_LOOKAHEAD_IN_SECONDS: i64 = 300i64; + +fn parse_and_validate_blueprint( + host: &mut Host, + bytes: &[u8], + delayed_inbox: &mut DelayedInbox, + current_blueprint_size: usize, + evm_node_flag: bool, + max_blueprint_lookahead_in_seconds: i64, +) -> anyhow::Result<(BlueprintValidity, usize)> { + // Decode + match rlp::decode::(bytes) { + Err(e) => Ok((BlueprintValidity::DecoderError(e), bytes.len())), + Ok(blueprint_with_hashes) => { + let head = block_storage::read_current(host); + let (head_hash, head_timestamp) = match head { + Ok(block) => (block.hash, block.timestamp), + Err(_) => (GENESIS_PARENT_HASH, Timestamp::from(0)), + }; + + // Validate parent hash + #[cfg(not(feature = "benchmark"))] + if head_hash != blueprint_with_hashes.parent_hash { + return Ok((BlueprintValidity::InvalidParentHash, bytes.len())); + } + + // Validate parent timestamp + #[cfg(not(feature = "benchmark"))] + if blueprint_with_hashes.timestamp < head_timestamp { + return Ok((BlueprintValidity::TimestampFromPast, bytes.len())); + } + + // The timestamp must be within max_blueprint_lookahead_in_seconds + // of the current view of the L1 timestamp. + // + // That means that the sequencer cannot produce blueprints too much + // in the future. If the L1 timestamp is not progressing i.e. + // the network is stuck, the sequencer will have to reinject the + // blueprint when the L1 timestamp is finally greater than + // blueprint timestamp. + // + // All this prevents the sequencer to manipulate too much the + // timestamps. + #[cfg(not(feature = "benchmark"))] + { + let last_seen_l1_timestamp = read_last_info_per_level_timestamp(host)?; + let accepted_bound = Timestamp::from( + last_seen_l1_timestamp + .i64() + .saturating_add(max_blueprint_lookahead_in_seconds), + ); + + // In the sequencer we don't have a valid `last_seen_l1_timesteamp` + // so it must not fails on this. + if !evm_node_flag && blueprint_with_hashes.timestamp > accepted_bound { + log!( + host, + Debug, + "Accepted bound is {}, Blueprint.timestamp is {}", + accepted_bound, + blueprint_with_hashes.timestamp + ); + return Ok((BlueprintValidity::TimestampFromFuture, bytes.len())); + } + } + + // Fetch delayed transactions + fetch_delayed_txs( + host, + blueprint_with_hashes, + delayed_inbox, + current_blueprint_size, + ) + } + } +} + +fn invalidate_blueprint( + host: &mut Host, + blueprint_path: &OwnedPath, + error: &BlueprintValidity, +) -> Result<(), RuntimeError> { + log!( + host, + Info, + "Deleting invalid blueprint at path {}, error: {:?}", + blueprint_path, + error + ); + // Remove invalid blueprint from storage + host.store_delete(blueprint_path) +} + +fn read_all_chunks_and_validate( + host: &mut Host, + blueprint_path: &OwnedPath, + nb_chunks: u16, + config: &mut Configuration, +) -> anyhow::Result<(Option, usize)> { + let mut chunks = vec![]; + let mut size = 0; + if nb_chunks > MAXIMUM_NUMBER_OF_CHUNKS { + invalidate_blueprint( + host, + blueprint_path, + &BlueprintValidity::BlueprintTooLarge, + )?; + return Ok((None, 0)); + }; + for i in 0..nb_chunks { + let path = blueprint_chunk_path(blueprint_path, i)?; + let stored_chunk = read_rlp(host, &path)?; + // The tick model is based on the size of the chunk, we overapproximate it. + size += MAX_INPUT_MESSAGE_SIZE; + match stored_chunk { + StoreBlueprint::InboxBlueprint(blueprint) => { + // Special case when there's an inbox blueprint stored. + // There must be only one chunk in this case. + return Ok((Some(blueprint), size)); + } + StoreBlueprint::SequencerChunk(chunk) => chunks.push(chunk), + } + } + match &mut config.mode { + ConfigurationMode::Proxy => Ok((None, size)), + ConfigurationMode::Sequencer { + delayed_inbox, + evm_node_flag, + max_blueprint_lookahead_in_seconds, + .. + } => { + let validity = parse_and_validate_blueprint( + host, + chunks.concat().as_slice(), + delayed_inbox, + size, + *evm_node_flag, + *max_blueprint_lookahead_in_seconds, + )?; + if let (BlueprintValidity::Valid(blueprint), size_with_delayed_transactions) = + validity + { + log!( + host, + Benchmarking, + "Number of transactions in blueprint: {}", + blueprint.transactions.len() + ); + Ok((Some(blueprint), size_with_delayed_transactions)) + } else { + invalidate_blueprint(host, blueprint_path, &validity.0)?; + Ok((None, size)) + } + } + } +} + +pub fn read_next_blueprint( + host: &mut Host, + config: &mut Configuration, +) -> anyhow::Result<(Option, usize)> { + let number = read_next_blueprint_number(host)?; + let blueprint_path = blueprint_path(number)?; + let exists = host.store_has(&blueprint_path)?.is_some(); + if exists { + let nb_chunks = read_blueprint_nb_chunks(host, &blueprint_path)?; + log!( + host, + Benchmarking, + "Number of chunks in blueprint: {}", + nb_chunks + ); + let n_subkeys = host.store_count_subkeys(&blueprint_path)?; + let available_chunks = n_subkeys as u16 - 1; + if available_chunks == nb_chunks { + // All chunks are available + let (blueprint, size) = + read_all_chunks_and_validate(host, &blueprint_path, nb_chunks, config)?; + Ok((blueprint, size)) + } else { + if available_chunks > nb_chunks { + // We are in an inconsistent state (a previous blueprint was submitted with more + // chunks). + // As-is, the rollup is blocked. Easiest way to recover is to delete the whole + // blueprint and let the sequencer re-submit it later. + host.store_delete(&blueprint_path).map_err(Error::from)?; + } + + Ok((None, 0)) + } + } else { + log!(host, Benchmarking, "Number of chunks in blueprint: {}", 0); + log!( + host, + Benchmarking, + "Number of transactions in blueprint: {}", + 0 + ); + Ok((None, 0)) + } +} + +pub fn drop_blueprint(host: &mut Host, number: U256) -> Result<(), Error> { + let path = blueprint_path(number)?; + host.store_delete(&path).map_err(Error::from) +} + +pub fn clear_all_blueprints(host: &mut Host) -> Result<(), Error> { + if host.store_has(&EVM_BLUEPRINTS)?.is_some() { + Ok(host.store_delete(&EVM_BLUEPRINTS)?) + } else { + Ok(()) + } +} + +#[cfg(test)] +mod tests { + + use super::*; + use crate::configuration::{DalConfiguration, Limits, TezosContracts}; + use crate::delayed_inbox::Hash; + use crate::storage::store_last_info_per_level_timestamp; + use primitive_types::H256; + use tezos_crypto_rs::hash::ContractKt1Hash; + use tezos_ethereum::transaction::TRANSACTION_HASH_SIZE; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_encoding::public_key::PublicKey; + use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; // Used to put traits interface in the scope + + fn test_invalid_sequencer_blueprint_is_removed(enable_dal: bool) { + let mut host = MockKernelHost::default(); + let delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + let delayed_bridge: ContractKt1Hash = + ContractKt1Hash::from_base58_check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(); + let sequencer: PublicKey = PublicKey::from_b58check( + "edpkuDMUm7Y53wp4gxeLBXuiAhXZrLn8XB1R83ksvvesH8Lp8bmCfK", + ) + .unwrap(); + let dal = if enable_dal { + Some(DalConfiguration { + slot_indices: vec![5], + }) + } else { + None + }; + let mut config = Configuration { + tezos_contracts: TezosContracts::default(), + mode: ConfigurationMode::Sequencer { + delayed_bridge, + delayed_inbox: Box::new(delayed_inbox), + sequencer, + dal, + evm_node_flag: false, + max_blueprint_lookahead_in_seconds: 100_000i64, + }, + limits: Limits::default(), + enable_fa_bridge: false, + garbage_collect_blocks: false, + }; + + let dummy_tx_hash = Hash([0u8; TRANSACTION_HASH_SIZE]); + let dummy_parent_hash = H256::from_slice( + &hex::decode( + "ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff", + ) + .unwrap(), + ); + + let blueprint_with_invalid_hash: BlueprintWithDelayedHashes = + BlueprintWithDelayedHashes { + delayed_hashes: vec![dummy_tx_hash], + parent_hash: dummy_parent_hash, + timestamp: Timestamp::from(42), + transactions: vec![], + }; + let blueprint_with_hashes_bytes = + rlp::Encodable::rlp_bytes(&blueprint_with_invalid_hash); + + let seq_blueprint = UnsignedSequencerBlueprint { + chunk: blueprint_with_hashes_bytes.clone().into(), + number: U256::from(0), + nb_chunks: 1u16, + chunk_index: 0u16, + chain_id: None, + }; + + store_last_info_per_level_timestamp(&mut host, Timestamp::from(40)).unwrap(); + + let mut delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + // Blueprint should have invalid parent hash + let validity = parse_and_validate_blueprint( + &mut host, + blueprint_with_hashes_bytes.as_ref(), + &mut delayed_inbox, + 0, + false, + 500, + ) + .expect("Should be able to parse blueprint"); + assert_eq!( + validity.0, + BlueprintValidity::DelayedHashMissing(dummy_tx_hash) + ); + + // Store blueprint + store_sequencer_blueprint(&mut host, seq_blueprint) + .expect("Should be able to store sequencer blueprint"); + + // Blueprint 0 should be stored + let blueprint_path = blueprint_path(U256::zero()).unwrap(); + let exists = host.store_has(&blueprint_path).unwrap().is_some(); + assert!(exists); + + // Reading the next blueprint should be None, as the delayed hash + // isn't in the delayed inbox + let blueprint = read_next_blueprint(&mut host, &mut config) + .expect("Reading next blueprint should work"); + assert!(blueprint.0.is_none()); + + // Next number should be 0, as we didn't read one + let number = read_next_blueprint_number(&host) + .expect("Should be able to read next blueprint number"); + assert!(number.is_zero()); + + // The blueprint 0 should have been removed + let exists = host.store_has(&blueprint_path).unwrap().is_some(); + assert!(!exists); + + // Test with invalid parent hash + let blueprint_with_invalid_parent_hash: BlueprintWithDelayedHashes = + BlueprintWithDelayedHashes { + delayed_hashes: vec![], + parent_hash: H256::zero(), + timestamp: Timestamp::from(42), + transactions: vec![], + }; + let blueprint_with_hashes_bytes = + rlp::Encodable::rlp_bytes(&blueprint_with_invalid_parent_hash); + + let seq_blueprint = UnsignedSequencerBlueprint { + chunk: blueprint_with_hashes_bytes.clone().into(), + number: U256::from(0), + nb_chunks: 1u16, + chunk_index: 0u16, + chain_id: None, + }; + + let mut delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + // Blueprint should have invalid parent hash + let validity = parse_and_validate_blueprint( + &mut host, + blueprint_with_hashes_bytes.as_ref(), + &mut delayed_inbox, + 0, + false, + 500, + ) + .expect("Should be able to parse blueprint"); + assert_eq!(validity.0, BlueprintValidity::InvalidParentHash); + + // Store blueprint + store_sequencer_blueprint(&mut host, seq_blueprint) + .expect("Should be able to store sequencer blueprint"); + // Blueprint 0 should be stored + let exists = host.store_has(&blueprint_path).unwrap().is_some(); + assert!(exists); + + // Reading the next blueprint should be None, as the parent hash + // is invalid + let blueprint = read_next_blueprint(&mut host, &mut config) + .expect("Reading next blueprint should work"); + assert!(blueprint.0.is_none()); + + // The blueprint 0 should have been removed + let exists = host.store_has(&blueprint_path).unwrap().is_some(); + assert!(!exists) + } + + #[test] + fn test_invalid_sequencer_blueprint_is_removed_without_dal() { + test_invalid_sequencer_blueprint_is_removed(false) + } + + #[test] + fn test_invalid_sequencer_blueprint_is_removed_with_dal() { + test_invalid_sequencer_blueprint_is_removed(true) + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/bridge.rs b/etherlink/kernel_calypso2/kernel/src/bridge.rs new file mode 100644 index 000000000000..5d705cbd5820 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/bridge.rs @@ -0,0 +1,390 @@ +// SPDX-FileCopyrightText: 2022-2023 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Native token (TEZ) bridge primitives and helpers. + +use std::borrow::Cow; + +use ethereum::Log; +use evm::{Config, ExitError}; +use evm_execution::{ + abi::{ABI_H160_LEFT_PADDING, ABI_U32_LEFT_PADDING}, + account_storage::{account_path, AccountStorageError, EthereumAccountStorage}, + handler::{ExecutionOutcome, ExecutionResult}, + EthereumError, +}; +use primitive_types::{H160, H256, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpEncodable}; +use sha3::{Digest, Keccak256}; +use tezos_ethereum::{ + rlp_helpers::{decode_field, next}, + wei::eth_from_mutez, +}; +use tezos_evm_logging::{log, Level::Info}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::michelson::{ticket::FA2_1Ticket, MichelsonBytes}; + +use crate::tick_model; + +/// Keccak256 of Deposit(uint256,address,uint256,uint256) +/// This is main topic (non-anonymous event): https://docs.soliditylang.org/en/latest/abi-spec.html#events +pub const DEPOSIT_EVENT_TOPIC: [u8; 32] = [ + 211, 106, 47, 103, 208, 109, 40, 87, 134, 246, 26, 50, 176, 82, 185, 172, 230, 176, + 183, 171, 239, 81, 119, 181, 67, 88, 171, 220, 131, 160, 182, 155, +]; + +/// Native token bridge error +#[derive(Debug, thiserror::Error)] +pub enum BridgeError { + #[error("Invalid deposit receiver address: {0:?}")] + InvalidDepositReceiver(Vec), +} + +/// Native token deposit +#[derive(Debug, PartialEq, Clone, RlpEncodable)] +pub struct Deposit { + /// Deposit amount in wei + pub amount: U256, + /// Deposit receiver on L2 + pub receiver: H160, + /// Inbox level containing the original deposit message + pub inbox_level: u32, + /// Inbox message id + pub inbox_msg_id: u32, +} + +impl Deposit { + const RECEIVER_LENGTH: usize = std::mem::size_of::(); + + const RECEIVER_AND_CHAIN_ID_LENGTH: usize = + Self::RECEIVER_LENGTH + std::mem::size_of::(); + + fn parse_receiver( + input: MichelsonBytes, + ) -> Result<(H160, Option), BridgeError> { + let input_bytes = input.0; + let input_length = input_bytes.len(); + if input_length == Self::RECEIVER_LENGTH { + // Legacy format, input is exactly the receiver EVM address + let receiver = H160::from_slice(&input_bytes); + Ok((receiver, None)) + } else if input_length == Self::RECEIVER_AND_CHAIN_ID_LENGTH { + // input is receiver followed by chain id + let receiver = H160::from_slice(&input_bytes[..Self::RECEIVER_LENGTH]); + let chain_id = + U256::from_little_endian(&input_bytes[Self::RECEIVER_LENGTH..]); + Ok((receiver, Some(chain_id))) + } else { + Err(BridgeError::InvalidDepositReceiver(input_bytes)) + } + } + + /// Parses a deposit from a ticket transfer (internal inbox message). + /// The "entrypoint" type is pair of ticket (FA2.1) and bytes (receiver address). + #[cfg_attr(feature = "benchmark", inline(never))] + pub fn try_parse( + ticket: FA2_1Ticket, + receiver: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + ) -> Result<(Self, Option), BridgeError> { + // Amount + let (_sign, amount_bytes) = ticket.amount().to_bytes_le(); + // We use the `U256::from_little_endian` as it takes arbitrary long + // bytes. Afterward it's transform to `u64` to use `eth_from_mutez`, it's + // obviously safe as we deposit CTEZ and the amount is limited by + // the XTZ quantity. + let amount_mutez: u64 = U256::from_little_endian(&amount_bytes).as_u64(); + let amount: U256 = eth_from_mutez(amount_mutez); + + // EVM address of the receiver and chain id both come from the + // Michelson byte parameter. + let (receiver, chain_id) = Self::parse_receiver(receiver)?; + + Ok(( + Self { + amount, + receiver, + inbox_level, + inbox_msg_id, + }, + chain_id, + )) + } + + /// Returns log structure for an implicit deposit event. + /// + /// This event is added to the outer transaction receipt, + /// so that we can index successful deposits and update status. + /// + /// Signature: Deposit(uint256,address,uint256,uint256) + pub fn event_log(&self) -> Log { + let mut data = Vec::with_capacity(4 * 32); + + data.extend_from_slice(&Into::<[u8; 32]>::into(self.amount)); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_H160_LEFT_PADDING); + data.extend_from_slice(&self.receiver.0); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_level.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + data.extend_from_slice(&ABI_U32_LEFT_PADDING); + data.extend_from_slice(&self.inbox_msg_id.to_be_bytes()); + debug_assert!(data.len() % 32 == 0); + + Log { + // Emitted by the "system" contract + address: H160::zero(), + // Event ID (non-anonymous) and indexed fields + topics: vec![H256(DEPOSIT_EVENT_TOPIC)], + // Non-indexed fields + data, + } + } + + /// Returns unique deposit digest that can be used as hash for the + /// pseudo transaction. + pub fn hash(&self, seed: &[u8]) -> H256 { + let mut hasher = Keccak256::new(); + hasher.update(&self.rlp_bytes()); + hasher.update(seed); + H256(hasher.finalize().into()) + } + + pub fn display(&self) -> String { + format!("Deposit {} to {}", self.amount, self.receiver) + } +} + +impl Decodable for Deposit { + /// Decode deposit from RLP bytes in a retro-compatible manner. + /// If it is a legacy deposit it will have zero inbox level and message ID. + /// + /// NOTE that [Deposit::hash] would give the same results for "legacy" deposits, + /// but since decoding is used only for items that are already in delayed inbox this is OK: + /// the hash is calculated for items that are to be submitted to delayed inbox. + fn decode(decoder: &Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + match decoder.item_count()? { + 2 => { + let mut it = decoder.iter(); + let amount: U256 = decode_field(&next(&mut it)?, "amount")?; + let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; + Ok(Self { + amount, + receiver, + inbox_level: 0, + inbox_msg_id: 0, + }) + } + 4 => { + let mut it = decoder.iter(); + let amount: U256 = decode_field(&next(&mut it)?, "amount")?; + let receiver: H160 = decode_field(&next(&mut it)?, "receiver")?; + let inbox_level: u32 = decode_field(&next(&mut it)?, "inbox_level")?; + let inbox_msg_id: u32 = decode_field(&next(&mut it)?, "inbox_msg_id")?; + Ok(Self { + amount, + receiver, + inbox_level, + inbox_msg_id, + }) + } + _ => Err(DecoderError::RlpIncorrectListLen), + } + } +} + +pub fn execute_deposit( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + deposit: &Deposit, + config: Config, +) -> Result { + // We should be able to obtain an account for arbitrary H160 address + // otherwise it is a fatal error. + let to_account_path = + account_path(&deposit.receiver).map_err(AccountStorageError::from)?; + let mut to_account = evm_account_storage.get_or_create(host, &to_account_path)?; + + let result = match to_account.balance_add(host, deposit.amount) { + Ok(()) => ExecutionResult::TransferSucceeded, + Err(err) => { + log!(host, Info, "Deposit failed with {:?}", err); + ExecutionResult::Error(ExitError::Other(Cow::from("Deposit failed"))) + } + }; + + let logs = if result.is_success() { + vec![deposit.event_log()] + } else { + vec![] + }; + + // TODO: estimate how emitting an event influenced tick consumption + let gas_used = config.gas_transaction_call; + let estimated_ticks_used = tick_model::constants::TICKS_FOR_DEPOSIT; + + let execution_outcome = ExecutionOutcome { + gas_used, + logs, + result, + withdrawals: vec![], + estimated_ticks_used, + }; + Ok(execution_outcome) +} + +#[cfg(test)] +mod tests { + use alloy_sol_types::SolEvent; + use evm_execution::account_storage::init_account_storage; + use evm_execution::fa_bridge::test_utils::create_fa_ticket; + use primitive_types::{H160, U256}; + use rlp::Decodable; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_encoding::michelson::MichelsonBytes; + + use crate::{bridge::DEPOSIT_EVENT_TOPIC, CONFIG}; + + use super::{execute_deposit, Deposit}; + + mod events { + alloy_sol_types::sol! { + event Deposit ( + uint256 amount, + address receiver, + uint256 inboxLevel, + uint256 inboxMsgId + ); + } + } + + fn dummy_deposit() -> Deposit { + Deposit { + amount: 1.into(), + receiver: H160([2u8; 20]), + inbox_level: 3, + inbox_msg_id: 4, + } + } + + #[test] + fn deposit_event_topic() { + assert_eq!(events::Deposit::SIGNATURE_HASH.0, DEPOSIT_EVENT_TOPIC); + } + + #[test] + fn deposit_parsing_no_chain_id() { + let ticket = + create_fa_ticket("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT", 0, &[0u8], 2.into()); + let receiver = MichelsonBytes(vec![1u8; 20]); + let (deposit, chain_id) = + Deposit::try_parse(ticket, receiver, 0, 0).expect("Failed to parse"); + pretty_assertions::assert_eq!( + deposit, + Deposit { + amount: tezos_ethereum::wei::eth_from_mutez(2), + receiver: H160([1u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } + ); + pretty_assertions::assert_eq!(chain_id, None) + } + + #[test] + fn deposit_parsing_chain_id() { + let ticket = + create_fa_ticket("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT", 0, &[0u8], 2.into()); + let mut receiver_and_chain_id = vec![]; + receiver_and_chain_id.extend(vec![1u8; 20]); + receiver_and_chain_id.extend(vec![1u8]); + receiver_and_chain_id.extend(vec![0u8; 31]); + let receiver_and_chain_id = MichelsonBytes(receiver_and_chain_id); + let (deposit, chain_id) = Deposit::try_parse(ticket, receiver_and_chain_id, 0, 0) + .expect("Failed to parse"); + pretty_assertions::assert_eq!( + deposit, + Deposit { + amount: tezos_ethereum::wei::eth_from_mutez(2), + receiver: H160([1u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } + ); + pretty_assertions::assert_eq!(chain_id, Some(U256::one())) + } + + #[test] + fn deposit_decode_legacy() { + let mut stream = rlp::RlpStream::new_list(2); + stream.append(&U256::one()).append(&H160([1u8; 20])); + let bytes = stream.out().to_vec(); + let decoder = rlp::Rlp::new(&bytes); + let res = Deposit::decode(&decoder).unwrap(); + assert_eq!( + res, + Deposit { + amount: U256::one(), + receiver: H160([1u8; 20]), + inbox_level: 0, + inbox_msg_id: 0, + } + ); + } + + #[test] + fn deposit_execution_outcome_contains_event() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let deposit = dummy_deposit(); + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(outcome.is_success()); + assert_eq!(outcome.logs.len(), 1); + + let log_data = alloy_primitives::LogData::new_unchecked( + outcome.logs[0].topics.iter().map(|x| x.0.into()).collect(), + outcome.logs[0].data.clone().into(), + ); + let event = events::Deposit::decode_log_data(&log_data, true).unwrap(); + assert_eq!(event.amount, alloy_primitives::U256::from(1)); + assert_eq!( + event.receiver, + alloy_primitives::Address::from_slice(&[2u8; 20]) + ); + assert_eq!(event.inboxLevel, alloy_primitives::U256::from(3)); + assert_eq!(event.inboxMsgId, alloy_primitives::U256::from(4)); + } + + #[test] + fn deposit_execution_fails_due_to_balance_overflow() { + let mut host = MockKernelHost::default(); + let mut evm_account_storage = init_account_storage().unwrap(); + + let mut deposit = dummy_deposit(); + deposit.amount = U256::MAX; + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(outcome.is_success()); + + let outcome = + execute_deposit(&mut host, &mut evm_account_storage, &deposit, CONFIG) + .unwrap(); + assert!(!outcome.is_success()); + assert!(outcome.logs.is_empty()); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/configuration.rs b/etherlink/kernel_calypso2/kernel/src/configuration.rs new file mode 100644 index 000000000000..0c7486f3e384 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/configuration.rs @@ -0,0 +1,251 @@ +use crate::{ + blueprint_storage::DEFAULT_MAX_BLUEPRINT_LOOKAHEAD_IN_SECONDS, + delayed_inbox::DelayedInbox, + storage::{ + dal_slots, enable_dal, evm_node_flag, is_enable_fa_bridge, + max_blueprint_lookahead_in_seconds, read_admin, read_delayed_transaction_bridge, + read_kernel_governance, read_kernel_security_governance, + read_maximum_allowed_ticks, read_or_set_maximum_gas_per_transaction, + read_sequencer_governance, sequencer, + }, + tick_model::constants::{MAXIMUM_GAS_LIMIT, MAX_ALLOWED_TICKS}, +}; +use evm_execution::read_ticketer; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::public_key::PublicKey; + +#[derive(Debug, Clone, Default)] +pub struct DalConfiguration { + pub slot_indices: Vec, +} + +pub enum ConfigurationMode { + Proxy, + Sequencer { + delayed_bridge: ContractKt1Hash, + delayed_inbox: Box, + sequencer: PublicKey, + dal: Option, + evm_node_flag: bool, + max_blueprint_lookahead_in_seconds: i64, + }, +} + +impl std::fmt::Display for ConfigurationMode { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + ConfigurationMode::Proxy => write!(f, "Proxy"), + ConfigurationMode::Sequencer { + delayed_bridge, + delayed_inbox: _, // Ignoring delayed_inbox + sequencer, + dal, + evm_node_flag, + max_blueprint_lookahead_in_seconds, + } => write!( + f, + "Sequencer {{ delayed_bridge: {:?}, sequencer: {:?}, dal: {:?}, evm_node_flag: {}, max_blueprints_lookahead_in_seconds: {} }}", + delayed_bridge, sequencer, dal, evm_node_flag, max_blueprint_lookahead_in_seconds + ), + } + } +} + +pub struct Limits { + pub maximum_allowed_ticks: u64, + pub maximum_gas_limit: u64, +} + +impl Default for Limits { + fn default() -> Self { + Self { + maximum_allowed_ticks: MAX_ALLOWED_TICKS, + maximum_gas_limit: MAXIMUM_GAS_LIMIT, + } + } +} + +pub struct Configuration { + pub tezos_contracts: TezosContracts, + pub mode: ConfigurationMode, + pub limits: Limits, + pub enable_fa_bridge: bool, + pub garbage_collect_blocks: bool, +} + +impl Default for Configuration { + fn default() -> Self { + Self { + tezos_contracts: TezosContracts::default(), + mode: ConfigurationMode::Proxy, + limits: Limits::default(), + enable_fa_bridge: false, + garbage_collect_blocks: false, + } + } +} + +impl std::fmt::Display for Configuration { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "Tezos Contracts: {}, Mode: {}, Enable FA Bridge: {}, Garbage collect blocks: {}", + &self.tezos_contracts, &self.mode, &self.enable_fa_bridge, &self.garbage_collect_blocks + ) + } +} + +#[derive(Debug, PartialEq, Clone, Default)] +pub struct TezosContracts { + pub ticketer: Option, + pub admin: Option, + pub sequencer_governance: Option, + pub kernel_governance: Option, + pub kernel_security_governance: Option, +} + +impl std::fmt::Display for TezosContracts { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let TezosContracts { + ticketer, + admin, + sequencer_governance, + kernel_governance, + kernel_security_governance, + } = self; + write!( + f, + "Ticketer is {:?}. Administrator is {:?}. Sequencer governance is {:?}. Kernel governance is {:?}. Kernel security governance is {:?}.", + ticketer, admin, sequencer_governance, kernel_governance, kernel_security_governance + ) + } +} +fn contains(contract: &Option, expected: &ContractKt1Hash) -> bool { + contract.as_ref().map_or(false, |kt1| kt1 == expected) +} + +impl TezosContracts { + pub fn is_admin(&self, contract: &ContractKt1Hash) -> bool { + contains(&self.admin, contract) + } + pub fn is_sequencer_governance(&self, contract: &ContractKt1Hash) -> bool { + contains(&self.sequencer_governance, contract) + } + pub fn is_ticketer(&self, contract: &ContractKt1Hash) -> bool { + contains(&self.ticketer, contract) + } + pub fn is_kernel_governance(&self, contract: &ContractKt1Hash) -> bool { + contains(&self.kernel_governance, contract) + } + + pub fn is_kernel_security_governance(&self, contract: &ContractKt1Hash) -> bool { + contains(&self.kernel_security_governance, contract) + } +} + +fn fetch_tezos_contracts(host: &mut impl Runtime) -> TezosContracts { + // 1. Fetch the kernel's ticketer, returns `None` if it is badly + // encoded or absent. + let ticketer = read_ticketer(host); + // 2. Fetch the kernel's administrator, returns `None` if it is badly + // encoded or absent. + let admin = read_admin(host); + // 3. Fetch the sequencer governance, returns `None` if it is badly + // encoded or absent. + let sequencer_governance = read_sequencer_governance(host); + // 4. Fetch the kernel_governance contract, returns `None` if it is badly + // encoded or absent. + let kernel_governance = read_kernel_governance(host); + // 5. Fetch the kernel_security_governance contract, returns `None` if it is badly + // encoded or absent. + let kernel_security_governance = read_kernel_security_governance(host); + + TezosContracts { + ticketer, + admin, + sequencer_governance, + kernel_governance, + kernel_security_governance, + } +} + +pub fn fetch_limits(host: &mut impl Runtime) -> Limits { + let maximum_allowed_ticks = + read_maximum_allowed_ticks(host).unwrap_or(MAX_ALLOWED_TICKS); + + let maximum_gas_limit = + read_or_set_maximum_gas_per_transaction(host).unwrap_or(MAXIMUM_GAS_LIMIT); + + Limits { + maximum_allowed_ticks, + maximum_gas_limit, + } +} + +fn fetch_dal_configuration(host: &mut Host) -> Option { + let enable_dal = enable_dal(host).unwrap_or(false); + if enable_dal { + let slot_indices: Vec = dal_slots(host).unwrap_or(None)?; + Some(DalConfiguration { slot_indices }) + } else { + None + } +} + +pub fn fetch_configuration(host: &mut Host) -> Configuration { + let tezos_contracts = fetch_tezos_contracts(host); + let limits = fetch_limits(host); + let sequencer = sequencer(host).unwrap_or_default(); + let enable_fa_bridge = is_enable_fa_bridge(host).unwrap_or_default(); + let dal: Option = fetch_dal_configuration(host); + let evm_node_flag = evm_node_flag(host).unwrap_or(false); + match sequencer { + Some(sequencer) => { + let delayed_bridge = read_delayed_transaction_bridge(host) + // The sequencer must declare a delayed transaction bridge. This + // default value is only to facilitate the testing. + .unwrap_or_else(|| { + ContractKt1Hash::from_base58_check( + "KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT", + ) + .unwrap() + }); + // Default to 5 minutes. + let max_blueprint_lookahead_in_seconds = + max_blueprint_lookahead_in_seconds(host) + .unwrap_or(DEFAULT_MAX_BLUEPRINT_LOOKAHEAD_IN_SECONDS); + match DelayedInbox::new(host) { + Ok(delayed_inbox) => Configuration { + tezos_contracts, + mode: ConfigurationMode::Sequencer { + delayed_bridge, + delayed_inbox: Box::new(delayed_inbox), + sequencer, + dal, + evm_node_flag, + max_blueprint_lookahead_in_seconds, + }, + limits, + enable_fa_bridge, + garbage_collect_blocks: !evm_node_flag, + }, + Err(err) => { + log!(host, Fatal, "The kernel failed to created the delayed inbox, reverting configuration to proxy ({:?})", err); + Configuration { + limits, + ..Configuration::default() + } + } + } + } + None => Configuration { + tezos_contracts, + mode: ConfigurationMode::Proxy, + limits, + enable_fa_bridge, + garbage_collect_blocks: false, + }, + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/dal.rs b/etherlink/kernel_calypso2/kernel/src/dal.rs new file mode 100644 index 000000000000..53f3231c67a8 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/dal.rs @@ -0,0 +1,601 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::parsing::{parse_unsigned_blueprint_chunk, SequencerBlueprintRes}; +use crate::sequencer_blueprint::UnsignedSequencerBlueprint; +use primitive_types::U256; +use rlp::{DecoderError, PayloadInfo}; +use tezos_evm_logging::{log, Level::*}; +use tezos_smart_rollup_host::dal_parameters::RollupDalParameters; + +use tezos_evm_runtime::runtime::Runtime; + +const TAG_SIZE: usize = 1; + +const DAL_BLUEPRINT_INPUT_TAG: u8 = 1; + +const DAL_PADDING_TAG: u8 = 0; + +enum ParsedInput { + UnsignedSequencerBlueprint(UnsignedSequencerBlueprint), + InvalidInput, + Padding, +} + +// Import all the pages of a DAL slot and concatenate them. +fn import_dal_slot( + host: &mut Host, + params: &RollupDalParameters, + published_level: u32, + slot_index: u8, +) -> Option> { + // From the protocol perspective the levels are encoded in [0; 2^31[, as + // such any levels above would be invalid and the rollup node will return + // the empty preimage. + if published_level > i32::MAX as u32 { + return None; + } + let page_size = params.page_size as usize; + let slot_size = params.slot_size as usize; + let mut slot: Vec = vec![0u8; slot_size]; + let number_of_pages = (params.slot_size / params.page_size) as i16; + let mut page_start = 0usize; + for page_index in 0..number_of_pages { + let imported_page_len = host + .reveal_dal_page( + published_level as i32, + slot_index, + page_index, + &mut slot[page_start..page_start + page_size], + ) + .unwrap_or(0); + if imported_page_len == page_size { + page_start += imported_page_len + } else { + return None; + } + } + Some(slot) +} + +// data is assumed to be one RLP object followed by some padding. +// this function returns the length of the RLP object, including its +// length prefix +fn rlp_length(data: &[u8]) -> Result { + let PayloadInfo { + header_len, + value_len, + } = PayloadInfo::from(data)?; + Result::Ok(header_len + value_len) +} + +fn parse_unsigned_sequencer_blueprint( + host: &mut Host, + bytes: &[u8], + head_level: &Option, +) -> (Option, usize) { + if let Result::Ok(chunk_length) = rlp_length(bytes) { + match parse_unsigned_blueprint_chunk(&bytes[..chunk_length], head_level) { + SequencerBlueprintRes::SequencerBlueprint(unsigned_chunk) => ( + Some(ParsedInput::UnsignedSequencerBlueprint(unsigned_chunk)), + chunk_length + TAG_SIZE, + ), + SequencerBlueprintRes::InvalidNumberOfChunks + | SequencerBlueprintRes::InvalidSignature + | SequencerBlueprintRes::InvalidNumber => { + (Some(ParsedInput::InvalidInput), chunk_length + TAG_SIZE) + } + SequencerBlueprintRes::Unparsable => (None, chunk_length + TAG_SIZE), + } + } else { + log!(host, Debug, "Read an invalid chunk from slot."); + (None, TAG_SIZE) + } +} + +fn parse_input( + host: &mut Host, + bytes: &[u8], + head_level: &Option, +) -> (Option, usize) { + // The expected format is: + + // blueprint tag (1 byte) / blueprint chunk (variable) + + match bytes[0] { + DAL_PADDING_TAG => (Some(ParsedInput::Padding), TAG_SIZE), + DAL_BLUEPRINT_INPUT_TAG => { + let bytes = &bytes[TAG_SIZE..]; + parse_unsigned_sequencer_blueprint(host, bytes, head_level) + } + invalid_tag => { + log!( + host, + Debug, + "DAL slot contains an invalid message tag: '{}'.", + invalid_tag + ); + (None, TAG_SIZE) + } // Tag is invalid, let's yield and give the responsibility to the + // caller to continue reading. + } +} + +fn parse_slot( + host: &mut Host, + slot: &[u8], + head_level: &Option, +) -> Vec { + // The format of a dal slot is + // tagged chunk | tagged chunk | .. | padding + // + // DAL slots are padded with zeros to have a constant size. We read chunks + // until reading `\x00` which is considered as `end of list`. + + let mut buffer = Vec::new(); + let mut offset = 0; + + // Invariant: at each loop, at least one byte has been read. Once + // `end_of_list` has been reached or the slot has been fully read, the + // buffer is returned. + loop { + // Checked at the beginning of the loop: if the slot is empty, it + // returns directly and avoids reading the tag outside of the bounds of the slot. + if offset >= slot.len() { + return buffer; + }; + let (next_input, length) = parse_input(host, &slot[offset..], head_level); + + match next_input { + None => return buffer, // Once an unparsable input has been read, + // stop reading and return the list of chunks read. + Some(ParsedInput::UnsignedSequencerBlueprint(b)) => buffer.push(b), + // Invalid inputs are ignored. + Some(ParsedInput::InvalidInput) => {} + Some(ParsedInput::Padding) => return buffer, + } + + // The offset is incremented by the number of bytes read, and the + // function returns if all the bytes have been read already. + offset += length; + } +} + +pub fn fetch_and_parse_sequencer_blueprint_from_dal( + host: &mut Host, + params: &RollupDalParameters, + head_level: &Option, + slot_index: u8, + published_level: u32, +) -> Option> { + if let Some(slot) = import_dal_slot(host, params, published_level, slot_index) { + log!( + host, + Debug, + "DAL slot at level {} and index {} successfully imported", + published_level, + slot_index + ); + + // DAL slots are padded with zeros to have a constant + // size, we need to remove this padding before parsing the + // slot as a blueprint chunk. + + let chunks = parse_slot(host, &slot, head_level); + log!( + host, + Debug, + "DAL slot successfully parsed as {} unsigned blueprint chunks", + chunks.len() + ); + Some(chunks) + } else { + log!( + host, + Debug, + "Slot {} at level {} is invalid", + slot_index, + published_level + ); + None + } +} + +#[cfg(test)] +pub mod tests { + use std::iter::repeat; + + use primitive_types::{H160, U256}; + use rlp::Encodable; + use tezos_ethereum::tx_common::EthereumTransactionCommon; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + use tezos_smart_rollup_host::runtime::Runtime; + + use crate::{ + block::GENESIS_PARENT_HASH, + sequencer_blueprint::{BlueprintWithDelayedHashes, UnsignedSequencerBlueprint}, + }; + + use super::{fetch_and_parse_sequencer_blueprint_from_dal, DAL_BLUEPRINT_INPUT_TAG}; + + fn dummy_transaction(i: u8) -> EthereumTransactionCommon { + let to = &hex::decode("423163e58aabec5daa3dd1130b759d24bef0f6ea").unwrap(); + let to = Some(H160::from_slice(to)); + + let unsigned_transaction = EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Legacy, + Some(U256::one()), + i as u64, + U256::from(40000000u64), + U256::from(40000000u64), + 21000u64, + to, + U256::from(500000000u64), + vec![], + vec![], + None, + ); + + unsigned_transaction + .sign_transaction( + "cb9db6b5878db2fa20586e23b7f7b51c22a7c6ed0530daafc2615b116f170cd3" + .to_string(), + ) + .expect("Transaction signature shouldn't fail") + } + + // See the size of chunks in `sequencer_blueprint.ml` in the sequencer. Note + // that the kernel doesn't enforce a size for the chunks, this constant is + // purely for consistency. + const MAX_CHUNK_SIZE: usize = 3957; + + pub fn dummy_big_blueprint(nb_transactions: usize) -> BlueprintWithDelayedHashes { + let transactions = repeat(()) + .enumerate() + .map(|(i, ())| dummy_transaction(i as u8).to_bytes()) + .take(nb_transactions) + .collect(); + let timestamp = Timestamp::from(123456); + BlueprintWithDelayedHashes { + timestamp, + transactions, + parent_hash: GENESIS_PARENT_HASH, + delayed_hashes: vec![], + } + } + + pub fn chunk_blueprint( + blueprint: BlueprintWithDelayedHashes, + number: U256, + ) -> Vec { + let bytes = blueprint.rlp_bytes(); + let chunks = bytes.chunks(MAX_CHUNK_SIZE); + let nb_chunks = chunks.len(); + chunks + .enumerate() + .map(|(chunk_index, chunk)| UnsignedSequencerBlueprint { + chunk: chunk.to_vec(), + nb_chunks: nb_chunks as u16, + number, + chunk_index: chunk_index as u16, + chain_id: None, + }) + .collect() + } + + pub fn prepare_dal_slot( + host: &mut MockKernelHost, + chunks: &[UnsignedSequencerBlueprint], + published_level: i32, + slot_index: u8, + ) { + let mut slot = Vec::with_capacity((MAX_CHUNK_SIZE + 1) * chunks.len()); + for chunk in chunks { + let bytes = chunk.rlp_bytes(); + slot.push(DAL_BLUEPRINT_INPUT_TAG); + slot.extend_from_slice(&bytes); + } + + host.host.set_dal_slot(published_level, slot_index, &slot) + } + + #[derive(PartialEq)] + enum InsertAt { + Start, + End, + AfterChunk(u16), + } + + fn prepare_invalid_dal_slot( + host: &mut MockKernelHost, + chunks: &[UnsignedSequencerBlueprint], + published_level: i32, + slot_index: u8, + invalid_data: &[u8], + insert_at: InsertAt, + ) { + let mut slot = + Vec::with_capacity((MAX_CHUNK_SIZE + 1) * chunks.len() + invalid_data.len()); + + if insert_at == InsertAt::Start { + slot.extend_from_slice(invalid_data); + } + + for chunk in chunks { + let bytes = chunk.rlp_bytes(); + slot.push(DAL_BLUEPRINT_INPUT_TAG); + slot.extend_from_slice(&bytes); + + if insert_at == InsertAt::AfterChunk(chunk.chunk_index) { + slot.extend_from_slice(invalid_data); + } + } + + if insert_at == InsertAt::End { + slot.extend_from_slice(invalid_data); + } + + host.host.set_dal_slot(published_level, slot_index, &slot) + } + + #[test] + fn test_parse_regular_slot() { + let mut host = MockKernelHost::default(); + + let blueprint = dummy_big_blueprint(100); + let chunks = chunk_blueprint(blueprint, 0.into()); + assert!( + chunks.len() >= 2, + "Blueprint is composed of {} chunks, but at least two are required for the test to make sense", + chunks.len() + ); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level() - (dal_parameters.attestation_lag as u32); + prepare_dal_slot(&mut host, &chunks, published_level as i32, 0); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &None, + 0, + published_level, + ); + + assert_eq!(Some(chunks), chunks_from_slot) + } + + fn make_invalid_slot( + invalid_data: &[u8], + insert_at: InsertAt, + ) -> ( + Vec, + Option>, + ) { + let mut host = MockKernelHost::default(); + + let blueprint = dummy_big_blueprint(100); + let chunks = chunk_blueprint(blueprint, 0.into()); + + assert!( + chunks.len() >= 2, + "Blueprint is composed of {} chunks, but at least two are required for the test to make sense", + chunks.len() + ); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level() - (dal_parameters.attestation_lag as u32); + prepare_invalid_dal_slot( + &mut host, + &chunks, + published_level as i32, + 0, + invalid_data, + insert_at, + ); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &None, + 0, + published_level, + ); + + (chunks, chunks_from_slot) + } + + #[test] + fn test_parse_slot_with_invalid_last_data() { + let invalid_data = vec![2; 10]; + let (chunks, parsed_chunks) = make_invalid_slot(&invalid_data, InsertAt::End); + + assert_eq!(Some(chunks), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_invalid_last_chunk() { + // The tag announces a chunk, the data is not an RLP encoded chunk + let mut invalid_data = vec![DAL_BLUEPRINT_INPUT_TAG]; + invalid_data.extend_from_slice(dummy_transaction(0).rlp_bytes().as_ref()); + let (chunks, parsed_chunks) = make_invalid_slot(&invalid_data, InsertAt::End); + + assert_eq!(Some(chunks), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_invalid_first_data() { + let invalid_data = vec![2; 10]; + let (_chunks, parsed_chunks) = make_invalid_slot(&invalid_data, InsertAt::Start); + + assert_eq!(Some(vec![]), parsed_chunks) + } + + #[test] + fn test_parse_slot_resume_after_invalid_chunk() { + let mut host = MockKernelHost::default(); + + let valid_blueprint_chunks_1 = chunk_blueprint(dummy_big_blueprint(1), 0.into()); + + let invalid_blueprint_chunks = { + let mut chunks = chunk_blueprint(dummy_big_blueprint(1), 0.into()); + for chunk in chunks.iter_mut() { + chunk.nb_chunks = crate::blueprint_storage::MAXIMUM_NUMBER_OF_CHUNKS + 1 + } + chunks + }; + + let valid_blueprint_chunks_2 = chunk_blueprint(dummy_big_blueprint(1), 0.into()); + + let mut chunks = vec![]; + chunks.extend(valid_blueprint_chunks_1.clone()); + chunks.extend(invalid_blueprint_chunks); + chunks.extend(valid_blueprint_chunks_2.clone()); + + let mut expected_chunks = vec![]; + expected_chunks.extend(valid_blueprint_chunks_1); + expected_chunks.extend(valid_blueprint_chunks_2); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level() - (dal_parameters.attestation_lag as u32); + prepare_dal_slot(&mut host, &chunks, published_level as i32, 0); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &None, + 0, + published_level, + ); + + assert_eq!(Some(expected_chunks), chunks_from_slot) + } + + #[test] + fn test_parse_slot_with_invalid_first_chunk() { + // The tag announces a chunk, the data is not an RLP encoded chunk + let mut invalid_data = vec![DAL_BLUEPRINT_INPUT_TAG]; + invalid_data.extend_from_slice(dummy_transaction(0).rlp_bytes().as_ref()); + let (_chunks, parsed_chunks) = make_invalid_slot(&invalid_data, InsertAt::Start); + + assert_eq!(Some(vec![]), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_padding_at_start() { + // The first byte being 0, the slot is parsed as padding + let mut invalid_data = vec![0; 1]; + invalid_data.extend_from_slice(dummy_transaction(0).rlp_bytes().as_ref()); + let (_chunks, parsed_chunks) = make_invalid_slot(&invalid_data, InsertAt::Start); + + assert_eq!(Some(vec![]), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_invalid_data_after_first_chunk() { + let invalid_data = vec![2; 10]; + let (chunks, parsed_chunks) = + make_invalid_slot(&invalid_data, InsertAt::AfterChunk(0)); + + let expected_chunks = vec![chunks[0].clone()]; + assert_eq!(Some(expected_chunks), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_invalid_chunk_after_first_chunk() { + // The tag announces a chunk, the data is not an RLP encoded chunk + let mut invalid_data = vec![DAL_BLUEPRINT_INPUT_TAG]; + invalid_data.extend_from_slice(dummy_transaction(0).rlp_bytes().as_ref()); + let (chunks, parsed_chunks) = + make_invalid_slot(&invalid_data, InsertAt::AfterChunk(0)); + + let expected_chunks = vec![chunks[0].clone()]; + assert_eq!(Some(expected_chunks), parsed_chunks) + } + + #[test] + fn test_parse_slot_with_padding_after_first_chunk() { + // The first byte being 0, the slot is parsed as padding + let mut invalid_data = vec![0; 1]; + invalid_data.extend_from_slice(dummy_transaction(0).rlp_bytes().as_ref()); + let (chunks, parsed_chunks) = + make_invalid_slot(&invalid_data, InsertAt::AfterChunk(0)); + + let expected_chunks = vec![chunks[0].clone()]; + assert_eq!(Some(expected_chunks), parsed_chunks) + } + + #[test] + fn test_parse_invalid_slot() { + let mut host = MockKernelHost::default(); + + let blueprint = dummy_big_blueprint(1); + let chunks = chunk_blueprint(blueprint, 0.into()); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level(); + + // Slot will be invalid as it hasn't been attested yet. + prepare_dal_slot(&mut host, &chunks, published_level as i32, 0); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &None, + 0, + published_level, + ); + + assert_eq!(None, chunks_from_slot); + + // Slot will be invalid as the level of its publication is negative + // (hence higher than i32::MAX when viewed as unsigned) + prepare_dal_slot(&mut host, &chunks, -1, 0); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &None, + 0, + published_level, + ); + + assert_eq!(None, chunks_from_slot) + } + + fn chunk_blueprint_range(min: usize, max: usize) -> Vec { + let mut chunks = vec![]; + for n in min..max { + chunks.extend(chunk_blueprint(dummy_big_blueprint(1), n.into())); + } + chunks + } + + #[test] + fn test_parse_slot_with_blueprints_from_the_past() { + let mut host = MockKernelHost::default(); + + let head_level = Some(2.into()); + let chunks = chunk_blueprint_range(0, 5); + let expected_chunks = chunk_blueprint_range(3, 5); + + assert_eq!(5, chunks.len()); + assert_eq!(2, expected_chunks.len()); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level() - (dal_parameters.attestation_lag as u32); + prepare_dal_slot(&mut host, &chunks, published_level as i32, 0); + + let chunks_from_slot = fetch_and_parse_sequencer_blueprint_from_dal( + &mut host, + &dal_parameters, + &head_level, + 0, + published_level, + ); + + assert_eq!(chunks_from_slot, Some(expected_chunks)); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/dal_slot_import_signal.rs b/etherlink/kernel_calypso2/kernel/src/dal_slot_import_signal.rs new file mode 100644 index 000000000000..97784eb2be13 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/dal_slot_import_signal.rs @@ -0,0 +1,371 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use rlp::{Decodable, DecoderError, Encodable}; +use tezos_crypto_rs::hash::UnknownSignature; +use tezos_ethereum::rlp_helpers::{self, append_u32_le, decode_field_u32_le}; + +#[derive(PartialEq, Debug, Clone)] +pub struct DalSlotIndicesList(pub Vec); + +impl Encodable for DalSlotIndicesList { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(self.0.len()); + for slot in &self.0 { + stream.append(slot); + } + } +} + +impl Decodable for DalSlotIndicesList { + fn decode(decoder: &rlp::Rlp) -> Result { + rlp_helpers::check_is_list(decoder)?; + let slot_indices: Vec = rlp_helpers::decode_list(decoder, "slot_indices")?; + Ok(DalSlotIndicesList(slot_indices)) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct DalSlotIndicesOfLevel { + pub published_level: u32, + pub slot_indices: DalSlotIndicesList, +} + +impl Encodable for DalSlotIndicesOfLevel { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + append_u32_le(stream, &self.published_level); + stream.append(&self.slot_indices); + } +} + +impl Decodable for DalSlotIndicesOfLevel { + fn decode(decoder: &rlp::Rlp) -> Result { + rlp_helpers::check_list(decoder, 2)?; + let mut it = decoder.iter(); + let published_level: u32 = + decode_field_u32_le(&rlp_helpers::next(&mut it)?, "published_level")?; + let slot_indices = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "slot_indices")?; + Ok(DalSlotIndicesOfLevel { + published_level, + slot_indices, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct UnsignedDalSlotSignals(pub Vec); + +impl Encodable for UnsignedDalSlotSignals { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(self.0.len()); + for slots_with_level in &self.0 { + stream.append(slots_with_level); + } + } +} + +impl Decodable for UnsignedDalSlotSignals { + fn decode(decoder: &rlp::Rlp) -> Result { + rlp_helpers::check_is_list(decoder)?; + let levels_with_slots: Vec = + rlp_helpers::decode_list(decoder, "unsigned_dal_slots_signals")?; + Ok(UnsignedDalSlotSignals(levels_with_slots)) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct DalSlotImportSignals { + pub signals: UnsignedDalSlotSignals, + pub signature: UnknownSignature, +} + +impl Encodable for DalSlotImportSignals { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append(&self.signals); + stream.append(&self.signature.as_ref()); + } +} + +impl Decodable for DalSlotImportSignals { + fn decode(decoder: &rlp::Rlp) -> Result { + rlp_helpers::check_list(decoder, 2)?; + let mut it = decoder.iter(); + let signals = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "unsigned_signals")?; + let signature_bytes: Vec = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "signature")?; + let signature = UnknownSignature::try_from(signature_bytes.as_slice()) + .map_err(|_| DecoderError::Custom("Invalid signature encoding"))?; + Ok(DalSlotImportSignals { signals, signature }) + } +} + +#[cfg(test)] +mod tests { + use super::{ + DalSlotImportSignals, DalSlotIndicesList, DalSlotIndicesOfLevel, + UnsignedDalSlotSignals, + }; + use rlp::{Decodable, DecoderError, Encodable}; + use tezos_crypto_rs::hash::UnknownSignature; + use tezos_ethereum::rlp_helpers::FromRlpBytes; + + #[derive(PartialEq, Debug, Clone)] + pub enum RlpTree { + Val(Vec), + List(Vec), + } + + impl Decodable for RlpTree { + fn decode(decoder: &rlp::Rlp) -> Result { + if decoder.is_list() { + let l: Vec = decoder.as_list()?; + Ok(RlpTree::List(l)) + } else { + let s: Vec = decoder.as_val()?; + Ok(RlpTree::Val(s)) + } + } + } + + impl Encodable for RlpTree { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + match self { + RlpTree::List(trees) => { + stream.begin_list(trees.len()); + for tree in trees { + stream.append(tree); + } + } + RlpTree::Val(bytes) => { + stream.append(bytes); + } + }; + } + } + + fn index_expected(v: u8, tree: RlpTree) { + let encoded = v.rlp_bytes(); + let encoded_as_tree: RlpTree = + FromRlpBytes::from_rlp_bytes(&encoded).expect("should decode as tree"); + assert_eq!( + encoded_as_tree, tree, + "Encoding gave unexpected result, source: {:?}, expected: {:?}, actual: {:?}", + v, tree, encoded_as_tree + ); + + let v2: u8 = + FromRlpBytes::from_rlp_bytes(&encoded).expect("Index should be decodable"); + assert_eq!(v, v2); + } + + fn indices_expected(v: Vec, tree: RlpTree) { + let v = DalSlotIndicesList(v); + let encoded = v.rlp_bytes(); + let encoded_as_tree: RlpTree = + FromRlpBytes::from_rlp_bytes(&encoded).expect("should decode as tree"); + assert_eq!( + encoded_as_tree, tree, + "Encoding gave unexpected result, source: {:?}, expected: {:?}, actual: {:?}", + v, tree, encoded_as_tree + ); + + let v2: DalSlotIndicesList = + FromRlpBytes::from_rlp_bytes(&encoded).expect("Indices should be decodable"); + assert_eq!(v, v2); + } + + fn indices_roundtrip(v: Vec) { + let v = DalSlotIndicesList(v); + let bytes = v.rlp_bytes(); + let v2: DalSlotIndicesList = + FromRlpBytes::from_rlp_bytes(&bytes).expect("Indices should be decodable"); + assert_eq!(v, v2, "Roundtrip failed for indices: {:?}", v); + } + + fn indices_of_level_roundtrip(published_level: u32, slot_indices: Vec) { + let slot_indices = DalSlotIndicesList(slot_indices); + let v = DalSlotIndicesOfLevel { + published_level, + slot_indices, + }; + let bytes = v.rlp_bytes(); + let v2: DalSlotIndicesOfLevel = FromRlpBytes::from_rlp_bytes(&bytes) + .expect("Slot indices of level should be decodable"); + assert_eq!(v, v2, "Roundtrip failed for slot indices of level: {:?}", v); + } + + fn unsigned_signals_roundtrip(v: UnsignedDalSlotSignals) { + let bytes = v.rlp_bytes(); + let v2: UnsignedDalSlotSignals = FromRlpBytes::from_rlp_bytes(&bytes) + .expect("Unsigned signals should be decodable"); + assert_eq!(v, v2, "Roundtrip failed for unsigned signals: {:?}", v); + + let signature = UnknownSignature::from_base58_check( + "sigdGBG68q2vskMuac4AzyNb1xCJTfuU8MiMbQtmZLUCYydYrtTd5Lessn1EFLTDJzjXoYxRasZxXbx6tHnirbEJtikcMHt3" + ).expect("signature decoding should work"); + + let v3 = DalSlotImportSignals { + signals: v, + signature, + }; + let bytes = v3.rlp_bytes(); + let v4: DalSlotImportSignals = FromRlpBytes::from_rlp_bytes(&bytes) + .expect("Signed signals should be decodable"); + assert_eq!(v3, v4, "Roundtrip failed for signed signals: {:?}", v3) + } + + #[test] + fn indices_roundtrip_tests() { + indices_roundtrip(vec![]); + indices_roundtrip(vec![0]); + indices_roundtrip(vec![1]); + indices_roundtrip(vec![0, 1]); + } + + #[test] + fn index_expected_zero() { + index_expected(0, RlpTree::Val(vec![])); + } + + #[test] + fn index_expected_one() { + index_expected(1, RlpTree::Val(vec![1])); + } + + #[test] + fn index_expected_128() { + index_expected(128, RlpTree::Val(vec![128])); + } + + #[test] + fn indices_expected_empty() { + indices_expected(vec![], RlpTree::List(vec![])); + } + + #[test] + fn indices_expected_zero() { + indices_expected(vec![0], RlpTree::List(vec![RlpTree::Val(vec![])])); + } + + #[test] + fn indices_expected_one() { + indices_expected(vec![1], RlpTree::List(vec![RlpTree::Val(vec![1])])); + } + + #[test] + fn indices_expected_zero_zero() { + indices_expected( + vec![0, 0], + RlpTree::List(vec![RlpTree::Val(vec![]), RlpTree::Val(vec![])]), + ); + } + + #[test] + fn indices_expected_zero_one() { + indices_expected( + vec![0, 1], + RlpTree::List(vec![RlpTree::Val(vec![]), RlpTree::Val(vec![1])]), + ); + } + + #[test] + fn indices_expected_one_one() { + indices_expected( + vec![1, 1], + RlpTree::List(vec![RlpTree::Val(vec![1]), RlpTree::Val(vec![1])]), + ); + } + + #[test] + fn indices_expected_one_one_one_one() { + indices_expected( + vec![1, 1, 1, 1], + RlpTree::List(vec![ + RlpTree::Val(vec![1]), + RlpTree::Val(vec![1]), + RlpTree::Val(vec![1]), + RlpTree::Val(vec![1]), + ]), + ); + } + + #[test] + fn indices_expected_zero_zero_zero_zero() { + indices_expected( + vec![0, 0, 0, 0], + RlpTree::List(vec![ + RlpTree::Val(vec![]), + RlpTree::Val(vec![]), + RlpTree::Val(vec![]), + RlpTree::Val(vec![]), + ]), + ); + } + + #[test] + fn indices_of_level_roundtrip_tests() { + indices_of_level_roundtrip(0, vec![]); + indices_of_level_roundtrip(0, vec![0]); + indices_of_level_roundtrip(0, vec![1]); + indices_of_level_roundtrip(0, vec![0, 1]); + indices_of_level_roundtrip(100, vec![]); + indices_of_level_roundtrip(100, vec![0]); + indices_of_level_roundtrip(100, vec![1]); + indices_of_level_roundtrip(100, vec![0, 1]); + } + + #[test] + fn roundtrip_empty() { + let v = UnsignedDalSlotSignals(vec![]); + unsigned_signals_roundtrip(v); + } + + #[test] + fn roundtrip_single_empty() { + let indices_of_level = DalSlotIndicesOfLevel { + published_level: 0, + slot_indices: DalSlotIndicesList(vec![]), + }; + + let v = UnsignedDalSlotSignals(vec![indices_of_level]); + unsigned_signals_roundtrip(v); + } + + #[test] + fn roundtrip_single_zero() { + let indices_of_level = DalSlotIndicesOfLevel { + published_level: 0, + slot_indices: DalSlotIndicesList(vec![0]), + }; + + let v = UnsignedDalSlotSignals(vec![indices_of_level]); + unsigned_signals_roundtrip(v); + } + + #[test] + fn roundtrip_single_one() { + let indices_of_level = DalSlotIndicesOfLevel { + published_level: 0, + slot_indices: DalSlotIndicesList(vec![1]), + }; + + let v = UnsignedDalSlotSignals(vec![indices_of_level]); + unsigned_signals_roundtrip(v); + } + + #[test] + fn roundtrip_single_zero_one() { + let indices_of_level = DalSlotIndicesOfLevel { + published_level: 0, + slot_indices: DalSlotIndicesList(vec![0, 1]), + }; + + let v = UnsignedDalSlotSignals(vec![indices_of_level]); + unsigned_signals_roundtrip(v); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/delayed_inbox.rs b/etherlink/kernel_calypso2/kernel/src/delayed_inbox.rs new file mode 100644 index 000000000000..840236908060 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/delayed_inbox.rs @@ -0,0 +1,470 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Trilitech + +use crate::{ + bridge::Deposit, + event::Event, + inbox::{Transaction, TransactionContent}, + linked_list::LinkedList, + storage::{self, read_last_info_per_level_timestamp}, +}; +use anyhow::Result; +use evm_execution::fa_bridge::deposit::FaDeposit; +use rlp::{Decodable, DecoderError, Encodable}; +use tezos_ethereum::{ + rlp_helpers, + transaction::{TransactionHash, TRANSACTION_HASH_SIZE}, + tx_common::EthereumTransactionCommon, +}; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::timestamp::Timestamp; +use tezos_smart_rollup_host::path::RefPath; +use tezos_storage::read_u16_le_default; + +pub struct DelayedInbox(LinkedList); + +pub const DELAYED_INBOX_PATH: RefPath = RefPath::assert_from(b"/evm/delayed-inbox"); + +// Maximum number of transaction included in a blueprint when +// forcing timed-out transactions from the delayed inbox. +pub const DEFAULT_MAX_DELAYED_INBOX_BLUEPRINT_LENGTH: u16 = 1000; + +// Path to override the default value. +pub const MAX_DELAYED_INBOX_BLUEPRINT_LENGTH_PATH: RefPath = + RefPath::assert_from(b"/evm/max_delayed_inbox_blueprint_length"); + +// Tag that indicates the delayed transaction is a eth transaction. +pub const DELAYED_TRANSACTION_TAG: u8 = 0x01; + +// Tag that indicates the delayed transaction is a deposit. +pub const DELAYED_DEPOSIT_TAG: u8 = 0x02; + +// Tag that indicates the delayed transaction is a FA deposit. +pub const DELAYED_FA_DEPOSIT_TAG: u8 = 0x03; + +/// Hash of a transaction +/// +/// It represents the key of the transaction in the delayed inbox. +#[derive(Clone, Copy, Debug, PartialEq)] +pub struct Hash(pub [u8; TRANSACTION_HASH_SIZE]); + +impl From for Hash { + fn from(v: TransactionHash) -> Self { + Self(v) + } +} + +impl Encodable for Hash { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + s.encoder().encode_value(&self.0); + } +} + +impl Decodable for Hash { + fn decode(decoder: &rlp::Rlp) -> Result { + let hash: Vec = decoder.as_val()?; + let hash = hash + .try_into() + .map_err(|_| DecoderError::Custom("expected a vec of 32 elements"))?; + Ok(Hash(hash)) + } +} + +impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + &self.0 + } +} + +/// Delayed transaction +#[allow(clippy::large_enum_variant)] +#[derive(Clone)] +pub enum DelayedTransaction { + Ethereum(EthereumTransactionCommon), + Deposit(Deposit), + FaDeposit(FaDeposit), +} + +impl Encodable for DelayedTransaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match self { + DelayedTransaction::Ethereum(delayed_tx) => { + stream.append(&DELAYED_TRANSACTION_TAG); + stream.append(&delayed_tx.to_bytes()); + } + DelayedTransaction::Deposit(delayed_deposit) => { + stream.append(&DELAYED_DEPOSIT_TAG); + stream.append(delayed_deposit); + } + DelayedTransaction::FaDeposit(delayed_fa_deposit) => { + stream.append(&DELAYED_FA_DEPOSIT_TAG); + stream.append(delayed_fa_deposit); + } + } + } +} + +impl Decodable for DelayedTransaction { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let tag: u8 = decoder.at(0)?.as_val()?; + let payload = decoder.at(1)?; + match tag { + DELAYED_TRANSACTION_TAG => { + let payload: Vec = payload.as_val()?; + let delayed_tx = EthereumTransactionCommon::from_bytes(&payload)?; + + Ok(Self::Ethereum(delayed_tx)) + } + DELAYED_DEPOSIT_TAG => { + let deposit = Deposit::decode(&payload)?; + Ok(DelayedTransaction::Deposit(deposit)) + } + DELAYED_FA_DEPOSIT_TAG => { + let fa_deposit = FaDeposit::decode(&payload)?; + Ok(DelayedTransaction::FaDeposit(fa_deposit)) + } + _ => Err(DecoderError::Custom("unknown tag")), + } + } +} + +// Elements in the delayed inbox +#[derive(Clone)] +pub struct DelayedInboxItem { + pub transaction: DelayedTransaction, + timestamp: Timestamp, + level: u32, +} + +impl DelayedInboxItem { + fn list_size() -> usize { + 3 + } +} + +impl Encodable for DelayedInboxItem { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(Self::list_size()); + stream.append(&self.transaction); + rlp_helpers::append_timestamp(stream, self.timestamp); + rlp_helpers::append_u32_le(stream, &self.level); + } +} + +impl Decodable for DelayedInboxItem { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != Self::list_size() { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut it = decoder.iter(); + let transaction = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "transaction")?; + let timestamp = rlp_helpers::decode_timestamp(&rlp_helpers::next(&mut it)?)?; + let level = + rlp_helpers::decode_field_u32_le(&rlp_helpers::next(&mut it)?, "level")?; + Ok(Self { + transaction, + timestamp, + level, + }) + } +} + +impl DelayedInbox { + pub fn new(host: &mut Host) -> Result { + let linked_list = LinkedList::new(&DELAYED_INBOX_PATH, host)?; + Ok(Self(linked_list)) + } + + pub fn save_transaction( + &mut self, + host: &mut Host, + tx: Transaction, + timestamp: Timestamp, + level: u32, + ) -> Result<()> { + let Transaction { tx_hash, content } = tx.clone(); + let transaction = match content { + TransactionContent::Ethereum(_) => anyhow::bail!("Non-delayed evm transaction should not be saved to the delayed inbox. {:?}", tx.tx_hash), + TransactionContent::EthereumDelayed(tx) => DelayedTransaction::Ethereum(tx), + TransactionContent::Deposit(deposit) => DelayedTransaction::Deposit(deposit), + TransactionContent::FaDeposit(fa_deposit) => DelayedTransaction::FaDeposit(fa_deposit) + }; + let item = DelayedInboxItem { + transaction, + timestamp, + level, + }; + + Event::NewDelayedTransaction(Box::new(tx)).store(host)?; + + self.0.push(host, &Hash(tx_hash), &item)?; + log!( + host, + Info, + "Saved transaction {} in the delayed inbox", + hex::encode(tx_hash) + ); + Ok(()) + } + + pub fn transaction_from_delayed( + tx_hash: Hash, + delayed: DelayedTransaction, + ) -> Transaction { + match delayed { + DelayedTransaction::Ethereum(tx) => Transaction { + tx_hash: tx_hash.0, + content: TransactionContent::EthereumDelayed(tx), + }, + DelayedTransaction::Deposit(deposit) => Transaction { + tx_hash: tx_hash.0, + content: TransactionContent::Deposit(deposit), + }, + DelayedTransaction::FaDeposit(fa_deposit) => Transaction { + tx_hash: tx_hash.0, + content: TransactionContent::FaDeposit(fa_deposit), + }, + } + } + + pub fn find_transaction( + &mut self, + host: &mut Host, + tx_hash: Hash, + ) -> Result> { + let tx = self.0.find(host, &tx_hash)?.map( + |DelayedInboxItem { + transaction, + timestamp, + level: _, + }| { + ( + Self::transaction_from_delayed(tx_hash, transaction), + timestamp, + ) + }, + ); + + Ok(tx) + } + + // Returns the oldest tx in the delayed inbox (and its hash) if it + // timed out + fn first_if_timed_out( + &mut self, + host: &mut Host, + now: Timestamp, + timeout: u64, + current_level: u32, + min_levels: u32, + ) -> Result> { + let to_pop = self.0.first_with_id(host)?.and_then( + |( + tx_hash, + DelayedInboxItem { + transaction, + timestamp, + level, + }, + )| { + if now.as_u64() - timestamp.as_u64() >= timeout + && current_level - level >= min_levels + { + log!( + host, + Info, + "Delayed transaction {} timed out", + hex::encode(tx_hash) + ); + Some((tx_hash, transaction)) + } else { + None + } + }, + ); + Ok(to_pop) + } + + #[cfg(test)] + pub fn is_empty(&self, host: &mut Host) -> Result { + let first = self.0.first_with_id(host)?; + Ok(first.is_none()) + } + + fn pop_first( + &mut self, + host: &mut Host, + ) -> Result> { + let to_pop = self.0.first_with_id(host)?; + match to_pop { + None => Ok(None), + Some((hash, delayed)) => { + let _ = self.0.remove(host, &hash)?; + let transaction = + Self::transaction_from_delayed(hash, delayed.transaction); + Ok(Some(transaction)) + } + } + } + + /// Returns whether the oldest tx in the delayed inbox has timed out. + pub fn first_has_timed_out( + &mut self, + host: &mut Host, + ) -> Result { + let now = read_last_info_per_level_timestamp(host)?; + let timeout = storage::delayed_inbox_timeout(host)?; + let current_level = storage::read_l1_level(host)?; + let min_levels = storage::delayed_inbox_min_levels(host)?; + let popped = + self.first_if_timed_out(host, now, timeout, current_level, min_levels)?; + Ok(popped.is_some()) + } + + /// Computes the next vector of timed-out delayed transactions. + /// If there are no timed-out transactions, None is returned to + /// signal that we're done. + /// Note that this function assumes we're on a "timeout" state, + /// which should be checked before calling it. + pub fn next_delayed_inbox_blueprint( + &mut self, + host: &mut Host, + ) -> Result>> { + let max_delayed_inbox_blueprint_length = read_u16_le_default( + host, + &MAX_DELAYED_INBOX_BLUEPRINT_LENGTH_PATH, + DEFAULT_MAX_DELAYED_INBOX_BLUEPRINT_LENGTH, + )?; + let mut popped: Vec = vec![]; + while let Some(tx) = self.pop_first(host)? { + popped.push(tx); + // Check if the number of transactions has reached the limit per + // blueprint + if popped.len() as u16 >= max_delayed_inbox_blueprint_length { + break; + } + } + Ok(if popped.is_empty() { + None + } else { + Some(popped) + }) + } + + /// Deletes a transaction from the delayed inbox. It does not check if + /// a transaction is removed or not. The only property ensured by the + /// function is that the transaction is not part of the delayed inbox + /// after the call. + pub fn delete( + &mut self, + host: &mut Host, + tx_hash: Hash, + ) -> Result<()> { + log!( + host, + Info, + "Removing transaction {} from the delayed inbox", + hex::encode(tx_hash) + ); + let _found = self.0.remove(host, &tx_hash)?; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::DelayedInbox; + use super::Hash; + use crate::inbox::Transaction; + use crate::storage::read_last_info_per_level_timestamp; + use primitive_types::{H160, U256}; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + + use crate::inbox::TransactionContent::{Ethereum, EthereumDelayed}; + use tezos_ethereum::{ + transaction::TRANSACTION_HASH_SIZE, tx_common::EthereumTransactionCommon, + }; + + fn address_from_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + + fn tx_(i: u64) -> EthereumTransactionCommon { + EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Legacy, + Some(U256::one()), + i, + U256::from(40000000u64), + U256::from(40000000u64), + 21000u64, + address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"), + U256::from(500000000u64), + vec![], + vec![], + None, + ) + } + + fn dummy_transaction(i: u8) -> Transaction { + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: EthereumDelayed(tx_(i.into())), + } + } + + #[test] + fn test_delayed_inbox_roundtrip() { + let mut host = MockKernelHost::default(); + let mut delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + + let tx: Transaction = dummy_transaction(0); + + let timestamp: Timestamp = + read_last_info_per_level_timestamp(&host).unwrap_or(Timestamp::from(0)); + delayed_inbox + .save_transaction(&mut host, tx.clone(), timestamp, 0) + .expect("Tx should be saved in the delayed inbox"); + + let mut delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should exist"); + + let read = delayed_inbox + .find_transaction(&mut host, Hash(tx.tx_hash)) + .expect("Reading from the delayed inbox should work") + .expect("Transaction should be in the delayed inbox"); + assert_eq!((tx, timestamp), read) + } + + #[test] + fn test_delayed_inbox_roundtrip_error_non_delayed() { + let mut host = MockKernelHost::default(); + let mut delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + + let tx: Transaction = Transaction { + tx_hash: [12; TRANSACTION_HASH_SIZE], + content: Ethereum(tx_(12)), + }; + + let timestamp: Timestamp = + read_last_info_per_level_timestamp(&host).unwrap_or(Timestamp::from(0)); + let res = delayed_inbox.save_transaction(&mut host, tx, timestamp, 0); + + assert!(res.is_err()); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/error.rs b/etherlink/kernel_calypso2/kernel/src/error.rs new file mode 100644 index 000000000000..c05335aae09e --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/error.rs @@ -0,0 +1,227 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023-2024 Functori +// SPDX-FileCopyrightText: 2023 Trilitech +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT +use core::str::Utf8Error; +use evm_execution::account_storage::AccountStorageError; +use evm_execution::{DurableStorageError, EthereumError}; +use primitive_types::U256; +use rlp::DecoderError; +use tezos_data_encoding::enc::BinError; +use tezos_ethereum::tx_common::SigError; +use tezos_indexable_storage::IndexableStorageError; +use tezos_smart_rollup_encoding::entrypoint::EntrypointError; +use tezos_smart_rollup_encoding::michelson::ticket::TicketError; +use tezos_smart_rollup_host::path::PathError; +use tezos_smart_rollup_host::runtime::RuntimeError; +use tezos_storage::error::Error as GenStorageError; +use thiserror::Error; + +#[derive(Error, Debug)] +pub enum TransferError { + #[error("Transaction error: couldn't reconstruct the caller address")] + InvalidCallerAddress, + #[error("Transaction error: couldn't verify the signature")] + InvalidSignature, + #[error("Transaction error: incorrect nonce, expected {expected} but got {actual}")] + InvalidNonce { expected: U256, actual: U256 }, + #[error("Transaction error: not enough funds to apply transaction")] + NotEnoughBalance, + #[error("Transaction error: cumulative gas overflowed")] + CumulativeGasUsedOverflow, + #[error("Transaction error: invalid address format {0}")] + InvalidAddressFormat(Utf8Error), + #[error("Transaction error: invalid chain id {expected} but got {actual}")] + InvalidChainId { expected: U256, actual: U256 }, +} + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum StorageError { + #[error(transparent)] + Path(PathError), + #[error(transparent)] + Runtime(RuntimeError), + #[error(transparent)] + Storage(tezos_smart_rollup_storage::StorageError), + #[error(transparent)] + AccountStorage(AccountStorageError), + #[error("Storage error: index out of bound")] + IndexOutOfBounds, + #[error("Storage error: failed to initialize an account")] + AccountInitialisation, + #[error("Storage error: failed to initialize a genesis account")] + GenesisAccountInitialisation, + #[error("Storage error: error while reading a value (incorrect size). Expected {expected} but got {actual}")] + InvalidLoadValue { expected: usize, actual: usize }, + #[error("Storage error: storing the current block hash failed")] + BlockHashStorageFailed, +} + +#[derive(Error, Debug)] +pub enum UpgradeProcessError { + #[error("Internal upgrade error: {0}")] + InternalUpgrade(&'static str), + #[error("Fallback mechanism was triggered")] + Fallback, + #[error("Missing dictator key")] + NoDictator, +} + +#[derive(Error, Debug)] +pub enum EncodingError { + #[error("Invalid ticket")] + Ticket(TicketError), + #[error("Invalid entrypoint")] + Entrypoint(EntrypointError), + #[error("Invalid serialization")] + Bin(BinError), +} + +#[derive(Error, Debug)] +#[allow(clippy::enum_variant_names)] +pub enum Error { + #[error(transparent)] + Transfer(TransferError), + #[error(transparent)] + Storage(StorageError), + #[error("Invalid conversion")] + InvalidConversion, + #[error("Failed to decode: {0}")] + RlpDecoderError(DecoderError), + #[error("Invalid parsing")] + InvalidParsing, + #[error(transparent)] + InvalidRunTransaction(EthereumError), + #[error("Simulation failed: {0}")] + Simulation(EthereumError), + #[error(transparent)] + UpgradeError(UpgradeProcessError), + #[error(transparent)] + InvalidSignature(SigError), + #[error("Invalid signature check")] + InvalidSignatureCheck, + #[error("Issue during reboot")] + Reboot, + #[error(transparent)] + Encoding(EncodingError), +} + +impl From for StorageError { + fn from(e: PathError) -> Self { + Self::Path(e) + } +} +impl From for StorageError { + fn from(e: RuntimeError) -> Self { + Self::Runtime(e) + } +} + +impl From for Error { + fn from(e: PathError) -> Self { + Self::Storage(StorageError::Path(e)) + } +} +impl From for Error { + fn from(e: RuntimeError) -> Self { + Self::Storage(StorageError::Runtime(e)) + } +} + +impl From for Error { + fn from(e: TransferError) -> Self { + Self::Transfer(e) + } +} + +impl From for Error { + fn from(e: DecoderError) -> Self { + Self::RlpDecoderError(e) + } +} + +impl From for Error { + fn from(e: UpgradeProcessError) -> Self { + Self::UpgradeError(e) + } +} + +impl From for Error { + fn from(e: StorageError) -> Self { + Self::Storage(e) + } +} + +impl From for Error { + fn from(e: DurableStorageError) -> Self { + match e { + DurableStorageError::PathError(e) => Self::Storage(StorageError::Path(e)), + DurableStorageError::RuntimeError(e) => { + Self::Storage(StorageError::Runtime(e)) + } + } + } +} + +impl From for Error { + fn from(e: tezos_smart_rollup_storage::StorageError) -> Self { + Self::Storage(StorageError::Storage(e)) + } +} + +impl From for Error { + fn from(e: AccountStorageError) -> Self { + Self::Storage(StorageError::AccountStorage(e)) + } +} + +impl From for Error { + fn from(e: TicketError) -> Self { + Self::Encoding(EncodingError::Ticket(e)) + } +} + +impl From for Error { + fn from(e: EntrypointError) -> Self { + Self::Encoding(EncodingError::Entrypoint(e)) + } +} + +impl From for Error { + fn from(e: BinError) -> Self { + Self::Encoding(EncodingError::Bin(e)) + } +} + +impl From for Error { + fn from(e: IndexableStorageError) -> Self { + match e { + IndexableStorageError::Path(e) => Error::Storage(StorageError::Path(e)), + IndexableStorageError::Runtime(e) => Error::Storage(StorageError::Runtime(e)), + IndexableStorageError::Storage(e) => Error::Storage(StorageError::Storage(e)), + IndexableStorageError::IndexOutOfBounds => { + Error::Storage(StorageError::IndexOutOfBounds) + } + IndexableStorageError::RlpDecoderError(e) => Error::RlpDecoderError(e), + IndexableStorageError::InvalidLoadValue { expected, actual } => { + Error::Storage(StorageError::InvalidLoadValue { expected, actual }) + } + } + } +} + +impl From for Error { + fn from(e: GenStorageError) -> Self { + match e { + GenStorageError::Path(e) => Error::Storage(StorageError::Path(e)), + GenStorageError::Runtime(e) => Error::Storage(StorageError::Runtime(e)), + GenStorageError::Storage(e) => Error::Storage(StorageError::Storage(e)), + GenStorageError::RlpDecoderError(e) => Error::RlpDecoderError(e), + GenStorageError::InvalidLoadValue { expected, actual } => { + Error::Storage(StorageError::InvalidLoadValue { expected, actual }) + } + } + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/event.rs b/etherlink/kernel_calypso2/kernel/src/event.rs new file mode 100644 index 000000000000..4a4266307c3e --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/event.rs @@ -0,0 +1,76 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::{inbox::Transaction, storage, upgrade}; +use primitive_types::{H256, U256}; +use rlp::{Encodable, RlpStream}; +use tezos_ethereum::rlp_helpers::{append_timestamp, append_u256_le}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::timestamp::Timestamp; + +pub const UPGRADE_TAG: u8 = 0x01; +pub const SEQUENCER_UPGRADE_TAG: u8 = 0x02; +pub const BLUEPRINT_APPLIED_TAG: u8 = 0x03; +pub const NEW_DELAYED_TRANSACTION_TAG: u8 = 0x04; +pub const FLUSH_DELAYED_INBOX: u8 = 0x05; + +#[derive(Debug, PartialEq, Clone)] +pub enum Event<'a> { + Upgrade(upgrade::KernelUpgrade), + SequencerUpgrade(upgrade::SequencerUpgrade), + BlueprintApplied { + number: U256, + hash: H256, + }, + NewDelayedTransaction(Box), + FlushDelayedInbox { + transactions: &'a Vec, + timestamp: Timestamp, + level: U256, + }, +} + +impl Encodable for Event<'_> { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(2); + match self { + Event::Upgrade(upgrade) => { + stream.append(&UPGRADE_TAG); + stream.append(upgrade); + } + Event::SequencerUpgrade(sequencer_upgrade) => { + stream.append(&SEQUENCER_UPGRADE_TAG); + stream.append(sequencer_upgrade); + } + Event::BlueprintApplied { number, hash } => { + stream.append(&BLUEPRINT_APPLIED_TAG); + stream.begin_list(2); + append_u256_le(stream, number); + stream.append(hash); + } + Event::NewDelayedTransaction(txn) => { + stream.append(&NEW_DELAYED_TRANSACTION_TAG); + stream.append(txn); + } + Event::FlushDelayedInbox { + transactions, + timestamp, + level, + } => { + stream.append(&FLUSH_DELAYED_INBOX); + stream.begin_list(3); + stream.append_list(transactions); + append_timestamp(stream, *timestamp); + stream.append(level); + } + } + } +} + +impl Event<'_> { + pub fn store(&self, host: &mut Host) -> anyhow::Result<()> { + storage::store_event(host, self) + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/evm_node_entrypoint.rs b/etherlink/kernel_calypso2/kernel/src/evm_node_entrypoint.rs new file mode 100644 index 000000000000..f2910ac177aa --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/evm_node_entrypoint.rs @@ -0,0 +1,30 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +//! EVM Node Entrypoints. +//! +//! The module contain functions that may be called by the evm node +//! only. It allows to call specific functions of the kernel without +//! using the inbox and a specific message. + +use crate::{delayed_inbox::DelayedInbox, inbox::Transaction}; +use tezos_ethereum::rlp_helpers::FromRlpBytes; +use tezos_evm_runtime::{internal_runtime::InternalRuntime, runtime::KernelHost}; +use tezos_smart_rollup_host::{path::RefPath, runtime::Runtime}; + +const DELAYED_INPUT_PATH: RefPath = RefPath::assert_from(b"/__delayed_input"); + +pub fn populate_delayed_inbox< + Host: tezos_smart_rollup_host::runtime::Runtime + InternalRuntime, +>( + sdk_host: &mut Host, +) { + let mut host: KernelHost = KernelHost::init(sdk_host); + let payload = host.store_read_all(&DELAYED_INPUT_PATH).unwrap(); + let transaction = Transaction::from_rlp_bytes(&payload).unwrap(); + let mut delayed_inbox = DelayedInbox::new(&mut host).unwrap(); + delayed_inbox + .save_transaction(&mut host, transaction, 0.into(), 0u32) + .unwrap(); +} diff --git a/etherlink/kernel_calypso2/kernel/src/fallback_upgrade.rs b/etherlink/kernel_calypso2/kernel/src/fallback_upgrade.rs new file mode 100644 index 000000000000..d1a8caecfb6f --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/fallback_upgrade.rs @@ -0,0 +1,50 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; +use tezos_smart_rollup_host::{path::RefPath, runtime::RuntimeError, KERNEL_BOOT_PATH}; + +use crate::upgrade::KERNEL_ROOT_HASH; + +const BACKUP_KERNEL_BOOT_PATH: RefPath = + RefPath::assert_from(b"/__backup_kernel/boot.wasm"); + +const BACKUP_KERNEL_ROOT_HASH: RefPath = + RefPath::assert_from(b"/__backup_kernel/root_hash"); + +pub fn backup_current_kernel(host: &mut impl Runtime) -> Result<(), RuntimeError> { + // Fallback preparation detected + // Storing the current kernel boot path under a temporary path in + // order to fallback on it if something goes wrong in the upcoming + // upgraded kernel. + log!( + host, + Info, + "Preparing potential fallback by backing up the current kernel." + ); + + // If there is a kernel root hash (which is not mandatory after origination, + // we copy it to the backup, otherwise we just copy empty bytes to have + // something to fallback on. + if host.store_has(&KERNEL_ROOT_HASH)?.is_some() { + host.store_copy(&KERNEL_ROOT_HASH, &BACKUP_KERNEL_ROOT_HASH)?; + } else { + host.store_write_all(&BACKUP_KERNEL_ROOT_HASH, &[0; PREIMAGE_HASH_SIZE])?; + } + + host.store_copy(&KERNEL_BOOT_PATH, &BACKUP_KERNEL_BOOT_PATH) +} + +pub fn fallback_backup_kernel(host: &mut impl Runtime) -> Result<(), RuntimeError> { + log!( + host, + Error, + "Something went wrong, fallback mechanism is triggered." + ); + + host.store_copy(&BACKUP_KERNEL_ROOT_HASH, &KERNEL_ROOT_HASH)?; + host.store_copy(&BACKUP_KERNEL_BOOT_PATH, &KERNEL_BOOT_PATH) +} diff --git a/etherlink/kernel_calypso2/kernel/src/fees.rs b/etherlink/kernel_calypso2/kernel/src/fees.rs new file mode 100644 index 000000000000..c2a0e82bf416 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/fees.rs @@ -0,0 +1,585 @@ +// SPDX-FileCopyrightText: 2024 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Adjustments & calculation of fees, over-and-above the execution gas fee. +//! +//! Users submit transactions which contain three values related to fees: +//! - `gas_limit` +//! - `max_fee_per_gas` +//! - `max_priority_fee_per_gas` +//! +//! We ignore `tx.max_priority_fee_per_gas` completely. For every transaction, we act as if the +//! user set `tx.max_priority_fee_per_gas = 0`. We therefore only care about `tx.gas_limit` and +//! `tx.max_fee_per_gas`. +//! +//! Additionally, we charge a _data-availability_ fee, for each tx posted through L1. + +use crate::inbox::TransactionContent; + +use evm_execution::account_storage::{account_path, EthereumAccountStorage}; +use evm_execution::handler::ExecutionOutcome; +use evm_execution::EthereumError; +use primitive_types::{H160, H256, U256}; +use tezos_ethereum::access_list::AccessListItem; +use tezos_ethereum::block::BlockFees; +use tezos_ethereum::tx_common::EthereumTransactionCommon; +use tezos_evm_runtime::runtime::Runtime; + +use std::mem::size_of; + +/// /!\ +/// If you update these constants, you need to update the evm-node as well. +/// The node uses the constants to compute the DA fees without calling the +/// kernel, therefore they need to be synchronised. +/// +/// If you happen to change the constants, it is recommended to write them +/// to the storage instead. If they're written in the storage, we don't need +/// need to default to a duplicated constant. +/// /!\ + +/// Minimum base fee per gas, set to 1 Gwei. +pub const MINIMUM_BASE_FEE_PER_GAS: u64 = 10_u64.pow(9); + +// We assume a tx (with empty data) consumes roughly 150 bytes in the inbox. +// +// This is a slight underestimate (when external message framing is included), but this is +// compensated by charging double the expected L1 gas fee for each byte. +const ASSUMED_TX_ENCODED_SIZE: usize = 150; + +// 4 mutez per byte. +// +// The fee for injection on L1 is composed of the following +// (at default minimal nanotez per gas / default minimal fee): +// - base cost for 'add smart rollup message': 259 mutez +// - cost per new message (tx chunk): 5 mutez +// - cost per additional byte: 1 mutez +// +// To avoid a more complex calculation, simplify by assuming double the +// default cost per byte, to cover the static fees. +// +// Since multiple messages are grouped up into a single operation of +// 'add smart rollup messages', together they will cover the larger base +// fee for this operation. +pub(crate) const DA_FEE_PER_BYTE: u64 = 4 * 10_u64.pow(12); + +/// Instructions for 'balancing the books'. +#[derive(Debug)] +pub struct FeeUpdates { + pub overall_gas_price: U256, + pub overall_gas_used: U256, + pub burn_amount: U256, + pub charge_user_amount: U256, + pub compensate_sequencer_amount: U256, +} + +impl TransactionContent { + /// Returns fee updates of the transaction. + /// + /// *NB* this is not the gas price used _for execution_, but rather the gas price that + /// should be reported in the transaction receipt. + /// + /// # Prerequisites + /// The user must have already paid for 'execution gas fees'. + pub fn fee_updates( + &self, + block_fees: &BlockFees, + execution_gas_used: U256, + ) -> FeeUpdates { + match self { + Self::Deposit(_) => FeeUpdates::for_deposit(execution_gas_used), + Self::Ethereum(tx) => FeeUpdates::for_tx(tx, block_fees, execution_gas_used), + Self::EthereumDelayed(_) => { + FeeUpdates::for_delayed_tx(block_fees, execution_gas_used) + } + Self::FaDeposit(_) => FeeUpdates::for_fa_deposit(execution_gas_used), + } + } +} + +impl FeeUpdates { + fn for_deposit(gas_used: U256) -> Self { + Self { + overall_gas_used: gas_used, + overall_gas_price: U256::zero(), + burn_amount: U256::zero(), + charge_user_amount: U256::zero(), + compensate_sequencer_amount: U256::zero(), + } + } + + fn for_fa_deposit(gas_used: U256) -> Self { + Self { + overall_gas_used: gas_used, + overall_gas_price: U256::zero(), + burn_amount: U256::zero(), + charge_user_amount: U256::zero(), + compensate_sequencer_amount: U256::zero(), + } + } + + fn for_tx( + tx: &EthereumTransactionCommon, + block_fees: &BlockFees, + execution_gas_used: U256, + ) -> Self { + let da_fee = da_fee(block_fees.da_fee_per_byte(), &tx.data, &tx.access_list); + + Self::of_gas_and_da_fee(block_fees, execution_gas_used, da_fee) + } + + fn for_delayed_tx(block_fees: &BlockFees, execution_gas_used: U256) -> Self { + Self::of_gas_and_da_fee(block_fees, execution_gas_used, U256::zero()) + } + + fn of_gas_and_da_fee( + block_fees: &BlockFees, + execution_gas_used: U256, + da_fee: U256, + ) -> Self { + let execution_gas_fees = execution_gas_used * block_fees.base_fee_per_gas(); + + let initial_added_fees = da_fee; + let initial_total_fees = initial_added_fees + execution_gas_fees; + + let gas_price = block_fees.base_fee_per_gas(); + + let gas_used = cdiv(initial_total_fees, gas_price); + + let total_fees = gas_price * gas_used; + + // Due to rounding, we may have a small amount of unaccounted-for gas. + // Assign this to the burned fee. + let burn_amount = execution_gas_fees + total_fees - initial_total_fees; + + Self { + overall_gas_price: gas_price, + overall_gas_used: gas_used, + burn_amount, + charge_user_amount: total_fees - execution_gas_fees, + compensate_sequencer_amount: da_fee, + } + } + + pub fn modify_outcome(&self, outcome: &mut ExecutionOutcome) { + outcome.gas_used = self.overall_gas_used.as_u64(); + } + + pub fn apply( + &self, + host: &mut impl Runtime, + accounts: &mut EthereumAccountStorage, + caller: H160, + sequencer_pool_address: Option, + ) -> Result<(), anyhow::Error> { + tezos_evm_logging::log!( + host, + tezos_evm_logging::Level::Debug, + "Applying {self:?} for {caller}" + ); + + let caller_account_path = account_path(&caller)?; + let mut caller_account = accounts.get_or_create(host, &caller_account_path)?; + if !caller_account.balance_remove(host, self.charge_user_amount)? { + return Err(anyhow::anyhow!( + "Failed to charge {caller} additional fees of {}", + self.charge_user_amount + )); + } + + let sequencer = match sequencer_pool_address { + None => { + let burned_fee = self + .burn_amount + .saturating_add(self.compensate_sequencer_amount); + + crate::storage::update_burned_fees(host, burned_fee)?; + return Ok(()); + } + Some(sequencer) => { + crate::storage::update_burned_fees(host, self.burn_amount)?; + sequencer + } + }; + + let sequencer_account_path = account_path(&sequencer)?; + accounts + .get_or_create(host, &sequencer_account_path)? + .balance_add(host, self.compensate_sequencer_amount)?; + + Ok(()) + } +} + +/// Adjust a simulation outcome, to take non-execution fees into account. +/// +/// This is done by adjusting `gas_used` upwards. +pub fn simulation_add_gas_for_fees( + mut outcome: ExecutionOutcome, + block_fees: &BlockFees, + tx_data: &[u8], +) -> Result { + // Simulation does not have an access list + let gas_for_fees = gas_for_fees( + block_fees.da_fee_per_byte(), + // We select minimum base fee per gas, to ensure that the user has sufficient gas + // even if the base_fee_per_gas falls by the time they submit their tx. + block_fees.minimum_base_fee_per_gas(), + tx_data, + &[], + )?; + + outcome.gas_used = outcome.gas_used.saturating_add(gas_for_fees); + Ok(outcome) +} + +/// Returns the gas limit for executing this transaction. +/// +/// This is strictly lower than the gas limit set by the user, as additional gas is required +/// in order to pay for the *data availability fee*. +/// +/// The user pre-pays this (in addition to the data availability fee) prior to execution. +/// If execution does not use all of the execution gas limit, they will be partially refunded. +/// If the transaction was sent through the delayed inbox, no additional fees need to be included +pub fn tx_execution_gas_limit( + tx: &EthereumTransactionCommon, + fees: &BlockFees, + delayed: bool, +) -> Result { + if delayed { + return Ok(tx.gas_limit_with_fees()); + } + + let gas_for_fees = gas_for_fees( + fees.da_fee_per_byte(), + fees.base_fee_per_gas(), + &tx.data, + &tx.access_list, + )?; + + tx.gas_limit_with_fees() + .checked_sub(gas_for_fees) + .ok_or(EthereumError::GasToFeesUnderflow) +} + +/// Calculate gas for fees +pub(crate) fn gas_for_fees( + da_fee_per_byte: U256, + gas_price: U256, + tx_data: &[u8], + tx_access_list: &[AccessListItem], +) -> Result { + let fees = da_fee(da_fee_per_byte, tx_data, tx_access_list); + + let gas_for_fees = cdiv(fees, gas_price); + gas_as_u64(gas_for_fees) +} + +/// Data availability fee for a transaction with given data size. +pub(crate) fn da_fee( + da_fee_per_byte: U256, + tx_data: &[u8], + access_list: &[AccessListItem], +) -> U256 { + let access_data_size: usize = access_list + .iter() + .map(|ali| size_of::() + size_of::() * ali.storage_keys.len()) + .sum(); + + U256::from(tx_data.len()) + .saturating_add(ASSUMED_TX_ENCODED_SIZE.into()) + .saturating_add(access_data_size.into()) + .saturating_mul(da_fee_per_byte) +} + +fn cdiv(l: U256, r: U256) -> U256 { + match l.div_mod(r) { + (res, rem) if rem.is_zero() => res, + (res, _) => res.saturating_add(U256::one()), + } +} + +fn gas_as_u64(gas_for_fees: U256) -> Result { + gas_for_fees + .try_into() + .map_err(|_e| EthereumError::FeesToGasOverflow) +} + +#[cfg(test)] +mod tests { + use super::*; + use evm::ExitSucceed; + use evm_execution::account_storage::{account_path, EthereumAccountStorage}; + use primitive_types::{H160, U256}; + use tezos_evm_runtime::runtime::MockKernelHost; + + use proptest::prelude::*; + + proptest! { + #[test] + fn fee_updates_consistent( + da_fee in any::().prop_map(U256::from), + data in any::>(), + execution_gas_used in (1_u64..).prop_map(U256::from), + [min_fee_per_gas, base_fee_per_gas, max_fee_per_gas] in [1_u64.., 1_u64.., 1_u64..] + .prop_map(|mut prices| {prices.sort(); prices} ) + .prop_map(|[a, b, c]| [a.into(), b.into(), c.into()]), + ) { + // Arrange + let data_size = data.len(); + let tx = mock_tx(max_fee_per_gas, data); + let block_fees = BlockFees::new(min_fee_per_gas, base_fee_per_gas, da_fee); + + // Act + let updates = FeeUpdates::for_tx(&tx, &block_fees, execution_gas_used); + + // Assert + let assumed_execution_gas_cost = base_fee_per_gas * execution_gas_used; + let total_fee = updates.overall_gas_price * updates.overall_gas_used; + let expected_sequencer_comp = da_fee * (data_size + ASSUMED_TX_ENCODED_SIZE); + + assert_eq!(updates.overall_gas_price, base_fee_per_gas, "gas price should be 'base fee per gas'"); + assert!(updates.burn_amount >= assumed_execution_gas_cost, "inconsistent burn amount"); + assert_eq!(updates.charge_user_amount, total_fee - assumed_execution_gas_cost, "inconsistent user charge"); + assert_eq!(total_fee, updates.burn_amount + updates.compensate_sequencer_amount, "inconsistent total fees"); + assert_eq!(updates.compensate_sequencer_amount, expected_sequencer_comp, "inconsistent sequencer comp"); + } + } + + #[test] + fn simulation_covers_extra_fees() { + let tx_data = &[0, 1, 2]; + + let fee = ((tx_data.len() + ASSUMED_TX_ENCODED_SIZE) as u64) * DA_FEE_PER_BYTE; + + expect_extra_gas(fee / 4, 4, DA_FEE_PER_BYTE, tx_data); + expect_extra_gas(fee / 5, 5, DA_FEE_PER_BYTE, tx_data); + expect_extra_gas(fee / 6, 6, DA_FEE_PER_BYTE, tx_data); + // expect extra gas to cover rounding + expect_extra_gas(fee / 7 + 1, 7, DA_FEE_PER_BYTE, tx_data); + } + + #[test] + fn apply_updates_balances_no_sequencer() { + // Arrange + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let balance = U256::from(1000); + set_balance(&mut host, &mut evm_account_storage, address, balance); + + let burn_amount = balance / 3; + let compensate_sequencer_amount = balance / 4; + + let fee_updates = FeeUpdates { + overall_gas_used: U256::zero(), + overall_gas_price: U256::zero(), + burn_amount, + charge_user_amount: balance / 2, + compensate_sequencer_amount, + }; + + // Act + let result = + fee_updates.apply(&mut host, &mut evm_account_storage, address, None); + + // Assert + assert!(result.is_ok()); + let new_balance = get_balance(&mut host, &mut evm_account_storage, address); + assert_eq!(balance / 2, new_balance); + + let burned = crate::storage::read_burned_fees(&mut host); + + // no sequencer reward address set - so everything is burned + assert_eq!(burn_amount + compensate_sequencer_amount, burned); + } + + #[test] + fn apply_updates_balances_with_sequencer() { + // Arrange + let mut host = MockKernelHost::default(); + let sequencer_address = + address_from_str("0123456789ABCDEF0123456789ABCDEF01234567"); + + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let balance = U256::from(1000); + set_balance(&mut host, &mut evm_account_storage, address, balance); + + let sequencer_balance = U256::from(500); + set_balance( + &mut host, + &mut evm_account_storage, + sequencer_address, + sequencer_balance, + ); + + let burn_amount = balance / 3; + let compensate_sequencer_amount = balance / 4; + + let fee_updates = FeeUpdates { + overall_gas_used: U256::zero(), + overall_gas_price: U256::zero(), + burn_amount, + charge_user_amount: balance / 2, + compensate_sequencer_amount, + }; + + // Act + let result = fee_updates.apply( + &mut host, + &mut evm_account_storage, + address, + Some(sequencer_address), + ); + + // Assert + assert!(result.is_ok()); + let new_balance = get_balance(&mut host, &mut evm_account_storage, address); + assert_eq!(balance / 2, new_balance); + + let burned = crate::storage::read_burned_fees(&mut host); + assert_eq!(burn_amount, burned); + + let sequencer_new_balance = + get_balance(&mut host, &mut evm_account_storage, sequencer_address); + assert_eq!( + sequencer_new_balance, + sequencer_balance + compensate_sequencer_amount + ); + } + + #[test] + fn apply_fails_user_charge_too_large() { + // Arrange + let mut host = MockKernelHost::default(); + let mut evm_account_storage = + evm_execution::account_storage::init_account_storage().unwrap(); + + let address = address_from_str("af1276cbb260bb13deddb4209ae99ae6e497f446"); + let balance = U256::from(1000); + set_balance(&mut host, &mut evm_account_storage, address, balance); + + let fee_updates = FeeUpdates { + overall_gas_used: U256::zero(), + overall_gas_price: U256::zero(), + burn_amount: U256::zero(), + charge_user_amount: balance * 2, + compensate_sequencer_amount: U256::zero(), + }; + + // Act + let result = + fee_updates.apply(&mut host, &mut evm_account_storage, address, None); + + // Assert + assert!(result.is_err()); + let new_balance = get_balance(&mut host, &mut evm_account_storage, address); + assert_eq!(balance, new_balance); + } + + #[test] + fn da_fee_includes_access_lists() { + // Arrange + let ali = AccessListItem { + address: H160::zero(), + storage_keys: vec![H256::zero(), H256::zero()], + }; + let al = &[ali.clone(), ali]; + + let data = &[1, 2, 3]; + + // Act + let fee = da_fee(super::DA_FEE_PER_BYTE.into(), data, al); + + // Assert + let expected_bytes = data.len() + 2 * (20 /* address */ + 2 * 32/* keys */); + let expected_fee = + (expected_bytes + ASSUMED_TX_ENCODED_SIZE) as u64 * DA_FEE_PER_BYTE; + + assert_eq!(fee, expected_fee.into()); + } + + fn address_from_str(s: &str) -> H160 { + let data = &hex::decode(s).unwrap(); + H160::from_slice(data) + } + + fn get_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: H160, + ) -> U256 { + let account = evm_account_storage + .get_or_create(host, &account_path(&address).unwrap()) + .unwrap(); + account.balance(host).unwrap() + } + + fn set_balance( + host: &mut MockKernelHost, + evm_account_storage: &mut EthereumAccountStorage, + address: H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_path(&address).unwrap()) + .unwrap(); + assert!(account.balance(host).unwrap().is_zero()); + + account.balance_add(host, balance).unwrap(); + } + + fn mock_execution_outcome(gas_used: u64) -> ExecutionOutcome { + ExecutionOutcome { + gas_used, + logs: Vec::new(), + result: evm_execution::handler::ExecutionResult::CallSucceeded( + ExitSucceed::Stopped, + vec![], + ), + withdrawals: Vec::new(), + estimated_ticks_used: 1, + } + } + + fn mock_tx(max_fee_per_gas: U256, data: Vec) -> EthereumTransactionCommon { + EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Eip1559, + None, + 0, + U256::zero(), + max_fee_per_gas, + 0, + Some(H160::zero()), + U256::zero(), + data, + vec![], + None, + ) + } + + fn expect_extra_gas(extra: u64, min_gas_price: u64, da_fee: u64, tx_data: &[u8]) { + // Arrange + let initial_gas_used = 100; + let block_fees = BlockFees::new(min_gas_price.into(), U256::one(), da_fee.into()); + + let simulated_outcome = mock_execution_outcome(initial_gas_used); + + // Act + let res = simulation_add_gas_for_fees(simulated_outcome, &block_fees, tx_data); + + // Assert + let expected = mock_execution_outcome(initial_gas_used + extra); + assert_eq!( + Ok(expected), + res, + "unexpected extra gas at price {min_gas_price} and data_len {}", + tx_data.len() + ); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/gas_price.rs b/etherlink/kernel_calypso2/kernel/src/gas_price.rs new file mode 100644 index 000000000000..8d79fec4f759 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/gas_price.rs @@ -0,0 +1,246 @@ +// SPDX-FileCopyrightText: 2024 TriliTech +// +// SPDX-License-Identifier: MIT + +//! Adjustments of the gas price (a.k.a `base_fee_per_gas`), in response to load. + +use crate::block_in_progress::BlockInProgress; + +use primitive_types::U256; +use softfloat::F64; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::timestamp::Timestamp; + +// actual ~34M, allow some overhead for less effecient ERC20 transfers. +const ERC20_TICKS: u64 = 40_000_000; +// 50 TPS of ERC20 transfers should be sustained without increasing price. +const SPEED_LIMIT: u64 = 50 * ERC20_TICKS; +const TOLERANCE: u64 = 10 * SPEED_LIMIT; + +// chosen so that gas price will decrease ~7/8 if there's no usage for ~10 seconds. +// ALPHA = -ln(7/8)/(SPEED_LIMIT * 10) +const ALPHA: F64 = softfloat::f64!(0.000_000_000_007); + +/// Register a completed block into the tick backlog +pub fn register_block( + host: &mut impl Runtime, + bip: &BlockInProgress, +) -> anyhow::Result<()> { + if bip.queue_length() > 0 { + anyhow::bail!("update_gas_price on non-empty block"); + } + + update_tick_backlog(host, bip.estimated_ticks_in_block, bip.timestamp)?; + + Ok(()) +} + +/// Retrieve *base fee per gas*, according to the current timestamp. +pub fn base_fee_per_gas( + host: &impl Runtime, + timestamp: Timestamp, + minimum_gas_price: U256, +) -> U256 { + let timestamp = timestamp.as_u64(); + let minimum_gas_price = minimum_gas_price.as_u64(); + + let last_timestamp = + crate::storage::read_tick_backlog_timestamp(host).unwrap_or(timestamp); + + let backlog = backlog_with_time_elapsed(host, 0, timestamp, last_timestamp); + + price_from_tick_backlog(backlog, minimum_gas_price).into() +} + +fn backlog_with_time_elapsed( + host: &impl Runtime, + extra_ticks: u64, + current_timestamp: u64, + last_timestamp: u64, +) -> u64 { + let diff = current_timestamp + .saturating_sub(last_timestamp) + .saturating_mul(SPEED_LIMIT); + + crate::storage::read_tick_backlog(host) + .unwrap_or_default() + .saturating_sub(diff) // first take into account time elapsed + .saturating_add(extra_ticks) // then add the extra ticks just consumed +} + +fn update_tick_backlog( + host: &mut impl Runtime, + ticks_in_block: u64, + timestamp: Timestamp, +) -> anyhow::Result<()> { + let timestamp = timestamp.as_u64(); + + let last_timestamp_opt = crate::storage::read_tick_backlog_timestamp(host).ok(); + let last_timestamp = last_timestamp_opt.unwrap_or(timestamp); + + if last_timestamp_opt.is_none() || timestamp > last_timestamp { + crate::storage::store_tick_backlog_timestamp(host, timestamp)?; + } + + let backlog = + backlog_with_time_elapsed(host, ticks_in_block, timestamp, last_timestamp); + + crate::storage::store_tick_backlog(host, backlog)?; + + Ok(()) +} + +fn price_from_tick_backlog(backlog: u64, minimum: u64) -> u64 { + if backlog <= TOLERANCE { + return minimum; + } + + let min = u64_to_f64(minimum); + + let price = min * F64::exp(ALPHA * u64_to_f64(backlog - TOLERANCE)); + + let price = f64_to_u64(price); + + if price < minimum { + minimum + } else { + price + } +} + +fn u64_to_f64(i: u64) -> F64 { + F64::from_bits(u64_to_f64_bits(i)) +} + +// compiler builtins +// https://github.com/rust-lang/compiler-builtins/blob/351d48e4b95f1665cfd3360e3ba8f3dd4d3fb3c1/src/float/conv.rs +// +// SPDX-License-Identifier: MIT +fn u64_to_f64_bits(i: u64) -> u64 { + if i == 0 { + return 0; + } + let n = i.leading_zeros(); + let a = (i << n) >> 11; // Significant bits, with bit 53 still in tact. + let b = (i << n) << 53; // Insignificant bits, only relevant for rounding. + let m = a + ((b - (b >> 63 & !a)) >> 63); // Add one when we need to round up. Break ties to even. + let e = 1085 - n as u64; // Exponent plus 1023, minus one. + (e << 52) + m // + not |, so the mantissa can overflow into the exponent. +} + +fn f64_to_u64(f: F64) -> u64 { + let fbits = f.to_bits(); + if fbits < 1023 << 52 { + // >= 0, < 1 + 0 + } else if fbits < 1087 << 52 { + // >= 1, < max + let m = 1 << 63 | fbits << 11; // Mantissa and the implicit 1-bit. + let s = 1086 - (fbits >> 52); // Shift based on the exponent and bias. + m >> s + } else if fbits <= 2047 << 52 { + // >= max (incl. inf) + u64::MAX + } else { + // Negative or NaN + 0 + } +} +// end 'compiler builtins' + +#[cfg(test)] +mod test { + use super::*; + use primitive_types::H160; + use proptest::prelude::*; + use std::collections::VecDeque; + use tezos_ethereum::block::BlockConstants; + use tezos_evm_runtime::runtime::{MockKernelHost, Runtime}; + + proptest! { + #[test] + fn f64_to_u64_conv(f in any::()) { + assert_eq!(f as u64, f64_to_u64(f.into())); + } + + #[test] + fn u64_to_f64_conv(u in any::()) { + let f: f64 = u64_to_f64(u).into(); + assert_eq!(u as f64, f); + } + + #[test] + fn gas_price(backlog in any::(), minimum in any::()) { + let price = price_from_tick_backlog(backlog, minimum); + + if backlog <= TOLERANCE { + assert_eq!(price, minimum); + } else { + let exponent = 0.000_000_000_007_f64 * (backlog - TOLERANCE) as f64; + let expected = (minimum as f64) * f64::exp(exponent); + assert_eq!(expected as u64, price); + } + } + } + + #[test] + fn gas_price_responds_to_load() { + let mut host = MockKernelHost::default(); + let timestamp = 0_i64; + let block_fees = crate::retrieve_block_fees(&mut host).unwrap(); + let dummy_block_constants = BlockConstants::first_block( + timestamp.into(), + U256::zero(), + block_fees, + crate::block::GAS_LIMIT, + H160::zero(), + ); + + let mut bip = BlockInProgress::new_with_ticks( + U256::zero(), + Default::default(), + VecDeque::new(), + // estimated ticks in run (ignored) + 0, + timestamp.into(), + U256::zero(), + ); + bip.estimated_ticks_in_block = TOLERANCE; + + register_block(&mut host, &bip).unwrap(); + bip.clone() + .finalize_and_store(&mut host, &dummy_block_constants, vec![], vec![]) + .unwrap(); + + // At tolerance, gas price should be min. + let (min, gas_price) = load_gas_price(&mut host); + assert_eq!(min, crate::fees::MINIMUM_BASE_FEE_PER_GAS.into()); + assert_eq!(gas_price, crate::fees::MINIMUM_BASE_FEE_PER_GAS.into()); + let gas_price_now = base_fee_per_gas(&host, timestamp.into(), min); + assert_eq!(gas_price, gas_price_now); + + // register more blocks - now double tolerance + bip.number = 1.into(); + register_block(&mut host, &bip).unwrap(); + bip.finalize_and_store(&mut host, &dummy_block_constants, vec![], vec![]) + .unwrap(); + let gas_price_now = base_fee_per_gas(&host, timestamp.into(), min); + + let (min, gas_price) = load_gas_price(&mut host); + assert!(gas_price > min); + assert_eq!(gas_price, gas_price_now); + + // after 10 seconds, reduces back to tolerance + let gas_price_after_10 = base_fee_per_gas(&host, (timestamp + 10).into(), min); + assert_eq!( + gas_price_after_10, + crate::fees::MINIMUM_BASE_FEE_PER_GAS.into() + ); + } + + fn load_gas_price(host: &mut impl Runtime) -> (U256, U256) { + let bf = crate::retrieve_block_fees(host).unwrap(); + + (bf.minimum_base_fee_per_gas(), bf.base_fee_per_gas()) + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/inbox.rs b/etherlink/kernel_calypso2/kernel/src/inbox.rs new file mode 100644 index 000000000000..c47ef7ed9eef --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/inbox.rs @@ -0,0 +1,1477 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use crate::blueprint_storage::store_sequencer_blueprint; +use crate::bridge::Deposit; +use crate::configuration::{fetch_limits, DalConfiguration, TezosContracts}; +use crate::dal::fetch_and_parse_sequencer_blueprint_from_dal; +use crate::dal_slot_import_signal::DalSlotImportSignals; +use crate::delayed_inbox::DelayedInbox; +use crate::parsing::{ + Input, InputResult, Parsable, ProxyInput, SequencerBlueprintRes::*, SequencerInput, + SequencerParsingContext, MAX_SIZE_PER_CHUNK, +}; + +use crate::fees::tx_execution_gas_limit; +use crate::sequencer_blueprint::UnsignedSequencerBlueprint; +use crate::storage::{ + chunked_hash_transaction_path, chunked_transaction_num_chunks, + chunked_transaction_path, clear_events, create_chunked_transaction, read_l1_level, + read_last_info_per_level_timestamp, remove_chunked_transaction, remove_sequencer, + store_l1_level, store_last_info_per_level_timestamp, store_transaction_chunk, +}; +use crate::tick_model::constants::{BASE_GAS, TICKS_FOR_BLUEPRINT_INTERCEPT}; +use crate::tick_model::maximum_ticks_for_sequencer_chunk; +use crate::upgrade::*; +use crate::Error; +use crate::{simulation, upgrade}; +use evm_execution::fa_bridge::{deposit::FaDeposit, FA_DEPOSIT_PROXY_GAS_LIMIT}; +use evm_execution::EthereumError; +use primitive_types::U256; +use rlp::{Decodable, DecoderError, Encodable}; +use sha3::{Digest, Keccak256}; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_ethereum::block::BlockFees; +use tezos_ethereum::rlp_helpers::{decode_field, decode_tx_hash, next}; +use tezos_ethereum::transaction::{ + TransactionHash, TransactionType, TRANSACTION_HASH_SIZE, +}; +use tezos_ethereum::tx_common::EthereumTransactionCommon; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::public_key::PublicKey; + +#[allow(clippy::large_enum_variant)] +#[derive(Debug, PartialEq, Clone)] +pub enum TransactionContent { + Ethereum(EthereumTransactionCommon), + Deposit(Deposit), + EthereumDelayed(EthereumTransactionCommon), + FaDeposit(FaDeposit), +} + +const ETHEREUM_TX_TAG: u8 = 1; +const DEPOSIT_TX_TAG: u8 = 2; +const ETHEREUM_DELAYED_TX_TAG: u8 = 3; +const FA_DEPOSIT_TX_TAG: u8 = 4; + +impl Encodable for TransactionContent { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match &self { + TransactionContent::Ethereum(eth) => { + stream.append(ÐEREUM_TX_TAG); + eth.rlp_append(stream) + } + TransactionContent::Deposit(dep) => { + stream.append(&DEPOSIT_TX_TAG); + dep.rlp_append(stream) + } + TransactionContent::EthereumDelayed(eth) => { + stream.append(ÐEREUM_DELAYED_TX_TAG); + eth.rlp_append(stream) + } + TransactionContent::FaDeposit(fa_dep) => { + stream.append(&FA_DEPOSIT_TX_TAG); + fa_dep.rlp_append(stream) + } + } + } +} + +impl Decodable for TransactionContent { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let tag: u8 = decoder.at(0)?.as_val()?; + let tx = decoder.at(1)?; + match tag { + DEPOSIT_TX_TAG => { + let deposit = Deposit::decode(&tx)?; + Ok(Self::Deposit(deposit)) + } + ETHEREUM_TX_TAG => { + let eth = EthereumTransactionCommon::decode(&tx)?; + Ok(Self::Ethereum(eth)) + } + ETHEREUM_DELAYED_TX_TAG => { + let eth = EthereumTransactionCommon::decode(&tx)?; + Ok(Self::EthereumDelayed(eth)) + } + FA_DEPOSIT_TX_TAG => { + let fa_deposit = FaDeposit::decode(&tx)?; + Ok(Self::FaDeposit(fa_deposit)) + } + _ => Err(DecoderError::Custom("Unknown transaction tag.")), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct Transaction { + pub tx_hash: TransactionHash, + pub content: TransactionContent, +} + +impl Transaction { + pub fn data_size(&self) -> u64 { + match &self.content { + TransactionContent::Deposit(_) => 0, + TransactionContent::Ethereum(e) | TransactionContent::EthereumDelayed(e) => { + // FIXME: probably need to take into account the access list + e.data.len() as u64 + } + TransactionContent::FaDeposit(_) => 0, + } + } + + pub fn is_delayed(&self) -> bool { + match &self.content { + TransactionContent::Deposit(_) + | TransactionContent::EthereumDelayed(_) + | TransactionContent::FaDeposit(_) => true, + TransactionContent::Ethereum(_) => false, + } + } + + pub fn execution_gas_limit(&self, fees: &BlockFees) -> Result { + match &self.content { + TransactionContent::Deposit(_) => Ok(BASE_GAS), + TransactionContent::Ethereum(e) => tx_execution_gas_limit(e, fees, false), + TransactionContent::EthereumDelayed(e) => { + tx_execution_gas_limit(e, fees, true) + } + TransactionContent::FaDeposit(_) => Ok(FA_DEPOSIT_PROXY_GAS_LIMIT), + } + } +} + +impl Encodable for Transaction { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append_iter(self.tx_hash); + stream.append(&self.content); + } +} + +impl Decodable for Transaction { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + let mut it = decoder.iter(); + let tx_hash: TransactionHash = decode_tx_hash(next(&mut it)?)?; + let content: TransactionContent = + decode_field(&next(&mut it)?, "Transaction content")?; + Ok(Transaction { tx_hash, content }) + } +} + +impl Transaction { + pub fn type_(&self) -> TransactionType { + match &self.content { + // The deposit is considered arbitrarily as a legacy transaction + TransactionContent::Deposit(_) | TransactionContent::FaDeposit(_) => { + TransactionType::Legacy + } + TransactionContent::Ethereum(tx) + | TransactionContent::EthereumDelayed(tx) => tx.type_, + } + } +} + +#[derive(Debug, PartialEq)] +pub struct ProxyInboxContent { + pub transactions: Vec, +} + +pub fn read_input( + host: &mut Host, + smart_rollup_address: [u8; 20], + tezos_contracts: &TezosContracts, + inbox_is_empty: &mut bool, + parsing_context: &mut Mode::Context, + enable_fa_deposits: bool, +) -> Result, Error> { + let input = host.read_input()?; + + match input { + Some(input) => { + *inbox_is_empty = false; + Ok(InputResult::parse( + host, + input, + smart_rollup_address, + tezos_contracts, + parsing_context, + enable_fa_deposits, + )) + } + None => Ok(InputResult::NoInput), + } +} + +/// The InputHandler abstracts how the input is handled once it has been parsed. +pub trait InputHandler +where + Self: Parsable, +{ + /// Abstracts the type used to store the inputs once handled + type Inbox; + + fn handle_input( + host: &mut Host, + input: Self, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()>; + + fn handle_deposit( + host: &mut Host, + deposit: Deposit, + chain_id: Option, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()>; + + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + chain_id: Option, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()>; +} + +impl InputHandler for ProxyInput { + // In case of the proxy, the Inbox is unchanged: we keep the InboxContent as + // everything is doable in a single kernel_run. + type Inbox = ProxyInboxContent; + + fn handle_input( + host: &mut Host, + input: Self, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()> { + match input { + Self::SimpleTransaction(tx) => inbox_content.transactions.push(*tx), + Self::NewChunkedTransaction { + tx_hash, + num_chunks, + chunk_hashes, + } => create_chunked_transaction(host, &tx_hash, num_chunks, chunk_hashes)?, + Self::TransactionChunk { + tx_hash, + i, + chunk_hash, + data, + } => { + if let Some(tx) = + handle_transaction_chunk(host, tx_hash, i, chunk_hash, data)? + { + inbox_content.transactions.push(tx) + } + } + } + Ok(()) + } + + fn handle_deposit( + host: &mut Host, + deposit: Deposit, + _chain_id: Option, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()> { + inbox_content + .transactions + .push(handle_deposit(host, deposit)?); + Ok(()) + } + + #[cfg_attr(feature = "benchmark", inline(never))] + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + _chain_id: Option, + inbox_content: &mut Self::Inbox, + ) -> anyhow::Result<()> { + inbox_content + .transactions + .push(handle_fa_deposit(host, fa_deposit)?); + Ok(()) + } +} + +fn handle_blueprint_chunk( + host: &mut Host, + blueprint: UnsignedSequencerBlueprint, +) -> anyhow::Result<()> { + log!(host, Benchmarking, "Handling a blueprint input"); + log!( + host, + Debug, + "Storing chunk {} of sequencer blueprint number {}", + blueprint.chunk_index, + blueprint.number + ); + store_sequencer_blueprint(host, blueprint).map_err(Error::into) +} + +impl InputHandler for SequencerInput { + // For the sequencer, inputs are stored directly in the storage. The delayed + // inbox represents part of the storage, but `Unit` would also be enough as + // there is nothing to return in the end. + type Inbox = DelayedInbox; + + fn handle_input( + host: &mut Host, + input: Self, + delayed_inbox: &mut Self::Inbox, + ) -> anyhow::Result<()> { + match input { + Self::DelayedInput(tx) => { + let previous_timestamp = read_last_info_per_level_timestamp(host)?; + let level = read_l1_level(host)?; + log!(host, Benchmarking, "Handling a delayed input"); + delayed_inbox.save_transaction(host, *tx, previous_timestamp, level) + } + Self::SequencerBlueprint(SequencerBlueprint(seq_blueprint)) => { + handle_blueprint_chunk(host, seq_blueprint) + } + Self::SequencerBlueprint( + InvalidNumberOfChunks | InvalidSignature | InvalidNumber | Unparsable, + ) => { + log!( + host, + Debug, + "Sequencer blueprint refused because: {:?}", + input + ); + Ok(()) + } + Self::DalSlotImportSignals(DalSlotImportSignals { + signals, + signature: _, + }) => { + log!(host, Debug, "Importing {} DAL signals", &signals.0.len()); + let params = host.reveal_dal_parameters(); + let head_level: Option = + crate::block_storage::read_current_number(host).ok(); + for signal in signals.0.iter() { + let published_level = signal.published_level; + let slot_indices = &signal.slot_indices; + for slot_index in slot_indices.0.iter() { + log!( + host, + Debug, + "Handling a signal for slot index {} and published_level {}", + slot_index, + published_level + ); + if let Some(unsigned_seq_blueprints) = + fetch_and_parse_sequencer_blueprint_from_dal( + host, + ¶ms, + &head_level, + *slot_index, + published_level, + ) + { + log!( + host, + Debug, + "DAL slot is a blueprint of {} chunks", + unsigned_seq_blueprints.len() + ); + for chunk in unsigned_seq_blueprints { + handle_blueprint_chunk(host, chunk)? + } + } + } + } + Ok(()) + } + } + } + + fn handle_deposit( + host: &mut Host, + deposit: Deposit, + _chain_id: Option, + delayed_inbox: &mut Self::Inbox, + ) -> anyhow::Result<()> { + let previous_timestamp = read_last_info_per_level_timestamp(host)?; + let level = read_l1_level(host)?; + let tx = handle_deposit(host, deposit)?; + delayed_inbox.save_transaction(host, tx, previous_timestamp, level) + } + + #[cfg_attr(feature = "benchmark", inline(never))] + fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, + _chain_id: Option, + delayed_inbox: &mut Self::Inbox, + ) -> anyhow::Result<()> { + let previous_timestamp = read_last_info_per_level_timestamp(host)?; + let level = read_l1_level(host)?; + let tx = handle_fa_deposit(host, fa_deposit)?; + delayed_inbox.save_transaction(host, tx, previous_timestamp, level) + } +} + +fn handle_transaction_chunk( + host: &mut Host, + tx_hash: TransactionHash, + i: u16, + chunk_hash: TransactionHash, + data: Vec, +) -> Result, Error> { + // If the number of chunks doesn't exist in the storage, the chunked + // transaction wasn't created, so the chunk is ignored. + let num_chunks = match chunked_transaction_num_chunks(host, &tx_hash) { + Ok(x) => x, + Err(_) => { + log!( + host, + Info, + "Ignoring chunk {} of unknown transaction {}", + i, + hex::encode(tx_hash) + ); + return Ok(None); + } + }; + // Checks that the transaction is not out of bounds. + if i >= num_chunks { + return Ok(None); + } + // Check if the chunk hash is part of the announced chunked hashes. + let chunked_transaction_path = chunked_transaction_path(&tx_hash)?; + let chunk_hash_path = + chunked_hash_transaction_path(&chunk_hash, &chunked_transaction_path)?; + if host.store_read(&chunk_hash_path, 0, 0).is_err() { + return Ok(None); + } + // Sanity check to verify that the transaction chunk uses the maximum + // space capacity allowed. + if i != num_chunks - 1 && data.len() < MAX_SIZE_PER_CHUNK { + remove_chunked_transaction(host, &tx_hash)?; + return Ok(None); + }; + // When the transaction is stored in the storage, it returns the full transaction + // if `data` was the missing chunk. + if let Some(data) = store_transaction_chunk(host, &tx_hash, i, data)? { + let full_data_hash: [u8; TRANSACTION_HASH_SIZE] = Keccak256::digest(&data).into(); + if full_data_hash == tx_hash { + if let Ok(tx) = EthereumTransactionCommon::from_bytes(&data) { + let content = TransactionContent::Ethereum(tx); + return Ok(Some(Transaction { tx_hash, content })); + } + } + } + Ok(None) +} + +fn handle_deposit( + host: &mut Host, + deposit: Deposit, +) -> Result { + let seed = host.reveal_metadata().raw_rollup_address; + let tx_hash = deposit.hash(&seed).into(); + Ok(Transaction { + tx_hash, + content: TransactionContent::Deposit(deposit), + }) +} + +#[cfg_attr(feature = "benchmark", inline(never))] +fn handle_fa_deposit( + host: &mut Host, + fa_deposit: FaDeposit, +) -> Result { + let seed = host.reveal_metadata().raw_rollup_address; + let tx_hash = fa_deposit.hash(&seed).into(); + Ok(Transaction { + tx_hash, + content: TransactionContent::FaDeposit(fa_deposit), + }) +} + +fn force_kernel_upgrade(host: &mut impl Runtime) -> anyhow::Result<()> { + match upgrade::read_kernel_upgrade(host)? { + Some(kernel_upgrade) => { + let current_timestamp = read_last_info_per_level_timestamp(host)?.i64(); + let activation_timestamp = kernel_upgrade.activation_timestamp.i64(); + + if current_timestamp >= (activation_timestamp + 86400i64) { + // If the kernel upgrade still exist 1 day after it was supposed + // to be activated. It is possible to force its execution. + upgrade::upgrade(host, kernel_upgrade.preimage_hash)? + }; + Ok(()) + } + None => Ok(()), + } +} + +pub fn handle_input( + host: &mut impl Runtime, + input: Input, + inbox_content: &mut Mode::Inbox, + garbage_collect_blocks: bool, +) -> anyhow::Result<()> { + match input { + Input::ModeSpecific(input) => Mode::handle_input(host, input, inbox_content)?, + Input::Upgrade(kernel_upgrade) => store_kernel_upgrade(host, &kernel_upgrade)?, + Input::SequencerUpgrade(sequencer_upgrade) => { + store_sequencer_upgrade(host, sequencer_upgrade)? + } + Input::RemoveSequencer => remove_sequencer(host)?, + Input::Info(info) => { + // New inbox level detected, remove all previous events. + clear_events(host)?; + if garbage_collect_blocks { + crate::block_storage::garbage_collect_blocks(host)?; + } + store_last_info_per_level_timestamp(host, info.info.predecessor_timestamp)?; + store_l1_level(host, info.level)? + } + Input::Deposit((deposit, chain_id)) => { + Mode::handle_deposit(host, deposit, chain_id, inbox_content)? + } + Input::FaDeposit((fa_deposit, chain_id)) => { + Mode::handle_fa_deposit(host, fa_deposit, chain_id, inbox_content)? + } + Input::ForceKernelUpgrade => force_kernel_upgrade(host)?, + } + Ok(()) +} + +enum ReadStatus { + FinishedIgnore, + FinishedRead, + Ongoing, +} + +#[allow(clippy::too_many_arguments)] +fn read_and_dispatch_input( + host: &mut Host, + smart_rollup_address: [u8; 20], + tezos_contracts: &TezosContracts, + parsing_context: &mut Mode::Context, + inbox_is_empty: &mut bool, + res: &mut Mode::Inbox, + enable_fa_bridge: bool, + garbage_collect_blocks: bool, +) -> anyhow::Result { + let input: InputResult = read_input( + host, + smart_rollup_address, + tezos_contracts, + inbox_is_empty, + parsing_context, + enable_fa_bridge, + )?; + match input { + InputResult::NoInput => { + if *inbox_is_empty { + // If `inbox_is_empty` is true, that means we haven't see + // any input in the current call of `read_inbox`. Therefore, + // the inbox of this level has already been consumed. + Ok(ReadStatus::FinishedIgnore) + } else { + // If it's a `NoInput` and `inbox_is_empty` is false, we + // have simply reached the end of the inbox. + Ok(ReadStatus::FinishedRead) + } + } + InputResult::Unparsable => Ok(ReadStatus::Ongoing), + InputResult::Simulation => { + // kernel enters in simulation mode, reading will be done by the + // simulation and all the previous and next transactions are + // discarded. + simulation::start_simulation_mode(host, enable_fa_bridge)?; + Ok(ReadStatus::FinishedIgnore) + } + InputResult::Input(input) => { + handle_input(host, input, res, garbage_collect_blocks)?; + Ok(ReadStatus::Ongoing) + } + } +} + +pub fn read_proxy_inbox( + host: &mut Host, + smart_rollup_address: [u8; 20], + tezos_contracts: &TezosContracts, + enable_fa_bridge: bool, + garbage_collect_blocks: bool, +) -> Result, anyhow::Error> { + let mut res = ProxyInboxContent { + transactions: vec![], + }; + // The mutable variable is used to retrieve the information of whether the + // inbox was empty or not. As we consume all the inbox in one go, if the + // variable remains true, that means that the inbox was already consumed + // during this kernel run. + let mut inbox_is_empty = true; + loop { + match read_and_dispatch_input::( + host, + smart_rollup_address, + tezos_contracts, + &mut (), + &mut inbox_is_empty, + &mut res, + enable_fa_bridge, + garbage_collect_blocks, + ) { + Err(err) => + // If we failed to read or dispatch the input. + // We allow ourselves to continue with the inbox consumption. + // In order to make sure we can retrieve any kernel upgrade + // present in the inbox. + { + log!( + host, + Fatal, + "An input made `read_and_dispatch_input` fail, we ignore it ({:?})", + err + ) + } + Ok(ReadStatus::Ongoing) => (), + Ok(ReadStatus::FinishedRead) => return Ok(Some(res)), + Ok(ReadStatus::FinishedIgnore) => return Ok(None), + } + } +} + +/// The StageOne can yield with three possible states: +/// +/// - Done: the inbox has been fully read during the current `kernel_run` +/// +/// - Reboot: the inbox cannot been read further as there are not enough ticks +/// and needs a reboot before continuing. This is only supported in sequencer +/// mode as the inputs are stored directly in the process. +/// +/// - Skipped: the inbox was empty during the current `kernel_run`, implying it +/// has been emptied during a previous `kernel_run` and the kernel is +/// currently processing blueprints. It prevents the automatic reboot after +/// completing the stage one to start the block production, and producing an +/// empty blueprint in proxy mode. +pub enum StageOneStatus { + Done, + Reboot, + Skipped, +} + +#[allow(clippy::too_many_arguments)] +pub fn read_sequencer_inbox( + host: &mut Host, + smart_rollup_address: [u8; 20], + tezos_contracts: &TezosContracts, + delayed_bridge: ContractKt1Hash, + sequencer: PublicKey, + delayed_inbox: &mut DelayedInbox, + enable_fa_bridge: bool, + dal: Option, + garbage_collect_blocks: bool, +) -> Result { + // The mutable variable is used to retrieve the information of whether the + // inbox was empty or not. As we consume all the inbox in one go, if the + // variable remains true, that means that the inbox was already consumed + // during this kernel run. + let mut inbox_is_empty = true; + let limits = fetch_limits(host); + let head_level: Option = crate::block_storage::read_current_number(host).ok(); + let mut parsing_context = SequencerParsingContext { + sequencer, + delayed_bridge, + allocated_ticks: limits + .maximum_allowed_ticks + .saturating_sub(TICKS_FOR_BLUEPRINT_INTERCEPT), + dal_configuration: dal, + buffer_transaction_chunks: None, + head_level, + }; + loop { + // Checks there will be enough ticks to handle at least another chunk of + // full size. If it is not the case, asks for reboot. + if parsing_context.allocated_ticks < maximum_ticks_for_sequencer_chunk() { + log!( + host, + Benchmarking, + "Estimated ticks: {}", + limits + .maximum_allowed_ticks + .saturating_sub(parsing_context.allocated_ticks) + ); + return Ok(StageOneStatus::Reboot); + }; + match read_and_dispatch_input::( + host, + smart_rollup_address, + tezos_contracts, + &mut parsing_context, + &mut inbox_is_empty, + delayed_inbox, + enable_fa_bridge, + garbage_collect_blocks, + ) { + Err(err) => + // If we failed to read or dispatch the input. + // We allow ourselves to continue with the inbox consumption. + // In order to make sure we can retrieve any kernel upgrade + // present in the inbox. + { + log!( + host, + Fatal, + "An input made `read_and_dispatch_input` fail, we ignore it ({:?})", + err + ) + } + Ok(ReadStatus::Ongoing) => (), + Ok(ReadStatus::FinishedRead) => { + log!( + host, + Benchmarking, + "Estimated ticks: {}", + limits + .maximum_allowed_ticks + .saturating_sub(parsing_context.allocated_ticks) + ); + return Ok(StageOneStatus::Done); + } + Ok(ReadStatus::FinishedIgnore) => return Ok(StageOneStatus::Skipped), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::blueprint_storage::blueprint_path; + use crate::configuration::TezosContracts; + use crate::dal_slot_import_signal::{ + DalSlotIndicesList, DalSlotIndicesOfLevel, UnsignedDalSlotSignals, + }; + use crate::inbox::TransactionContent::Ethereum; + use crate::parsing::RollupType; + use crate::storage::*; + use primitive_types::U256; + use std::fmt::Write; + use tezos_crypto_rs::hash::SmartRollupHash; + use tezos_crypto_rs::hash::UnknownSignature; + use tezos_crypto_rs::hash::{HashTrait, SecretKeyEd25519}; + use tezos_data_encoding::types::Bytes; + use tezos_ethereum::transaction::TRANSACTION_HASH_SIZE; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; + use tezos_smart_rollup_debug::Runtime; + use tezos_smart_rollup_encoding::contract::Contract; + use tezos_smart_rollup_encoding::inbox::ExternalMessageFrame; + use tezos_smart_rollup_encoding::michelson::{MichelsonBytes, MichelsonOr}; + use tezos_smart_rollup_encoding::public_key_hash::PublicKeyHash; + use tezos_smart_rollup_encoding::smart_rollup::SmartRollupAddress; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + use tezos_smart_rollup_mock::TransferMetadata; + + const SMART_ROLLUP_ADDRESS: [u8; 20] = [ + 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1, + ]; + + const ZERO_TX_HASH: TransactionHash = [0; TRANSACTION_HASH_SIZE]; + + fn smart_rollup_address() -> SmartRollupAddress { + SmartRollupAddress::new( + SmartRollupHash::try_from_bytes(&SMART_ROLLUP_ADDRESS).unwrap(), + ) + } + + fn input_to_bytes( + smart_rollup_address: [u8; 20], + input: Input, + ) -> Vec { + let mut buffer = Vec::new(); + // Targetted framing protocol + buffer.push(0); + buffer.extend_from_slice(&smart_rollup_address); + match input { + Input::ModeSpecific(ProxyInput::SimpleTransaction(tx)) => { + // Simple transaction tag + buffer.push(0); + buffer.extend_from_slice(&tx.tx_hash); + let mut tx_bytes = match tx.content { + Ethereum(tx) => tx.into(), + _ => panic!( + "Simple transaction can contain only ethereum transactions" + ), + }; + + buffer.append(&mut tx_bytes) + } + Input::ModeSpecific(ProxyInput::NewChunkedTransaction { + tx_hash, + num_chunks, + chunk_hashes, + }) => { + // New chunked transaction tag + buffer.push(1); + buffer.extend_from_slice(&tx_hash); + buffer.extend_from_slice(&u16::to_le_bytes(num_chunks)); + for chunk_hash in chunk_hashes.iter() { + buffer.extend_from_slice(chunk_hash) + } + } + Input::ModeSpecific(ProxyInput::TransactionChunk { + tx_hash, + i, + chunk_hash, + data, + }) => { + // Transaction chunk tag + buffer.push(2); + buffer.extend_from_slice(&tx_hash); + buffer.extend_from_slice(&u16::to_le_bytes(i)); + buffer.extend_from_slice(&chunk_hash); + buffer.extend_from_slice(&data); + } + _ => (), + }; + buffer + } + + fn make_chunked_transactions( + tx_hash: TransactionHash, + data: Vec, + ) -> Vec> { + let mut chunk_hashes = vec![]; + let mut chunks: Vec> = data + .chunks(MAX_SIZE_PER_CHUNK) + .enumerate() + .map(|(i, bytes)| { + let data = bytes.to_vec(); + let chunk_hash = Keccak256::digest(&data).into(); + chunk_hashes.push(chunk_hash); + Input::ModeSpecific(ProxyInput::TransactionChunk { + tx_hash, + i: i as u16, + chunk_hash, + data, + }) + }) + .collect(); + let number_of_chunks = chunks.len() as u16; + + let new_chunked_transaction = + Input::ModeSpecific(ProxyInput::NewChunkedTransaction { + tx_hash, + num_chunks: number_of_chunks, + chunk_hashes, + }); + + let mut buffer = Vec::new(); + buffer.push(new_chunked_transaction); + buffer.append(&mut chunks); + buffer + } + + fn large_transaction() -> (Vec, EthereumTransactionCommon) { + let data: Vec = hex::decode(["f917e180843b9aca0082520894b53dc01974176e5dff2298c5a94343c2585e3c548a021dfe1f5c5363780000b91770".to_string(), "a".repeat(12_000), "820a96a07fd9567a72223bbc8f70bd2b42011339b61044d16b5a2233534db8ca01f3e57aa03ea489c4bb2b2b52f3c1a18966881114767654c9ab61d46b1fbff78a498043c2".to_string()].join("")).unwrap(); + let tx = EthereumTransactionCommon::from_bytes(&data).unwrap(); + (data, tx) + } + + #[test] + fn parse_valid_simple_transaction() { + let mut host = MockKernelHost::default(); + + let tx_bytes = &hex::decode("f86d80843b9aca00825208940b52d4d3be5d18a7ab5e4476a2f5382bbf2b38d888016345785d8a000080820a95a0d9ef1298c18c88604e3f08e14907a17dfa81b1dc6b37948abe189d8db5cb8a43a06fc7040a71d71d3cb74bd05ead7046b10668ad255da60391c017eea31555f156").unwrap(); + let tx = EthereumTransactionCommon::from_bytes(tx_bytes).unwrap(); + let tx_hash = Keccak256::digest(tx_bytes).into(); + let input = + Input::ModeSpecific(ProxyInput::SimpleTransaction(Box::new(Transaction { + tx_hash, + content: Ethereum(tx.clone()), + }))); + + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))); + + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap() + .unwrap(); + let expected_transactions = vec![Transaction { + tx_hash, + content: Ethereum(tx), + }]; + assert_eq!(inbox_content.transactions, expected_transactions); + } + + #[test] + fn parse_valid_chunked_transaction() { + let address = smart_rollup_address(); + let mut host = MockKernelHost::with_address(address); + + let (data, tx) = large_transaction(); + let tx_hash: [u8; TRANSACTION_HASH_SIZE] = Keccak256::digest(data.clone()).into(); + + let inputs = make_chunked_transactions(tx_hash, data); + + for input in inputs { + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) + } + + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap() + .unwrap(); + let expected_transactions = vec![Transaction { + tx_hash, + content: Ethereum(tx), + }]; + assert_eq!(inbox_content.transactions, expected_transactions); + } + + #[test] + fn parse_valid_kernel_upgrade() { + let mut host = MockKernelHost::default(); + + // Prepare the upgrade's payload + let preimage_hash: [u8; PREIMAGE_HASH_SIZE] = hex::decode( + "004b28109df802cb1885ab29461bc1b410057a9f3a848d122ac7a742351a3a1f4e", + ) + .unwrap() + .try_into() + .unwrap(); + let activation_timestamp = Timestamp::from(0i64); + + let kernel_upgrade = KernelUpgrade { + preimage_hash, + activation_timestamp, + }; + let kernel_upgrade_payload = kernel_upgrade.rlp_bytes().to_vec(); + + // Create a transfer from the bridge contract, that act as the + // dictator (or administrator). + let source = + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(); + let contract = + Contract::from_b58check("KT1HJphVV3LUxqZnc7YSH6Zdfd3up1DjLqZv").unwrap(); + let sender = match contract { + Contract::Originated(kt1) => kt1, + _ => panic!("The contract must be a KT1"), + }; + let payload: RollupType = + MichelsonOr::Right(MichelsonBytes(kernel_upgrade_payload)); + + let transfer_metadata = TransferMetadata::new(sender.clone(), source); + host.host.add_transfer(payload, &transfer_metadata); + let _inbox_content = read_proxy_inbox( + &mut host, + [0; 20], + &TezosContracts { + ticketer: None, + admin: Some(sender), + sequencer_governance: None, + kernel_governance: None, + kernel_security_governance: None, + }, + false, + false, + ) + .unwrap() + .unwrap(); + let expected_upgrade = Some(KernelUpgrade { + preimage_hash, + activation_timestamp, + }); + + let stored_kernel_upgrade = crate::upgrade::read_kernel_upgrade(&host).unwrap(); + assert_eq!(stored_kernel_upgrade, expected_upgrade); + } + + #[test] + // Assert that trying to create a chunked transaction has no impact. Only + // the first `NewChunkedTransaction` should be considered. + fn recreate_chunked_transaction() { + let mut host = MockKernelHost::default(); + + let chunk_hashes = vec![[1; TRANSACTION_HASH_SIZE], [2; TRANSACTION_HASH_SIZE]]; + let tx_hash = [0; TRANSACTION_HASH_SIZE]; + let new_chunk1 = Input::ModeSpecific(ProxyInput::NewChunkedTransaction { + tx_hash, + num_chunks: 2, + chunk_hashes: chunk_hashes.clone(), + }); + let new_chunk2 = Input::ModeSpecific(ProxyInput::NewChunkedTransaction { + tx_hash, + num_chunks: 42, + chunk_hashes, + }); + + host.host.add_external(Bytes::from(input_to_bytes( + SMART_ROLLUP_ADDRESS, + new_chunk1, + ))); + host.host.add_external(Bytes::from(input_to_bytes( + SMART_ROLLUP_ADDRESS, + new_chunk2, + ))); + + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap(); + + let num_chunks = chunked_transaction_num_chunks(&mut host, &tx_hash) + .expect("The number of chunks should exist"); + // Only the first `NewChunkedTransaction` should be considered. + assert_eq!(num_chunks, 2); + } + + #[test] + // Assert that an out of bound chunk is simply ignored and does + // not make the kernel fail. + fn out_of_bound_chunk_is_ignored() { + let mut host = MockKernelHost::default(); + + let (data, _tx) = large_transaction(); + let tx_hash = ZERO_TX_HASH; + + let mut inputs = make_chunked_transactions(tx_hash, data); + let new_chunk = inputs.remove(0); + let chunk = inputs.remove(0); + + // Announce a chunked transaction. + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, new_chunk))); + + // Give a chunk with an invalid `i`. + let out_of_bound_i = 42; + let chunk = match chunk { + Input::ModeSpecific(ProxyInput::TransactionChunk { + tx_hash, + i: _, + chunk_hash, + data, + }) => Input::ModeSpecific(ProxyInput::TransactionChunk { + tx_hash, + i: out_of_bound_i, + chunk_hash, + data, + }), + _ => panic!("Expected a transaction chunk"), + }; + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); + + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap(); + + // The out of bounds chunk should not exist. + let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); + let transaction_chunk_path = + transaction_chunk_path(&chunked_transaction_path, out_of_bound_i).unwrap(); + if read_transaction_chunk_data(&mut host, &transaction_chunk_path).is_ok() { + panic!("The chunk should not exist in the storage") + } + } + + #[test] + // Assert that an unknown chunk is simply ignored and does + // not make the kernel fail. + fn unknown_chunk_is_ignored() { + let mut host = MockKernelHost::default(); + + let (data, _tx) = large_transaction(); + let tx_hash = ZERO_TX_HASH; + + let mut inputs = make_chunked_transactions(tx_hash, data); + let chunk = inputs.remove(1); + + // Extract the index of the non existing chunked transaction. + let index = match chunk { + Input::ModeSpecific(ProxyInput::TransactionChunk { i, .. }) => i, + _ => panic!("Expected a transaction chunk"), + }; + + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk))); + + let _inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap(); + + // The unknown chunk should not exist. + let chunked_transaction_path = chunked_transaction_path(&tx_hash).unwrap(); + let transaction_chunk_path = + transaction_chunk_path(&chunked_transaction_path, index).unwrap(); + if read_transaction_chunk_data(&mut host, &transaction_chunk_path).is_ok() { + panic!("The chunk should not exist in the storage") + } + } + + #[test] + // Assert that a transaction is marked as complete only when each chunk + // is stored in the storage. That is, if a transaction chunk is sent twice, + // it rewrites the chunk. + // + // This serves as a non-regression test, a previous optimization made unwanted + // behavior for very little gain: + // + // Level 0: + // - New chunk of size 2 + // - Chunk 0 + // + // Level 1: + // - New chunk of size 2 (ignored) + // - Chunk 0 + // |--> Oh great! I have the two chunks for my transaction, it is then complete! + // - Chunk 1 + // |--> Fails because the chunk is unknown + fn transaction_is_complete_when_each_chunk_is_stored() { + let mut host = MockKernelHost::default(); + + let (data, tx) = large_transaction(); + let tx_hash: [u8; TRANSACTION_HASH_SIZE] = Keccak256::digest(data.clone()).into(); + + let inputs = make_chunked_transactions(tx_hash, data); + // The test works if there are 3 inputs: new chunked of size 2, first and second + // chunks. + assert_eq!(inputs.len(), 3); + + let new_chunk = inputs[0].clone(); + let chunk0 = inputs[1].clone(); + + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, new_chunk))); + + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, chunk0))); + + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap() + .unwrap(); + assert_eq!( + inbox_content, + ProxyInboxContent { + transactions: vec![], + } + ); + + // On the next level, try to re-give the chunks, but this time in full: + for input in inputs { + host.host + .add_external(Bytes::from(input_to_bytes(SMART_ROLLUP_ADDRESS, input))) + } + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap() + .unwrap(); + + let expected_transactions = vec![Transaction { + tx_hash, + content: Ethereum(tx), + }]; + assert_eq!(inbox_content.transactions, expected_transactions); + } + + #[test] + fn parse_valid_simple_transaction_framed() { + // Don't use zero-hash for rollup here - as the long string of zeros is still valid under the previous + // parsing. This won't happen in practice, though + let address = smart_rollup_address(); + + let mut host = MockKernelHost::with_address(address.clone()); + + let tx_bytes = &hex::decode("f86d80843b9aca00825208940b52d4d3be5d18a7ab5\ + e4476a2f5382bbf2b38d888016345785d8a000080820a95a0d9ef1298c18c88604e3f08e14907a17dfa81b1dc6b37948abe189d8db5cb8a43a06\ + fc7040a71d71d3cb74bd05ead7046b10668ad255da60391c017eea31555f156").unwrap(); + let tx_hash = Keccak256::digest(tx_bytes).into(); + let tx = EthereumTransactionCommon::from_bytes(tx_bytes).unwrap(); + + let input = + Input::ModeSpecific(ProxyInput::SimpleTransaction(Box::new(Transaction { + tx_hash, + content: Ethereum(tx.clone()), + }))); + + let mut buffer = Vec::new(); + match input { + Input::ModeSpecific(ProxyInput::SimpleTransaction(tx)) => { + // Simple transaction tag + buffer.push(0); + buffer.extend_from_slice(&tx.tx_hash); + let mut tx_bytes = match tx.content { + Ethereum(tx) => tx.into(), + _ => panic!( + "Simple transaction can contain only ethereum transactions" + ), + }; + + buffer.append(&mut tx_bytes) + } + _ => unreachable!("Not tested"), + }; + + let framed = ExternalMessageFrame::Targetted { + address, + contents: buffer, + }; + + host.host.add_external(framed); + + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap() + .unwrap(); + let expected_transactions = vec![Transaction { + tx_hash, + content: Ethereum(tx), + }]; + assert_eq!(inbox_content.transactions, expected_transactions); + } + + #[test] + fn empty_inbox_returns_none() { + let mut host = MockKernelHost::default(); + + // Even reading the inbox with only the default elements returns + // an empty inbox content. As we test in isolation there is nothing + // in the inbox, we mock it by adding a single input. + host.host.add_external(Bytes::from(vec![])); + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap(); + assert!(inbox_content.is_some()); + + // Reading again the inbox returns no inbox content at all. + let inbox_content = read_proxy_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + false, + false, + ) + .unwrap(); + assert!(inbox_content.is_none()); + } + + fn bytes_to_hex(bytes: &[u8]) -> String { + bytes.iter().fold(String::new(), |mut acc, &b| { + write!(acc, "{:02x}", b).expect("Failed to write to string"); + acc + }) + } + + #[test] + fn rlp_encode_decode_dal_slot_signals_with_signature() { + let signal_1 = DalSlotIndicesOfLevel { + published_level: 100, + slot_indices: DalSlotIndicesList(vec![1, 2, 3]), + }; + let signal_2 = DalSlotIndicesOfLevel { + published_level: 200, + slot_indices: DalSlotIndicesList(vec![4, 2, 6]), + }; + let signal_3 = DalSlotIndicesOfLevel { + published_level: 100, + slot_indices: DalSlotIndicesList(vec![10, 2, 5]), + }; + + let signals = UnsignedDalSlotSignals(vec![signal_1, signal_2, signal_3]); + + let signature = UnknownSignature::from_base58_check( + "sigdGBG68q2vskMuac4AzyNb1xCJTfuU8MiMbQtmZLUCYydYrtTd5Lessn1EFLTDJzjXoYxRasZxXbx6tHnirbEJtikcMHt3" + ).expect("signature decoding should work"); + + let dal_slot_signal_list = DalSlotImportSignals { signals, signature }; + + println!("Initial dal_slot_signal_list: {:?}", dal_slot_signal_list); + + // Encode the structure + let encoded = rlp::encode(&dal_slot_signal_list); + let encoded_hex = bytes_to_hex(&encoded); + + println!("Encoded DAL slot signal (hex): {}", encoded_hex); + + // Decode the structure + let decoded: DalSlotImportSignals = + rlp::decode(&encoded).expect("RLP decoding should succeed."); + + println!("Decoded dal_slot_signal_list: {:?}", decoded); + + // Ensure that the encoded and decoded structures match + assert_eq!(dal_slot_signal_list, decoded); + } + + fn insert_blueprint_and_read_inbox( + head_level: U256, + sk: &SecretKeyEd25519, + pk: &PublicKey, + unsigned_blueprint: &UnsignedSequencerBlueprint, + ) -> bool { + // Prepare the host. + let mut host = MockKernelHost::default(); + let address = smart_rollup_address(); + crate::block_storage::internal_for_tests::store_current_number( + &mut host, head_level, + ) + .unwrap(); + + // Prepare the blueprint. + let mut blueprint_bytes = + crate::parsing::tests::sequencer_signed_blueprint_chunk_bytes( + unsigned_blueprint, + sk.clone(), + ); + blueprint_bytes.insert(0, 3); + let framed = ExternalMessageFrame::Targetted { + address: address.clone(), + contents: blueprint_bytes, + }; + // Add to the inbox. + host.host.add_external(framed); + // Consume the inbox + let mut delayed_inbox = DelayedInbox::new(&mut host).unwrap(); + let _ = read_sequencer_inbox( + &mut host, + SMART_ROLLUP_ADDRESS, + &TezosContracts::default(), + ContractKt1Hash::from_b58check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(), + pk.clone(), + &mut delayed_inbox, + false, + None, + false, + ) + .unwrap(); + + let path = blueprint_path(unsigned_blueprint.number).unwrap(); + // The blueprint was valid if it was stored in the storage. + host.host.store_has(&path).unwrap().is_some() + } + + #[test] + fn test_read_sequencer_inbox_blueprint_chunk() { + let head_level: U256 = U256::from(6); + + // Pick a sequencer public key. + let pk = PublicKey::from_b58check( + "edpkv4NmL2YPe8eiVGXUDXmPQybD725ofKirTzGRxs1X9UmaG3voKw", + ) + .unwrap(); + + // This is the secret key associated to the sequencer key in + // this test. + let valid_sk = SecretKeyEd25519::from_base58_check( + "edsk422LGdmDnai4Cya6csM6oFmgHpDQKUhatTURJRAY4h7NHNz9sz", + ) + .unwrap(); + + // Insert a valid blueprint chunk on level 7. + let blueprint = UnsignedSequencerBlueprint { + chunk: vec![], + number: 7.into(), + nb_chunks: 3, + chunk_index: 0, + chain_id: None, + }; + let is_valid = + insert_blueprint_and_read_inbox(head_level, &valid_sk, &pk, &blueprint); + assert!(is_valid); + + // Insert a blueprint chunk on level 7 incorrectly signed. + let invalid_sk = SecretKeyEd25519::from_base58_check( + "edsk37VEgDUMt7wje8vxfao55y7JhiamjTbVM1xABSCamFgtcUqhdT", + ) + .unwrap(); + let is_valid = + insert_blueprint_and_read_inbox(head_level, &invalid_sk, &pk, &blueprint); + assert!(!is_valid); + + // Insert a blueprint chunk on level 6. + let is_valid = insert_blueprint_and_read_inbox( + head_level, + &valid_sk, + &pk, + &UnsignedSequencerBlueprint { + number: 6.into(), + ..blueprint.clone() + }, + ); + assert!(!is_valid); + + // Insert a blueprint chunk on level 5. + let is_valid = insert_blueprint_and_read_inbox( + head_level, + &valid_sk, + &pk, + &UnsignedSequencerBlueprint { + number: 5.into(), + ..blueprint + }, + ); + assert!(!is_valid); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/lib.rs b/etherlink/kernel_calypso2/kernel/src/lib.rs new file mode 100644 index 000000000000..9fd12ca7016d --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/lib.rs @@ -0,0 +1,1016 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use crate::configuration::{fetch_configuration, Configuration}; +use crate::error::Error; +use crate::error::UpgradeProcessError::Fallback; +use crate::migration::storage_migration; +use crate::stage_one::fetch_blueprints; +use crate::storage::{read_sequencer_pool_address, PRIVATE_FLAG_PATH}; +use anyhow::Context; +use delayed_inbox::DelayedInbox; +use evm_execution::Config; +use fallback_upgrade::fallback_backup_kernel; +use inbox::StageOneStatus; +use migration::MigrationStatus; +use primitive_types::U256; +use reveal_storage::{is_revealed_storage, reveal_storage}; +use storage::{ + read_chain_id, read_da_fee, read_kernel_version, read_minimum_base_fee_per_gas, + read_tracer_input, store_chain_id, store_da_fee, store_kernel_version, + store_minimum_base_fee_per_gas, store_storage_version, STORAGE_VERSION, + STORAGE_VERSION_PATH, +}; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_evm_logging::{log, Level::*, Verbosity}; +use tezos_evm_runtime::internal_runtime::InternalRuntime; +use tezos_evm_runtime::runtime::{KernelHost, Runtime}; +use tezos_evm_runtime::safe_storage::WORLD_STATE_PATH; +use tezos_smart_rollup::michelson::MichelsonUnit; +use tezos_smart_rollup::outbox::{ + OutboxMessage, OutboxMessageWhitelistUpdate, OUTBOX_QUEUE, +}; +use tezos_smart_rollup_encoding::public_key::PublicKey; +use tezos_smart_rollup_host::runtime::ValueType; + +mod apply; +mod block; +mod block_in_progress; +mod block_storage; +mod blueprint; +mod blueprint_storage; +mod bridge; +mod configuration; +mod dal; +mod dal_slot_import_signal; +mod delayed_inbox; +mod error; +mod event; +pub mod evm_node_entrypoint; +mod fallback_upgrade; +mod fees; +mod gas_price; +mod inbox; +mod linked_list; +mod migration; +mod parsing; +mod reveal_storage; +mod sequencer_blueprint; +mod simulation; +mod stage_one; +mod storage; +mod tick_model; +mod upgrade; + +extern crate alloc; + +/// The chain id will need to be unique when the EVM rollup is deployed in +/// production. +pub const CHAIN_ID: u32 = 1337; + +/// The configuration for the EVM execution. +const CONFIG: Config = Config { + // The current implementation doesn't support Shanghai call stack limit of 256. + // We need to set a lower limit until we have switched to a head-based + // recursive calls. + // + // TODO: When this limitation is removed, some evm evaluation tests needs + // to be reactivated. As well as tests `call_too_deep_not_revert` and + // `multiple_call_all_the_way_to_1024` in the evm execution crate. + call_stack_limit: 256, + ..Config::shanghai() +}; + +const KERNEL_VERSION: &str = env!("GIT_HASH"); + +fn switch_to_public_rollup(host: &mut Host) -> Result<(), Error> { + if let Some(ValueType::Value) = host.store_has(&PRIVATE_FLAG_PATH)? { + log!( + host, + Info, + "Submitting outbox message to make the rollup public." + ); + let whitelist_update: OutboxMessage<_> = + OutboxMessage::::WhitelistUpdate( + OutboxMessageWhitelistUpdate { whitelist: None }, + ); + OUTBOX_QUEUE.queue_message(host, whitelist_update)?; + OUTBOX_QUEUE.flush_queue(host); + host.store_delete_value(&PRIVATE_FLAG_PATH) + .map_err(Error::from) + } else { + Ok(()) + } +} + +pub fn stage_zero(host: &mut Host) -> Result { + log!(host, Debug, "Entering stage zero."); + init_storage_versioning(host)?; + switch_to_public_rollup(host)?; + storage_migration(host) +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn stage_one( + host: &mut Host, + smart_rollup_address: [u8; 20], + configuration: &mut Configuration, +) -> Result { + log!(host, Debug, "Entering stage one."); + log!(host, Debug, "Configuration: {}", configuration); + + fetch_blueprints(host, smart_rollup_address, configuration) +} + +fn set_kernel_version(host: &mut Host) -> Result<(), Error> { + match read_kernel_version(host) { + Ok(kernel_version) => { + if kernel_version != KERNEL_VERSION { + store_kernel_version(host, KERNEL_VERSION)? + }; + Ok(()) + } + Err(_) => store_kernel_version(host, KERNEL_VERSION), + } +} + +fn init_storage_versioning(host: &mut Host) -> Result<(), Error> { + match host.store_read(&STORAGE_VERSION_PATH, 0, 0) { + Ok(_) => Ok(()), + Err(_) => store_storage_version(host, STORAGE_VERSION), + } +} + +fn retrieve_chain_id(host: &mut Host) -> Result { + match read_chain_id(host) { + Ok(chain_id) => Ok(chain_id), + Err(_) => { + let chain_id = U256::from(CHAIN_ID); + store_chain_id(host, chain_id)?; + Ok(chain_id) + } + } +} + +fn retrieve_minimum_base_fee_per_gas( + host: &mut Host, +) -> Result { + match read_minimum_base_fee_per_gas(host) { + Ok(minimum_base_fee_per_gas) => Ok(minimum_base_fee_per_gas), + Err(_) => { + let minimum_base_fee_per_gas = crate::fees::MINIMUM_BASE_FEE_PER_GAS.into(); + store_minimum_base_fee_per_gas(host, minimum_base_fee_per_gas)?; + Ok(minimum_base_fee_per_gas) + } + } +} + +#[cfg(test)] +fn retrieve_base_fee_per_gas( + host: &mut Host, + minimum_base_fee_per_gas: U256, +) -> U256 { + match block_storage::read_current(host) { + Ok(current_block) => { + let current_base_fee_per_gas = current_block.base_fee_per_gas; + if current_base_fee_per_gas < minimum_base_fee_per_gas { + minimum_base_fee_per_gas + } else { + current_base_fee_per_gas + } + } + Err(_) => minimum_base_fee_per_gas, + } +} + +fn retrieve_da_fee(host: &mut Host) -> Result { + match read_da_fee(host) { + Ok(da_fee) => Ok(da_fee), + Err(_) => { + let da_fee = U256::from(fees::DA_FEE_PER_BYTE); + store_da_fee(host, da_fee)?; + Ok(da_fee) + } + } +} + +#[cfg(test)] +fn retrieve_block_fees( + host: &mut Host, +) -> Result { + let minimum_base_fee_per_gas = retrieve_minimum_base_fee_per_gas(host)?; + let base_fee_per_gas = retrieve_base_fee_per_gas(host, minimum_base_fee_per_gas); + let da_fee = retrieve_da_fee(host)?; + let block_fees = tezos_ethereum::block::BlockFees::new( + minimum_base_fee_per_gas, + base_fee_per_gas, + da_fee, + ); + + Ok(block_fees) +} + +pub fn main(host: &mut Host) -> Result<(), anyhow::Error> { + let chain_id = retrieve_chain_id(host).context("Failed to retrieve chain id")?; + + // We always start by doing the migration if needed. + match stage_zero(host) { + Ok(MigrationStatus::None) => { + // No migration in progress. However as we want to have the kernel + // version written in the storage, we check for its existence + // at every kernel run. + // The alternative is to enforce every new kernels use the + // installer configuration to initialize this value. + set_kernel_version(host)?; + } + // If the migration is still in progress or was finished, we abort the + // current kernel run. + Ok(MigrationStatus::InProgress) => { + host.mark_for_reboot()?; + return Ok(()); + } + Ok(MigrationStatus::Done) => { + // If a migrtion was finished, we update the kernel version + // in the storage. + set_kernel_version(host)?; + host.mark_for_reboot()?; + let configuration = fetch_configuration(host); + log!( + host, + Info, + "Configuration after migration: {}", + configuration + ); + return Ok(()); + } + Err(Error::UpgradeError(Fallback)) => { + // If the migration failed we backup to the previous kernel + // and force a reboot to reload the kernel. + fallback_backup_kernel(host)?; + host.mark_for_reboot()?; + return Ok(()); + } + Err(err) => return Err(err.into()), + }; + + // In the very worst case, we want to be able to upgrade the kernel at + // any time. The kernel upgrades are retrieved from the inbox, therefore + // we need be able to to always reach the inbox and the kernel upgrade + // message. + // + // Therefore, the code between here and block production is allowed to + // fail. It should already not be the case, but we do not want to + // take the risk. + + // Fetch kernel metadata: + + // 1. Fetch the smart rollup address via the host function, it cannot fail. + let smart_rollup_address = host.reveal_metadata().raw_rollup_address; + // 2. Fetch the per mode configuration of the kernel. Returns the default + // configuration if it fails. + let mut configuration = fetch_configuration(host); + let sequencer_pool_address = read_sequencer_pool_address(host); + + // Run the stage one, this is a no-op if the inbox was already consumed + // by another kernel run. This ensures that if the migration does not + // consume all reboots. At least one reboot will be used to consume the + // inbox. + if let StageOneStatus::Reboot = + stage_one(host, smart_rollup_address, &mut configuration) + .context("Failed during stage 1")? + { + host.mark_for_reboot()?; + return Ok(()); + }; + + let trace_input = read_tracer_input(host)?; + + // Start processing blueprints + #[cfg(not(feature = "benchmark-bypass-stage2"))] + { + log!(host, Debug, "Entering stage two."); + if let block::ComputationResult::RebootNeeded = block::produce( + host, + chain_id, + &mut configuration, + sequencer_pool_address, + trace_input, + ) + .context("Failed during stage 2")? + { + host.mark_for_reboot()?; + } + } + + #[cfg(feature = "benchmark-bypass-stage2")] + { + log!(host, Benchmarking, "Shortcircuiting computation"); + return Ok(()); + } + Ok(()) +} + +pub fn kernel_loop( + host: &mut Host, +) { + // In order to setup the temporary directory, we need to move something + // from /evm to /tmp, so /evm must be non empty, this only happen + // at the first run. + + // The kernel host is initialized as soon as possible. `kernel_loop` + // shouldn't be called in tests as it won't use `MockInternal` for the + // internal runtime. + let mut host: KernelHost = KernelHost::init(host); + + let reboot_counter = host + .host + .reboot_left() + .expect("The kernel failed to get the number of reboot left"); + if reboot_counter == 1000 { + tezos_smart_rollup_debug::debug_msg!( + host, + "------------------ Kernel Invocation ------------------\n" + ) + } + + let world_state_subkeys = host + .host + .store_count_subkeys(&WORLD_STATE_PATH) + .expect("The kernel failed to read the number of /evm/world_state subkeys"); + + if world_state_subkeys == 0 { + host.host + .store_write(&WORLD_STATE_PATH, "Un festival de GADT".as_bytes(), 0) + .unwrap(); + } + + if is_revealed_storage(&host) { + reveal_storage( + &mut host, + option_env!("EVM_SEQUENCER").map(|s| { + PublicKey::from_b58check(s).expect("Failed parsing EVM_SEQUENCER") + }), + option_env!("EVM_ADMIN").map(|s| { + ContractKt1Hash::from_base58_check(s).expect("Failed parsing EVM_ADMIN") + }), + ); + } + + match main(&mut host) { + Ok(()) => (), + Err(err) => { + log!(&mut host, Fatal, "The kernel produced an error: {:?}", err); + } + } +} + +#[cfg(test)] +mod tests { + use std::str::FromStr; + + use crate::block_storage; + use crate::blueprint_storage::store_inbox_blueprint_by_number; + use crate::configuration::{Configuration, Limits}; + use crate::fees; + use crate::main; + use crate::parsing::RollupType; + use crate::storage::{ + read_transaction_receipt_status, store_chain_id, ENABLE_FA_BRIDGE, + }; + use crate::{ + blueprint::Blueprint, + inbox::{Transaction, TransactionContent}, + upgrade::KernelUpgrade, + }; + use evm_execution::account_storage::{self, EthereumAccountStorage}; + use evm_execution::fa_bridge::deposit::{ticket_hash, FaDeposit}; + use evm_execution::fa_bridge::test_utils::{ + convert_h160, convert_u256, dummy_ticket, kernel_wrapper, ticket_balance_add, + ticket_id, SolCall, + }; + use evm_execution::handler::RouterInterface; + use evm_execution::precompiles::FA_BRIDGE_PRECOMPILE_ADDRESS; + use evm_execution::utilities::{bigint_to_u256, keccak256_hash}; + use evm_execution::NATIVE_TOKEN_TICKETER_PATH; + use pretty_assertions::assert_eq; + use primitive_types::{H160, U256}; + use tezos_crypto_rs::hash::ContractKt1Hash; + use tezos_data_encoding::nom::NomReader; + use tezos_ethereum::block::BlockFees; + use tezos_ethereum::transaction::TransactionStatus; + use tezos_ethereum::{ + transaction::{TransactionHash, TransactionType}, + tx_common::EthereumTransactionCommon, + }; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_evm_runtime::safe_storage::SafeStorage; + + use tezos_evm_runtime::runtime::Runtime; + use tezos_smart_rollup::michelson::ticket::FA2_1Ticket; + use tezos_smart_rollup::michelson::{ + MichelsonBytes, MichelsonContract, MichelsonNat, MichelsonOption, MichelsonPair, + }; + use tezos_smart_rollup::outbox::{OutboxMessage, OutboxMessageTransaction}; + use tezos_smart_rollup::types::{Contract, Entrypoint}; + use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; + use tezos_smart_rollup_encoding::inbox::ExternalMessageFrame; + use tezos_smart_rollup_encoding::smart_rollup::SmartRollupAddress; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + use tezos_smart_rollup_host::path::RefPath; + use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; // Used to put traits interface in the scope + use tezos_smart_rollup_mock::TransferMetadata; + + const DUMMY_CHAIN_ID: U256 = U256::one(); + const DUMMY_BASE_FEE_PER_GAS: u64 = 12345u64; + const DUMMY_DA_FEE: u64 = 2_000_000_000_000u64; + + fn dummy_block_fees() -> BlockFees { + BlockFees::new( + DUMMY_BASE_FEE_PER_GAS.into(), + U256::from(DUMMY_BASE_FEE_PER_GAS), + DUMMY_DA_FEE.into(), + ) + } + + fn set_balance( + host: &mut Host, + evm_account_storage: &mut EthereumAccountStorage, + address: &H160, + balance: U256, + ) { + let mut account = evm_account_storage + .get_or_create(host, &account_storage::account_path(address).unwrap()) + .unwrap(); + let current_balance = account.balance(host).unwrap(); + if current_balance > balance { + account + .balance_remove(host, current_balance - balance) + .unwrap(); + } else { + account + .balance_add(host, balance - current_balance) + .unwrap(); + } + } + + fn hash_from_nonce(nonce: u64) -> TransactionHash { + let nonce = u64::to_le_bytes(nonce); + let mut hash = [0; 32]; + hash[..8].copy_from_slice(&nonce); + hash + } + + fn wrap_transaction(nonce: u64, tx: EthereumTransactionCommon) -> Transaction { + Transaction { + tx_hash: hash_from_nonce(nonce), + content: TransactionContent::Ethereum(tx), + } + } + + fn blueprint(transactions: Vec) -> Blueprint { + Blueprint { + transactions, + timestamp: Timestamp::from(0i64), + } + } + + const CREATE_LOOP_DATA: &str = "608060405234801561001057600080fd5b506101d0806100206000396000f3fe608060405234801561001057600080fd5b506004361061002b5760003560e01c80630b7d796e14610030575b600080fd5b61004a600480360381019061004591906100c2565b61004c565b005b60005b81811015610083576001600080828254610069919061011e565b92505081905550808061007b90610152565b91505061004f565b5050565b600080fd5b6000819050919050565b61009f8161008c565b81146100aa57600080fd5b50565b6000813590506100bc81610096565b92915050565b6000602082840312156100d8576100d7610087565b5b60006100e6848285016100ad565b91505092915050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b60006101298261008c565b91506101348361008c565b925082820190508082111561014c5761014b6100ef565b5b92915050565b600061015d8261008c565b91507fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff820361018f5761018e6100ef565b5b60018201905091905056fea26469706673582212200cd6584173dbec22eba4ce6cc7cc4e702e00e018d340f84fc0ff197faf980ad264736f6c63430008150033"; + + const LOOP_1300: &str = + "0b7d796e0000000000000000000000000000000000000000000000000000000000000514"; + + const LOOP_4600: &str = + "0b7d796e00000000000000000000000000000000000000000000000000000000000011f8"; + + const TEST_SK: &str = + "84e147b8bc36d99cc6b1676318a0635d8febc9f02897b0563ad27358589ee502"; + + const TEST_ADDR: &str = "f0affc80a5f69f4a9a3ee01a640873b6ba53e539"; + + fn create_and_sign_transaction( + data: &str, + nonce: u64, + gas_limit: u64, + to: Option, + secret_key: &str, + ) -> EthereumTransactionCommon { + let data = hex::decode(data).unwrap(); + + let gas_price = U256::from(DUMMY_BASE_FEE_PER_GAS); + let gas_for_fees = + crate::fees::gas_for_fees(DUMMY_DA_FEE.into(), gas_price, &data, &[]) + .unwrap(); + + let unsigned_tx = EthereumTransactionCommon::new( + TransactionType::Eip1559, + Some(DUMMY_CHAIN_ID), + nonce, + gas_price, + gas_price, + gas_limit + gas_for_fees, + to, + U256::zero(), + data, + vec![], + None, + ); + unsigned_tx + .sign_transaction(String::from(secret_key)) + .unwrap() + } + + #[test] + fn test_reboot_during_block_production() { + // init host + let mut host = MockKernelHost::default(); + + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + DUMMY_BASE_FEE_PER_GAS.into(), + ) + .unwrap(); + + // sanity check: no current block + assert!( + block_storage::read_current_number(&host).is_err(), + "Should not have found current block number" + ); + + //provision sender account + let sender = H160::from_str(TEST_ADDR).unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = account_storage::init_account_storage().unwrap(); + set_balance( + &mut host, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // These transactions are generated with the loop.sol contract, which are: + // - create the contract + // - call `loop(1200)` + // - call `loop(4600)` + let create_transaction = + create_and_sign_transaction(CREATE_LOOP_DATA, 0, 3_000_000, None, TEST_SK); + let loop_addr: H160 = + evm_execution::utilities::create_address_legacy(&sender, &0); + let loop_1200_tx = + create_and_sign_transaction(LOOP_1300, 1, 900_000, Some(loop_addr), TEST_SK); + let loop_4600_tx = create_and_sign_transaction( + LOOP_4600, + 2, + 2_600_000, + Some(loop_addr), + TEST_SK, + ); + + let proposals = vec![ + blueprint(vec![wrap_transaction(0, create_transaction)]), + blueprint(vec![ + wrap_transaction(1, loop_1200_tx), + wrap_transaction(2, loop_4600_tx), + ]), + ]; + // Store blueprints + for (i, blueprint) in proposals.into_iter().enumerate() { + store_inbox_blueprint_by_number(&mut host, blueprint, U256::from(i)) + .expect("Should have stored blueprint"); + } + // the upgrade mechanism should not start otherwise it will fail + let broken_kernel_upgrade = KernelUpgrade { + preimage_hash: [0u8; PREIMAGE_HASH_SIZE], + activation_timestamp: Timestamp::from(1_000_000i64), + }; + crate::upgrade::store_kernel_upgrade(&mut host, &broken_kernel_upgrade) + .expect("Should be able to store kernel upgrade"); + + let block_fees = dummy_block_fees(); + + // Set the tick limit to 11bn ticks - 2bn, which is the old limit minus the safety margin. + let limits = Limits { + maximum_allowed_ticks: 9_000_000_000, + ..Limits::default() + }; + + let mut configuration = Configuration { + limits, + ..Configuration::default() + }; + + crate::storage::store_minimum_base_fee_per_gas( + &mut host, + block_fees.minimum_base_fee_per_gas(), + ) + .unwrap(); + crate::storage::store_da_fee(&mut host, block_fees.da_fee_per_byte()).unwrap(); + + // If the upgrade is started, it should raise an error + let computation_result = crate::block::produce( + &mut host, + DUMMY_CHAIN_ID, + &mut configuration, + None, + None, + ) + .expect("Should have produced"); + + // test there is a new block + assert_eq!( + block_storage::read_current_number(&host) + .expect("should have found a block number"), + U256::zero(), + "There should have been a block registered" + ); + + // test reboot is set + matches!( + computation_result, + crate::block::ComputationResult::RebootNeeded + ); + } + + #[test] + fn load_block_fees_new() { + // Arrange + let mut host = MockKernelHost::default(); + + // Act + let result = crate::retrieve_block_fees(&mut host); + + // Assert + let expected = BlockFees::new( + fees::MINIMUM_BASE_FEE_PER_GAS.into(), + fees::MINIMUM_BASE_FEE_PER_GAS.into(), + fees::DA_FEE_PER_BYTE.into(), + ); + + assert!(result.is_ok()); + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn load_min_block_fees() { + let min_path = + RefPath::assert_from(b"/evm/world_state/fees/minimum_base_fee_per_gas"); + + // Arrange + let mut host = MockKernelHost::default(); + + let min_base_fee = U256::from(17); + tezos_storage::write_u256_le(&mut host, &min_path, min_base_fee).unwrap(); + + // Act + let result = crate::retrieve_block_fees(&mut host); + + // Assert + let expected = + BlockFees::new(min_base_fee, min_base_fee, fees::DA_FEE_PER_BYTE.into()); + + assert!(result.is_ok()); + assert_eq!(expected, result.unwrap()); + } + + #[test] + fn test_xtz_withdrawal_applied() { + // init host + let mut host = MockKernelHost::default(); + let mut safe_storage = SafeStorage { host: &mut host }; + safe_storage + .store_write_all( + &NATIVE_TOKEN_TICKETER_PATH, + b"KT1DWVsu4Jtu2ficZ1qtNheGPunm5YVniegT", + ) + .unwrap(); + store_chain_id(&mut safe_storage, DUMMY_CHAIN_ID).unwrap(); + + // run level in order to initialize outbox counter (by SOL message) + let level = safe_storage.host.host.run_level(|_| ()); + + // provision sender account + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = account_storage::init_account_storage().unwrap(); + set_balance( + &mut safe_storage, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // cast calldata "withdraw_base58(string)" "tz1RjtZUVeLhADFHDL8UwDZA6vjWWhojpu5w": + let data = hex::decode( + "cda4fee2\ + 0000000000000000000000000000000000000000000000000000000000000020\ + 0000000000000000000000000000000000000000000000000000000000000024\ + 747a31526a745a5556654c6841444648444c385577445a4136766a5757686f6a70753577\ + 00000000000000000000000000000000000000000000000000000000", + ) + .unwrap(); + + // create and sign precompile call + let gas_price = U256::from(40000000000u64); + let to = H160::from_str("ff00000000000000000000000000000000000001").unwrap(); + let tx = EthereumTransactionCommon::new( + TransactionType::Legacy, + Some(DUMMY_CHAIN_ID), + 0, + gas_price, + gas_price, + 30_000_000, + Some(to), + U256::from(1000000000000000000u64), + data, + vec![], + None, + ); + + // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + let tx_payload = tx + .sign_transaction( + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701" + .to_string(), + ) + .unwrap() + .to_bytes(); + + let tx_hash = keccak256_hash(&tx_payload); + + // encode as external message and submit to inbox + let mut contents = Vec::new(); + contents.push(0x00); // simple tx tag + contents.extend_from_slice(tx_hash.as_bytes()); + contents.extend_from_slice(&tx_payload); + + let message = ExternalMessageFrame::Targetted { + address: SmartRollupAddress::from_b58check( + "sr163Lv22CdE8QagCwf48PWDTquk6isQwv57", + ) + .unwrap(), + contents, + }; + + safe_storage.host.host.add_external(message); + + // run kernel twice to get to the stage with block creation: + main(&mut safe_storage).expect("Kernel error"); + main(&mut safe_storage).expect("Kernel error"); + + // verify outbox is not empty + let outbox = safe_storage.host.host.outbox_at(level + 1); + assert!(!outbox.is_empty()); + + // check message contents: + let message_bytes = &outbox[0]; + let (remaining, decoded_message) = + OutboxMessage::nom_read(message_bytes.as_slice()).unwrap(); + assert!(remaining.is_empty()); + + let ticketer = + Contract::from_b58check("KT1DWVsu4Jtu2ficZ1qtNheGPunm5YVniegT").unwrap(); + let ticket = FA2_1Ticket::new( + ticketer.clone(), + MichelsonPair(0.into(), MichelsonOption(None)), + 1000000u64, + ) + .unwrap(); + let receiver = + Contract::from_b58check("tz1RjtZUVeLhADFHDL8UwDZA6vjWWhojpu5w").unwrap(); + let parameters: RouterInterface = + MichelsonPair(MichelsonContract(receiver), ticket); + let destination = ticketer; + let entrypoint = Entrypoint::try_from("burn".to_string()).unwrap(); + + let expected_transaction = OutboxMessageTransaction { + parameters, + destination, + entrypoint, + }; + let expected_message = + OutboxMessage::AtomicTransactionBatch(vec![expected_transaction].into()); + + assert_eq!(expected_message, decoded_message); + } + + fn send_fa_deposit(enable_fa_bridge: bool) -> Option { + // init host + let mut mock_host = MockKernelHost::default(); + let mut safe_storage = SafeStorage { + host: &mut mock_host, + }; + + // enable FA bridge feature + if enable_fa_bridge { + safe_storage + .store_write_all(&ENABLE_FA_BRIDGE, &[1u8]) + .unwrap(); + } + + // init rollup parameters (legacy optimized forge) + // type: + // (or + // (or (pair %deposit (bytes %routing_info) + // (ticket %ticket (pair %content (nat %token_id) + // (option %metadata bytes)))) + // (bytes %b)) + // (bytes %c)) + // value: + // { + // "deposit": { + // "routing_info": "01" * 20 + "02" * 20, + // "ticket": ( + // "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + // (1, None), + // 42 + // ) + // } + // } + let params = hex::decode( + "\ + 0505050507070a00000028010101010101010101010101010101010101010102\ + 0202020202020202020202020202020202020207070a0000001601d496def47a\ + 3be89f5d54c6e6bb13cc6645d6e166000707070700010306002a", + ) + .unwrap(); + let (_, payload) = + RollupType::nom_read(¶ms).expect("Failed to decode params"); + + let metadata = TransferMetadata::new( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + "tz1P2Po7YM526ughEsRbY4oR9zaUPDZjxFrb", + ); + safe_storage.host.host.add_transfer(payload, &metadata); + + // run kernel + main(&mut safe_storage).expect("Kernel error"); + // QUESTION: looks like to get to the stage with block creation we need to call main twice (maybe check blueprint instead?) [1] + main(&mut safe_storage).expect("Kernel error"); + + // reconstruct ticket + let ticket = FA2_1Ticket::new( + Contract::Originated( + ContractKt1Hash::from_base58_check( + "KT1TxqZ8QtKvLu3V3JH7Gx58n7Co8pgtpQU5", + ) + .unwrap(), + ), + MichelsonPair::>( + 1u32.into(), + MichelsonOption(None), + ), + 42i32, + ) + .expect("Failed to construct ticket"); + + // reconstruct deposit + let deposit = FaDeposit { + amount: 42.into(), + proxy: Some(H160([2u8; 20])), + inbox_level: safe_storage.host.host.level(), // level not yet advanced + inbox_msg_id: 2, + receiver: H160([1u8; 20]), + ticket_hash: ticket_hash(&ticket).unwrap(), + }; + // smart rollup address as a seed + let tx_hash = deposit.hash(&[0u8; 20]); + + // read transaction receipt + read_transaction_receipt_status(&mut safe_storage, &tx_hash.0).ok() + } + + #[test] + fn test_fa_deposit_applied_if_feature_enabled() { + assert_eq!(send_fa_deposit(true), Some(TransactionStatus::Success)); + } + + #[test] + fn test_fa_deposit_rejected_if_feature_disabled() { + assert_eq!(send_fa_deposit(false), None); + } + + fn send_fa_withdrawal(enable_fa_bridge: bool) -> Vec> { + // init host + let mut mock_host = MockKernelHost::default(); + let mut safe_storage = SafeStorage { + host: &mut mock_host, + }; + + // enable FA bridge feature + if enable_fa_bridge { + safe_storage + .store_write_all(&ENABLE_FA_BRIDGE, &[1u8]) + .unwrap(); + } + + // run level in order to initialize outbox counter (by SOL message) + let level = safe_storage.host.host.run_level(|_| ()); + + // provision sender account + let sender = H160::from_str("af1276cbb260bb13deddb4209ae99ae6e497f446").unwrap(); + let sender_initial_balance = U256::from(10000000000000000000u64); + let mut evm_account_storage = account_storage::init_account_storage().unwrap(); + set_balance( + &mut safe_storage, + &mut evm_account_storage, + &sender, + sender_initial_balance, + ); + + // construct ticket + let ticket = dummy_ticket(); + let ticket_hash = ticket_hash(&ticket).unwrap(); + let amount = bigint_to_u256(ticket.amount()).unwrap(); + + // patch ticket table + ticket_balance_add( + &mut safe_storage, + &mut evm_account_storage, + &ticket_hash, + &sender, + amount, + ); + + // construct withdraw calldata + let (ticketer, content) = ticket_id(&ticket); + let routing_info = hex::decode("0000000000000000000000000000000000000000000001000000000000000000000000000000000000000000").unwrap(); + + let data = kernel_wrapper::withdrawCall::new(( + convert_h160(&sender), + routing_info.into(), + convert_u256(&amount), + ticketer.into(), + content.into(), + )) + .abi_encode(); + + // create and sign precompile call + let gas_price = U256::from(40000000000u64); + let to = FA_BRIDGE_PRECOMPILE_ADDRESS; + let tx = EthereumTransactionCommon::new( + TransactionType::Legacy, + Some(U256::from(1337)), + 0, + gas_price, + gas_price, + 10_000_000, + Some(to), + U256::zero(), + data, + vec![], + None, + ); + + // corresponding caller's address is 0xaf1276cbb260bb13deddb4209ae99ae6e497f446 + let tx_payload = tx + .sign_transaction( + "dcdff53b4f013dbcdc717f89fe3bf4d8b10512aae282b48e01d7530470382701" + .to_string(), + ) + .unwrap() + .to_bytes(); + + let tx_hash = keccak256_hash(&tx_payload); + + // encode as external message and submit to inbox + let mut contents = Vec::new(); + contents.push(0x00); // simple tx tag + contents.extend_from_slice(tx_hash.as_bytes()); + contents.extend_from_slice(&tx_payload); + + let message = ExternalMessageFrame::Targetted { + address: SmartRollupAddress::from_b58check( + "sr163Lv22CdE8QagCwf48PWDTquk6isQwv57", + ) + .unwrap(), + contents, + }; + + safe_storage.host.host.add_external(message); + + // run kernel + main(&mut safe_storage).expect("Kernel error"); + // QUESTION: looks like to get to the stage with block creation we need to call main twice (maybe check blueprint instead?) [2] + main(&mut safe_storage).expect("Kernel error"); + + safe_storage.host.host.outbox_at(level + 1) + } + + #[test] + fn test_fa_withdrawal_applied_if_feature_enabled() { + // verify outbox is not empty + assert_eq!(send_fa_withdrawal(true).len(), 1); + } + + #[test] + fn test_fa_withdrawal_rejected_if_feature_disabled() { + // verify outbox is empty + assert!(send_fa_withdrawal(false).is_empty()); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/linked_list.rs b/etherlink/kernel_calypso2/kernel/src/linked_list.rs new file mode 100644 index 000000000000..8ec7d95f9c9a --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/linked_list.rs @@ -0,0 +1,984 @@ +// SPDX-FileCopyrightText: 2023 Marigold + +use anyhow::{Context, Result}; +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpIterator, RlpStream}; +use std::marker::PhantomData; +use tezos_ethereum::rlp_helpers::{append_option, decode_field, decode_option, next}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_host::path::{concat, OwnedPath, Path}; +use tezos_storage::{read_optional_rlp, read_rlp, store_rlp}; + +/// Doubly linked list using the durable storage. +/// +/// The list is generic over element and index. +/// The element has to implement Decodable and Encodable +/// The Id has to implement AsRef<[u8]> +/// +/// The list is lazy. Not all the list is loaded at initialization. +/// Elements are saved/read at the appropriate moment. +pub struct LinkedList +where + Id: AsRef<[u8]> + Encodable + Decodable + Clone, + Elt: Encodable + Decodable + Clone, +{ + /// Absolute path to queue + path: OwnedPath, + /// None indicates an empty list + pointers: Option>, + _type: PhantomData<(Id, Elt)>, +} + +/// Pointers that indicates the front and the back of the list +struct LinkedListPointer { + front: Pointer, + back: Pointer, +} + +/// Each element in the list has a pointer +#[derive(Clone, Debug, PartialEq)] +struct Pointer { + /// Current index of the pointer + id: Id, + /// Previous index of the pointer + previous: Option, + /// Next index of the pointer + next: Option, + _type: PhantomData, +} + +/// Helper function to decode a path from rlp +fn decode_path( + it: &mut RlpIterator, + field_name: &'static str, +) -> Result { + let path: Vec = decode_field(&next(it)?, field_name)?; + OwnedPath::try_from(path).map_err(|_| DecoderError::Custom("not a path")) +} + +impl Decodable for LinkedListPointer { + fn decode(decoder: &Rlp) -> Result { + if !decoder.is_list() { + return Err(rlp::DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(rlp::DecoderError::RlpInvalidLength); + } + let mut it = decoder.iter(); + let front = decode_field(&next(&mut it)?, "front")?; + let back = decode_field(&next(&mut it)?, "back")?; + Ok(LinkedListPointer { front, back }) + } +} + +impl Encodable for LinkedListPointer { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(2); + stream.append(&self.front); + stream.append(&self.back); + } +} + +impl Decodable for Pointer { + fn decode(decoder: &Rlp) -> Result { + if !decoder.is_list() { + return Err(rlp::DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 3 { + return Err(rlp::DecoderError::RlpInvalidLength); + } + let mut it = decoder.iter(); + let id = decode_field(&next(&mut it)?, "id")?; + let previous = decode_option(&next(&mut it)?, "previous")?; + let next = decode_option(&next(&mut it)?, "next")?; + + Ok(Pointer { + id, + next, + previous, + _type: PhantomData, + }) + } +} + +impl Encodable for Pointer { + fn rlp_append(&self, stream: &mut RlpStream) { + stream.begin_list(3); + stream.append(&self.id); + append_option(stream, &self.previous); + append_option(stream, &self.next); + } +} + +impl Encodable for LinkedList +where + Id: AsRef<[u8]> + Encodable + Decodable + Clone, + Elt: Encodable + Decodable + Clone, +{ + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append(&self.path.as_bytes()); + append_option(stream, &self.pointers); + } +} + +impl Decodable for LinkedList +where + Id: AsRef<[u8]> + Encodable + Decodable + Clone, + Elt: Encodable + Decodable + Clone, +{ + fn decode(decoder: &Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != 2 { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let path = decode_path(&mut it, "path")?; + let pointers = decode_option(&next(&mut it)?, "pointers")?; + + Ok(Self { + path, + pointers, + _type: PhantomData, + }) + } +} + +#[allow(dead_code)] +impl, Elt: Encodable + Decodable> + Pointer +{ + fn pointer_path(id: &Id, prefix: &impl Path) -> Result { + let path = hex::encode(id); + let path: Vec = format!("/{}/pointer", path).into(); + let path = OwnedPath::try_from(path)?; + let path = concat(prefix, &path)?; + Ok(path) + } + + /// Path to the pointer + /// + /// This path is used when you want to read a pointer or to remove it. + fn path(&self, prefix: &impl Path) -> Result { + Self::pointer_path(&self.id, prefix) + } + + /// Path to the data held by the pointer. + fn data_path(&self, prefix: &impl Path) -> Result { + let path = hex::encode(&self.id); + let path: Vec = format!("/{}/data", path).into(); + let path = OwnedPath::try_from(path)?; + let path = concat(prefix, &path)?; + Ok(path) + } + + fn save_data( + &self, + host: &mut impl Runtime, + prefix: &impl Path, + data: &Elt, + ) -> Result<()> { + let path = self.data_path(prefix)?; + store_rlp(data, host, &path).context("cannot save the pointer's data") + } + + fn get_data(&self, host: &impl Runtime, prefix: &impl Path) -> Result { + let path = self.data_path(prefix)?; + read_rlp(host, &path).context("cannot read the pointer's data") + } + + /// Load the pointer from the durable storage + fn read(host: &impl Runtime, prefix: &impl Path, id: &Id) -> Result> { + read_optional_rlp(host, &Self::pointer_path(id, prefix)?) + } + + /// Save the pointer in the durable storage + fn save(&self, host: &mut impl Runtime, prefix: &impl Path) -> Result<()> { + store_rlp(self, host, &self.path(prefix)?) + .context("cannot save pointer to storage") + } + + /// Removes the pointer and its data frm the durable storage. + fn remove_with_data( + &self, + host: &mut impl Runtime, + prefix: &impl Path, + ) -> Result<()> { + let path = hex::encode(&self.id); + let path: Vec = format!("/{}", path).into(); + let path = OwnedPath::try_from(path)?; + let path = concat(prefix, &path)?; + host.store_delete(&path) + .context("cannot remove the pointer") + } +} + +#[allow(dead_code)] +impl LinkedList +where + Id: AsRef<[u8]> + Encodable + Decodable + Clone + PartialEq, + Elt: Encodable + Decodable + Clone, +{ + /// Load a list from the storage. + /// If the list does not exist, a new empty list is created. + /// Otherwise the existing list is read from the storage. + pub fn new(path: &impl Path, host: &impl Runtime) -> Result { + let list = Self::read(host, path)?.unwrap_or(Self { + path: path.into(), + pointers: None, + _type: PhantomData, + }); + Ok(list) + } + + /// Path to the metadata that defines the list. + fn metadata_path(root: &impl Path) -> Result { + let meta_path: Vec = "/meta".into(); + let meta_path = OwnedPath::try_from(meta_path)?; + let path = concat(root, &meta_path)?; + Ok(path) + } + + /// Saves the LinkedList in the durable storage. + /// + /// Only save the back and front pointers. + fn save(&self, host: &mut impl Runtime) -> Result<()> { + let path = Self::metadata_path(&self.path)?; + store_rlp(self, host, &path).context("cannot save linked list from the storage") + } + + /// Load the LinkedList from the durable storage. + fn read(host: &impl Runtime, path: &impl Path) -> Result> { + let path = Self::metadata_path(path)?; + read_optional_rlp(host, &path) + } + + /// Returns true if the list contains no elements. + pub fn is_empty(&self) -> bool { + self.pointers.is_none() + } + + /// Appends an element to the back of a list. + /// + /// An element cannot be mutated. + /// The Id has to be unique by element. + /// The Id will be later use to retrieve the element + /// (example: it can be the hash of the element). + pub fn push(&mut self, host: &mut impl Runtime, id: &Id, elt: &Elt) -> Result<()> { + // Check if the path already exist + if (Pointer::read(host, &self.path, id)? as Option>).is_some() { + return Ok(()); + } + match &self.pointers { + Some(LinkedListPointer { front, back }) => { + // The list is not empty + // Modifies the old back pointer + let penultimate = Pointer { + next: Some(id.clone()), // Points to the inserted element + ..back.clone() + }; + // Creates a new back pointer + let back = Pointer { + id: id.clone(), + previous: Some(penultimate.id.clone()), // Points to the penultimate pointer + next: None, // None because there is no element after + _type: PhantomData, + }; + // Saves the pointer + penultimate.save(host, &self.path)?; + back.save(host, &self.path)?; + // And the save the data + back.save_data(host, &self.path, elt)?; + // update the back pointer of the list + + // Before the addition, penultimate might be the front + // *and* the back of the list. The back is always updated to + // the inserted cell, but the front cell might need an update. + if penultimate.id == front.id { + self.pointers = Some(LinkedListPointer { + front: penultimate, + back, + }); + } else { + // If not, we need to update only the back pointer. + self.pointers = Some(LinkedListPointer { + front: front.clone(), + back, + }); + } + } + None => { + // This case corresponds to the empty list + // A new pointer has to be created + let back = Pointer { + id: id.clone(), + previous: None, // None because it's the only element of the list + next: None, // None because it's the only element of the list + _type: PhantomData, + }; + // Saves the pointer and its data + back.save(host, &self.path)?; + back.save_data(host, &self.path, elt)?; + // update the front and back pointers of the list + self.pointers = Some(LinkedListPointer { + front: back.clone(), + back, + }); + } + }; + self.save(host) + } + + /// Returns an element at a given index. + /// + /// Returns None if the element is not present + pub fn find(&self, host: &impl Runtime, id: &Id) -> Result> { + let Some::>(pointer) = Pointer::read(host, &self.path, id)? + else { + return Ok(None); + }; + read_optional_rlp(host, &pointer.data_path(&self.path)?) + } + + /// Removes and returns the element at position index within the vector. + pub fn remove(&mut self, host: &mut impl Runtime, id: &Id) -> Result> { + // Check if the list is empty + let Some(LinkedListPointer { front, back }) = &self.pointers else { + return Ok(None); + }; + // Get the previous and the next pointer + let Some(pointer) = Pointer::read(host, &self.path, id)? else { + return Ok(None); + }; + let previous = match pointer.previous { + Some(ref previous) => Pointer::read(host, &self.path, previous)?, + None => None, + }; + let next = match pointer.next { + Some(ref next) => Pointer::read(host, &self.path, next)?, + None => None, + }; + + // retrieve the data + let data = pointer.get_data(host, &self.path)?; + // delete the pointer and the data + pointer.remove_with_data(host, &self.path)?; + match (previous, next) { + // This case represents the list with only one element + (None, None) => { + // update the list pointers + self.pointers = None; + } + // The head of the list is being removed + (None, Some(next)) => { + let new_front = Pointer { + previous: None, // because it's the head + ..next + }; + // update the pointer + new_front.save(host, &self.path)?; + + // If the list has 2 elements and if the front is removed, + // then the front and the back are equal. + if new_front.id == back.id { + self.pointers = Some(LinkedListPointer { + front: new_front.clone(), + back: new_front, + }); + } else { + self.pointers = Some(LinkedListPointer { + front: new_front, + back: back.clone(), + }); + } + } + // The end of the list is being removed + (Some(previous), None) => { + let new_back = Pointer { + next: None, // because it's the end of the list + ..previous + }; + new_back.save(host, &self.path)?; + // If the list has 2 elements and if the back is removed, + // then the front and the back are equal. + if new_back.id == front.id { + self.pointers = Some(LinkedListPointer { + front: new_back.clone(), + back: new_back, + }); + } else { + self.pointers = Some(LinkedListPointer { + front: front.clone(), + back: new_back, + }); + } + } + // Removes an element between two elements + (Some(previous), Some(next)) => { + let previous_id = previous.id.clone(); + let new_previous = Pointer { + next: Some(next.id.clone()), + ..previous + }; + let new_next = Pointer { + previous: Some(previous_id), + ..next + }; + new_previous.save(host, &self.path)?; + new_next.save(host, &self.path)?; + + // We remove the second item of a list of three. It means back and front + // have been updated and are pointing to each other now. + if new_previous.clone().id == front.clone().id + && new_next.clone().id == back.clone().id + { + self.pointers = Some(LinkedListPointer { + front: new_previous.clone(), + back: new_next.clone(), + }); + } else { + // If the new previous is the front pointer, we need to + // update the front pointer to handle the new next pointer + if new_previous.clone().id == front.clone().id { + self.pointers = Some(LinkedListPointer { + front: new_previous, + back: back.clone(), + }); + } else { + // Similarly to the previous case. If the new next is the + // back pointer, we need to update the back pointer to handle + // the previous pointer. + if new_next.clone().id == back.clone().id { + self.pointers = Some(LinkedListPointer { + front: front.clone(), + back: new_next.clone(), + }); + } + } + } + } + }; + self.save(host)?; + Ok(Some(data)) + } + + /// Returns the first element of the list + /// or `None` if it is empty. + pub fn first(&self, host: &impl Runtime) -> Result> { + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; + Ok(Some(front.get_data(host, &self.path)?)) + } + + /// Returns the first element of the list alongside its id + /// or `None` if it is empty. + pub fn first_with_id(&self, host: &impl Runtime) -> Result> { + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; + Ok(Some((front.id.clone(), front.get_data(host, &self.path)?))) + } + + /// Removes the first element of the list and returns it + pub fn pop_first(&mut self, host: &mut impl Runtime) -> Result> { + let Some(LinkedListPointer { front, .. }) = &self.pointers else { + return Ok(None); + }; + let to_remove = front.id.clone(); + self.remove(host, &to_remove) + } + + /// Deletes the entire list + pub fn delete(&mut self, host: &mut impl Runtime) -> Result<()> { + if host.store_has(&self.path)?.is_some() { + host.store_delete(&self.path)?; + }; + self.pointers = None; + Ok(()) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::*; + use proptest::proptest; + use rlp::{Decodable, DecoderError, Encodable}; + use std::collections::HashMap; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_debug::Runtime; + use tezos_smart_rollup_host::path::RefPath; + + #[derive(Clone, PartialEq, Debug)] + struct Hash([u8; 8]); + + impl Encodable for Hash { + fn rlp_append(&self, s: &mut rlp::RlpStream) { + s.encoder().encode_value(&self.0); + } + } + + impl Decodable for Hash { + fn decode(decoder: &rlp::Rlp) -> Result { + let hash: Vec = decoder.as_val()?; + let hash = hash + .try_into() + .map_err(|_| DecoderError::Custom("expected a vec of 32 elements"))?; + Ok(Hash(hash)) + } + } + + impl AsRef<[u8]> for Hash { + fn as_ref(&self) -> &[u8] { + &self.0 + } + } + + fn traverse_f( + host: &MockKernelHost, + list_path: &OwnedPath, + pointer: Pointer, + f: &dyn Fn(&Pointer) -> Option, + ) -> (Pointer, usize) { + let mut size = 0; + let mut cell = pointer.clone(); + loop { + size += 1; + match f(&cell) { + None => break, + Some(next) => { + match Pointer::::read(host, list_path, &next).unwrap() { + Some(next) => cell = next, + None => { + panic!("Pointer should exist in storage"); + } + } + } + } + } + (cell, size) + } + + fn traverse(host: &MockKernelHost, list: &LinkedList) -> usize { + let Some(LinkedListPointer { + ref front, + ref back, + }) = list.pointers + else { + return 0; + }; + + let (found_front, size_from_back) = + traverse_f(host, &list.path, back.clone(), &|cell: &Pointer< + Hash, + u8, + >| { + cell.previous.clone() + }); + let (found_back, size_from_front) = + traverse_f(host, &list.path, front.clone(), &|cell: &Pointer< + Hash, + u8, + >| { + cell.next.clone() + }); + + assert_eq!(found_front, front.clone()); + assert_eq!(found_back, back.clone()); + assert_eq!(size_from_back, size_from_front); + + size_from_back + } + + fn assert_length( + host: &MockKernelHost, + list: &LinkedList, + expected_len: u64, + ) { + // Both the linked list pointers and the element pointers lies under + // the `list.path`. + let keys = host.store_count_subkeys(&list.path).unwrap_or(0u64); + // Removes the linked list pointers to compute the actual length. + let actual_length = keys.saturating_sub(1u64); + assert_eq!( + expected_len, actual_length, + "Unexpected length for the list" + ); + let traverse_length = traverse(host, list); + assert_eq!( + expected_len, traverse_length as u64, + "Traverse size is unexpected" + ); + } + + #[test] + fn test_empty() { + let host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let list = + LinkedList::::new(&path, &host).expect("list should be created"); + assert!(list.is_empty()) + } + + #[test] + fn test_find_returns_none_when_empty() { + let host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let list = LinkedList::new(&path, &host).expect("list should be created"); + let hash = Hash([0; 8]); + let read: Option = list.find(&host, &hash).expect("storage should work"); + assert!(read.is_none()) + } + + #[test] + fn test_insert() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id = Hash([0x0; 8]); + let elt = 0x32_u8; + + list.push(&mut host, &id, &elt) + .expect("storage should work"); + + let read: u8 = list + .find(&host, &id) + .expect("storage should work") + .expect("element should be present"); + + assert_eq!(read, elt); + } + + #[test] + fn test_remove() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id = Hash([0x0; 8]); + let elt = 0x32_u8; + + assert_length(&host, &list, 0u64); + list.push(&mut host, &id, &elt) + .expect("storage should work"); + assert_length(&host, &list, 1u64); + let _: Option = list.remove(&mut host, &id).expect("storage should work"); + assert_length(&host, &list, 0u64); + + let read: Option = list.find(&host, &id).expect("storage should work"); + + assert!(read.is_none()) + } + + #[test] + fn test_remove_nothing() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id = Hash([0x0; 8]); + + let removed: Option = + list.remove(&mut host, &id).expect("storage should work"); + + assert!(removed.is_none()) + } + + #[test] + fn test_first() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id = Hash([0x0; 8]); + let elt = 0x32_u8; + + list.push(&mut host, &id, &elt) + .expect("storage should workd"); + + let read: u8 = list + .first(&host) + .expect("storage should work") + .expect("element should be present"); + + assert_eq!(read, 0x32); + } + + #[test] + fn test_first_when_only_two_elements() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id_1 = Hash([0x0; 8]); + let id_2 = Hash([0x1; 8]); + + list.push(&mut host, &id_1, &0x32_u8) + .expect("storage should workd"); + list.push(&mut host, &id_2, &0x33_u8) + .expect("storage should workd"); + + let read: u8 = list + .first(&host) + .expect("storage should work") + .expect("element should be present"); + + assert_eq!(read, 0x32); + } + + #[test] + fn test_first_after_two_push() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + let id_1 = Hash([0x0; 8]); + let id_2 = Hash([0x1; 8]); + + list.push(&mut host, &id_1, &0x32_u8) + .expect("storage should workd"); + list.push(&mut host, &id_2, &0x33_u8) + .expect("storage should workd"); + let _: Option = list.remove(&mut host, &id_1).expect("storage should work"); + + let read: u8 = list + .first(&host) + .expect("storage should work") + .expect("element should be present"); + + assert_eq!(read, 0x33); + } + + #[test] + fn test_delete() { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list: LinkedList = + LinkedList::new(&path, &host).expect("list should be created"); + let id_1 = Hash([0x0; 8]); + let id_2 = Hash([0x1; 8]); + list.push(&mut host, &id_1, &0x32_u8) + .expect("push should have worked"); + list.push(&mut host, &id_2, &0x33_u8) + .expect("push should have worked"); + list.delete(&mut host).expect("delete should have worked"); + let reloaded_list: LinkedList = + LinkedList::new(&path, &host).expect("list should be created"); + assert!(reloaded_list.is_empty()); + } + + fn fill_list( + elements: &HashMap<[u8; 8], u8>, + ) -> (MockKernelHost, LinkedList) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + for (pos, (id, elt)) in elements.iter().enumerate() { + list.push(&mut host, &Hash(*id), elt) + .expect("storage should work"); + + assert_length(&host, &list, pos as u64 + 1); + } + (host, list) + } + + fn make_list(n: u8, host: &mut MockKernelHost) -> (LinkedList, Vec) { + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, host).expect("list should be created"); + let mut elements = vec![]; + for i in 0..n { + let id = Hash([i; 8]); + list.push(host, &id, &i).unwrap(); + elements.push(id); + } + + (list, elements) + } + + #[test] + fn test_push_4_remove_pos_0() { + let mut host = MockKernelHost::default(); + let (mut list, elements) = make_list(4, &mut host); + + assert_length(&host, &list, 4); + let _: Option = list + .remove(&mut host, &elements[0]) + .expect("storage should work"); + assert_length(&host, &list, 3); + } + + #[test] + fn test_push_4_remove_pos_1() { + let mut host = MockKernelHost::default(); + let (mut list, elements) = make_list(4, &mut host); + + assert_length(&host, &list, 4); + let _: Option = list + .remove(&mut host, &elements[1]) + .expect("storage should work"); + assert_length(&host, &list, 3); + } + + #[test] + fn test_push_4_remove_pos_2() { + let mut host = MockKernelHost::default(); + let (mut list, elements) = make_list(4, &mut host); + + assert_length(&host, &list, 4); + let _: Option = list + .remove(&mut host, &elements[2]) + .expect("storage should work"); + assert_length(&host, &list, 3); + } + + #[test] + fn test_push_4_remove_pos_3() { + let mut host = MockKernelHost::default(); + let (mut list, elements) = make_list(4, &mut host); + + assert_length(&host, &list, 4); + let _: Option = list + .remove(&mut host, &elements[3]) + .expect("storage should work"); + assert_length(&host, &list, 3); + } + + fn elements() -> impl Strategy, usize)> { + // Generate a HashMap with keys of size [u8; 32] and u8 values + prop::collection::hash_map(any::<[u8; 8]>(), any::(), 1..10).prop_flat_map( + |map| { + let map_len = map.len(); // Get the length of the HashMap + + // Ensure there's at least one element in the map + let random_index_strategy = 0..map_len; // Strategy to pick a random index + + // Return the HashMap and the randomly selected index value + (Just(map.clone()), random_index_strategy) + .prop_map(move |(map, idx)| (map, idx)) + }, + ) + } + + proptest! { + + #![proptest_config(ProptestConfig::with_cases(200))] + + #[test] + fn test_push_remove_consistency((elements, idx) in elements()) { + let (mut host, mut list) = fill_list(&elements); + + let elt = elements.keys().nth(idx).unwrap(); + list.remove(&mut host, &Hash(*elt)).unwrap(); + + assert_length(&host, &list, (elements.len() as u64) - 1); + + } + + #[test] + fn test_pushed_elements_are_present((elements, _) in elements()) { + let (host, list) = fill_list(&elements); + for (id, elt) in & elements { + let read: u8 = list.find(&host, &Hash(*id)).expect("storage should work").expect("element should be present"); + assert_eq!(elt, &read) + } + } + + #[test] + fn test_push_element_create_non_empty_list((elements, _) in elements()) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + assert!(list.is_empty()); + for (id, elt) in elements { + list.push(&mut host, &Hash(id), &elt).expect("storage should work"); + assert!(!list.is_empty()) + } + } + + #[test] + fn test_remove_from_empty_creates_empty_list(elements: Vec<[u8; 8]>) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + assert!(list.is_empty()); + for id in elements { + let _: Option = list.remove(&mut host, &Hash(id)).expect("storage to work"); + } + assert!(list.is_empty()); + } + + #[test] + fn test_remove_returns_the_appropriate_element((elements, _) in elements()) { + let (mut host, mut list) = fill_list(&elements); + let mut length : u64 = elements.len().try_into().unwrap(); + for (id, elt) in &elements { + let removed: u8 = list.remove(&mut host, &Hash(*id)).expect("storage should work").expect("element should be present"); + length -= 1; + assert_length(&host, &list, length); + assert_eq!(elt, &removed) + } + } + + #[test] + fn test_remove_everything_creates_the_empty_list((elements, _) in elements()) { + let (mut host, mut list) = fill_list(&elements); + for (id, _) in elements { + let _: u8 = list.remove(&mut host, &Hash(id)).expect("storage should work").expect("element should be present"); + } + assert!(list.is_empty()) + } + + #[test] + fn test_list_is_kept_between_reboots((elements, _) in elements()) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + for (id, elt) in &elements { + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + list.push(&mut host, &Hash(*id), elt) + .expect("storage should work"); + assert!(!list.is_empty()) + } + for (id, elt) in &elements { + let list = LinkedList::new(&path, &host).expect("list should be created"); + let read: u8 = list + .find(&host, &Hash(*id)) + .expect("storage should work") + .expect("element should be present"); + assert_eq!(elt, &read); + } + } + + #[test] + fn test_pop_first_after_push((elements, _) in elements()) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + + for (id, elt) in elements { + list.push(&mut host, &Hash(id), &elt).expect("storage should work"); + let removed = list.pop_first(&mut host).expect("storage should work").expect("element should be present"); + assert_eq!(elt, removed); + } + } + + #[test] + fn test_pop_first_keep_the_order((elements, _) in elements()) { + let mut host = MockKernelHost::default(); + let path = RefPath::assert_from(b"/list"); + let mut list = LinkedList::new(&path, &host).expect("list should be created"); + + let mut inserted = vec![]; + let mut removed = vec![]; + + for (id, elt) in elements { + list.push(&mut host, &Hash(id), &elt).expect("storage should work"); + inserted.push(elt); + } + + while !list.is_empty() { + let pop = list.pop_first(&mut host).expect("storage should work").expect("element should be present"); + removed.push(pop); + } + + assert_eq!(inserted, removed); + } + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/migration.rs b/etherlink/kernel_calypso2/kernel/src/migration.rs new file mode 100644 index 000000000000..738493df611a --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/migration.rs @@ -0,0 +1,292 @@ +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +use crate::block_storage; +use crate::blueprint_storage::{ + blueprint_path, clear_all_blueprints, read_next_blueprint_number, +}; +use crate::error::Error; +use crate::error::StorageError; +use crate::error::UpgradeProcessError; +use crate::storage::{ + read_chain_id, read_storage_version, store_storage_version, StorageVersion, + DELAYED_BRIDGE, ENABLE_FA_BRIDGE, KERNEL_GOVERNANCE, KERNEL_SECURITY_GOVERNANCE, + SEQUENCER_GOVERNANCE, +}; +use evm_execution::account_storage::account_path; +use evm_execution::account_storage::init_account_storage; +use evm_execution::precompiles::SYSTEM_ACCOUNT_ADDRESS; +use evm_execution::precompiles::WITHDRAWAL_ADDRESS; +use evm_execution::{ENABLE_FAST_WITHDRAWAL, NATIVE_TOKEN_TICKETER_PATH}; +use primitive_types::U256; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::storage::path::RefPath; +use tezos_smart_rollup_host::path::OwnedPath; +use tezos_smart_rollup_host::runtime::RuntimeError; + +#[derive(Eq, PartialEq)] +pub enum MigrationStatus { + None, + InProgress, + Done, +} + +// /!\ the following functions are migratin helpers, do not remove them /!\ + +#[allow(dead_code)] +const TESTNET_CHAIN_ID: u64 = 128123; + +#[allow(dead_code)] +const MAINNET_CHAIN_ID: u64 = 42793; + +#[allow(dead_code)] +fn is_etherlink_network( + host: &impl Runtime, + expected_chain_id: u64, +) -> Result { + match read_chain_id(host) { + Ok(chain_id) => Ok(chain_id == expected_chain_id.into()), + Err(Error::Storage(StorageError::Runtime(RuntimeError::PathNotFound))) => { + Ok(false) + } + Err(err) => Err(err), + } +} + +#[allow(dead_code)] +pub fn allow_path_not_found(res: Result<(), RuntimeError>) -> Result<(), RuntimeError> { + match res { + Ok(()) => Ok(()), + Err(RuntimeError::PathNotFound) => Ok(()), + Err(err) => Err(err), + } +} + +const TMP_NEXT_BLUEPRINT_PATH: RefPath = + RefPath::assert_from(b"/__tmp_next_blueprint_path"); + +fn migrate_to( + host: &mut Host, + version: StorageVersion, +) -> anyhow::Result { + log!(host, Info, "Migrating to {:?}", version); + match version { + StorageVersion::V11 => anyhow::bail!(Error::UpgradeError( + UpgradeProcessError::InternalUpgrade("V11 has no predecessor"), + )), + StorageVersion::V12 => { + let legacy_ticketer_path = RefPath::assert_from(b"/evm/ticketer"); + if host.store_has(&legacy_ticketer_path)?.is_some() { + host.store_move(&legacy_ticketer_path, &NATIVE_TOKEN_TICKETER_PATH)?; + } + + Ok(MigrationStatus::Done) + } + StorageVersion::V13 => Ok(MigrationStatus::Done), + StorageVersion::V14 => { + if is_etherlink_network(host, TESTNET_CHAIN_ID)? { + host.store_write_all(&ENABLE_FA_BRIDGE, &[1u8])?; + Ok(MigrationStatus::Done) + } else { + // Not applicable for other networks + Ok(MigrationStatus::None) + } + } + StorageVersion::V15 => { + // Starting version 15, the entrypoint `populate_delayed_inbox` + // is available. + Ok(MigrationStatus::Done) + } + StorageVersion::V16 => { + // Allow path not found in case the migration is performed + // on a context with no blocks or no transactions. + allow_path_not_found(host.store_delete(&RefPath::assert_from( + b"/evm/world_state/indexes/accounts", + )))?; + allow_path_not_found(host.store_delete(&RefPath::assert_from( + b"/evm/world_state/indexes/transactions", + )))?; + // Starting version 16, the `callTracer` configuration is available + // for tracing. + Ok(MigrationStatus::Done) + } + StorageVersion::V17 => { + // Starting version 17 the kernel no longer needs all transactions + // in its storage to produce the receipts and transactions root. + Ok(MigrationStatus::Done) + } + StorageVersion::V18 => { + // Blocks were indexed twice in the storage. + // [/evm/world_state/indexes/blocks] is the mapping of all block + // numbers to hashes. + // [/evm/world_state/blocks//hash] is the mapgping of the + // last 256 blocks to hashes + // + // We need only the former. + + let current_number = block_storage::read_current_number(host)?; + let to_clean = U256::min( + current_number + 1, + evm_execution::storage::blocks::BLOCKS_STORED.into(), + ); + for i in 0..to_clean.as_usize() { + let number = current_number - i; + let path: Vec = + format!("/evm/world_state/blocks/{}/hash", number).into(); + let owned_path = OwnedPath::try_from(path)?; + host.store_delete(&owned_path)?; + } + Ok(MigrationStatus::Done) + } + StorageVersion::V19 => { + // We do not support EIP161 yet. If we start doing it, we + // might clean the zero account by accident, and the account + // must always exist as the ticket table is stored in the + // zero address. + let account_storage = init_account_storage()?; + let account_path = account_path(&SYSTEM_ACCOUNT_ADDRESS)?; + let mut account = account_storage.get_or_create(host, &account_path)?; + account.increment_nonce(host)?; + Ok(MigrationStatus::Done) + } + StorageVersion::V20 => { + let account_storage = init_account_storage()?; + let mut withdrawal_precompiled = account_storage + .get_or_create(host, &account_path(&WITHDRAWAL_ADDRESS)?)?; + let balance = withdrawal_precompiled.balance(host)?; + if !balance.is_zero() { + withdrawal_precompiled.balance_remove(host, balance)?; + } + Ok(MigrationStatus::Done) + } + StorageVersion::V21 => { + if is_etherlink_network(host, MAINNET_CHAIN_ID)? { + host.store_write_all( + &DELAYED_BRIDGE, + b"KT1Vocor3bL5ZSgsYH9ztt42LNhqFK64soR4", + )?; + Ok(MigrationStatus::Done) + } else if is_etherlink_network(host, TESTNET_CHAIN_ID)? { + host.store_write_all( + &DELAYED_BRIDGE, + b"KT1X1M4ywyz9cHvUgBLTUUdz3GTiYJhPcyPh", + )?; + Ok(MigrationStatus::Done) + } else { + // Not applicable for other networks + Ok(MigrationStatus::None) + } + } + StorageVersion::V22 => { + if is_etherlink_network(host, MAINNET_CHAIN_ID)? { + host.store_write_all(&ENABLE_FA_BRIDGE, &[1u8])?; + Ok(MigrationStatus::Done) + } else { + // Not applicable for other networks + Ok(MigrationStatus::None) + } + } + StorageVersion::V23 => { + // Clear all the blueprints, we accumulated a lot of old + // blueprints without cleaning them. + // + // As we remove everything that means the sequencer will + // have to republish some. + // + // However we need to keep the next blueprint as it + // trigerred the upgrade. + + let next_blueprint_number = read_next_blueprint_number(host)?; + let blueprint_path = blueprint_path(next_blueprint_number)?; + allow_path_not_found( + host.store_move(&blueprint_path, &TMP_NEXT_BLUEPRINT_PATH), + )?; + clear_all_blueprints(host)?; + allow_path_not_found( + host.store_move(&TMP_NEXT_BLUEPRINT_PATH, &blueprint_path), + )?; + Ok(MigrationStatus::Done) + } + StorageVersion::V24 => { + const EVM_BASE_FEE_PER_GAS: RefPath = + RefPath::assert_from(b"/evm/world_state/fees/base_fee_per_gas"); + host.store_delete(&EVM_BASE_FEE_PER_GAS)?; + Ok(MigrationStatus::Done) + } + StorageVersion::V25 => { + if is_etherlink_network(host, MAINNET_CHAIN_ID)? { + const REGULAR_GOVERNANCE_KT: &[u8] = + b"KT1FPG4NApqTJjwvmhWvqA14m5PJxu9qgpBK"; + const SECURITY_GOVERNANCE_KT: &[u8] = + b"KT1GRAN26ni19mgd6xpL6tsH52LNnhKSQzP2"; + const SEQUENCER_GOVERNANCE_KT: &[u8] = + b"KT1UvCsnXpLAssgeJmrbQ6qr3eFkYXxsTG9U"; + + host.store_write_all(&KERNEL_GOVERNANCE, REGULAR_GOVERNANCE_KT)?; + host.store_write_all( + &KERNEL_SECURITY_GOVERNANCE, + SECURITY_GOVERNANCE_KT, + )?; + host.store_write_all(&SEQUENCER_GOVERNANCE, SEQUENCER_GOVERNANCE_KT)?; + + Ok(MigrationStatus::Done) + } else { + Ok(MigrationStatus::None) + } + } + StorageVersion::V26 => { + host.store_write_all(&ENABLE_FAST_WITHDRAWAL, &[1_u8])?; + Ok(MigrationStatus::Done) + } + } +} + +// The workflow for migration is the following: +// +// - add a new variant to `storage::StorageVersion`, update `STORAGE_VERSION` +// accordingly. +// - update `migrate_to` pattern matching with all the needed migration functions +// - compile the kernel and run all the E2E migration tests to make sure all the +// data is still available from the EVM proxy-node. +// - upgrade the failed_migration.wasm kernel, see tests/ressources/README.md +// +// /!\ +// If the migration takes more than 999 reboots, we will lose the inbox +// of a level. At least one reboot must be allocated to the stage one +// to consume the inbox. Therefore, if the migration happens to take more +// than 999 reboots, you have to rethink this. This limitation exists +// because we consider that the inbox should not be collected during +// a migration because it impacts the storage. We could in theory end up +// in an inconsistent storage. +// /!\ +// +fn migration(host: &mut Host) -> anyhow::Result { + match read_storage_version(host)?.next() { + Some(next_version) => { + let status = migrate_to(host, next_version)?; + + // Record the migration was applied. Even if the migration for `next_version` returns + // `None`, we consider it done. A good use case for `None` is for instance for a + // migration that does not apply to the current network. + if status != MigrationStatus::InProgress { + store_storage_version(host, next_version)?; + // `InProgress` so that we reboot and try apply the next migration, if any. + return Ok(MigrationStatus::InProgress); + } + + Ok(status) + } + None => Ok(MigrationStatus::None), + } +} + +pub fn storage_migration( + host: &mut Host, +) -> Result { + let migration_result = migration(host); + migration_result.map_err(|_| Error::UpgradeError(UpgradeProcessError::Fallback)) +} diff --git a/etherlink/kernel_calypso2/kernel/src/parsing.rs b/etherlink/kernel_calypso2/kernel/src/parsing.rs new file mode 100644 index 000000000000..b32aa6852285 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/parsing.rs @@ -0,0 +1,955 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// +// SPDX-License-Identifier: MIT + +use crate::blueprint_storage::MAXIMUM_NUMBER_OF_CHUNKS; +use crate::configuration::{DalConfiguration, TezosContracts}; +use crate::tick_model::constants::{ + TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE, TICKS_FOR_DELAYED_MESSAGES, + TICKS_PER_DEPOSIT_PARSING, +}; +use crate::{ + bridge::Deposit, + dal_slot_import_signal::DalSlotImportSignals, + inbox::{Transaction, TransactionContent}, + sequencer_blueprint::{SequencerBlueprint, UnsignedSequencerBlueprint}, + upgrade::KernelUpgrade, + upgrade::SequencerUpgrade, +}; +use evm_execution::fa_bridge::deposit::FaDeposit; +use evm_execution::fa_bridge::TICKS_PER_FA_DEPOSIT_PARSING; +use primitive_types::U256; +use rlp::Encodable; +use sha3::{Digest, Keccak256}; +use tezos_crypto_rs::{hash::ContractKt1Hash, PublicKeySignatureVerifier}; +use tezos_ethereum::{ + rlp_helpers::FromRlpBytes, + transaction::{TransactionHash, TRANSACTION_HASH_SIZE}, + tx_common::EthereumTransactionCommon, +}; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::{ + contract::Contract, + inbox::{ + ExternalMessageFrame, InboxMessage, InfoPerLevel, InternalInboxMessage, Transfer, + }, + michelson::{ticket::FA2_1Ticket, MichelsonBytes, MichelsonOr, MichelsonPair}, + public_key::PublicKey, +}; +use tezos_smart_rollup_host::input::Message; + +/// On an option, either the value, or if `None`, interrupt and return the +/// default value of the return type instead. +#[macro_export] +macro_rules! parsable { + ($expr : expr) => { + match $expr { + Some(x) => x, + None => return Default::default(), + } + }; +} + +/// Divides one slice into two at an index. +/// +/// The first will contain all indices from `[0, mid)` (excluding the index +/// `mid` itself) and the second will contain all indices from `[mid, len)` +/// (excluding the index `len` itself). +/// +/// Will return None if `mid > len`. +pub fn split_at(bytes: &[u8], mid: usize) -> Option<(&[u8], &[u8])> { + let left = bytes.get(0..mid)?; + let right = bytes.get(mid..)?; + Some((left, right)) +} + +pub const SIMULATION_TAG: u8 = u8::MAX; + +const SIMPLE_TRANSACTION_TAG: u8 = 0; + +const NEW_CHUNKED_TRANSACTION_TAG: u8 = 1; + +const TRANSACTION_CHUNK_TAG: u8 = 2; + +const SEQUENCER_BLUEPRINT_TAG: u8 = 3; + +pub const DAL_SLOT_IMPORT_SIGNAL_TAG: u8 = 4; + +const FORCE_KERNEL_UPGRADE_TAG: u8 = 0xff; + +pub const MAX_SIZE_PER_CHUNK: usize = 4095 // Max input size minus external tag + - 1 // ExternalMessageFrame tag + - 20 // Smart rollup address size (ExternalMessageFrame::Targetted) + - 1 // Transaction chunk tag + - 2 // Number of chunks (u16) + - 32 // Transaction hash size + - 32; // Chunk hash size + +#[derive(Debug, PartialEq, Clone)] +pub struct LevelWithInfo { + pub level: u32, + pub info: InfoPerLevel, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum ProxyInput { + SimpleTransaction(Box), + NewChunkedTransaction { + tx_hash: TransactionHash, + num_chunks: u16, + chunk_hashes: Vec, + }, + TransactionChunk { + tx_hash: TransactionHash, + i: u16, + chunk_hash: TransactionHash, + data: Vec, + }, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum SequencerBlueprintRes { + SequencerBlueprint(UnsignedSequencerBlueprint), + InvalidNumberOfChunks, + InvalidSignature, + InvalidNumber, + Unparsable, +} + +#[derive(Debug, PartialEq, Clone)] +pub enum SequencerInput { + DelayedInput(Box), + SequencerBlueprint(SequencerBlueprintRes), + DalSlotImportSignals(DalSlotImportSignals), +} + +#[derive(Debug, PartialEq, Clone)] +pub enum Input { + ModeSpecific(Mode), + Deposit((Deposit, Option)), + FaDeposit((FaDeposit, Option)), + Upgrade(KernelUpgrade), + SequencerUpgrade(SequencerUpgrade), + RemoveSequencer, + Info(LevelWithInfo), + ForceKernelUpgrade, +} + +#[derive(Debug, PartialEq, Default)] +pub enum InputResult { + /// No further inputs + NoInput, + /// Some decoded input + Input(Input), + /// Simulation mode starts after this input + Simulation, + #[default] + /// Unparsable input, to be ignored + Unparsable, +} + +pub type RollupType = MichelsonOr< + MichelsonOr, MichelsonBytes>, + MichelsonBytes, +>; + +/// Implements the trait for an input to be readable from the inbox, either +/// being an external input or an L1 smart contract input. It assumes all +/// verifications have already been done: +/// +/// - The original inputs are prefixed by the frame protocol for the correct +/// rollup, and the prefix has been removed +/// +/// - The internal message was addressed to the rollup, and `parse_internal` +/// expects the bytes from `Left (Right )` +pub trait Parsable { + type Context; + + fn parse_external( + tag: &u8, + input: &[u8], + context: &mut Self::Context, + ) -> InputResult + where + Self: std::marker::Sized; + + fn parse_internal_bytes( + source: ContractKt1Hash, + bytes: &[u8], + context: &mut Self::Context, + ) -> InputResult + where + Self: std::marker::Sized; + + fn on_deposit(context: &mut Self::Context); + + fn on_fa_deposit(context: &mut Self::Context); +} + +impl ProxyInput { + fn parse_simple_transaction(bytes: &[u8]) -> InputResult { + // Next 32 bytes is the transaction hash. + // Remaining bytes is the rlp encoded transaction. + let (tx_hash, remaining) = parsable!(split_at(bytes, TRANSACTION_HASH_SIZE)); + let tx_hash: TransactionHash = parsable!(tx_hash.try_into().ok()); + let produced_hash: [u8; TRANSACTION_HASH_SIZE] = + Keccak256::digest(remaining).into(); + if tx_hash != produced_hash { + // The produced hash from the transaction data is not the same as the + // one sent, the message is ignored. + return InputResult::Unparsable; + } + let tx: EthereumTransactionCommon = parsable!(remaining.try_into().ok()); + InputResult::Input(Input::ModeSpecific(Self::SimpleTransaction(Box::new( + Transaction { + tx_hash, + content: TransactionContent::Ethereum(tx), + }, + )))) + } + + fn parse_new_chunked_transaction(bytes: &[u8]) -> InputResult { + // Next 32 bytes is the transaction hash. + let (tx_hash, remaining) = parsable!(split_at(bytes, TRANSACTION_HASH_SIZE)); + let tx_hash: TransactionHash = parsable!(tx_hash.try_into().ok()); + // Next 2 bytes is the number of chunks. + let (num_chunks, remaining) = parsable!(split_at(remaining, 2)); + let num_chunks = u16::from_le_bytes(num_chunks.try_into().unwrap()); + if remaining.len() != (TRANSACTION_HASH_SIZE * usize::from(num_chunks)) { + return InputResult::Unparsable; + } + let mut chunk_hashes = vec![]; + let mut remaining = remaining; + for _ in 0..num_chunks { + let (chunk_hash, remaining_hashes) = + parsable!(split_at(remaining, TRANSACTION_HASH_SIZE)); + let chunk_hash: TransactionHash = parsable!(chunk_hash.try_into().ok()); + remaining = remaining_hashes; + chunk_hashes.push(chunk_hash) + } + InputResult::Input(Input::ModeSpecific(Self::NewChunkedTransaction { + tx_hash, + num_chunks, + chunk_hashes, + })) + } + + fn parse_transaction_chunk(bytes: &[u8]) -> InputResult { + // Next 32 bytes is the transaction hash. + let (tx_hash, remaining) = parsable!(split_at(bytes, TRANSACTION_HASH_SIZE)); + let tx_hash: TransactionHash = parsable!(tx_hash.try_into().ok()); + // Next 2 bytes is the index. + let (i, remaining) = parsable!(split_at(remaining, 2)); + let i = u16::from_le_bytes(i.try_into().unwrap()); + // Next 32 bytes is the chunk hash. + let (chunk_hash, remaining) = + parsable!(split_at(remaining, TRANSACTION_HASH_SIZE)); + let chunk_hash: TransactionHash = parsable!(chunk_hash.try_into().ok()); + let data_hash: [u8; TRANSACTION_HASH_SIZE] = Keccak256::digest(remaining).into(); + // Check if the produced hash from the data is the same as the chunk hash. + if chunk_hash != data_hash { + return InputResult::Unparsable; + } + InputResult::Input(Input::ModeSpecific(Self::TransactionChunk { + tx_hash, + i, + chunk_hash, + data: remaining.to_vec(), + })) + } +} + +impl Parsable for ProxyInput { + type Context = (); + + fn parse_external(tag: &u8, input: &[u8], _: &mut ()) -> InputResult { + // External transactions are only allowed in proxy mode + match *tag { + SIMPLE_TRANSACTION_TAG => Self::parse_simple_transaction(input), + NEW_CHUNKED_TRANSACTION_TAG => Self::parse_new_chunked_transaction(input), + TRANSACTION_CHUNK_TAG => Self::parse_transaction_chunk(input), + _ => InputResult::Unparsable, + } + } + + fn parse_internal_bytes( + _: ContractKt1Hash, + _: &[u8], + _: &mut (), + ) -> InputResult { + InputResult::Unparsable + } + + fn on_deposit(_: &mut Self::Context) {} + + fn on_fa_deposit(_: &mut Self::Context) {} +} + +pub struct BufferTransactionChunks { + pub total: u8, + pub accumulated: u8, + pub chunks: Vec, +} + +pub struct SequencerParsingContext { + pub sequencer: PublicKey, + pub delayed_bridge: ContractKt1Hash, + pub allocated_ticks: u64, + pub dal_configuration: Option, + // Delayed inbox transactions may come in chunks. If the buffer is + // [Some _] a chunked transaction is being parsed, + pub buffer_transaction_chunks: Option, + // Head level of the chain, handling blueprints before the head is useless. + // It is optional to handle when the first block has not been produced + // yet. + pub head_level: Option, +} + +fn check_unsigned_blueprint_chunk( + unsigned_seq_blueprint: UnsignedSequencerBlueprint, + head_level: &Option, +) -> SequencerBlueprintRes { + if MAXIMUM_NUMBER_OF_CHUNKS < unsigned_seq_blueprint.nb_chunks { + return SequencerBlueprintRes::InvalidNumberOfChunks; + } + + if let Some(head_level) = head_level { + if unsigned_seq_blueprint.number.le(head_level) { + return SequencerBlueprintRes::InvalidNumber; + } + } + + SequencerBlueprintRes::SequencerBlueprint(unsigned_seq_blueprint) +} + +pub fn parse_unsigned_blueprint_chunk( + bytes: &[u8], + head_level: &Option, +) -> SequencerBlueprintRes { + // Parse an unsigned sequencer blueprint + match UnsignedSequencerBlueprint::from_rlp_bytes(bytes).ok() { + None => SequencerBlueprintRes::Unparsable, + Some(unsigned_seq_blueprint) => { + check_unsigned_blueprint_chunk(unsigned_seq_blueprint, head_level) + } + } +} + +pub fn parse_blueprint_chunk( + bytes: &[u8], + sequencer: &PublicKey, + head_level: &Option, +) -> SequencerBlueprintRes { + // Parse the sequencer blueprint + match SequencerBlueprint::from_rlp_bytes(bytes).ok() { + None => SequencerBlueprintRes::Unparsable, + Some(SequencerBlueprint { + blueprint, + signature, + }) => match check_unsigned_blueprint_chunk(blueprint, head_level) { + SequencerBlueprintRes::SequencerBlueprint(unsigned_seq_blueprint) => { + let bytes = unsigned_seq_blueprint.rlp_bytes().to_vec(); + + let correctly_signed = sequencer + .verify_signature(&signature.clone().into(), &bytes) + .unwrap_or(false); + + if correctly_signed { + SequencerBlueprintRes::SequencerBlueprint(unsigned_seq_blueprint) + } else { + SequencerBlueprintRes::InvalidSignature + } + } + res @ (SequencerBlueprintRes::InvalidNumberOfChunks + | SequencerBlueprintRes::InvalidSignature + | SequencerBlueprintRes::InvalidNumber + | SequencerBlueprintRes::Unparsable) => res, + }, + } +} + +impl SequencerInput { + fn parse_sequencer_blueprint_input( + bytes: &[u8], + context: &mut SequencerParsingContext, + ) -> InputResult { + // Inputs are 4096 bytes longs at most, and even in the future they + // should be limited by the size of native words of the VM which is + // 32bits. + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE); + + let res = parse_blueprint_chunk(bytes, &context.sequencer, &context.head_level); + InputResult::Input(Input::ModeSpecific(Self::SequencerBlueprint(res))) + } + + pub fn parse_dal_slot_import_signal( + bytes: &[u8], + context: &mut SequencerParsingContext, + ) -> InputResult { + // Inputs are 4096 bytes longs at most, and even in the future they + // should be limited by the size of native words of the VM which is + // 32bits. + // TODO: Define the tick model as this is temporary. + // https://gitlab.com/tezos/tezos/-/issues/7455 + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE); + + let Some(dal) = &context.dal_configuration else { + return InputResult::Unparsable; + }; + + // Parse the signals + let signed_signals: DalSlotImportSignals = + parsable!(FromRlpBytes::from_rlp_bytes(bytes).ok()); + + // Check if all slot indices are valid + for unsigned_signal in &signed_signals.signals.0 { + for slot_index in &unsigned_signal.slot_indices.0 { + if !dal.slot_indices.contains(slot_index) { + return InputResult::Unparsable; + } + } + } + + // Encode the entire list of unsigned signals + let bytes = signed_signals.signals.rlp_bytes().to_vec(); + + // Verify the signature against the entire encoded list + let correctly_signed = context + .sequencer + .verify_signature(&signed_signals.signature.clone().into(), &bytes) + .unwrap_or(false); + + if correctly_signed { + InputResult::Input(Input::ModeSpecific(Self::DalSlotImportSignals( + signed_signals, + ))) + } else { + InputResult::Unparsable + } + } +} + +mod delayed_chunked_transaction { + + // This module implements the fairly simple logic of messaging protocol + // for delayed transactions that does not fit in a single inbox message. + // + // The protocol is the following: + // + // [NEW_CHUNK_TAG; ] => Message announcing the number of chunks. The + // next messages will be the chunks. + // [CHUNK_TAG; ] => Message containing one chunk. + // + // We consider that all messages are transmitted within a single + // L1 operation, but in several inbox messages. + + use crate::parsing::BufferTransactionChunks; + use sha3::{Digest, Keccak256}; + use tezos_ethereum::{ + transaction::TransactionHash, tx_common::EthereumTransactionCommon, + }; + + pub const NEW_CHUNK_TAG: u8 = 0x0; + pub const CHUNK_TAG: u8 = 0x1; + + pub fn parse_new_chunk( + bytes: &[u8], + buffer_transaction_chunks: &mut Option, + ) { + if let [len] = bytes { + // Overwrites any existing transaction chunks buffer. It's the + // responsibility of the contract to send correct values. + *buffer_transaction_chunks = Some(BufferTransactionChunks { + total: *len, + accumulated: 0, + chunks: vec![], + }) + } + } + + pub fn parse_chunk( + bytes: &[u8], + buffer_transaction_chunks_opt: &mut Option, + ) -> Option<(EthereumTransactionCommon, TransactionHash)> { + match buffer_transaction_chunks_opt { + None => { + // Again, it's the responsibility of the contract to respect + // the message protocol. + None + } + Some(buffer_transaction_chunks) => { + buffer_transaction_chunks.chunks.extend(bytes); + buffer_transaction_chunks.accumulated += 1; + + if buffer_transaction_chunks.total + == buffer_transaction_chunks.accumulated + { + // Transaction is complete + let res = match EthereumTransactionCommon::from_bytes( + &buffer_transaction_chunks.chunks, + ) { + Ok(transaction) => { + let tx_hash: TransactionHash = + Keccak256::digest(&buffer_transaction_chunks.chunks) + .into(); + Some((transaction, tx_hash)) + } + Err(_) => None, + }; + *buffer_transaction_chunks_opt = None; + res + } else { + None + } + } + } + } +} + +impl Parsable for SequencerInput { + type Context = SequencerParsingContext; + + fn parse_external( + tag: &u8, + input: &[u8], + context: &mut Self::Context, + ) -> InputResult { + // External transactions are only allowed in proxy mode + match *tag { + SEQUENCER_BLUEPRINT_TAG => { + Self::parse_sequencer_blueprint_input(input, context) + } + DAL_SLOT_IMPORT_SIGNAL_TAG => { + Self::parse_dal_slot_import_signal(input, context) + } + _ => InputResult::Unparsable, + } + } + + /// Parses transactions that come from the delayed inbox. + fn parse_internal_bytes( + source: ContractKt1Hash, + bytes: &[u8], + context: &mut Self::Context, + ) -> InputResult { + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_FOR_DELAYED_MESSAGES); + + if context.delayed_bridge.as_ref() != source.as_ref() { + return InputResult::Unparsable; + }; + + let (tag, remaining) = parsable!(bytes.split_first()); + + let (tx, tx_hash) = parsable!(match *tag { + delayed_chunked_transaction::NEW_CHUNK_TAG => { + delayed_chunked_transaction::parse_new_chunk( + remaining, + &mut context.buffer_transaction_chunks, + ); + None + } + delayed_chunked_transaction::CHUNK_TAG => + delayed_chunked_transaction::parse_chunk( + remaining, + &mut context.buffer_transaction_chunks, + ), + _ => None, + }); + + InputResult::Input(Input::ModeSpecific(Self::DelayedInput(Box::new( + Transaction { + tx_hash, + content: TransactionContent::EthereumDelayed(tx), + }, + )))) + } + + fn on_deposit(context: &mut Self::Context) { + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_PER_DEPOSIT_PARSING); + } + + fn on_fa_deposit(context: &mut Self::Context) { + context.allocated_ticks = context + .allocated_ticks + .saturating_sub(TICKS_PER_FA_DEPOSIT_PARSING); + } +} + +impl InputResult { + fn parse_kernel_upgrade(bytes: &[u8]) -> Self { + let kernel_upgrade = parsable!(KernelUpgrade::from_rlp_bytes(bytes).ok()); + Self::Input(Input::Upgrade(kernel_upgrade)) + } + + fn parse_sequencer_update(bytes: &[u8]) -> Self { + if bytes.is_empty() { + Self::Input(Input::RemoveSequencer) + } else { + let sequencer_upgrade = + parsable!(SequencerUpgrade::from_rlp_bytes(bytes).ok()); + Self::Input(Input::SequencerUpgrade(sequencer_upgrade)) + } + } + + /// Parses an external message + /// + // External message structure : + // FRAMING_PROTOCOL_TARGETTED 21B / MESSAGE_TAG 1B / DATA + pub fn parse_external( + input: &[u8], + smart_rollup_address: &[u8], + context: &mut Mode::Context, + ) -> Self { + // Compatibility with framing protocol for external messages + let remaining = match ExternalMessageFrame::parse(input) { + Ok(ExternalMessageFrame::Targetted { address, contents }) + if address.hash().as_ref() == smart_rollup_address => + { + contents + } + _ => return InputResult::Unparsable, + }; + + let (transaction_tag, remaining) = parsable!(remaining.split_first()); + // External transactions are only allowed in proxy mode + match *transaction_tag { + FORCE_KERNEL_UPGRADE_TAG => Self::Input(Input::ForceKernelUpgrade), + _ => Mode::parse_external(transaction_tag, remaining, context), + } + } + + fn parse_simulation(input: &[u8]) -> Self { + if input.is_empty() { + InputResult::Simulation + } else { + InputResult::Unparsable + } + } + + fn parse_fa_deposit( + host: &mut Host, + ticket: FA2_1Ticket, + routing_info: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + context: &mut Mode::Context, + ) -> Self { + // Account for tick at the beginning of the deposit, in case it fails + // directly. We prefer to overapproximate rather than under approximate. + Mode::on_fa_deposit(context); + match FaDeposit::try_parse(ticket, routing_info, inbox_level, inbox_msg_id) { + Ok((fa_deposit, chain_id)) => { + log!(host, Debug, "Parsed from input: {}", fa_deposit.display()); + InputResult::Input(Input::FaDeposit((fa_deposit, chain_id))) + } + Err(err) => { + log!( + host, + Debug, + "FA deposit ignored because of parsing errors: {}", + err + ); + InputResult::Unparsable + } + } + } + + fn parse_deposit( + host: &mut Host, + ticket: FA2_1Ticket, + receiver: MichelsonBytes, + inbox_level: u32, + inbox_msg_id: u32, + context: &mut Mode::Context, + ) -> Self { + // Account for tick at the beginning of the deposit, in case it fails + // directly. We prefer to overapproximate rather than under approximate. + Mode::on_deposit(context); + match Deposit::try_parse(ticket, receiver, inbox_level, inbox_msg_id) { + Ok((deposit, chain_id)) => { + log!(host, Info, "Parsed from input: {}", deposit.display()); + Self::Input(Input::Deposit((deposit, chain_id))) + } + Err(err) => { + log!( + host, + Info, + "Deposit ignored because of parsing errors: {}", + err + ); + Self::Unparsable + } + } + } + + #[allow(clippy::too_many_arguments)] + fn parse_internal_transfer( + host: &mut Host, + transfer: Transfer, + smart_rollup_address: &[u8], + tezos_contracts: &TezosContracts, + context: &mut Mode::Context, + inbox_level: u32, + inbox_msg_id: u32, + enable_fa_deposits: bool, + ) -> Self { + if transfer.destination.hash().as_ref() != smart_rollup_address { + log!( + host, + Info, + "Deposit ignored because of different smart rollup address" + ); + return InputResult::Unparsable; + } + + let source = transfer.sender; + + match transfer.payload { + MichelsonOr::Left(left) => match left { + MichelsonOr::Left(MichelsonPair(receiver, ticket)) => { + match &ticket.creator().0 { + Contract::Originated(kt1) => { + if Some(kt1) == tezos_contracts.ticketer.as_ref() { + Self::parse_deposit( + host, + ticket, + receiver, + inbox_level, + inbox_msg_id, + context, + ) + } else if enable_fa_deposits { + Self::parse_fa_deposit( + host, + ticket, + receiver, + inbox_level, + inbox_msg_id, + context, + ) + } else { + log!( + host, + Info, + "FA deposit ignored because the feature is disabled" + ); + InputResult::Unparsable + } + } + _ => { + log!( + host, + Info, + "Deposit ignored because of invalid ticketer" + ); + InputResult::Unparsable + } + } + } + MichelsonOr::Right(MichelsonBytes(bytes)) => { + Mode::parse_internal_bytes(source, &bytes, context) + } + }, + MichelsonOr::Right(MichelsonBytes(bytes)) => { + if tezos_contracts.is_admin(&source) + || tezos_contracts.is_kernel_governance(&source) + || tezos_contracts.is_kernel_security_governance(&source) + { + Self::parse_kernel_upgrade(&bytes) + } else if tezos_contracts.is_sequencer_governance(&source) { + Self::parse_sequencer_update(&bytes) + } else { + Self::Unparsable + } + } + } + } + + #[allow(clippy::too_many_arguments)] + fn parse_internal( + host: &mut Host, + message: InternalInboxMessage, + smart_rollup_address: &[u8], + tezos_contracts: &TezosContracts, + context: &mut Mode::Context, + level: u32, + msg_id: u32, + enable_fa_deposits: bool, + ) -> Self { + match message { + InternalInboxMessage::InfoPerLevel(info) => { + InputResult::Input(Input::Info(LevelWithInfo { level, info })) + } + InternalInboxMessage::Transfer(transfer) => Self::parse_internal_transfer( + host, + transfer, + smart_rollup_address, + tezos_contracts, + context, + level, + msg_id, + enable_fa_deposits, + ), + _ => InputResult::Unparsable, + } + } + + pub fn parse( + host: &mut Host, + input: Message, + smart_rollup_address: [u8; 20], + tezos_contracts: &TezosContracts, + context: &mut Mode::Context, + enable_fa_deposits: bool, + ) -> Self { + let bytes = Message::as_ref(&input); + let (input_tag, remaining) = parsable!(bytes.split_first()); + if *input_tag == SIMULATION_TAG { + return Self::parse_simulation(remaining); + }; + + match InboxMessage::::parse(bytes) { + Ok((_remaing, message)) => match message { + InboxMessage::External(message) => { + Self::parse_external(message, &smart_rollup_address, context) + } + InboxMessage::Internal(message) => Self::parse_internal( + host, + message, + &smart_rollup_address, + tezos_contracts, + context, + input.level, + input.id, + enable_fa_deposits, + ), + }, + Err(_) => InputResult::Unparsable, + } + } +} + +#[cfg(test)] +pub mod tests { + use super::*; + use tezos_crypto_rs::hash::SecretKeyEd25519; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup_host::input::Message; + + const ZERO_SMART_ROLLUP_ADDRESS: [u8; 20] = [0; 20]; + + #[test] + fn parse_unparsable_transaction() { + let mut host = MockKernelHost::default(); + + let message = Message::new(0, 0, vec![1, 9, 32, 58, 59, 30]); + assert_eq!( + InputResult::::parse( + &mut host, + message, + ZERO_SMART_ROLLUP_ADDRESS, + &TezosContracts { + ticketer: None, + admin: None, + sequencer_governance: None, + kernel_governance: None, + kernel_security_governance: None, + }, + &mut (), + false, + ), + InputResult::Unparsable + ) + } + + pub fn sequencer_signed_blueprint_chunk_bytes( + unsigned_blueprint: &UnsignedSequencerBlueprint, + sk: SecretKeyEd25519, + ) -> Vec { + let unsigned_blueprint_bytes = unsigned_blueprint.rlp_bytes(); + let blueprint_signature = sk.sign(unsigned_blueprint_bytes).unwrap(); + let blueprint = crate::sequencer_blueprint::SequencerBlueprint { + blueprint: unsigned_blueprint.clone(), + signature: blueprint_signature.into(), + }; + blueprint.rlp_bytes().into() + } + + #[test] + fn test_parsing_blueprint_chunk() { + let pk = PublicKey::from_b58check( + "edpkv4NmL2YPe8eiVGXUDXmPQybD725ofKirTzGRxs1X9UmaG3voKw", + ) + .unwrap(); + let sk = SecretKeyEd25519::from_base58_check( + "edsk422LGdmDnai4Cya6csM6oFmgHpDQKUhatTURJRAY4h7NHNz9sz", + ) + .unwrap(); + let head_level: Option = Some(6.into()); + + let blueprint = UnsignedSequencerBlueprint { + chunk: vec![], + number: 7.into(), + nb_chunks: 3, + chunk_index: 0, + chain_id: None, + }; + + // Blueprint chunk for level 7 is ok. + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes(&blueprint, sk.clone()), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::SequencerBlueprint(_))); + + let invalid_sk = SecretKeyEd25519::from_base58_check( + "edsk37VEgDUMt7wje8vxfao55y7JhiamjTbVM1xABSCamFgtcUqhdT", + ) + .unwrap(); + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes(&blueprint, invalid_sk), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidSignature)); + + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes( + &UnsignedSequencerBlueprint { + number: 5.into(), + ..blueprint.clone() + }, + sk.clone(), + ), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidNumber)); + + let res = parse_blueprint_chunk( + &sequencer_signed_blueprint_chunk_bytes( + &UnsignedSequencerBlueprint { + number: 6.into(), + ..blueprint + }, + sk.clone(), + ), + &pk, + &head_level, + ); + assert!(matches!(res, SequencerBlueprintRes::InvalidNumber)); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/reveal_storage.rs b/etherlink/kernel_calypso2/kernel/src/reveal_storage.rs new file mode 100644 index 000000000000..294670a97282 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/reveal_storage.rs @@ -0,0 +1,110 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::configuration::fetch_configuration; +use crate::storage::{ADMIN, SEQUENCER}; +use rlp::{Decodable, DecoderError, Rlp}; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_ethereum::rlp_helpers::{decode_field, next, FromRlpBytes}; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::public_key::PublicKey; +use tezos_smart_rollup_host::path::{OwnedPath, RefPath}; +use tezos_smart_rollup_host::runtime::ValueType; + +/// This module is a testing device, allowing to replicate the state of an existing EVM rollup +/// chain into a new deployment. It is not tick-safe, and should obviously not be used in a +/// production setup. + +const CONFIG_PATH: RefPath = RefPath::assert_from(b"/__tmp/reveal_config"); + +#[derive(Debug)] +struct Set { + to: OwnedPath, + value: Vec, +} + +impl Decodable for Set { + fn decode(decoder: &Rlp<'_>) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if Ok(2) != decoder.item_count() { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let to: Vec = decode_field(&next(&mut it)?, "to")?; + let to: RefPath = RefPath::assert_from(&to); + let to: OwnedPath = to.into(); + let value: Vec = decode_field(&next(&mut it)?, "value")?; + + Ok(Self { to, value }) + } +} + +#[derive(Debug)] +struct Sets(Vec); + +impl Decodable for Sets { + fn decode(decoder: &Rlp<'_>) -> Result { + let sets = decoder.as_list()?; + Ok(Sets(sets)) + } +} + +pub fn is_revealed_storage(host: &impl Runtime) -> bool { + matches!( + host.store_has(&CONFIG_PATH).unwrap_or_default(), + Some(ValueType::Value) + ) +} + +pub fn reveal_storage( + host: &mut impl Runtime, + sequencer: Option, + admin: Option, +) { + log!(host, Info, "Starting the reveal dump"); + + let config_bytes = host + .store_read_all(&CONFIG_PATH) + .expect("Failed reading the configuration"); + + // Decode the RLP list of instructions. + let sets = + Sets::from_rlp_bytes(&config_bytes).expect("Failed to decode the list of set"); + let length = sets.0.len(); + + for (index, Set { to, value }) in sets.0.iter().enumerate() { + if index % 50_000 == 0 { + log!(host, Info, "{}/{}", index, length) + }; + host.store_write_all(to, value) + .expect("Failed to write value"); + } + + // Remove temporary configuration + host.store_delete(&CONFIG_PATH) + .expect("Failed to remove the configuration"); + + // Change the sequencer if asked: + if let Some(sequencer) = sequencer { + let pk_b58 = PublicKey::to_b58check(&sequencer); + let bytes = String::as_bytes(&pk_b58); + host.store_write_all(&SEQUENCER, bytes).unwrap(); + } + + // Change the admin if asked: + if let Some(admin) = admin { + let kt1_b58 = admin.to_base58_check(); + let bytes = String::as_bytes(&kt1_b58); + host.store_write_all(&ADMIN, bytes).unwrap(); + } + + log!(host, Info, "Done revealing"); + + let configuration = fetch_configuration(host); + log!(host, Info, "Configuration {}", configuration); +} diff --git a/etherlink/kernel_calypso2/kernel/src/sequencer_blueprint.rs b/etherlink/kernel_calypso2/kernel/src/sequencer_blueprint.rs new file mode 100644 index 000000000000..62c778988005 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/sequencer_blueprint.rs @@ -0,0 +1,347 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +use primitive_types::{H256, U256}; +use rlp::{Decodable, DecoderError, Encodable}; +use tezos_crypto_rs::hash::UnknownSignature; +use tezos_ethereum::rlp_helpers::{ + self, append_timestamp, append_u16_le, append_u256_le, decode_field_u16_le, + decode_field_u256_le, decode_timestamp, +}; +use tezos_smart_rollup::types::Timestamp; + +use crate::delayed_inbox::Hash; + +#[derive(Debug, Clone)] +pub struct BlueprintWithDelayedHashes { + pub parent_hash: H256, + pub delayed_hashes: Vec, + // We are using `Vec` for the transaction instead of `EthereumTransactionCommon` + // to avoid decoding then re-encoding to compute the hash. + pub transactions: Vec>, + pub timestamp: Timestamp, +} + +impl Encodable for BlueprintWithDelayedHashes { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let BlueprintWithDelayedHashes { + parent_hash, + delayed_hashes, + transactions, + timestamp, + } = self; + stream.begin_list(4); + stream.append(parent_hash); + stream.append_list(delayed_hashes); + stream.append_list::, _>(transactions); + append_timestamp(stream, *timestamp); + } +} + +impl Decodable for BlueprintWithDelayedHashes { + fn decode(decoder: &rlp::Rlp) -> Result { + rlp_helpers::check_list(decoder, 4)?; + + let mut it = decoder.iter(); + let parent_hash = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "parent_hash")?; + let delayed_hashes = + rlp_helpers::decode_list(&rlp_helpers::next(&mut it)?, "delayed_hashes")?; + let transactions = + rlp_helpers::decode_list(&rlp_helpers::next(&mut it)?, "transactions")?; + let timestamp = decode_timestamp(&rlp_helpers::next(&mut it)?)?; + + Ok(Self { + delayed_hashes, + parent_hash, + transactions, + timestamp, + }) + } +} + +#[derive(PartialEq, Debug, Clone)] +pub struct UnsignedSequencerBlueprint { + pub chunk: Vec, + pub number: U256, + pub nb_chunks: u16, + pub chunk_index: u16, + pub chain_id: Option, +} + +#[derive(PartialEq, Debug, Clone)] +pub struct SequencerBlueprint { + pub blueprint: UnsignedSequencerBlueprint, + pub signature: UnknownSignature, +} + +impl From<&SequencerBlueprint> for UnsignedSequencerBlueprint { + fn from(val: &SequencerBlueprint) -> UnsignedSequencerBlueprint { + val.blueprint.clone() + } +} + +impl Encodable for UnsignedSequencerBlueprint { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let Self { + chunk, + number, + nb_chunks, + chunk_index, + chain_id, + } = self; + stream.begin_list(4 + if chain_id.is_some() { 1 } else { 0 }); + stream.append(chunk); + append_u256_le(stream, number); + append_u16_le(stream, nb_chunks); + append_u16_le(stream, chunk_index); + if let Some(chain_id) = chain_id { + append_u256_le(stream, chain_id); + } + } +} + +impl Encodable for SequencerBlueprint { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + let UnsignedSequencerBlueprint { + chunk, + number, + nb_chunks, + chunk_index, + chain_id, + } = &self.blueprint; + stream.begin_list(5 + if chain_id.is_some() { 1 } else { 0 }); + stream.append(chunk); + append_u256_le(stream, number); + append_u16_le(stream, nb_chunks); + append_u16_le(stream, chunk_index); + if let Some(chain_id) = chain_id { + append_u256_le(stream, chain_id); + } + stream.append(&self.signature.as_ref()); + } +} + +impl Decodable for UnsignedSequencerBlueprint { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + match decoder.item_count()? { + 4 => { + // Optional chain_id field is absent + let mut it = decoder.iter(); + let chunk = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "chunk")?; + let number = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "number")?; + let nb_chunks = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "nb_chunks")?; + let chunk_index = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "chunk_index")?; + Ok(Self { + chunk, + number, + nb_chunks, + chunk_index, + chain_id: None, + }) + } + 5 => { + // Optional chain_id field is provided + let mut it = decoder.iter(); + let chunk = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "chunk")?; + let number = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "number")?; + let nb_chunks = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "nb_chunks")?; + let chunk_index = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "chunk_index")?; + let chain_id = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "chain_id")?; + Ok(Self { + chunk, + number, + nb_chunks, + chunk_index, + chain_id: Some(chain_id), + }) + } + _ => Err(DecoderError::RlpInvalidLength), + } + } +} + +impl Decodable for SequencerBlueprint { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + match decoder.item_count()? { + 5 => { + // Optional chain_id field is absent + let mut it = decoder.iter(); + let chunk = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "chunk")?; + let number = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "number")?; + let nb_chunks = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "nb_chunks")?; + let chunk_index = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "chunk_index")?; + let bytes: Vec = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "signature")?; + let signature = UnknownSignature::try_from(bytes.as_slice()) + .map_err(|_| DecoderError::Custom("Invalid signature encoding"))?; + let blueprint = UnsignedSequencerBlueprint { + chunk, + number, + nb_chunks, + chunk_index, + chain_id: None, + }; + Ok(Self { + blueprint, + signature, + }) + } + 6 => { + // Optional chain_id field is provided + let mut it = decoder.iter(); + let chunk = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "chunk")?; + let number = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "number")?; + let nb_chunks = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "nb_chunks")?; + let chunk_index = + decode_field_u16_le(&rlp_helpers::next(&mut it)?, "chunk_index")?; + let chain_id = + decode_field_u256_le(&rlp_helpers::next(&mut it)?, "chain_id")?; + let bytes: Vec = + rlp_helpers::decode_field(&rlp_helpers::next(&mut it)?, "signature")?; + let signature = UnknownSignature::try_from(bytes.as_slice()) + .map_err(|_| DecoderError::Custom("Invalid signature encoding"))?; + let blueprint = UnsignedSequencerBlueprint { + chunk, + number, + nb_chunks, + chunk_index, + chain_id: Some(chain_id), + }; + Ok(Self { + blueprint, + signature, + }) + } + _ => Err(DecoderError::RlpInvalidLength), + } + } +} + +#[cfg(test)] +mod tests { + use super::{SequencerBlueprint, UnsignedSequencerBlueprint}; + use crate::blueprint::Blueprint; + use crate::inbox::Transaction; + use crate::inbox::TransactionContent::Ethereum; + use primitive_types::{H160, U256}; + use rlp::{Decodable, Encodable}; + use tezos_crypto_rs::hash::UnknownSignature; + use tezos_ethereum::rlp_helpers::FromRlpBytes; + use tezos_ethereum::{ + transaction::TRANSACTION_HASH_SIZE, tx_common::EthereumTransactionCommon, + }; + use tezos_smart_rollup_encoding::timestamp::Timestamp; + + fn rlp_roundtrip(v: S) { + let bytes = v.rlp_bytes(); + let v2: S = FromRlpBytes::from_rlp_bytes(&bytes).expect("Should be decodable"); + assert_eq!(v, v2, "Roundtrip failed on {:?}", v) + } + + fn address_from_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + + fn tx_(i: u64) -> EthereumTransactionCommon { + EthereumTransactionCommon::new( + tezos_ethereum::transaction::TransactionType::Legacy, + Some(U256::one()), + i, + U256::from(40000000u64), + U256::from(40000000u64), + 21000u64, + address_from_str("423163e58aabec5daa3dd1130b759d24bef0f6ea"), + U256::from(500000000u64), + vec![], + vec![], + None, + ) + } + + fn dummy_transaction(i: u8) -> Transaction { + Transaction { + tx_hash: [i; TRANSACTION_HASH_SIZE], + content: Ethereum(tx_(i.into())), + } + } + + fn dummy_blueprint_unsigned(chain_id: Option) -> UnsignedSequencerBlueprint { + let transactions = vec![dummy_transaction(0), dummy_transaction(1)]; + let timestamp = Timestamp::from(42); + let blueprint = Blueprint { + timestamp, + transactions, + }; + let chunk = rlp::Encodable::rlp_bytes(&blueprint); + UnsignedSequencerBlueprint { + chunk: chunk.into(), + number: U256::from(42), + nb_chunks: 1u16, + chunk_index: 0u16, + chain_id, + } + } + + fn dummy_blueprint(chain_id: Option) -> SequencerBlueprint { + let signature = UnknownSignature::from_base58_check( + "sigdGBG68q2vskMuac4AzyNb1xCJTfuU8MiMbQtmZLUCYydYrtTd5Lessn1EFLTDJzjXoYxRasZxXbx6tHnirbEJtikcMHt3" + ).expect("signature decoding should work"); + + SequencerBlueprint { + blueprint: dummy_blueprint_unsigned(chain_id), + signature, + } + } + + #[test] + fn roundtrip_rlp_no_chain_id() { + let chunk = dummy_blueprint(None); + rlp_roundtrip(chunk); + } + + #[test] + fn roundtrip_rlp() { + let chain_id = U256::one(); + let chunk = dummy_blueprint(Some(chain_id)); + rlp_roundtrip(chunk); + } + + #[test] + fn roundtrip_rlp_no_chain_id_unsigned() { + let chunk = dummy_blueprint_unsigned(None); + rlp_roundtrip(chunk); + } + + #[test] + fn roundtrip_rlp_unsigned() { + let chain_id = U256::one(); + let chunk = dummy_blueprint_unsigned(Some(chain_id)); + rlp_roundtrip(chunk); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/simulation.rs b/etherlink/kernel_calypso2/kernel/src/simulation.rs new file mode 100644 index 000000000000..9f45101ee254 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/simulation.rs @@ -0,0 +1,1031 @@ +// SPDX-FileCopyrightText: 2022-2024 TriliTech +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +// Module containing most Simulation related code, in one place, to be deleted +// when the proxy node simulates directly + +use crate::block_storage; +use crate::configuration::fetch_limits; +use crate::fees::simulation_add_gas_for_fees; +use crate::storage::{ + read_last_info_per_level_timestamp, read_sequencer_pool_address, read_tracer_input, +}; +use crate::tick_model::constants::MAXIMUM_GAS_LIMIT; +use crate::{error::Error, error::StorageError, storage}; + +use crate::{parsable, parsing, retrieve_chain_id, tick_model, CONFIG}; + +use evm_execution::account_storage::account_path; +use evm_execution::trace::TracerInput; +use evm_execution::{ + account_storage, + handler::{ExecutionOutcome, ExecutionResult as ExecutionOutcomeResult}, + precompiles, +}; +use evm_execution::{run_transaction, EthereumError}; +use primitive_types::{H160, U256}; +use rlp::{Decodable, DecoderError, Encodable, Rlp}; +use tezos_ethereum::block::{BlockConstants, BlockFees}; +use tezos_ethereum::rlp_helpers::{ + append_option_u64_le, check_list, decode_field, decode_option, decode_option_u64_le, + decode_timestamp, next, VersionedEncoding, +}; +use tezos_ethereum::transaction::TransactionObject; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup::types::Timestamp; + +// SIMULATION/SIMPLE/RLP_ENCODED_SIMULATION +pub const SIMULATION_SIMPLE_TAG: u8 = 1; +// SIMULATION/CREATE/NUM_CHUNKS 2B +pub const SIMULATION_CREATE_TAG: u8 = 2; +// SIMULATION/CHUNK/NUM 2B/CHUNK +pub const SIMULATION_CHUNK_TAG: u8 = 3; +/// Tag indicating simulation is an evaluation. +pub const EVALUATION_TAG: u8 = 0x00; +/// Tag indicating simulation is a validation. +/// +/// This tag can no longer be used, if you plan to add another tag, +/// do not reuse this one to avoid mistakes. +const _VALIDATION_TAG: u8 = 0x01; + +/// Version of the encoding in use. +pub const SIMULATION_ENCODING_VERSION: u8 = 0x01; + +pub const OK_TAG: u8 = 0x1; +pub const ERR_TAG: u8 = 0x2; + +const OUT_OF_TICKS_MSG: &str = "The transaction would exhaust all the ticks it + is allocated. Try reducing its gas consumption or splitting the call in + multiple steps, if possible."; +// Redefined Result as we cannot implement Decodable and Encodable traits on Result +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum SimulationResult { + Ok(T), + Err(E), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ExecutionResult { + value: Option>, + gas_used: Option, +} + +type CallResult = SimulationResult>; + +#[derive(Debug, PartialEq, Clone)] +pub struct ValidationResult { + transaction_object: TransactionObject, +} + +impl Encodable for SimulationResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + match self { + Self::Ok(value) => { + stream.append(&OK_TAG); + stream.append(value) + } + Self::Err(e) => { + stream.append(&ERR_TAG); + stream.append(e) + } + }; + } +} + +impl Decodable for SimulationResult { + fn decode(decoder: &Rlp<'_>) -> Result { + check_list(decoder, 2)?; + + let mut it = decoder.iter(); + match decode_field(&next(&mut it)?, "tag")? { + OK_TAG => Ok(Self::Ok(decode_field(&next(&mut it)?, "ok")?)), + ERR_TAG => Ok(Self::Err(decode_field(&next(&mut it)?, "error")?)), + _ => Err(DecoderError::Custom("Invalid execution tag")), + } + } +} + +impl Encodable for ExecutionResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(2); + stream.append(&self.value); + append_option_u64_le(&self.gas_used, stream); + } +} + +impl Decodable for ExecutionResult { + fn decode(decoder: &Rlp<'_>) -> Result { + check_list(decoder, 2)?; + + let mut it = decoder.iter(); + let value = decode_field(&next(&mut it)?, "value")?; + let gas_used = decode_option_u64_le(&next(&mut it)?, "gas_used")?; + Ok(ExecutionResult { value, gas_used }) + } +} + +impl Encodable for ValidationResult { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + self.transaction_object.rlp_append(stream); + } +} + +impl Decodable for ValidationResult { + fn decode(decoder: &Rlp) -> Result { + Ok(ValidationResult { + transaction_object: TransactionObject::decode(decoder)?, + }) + } +} + +/// Container for eth_call data, used in messages sent by the rollup node +/// simulation. +/// +/// They are transmitted in RLP encoded form, in messages of the form\ +/// `\parsing::SIMULATION_TAG \SIMULATION_SIMPLE_TAG \`\ +/// or in chunks if they are bigger than what the inbox can receive, with a +/// first message giving the number of chunks\ +/// `\parsing::SIMULATION_TAG \SIMULATION_CREATE_TAG \XXXX` +/// where `XXXX` is 2 bytes containing the number of chunks, followed by the +/// chunks:\ +/// `\parsing::SIMULATION_TAG \SIMULATION_CHUNK_TAG \XXXX \`\ +/// where `XXXX` is the number of the chunk over 2 bytes, and the rest is a +/// chunk of the rlp encoded evaluation. +/// +/// Ethereum doc: https://ethereum.org/en/developers/docs/apis/json-rpc/#eth_call +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct Evaluation { + /// (optional) The address the transaction is sent from.\ + /// Encoding: 20 bytes or empty (0x80) + pub from: Option, + /// The address the transaction is directed to. + /// Some indexer seem to expect it to be optionnal\ + /// Encoding: 20 bytes + pub to: Option, + /// (optional) Integer of the gas provided for the transaction execution. + /// eth_call consumes zero gas, but this parameter may be needed by some + /// executions.\ + /// Encoding: little endian + pub gas: Option, + /// (optional) Integer of the gasPrice used for each paid gas\ + /// Encoding: little endian + pub gas_price: Option, + /// (optional) Integer of the value sent with this transaction (in Wei)\ + /// Encoding: little endian + pub value: Option, + /// (optional) Hash of the method signature and encoded parameters. + pub data: Vec, + /// The gas returned by the simualtion include the DA fees if this parameter + /// is set to true. + /// + /// Important: latest versions of the node no longer use with_da_fees. All + /// simulation set with_da_fees to false. The node now adds the da fees + /// itself. + /// The field is not removed from the kernel simulation for retro-compatibility + /// reason, and in case we want to revert and switch back to this implementation. + pub with_da_fees: bool, + /// The timestamp used during simulation. It is marked as optional as + /// the support has been added in `StorageVersion::V13`. + pub timestamp: Option, +} + +impl From for SimulationResult { + fn from(err: EthereumError) -> Self { + let msg = format!("The transaction failed: {:?}.", err); + Self::Err(msg) + } +} + +impl From, EthereumError>> + for SimulationResult +{ + fn from(result: Result, EthereumError>) -> Self { + match result { + Ok(Some(ExecutionOutcome { + gas_used, result, .. + })) if result.is_success() => { + Self::Ok(SimulationResult::Ok(ExecutionResult { + value: result.output().map(|x| x.to_vec()), + gas_used: Some(gas_used), + })) + } + Ok(Some( + outcome @ ExecutionOutcome { + result: ExecutionOutcomeResult::CallReverted(_), + .. + }, + )) => Self::Ok(SimulationResult::Err( + outcome.output().unwrap_or_default().to_vec(), + )), + Ok(Some(ExecutionOutcome { + result: ExecutionOutcomeResult::OutOfTicks, + .. + })) => Self::Err(String::from(OUT_OF_TICKS_MSG)), + Ok(Some(ExecutionOutcome { result, .. })) => { + let msg = format!("The transaction failed: {:?}.", result); + Self::Err(msg) + } + Ok(None) => Self::Err(String::from( + "No outcome was produced when the transaction was ran", + )), + Err(err) => err.into(), + } + } +} + +impl Evaluation { + fn rlp_decode_v0(decoder: &Rlp<'_>) -> Result { + // the proxynode works preferably with little endian + let u64_from_le = |v: Vec| u64::from_le_bytes(parsable!(v.try_into().ok())); + let u256_from_le = |v: Vec| U256::from_little_endian(&v); + if decoder.is_list() { + if Ok(6) == decoder.item_count() { + let mut it = decoder.iter(); + let from: Option = decode_option(&next(&mut it)?, "from")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let gas: Option = + decode_option(&next(&mut it)?, "gas")?.map(u64_from_le); + let gas_price: Option = + decode_option(&next(&mut it)?, "gas_price")?.map(u64_from_le); + let value: Option = + decode_option(&next(&mut it)?, "value")?.map(u256_from_le); + let data: Vec = decode_field(&next(&mut it)?, "data")?; + Ok(Self { + from, + to, + gas, + gas_price, + value, + data, + with_da_fees: true, + timestamp: None, + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + fn rlp_decode_v1(decoder: &Rlp<'_>) -> Result { + // the proxynode works preferably with little endian + let u64_from_le = |v: Vec| u64::from_le_bytes(parsable!(v.try_into().ok())); + let u256_from_le = |v: Vec| U256::from_little_endian(&v); + if decoder.is_list() { + if Ok(7) == decoder.item_count() { + let mut it = decoder.iter(); + let from: Option = decode_option(&next(&mut it)?, "from")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let gas: Option = + decode_option(&next(&mut it)?, "gas")?.map(u64_from_le); + let gas_price: Option = + decode_option(&next(&mut it)?, "gas_price")?.map(u64_from_le); + let value: Option = + decode_option(&next(&mut it)?, "value")?.map(u256_from_le); + let data: Vec = decode_field(&next(&mut it)?, "data")?; + let with_da_fees: bool = decode_field(&next(&mut it)?, "with_da_fees")?; + Ok(Self { + from, + to, + gas, + gas_price, + value, + data, + with_da_fees, + timestamp: None, + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + fn rlp_decode_v2(decoder: &Rlp<'_>) -> Result { + // the proxynode works preferably with little endian + let u64_from_le = |v: Vec| u64::from_le_bytes(parsable!(v.try_into().ok())); + let u256_from_le = |v: Vec| U256::from_little_endian(&v); + if decoder.is_list() { + if Ok(8) == decoder.item_count() { + let mut it = decoder.iter(); + let from: Option = decode_option(&next(&mut it)?, "from")?; + let to: Option = decode_option(&next(&mut it)?, "to")?; + let gas: Option = + decode_option(&next(&mut it)?, "gas")?.map(u64_from_le); + let gas_price: Option = + decode_option(&next(&mut it)?, "gas_price")?.map(u64_from_le); + let value: Option = + decode_option(&next(&mut it)?, "value")?.map(u256_from_le); + let data: Vec = decode_field(&next(&mut it)?, "data")?; + let with_da_fees: bool = decode_field(&next(&mut it)?, "with_da_fees")?; + let timestamp: Timestamp = decode_timestamp(&next(&mut it)?)?; + Ok(Self { + from, + to, + gas, + gas_price, + value, + data, + with_da_fees, + timestamp: Some(timestamp), + }) + } else { + Err(DecoderError::RlpIncorrectListLen) + } + } else { + Err(DecoderError::RlpExpectedToBeList) + } + } + + pub fn from_bytes(bytes: &[u8]) -> Result { + let first = *bytes.first().ok_or(DecoderError::Custom("Empty bytes"))?; + match first { + 0x01 => { + let decoder = Rlp::new(&bytes[1..]); + Self::rlp_decode_v1(&decoder) + } + 0x02 => { + let decoder = Rlp::new(&bytes[1..]); + Self::rlp_decode_v2(&decoder) + } + _ => { + let decoder = Rlp::new(bytes); + Self::rlp_decode_v0(&decoder) + } + } + } + + /// Execute the simulation + pub fn run( + &self, + host: &mut Host, + tracer_input: Option, + enable_fa_withdrawals: bool, + ) -> Result, Error> { + let chain_id = retrieve_chain_id(host)?; + let minimum_base_fee_per_gas = crate::retrieve_minimum_base_fee_per_gas(host)?; + let da_fee = crate::retrieve_da_fee(host)?; + let coinbase = read_sequencer_pool_address(host).unwrap_or_default(); + let mut evm_account_storage = account_storage::init_account_storage() + .map_err(|_| Error::Storage(StorageError::AccountInitialisation))?; + + // If the simulation is performed with the zero address and has a non + // null value, the simulation will fail with out of funds. + // This can be problematic as some tools doesn't provide the `from` + // field but provide a non null `value`. + // + // We solve the issue by giving funds before the simulation to the + // zero address if necessary. + let from = self.from.unwrap_or(H160::zero()); + if let Some(value) = self.value { + if from.is_zero() { + let mut account = + evm_account_storage.get_or_create(host, &account_path(&from)?)?; + account.balance_add(host, value)?; + } + } + + let constants = match block_storage::read_current(host) { + Ok(block) => { + // Timestamp is taken from the simulation caller if provided. + // If the timestamp is missing, because of an older evm-node, + // default to last block timestamp. + let timestamp = self + .timestamp + .map(|timestamp| U256::from(timestamp.as_u64())) + .unwrap_or_else(|| U256::from(block.timestamp.as_u64())); + + let block_fees = BlockFees::new( + minimum_base_fee_per_gas, + block.base_fee_per_gas, + da_fee, + ); + BlockConstants { + number: block.number + 1, + coinbase, + timestamp, + gas_limit: crate::block::GAS_LIMIT, + block_fees, + chain_id, + prevrandao: None, + } + } + Err(_) => { + // Timestamp is taken from the simulation caller if provided. + // If the timestamp is missing, because of an older evm-node, + // default to current timestamp. + let timestamp = self + .timestamp + .map(|timestamp| U256::from(timestamp.as_u64())) + .unwrap_or_else(|| { + U256::from( + read_last_info_per_level_timestamp(host) + .unwrap_or(Timestamp::from(0)) + .as_u64(), + ) + }); + + let base_fee_per_gas = minimum_base_fee_per_gas; + let block_fees = + BlockFees::new(minimum_base_fee_per_gas, base_fee_per_gas, da_fee); + BlockConstants::first_block( + timestamp, + chain_id, + block_fees, + crate::block::GAS_LIMIT, + coinbase, + ) + } + }; + + let precompiles = precompiles::precompile_set::(enable_fa_withdrawals); + let tx_data_size = self.data.len() as u64; + let limits = fetch_limits(host); + let allocated_ticks = + tick_model::estimate_remaining_ticks_for_transaction_execution( + limits.maximum_allowed_ticks, + 0, + tx_data_size, + ); + + let gas_price = if let Some(gas_price) = self.gas_price { + U256::from(gas_price) + } else { + constants.block_fees.base_fee_per_gas() + }; + + match run_transaction( + host, + &constants, + &mut evm_account_storage, + &precompiles, + CONFIG, + self.to, + from, + self.data.clone(), + self.gas.map_or(Some(MAXIMUM_GAS_LIMIT), |gas| { + Some(u64::min(gas, MAXIMUM_GAS_LIMIT)) + }), + gas_price, + self.value.unwrap_or_default(), + false, + allocated_ticks, + false, + false, + tracer_input, + ) { + Ok(Some(outcome)) if !self.with_da_fees => { + let result: SimulationResult = + Result::Ok(Some(outcome)).into(); + + Ok(result) + } + Ok(Some(outcome)) => { + let outcome = simulation_add_gas_for_fees( + outcome, + &constants.block_fees, + &self.data, + ) + .map_err(Error::Simulation)?; + + let result: SimulationResult = + Result::Ok(Some(outcome)).into(); + + Ok(result) + } + result => Ok(result.into()), + } + } +} + +#[derive(Debug, PartialEq)] +enum Message { + Evaluation(Evaluation), +} + +impl TryFrom<&[u8]> for Message { + type Error = DecoderError; + + fn try_from(bytes: &[u8]) -> Result { + let Some(&tag) = bytes.first() else { + return Err(DecoderError::Custom("Empty simulation message")); + }; + let Some(bytes) = bytes.get(1..) else { + return Err(DecoderError::Custom("Empty simulation message")); + }; + + match tag { + EVALUATION_TAG => Evaluation::from_bytes(bytes).map(Message::Evaluation), + _ => Err(DecoderError::Custom("Unknown message to simulate")), + } + } +} + +#[derive(Default, Debug, PartialEq)] +enum Input { + #[default] + Unparsable, + Simple(Box), + NewChunked(u16), + Chunk { + i: u16, + data: Vec, + }, +} + +impl Input { + fn parse_new_chunk_simulation(bytes: &[u8]) -> Self { + let num_chunks = u16::from_le_bytes(parsable!(bytes.try_into().ok())); + Self::NewChunked(num_chunks) + } + + fn parse_simulation_chunk(bytes: &[u8]) -> Self { + let (num, remaining) = parsable!(parsing::split_at(bytes, 2)); + let i = u16::from_le_bytes(num.try_into().unwrap()); + Self::Chunk { + i, + data: remaining.to_vec(), + } + } + fn parse_simple_simulation(bytes: &[u8]) -> Self { + let message = parsable!(bytes.try_into().ok()); + Input::Simple(Box::new(message)) + } + + // Internal custom message structure : + // SIMULATION_TAG 1B / MESSAGE_TAG 1B / DATA + fn parse(input: &[u8]) -> Self { + if input.len() <= 3 { + return Self::Unparsable; + } + let internal = parsable!(input.first()); + let message = parsable!(input.get(1)); + let data = parsable!(input.get(2..)); + if *internal != parsing::SIMULATION_TAG { + return Self::Unparsable; + } + match *message { + SIMULATION_SIMPLE_TAG => Self::parse_simple_simulation(data), + SIMULATION_CREATE_TAG => Self::parse_new_chunk_simulation(data), + SIMULATION_CHUNK_TAG => Self::parse_simulation_chunk(data), + _ => Self::Unparsable, + } + } +} + +fn read_chunks( + host: &mut Host, + num_chunks: u16, +) -> Result { + let mut buffer: Vec = Vec::new(); + for n in 0..num_chunks { + match read_input(host)? { + Input::Chunk { i, data } => { + if i != n { + return Err(Error::InvalidConversion); + } else { + buffer.extend(&data); + } + } + _ => return Err(Error::InvalidConversion), + } + } + Ok(buffer.as_slice().try_into()?) +} + +fn read_input(host: &mut Host) -> Result { + match host.read_input()? { + Some(input) => Ok(Input::parse(input.as_ref())), + None => Ok(Input::Unparsable), + } +} + +fn parse_inbox(host: &mut Host) -> Result { + // we just received simulation tag + // next message is either a simulation or the nb of chunks needed + match read_input(host)? { + Input::Simple(s) => Ok(*s), + Input::NewChunked(num_chunks) => { + // loop to find the chunks + read_chunks(host, num_chunks) + } + _ => Err(Error::InvalidConversion), + } +} + +impl VersionedEncoding for SimulationResult { + const VERSION: u8 = SIMULATION_ENCODING_VERSION; + fn unversionned_encode(&self) -> bytes::BytesMut { + self.rlp_bytes() + } + fn unversionned_decode(decoder: &Rlp) -> Result { + Self::decode(decoder) + } +} + +pub fn start_simulation_mode( + host: &mut Host, + enable_fa_withdrawals: bool, +) -> Result<(), anyhow::Error> { + log!(host, Debug, "Starting simulation mode "); + let simulation = parse_inbox(host)?; + match simulation { + Message::Evaluation(simulation) => { + let tracer_input = read_tracer_input(host)?; + let outcome = simulation.run(host, tracer_input, enable_fa_withdrawals)?; + storage::store_simulation_result(host, outcome) + } + } +} + +#[cfg(test)] +mod tests { + + use primitive_types::H256; + use tezos_ethereum::{block::BlockConstants, tx_signature::TxSignature}; + use tezos_evm_runtime::runtime::MockKernelHost; + + use crate::{retrieve_block_fees, retrieve_chain_id}; + + use super::*; + + fn address_of_str(s: &str) -> Option { + let data = &hex::decode(s).unwrap(); + Some(H160::from_slice(data)) + } + + #[test] + fn test_decode_empty() { + let input_string = + hex::decode("da8094353535353535353535353535353535353535353580808080") + .unwrap(); + let address = address_of_str("3535353535353535353535353535353535353535"); + let expected = Evaluation { + from: None, + to: address, + gas: None, + gas_price: None, + value: None, + data: vec![], + with_da_fees: true, + timestamp: None, + }; + + let evaluation = Evaluation::from_bytes(&input_string); + + assert!(evaluation.is_ok(), "Simulation input should be decodable"); + assert_eq!( + expected, + evaluation.unwrap(), + "The decoded result is not the one expected" + ); + } + + #[test] + fn test_decode_non_empty() { + let input_string = + hex::decode("f84894242424242424242424242424242424242424242494353535353535353535353535353535353535353588672b00000000000088ce56000000000000883582000000000000821616").unwrap(); + let to = address_of_str("3535353535353535353535353535353535353535"); + let from = address_of_str("2424242424242424242424242424242424242424"); + let data = hex::decode("1616").unwrap(); + let expected = Evaluation { + from, + to, + gas: Some(11111), + gas_price: Some(22222), + value: Some(U256::from(33333)), + data, + with_da_fees: true, + timestamp: None, + }; + + let evaluation = Evaluation::from_bytes(&input_string); + + assert!(evaluation.is_ok(), "Simulation input should be decodable"); + assert_eq!( + expected, + evaluation.unwrap(), + "The decoded result is not the one expected" + ); + } + + // The compiled initialization code for the Ethereum demo contract given + // as an example in kernel_evm/solidity_examples/storage.sol + const STORAGE_CONTRACT_INITIALIZATION: &str = "608060405234801561001057600080fd5b5061017f806100206000396000f3fe608060405234801561001057600080fd5b50600436106100415760003560e01c80634e70b1dc1461004657806360fe47b1146100645780636d4ce63c14610080575b600080fd5b61004e61009e565b60405161005b91906100d0565b60405180910390f35b61007e6004803603810190610079919061011c565b6100a4565b005b6100886100ae565b60405161009591906100d0565b60405180910390f35b60005481565b8060008190555050565b60008054905090565b6000819050919050565b6100ca816100b7565b82525050565b60006020820190506100e560008301846100c1565b92915050565b600080fd5b6100f9816100b7565b811461010457600080fd5b50565b600081359050610116816100f0565b92915050565b600060208284031215610132576101316100eb565b5b600061014084828501610107565b9150509291505056fea2646970667358221220ec57e49a647342208a1f5c9b1f2049bf1a27f02e19940819f38929bf67670a5964736f6c63430008120033"; + // call: num (direct access to state variable) + const STORAGE_CONTRACT_CALL_NUM: &str = "4e70b1dc"; + // call: get (public view) + const STORAGE_CONTRACT_CALL_GET: &str = "6d4ce63c"; + + const DUMMY_ALLOCATED_TICKS: u64 = 1_000_000_000; + + fn create_contract(host: &mut Host) -> H160 + where + Host: Runtime, + { + let timestamp = + read_last_info_per_level_timestamp(host).unwrap_or(Timestamp::from(0)); + let timestamp = U256::from(timestamp.as_u64()); + let chain_id = retrieve_chain_id(host); + assert!(chain_id.is_ok(), "chain_id should be defined"); + let block_fees = retrieve_block_fees(host); + assert!(chain_id.is_ok(), "chain_id should be defined"); + assert!(block_fees.is_ok(), "block_fees should be defined"); + let block = BlockConstants::first_block( + timestamp, + chain_id.unwrap(), + block_fees.unwrap(), + crate::block::GAS_LIMIT, + H160::zero(), + ); + let precompiles = precompiles::precompile_set::(false); + let mut evm_account_storage = account_storage::init_account_storage().unwrap(); + + let callee = None; + let caller = H160::from_low_u64_be(117); + let transaction_value = U256::from(0); + let call_data: Vec = hex::decode(STORAGE_CONTRACT_INITIALIZATION).unwrap(); + + // gas limit was estimated using Remix on Shanghai network (256,842) + // plus a safety margin for gas accounting discrepancies + let gas_limit = 300_000; + let gas_price = U256::from(21000); + // create contract + let outcome = evm_execution::run_transaction( + host, + &block, + &mut evm_account_storage, + &precompiles, + CONFIG, + callee, + caller, + call_data, + Some(gas_limit), + gas_price, + transaction_value, + false, + DUMMY_ALLOCATED_TICKS, + false, + false, + None, + ); + assert!(outcome.is_ok(), "contract should have been created"); + let outcome = outcome.unwrap(); + assert!( + outcome.is_some(), + "execution should have produced some outcome" + ); + outcome.unwrap().new_address().unwrap() + } + + #[test] + fn simulation_result() { + // setup + let mut host = MockKernelHost::default(); + let new_address = create_contract(&mut host); + + // run evaluation num + let evaluation = Evaluation { + from: None, + gas_price: None, + to: Some(new_address), + data: hex::decode(STORAGE_CONTRACT_CALL_NUM).unwrap(), + gas: Some(100000), + value: None, + with_da_fees: false, + timestamp: None, + }; + let outcome = evaluation.run(&mut host, None, false); + + assert!(outcome.is_ok(), "evaluation should have succeeded"); + let outcome = outcome.unwrap(); + + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "simulation result should be 0"); + } else { + panic!("evaluation should have reached outcome"); + } + + // run simulation get + let evaluation = Evaluation { + from: None, + gas_price: None, + to: Some(new_address), + data: hex::decode(STORAGE_CONTRACT_CALL_GET).unwrap(), + gas: Some(111111), + value: None, + with_da_fees: false, + timestamp: None, + }; + let outcome = evaluation.run(&mut host, None, false); + + assert!(outcome.is_ok(), "simulation should have succeeded"); + let outcome = outcome.unwrap(); + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "evaluation result should be 0"); + } else { + panic!("evaluation should have reached outcome"); + } + } + + #[test] + fn evaluation_result_no_gas() { + // setup + let mut host = MockKernelHost::default(); + let new_address = create_contract(&mut host); + + // run evaluation num + let evaluation = Evaluation { + from: None, + gas_price: None, + to: Some(new_address), + data: hex::decode(STORAGE_CONTRACT_CALL_NUM).unwrap(), + gas: None, + value: None, + with_da_fees: false, + timestamp: None, + }; + let outcome = evaluation.run(&mut host, None, false); + + assert!(outcome.is_ok(), "evaluation should have succeeded"); + let outcome = outcome.unwrap(); + if let SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value, + gas_used: _, + })) = outcome + { + assert_eq!(Some(vec![0u8; 32]), value, "evaluation result should be 0"); + } else { + panic!("evaluation should have reached outcome"); + } + } + + #[ignore] + #[test] + fn parse_simulation() { + let to = address_of_str("3535353535353535353535353535353535353535"); + let from = address_of_str("2424242424242424242424242424242424242424"); + let data = hex::decode("1616").unwrap(); + let expected = Evaluation { + from, + to, + gas: Some(11111), + gas_price: Some(22222), + value: Some(U256::from(33333)), + data, + with_da_fees: false, + timestamp: None, + }; + + let mut encoded = + hex::decode("f84894242424242424242424242424242424242424242494353535353535353535353535353535353535353588672b00000000000088ce56000000000000883582000000000000821616").unwrap(); + let mut input = vec![ + parsing::SIMULATION_TAG, + SIMULATION_SIMPLE_TAG, + EVALUATION_TAG, + ]; + input.append(&mut encoded); + + let parsed = Input::parse(&input); + + assert_eq!( + Input::Simple(Box::new(Message::Evaluation(expected))), + parsed, + "should have been parsed as complete simulation" + ); + } + + #[ignore] + #[test] + fn parse_simulation2() { + // setup + let mut host = MockKernelHost::default(); + let new_address = create_contract(&mut host); + + let to = Some(new_address); + let data = hex::decode(STORAGE_CONTRACT_CALL_GET).unwrap(); + let gas = Some(11111); + let expected = Evaluation { + from: None, + to, + gas, + gas_price: None, + value: None, + data, + with_da_fees: false, + timestamp: None, + }; + + let encoded = hex::decode( + "ff0100e68094907823e0a92f94355968feb2cbf0fbb594fe321488672b0000000000008080846d4ce63c", + ) + .unwrap(); + + let parsed = Input::parse(&encoded); + assert_eq!( + Input::Simple(Box::new(Message::Evaluation(expected))), + parsed, + "should have been parsed as complete simulation" + ); + } + + #[test] + fn parse_num_chunks() { + let num: u16 = 42; + let mut input = vec![parsing::SIMULATION_TAG, SIMULATION_CREATE_TAG]; + input.extend(num.to_le_bytes()); + + let parsed = Input::parse(&input); + + assert_eq!( + Input::NewChunked(42), + parsed, + "should have parsed start of chunked simulation" + ); + } + + #[test] + fn parse_chunk() { + let i: u16 = 20; + let mut input = vec![parsing::SIMULATION_TAG, SIMULATION_CHUNK_TAG]; + input.extend(i.to_le_bytes()); + input.extend(hex::decode("aaaaaa").unwrap()); + + let expected = Input::Chunk { + i: 20, + data: vec![170u8; 3], + }; + + let parsed = Input::parse(&input); + + assert_eq!(expected, parsed, "should have parsed a chunk"); + } + + pub fn check_roundtrip( + v: R, + ) { + let bytes = v.rlp_bytes(); + let decoder = Rlp::new(&bytes); + println!("{:?}", bytes); + let decoded = R::decode(&decoder).expect("Value should be decodable"); + assert_eq!(v, decoded, "Roundtrip failed on {:?}", v) + } + + #[test] + fn test_simulation_result_encoding_roundtrip() { + let valid: SimulationResult = + SimulationResult::Ok(ValidationResult { + transaction_object: TransactionObject { + block_number: U256::from(532532), + from: address_of_str("3535353535353535353535353535353535353535") + .unwrap(), + gas_used: U256::from(32523), + gas_price: U256::from(100432432), + hash: [5; 32], + input: vec![], + nonce: 8888, + to: address_of_str("3635353535353535353535353535353535353536"), + index: 15u32, + value: U256::from(0), + signature: Some( + TxSignature::new( + U256::from(1337), + H256::from_low_u64_be(1), + H256::from_low_u64_be(2), + ) + .unwrap(), + ), + }, + }); + let call: SimulationResult = + SimulationResult::Ok(SimulationResult::Ok(ExecutionResult { + value: Some(vec![0, 1, 2, 3]), + gas_used: Some(123), + })); + let revert: SimulationResult = + SimulationResult::Ok(SimulationResult::Err(vec![3, 2, 1, 0])); + let error: SimulationResult = + SimulationResult::Err(String::from("Un festival de GADTs")); + + check_roundtrip(valid); + check_roundtrip(call); + check_roundtrip(revert); + check_roundtrip(error) + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/stage_one.rs b/etherlink/kernel_calypso2/kernel/src/stage_one.rs new file mode 100644 index 000000000000..119b5da0498d --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/stage_one.rs @@ -0,0 +1,908 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use crate::blueprint::Blueprint; +use crate::blueprint_storage::{ + clear_all_blueprints, store_forced_blueprint, store_inbox_blueprint, +}; +use crate::configuration::{ + Configuration, ConfigurationMode, DalConfiguration, TezosContracts, +}; +use crate::delayed_inbox::DelayedInbox; +use crate::event::Event; +use crate::inbox::{read_proxy_inbox, read_sequencer_inbox}; +use crate::inbox::{ProxyInboxContent, StageOneStatus}; +use crate::storage::read_last_info_per_level_timestamp; +use anyhow::Ok; +use std::ops::Add; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_ethereum::block::L2Block; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_encoding::public_key::PublicKey; +use tezos_smart_rollup_encoding::timestamp::Timestamp; +use tezos_smart_rollup_host::metadata::RAW_ROLLUP_ADDRESS_SIZE; + +pub fn fetch_proxy_blueprints( + host: &mut Host, + smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], + tezos_contracts: &TezosContracts, + enable_fa_bridge: bool, + garbage_collect_blocks: bool, +) -> Result { + if let Some(ProxyInboxContent { transactions }) = read_proxy_inbox( + host, + smart_rollup_address, + tezos_contracts, + enable_fa_bridge, + garbage_collect_blocks, + )? { + let timestamp = + read_last_info_per_level_timestamp(host).unwrap_or(Timestamp::from(0)); + let blueprint = Blueprint { + transactions, + timestamp, + }; + // Store the blueprint. + store_inbox_blueprint(host, blueprint)?; + Ok(StageOneStatus::Reboot) + } else { + Ok(StageOneStatus::Done) + } +} + +fn fetch_delayed_transactions( + host: &mut Host, + delayed_inbox: &mut DelayedInbox, +) -> anyhow::Result<()> { + let timestamp = read_last_info_per_level_timestamp(host)?; + // Number for the first forced blueprint + let base = crate::blueprint_storage::read_next_blueprint_number(host)?; + // Accumulator of how many blueprints we fetched + let mut offset: u32 = 0; + + while let Some(timed_out) = delayed_inbox.next_delayed_inbox_blueprint(host)? { + log!( + host, + Info, + "Creating blueprint from timed out delayed transactions of length {}", + timed_out.len() + ); + + let timestamp = match crate::block_storage::read_current(host) { + Result::Ok(L2Block { + timestamp: head_timestamp, + .. + }) => { + // Timestamp has to be at least equal or greater than previous timestamp. + // If it's not the case, we fallback and take the previous block timestamp. + std::cmp::max(timestamp, head_timestamp) + } + Err(_) => { + // If there's no current block, there's no previous + // timestamp. So we take whatever is the current + // timestamp. + timestamp + } + }; + + let level = base.add(offset); + Event::FlushDelayedInbox { + transactions: &timed_out, + timestamp, + level, + } + .store(host)?; + + // Clean existing blueprints + if offset == 0 { + log!( + host, + Info, + "Deleting all blueprints following flush at {}", + level + ); + clear_all_blueprints(host)?; + } + + // Create a new blueprint with the timed out transactions + let blueprint = Blueprint { + transactions: timed_out, + timestamp, + }; + // Store the blueprint. + store_forced_blueprint(host, blueprint, level)?; + offset += 1; + } + + Ok(()) +} + +#[allow(clippy::too_many_arguments)] +fn fetch_sequencer_blueprints( + host: &mut Host, + smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], + tezos_contracts: &TezosContracts, + delayed_bridge: ContractKt1Hash, + delayed_inbox: &mut DelayedInbox, + sequencer: PublicKey, + dal: Option, + enable_fa_bridge: bool, + garbage_collect_blocks: bool, +) -> Result { + match read_sequencer_inbox( + host, + smart_rollup_address, + tezos_contracts, + delayed_bridge, + sequencer, + delayed_inbox, + enable_fa_bridge, + dal, + garbage_collect_blocks, + )? { + StageOneStatus::Done => { + // Check if there are timed-out transactions in the delayed inbox + let timed_out = delayed_inbox.first_has_timed_out(host)?; + if timed_out { + fetch_delayed_transactions(host, delayed_inbox)? + }; + // Force the kernel to reboot, so that the first blueprint will have + // the maximum tick capacity + Ok(StageOneStatus::Reboot) + } + status => Ok(status), + } +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn fetch_blueprints( + host: &mut Host, + smart_rollup_address: [u8; RAW_ROLLUP_ADDRESS_SIZE], + config: &mut Configuration, +) -> Result { + match &mut config.mode { + ConfigurationMode::Sequencer { + delayed_bridge, + delayed_inbox, + sequencer, + dal, + evm_node_flag: _, + max_blueprint_lookahead_in_seconds: _, + } => fetch_sequencer_blueprints( + host, + smart_rollup_address, + &config.tezos_contracts, + delayed_bridge.clone(), + delayed_inbox, + sequencer.clone(), + dal.clone(), + config.enable_fa_bridge, + config.garbage_collect_blocks, + ), + ConfigurationMode::Proxy => fetch_proxy_blueprints( + host, + smart_rollup_address, + &config.tezos_contracts, + config.enable_fa_bridge, + config.garbage_collect_blocks, + ), + } +} + +#[cfg(test)] +mod tests { + use crate::{ + configuration::Limits, + dal_slot_import_signal::{ + DalSlotImportSignals, DalSlotIndicesList, DalSlotIndicesOfLevel, + UnsignedDalSlotSignals, + }, + parsing::DAL_SLOT_IMPORT_SIGNAL_TAG, + }; + use primitive_types::U256; + use rlp::Encodable; + use tezos_crypto_rs::hash::{HashTrait, SecretKeyEd25519, UnknownSignature}; + use tezos_data_encoding::types::Bytes; + use tezos_evm_runtime::runtime::MockKernelHost; + use tezos_smart_rollup::{ + michelson::{ + ticket::FA2_1Ticket, MichelsonBytes, MichelsonOption, MichelsonOr, + MichelsonPair, + }, + types::PublicKeyHash, + }; + use tezos_smart_rollup_encoding::contract::Contract; + use tezos_smart_rollup_host::runtime::Runtime as SdkRuntime; + use tezos_smart_rollup_mock::TransferMetadata; + // SdkRuntime is not used directly but necessary to add the Runtime trait in + // context for typechecking. Feel free to remove it and look at rustc + // errors. + + use crate::{ + block_storage::internal_for_tests::store_current_number, + blueprint_storage::read_next_blueprint, dal::tests::*, parsing::RollupType, + }; + + use super::*; + + // This is the secret key of the dummy sequencer configuration + const DUMMY_SEQUENCER_SECRET_KEY: &str = + "edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh"; + + fn dummy_sequencer_config( + enable_dal: bool, + kernel_slots: Option>, + ) -> Configuration { + let mut host = MockKernelHost::default(); + let delayed_inbox = + DelayedInbox::new(&mut host).expect("Delayed inbox should be created"); + let delayed_bridge: ContractKt1Hash = + ContractKt1Hash::from_base58_check("KT18amZmM5W7qDWVt2pH6uj7sCEd3kbzLrHT") + .unwrap(); + // This is tezt/lib_tezos/account: bootstrap1 + let sequencer: PublicKey = PublicKey::from_b58check( + "edpkuBknW28nW72KG6RoHtYW7p12T6GKc7nAbwYX5m8Wd9sDVC9yav", + ) + .unwrap(); + let dal = if enable_dal { + Some(DalConfiguration { + slot_indices: kernel_slots.unwrap_or(vec![6]), + }) + } else { + None + }; + + let contracts = TezosContracts::default(); + Configuration { + tezos_contracts: TezosContracts { + ticketer: Some(ContractKt1Hash::from_b58check(DUMMY_TICKETER).unwrap()), + ..contracts + }, + mode: ConfigurationMode::Sequencer { + delayed_bridge, + delayed_inbox: Box::new(delayed_inbox), + sequencer, + dal, + evm_node_flag: false, + max_blueprint_lookahead_in_seconds: 100_000i64, + }, + limits: Limits::default(), + enable_fa_bridge: false, + garbage_collect_blocks: false, + } + } + + fn dummy_proxy_configuration() -> Configuration { + let contracts = TezosContracts::default(); + Configuration { + tezos_contracts: TezosContracts { + ticketer: Some(ContractKt1Hash::from_b58check(DUMMY_TICKETER).unwrap()), + ..contracts + }, + mode: ConfigurationMode::Proxy, + limits: Limits::default(), + enable_fa_bridge: false, + garbage_collect_blocks: false, + } + } + + fn get_dal_slots(conf: &Configuration) -> Option> { + match &conf.mode { + ConfigurationMode::Sequencer { + dal: Some(DalConfiguration { slot_indices }), + .. + } => Some(slot_indices.clone()), + _ => None, + } + } + + fn make_dal_signal( + host: &mut MockKernelHost, + dal_slots: &[u8], + ) -> DalSlotImportSignals { + let dal_parameters = host.reveal_dal_parameters(); + let unsigned_signals = UnsignedDalSlotSignals(vec![DalSlotIndicesOfLevel { + published_level: host.host.level() - (dal_parameters.attestation_lag as u32), + slot_indices: DalSlotIndicesList(dal_slots.to_vec()), + }]); + let to_sign = unsigned_signals.rlp_bytes(); + let sk = SecretKeyEd25519::from_b58check(DUMMY_SEQUENCER_SECRET_KEY).unwrap(); + let signature: UnknownSignature = + sk.sign(&to_sign).expect("Signature shouldn't fail").into(); + DalSlotImportSignals { + signals: unsigned_signals, + signature, + } + } + + fn frame_message(bytes: &[u8], message_kind: u8) -> Vec { + let mut buffer = Vec::with_capacity(1 + RAW_ROLLUP_ADDRESS_SIZE + bytes.len()); + buffer.push(0_u8); + buffer.extend_from_slice(&DEFAULT_SR_ADDRESS); + buffer.push(message_kind); + buffer.extend_from_slice(bytes); + buffer + } + + // Those blueprints are produced with `chunk data --as-blueprint --sequencer-key unencrypted:edsk3gUfUPyBSfrS9CCgmCiQsTCHGkviBDusMxDJstFtojtc1zcpsh --number 10` + const DUMMY_BLUEPRINT_CHUNK_NUMBER_10: &str = "00000000000000000000000000000000000000000003f897adeca0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0c0880000000000000000a00a00000000000000000000000000000000000000000000000000000000000000820100820000b84063cdece4fc53ba967664f29e7fe214ece817d72bd8b122c186d48b43d9d8b7e650f1503e7656fc48ac187b84f59f010959c83ea5df3ec4e30242b59ee0523609"; + const DUMMY_BLUEPRINT_CHUNK_UNPARSABLE: &str = "00000000000000000000000000000000000000000003f897adeca0ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffc0c0880000000000000000a00000000000000000000000000000000000000000000000000000000000000000820100820000b8403b4f48059e64cbe8a60fef482bff04ed453291d17fe888d2bae4420a623157570af4bd7ab944354583bcdce1149d004a4699b027c7370cd5e0b16172f3340406"; + + const DUMMY_RAW_TRANSACTION: &str = "f873808402faf08083c71b0094f0affc80a5f69f4a9a3ee01a640873b6ba53e5398d01431e0fae6d7210000000000080820a96a0a4160335a080cd3501fd88fe4d28cc3cd8f1283fa597c10f59028c0517efce27a0444512519b3346609d2ca7c400dc1f5263d2bdad8a2ac0a5ab942336e7ed603a"; + const DUMMY_TRANSACTION: &str = "00000000000000000000000000000000000000000000bca8ac5e67ba4cb84d1d223be2cd900491012e9848fcdc496aa2d5e4566d564bf873808402faf08083c71b0094f0affc80a5f69f4a9a3ee01a640873b6ba53e5398d01431e0fae6d7210000000000080820a96a0a4160335a080cd3501fd88fe4d28cc3cd8f1283fa597c10f59028c0517efce27a0444512519b3346609d2ca7c400dc1f5263d2bdad8a2ac0a5ab942336e7ed603a"; + + const DUMMY_NEW_CHUNKED_TX: &str = "00000000000000000000000000000000000000000001b00a070bfab8455de52f9a4256b1e4ec01d402ad48db395fc626cf56784da3a50200e9a4b3236353a4975255c3c4fe87ef26ee784eedc8f304eb6f7d7051e7ce95566606ba1560324fefe61abd1810149603e9cc8b11d0f07449b553496a25980368"; + + const DUMMY_CHUNK1: &str= "00000000000000000000000000000000000000000002b00a070bfab8455de52f9a4256b1e4ec01d402ad48db395fc626cf56784da3a501006606ba1560324fefe61abd1810149603e9cc8b11d0f07449b553496a25980368565b9150509250925092565b600060ff82169050919050565b610b8281610b6c565b82525050565b6000602082019050610b9d6000830184610b79565b92915050565b600060208284031215610bb957610bb86109e0565b5b6000610bc784828501610a64565b91505092915050565b600060208284031215610be657610be56109e0565b5b6000610bf484828501610a2e565b91505092915050565b60008060408385031215610c1457610c136109e0565b5b6000610c2285828601610a2e565b9250506020610c3385828601610a2e565b9150509250929050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b60006002820490506001821680610c8457607f821691505b602082108103610c9757610c96610c3d565b5b50919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052601160045260246000fd5b6000610cd782610a43565b9150610ce283610a43565b9250828203905081811115610cfa57610cf9610c9d565b5b92915050565b6000610d0b82610a43565b9150610d1683610a43565b9250828201905080821115610d2e57610d2d610c9d565b5b9291505056fea264697066735822122030971f380a01674dace4fc0292dcf2896b91915c6e633e0f2cf32f3e0969e9b864736f6c63430008150033820a96a0444b512de8e10806032b4e61579d347e8f7fd168d20b53e6d16a595e4598c5f1a03d18d45f44571af791cfb84e74ac0455cb3e46fa06f3c03f90edb8b21cf0016f"; + + const DUMMY_CHUNK2: &str = "00000000000000000000000000000000000000000002b00a070bfab8455de52f9a4256b1e4ec01d402ad48db395fc626cf56784da3a50000e9a4b3236353a4975255c3c4fe87ef26ee784eedc8f304eb6f7d7051e7ce9556f911f2808402faf0808416487a008080b9119d60806040526040518060400160405280601381526020017f536f6c6964697479206279204578616d706c6500000000000000000000000000815250600390816200004a91906200033c565b506040518060400160405280600781526020017f534f4c4259455800000000000000000000000000000000000000000000000000815250600490816200009191906200033c565b506012600560006101000a81548160ff021916908360ff160217905550348015620000bb57600080fd5b5062000423565b600081519050919050565b7f4e487b7100000000000000000000000000000000000000000000000000000000600052604160045260246000fd5b7f4e487b7100000000000000000000000000000000000000000000000000000000600052602260045260246000fd5b600060028204905060018216806200014457607f821691505b6020821081036200015a5762000159620000fc565b5b50919050565b60008190508160005260206000209050919050565b60006020601f8301049050919050565b600082821b905092915050565b600060088302620001c47fffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff8262000185565b620001d0868362000185565b95508019841693508086168417925050509392505050565b6000819050919050565b6000819050919050565b60006200021d620002176200021184620001e8565b620001f2565b620001e8565b9050919050565b6000819050919050565b6200023983620001fc565b62000251620002488262000224565b84845462000192565b825550505050565b600090565b6200026862000259565b620002758184846200022e565b505050565b5b818110156200029d57620002916000826200025e565b6001810190506200027b565b5050565b601f821115620002ec57620002b68162000160565b620002c18462000175565b81016020851015620002d1578190505b620002e9620002e08562000175565b8301826200027a565b50505b505050565b600082821c905092915050565b60006200031160001984600802620002f1565b1980831691505092915050565b60006200032c8383620002fe565b9150826002028217905092915050565b6200034782620000c2565b67ffffffffffffffff811115620003635762000362620000cd565b5b6200036f82546200012b565b6200037c828285620002a1565b600060209050601f831160018114620003b457600084156200039f578287015190505b620003ab85826200031e565b8655506200041b565b601f198416620003c48662000160565b60005b82811015620003ee57848901518255600182019150602085019450602081019050620003c7565b868310156200040e57848901516200040a601f891682620002fe565b8355505b6001600288020188555050505b505050505050565b610d6a80620004336000396000f3fe608060405234801561001057600080fd5b50600436106100a95760003560e01c806342966c681161007157806342966c681461016857806370a082311461018457806395d89b41146101b4578063a0712d68146101d2578063a9059cbb146101ee578063dd62ed3e1461021e576100a9565b806306fdde03146100ae578063095ea7b3146100cc57806318160ddd146100fc57806323b872dd1461011a578063313ce5671461014a575b600080fd5b6100b661024e565b6040516100c391906109be565b60405180910390f35b6100e660048036038101906100e19190610a79565b6102dc565b6040516100f39190610ad4565b60405180910390f35b6101046103ce565b6040516101119190610afe565b60405180910390f35b610134600480360381019061012f9190610b19565b6103d4565b6040516101419190610ad4565b60405180910390f35b610152610585565b60405161015f9190610b88565b60405180910390f35b610182600480360381019061017d9190610ba3565b610598565b005b61019e60048036038101906101999190610bd0565b61066f565b6040516101ab9190610afe565b60405180910390f35b6101bc610687565b6040516101c991906109be565b60405180910390f35b6101ec60048036038101906101e79190610ba3565b610715565b005b61020860048036038101906102039190610a79565b6107ec565b6040516102159190610ad4565b60405180910390f35b61023860048036038101906102339190610bfd565b610909565b6040516102459190610afe565b60405180910390f35b6003805461025b90610c6c565b80601f016020809104026020016040519081016040528092919081815260200182805461028790610c6c565b80156102d45780601f106102a9576101008083540402835291602001916102d4565b820191906000526020600020905b8154815290600101906020018083116102b757829003601f168201915b505050505081565b600081600260003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167f8c5be1e5ebec7d5bd14f71427d1e84f3dd0314c0f7b2291e5b200ac8c7c3b925846040516103bc9190610afe565b60405180910390a36001905092915050565b60005481565b600081600260008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104629190610ccc565b9250508190555081600160008673ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546104b89190610ccc565b9250508190555081600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461050e9190610d00565b925050819055508273ffffffffffffffffffffffffffffffffffffffff168473ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516105729190610afe565b60405180910390a3600190509392505050565b600560009054906101000a900460ff1681565b80600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546105e79190610ccc565b92505081905550806000808282546105ff9190610ccc565b92505081905550600073ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516106649190610afe565b60405180910390a350565b60016020528060005260406000206000915090505481565b6004805461069490610c6c565b80601f01602080910402602001604051908101604052809291908181526020018280546106c090610c6c565b801561070d5780601f106106e25761010080835404028352916020019161070d565b820191906000526020600020905b8154815290600101906020018083116106f057829003601f168201915b505050505081565b80600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546107649190610d00565b925050819055508060008082825461077c9190610d00565b925050819055503373ffffffffffffffffffffffffffffffffffffffff16600073ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef836040516107e19190610afe565b60405180910390a350565b600081600160003373ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff168152602001908152602001600020600082825461083d9190610ccc565b9250508190555081600160008573ffffffffffffffffffffffffffffffffffffffff1673ffffffffffffffffffffffffffffffffffffffff16815260200190815260200160002060008282546108939190610d00565b925050819055508273ffffffffffffffffffffffffffffffffffffffff163373ffffffffffffffffffffffffffffffffffffffff167fddf252ad1be2c89b69c2b068fc378daa952ba7f163c4a11628f55a4df523b3ef846040516108f79190610afe565b60405180910390a36001905092915050565b6002602052816000526040600020602052806000526040600020600091509150505481565b600081519050919050565b600082825260208201905092915050565b60005b8381101561096857808201518184015260208101905061094d565b60008484015250505050565b6000601f19601f8301169050919050565b60006109908261092e565b61099a8185610939565b93506109aa81856020860161094a565b6109b381610974565b840191505092915050565b600060208201905081810360008301526109d88184610985565b905092915050565b600080fd5b600073ffffffffffffffffffffffffffffffffffffffff82169050919050565b6000610a10826109e5565b9050919050565b610a2081610a05565b8114610a2b57600080fd5b50565b600081359050610a3d81610a17565b92915050565b6000819050919050565b610a5681610a43565b8114610a6157600080fd5b50565b600081359050610a7381610a4d565b92915050565b60008060408385031215610a9057610a8f6109e0565b5b6000610a9e85828601610a2e565b9250506020610aaf85828601610a64565b9150509250929050565b60008115159050919050565b610ace81610ab9565b82525050565b6000602082019050610ae96000830184610ac5565b92915050565b610af881610a43565b82525050565b6000602082019050610b136000830184610aef565b92915050565b600080600060608486031215610b3257610b316109e0565b5b6000610b4086828701610a2e565b9350506020610b5186828701610a2e565b9250506040610b6286828701610a64"; + + const DUMMY_TICKETER: &str = "KT1VEjeQfDBSfpDH5WeBM5LukHPGM2htYEh3"; + + const DUMMY_INVALID_TICKETER: &str = "KT1Nn8bjcPSpg7EUHcZwYPCcfT1W8Z5Q8ug5"; + + const DEFAULT_SR_ADDRESS: [u8; 20] = [0; 20]; + + fn dummy_deposit(ticketer: ContractKt1Hash) -> RollupType { + let receiver = MichelsonBytes( + hex::decode("CdaC74220Da1399A78C3c850d2cA4b24ac4051E1") + .unwrap() + .to_vec(), + ); + let ticket: FA2_1Ticket = FA2_1Ticket::new( + Contract::from_b58check(&ticketer.to_base58_check()).unwrap(), + MichelsonPair(0.into(), MichelsonOption(None)), + 1_000_000, + ) + .unwrap(); + MichelsonOr::Left(MichelsonOr::Left(MichelsonPair(receiver, ticket))) + } + + fn dummy_delayed_transaction() -> [RollupType; 2] { + let mut raw_tx: Vec = vec![1]; + raw_tx.extend(hex::decode(DUMMY_RAW_TRANSACTION).unwrap().to_vec()); + [ + MichelsonOr::Left(MichelsonOr::Right(MichelsonBytes(vec![0, 1]))), + MichelsonOr::Left(MichelsonOr::Right(MichelsonBytes(raw_tx))), + ] + } + + fn delayed_bridge(conf: &Configuration) -> ContractKt1Hash { + match &conf.mode { + ConfigurationMode::Proxy => panic!("No delayed bridge in proxy mode"), + ConfigurationMode::Sequencer { delayed_bridge, .. } => delayed_bridge.clone(), + } + } + + fn delayed_inbox_is_empty( + conf: &Configuration, + host: &mut Host, + ) -> bool { + match &conf.mode { + ConfigurationMode::Proxy => panic!("No delayed inbox in proxy mode"), + ConfigurationMode::Sequencer { delayed_inbox, .. } => { + delayed_inbox.is_empty(host).unwrap() + } + } + } + + #[test] + fn test_parsing_proxy_transaction() { + let mut host = MockKernelHost::default(); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_TRANSACTION).unwrap())); + let mut conf = dummy_proxy_configuration(); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + match read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + { + Some(Blueprint { transactions, .. }) => assert!(transactions.len() == 1), + _ => panic!("There should be a blueprint"), + } + } + + #[test] + fn test_parsing_proxy_chunked_transaction() { + let mut host = MockKernelHost::default(); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_NEW_CHUNKED_TX).unwrap())); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_CHUNK1).unwrap())); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_CHUNK2).unwrap())); + let mut conf = dummy_proxy_configuration(); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + match read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + { + Some(Blueprint { transactions, .. }) => assert!(transactions.len() == 1), + _ => panic!("There should be a blueprint"), + } + } + + fn test_sequencer_reject_proxy_transactions(enable_dal: bool) { + let mut host = MockKernelHost::default(); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_TRANSACTION).unwrap())); + let mut conf = dummy_sequencer_config(enable_dal, None); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("No blueprint should have been produced") + } + } + + #[test] + fn test_sequencer_reject_proxy_transactions_without_dal() { + test_sequencer_reject_proxy_transactions(false) + } + + #[test] + fn test_sequencer_reject_proxy_transactions_with_dal() { + test_sequencer_reject_proxy_transactions(true) + } + + fn test_sequencer_reject_proxy_chunked_transactions(enable_dal: bool) { + let mut host = MockKernelHost::default(); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_NEW_CHUNKED_TX).unwrap())); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_CHUNK1).unwrap())); + host.host + .add_external(Bytes::from(hex::decode(DUMMY_CHUNK2).unwrap())); + let mut conf = dummy_sequencer_config(enable_dal, None); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("No blueprint should have been produced") + } + } + + #[test] + fn test_sequencer_reject_proxy_chunked_transactions_without_dal() { + test_sequencer_reject_proxy_chunked_transactions(false) + } + + #[test] + fn test_sequencer_reject_proxy_chunked_transactions_with_dal() { + test_sequencer_reject_proxy_chunked_transactions(true) + } + + fn test_parsing_valid_sequencer_chunk(enable_dal: bool) { + let mut host = MockKernelHost::default(); + host.host.add_external(Bytes::from( + hex::decode(DUMMY_BLUEPRINT_CHUNK_NUMBER_10).unwrap(), + )); + let mut conf = dummy_sequencer_config(enable_dal, None); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + // The dummy chunk in the inbox is registered at block 10 + store_current_number(&mut host, U256::from(9)).unwrap(); + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_none() + { + panic!("There should be a blueprint") + } + } + + #[test] + fn test_parsing_valid_sequencer_chunk_without_dal() { + test_parsing_valid_sequencer_chunk(false) + } + + #[test] + fn test_parsing_valid_sequencer_chunk_with_dal() { + test_parsing_valid_sequencer_chunk(true) + } + + fn test_parsing_invalid_sequencer_chunk(enable_dal: bool) { + let mut host = MockKernelHost::default(); + host.host.add_external(Bytes::from( + hex::decode(DUMMY_BLUEPRINT_CHUNK_UNPARSABLE).unwrap(), + )); + let mut conf = dummy_sequencer_config(enable_dal, None); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("There shouldn't be a blueprint, the chunk is signed by the wrong key") + } + } + + #[test] + fn test_parsing_invalid_sequencer_chunk_without_dal() { + test_parsing_invalid_sequencer_chunk(false) + } + + #[test] + fn test_parsing_invalid_sequencer_chunk_with_dal() { + test_parsing_invalid_sequencer_chunk(true) + } + + fn test_proxy_rejects_sequencer_chunk(enable_dal: bool) { + let mut host = MockKernelHost::default(); + host.host.add_external(Bytes::from( + hex::decode(DUMMY_BLUEPRINT_CHUNK_NUMBER_10).unwrap(), + )); + let mut conf = dummy_sequencer_config(enable_dal, None); + + match read_proxy_inbox( + &mut host, + DEFAULT_SR_ADDRESS, + &conf.tezos_contracts, + false, + false, + ) + .unwrap() + { + None => panic!("There should be an InboxContent"), + Some(ProxyInboxContent { transactions, .. }) => assert_eq!( + transactions, + vec![], + "The proxy shouldn't have read any transaction" + ), + }; + + // The dummy chunk in the inbox is registered at block 10 + store_current_number(&mut host, U256::from(9)).unwrap(); + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("The sequencer chunk shouldn't have been injected") + } + } + + #[test] + fn test_proxy_rejects_sequencer_chunk_without_dal() { + test_proxy_rejects_sequencer_chunk(false) + } + + #[test] + fn test_proxy_rejects_sequencer_chunk_with_dal() { + test_proxy_rejects_sequencer_chunk(true) + } + + fn test_parsing_delayed_inbox(enable_dal: bool) { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(enable_dal, None); + let metadata = TransferMetadata::new( + delayed_bridge(&conf), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + for message in dummy_delayed_transaction() { + host.host.add_transfer(message, &metadata); + } + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("There shouldn't be a blueprint, the transaction comes from the delayed bridge") + } + + if delayed_inbox_is_empty(&conf, &mut host) { + panic!("The delayed inbox shouldn't be empty") + } + } + + #[test] + fn test_parsing_delayed_inbox_without_dal() { + test_parsing_delayed_inbox(false) + } + + #[test] + fn test_parsing_delayed_inbox_with_dal() { + test_parsing_delayed_inbox(true) + } + + fn test_parsing_l1_contract_inbox(enable_dal: bool) { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(enable_dal, None); + let metadata = TransferMetadata::new( + ContractKt1Hash::from_b58check(DUMMY_INVALID_TICKETER).unwrap(), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + for message in dummy_delayed_transaction() { + host.host.add_transfer(message, &metadata); + } + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("There shouldn't be a blueprint, the transaction comes from the delayed bridge") + } + + if !delayed_inbox_is_empty(&conf, &mut host) { + panic!("The delayed inbox should be empty, as it comes from the wrong delayed bridge") + } + } + + #[test] + fn test_parsing_l1_contract_inbox_without_dal() { + test_parsing_l1_contract_inbox(false) + } + + #[test] + fn test_parsing_l1_contract_inbox_with_dal() { + test_parsing_l1_contract_inbox(true) + } + + #[test] + fn test_parsing_delayed_inbox_rejected_in_proxy() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_proxy_configuration(); + let metadata = TransferMetadata::new( + ContractKt1Hash::from_b58check(DUMMY_INVALID_TICKETER).unwrap(), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + for message in dummy_delayed_transaction() { + host.host.add_transfer(message, &metadata) + } + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + match read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail").0 + { + None => panic!("There should be a blueprint"), + Some(Blueprint { transactions, .. }) => + assert_eq!(transactions.len(), 0, + "The transaction from the delayed bridge entrypoint should have been rejected in proxy mode"), + } + } + + #[test] + fn test_deposit_in_proxy_mode() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_proxy_configuration(); + let metadata = TransferMetadata::new( + conf.tezos_contracts.ticketer.clone().unwrap(), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + host.host.add_transfer( + dummy_deposit(conf.tezos_contracts.ticketer.clone().unwrap()), + &metadata, + ); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + match read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + { + None => panic!("There should be a blueprint"), + Some(Blueprint { transactions, .. }) => assert_eq!( + transactions.len(), + 1, + "The deposit should have been picked in the blueprint" + ), + } + } + + #[test] + fn test_deposit_with_invalid_ticketer() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_proxy_configuration(); + let metadata = TransferMetadata::new( + ContractKt1Hash::from_b58check(DUMMY_INVALID_TICKETER).unwrap(), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + host.host.add_transfer( + dummy_deposit( + ContractKt1Hash::from_b58check(DUMMY_INVALID_TICKETER).unwrap(), + ), + &metadata, + ); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + match read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + { + None => panic!("There should be a blueprint"), + Some(Blueprint { transactions, .. }) => assert_eq!( + transactions.len(), + 0, + "The deposit shouldn't have been picked in the blueprint as it is invalid" + ), + } + } + + fn test_deposit_in_sequencer_mode(enable_dal: bool) { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(enable_dal, None); + let metadata = TransferMetadata::new( + conf.tezos_contracts.ticketer.clone().unwrap(), + PublicKeyHash::from_b58check("tz1NiaviJwtMbpEcNqSP6neeoBYj8Brb3QPv").unwrap(), + ); + host.host.add_transfer( + dummy_deposit(conf.tezos_contracts.ticketer.clone().unwrap()), + &metadata, + ); + fetch_blueprints(&mut host, DEFAULT_SR_ADDRESS, &mut conf).expect("fetch failed"); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("There shouldn't be a blueprint, the transaction comes from the delayed bridge") + } + + if delayed_inbox_is_empty(&conf, &mut host) { + panic!("The delayed inbox shouldn't be empty") + } + } + + #[test] + fn test_deposit_in_sequencer_mode_without_dal() { + test_deposit_in_sequencer_mode(false) + } + + #[test] + fn test_deposit_in_sequencer_mode_with_dal() { + test_deposit_in_sequencer_mode(true) + } + + fn fill_slots(host: &mut MockKernelHost, slots: Vec) { + let blueprint = dummy_big_blueprint(100); + let chunks = chunk_blueprint(blueprint, 0.into()); + + let dal_parameters = host.reveal_dal_parameters(); + let published_level = host.host.level() - (dal_parameters.attestation_lag as u32); + + let mut remaining_chunks = chunks; + // If slots is empty, the chunks are not put in any slot. + for (i, slot) in slots.iter().enumerate() { + // The last slot contains all the remaining chunks + if i == slots.len() - 1 { + prepare_dal_slot(host, &remaining_chunks, published_level as i32, *slot); + } else { + // Take the first chunk, put it in the slot, and leave the + // rest for the next slots. + if let Some((first_chunk, rem_chunks)) = remaining_chunks.split_first() { + prepare_dal_slot( + host, + &[first_chunk.clone()], + published_level as i32, + *slot, + ); + remaining_chunks = rem_chunks.to_vec(); + } else { + break; + } + } + } + } + + fn setup_dal_signal( + host: &mut MockKernelHost, + conf: &mut Configuration, + signal_slots: Option>, + filled_slots: Option>, + ) { + let dal_slots = if let Some(slots) = signal_slots { + slots + } else { + get_dal_slots(conf).unwrap() + }; + + let signal = make_dal_signal(host, &dal_slots).rlp_bytes(); + let input = frame_message(signal.as_ref(), DAL_SLOT_IMPORT_SIGNAL_TAG); + + host.host.add_external(Bytes::from(input)); + + let filled_slots = filled_slots.unwrap_or(dal_slots); + fill_slots(host, filled_slots); + + fetch_blueprints(host, DEFAULT_SR_ADDRESS, conf).expect("fetch failed"); + } + + #[test] + fn test_dal_signal() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(true, None); + + setup_dal_signal(&mut host, &mut conf, None, None); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_none() + { + panic!("There should be a blueprint in the DAL slot") + } + } + + #[test] + fn test_dal_signal_empty_slot() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(false, Some(vec![8])); + + setup_dal_signal(&mut host, &mut conf, Some(vec![21]), Some(vec![])); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("There shouldn't be a blueprint fetched from DAL slots, as the slot is empty") + } + } + + #[test] + fn test_dal_signal_with_multiple_slots_filled() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(true, Some(vec![6, 8])); + + setup_dal_signal(&mut host, &mut conf, None, None); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_none() + { + panic!("There should be a blueprint in the DAL slot") + } + } + + #[test] + fn test_parsable_dal_signal_without_dal() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(false, None); + + setup_dal_signal(&mut host, &mut conf, Some(vec![6]), None); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("The DAL signal shouldn't have been applied by the kernel, and the blueprint shouldn't have been read") + } + } + + #[test] + fn test_invalid_dal_signal() { + let mut host = MockKernelHost::default(); + let mut conf = dummy_sequencer_config(true, Some(vec![8])); + + setup_dal_signal(&mut host, &mut conf, Some(vec![21]), None); + + if read_next_blueprint(&mut host, &mut conf) + .expect("Blueprint reading shouldn't fail") + .0 + .is_some() + { + panic!("The DAL signal shouldn't have been applied by the kernel, and the blueprint shouldn't have been read") + } + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/storage.rs b/etherlink/kernel_calypso2/kernel/src/storage.rs new file mode 100644 index 000000000000..7d134e2140a2 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/storage.rs @@ -0,0 +1,973 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023-2024 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +use crate::block_in_progress::BlockInProgress; +use crate::event::Event; +use crate::simulation::SimulationResult; +use crate::tick_model::constants::MAXIMUM_GAS_LIMIT; +use anyhow::Context; +use evm_execution::trace::{ + CallTracerInput, StructLoggerInput, TracerInput, CALL_TRACER_CONFIG_PREFIX, +}; +use num_derive::{FromPrimitive, ToPrimitive}; +use num_traits::{FromPrimitive, ToPrimitive}; +use tezos_crypto_rs::hash::ContractKt1Hash; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_indexable_storage::IndexableStorage; +use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; +use tezos_smart_rollup_encoding::public_key::PublicKey; +use tezos_smart_rollup_encoding::timestamp::Timestamp; +use tezos_smart_rollup_host::path::*; +use tezos_smart_rollup_host::runtime::ValueType; +use tezos_storage::{ + read_b58_kt1, read_u256_le, read_u64_le, store_read_slice, write_u256_le, + write_u64_le, +}; + +use crate::error::Error; +use rlp::{Decodable, Encodable, Rlp}; +use tezos_ethereum::rlp_helpers::{FromRlpBytes, VersionedEncoding}; +use tezos_ethereum::transaction::{ + TransactionHash, TransactionObject, TransactionReceipt, +}; + +use primitive_types::{H160, U256}; + +#[derive( + FromPrimitive, ToPrimitive, Copy, Debug, Clone, PartialEq, Eq, PartialOrd, Ord, +)] +#[repr(u64)] +pub enum StorageVersion { + V11 = 11, + V12, + V13, + V14, + V15, + V16, + V17, + V18, + V19, + V20, + V21, + V22, + V23, + V24, + V25, + V26, +} + +impl From for u64 { + fn from(value: StorageVersion) -> Self { + ToPrimitive::to_u64(&value).expect("StorageVersion fits in `u64` primitive") + } +} + +impl StorageVersion { + pub fn next(self) -> Option { + FromPrimitive::from_u64(u64::from(self) + 1) + } +} + +pub const STORAGE_VERSION: StorageVersion = StorageVersion::V26; + +pub const PRIVATE_FLAG_PATH: RefPath = RefPath::assert_from(b"/evm/remove_whitelist"); + +pub const STORAGE_VERSION_PATH: RefPath = RefPath::assert_from(b"/evm/storage_version"); + +const KERNEL_VERSION_PATH: RefPath = RefPath::assert_from(b"/evm/kernel_version"); + +pub const ADMIN: RefPath = RefPath::assert_from(b"/evm/admin"); +pub const SEQUENCER_GOVERNANCE: RefPath = + RefPath::assert_from(b"/evm/sequencer_governance"); +pub const KERNEL_GOVERNANCE: RefPath = RefPath::assert_from(b"/evm/kernel_governance"); +pub const KERNEL_SECURITY_GOVERNANCE: RefPath = + RefPath::assert_from(b"/evm/kernel_security_governance"); +pub const DELAYED_BRIDGE: RefPath = RefPath::assert_from(b"/evm/delayed_bridge"); + +pub const MAXIMUM_ALLOWED_TICKS: RefPath = + RefPath::assert_from(b"/evm/maximum_allowed_ticks"); + +pub const MAXIMUM_GAS_PER_TRANSACTION: RefPath = + RefPath::assert_from(b"/evm/maximum_gas_per_transaction"); + +// Path to the block in progress, used between reboots +const EVM_BLOCK_IN_PROGRESS: RefPath = + RefPath::assert_from(b"/evm/world_state/blocks/in_progress"); + +const EVENTS: RefPath = RefPath::assert_from(b"/evm/events"); + +pub const EVM_TRANSACTIONS_RECEIPTS: RefPath = + RefPath::assert_from(b"/evm/world_state/transactions_receipts"); + +pub const EVM_TRANSACTIONS_OBJECTS: RefPath = + RefPath::assert_from(b"/evm/world_state/transactions_objects"); + +const EVM_CHAIN_ID: RefPath = RefPath::assert_from(b"/evm/chain_id"); + +// Path to the Multichain feature flag. If there is nothing at this path, +// a single chain is used. +#[allow(dead_code)] +pub const ENABLE_MULTICHAIN: RefPath = + RefPath::assert_from(b"/evm/feature_flags/enable_multichain"); + +const EVM_MINIMUM_BASE_FEE_PER_GAS: RefPath = + RefPath::assert_from(b"/evm/world_state/fees/minimum_base_fee_per_gas"); +const EVM_DA_FEE: RefPath = + RefPath::assert_from(b"/evm/world_state/fees/da_fee_per_byte"); +const TICK_BACKLOG_PATH: RefPath = RefPath::assert_from(b"/evm/world_state/fees/backlog"); +const TICK_BACKLOG_TIMESTAMP_PATH: RefPath = + RefPath::assert_from(b"/evm/world_state/fees/last_timestamp"); + +/// The sequencer pool is the designated account that the data-availability fees are sent to. +/// +/// This may be updated by the governance mechanism over time. If it is not set, the data-availability +/// fees are instead burned. +pub const SEQUENCER_POOL_PATH: RefPath = + RefPath::assert_from(b"/evm/sequencer_pool_address"); + +/// Path to the last L1 level seen. +const EVM_L1_LEVEL: RefPath = RefPath::assert_from(b"/evm/l1_level"); + +const EVM_BURNED_FEES: RefPath = RefPath::assert_from(b"/evm/world_state/fees/burned"); + +/// Path to the last info per level timestamp seen. +const EVM_INFO_PER_LEVEL_TIMESTAMP: RefPath = + RefPath::assert_from(b"/evm/info_per_level/timestamp"); +/// Path to the number of timestamps read, use to compute the average block time. +const EVM_INFO_PER_LEVEL_STATS_NUMBERS: RefPath = + RefPath::assert_from(b"/evm/info_per_level/stats/numbers"); +/// Path to the sum of distance between blocks, used to compute the average block time. +const EVM_INFO_PER_LEVEL_STATS_TOTAL: RefPath = + RefPath::assert_from(b"/evm/info_per_level/stats/total"); + +pub const SIMULATION_RESULT: RefPath = RefPath::assert_from(b"/evm/simulation_result"); + +// Path to the number of seconds until delayed txs are timed out. +const EVM_DELAYED_INBOX_TIMEOUT: RefPath = + RefPath::assert_from(b"/evm/delayed_inbox_timeout"); + +// Path to the number of l1 levels that need to pass for a +// delayed tx to be timed out. +const EVM_DELAYED_INBOX_MIN_LEVELS: RefPath = + RefPath::assert_from(b"/evm/delayed_inbox_min_levels"); + +// Path to the tz1 administrating the sequencer. If there is nothing +// at this path, the kernel is in proxy mode. +pub const SEQUENCER: RefPath = RefPath::assert_from(b"/evm/sequencer"); + +// Path to the DAL feature flag. If there is nothing at this path, DAL +// is not used. +pub const ENABLE_DAL: RefPath = RefPath::assert_from(b"/evm/feature_flags/enable_dal"); + +// Path to the DAL slot indices to use. +pub const DAL_SLOTS: RefPath = RefPath::assert_from(b"/evm/dal_slots"); + +// Path where the input for the tracer is stored by the sequencer. +const TRACER_INPUT: RefPath = RefPath::assert_from(b"/evm/trace/input"); + +// If this path contains a value, the fa bridge is enabled in the kernel. +pub const ENABLE_FA_BRIDGE: RefPath = + RefPath::assert_from(b"/evm/feature_flags/enable_fa_bridge"); + +// If the flag is set, the kernel consider that this is local evm node execution. +const EVM_NODE_FLAG: RefPath = RefPath::assert_from(b"/__evm_node"); + +const MAX_BLUEPRINT_LOOKAHEAD_IN_SECONDS: RefPath = + RefPath::assert_from(b"/evm/max_blueprint_lookahead_in_seconds"); + +pub fn receipt_path(receipt_hash: &TransactionHash) -> Result { + let hash = hex::encode(receipt_hash); + let raw_receipt_path: Vec = format!("/{}", &hash).into(); + let receipt_path = OwnedPath::try_from(raw_receipt_path)?; + concat(&EVM_TRANSACTIONS_RECEIPTS, &receipt_path).map_err(Error::from) +} + +pub fn object_path(object_hash: &TransactionHash) -> Result { + let hash = hex::encode(object_hash); + let raw_object_path: Vec = format!("/{}", &hash).into(); + let object_path = OwnedPath::try_from(raw_object_path)?; + concat(&EVM_TRANSACTIONS_OBJECTS, &object_path).map_err(Error::from) +} + +pub fn store_simulation_result( + host: &mut Host, + result: SimulationResult, +) -> Result<(), anyhow::Error> { + let encoded = result.to_bytes(); + host.store_write(&SIMULATION_RESULT, &encoded, 0) + .context("Failed to write the simulation result.") +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn store_transaction_receipt( + host: &mut Host, + receipt: &TransactionReceipt, +) -> Result { + let receipt_path = receipt_path(&receipt.hash)?; + let src: &[u8] = &receipt.rlp_bytes(); + log!(host, Benchmarking, "Storing receipt of size {}", src.len()); + host.store_write_all(&receipt_path, src)?; + Ok(src.len().try_into()?) +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn store_transaction_object( + host: &mut Host, + object: &TransactionObject, +) -> Result { + let object_path = object_path(&object.hash)?; + let encoded: &[u8] = &object.rlp_bytes(); + log!( + host, + Benchmarking, + "Storing transaction object of size {}", + encoded.len() + ); + host.store_write_all(&object_path, encoded)?; + Ok(encoded.len().try_into()?) +} + +const CHUNKED_TRANSACTIONS: RefPath = RefPath::assert_from(b"/chunked_transactions"); +const CHUNKED_TRANSACTION_NUM_CHUNKS: RefPath = RefPath::assert_from(b"/num_chunks"); +const CHUNKED_HASHES: RefPath = RefPath::assert_from(b"/chunk_hashes"); + +pub fn chunked_transaction_path(tx_hash: &TransactionHash) -> Result { + let hash = hex::encode(tx_hash); + let raw_chunked_transaction_path: Vec = format!("/{}", hash).into(); + let chunked_transaction_path = OwnedPath::try_from(raw_chunked_transaction_path)?; + concat(&CHUNKED_TRANSACTIONS, &chunked_transaction_path).map_err(Error::from) +} + +fn chunked_transaction_num_chunks_path( + chunked_transaction_path: &OwnedPath, +) -> Result { + concat(chunked_transaction_path, &CHUNKED_TRANSACTION_NUM_CHUNKS).map_err(Error::from) +} + +pub fn chunked_hash_transaction_path( + chunked_hash: &[u8], + chunked_transaction_path: &OwnedPath, +) -> Result { + let hash = hex::encode(chunked_hash); + let raw_chunked_hash_key: Vec = format!("/{}", hash).into(); + let chunked_hash_key = OwnedPath::try_from(raw_chunked_hash_key)?; + let chunked_hash_path = concat(&CHUNKED_HASHES, &chunked_hash_key)?; + concat(chunked_transaction_path, &chunked_hash_path).map_err(Error::from) +} + +pub fn transaction_chunk_path( + chunked_transaction_path: &OwnedPath, + i: u16, +) -> Result { + let raw_i_path: Vec = format!("/{}", i).into(); + let i_path = OwnedPath::try_from(raw_i_path)?; + concat(chunked_transaction_path, &i_path).map_err(Error::from) +} + +fn is_transaction_complete( + host: &mut Host, + chunked_transaction_path: &OwnedPath, + num_chunks: u16, +) -> Result { + let n_subkeys = host.store_count_subkeys(chunked_transaction_path)? as u16; + // `n_subkeys` includes `num_chunks` and `chunk_hashes` keys + Ok(n_subkeys >= num_chunks + 2) +} + +fn chunked_transaction_num_chunks_by_path( + host: &mut Host, + chunked_transaction_path: &OwnedPath, +) -> Result { + let chunked_transaction_num_chunks_path = + chunked_transaction_num_chunks_path(chunked_transaction_path)?; + let mut buffer = [0u8; 2]; + store_read_slice(host, &chunked_transaction_num_chunks_path, &mut buffer, 2)?; + Ok(u16::from_le_bytes(buffer)) +} + +pub fn chunked_transaction_num_chunks( + host: &mut Host, + tx_hash: &TransactionHash, +) -> Result { + let chunked_transaction_path = chunked_transaction_path(tx_hash)?; + chunked_transaction_num_chunks_by_path(host, &chunked_transaction_path) +} + +fn store_transaction_chunk_data( + host: &mut Host, + transaction_chunk_path: &OwnedPath, + data: Vec, +) -> Result<(), Error> { + match host.store_has(transaction_chunk_path)? { + Some(ValueType::Value | ValueType::ValueWithSubtree) => Ok(()), + _ => { + if data.len() > MAX_FILE_CHUNK_SIZE { + // It comes from an input so it's maximum 4096 bytes (with the message header). + let (data1, data2) = data.split_at(MAX_FILE_CHUNK_SIZE); + host.store_write(transaction_chunk_path, data1, 0)?; + host.store_write(transaction_chunk_path, data2, MAX_FILE_CHUNK_SIZE) + } else { + host.store_write(transaction_chunk_path, &data, 0) + }?; + Ok(()) + } + } +} + +pub fn read_transaction_chunk_data( + host: &mut Host, + transaction_chunk_path: &OwnedPath, +) -> Result, Error> { + let data_size = host.store_value_size(transaction_chunk_path)?; + + if data_size > MAX_FILE_CHUNK_SIZE { + let mut data1 = + host.store_read(transaction_chunk_path, 0, MAX_FILE_CHUNK_SIZE)?; + let mut data2 = host.store_read( + transaction_chunk_path, + MAX_FILE_CHUNK_SIZE, + MAX_FILE_CHUNK_SIZE, + )?; + let _ = &mut data1.append(&mut data2); + Ok(data1) + } else { + Ok(host.store_read(transaction_chunk_path, 0, MAX_FILE_CHUNK_SIZE)?) + } +} + +fn get_full_transaction( + host: &mut Host, + chunked_transaction_path: &OwnedPath, + num_chunks: u16, +) -> Result, Error> { + let mut buffer = Vec::new(); + for i in 0..num_chunks { + let transaction_chunk_path = transaction_chunk_path(chunked_transaction_path, i)?; + let mut data = read_transaction_chunk_data(host, &transaction_chunk_path)?; + let _ = &mut buffer.append(&mut data); + } + Ok(buffer) +} + +pub fn remove_chunked_transaction_by_path( + host: &mut Host, + path: &OwnedPath, +) -> Result<(), Error> { + host.store_delete(path).map_err(Error::from) +} + +pub fn remove_chunked_transaction( + host: &mut Host, + tx_hash: &TransactionHash, +) -> Result<(), Error> { + let chunked_transaction_path = chunked_transaction_path(tx_hash)?; + remove_chunked_transaction_by_path(host, &chunked_transaction_path) +} + +/// Store the transaction chunk in the storage. Returns the full transaction +/// if the last chunk to store is the last missing chunk. +pub fn store_transaction_chunk( + host: &mut Host, + tx_hash: &TransactionHash, + i: u16, + data: Vec, +) -> Result>, Error> { + let chunked_transaction_path = chunked_transaction_path(tx_hash)?; + let num_chunks = + chunked_transaction_num_chunks_by_path(host, &chunked_transaction_path)?; + + // Store the new transaction chunk. + let transaction_chunk_path = transaction_chunk_path(&chunked_transaction_path, i)?; + store_transaction_chunk_data(host, &transaction_chunk_path, data)?; + + // If the chunk was the last one, we gather all the chunks and remove the + // sub elements. + if is_transaction_complete(host, &chunked_transaction_path, num_chunks)? { + let data = get_full_transaction(host, &chunked_transaction_path, num_chunks)?; + host.store_delete(&chunked_transaction_path)?; + Ok(Some(data)) + } else { + Ok(None) + } +} + +pub fn create_chunked_transaction( + host: &mut Host, + tx_hash: &TransactionHash, + num_chunks: u16, + chunk_hashes: Vec, +) -> Result<(), Error> { + let chunked_transaction_path = chunked_transaction_path(tx_hash)?; + + // A new chunked transaction creates the `..//num_chunks`, if there + // is at least one key, it was already created. + if host + .store_count_subkeys(&chunked_transaction_path) + .unwrap_or(0) + > 0 + { + log!( + host, + Info, + "The chunked transaction {} already exist, ignoring the message.\n", + hex::encode(tx_hash) + ); + return Ok(()); + } + + let chunked_transaction_num_chunks_path = + chunked_transaction_num_chunks_path(&chunked_transaction_path)?; + host.store_write( + &chunked_transaction_num_chunks_path, + &u16::to_le_bytes(num_chunks), + 0, + )?; + + for chunk_hash in chunk_hashes.iter() { + let chunk_hash_path = + chunked_hash_transaction_path(chunk_hash, &chunked_transaction_path)?; + host.store_write(&chunk_hash_path, &[0], 0)? + } + + Ok(()) +} + +pub fn store_chain_id( + host: &mut Host, + chain_id: U256, +) -> Result<(), Error> { + write_u256_le(host, &EVM_CHAIN_ID, chain_id).map_err(Error::from) +} + +pub fn read_chain_id(host: &Host) -> Result { + read_u256_le(host, &EVM_CHAIN_ID).map_err(Error::from) +} + +pub fn read_minimum_base_fee_per_gas(host: &Host) -> Result { + read_u256_le(host, &EVM_MINIMUM_BASE_FEE_PER_GAS).map_err(Error::from) +} + +pub fn read_tick_backlog(host: &impl Runtime) -> Result { + read_u64_le(host, &TICK_BACKLOG_PATH).map_err(Error::from) +} + +pub fn store_tick_backlog(host: &mut impl Runtime, value: u64) -> Result<(), Error> { + write_u64_le(host, &TICK_BACKLOG_PATH, value).map_err(Error::from) +} + +pub fn read_tick_backlog_timestamp(host: &impl Runtime) -> Result { + read_u64_le(host, &TICK_BACKLOG_TIMESTAMP_PATH).map_err(Error::from) +} + +pub fn store_tick_backlog_timestamp( + host: &mut impl Runtime, + value: u64, +) -> Result<(), Error> { + write_u64_le(host, &TICK_BACKLOG_TIMESTAMP_PATH, value)?; + Ok(()) +} + +pub fn store_minimum_base_fee_per_gas( + host: &mut Host, + price: U256, +) -> Result<(), Error> { + write_u256_le(host, &EVM_MINIMUM_BASE_FEE_PER_GAS, price).map_err(Error::from) +} + +pub fn store_da_fee( + host: &mut impl Runtime, + base_fee_per_gas: U256, +) -> Result<(), Error> { + write_u256_le(host, &EVM_DA_FEE, base_fee_per_gas).map_err(Error::from) +} + +pub fn read_da_fee(host: &impl Runtime) -> Result { + read_u256_le(host, &EVM_DA_FEE).map_err(Error::from) +} + +pub fn update_burned_fees( + host: &mut impl Runtime, + burned_fee: U256, +) -> Result<(), Error> { + let path = &EVM_BURNED_FEES; + let current = read_u256_le(host, path).unwrap_or_else(|_| U256::zero()); + let new = current.saturating_add(burned_fee); + write_u256_le(host, path, new).map_err(Error::from) +} + +#[cfg(test)] +pub fn read_burned_fees(host: &mut impl Runtime) -> U256 { + let path = &EVM_BURNED_FEES; + read_u256_le(host, path).unwrap_or_else(|_| U256::zero()) +} + +pub fn read_sequencer_pool_address(host: &impl Runtime) -> Option { + let mut bytes = [0; std::mem::size_of::()]; + let Ok(20) = host.store_read_slice(&SEQUENCER_POOL_PATH, 0, bytes.as_mut_slice()) + else { + log!(host, Debug, "No sequencer pool address set"); + return None; + }; + Some(bytes.into()) +} + +pub fn store_sequencer_pool_address( + host: &mut impl Runtime, + address: H160, +) -> Result<(), Error> { + let bytes = address.to_fixed_bytes(); + host.store_write_all(&SEQUENCER_POOL_PATH, bytes.as_slice())?; + Ok(()) +} + +pub fn store_timestamp_path( + host: &mut Host, + path: &OwnedPath, + timestamp: &Timestamp, +) -> Result<(), Error> { + host.store_write(path, ×tamp.i64().to_le_bytes(), 0)?; + Ok(()) +} + +#[allow(dead_code)] +pub fn read_l1_level(host: &mut Host) -> Result { + let mut buffer = [0u8; 4]; + store_read_slice(host, &EVM_L1_LEVEL, &mut buffer, 4)?; + let level = u32::from_le_bytes(buffer); + Ok(level) +} + +pub fn store_l1_level(host: &mut Host, level: u32) -> Result<(), Error> { + host.store_write(&EVM_L1_LEVEL, &level.to_le_bytes(), 0)?; + Ok(()) +} + +pub fn read_last_info_per_level_timestamp_stats( + host: &mut Host, +) -> Result<(i64, i64), Error> { + let mut buffer = [0u8; 8]; + store_read_slice(host, &EVM_INFO_PER_LEVEL_STATS_NUMBERS, &mut buffer, 8)?; + let numbers = i64::from_le_bytes(buffer); + + let mut buffer = [0u8; 8]; + store_read_slice(host, &EVM_INFO_PER_LEVEL_STATS_TOTAL, &mut buffer, 8)?; + let total = i64::from_le_bytes(buffer); + + Ok((numbers, total)) +} + +fn store_info_per_level_stats( + host: &mut Host, + new_timestamp: Timestamp, +) -> Result<(), Error> { + let old_timestamp = + read_last_info_per_level_timestamp(host).unwrap_or_else(|_| Timestamp::from(0)); + let diff = new_timestamp.i64() - old_timestamp.i64(); + let (numbers, total) = + read_last_info_per_level_timestamp_stats(host).unwrap_or((0i64, 0i64)); + let total = total + diff; + let numbers = numbers + 1; + + host.store_write(&EVM_INFO_PER_LEVEL_STATS_TOTAL, &total.to_le_bytes(), 0)?; + host.store_write(&EVM_INFO_PER_LEVEL_STATS_NUMBERS, &numbers.to_le_bytes(), 0)?; + + Ok(()) +} + +pub fn store_last_info_per_level_timestamp( + host: &mut Host, + timestamp: Timestamp, +) -> Result<(), Error> { + store_timestamp_path(host, &EVM_INFO_PER_LEVEL_TIMESTAMP.into(), ×tamp)?; + store_info_per_level_stats(host, timestamp) +} + +pub fn read_timestamp_path( + host: &Host, + path: &OwnedPath, +) -> Result { + let mut buffer = [0u8; 8]; + store_read_slice(host, path, &mut buffer, 8)?; + let timestamp_as_i64 = i64::from_le_bytes(buffer); + Ok(timestamp_as_i64.into()) +} + +pub fn read_last_info_per_level_timestamp( + host: &Host, +) -> Result { + read_timestamp_path(host, &EVM_INFO_PER_LEVEL_TIMESTAMP.into()) +} + +pub fn read_admin(host: &mut Host) -> Option { + read_b58_kt1(host, &ADMIN) +} + +pub fn read_sequencer_governance( + host: &mut Host, +) -> Option { + read_b58_kt1(host, &SEQUENCER_GOVERNANCE) +} + +pub fn read_kernel_governance(host: &mut Host) -> Option { + read_b58_kt1(host, &KERNEL_GOVERNANCE) +} + +pub fn read_kernel_security_governance( + host: &mut Host, +) -> Option { + read_b58_kt1(host, &KERNEL_SECURITY_GOVERNANCE) +} + +pub fn read_maximum_allowed_ticks(host: &mut Host) -> Option { + read_u64_le(host, &MAXIMUM_ALLOWED_TICKS).ok() +} + +/// Reads the maximum gas per transaction. If the value cannot found in the storage, +/// we write the kernel default value in the storage. The value becomes accessible +/// from outside the kernel. +pub fn read_or_set_maximum_gas_per_transaction( + host: &mut Host, +) -> anyhow::Result { + match read_u64_le(host, &MAXIMUM_GAS_PER_TRANSACTION) { + Ok(gas_limit) => Ok(gas_limit), + Err(_) => { + write_u64_le(host, &MAXIMUM_GAS_PER_TRANSACTION, MAXIMUM_GAS_LIMIT)?; + Ok(MAXIMUM_GAS_LIMIT) + } + } +} + +pub fn store_storage_version( + host: &mut Host, + storage_version: StorageVersion, +) -> Result<(), Error> { + let storage_version = u64::from(storage_version); + host.store_write_all(&STORAGE_VERSION_PATH, &storage_version.to_le_bytes()) + .map_err(Error::from) +} + +pub fn read_storage_version( + host: &mut Host, +) -> Result { + match host.store_read_all(&STORAGE_VERSION_PATH) { + Ok(bytes) => { + let slice_of_bytes: [u8; 8] = + bytes[..].try_into().map_err(|_| Error::InvalidConversion)?; + let version_u64 = u64::from_le_bytes(slice_of_bytes); + let version = + FromPrimitive::from_u64(version_u64).ok_or(Error::InvalidConversion)?; + Ok(version) + } + Err(e) => Err(e.into()), + } +} + +pub fn read_kernel_version(host: &mut Host) -> Result { + match host.store_read_all(&KERNEL_VERSION_PATH) { + Ok(bytes) => { + let kernel_version = + std::str::from_utf8(&bytes).map_err(|_| Error::InvalidConversion)?; + Ok(kernel_version.to_owned()) + } + Err(e) => Err(e.into()), + } +} + +pub fn store_kernel_version( + host: &mut Host, + kernel_version: &str, +) -> Result<(), Error> { + let kernel_version = kernel_version.as_bytes(); + host.store_write_all(&KERNEL_VERSION_PATH, kernel_version) + .map_err(Error::from) +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn store_block_in_progress( + host: &mut Host, + bip: &BlockInProgress, +) -> anyhow::Result<()> { + let path = OwnedPath::from(EVM_BLOCK_IN_PROGRESS); + let bytes = &bip.rlp_bytes(); + log!( + host, + Benchmarking, + "Storing Block In Progress of size {}", + bytes.len() + ); + host.store_write_all(&path, bytes) + .context("Failed to store current block in progress") +} + +// DO NOT RENAME: function name is used during benchmark +// Never inlined when the kernel is compiled for benchmarks, to ensure the +// function is visible in the profiling results. +#[cfg_attr(feature = "benchmark", inline(never))] +pub fn read_block_in_progress( + host: &Host, +) -> anyhow::Result> { + let path = OwnedPath::from(EVM_BLOCK_IN_PROGRESS); + if let Some(ValueType::Value) = host.store_has(&path)? { + let bytes = host + .store_read_all(&path) + .context("Failed to read current block in progress")?; + log!( + host, + Benchmarking, + "Reading Block In Progress of size {}", + bytes.len() + ); + let decoder = Rlp::new(bytes.as_slice()); + let bip = BlockInProgress::decode(&decoder) + .context("Failed to decode current block in progress")?; + Ok(Some(bip)) + } else { + Ok(None) + } +} + +pub fn delete_block_in_progress(host: &mut Host) -> anyhow::Result<()> { + host.store_delete(&EVM_BLOCK_IN_PROGRESS) + .context("Failed to delete block in progress") +} + +pub fn sequencer(host: &Host) -> anyhow::Result> { + if host.store_has(&SEQUENCER)?.is_some() { + let bytes = host.store_read_all(&SEQUENCER)?; + let Ok(tz1_b58) = String::from_utf8(bytes) else { + return Ok(None); + }; + let Ok(tz1) = PublicKey::from_b58check(&tz1_b58) else { + return Ok(None); + }; + Ok(Some(tz1)) + } else { + Ok(None) + } +} + +pub fn enable_dal(host: &Host) -> anyhow::Result { + if let Some(ValueType::Value) = host.store_has(&ENABLE_DAL)? { + // When run from the EVM node, the DAL feature is always + // considered as disabled. + let b = evm_node_flag(host)?; + Ok(!b) + } else { + Ok(false) + } +} + +pub fn dal_slots(host: &Host) -> anyhow::Result>> { + if host.store_has(&DAL_SLOTS)?.is_some() { + let bytes = host.store_read_all(&DAL_SLOTS)?; + Ok(Some(bytes)) + } else { + Ok(None) + } +} + +pub fn remove_sequencer(host: &mut Host) -> anyhow::Result<()> { + host.store_delete(&SEQUENCER).map_err(Into::into) +} + +pub fn store_sequencer( + host: &mut Host, + public_key: &PublicKey, +) -> anyhow::Result<()> { + let pk_b58 = PublicKey::to_b58check(public_key); + let bytes = String::as_bytes(&pk_b58); + host.store_write_all(&SEQUENCER, bytes).map_err(Into::into) +} + +pub fn clear_events(host: &mut Host) -> anyhow::Result<()> { + if host.store_has(&EVENTS)?.is_some() { + host.store_delete(&EVENTS) + .context("Failed to delete old events") + } else { + Ok(()) + } +} + +pub fn store_event(host: &mut Host, event: &Event) -> anyhow::Result<()> { + let index = IndexableStorage::new(&EVENTS)?; + index + .push_value(host, &event.rlp_bytes()) + .map_err(Into::into) +} + +pub fn delayed_inbox_timeout(host: &Host) -> anyhow::Result { + // The default timeout is 12 hours + let default_timeout = 43200; + if host.store_has(&EVM_DELAYED_INBOX_TIMEOUT)?.is_some() { + let mut buffer = [0u8; 8]; + store_read_slice(host, &EVM_DELAYED_INBOX_TIMEOUT, &mut buffer, 8)?; + let timeout = u64::from_le_bytes(buffer); + log!( + host, + Debug, + "Using delayed inbox timeout of {} seconds ({} hours)", + timeout, + timeout / 3600 + ); + Ok(timeout) + } else { + log!( + host, + Debug, + "Using default delayed inbox timeout of {} seconds ({} hours)", + default_timeout, + default_timeout / 3600 + ); + Ok(default_timeout) + } +} + +pub fn delayed_inbox_min_levels(host: &Host) -> anyhow::Result { + let default_min_levels = 720; + if host.store_has(&EVM_DELAYED_INBOX_MIN_LEVELS)?.is_some() { + let mut buffer = [0u8; 4]; + store_read_slice(host, &EVM_DELAYED_INBOX_MIN_LEVELS, &mut buffer, 4)?; + let min_levels = u32::from_le_bytes(buffer); + log!( + host, + Debug, + "Using delayed inbox minimum levels: {}", + min_levels + ); + Ok(min_levels) + } else { + log!( + host, + Debug, + "Using default delayed inbox minimum levels: {}", + default_min_levels + ); + Ok(default_min_levels) + } +} + +pub fn read_tracer_input( + host: &mut Host, +) -> anyhow::Result> { + if let Some(ValueType::Value) = host.store_has(&TRACER_INPUT).map_err(Error::from)? { + let bytes = host + .store_read_all(&TRACER_INPUT) + .context("Cannot read tracer input")?; + + let tracer = if bytes[0] == CALL_TRACER_CONFIG_PREFIX { + let call_tracer_input: CallTracerInput = + FromRlpBytes::from_rlp_bytes(&bytes[1..])?; + TracerInput::CallTracer(call_tracer_input) + } else { + let struct_logger_input: StructLoggerInput = + FromRlpBytes::from_rlp_bytes(&bytes)?; + TracerInput::StructLogger(struct_logger_input) + }; + log!(host, Debug, "Tracer input found: {:?}", tracer); + + Ok(Some(tracer)) + } else { + Ok(None) + } +} + +pub fn is_enable_fa_bridge(host: &impl Runtime) -> anyhow::Result { + if let Some(ValueType::Value) = host.store_has(&ENABLE_FA_BRIDGE)? { + Ok(true) + } else { + Ok(false) + } +} + +pub fn evm_node_flag(host: &impl Runtime) -> anyhow::Result { + if let Some(ValueType::Value) = host.store_has(&EVM_NODE_FLAG)? { + Ok(true) + } else { + Ok(false) + } +} + +pub fn max_blueprint_lookahead_in_seconds(host: &impl Runtime) -> anyhow::Result { + let bytes = host.store_read_all(&MAX_BLUEPRINT_LOOKAHEAD_IN_SECONDS)?; + let bytes: [u8; 8] = bytes.as_slice().try_into()?; + Ok(i64::from_le_bytes(bytes)) +} + +#[cfg(test)] +mod internal_for_tests { + use super::*; + + use tezos_ethereum::transaction::TransactionStatus; + + /// Reads status from the receipt in storage. + pub fn read_transaction_receipt_status( + host: &mut Host, + tx_hash: &TransactionHash, + ) -> Result { + let receipt = read_transaction_receipt(host, tx_hash)?; + Ok(receipt.status) + } + + /// Reads a transaction receipt from storage. + pub fn read_transaction_receipt( + host: &mut Host, + tx_hash: &TransactionHash, + ) -> Result { + let receipt_path = receipt_path(tx_hash)?; + let bytes = host.store_read_all(&receipt_path)?; + let receipt = TransactionReceipt::from_rlp_bytes(&bytes)?; + Ok(receipt) + } +} + +#[cfg(test)] +pub use internal_for_tests::*; + +/// Smart Contract of the delayed bridge +/// +/// This smart contract is used to submit transactions to the rollup +/// when in sequencer mode +pub fn read_delayed_transaction_bridge( + host: &Host, +) -> Option { + read_b58_kt1(host, &DELAYED_BRIDGE) +} + +#[cfg(test)] +mod tests { + use tezos_evm_runtime::runtime::MockKernelHost; + + #[test] + fn update_burned_fees() { + // Arrange + let mut host = MockKernelHost::default(); + + let fst = 17.into(); + let snd = 19.into(); + + // Act + let result_fst = super::update_burned_fees(&mut host, fst); + let result_snd = super::update_burned_fees(&mut host, snd); + + // Assert + assert!(result_fst.is_ok()); + assert!(result_snd.is_ok()); + + let burned = super::read_burned_fees(&mut host); + assert_eq!(fst + snd, burned); + } +} diff --git a/etherlink/kernel_calypso2/kernel/src/tick_model.rs b/etherlink/kernel_calypso2/kernel/src/tick_model.rs new file mode 100644 index 000000000000..4b21d8c14506 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/tick_model.rs @@ -0,0 +1,182 @@ +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use tezos_ethereum::transaction::IndexedLog; + +use crate::inbox::Transaction; + +use self::constants::TICKS_FOR_CRYPTO; + +/// Tick model constants +/// +/// Some of the following values were estimated using benchmarking, and should +/// be updated only when the benchmarks are executed. +/// This doesn't apply to inherited constants from the PVM, e.g. maximum +/// number of reboots. +pub mod constants { + + /// Maximum of gas allowed for a transaction. + /// Comes from the block limit, defined in EIP-1559 as 2 * gas target + pub const MAXIMUM_GAS_LIMIT: u64 = 30_000_000; + + /// Maximum number of ticks for a kernel run. + /// Order of magnitude lower than the limit set by the PVM to provide + /// security margin. + pub(crate) const MAX_TICKS: u64 = 30_000_000_000; + + /// Maximum number of allowed ticks for a kernel run. We consider a safety + /// margin and an incompressible initilisation overhead. + pub const MAX_ALLOWED_TICKS: u64 = MAX_TICKS; + + /// Maximum number of reboots for a level as set by the PVM. + pub(crate) const _MAX_NUMBER_OF_REBOOTS: u32 = 1_000; + + /// Overapproximation of the amount of ticks for a deposit. Should take + /// everything into account, execution and registering + pub const TICKS_FOR_DEPOSIT: u64 = 2_000_000; + + /// Overapproximation of the amount of ticks per gas unit. + pub const TICKS_PER_GAS: u64 = 2000; + + // Overapproximation of ticks used in signature verification. + pub const TICKS_FOR_CRYPTO: u64 = 25_000_000; + + /// The minimum amount of gas for an ethereum transaction. + pub const BASE_GAS: u64 = crate::CONFIG.gas_transaction_call; + + /// Overapproximation of the upper bound of the number of ticks used to + /// finalize a block. Considers a block corresponding to an inbox full of + /// transfers, and apply a tick model affine in the number of tx. + pub const FINALIZE_UPPER_BOUND: u64 = 150_000_000; + + /// The number of ticks used for storing receipt is overapproximated by + /// an affine function of the size of the receipt + pub const RECEIPT_TICKS_COEF: u64 = 1028; + pub const RECEIPT_TICKS_INTERCEPT: u64 = 200_000; + + /// The number of ticks used for storing transactions is overapproximated by + /// an affine function of the size of the transaction + pub const TX_OBJ_TICKS_COEF: u64 = 880; + pub const TX_OBJ_TICKS_INTERCEPT: u64 = 900_000; + + /// The number of ticks used to compute the bloom filter is overapproximated + /// by an affine function of the size of the bloom + /// (nb of logs + nb of topics) + pub const BLOOM_TICKS_INTERCEPT: u64 = 10000; + pub const BLOOM_TICKS_COEF: u64 = 85000; + + /// The number of ticks used during transaction execution doing something + /// other than executing an opcode is overapproximated by an affine function + /// of the size of a transaction object + pub const TRANSACTION_OVERHEAD_INTERCEPT: u64 = 1_150_000; + pub const TRANSACTION_OVERHEAD_COEF: u64 = 880; + pub const TRANSFERT_OBJ_SIZE: u64 = 347; + + pub const TRANSACTION_HASH_INTERCEPT: u64 = 200_000; + pub const TRANSACTION_HASH_COEF: u64 = 1400; + + /// The number of ticks to parse a blueprint chunk + pub const TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE: u64 = 27_000_000; + pub const TICKS_FOR_BLUEPRINT_INTERCEPT: u64 = 25_000_000; + + /// The number of ticks to parse a transaction from the delayed bridge + pub const TICKS_FOR_DELAYED_MESSAGES: u64 = 1_380_000; + + /// Number of ticks used to parse deposits + pub const TICKS_PER_DEPOSIT_PARSING: u64 = 1_500_000; +} + +/// Estimation of the number of ticks the kernel can safely spend in the +/// execution of the opcodes. +pub fn estimate_remaining_ticks_for_transaction_execution( + max_allowed_ticks: u64, + ticks: u64, + tx_data_size: u64, +) -> u64 { + max_allowed_ticks + .saturating_sub(TICKS_FOR_CRYPTO) + .saturating_sub(ticks_of_transaction_overhead(tx_data_size)) + .saturating_sub(ticks) +} + +/// Estimation of the number of ticks used up for executing a transaction +/// besides executing the opcodes. +fn ticks_of_transaction_overhead(tx_data_size: u64) -> u64 { + // analysis was done using the object size. It is approximated from the + // data size + let tx_obj_size = tx_data_size + constants::TRANSFERT_OBJ_SIZE; + let tx_hash = tx_data_size + .saturating_mul(constants::TRANSACTION_HASH_COEF) + .saturating_add(constants::TRANSACTION_HASH_INTERCEPT); + tx_obj_size + .saturating_mul(constants::TRANSACTION_OVERHEAD_COEF) + .saturating_add(constants::TRANSACTION_OVERHEAD_INTERCEPT) + .saturating_add(tx_hash) +} + +/// An invalid transaction could not be transmitted to the VM, eg. the nonce +/// was wrong, or the signature verification failed. +pub fn ticks_of_invalid_transaction(tx_data_size: u64) -> u64 { + // If the transaction is invalid, only the base cost is considered. + constants::BASE_GAS + .saturating_mul(constants::TICKS_PER_GAS) + .saturating_add(ticks_of_transaction_overhead(tx_data_size)) +} + +/// Adds the possible overhead this is not accounted during the validation of +/// the transaction. Transaction evaluation (the interpreter) accounts for the +/// ticks itself [resulting_ticks]. +pub fn ticks_of_valid_transaction( + transaction: &Transaction, + resulting_ticks: u64, +) -> u64 { + use crate::inbox::TransactionContent::*; + + match &transaction.content { + Ethereum(_) | EthereumDelayed(_) => { + ticks_of_valid_transaction_ethereum(resulting_ticks, transaction.data_size()) + } + // Ticks are already spent during the validation of the transaction (see + // apply.rs). + Deposit(_) | FaDeposit(_) => resulting_ticks, + } +} + +/// A valid transaction is a transaction that could be transmitted to +/// evm_execution. It can succeed (with or without effect on the state) +/// or fail (if the VM encountered an error). +fn ticks_of_valid_transaction_ethereum(resulting_ticks: u64, tx_data_size: u64) -> u64 { + resulting_ticks + .saturating_add(constants::TICKS_FOR_CRYPTO) + .saturating_add(ticks_of_transaction_overhead(tx_data_size)) +} + +/// The bloom size is the number of logs plus the size of each one, ie the nb of +/// times a new value is added to the bloom filter. See [logs_to_bloom]. +pub fn bloom_size(logs: &[IndexedLog]) -> usize { + let mut size = logs.len(); + for item in logs.iter() { + size += item.log.topics.len(); + } + size +} + +pub fn ticks_of_register(receipt_size: u64, obj_size: u64, bloom_size: u64) -> u64 { + let receipt_ticks: u64 = receipt_size + .saturating_mul(constants::RECEIPT_TICKS_COEF) + .saturating_add(constants::RECEIPT_TICKS_INTERCEPT); + let obj_ticks: u64 = obj_size + .saturating_mul(constants::TX_OBJ_TICKS_COEF) + .saturating_add(constants::TX_OBJ_TICKS_INTERCEPT); + let bloom_ticks: u64 = bloom_size + .saturating_mul(constants::BLOOM_TICKS_COEF) + .saturating_add(constants::BLOOM_TICKS_INTERCEPT); + receipt_ticks + .saturating_add(obj_ticks) + .saturating_add(bloom_ticks) +} + +pub fn maximum_ticks_for_sequencer_chunk() -> u64 { + constants::TICKS_FOR_BLUEPRINT_CHUNK_SIGNATURE +} diff --git a/etherlink/kernel_calypso2/kernel/src/upgrade.rs b/etherlink/kernel_calypso2/kernel/src/upgrade.rs new file mode 100644 index 000000000000..90c413ff4f09 --- /dev/null +++ b/etherlink/kernel_calypso2/kernel/src/upgrade.rs @@ -0,0 +1,243 @@ +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 TriliTech +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::blueprint_storage; +use crate::fallback_upgrade::backup_current_kernel; +use core::fmt; + +use crate::error::UpgradeProcessError; +use crate::event::Event; +use crate::storage; +use crate::storage::{store_sequencer, store_sequencer_pool_address}; +use anyhow::Context; +use primitive_types::H160; +use rlp::Decodable; +use rlp::DecoderError; +use rlp::Encodable; +use tezos_ethereum::rlp_helpers::append_public_key; +use tezos_ethereum::rlp_helpers::append_timestamp; +use tezos_ethereum::rlp_helpers::decode_field; +use tezos_ethereum::rlp_helpers::decode_public_key; +use tezos_ethereum::rlp_helpers::decode_timestamp; +use tezos_ethereum::rlp_helpers::next; +use tezos_evm_logging::{log, Level::*}; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; +use tezos_smart_rollup_encoding::public_key::PublicKey; +use tezos_smart_rollup_encoding::timestamp::Timestamp; +use tezos_smart_rollup_host::path::OwnedPath; +use tezos_smart_rollup_host::path::Path; +use tezos_smart_rollup_host::path::RefPath; +use tezos_smart_rollup_installer_config::binary::promote::upgrade_reveal_flow; +use tezos_storage::read_optional_rlp; + +const KERNEL_UPGRADE: RefPath = RefPath::assert_from(b"/evm/kernel_upgrade"); +pub const KERNEL_ROOT_HASH: RefPath = RefPath::assert_from(b"/evm/kernel_root_hash"); +const SEQUENCER_UPGRADE: RefPath = RefPath::assert_from(b"/evm/sequencer_upgrade"); + +#[derive(Debug, PartialEq, Clone)] +pub struct KernelUpgrade { + pub preimage_hash: [u8; PREIMAGE_HASH_SIZE], + pub activation_timestamp: Timestamp, +} + +impl KernelUpgrade { + const RLP_LIST_SIZE: usize = 2; +} + +impl Decodable for KernelUpgrade { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != KernelUpgrade::RLP_LIST_SIZE { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let preimage_hash: Vec = decode_field(&next(&mut it)?, "preimage_hash")?; + let preimage_hash: [u8; PREIMAGE_HASH_SIZE] = preimage_hash + .try_into() + .map_err(|_| DecoderError::RlpInvalidLength)?; + let activation_timestamp = decode_timestamp(&next(&mut it)?)?; + + Ok(Self { + preimage_hash, + activation_timestamp, + }) + } +} + +impl Encodable for KernelUpgrade { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(KernelUpgrade::RLP_LIST_SIZE); + stream.append_iter(self.preimage_hash); + append_timestamp(stream, self.activation_timestamp); + } +} + +pub fn store_kernel_upgrade( + host: &mut Host, + kernel_upgrade: &KernelUpgrade, +) -> anyhow::Result<()> { + log!( + host, + Info, + "An upgrade to {} is planned for {}", + hex::encode(kernel_upgrade.preimage_hash), + kernel_upgrade.activation_timestamp + ); + Event::Upgrade(kernel_upgrade.clone()).store(host)?; + let path = OwnedPath::from(KERNEL_UPGRADE); + let bytes = &kernel_upgrade.rlp_bytes(); + host.store_write_all(&path, bytes) + .context("Failed to store kernel upgrade") +} + +fn read_kernel_upgrade_at( + host: &impl Runtime, + path: &impl Path, +) -> anyhow::Result> { + read_optional_rlp(host, path).context("Failed to decode kernel upgrade") +} + +pub fn read_kernel_upgrade( + host: &Host, +) -> anyhow::Result> { + read_kernel_upgrade_at(host, &KERNEL_UPGRADE) +} + +pub fn upgrade( + host: &mut Host, + root_hash: [u8; PREIMAGE_HASH_SIZE], +) -> anyhow::Result<()> { + log!(host, Info, "Kernel upgrade initialisation."); + + backup_current_kernel(host)?; + let config = upgrade_reveal_flow(root_hash); + config + .evaluate(host) + .map_err(UpgradeProcessError::InternalUpgrade)?; + + host.store_write_all(&KERNEL_ROOT_HASH, &root_hash)?; + host.store_delete(&KERNEL_UPGRADE)?; + + // Mark for reboot, the upgrade/migration will happen at next + // kernel run, it doesn't matter if it is within the Tezos level + // or not. + host.mark_for_reboot()?; + + log!(host, Info, "Kernel is ready to be upgraded."); + Ok(()) +} + +#[derive(Debug, PartialEq, Clone)] +pub struct SequencerUpgrade { + pub sequencer: PublicKey, + pub pool_address: H160, + pub activation_timestamp: Timestamp, +} + +impl SequencerUpgrade { + const RLP_LIST_SIZE: usize = 3; +} + +impl fmt::Display for SequencerUpgrade { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!( + f, + "sequencer: {}, pool_address: {}, activation_timestamp: {}", + self.sequencer, self.pool_address, self.activation_timestamp + ) + } +} + +impl Decodable for SequencerUpgrade { + fn decode(decoder: &rlp::Rlp) -> Result { + if !decoder.is_list() { + return Err(DecoderError::RlpExpectedToBeList); + } + if decoder.item_count()? != SequencerUpgrade::RLP_LIST_SIZE { + return Err(DecoderError::RlpIncorrectListLen); + } + + let mut it = decoder.iter(); + let sequencer = decode_public_key(&next(&mut it)?)?; + let pool_address: H160 = decode_field(&next(&mut it)?, "sequencer_pool_address")?; + let activation_timestamp = decode_timestamp(&next(&mut it)?)?; + + Ok(Self { + sequencer, + pool_address, + activation_timestamp, + }) + } +} + +impl Encodable for SequencerUpgrade { + fn rlp_append(&self, stream: &mut rlp::RlpStream) { + stream.begin_list(SequencerUpgrade::RLP_LIST_SIZE); + append_public_key(stream, &self.sequencer); + stream.append(&self.pool_address); + append_timestamp(stream, self.activation_timestamp); + } +} + +pub fn store_sequencer_upgrade( + host: &mut Host, + sequencer_upgrade: SequencerUpgrade, +) -> anyhow::Result<()> { + log!( + host, + Info, + "A sequencer upgrade to {} is planned for {}", + sequencer_upgrade.sequencer.to_b58check(), + sequencer_upgrade.activation_timestamp + ); + let bytes = &sequencer_upgrade.rlp_bytes(); + Event::SequencerUpgrade(sequencer_upgrade).store(host)?; + let path = OwnedPath::from(SEQUENCER_UPGRADE); + host.store_write_all(&path, bytes) + .context("Failed to store sequencer upgrade") +} + +pub fn read_sequencer_upgrade( + host: &Host, +) -> anyhow::Result> { + let path = OwnedPath::from(SEQUENCER_UPGRADE); + read_optional_rlp(host, &path).context("Failed to decode sequencer upgrade") +} + +fn delete_sequencer_upgrade(host: &mut Host) -> anyhow::Result<()> { + host.store_delete(&SEQUENCER_UPGRADE) + .context("Failed to delete sequencer upgrade") +} + +fn sequencer_upgrade( + host: &mut Host, + pool_address: H160, + sequencer: &PublicKey, +) -> anyhow::Result<()> { + log!(host, Info, "sequencer upgrade initialisation."); + + store_sequencer(host, sequencer)?; + store_sequencer_pool_address(host, pool_address)?; + delete_sequencer_upgrade(host)?; + log!(host, Info, "Sequencer has been updated."); + Ok(()) +} + +pub fn possible_sequencer_upgrade(host: &mut Host) -> anyhow::Result<()> { + let upgrade = read_sequencer_upgrade(host)?; + if let Some(upgrade) = upgrade { + let ipl_timestamp = storage::read_last_info_per_level_timestamp(host)?; + if ipl_timestamp >= upgrade.activation_timestamp { + sequencer_upgrade(host, upgrade.pool_address, &upgrade.sequencer)?; + blueprint_storage::clear_all_blueprints(host)?; + } + } + Ok(()) +} diff --git a/etherlink/kernel_calypso2/logging/Cargo.toml b/etherlink/kernel_calypso2/logging/Cargo.toml new file mode 100644 index 000000000000..8e5d7679706c --- /dev/null +++ b/etherlink/kernel_calypso2/logging/Cargo.toml @@ -0,0 +1,20 @@ +# SPDX-FileCopyrightText: 2023 Nomadic Labs +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos-evm-logging-calypso2" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +num-traits.workspace = true +num-derive.workspace = true +tezos-smart-rollup-debug.workspace = true + +[features] +default = ["alloc"] +alloc = [] +debug = [] +benchmark = [] diff --git a/etherlink/kernel_calypso2/logging/src/lib.rs b/etherlink/kernel_calypso2/logging/src/lib.rs new file mode 100644 index 000000000000..e15255dab31b --- /dev/null +++ b/etherlink/kernel_calypso2/logging/src/lib.rs @@ -0,0 +1,65 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +#[doc(hidden)] +pub use tezos_smart_rollup_debug::debug_str; + +use num_derive::FromPrimitive; +use num_traits::FromPrimitive; + +#[repr(u8)] +#[derive(PartialEq, Clone, Copy, PartialOrd, FromPrimitive)] +pub enum Level { + Fatal = 0, + Error, + Info, + Debug, + Benchmarking, +} + +impl TryFrom for Level { + type Error = (); + fn try_from(value: u8) -> Result { + FromPrimitive::from_u8(value).ok_or(()) + } +} + +impl Default for Level { + fn default() -> Self { + if cfg!(feature = "debug") { + Self::Debug + } else if cfg!(feature = "benchmark") { + Self::Benchmarking + } else { + Self::Info + } + } +} + +impl std::fmt::Display for Level { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match &self { + Level::Info => write!(f, "Info"), + Level::Error => write!(f, "Error"), + Level::Fatal => write!(f, "Fatal"), + Level::Debug => write!(f, "Debug"), + Level::Benchmarking => write!(f, "Benchmarking"), + } + } +} + +pub trait Verbosity { + fn verbosity(&self) -> Level; +} + +#[cfg(feature = "alloc")] +#[macro_export] +macro_rules! log { + ($host: expr, $level: expr, $fmt: expr $(, $arg:expr)*) => { + if $host.verbosity() >= $level { + let msg = format!("[{}] {}\n", $level, format_args!($fmt $(, $arg)*)); + $crate::debug_str!($host, &msg); + } + }; +} diff --git a/etherlink/kernel_calypso2/runtime/Cargo.toml b/etherlink/kernel_calypso2/runtime/Cargo.toml new file mode 100644 index 000000000000..e4de4647e7fa --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/Cargo.toml @@ -0,0 +1,19 @@ +# SPDX-FileCopyrightText: 2024 Nomadic Labs +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos-evm-runtime-calypso2" +version = "0.1.0" +edition = "2021" + +[dependencies] +tezos-smart-rollup-debug.workspace = true +tezos-smart-rollup-core.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-mock.workspace = true +tezos-smart-rollup-encoding.workspace = true +tezos-evm-logging.workspace = true +sha3.workspace = true + +[dev-dependencies] diff --git a/etherlink/kernel_calypso2/runtime/src/extensions.rs b/etherlink/kernel_calypso2/runtime/src/extensions.rs new file mode 100644 index 000000000000..40c148f53892 --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/extensions.rs @@ -0,0 +1,10 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +// The kernel runtime requires both the standard Runtime and the +// new Extended one. + +pub trait WithGas { + fn add_execution_gas(&mut self, gas: u64); +} diff --git a/etherlink/kernel_calypso2/runtime/src/internal_runtime.rs b/etherlink/kernel_calypso2/runtime/src/internal_runtime.rs new file mode 100644 index 000000000000..495da6f232de --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/internal_runtime.rs @@ -0,0 +1,58 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +// The [__internal_store_get_hash] host function is not made available by the +// SDK. We expose it through an [InternalRuntime] trait. + +use tezos_smart_rollup_host::{path::Path, runtime::RuntimeError, Error}; + +const STORE_HASH_SIZE: usize = 32; + +#[link(wasm_import_module = "smart_rollup_core")] +extern "C" { + pub fn __internal_store_get_hash( + path: *const u8, + path_len: usize, + dst: *mut u8, + max_size: usize, + ) -> i32; +} + +pub trait InternalRuntime { + fn __internal_store_get_hash( + &mut self, + path: &T, + ) -> Result, RuntimeError>; +} + +// Wrapper for InternalRuntime, this will be added +// to the Runtime for the Kernel to use. +// The path is optional to be able to get the hash +// of the root directory. +pub trait ExtendedRuntime { + fn store_get_hash(&mut self, path: &T) -> Result, RuntimeError>; +} + +pub struct InternalHost(); + +impl InternalRuntime for InternalHost { + fn __internal_store_get_hash( + &mut self, + path: &T, + ) -> Result, RuntimeError> { + let mut buffer = [0u8; STORE_HASH_SIZE]; + let result = unsafe { + __internal_store_get_hash( + path.as_ptr(), + path.size(), + buffer.as_mut_ptr(), + STORE_HASH_SIZE, + ) + }; + match Error::wrap(result) { + Ok(_i) => Ok(buffer.to_vec()), + Err(e) => Err(RuntimeError::HostErr(e)), + } + } +} diff --git a/etherlink/kernel_calypso2/runtime/src/lib.rs b/etherlink/kernel_calypso2/runtime/src/lib.rs new file mode 100644 index 000000000000..fc7a995790bb --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/lib.rs @@ -0,0 +1,9 @@ +// SPDX-FileCopyrightText: 2024 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +pub mod extensions; +pub mod internal_runtime; +pub mod mock_internal; +pub mod runtime; +pub mod safe_storage; diff --git a/etherlink/kernel_calypso2/runtime/src/mock_internal.rs b/etherlink/kernel_calypso2/runtime/src/mock_internal.rs new file mode 100644 index 000000000000..37e820a7b50b --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/mock_internal.rs @@ -0,0 +1,18 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// +// SPDX-License-Identifier: MIT + +use crate::internal_runtime::InternalRuntime; +use sha3::{Digest, Keccak256}; +use tezos_smart_rollup_host::path::Path; +use tezos_smart_rollup_host::runtime::RuntimeError; +pub struct MockInternal(); +impl InternalRuntime for MockInternal { + fn __internal_store_get_hash( + &mut self, + path: &T, + ) -> Result, RuntimeError> { + let hash: [u8; 32] = Keccak256::digest(path.as_bytes()).into(); + Ok(hash.into()) + } +} diff --git a/etherlink/kernel_calypso2/runtime/src/runtime.rs b/etherlink/kernel_calypso2/runtime/src/runtime.rs new file mode 100644 index 000000000000..5a0970a614d4 --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/runtime.rs @@ -0,0 +1,343 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Trilitech +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +// The kernel runtime requires both the standard Runtime and the +// new Extended one. + +use std::{ + borrow::{Borrow, BorrowMut}, + marker::PhantomData, +}; + +use crate::{ + extensions::WithGas, + internal_runtime::{ExtendedRuntime, InternalRuntime}, +}; +use tezos_evm_logging::{Level, Verbosity}; +use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; +use tezos_smart_rollup_encoding::smart_rollup::SmartRollupAddress; +use tezos_smart_rollup_host::{ + dal_parameters::RollupDalParameters, + input::Message, + metadata::RollupMetadata, + path::{Path, RefPath}, + runtime::{Runtime as SdkRuntime, RuntimeError, ValueType}, +}; +use tezos_smart_rollup_mock::MockHost; + +// Set by the node, contains the verbosity for the logs +pub const VERBOSITY_PATH: RefPath = RefPath::assert_from(b"/evm/logging_verbosity"); + +pub trait Runtime: + SdkRuntime + InternalRuntime + ExtendedRuntime + Verbosity + WithGas +{ +} + +// If a type implements the Runtime, InternalRuntime and ExtendedRuntime traits, +// it also implements the kernel Runtime. +impl Runtime + for T +{ +} + +// This type has two interesting parts: +// 1. Host: BorrowMut + Borrow +// +// This allows building a KernelHost that can own its host (see +// KernelHost::default()) as long as this host can be borrowed. It makes it +// compatible with type `KernelHost<&mut Host, _>`, which is the type built +// in the kernel as the entrypoint only gives a mutable reference to the +// host. As such, the implementation of the *Runtime traits work for both. +// +// 2. _pd: PhantomData +// +// SdkRuntime cannot be used directly as parameter of Borrow and BorrowMut as +// it is a trait, it needs to be a parameter itself of the type. +// However it is never used in the type itself, which will be rejected by the +// compiler. PhantomData associates `R` to the struct with no cost at +// runtime. +pub struct KernelHost + Borrow> { + pub host: Host, + pub logs_verbosity: Level, + pub execution_gas_used: u64, + pub _pd: PhantomData, +} + +impl + Borrow> SdkRuntime + for KernelHost +{ + #[inline(always)] + fn write_output(&mut self, from: &[u8]) -> Result<(), RuntimeError> { + self.host.borrow_mut().write_output(from) + } + + #[inline(always)] + fn write_debug(&self, msg: &str) { + self.host.borrow().write_debug(msg) + } + + #[inline(always)] + fn read_input(&mut self) -> Result, RuntimeError> { + self.host.borrow_mut().read_input() + } + + #[inline(always)] + fn store_has(&self, path: &T) -> Result, RuntimeError> { + self.host.borrow().store_has(path) + } + + #[inline(always)] + fn store_read( + &self, + path: &T, + from_offset: usize, + max_bytes: usize, + ) -> Result, RuntimeError> { + self.host.borrow().store_read(path, from_offset, max_bytes) + } + + #[inline(always)] + fn store_read_slice( + &self, + path: &T, + from_offset: usize, + buffer: &mut [u8], + ) -> Result { + self.host + .borrow() + .store_read_slice(path, from_offset, buffer) + } + + #[inline(always)] + fn store_read_all(&self, path: &impl Path) -> Result, RuntimeError> { + self.host.borrow().store_read_all(path) + } + + #[inline(always)] + fn store_write( + &mut self, + path: &T, + src: &[u8], + at_offset: usize, + ) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_write(path, src, at_offset) + } + + #[inline(always)] + fn store_write_all( + &mut self, + path: &T, + src: &[u8], + ) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_write_all(path, src) + } + + #[inline(always)] + fn store_delete(&mut self, path: &T) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_delete(path) + } + + #[inline(always)] + fn store_delete_value(&mut self, path: &T) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_delete_value(path) + } + + #[inline(always)] + fn store_count_subkeys(&self, prefix: &T) -> Result { + self.host.borrow().store_count_subkeys(prefix) + } + + #[inline(always)] + fn store_move( + &mut self, + from_path: &impl Path, + to_path: &impl Path, + ) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_move(from_path, to_path) + } + + #[inline(always)] + fn store_copy( + &mut self, + from_path: &impl Path, + to_path: &impl Path, + ) -> Result<(), RuntimeError> { + self.host.borrow_mut().store_copy(from_path, to_path) + } + + #[inline(always)] + fn reveal_preimage( + &self, + hash: &[u8; PREIMAGE_HASH_SIZE], + destination: &mut [u8], + ) -> Result { + self.host.borrow().reveal_preimage(hash, destination) + } + + #[inline(always)] + fn store_value_size(&self, path: &impl Path) -> Result { + self.host.borrow().store_value_size(path) + } + + #[inline(always)] + fn mark_for_reboot(&mut self) -> Result<(), RuntimeError> { + self.host.borrow_mut().mark_for_reboot() + } + + #[inline(always)] + fn reveal_metadata(&self) -> RollupMetadata { + self.host.borrow().reveal_metadata() + } + + #[inline(always)] + fn reveal_dal_page( + &self, + published_level: i32, + slot_index: u8, + page_index: i16, + destination: &mut [u8], + ) -> Result { + self.host.borrow().reveal_dal_page( + published_level, + slot_index, + page_index, + destination, + ) + } + + #[inline(always)] + fn reveal_dal_parameters(&self) -> RollupDalParameters { + self.host.borrow().reveal_dal_parameters() + } + + #[inline(always)] + fn last_run_aborted(&self) -> Result { + // This function is never used by the kernel. Be aware that if you need to use it, you also + // need to modify the WASM Runtime. + unimplemented!() + } + + #[inline(always)] + fn upgrade_failed(&self) -> Result { + // This function is never used by the kernel. Be aware that if you need to use it, you also + // need to modify the WASM Runtime. + unimplemented!() + } + + #[inline(always)] + fn restart_forced(&self) -> Result { + // This function is never used by the kernel. Be aware that if you need to use it, you also + // need to modify the WASM Runtime. + unimplemented!() + } + + #[inline(always)] + fn reboot_left(&self) -> Result { + self.host.borrow().reboot_left() + } + + #[inline(always)] + fn runtime_version(&self) -> Result { + // This function is never used by the kernel. Be aware that if you need to use it, you also + // need to modify the WASM Runtime. + unimplemented!() + } +} + +impl + BorrowMut> InternalRuntime + for KernelHost +{ + #[inline(always)] + fn __internal_store_get_hash( + &mut self, + path: &T, + ) -> Result, RuntimeError> { + self.host.borrow_mut().__internal_store_get_hash(path) + } +} + +impl + Borrow> ExtendedRuntime + for KernelHost +{ + #[inline(always)] + fn store_get_hash(&mut self, path: &T) -> Result, RuntimeError> { + self.__internal_store_get_hash(path) + } +} + +impl + BorrowMut> KernelHost +where + for<'a> &'a mut Host: BorrowMut, +{ + pub fn to_ref_host(&mut self) -> KernelHost { + KernelHost { + host: &mut self.host, + logs_verbosity: self.logs_verbosity, + execution_gas_used: self.execution_gas_used, + _pd: PhantomData, + } + } +} + +impl + Borrow> Verbosity for KernelHost { + fn verbosity(&self) -> Level { + self.logs_verbosity + } +} + +pub fn read_logs_verbosity( + host: &Host, +) -> Level { + match host.store_read_all(&VERBOSITY_PATH) { + Ok(value) if value.len() == 1 => { + Level::try_from(value[0]).unwrap_or(Level::default()) + } + _ => Level::default(), + } +} + +impl + Borrow> WithGas for KernelHost { + fn add_execution_gas(&mut self, gas: u64) { + self.execution_gas_used += gas; + } +} + +impl + Borrow> KernelHost { + pub fn init(host: Host) -> Self { + let logs_verbosity = read_logs_verbosity(host.borrow()); + Self { + host, + logs_verbosity, + execution_gas_used: 0, + _pd: PhantomData, + } + } +} +pub type MockKernelHost = KernelHost; + +impl Default for MockKernelHost { + fn default() -> Self { + Self { + host: MockHost::default(), + logs_verbosity: Level::default(), + execution_gas_used: 0, + _pd: PhantomData, + } + } +} + +impl MockKernelHost { + pub fn with_address(address: SmartRollupAddress) -> Self { + let host = MockHost::with_address(&address); + KernelHost { + host, + logs_verbosity: Level::default(), + execution_gas_used: 0, + _pd: PhantomData, + } + } +} diff --git a/etherlink/kernel_calypso2/runtime/src/safe_storage.rs b/etherlink/kernel_calypso2/runtime/src/safe_storage.rs new file mode 100644 index 000000000000..d55feb706e33 --- /dev/null +++ b/etherlink/kernel_calypso2/runtime/src/safe_storage.rs @@ -0,0 +1,266 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023 Functori +// SPDX-FileCopyrightText: 2023 Trilitech +// SPDX-FileCopyrightText: 2023 Marigold +// +// SPDX-License-Identifier: MIT + +use crate::extensions::WithGas; +use crate::internal_runtime::{ExtendedRuntime, InternalRuntime}; +use crate::runtime::Runtime; +use tezos_evm_logging::Verbosity; +use tezos_smart_rollup_core::PREIMAGE_HASH_SIZE; +use tezos_smart_rollup_host::dal_parameters::RollupDalParameters; +use tezos_smart_rollup_host::{ + input::Message, + metadata::RollupMetadata, + path::{concat, OwnedPath, Path, RefPath}, + runtime::{Runtime as SdkRuntime, RuntimeError, ValueType}, +}; + +pub const TMP_PATH: RefPath = RefPath::assert_from(b"/tmp"); +pub const WORLD_STATE_PATH: RefPath = RefPath::assert_from(b"/evm/world_state"); +pub const TMP_WORLD_STATE_PATH: RefPath = RefPath::assert_from(b"/tmp/evm/world_state"); +pub const TRACE_PATH: RefPath = RefPath::assert_from(b"/evm/trace"); +pub const TMP_TRACE_PATH: RefPath = RefPath::assert_from(b"/tmp/evm/trace"); + +pub fn safe_path(path: &T) -> Result { + concat(&TMP_PATH, path).map_err(|_| RuntimeError::PathNotFound) +} + +pub struct SafeStorage { + pub host: Runtime, +} + +impl InternalRuntime for SafeStorage<&mut Host> { + fn __internal_store_get_hash( + &mut self, + path: &T, + ) -> Result, RuntimeError> { + self.host.__internal_store_get_hash(path) + } +} + +impl ExtendedRuntime for SafeStorage<&mut Host> { + #[inline(always)] + fn store_get_hash(&mut self, path: &P) -> Result, RuntimeError> { + let path = safe_path(path)?; + self.__internal_store_get_hash(&path) + } +} + +impl SdkRuntime for SafeStorage<&mut Host> { + #[inline(always)] + fn write_output(&mut self, from: &[u8]) -> Result<(), RuntimeError> { + self.host.write_output(from) + } + + #[inline(always)] + fn write_debug(&self, msg: &str) { + self.host.write_debug(msg) + } + + #[inline(always)] + fn read_input(&mut self) -> Result, RuntimeError> { + self.host.read_input() + } + + #[inline(always)] + fn store_has(&self, path: &T) -> Result, RuntimeError> { + let path = safe_path(path)?; + self.host.store_has(&path) + } + + #[inline(always)] + fn store_read( + &self, + path: &T, + from_offset: usize, + max_bytes: usize, + ) -> Result, RuntimeError> { + let path = safe_path(path)?; + self.host.store_read(&path, from_offset, max_bytes) + } + + #[inline(always)] + fn store_read_slice( + &self, + path: &T, + from_offset: usize, + buffer: &mut [u8], + ) -> Result { + let path = safe_path(path)?; + self.host.store_read_slice(&path, from_offset, buffer) + } + + #[inline(always)] + fn store_read_all(&self, path: &impl Path) -> Result, RuntimeError> { + let path = safe_path(path)?; + self.host.store_read_all(&path) + } + + #[inline(always)] + fn store_write( + &mut self, + path: &T, + src: &[u8], + at_offset: usize, + ) -> Result<(), RuntimeError> { + let path = safe_path(path)?; + self.host.store_write(&path, src, at_offset) + } + + #[inline(always)] + fn store_write_all( + &mut self, + path: &T, + src: &[u8], + ) -> Result<(), RuntimeError> { + let path = safe_path(path)?; + self.host.store_write_all(&path, src) + } + + #[inline(always)] + fn store_delete(&mut self, path: &T) -> Result<(), RuntimeError> { + let path = safe_path(path)?; + self.host.store_delete(&path) + } + + #[inline(always)] + fn store_delete_value(&mut self, path: &T) -> Result<(), RuntimeError> { + let path = safe_path(path)?; + self.host.store_delete_value(&path) + } + + #[inline(always)] + fn store_count_subkeys(&self, prefix: &T) -> Result { + let prefix = safe_path(prefix)?; + self.host.store_count_subkeys(&prefix) + } + + #[inline(always)] + fn store_move( + &mut self, + from_path: &impl Path, + to_path: &impl Path, + ) -> Result<(), RuntimeError> { + let from_path = safe_path(from_path)?; + let to_path = safe_path(to_path)?; + self.host.store_move(&from_path, &to_path) + } + + #[inline(always)] + fn store_copy( + &mut self, + from_path: &impl Path, + to_path: &impl Path, + ) -> Result<(), RuntimeError> { + let from_path = safe_path(from_path)?; + let to_path = safe_path(to_path)?; + self.host.store_copy(&from_path, &to_path) + } + + #[inline(always)] + fn reveal_preimage( + &self, + hash: &[u8; PREIMAGE_HASH_SIZE], + destination: &mut [u8], + ) -> Result { + self.host.reveal_preimage(hash, destination) + } + + #[inline(always)] + fn store_value_size(&self, path: &impl Path) -> Result { + let path = safe_path(path)?; + self.host.store_value_size(&path) + } + + #[inline(always)] + fn mark_for_reboot(&mut self) -> Result<(), RuntimeError> { + self.host.mark_for_reboot() + } + + #[inline(always)] + fn reveal_metadata(&self) -> RollupMetadata { + self.host.reveal_metadata() + } + + #[inline(always)] + fn reveal_dal_page( + &self, + published_level: i32, + slot_index: u8, + page_index: i16, + destination: &mut [u8], + ) -> Result { + self.host + .reveal_dal_page(published_level, slot_index, page_index, destination) + } + + #[inline(always)] + fn reveal_dal_parameters(&self) -> RollupDalParameters { + self.host.reveal_dal_parameters() + } + + #[inline(always)] + fn last_run_aborted(&self) -> Result { + self.host.last_run_aborted() + } + + #[inline(always)] + fn upgrade_failed(&self) -> Result { + self.host.upgrade_failed() + } + + #[inline(always)] + fn restart_forced(&self) -> Result { + self.host.restart_forced() + } + + #[inline(always)] + fn reboot_left(&self) -> Result { + self.host.reboot_left() + } + + #[inline(always)] + fn runtime_version(&self) -> Result { + self.host.runtime_version() + } +} + +impl Verbosity for SafeStorage<&mut Host> { + fn verbosity(&self) -> tezos_evm_logging::Level { + self.host.verbosity() + } +} + +impl SafeStorage<&mut Host> { + pub fn start(&mut self) -> Result<(), RuntimeError> { + self.host + .store_copy(&WORLD_STATE_PATH, &TMP_WORLD_STATE_PATH) + } + + pub fn promote(&mut self) -> Result<(), RuntimeError> { + self.host + .store_move(&TMP_WORLD_STATE_PATH, &WORLD_STATE_PATH) + } + + // Only used in tracing mode, so that the trace doesn't polute the world + // state but is still promoted at the end and accessible from the node. + pub fn promote_trace(&mut self) -> Result<(), RuntimeError> { + if let Ok(Some(_)) = self.host.store_has(&TMP_TRACE_PATH) { + self.host.store_move(&TMP_TRACE_PATH, &TRACE_PATH)? + } + Ok(()) + } + + pub fn revert(&mut self) -> Result<(), RuntimeError> { + self.host.store_delete(&TMP_PATH) + } +} + +impl WithGas for SafeStorage<&mut Host> { + fn add_execution_gas(&mut self, gas: u64) { + self.host.add_execution_gas(gas) + } +} diff --git a/etherlink/kernel_calypso2/storage/Cargo.toml b/etherlink/kernel_calypso2/storage/Cargo.toml new file mode 100644 index 000000000000..750d6f8e0c18 --- /dev/null +++ b/etherlink/kernel_calypso2/storage/Cargo.toml @@ -0,0 +1,26 @@ +# SPDX-FileCopyrightText: 2024 Functori +# +# SPDX-License-Identifier: MIT + +[package] +name = "tezos-storage-calypso2" +version = "0.1.0" +edition = "2021" +license = "MIT" + +[dependencies] +thiserror.workspace = true +anyhow.workspace = true + +primitive-types.workspace = true + +rlp.workspace = true +hex.workspace = true + +tezos_crypto_rs.workspace = true +sha3.workspace = true + +tezos_ethereum.workspace = true +tezos-evm-runtime.workspace = true +tezos-smart-rollup-host.workspace = true +tezos-smart-rollup-storage.workspace = true diff --git a/etherlink/kernel_calypso2/storage/src/error.rs b/etherlink/kernel_calypso2/storage/src/error.rs new file mode 100644 index 000000000000..55ac5bca2151 --- /dev/null +++ b/etherlink/kernel_calypso2/storage/src/error.rs @@ -0,0 +1,39 @@ +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use rlp::DecoderError; +use tezos_smart_rollup_host::path::PathError; +use tezos_smart_rollup_host::runtime::RuntimeError; +use thiserror::Error; + +#[derive(Error, Debug, Eq, PartialEq)] +pub enum Error { + #[error(transparent)] + Path(PathError), + #[error(transparent)] + Runtime(RuntimeError), + #[error(transparent)] + Storage(tezos_smart_rollup_storage::StorageError), + #[error("Failed to decode: {0}")] + RlpDecoderError(DecoderError), + #[error("Storage error: error while reading a value (incorrect size). Expected {expected} but got {actual}")] + InvalidLoadValue { expected: usize, actual: usize }, +} + +impl From for Error { + fn from(e: PathError) -> Self { + Self::Path(e) + } +} +impl From for Error { + fn from(e: RuntimeError) -> Self { + Self::Runtime(e) + } +} + +impl From for Error { + fn from(e: DecoderError) -> Self { + Self::RlpDecoderError(e) + } +} diff --git a/etherlink/kernel_calypso2/storage/src/helpers.rs b/etherlink/kernel_calypso2/storage/src/helpers.rs new file mode 100644 index 000000000000..62f7ecd39244 --- /dev/null +++ b/etherlink/kernel_calypso2/storage/src/helpers.rs @@ -0,0 +1,11 @@ +// SPDX-FileCopyrightText: 2024 Functori +// +// SPDX-License-Identifier: MIT + +use primitive_types::H256; +use sha3::{Digest, Keccak256}; + +/// Compute the Keccak256 hash of `bytes`. +pub fn bytes_hash(bytes: &[u8]) -> H256 { + H256(Keccak256::digest(bytes).into()) +} diff --git a/etherlink/kernel_calypso2/storage/src/lib.rs b/etherlink/kernel_calypso2/storage/src/lib.rs new file mode 100644 index 000000000000..a4834e10f972 --- /dev/null +++ b/etherlink/kernel_calypso2/storage/src/lib.rs @@ -0,0 +1,242 @@ +// SPDX-FileCopyrightText: 2023 Nomadic Labs +// SPDX-FileCopyrightText: 2023-2024 Functori +// SPDX-FileCopyrightText: 2023 Marigold +// SPDX-FileCopyrightText: 2024 Trilitech +// +// SPDX-License-Identifier: MIT + +pub mod error; +pub mod helpers; + +use crate::error::Error; + +use primitive_types::{H256, U256}; +use rlp::{Decodable, Encodable}; + +use tezos_crypto_rs::hash::{ContractKt1Hash, HashTrait}; +use tezos_ethereum::rlp_helpers::FromRlpBytes; +use tezos_evm_runtime::runtime::Runtime; +use tezos_smart_rollup_host::path::*; +use tezos_smart_rollup_host::runtime::{RuntimeError, ValueType}; + +/// The size of one 256 bit word. Size in bytes. +pub const WORD_SIZE: usize = 32usize; + +/// Return up to buffer.len() from the given path in storage and +/// store the read slice in `buffer`. +/// +/// NB: Value is read starting 0. +pub fn store_read_slice( + host: &impl Runtime, + path: &impl Path, + buffer: &mut [u8], + expected_size: usize, +) -> Result<(), Error> { + let size = host.store_read_slice(path, 0, buffer)?; + if size == expected_size { + Ok(()) + } else { + Err(Error::InvalidLoadValue { + expected: expected_size, + actual: size, + }) + } +} + +/// Get the path corresponding to an index of H256. +pub fn path_from_h256(index: &H256) -> Result { + let path_string = format!("/{}", hex::encode(index.to_fixed_bytes())); + OwnedPath::try_from(path_string).map_err(Error::from) +} + +/// Return a 32 bytes hash from storage at the given `path`. +/// +/// NB: The given bytes are interpreted in big endian order. +pub fn read_h256_be(host: &impl Runtime, path: &impl Path) -> anyhow::Result { + let mut buffer = [0_u8; WORD_SIZE]; + store_read_slice(host, path, &mut buffer, WORD_SIZE)?; + Ok(H256::from_slice(&buffer)) +} + +/// Return a 32 bytes hash from storage at the given `path`. +/// +/// NB: The given bytes are interpreted in big endian order. +pub fn read_h256_be_opt( + host: &impl Runtime, + path: &impl Path, +) -> Result, Error> { + match host.store_read_all(path) { + Ok(bytes) if bytes.len() == WORD_SIZE => Ok(Some(H256::from_slice(&bytes))), + Ok(_) | Err(RuntimeError::PathNotFound) => Ok(None), + Err(err) => Err(err.into()), + } +} + +/// Return a 32 bytes hash from storage at the given `path`. +/// If the path is not found, `default` is returned. +/// +/// NB: The given bytes are interpreted in big endian order. +pub fn read_h256_be_default( + host: &impl Runtime, + path: &impl Path, + default: H256, +) -> Result { + match read_h256_be_opt(host, path)? { + Some(v) => Ok(v), + None => Ok(default), + } +} + +/// Write a 32 bytes hash into storage at the given `path`. +/// +/// NB: The hash is stored in big endian order. +pub fn write_h256_be( + host: &mut impl Runtime, + path: &impl Path, + hash: H256, +) -> anyhow::Result<()> { + Ok(host.store_write_all(path, hash.as_bytes())?) +} + +/// Return an unsigned 32 bytes value from storage at the given `path`. +/// +/// NB: The given bytes are interpreted in little endian order. +pub fn read_u256_le(host: &impl Runtime, path: &impl Path) -> Result { + let bytes = host.store_read_all(path)?; + Ok(U256::from_little_endian(&bytes)) +} + +/// Return an unsigned 32 bytes value from storage at the given `path`. +/// If the path is not found, `default` is returned. +/// +/// NB: The given bytes are interpreted in little endian order. +pub fn read_u256_le_default( + host: &impl Runtime, + path: &impl Path, + default: U256, +) -> Result { + match host.store_read_all(path) { + Ok(bytes) if bytes.len() == WORD_SIZE => Ok(U256::from_little_endian(&bytes)), + Ok(_) | Err(RuntimeError::PathNotFound) => Ok(default), + Err(err) => Err(err.into()), + } +} + +/// Write an unsigned 32 bytes value into storage at the given `path`. +/// +/// NB: The value is stored in little endian order. +pub fn write_u256_le( + host: &mut impl Runtime, + path: &impl Path, + value: U256, +) -> Result<(), Error> { + let mut bytes: [u8; WORD_SIZE] = value.into(); + value.to_little_endian(&mut bytes); + host.store_write_all(path, &bytes).map_err(Error::from) +} + +/// Return an unsigned 8 bytes value from storage at the given `path`. +/// +/// NB: The given bytes are interpreted in little endian order. +pub fn read_u64_le(host: &impl Runtime, path: &impl Path) -> Result { + let mut bytes = [0; std::mem::size_of::()]; + host.store_read_slice(path, 0, bytes.as_mut_slice())?; + Ok(u64::from_le_bytes(bytes)) +} + +/// Return an unsigned 8 bytes value from storage at the given `path`. +/// If the path is not found, `default` is returned. +/// +/// NB: The given bytes are interpreted in little endian order. +pub fn read_u64_le_default( + host: &impl Runtime, + path: &impl Path, + default: u64, +) -> Result { + match host.store_read_all(path) { + Ok(bytes) if bytes.len() == std::mem::size_of::() => { + let bytes_array: [u8; std::mem::size_of::()] = bytes + .try_into() + .map_err(|_| Error::Runtime(RuntimeError::DecodingError))?; + Ok(u64::from_le_bytes(bytes_array)) + } + Ok(_) | Err(RuntimeError::PathNotFound) => Ok(default), + Err(err) => Err(err.into()), + } +} + +/// Return an unsigned 2 bytes value from storage at the given `path`. +/// If the path is not found, `default` is returned. +/// +/// NB: The given bytes are interpreted in little endian order. +pub fn read_u16_le_default( + host: &impl Runtime, + path: &impl Path, + default: u16, +) -> Result { + // This is exactly the same function as `read_u64_le_default`, but you know + // the rule, you start sharing code on the 3rd duplication ;-). + match host.store_read_all(path) { + Ok(bytes) if bytes.len() == std::mem::size_of::() => { + let bytes_array: [u8; std::mem::size_of::()] = bytes + .try_into() + .map_err(|_| Error::Runtime(RuntimeError::DecodingError))?; + Ok(u16::from_le_bytes(bytes_array)) + } + Ok(_) | Err(RuntimeError::PathNotFound) => Ok(default), + Err(err) => Err(err.into()), + } +} + +/// Write an unsigned 8 bytes value into storage at the given `path`. +/// +/// NB: The value is stored in little endian order. +pub fn write_u64_le( + host: &mut impl Runtime, + path: &impl Path, + value: u64, +) -> Result<(), Error> { + host.store_write_all(path, value.to_le_bytes().as_slice()) + .map_err(Error::from) +} + +/// Store `src` (which must be encodable) as rlp bytes into storage +/// at the given `path`. +pub fn store_rlp( + src: &T, + host: &mut impl Runtime, + path: &impl Path, +) -> Result<(), Error> { + host.store_write_all(path, &src.rlp_bytes()) + .map_err(Error::from) +} + +/// Return a decodable value from storage as rlp bytes +/// at the given `path`. +pub fn read_rlp(host: &impl Runtime, path: &impl Path) -> Result { + let bytes = host.store_read_all(path)?; + FromRlpBytes::from_rlp_bytes(&bytes).map_err(Error::from) +} + +/// Return a potential decodable value from storage as rlp bytes +/// at the given `path`. +/// +/// If there is no data, `None` is returned. +pub fn read_optional_rlp( + host: &impl Runtime, + path: &impl Path, +) -> Result, anyhow::Error> { + if let Some(ValueType::Value) = host.store_has(path)? { + let elt = read_rlp(host, path)?; + Ok(Some(elt)) + } else { + Ok(None) + } +} + +/// Return a base58 contract address from storage at the given `path`. +pub fn read_b58_kt1(host: &impl Runtime, path: &impl Path) -> Option { + let bytes = host.store_read_all(path).ok()?; + let kt1_b58 = String::from_utf8(bytes).ok()?; + ContractKt1Hash::from_b58check(&kt1_b58).ok() +} diff --git a/etherlink/lib_wasm_runtime/Cargo.lock b/etherlink/lib_wasm_runtime/Cargo.lock index fad9f94dbc71..02f8d1214cc4 100644 --- a/etherlink/lib_wasm_runtime/Cargo.lock +++ b/etherlink/lib_wasm_runtime/Cargo.lock @@ -1,6 +1,6 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. -version = 3 +version = 4 [[package]] name = "addr2line" @@ -1657,6 +1657,40 @@ dependencies = [ "thiserror", ] +[[package]] +name = "evm-execution-calypso2" +version = "0.1.0" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "aurora-engine-modexp", + "const-decoder", + "evm", + "hex", + "libsecp256k1", + "num-bigint", + "num-traits", + "primitive-types 0.12.2", + "ripemd", + "rlp", + "sha2 0.10.8", + "sha3", + "substrate-bn", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-indexable-storage-calypso2", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "tezos_crypto_rs", + "tezos_data_encoding", + "tezos_ethereum_calypso2", + "thiserror", +] + [[package]] name = "evm-gasometer" version = "0.39.0" @@ -1748,6 +1782,42 @@ dependencies = [ "thiserror", ] +[[package]] +name = "evm_kernel_calypso2" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "ethbloom", + "ethereum", + "evm", + "evm-execution-calypso2", + "hex", + "libsecp256k1", + "num-derive", + "num-traits", + "primitive-types 0.12.2", + "rlp", + "sha3", + "softfloat", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-indexable-storage-calypso2", + "tezos-smart-rollup", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-entrypoint", + "tezos-smart-rollup-host", + "tezos-smart-rollup-installer-config", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "tezos_crypto_rs", + "tezos_data_encoding", + "tezos_ethereum_calypso2", + "thiserror", +] + [[package]] name = "f4jumble" version = "0.1.0" @@ -3105,6 +3175,7 @@ dependencies = [ "env_logger", "evm_kernel_bifrost", "evm_kernel_calypso", + "evm_kernel_calypso2", "hex", "librustzcash", "log", @@ -3113,6 +3184,7 @@ dependencies = [ "octez-riscv", "tezos-evm-runtime-bifrost", "tezos-evm-runtime-calypso", + "tezos-evm-runtime-calypso2", "tezos-smart-rollup-core", "tezos-smart-rollup-host", "tezos_crypto_rs", @@ -4580,6 +4652,15 @@ dependencies = [ "tezos-smart-rollup-debug", ] +[[package]] +name = "tezos-evm-logging-calypso2" +version = "0.1.0" +dependencies = [ + "num-derive", + "num-traits", + "tezos-smart-rollup-debug", +] + [[package]] name = "tezos-evm-runtime-bifrost" version = "0.1.0" @@ -4606,6 +4687,19 @@ dependencies = [ "tezos-smart-rollup-mock", ] +[[package]] +name = "tezos-evm-runtime-calypso2" +version = "0.1.0" +dependencies = [ + "sha3", + "tezos-evm-logging-calypso2", + "tezos-smart-rollup-core", + "tezos-smart-rollup-debug", + "tezos-smart-rollup-encoding", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", +] + [[package]] name = "tezos-indexable-storage-bifrost" version = "0.1.0" @@ -4634,6 +4728,20 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tezos-indexable-storage-calypso2" +version = "0.1.0" +dependencies = [ + "rlp", + "tezos-evm-logging-calypso2", + "tezos-evm-runtime-calypso2", + "tezos-smart-rollup-host", + "tezos-smart-rollup-mock", + "tezos-smart-rollup-storage", + "tezos-storage-calypso2", + "thiserror", +] + [[package]] name = "tezos-smart-rollup" version = "0.2.2" @@ -4833,6 +4941,23 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tezos-storage-calypso2" +version = "0.1.0" +dependencies = [ + "anyhow", + "hex", + "primitive-types 0.12.2", + "rlp", + "sha3", + "tezos-evm-runtime-calypso2", + "tezos-smart-rollup-host", + "tezos-smart-rollup-storage", + "tezos_crypto_rs", + "tezos_ethereum_calypso2", + "thiserror", +] + [[package]] name = "tezos_crypto_rs" version = "0.6.0" @@ -4921,6 +5046,24 @@ dependencies = [ "thiserror", ] +[[package]] +name = "tezos_ethereum_calypso2" +version = "0.1.0" +dependencies = [ + "anyhow", + "bytes", + "ethbloom", + "ethereum", + "hex", + "libsecp256k1", + "primitive-types 0.12.2", + "rlp", + "sha3", + "tezos-smart-rollup-encoding", + "tezos_crypto_rs", + "thiserror", +] + [[package]] name = "thiserror" version = "1.0.69" diff --git a/etherlink/lib_wasm_runtime/Cargo.toml b/etherlink/lib_wasm_runtime/Cargo.toml index 2130fa80461b..5eb9d96984c8 100644 --- a/etherlink/lib_wasm_runtime/Cargo.toml +++ b/etherlink/lib_wasm_runtime/Cargo.toml @@ -31,3 +31,6 @@ runtime-bifrost = { package = "tezos-evm-runtime-bifrost", path = "../kernel_bif # Calypso kernel-calypso = { package = "evm_kernel_calypso", path = "../kernel_calypso/kernel", default-features = false } runtime-calypso = { package = "tezos-evm-runtime-calypso", path = "../kernel_calypso/runtime", default-features = false } +# Calypso 2 +kernel-calypso2 = { package = "evm_kernel_calypso2", path = "../kernel_calypso2/kernel", default-features = false } +runtime-calypso2 = { package = "tezos-evm-runtime-calypso2", path = "../kernel_calypso2/runtime", default-features = false } diff --git a/etherlink/lib_wasm_runtime/dune b/etherlink/lib_wasm_runtime/dune index 5aeb83f66f95..b84ac89af6bb 100644 --- a/etherlink/lib_wasm_runtime/dune +++ b/etherlink/lib_wasm_runtime/dune @@ -20,6 +20,7 @@ (source_tree ../sputnikvm) (source_tree ../kernel_bifrost) (source_tree ../kernel_calypso) + (source_tree ../kernel_calypso2) (source_tree ../../src/rustzcash_deps) (source_tree ../../src/rust_deps/wasmer-3.3.0) (source_tree ../../src/riscv) diff --git a/etherlink/lib_wasm_runtime/src/host.rs b/etherlink/lib_wasm_runtime/src/host.rs index e8d1704c5966..73967cc89d02 100644 --- a/etherlink/lib_wasm_runtime/src/host.rs +++ b/etherlink/lib_wasm_runtime/src/host.rs @@ -9,6 +9,7 @@ use log::{debug, error, info, trace, warn}; use ocaml::Error; use runtime_bifrost::internal_runtime::InternalRuntime as BifrostInternalRuntime; use runtime_calypso::internal_runtime::InternalRuntime as CalypsoInternalRuntime; +use runtime_calypso2::internal_runtime::InternalRuntime as Calypso2InternalRuntime; use tezos_smart_rollup_core::MAX_FILE_CHUNK_SIZE; use tezos_smart_rollup_host::{ input::Message, @@ -504,3 +505,13 @@ impl CalypsoInternalRuntime for Host { Ok(hash.as_bytes().to_vec()) } } + +impl Calypso2InternalRuntime for Host { + fn __internal_store_get_hash(&mut self, path: &T) -> Result, RuntimeError> { + trace!("store_get_hash({path})"); + let hash = + bindings::store_get_hash(self.tree(), path.as_bytes()).map_err(from_binding_error)?; + + Ok(hash.as_bytes().to_vec()) + } +} diff --git a/etherlink/lib_wasm_runtime/src/runtime/mod.rs b/etherlink/lib_wasm_runtime/src/runtime/mod.rs index 5eeaa05f6b35..130f7120139c 100644 --- a/etherlink/lib_wasm_runtime/src/runtime/mod.rs +++ b/etherlink/lib_wasm_runtime/src/runtime/mod.rs @@ -108,6 +108,7 @@ impl Runtime for WasmRuntime { enum NativeKernel { Bifrost, Calypso, + Calypso2, } impl NativeKernel { @@ -115,6 +116,7 @@ impl NativeKernel { match self { Self::Bifrost => RuntimeVersion::V0, Self::Calypso => RuntimeVersion::V1, + Self::Calypso2 => RuntimeVersion::V1, } } } @@ -123,6 +125,8 @@ const BIFROST_ROOT_HASH_HEX: &'static str = "7ff257e4f6ddb11766ec2266857c8fc75bd00e73230a7b598fec2bd9a68b6908"; const CALYPSO_ROOT_HASH_HEX: &'static str = "96114bf7a28e617a3788d8554aa24711b4b11f9c54cd0b12c00bc358beb814a7"; +const CALYPSO2_ROOT_HASH_HEX: &'static str = + "7b42577597504d6a705cdd56e59c770125223a0ffda471d70b463a2dc2d5f84f"; impl NativeKernel { fn of_root_hash(root_hash: &ContextHash) -> Option { @@ -132,6 +136,7 @@ impl NativeKernel { match root_hash_hex.as_str() { BIFROST_ROOT_HASH_HEX => Some(NativeKernel::Bifrost), CALYPSO_ROOT_HASH_HEX => Some(NativeKernel::Calypso), + CALYPSO2_ROOT_HASH_HEX => Some(NativeKernel::Calypso2), _ => None, } } @@ -174,6 +179,16 @@ impl Runtime for NativeRuntime { kernel_calypso::evm_node_entrypoint::populate_delayed_inbox(self.mut_host()); Ok(()) } + ("kernel_run", NativeKernel::Calypso2) => { + trace!("calypso2::kernel_loop"); + kernel_calypso2::kernel_loop(self.mut_host()); + Ok(()) + } + ("populate_delayed_inbox", NativeKernel::Calypso2) => { + trace!("calypso2::populate_delayed_inbox"); + kernel_calypso2::evm_node_entrypoint::populate_delayed_inbox(self.mut_host()); + Ok(()) + } (missing_entrypoint, _) => todo!("entrypoint {missing_entrypoint} not covered yet"), } } diff --git a/manifest/product_etherlink.ml b/manifest/product_etherlink.ml index 0e9ee87b1dfc..defcb30e2a03 100644 --- a/manifest/product_etherlink.ml +++ b/manifest/product_etherlink.ml @@ -85,6 +85,7 @@ let evm_node_rust_deps = [S "source_tree"; S "../sputnikvm"]; [S "source_tree"; S "../kernel_bifrost"]; [S "source_tree"; S "../kernel_calypso"]; + [S "source_tree"; S "../kernel_calypso2"]; [S "source_tree"; S "../../src/rustzcash_deps"]; [S "source_tree"; S "../../src/rust_deps/wasmer-3.3.0"]; [S "source_tree"; S "../../src/riscv"]; -- GitLab From bf4c64773e461ec62a49a420550446aaea751f9e Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Fri, 11 Apr 2025 22:23:32 +0200 Subject: [PATCH 2/5] EVM Node: Revert backport !16588 to Calypso 2 This reverts commit 1a88f48714814e3cacbe619ee2138e7979f620e4 --- .../evm_execution/src/handler.rs | 34 ++++++------------- 1 file changed, 10 insertions(+), 24 deletions(-) diff --git a/etherlink/kernel_calypso2/evm_execution/src/handler.rs b/etherlink/kernel_calypso2/evm_execution/src/handler.rs index 81fa4be8e4d1..ee035e101adf 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/handler.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/handler.rs @@ -2734,7 +2734,7 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { code_address, transfer, input.clone(), - transaction_context.clone(), + transaction_context, ); let gas_after = self.gas_used(); @@ -2752,25 +2752,15 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { }, })) = self.tracer { - let (type_, from) = match call_scheme { - CallScheme::Call => ("CALL", caller), - CallScheme::StaticCall => ("STATICCALL", caller), - CallScheme::DelegateCall => { - // FIXME: #7738 this only point to parent call - // address if it was not a DELEGATECALL or - // CALLCODE itself - ("DELEGATECALL", transaction_context.context.address) - } - CallScheme::CallCode => { - // FIXME: #7738 this only point to parent call - // address if it was not a DELEGATECALL or - // CALLCODE itself - ("CALLCODE", transaction_context.context.address) - } - }; let mut call_trace = CallTrace::new_minimal_trace( - type_.into(), - from, + match call_scheme { + CallScheme::Call => "CALL", + CallScheme::CallCode => "CALLCODE", + CallScheme::DelegateCall => "DELEGATECALL", + CallScheme::StaticCall => "STATICCALL", + } + .into(), + caller, value, gas_after - gas_before, input, @@ -2779,11 +2769,7 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { (self.stack_depth() + 1).try_into().unwrap_or_default(), ); - // for the trace we want the contract address to always be the "to" - // field, not necessarily the address used in the transition context - // which may be something else (eg DELEGATECALL) - call_trace.add_to(Some(code_address)); - + call_trace.add_to(Some(address)); call_trace.add_gas(target_gas); call_trace.add_output(Some(output.to_owned())); -- GitLab From 2ec5a801700ff5e9c401cdee6d6fa3e09fd442c2 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Tue, 7 Jan 2025 17:14:13 +0100 Subject: [PATCH 3/5] =?UTF-8?q?EVM=20Node:=20Apply=20Calypso2=E2=80=99s=20?= =?UTF-8?q?patch=20to=20its=20native=20execution=20kernel?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../evm_execution/src/fa_bridge/mod.rs | 113 ++++++++++++-- .../evm_execution/src/fa_bridge/test_utils.rs | 1 + .../evm_execution/src/handler.rs | 143 ++++++++++-------- etherlink/kernel_calypso2/kernel/src/apply.rs | 110 ++++++++------ 4 files changed, 247 insertions(+), 120 deletions(-) diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs index 2c31dc896318..e3af45a5c829 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs @@ -34,12 +34,12 @@ use std::borrow::Cow; use deposit::FaDeposit; -use evm::{Config, ExitReason}; +use evm::{CallScheme, Capture, Config, ExitReason}; use primitive_types::{H160, U256}; use tezos_ethereum::block::BlockConstants; use tezos_evm_logging::{ log, - Level::{Debug, Info}, + Level::{Debug, Error, Info}, }; use tezos_evm_runtime::runtime::Runtime; use ticket_table::TicketTable; @@ -47,7 +47,7 @@ use withdrawal::FaWithdrawal; use crate::{ account_storage::EthereumAccountStorage, - handler::{CreateOutcome, EvmHandler, ExecutionOutcome, Withdrawal}, + handler::{trace_call, CreateOutcome, EvmHandler, ExecutionOutcome, Withdrawal}, precompiles::{PrecompileBTreeMap, PrecompileOutcome, SYSTEM_ACCOUNT_ADDRESS}, trace::TracerInput, transaction::TransactionContext, @@ -135,6 +135,7 @@ pub fn execute_fa_deposit<'a, Host: Runtime>( allocated_ticks: u64, tracer_input: Option, gas_limit: u64, + retriable: bool, ) -> Result { log!(host, Info, "Going to execute a {}", deposit.display()); @@ -156,20 +157,91 @@ pub fn execute_fa_deposit<'a, Host: Runtime>( // It's ok if internal proxy call fails, we will update the ticket table anyways. let ticket_owner = if let Some(proxy) = deposit.proxy { - let (exit_reason, _, _) = - inner_execute_proxy(&mut handler, caller, proxy, deposit.calldata())?; - // If proxy contract call succeeded, proxy becomes the owner, - // otherwise we fall back and set the receiver as the owner instead. - if exit_reason.is_succeed() { - proxy - } else { + // We create an intermediate transaction layer to be able to revert what's + // done inside the proxy if it's meant to revert. + // The rest of the FA deposit logic remains the same. + + // Create a new transaction layer with 63/64 of the remaining gas. + let gas_limit = handler.nested_call_gas_limit(Some(gas_limit)); + + if let Err(err) = handler.record_cost(gas_limit.unwrap_or_default()) { log!( - handler.borrow_host(), - Info, - "FA deposit: proxy call failed w/ {:?}", - exit_reason + handler.host, + Debug, + "Not enough gas for the proxy call. Returned with error {:?}. \ + Required at least: {:?}", + err, + gas_limit ); + deposit.receiver + } else { + handler.begin_inter_transaction(false, gas_limit)?; + + let gas_before = handler.gas_used(); + let inner_result = + inner_execute_proxy(&mut handler, caller, proxy, deposit.calldata()); + let gas_after = handler.gas_used(); + + // If proxy contract call succeeded, proxy becomes the owner, otherwise we fall back and + // set the receiver as the owner instead—except if the proxy ran out of ticks and the + // transaction is retriable. + let ticket_owner = match &inner_result { + Ok((exit_reason, _, _)) if exit_reason.is_succeed() => proxy, + Ok((exit_reason, _, _)) => { + log!( + handler.borrow_host(), + Debug, + "FA deposit: proxy call failed w/ {:?}", + exit_reason + ); + deposit.receiver + } + Err(EthereumError::OutOfTicks) if retriable => { + // We ran out of ticks, and the execution is retriable: we shortcut the rest of the + // call, and return early. This will trigger a reboot request. + log!( + handler.borrow_host(), + Debug, + "FA deposit: proxy call hard failed w/ OutOfTicks but is retriable" + ); + + let _ = handler.end_inter_transaction::(Err( + EthereumError::OutOfTicks, + )); + return handler + .end_initial_transaction(Err(EthereumError::OutOfTicks)); + } + Err(err) => { + log!( + handler.borrow_host(), + Error, + "FA deposit: proxy call hard failed w/ {:?}, fallback to receiver: {}", + err, + deposit.receiver + ); + deposit.receiver + } + }; + + let proxy_res = handler.end_inter_transaction::(inner_result); + + if let Capture::Exit((ref reason, _, ref output)) = proxy_res { + trace_call( + &mut handler, + CallScheme::Call, + proxy, + U256::zero(), + gas_after - gas_before, + deposit.calldata(), + proxy, + gas_limit, + output, + reason, + ); + } + + ticket_owner } } else { // Proxy contract is not specified @@ -180,6 +252,19 @@ pub fn execute_fa_deposit<'a, Host: Runtime>( // so we need to rollback the entire transaction in that case. let deposit_res = inner_execute_deposit(&mut handler, ticket_owner, deposit); + // Even if the proxy fails, we can't revert the transaction: we need the + // tickets to be moved in the ticket table. + if let Err(ref ticket_err) = deposit_res { + // This is really problematic: the ticket has been lost. Will need + // an update to unlock. + log!( + handler.borrow_host(), + Error, + "FA deposit failed: couldn't even move the tickets into the \ + ticket table {ticket_err:?}" + ); + } + let mut outcome = handler.end_initial_transaction(deposit_res)?; // Adjust resource consumption to account for the outer transaction diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs index 38d874928511..1f0e7b8de603 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/test_utils.rs @@ -174,6 +174,7 @@ pub fn run_fa_deposit( 100_000_000_000, None, gas_limit, + false, ) .expect("Failed to execute deposit") } diff --git a/etherlink/kernel_calypso2/evm_execution/src/handler.rs b/etherlink/kernel_calypso2/evm_execution/src/handler.rs index ee035e101adf..287a799d1314 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/handler.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/handler.rs @@ -167,6 +167,9 @@ impl ExecutionOutcome { pub const fn new_address(&self) -> Option { self.result.new_address() } + pub fn should_be_retried(&self, retriable: bool) -> bool { + retriable && self.result == ExecutionResult::OutOfTicks + } } /// The result of calling a contract as expected by the SputnikVM EVM implementation. @@ -2252,6 +2255,74 @@ fn cached_storage_access( } } +#[allow(clippy::too_many_arguments)] +pub fn trace_call( + handler: &mut EvmHandler, + call_scheme: CallScheme, + caller: H160, + value: U256, + gas_used: u64, + input: Vec, + address: H160, + target_gas: Option, + output: &[u8], + reason: &ExitReason, +) { + if let Some(CallTracer(CallTracerInput { + transaction_hash, + config: + CallTracerConfig { + with_logs, + only_top_call: false, + }, + })) = handler.tracer + { + let mut call_trace = CallTrace::new_minimal_trace( + match call_scheme { + CallScheme::Call => "CALL", + CallScheme::CallCode => "CALLCODE", + CallScheme::DelegateCall => "DELEGATECALL", + CallScheme::StaticCall => "STATICCALL", + } + .into(), + caller, + value, + gas_used, + input, + // We need to make the distinction between the initial call (depth 0) + // and the other subcalls + (handler.stack_depth() + 1).try_into().unwrap_or_default(), + ); + + call_trace.add_to(Some(address)); + call_trace.add_gas(target_gas); + call_trace.add_output(Some(output.to_owned())); + + // TODO: https://gitlab.com/tezos/tezos/-/issues/7437 + // For errors and revert reasons, find the appropriate values + // to return for tracing. The following values are kind of placeholders. + match reason { + ExitReason::Succeed(_) => (), + ExitReason::Error(e) => call_trace.add_error(Some(format!("{:?}", e).into())), + ExitReason::Revert(r) => { + call_trace.add_error(Some(format!("{:?}", r).into())) + } + ExitReason::Fatal(f) => call_trace.add_error(Some(format!("{:?}", f).into())), + }; + + if with_logs { + call_trace.add_logs( + handler + .transaction_data + .last() + .map(|tx_layer| tx_layer.logs.clone()), + ) + } + + let _ = tracer::store_call_trace(handler.host, call_trace, &transaction_hash); + } +} + #[allow(unused_variables)] impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { type CreateInterrupt = Infallible; @@ -2743,66 +2814,18 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { log!(self.host, Debug, "Call ended with reason: {:?}", reason); // TRACING - if let Some(CallTracer(CallTracerInput { - transaction_hash, - config: - CallTracerConfig { - with_logs, - only_top_call: false, - }, - })) = self.tracer - { - let mut call_trace = CallTrace::new_minimal_trace( - match call_scheme { - CallScheme::Call => "CALL", - CallScheme::CallCode => "CALLCODE", - CallScheme::DelegateCall => "DELEGATECALL", - CallScheme::StaticCall => "STATICCALL", - } - .into(), - caller, - value, - gas_after - gas_before, - input, - // We need to make the distinction between the initial call (depth 0) - // and the other subcalls - (self.stack_depth() + 1).try_into().unwrap_or_default(), - ); - - call_trace.add_to(Some(address)); - call_trace.add_gas(target_gas); - call_trace.add_output(Some(output.to_owned())); - - // TODO: https://gitlab.com/tezos/tezos/-/issues/7437 - // For errors and revert reasons, find the appropriate values - // to return for tracing. The following values are kind of placeholders. - match &reason { - ExitReason::Succeed(_) => (), - ExitReason::Error(e) => { - call_trace.add_error(Some(format!("{:?}", e).into())) - } - ExitReason::Revert(r) => { - call_trace.add_error(Some(format!("{:?}", r).into())) - } - ExitReason::Fatal(f) => { - call_trace.add_error(Some(format!("{:?}", f).into())) - } - }; - - if with_logs { - call_trace.add_logs( - self.transaction_data - .last() - .map(|tx_layer| tx_layer.logs.clone()), - ) - } - - let _ = tracer::store_call_trace( - self.host, - call_trace, - &transaction_hash, - ); - } + trace_call( + self, + call_scheme, + caller, + value, + gas_after - gas_before, + input, + address, + target_gas, + &output, + &reason, + ); Capture::Exit((reason, output)) } diff --git a/etherlink/kernel_calypso2/kernel/src/apply.rs b/etherlink/kernel_calypso2/kernel/src/apply.rs index e7328911d364..4503efb6045a 100644 --- a/etherlink/kernel_calypso2/kernel/src/apply.rs +++ b/etherlink/kernel_calypso2/kernel/src/apply.rs @@ -11,8 +11,7 @@ use evm_execution::account_storage::{EthereumAccount, EthereumAccountStorage}; use evm_execution::fa_bridge::deposit::FaDeposit; use evm_execution::fa_bridge::{execute_fa_deposit, FA_DEPOSIT_PROXY_GAS_LIMIT}; use evm_execution::handler::{ - ExecutionOutcome, ExecutionResult as ExecutionOutcomeResult, FastWithdrawalInterface, - RouterInterface, + ExecutionOutcome, FastWithdrawalInterface, RouterInterface, }; use evm_execution::precompiles::{self, PrecompileBTreeMap}; use evm_execution::run_transaction; @@ -299,6 +298,46 @@ fn log_transaction_type(host: &Host, to: Option, data: &[u8 } } +fn execution_result_from_outcome( + host: &mut Host, + execution_outcome: Option, + caller: H160, + retriable: bool, +) -> ExecutionResult { + let (gas_used, estimated_ticks_used, should_be_retried) = match &execution_outcome { + Some(execution_outcome) => { + log!( + host, + Benchmarking, + "Transaction status: OK_{}.", + execution_outcome.is_success() + ); + ( + execution_outcome.gas_used.into(), + execution_outcome.estimated_ticks_used, + execution_outcome.should_be_retried(retriable), + ) + } + None => { + log!(host, Benchmarking, "Transaction status: OK_UNKNOWN."); + (U256::zero(), 0, false) + } + }; + + let transaction_result = TransactionResult { + caller, + execution_outcome, + gas_used, + estimated_ticks_used, + }; + + if should_be_retried { + ExecutionResult::Retriable(transaction_result) + } else { + ExecutionResult::Valid(transaction_result) + } +} + #[allow(clippy::too_many_arguments)] fn apply_ethereum_transaction_common( host: &mut Host, @@ -355,38 +394,12 @@ fn apply_ethereum_transaction_common( } }; - let (gas_used, estimated_ticks_used, out_of_ticks) = match &execution_outcome { - Some(execution_outcome) => { - log!( - host, - Benchmarking, - "Transaction status: OK_{}.", - execution_outcome.is_success() - ); - ( - execution_outcome.gas_used.into(), - execution_outcome.estimated_ticks_used, - execution_outcome.result == ExecutionOutcomeResult::OutOfTicks, - ) - } - None => { - log!(host, Benchmarking, "Transaction status: OK_UNKNOWN."); - (U256::zero(), 0, false) - } - }; - - let transaction_result = TransactionResult { - caller, + Ok(execution_result_from_outcome( + host, execution_outcome, - gas_used, - estimated_ticks_used, - }; - - if out_of_ticks && retriable { - Ok(ExecutionResult::Retriable(transaction_result)) - } else { - Ok(ExecutionResult::Valid(transaction_result)) - } + caller, + retriable, + )) } fn trace_deposit( @@ -457,6 +470,7 @@ fn apply_fa_deposit( allocated_ticks: u64, transaction: &Transaction, tracer_input: Option, + retriable: bool, ) -> Result, Error> { let caller = H160::zero(); // Prevent inner calls to XTZ/FA withdrawal precompiles @@ -472,6 +486,7 @@ fn apply_fa_deposit( allocated_ticks, tracer_input, FA_DEPOSIT_PROXY_GAS_LIMIT, + retriable, ) .map_err(Error::InvalidRunTransaction)?; @@ -481,22 +496,24 @@ fn apply_fa_deposit( "Transaction status: OK_{}.", outcome.is_success() ); + // If transaction will be retried, we shouldn't trace yet + if !outcome.should_be_retried(retriable) { + trace_deposit( + host, + transaction.value(), + transaction.to(), + outcome.gas_used, + &outcome.logs, + tracer_input, + ); + } - trace_deposit( + Ok(execution_result_from_outcome( host, - transaction.value(), - transaction.to(), - outcome.gas_used, - &outcome.logs, - tracer_input, - ); - - Ok(ExecutionResult::Valid(TransactionResult { + Some(outcome), caller, - gas_used: outcome.gas_used.into(), - estimated_ticks_used: outcome.estimated_ticks_used, - execution_outcome: Some(outcome), - })) + retriable, + )) } pub const WITHDRAWAL_OUTBOX_QUEUE: RefPath = @@ -650,6 +667,7 @@ pub fn apply_transaction( allocated_ticks, transaction, tracer_input, + retriable, )? } }; -- GitLab From ea3fbafb986f33470e0d78dd1f617ea101ddc66d Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Fri, 11 Apr 2025 21:17:42 +0200 Subject: [PATCH 4/5] EVM Node: Adapt !16588 to Calypso2 --- .../evm_execution/src/fa_bridge/mod.rs | 3 ++ .../evm_execution/src/handler.rs | 40 +++++++++++++------ 2 files changed, 30 insertions(+), 13 deletions(-) diff --git a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs index e3af45a5c829..86ac49dc1fdf 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/fa_bridge/mod.rs @@ -234,6 +234,9 @@ pub fn execute_fa_deposit<'a, Host: Runtime>( U256::zero(), gas_after - gas_before, deposit.calldata(), + // It’s a simple call, so the context address and the address of the code being + // executed are the same + proxy, proxy, gas_limit, output, diff --git a/etherlink/kernel_calypso2/evm_execution/src/handler.rs b/etherlink/kernel_calypso2/evm_execution/src/handler.rs index 287a799d1314..a52a1c41133d 100644 --- a/etherlink/kernel_calypso2/evm_execution/src/handler.rs +++ b/etherlink/kernel_calypso2/evm_execution/src/handler.rs @@ -2263,7 +2263,8 @@ pub fn trace_call( value: U256, gas_used: u64, input: Vec, - address: H160, + context_address: H160, + code_address: H160, target_gas: Option, output: &[u8], reason: &ExitReason, @@ -2277,15 +2278,25 @@ pub fn trace_call( }, })) = handler.tracer { - let mut call_trace = CallTrace::new_minimal_trace( - match call_scheme { - CallScheme::Call => "CALL", - CallScheme::CallCode => "CALLCODE", - CallScheme::DelegateCall => "DELEGATECALL", - CallScheme::StaticCall => "STATICCALL", + let (type_, from) = match call_scheme { + CallScheme::Call => ("CALL", caller), + CallScheme::StaticCall => ("STATICCALL", caller), + CallScheme::DelegateCall => { + // FIXME: #7738 this only point to parent call + // address if it was not a DELEGATECALL or + // CALLCODE itself + ("DELEGATECALL", context_address) } - .into(), - caller, + CallScheme::CallCode => { + // FIXME: #7738 this only point to parent call + // address if it was not a DELEGATECALL or + // CALLCODE itself + ("CALLCODE", context_address) + } + }; + let mut call_trace = CallTrace::new_minimal_trace( + type_.into(), + from, value, gas_used, input, @@ -2294,7 +2305,10 @@ pub fn trace_call( (handler.stack_depth() + 1).try_into().unwrap_or_default(), ); - call_trace.add_to(Some(address)); + // for the trace we want the contract address to always be the "to" + // field, not necessarily the address used in the transition context + // which may be something else (eg DELEGATECALL) + call_trace.add_to(Some(code_address)); call_trace.add_gas(target_gas); call_trace.add_output(Some(output.to_owned())); @@ -2798,9 +2812,8 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { return Capture::Exit((ethereum_error_to_exit_reason(&err), vec![])); } - let address = transaction_context.context.address; - let gas_before = self.gas_used(); + let context_address = transaction_context.context.address; let result = self.execute_call( code_address, transfer, @@ -2821,7 +2834,8 @@ impl<'a, Host: Runtime> Handler for EvmHandler<'a, Host> { value, gas_after - gas_before, input, - address, + context_address, + code_address, target_gas, &output, &reason, -- GitLab From 31d2ae5c9614c21b9cf13f35968ae0d32c3c19a0 Mon Sep 17 00:00:00 2001 From: Thomas Letan Date: Sun, 13 Apr 2025 11:23:50 +0200 Subject: [PATCH 5/5] Changelog: Calypso2 native execution --- etherlink/CHANGES_NODE.md | 1 + 1 file changed, 1 insertion(+) diff --git a/etherlink/CHANGES_NODE.md b/etherlink/CHANGES_NODE.md index bfd7ec5a2e9b..c8e7e9d31326 100644 --- a/etherlink/CHANGES_NODE.md +++ b/etherlink/CHANGES_NODE.md @@ -16,6 +16,7 @@ `minimal` provides a file to which it streamlines tick and gas consumption, `flamegraph` creates a flamegraph indexed on tick consumption (!17608) +- Adds support for Calypso2 native execution. (!17693) ### Storage changes -- GitLab