From 43e852f5b081a9c9355bf89013db573df7b0d810 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Fri, 27 Sep 2024 11:49:59 -0300 Subject: [PATCH 1/8] Test Stylus gas usage These tests compare the gas usage of EVM opcodes and Stylus HostIOs. Co-authored-by: Aman Sanghi Co-authored-by: Tsahi Zidenberg --- Makefile | 8 +- .../stylus/tests/hostio-test/Cargo.lock | 636 ++++++++++++++++++ .../stylus/tests/hostio-test/Cargo.toml | 17 + .../stylus/tests/hostio-test/src/main.rs | 201 ++++++ contracts | 2 +- system_tests/program_gas_test.go | 437 ++++++++++++ 6 files changed, 1299 insertions(+), 2 deletions(-) create mode 100644 arbitrator/stylus/tests/hostio-test/Cargo.lock create mode 100644 arbitrator/stylus/tests/hostio-test/Cargo.toml create mode 100644 arbitrator/stylus/tests/hostio-test/src/main.rs create mode 100644 system_tests/program_gas_test.go diff --git a/Makefile b/Makefile index c3cf1a5144..88bbd8dabe 100644 --- a/Makefile +++ b/Makefile @@ -149,8 +149,10 @@ stylus_test_erc20_wasm = $(call get_stylus_test_wasm,erc20) stylus_test_erc20_src = $(call get_stylus_test_rust,erc20) stylus_test_read-return-data_wasm = $(call get_stylus_test_wasm,read-return-data) stylus_test_read-return-data_src = $(call get_stylus_test_rust,read-return-data) +stylus_test_hostio-test_wasm = $(call get_stylus_test_wasm,hostio-test) +stylus_test_hostio-test_src = $(call get_stylus_test_rust,hostio-test) -stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_multicall_wasm) $(stylus_test_log_wasm) $(stylus_test_create_wasm) $(stylus_test_math_wasm) $(stylus_test_sdk-storage_wasm) $(stylus_test_erc20_wasm) $(stylus_test_read-return-data_wasm) $(stylus_test_evm-data_wasm) $(stylus_test_bfs:.b=.wasm) +stylus_test_wasms = $(stylus_test_keccak_wasm) $(stylus_test_keccak-100_wasm) $(stylus_test_fallible_wasm) $(stylus_test_storage_wasm) $(stylus_test_multicall_wasm) $(stylus_test_log_wasm) $(stylus_test_create_wasm) $(stylus_test_math_wasm) $(stylus_test_sdk-storage_wasm) $(stylus_test_erc20_wasm) $(stylus_test_read-return-data_wasm) $(stylus_test_evm-data_wasm) $(stylus_test_hostio-test_wasm) $(stylus_test_bfs:.b=.wasm) stylus_benchmarks = $(wildcard $(stylus_dir)/*.toml $(stylus_dir)/src/*.rs) $(stylus_test_wasms) # user targets @@ -482,6 +484,10 @@ $(stylus_test_erc20_wasm): $(stylus_test_erc20_src) $(cargo_nightly) --manifest-path $< --release --config $(stylus_cargo) @touch -c $@ # cargo might decide to not rebuild the binary +$(stylus_test_hostio-test_wasm): $(stylus_test_hostio-test_src) + $(cargo_nightly) --manifest-path $< --release --config $(stylus_cargo) + @touch -c $@ # cargo might decide to not rebuild the binary + contracts/test/prover/proofs/float%.json: $(arbitrator_cases)/float%.wasm $(prover_bin) $(output_latest)/soft-float.wasm $(prover_bin) $< -l $(output_latest)/soft-float.wasm -o $@ -b --allow-hostapi --require-success diff --git a/arbitrator/stylus/tests/hostio-test/Cargo.lock b/arbitrator/stylus/tests/hostio-test/Cargo.lock new file mode 100644 index 0000000000..1e726910b1 --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/Cargo.lock @@ -0,0 +1,636 @@ +# This file is automatically @generated by Cargo. +# It is not intended for manual editing. +version = 3 + +[[package]] +name = "aho-corasick" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" +dependencies = [ + "memchr", +] + +[[package]] +name = "alloy-primitives" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e416903084d3392ebd32d94735c395d6709415b76c7728e594d3f996f2b03e65" +dependencies = [ + "bytes", + "cfg-if 1.0.0", + "const-hex", + "derive_more", + "hex-literal", + "itoa", + "ruint", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-macro" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a74ceeffdacf9dd0910404d743d07273776fd17b85f9cb17b49a97e5c6055ce9" +dependencies = [ + "dunce", + "heck", + "proc-macro2", + "quote", + "syn 2.0.77", + "syn-solidity", + "tiny-keccak", +] + +[[package]] +name = "alloy-sol-types" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d5f347cb6bb307b3802ec455ef43ce00f5e590e0ceca3d2f3b070f5ee367e235" +dependencies = [ + "alloy-primitives", + "alloy-sol-macro", + "const-hex", +] + +[[package]] +name = "autocfg" +version = "1.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" + +[[package]] +name = "bitflags" +version = "2.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de" + +[[package]] +name = "block-buffer" +version = "0.10.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" +dependencies = [ + "generic-array", +] + +[[package]] +name = "byteorder" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" + +[[package]] +name = "bytes" +version = "1.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "428d9aa8fbc0670b7b8d6030a7fadd0f86151cae55e4dbbece15f3780a3dfaf3" + +[[package]] +name = "cfg-if" +version = "0.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822" + +[[package]] +name = "cfg-if" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" + +[[package]] +name = "const-hex" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8a24a26d37e1ffd45343323dc9fe6654ceea44c12f2fcb3d7ac29e610bc6" +dependencies = [ + "cfg-if 1.0.0", + "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 = "convert_case" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec182b0ca2f35d8fc196cf3404988fd8b8c739a4d270ff118a398feb0cbec1ca" +dependencies = [ + "unicode-segmentation", +] + +[[package]] +name = "cpufeatures" +version = "0.2.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "608697df725056feaccfa42cffdaeeec3fccc4ffc38358ecd19b243e716a78e0" +dependencies = [ + "libc", +] + +[[package]] +name = "crunchy" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a81dae078cea95a014a339291cec439d2f232ebe854a9d672b796c6afafa9b7" + +[[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 = "derivative" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcc3dd5e9e9c0b295d6e1e4d811fb6f157d5ffd784b8d202fc62eac8035a770b" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + +[[package]] +name = "derive_more" +version = "0.99.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f33878137e4dafd7fa914ad4e259e18a4e8e532b9617a2d0150262bf53abfce" +dependencies = [ + "convert_case 0.4.0", + "proc-macro2", + "quote", + "rustc_version", + "syn 2.0.77", +] + +[[package]] +name = "digest" +version = "0.10.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" +dependencies = [ + "block-buffer", + "crypto-common", +] + +[[package]] +name = "dunce" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "92773504d58c093f6de2459af4af33faa518c13451eb8f2b5698ed3d36e7c813" + +[[package]] +name = "generic-array" +version = "0.14.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" +dependencies = [ + "typenum", + "version_check", +] + +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + +[[package]] +name = "hex" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" + +[[package]] +name = "hex-literal" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6fe2267d4ed49bc07b63801559be28c718ea06c4738b7a03c94df7386d2cde46" + +[[package]] +name = "hostio-test" +version = "0.1.0" +dependencies = [ + "mini-alloc", + "stylus-sdk", +] + +[[package]] +name = "itoa" +version = "1.0.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b" + +[[package]] +name = "keccak" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc2af9a1119c51f12a14607e783cb977bde58bc069ff0c3da1095e635d70654" +dependencies = [ + "cpufeatures", +] + +[[package]] +name = "keccak-const" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "57d8d8ce877200136358e0bbff3a77965875db3af755a11e1fa6b1b3e2df13ea" + +[[package]] +name = "lazy_static" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" + +[[package]] +name = "libc" +version = "0.2.159" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "561d97a539a36e26a9a5fad1ea11a3039a67714694aaa379433e580854bc3dc5" + +[[package]] +name = "libm" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" + +[[package]] +name = "memchr" +version = "2.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" + +[[package]] +name = "memory_units" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8452105ba047068f40ff7093dd1d9da90898e63dd61736462e9cdda6a90ad3c3" + +[[package]] +name = "mini-alloc" +version = "0.4.2" +dependencies = [ + "cfg-if 1.0.0", + "wee_alloc", +] + +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", + "libm", +] + +[[package]] +name = "ppv-lite86" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "77957b295656769bb8ad2b6a6b09d897d94f05c41b069aede1fcdaa675eaea04" +dependencies = [ + "zerocopy", +] + +[[package]] +name = "proc-macro2" +version = "1.0.86" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e719e8df665df0d1c8fbfd238015744736151d4445ec0836b8e628aae103b77" +dependencies = [ + "unicode-ident", +] + +[[package]] +name = "proptest" +version = "1.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b4c2511913b88df1637da85cc8d96ec8e43a3f8bb8ccb71ee1ac240d6f3df58d" +dependencies = [ + "bitflags", + "num-traits", + "rand", + "rand_chacha", + "rand_xorshift", + "unarray", +] + +[[package]] +name = "quote" +version = "1.0.37" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b5b9d34b8991d19d98081b46eacdd8eb58c6f2b201139f7c5f643cc155a633af" +dependencies = [ + "proc-macro2", +] + +[[package]] +name = "rand" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +dependencies = [ + "rand_core", +] + +[[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", +] + +[[package]] +name = "rand_core" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" + +[[package]] +name = "rand_xorshift" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d25bf25ec5ae4a3f1b92f929810509a2f53d7dca2f50b794ff57e3face536c8f" +dependencies = [ + "rand_core", +] + +[[package]] +name = "regex" +version = "1.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4219d74c6b67a3654a9fbebc4b419e22126d13d2f3c4a07ee0cb61ff79a79619" +dependencies = [ + "aho-corasick", + "memchr", + "regex-automata", + "regex-syntax", +] + +[[package]] +name = "regex-automata" +version = "0.4.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38caf58cc5ef2fed281f89292ef23f6365465ed9a41b7a7754eb4e26496c92df" +dependencies = [ + "aho-corasick", + "memchr", + "regex-syntax", +] + +[[package]] +name = "regex-syntax" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7a66a03ae7c801facd77a29370b4faec201768915ac14a721ba36f20bc9c209b" + +[[package]] +name = "ruint" +version = "1.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c3cc4c2511671f327125da14133d0c5c5d137f006a1017a16f557bc85b16286" +dependencies = [ + "proptest", + "rand", + "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_version" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" +dependencies = [ + "semver", +] + +[[package]] +name = "semver" +version = "1.0.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" + +[[package]] +name = "serde" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8e3592472072e6e22e0a54d5904d9febf8508f65fb8552499a1abc7d1078c3a" +dependencies = [ + "serde_derive", +] + +[[package]] +name = "serde_derive" +version = "1.0.210" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "243902eda00fad750862fc144cea25caca5e20d615af0a81bee94ca738f1df1f" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "sha3" +version = "0.10.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +dependencies = [ + "digest", + "keccak", +] + +[[package]] +name = "stylus-proc" +version = "0.4.2" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.0", + "convert_case 0.6.0", + "lazy_static", + "proc-macro2", + "quote", + "regex", + "sha3", + "syn 1.0.109", + "syn-solidity", +] + +[[package]] +name = "stylus-sdk" +version = "0.4.2" +dependencies = [ + "alloy-primitives", + "alloy-sol-types", + "cfg-if 1.0.0", + "derivative", + "hex", + "keccak-const", + "lazy_static", + "stylus-proc", +] + +[[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.77" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9f35bcdf61fd8e7be6caf75f429fdca8beb3ed76584befb503b1569faee373ed" +dependencies = [ + "proc-macro2", + "quote", + "unicode-ident", +] + +[[package]] +name = "syn-solidity" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e5f995d2140b0f751dbe94365be2591edbf3d1b75dcfaeac14183abbd2ff07bd" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "tiny-keccak" +version = "2.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2c9d3793400a45f954c52e73d068316d76b6f4e36977e3fcebb13a2721e80237" +dependencies = [ + "crunchy", +] + +[[package]] +name = "typenum" +version = "1.17.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "42ff0bf0c66b8238c6f3b578df37d0b7848e55df8577b3f74f92a69acceeb825" + +[[package]] +name = "unarray" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eaea85b334db583fe3274d12b4cd1880032beab409c0d774be044d4480ab9a94" + +[[package]] +name = "unicode-ident" +version = "1.0.13" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e91b56cd4cadaeb79bbf1a5645f6b4f8dc5bde8834ad5894a8db35fda9efa1fe" + +[[package]] +name = "unicode-segmentation" +version = "1.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6ccf251212114b54433ec949fd6a7841275f9ada20dddd2f29e9ceea4501493" + +[[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.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" + +[[package]] +name = "wee_alloc" +version = "0.4.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dbb3b5a6b2bb17cb6ad44a2e68a43e8d2722c997da10e928665c72ec6c0a0b8e" +dependencies = [ + "cfg-if 0.1.10", + "libc", + "memory_units", + "winapi", +] + +[[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 = "zerocopy" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b9b4fd18abc82b8136838da5d50bae7bdea537c574d8dc1a34ed098d6c166f0" +dependencies = [ + "byteorder", + "zerocopy-derive", +] + +[[package]] +name = "zerocopy-derive" +version = "0.7.35" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa4f8080344d4671fb4e831a13ad1e68092748387dfc4f55e356242fae12ce3e" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.77", +] + +[[package]] +name = "zeroize" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" diff --git a/arbitrator/stylus/tests/hostio-test/Cargo.toml b/arbitrator/stylus/tests/hostio-test/Cargo.toml new file mode 100644 index 0000000000..da7bbce7a3 --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/Cargo.toml @@ -0,0 +1,17 @@ +[package] +name = "hostio-test" +version = "0.1.0" +edition = "2021" + +[dependencies] +stylus-sdk = { path = "../../../langs/rust/stylus-sdk", features = ["debug", "hostio"] } +mini-alloc.path = "../../../langs/rust/mini-alloc" + +[profile.release] +codegen-units = 1 +strip = true +lto = true +panic = "abort" +opt-level = "s" + +[workspace] diff --git a/arbitrator/stylus/tests/hostio-test/src/main.rs b/arbitrator/stylus/tests/hostio-test/src/main.rs new file mode 100644 index 0000000000..5aa465598e --- /dev/null +++ b/arbitrator/stylus/tests/hostio-test/src/main.rs @@ -0,0 +1,201 @@ +// Copyright 2024, Offchain Labs, Inc. +// For license information, see https://github.com/OffchainLabs/nitro/blob/master/LICENSE + +#![no_main] + +use stylus_sdk::{ + abi::Bytes, + alloy_primitives::{Address, B256, U256}, + block, console, contract, evm, hostio, msg, + prelude::*, + stylus_proc::entrypoint, + tx, + types::AddressVM, +}; +extern crate alloc; + +#[cfg(target_arch = "wasm32")] +#[global_allocator] +static ALLOC: mini_alloc::MiniAlloc = mini_alloc::MiniAlloc::INIT; + +sol_storage! { + #[entrypoint] + pub struct HostioTest { + } +} + +type Result = std::result::Result>; + +// These are not available as hostios in the sdk, so we import them directly. +#[link(wasm_import_module = "vm_hooks")] +extern "C" { + fn math_div(value: *mut u8, divisor: *const u8); + fn math_mod(value: *mut u8, modulus: *const u8); + fn math_pow(value: *mut u8, exponent: *const u8); + fn math_add_mod(value: *mut u8, addend: *const u8, modulus: *const u8); + fn math_mul_mod(value: *mut u8, multiplier: *const u8, modulus: *const u8); + fn transient_load_bytes32(key: *const u8, dest: *mut u8); + fn transient_store_bytes32(key: *const u8, value: *const u8); + fn exit_early(status: u32); +} + +#[external] +impl HostioTest { + fn exit_early() -> Result<()> { + unsafe { + exit_early(0); + } + Ok(()) + } + + fn transient_load_bytes32(key: B256) -> Result { + let mut result = B256::ZERO; + unsafe { + transient_load_bytes32(key.as_ptr(), result.as_mut_ptr()); + } + Ok(result) + } + + fn transient_store_bytes32(key: B256, value: B256) { + unsafe { + transient_store_bytes32(key.as_ptr(), value.as_ptr()); + } + } + + fn return_data_size() -> Result { + unsafe { Ok(hostio::return_data_size().try_into().unwrap()) } + } + + fn emit_log(data: Bytes, n: i8, t1: B256, t2: B256, t3: B256, t4: B256) -> Result<()> { + let topics = &[t1, t2, t3, t4]; + evm::raw_log(&topics[0..n as usize], data.as_slice())?; + Ok(()) + } + + fn account_balance(account: Address) -> Result { + Ok(account.balance()) + } + + fn account_code(account: Address) -> Result> { + Ok(account.code()) + } + + fn account_code_size(account: Address) -> Result { + Ok(account.code_size().try_into().unwrap()) + } + + fn account_codehash(account: Address) -> Result { + Ok(account.codehash()) + } + + fn evm_gas_left() -> Result { + Ok(evm::gas_left().try_into().unwrap()) + } + + fn evm_ink_left() -> Result { + Ok(tx::ink_to_gas(evm::ink_left()).try_into().unwrap()) + } + + fn block_basefee() -> Result { + Ok(block::basefee()) + } + + fn chainid() -> Result { + Ok(block::chainid().try_into().unwrap()) + } + + fn block_coinbase() -> Result
{ + Ok(block::coinbase()) + } + + fn block_gas_limit() -> Result { + Ok(block::gas_limit().try_into().unwrap()) + } + + fn block_number() -> Result { + Ok(block::number().try_into().unwrap()) + } + + fn block_timestamp() -> Result { + Ok(block::timestamp().try_into().unwrap()) + } + + fn contract_address() -> Result
{ + Ok(contract::address()) + } + + fn math_div(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_div(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_mod(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_pow(a: U256, b: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + unsafe { + math_pow(a_bytes.as_mut_ptr(), b_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_add_mod(a: U256, b: U256, c: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + let c_bytes: B256 = c.into(); + unsafe { + math_add_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr(), c_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn math_mul_mod(a: U256, b: U256, c: U256) -> Result { + let mut a_bytes: B256 = a.into(); + let b_bytes: B256 = b.into(); + let c_bytes: B256 = c.into(); + unsafe { + math_mul_mod(a_bytes.as_mut_ptr(), b_bytes.as_ptr(), c_bytes.as_ptr()); + } + Ok(a_bytes.into()) + } + + fn msg_sender() -> Result
{ + Ok(msg::sender()) + } + + fn msg_value() -> Result { + Ok(msg::value()) + } + + fn keccak(preimage: Bytes) -> Result { + let mut result = B256::ZERO; + unsafe { + hostio::native_keccak256(preimage.as_ptr(), preimage.len(), result.as_mut_ptr()); + } + Ok(result) + } + + fn tx_gas_price() -> Result { + Ok(tx::gas_price()) + } + + fn tx_ink_price() -> Result { + Ok(tx::ink_to_gas(tx::ink_price().into()).try_into().unwrap()) + } + + fn tx_origin() -> Result
{ + Ok(tx::origin()) + } +} diff --git a/contracts b/contracts index 7396313311..cde7f96486 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 7396313311ab17cb30e2eef27cccf96f0a9e8f7f +Subproject commit cde7f96486cc21befc9a05fc3b27a7d6ebb5d5f8 diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go new file mode 100644 index 0000000000..48db268907 --- /dev/null +++ b/system_tests/program_gas_test.go @@ -0,0 +1,437 @@ +package arbtest + +import ( + "context" + "fmt" + "math" + "math/big" + "regexp" + "strings" + "testing" + + "github.com/ethereum/go-ethereum/accounts/abi/bind" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/core/vm" + "github.com/ethereum/go-ethereum/eth/tracers/logger" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/ethereum/go-ethereum/rpc" + "github.com/offchainlabs/nitro/arbos/util" + "github.com/offchainlabs/nitro/execution/gethexec" + "github.com/offchainlabs/nitro/solgen/go/mocksgen" + "github.com/offchainlabs/nitro/solgen/go/precompilesgen" + "github.com/offchainlabs/nitro/util/testhelpers" +) + +func TestProgramSimpleCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + otherProgram := deployWasm(t, ctx, auth, l2client, rustFile("storage")) + matchSnake := regexp.MustCompile("_[a-z]") + + for _, tc := range []struct { + hostio string + opcode vm.OpCode + params []any + maxDiff float64 + }{ + {hostio: "exit_early", opcode: vm.STOP}, + {hostio: "transient_load_bytes32", opcode: vm.TLOAD, params: []any{common.HexToHash("dead")}}, + {hostio: "transient_store_bytes32", opcode: vm.TSTORE, params: []any{common.HexToHash("dead"), common.HexToHash("beef")}}, + {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.0}, + {hostio: "account_balance", opcode: vm.BALANCE, params: []any{l2info.GetAddress("Owner")}}, + {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}, maxDiff: 2}, + {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}}, + {hostio: "account_codehash", opcode: vm.EXTCODEHASH, params: []any{otherProgram}}, + {hostio: "evm_gas_left", opcode: vm.GAS, maxDiff: 1.0}, + {hostio: "evm_ink_left", opcode: vm.GAS, maxDiff: 1.0}, + {hostio: "block_basefee", opcode: vm.BASEFEE, maxDiff: 0.5}, + {hostio: "chainid", opcode: vm.CHAINID, maxDiff: 1.0}, + {hostio: "block_coinbase", opcode: vm.COINBASE, maxDiff: 0.5}, + {hostio: "block_gas_limit", opcode: vm.GASLIMIT, maxDiff: 1.0}, + {hostio: "block_number", opcode: vm.NUMBER, maxDiff: 1.0}, + {hostio: "block_timestamp", opcode: vm.TIMESTAMP, maxDiff: 1.0}, + {hostio: "contract_address", opcode: vm.ADDRESS, maxDiff: 0.5}, + {hostio: "math_div", opcode: vm.DIV, params: []any{big.NewInt(1), big.NewInt(3)}}, + {hostio: "math_mod", opcode: vm.MOD, params: []any{big.NewInt(1), big.NewInt(3)}}, + {hostio: "math_pow", opcode: vm.EXP, params: []any{big.NewInt(1), new(big.Int).Lsh(big.NewInt(1), 255)}, maxDiff: 2}, // worst case + {hostio: "math_add_mod", opcode: vm.ADDMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, + {hostio: "math_mul_mod", opcode: vm.MULMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, + {hostio: "msg_sender", opcode: vm.CALLER, maxDiff: 0.5}, + {hostio: "msg_value", opcode: vm.CALLVALUE, maxDiff: 0.5}, + {hostio: "tx_gas_price", opcode: vm.GASPRICE, maxDiff: 0.5}, + {hostio: "tx_ink_price", opcode: vm.GASPRICE, maxDiff: 1.0}, + {hostio: "tx_origin", opcode: vm.ORIGIN, maxDiff: 0.5}, + } { + t.Run(tc.hostio, func(t *testing.T) { + solFunc := matchSnake.ReplaceAllStringFunc(tc.hostio, func(s string) string { + return strings.ToUpper(strings.TrimPrefix(s, "_")) + }) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, solFunc) + data, err := packer(tc.params...) + Require(t, err) + compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, + compareGasForEach, tc.maxDiff, compareGasPair{tc.opcode, tc.hostio}) + }) + } +} + +func TestProgramStorageCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + + stylusMulticall := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, ctx, auth, l2client, mocksgen.MultiCallTestMetaData) + + const numSlots = 42 + rander := testhelpers.NewPseudoRandomDataSource(t, 0) + readData := multicallEmptyArgs() + writeRandAData := multicallEmptyArgs() + writeRandBData := multicallEmptyArgs() + writeZeroData := multicallEmptyArgs() + for i := 0; i < numSlots; i++ { + slot := rander.GetHash() + readData = multicallAppendLoad(readData, slot, false) + writeRandAData = multicallAppendStore(writeRandAData, slot, rander.GetHash(), false) + writeRandBData = multicallAppendStore(writeRandBData, slot, rander.GetHash(), false) + writeZeroData = multicallAppendStore(writeZeroData, slot, common.Hash{}, false) + } + + for _, tc := range []struct { + name string + data []byte + }{ + {"initialWrite", writeRandAData}, + {"read", readData}, + {"writeAgain", writeRandBData}, + {"delete", writeZeroData}, + {"readZeros", readData}, + {"writeAgainAgain", writeRandAData}, + } { + t.Run(tc.name, func(t *testing.T) { + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, tc.data, nil, + compareGasSum, 0, compareGasPair{vm.SSTORE, "storage_flush_cache"}, + compareGasPair{vm.SLOAD, "storage_load_bytes32"}) + }) + } +} + +func TestProgramLogCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + + stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "emitLog") + + for ntopics := int8(0); ntopics < 5; ntopics++ { + for _, dataSize := range []uint64{10, 100, 1000} { + name := fmt.Sprintf("emitLog%dData%d", ntopics, dataSize) + t.Run(name, func(t *testing.T) { + args := []any{ + testhelpers.RandomSlice(dataSize), + ntopics, + } + for t := 0; t < 4; t++ { + args = append(args, testhelpers.RandomHash()) + } + data, err := packer(args...) + Require(t, err) + opcode := vm.LOG0 + vm.OpCode(ntopics) + compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, + compareGasForEach, 0, compareGasPair{opcode, "emit_log"}) + }) + } + } + +} + +func TestProgramCallCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + + stylusMulticall := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, ctx, auth, l2client, mocksgen.MultiCallTestMetaData) + otherStylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) + otherEvmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "msgValue") + otherData, err := packer() + Require(t, err) + + for _, pair := range []compareGasPair{ + {vm.CALL, "call_contract"}, + {vm.DELEGATECALL, "delegate_call_contract"}, + {vm.STATICCALL, "static_call_contract"}, + } { + t.Run(pair.hostio+"/burnGas", func(t *testing.T) { + arbTest := common.HexToAddress("0x0000000000000000000000000000000000000069") + burnArbGas, _ := util.NewCallParser(precompilesgen.ArbosTestABI, "burnArbGas") + burnData, err := burnArbGas(big.NewInt(0)) + Require(t, err) + data := argsForMulticall(pair.opcode, arbTest, nil, burnData) + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + }) + + t.Run(pair.hostio+"/evmContract", func(t *testing.T) { + data := argsForMulticall(pair.opcode, otherEvmProgram, nil, otherData) + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data + }) + + t.Run(pair.hostio+"/stylusContract", func(t *testing.T) { + data := argsForMulticall(pair.opcode, otherStylusProgram, nil, otherData) + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data + }) + + t.Run(pair.hostio+"/multipleTimes", func(t *testing.T) { + data := multicallEmptyArgs() + for i := 0; i < 9; i++ { + data = multicallAppend(data, pair.opcode, otherEvmProgram, otherData) + } + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + }) + } + + t.Run("call_contract/evmContractWithValue", func(t *testing.T) { + value := big.NewInt(1000) + data := argsForMulticall(vm.CALL, otherEvmProgram, value, otherData) + compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, value, compareGasForEach, 0, compareGasPair{vm.CALL, "call_contract"}) + }) +} + +func TestProgramCreateCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + + stylusCreate := deployWasm(t, ctx, auth, l2client, rustFile("create")) + evmCreate := deployEvmContract(t, ctx, auth, l2client, mocksgen.CreateTestMetaData) + deployCode := common.FromHex(mocksgen.ProgramTestMetaData.Bin) + + t.Run("create1", func(t *testing.T) { + data := []byte{0x01} + data = append(data, (common.Hash{}).Bytes()...) // endowment + data = append(data, deployCode...) + compareGasUsage(t, ctx, l2client, l2info, evmCreate, stylusCreate, data, nil, + compareGasForEach, 0, compareGasPair{vm.CREATE, "create1"}) + }) + + t.Run("create2", func(t *testing.T) { + data := []byte{0x02} + data = append(data, (common.Hash{}).Bytes()...) // endowment + data = append(data, (common.HexToHash("beef")).Bytes()...) // salt + data = append(data, deployCode...) + compareGasUsage(t, ctx, l2client, l2info, evmCreate, stylusCreate, data, nil, + compareGasForEach, 0, compareGasPair{vm.CREATE2, "create2"}) + }) +} + +func TestProgramKeccakCost(t *testing.T) { + ctx, l2info, l2client, auth := setupGasCostTest(t) + + stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "keccak") + + for i := 1; i < 5; i++ { + size := uint64(math.Pow10(i)) + name := fmt.Sprintf("keccak%d", size) + t.Run(name, func(t *testing.T) { + preImage := testhelpers.RandomSlice(size) + preImage[len(preImage)-1] = 0 + data, err := packer(preImage) + Require(t, err) + const maxDiff = 1.1 + compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, + compareGasForEach, maxDiff, compareGasPair{vm.KECCAK256, "native_keccak256"}) + }) + } +} + +func setupGasCostTest(t *testing.T) (ctx context.Context, l2info *BlockchainTestInfo, l2client *ethclient.Client, auth bind.TransactOpts) { + ctx, cancel := context.WithCancel(context.Background()) + t.Cleanup(cancel) + builder := NewNodeBuilder(ctx).DefaultConfig(t, true) + cleanup := builder.Build(t) + t.Cleanup(cleanup) + l2info = builder.L2Info + l2client = builder.L2.Client + auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) + return +} + +// deployEvmContract deploys an Evm contract and return its address. +func deployEvmContract(t *testing.T, ctx context.Context, auth bind.TransactOpts, client *ethclient.Client, metadata *bind.MetaData) common.Address { + t.Helper() + parsed, err := metadata.GetAbi() + Require(t, err) + address, tx, _, err := bind.DeployContract(&auth, *parsed, common.FromHex(metadata.Bin), client) + Require(t, err) + _, err = EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + return address +} + +type compareGasPair struct { + opcode vm.OpCode + hostio string +} + +type compareGasMode int + +const ( + compareGasForEach compareGasMode = iota + compareGasSum +) + +// compareGasUsage calls an EVM and a Wasm contract passing the same data and the same value. +// Then, it ensures the given opcodes and hostios cost roughly the same amount of gas. +func compareGasUsage( + t *testing.T, + ctx context.Context, + client *ethclient.Client, + info *BlockchainTestInfo, + evmContract common.Address, + stylusContract common.Address, + txData []byte, + txValue *big.Int, + mode compareGasMode, + maxAllowedDifference float64, + pairs ...compareGasPair, +) { + if evmContract == stylusContract { + Fatal(t, "evm and stylus contract are the same") + } + + const txGas uint64 = 32_000_000 + tx := info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData) + evmGas := sendAndEnsureTransaction(t, ctx, client, tx) + evmGasUsage, err := evmOpcodesGasUsage(ctx, client.Client(), tx) + Require(t, err) + + tx = info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData) + stylusGas := sendAndEnsureTransaction(t, ctx, client, tx) + stylusGasUsage, err := stylusHostiosGasUsage(ctx, client.Client(), tx) + Require(t, err) + + t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) + + for i := range pairs { + opcode := pairs[i].opcode + hostio := pairs[i].hostio + switch mode { + case compareGasForEach: + if len(evmGasUsage[opcode]) != len(stylusGasUsage[hostio]) { + Fatal(t, "mismatch between hostios and opcodes", evmGasUsage, stylusGasUsage) + } + for i := range evmGasUsage[opcode] { + opcodeGas := evmGasUsage[opcode][i] + hostioGas := stylusGasUsage[hostio][i] + t.Logf("evm %v usage: %v - stylus %v usage: %v", opcode, opcodeGas, hostio, hostioGas) + checkPercentDiff(t, float64(opcodeGas), hostioGas, maxAllowedDifference) + } + case compareGasSum: + evmSum := float64(0) + stylusSum := float64(0) + for i := range evmGasUsage[opcode] { + evmSum += float64(evmGasUsage[opcode][i]) + stylusSum += stylusGasUsage[hostio][i] + } + t.Logf("evm %v usage: %v - stylus %v usage: %v", opcode, evmSum, hostio, stylusSum) + checkPercentDiff(t, evmSum, stylusSum, maxAllowedDifference) + } + } +} + +func evmOpcodesGasUsage(ctx context.Context, rpcClient rpc.ClientInterface, tx *types.Transaction) ( + map[vm.OpCode][]uint64, error) { + + var result logger.ExecutionResult + err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), nil) + if err != nil { + return nil, fmt.Errorf("failed to trace evm call: %w", err) + } + + gasUsage := map[vm.OpCode][]uint64{} + for i := range result.StructLogs { + op := vm.StringToOp(result.StructLogs[i].Op) + gasUsed := uint64(0) + if op == vm.CALL || op == vm.STATICCALL || op == vm.DELEGATECALL || op == vm.CREATE || op == vm.CREATE2 { + // For the CALL* opcodes, the GasCost in the tracer represents the gas sent + // to the callee contract, which is 63/64 of the remaining gas. This happens + // because the tracer is evaluated before the call is executed, so the EVM + // doesn't know how much gas will being used. + // + // In the case of the Stylus tracer, the trace is emitted after the + // execution, so the EndInk field is set to the ink after the call returned. + // Hence, it also includes the ink spent by the callee contract. + // + // To make a precise comparison between the EVM and Stylus, we modify the + // EVM measurement to include the gas spent by the callee contract. To do + // so, we go through the opcodes after CALL until we find the first opcode + // in the caller's depth. Then, we subtract the gas before the call by the + // gas after the call returned. + var gasAfterCall uint64 + for j := i + 1; j < len(result.StructLogs); j++ { + if result.StructLogs[j].Depth == result.StructLogs[i].Depth { + // back to the original call + gasAfterCall = result.StructLogs[j].Gas + result.StructLogs[j].GasCost + break + } + } + if gasAfterCall == 0 { + return nil, fmt.Errorf("malformed log: didn't get back to call original depth") + } + if i == 0 { + return nil, fmt.Errorf("malformed log: call is first opcode") + } + gasUsed = result.StructLogs[i-1].Gas - gasAfterCall + } else { + gasUsed = result.StructLogs[i].GasCost + } + gasUsage[op] = append(gasUsage[op], gasUsed) + } + return gasUsage, nil +} + +func stylusHostiosGasUsage(ctx context.Context, rpcClient rpc.ClientInterface, tx *types.Transaction) ( + map[string][]float64, error) { + + traceOpts := struct { + Tracer string `json:"tracer"` + }{ + Tracer: "stylusTracer", + } + var result []gethexec.HostioTraceInfo + err := rpcClient.CallContext(ctx, &result, "debug_traceTransaction", tx.Hash(), traceOpts) + if err != nil { + return nil, fmt.Errorf("failed to trace stylus call: %w", err) + } + + const InkPerGas = 10000 + gasUsage := map[string][]float64{} + for _, hostioLog := range result { + gasCost := float64(hostioLog.StartInk-hostioLog.EndInk) / InkPerGas + gasUsage[hostioLog.Name] = append(gasUsage[hostioLog.Name], gasCost) + } + return gasUsage, nil +} + +// checkPercentDiff checks whether the two values are close enough. +func checkPercentDiff(t *testing.T, a, b float64, maxAllowedDifference float64) { + t.Helper() + if maxAllowedDifference == 0 { + maxAllowedDifference = 0.25 + } + percentageDifference := math.Abs(a-b) / ((a + b) / 2) + if percentageDifference > maxAllowedDifference { + Fatal(t, fmt.Sprintf("gas usages are too different; got %v, max allowed is %v", percentageDifference, maxAllowedDifference)) + } +} + +// sendAndEnsureTransaction sends a transaction, ensures it succeed, and returns the total gas cost. +func sendAndEnsureTransaction(t *testing.T, ctx context.Context, client *ethclient.Client, tx *types.Transaction) uint64 { + t.Helper() + err := client.SendTransaction(ctx, tx) + Require(t, err) + receipt, err := EnsureTxSucceeded(ctx, client, tx) + Require(t, err) + return receipt.GasUsedForL2() +} From a528c6e025984fb14dfebf5e6b68eeb7416c89ed Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Fri, 27 Sep 2024 15:47:50 -0300 Subject: [PATCH 2/8] Improve account_code hostio gas test --- arbitrator/stylus/tests/hostio-test/src/main.rs | 8 +++++++- contracts | 2 +- system_tests/program_gas_test.go | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/arbitrator/stylus/tests/hostio-test/src/main.rs b/arbitrator/stylus/tests/hostio-test/src/main.rs index 5aa465598e..17a5d10266 100644 --- a/arbitrator/stylus/tests/hostio-test/src/main.rs +++ b/arbitrator/stylus/tests/hostio-test/src/main.rs @@ -77,7 +77,13 @@ impl HostioTest { } fn account_code(account: Address) -> Result> { - Ok(account.code()) + let mut size = 10000; + let mut code = vec![0; size]; + unsafe { + size = hostio::account_code(account.as_ptr(), 0, size, code.as_mut_ptr()); + } + code.resize(size, 0); + Ok(code) } fn account_code_size(account: Address) -> Result { diff --git a/contracts b/contracts index cde7f96486..b5fd6ecbaa 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit cde7f96486cc21befc9a05fc3b27a7d6ebb5d5f8 +Subproject commit b5fd6ecbaa0bc37f946f07278c4c543d992b095b diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go index 48db268907..cdc75345cb 100644 --- a/system_tests/program_gas_test.go +++ b/system_tests/program_gas_test.go @@ -41,7 +41,7 @@ func TestProgramSimpleCost(t *testing.T) { {hostio: "transient_store_bytes32", opcode: vm.TSTORE, params: []any{common.HexToHash("dead"), common.HexToHash("beef")}}, {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.0}, {hostio: "account_balance", opcode: vm.BALANCE, params: []any{l2info.GetAddress("Owner")}}, - {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}, maxDiff: 2}, + {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}}, {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}}, {hostio: "account_codehash", opcode: vm.EXTCODEHASH, params: []any{otherProgram}}, {hostio: "evm_gas_left", opcode: vm.GAS, maxDiff: 1.0}, From 72879b79d732f74403a286ebd63c6fbe31ccf292 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Mon, 30 Sep 2024 14:34:22 -0300 Subject: [PATCH 3/8] Update nitro contracts --- contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts b/contracts index b5fd6ecbaa..6740300fc1 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit b5fd6ecbaa0bc37f946f07278c4c543d992b095b +Subproject commit 6740300fc1da9c1d67764068463ed96d7ca9ec2c From 8b1e45baa1f1689e1cd2fb44831813715e4ed025 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Thu, 3 Oct 2024 10:57:18 -0300 Subject: [PATCH 4/8] Set contracts submodule to develop --- contracts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/contracts b/contracts index 6740300fc1..b140ed63ac 160000 --- a/contracts +++ b/contracts @@ -1 +1 @@ -Subproject commit 6740300fc1da9c1d67764068463ed96d7ca9ec2c +Subproject commit b140ed63acdb53cb906ffd1fa3c36fdbd474364e From 7b0fe6d1c8bcb5e36f445d49243d073d7b146378 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Thu, 3 Oct 2024 11:23:03 -0300 Subject: [PATCH 5/8] Use builder variable instead of creating aliases --- system_tests/program_gas_test.go | 108 ++++++++++++++----------------- 1 file changed, 49 insertions(+), 59 deletions(-) diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go index cdc75345cb..063a21d3b0 100644 --- a/system_tests/program_gas_test.go +++ b/system_tests/program_gas_test.go @@ -24,10 +24,11 @@ import ( ) func TestProgramSimpleCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) - evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) - otherProgram := deployWasm(t, ctx, auth, l2client, rustFile("storage")) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + otherProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("storage")) matchSnake := regexp.MustCompile("_[a-z]") for _, tc := range []struct { @@ -40,7 +41,7 @@ func TestProgramSimpleCost(t *testing.T) { {hostio: "transient_load_bytes32", opcode: vm.TLOAD, params: []any{common.HexToHash("dead")}}, {hostio: "transient_store_bytes32", opcode: vm.TSTORE, params: []any{common.HexToHash("dead"), common.HexToHash("beef")}}, {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.0}, - {hostio: "account_balance", opcode: vm.BALANCE, params: []any{l2info.GetAddress("Owner")}}, + {hostio: "account_balance", opcode: vm.BALANCE, params: []any{builder.L2Info.GetAddress("Owner")}}, {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}}, {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}}, {hostio: "account_codehash", opcode: vm.EXTCODEHASH, params: []any{otherProgram}}, @@ -71,17 +72,16 @@ func TestProgramSimpleCost(t *testing.T) { packer, _ := util.NewCallParser(mocksgen.HostioTestABI, solFunc) data, err := packer(tc.params...) Require(t, err) - compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, - compareGasForEach, tc.maxDiff, compareGasPair{tc.opcode, tc.hostio}) + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, tc.maxDiff, compareGasPair{tc.opcode, tc.hostio}) }) } } func TestProgramStorageCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - - stylusMulticall := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) - evmMulticall := deployEvmContract(t, ctx, auth, l2client, mocksgen.MultiCallTestMetaData) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusMulticall := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.MultiCallTestMetaData) const numSlots = 42 rander := testhelpers.NewPseudoRandomDataSource(t, 0) @@ -109,18 +109,17 @@ func TestProgramStorageCost(t *testing.T) { {"writeAgainAgain", writeRandAData}, } { t.Run(tc.name, func(t *testing.T) { - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, tc.data, nil, - compareGasSum, 0, compareGasPair{vm.SSTORE, "storage_flush_cache"}, - compareGasPair{vm.SLOAD, "storage_load_bytes32"}) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, tc.data, nil, compareGasSum, 0, + compareGasPair{vm.SSTORE, "storage_flush_cache"}, compareGasPair{vm.SLOAD, "storage_load_bytes32"}) }) } } func TestProgramLogCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - - stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) - evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "emitLog") for ntopics := int8(0); ntopics < 5; ntopics++ { @@ -137,8 +136,7 @@ func TestProgramLogCost(t *testing.T) { data, err := packer(args...) Require(t, err) opcode := vm.LOG0 + vm.OpCode(ntopics) - compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, - compareGasForEach, 0, compareGasPair{opcode, "emit_log"}) + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, 0, compareGasPair{opcode, "emit_log"}) }) } } @@ -146,12 +144,12 @@ func TestProgramLogCost(t *testing.T) { } func TestProgramCallCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - - stylusMulticall := deployWasm(t, ctx, auth, l2client, rustFile("multicall")) - evmMulticall := deployEvmContract(t, ctx, auth, l2client, mocksgen.MultiCallTestMetaData) - otherStylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) - otherEvmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusMulticall := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("multicall")) + evmMulticall := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.MultiCallTestMetaData) + otherStylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + otherEvmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "msgValue") otherData, err := packer() Require(t, err) @@ -167,18 +165,18 @@ func TestProgramCallCost(t *testing.T) { burnData, err := burnArbGas(big.NewInt(0)) Require(t, err) data := argsForMulticall(pair.opcode, arbTest, nil, burnData) - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) }) t.Run(pair.hostio+"/evmContract", func(t *testing.T) { data := argsForMulticall(pair.opcode, otherEvmProgram, nil, otherData) - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data }) t.Run(pair.hostio+"/stylusContract", func(t *testing.T) { data := argsForMulticall(pair.opcode, otherStylusProgram, nil, otherData) - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair, compareGasPair{vm.RETURNDATACOPY, "read_return_data"}) // also test read_return_data }) @@ -187,30 +185,29 @@ func TestProgramCallCost(t *testing.T) { for i := 0; i < 9; i++ { data = multicallAppend(data, pair.opcode, otherEvmProgram, otherData) } - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, nil, compareGasForEach, 0, pair) }) } t.Run("call_contract/evmContractWithValue", func(t *testing.T) { value := big.NewInt(1000) data := argsForMulticall(vm.CALL, otherEvmProgram, value, otherData) - compareGasUsage(t, ctx, l2client, l2info, evmMulticall, stylusMulticall, data, value, compareGasForEach, 0, compareGasPair{vm.CALL, "call_contract"}) + compareGasUsage(t, builder, evmMulticall, stylusMulticall, data, value, compareGasForEach, 0, compareGasPair{vm.CALL, "call_contract"}) }) } func TestProgramCreateCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - - stylusCreate := deployWasm(t, ctx, auth, l2client, rustFile("create")) - evmCreate := deployEvmContract(t, ctx, auth, l2client, mocksgen.CreateTestMetaData) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusCreate := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("create")) + evmCreate := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.CreateTestMetaData) deployCode := common.FromHex(mocksgen.ProgramTestMetaData.Bin) t.Run("create1", func(t *testing.T) { data := []byte{0x01} data = append(data, (common.Hash{}).Bytes()...) // endowment data = append(data, deployCode...) - compareGasUsage(t, ctx, l2client, l2info, evmCreate, stylusCreate, data, nil, - compareGasForEach, 0, compareGasPair{vm.CREATE, "create1"}) + compareGasUsage(t, builder, evmCreate, stylusCreate, data, nil, compareGasForEach, 0, compareGasPair{vm.CREATE, "create1"}) }) t.Run("create2", func(t *testing.T) { @@ -218,16 +215,15 @@ func TestProgramCreateCost(t *testing.T) { data = append(data, (common.Hash{}).Bytes()...) // endowment data = append(data, (common.HexToHash("beef")).Bytes()...) // salt data = append(data, deployCode...) - compareGasUsage(t, ctx, l2client, l2info, evmCreate, stylusCreate, data, nil, - compareGasForEach, 0, compareGasPair{vm.CREATE2, "create2"}) + compareGasUsage(t, builder, evmCreate, stylusCreate, data, nil, compareGasForEach, 0, compareGasPair{vm.CREATE2, "create2"}) }) } func TestProgramKeccakCost(t *testing.T) { - ctx, l2info, l2client, auth := setupGasCostTest(t) - - stylusProgram := deployWasm(t, ctx, auth, l2client, rustFile("hostio-test")) - evmProgram := deployEvmContract(t, ctx, auth, l2client, mocksgen.HostioTestMetaData) + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "keccak") for i := 1; i < 5; i++ { @@ -239,22 +235,18 @@ func TestProgramKeccakCost(t *testing.T) { data, err := packer(preImage) Require(t, err) const maxDiff = 1.1 - compareGasUsage(t, ctx, l2client, l2info, evmProgram, stylusProgram, data, nil, - compareGasForEach, maxDiff, compareGasPair{vm.KECCAK256, "native_keccak256"}) + compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, maxDiff, compareGasPair{vm.KECCAK256, "native_keccak256"}) }) } } -func setupGasCostTest(t *testing.T) (ctx context.Context, l2info *BlockchainTestInfo, l2client *ethclient.Client, auth bind.TransactOpts) { +func setupGasCostTest(t *testing.T) *NodeBuilder { ctx, cancel := context.WithCancel(context.Background()) t.Cleanup(cancel) builder := NewNodeBuilder(ctx).DefaultConfig(t, true) cleanup := builder.Build(t) t.Cleanup(cleanup) - l2info = builder.L2Info - l2client = builder.L2.Client - auth = builder.L2Info.GetDefaultTransactOpts("Owner", ctx) - return + return builder } // deployEvmContract deploys an Evm contract and return its address. @@ -285,9 +277,7 @@ const ( // Then, it ensures the given opcodes and hostios cost roughly the same amount of gas. func compareGasUsage( t *testing.T, - ctx context.Context, - client *ethclient.Client, - info *BlockchainTestInfo, + builder *NodeBuilder, evmContract common.Address, stylusContract common.Address, txData []byte, @@ -301,14 +291,14 @@ func compareGasUsage( } const txGas uint64 = 32_000_000 - tx := info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData) - evmGas := sendAndEnsureTransaction(t, ctx, client, tx) - evmGasUsage, err := evmOpcodesGasUsage(ctx, client.Client(), tx) + tx := builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData) + evmGas := sendAndEnsureTransaction(t, builder.ctx, builder.L2.Client, tx) + evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), tx) Require(t, err) - tx = info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData) - stylusGas := sendAndEnsureTransaction(t, ctx, client, tx) - stylusGasUsage, err := stylusHostiosGasUsage(ctx, client.Client(), tx) + tx = builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData) + stylusGas := sendAndEnsureTransaction(t, builder.ctx, builder.L2.Client, tx) + stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), tx) Require(t, err) t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) From 103ad5b8df9dc2e46b4cea8a456c4b47d7872adc Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Thu, 3 Oct 2024 11:38:46 -0300 Subject: [PATCH 6/8] Replace util function with existing TestClient one --- system_tests/common_test.go | 13 ++++++++----- system_tests/program_gas_test.go | 26 ++++++++++---------------- 2 files changed, 18 insertions(+), 21 deletions(-) diff --git a/system_tests/common_test.go b/system_tests/common_test.go index 67bc01c92d..93c38b5eae 100644 --- a/system_tests/common_test.go +++ b/system_tests/common_test.go @@ -138,8 +138,8 @@ func (tc *TestClient) GetBaseFeeAt(t *testing.T, blockNum *big.Int) *big.Int { return GetBaseFeeAt(t, tc.Client, tc.ctx, blockNum) } -func (tc *TestClient) SendWaitTestTransactions(t *testing.T, txs []*types.Transaction) { - SendWaitTestTransactions(t, tc.ctx, tc.Client, txs) +func (tc *TestClient) SendWaitTestTransactions(t *testing.T, txs []*types.Transaction) []*types.Receipt { + return SendWaitTestTransactions(t, tc.ctx, tc.Client, txs) } func (tc *TestClient) DeploySimple(t *testing.T, auth bind.TransactOpts) (common.Address, *mocksgen.Simple) { @@ -763,15 +763,18 @@ func (b *NodeBuilder) BridgeBalance(t *testing.T, account string, amount *big.In return BridgeBalance(t, account, amount, b.L1Info, b.L2Info, b.L1.Client, b.L2.Client, b.ctx) } -func SendWaitTestTransactions(t *testing.T, ctx context.Context, client *ethclient.Client, txs []*types.Transaction) { +func SendWaitTestTransactions(t *testing.T, ctx context.Context, client *ethclient.Client, txs []*types.Transaction) []*types.Receipt { t.Helper() + receipts := make([]*types.Receipt, len(txs)) for _, tx := range txs { Require(t, client.SendTransaction(ctx, tx)) } - for _, tx := range txs { - _, err := EnsureTxSucceeded(ctx, client, tx) + for i, tx := range txs { + var err error + receipts[i], err = EnsureTxSucceeded(ctx, client, tx) Require(t, err) } + return receipts } func TransferBalance( diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go index 063a21d3b0..fdcd53eeac 100644 --- a/system_tests/program_gas_test.go +++ b/system_tests/program_gas_test.go @@ -291,14 +291,18 @@ func compareGasUsage( } const txGas uint64 = 32_000_000 - tx := builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData) - evmGas := sendAndEnsureTransaction(t, builder.ctx, builder.L2.Client, tx) - evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), tx) + txs := []*types.Transaction{ + builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData), + builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData), + } + receipts := builder.L2.SendWaitTestTransactions(t, txs) + + evmGas := receipts[0].GasUsedForL2() + evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), txs[0]) Require(t, err) - tx = builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData) - stylusGas := sendAndEnsureTransaction(t, builder.ctx, builder.L2.Client, tx) - stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), tx) + stylusGas := receipts[1].GasUsedForL2() + stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), txs[1]) Require(t, err) t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) @@ -415,13 +419,3 @@ func checkPercentDiff(t *testing.T, a, b float64, maxAllowedDifference float64) Fatal(t, fmt.Sprintf("gas usages are too different; got %v, max allowed is %v", percentageDifference, maxAllowedDifference)) } } - -// sendAndEnsureTransaction sends a transaction, ensures it succeed, and returns the total gas cost. -func sendAndEnsureTransaction(t *testing.T, ctx context.Context, client *ethclient.Client, tx *types.Transaction) uint64 { - t.Helper() - err := client.SendTransaction(ctx, tx) - Require(t, err) - receipt, err := EnsureTxSucceeded(ctx, client, tx) - Require(t, err) - return receipt.GasUsedForL2() -} From b2b54f4fcc0597cfdd98f7de5fb0f7b7ca3a58a6 Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Thu, 3 Oct 2024 15:38:25 -0300 Subject: [PATCH 7/8] Move math_pow gas test to a separate function --- system_tests/program_gas_test.go | 79 +++++++++++++++++++++++--------- 1 file changed, 58 insertions(+), 21 deletions(-) diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go index fdcd53eeac..e434d72f2e 100644 --- a/system_tests/program_gas_test.go +++ b/system_tests/program_gas_test.go @@ -56,7 +56,6 @@ func TestProgramSimpleCost(t *testing.T) { {hostio: "contract_address", opcode: vm.ADDRESS, maxDiff: 0.5}, {hostio: "math_div", opcode: vm.DIV, params: []any{big.NewInt(1), big.NewInt(3)}}, {hostio: "math_mod", opcode: vm.MOD, params: []any{big.NewInt(1), big.NewInt(3)}}, - {hostio: "math_pow", opcode: vm.EXP, params: []any{big.NewInt(1), new(big.Int).Lsh(big.NewInt(1), 255)}, maxDiff: 2}, // worst case {hostio: "math_add_mod", opcode: vm.ADDMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, {hostio: "math_mul_mod", opcode: vm.MULMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, {hostio: "msg_sender", opcode: vm.CALLER, maxDiff: 0.5}, @@ -77,6 +76,32 @@ func TestProgramSimpleCost(t *testing.T) { } } +func TestProgramPowCost(t *testing.T) { + builder := setupGasCostTest(t) + auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) + stylusProgram := deployWasm(t, builder.ctx, auth, builder.L2.Client, rustFile("hostio-test")) + evmProgram := deployEvmContract(t, builder.ctx, auth, builder.L2.Client, mocksgen.HostioTestMetaData) + packer, _ := util.NewCallParser(mocksgen.HostioTestABI, "mathPow") + + for _, exponentNumBytes := range []uint{1, 2, 10, 32} { + name := fmt.Sprintf("exponentNumBytes%v", exponentNumBytes) + t.Run(name, func(t *testing.T) { + exponent := new(big.Int).Lsh(big.NewInt(1), exponentNumBytes*8-1) + params := []any{big.NewInt(1), exponent} + data, err := packer(params...) + Require(t, err) + evmGasUsage, stylusGasUsage := measureGasUsage(t, builder, evmProgram, stylusProgram, data, nil) + expectedGas := 2.652 + 1.75*float64(exponentNumBytes+1) + t.Logf("evm EXP usage: %v - stylus math_pow usage: %v - expected math_pow usage: %v", + evmGasUsage[vm.EXP][0], stylusGasUsage["math_pow"][0], expectedGas) + // The math_pow HostIO uses significally less gas than the EXP opcode. So, + // instead of comparing it to EVM, we compare it to the expected gas usage + // for each test case. + checkPercentDiff(t, stylusGasUsage["math_pow"][0], expectedGas, 0.001) + }) + } +} + func TestProgramStorageCost(t *testing.T) { builder := setupGasCostTest(t) auth := builder.L2Info.GetDefaultTransactOpts("Owner", builder.ctx) @@ -261,6 +286,35 @@ func deployEvmContract(t *testing.T, ctx context.Context, auth bind.TransactOpts return address } +// measureGasUsage calls an EVM and a Wasm contract passing the same data and the same value. +func measureGasUsage( + t *testing.T, + builder *NodeBuilder, + evmContract common.Address, + stylusContract common.Address, + txData []byte, + txValue *big.Int, +) (map[vm.OpCode][]uint64, map[string][]float64) { + const txGas uint64 = 32_000_000 + txs := []*types.Transaction{ + builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData), + builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData), + } + receipts := builder.L2.SendWaitTestTransactions(t, txs) + + evmGas := receipts[0].GasUsedForL2() + evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), txs[0]) + Require(t, err) + + stylusGas := receipts[1].GasUsedForL2() + stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), txs[1]) + Require(t, err) + + t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) + + return evmGasUsage, stylusGasUsage +} + type compareGasPair struct { opcode vm.OpCode hostio string @@ -273,8 +327,8 @@ const ( compareGasSum ) -// compareGasUsage calls an EVM and a Wasm contract passing the same data and the same value. -// Then, it ensures the given opcodes and hostios cost roughly the same amount of gas. +// compareGasUsage calls measureGasUsage and then it ensures the given opcodes and hostios cost +// roughly the same amount of gas. func compareGasUsage( t *testing.T, builder *NodeBuilder, @@ -289,24 +343,7 @@ func compareGasUsage( if evmContract == stylusContract { Fatal(t, "evm and stylus contract are the same") } - - const txGas uint64 = 32_000_000 - txs := []*types.Transaction{ - builder.L2Info.PrepareTxTo("Owner", &evmContract, txGas, txValue, txData), - builder.L2Info.PrepareTxTo("Owner", &stylusContract, txGas, txValue, txData), - } - receipts := builder.L2.SendWaitTestTransactions(t, txs) - - evmGas := receipts[0].GasUsedForL2() - evmGasUsage, err := evmOpcodesGasUsage(builder.ctx, builder.L2.Client.Client(), txs[0]) - Require(t, err) - - stylusGas := receipts[1].GasUsedForL2() - stylusGasUsage, err := stylusHostiosGasUsage(builder.ctx, builder.L2.Client.Client(), txs[1]) - Require(t, err) - - t.Logf("evm total usage: %v - stylus total usage: %v", evmGas, stylusGas) - + evmGasUsage, stylusGasUsage := measureGasUsage(t, builder, evmContract, stylusContract, txData, txValue) for i := range pairs { opcode := pairs[i].opcode hostio := pairs[i].hostio From 95732ed32d14edad339b1c6cad66bedbb8672beb Mon Sep 17 00:00:00 2001 From: Gabriel de Quadros Ligneul Date: Thu, 3 Oct 2024 16:17:28 -0300 Subject: [PATCH 8/8] Make percentDiff formula easier to understand --- system_tests/program_gas_test.go | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/system_tests/program_gas_test.go b/system_tests/program_gas_test.go index e434d72f2e..119897cbfe 100644 --- a/system_tests/program_gas_test.go +++ b/system_tests/program_gas_test.go @@ -40,28 +40,28 @@ func TestProgramSimpleCost(t *testing.T) { {hostio: "exit_early", opcode: vm.STOP}, {hostio: "transient_load_bytes32", opcode: vm.TLOAD, params: []any{common.HexToHash("dead")}}, {hostio: "transient_store_bytes32", opcode: vm.TSTORE, params: []any{common.HexToHash("dead"), common.HexToHash("beef")}}, - {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.0}, + {hostio: "return_data_size", opcode: vm.RETURNDATASIZE, maxDiff: 1.5}, {hostio: "account_balance", opcode: vm.BALANCE, params: []any{builder.L2Info.GetAddress("Owner")}}, {hostio: "account_code", opcode: vm.EXTCODECOPY, params: []any{otherProgram}}, - {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}}, + {hostio: "account_code_size", opcode: vm.EXTCODESIZE, params: []any{otherProgram}, maxDiff: 0.3}, {hostio: "account_codehash", opcode: vm.EXTCODEHASH, params: []any{otherProgram}}, - {hostio: "evm_gas_left", opcode: vm.GAS, maxDiff: 1.0}, - {hostio: "evm_ink_left", opcode: vm.GAS, maxDiff: 1.0}, + {hostio: "evm_gas_left", opcode: vm.GAS, maxDiff: 1.5}, + {hostio: "evm_ink_left", opcode: vm.GAS, maxDiff: 1.5}, {hostio: "block_basefee", opcode: vm.BASEFEE, maxDiff: 0.5}, - {hostio: "chainid", opcode: vm.CHAINID, maxDiff: 1.0}, + {hostio: "chainid", opcode: vm.CHAINID, maxDiff: 1.5}, {hostio: "block_coinbase", opcode: vm.COINBASE, maxDiff: 0.5}, - {hostio: "block_gas_limit", opcode: vm.GASLIMIT, maxDiff: 1.0}, - {hostio: "block_number", opcode: vm.NUMBER, maxDiff: 1.0}, - {hostio: "block_timestamp", opcode: vm.TIMESTAMP, maxDiff: 1.0}, + {hostio: "block_gas_limit", opcode: vm.GASLIMIT, maxDiff: 1.5}, + {hostio: "block_number", opcode: vm.NUMBER, maxDiff: 1.5}, + {hostio: "block_timestamp", opcode: vm.TIMESTAMP, maxDiff: 1.5}, {hostio: "contract_address", opcode: vm.ADDRESS, maxDiff: 0.5}, {hostio: "math_div", opcode: vm.DIV, params: []any{big.NewInt(1), big.NewInt(3)}}, {hostio: "math_mod", opcode: vm.MOD, params: []any{big.NewInt(1), big.NewInt(3)}}, - {hostio: "math_add_mod", opcode: vm.ADDMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, - {hostio: "math_mul_mod", opcode: vm.MULMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.5}, + {hostio: "math_add_mod", opcode: vm.ADDMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.7}, + {hostio: "math_mul_mod", opcode: vm.MULMOD, params: []any{big.NewInt(1), big.NewInt(3), big.NewInt(5)}, maxDiff: 0.7}, {hostio: "msg_sender", opcode: vm.CALLER, maxDiff: 0.5}, {hostio: "msg_value", opcode: vm.CALLVALUE, maxDiff: 0.5}, {hostio: "tx_gas_price", opcode: vm.GASPRICE, maxDiff: 0.5}, - {hostio: "tx_ink_price", opcode: vm.GASPRICE, maxDiff: 1.0}, + {hostio: "tx_ink_price", opcode: vm.GASPRICE, maxDiff: 1.5}, {hostio: "tx_origin", opcode: vm.ORIGIN, maxDiff: 0.5}, } { t.Run(tc.hostio, func(t *testing.T) { @@ -259,7 +259,7 @@ func TestProgramKeccakCost(t *testing.T) { preImage[len(preImage)-1] = 0 data, err := packer(preImage) Require(t, err) - const maxDiff = 1.1 + const maxDiff = 2.5 // stylus keccak charges significantly less gas compareGasUsage(t, builder, evmProgram, stylusProgram, data, nil, compareGasForEach, maxDiff, compareGasPair{vm.KECCAK256, "native_keccak256"}) }) } @@ -451,7 +451,7 @@ func checkPercentDiff(t *testing.T, a, b float64, maxAllowedDifference float64) if maxAllowedDifference == 0 { maxAllowedDifference = 0.25 } - percentageDifference := math.Abs(a-b) / ((a + b) / 2) + percentageDifference := (max(a, b) / min(a, b)) - 1 if percentageDifference > maxAllowedDifference { Fatal(t, fmt.Sprintf("gas usages are too different; got %v, max allowed is %v", percentageDifference, maxAllowedDifference)) }