diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 0de6f639..db13a359 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -114,6 +114,8 @@ jobs: strategy: matrix: include: + - command: clippy + args: -p fuel-core-wasm-executor --target wasm32-unknown-unknown --no-default-features - command: clippy args: --all-targets --all-features - command: check @@ -122,6 +124,8 @@ jobs: args: --all-features --workspace --no-deps - command: make args: check --locked + - command: test + args: --workspace - command: test args: --all-features --workspace - command: test diff --git a/CHANGELOG.md b/CHANGELOG.md index c1b8a369..3b6957bc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ Description of the upcoming release here. ### Added +- [#1716](https://github.com/FuelLabs/fuel-core/pull/1716): Added support of WASM state transition along with upgradable execution that works with native(std) and WASM(non-std) executors. The `fuel-core` now requires a `wasm32-unknown-unknown` target to build. - [#1770](https://github.com/FuelLabs/fuel-core/pull/1770): Add the new L1 event type for forced transactions. - [#1767](https://github.com/FuelLabs/fuel-core/pull/1767): Added consensus parameters version and state transition version to the `ApplicationHeader` to describe what was used to produce this block. - [#1760](https://github.com/FuelLabs/fuel-core/pull/1760): Added tests to verify that the network operates with a custom chain id and base asset id. diff --git a/Cargo.lock b/Cargo.lock index ae99b4d1..698d4aa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -182,6 +182,12 @@ version = "1.0.81" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247" +[[package]] +name = "arbitrary" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" + [[package]] name = "arrayref" version = "0.3.7" @@ -773,6 +779,15 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" +[[package]] +name = "bincode" +version = "1.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b1f45e9417d87227c7a56d22e471c6206462cba514c7590c09aff4cf6d1ddcad" +dependencies = [ + "serde", +] + [[package]] name = "bindgen" version = "0.65.1" @@ -1431,6 +1446,115 @@ dependencies = [ "libc", ] +[[package]] +name = "cranelift-bforest" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "16d5521e2abca66bbb1ddeecbb6f6965c79160352ae1579b39f8c86183895c24" +dependencies = [ + "cranelift-entity", +] + +[[package]] +name = "cranelift-codegen" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ef40a4338a47506e832ac3e53f7f1375bc59351f049a8379ff736dd02565bd95" +dependencies = [ + "bumpalo", + "cranelift-bforest", + "cranelift-codegen-meta", + "cranelift-codegen-shared", + "cranelift-control", + "cranelift-entity", + "cranelift-isle", + "gimli", + "hashbrown 0.14.3", + "log", + "regalloc2", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-codegen-meta" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d24cd5d85985c070f73dfca07521d09086362d1590105ba44b0932bf33513b61" +dependencies = [ + "cranelift-codegen-shared", +] + +[[package]] +name = "cranelift-codegen-shared" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0584c4363e3aa0a3c7cb98a778fbd5326a3709f117849a727da081d4051726c" + +[[package]] +name = "cranelift-control" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f25ecede098c6553fdba362a8e4c9ecb8d40138363bff47f9712db75be7f0571" +dependencies = [ + "arbitrary", +] + +[[package]] +name = "cranelift-entity" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6ea081a42f25dc4c5b248b87efdd87dcd3842a1050a37524ec5391e6172058cb" +dependencies = [ + "serde", + "serde_derive", +] + +[[package]] +name = "cranelift-frontend" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9796e712f5af797e247784f7518e6b0a83a8907d73d51526982d86ecb3a58b68" +dependencies = [ + "cranelift-codegen", + "log", + "smallvec", + "target-lexicon", +] + +[[package]] +name = "cranelift-isle" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f4a66ccad5782f15c80e9dd5af0df4acfe6e3eee98e8f7354a2e5c8ec3104bdd" + +[[package]] +name = "cranelift-native" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "285e80df1d9b79ded9775b285df68b920a277b84f88a7228d2f5bc31fcdc58eb" +dependencies = [ + "cranelift-codegen", + "libc", + "target-lexicon", +] + +[[package]] +name = "cranelift-wasm" +version = "0.105.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4135b0ab01fd16aa8f8821196e9e2fe15953552ccaef8ba5153be0ced04ef757" +dependencies = [ + "cranelift-codegen", + "cranelift-entity", + "cranelift-frontend", + "itertools 0.10.5", + "log", + "smallvec", + "wasmparser", + "wasmtime-types", +] + [[package]] name = "crc32fast" version = "1.4.0" @@ -1918,6 +2042,16 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories-next" +version = "2.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "339ee130d97a610ea5a5872d2bbb130fdf68884ff09d3028b81bec8a1ac23bbc" +dependencies = [ + "cfg-if", + "dirs-sys-next", +] + [[package]] name = "dirs" version = "4.0.0" @@ -2590,6 +2724,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 = "fastrand" version = "1.9.0" @@ -2742,6 +2882,7 @@ dependencies = [ "fuel-core-trace", "fuel-core-txpool", "fuel-core-types", + "fuel-core-upgradable-executor", "futures", "hex", "hyper", @@ -2851,7 +2992,7 @@ dependencies = [ "rand", "serde", "serde_json", - "serde_with 3.7.0", + "serde_with", "strum 0.25.0", "tempfile", "tracing", @@ -2951,6 +3092,7 @@ dependencies = [ "fuel-core-types", "hex", "parking_lot", + "serde", "tracing", ] @@ -3035,7 +3177,7 @@ dependencies = [ "quick-protobuf-codec 0.3.1", "rand", "serde", - "serde_with 1.14.0", + "serde_with", "sha2 0.10.8", "thiserror", "tokio", @@ -3139,6 +3281,7 @@ dependencies = [ "impl-tools", "itertools 0.12.1", "mockall", + "num_enum", "paste", "postcard", "primitive-types", @@ -3255,6 +3398,32 @@ dependencies = [ "zeroize", ] +[[package]] +name = "fuel-core-upgradable-executor" +version = "0.23.0" +dependencies = [ + "anyhow", + "fuel-core-executor", + "fuel-core-storage", + "fuel-core-types", + "fuel-core-wasm-executor", + "lazy_static", + "postcard", + "wasmtime", +] + +[[package]] +name = "fuel-core-wasm-executor" +version = "0.23.0" +dependencies = [ + "anyhow", + "fuel-core-executor", + "fuel-core-storage", + "fuel-core-types", + "postcard", + "proptest", +] + [[package]] name = "fuel-crypto" version = "0.47.1" @@ -3597,6 +3766,11 @@ name = "gimli" version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4271d37baee1b8c7e4b708028c57d816cf9d2434acb33a549475f78c181f6253" +dependencies = [ + "fallible-iterator", + "indexmap 2.2.5", + "stable_deref_trait", +] [[package]] name = "glob" @@ -4192,6 +4366,7 @@ checksum = "7b0b929d511467233429c45a44ac1dcaa21ba0f5ba11e4879e6ed28ddb4f9df4" dependencies = [ "equivalent", "hashbrown 0.14.3", + "serde", ] [[package]] @@ -4422,6 +4597,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "884e2677b40cc8c339eaefcb701c32ef1fd2493d71118dc0ca4b6a736c93bd67" + [[package]] name = "libc" version = "0.2.153" @@ -5131,6 +5312,15 @@ dependencies = [ "libc", ] +[[package]] +name = "mach" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b823e83b2affd8f40a9ee8c29dbc56404c1e34cd2710921f2801e2cf29527afa" +dependencies = [ + "libc", +] + [[package]] name = "match_cfg" version = "0.1.0" @@ -5168,6 +5358,15 @@ version = "2.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "523dc4f511e55ab87b694dc30d0f820d60906ef06413f93d4d7a1385599cc149" +[[package]] +name = "memfd" +version = "0.6.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2cffa4ad52c6f791f4f8b15f0c05f9824b2ced1160e88cc393d64fff9a8ac64" +dependencies = [ + "rustix 0.38.31", +] + [[package]] name = "memmap2" version = "0.9.4" @@ -5177,6 +5376,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "mime" version = "0.3.17" @@ -5581,6 +5789,9 @@ version = "0.32.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a6a622008b6e321afc04970976f62ee297fdbaa6f95318ca343e3eebb9648441" dependencies = [ + "crc32fast", + "hashbrown 0.14.3", + "indexmap 2.2.5", "memchr", ] @@ -6326,6 +6537,15 @@ version = "2.0.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "33cb294fe86a74cbcf50d4445b37da762029549ebeea341421c7c70370f86cac" +[[package]] +name = "psm" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5787f7cda34e3033a72192c018bc5883100330f362ef279a8cbccfce8bb4e874" +dependencies = [ + "cc", +] + [[package]] name = "publicsuffix" version = "2.2.3" @@ -6591,6 +6811,19 @@ dependencies = [ "thiserror", ] +[[package]] +name = "regalloc2" +version = "0.9.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ad156d539c879b7a24a363a2016d77961786e71f48f2e2fc8302a92abd2429a6" +dependencies = [ + "hashbrown 0.13.2", + "log", + "rustc-hash", + "slice-group-by", + "smallvec", +] + [[package]] name = "regex" version = "1.10.3" @@ -7241,16 +7474,6 @@ dependencies = [ "serde", ] -[[package]] -name = "serde_with" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "678b5a069e50bf00ecd22d0cd8ddf7c236f68581b03db652061ed5eb13a312ff" -dependencies = [ - "serde", - "serde_with_macros 1.5.2", -] - [[package]] name = "serde_with" version = "3.7.0" @@ -7263,22 +7486,10 @@ dependencies = [ "serde", "serde_derive", "serde_json", - "serde_with_macros 3.7.0", + "serde_with_macros", "time", ] -[[package]] -name = "serde_with_macros" -version = "1.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" -dependencies = [ - "darling 0.13.4", - "proc-macro2", - "quote", - "syn 1.0.109", -] - [[package]] name = "serde_with_macros" version = "3.7.0" @@ -7450,6 +7661,12 @@ dependencies = [ "autocfg", ] +[[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.1" @@ -7564,6 +7781,12 @@ 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" @@ -7826,6 +8049,12 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" +[[package]] +name = "target-lexicon" +version = "0.12.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1fc403891a21bcfb7c37834ba66a547a8f402146eba7265b5a6d88059c9ff2f" + [[package]] name = "tempfile" version = "3.10.1" @@ -8739,6 +8968,217 @@ version = "0.2.92" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "af190c94f2773fdb3729c55b007a722abb5384da03bc0986df4c289bf5567e96" +[[package]] +name = "wasm-encoder" +version = "0.41.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "972f97a5d8318f908dded23594188a90bcd09365986b1163e66d70170e5287ae" +dependencies = [ + "leb128", +] + +[[package]] +name = "wasmparser" +version = "0.121.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9dbe55c8f9d0dbd25d9447a5a889ff90c0cc3feaa7395310d3d826b2c703eaab" +dependencies = [ + "bitflags 2.4.2", + "indexmap 2.2.5", + "semver", +] + +[[package]] +name = "wasmtime" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8106d7d22d63d1bcb940e22dcc7b03e46f0fc8bfbaf2fd7b6cb8f448f9449774" +dependencies = [ + "anyhow", + "bincode", + "bumpalo", + "cfg-if", + "gimli", + "indexmap 2.2.5", + "libc", + "log", + "object", + "once_cell", + "paste", + "rayon", + "rustix 0.38.31", + "serde", + "serde_derive", + "serde_json", + "target-lexicon", + "wasmparser", + "wasmtime-cache", + "wasmtime-cranelift", + "wasmtime-environ", + "wasmtime-jit-icache-coherence", + "wasmtime-runtime", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-asm-macros" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b0cf02cea951ace34ee3b0e64b7f446c3519d1c95ad75bc5330f405e275ee8f" +dependencies = [ + "cfg-if", +] + +[[package]] +name = "wasmtime-cache" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3249204a71d728d53fb3eea18afd0473f87e520445707a4d567ac4da0bb3eb5d" +dependencies = [ + "anyhow", + "base64 0.21.7", + "bincode", + "directories-next", + "log", + "rustix 0.38.31", + "serde", + "serde_derive", + "sha2 0.10.8", + "toml 0.5.11", + "windows-sys 0.52.0", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "wasmtime-cranelift" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "595abdb067acdc812ab0f21d8d46d5aa4022392aa7c3e0632c20bff9ec49ffb4" +dependencies = [ + "anyhow", + "cfg-if", + "cranelift-codegen", + "cranelift-control", + "cranelift-entity", + "cranelift-frontend", + "cranelift-native", + "cranelift-wasm", + "gimli", + "log", + "object", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-cranelift-shared", + "wasmtime-environ", + "wasmtime-versioned-export-macros", +] + +[[package]] +name = "wasmtime-cranelift-shared" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8c24c1fdea167b992d82ebe76471fd1cbe7b0b406bc72f9250f86353000134e" +dependencies = [ + "anyhow", + "cranelift-codegen", + "cranelift-control", + "cranelift-native", + "gimli", + "object", + "target-lexicon", + "wasmtime-environ", +] + +[[package]] +name = "wasmtime-environ" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3279d510005358141550d8a90a5fc989d7e81748e5759d582fe6bfdcbf074a04" +dependencies = [ + "anyhow", + "bincode", + "cranelift-entity", + "gimli", + "indexmap 2.2.5", + "log", + "object", + "serde", + "serde_derive", + "target-lexicon", + "thiserror", + "wasmparser", + "wasmtime-types", +] + +[[package]] +name = "wasmtime-jit-icache-coherence" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "866634605089b4632b32226b54aa3670d72e1849f9fc425c7e50b3749c2e6df3" +dependencies = [ + "cfg-if", + "libc", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-runtime" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e11185c88cadf595d228f5ae4ff9b4badbf9ca98dcb37b0310c36e31fa74867f" +dependencies = [ + "anyhow", + "cc", + "cfg-if", + "indexmap 2.2.5", + "libc", + "log", + "mach", + "memfd", + "memoffset", + "paste", + "psm", + "rustix 0.38.31", + "sptr", + "wasm-encoder", + "wasmtime-asm-macros", + "wasmtime-environ", + "wasmtime-versioned-export-macros", + "wasmtime-wmemcheck", + "windows-sys 0.52.0", +] + +[[package]] +name = "wasmtime-types" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f32377cbd827bee06fcb2f6bf97b0477fdcc86888bbe6db7b9cab8e644082e0a" +dependencies = [ + "cranelift-entity", + "serde", + "serde_derive", + "thiserror", + "wasmparser", +] + +[[package]] +name = "wasmtime-versioned-export-macros" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4ab8d7566d206c42f8cf1d4ac90c5e40d3582e8eabad9b3b67e9e73c61fc47a1" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.53", +] + +[[package]] +name = "wasmtime-wmemcheck" +version = "18.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3847d969bd203b8cd239f89581e52432a0f00b8c5c9bc917be2fccd7542c4f2f" + [[package]] name = "web-sys" version = "0.3.69" diff --git a/Cargo.toml b/Cargo.toml index 3b513e03..73b84cd7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,6 +23,8 @@ members = [ "crates/services/relayer", "crates/services/sync", "crates/services/txpool", + "crates/services/upgradable-executor", + "crates/services/upgradable-executor/wasm-executor", "crates/storage", "crates/trace", "crates/types", @@ -63,17 +65,19 @@ fuel-core-services = { version = "0.23.0", path = "./crates/services" } fuel-core-consensus-module = { version = "0.23.0", path = "./crates/services/consensus_module" } fuel-core-bft = { version = "0.23.0", path = "./crates/services/consensus_module/bft" } fuel-core-poa = { version = "0.23.0", path = "./crates/services/consensus_module/poa" } -fuel-core-executor = { version = "0.23.0", path = "./crates/services/executor" } +fuel-core-executor = { version = "0.23.0", path = "./crates/services/executor", default-features = false } fuel-core-importer = { version = "0.23.0", path = "./crates/services/importer" } fuel-core-p2p = { version = "0.23.0", path = "./crates/services/p2p" } fuel-core-producer = { version = "0.23.0", path = "./crates/services/producer" } fuel-core-relayer = { version = "0.23.0", path = "./crates/services/relayer" } fuel-core-sync = { version = "0.23.0", path = "./crates/services/sync" } fuel-core-txpool = { version = "0.23.0", path = "./crates/services/txpool" } -fuel-core-storage = { version = "0.23.0", path = "./crates/storage" } +fuel-core-storage = { version = "0.23.0", path = "./crates/storage", default-features = false } fuel-core-trace = { version = "0.23.0", path = "./crates/trace" } fuel-core-types = { version = "0.23.0", path = "./crates/types", default-features = false } fuel-core-tests = { version = "0.0.0", path = "./tests" } +fuel-core-upgradable-executor = { version = "0.23.0", path = "./crates/services/upgradable-executor" } +fuel-core-wasm-executor = { version = "0.23.0", path = "./crates/services/upgradable-executor/wasm-executor" } fuel-core-xtask = { version = "0.0.0", path = "./xtask" } # Fuel dependencies @@ -110,6 +114,7 @@ reqwest = { version = "0.11.16", default-features = false, features = [ "cookies", ] } mockall = "0.11" +num_enum = "0.7" test-case = "3.3" impl-tools = "0.10" test-strategy = "0.3" diff --git a/ci_checks.sh b/ci_checks.sh index 22c3bb9f..5e042f03 100755 --- a/ci_checks.sh +++ b/ci_checks.sh @@ -12,6 +12,7 @@ npx prettier --check "**/Cargo.toml" && cargo +nightly fmt --all -- --check && cargo sort -w --check && source .github/workflows/scripts/verify_openssl.sh && +cargo clippy -p fuel-core-wasm-executor --target wasm32-unknown-unknown --no-default-features && cargo clippy --all-targets --all-features && cargo doc --all-features --workspace --no-deps && cargo make check --locked && @@ -21,6 +22,7 @@ cargo check -p fuel-core-storage --target wasm32-unknown-unknown --no-default-fe cargo check -p fuel-core-client --target wasm32-unknown-unknown --no-default-features && cargo check -p fuel-core-chain-config --target wasm32-unknown-unknown --no-default-features && cargo check -p fuel-core-executor --target wasm32-unknown-unknown --no-default-features && +cargo test --workspace && cargo test --all-features --workspace && cargo test -p fuel-core --no-default-features && cargo test -p fuel-core-client --no-default-features && diff --git a/crates/database/src/lib.rs b/crates/database/src/lib.rs index c5273fa6..0a9000ab 100644 --- a/crates/database/src/lib.rs +++ b/crates/database/src/lib.rs @@ -78,7 +78,7 @@ impl From for StorageError { impl From for ExecutorError { fn from(e: Error) -> Self { - ExecutorError::StorageError(anyhow::anyhow!(StorageError::from(e))) + ExecutorError::StorageError(format!("{}", StorageError::from(e))) } } diff --git a/crates/fuel-core/Cargo.toml b/crates/fuel-core/Cargo.toml index 5e6f5fd3..655b50e5 100644 --- a/crates/fuel-core/Cargo.toml +++ b/crates/fuel-core/Cargo.toml @@ -35,6 +35,7 @@ fuel-core-storage = { workspace = true } fuel-core-sync = { workspace = true, optional = true } fuel-core-txpool = { workspace = true } fuel-core-types = { workspace = true, features = ["serde"] } +fuel-core-upgradable-executor = { workspace = true } futures = { workspace = true } hex = { version = "0.4", features = ["serde"] } hyper = { workspace = true } @@ -67,6 +68,9 @@ fuel-core-services = { path = "./../services", features = ["test-helpers"] } fuel-core-storage = { path = "./../storage", features = ["test-helpers"] } fuel-core-trace = { path = "./../trace" } fuel-core-types = { path = "./../types", features = ["test-helpers"] } +fuel-core-upgradable-executor = { workspace = true, features = [ + "test-helpers", +] } mockall = { workspace = true } proptest = { workspace = true } test-case = { workspace = true } @@ -80,3 +84,4 @@ rocksdb = ["dep:rocksdb", "dep:tempfile", "dep:num_cpus"] test-helpers = ["fuel-core-p2p?/test-helpers", "fuel-core-storage/test-helpers"] # features to enable in production, but increase build times rocksdb-production = ["rocksdb", "rocksdb/jemalloc"] +wasm-executor = ["fuel-core-upgradable-executor/wasm-executor"] diff --git a/crates/fuel-core/src/executor.rs b/crates/fuel-core/src/executor.rs index 3773bd83..4106bbcd 100644 --- a/crates/fuel-core/src/executor.rs +++ b/crates/fuel-core/src/executor.rs @@ -6,16 +6,9 @@ mod tests { use crate as fuel_core; use fuel_core::database::Database; use fuel_core_executor::{ - executor::{ - block_component::PartialBlockComponent, - ExecutionData, - ExecutionOptions, - Executor, - OnceTransactionsSource, - }, + executor::OnceTransactionsSource, ports::RelayerPort, refs::ContractRef, - Config, }; use fuel_core_storage::{ tables::{ @@ -121,7 +114,6 @@ mod tests { Event as ExecutorEvent, ExecutionBlock, ExecutionResult, - ExecutionType, ExecutionTypes, TransactionExecutionResult, TransactionValidityError, @@ -130,13 +122,16 @@ mod tests { }, tai64::Tai64, }; + use fuel_core_upgradable_executor::{ + config::Config, + executor::Executor, + }; use itertools::Itertools; use rand::{ prelude::StdRng, Rng, SeedableRng, }; - use std::sync::Arc; #[derive(Clone, Debug)] struct DisabledRelayer; @@ -172,11 +167,7 @@ mod tests { database: Database, config: Config, ) -> Executor { - Executor { - database_view_provider: database, - relayer_view_provider: DisabledRelayer, - config: Arc::new(config), - } + Executor::new(database, DisabledRelayer, config) } pub(crate) fn setup_executable_script() -> (Create, Script) { @@ -310,14 +301,11 @@ mod tests { skipped_transactions, .. } = producer - .execute_and_commit( - ExecutionTypes::Production(block.into()), - Default::default(), - ) + .execute_and_commit(ExecutionTypes::Production(block.into())) .unwrap(); - let validation_result = verifier - .execute_and_commit(ExecutionTypes::Validation(block), Default::default()); + let validation_result = + verifier.execute_and_commit(ExecutionTypes::Validation(block)); assert!(validation_result.is_ok()); assert!(skipped_transactions.is_empty()); } @@ -334,10 +322,7 @@ mod tests { skipped_transactions, .. } = producer - .execute_and_commit( - ExecutionBlock::Production(block.into()), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Production(block.into())) .unwrap(); assert!(skipped_transactions.is_empty()); @@ -405,12 +390,12 @@ mod tests { gas_price_factor, ..Default::default() }; + let consensus_parameters = ConsensusParameters { + fee_params, + ..Default::default() + }; let config = Config { - coinbase_recipient: recipient, - consensus_parameters: ConsensusParameters { - fee_params, - ..Default::default() - }, + consensus_parameters: consensus_parameters.clone(), ..Default::default() }; @@ -423,8 +408,8 @@ mod tests { let mut producer = create_executor(database.clone(), config); let expected_fee_amount_1 = TransactionFee::checked_from_tx( - producer.config.consensus_parameters.gas_costs(), - producer.config.consensus_parameters.fee_params(), + consensus_parameters.gas_costs(), + consensus_parameters.fee_params(), &script, price, ) @@ -452,12 +437,13 @@ mod tests { ]), gas_price: price, gas_limit: u64::MAX, + coinbase_recipient: recipient, }, )) .unwrap() .into(); producer - .database_view_provider + .storage_view_provider .commit_changes(changes) .unwrap(); @@ -487,7 +473,7 @@ mod tests { } let (asset_id, amount) = producer - .database_view_provider + .storage_view_provider .latest_view() .contract_balances(recipient, None, None) .next() @@ -506,8 +492,8 @@ mod tests { .clone(); let expected_fee_amount_2 = TransactionFee::checked_from_tx( - producer.config.consensus_parameters.gas_costs(), - producer.config.consensus_parameters.fee_params(), + consensus_parameters.gas_costs(), + consensus_parameters.fee_params(), &script, price, ) @@ -533,12 +519,13 @@ mod tests { ]), gas_price: price, gas_limit: u64::MAX, + coinbase_recipient: recipient, }, )) .unwrap() .into(); producer - .database_view_provider + .storage_view_provider .commit_changes(changes) .unwrap(); @@ -560,7 +547,7 @@ mod tests { ); assert_eq!( second_mint.input_contract().utxo_id, - UtxoId::new(first_mint.cached_id().expect("Id exists"), 0) + UtxoId::new(first_mint.id(&consensus_parameters.chain_id), 0) ); assert_eq!( second_mint.input_contract().tx_pointer, @@ -579,7 +566,7 @@ mod tests { panic!("Invalid coinbase transaction"); } let (asset_id, amount) = producer - .database_view_provider + .storage_view_provider .latest_view() .contract_balances(recipient, None, None) .next() @@ -604,11 +591,13 @@ mod tests { .transaction() .clone(); - let mut config = Config::default(); + let mut consensus_parameters = ConsensusParameters::default(); + consensus_parameters.fee_params.gas_price_factor = gas_price_factor; + let config = Config { + consensus_parameters, + ..Default::default() + }; let recipient = [1u8; 32].into(); - config.coinbase_recipient = recipient; - - config.consensus_parameters.fee_params.gas_price_factor = gas_price_factor; let producer = create_executor(Default::default(), config); @@ -616,6 +605,7 @@ mod tests { .execute_without_commit_with_source(ExecutionTypes::DryRun(Components { header_to_produce: Default::default(), transactions_source: OnceTransactionsSource::new(vec![script.into()]), + coinbase_recipient: recipient, gas_price: 0, gas_limit: u64::MAX, })) @@ -646,7 +636,6 @@ mod tests { ..Default::default() }; let config = Config { - coinbase_recipient: recipient, consensus_parameters: ConsensusParameters { fee_params, ..Default::default() @@ -660,7 +649,7 @@ mod tests { .insert(&recipient, &[]) .expect("Should insert coinbase contract"); - let producer = create_executor(database.clone(), config); + let producer = create_executor(database.clone(), config.clone()); let ExecutionResult { block: produced_block, @@ -675,6 +664,7 @@ mod tests { ]), gas_price: price, gas_limit: u64::MAX, + coinbase_recipient: recipient, }, )) .unwrap() @@ -685,20 +675,17 @@ mod tests { let mut validator = create_executor( Default::default(), // Use the same config as block producer - producer.config.as_ref().clone(), + config, ); let ExecutionResult { block: validated_block, .. } = validator - .execute_and_commit( - ExecutionBlock::Validation(produced_block), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Validation(produced_block)) .unwrap(); assert_eq!(validated_block.transactions(), produced_txs); let (asset_id, amount) = validator - .database_view_provider + .storage_view_provider .latest_view() .contract_balances(recipient, None, None) .next() @@ -747,21 +734,24 @@ mod tests { .transaction() .clone(); - let config = Config { - coinbase_recipient: config_coinbase, - ..Default::default() - }; - let mut producer = create_executor(Default::default(), config); + let mut producer = + create_executor(Default::default(), Default::default()); let mut block = Block::default(); *block.transactions_mut() = vec![script.clone().into()]; - let ExecutionResult { tx_status, .. } = producer - .execute_and_commit( + let (ExecutionResult { tx_status, .. }, changes) = producer + .execute_without_commit_with_coinbase( ExecutionBlock::Production(block.into()), - Default::default(), + config_coinbase, + 0, ) - .expect("Should execute the block"); + .expect("Should execute the block") + .into(); + producer + .storage_view_provider + .commit_changes(changes) + .unwrap(); let receipts = tx_status[0].result.receipts(); if let Some(Receipt::Return { val, .. }) = receipts.first() { @@ -812,7 +802,7 @@ mod tests { }, ); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase if invalid"); assert!(matches!( validation_err, @@ -838,7 +828,7 @@ mod tests { let mut validator = create_executor(Default::default(), Default::default()); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase if invalid"); assert!(matches!( validation_err, @@ -852,7 +842,7 @@ mod tests { let mut validator = create_executor(Default::default(), Default::default()); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase is missing"); assert!(matches!(validation_err, ExecutorError::MintMissing)); } @@ -874,7 +864,7 @@ mod tests { let mut validator = create_executor(Default::default(), Default::default()); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase if invalid"); assert!(matches!( @@ -900,11 +890,18 @@ mod tests { *block.transactions_mut() = vec![mint.into()]; block.header_mut().recalculate_metadata(); - let mut config = Config::default(); - config.consensus_parameters.base_asset_id = [1u8; 32].into(); + let consensus_parameters = ConsensusParameters { + base_asset_id: [1u8; 32].into(), + ..Default::default() + }; + + let config = Config { + consensus_parameters, + ..Default::default() + }; let mut validator = create_executor(Default::default(), config); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase if invalid"); assert!(matches!( @@ -932,7 +929,7 @@ mod tests { let mut validator = create_executor(Default::default(), Default::default()); let validation_err = validator - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect_err("Expected error because coinbase if invalid"); assert!(matches!( validation_err, @@ -945,8 +942,14 @@ mod tests { #[test] fn executor_invalidates_missing_gas_input() { let mut rng = StdRng::seed_from_u64(2322u64); - let producer = create_executor(Default::default(), Default::default()); - let verifier = create_executor(Default::default(), Default::default()); + let consensus_parameters = ConsensusParameters::default(); + let config = Config { + consensus_parameters: consensus_parameters.clone(), + ..Default::default() + }; + let producer = create_executor(Default::default(), config.clone()); + + let verifier = create_executor(Default::default(), config); let gas_limit = 100; let max_fee = 1; @@ -963,22 +966,19 @@ mod tests { .finalize(); let tx: Transaction = script.into(); - let mut block = PartialFuelBlock { + let block = PartialFuelBlock { header: Default::default(), transactions: vec![tx.clone()], }; - let ExecutionData { + let ExecutionResult { skipped_transactions, + block, .. } = producer - .execute_block( - ExecutionType::Production(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ) - .unwrap(); + .execute_without_commit(ExecutionTypes::Production(block)) + .unwrap() + .into_result(); let produce_result = &skipped_transactions[0].1; assert!(matches!( produce_result, @@ -990,23 +990,16 @@ mod tests { )); // Produced block is valid - verifier - .execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ) - .unwrap(); + let ExecutionResult { mut block, .. } = verifier + .execute_without_commit(ExecutionTypes::Validation(block)) + .unwrap() + .into_result(); // Invalidate the block with Insufficient tx - block.transactions.insert(block.transactions.len() - 1, tx); - let verify_result = verifier.execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ); + let len = block.transactions().len(); + block.transactions_mut().insert(len - 1, tx); + let verify_result = + verifier.execute_without_commit(ExecutionTypes::Validation(block)); assert!(matches!( verify_result, Err(ExecutorError::InvalidTransaction( @@ -1023,7 +1016,7 @@ mod tests { let verifier = create_executor(Default::default(), Default::default()); - let mut block = PartialFuelBlock { + let block = PartialFuelBlock { header: Default::default(), transactions: vec![ Transaction::default_test_tx(), @@ -1031,17 +1024,14 @@ mod tests { ], }; - let ExecutionData { + let ExecutionResult { skipped_transactions, + block, .. } = producer - .execute_block( - ExecutionType::Production(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ) - .unwrap(); + .execute_without_commit(ExecutionTypes::Production(block)) + .unwrap() + .into_result(); let produce_result = &skipped_transactions[0].1; assert!(matches!( produce_result, @@ -1049,25 +1039,18 @@ mod tests { )); // Produced block is valid - verifier - .execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ) - .unwrap(); + let ExecutionResult { mut block, .. } = verifier + .execute_without_commit(ExecutionTypes::Validation(block)) + .unwrap() + .into_result(); // Make the block invalid by adding of the duplicating transaction + let len = block.transactions().len(); block - .transactions - .insert(block.transactions.len() - 1, Transaction::default_test_tx()); - let verify_result = verifier.execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - Default::default(), - ); + .transactions_mut() + .insert(len - 1, Transaction::default_test_tx()); + let verify_result = + verifier.execute_without_commit(ExecutionTypes::Validation(block)); assert!(matches!( verify_result, Err(ExecutorError::TransactionIdCollision(_)) @@ -1107,24 +1090,19 @@ mod tests { let verifier = create_executor(Default::default(), config); - let mut block = PartialFuelBlock { + let block = PartialFuelBlock { header: Default::default(), transactions: vec![tx.clone()], }; - let ExecutionData { + let ExecutionResult { skipped_transactions, + block, .. } = producer - .execute_block( - ExecutionType::Production(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ) - .unwrap(); + .execute_without_commit(ExecutionTypes::Production(block)) + .unwrap() + .into_result(); let produce_result = &skipped_transactions[0].1; assert!(matches!( produce_result, @@ -1134,27 +1112,16 @@ mod tests { )); // Produced block is valid - verifier - .execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ) - .unwrap(); + let ExecutionResult { mut block, .. } = verifier + .execute_without_commit(ExecutionTypes::Validation(block)) + .unwrap() + .into_result(); // Invalidate block by adding transaction with not existing coin - block.transactions.insert(block.transactions.len() - 1, tx); - let verify_result = verifier.execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ); + let len = block.transactions().len(); + block.transactions_mut().insert(len - 1, tx); + let verify_result = + verifier.execute_without_commit(ExecutionTypes::Validation(block)); assert!(matches!( verify_result, Err(ExecutorError::TransactionValidity( @@ -1189,10 +1156,7 @@ mod tests { *block.transactions_mut() = vec![tx]; let ExecutionResult { mut block, .. } = producer - .execute_and_commit( - ExecutionBlock::Production(block.into()), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Production(block.into())) .unwrap(); // modify change amount @@ -1202,8 +1166,8 @@ mod tests { } } - let verify_result = verifier - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()); + let verify_result = + verifier.execute_and_commit(ExecutionBlock::Validation(block)); assert!(matches!( verify_result, Err(ExecutorError::InvalidTransactionOutcome { transaction_id }) if transaction_id == tx_id @@ -1232,18 +1196,15 @@ mod tests { *block.transactions_mut() = vec![tx]; let ExecutionResult { mut block, .. } = producer - .execute_and_commit( - ExecutionBlock::Production(block.into()), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Production(block.into())) .unwrap(); // randomize transaction commitment block.header_mut().set_transaction_root(rng.gen()); block.header_mut().recalculate_metadata(); - let verify_result = verifier - .execute_and_commit(ExecutionBlock::Validation(block), Default::default()); + let verify_result = + verifier.execute_and_commit(ExecutionBlock::Validation(block)); assert!(matches!(verify_result, Err(ExecutorError::InvalidBlockId))) } @@ -1271,12 +1232,7 @@ mod tests { skipped_transactions, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); let err = &skipped_transactions[0].1; @@ -1358,12 +1314,7 @@ mod tests { skipped_transactions, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // `tx2` should be skipped. assert_eq!(block.transactions().len(), 2 /* coinbase and `tx1` */); @@ -1422,12 +1373,7 @@ mod tests { skipped_transactions, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // `tx` should be skipped. assert_eq!(skipped_transactions.len(), 1); @@ -1478,12 +1424,7 @@ mod tests { skipped_transactions, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // `tx` should be skipped. assert_eq!(skipped_transactions.len(), 1); @@ -1524,7 +1465,7 @@ mod tests { skipped_transactions, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); assert_eq!( block.transactions().len(), @@ -1569,7 +1510,7 @@ mod tests { }; let ExecutionResult { block, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // assert the tx coin is spent @@ -1628,7 +1569,7 @@ mod tests { let ExecutionResult { block, tx_status, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // Assert the balance and state roots should be the same before and after execution. @@ -1685,7 +1626,7 @@ mod tests { let ExecutionResult { block, tx_status, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // Assert the balance and state roots should be the same before and after execution. @@ -1788,7 +1729,7 @@ mod tests { let ExecutionResult { block, tx_status, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); let empty_state = (*sparse::empty_sum()).into(); @@ -1872,10 +1813,12 @@ mod tests { .clone(); let db = Database::default(); + let consensus_parameters = ConsensusParameters::default(); let mut executor = create_executor( db.clone(), Config { utxo_validation_default: false, + consensus_parameters: consensus_parameters.clone(), ..Default::default() }, ); @@ -1892,7 +1835,7 @@ mod tests { }; let ExecutionResult { block, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); let executed_tx = block.transactions()[1].as_script().unwrap(); @@ -1901,9 +1844,7 @@ mod tests { let mut new_tx = executed_tx.clone(); *new_tx.script_mut() = vec![]; - new_tx - .precompute(&executor.config.consensus_parameters.chain_id) - .unwrap(); + new_tx.precompute(&consensus_parameters.chain_id).unwrap(); let block = PartialFuelBlock { header: PartialBlockHeader { @@ -1924,6 +1865,7 @@ mod tests { transactions_source: OnceTransactionsSource::new(block.transactions), gas_price: 0, gas_limit: u64::MAX, + coinbase_recipient: Default::default(), })) .unwrap() .into_result(); @@ -1986,7 +1928,7 @@ mod tests { }; let _ = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // Assert the balance root should not be affected. @@ -2060,12 +2002,7 @@ mod tests { }; let ExecutionResult { block, events, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // assert the tx coin is spent @@ -2121,10 +2058,7 @@ mod tests { skipped_transactions, .. } = setup - .execute_and_commit( - ExecutionBlock::Production(first_block), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Production(first_block)) .unwrap(); assert!(skipped_transactions.is_empty()); @@ -2134,19 +2068,14 @@ mod tests { skipped_transactions, .. } = producer - .execute_without_commit( - ExecutionBlock::Production(second_block), - Default::default(), - ) + .execute_without_commit(ExecutionBlock::Production(second_block)) .unwrap() .into_result(); assert!(skipped_transactions.is_empty()); let verifier = create_executor(db, Default::default()); - let verify_result = verifier.execute_without_commit( - ExecutionBlock::Validation(second_block), - Default::default(), - ); + let verify_result = + verifier.execute_without_commit(ExecutionBlock::Validation(second_block)); assert!(verify_result.is_ok()); } @@ -2200,10 +2129,7 @@ mod tests { let mut setup = create_executor(db.clone(), Default::default()); setup - .execute_and_commit( - ExecutionBlock::Production(first_block), - Default::default(), - ) + .execute_and_commit(ExecutionBlock::Production(first_block)) .unwrap(); let producer = create_executor(db.clone(), Default::default()); @@ -2212,10 +2138,7 @@ mod tests { block: mut second_block, .. } = producer - .execute_without_commit( - ExecutionBlock::Production(second_block), - Default::default(), - ) + .execute_without_commit(ExecutionBlock::Production(second_block)) .unwrap() .into_result(); // Corrupt the utxo_id of the contract output @@ -2229,10 +2152,8 @@ mod tests { } let verifier = create_executor(db, Default::default()); - let verify_result = verifier.execute_without_commit( - ExecutionBlock::Validation(second_block), - Default::default(), - ); + let verify_result = + verifier.execute_without_commit(ExecutionBlock::Validation(second_block)); assert!(matches!( verify_result, @@ -2256,7 +2177,7 @@ mod tests { }; let ExecutionResult { block, .. } = executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); // ensure that all utxos with an amount are stored into the utxo set @@ -2307,7 +2228,7 @@ mod tests { }; executor - .execute_and_commit(ExecutionBlock::Production(block), Default::default()) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); for idx in 0..2 { @@ -2342,6 +2263,7 @@ mod tests { 1000, vec![], ) + .add_output(Output::change(rng.gen(), 1000, AssetId::BASE)) .finalize(); let message = message_from_input(&tx.inputs()[0], da_height); @@ -2381,21 +2303,11 @@ mod tests { }; let ExecutionResult { block, .. } = make_executor(&[&message]) - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .expect("block execution failed unexpectedly"); make_executor(&[&message]) - .execute_and_commit( - ExecutionBlock::Validation(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Validation(block)) .expect("block validation failed unexpectedly"); } @@ -2424,7 +2336,7 @@ mod tests { }; let mut exec = make_executor(&messages); - let view = exec.database_view_provider.latest_view(); + let view = exec.storage_view_provider.latest_view(); assert!(!view.message_is_spent(message_coin.nonce()).unwrap()); assert!(!view.message_is_spent(message_data.nonce()).unwrap()); @@ -2432,17 +2344,12 @@ mod tests { skipped_transactions, .. } = exec - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); assert_eq!(skipped_transactions.len(), 0); // Successful execution consumes `message_coin` and `message_data`. - let view = exec.database_view_provider.latest_view(); + let view = exec.storage_view_provider.latest_view(); assert!(view.message_is_spent(message_coin.nonce()).unwrap()); assert!(view.message_is_spent(message_data.nonce()).unwrap()); assert_eq!( @@ -2478,7 +2385,7 @@ mod tests { }; let mut exec = make_executor(&messages); - let view = exec.database_view_provider.latest_view(); + let view = exec.storage_view_provider.latest_view(); assert!(!view.message_is_spent(message_coin.nonce()).unwrap()); assert!(!view.message_is_spent(message_data.nonce()).unwrap()); @@ -2486,17 +2393,12 @@ mod tests { skipped_transactions, .. } = exec - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .unwrap(); assert_eq!(skipped_transactions.len(), 0); // We should spend only `message_coin`. The `message_data` should be unspent. - let view = exec.database_view_provider.latest_view(); + let view = exec.storage_view_provider.latest_view(); assert!(view.message_is_spent(message_coin.nonce()).unwrap()); assert!(!view.message_is_spent(message_data.nonce()).unwrap()); assert_eq!(*view.coin(&UtxoId::new(tx_id, 0)).unwrap().amount(), amount); @@ -2518,9 +2420,6 @@ mod tests { } = make_executor(&[]) // No messages in the db .execute_and_commit( ExecutionBlock::Production(block.clone().into()), - ExecutionOptions { - utxo_validation: true, - }, ) .unwrap(); let err = &skipped_transactions[0].1; @@ -2533,24 +2432,14 @@ mod tests { // Produced block is valid make_executor(&[]) // No messages in the db - .execute_and_commit( - ExecutionBlock::Validation(block.clone()), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Validation(block.clone())) .unwrap(); // Invalidate block by returning back `tx` with not existing message let index = block.transactions().len() - 1; block.transactions_mut().insert(index, tx); let res = make_executor(&[]) // No messages in the db - .execute_and_commit( - ExecutionBlock::Validation(block), - ExecutionOptions { - utxo_validation: true, - }, - ); + .execute_and_commit(ExecutionBlock::Validation(block)); assert!(matches!( res, Err(ExecutorError::TransactionValidity( @@ -2573,12 +2462,7 @@ mod tests { mut block, .. } = make_executor(&[&message]) - .execute_and_commit( - ExecutionBlock::Production(block.clone().into()), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block.clone().into())) .unwrap(); let err = &skipped_transactions[0].1; assert!(matches!( @@ -2590,23 +2474,14 @@ mod tests { // Produced block is valid make_executor(&[&message]) - .execute_and_commit( - ExecutionBlock::Validation(block.clone()), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Validation(block.clone())) .unwrap(); // Invalidate block by return back `tx` with not ready message. let index = block.transactions().len() - 1; block.transactions_mut().insert(index, tx); - let res = make_executor(&[&message]).execute_and_commit( - ExecutionBlock::Validation(block), - ExecutionOptions { - utxo_validation: true, - }, - ); + let res = make_executor(&[&message]) + .execute_and_commit(ExecutionBlock::Validation(block)); assert!(matches!( res, Err(ExecutorError::TransactionValidity( @@ -2631,12 +2506,7 @@ mod tests { skipped_transactions, .. } = make_executor(&[&message]) - .execute_and_commit( - ExecutionBlock::Production(block.clone().into()), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block.clone().into())) .unwrap(); let err = &skipped_transactions[0].1; assert!(matches!( @@ -2657,25 +2527,20 @@ mod tests { tx2.as_script_mut().unwrap().inputs_mut()[0] = tx1.as_script().unwrap().inputs()[0].clone(); - let mut block = PartialFuelBlock { + let block = PartialFuelBlock { header: Default::default(), transactions: vec![tx1, tx2.clone()], }; let exec = make_executor(&[&message]); - let ExecutionData { + let ExecutionResult { skipped_transactions, + block, .. } = exec - .execute_block( - ExecutionType::Production(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ) - .unwrap(); + .execute_without_commit(ExecutionTypes::Production(block)) + .unwrap() + .into_result(); // One of two transactions is skipped. assert_eq!(skipped_transactions.len(), 1); let err = &skipped_transactions[0].1; @@ -2688,27 +2553,16 @@ mod tests { // Produced block is valid let exec = make_executor(&[&message]); - exec.execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ) - .unwrap(); + let ExecutionResult { mut block, .. } = exec + .execute_without_commit(ExecutionTypes::Validation(block)) + .unwrap() + .into_result(); // Invalidate block by return back `tx2` transaction skipped during production. - block.transactions.insert(block.transactions.len() - 1, tx2); + let len = block.transactions().len(); + block.transactions_mut().insert(len - 1, tx2); let exec = make_executor(&[&message]); - let res = exec.execute_block( - ExecutionType::Validation(PartialBlockComponent::from_partial_block( - &mut block, - )), - ExecutionOptions { - utxo_validation: true, - }, - ); + let res = exec.execute_without_commit(ExecutionTypes::Validation(block)); assert!(matches!( res, Err(ExecutorError::TransactionValidity( @@ -2774,12 +2628,7 @@ mod tests { ); let ExecutionResult { tx_status, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .expect("Should execute the block"); let receipts = tx_status[0].result.receipts(); @@ -2843,12 +2692,7 @@ mod tests { ); let ExecutionResult { tx_status, .. } = executor - .execute_and_commit( - ExecutionBlock::Production(block), - ExecutionOptions { - utxo_validation: true, - }, - ) + .execute_and_commit(ExecutionBlock::Production(block)) .expect("Should execute the block"); let receipts = tx_status[0].result.receipts(); @@ -2862,8 +2706,10 @@ mod tests { let owner = Input::predicate_owner(&predicate); let amount = 1000; + let consensus_parameters = ConsensusParameters::default(); let config = Config { utxo_validation_default: true, + consensus_parameters: consensus_parameters.clone(), ..Default::default() }; @@ -2888,7 +2734,7 @@ mod tests { asset_id: Default::default(), }) .finalize(); - tx.estimate_predicates(&config.consensus_parameters.clone().into()) + tx.estimate_predicates(&consensus_parameters.clone().into()) .unwrap(); let db = &mut Database::default(); @@ -2920,6 +2766,7 @@ mod tests { .execute_without_commit_with_source(ExecutionTypes::Production(Components { header_to_produce: PartialBlockHeader::default(), transactions_source: OnceTransactionsSource::new(vec![tx.into()]), + coinbase_recipient: Default::default(), gas_price: 1, gas_limit: u64::MAX, })) @@ -3002,11 +2849,7 @@ mod tests { on_chain: Database, relayer: Database, ) -> Executor, Database> { - Executor { - database_view_provider: on_chain, - relayer_view_provider: relayer, - config: Arc::new(Default::default()), - } + Executor::new(on_chain, relayer, Default::default()) } struct Input { @@ -3086,10 +2929,7 @@ mod tests { let producer = create_relayer_executor(on_chain_db, relayer_db); let block = test_block(block_height.into(), block_da_height.into(), 0); let (result, changes) = producer - .execute_without_commit( - ExecutionTypes::Production(block.into()), - Default::default(), - )? + .execute_without_commit(ExecutionTypes::Production(block.into()))? .into(); // Then @@ -3148,10 +2988,7 @@ mod tests { // when let (result, _) = producer - .execute_without_commit( - ExecutionTypes::Production(block.into()), - Default::default(), - ) + .execute_without_commit(ExecutionTypes::Production(block.into())) .unwrap() .into(); @@ -3178,10 +3015,7 @@ mod tests { let producer = create_relayer_executor(on_chain_db, relayer_db); let block = test_block(block_height.into(), block_da_height.into(), 10); let (result, changes) = producer - .execute_without_commit( - ExecutionTypes::Production(block.into()), - Default::default(), - ) + .execute_without_commit(ExecutionTypes::Production(block.into())) .unwrap() .into(); @@ -3224,10 +3058,7 @@ mod tests { *block.transactions_mut() = vec![tx]; let producer = create_relayer_executor(on_chain_db, relayer_db); let (result, changes) = producer - .execute_without_commit( - ExecutionTypes::Production(block.into()), - Default::default(), - ) + .execute_without_commit(ExecutionTypes::Production(block.into())) .unwrap() .into(); diff --git a/crates/fuel-core/src/graphql_api/worker_service.rs b/crates/fuel-core/src/graphql_api/worker_service.rs index 9d1d2f5f..4fdc7501 100644 --- a/crates/fuel-core/src/graphql_api/worker_service.rs +++ b/crates/fuel-core/src/graphql_api/worker_service.rs @@ -47,6 +47,7 @@ use fuel_core_types::{ fuel_types::{ BlockHeight, Bytes32, + ChainId, }, services::{ block_importer::{ @@ -75,6 +76,7 @@ pub struct Task { tx_pool: TxPool, block_importer: BoxStream, database: D, + chain_id: ChainId, } impl Task @@ -89,7 +91,7 @@ where persist_transaction_status(&result, &mut transaction)?; // save the associated owner for each transaction in the block - index_tx_owners_for_block(block, &mut transaction)?; + index_tx_owners_for_block(block, &mut transaction, &self.chain_id)?; // save the transaction related information process_transactions(block.transactions().iter(), &mut transaction)?; @@ -172,6 +174,7 @@ where fn index_tx_owners_for_block( block: &Block, block_st_transaction: &mut T, + chain_id: &ChainId, ) -> anyhow::Result<()> where T: OffChainDatabase, @@ -183,9 +186,7 @@ where let tx_idx = u16::try_from(tx_idx).map_err(|e| { anyhow::anyhow!("The block has more than `u16::MAX` transactions, {}", e) })?; - let tx_id = tx.cached_id().expect( - "The imported block should contains only transactions with cached id", - ); + let tx_id = tx.id(chain_id); match tx { Transaction::Script(tx) => { inputs = tx.inputs().as_slice(); @@ -385,6 +386,7 @@ pub fn new_service( tx_pool: TxPool, block_importer: I, database: D, + chain_id: ChainId, ) -> ServiceRunner> where TxPool: ports::worker::TxPool, @@ -396,5 +398,6 @@ where tx_pool, block_importer, database, + chain_id, }) } diff --git a/crates/fuel-core/src/service/adapters.rs b/crates/fuel-core/src/service/adapters.rs index 58f0405f..0aadd0e0 100644 --- a/crates/fuel-core/src/service/adapters.rs +++ b/crates/fuel-core/src/service/adapters.rs @@ -9,7 +9,6 @@ use fuel_core_consensus_module::{ block_verifier::Verifier, RelayerConsensusConfig, }; -use fuel_core_executor::executor::Executor; use fuel_core_services::stream::BoxStream; use fuel_core_txpool::service::SharedState as TxPoolSharedState; #[cfg(feature = "p2p")] @@ -18,6 +17,7 @@ use fuel_core_types::{ fuel_types::BlockHeight, services::block_importer::SharedImportResult, }; +use fuel_core_upgradable_executor::executor::Executor; use std::sync::Arc; pub mod block_importer; @@ -87,13 +87,9 @@ impl ExecutorAdapter { pub fn new( database: Database, relayer_database: Database, - config: fuel_core_executor::Config, + config: fuel_core_upgradable_executor::config::Config, ) -> Self { - let executor = Executor { - database_view_provider: database, - relayer_view_provider: relayer_database, - config: Arc::new(config), - }; + let executor = Executor::new(database, relayer_database, config); Self { executor: Arc::new(executor), } diff --git a/crates/fuel-core/src/service/adapters/executor.rs b/crates/fuel-core/src/service/adapters/executor.rs index 7250c20e..07d3f2f4 100644 --- a/crates/fuel-core/src/service/adapters/executor.rs +++ b/crates/fuel-core/src/service/adapters/executor.rs @@ -43,7 +43,7 @@ impl ExecutorAdapter { block: ExecutionBlockWithSource, ) -> ExecutorResult> where - TxSource: fuel_core_executor::ports::TransactionsSource, + TxSource: fuel_core_executor::ports::TransactionsSource + Send + Sync + 'static, { self.executor.execute_without_commit_with_source(block) } diff --git a/crates/fuel-core/src/service/adapters/producer.rs b/crates/fuel-core/src/service/adapters/producer.rs index c60ce482..499f4be5 100644 --- a/crates/fuel-core/src/service/adapters/producer.rs +++ b/crates/fuel-core/src/service/adapters/producer.rs @@ -100,12 +100,14 @@ impl fuel_core_producer::ports::Executor> for ExecutorAdapter { header_to_produce, transactions_source, gas_price, + coinbase_recipient, gas_limit, } = component; self._execute_without_commit(ExecutionTypes::Production(Components { header_to_produce, transactions_source: OnceTransactionsSource::new(transactions_source), gas_price, + coinbase_recipient, gas_limit, })) } diff --git a/crates/fuel-core/src/service/sub_services.rs b/crates/fuel-core/src/service/sub_services.rs index 9a764407..49e7819e 100644 --- a/crates/fuel-core/src/service/sub_services.rs +++ b/crates/fuel-core/src/service/sub_services.rs @@ -62,12 +62,8 @@ pub fn init_sub_services( let executor = ExecutorAdapter::new( database.on_chain().clone(), database.relayer().clone(), - fuel_core_executor::Config { + fuel_core_upgradable_executor::config::Config { consensus_parameters: config.chain_config.consensus_parameters.clone(), - coinbase_recipient: config - .block_producer - .coinbase_recipient - .unwrap_or_default(), backtrace: config.vm.backtrace, utxo_validation_default: config.utxo_validation, }, @@ -201,6 +197,7 @@ pub fn init_sub_services( tx_pool_adapter.clone(), importer_adapter.clone(), database.off_chain().clone(), + config.chain_config.consensus_parameters.chain_id, ); let graphql_config = GraphQLConfig { diff --git a/crates/services/executor/Cargo.toml b/crates/services/executor/Cargo.toml index 496dc05e..28e12809 100644 --- a/crates/services/executor/Cargo.toml +++ b/crates/services/executor/Cargo.toml @@ -16,6 +16,7 @@ fuel-core-storage = { workspace = true } fuel-core-types = { workspace = true, default-features = false } hex = { version = "0.4", features = ["serde"] } parking_lot = { workspace = true } +serde = { workspace = true } tracing = { workspace = true } [dev-dependencies] diff --git a/crates/services/executor/src/config.rs b/crates/services/executor/src/config.rs deleted file mode 100644 index 16f5bc70..00000000 --- a/crates/services/executor/src/config.rs +++ /dev/null @@ -1,16 +0,0 @@ -use fuel_core_types::fuel_tx::{ - ConsensusParameters, - ContractId, -}; - -#[derive(Clone, Debug, Default)] -pub struct Config { - /// Network-wide common parameters used for validating the chain - pub consensus_parameters: ConsensusParameters, - /// The `ContractId` of the fee recipient. - pub coinbase_recipient: ContractId, - /// Print execution backtraces if transaction execution reverts. - pub backtrace: bool, - /// Default mode for utxo_validation - pub utxo_validation_default: bool, -} diff --git a/crates/services/executor/src/executor.rs b/crates/services/executor/src/executor.rs index 208fb371..a4f29f19 100644 --- a/crates/services/executor/src/executor.rs +++ b/crates/services/executor/src/executor.rs @@ -5,7 +5,6 @@ use crate::{ TransactionsSource, }, refs::ContractRef, - Config, }; use block_component::*; use fuel_core_storage::{ @@ -21,7 +20,6 @@ use fuel_core_storage::{ SpentMessages, }, transactional::{ - AtomicView, Changes, ConflictPolicy, Modifiable, @@ -84,6 +82,7 @@ use fuel_core_types::{ Bytes32, Cacheable, Chargeable, + ConsensusParameters, Input, Mint, Output, @@ -116,7 +115,6 @@ use fuel_core_types::{ state::StateTransition, Backtrace as FuelBacktrace, Interpreter, - InterpreterError, }, services::{ block_producer::Components, @@ -137,10 +135,7 @@ use fuel_core_types::{ }, }; use parking_lot::Mutex as ParkingMutex; -use std::{ - borrow::Cow, - sync::Arc, -}; +use std::borrow::Cow; use tracing::{ debug, warn, @@ -172,127 +167,6 @@ impl TransactionsSource for OnceTransactionsSource { } } -/// The executor is used for block production and validation of the blocks. -#[derive(Clone, Debug)] -pub struct Executor { - pub database_view_provider: D, - pub relayer_view_provider: R, - pub config: Arc, -} - -impl Executor -where - R: AtomicView, - R::View: RelayerPort, - D: AtomicView + Modifiable, - D::View: KeyValueInspect, -{ - #[cfg(any(test, feature = "test-helpers"))] - /// Executes the block and commits the result of the execution into the inner `Database`. - pub fn execute_and_commit( - &mut self, - block: fuel_core_types::services::executor::ExecutionBlock, - options: ExecutionOptions, - ) -> ExecutorResult { - let (result, changes) = self.execute_without_commit(block, options)?.into(); - - self.database_view_provider.commit_changes(changes)?; - Ok(result) - } - - #[cfg(any(test, feature = "test-helpers"))] - pub fn execute_without_commit( - &self, - block: fuel_core_types::services::executor::ExecutionBlock, - options: ExecutionOptions, - ) -> ExecutorResult> { - let executor = ExecutionInstance { - database: self.database_view_provider.latest_view(), - relayer: self.relayer_view_provider.latest_view(), - config: self.config.clone(), - options, - }; - - let component = match block { - ExecutionTypes::DryRun(_) => { - panic!("It is not possible to commit the dry run result"); - } - ExecutionTypes::Production(block) => ExecutionTypes::Production(Components { - header_to_produce: block.header, - transactions_source: OnceTransactionsSource::new(block.transactions), - gas_price: 0, - gas_limit: u64::MAX, - }), - ExecutionTypes::Validation(block) => ExecutionTypes::Validation(block), - }; - - executor.execute_without_commit(component) - } -} - -impl Executor -where - R: AtomicView, - R::View: RelayerPort, - D: AtomicView, - D::View: KeyValueInspect, -{ - /// Executes the partial block and returns `ExecutionData` as a result. - #[cfg(any(test, feature = "test-helpers"))] - pub fn execute_block( - &self, - block: ExecutionType>, - options: ExecutionOptions, - ) -> ExecutorResult - where - TxSource: TransactionsSource, - { - let executor = ExecutionInstance { - database: self.database_view_provider.latest_view(), - relayer: self.relayer_view_provider.latest_view(), - config: self.config.clone(), - options, - }; - executor.execute_block(block) - } - - pub fn execute_without_commit_with_source( - &self, - block: ExecutionBlockWithSource, - ) -> ExecutorResult> - where - TxSource: TransactionsSource, - { - let executor = ExecutionInstance { - database: self.database_view_provider.latest_view(), - relayer: self.relayer_view_provider.latest_view(), - config: self.config.clone(), - options: self.config.as_ref().into(), - }; - executor.execute_inner(block) - } - - pub fn dry_run( - &self, - component: Components>, - utxo_validation: Option, - ) -> ExecutorResult> { - // fallback to service config value if no utxo_validation override is provided - let utxo_validation = - utxo_validation.unwrap_or(self.config.utxo_validation_default); - - let options = ExecutionOptions { utxo_validation }; - - let executor = ExecutionInstance { - database: self.database_view_provider.latest_view(), - relayer: self.relayer_view_provider.latest_view(), - config: self.config.clone(), - options, - }; - executor.dry_run(component) - } -} - /// Data that is generated after executing all transactions. #[derive(Default)] pub struct ExecutionData { @@ -309,18 +183,17 @@ pub struct ExecutionData { } /// Per-block execution options -#[derive(Copy, Clone, Default, Debug)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Default, Debug)] pub struct ExecutionOptions { /// UTXO Validation flag, when disabled the executor skips signature and UTXO existence checks pub utxo_validation: bool, -} - -impl From<&Config> for ExecutionOptions { - fn from(value: &Config) -> Self { - Self { - utxo_validation: value.utxo_validation_default, - } - } + /// Print execution backtraces if transaction execution reverts. + pub backtrace: bool, + // TODO: It is a temporary workaround to pass the `consensus_params` + // into the WASM executor. Later WASM will fetch it from the storage directly. + // https://github.com/FuelLabs/fuel-core/issues/1753 + /// The configuration allows overriding the default consensus parameters. + pub consensus_params: Option, } /// The executor instance performs block production and validation. Given a block, it will execute all @@ -328,10 +201,10 @@ impl From<&Config> for ExecutionOptions { /// In production mode, block fields like transaction commitments are set based on the executed txs. /// In validation mode, the processed block commitments are compared with the proposed block. #[derive(Clone, Debug)] -struct ExecutionInstance { +pub struct ExecutionInstance { pub relayer: R, pub database: D, - pub config: Arc, + pub consensus_params: ConsensusParameters, pub options: ExecutionOptions, } @@ -349,49 +222,16 @@ where { self.execute_inner(block) } - - pub fn dry_run( - self, - component: Components>, - ) -> ExecutorResult> { - let component = Components { - header_to_produce: component.header_to_produce, - transactions_source: OnceTransactionsSource::new( - component.transactions_source, - ), - gas_price: component.gas_price, - gas_limit: component.gas_limit, - }; - - let ( - ExecutionResult { - skipped_transactions, - tx_status, - .. - }, - _temporary_db, - ) = self - .execute_without_commit(ExecutionTypes::DryRun(component))? - .into(); - - // If one of the transactions fails, return an error. - if let Some((_, err)) = skipped_transactions.into_iter().next() { - return Err(err) - } - - Ok(tx_status) - // drop `_temporary_db` without committing to avoid altering state. - } } // TODO: Make this module private after moving unit tests from `fuel-core` here. pub mod block_component { use super::*; - use fuel_core_types::fuel_tx::field::MintGasPrice; pub struct PartialBlockComponent<'a, TxSource> { pub empty_block: &'a mut PartialFuelBlock, pub transactions_source: TxSource, + pub coinbase_contract_id: ContractId, pub gas_price: u64, pub gas_limit: u64, /// The private marker to allow creation of the type only by constructor. @@ -401,15 +241,17 @@ pub mod block_component { impl<'a> PartialBlockComponent<'a, OnceTransactionsSource> { pub fn from_partial_block(block: &'a mut PartialFuelBlock) -> Self { let transaction = core::mem::take(&mut block.transactions); - let gas_price = if let Some(Transaction::Mint(mint)) = transaction.last() { - *mint.gas_price() - } else { - 0 - }; + let (gas_price, coinbase_contract_id) = + if let Some(Transaction::Mint(mint)) = transaction.last() { + (*mint.gas_price(), mint.input_contract().contract_id) + } else { + (0, Default::default()) + }; Self { empty_block: block, transactions_source: OnceTransactionsSource::new(transaction), + coinbase_contract_id, gas_price, gas_limit: u64::MAX, _marker: Default::default(), @@ -421,6 +263,7 @@ pub mod block_component { pub fn from_component( block: &'a mut PartialFuelBlock, transactions_source: TxSource, + coinbase_contract_id: ContractId, gas_price: u64, gas_limit: u64, ) -> Self { @@ -429,6 +272,7 @@ pub mod block_component { empty_block: block, transactions_source, gas_price, + coinbase_contract_id, gas_limit, _marker: Default::default(), } @@ -463,6 +307,7 @@ where let component = PartialBlockComponent::from_component( &mut block, component.transactions_source, + component.coinbase_recipient, component.gas_price, component.gas_limit, ); @@ -477,6 +322,7 @@ where let component = PartialBlockComponent::from_component( &mut block, component.transactions_source, + component.coinbase_recipient, component.gas_price, component.gas_limit, ); @@ -569,6 +415,7 @@ where let block = component.empty_block; let source = component.transactions_source; let gas_price = component.gas_price; + let coinbase_contract_id = component.coinbase_contract_id; let mut remaining_gas_limit = component.gas_limit; let block_height = *block.header.height(); @@ -602,11 +449,12 @@ where let mut tx_st_transaction = thread_block_transaction .write_transaction() .with_policy(ConflictPolicy::Overwrite); - let tx_id = tx.id(&self.config.consensus_parameters.chain_id); + let tx_id = tx.id(&self.consensus_params.chain_id); let result = self.execute_transaction( tx, &tx_id, &block.header, + coinbase_contract_id, gas_price, execution_data, execution_kind, @@ -660,8 +508,7 @@ where // After the execution of all transactions in production mode, we can set the final fee. if execution_kind == ExecutionKind::Production { - let amount_to_mint = if self.config.coinbase_recipient != ContractId::zeroed() - { + let amount_to_mint = if coinbase_contract_id != ContractId::zeroed() { execution_data.coinbase } else { 0 @@ -674,7 +521,7 @@ where balance_root: Bytes32::zeroed(), state_root: Bytes32::zeroed(), tx_pointer: TxPointer::new(BlockHeight::new(0), 0), - contract_id: self.config.coinbase_recipient, + contract_id: coinbase_contract_id, }, output::contract::Contract { input_index: 0, @@ -682,7 +529,7 @@ where state_root: Bytes32::zeroed(), }, amount_to_mint, - self.config.consensus_parameters.base_asset_id, + self.consensus_params.base_asset_id, gas_price, ); @@ -735,7 +582,7 @@ where let events = self .relayer .get_events(&da_height) - .map_err(|err| ExecutorError::RelayerError(err.into()))?; + .map_err(|err| ExecutorError::RelayerError(err.to_string()))?; for event in events { root_calculator.push(event.hash().as_ref()); match event { @@ -768,6 +615,7 @@ where tx: MaybeCheckedTransaction, tx_id: &TxId, header: &PartialBlockHeader, + coinbase_contract_id: ContractId, gas_price: Word, execution_data: &mut ExecutionData, execution_kind: ExecutionKind, @@ -791,7 +639,7 @@ where let block_height = *header.height(); let checked_tx = match tx { MaybeCheckedTransaction::Transaction(tx) => tx - .into_checked_basic(block_height, &self.config.consensus_parameters)? + .into_checked_basic(block_height, &self.consensus_params)? .into(), MaybeCheckedTransaction::CheckedTransaction(checked_tx) => checked_tx, }; @@ -800,6 +648,7 @@ where CheckedTransaction::Script(script) => self.execute_create_or_script( script, header, + coinbase_contract_id, gas_price, execution_data, tx_st_transaction, @@ -808,6 +657,7 @@ where CheckedTransaction::Create(create) => self.execute_create_or_script( create, header, + coinbase_contract_id, gas_price, execution_data, tx_st_transaction, @@ -816,6 +666,7 @@ where CheckedTransaction::Mint(mint) => self.execute_mint( mint, header, + coinbase_contract_id, gas_price, execution_data, tx_st_transaction, @@ -824,10 +675,12 @@ where } } + #[allow(clippy::too_many_arguments)] fn execute_mint( &self, checked_mint: Checked, header: &PartialBlockHeader, + coinbase_contract_id: ContractId, gas_price: Word, execution_data: &mut ExecutionData, block_st_transaction: &mut StorageTransaction, @@ -914,7 +767,7 @@ where let mut vm_db = VmStorage::new( &mut sub_block_db_commit, &header.consensus, - self.config.coinbase_recipient, + coinbase_contract_id, ); fuel_vm::interpreter::contract::balance_increase( @@ -923,7 +776,7 @@ where mint.mint_asset_id(), *mint.mint_amount(), ) - .map_err(|e| anyhow::anyhow!(format!("{e}"))) + .map_err(|e| format!("{e}")) .map_err(ExecutorError::CoinbaseCannotIncreaseBalance)?; sub_block_db_commit.commit()?; @@ -983,6 +836,7 @@ where &self, mut checked_tx: Checked, header: &PartialBlockHeader, + coinbase_contract_id: ContractId, gas_price: Word, execution_data: &mut ExecutionData, tx_st_transaction: &mut StorageTransaction, @@ -998,9 +852,7 @@ where if self.options.utxo_validation { checked_tx = checked_tx - .check_predicates(&CheckPredicateParams::from( - &self.config.consensus_parameters, - )) + .check_predicates(&CheckPredicateParams::from(&self.consensus_params)) .map_err(|e| { ExecutorError::TransactionValidity( TransactionValidityError::Validation(e), @@ -1016,7 +868,7 @@ where )?; // validate transaction signature checked_tx = checked_tx - .check_signatures(&self.config.consensus_parameters.chain_id) + .check_signatures(&self.consensus_params.chain_id) .map_err(TransactionValidityError::from)?; debug_assert!(checked_tx.checks().contains(Checks::Signatures)); } @@ -1039,16 +891,16 @@ where let vm_db = VmStorage::new( &mut sub_block_db_commit, &header.consensus, - self.config.coinbase_recipient, + coinbase_contract_id, ); let mut vm = Interpreter::with_storage( vm_db, - InterpreterParams::new(gas_price, &self.config.consensus_parameters), + InterpreterParams::new(gas_price, &self.consensus_params), ); - let gas_costs = &self.config.consensus_parameters.gas_costs; - let fee_params = &self.config.consensus_parameters.fee_params; + let gas_costs = &self.consensus_params.gas_costs; + let fee_params = &self.consensus_params.fee_params; let ready_tx = checked_tx .clone() @@ -1057,7 +909,7 @@ where let vm_result: StateTransition<_> = vm .transact(ready_tx) .map_err(|error| ExecutorError::VmExecution { - error: InterpreterError::Storage(anyhow::anyhow!(format!("{error:?}"))), + error: error.to_string(), transaction_id: tx_id, })? .into(); @@ -1066,8 +918,8 @@ where let (state, mut tx, receipts): (_, Tx, _) = vm_result.into_inner(); #[cfg(debug_assertions)] { - tx.precompute(&self.config.consensus_parameters.chain_id)?; - debug_assert_eq!(tx.id(&self.config.consensus_parameters.chain_id), tx_id); + tx.precompute(&self.consensus_params.chain_id)?; + debug_assert_eq!(tx.id(&self.consensus_params.chain_id), tx_id); } for (original_input, produced_input) in checked_tx @@ -1333,7 +1185,7 @@ where let message = db .storage::() .remove(nonce)? - .ok_or_else(|| ExecutorError::MessageAlreadySpent(*nonce))?; + .ok_or(ExecutorError::MessageAlreadySpent(*nonce))?; execution_data .events .push(ExecutorEvent::MessageConsumed(message)); @@ -1361,8 +1213,8 @@ where let fee = tx .refund_fee( - self.config.consensus_parameters.gas_costs(), - self.config.consensus_parameters.fee_params(), + self.consensus_params.gas_costs(), + self.consensus_params.fee_params(), used_gas, gas_price, ) @@ -1577,7 +1429,7 @@ where vm: &Interpreter, Tx>, receipts: &[Receipt], ) { - if self.config.backtrace { + if self.options.backtrace { if let Some(backtrace) = receipts .iter() .find_map(Receipt::result) diff --git a/crates/services/executor/src/lib.rs b/crates/services/executor/src/lib.rs index b1747080..9079243b 100644 --- a/crates/services/executor/src/lib.rs +++ b/crates/services/executor/src/lib.rs @@ -3,15 +3,9 @@ #![deny(unused_crate_dependencies)] #![deny(warnings)] -mod config; - pub mod executor; pub mod ports; pub mod refs; -pub struct BlockExecutor {} - -pub use config::Config; - #[cfg(test)] fuel_core_trace::enable_tracing!(); diff --git a/crates/services/p2p/Cargo.toml b/crates/services/p2p/Cargo.toml index ac3a8f32..2ab31a5d 100644 --- a/crates/services/p2p/Cargo.toml +++ b/crates/services/p2p/Cargo.toml @@ -43,7 +43,7 @@ quick-protobuf = "0.8.1" quick-protobuf-codec = "0.3.0" rand = { workspace = true } serde = { workspace = true, features = ["derive"] } -serde_with = "1.11" +serde_with = { workspace = true } sha2 = "0.10" thiserror = "1.0.47" tokio = { workspace = true, features = ["sync"] } diff --git a/crates/services/producer/src/block_producer.rs b/crates/services/producer/src/block_producer.rs index b964f682..16492f6b 100644 --- a/crates/services/producer/src/block_producer.rs +++ b/crates/services/producer/src/block_producer.rs @@ -126,6 +126,7 @@ where let component = Components { header_to_produce: header, transactions_source: source, + coinbase_recipient: self.config.coinbase_recipient.unwrap_or_default(), gas_price, gas_limit: max_gas, }; @@ -231,6 +232,7 @@ where let component = Components { header_to_produce: header, transactions_source: transactions.clone(), + coinbase_recipient: self.config.coinbase_recipient.unwrap_or_default(), gas_price, gas_limit: u64::MAX, }; diff --git a/crates/services/txpool/src/mock_db.rs b/crates/services/txpool/src/mock_db.rs index 0b83e7a5..962a59ed 100644 --- a/crates/services/txpool/src/mock_db.rs +++ b/crates/services/txpool/src/mock_db.rs @@ -69,13 +69,7 @@ impl MockDb { impl TxPoolDb for MockDb { fn utxo(&self, utxo_id: &UtxoId) -> StorageResult> { - Ok(self - .data - .lock() - .unwrap() - .coins - .get(utxo_id) - .map(Clone::clone)) + Ok(self.data.lock().unwrap().coins.get(utxo_id).cloned()) } fn contract_exist(&self, contract_id: &ContractId) -> StorageResult { @@ -88,7 +82,7 @@ impl TxPoolDb for MockDb { } fn message(&self, id: &Nonce) -> StorageResult> { - Ok(self.data.lock().unwrap().messages.get(id).map(Clone::clone)) + Ok(self.data.lock().unwrap().messages.get(id).cloned()) } fn is_message_spent(&self, id: &Nonce) -> StorageResult { diff --git a/crates/services/upgradable-executor/Cargo.toml b/crates/services/upgradable-executor/Cargo.toml new file mode 100644 index 00000000..a0d3dbcc --- /dev/null +++ b/crates/services/upgradable-executor/Cargo.toml @@ -0,0 +1,46 @@ +[package] +name = "fuel-core-upgradable-executor" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = ["blockchain", "fuel", "fuel-vm", "upgradable"] +license = { workspace = true } +repository = { workspace = true } +description = "Fuel Block Upgradable Executor" +build = "build.rs" + +[dependencies] +anyhow = { workspace = true, optional = true } +fuel-core-executor = { workspace = true } +fuel-core-storage = { workspace = true } +fuel-core-types = { workspace = true } +fuel-core-wasm-executor = { workspace = true, optional = true } +lazy_static = { workspace = true, optional = true } +postcard = { workspace = true, optional = true } +wasmtime = { version = "18.0.1", default-features = false, features = [ + "cache", + "cranelift", + "parallel-compilation", + "pooling-allocator", + "runtime", +], optional = true } + +[dev-dependencies] +fuel-core-storage = { workspace = true, features = ["test-helpers"] } +fuel-core-types = { workspace = true, features = ["test-helpers"] } + +[features] +default = ["std"] +std = ["fuel-core-executor/std", "fuel-core-storage/std", "fuel-core-types/std"] +wasm-executor = [ + "dep:anyhow", + "dep:lazy_static", + "dep:postcard", + "dep:fuel-core-wasm-executor", + "dep:wasmtime", +] +test-helpers = [ + "fuel-core-storage/test-helpers", + "fuel-core-types/test-helpers", +] diff --git a/crates/services/upgradable-executor/build.rs b/crates/services/upgradable-executor/build.rs new file mode 100644 index 00000000..c4a6f9ea --- /dev/null +++ b/crates/services/upgradable-executor/build.rs @@ -0,0 +1,68 @@ +use std::{ + env, + path::{ + Path, + PathBuf, + }, + process::Command, +}; + +fn main() { + let wasm_executor_enabled = env::var_os("CARGO_FEATURE_WASM_EXECUTOR").is_some(); + + println!("cargo:rerun-if-changed=build.rs"); + println!("cargo:rerun-if-changed=wasm-executor/src/*"); + + if !wasm_executor_enabled { + return; + } + + let out_dir = env::var_os("OUT_DIR").unwrap(); + let dest_path = Path::new(&out_dir); + + let cargo = env::var("CARGO").unwrap_or_else(|_| "cargo".to_string()); + + let target_dir = format!("--target-dir={}", dest_path.to_string_lossy()); + + let args = vec![ + "build".to_owned(), + "-p".to_owned(), + "fuel-core-wasm-executor".to_owned(), + "--target=wasm32-unknown-unknown".to_owned(), + "--no-default-features".to_owned(), + "--release".to_owned(), + target_dir, + ]; + + let mut cargo = Command::new(cargo); + cargo.env("CARGO_PROFILE_RELEASE_LTO", "true"); + cargo.env("CARGO_PROFILE_RELEASE_PANIC", "abort"); + cargo.env("CARGO_PROFILE_RELEASE_CODEGEN_UNITS", "1"); + cargo.env("CARGO_PROFILE_RELEASE_OPT_LEVEL", "3"); + cargo.env("CARGO_PROFILE_RELEASE_STRIP", "symbols"); + cargo.current_dir(project_root()).args(args); + + let output = cargo.output(); + + match output { + Ok(output) => { + if !output.status.success() { + println!( + "cargo:warning=Got an error status during compiling WASM executor: {:?}", + output + ); + } + } + Err(err) => { + println!("cargo:warning={:?}", err); + } + } +} + +fn project_root() -> PathBuf { + Path::new(&env!("CARGO_MANIFEST_DIR")) + .ancestors() + .nth(1) + .unwrap() + .to_path_buf() +} diff --git a/crates/services/upgradable-executor/src/config.rs b/crates/services/upgradable-executor/src/config.rs new file mode 100644 index 00000000..bf2e0140 --- /dev/null +++ b/crates/services/upgradable-executor/src/config.rs @@ -0,0 +1,24 @@ +use fuel_core_executor::executor::ExecutionOptions; +use fuel_core_types::fuel_tx::ConsensusParameters; + +#[derive(Clone, Debug, Default)] +pub struct Config { + /// Network-wide common parameters used for validating the chain. + /// The executor already has these parameters, and this field allows us + /// to override the existing value. + pub consensus_parameters: ConsensusParameters, + /// Print execution backtraces if transaction execution reverts. + pub backtrace: bool, + /// Default mode for utxo_validation + pub utxo_validation_default: bool, +} + +impl From<&Config> for ExecutionOptions { + fn from(value: &Config) -> Self { + Self { + utxo_validation: value.utxo_validation_default, + backtrace: value.backtrace, + consensus_params: Some(value.consensus_parameters.clone()), + } + } +} diff --git a/crates/services/upgradable-executor/src/executor.rs b/crates/services/upgradable-executor/src/executor.rs new file mode 100644 index 00000000..a65a1d26 --- /dev/null +++ b/crates/services/upgradable-executor/src/executor.rs @@ -0,0 +1,280 @@ +use crate::config::Config; +use fuel_core_executor::{ + executor::{ + ExecutionBlockWithSource, + ExecutionOptions, + OnceTransactionsSource, + }, + ports::{ + RelayerPort, + TransactionsSource, + }, +}; +use fuel_core_storage::{ + column::Column, + kv_store::KeyValueInspect, + transactional::{ + AtomicView, + Changes, + Modifiable, + }, +}; +#[cfg(any(test, feature = "test-helpers"))] +use fuel_core_types::services::executor::UncommittedResult; +use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, + fuel_tx::Transaction, + fuel_types::BlockHeight, + services::{ + block_producer::Components, + executor::{ + ExecutionResult, + ExecutionTypes, + Result as ExecutorResult, + TransactionExecutionStatus, + }, + Uncommitted, + }, +}; +use std::sync::Arc; + +/// The upgradable executor supports the WASM version of the state transition function. +/// If the block has a version the same as a native executor, we will use it. +/// If not, the WASM version of the state transition function will be used +/// (if the database has a corresponding bytecode). +pub struct Executor { + pub storage_view_provider: S, + pub relayer_view_provider: R, + pub config: Arc, + #[cfg(feature = "wasm-executor")] + engine: wasmtime::Engine, + #[cfg(feature = "wasm-executor")] + module: wasmtime::Module, +} + +impl Executor { + pub fn new( + storage_view_provider: S, + relayer_view_provider: R, + config: Config, + ) -> Self { + Self { + storage_view_provider, + relayer_view_provider, + config: Arc::new(config), + #[cfg(feature = "wasm-executor")] + engine: crate::DEFAULT_ENGINE.clone(), + #[cfg(feature = "wasm-executor")] + module: crate::COMPILED_UNDERLYING_EXECUTOR.clone(), + } + } +} + +impl Executor +where + R: AtomicView, + R::View: RelayerPort + Send + Sync + 'static, + D: AtomicView + Modifiable, + D::View: KeyValueInspect + Send + Sync + 'static, +{ + #[cfg(any(test, feature = "test-helpers"))] + /// Executes the block and commits the result of the execution into the inner `Database`. + pub fn execute_and_commit( + &mut self, + block: fuel_core_types::services::executor::ExecutionBlock, + ) -> fuel_core_types::services::executor::Result { + let (result, changes) = self.execute_without_commit(block)?.into(); + + self.storage_view_provider.commit_changes(changes)?; + Ok(result) + } +} + +impl Executor +where + S: AtomicView, + S::View: KeyValueInspect + Send + Sync + 'static, + R: AtomicView, + R::View: RelayerPort + Send + Sync + 'static, +{ + #[cfg(any(test, feature = "test-helpers"))] + /// Executes the block and returns the result of the execution with storage changes. + pub fn execute_without_commit( + &self, + block: fuel_core_types::services::executor::ExecutionBlock, + ) -> fuel_core_types::services::executor::Result> { + self.execute_without_commit_with_coinbase(block, Default::default(), 0) + } + + #[cfg(any(test, feature = "test-helpers"))] + /// The analog of the [`Self::execute_without_commit`] method, + /// but with the ability to specify the coinbase recipient and the gas price. + pub fn execute_without_commit_with_coinbase( + &self, + block: fuel_core_types::services::executor::ExecutionBlock, + coinbase_recipient: fuel_core_types::fuel_types::ContractId, + gas_price: u64, + ) -> fuel_core_types::services::executor::Result> { + let component = match block { + ExecutionTypes::DryRun(_) => { + panic!("It is not possible to commit the dry run result"); + } + ExecutionTypes::Production(block) => ExecutionTypes::Production(Components { + header_to_produce: block.header, + transactions_source: OnceTransactionsSource::new(block.transactions), + coinbase_recipient, + gas_price, + gas_limit: u64::MAX, + }), + ExecutionTypes::Validation(block) => ExecutionTypes::Validation(block), + }; + + let option = self.config.as_ref().into(); + self.execute_inner(component, option) + } +} + +impl Executor +where + S: AtomicView, + S::View: KeyValueInspect + Send + Sync + 'static, + R: AtomicView, + R::View: RelayerPort + Send + Sync + 'static, +{ + /// Executes the block and returns the result of the execution without committing the changes. + pub fn execute_without_commit_with_source( + &self, + block: ExecutionBlockWithSource, + ) -> ExecutorResult> + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + let options = self.config.as_ref().into(); + self.execute_inner(block, options) + } + + /// Executes the block and returns the result of the execution without committing + /// the changes in the dry run mode. + pub fn dry_run( + &self, + component: Components>, + utxo_validation: Option, + ) -> ExecutorResult> { + // fallback to service config value if no utxo_validation override is provided + let utxo_validation = + utxo_validation.unwrap_or(self.config.utxo_validation_default); + + let options = ExecutionOptions { + utxo_validation, + backtrace: self.config.backtrace, + consensus_params: Some(self.config.consensus_parameters.clone()), + }; + + let component = Components { + header_to_produce: component.header_to_produce, + transactions_source: OnceTransactionsSource::new( + component.transactions_source, + ), + coinbase_recipient: Default::default(), + gas_price: component.gas_price, + gas_limit: component.gas_limit, + }; + + let ExecutionResult { + skipped_transactions, + tx_status, + .. + } = self + .execute_inner(ExecutionTypes::DryRun(component), options)? + .into_result(); + + // If one of the transactions fails, return an error. + if let Some((_, err)) = skipped_transactions.into_iter().next() { + return Err(err) + } + + Ok(tx_status) + } + + fn execute_inner( + &self, + block: ExecutionBlockWithSource, + options: ExecutionOptions, + ) -> ExecutorResult> + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + #[cfg(feature = "wasm-executor")] + return self.wasm_execute_inner(block, options); + + #[cfg(not(feature = "wasm-executor"))] + return self.native_execute_inner(block, options); + } + + #[cfg(feature = "wasm-executor")] + fn wasm_execute_inner( + &self, + block: ExecutionBlockWithSource, + options: ExecutionOptions, + ) -> ExecutorResult> + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + let mut source = None; + let block = block.map_p(|component| { + let Components { + header_to_produce, + transactions_source, + coinbase_recipient, + gas_price, + gas_limit, + } = component; + + source = Some(transactions_source); + + Components { + header_to_produce, + transactions_source: (), + coinbase_recipient, + gas_price, + gas_limit, + } + }); + + let storage = self.storage_view_provider.latest_view(); + let relayer = self.relayer_view_provider.latest_view(); + + let instance = crate::instance::Instance::new(&self.engine) + .add_source(source)? + .add_storage(storage)? + .add_relayer(relayer)? + .add_input_data(block, options)?; + + instance.run(&self.module) + } + + #[cfg(not(feature = "wasm-executor"))] + fn native_execute_inner( + &self, + block: ExecutionBlockWithSource, + mut options: ExecutionOptions, + ) -> ExecutorResult> + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + let storage = self.storage_view_provider.latest_view(); + let relayer = self.relayer_view_provider.latest_view(); + let consensus_params = options + .consensus_params + .take() + .unwrap_or_else(|| self.config.consensus_parameters.clone()); + + let instance = fuel_core_executor::executor::ExecutionInstance { + relayer, + database: storage, + consensus_params, + options, + }; + instance.execute_without_commit(block) + } +} diff --git a/crates/services/upgradable-executor/src/instance.rs b/crates/services/upgradable-executor/src/instance.rs new file mode 100644 index 00000000..7f26dcec --- /dev/null +++ b/crates/services/upgradable-executor/src/instance.rs @@ -0,0 +1,574 @@ +use fuel_core_executor::{ + executor::{ + ExecutionBlockWithSource, + ExecutionOptions, + }, + ports::{ + MaybeCheckedTransaction, + RelayerPort, + TransactionsSource, + }, +}; +use fuel_core_storage::{ + column::Column, + kv_store::{ + KeyValueInspect, + Value, + }, +}; +use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, + fuel_tx::Transaction, + fuel_vm::checked_transaction::Checked, + services::executor::{ + Error as ExecutorError, + Result as ExecutorResult, + }, +}; +use fuel_core_wasm_executor::utils::{ + pack_exists_size_result, + unpack_ptr_and_len, + ReturnType, +}; +use std::{ + collections::HashMap, + sync::Arc, +}; +use wasmtime::{ + AsContextMut, + Caller, + Engine, + Func, + Linker, + Memory, + Module, + Store, +}; + +trait CallerHelper { + /// Writes the encoded data to the memory at the provided pointer. + fn write(&mut self, ptr: u32, encoded: &[u8]) -> anyhow::Result<()>; +} + +impl<'a> CallerHelper for Caller<'a, ExecutionState> { + fn write(&mut self, ptr: u32, encoded: &[u8]) -> anyhow::Result<()> { + let memory = self.data_mut().memory.expect("Memory is initialized; qed"); + let mut store = self.as_context_mut(); + memory + .write(&mut store, ptr as usize, encoded) + .map_err(|e| anyhow::anyhow!("Failed to write to the memory: {}", e)) + } +} + +/// The state used by the host functions provided for the WASM executor. +struct ExecutionState { + /// The memory used by the WASM module. + memory: Option, + next_transactions: HashMap>>, + relayer_events: HashMap, +} + +/// The WASM instance has several stages of initialization. +/// When the instance is fully initialized, it is possible to run it. +/// +/// Each stage has a state. Advancing the stage may add a new state that +/// will later be used by the next stages or a `run` function. +/// +/// Currently, the order of the definition of the host functions doesn't matter, +/// but later, the data from previous stages may be used to initialize new stages. +pub struct Instance { + store: Store, + linker: Linker, + stage: Stage, +} + +/// The stage indicates that the instance is fresh and newly created. +pub struct Created; + +impl Instance { + pub fn new(engine: &Engine) -> Instance { + Instance { + store: Store::new( + engine, + ExecutionState { + memory: None, + next_transactions: Default::default(), + relayer_events: Default::default(), + }, + ), + linker: Linker::new(engine), + stage: Created {}, + } + } +} + +impl Instance { + const LATEST_HOST_MODULE: &'static str = "host_v0"; + + /// Adds a new host function to the instance. + pub fn add_method(&mut self, method: &str, func: Func) -> ExecutorResult<()> { + self.linker + .define(&self.store, Self::LATEST_HOST_MODULE, method, func) + .map_err(|e| { + ExecutorError::Other(format!( + "Failed definition of the `{}` function: {}", + method, e + )) + })?; + Ok(()) + } +} + +/// This stage adds host functions to get transactions from the transaction source. +pub struct Source; + +impl Instance { + /// Adds host functions for the `source`. + pub fn add_source( + mut self, + source: Option, + ) -> ExecutorResult> + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + let source = source.map(|source| Arc::new(source)); + let peek_next_txs_size = self.peek_next_txs_size(source.clone()); + self.add_method("peek_next_txs_size", peek_next_txs_size)?; + + let consume_next_txs = self.consume_next_txs(); + self.add_method("consume_next_txs", consume_next_txs)?; + + Ok(Instance { + store: self.store, + linker: self.linker, + stage: Source {}, + }) + } + + fn peek_next_txs_size(&mut self, source: Option>) -> Func + where + TxSource: TransactionsSource + Send + Sync + 'static, + { + let closure = move |mut caller: Caller<'_, ExecutionState>, + gas_limit: u64| + -> anyhow::Result { + let Some(source) = source.clone() else { + return Ok(0); + }; + + let txs: Vec<_> = source + .next(gas_limit) + .into_iter() + .map(|tx| match tx { + MaybeCheckedTransaction::CheckedTransaction(checked) => { + let checked: Checked = checked.into(); + let (tx, _) = checked.into(); + tx + } + MaybeCheckedTransaction::Transaction(tx) => tx, + }) + .collect(); + + let encoded_txs = postcard::to_allocvec(&txs).map_err(|e| { + ExecutorError::Other(format!( + "Failed encoding of the transactions for `peek_next_txs_size` function: {}", + e + )) + })?; + let encoded_size = u32::try_from(encoded_txs.len()).map_err(|e| { + ExecutorError::Other(format!( + "The encoded transactions are more than `u32::MAX`. We support only wasm32: {}", + e + )) + })?; + + caller + .data_mut() + .next_transactions + .entry(encoded_size) + .or_default() + .push(encoded_txs); + Ok(encoded_size) + }; + + Func::wrap(&mut self.store, closure) + } + + fn consume_next_txs(&mut self) -> Func { + let closure = move |mut caller: Caller<'_, ExecutionState>, + output_ptr: u32, + output_size: u32| + -> anyhow::Result<()> { + let encoded = caller + .data_mut() + .next_transactions + .get_mut(&output_size) + .and_then(|vector| vector.pop()) + .unwrap_or_default(); + + caller.write(output_ptr, &encoded) + }; + + Func::wrap(&mut self.store, closure) + } +} + +/// This stage adds host functions for the storage. +pub struct Storage; + +impl Instance { + /// Adds getters to the `storage`. + pub fn add_storage(mut self, storage: S) -> ExecutorResult> + where + S: KeyValueInspect + Send + Sync + 'static, + { + let storage = Arc::new(storage); + + let storage_size_of_value = self.storage_size_of_value(storage.clone()); + self.add_method("storage_size_of_value", storage_size_of_value)?; + + let storage_get = self.storage_get(storage); + self.add_method("storage_get", storage_get)?; + + Ok(Instance { + store: self.store, + linker: self.linker, + stage: Storage {}, + }) + } + + fn storage_size_of_value(&mut self, storage: Arc) -> Func + where + S: KeyValueInspect + Send + Sync + 'static, + { + let closure = move |caller: Caller<'_, ExecutionState>, + key_ptr: u32, + key_len: u32, + column: u32| + -> anyhow::Result { + let column = fuel_core_storage::column::Column::try_from(column) + .map_err(|e| anyhow::anyhow!("Unknown column: {}", e))?; + + let (ptr, len) = (key_ptr as usize, key_len as usize); + let memory = caller + .data() + .memory + .expect("Memory was initialized above; qed"); + + let key = &memory.data(&caller)[ptr..ptr.saturating_add(len)]; + if let Ok(value) = storage.size_of_value(key, column) { + let size = u32::try_from(value.unwrap_or_default()).map_err(|e| { + anyhow::anyhow!( + "The size of the value is more than `u32::MAX`. We support only wasm32: {}", + e + ) + })?; + + Ok(pack_exists_size_result(value.is_some(), size, 0)) + } else { + Ok(pack_exists_size_result(false, 0, 0)) + } + }; + + Func::wrap(&mut self.store, closure) + } + + fn storage_get(&mut self, storage: Arc) -> Func + where + S: KeyValueInspect + Send + Sync + 'static, + { + let closure = move |mut caller: Caller<'_, ExecutionState>, + key_ptr: u32, + key_len: u32, + column: u32, + out_ptr: u32, + out_len: u32| + -> anyhow::Result { + let column = fuel_core_storage::column::Column::try_from(column) + .map_err(|e| anyhow::anyhow!("Unknown column: {}", e))?; + let (ptr, len) = (key_ptr as usize, key_len as usize); + let memory = caller + .data() + .memory + .expect("Memory was initialized above; qed"); + + let key = &memory.data(&caller)[ptr..ptr.saturating_add(len)]; + if let Ok(value) = storage.get(key, column) { + let value = value.ok_or(anyhow::anyhow!("\ + The WASM executor should call `get` only after `storage_size_of_value`."))?; + + if value.len() != out_len as usize { + return Err(anyhow::anyhow!( + "The provided buffer size is not equal to the value size." + )); + } + + caller.write(out_ptr, value.as_slice())?; + Ok(0) + } else { + Ok(1) + } + }; + + Func::wrap(&mut self.store, closure) + } +} + +/// This stage adds host functions for the relayer. +pub struct Relayer; + +impl Instance { + /// Adds host functions for the `relayer`. + pub fn add_relayer(mut self, relayer: R) -> ExecutorResult> + where + R: RelayerPort + Send + Sync + 'static, + { + let relayer = Arc::new(relayer); + + let relayer_enabled = self.relayer_enabled(relayer.clone()); + self.add_method("relayer_enabled", relayer_enabled)?; + + let relayer_size_of_events = self.relayer_size_of_events(relayer); + self.add_method("relayer_size_of_events", relayer_size_of_events)?; + + let relayer_get_events = self.relayer_get_events(); + self.add_method("relayer_get_events", relayer_get_events)?; + + Ok(Instance { + store: self.store, + linker: self.linker, + stage: Relayer {}, + }) + } + + fn relayer_enabled(&mut self, relayer: Arc) -> Func + where + R: RelayerPort + Send + Sync + 'static, + { + let closure = + move |_: Caller<'_, ExecutionState>| -> u32 { relayer.enabled() as u32 }; + + Func::wrap(&mut self.store, closure) + } + + fn relayer_size_of_events(&mut self, relayer: Arc) -> Func + where + R: RelayerPort + Send + Sync + 'static, + { + let closure = move |mut caller: Caller<'_, ExecutionState>, + da_block_height: u64| + -> anyhow::Result { + let da_block_height: DaBlockHeight = da_block_height.into(); + + if let Some(encoded_events) = + caller.data().relayer_events.get(&da_block_height) + { + let encoded_size = u32::try_from(encoded_events.len()).map_err(|e| { + anyhow::anyhow!( + "The size of encoded events is more than `u32::MAX`. We support only wasm32: {}", + e + ) + })?; + Ok(pack_exists_size_result(true, encoded_size, 0)) + } else { + let events = relayer.get_events(&da_block_height); + + if let Ok(events) = events { + let encoded_events = + postcard::to_allocvec(&events).map_err(|e| anyhow::anyhow!(e))?; + let encoded_size = + u32::try_from(encoded_events.len()).map_err(|e| { + anyhow::anyhow!( + "The size of encoded events is more than `u32::MAX`. We support only wasm32: {}", + e + ) + })?; + + caller + .data_mut() + .relayer_events + .insert(da_block_height, encoded_events.into()); + Ok(pack_exists_size_result(true, encoded_size, 0)) + } else { + Ok(pack_exists_size_result(false, 0, 1)) + } + } + }; + + Func::wrap(&mut self.store, closure) + } + + fn relayer_get_events(&mut self) -> Func { + let closure = move |mut caller: Caller<'_, ExecutionState>, + da_block_height: u64, + out_ptr: u32| + -> anyhow::Result<()> { + let da_block_height: DaBlockHeight = da_block_height.into(); + let encoded_events = caller + .data() + .relayer_events + .get(&da_block_height) + .ok_or(anyhow::anyhow!( + "The `relayer_size_of_events` should be called before `relayer_get_events`" + ))? + .clone(); + + caller.write(out_ptr, encoded_events.as_ref())?; + Ok(()) + }; + + Func::wrap(&mut self.store, closure) + } +} + +/// This stage adds host functions - getter for the input data like `Block` and `ExecutionOptions`. +pub struct InputData { + input_component_size: u32, + input_options_size: u32, +} + +impl Instance { + /// Adds getters for the `block` and `options`. + pub fn add_input_data( + mut self, + block: ExecutionBlockWithSource<()>, + options: ExecutionOptions, + ) -> ExecutorResult> { + let encoded_block = postcard::to_allocvec(&block).map_err(|e| { + ExecutorError::Other(format!( + "Failed encoding of the block for `input` function: {}", + e + )) + })?; + let encoded_block_size = u32::try_from(encoded_block.len()).map_err(|e| { + ExecutorError::Other(format!( + "The encoded block is more than `u32::MAX`. We support only wasm32: {}", + e + )) + })?; + + let input_component = self.input_component(encoded_block, encoded_block_size); + self.add_method("input_component", input_component)?; + + let encoded_options = postcard::to_allocvec(&options).map_err(|e| { + ExecutorError::Other(format!( + "Failed encoding of the execution options for `input_options` function: {}", + e + )) + })?; + let encoded_options_size = u32::try_from(encoded_options.len()).map_err(|e| { + ExecutorError::Other(format!( + "The encoded option is more than `u32::MAX`. We support only wasm32: {}", + e + )) + })?; + + let input = self.input_options(encoded_options, encoded_options_size); + self.add_method("input_options", input)?; + + Ok(Instance { + store: self.store, + linker: self.linker, + stage: InputData { + input_component_size: encoded_block_size, + input_options_size: encoded_options_size, + }, + }) + } + + fn input_component( + &mut self, + encoded_block: Vec, + encoded_block_size: u32, + ) -> Func { + let closure = move |mut caller: Caller<'_, ExecutionState>, + out_ptr: u32, + out_len: u32| + -> anyhow::Result<()> { + if out_len != encoded_block_size { + return Err(anyhow::anyhow!( + "The provided buffer size is not equal to the encoded block size." + )); + } + + caller.write(out_ptr, &encoded_block) + }; + + Func::wrap(&mut self.store, closure) + } + + fn input_options( + &mut self, + encoded_options: Vec, + encoded_options_size: u32, + ) -> Func { + let closure = move |mut caller: Caller<'_, ExecutionState>, + out_ptr: u32, + out_len: u32| + -> anyhow::Result<()> { + if out_len != encoded_options_size { + return Err(anyhow::anyhow!( + "The provided buffer size is not equal to the encoded options size." + )); + } + + caller.write(out_ptr, &encoded_options) + }; + + Func::wrap(&mut self.store, closure) + } +} + +impl Instance { + /// Runs the WASM instance from the compiled `Module`. + pub fn run(self, module: &Module) -> ReturnType { + self.internal_run(module).map_err(|e| { + ExecutorError::Other(format!("Error with WASM initialization: {}", e)) + })? + } + + fn internal_run(mut self, module: &Module) -> anyhow::Result { + let instance = self + .linker + .instantiate(&mut self.store, module) + .map_err(|e| { + anyhow::anyhow!("Failed to instantiate the module: {}", e.to_string()) + })?; + + let memory_export = + instance + .get_export(&mut self.store, "memory") + .ok_or_else(|| { + anyhow::anyhow!("memory is not exported under `memory` name") + })?; + + let memory = memory_export.into_memory().ok_or_else(|| { + anyhow::anyhow!("the `memory` export should have memory type") + })?; + self.store.data_mut().memory = Some(memory); + + let run = instance + .get_typed_func::<(u32, u32), u64>(&mut self.store, "execute") + .map_err(|e| { + anyhow::anyhow!("Failed to get the `execute` function: {}", e.to_string()) + })?; + let result = run.call( + &mut self.store, + ( + self.stage.input_component_size, + self.stage.input_options_size, + ), + )?; + + let (ptr, len) = unpack_ptr_and_len(result); + let (ptr, len) = (ptr as usize, len as usize); + let memory = self + .store + .data() + .memory + .expect("Memory was initialized above; qed"); + let slice = &memory.data(&self.store)[ptr..ptr.saturating_add(len)]; + + postcard::from_bytes(slice).map_err(|e| anyhow::anyhow!(e)) + } +} diff --git a/crates/services/upgradable-executor/src/lib.rs b/crates/services/upgradable-executor/src/lib.rs new file mode 100644 index 00000000..6ecca2dd --- /dev/null +++ b/crates/services/upgradable-executor/src/lib.rs @@ -0,0 +1,38 @@ +#![deny(clippy::arithmetic_side_effects)] +#![deny(clippy::cast_possible_truncation)] +#![deny(unused_crate_dependencies)] +#![deny(warnings)] + +#[cfg(feature = "wasm-executor")] +use wasmtime::{ + Config, + Engine, + Module, +}; + +pub mod config; +pub mod executor; + +#[cfg(feature = "wasm-executor")] +pub mod instance; + +/// The WASM version of the underlying [`fuel_core_executor::executor::ExecutionInstance`]. +#[cfg(feature = "wasm-executor")] +pub const WASM_BYTECODE: &[u8] = include_bytes!(concat!( + env!("OUT_DIR"), + "/wasm32-unknown-unknown/release/fuel-core-wasm-executor.wasm" +)); + +#[cfg(feature = "wasm-executor")] +lazy_static::lazy_static! { + /// The default engine for the WASM executor. It is used to compile the WASM bytecode. + pub static ref DEFAULT_ENGINE: Engine = { + Engine::new(&Config::new()).expect("Failed to instantiate the `Engine`") + }; + + /// The default module compiles the WASM bytecode of the native executor. + /// It is used to create the WASM instance of the executor. + pub static ref COMPILED_UNDERLYING_EXECUTOR: Module = { + Module::new(&DEFAULT_ENGINE, WASM_BYTECODE).expect("Failed to compile the underlying executor") + }; +} diff --git a/crates/services/upgradable-executor/wasm-executor/Cargo.toml b/crates/services/upgradable-executor/wasm-executor/Cargo.toml new file mode 100644 index 00000000..32f4211e --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "fuel-core-wasm-executor" +version = { workspace = true } +authors = { workspace = true } +edition = { workspace = true } +homepage = { workspace = true } +keywords = ["blockchain", "fuel", "fuel-vm"] +license = { workspace = true } +repository = { workspace = true } +description = "Fuel Block WASM version of the Executor" + +[[bin]] +name = "fuel-core-wasm-executor" +path = "src/main.rs" + +[lib] +path = "src/lib.rs" + +[dependencies] +anyhow = { workspace = true } +fuel-core-executor = { workspace = true, default-features = false } +fuel-core-storage = { workspace = true, default-features = false } +fuel-core-types = { workspace = true, default-features = false } +postcard = { workspace = true } + +[dev-dependencies] +proptest = { workspace = true } + +[features] +default = ["std"] +std = [] diff --git a/crates/services/upgradable-executor/wasm-executor/src/ext.rs b/crates/services/upgradable-executor/wasm-executor/src/ext.rs new file mode 100644 index 00000000..e4eb5d92 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/ext.rs @@ -0,0 +1,281 @@ +use crate::utils::{ + unpack_exists_size_result, + InputType, +}; +use core::marker::PhantomData; +use fuel_core_executor::{ + executor::ExecutionOptions, + ports::MaybeCheckedTransaction, +}; +use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, + fuel_tx::Transaction, + services::relayer::Event, +}; + +/// A wrapper around a `u32` representing a pointer for WASM. +#[derive(Debug)] +#[repr(transparent)] +pub struct Ptr32<'a, T> +where + T: ?Sized, +{ + /// The internal WASM raw pointer value. + _value: u32, + marker: PhantomData &'a T>, +} + +impl<'a, T> Ptr32<'a, T> +where + T: ?Sized, +{ + fn new(value: u32) -> Self { + Self { + _value: value, + marker: Default::default(), + } + } +} + +impl<'a, T> Ptr32<'a, [T]> { + /// Creates a new WASM pointer from the given slice. + pub fn from_slice(slice: &'a [T]) -> Self { + Self::new(slice.as_ptr() as u32) + } +} + +/// A wrapper around a `u32` representing a pointer for WASM. +#[derive(Debug)] +#[repr(transparent)] +pub struct Ptr32Mut<'a, T> +where + T: ?Sized, +{ + /// The internal WASM raw pointer value. + _value: u32, + marker: PhantomData &'a mut T>, +} + +impl<'a, T> Ptr32Mut<'a, T> +where + T: ?Sized, +{ + fn new(value: u32) -> Self { + Self { + _value: value, + marker: Default::default(), + } + } +} + +impl<'a, T> Ptr32Mut<'a, [T]> { + /// Creates a new WASM pointer from the given slice. + pub fn from_slice(slice: &'a mut [T]) -> Self { + Self::new(slice.as_ptr() as u32) + } +} + +mod host { + use super::*; + + #[link(wasm_import_module = "host_v0")] + extern "C" { + // Initialization API + + /// Returns the encoded component as an input to the executor. + pub(crate) fn input_component(output_ptr: Ptr32Mut<[u8]>, output_size: u32); + + /// Returns the encoded execution options as an input to the executor. + pub(crate) fn input_options(output_ptr: Ptr32Mut<[u8]>, output_size: u32); + + // TxSource API + + /// Returns the size of the next encoded transactions. + /// If the size is 0, there are no more transactions. + pub(crate) fn peek_next_txs_size(gas_limit: u64) -> u32; + + /// Consumes the next transactions from the host. + /// Calling this function before `peek_next_txs_size` do nothing. + pub(crate) fn consume_next_txs(output_ptr: Ptr32Mut<[u8]>, output_size: u32); + + // Storage API + + /// Returns the size of the value from the storage. + pub(crate) fn storage_size_of_value( + key_ptr: Ptr32<[u8]>, + key_len: u32, + column: u32, + ) -> u64; + + /// Returns the value from the storage. + pub(crate) fn storage_get( + key_ptr: Ptr32<[u8]>, + key_len: u32, + column: u32, + out_ptr: Ptr32Mut<[u8]>, + out_len: u32, + ) -> ReturnResult; + + // Relayer API + + /// Returns the `true` of relayer is enabled. + pub(crate) fn relayer_enabled() -> bool; + + /// Returns the size of the encoded events at `da_block_height`. + pub(crate) fn relayer_size_of_events(da_block_height: u64) -> u64; + + /// Writes the encoded events at `da_block_height` into the `output_ptr`. + pub(crate) fn relayer_get_events( + da_block_height: u64, + output_ptr: Ptr32Mut<[u8]>, + ); + } +} + +/// The result returned by the host function. +#[repr(transparent)] +pub struct ReturnResult(u16); + +/// Gets the `InputType` by using the host function. The `size` is the size of the encoded input. +pub fn input_component(size: usize) -> anyhow::Result { + let mut encoded_block = vec![0u8; size]; + let size = encoded_block.len(); + unsafe { + host::input_component( + Ptr32Mut::from_slice(encoded_block.as_mut_slice()), + u32::try_from(size).expect("We only support wasm32 target; qed"), + ) + }; + + let input: InputType = postcard::from_bytes(&encoded_block) + .map_err(|e| anyhow::anyhow!("Failed to decode the block: {:?}", e))?; + + Ok(input) +} + +/// Gets the `ExecutionOptions` by using the host function. The `size` is the size of the encoded input. +pub fn input_options(size: usize) -> anyhow::Result { + let mut encoded_block = vec![0u8; size]; + let size = encoded_block.len(); + unsafe { + host::input_options( + Ptr32Mut::from_slice(encoded_block.as_mut_slice()), + u32::try_from(size).expect("We only support wasm32 target; qed"), + ) + }; + + let input: ExecutionOptions = postcard::from_bytes(&encoded_block).map_err(|e| { + anyhow::anyhow!("Failed to decode the execution options: {:?}", e) + })?; + + Ok(input) +} + +/// Gets the next transactions by using the host function. +pub fn next_transactions(gas_limit: u64) -> anyhow::Result> { + let next_size = unsafe { host::peek_next_txs_size(gas_limit) }; + + if next_size == 0 { + return Ok(Vec::new()); + } + + let mut encoded_block = vec![0u8; next_size as usize]; + let size = encoded_block.len(); + + unsafe { + host::consume_next_txs( + Ptr32Mut::from_slice(encoded_block.as_mut_slice()), + u32::try_from(size).expect("We only support wasm32 target; qed"), + ) + }; + + let txs: Vec = postcard::from_bytes(&encoded_block) + .map_err(|e| anyhow::anyhow!("Failed to decode the transactions: {:?}", e))?; + + Ok(txs + .into_iter() + .map(MaybeCheckedTransaction::Transaction) + .collect()) +} + +/// Gets the size of the value under the `key` in the `column` from the storage. +pub fn size_of_value(key: &[u8], column: u32) -> anyhow::Result> { + let val = unsafe { + host::storage_size_of_value( + Ptr32::from_slice(key), + u32::try_from(key.len()).expect("We only support wasm32 target; qed"), + column, + ) + }; + + let (exists, size, result) = unpack_exists_size_result(val); + + if result != 0 { + return Err(anyhow::anyhow!( + "Failed to get the size of the value from the WASM storage" + )); + } + + if exists { + Ok(Some(size as usize)) + } else { + Ok(None) + } +} + +/// Gets the value under the `key` in the `column` from the storage and copies it to the `out`. +pub fn get(key: &[u8], column: u32, out: &mut [u8]) -> anyhow::Result<()> { + let output_size = out.len(); + let result = unsafe { + host::storage_get( + Ptr32::from_slice(key), + u32::try_from(key.len()).expect("We only support wasm32 target; qed"), + column, + Ptr32Mut::from_slice(out), + u32::try_from(output_size).expect("We only support wasm32 target; qed"), + ) + }; + + if result.0 != 0 { + return Err(anyhow::anyhow!( + "Failed to get the value from the WASM storage" + )); + } + + Ok(()) +} + +/// Returns `true` if the relayer is enabled. Uses a host function. +pub fn relayer_enabled() -> bool { + unsafe { host::relayer_enabled() } +} + +/// Gets the events at the `da_block_height` from the relayer by using host functions. +pub fn relayer_get_events(da_block_height: DaBlockHeight) -> anyhow::Result> { + let size = unsafe { host::relayer_size_of_events(da_block_height.into()) }; + let (exists, size, result) = unpack_exists_size_result(size); + + if result != 0 { + return Err(anyhow::anyhow!( + "Failed to get the size of events from the WASM relayer" + )); + } + + if !exists || size == 0 { + return Ok(vec![]); + } + + let mut encoded_events = vec![0u8; size as usize]; + + unsafe { + host::relayer_get_events( + da_block_height.into(), + Ptr32Mut::from_slice(encoded_events.as_mut_slice()), + ) + }; + + let events: Vec = postcard::from_bytes(&encoded_events) + .map_err(|e| anyhow::anyhow!("Failed to decode the events: {:?}", e))?; + + Ok(events) +} diff --git a/crates/services/upgradable-executor/wasm-executor/src/lib.rs b/crates/services/upgradable-executor/wasm-executor/src/lib.rs new file mode 100644 index 00000000..2bc89032 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/lib.rs @@ -0,0 +1,7 @@ +//! The library defines some common parts used by the WASM executor and upgradable executor. + +#![deny(clippy::arithmetic_side_effects)] +#![deny(clippy::cast_possible_truncation)] +#![deny(warnings)] + +pub mod utils; diff --git a/crates/services/upgradable-executor/wasm-executor/src/main.rs b/crates/services/upgradable-executor/wasm-executor/src/main.rs new file mode 100644 index 00000000..af08ebc0 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/main.rs @@ -0,0 +1,116 @@ +//! This is the main entry point for the wasm executor. +//! The module defines the `execute` function that the host will call. +//! The result of the execution is the `ExecutionResult` with the list of changes to the storage. +//! +//! During return, the result of the execution modules leaks the memory, +//! allowing the WASM runner to get access to the data. +//! +//! Currently, the WASM executor is designed only for one block execution per WASM instance. +//! But later, it will be improved, and the instance will be reusable. + +#![deny(clippy::arithmetic_side_effects)] +#![deny(clippy::cast_possible_truncation)] +#![deny(unused_crate_dependencies)] +#![deny(warnings)] + +use crate::{ + relayer::WasmRelayer, + storage::WasmStorage, + tx_source::WasmTxSource, + utils::{ + pack_ptr_and_len, + ReturnType, + }, +}; +use fuel_core_executor::executor::ExecutionInstance; +use fuel_core_types::{ + fuel_tx::ConsensusParameters, + services::{ + block_producer::Components, + executor::{ + Error as ExecutorError, + ExecutionResult, + }, + Uncommitted, + }, +}; +use fuel_core_wasm_executor as _; + +mod ext; +mod relayer; +mod storage; +mod tx_source; +pub mod utils; + +#[no_mangle] +pub extern "C" fn execute(component_len: u32, options_len: u32) -> u64 { + let result = execute_without_commit(component_len, options_len); + let encoded = postcard::to_allocvec(&result).expect("Failed to encode the result"); + let static_slice = encoded.leak(); + pack_ptr_and_len( + static_slice.as_ptr() as u32, + u32::try_from(static_slice.len()).expect("We only support wasm32 target; qed"), + ) +} + +pub fn execute_without_commit(component_len: u32, options_len: u32) -> ReturnType { + let block = ext::input_component(component_len as usize) + .map_err(|e| ExecutorError::Other(e.to_string()))?; + let mut options = ext::input_options(options_len as usize) + .map_err(|e| ExecutorError::Other(e.to_string()))?; + + let consensus_params = if let Some(consensus_params) = options.consensus_params.take() + { + consensus_params + } else { + ConsensusParameters::default() + }; + + let instance = ExecutionInstance { + relayer: WasmRelayer {}, + database: WasmStorage {}, + consensus_params, + options, + }; + + let block = block.map_p(|component| { + let Components { + header_to_produce, + gas_limit, + gas_price, + coinbase_recipient, + .. + } = component; + + Components { + header_to_produce, + gas_limit, + gas_price, + transactions_source: WasmTxSource::new(), + coinbase_recipient, + } + }); + + let ( + ExecutionResult { + block, + skipped_transactions, + tx_status, + events, + }, + changes, + ) = instance.execute_without_commit(block)?.into(); + + Ok(Uncommitted::new( + ExecutionResult { + block, + skipped_transactions, + tx_status, + events, + }, + changes, + )) +} + +// It is not used. It was added to make clippy happy. +fn main() {} diff --git a/crates/services/upgradable-executor/wasm-executor/src/relayer.rs b/crates/services/upgradable-executor/wasm-executor/src/relayer.rs new file mode 100644 index 00000000..940a903a --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/relayer.rs @@ -0,0 +1,18 @@ +use crate::ext; +use fuel_core_executor::ports::RelayerPort; +use fuel_core_types::{ + blockchain::primitives::DaBlockHeight, + services::relayer::Event, +}; + +pub struct WasmRelayer; + +impl RelayerPort for WasmRelayer { + fn enabled(&self) -> bool { + ext::relayer_enabled() + } + + fn get_events(&self, da_block_height: &DaBlockHeight) -> anyhow::Result> { + ext::relayer_get_events(*da_block_height) + } +} diff --git a/crates/services/upgradable-executor/wasm-executor/src/storage.rs b/crates/services/upgradable-executor/wasm-executor/src/storage.rs new file mode 100644 index 00000000..add3fe77 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/storage.rs @@ -0,0 +1,35 @@ +use super::ext; +use fuel_core_storage::{ + column::Column, + kv_store::{ + KeyValueInspect, + Value, + }, + Result as StorageResult, +}; + +pub struct WasmStorage; + +impl KeyValueInspect for WasmStorage { + type Column = Column; + + fn size_of_value( + &self, + key: &[u8], + column: Self::Column, + ) -> StorageResult> { + ext::size_of_value(key, column.as_u32()).map_err(Into::into) + } + + fn get(&self, key: &[u8], column: Self::Column) -> StorageResult> { + let size = ext::size_of_value(key, column.as_u32())?; + + if let Some(size) = size { + let mut value = vec![0u8; size]; + ext::get(key, column.as_u32(), &mut value)?; + Ok(Some(value.into())) + } else { + Ok(None) + } + } +} diff --git a/crates/services/upgradable-executor/wasm-executor/src/tx_source.rs b/crates/services/upgradable-executor/wasm-executor/src/tx_source.rs new file mode 100644 index 00000000..772b8159 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/tx_source.rs @@ -0,0 +1,19 @@ +use crate::ext; +use fuel_core_executor::ports::{ + MaybeCheckedTransaction, + TransactionsSource, +}; + +pub struct WasmTxSource; + +impl WasmTxSource { + pub fn new() -> Self { + Self + } +} + +impl TransactionsSource for WasmTxSource { + fn next(&self, gas_limit: u64) -> Vec { + ext::next_transactions(gas_limit).expect("Failed to get next transactions") + } +} diff --git a/crates/services/upgradable-executor/wasm-executor/src/utils.rs b/crates/services/upgradable-executor/wasm-executor/src/utils.rs new file mode 100644 index 00000000..18c67386 --- /dev/null +++ b/crates/services/upgradable-executor/wasm-executor/src/utils.rs @@ -0,0 +1,73 @@ +use fuel_core_executor::executor::ExecutionBlockWithSource; +use fuel_core_storage::transactional::Changes; +use fuel_core_types::services::{ + executor::{ + ExecutionResult, + Result as ExecutorResult, + }, + Uncommitted, +}; + +/// Pack a pointer and length into an `u64`. +pub fn pack_ptr_and_len(ptr: u32, len: u32) -> u64 { + (u64::from(len) << 32) | u64::from(ptr) +} + +/// Unpacks an `u64` into the pointer and length. +pub fn unpack_ptr_and_len(val: u64) -> (u32, u32) { + let ptr = u32::try_from(val & (u32::MAX as u64)) + .expect("It ony contains first 32 bytes; qed"); + let len = u32::try_from(val >> 32).expect("It ony contains first 32 bytes; qed"); + + (ptr, len) +} + +/// Pack a `exists`, `size` and `result` into one `u64`. +pub fn pack_exists_size_result(exists: bool, size: u32, result: u16) -> u64 { + (u64::from(result) << 33 | u64::from(size) << 1) | u64::from(exists) +} + +/// Unpacks an `u64` into `exists`, `size` and `result`. +pub fn unpack_exists_size_result(val: u64) -> (bool, u32, u16) { + let exists = (val & 1u64) != 0; + let size = u32::try_from((val >> 1) & (u32::MAX as u64)) + .expect("It only contains first 32 bytes; qed"); + let result = u16::try_from(val >> 33 & (u16::MAX as u64)) + .expect("It only contains first 16 bytes; qed"); + + (exists, size, result) +} + +pub type InputType = ExecutionBlockWithSource<()>; + +pub type ReturnType = ExecutorResult>; + +#[cfg(test)] +mod tests { + use super::*; + use proptest::prelude::prop::*; + + proptest::proptest! { + #[test] + fn can_pack_any_values(exists: bool, size: u32, result: u16) { + pack_exists_size_result(exists, size, result); + } + + #[test] + fn can_unpack_any_values(value: u64) { + let _ = unpack_exists_size_result(value); + } + + + #[test] + fn unpacks_packed_values(exists: bool, size: u32, result: u16) { + let packed = pack_exists_size_result(exists, size, result); + let (unpacked_exists, unpacked_size, unpacked_result) = + unpack_exists_size_result(packed); + + proptest::prop_assert_eq!(exists, unpacked_exists); + proptest::prop_assert_eq!(size, unpacked_size); + proptest::prop_assert_eq!(result, unpacked_result); + } + } +} diff --git a/crates/storage/Cargo.toml b/crates/storage/Cargo.toml index df9ea8b0..655ab988 100644 --- a/crates/storage/Cargo.toml +++ b/crates/storage/Cargo.toml @@ -27,6 +27,7 @@ fuel-vm-private = { workspace = true, default-features = false } impl-tools = "0.10" itertools = { workspace = true, features = ["use_alloc"] } mockall = { workspace = true, optional = true } +num_enum = { workspace = true } paste = "1" postcard = { workspace = true, features = ["alloc"] } primitive-types = { workspace = true, default-features = false } @@ -45,4 +46,6 @@ fuel-core-types = { workspace = true, default-features = false, features = [ test-case = { workspace = true } [features] +default = ["std"] +std = ["fuel-core-types/std"] test-helpers = ["dep:mockall", "dep:rand"] diff --git a/crates/storage/src/column.rs b/crates/storage/src/column.rs index 3848a030..078f20fc 100644 --- a/crates/storage/src/column.rs +++ b/crates/storage/src/column.rs @@ -16,6 +16,7 @@ use crate::kv_store::StorageColumn; Eq, enum_iterator::Sequence, Hash, + num_enum::TryFromPrimitive, )] pub enum Column { /// The column id of metadata about the blockchain diff --git a/crates/storage/src/kv_store.rs b/crates/storage/src/kv_store.rs index ae60b9a0..9bdb0809 100644 --- a/crates/storage/src/kv_store.rs +++ b/crates/storage/src/kv_store.rs @@ -115,7 +115,7 @@ pub trait KeyValueMutate: KeyValueInspect { } /// The operation to write into the storage. -#[derive(Clone, Debug, PartialEq, Eq)] +#[derive(serde::Serialize, serde::Deserialize, Clone, Debug, PartialEq, Eq)] pub enum WriteOperation { /// Insert the value into the storage. Insert(Value), diff --git a/crates/storage/src/lib.rs b/crates/storage/src/lib.rs index 4d25f4d4..50f3f1d8 100644 --- a/crates/storage/src/lib.rs +++ b/crates/storage/src/lib.rs @@ -80,7 +80,7 @@ impl From for Error { impl From for ExecutorError { fn from(e: Error) -> Self { - ExecutorError::StorageError(anyhow::anyhow!(e)) + ExecutorError::StorageError(e.to_string()) } } diff --git a/crates/types/src/blockchain/header.rs b/crates/types/src/blockchain/header.rs index d6a55cb7..32154fb3 100644 --- a/crates/types/src/blockchain/header.rs +++ b/crates/types/src/blockchain/header.rs @@ -147,6 +147,7 @@ impl BlockHeader { } #[derive(Clone, Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[cfg_attr(any(test, feature = "test-helpers"), derive(Default))] /// A partially complete fuel block header that doesn't not /// have any generated fields because it has not been executed yet. diff --git a/crates/types/src/blockchain/primitives.rs b/crates/types/src/blockchain/primitives.rs index bbf63ce5..bc3c9e04 100644 --- a/crates/types/src/blockchain/primitives.rs +++ b/crates/types/src/blockchain/primitives.rs @@ -20,13 +20,13 @@ use derive_more::{ UpperHex, }; use secrecy::{ - zeroize, CloneableSecret, DebugSecret, }; use zeroize::Zeroize; #[derive(Clone, Copy, Debug, Default)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] /// Empty generated fields. pub struct Empty; diff --git a/crates/types/src/services.rs b/crates/types/src/services.rs index aa5bc8eb..3082ae3c 100644 --- a/crates/types/src/services.rs +++ b/crates/types/src/services.rs @@ -13,6 +13,7 @@ pub mod txpool; /// The uncommitted `Result` of some action with storage changes. /// The user should commit the result by itself. #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[must_use] pub struct Uncommitted { /// The result of the action. diff --git a/crates/types/src/services/block_producer.rs b/crates/types/src/services/block_producer.rs index 586ea008..d1e03335 100644 --- a/crates/types/src/services/block_producer.rs +++ b/crates/types/src/services/block_producer.rs @@ -1,9 +1,13 @@ //! Types related to block producer service. -use crate::blockchain::header::PartialBlockHeader; +use crate::{ + blockchain::header::PartialBlockHeader, + fuel_tx::ContractId, +}; /// The components required to produce a block. #[derive(Debug)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Components { /// The partial block header of the future block without transactions related information. pub header_to_produce: PartialBlockHeader, @@ -11,6 +15,8 @@ pub struct Components { /// It can be a predefined vector of transactions, a stream of transactions, /// or any other type that carries the transactions. pub transactions_source: Source, + /// The `ContractId` of the fee recipient. + pub coinbase_recipient: ContractId, /// The gas price for all transactions in the block. pub gas_price: u64, /// The gas limit of the block. diff --git a/crates/types/src/services/executor.rs b/crates/types/src/services/executor.rs index f1b82945..78ae5bdd 100644 --- a/crates/types/src/services/executor.rs +++ b/crates/types/src/services/executor.rs @@ -25,13 +25,10 @@ use crate::{ }, fuel_vm::{ checked_transaction::CheckError, - Backtrace, - InterpreterError, ProgramState, }, services::Uncommitted, }; -use std::error::Error as StdError; /// The alias for executor result. pub type Result = core::result::Result; @@ -40,6 +37,7 @@ pub type UncommittedResult = Uncommitted; /// The result of transactions execution. +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[derive(Debug)] pub struct ExecutionResult { /// Created block during the execution of transactions. It contains only valid transactions. @@ -55,6 +53,7 @@ pub struct ExecutionResult { /// The event represents some internal state changes caused by the block execution. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Event { /// Imported a new spendable message from the relayer. MessageImported(Message), @@ -68,6 +67,7 @@ pub enum Event { /// The status of a transaction after it is executed. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct TransactionExecutionStatus { /// The id of the transaction. pub id: Bytes32, @@ -77,6 +77,7 @@ pub struct TransactionExecutionStatus { /// The result of transaction execution. #[derive(Debug, Clone)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum TransactionExecutionResult { /// Transaction was successfully executed. Success { @@ -121,6 +122,7 @@ impl TransactionExecutionResult { /// Execution wrapper where the types /// depend on the type of execution. #[derive(Debug, Clone, Copy)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum ExecutionTypes { /// DryRun mode where P is being produced. DryRun(P), @@ -289,7 +291,8 @@ impl ExecutionKind { } #[allow(missing_docs)] -#[derive(Debug, derive_more::Display, derive_more::From)] +#[derive(Debug, PartialEq, derive_more::Display, derive_more::From)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[non_exhaustive] pub enum Error { #[display(fmt = "Transaction id was already used: {_0:#x}")] @@ -311,7 +314,7 @@ pub enum Error { #[display(fmt = "The `Mint` transaction mismatches expectations.")] MintMismatch, #[display(fmt = "Can't increase the balance of the coinbase contract: {_0}.")] - CoinbaseCannotIncreaseBalance(anyhow::Error), + CoinbaseCannotIncreaseBalance(String), #[display(fmt = "Coinbase amount mismatches with expected.")] CoinbaseAmountMismatch, #[display(fmt = "Coinbase gas price mismatches with expected.")] @@ -321,20 +324,17 @@ pub enum Error { // TODO: Replace with `fuel_core_storage::Error` when execution error will live in the // `fuel-core-executor`. #[display(fmt = "got error during work with storage {_0}")] - StorageError(anyhow::Error), + StorageError(String), #[display(fmt = "got error during work with relayer {_0}")] - RelayerError(Box), + RelayerError(String), #[display(fmt = "Transaction({transaction_id:#x}) execution error: {error:?}")] VmExecution { - // TODO: Replace with `fuel_core_storage::Error` when execution error will live in the - // `fuel-core-executor`. - error: InterpreterError, + // TODO: Use `InterpreterError` when `InterpreterError` implements serde + error: String, transaction_id: Bytes32, }, #[display(fmt = "{_0:?}")] InvalidTransaction(CheckError), - #[display(fmt = "Execution error with backtrace")] - Backtrace(Box), #[display(fmt = "Transaction doesn't match expected result: {transaction_id:#x}")] InvalidTransactionOutcome { transaction_id: Bytes32 }, #[display(fmt = "The amount of charged fees is invalid")] @@ -355,6 +355,9 @@ pub enum Error { PreviousBlockIsNotFound, #[display(fmt = "The relayer gives incorrect messages for the requested da height")] RelayerGivesIncorrectMessages, + /// It is possible to occur untyped errors in the case of the upgrade. + #[display(fmt = "Occurred untyped error: {_0}")] + Other(String), } impl From for anyhow::Error { @@ -363,12 +366,6 @@ impl From for anyhow::Error { } } -impl From for Error { - fn from(e: Backtrace) -> Self { - Error::Backtrace(Box::new(e)) - } -} - impl From for Error { fn from(e: CheckError) -> Self { Self::InvalidTransaction(e) @@ -382,7 +379,8 @@ impl From for Error { } #[allow(missing_docs)] -#[derive(thiserror::Error, Debug)] +#[derive(thiserror::Error, Debug, PartialEq)] +#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] #[non_exhaustive] pub enum TransactionValidityError { #[error("Coin({0:#x}) input was already spent")] diff --git a/deployment/Dockerfile b/deployment/Dockerfile index bd5afe23..e83b75b9 100644 --- a/deployment/Dockerfile +++ b/deployment/Dockerfile @@ -3,7 +3,7 @@ FROM --platform=$BUILDPLATFORM tonistiigi/xx AS xx FROM --platform=$BUILDPLATFORM rust:1.75.0 AS chef ARG TARGETPLATFORM -RUN cargo install cargo-chef +RUN cargo install cargo-chef && rustup target add wasm32-unknown-unknown WORKDIR /build/ COPY --from=xx / / diff --git a/deployment/e2e-client.Dockerfile b/deployment/e2e-client.Dockerfile index bced2f74..7741734b 100644 --- a/deployment/e2e-client.Dockerfile +++ b/deployment/e2e-client.Dockerfile @@ -1,6 +1,6 @@ # Stage 1: Build FROM rust:1.75.0 AS chef -RUN cargo install cargo-chef +RUN cargo install cargo-chef && rustup target add wasm32-unknown-unknown WORKDIR /build/ # hadolint ignore=DL3008 RUN apt-get update && \ diff --git a/tests/Cargo.toml b/tests/Cargo.toml index 399e7f49..0551b632 100644 --- a/tests/Cargo.toml +++ b/tests/Cargo.toml @@ -64,3 +64,4 @@ pretty_assertions = "1.4" default = ["fuel-core/default", "relayer"] p2p = ["fuel-core/p2p", "fuel-core-p2p"] relayer = ["fuel-core/relayer", "fuel-core-relayer"] +wasm-executor = ["fuel-core/wasm-executor"]