From a92631d7cccef48ad43276df9724a3396391b3bd Mon Sep 17 00:00:00 2001 From: Graydon Hoare Date: Fri, 10 Jan 2025 11:34:22 -0800 Subject: [PATCH] Add wasmtime support and make wasmi optional. # Conflicts: # soroban-env-host/src/vm.rs --- Cargo.lock | 639 +++++++++++++++++- Cargo.toml | 7 + soroban-env-common/Cargo.toml | 2 + soroban-env-common/src/error.rs | 43 ++ soroban-env-common/src/lib.rs | 2 + soroban-env-common/src/val.rs | 56 ++ soroban-env-common/src/vmcaller_env.rs | 53 +- soroban-env-host/Cargo.toml | 8 +- soroban-env-host/src/budget.rs | 8 + .../src/budget/wasmtime_helper.rs | 15 + .../src/cost_runner/cost_types/vm_ops.rs | 1 + soroban-env-host/src/host.rs | 17 + soroban-env-host/src/host/error.rs | 56 ++ soroban-env-host/src/host/mem_helper.rs | 99 ++- soroban-env-host/src/lib.rs | 8 + soroban-env-host/src/vm.rs | 244 ++++++- soroban-env-host/src/vm/dispatch.rs | 190 +++++- soroban-env-host/src/vm/fuel_refillable.rs | 71 +- soroban-env-host/src/vm/func_info.rs | 10 + soroban-env-host/src/vm/module_cache.rs | 30 +- soroban-env-host/src/vm/parsed_module.rs | 61 +- 21 files changed, 1518 insertions(+), 102 deletions(-) create mode 100644 soroban-env-host/src/budget/wasmtime_helper.rs diff --git a/Cargo.lock b/Cargo.lock index 279b8109f..49dd718b9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a30b2e23b9e17a9f90641c7ab1549cd9b44f296d3ccbf309d2863cfe398a0cb" dependencies = [ - "gimli", + "gimli 0.28.0", ] [[package]] @@ -101,7 +101,7 @@ dependencies = [ "ark-std", "derivative", "hashbrown 0.13.2", - "itertools", + "itertools 0.10.5", "num-traits", "zeroize", ] @@ -118,7 +118,7 @@ dependencies = [ "ark-std", "derivative", "digest", - "itertools", + "itertools 0.10.5", "num-bigint", "num-traits", "paste", @@ -212,7 +212,7 @@ dependencies = [ "cfg-if", "libc", "miniz_oxide", - "object", + "object 0.32.1", "rustc-demangle", ] @@ -257,9 +257,9 @@ dependencies = [ [[package]] name = "bumpalo" -version = "3.13.0" +version = "3.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3e2c3daef883ecc1b5d58c15adae93470a91d425f3532ba1695849656af3fc1" +checksum = "79296716171880943b8470b5f8d03aa55eb2e645a4874bdbb28adb49162e012c" [[package]] name = "bytemuck" @@ -307,6 +307,12 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "cobs" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67ba02a97a2bd10f4b59b25c7973101c79642302776489e030cd13cdab09ed15" + [[package]] name = "colored" version = "2.0.4" @@ -315,7 +321,7 @@ checksum = "2674ec482fbc38012cf31e6c42ba0177b431a0cb6f15fe40efa5aab1bda516f6" dependencies = [ "is-terminal", "lazy_static", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -339,6 +345,103 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-bforest" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-bitset" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-codegen" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-bitset", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli 0.31.1", + "hashbrown 0.14.5", + "log", + "regalloc2", + "rustc-hash", + "serde", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "cranelift-control" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-bitset", + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "cranelift-native" +version = "0.115.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + [[package]] name = "crate-git-revision" version = "0.0.6" @@ -350,6 +453,15 @@ dependencies = [ "serde_json", ] +[[package]] +name = "crc32fast" +version = "1.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a97769d94ddab943e4510d138150169a2758b5ef3eb191a9ee688de3e23ef7b3" +dependencies = [ + "cfg-if", +] + [[package]] name = "crypto-bigint" version = "0.5.2" @@ -586,6 +698,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "embedded-io" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" + +[[package]] +name = "embedded-io" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" + [[package]] name = "equivalent" version = "1.0.1" @@ -594,12 +718,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.6" +version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c18ee0ed65a5f1f81cac6b1d213b69c35fa47d4252ad41f1486dbd8226fe36e" +checksum = "534c5cf6194dfab3db3242765c03bbe257cf92f22b38f6bc0c58d59108a820ba" dependencies = [ "libc", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -624,6 +748,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + [[package]] name = "ff" version = "0.13.0" @@ -646,6 +776,12 @@ version = "1.0.7" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" +[[package]] +name = "foldhash" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f81ec6369c545a7d40e4589b5597581fa1c441fe1cce96dd1de43159910a36a2" + [[package]] name = "generator" version = "0.8.3" @@ -689,6 +825,17 @@ version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0" +[[package]] +name = "gimli" +version = "0.31.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "07e28edb80900c19c28f1072f2e8aeca7fa06b23cd4169cefe1af5aa3260783f" +dependencies = [ + "fallible-iterator", + "indexmap 2.0.2", + "stable_deref_trait", +] + [[package]] name = "group" version = "0.13.0" @@ -717,9 +864,28 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.1" +version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" +checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" +dependencies = [ + "ahash", + "serde", +] + +[[package]] +name = "hashbrown" +version = "0.15.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3a9bfc1af68b1726ea47d3d5109de126281def866b33970e10fbab11b5dafab3" +dependencies = [ + "foldhash", +] + +[[package]] +name = "heck" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" [[package]] name = "hermit-abi" @@ -774,6 +940,12 @@ dependencies = [ "cc", ] +[[package]] +name = "id-arena" +version = "2.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "25a2bc672d1148e28034f176e01fffebb08b35768468cc954630da77a1449005" + [[package]] name = "ident_case" version = "1.0.1" @@ -798,7 +970,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.1", + "hashbrown 0.14.5", "serde", ] @@ -816,7 +988,7 @@ checksum = "cb0889898416213fab133e1d33a0e5858a48177452750691bde3666d0fdbaf8b" dependencies = [ "hermit-abi", "rustix", - "windows-sys", + "windows-sys 0.48.0", ] [[package]] @@ -828,6 +1000,15 @@ dependencies = [ "either", ] +[[package]] +name = "itertools" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" +dependencies = [ + "either", +] + [[package]] name = "itoa" version = "1.0.6" @@ -878,9 +1059,9 @@ checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" [[package]] name = "libc" -version = "0.2.150" +version = "0.2.161" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89d92a4743f9a61002fae18374ed11e7973f530cb3a3255fb354818118b2203c" +checksum = "8e9489c2807c139ffd9c1794f4af0ebe86a828db53ecdc7fea2111d0fed085d1" [[package]] name = "libm" @@ -890,9 +1071,9 @@ checksum = "4ec2a862134d2a7d32d7983ddcdd1c4923530833c9f2ea1a44fc5fa473989058" [[package]] name = "linux-raw-sys" -version = "0.4.11" +version = "0.4.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "969488b55f8ac402214f3f5fd243ebb7206cf82de60d3172994707a4bcc2b829" +checksum = "78b3ae25bc7c8c38cec158d1f2757ee79e9b3740fbc7ccf0e59e4b08d793fa89" [[package]] name = "log" @@ -922,6 +1103,15 @@ dependencies = [ "nalgebra", ] +[[package]] +name = "mach2" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19b955cdeb2a02b9117f121ce63aa52d08ade45de53e48fe6a38b39c10f6f709" +dependencies = [ + "libc", +] + [[package]] name = "matchers" version = "0.1.0" @@ -947,6 +1137,15 @@ version = "2.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f665ee40bc4a3c5590afb1e9677db74a508659dfd71e126420da8274909a0167" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix", +] + [[package]] name = "miniz_oxide" version = "0.7.1" @@ -1078,6 +1277,18 @@ dependencies = [ "memchr", ] +[[package]] +name = "object" +version = "0.36.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aedf0a2d09c573ed1d8d85b30c119153926a2b36dce0ab28322c09a117a4683e" +dependencies = [ + "crc32fast", + "hashbrown 0.15.1", + "indexmap 2.0.2", + "memchr", +] + [[package]] name = "once_cell" version = "1.19.0" @@ -1143,6 +1354,18 @@ dependencies = [ "spki", ] +[[package]] +name = "postcard" +version = "1.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5f7f0a8d620d71c457dd1d47df76bb18960378da56af4527aaa10f515eee732e" +dependencies = [ + "cobs", + "embedded-io 0.4.0", + "embedded-io 0.6.1", + "serde", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -1192,6 +1415,25 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "psm" +version = "0.1.23" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa37f80ca58604976033fae9515a8a2989fc13797d953f7c04fb8fa36a11f205" +dependencies = [ + "cc", +] + +[[package]] +name = "pulley-interpreter" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cranelift-bitset", + "log", + "sptr", +] + [[package]] name = "quote" version = "1.0.33" @@ -1237,6 +1479,19 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "60a357793950651c4ed0f3f52338f53b2f809f32d83a07f72909fa13e4c6c1e3" +[[package]] +name = "regalloc2" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "12908dbeb234370af84d0579b9f68258a0f67e201412dd9a2814e6f45b2fc0f0" +dependencies = [ + "hashbrown 0.14.5", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + [[package]] name = "regex" version = "1.10.2" @@ -1306,6 +1561,12 @@ version = "0.1.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76" +[[package]] +name = "rustc-hash" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "583034fd73374156e66797ed8e5b0d5690409c9226b22d87cb7f19821c05d152" + [[package]] name = "rustc_version" version = "0.4.0" @@ -1317,15 +1578,15 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.23" +version = "0.38.37" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ffb93593068e9babdad10e4fce47dc9b3ac25315a72a59766ffd9e9a71996a04" +checksum = "8acb788b847c24f28525660c4d7758620a7210875711f79e7f663cc152726811" dependencies = [ "bitflags", "errno", "libc", "linux-raw-sys", - "windows-sys", + "windows-sys 0.52.0", ] [[package]] @@ -1488,11 +1749,20 @@ dependencies = [ "wide", ] +[[package]] +name = "slice-group-by" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "826167069c09b99d56f31e9ae5c99049e932a98c9dc2dac47645b08dbbf76ba7" + [[package]] name = "smallvec" version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c5e1a9a646d36c3599cd173a41282daf47c44583ad367b8e6837255952e5c67" +dependencies = [ + "serde", +] [[package]] name = "soroban-bench-utils" @@ -1508,7 +1778,7 @@ dependencies = [ name = "soroban-builtin-sdk-macros" version = "22.1.3" dependencies = [ - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "syn 2.0.39", @@ -1530,7 +1800,8 @@ dependencies = [ "static_assertions", "stellar-xdr", "tracy-client", - "wasmparser", + "wasmparser 0.116.1", + "wasmtime", ] [[package]] @@ -1562,7 +1833,7 @@ dependencies = [ "hex", "hex-literal", "hmac", - "itertools", + "itertools 0.10.5", "k256", "lstsq", "more-asserts", @@ -1594,9 +1865,10 @@ dependencies = [ "textplots", "thousands", "tracy-client", - "wasm-encoder", - "wasmparser", - "wasmprinter", + "wasm-encoder 0.36.2", + "wasmparser 0.116.1", + "wasmprinter 0.2.72", + "wasmtime", "wycheproof", ] @@ -1604,7 +1876,7 @@ dependencies = [ name = "soroban-env-macros" version = "22.1.3" dependencies = [ - "itertools", + "itertools 0.10.5", "proc-macro2", "quote", "serde", @@ -1636,9 +1908,9 @@ dependencies = [ "soroban-env-common", "soroban-env-macros", "stellar-xdr", - "wasm-encoder", - "wasmparser", - "wasmprinter", + "wasm-encoder 0.36.2", + "wasmparser 0.116.1", + "wasmprinter 0.2.72", ] [[package]] @@ -1673,6 +1945,18 @@ dependencies = [ "der", ] +[[package]] +name = "sptr" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b9b39299b249ad65f3b7e96443bad61c02ca5cd3589f46cb6d610a0fd6c0d6a" + +[[package]] +name = "stable_deref_trait" +version = "1.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" + [[package]] name = "static_assertions" version = "1.1.0" @@ -1754,6 +2038,21 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61c41af27dd6d1e27b1b16b489db798443478cef1f06a660c96db617ba5de3b1" + +[[package]] +name = "termcolor" +version = "1.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755" +dependencies = [ + "winapi-util", +] + [[package]] name = "test_no_std" version = "22.1.3" @@ -1773,18 +2072,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.40" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac" +checksum = "6e3de26b0965292219b4287ff031fcba86837900fe9cd2b34ea8ad893c0953d2" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.40" +version = "1.0.55" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f" +checksum = "268026685b2be38d7103e9e507c938a1fcb3d7e6eb15e87870b617bf37b6d581" dependencies = [ "proc-macro2", "quote", @@ -1950,6 +2249,12 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85" +[[package]] +name = "unicode-xid" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" + [[package]] name = "valuable" version = "0.1.0" @@ -2031,6 +2336,16 @@ dependencies = [ "leb128", ] +[[package]] +name = "wasm-encoder" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "29cbbd772edcb8e7d524a82ee8cef8dd046fc14033796a754c3ad246d019fa54" +dependencies = [ + "leb128", + "wasmparser 0.219.1", +] + [[package]] name = "wasmi_arena" version = "0.4.0" @@ -2057,6 +2372,20 @@ dependencies = [ "semver", ] +[[package]] +name = "wasmparser" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c771866898879073c53b565a6c7b49953795159836714ac56a5befb581227c5" +dependencies = [ + "ahash", + "bitflags", + "hashbrown 0.14.5", + "indexmap 2.0.2", + "semver", + "serde", +] + [[package]] name = "wasmparser-nostd" version = "0.100.2" @@ -2073,7 +2402,184 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9aff4df0cdf1906ec040e97d78c3fc8fd26d3f8d70adaac81f07f80957b63b54" dependencies = [ "anyhow", - "wasmparser", + "wasmparser 0.116.1", +] + +[[package]] +name = "wasmprinter" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "228cdc1f30c27816da225d239ce4231f28941147d34713dee8f1fff7cb330e54" +dependencies = [ + "anyhow", + "termcolor", + "wasmparser 0.219.1", +] + +[[package]] +name = "wasmtime" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "bitflags", + "bumpalo", + "cc", + "cfg-if", + "hashbrown 0.14.5", + "indexmap 2.0.2", + "libc", + "libm", + "log", + "mach2", + "memfd", + "object 0.36.5", + "once_cell", + "paste", + "postcard", + "psm", + "pulley-interpreter", + "rustix", + "serde", + "serde_derive", + "smallvec", + "sptr", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-asm-macros", + "wasmtime-component-macro", + "wasmtime-environ", + "wasmtime-jit-icache-coherence", + "wasmtime-slab", + "wasmtime-versioned-export-macros", + "wasmtime-winch", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-component-macro" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "proc-macro2", + "quote", + "syn 2.0.39", + "wasmtime-component-util", + "wasmtime-wit-bindgen", + "wit-parser", +] + +[[package]] +name = "wasmtime-component-util" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "wasmtime-cranelift" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "gimli 0.31.1", + "itertools 0.12.1", + "log", + "object 0.36.5", + "smallvec", + "target-lexicon", + "thiserror", + "wasmparser 0.219.1", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-environ" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-bitset", + "cranelift-entity", + "gimli 0.31.1", + "indexmap 2.0.2", + "log", + "object 0.36.5", + "postcard", + "serde", + "serde_derive", + "smallvec", + "target-lexicon", + "wasm-encoder 0.219.1", + "wasmparser 0.219.1", + "wasmprinter 0.219.1", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cfg-if", + "libc", + "windows-sys 0.59.0", +] + +[[package]] +name = "wasmtime-slab" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.39", +] + +[[package]] +name = "wasmtime-winch" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "object 0.36.5", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-cranelift", + "wasmtime-environ", + "winch-codegen", +] + +[[package]] +name = "wasmtime-wit-bindgen" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "heck", + "indexmap 2.0.2", + "wit-parser", ] [[package]] @@ -2102,12 +2608,37 @@ version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" +[[package]] +name = "winapi-util" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb" +dependencies = [ + "windows-sys 0.59.0", +] + [[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 = "winch-codegen" +version = "28.0.0" +source = "git+https://github.com/bytecodealliance/wasmtime?rev=b5627a86a7740ffc732f4c22b9f0b2c66252638b#b5627a86a7740ffc732f4c22b9f0b2c66252638b" +dependencies = [ + "anyhow", + "cranelift-codegen", + "gimli 0.31.1", + "regalloc2", + "smallvec", + "target-lexicon", + "wasmparser 0.219.1", + "wasmtime-cranelift", + "wasmtime-environ", +] + [[package]] name = "windows" version = "0.58.0" @@ -2190,6 +2721,24 @@ 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.6", +] + +[[package]] +name = "windows-sys" +version = "0.59.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" +dependencies = [ + "windows-targets 0.52.6", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -2320,6 +2869,24 @@ dependencies = [ "memchr", ] +[[package]] +name = "wit-parser" +version = "0.219.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4a86f669283257e8e424b9a4fc3518e3ade0b95deb9fbc0f93a1876be3eda598" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.0.2", + "log", + "semver", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser 0.219.1", +] + [[package]] name = "wycheproof" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index a32761bd8..d18f8a238 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -32,6 +32,13 @@ soroban-builtin-sdk-macros = { version = "=22.1.3", path = "soroban-builtin-sdk- # NB: this must match the wasmparser version wasmi is using wasmparser = "=0.116.1" +[workspace.dependencies.wasmtime] +version = "28.0" +default-features = false +features = ["runtime", "winch"] +git = "https://github.com/bytecodealliance/wasmtime" +rev = "b5627a86a7740ffc732f4c22b9f0b2c66252638b" + # NB: When updating, also update the version in rs-soroban-env dev-dependencies [workspace.dependencies.stellar-xdr] version = "=22.1.0" diff --git a/soroban-env-common/Cargo.toml b/soroban-env-common/Cargo.toml index cd58077ff..10b7f65fe 100644 --- a/soroban-env-common/Cargo.toml +++ b/soroban-env-common/Cargo.toml @@ -24,6 +24,7 @@ ethnum = "1.5.0" arbitrary = { version = "1.3.2", features = ["derive"], optional = true } num-traits = {version = "0.2.17", default-features = false} num-derive = "0.4.1" +wasmtime = { workspace = true, optional = true} [target.'cfg(not(target_family = "wasm"))'.dependencies] tracy-client = { version = "0.17.0", features = ["enable", "timer-fallback"], default-features = false, optional = true } @@ -36,6 +37,7 @@ num-traits = "0.2.17" std = ["stellar-xdr/std", "stellar-xdr/base64"] serde = ["dep:serde", "stellar-xdr/serde"] wasmi = ["dep:wasmi", "dep:wasmparser"] +wasmtime = ["dep:wasmtime"] testutils = ["dep:arbitrary", "stellar-xdr/arbitrary"] next = ["stellar-xdr/next", "soroban-env-macros/next"] tracy = ["dep:tracy-client"] diff --git a/soroban-env-common/src/error.rs b/soroban-env-common/src/error.rs index 56983ea44..4e6cd326c 100644 --- a/soroban-env-common/src/error.rs +++ b/soroban-env-common/src/error.rs @@ -283,6 +283,49 @@ impl From for Error { } } +#[cfg(feature = "wasmtime")] +impl From for Error { + #[allow(clippy::wildcard_in_or_patterns)] + fn from(trap: wasmtime::Trap) -> Self { + let ec = match trap { + wasmtime::Trap::UnreachableCodeReached => ScErrorCode::InvalidAction, + + wasmtime::Trap::MemoryOutOfBounds | wasmtime::Trap::TableOutOfBounds => { + ScErrorCode::IndexBounds + } + + wasmtime::Trap::IndirectCallToNull => ScErrorCode::MissingValue, + + wasmtime::Trap::IntegerDivisionByZero + | wasmtime::Trap::IntegerOverflow + | wasmtime::Trap::BadConversionToInteger => ScErrorCode::ArithDomain, + + wasmtime::Trap::BadSignature => ScErrorCode::UnexpectedType, + + wasmtime::Trap::StackOverflow + | wasmtime::Trap::Interrupt + | wasmtime::Trap::OutOfFuel => { + return Error::from_type_and_code(ScErrorType::Budget, ScErrorCode::ExceededLimit) + } + + wasmtime::Trap::HeapMisaligned + | wasmtime::Trap::AlwaysTrapAdapter + | wasmtime::Trap::AtomicWaitNonSharedMemory + | wasmtime::Trap::NullReference + | wasmtime::Trap::CannotEnterComponent + | _ => ScErrorCode::InvalidAction, + }; + Error::from_type_and_code(ScErrorType::WasmVm, ec) + } +} + +#[cfg(feature = "wasmtime")] +impl From for Error { + fn from(_: wasmtime::MemoryAccessError) -> Self { + Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::IndexBounds) + } +} + impl Error { // NB: we don't provide a "get_type" to avoid casting a bad bit-pattern into // an ScErrorType. Instead we provide an "is_type" to check any specific diff --git a/soroban-env-common/src/lib.rs b/soroban-env-common/src/lib.rs index 9ff21e563..38f1d5534 100644 --- a/soroban-env-common/src/lib.rs +++ b/soroban-env-common/src/lib.rs @@ -113,6 +113,8 @@ pub use val::{ConversionError, Tag, Val}; #[cfg(feature = "wasmi")] pub use val::WasmiMarshal; +#[cfg(feature = "wasmtime")] +pub use val::WasmtimeMarshal; pub use val::{AddressObject, MapObject, VecObject}; pub use val::{Bool, Void}; diff --git a/soroban-env-common/src/val.rs b/soroban-env-common/src/val.rs index 4224b4c0b..6fc688498 100644 --- a/soroban-env-common/src/val.rs +++ b/soroban-env-common/src/val.rs @@ -465,6 +465,62 @@ impl WasmiMarshal for i64 { } } +#[cfg(feature = "wasmtime")] +pub trait WasmtimeMarshal: Sized { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option; + fn marshal_wasmtime_from_self(self) -> wasmtime::Val; +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for Val { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + let v = Val::from_payload(i as u64); + if v.is_good() { + Some(v) + } else { + None + } + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self.get_payload() as i64) + } +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for u64 { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + Some(i as u64) + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self as i64) + } +} + +#[cfg(feature = "wasmtime")] +impl WasmtimeMarshal for i64 { + fn try_marshal_from_wasmtime_value(v: wasmtime::Val) -> Option { + if let wasmtime::Val::I64(i) = v { + Some(i) + } else { + None + } + } + + fn marshal_wasmtime_from_self(self) -> wasmtime::Val { + wasmtime::Val::I64(self) + } +} + // Manually implement all the residual pieces: ValConverts // and Froms. diff --git a/soroban-env-common/src/vmcaller_env.rs b/soroban-env-common/src/vmcaller_env.rs index 933040a40..6006856ce 100644 --- a/soroban-env-common/src/vmcaller_env.rs +++ b/soroban-env-common/src/vmcaller_env.rs @@ -25,26 +25,55 @@ use core::marker::PhantomData; /// allows code to import and use `Env` directly (such as the native /// contract) to call host methods without having to write `VmCaller::none()` /// everywhere. -#[cfg(feature = "wasmi")] -pub struct VmCaller<'a, T>(pub Option>); -#[cfg(feature = "wasmi")] + +#[cfg(any(feature = "wasmi", feature = "wasmtime"))] +pub enum VmCaller<'a, T> { + #[cfg(feature = "wasmi")] + WasmiCaller(wasmi::Caller<'a, T>), + #[cfg(feature = "wasmtime")] + WasmtimeCaller(wasmtime::Caller<'a, T>), + NoCaller, +} +#[cfg(any(feature = "wasmi", feature = "wasmtime"))] impl<'a, T> VmCaller<'a, T> { pub fn none() -> Self { - VmCaller(None) + VmCaller::NoCaller } + #[cfg(feature = "wasmi")] pub fn try_ref(&self) -> Result<&wasmi::Caller<'a, T>, Error> { - match &self.0 { - Some(caller) => Ok(caller), - None => Err(Error::from_type_and_code( + match self { + VmCaller::WasmiCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( ScErrorType::Context, ScErrorCode::InternalError, )), } } + #[cfg(feature = "wasmi")] pub fn try_mut(&mut self) -> Result<&mut wasmi::Caller<'a, T>, Error> { - match &mut self.0 { - Some(caller) => Ok(caller), - None => Err(Error::from_type_and_code( + match self { + VmCaller::WasmiCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + )), + } + } + #[cfg(feature = "wasmtime")] + pub fn try_ref_wasmtime(&self) -> Result<&wasmtime::Caller<'a, T>, Error> { + match self { + VmCaller::WasmtimeCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + )), + } + } + #[cfg(feature = "wasmtime")] + pub fn try_mut_wasmtime(&mut self) -> Result<&mut wasmtime::Caller<'a, T>, Error> { + match self { + VmCaller::WasmtimeCaller(caller) => Ok(caller), + _ => Err(Error::from_type_and_code( ScErrorType::Context, ScErrorCode::InternalError, )), @@ -52,11 +81,11 @@ impl<'a, T> VmCaller<'a, T> { } } -#[cfg(not(feature = "wasmi"))] +#[cfg(not(any(feature = "wasmi", feature = "wasmtime")))] pub struct VmCaller<'a, T> { _nothing: PhantomData<&'a T>, } -#[cfg(not(feature = "wasmi"))] +#[cfg(not(any(feature = "wasmi", feature = "wasmtime")))] impl<'a, T> VmCaller<'a, T> { pub fn none() -> Self { VmCaller { diff --git a/soroban-env-host/Cargo.toml b/soroban-env-host/Cargo.toml index a5174cbf6..5633e1b04 100644 --- a/soroban-env-host/Cargo.toml +++ b/soroban-env-host/Cargo.toml @@ -15,8 +15,9 @@ exclude = ["observations/"] [dependencies] soroban-builtin-sdk-macros = { workspace = true } -soroban-env-common = { workspace = true, features = ["std", "wasmi", "shallow-val-hash"] } -wasmi = { workspace = true } +soroban-env-common = { workspace = true, features = ["std", "shallow-val-hash"] } +wasmi = { workspace = true, optional = true } +wasmtime = { workspace = true, optional = true } wasmparser = { workspace = true } stellar-strkey = "0.0.9" static_assertions = "1.1.0" @@ -90,6 +91,9 @@ default-features = false features = ["arbitrary"] [features] +default = ["wasmi", "wasmtime"] +wasmi = ["dep:wasmi", "soroban-env-common/wasmi"] +wasmtime = ["dep:wasmtime", "soroban-env-common/wasmtime"] testutils = ["soroban-env-common/testutils", "recording_mode"] backtrace = ["dep:backtrace"] next = ["soroban-env-common/next", "stellar-xdr/next"] diff --git a/soroban-env-host/src/budget.rs b/soroban-env-host/src/budget.rs index 453f77f55..9373017b1 100644 --- a/soroban-env-host/src/budget.rs +++ b/soroban-env-host/src/budget.rs @@ -2,13 +2,21 @@ mod dimension; mod limits; mod model; mod util; +#[cfg(feature = "wasmi")] mod wasmi_helper; +#[cfg(feature = "wasmtime")] +mod wasmtime_helper; pub(crate) use limits::DepthLimiter; pub use limits::{DEFAULT_HOST_DEPTH_LIMIT, DEFAULT_XDR_RW_LIMITS}; pub use model::{MeteredCostComponent, ScaledU64}; + +#[cfg(feature = "wasmi")] pub(crate) use wasmi_helper::{get_wasmi_config, load_calibrated_fuel_costs}; +#[cfg(feature = "wasmtime")] +pub(crate) use wasmtime_helper::get_wasmtime_config; + use std::{ cell::{RefCell, RefMut}, fmt::{Debug, Display}, diff --git a/soroban-env-host/src/budget/wasmtime_helper.rs b/soroban-env-host/src/budget/wasmtime_helper.rs new file mode 100644 index 000000000..4cad3afe9 --- /dev/null +++ b/soroban-env-host/src/budget/wasmtime_helper.rs @@ -0,0 +1,15 @@ +use crate::{budget::Budget, HostError}; + +pub(crate) fn get_wasmtime_config(_budget: &Budget) -> Result { + let mut config = wasmtime::Config::new(); + config + .strategy(wasmtime::Strategy::Winch) + .debug_info(false) + .generate_address_map(false) + .consume_fuel(true) + .wasm_bulk_memory(true) + .wasm_multi_value(false) + .wasm_simd(false) + .wasm_tail_call(false); + Ok(config) +} diff --git a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs index 76dee7c99..1f252e238 100644 --- a/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs +++ b/soroban-env-host/src/cost_runner/cost_types/vm_ops.rs @@ -86,6 +86,7 @@ mod v21 { host.get_ledger_protocol_version() .expect("protocol version"), sample.module.wasmi_module.engine(), + sample.module.wasmtime_module.engine(), &sample.wasm[..], sample.module.cost_inputs.clone(), ) diff --git a/soroban-env-host/src/host.rs b/soroban-env-host/src/host.rs index f64506d63..a89631711 100644 --- a/soroban-env-host/src/host.rs +++ b/soroban-env-host/src/host.rs @@ -92,6 +92,7 @@ pub(crate) const MIN_LEDGER_PROTOCOL_VERSION: u32 = 22; #[derive(Clone, Default)] struct HostImpl { module_cache: RefCell>, + last_vm_fuel: RefCell, source_account: RefCell>, ledger: RefCell>, objects: RefCell>, @@ -216,6 +217,12 @@ impl_checked_borrow_helpers!( try_borrow_module_cache, try_borrow_module_cache_mut ); +impl_checked_borrow_helpers!( + last_vm_fuel, + u64, + try_borrow_last_vm_fuel, + try_borrow_last_vm_fuel_mut +); impl_checked_borrow_helpers!( source_account, Option, @@ -353,6 +360,7 @@ impl Host { let _client = tracy_client::Client::start(); Self(Rc::new(HostImpl { module_cache: RefCell::new(None), + last_vm_fuel: RefCell::new(0), source_account: RefCell::new(None), ledger: RefCell::new(None), objects: Default::default(), @@ -428,6 +436,15 @@ impl Host { }) } + pub(crate) fn get_last_vm_fuel(&self) -> Result { + Ok(*self.try_borrow_last_vm_fuel()?) + } + + pub(crate) fn set_last_vm_fuel(&self, fuel: u64) -> Result<(), HostError> { + *self.try_borrow_last_vm_fuel_mut()? = fuel; + Ok(()) + } + #[cfg(any(test, feature = "recording_mode"))] pub fn in_storage_recording_mode(&self) -> Result { if let crate::storage::FootprintMode::Recording(_) = self.try_borrow_storage()?.mode { diff --git a/soroban-env-host/src/host/error.rs b/soroban-env-host/src/host/error.rs index a9275c820..a329a0848 100644 --- a/soroban-env-host/src/host/error.rs +++ b/soroban-env-host/src/host/error.rs @@ -176,6 +176,31 @@ impl HostError { true } + + // Wasmtime uses anyhow::Error for its error type which may carry either a + // HostError or a wasmtime::Trap, or "something else entirely" since it's a + // dyn Error type. This is a somewhat different pattern to what we have in + // wasmi. + #[cfg(feature = "wasmtime")] + pub fn map_wasmtime_error(r: Result) -> Result { + match r { + Ok(t) => Ok(t), + Err(e) => match e.downcast::() { + Ok(hosterror) => Err(hosterror), + Err(e) => { + let e = if let Some(trap) = e.root_cause().downcast_ref::() { + HostError::from(Error::from(*trap)) + } else { + HostError::from(Error::from_type_and_code( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + )) + }; + Err(e) + } + }, + } + } } impl From for HostError @@ -245,6 +270,8 @@ pub trait ErrorHandler { where Error: From, E: Debug; + #[cfg(feature = "wasmtime")] + fn map_wasmtime_error(&self, r: Result) -> Result; fn error(&self, error: Error, msg: &str, args: &[Val]) -> HostError; } @@ -285,6 +312,35 @@ impl ErrorHandler for Host { }) } + // Wasmtime uses anyhow::Error for its error type which may carry either a + // HostError or a wasmtime::Trap, or "something else entirely" since it's a + // dyn Error type. This is a somewhat different pattern to what we have in + // wasmi. + #[cfg(feature = "wasmtime")] + fn map_wasmtime_error(&self, r: Result) -> Result { + match r { + Ok(t) => Ok(t), + Err(e) => match e.downcast::() { + Ok(hosterror) => Err(hosterror), + Err(e) => { + let e = if let Some(trap) = e.root_cause().downcast_ref::() { + self.error(Error::from(*trap), "wasmtime trap", &[]) + } else { + self.error( + Error::from_type_and_code( + ScErrorType::WasmVm, + ScErrorCode::InvalidAction, + ), + "wasmtime error", + &[], + ) + }; + Err(e) + } + }, + } + } + /// At minimum constructs and returns a [HostError] built from the provided /// [Error], and when running in [DiagnosticMode::Debug] additionally /// records a diagnostic event with the provided `msg` and `args` and then diff --git a/soroban-env-host/src/host/mem_helper.rs b/soroban-env-host/src/host/mem_helper.rs index 788bd704f..000f5f55a 100644 --- a/soroban-env-host/src/host/mem_helper.rs +++ b/soroban-env-host/src/host/mem_helper.rs @@ -81,11 +81,24 @@ impl Host { buf: &[u8], ) -> Result<(), HostError> { self.charge_budget(ContractCostType::MemCpy, Some(buf.len() as u64))?; - let mem = vm.get_memory(self)?; - self.map_err( - mem.write(vmcaller.try_mut()?, mem_pos as usize, buf) - .map_err(|me| wasmi::Error::Memory(me)), - ) + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + self.map_err( + mem.write(ctx, mem_pos as usize, buf) + .map_err(|me| wasmi::Error::Memory(me)), + ) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + self.map_err(mem.write(ctx, mem_pos as usize, buf)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } } pub(crate) fn metered_vm_read_bytes_from_linear_memory( @@ -96,11 +109,71 @@ impl Host { buf: &mut [u8], ) -> Result<(), HostError> { self.charge_budget(ContractCostType::MemCpy, Some(buf.len() as u64))?; - let mem = vm.get_memory(self)?; - self.map_err( - mem.read(vmcaller.try_mut()?, mem_pos as usize, buf) - .map_err(|me| wasmi::Error::Memory(me)), - ) + + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + self.map_err( + mem.read(ctx, mem_pos as usize, buf) + .map_err(|me| wasmi::Error::Memory(me)), + ) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + self.map_err(mem.read(ctx, mem_pos as usize, buf)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } + } + + #[allow(clippy::needless_lifetimes)] + fn get_data_mut<'host, 'caller, 'vm>( + &'host self, + vmcaller: &'caller mut VmCaller, + vm: &'vm Rc, + ) -> Result<&'caller mut [u8], HostError> { + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + Ok(mem.data_mut(ctx)) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + Ok(mem.data_mut(ctx)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } + } + + #[allow(clippy::needless_lifetimes)] + fn get_data<'host, 'caller, 'vm>( + &'host self, + vmcaller: &'caller VmCaller, + vm: &'vm Rc, + ) -> Result<&'caller [u8], HostError> { + match vmcaller { + VmCaller::WasmiCaller(ctx) => { + let mem = vm.get_memory(self)?; + Ok(mem.data(ctx)) + } + VmCaller::WasmtimeCaller(ctx) => { + let mem = vm.get_wasmtime_memory(self)?; + Ok(mem.data(ctx)) + } + _ => Err(crate::Error::from_type_and_code( + ScErrorType::Context, + ScErrorCode::InternalError, + ) + .into()), + } } // Note on metering: covers the cost of memcpy from bytes into the linear memory. @@ -125,7 +198,7 @@ impl Host { .ok_or_else(|| self.err_arith_overflow())?; let mem_range = (mem_pos as usize)..(mem_end as usize); - let mem_data = vm.get_memory(self)?.data_mut(vmcaller.try_mut()?); + let mem_data = self.get_data_mut(vmcaller, vm)?; let mem_slice = mem_data .get_mut(mem_range) .ok_or_else(|| self.err_oob_linear_memory())?; @@ -169,7 +242,7 @@ impl Host { .ok_or_else(|| self.err_arith_overflow())?; let mem_range = (mem_pos as usize)..(mem_end as usize); - let mem_data = vm.get_memory(self)?.data(vmcaller.try_mut()?); + let mem_data = self.get_data(vmcaller, vm)?; let mem_slice = mem_data .get(mem_range) .ok_or_else(|| self.err_oob_linear_memory())?; @@ -218,7 +291,7 @@ impl Host { num_slices: usize, mut callback: impl FnMut(usize, &[u8]) -> Result<(), HostError>, ) -> Result<(), HostError> { - let mem_data = vm.get_memory(self)?.data(vmcaller.try_mut()?); + let mem_data = self.get_data(vmcaller, vm)?; // charge the cost of copying the slices (pointers to the content, not // the content themselves) upfront. self.charge_budget( diff --git a/soroban-env-host/src/lib.rs b/soroban-env-host/src/lib.rs index 9c0343cfa..c310eeeda 100644 --- a/soroban-env-host/src/lib.rs +++ b/soroban-env-host/src/lib.rs @@ -54,6 +54,14 @@ pub mod fees; #[doc(hidden)] pub use host::{TraceEvent, TraceHook, TraceRecord, TraceState}; +#[doc(hidden)] +#[cfg(feature = "wasmi")] +pub use wasmi; + +#[doc(hidden)] +#[cfg(feature = "wasmtime")] +pub use wasmtime; + #[cfg(feature = "bench")] #[doc(hidden)] pub mod cost_runner; diff --git a/soroban-env-host/src/vm.rs b/soroban-env-host/src/vm.rs index 4949f9f27..8f1fda4a1 100644 --- a/soroban-env-host/src/vm.rs +++ b/soroban-env-host/src/vm.rs @@ -18,9 +18,10 @@ mod parsed_module; pub(crate) use dispatch::dummy0; #[cfg(test)] pub(crate) use dispatch::protocol_gated_dummy; +use soroban_env_common::WasmtimeMarshal; use crate::{ - budget::{get_wasmi_config, AsBudget, Budget}, + budget::{get_wasmi_config, get_wasmtime_config, AsBudget, Budget}, host::{ error::TryBorrowOrErr, metered_clone::MeteredContainer, @@ -89,6 +90,10 @@ pub struct Vm { wasmi_store: RefCell>, wasmi_instance: wasmi::Instance, pub(crate) wasmi_memory: Option, + + wasmtime_store: RefCell>, + wasmtime_instance: wasmtime::Instance, + pub(crate) wasmtime_memory: Option, } impl std::hash::Hash for Vm { @@ -125,6 +130,34 @@ impl Host { } Ok(linker) } + + // Make a wasmtime linker restricted to _only_ importing the symbols + // mentioned in `symbols`. + pub(crate) fn make_minimal_wasmtime_linker_for_symbols( + context: &Ctx, + engine: &wasmtime::Engine, + symbols: &BTreeSet<(&str, &str)>, + ) -> Result, HostError> { + let mut linker = wasmtime::Linker::new(engine); + for hf in HOST_FUNCTIONS { + if symbols.contains(&(hf.mod_str, hf.fn_str)) { + context.map_wasmtime_error((hf.wrap_wasmtime)(&mut linker))?; + } + } + Ok(linker) + } + + // Make a wasmtime linker that imports all the symbols. + pub(crate) fn make_maximal_wasmtime_linker( + context: &Ctx, + engine: &wasmtime::Engine, + ) -> Result, HostError> { + let mut linker = wasmtime::Linker::new(engine); + for hf in HOST_FUNCTIONS { + context.map_wasmtime_error((hf.wrap_wasmtime)(&mut linker))?; + } + Ok(linker) + } } // In one very narrow context -- when recording, and with a module cache -- we @@ -195,6 +228,40 @@ impl Vm { Ok((store, instance, memory)) } + fn instantiate_wasmtime( + host: &Host, + parsed_module: &Arc, + wasmtime_linker: &wasmtime::Linker, + ) -> Result< + ( + wasmtime::Store, + wasmtime::Instance, + Option, + ), + HostError, + > { + let span = tracy_span!("Vm::instantiate_wasmtime"); + + let wasmtime_engine = parsed_module.wasmtime_module.engine(); + let mut wasmtime_store = { + let _span = tracy_span!("Vm::instantiate_wasmtime - store"); + wasmtime::Store::new(&wasmtime_engine, host.clone()) + }; + let wasmtime_instance = { + let _span = tracy_span!("Vm::instantiate_wasmtime - instantiate"); + host.map_wasmtime_error( + wasmtime_linker.instantiate(&mut wasmtime_store, &parsed_module.wasmtime_module), + )? + }; + let wasmtime_memory = + if let Some(ext) = wasmtime_instance.get_export(&mut wasmtime_store, "memory") { + ext.into_memory() + } else { + None + }; + Ok((wasmtime_store, wasmtime_instance, wasmtime_memory)) + } + /// Instantiates a VM given the arguments provided in [`Self::new`], /// or [`Self::new_from_module_cache`] fn instantiate( @@ -202,6 +269,7 @@ impl Vm { contract_id: Hash, parsed_module: Arc, wasmi_linker: &wasmi::Linker, + wasmtime_linker: &wasmtime::Linker, ) -> Result, HostError> { let _span = tracy_span!("Vm::instantiate"); @@ -214,6 +282,9 @@ impl Vm { let (wasmi_store, wasmi_instance, wasmi_memory) = Self::instantiate_wasmi(host, &parsed_module, wasmi_linker)?; + let (wasmtime_store, wasmtime_instance, wasmtime_memory) = + Self::instantiate_wasmtime(host, &parsed_module, wasmtime_linker)?; + // Here we do _not_ supply the store with any fuel. Fuel is supplied // right before the VM is being run, i.e., before crossing the host->VM // boundary. @@ -223,6 +294,9 @@ impl Vm { wasmi_store: RefCell::new(wasmi_store), wasmi_instance, wasmi_memory, + wasmtime_store: RefCell::new(wasmtime_store), + wasmtime_instance, + wasmtime_memory, })) } @@ -234,10 +308,23 @@ impl Vm { let _span = tracy_span!("Vm::from_parsed_module"); VmInstantiationTimer::new(host.clone()); if let Some(cache) = &*host.try_borrow_module_cache()? { - Self::instantiate(host, contract_id, parsed_module, &cache.wasmi_linker) + Self::instantiate( + host, + contract_id, + parsed_module, + &cache.wasmi_linker, + &cache.wasmtime_linker, + ) } else { let wasmi_linker = parsed_module.make_wasmi_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) + let wasmtime_linker = parsed_module.make_wasmtime_linker(host)?; + Self::instantiate( + host, + contract_id, + parsed_module, + &wasmi_linker, + &wasmtime_linker, + ) } } @@ -284,7 +371,14 @@ impl Vm { VmInstantiationTimer::new(host.clone()); let parsed_module = Self::parse_module(host, wasm, cost_inputs, cost_mode)?; let wasmi_linker = parsed_module.make_wasmi_linker(host)?; - Self::instantiate(host, contract_id, parsed_module, &wasmi_linker) + let wasmtime_linker = parsed_module.make_wasmtime_linker(host)?; + Self::instantiate( + host, + contract_id, + parsed_module, + &wasmi_linker, + &wasmtime_linker, + ) } #[cfg(not(any(test, feature = "recording_mode")))] @@ -361,6 +455,18 @@ impl Vm { } } + pub(crate) fn get_wasmtime_memory(&self, host: &Host) -> Result { + match self.wasmtime_memory { + Some(mem) => Ok(mem), + None => Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::MissingValue, + "no linear memory named `memory`", + &[], + )), + } + } + // Wrapper for the [`Func`] call which is metered as a component. // Resolves the function entity, and takes care the conversion between and // tranfering of the host budget / VM fuel. This is where the host->VM->host @@ -420,6 +526,8 @@ impl Vm { self.wasmi_store .try_borrow_mut_or_err()? .add_fuel_to_vm(host)?; + host.set_last_vm_fuel(added_fuel)?; + // Metering: the `func.call` will trigger `wasmi::Call` (or `CallIndirect`) instruction, // which is technically covered by wasmi fuel metering. So we are double charging a bit // here (by a few 100s cpu insns). It is better to be safe. @@ -433,9 +541,10 @@ impl Vm { // wasmi instruction) remaining when the `OutOfFuel` trap occurs. This is only observable // if the contract traps with `OutOfFuel`, which may appear confusing if they look closely // at the budget amount consumed. So it should be fine. + let last_fuel = host.get_last_vm_fuel()?; self.wasmi_store .try_borrow_mut_or_err()? - .return_fuel_to_host(host)?; + .return_fuel_to_host(host, last_fuel)?; if let Err(e) = res { use std::borrow::Cow; @@ -483,6 +592,93 @@ impl Vm { ) } + pub(crate) fn metered_wasmtime_func_call( + self: &Rc, + host: &Host, + func_sym: &Symbol, + inputs: &[wasmtime::Val], + treat_missing_function_as_noop: bool, + ) -> Result { + let _span = tracy_span!("Vm::metered_wasmtime_func_call"); + + host.charge_budget(ContractCostType::InvokeVmFunction, None)?; + + // resolve the function entity to be called + let func_ss: SymbolStr = func_sym.try_into_val(host)?; + let ext = match self.wasmtime_instance.get_export( + &mut *self.wasmtime_store.try_borrow_mut_or_err()?, + func_ss.as_ref(), + ) { + None => { + if treat_missing_function_as_noop { + return Ok(Val::VOID.into()); + } else { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::MissingValue, + "trying to invoke non-existent contract function", + &[func_sym.to_val()], + )); + } + } + Some(e) => e, + }; + let func = match ext.into_func() { + None => { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::UnexpectedType, + "trying to invoke Wasm export that is not a function", + &[func_sym.to_val()], + )) + } + Some(e) => e, + }; + + if inputs.len() > Vm::MAX_VM_ARGS { + return Err(host.err( + ScErrorType::WasmVm, + ScErrorCode::InvalidInput, + "Too many arguments in Wasm invocation", + &[func_sym.to_val()], + )); + } + + // call the function + let mut wasm_ret: [wasmtime::Val; 1] = [wasmtime::Val::I64(0)]; + let added_fuel = self + .wasmtime_store + .try_borrow_mut_or_err()? + .add_fuel_to_vm(host)?; + host.set_last_vm_fuel(added_fuel)?; + + let res = { + let _span = tracy_span!("Vm::metered_wasmtime_func_call - actual call"); + func.call( + &mut *self.wasmtime_store.try_borrow_mut_or_err()?, + inputs, + &mut wasm_ret, + ) + }; + + let last_fuel = host.get_last_vm_fuel()?; + self.wasmtime_store + .try_borrow_mut_or_err()? + .return_fuel_to_host(host, last_fuel)?; + + if let Err(e) = res { + // FIXME: this needs to be fairly careful about correct propagation. + // currently we're just doing a crude downcast attempt. + return host.map_wasmtime_error(Err(e)); + } + host.relative_to_absolute( + Val::try_marshal_from_wasmtime_value(wasm_ret[0]).ok_or(ConversionError)?, + ) + } + + // FIXME: remove when/if we decide to commit to this transition. + const FIRST_PROTOCOL_TO_RUN_ON_WASMTIME: u32 = 21; + pub(crate) fn invoke_function_raw( self: &Rc, host: &Host, @@ -492,16 +688,32 @@ impl Vm { ) -> Result { let _span = tracy_span!("Vm::invoke_function_raw"); Vec::::charge_bulk_init_cpy(args.len() as u64, host.as_budget())?; - let wasm_args: Vec = args - .iter() - .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self())) - .collect::, HostError>>()?; - self.metered_func_call( - host, - func_sym, - wasm_args.as_slice(), - treat_missing_function_as_noop, - ) + if host.get_ledger_protocol_version()? >= Self::FIRST_PROTOCOL_TO_RUN_ON_WASMTIME { + let wasmtime_args: Vec = args + .iter() + .map(|i| { + host.absolute_to_relative(*i) + .map(|v| v.marshal_wasmtime_from_self()) + }) + .collect::, HostError>>()?; + self.metered_wasmtime_func_call( + host, + func_sym, + wasmtime_args.as_slice(), + treat_missing_function_as_noop, + ) + } else { + let wasm_args: Vec = args + .iter() + .map(|i| host.absolute_to_relative(*i).map(|v| v.marshal_from_self())) + .collect::, HostError>>()?; + self.metered_func_call( + host, + func_sym, + wasm_args.as_slice(), + treat_missing_function_as_noop, + ) + } } /// Returns the raw bytes content of a named custom section from the WASM @@ -520,7 +732,7 @@ impl Vm { let store: &mut wasmi::Store = &mut *self.wasmi_store.try_borrow_mut_or_err()?; let mut ctx: StoreContextMut = store.into(); let caller: Caller = Caller::new(&mut ctx, Some(&self.wasmi_instance)); - let mut vmcaller: VmCaller = VmCaller(Some(caller)); + let mut vmcaller: VmCaller = VmCaller::WasmiCaller(caller); f(&mut vmcaller) } diff --git a/soroban-env-host/src/vm/dispatch.rs b/soroban-env-host/src/vm/dispatch.rs index 39997adf6..5b04467f9 100644 --- a/soroban-env-host/src/vm/dispatch.rs +++ b/soroban-env-host/src/vm/dispatch.rs @@ -1,13 +1,12 @@ use super::FuelRefillable; use crate::{ xdr::{ContractCostType, ScErrorCode, ScErrorType}, - CheckedEnvArg, EnvBase, Host, HostError, VmCaller, VmCallerEnv, + CheckedEnvArg, EnvBase, ErrorHandler, Host, HostError, VmCaller, VmCallerEnv, }; use crate::{ - AddressObject, Bool, BytesObject, DurationObject, Error, ErrorHandler, I128Object, I256Object, - I256Val, I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, - TimepointObject, U128Object, U256Object, U256Val, U32Val, U64Object, U64Val, Val, VecObject, - Void, + AddressObject, Bool, BytesObject, DurationObject, Error, I128Object, I256Object, I256Val, + I64Object, MapObject, StorageType, StringObject, Symbol, SymbolObject, TimepointObject, + U128Object, U256Object, U256Val, U32Val, U64Object, U64Val, Val, VecObject, Void, }; use core::fmt::Debug; use soroban_env_common::{call_macro_with_all_host_functions, WasmiMarshal}; @@ -223,13 +222,14 @@ macro_rules! generate_dispatch_functions { // This is where the VM -> Host boundary is crossed. // We first return all fuels from the VM back to the host such that // the host maintains control of the budget. - FuelRefillable::return_fuel_to_host(&mut caller, &host).map_err(|he| Trap::from(he))?; + let last_fuel = host.get_last_vm_fuel()?; + FuelRefillable::return_fuel_to_host(&mut caller, &host, last_fuel).map_err(|he| Trap::from(he))?; // Charge for the host function dispatching: conversion between VM fuel and // host budget, marshalling values. This does not account for the actual work // being done in those functions, which are metered individually by the implementation. host.charge_budget(ContractCostType::DispatchHostFunction, None)?; - let mut vmcaller = VmCaller(Some(caller)); + let mut vmcaller = VmCaller::WasmiCaller(caller); // The odd / seemingly-redundant use of `wasmi::Value` here // as intermediates -- rather than just passing Vals -- // has to do with the fact that some host functions are @@ -280,7 +280,8 @@ macro_rules! generate_dispatch_functions { // This is where the Host->VM boundary is crossed. // We supply the remaining host budget as fuel to the VM. let caller = vmcaller.try_mut().map_err(|e| Trap::from(HostError::from(e)))?; - FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + let added_fuel = FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + host.set_last_vm_fuel(added_fuel)?; res } @@ -291,3 +292,176 @@ macro_rules! generate_dispatch_functions { // Here we invoke the x-macro passing generate_dispatch_functions as its callback macro. call_macro_with_all_host_functions! { generate_dispatch_functions } + +pub(crate) mod wasmtime_dispatch { + use super::*; + + /////////////////////////////////////////////////////////////////////////////// + /// X-macro use: dispatch functions + /////////////////////////////////////////////////////////////////////////////// + + // This is a callback macro that pattern-matches the token-tree passed by the + // x-macro (call_macro_with_all_host_functions) and produces a suite of + // dispatch-function definitions. + macro_rules! generate_wasmtime_dispatch_functions { + { + $( + // This outer pattern matches a single 'mod' block of the token-tree + // passed from the x-macro to this macro. It is embedded in a `$()*` + // pattern-repetition matcher so that it will match all provided + // 'mod' blocks provided. + $(#[$mod_attr:meta])* + mod $mod_name:ident $mod_str:literal + { + $( + // This inner pattern matches a single function description + // inside a 'mod' block in the token-tree passed from the + // x-macro to this macro. It is embedded in a `$()*` + // pattern-repetition matcher so that it will match all such + // descriptions. + $(#[$fn_attr:meta])* + { $fn_str:literal, $($min_proto:literal)?, $($max_proto:literal)?, fn $fn_id:ident ($($arg:ident:$type:ty),*) -> $ret:ty } + )* + } + )* + } + + => // The part of the macro above this line is a matcher; below is its expansion. + + { + // This macro expands to multiple items: a set of free functions in the + // current module, which are called by functions registered with the VM + // to forward calls to the host. + $( + $( + // This defines a "dispatch function" that does several things: + // + // 1. Transfers the running "VM fuel" balance from wasmi to the + // host's CPU budget. + // 2. Charges the host budget for the call, failing if over. + // 3. Attempts to convert incoming wasmi i64 args to Vals or + // Val-wrappers expected by host functions, failing if any + // conversions fail. This step also does + // relative-to-absolute object reference conversion. + // 4. Calls the host function. + // 5. Augments any error result with this calling context, so + // that we get at minimum a "which host function failed" + // context on error. + // 6. Converts the result back to an i64 for wasmi, again + // converting from absolute object references to relative + // along the way. + // 7. Checks the result is Ok, or escalates Err to a VM Trap. + // 8. Transfers the residual CPU budget back to wasmi "VM + // fuel". + // + // It is embedded in two nested `$()*` pattern-repetition + // expanders that correspond to the pattern-repetition matchers + // in the match section, but we ignore the structure of the + // 'mod' block repetition-level from the outer pattern in the + // expansion, flattening all functions from all 'mod' blocks + // into a set of functions. + $(#[$fn_attr])* + pub(crate) fn $fn_id(mut caller: wasmtime::Caller<'_, Host>, $($arg:i64),*) -> + Result<(i64,), wasmtime::Error> + { + let _span = tracy_span!(core::stringify!($fn_id)); + + let host = caller.data().clone(); + + // This is an additional protocol version guardrail that + // should not be necessary. Any wasm contract containing a + // call to an out-of-protocol-range host function should + // have been rejected by the linker during VM instantiation. + // This is just an additional guard rail for future proof. + $( host.check_protocol_version_lower_bound($min_proto)?; )? + $( host.check_protocol_version_upper_bound($max_proto)?; )? + + if host.tracing_enabled() + { + #[allow(unused)] + let trace_args = ($( + match <$type>::try_marshal_from_relative_value(Value::I64($arg), &host) { + Ok(val) => TraceArg::Ok(val), + Err(_) => TraceArg::Bad($arg), + } + ),*); + let hook_args: &[&dyn std::fmt::Debug] = homogenize_tuple!(trace_args, ($($arg),*)); + host.trace_env_call(&core::stringify!($fn_id), hook_args)?; + } + + // This is where the VM -> Host boundary is crossed. + // We first return all fuels from the VM back to the host such that + // the host maintains control of the budget. + + let last_fuel = host.get_last_vm_fuel()?; + FuelRefillable::return_fuel_to_host(&mut caller, &host, last_fuel).map_err(|he| Trap::from(he))?; + + // Charge for the host function dispatching: conversion between VM fuel and + // host budget, marshalling values. This does not account for the actual work + // being done in those functions, which are metered individually by the implementation. + host.charge_budget(ContractCostType::DispatchHostFunction, None)?; + + let mut vmcaller = VmCaller::WasmtimeCaller(caller); + + // The odd / seemingly-redundant use of `wasmi::Value` here + // as intermediates -- rather than just passing Vals -- + // has to do with the fact that some host functions are + // typed as receiving or returning plain _non-val_ i64 or + // u64 values. So the call here has to be able to massage + // both types into and out of i64, and `wasmi::Value` + // happens to be a natural switching point for that: we have + // conversions to and from both Val and i64 / u64 for + // wasmi::Value. + let res: Result<_, HostError> = host.$fn_id(&mut vmcaller, $(<$type>::check_env_arg(<$type>::try_marshal_from_relative_value(Value::I64($arg), &host)?, &host)?),*); + + if host.tracing_enabled() + { + let dyn_res: Result<&dyn core::fmt::Debug,&HostError> = match &res { + Ok(ref ok) => Ok(ok), + Err(err) => Err(err) + }; + host.trace_env_ret(&core::stringify!($fn_id), &dyn_res)?; + } + + // On the off chance we got an error with no context, we can + // at least attach some here "at each host function call", + // fairly systematically. This will cause the context to + // propagate back through wasmi to its caller. + let res = host.augment_err_result(res); + + let res = match res { + Ok(ok) => { + let ok = ok.check_env_arg(&host)?; + let val: Value = ok.marshal_relative_from_self(&host)?; + if let Value::I64(v) = val { + Ok((v,)) + } else { + Err(BadSignature.into()) + } + }, + Err(hosterr) => { + // We make a new HostError here to capture the escalation event itself. + let escalation: HostError = + host.error(hosterr.error, + concat!("escalating error to VM trap from failed host function call: ", + stringify!($fn_id)), &[]); + let trap: Trap = escalation.into(); + Err(trap) + } + }; + + // This is where the Host->VM boundary is crossed. + // We supply the remaining host budget as fuel to the VM. + let caller = vmcaller.try_mut_wasmtime().map_err(|e| Trap::from(HostError::from(e)))?; + let added_fuel = FuelRefillable::add_fuel_to_vm(caller, &host).map_err(|he| Trap::from(he))?; + host.set_last_vm_fuel(added_fuel)?; + + Ok(res?) + } + )* + )* + }; +} + + call_macro_with_all_host_functions! { generate_wasmtime_dispatch_functions } +} diff --git a/soroban-env-host/src/vm/fuel_refillable.rs b/soroban-env-host/src/vm/fuel_refillable.rs index 68448fcef..5f36b9a69 100644 --- a/soroban-env-host/src/vm/fuel_refillable.rs +++ b/soroban-env-host/src/vm/fuel_refillable.rs @@ -4,10 +4,17 @@ use crate::{ Host, HostError, }; +use soroban_env_common::Error; use wasmi::{errors::FuelError, Caller, Store}; pub(crate) trait FuelRefillable { - fn fuel_consumed(&self) -> Result; + // Returns the amount of fuel consumed by the VM since the last call to + // `reset_fuel` / `add_fuel`. This is somewhat error-prone but the idea is + // that some VMs keep track of "how much fuel is left" and others keep track + // of "how much fuel was consumed". The former needs to be provided with the + // actual last fuel amount that the VM was filled with, in order to + // calculate the consumed amount. + fn fuel_consumed(&self, last_fuel: u64) -> Result; fn fuel_total(&self) -> Result; @@ -16,10 +23,13 @@ pub(crate) trait FuelRefillable { fn reset_fuel(&mut self) -> Result<(), HostError>; fn is_clean(&self) -> Result { - Ok(self.fuel_consumed()? == 0 && self.fuel_total()? == 0) + Ok(self.fuel_consumed(self.fuel_total()?)? == 0 && self.fuel_total()? == 0) } - fn add_fuel_to_vm(&mut self, host: &Host) -> Result<(), HostError> { + // Asserts that the VM has no fuel in it, then calculates the current amount + // of VM fuel the host's current CPU budget represents, and adds that fuel + // to the VM. Returns the amount of fuel added. + fn add_fuel_to_vm(&mut self, host: &Host) -> Result { if !self.is_clean()? { return Err(host.err( ScErrorType::WasmVm, @@ -29,13 +39,20 @@ pub(crate) trait FuelRefillable { )); } let fuel = host.as_budget().get_wasmi_fuel_remaining()?; - self.add_fuel(fuel) + self.add_fuel(fuel)?; + Ok(fuel) } - fn return_fuel_to_host(&mut self, host: &Host) -> Result<(), HostError> { - let fuel = self.fuel_consumed()?; + // Computes the amount of fuel consumed by the VM since the last call to + // `add_fuel_to_vm`, and charges it to the host's CPU budget, logically + // accounting for a "return" of the remainder that was _not_ consumed to the + // host for further use. Takes the last fuel amount supplied to the VM + // (which was returned from `add_fuel_to_vm`) as an argument, in case the VM + // does not keep track of its fuel consumption, only remaining balance. + fn return_fuel_to_host(&mut self, host: &Host, last_fuel: u64) -> Result<(), HostError> { + let fuel_consumed = self.fuel_consumed(last_fuel)?; host.as_budget() - .bulk_charge(ContractCostType::WasmInsnExec, fuel, None)?; + .bulk_charge(ContractCostType::WasmInsnExec, fuel_consumed, None)?; self.reset_fuel() } } @@ -43,7 +60,7 @@ pub(crate) trait FuelRefillable { macro_rules! impl_refillable_for_store { ($store: ty) => { impl<'a> FuelRefillable for $store { - fn fuel_consumed(&self) -> Result { + fn fuel_consumed(&self, _initial_fuel: u64) -> Result { self.fuel_consumed().ok_or_else(|| { HostError::from(wasmi::Error::Store(FuelError::FuelMeteringDisabled)) }) @@ -69,3 +86,41 @@ macro_rules! impl_refillable_for_store { } impl_refillable_for_store!(Store); impl_refillable_for_store!(Caller<'a, Host>); + +const VM_INTERNAL_ERROR: Error = + Error::from_type_and_code(ScErrorType::WasmVm, ScErrorCode::InternalError); + +const WASMTIME_FUEL_FACTOR: u64 = 1; + +macro_rules! impl_refillable_for_wasmtime_store { + ($store: ty) => { + impl<'a> FuelRefillable for $store { + fn fuel_consumed(&self, initial_fuel: u64) -> Result { + let fuel = self.fuel_total()?; + Ok(initial_fuel.saturating_sub(fuel)) + } + + fn fuel_total(&self) -> Result { + self.get_fuel() + .map(|fuel| fuel.saturating_div(WASMTIME_FUEL_FACTOR)) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + + fn add_fuel(&mut self, fuel: u64) -> Result<(), HostError> { + let existing_fuel = self.fuel_total()?; + let new_fuel = existing_fuel + .saturating_add(fuel) + .saturating_mul(WASMTIME_FUEL_FACTOR); + self.set_fuel(new_fuel) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + + fn reset_fuel(&mut self) -> Result<(), HostError> { + self.set_fuel(0) + .map_err(|_| HostError::from(VM_INTERNAL_ERROR)) + } + } + }; +} +impl_refillable_for_wasmtime_store!(wasmtime::Store); +impl_refillable_for_wasmtime_store!(wasmtime::Caller<'a, Host>); diff --git a/soroban-env-host/src/vm/func_info.rs b/soroban-env-host/src/vm/func_info.rs index d70d9faa5..1435543ae 100644 --- a/soroban-env-host/src/vm/func_info.rs +++ b/soroban-env-host/src/vm/func_info.rs @@ -20,6 +20,12 @@ pub(crate) struct HostFuncInfo { /// into a Func in the Linker. pub(crate) wrap: fn(&mut Linker) -> Result<&mut Linker, LinkerError>, + /// Function that takes a wasmtime::Linker and adds a dispatch function + /// for this host function, with the specific type of the dispatch function, + /// into a Func in the Linker. + pub(crate) wrap_wasmtime: + fn(&mut wasmtime::Linker) -> Result<&mut wasmtime::Linker, wasmtime::Error>, + /// Minimal supported protocol version of this host function pub(crate) min_proto: Option, @@ -46,6 +52,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: Some($min_proto), max_proto: Some($max_proto), } @@ -56,6 +63,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: Some($min_proto), max_proto: None, } @@ -66,6 +74,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: None, max_proto: Some($max_proto), } @@ -76,6 +85,7 @@ macro_rules! host_function_info_helper { fn_str: $fn_id, arity: fn_arity!($args), wrap: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::$func_id), + wrap_wasmtime: |linker| linker.func_wrap($mod_str, $fn_id, dispatch::wasmtime_dispatch::$func_id), min_proto: None, max_proto: None, } diff --git a/soroban-env-host/src/vm/module_cache.rs b/soroban-env-host/src/vm/module_cache.rs index 1736ea018..cc695c5c6 100644 --- a/soroban-env-host/src/vm/module_cache.rs +++ b/soroban-env-host/src/vm/module_cache.rs @@ -3,10 +3,10 @@ use super::{ parsed_module::{CompilationContext, ParsedModule, VersionedContractCodeCostInputs}, }; use crate::{ - budget::{get_wasmi_config, AsBudget, Budget}, + budget::{get_wasmi_config, get_wasmtime_config, AsBudget, Budget}, host::metered_clone::{MeteredClone, MeteredContainer}, xdr::{Hash, ScErrorCode, ScErrorType}, - Host, HostError, MeteredOrdMap, + ErrorHandler, Host, HostError, MeteredOrdMap, }; use std::{ collections::{BTreeMap, BTreeSet}, @@ -21,7 +21,9 @@ use std::{ #[derive(Clone, Default)] pub struct ModuleCache { pub(crate) wasmi_engine: wasmi::Engine, + pub(crate) wasmtime_engine: wasmtime::Engine, pub(crate) wasmi_linker: wasmi::Linker, + pub(crate) wasmtime_linker: wasmtime::Linker, modules: ModuleCacheMap, } @@ -106,17 +108,24 @@ impl ModuleCache { let wasmi_config = get_wasmi_config(host.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(host.as_budget())?; + let wasmtime_engine = host.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + let modules = ModuleCacheMap::MeteredSingleUseMap(MeteredOrdMap::new()); let wasmi_linker = wasmi::Linker::new(&wasmi_engine); + let wasmtime_linker = wasmtime::Linker::new(&wasmtime_engine); let mut cache = Self { wasmi_engine, + wasmtime_engine, modules, wasmi_linker, + wasmtime_linker, }; // Now add the contracts and rebuild linkers restricted to them. cache.add_stored_contracts(host)?; cache.wasmi_linker = cache.make_minimal_wasmi_linker_for_cached_modules(host)?; + cache.wasmtime_linker = cache.make_minimal_wasmtime_linker_for_cached_modules(host)?; Ok(cache) } @@ -124,14 +133,21 @@ impl ModuleCache { let wasmi_config = get_wasmi_config(context.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(context.as_budget())?; + let wasmtime_engine = + context.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + let modules = ModuleCacheMap::UnmeteredReusableMap(Arc::new(Mutex::new(BTreeMap::new()))); let wasmi_linker = Host::make_maximal_wasmi_linker(context, &wasmi_engine)?; + let wasmtime_linker = Host::make_maximal_wasmtime_linker(context, &wasmtime_engine)?; Ok(Self { wasmi_engine, + wasmtime_engine, modules, wasmi_linker, + wasmtime_linker, }) } @@ -244,6 +260,7 @@ impl ModuleCache { context, curr_ledger_protocol, &self.wasmi_engine, + &self.wasmtime_engine, &wasm, cost_inputs, )?; @@ -300,6 +317,15 @@ impl ModuleCache { }) } + fn make_minimal_wasmtime_linker_for_cached_modules( + &self, + host: &Host, + ) -> Result, HostError> { + self.with_minimal_import_symbols(host, |symbols| { + Host::make_minimal_wasmtime_linker_for_symbols(host, &self.wasmtime_engine, symbols) + }) + } + pub fn contains_module( &self, wasm_hash: &Hash, diff --git a/soroban-env-host/src/vm/parsed_module.rs b/soroban-env-host/src/vm/parsed_module.rs index 84f320172..24e52743a 100644 --- a/soroban-env-host/src/vm/parsed_module.rs +++ b/soroban-env-host/src/vm/parsed_module.rs @@ -3,6 +3,7 @@ use crate::{ err, host::metered_clone::MeteredContainer, meta, + vm::get_wasmtime_config, xdr::{ ContractCostType, Limited, ReadXdr, ScEnvMetaEntry, ScEnvMetaEntryInterfaceVersion, ScErrorCode, ScErrorType, @@ -147,6 +148,7 @@ impl CompilationContext for Host {} /// from the module when it was parsed. pub struct ParsedModule { pub wasmi_module: wasmi::Module, + pub wasmtime_module: wasmtime::Module, pub proto_version: u32, pub cost_inputs: VersionedContractCodeCostInputs, } @@ -156,14 +158,21 @@ impl ParsedModule { context: &Ctx, curr_ledger_protocol: u32, wasmi_engine: &wasmi::Engine, + wasmtime_engine: &wasmtime::Engine, wasm: &[u8], cost_inputs: VersionedContractCodeCostInputs, ) -> Result, HostError> { cost_inputs.charge_for_parsing(context.as_budget())?; - let (wasmi_module, proto_version) = - Self::parse_wasm(context, curr_ledger_protocol, wasmi_engine, wasm)?; + let (wasmi_module, wasmtime_module, proto_version) = Self::parse_wasm( + context, + curr_ledger_protocol, + wasmi_engine, + wasmtime_engine, + wasm, + )?; Ok(Arc::new(Self { wasmi_module, + wasmtime_module, proto_version, cost_inputs, })) @@ -179,6 +188,26 @@ impl ParsedModule { // we'll leave some future-proofing room here. The important point // is to not be introducing a DoS vector. const SYM_LEN_LIMIT: usize = 10; + + #[cfg(feature = "wasmtime")] + #[allow(unused_variables)] + let symbols: BTreeSet<(&str, &str)> = self + .wasmtime_module + .imports() + .filter_map(|i| { + if i.ty().func().is_some() { + let mod_str = i.module(); + let fn_str = i.name(); + if mod_str.len() < SYM_LEN_LIMIT && fn_str.len() < SYM_LEN_LIMIT { + return Some((mod_str, fn_str)); + } + } + None + }) + .collect(); + + #[cfg(feature = "wasmi")] + #[allow(unused_variables)] let symbols: BTreeSet<(&str, &str)> = self .wasmi_module .imports() @@ -210,6 +239,16 @@ impl ParsedModule { }) } + pub fn make_wasmtime_linker(&self, host: &Host) -> Result, HostError> { + self.with_import_symbols(host, |symbols| { + Host::make_minimal_wasmtime_linker_for_symbols( + host, + self.wasmtime_module.engine(), + symbols, + ) + }) + } + pub fn new_with_isolated_engine( host: &Host, wasm: &[u8], @@ -219,10 +258,14 @@ impl ParsedModule { let wasmi_config = crate::vm::get_wasmi_config(host.as_budget())?; let wasmi_engine = wasmi::Engine::new(&wasmi_config); + let wasmtime_config = get_wasmtime_config(host.as_budget())?; + let wasmtime_engine = host.map_wasmtime_error(wasmtime::Engine::new(&wasmtime_config))?; + Self::new( host, host.get_ledger_protocol_version()?, &wasmi_engine, + &wasmtime_engine, wasm, cost_inputs, ) @@ -233,17 +276,22 @@ impl ParsedModule { context: &Ctx, curr_ledger_protocol: u32, wasmi_engine: &wasmi::Engine, + wasmtime_engine: &wasmtime::Engine, wasm: &[u8], - ) -> Result<(wasmi::Module, u32), HostError> { + ) -> Result<(wasmi::Module, wasmtime::Module, u32), HostError> { let module = { let _span = tracy_span!("wasmi::Module::new"); context.map_err(wasmi::Module::new(&wasmi_engine, wasm))? }; + let wasmtime_module = { + let _span = tracy_span!("wasmtime::Module::new"); + context.map_wasmtime_error(wasmtime::Module::new(&wasmtime_engine, &wasm))? + }; Self::check_max_args(context, &module)?; let interface_version = Self::check_meta_section(context, curr_ledger_protocol, &module)?; let contract_proto = interface_version.protocol; - Ok((module, contract_proto)) + Ok((module, wasmtime_module, contract_proto)) } fn check_contract_interface_version( @@ -327,7 +375,10 @@ impl ParsedModule { Ok(()) } - pub(crate) fn check_contract_imports_match_host_protocol(&self, host: &Host) -> Result<(), HostError> { + pub(crate) fn check_contract_imports_match_host_protocol( + &self, + host: &Host, + ) -> Result<(), HostError> { // We perform instantiation-time protocol version gating of // all module-imported symbols here. // Reasons for doing link-time instead of run-time check: