diff --git a/Cargo.lock b/Cargo.lock index 35760a178..b6956e358 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -874,9 +874,9 @@ dependencies = [ [[package]] name = "arc-swap" -version = "1.8.2" +version = "1.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f9f3647c145568cec02c42054e07bdf9a5a698e15b466fb2341bfc393cd24aa5" +checksum = "51d03449bb8ca2cc2ef70869af31463d1ae5ccc8fa3e334b307203fbf815207e" dependencies = [ "rustversion", ] @@ -1659,7 +1659,7 @@ version = "0.68.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "726e4313eb6ec35d2730258ad4e15b547ee75d6afaa1361a922e78e59b7d8078" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "lazy_static", @@ -1679,7 +1679,7 @@ version = "0.69.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "271383c67ccabffb7381723dea0672a673f292304fcb45c01cc648c7a8d58088" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.12.1", @@ -1702,7 +1702,7 @@ version = "0.70.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f49d8fed880d473ea71efb9bf597651e77201bdd4893efe54c9e5d65ae04ce6f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1720,7 +1720,7 @@ version = "0.71.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f58bf3d7db68cfbac37cfc485a8d711e87e064c3d0fe0435b92f7a407f9d6b3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cexpr", "clang-sys", "itertools 0.13.0", @@ -1793,9 +1793,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bitflags" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "812e12b5285cc515a9c72a5c1d3b6d46a19dac5acfef5265968c166106e31dd3" +checksum = "843867be96c8daad0d758b57df9392b6d8d271134fce549de6ce169ff98a92af" [[package]] name = "bitvec" @@ -2885,6 +2885,7 @@ version = "1.7.0" dependencies = [ "anyhow", "ark-ff 0.4.2", + "cainome-cairo-serde", "katana-contracts", "katana-genesis", "katana-primitives", @@ -2932,7 +2933,7 @@ dependencies = [ "serde_json", "syn 2.0.104", "tempfile", - "toml 0.9.6", + "toml 0.9.5", ] [[package]] @@ -2985,7 +2986,7 @@ dependencies = [ "num-traits", "serde", "wasm-bindgen", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -3117,7 +3118,7 @@ dependencies = [ "serde", "serde-big-array", "sysinfo 0.35.2", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -3443,7 +3444,7 @@ version = "0.28.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "829d955a0bb380ef178a640b91779e3987da38c9aea133b20614cfed8cdea9c6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm_winapi", "parking_lot", "rustix 0.38.44", @@ -3687,7 +3688,7 @@ version = "0.8.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bef552e6f588e446098f6ba40d89ac146c8c7b64aade83c051ee00bb5d2bc18d" dependencies = [ - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -3722,12 +3723,12 @@ dependencies = [ [[package]] name = "deranged" -version = "0.4.0" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c9e6a11ca8224451684bc0d7d5a7adbf8f2fd6887261a1cfc3c0432f9d4068e" +checksum = "ececcb659e7ba858fb4f10388c250a7252eb0a27373f1a72b8748afdd248e587" dependencies = [ "powerfmt", - "serde", + "serde_core", ] [[package]] @@ -4558,6 +4559,19 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "getrandom" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "139ef39800118c7683f2fd3c98c1b23c09ae076556b435f8e9064ae108aaeeec" +dependencies = [ + "cfg-if", + "libc", + "r-efi", + "wasip2", + "wasip3", +] + [[package]] name = "gimli" version = "0.31.1" @@ -4589,7 +4603,7 @@ version = "0.9.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0bf760ebf69878d9fd8f110c89703d90ce35095324d1f1edcb595c63945ee757" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "ignore", "walkdir", ] @@ -5367,7 +5381,7 @@ version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0fddf93031af70e75410a2511ec04d49e758ed2f26dad3404a934e0fb45cc12a" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "crossterm 0.25.0", "dyn-clone", "fuzzy-matcher", @@ -5402,7 +5416,7 @@ version = "0.7.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d93587f37623a1a17d94ef2bc9ada592f5465fe7732084ab7beefabe5c77c0c4" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "libc", ] @@ -6553,6 +6567,7 @@ dependencies = [ "arbitrary", "assert_matches", "blockifier", + "cainome", "cainome-cairo-serde", "cairo-lang-sierra", "cairo-lang-starknet-classes", @@ -6681,6 +6696,7 @@ dependencies = [ "anyhow", "assert_matches", "auto_impl", + "axum 0.7.9", "cainome", "cairo-lang-starknet-classes", "cartridge", @@ -6794,6 +6810,7 @@ name = "katana-sequencer-node" version = "1.7.0" dependencies = [ "anyhow", + "cartridge", "futures", "http 1.3.1", "jsonrpsee 0.26.0", @@ -6819,7 +6836,9 @@ dependencies = [ "katana-tee", "num-traits", "serde", + "starknet 0.17.0 (registry+https://github.com/rust-lang/crates.io-index)", "tempfile", + "tower 0.5.2", "tracing", "url", ] @@ -7104,11 +7123,17 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55" +[[package]] +name = "leb128fmt" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" + [[package]] name = "libc" -version = "0.2.180" +version = "0.2.174" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bcc35a38544a891a5f7c865aca548a982ccb3b8650a5b06d0fd33a10283c56fc" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" [[package]] name = "libloading" @@ -7143,7 +7168,7 @@ version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "391290121bad3d37fbddad76d8f5d1c1c314cfc646d143d7e07a3086ddff0ce3" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "libc", ] @@ -7191,9 +7216,9 @@ dependencies = [ [[package]] name = "log" -version = "0.4.27" +version = "0.4.29" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" +checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897" [[package]] name = "lru" @@ -7528,9 +7553,9 @@ dependencies = [ [[package]] name = "moka" -version = "0.12.13" +version = "0.12.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4ac832c50ced444ef6be0767a008b02c106a909ba79d1d830501e94b96f6b7e" +checksum = "a3dec6bd31b08944e08b58fd99373893a6c17054d6f3ea5006cc894f4f4eee2a" dependencies = [ "crossbeam-channel", "crossbeam-epoch", @@ -7540,7 +7565,7 @@ dependencies = [ "portable-atomic", "smallvec", "tagptr", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -7551,17 +7576,17 @@ checksum = "1d87ecb2933e8aeadb3e3a02b828fed80a7528047e68b4f424523a0981a3a084" [[package]] name = "native-tls" -version = "0.2.18" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "465500e14ea162429d264d44189adc38b199b62b1c21eea9f69e4b73cb03bbf2" +checksum = "87de3442987e9dbec73158d5c715e7ad9072fda936bb03d19d7fa10e00520f0e" dependencies = [ "libc", "log", "openssl", - "openssl-probe 0.2.1", + "openssl-probe", "openssl-sys", "schannel", - "security-framework 3.6.0", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -7622,7 +7647,7 @@ version = "0.30.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "74523f3a35e05aba87a1d978330aef40f67b0304ac79c1c00b294c9830543db6" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "cfg_aliases", "libc", @@ -7689,9 +7714,9 @@ dependencies = [ [[package]] name = "num-conv" -version = "0.1.0" +version = "0.2.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +checksum = "cf97ec579c3c42f953ef76dbf8d55ac91fb219dde70e49aa4a6b7d74e9919050" [[package]] name = "num-format" @@ -7828,7 +7853,7 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c10c2894a6fed806ade6027bcd50662746363a9589d3ec9d9bef30a4e4bc166" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -7889,7 +7914,7 @@ version = "0.10.75" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "08838db121398ad17ab8531ce9de97b244589089e290a384c900cb9ff7434328" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "cfg-if", "foreign-types", "libc", @@ -7915,12 +7940,6 @@ version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d05e27ee213611ffe7d6348b942e8f942b37114c00cc03cec254295a4a17852e" -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - [[package]] name = "openssl-src" version = "300.5.4+3.5.4" @@ -8290,7 +8309,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -8396,7 +8415,7 @@ dependencies = [ "thiserror 2.0.12", "tokio", "tracing", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -8833,7 +8852,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cc5b72d8145275d844d4b5f6d4e1eef00c8cd889edb6035c21675d1bb1f45c9f" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hex", "procfs-core", "rustix 0.38.44", @@ -8845,7 +8864,7 @@ version = "0.17.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "239df02d8349b06fc07398a3a1697b06418223b1c7725085e801e7c0fc6a12ec" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "hex", ] @@ -8857,7 +8876,7 @@ checksum = "6fcdab19deb5195a31cf7726a210015ff1496ba1464fd42cb4f537b8b01b471f" dependencies = [ "bit-set", "bit-vec", - "bitflags 2.10.0", + "bitflags 2.11.0", "lazy_static", "num-traits", "rand 0.9.2", @@ -9231,7 +9250,7 @@ version = "11.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c6df7ab838ed27997ba19a4664507e6f82b41fe6e20be42929332156e5e85146" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -9287,7 +9306,7 @@ version = "0.5.17" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5407465600fb0548f1442edf71dd20683c6ed326200ace4b1ef0763521bb3b77" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", ] [[package]] @@ -9442,7 +9461,7 @@ name = "reth-libmdbx" version = "0.1.0-alpha.13" source = "git+https://github.com/paradigmxyz/reth.git?rev=b34b0d3#b34b0d3c8de2598b2976f7ee2fc1a166c50b1b94" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "derive_more 0.99.20", "indexmap 2.10.0", @@ -9521,7 +9540,7 @@ dependencies = [ "rkyv_derive", "seahash", "tinyvec", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -9830,7 +9849,7 @@ version = "0.38.44" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fdb5bc1ae2baa591800df16c9ca78619bf65c0488b41b96ccec5d11220d8c154" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.4.15", @@ -9843,7 +9862,7 @@ version = "1.0.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "11181fbabf243db407ef8df94a6ce0b2f9a733bd8be4ad02b4eda9602296cac8" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "errno", "libc", "linux-raw-sys 0.9.4", @@ -9886,7 +9905,7 @@ version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5bfb394eeed242e909609f56089eecfe5fda225042e8b171791b9c95f5931e5" dependencies = [ - "openssl-probe 0.1.6", + "openssl-probe", "rustls-pemfile", "rustls-pki-types", "schannel", @@ -9899,10 +9918,10 @@ version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ - "openssl-probe 0.1.6", + "openssl-probe", "rustls-pki-types", "schannel", - "security-framework 3.6.0", + "security-framework 3.3.0", ] [[package]] @@ -9939,7 +9958,7 @@ dependencies = [ "rustls-native-certs 0.8.1", "rustls-platform-verifier-android", "rustls-webpki 0.103.4", - "security-framework 3.6.0", + "security-framework 3.3.0", "security-framework-sys", "webpki-root-certs 0.26.11", "windows-sys 0.59.0", @@ -10038,11 +10057,11 @@ dependencies = [ [[package]] name = "schannel" -version = "0.1.28" +version = "0.1.27" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" +checksum = "1f29ebaa345f945cec9fbbc532eb307f0fdad8161f281b6369539c8d84876b3d" dependencies = [ - "windows-sys 0.61.2", + "windows-sys 0.59.0", ] [[package]] @@ -10173,7 +10192,7 @@ version = "2.11.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "core-foundation-sys", "libc", @@ -10182,11 +10201,11 @@ dependencies = [ [[package]] name = "security-framework" -version = "3.6.0" +version = "3.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d17b898a6d6948c3a8ee4372c17cb384f90d2e6e912ef00895b14fd7ab54ec38" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.10.1", "core-foundation-sys", "libc", @@ -10195,9 +10214,9 @@ dependencies = [ [[package]] name = "security-framework-sys" -version = "2.17.0" +version = "2.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ce2691df843ecc5d231c0b14ece2acc3efb62c0a398c7e1d875f3983ce020e3" +checksum = "49db231d56a190491cb4aeda9527f1ad45345af50b0851622a7adb8c03b01c32" dependencies = [ "core-foundation-sys", "libc", @@ -10338,11 +10357,11 @@ dependencies = [ [[package]] name = "serde_spanned" -version = "1.0.4" +version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8bbf91e5a4d6315eee45e704372590b30e260ee83af6639d64557f51b067776" +checksum = "40734c41988f7306bb04f0ecf60ec0f3f1caa34290e4e8ea471dcd3346483b83" dependencies = [ - "serde_core", + "serde", ] [[package]] @@ -10407,7 +10426,7 @@ checksum = "c2ff74d7e7d1cc172f3a45adec74fbeee928d71df095b85aaaf66eb84e1e31e6" dependencies = [ "base64 0.22.1", "bitfield", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "dirs", "hex", @@ -10415,17 +10434,17 @@ dependencies = [ "lazy_static", "libc", "static_assertions", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] name = "sev" version = "7.1.0" -source = "git+https://github.com/virtee/sev?branch=main#3d5c52ff7dcd3f67b14f77d96d76aae1eb9f7f8b" +source = "git+https://github.com/virtee/sev?branch=main#900d42d6a1f9102ed52faa3a3889b54e8a7e12c8" dependencies = [ "base64 0.22.1", "bitfield", - "bitflags 2.10.0", + "bitflags 2.11.0", "byteorder", "dirs", "hex", @@ -10434,13 +10453,13 @@ dependencies = [ "libc", "openssl", "static_assertions", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] name = "sev-snp" version = "0.3.0" -source = "git+https://github.com/automata-network/amd-sev-snp-attestation-sdk?branch=main#ce1bf49f7d6a457df55894c21182accf081c1dd4" +source = "git+https://github.com/automata-network/amd-sev-snp-attestation-sdk?branch=main#07162a4dd8d692af68484084b972f8b9b286359b" dependencies = [ "asn1-rs", "bincode 1.3.3", @@ -11354,7 +11373,7 @@ dependencies = [ "debugid", "memmap2", "stable_deref_trait", - "uuid 1.20.0", + "uuid 1.21.0", ] [[package]] @@ -11475,7 +11494,7 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "core-foundation 0.9.4", "system-configuration-sys", ] @@ -11625,9 +11644,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.41" +version = "0.3.46" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" +checksum = "9da98b7d9b7dad93488a84b8248efc35352b0b2657397d4167e7ad67e5d535e5" dependencies = [ "deranged", "itoa", @@ -11635,22 +11654,22 @@ dependencies = [ "num-conv", "num_threads", "powerfmt", - "serde", + "serde_core", "time-core", "time-macros", ] [[package]] name = "time-core" -version = "0.1.4" +version = "0.1.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" +checksum = "7694e1cfe791f8d31026952abf09c69ca6f6fa4e1a1229e18988f06a04a12dca" [[package]] name = "time-macros" -version = "0.2.22" +version = "0.2.26" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" +checksum = "78cc610bac2dcee56805c99642447d4c5dbde4d01f752ffea0199aee1f601dc4" dependencies = [ "num-conv", "time-core", @@ -11813,14 +11832,14 @@ dependencies = [ [[package]] name = "toml" -version = "0.9.6" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae2a4cf385da23d1d53bc15cdfa5c2109e93d8d362393c801e87da2f72f0e201" +checksum = "75129e1dc5000bfbaa9fee9d1b21f974f9fbad9daec557a521ee6e080825f6e8" dependencies = [ "indexmap 2.10.0", - "serde_core", - "serde_spanned 1.0.4", - "toml_datetime 0.7.5+spec-1.1.0", + "serde", + "serde_spanned 1.0.0", + "toml_datetime 0.7.0", "toml_parser", "toml_writer", "winnow", @@ -11837,11 +11856,11 @@ dependencies = [ [[package]] name = "toml_datetime" -version = "0.7.5+spec-1.1.0" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "92e1cfed4a3038bc5a127e35a2d360f145e1f4b971b551a2ba5fd7aedf7e1347" +checksum = "bade1c3e902f58d73d3f294cd7f20391c1cb2fbcb643b73566bc773971df91e3" dependencies = [ - "serde_core", + "serde", ] [[package]] @@ -11875,9 +11894,9 @@ checksum = "5d99f8c9a7727884afe522e9bd5edbfc91a3312b36a77b5fb8926e4c31a41801" [[package]] name = "toml_writer" -version = "1.0.6+spec-1.1.0" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab16f14aed21ee8bfd8ec22513f7287cd4a91aa92e44edfe2c17ddd004e92607" +checksum = "df8b2b54733674ad286d16267dcfc7a71ed5c776e4ac7aa3c3e2561f7c637bf2" [[package]] name = "tonic" @@ -12036,7 +12055,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e9cd434a998747dd2c4276bc96ee2e0c7a2eadf3cae88e52be55a05fa9053f5" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "http 1.3.1", "http-body 1.0.1", @@ -12052,7 +12071,7 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "adc82fd73de2a9722ac5da747f12383d2bfdb93591ee6c58486e0097890f05f2" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", "bytes", "futures-util", "http 1.3.1", @@ -12549,11 +12568,11 @@ dependencies = [ [[package]] name = "uuid" -version = "1.20.0" +version = "1.21.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee48d38b119b0cd71fe4141b30f5ba9c7c5d9f4e7a3a8b4a674e4b6ef789976f" +checksum = "b672338555252d43fd2240c714dc444b8c6fb0a5c5335e65a07bba7742735ddb" dependencies = [ - "getrandom 0.3.3", + "getrandom 0.4.1", "js-sys", "serde_core", "wasm-bindgen", @@ -12707,6 +12726,24 @@ dependencies = [ "wit-bindgen-rt", ] +[[package]] +name = "wasip2" +version = "1.0.2+wasi-0.2.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +dependencies = [ + "wit-bindgen", +] + +[[package]] +name = "wasip3" +version = "0.4.0+wasi-0.3.0-rc-2026-01-06" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" +dependencies = [ + "wit-bindgen", +] + [[package]] name = "wasm-bindgen" version = "0.2.100" @@ -12778,6 +12815,28 @@ dependencies = [ "unicode-ident", ] +[[package]] +name = "wasm-encoder" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "990065f2fe63003fe337b932cfb5e3b80e0b4d0f5ff650e6985b1048f62c8319" +dependencies = [ + "leb128fmt", + "wasmparser", +] + +[[package]] +name = "wasm-metadata" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bb0e353e6a2fbdc176932bbaab493762eb1255a7900fe0fea1a2f96c296cc909" +dependencies = [ + "anyhow", + "indexmap 2.10.0", + "wasm-encoder", + "wasmparser", +] + [[package]] name = "wasm-streams" version = "0.4.2" @@ -12791,6 +12850,18 @@ dependencies = [ "web-sys", ] +[[package]] +name = "wasmparser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "47b807c72e1bac69382b3a6fb3dbe8ea4c0ed87ff5629b8685ae6b9a611028fe" +dependencies = [ + "bitflags 2.11.0", + "hashbrown 0.15.4", + "indexmap 2.10.0", + "semver 1.0.26", +] + [[package]] name = "wasmtimer" version = "0.4.2" @@ -12945,7 +13016,7 @@ dependencies = [ "windows-collections", "windows-core 0.61.2", "windows-future", - "windows-link 0.1.3", + "windows-link", "windows-numerics", ] @@ -12991,7 +13062,7 @@ checksum = "c0fdd3ddb90610c7638aa2b3a3ab2904fb9e5cdbecc643ddb3647212781c4ae3" dependencies = [ "windows-implement 0.60.0", "windows-interface 0.59.1", - "windows-link 0.1.3", + "windows-link", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -13003,7 +13074,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc6a41e98427b19fe4b73c550f060b59fa592d7d686537eebf9385621bfbad8e" dependencies = [ "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-link", "windows-threading", ] @@ -13079,12 +13150,6 @@ version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5e6ad25900d524eaabdbbb96d20b4311e1e7ae1699af4fb28c17ae66c80d798a" -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - [[package]] name = "windows-numerics" version = "0.2.0" @@ -13092,7 +13157,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9150af68066c4c5c07ddc0ce30421554771e528bde427614c61038bc2c92c2b1" dependencies = [ "windows-core 0.61.2", - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -13101,7 +13166,7 @@ version = "0.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5b8a9ed28765efc97bbc954883f4e6796c33a06546ebafacbabee9696967499e" dependencies = [ - "windows-link 0.1.3", + "windows-link", "windows-result 0.3.4", "windows-strings 0.4.2", ] @@ -13130,7 +13195,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56f42bd332cc6c8eac5af113fc0c1fd6a8fd2aa08a0119358686e5160d0586c6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -13149,7 +13214,7 @@ version = "0.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "56e6c93f3a0c3b36176cb1327a4958a0353d5d166c2a35cb268ace15e91d3b57" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -13197,15 +13262,6 @@ dependencies = [ "windows-targets 0.53.3", ] -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -dependencies = [ - "windows-link 0.2.1", -] - [[package]] name = "windows-targets" version = "0.42.2" @@ -13258,7 +13314,7 @@ version = "0.53.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d5fe6031c4041849d7c496a8ded650796e7b6ecc19df1a431c1a363342e5dc91" dependencies = [ - "windows-link 0.1.3", + "windows-link", "windows_aarch64_gnullvm 0.53.0", "windows_aarch64_msvc 0.53.0", "windows_i686_gnu 0.53.0", @@ -13275,7 +13331,7 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b66463ad2e0ea3bbf808b7f1d371311c80e115c0b71d60efc142cafbcfb057a6" dependencies = [ - "windows-link 0.1.3", + "windows-link", ] [[package]] @@ -13507,13 +13563,101 @@ dependencies = [ "url", ] +[[package]] +name = "wit-bindgen" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" +dependencies = [ + "wit-bindgen-rust-macro", +] + +[[package]] +name = "wit-bindgen-core" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ea61de684c3ea68cb082b7a88508a8b27fcc8b797d738bfc99a82facf1d752dc" +dependencies = [ + "anyhow", + "heck 0.5.0", + "wit-parser", +] + [[package]] name = "wit-bindgen-rt" version = "0.39.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6f42320e61fe2cfd34354ecb597f86f413484a798ba44a8ca1165c58d42da6c1" dependencies = [ - "bitflags 2.10.0", + "bitflags 2.11.0", +] + +[[package]] +name = "wit-bindgen-rust" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b7c566e0f4b284dd6561c786d9cb0142da491f46a9fbed79ea69cdad5db17f21" +dependencies = [ + "anyhow", + "heck 0.5.0", + "indexmap 2.10.0", + "prettyplease", + "syn 2.0.104", + "wasm-metadata", + "wit-bindgen-core", + "wit-component", +] + +[[package]] +name = "wit-bindgen-rust-macro" +version = "0.51.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0c0f9bfd77e6a48eccf51359e3ae77140a7f50b1e2ebfe62422d8afdaffab17a" +dependencies = [ + "anyhow", + "prettyplease", + "proc-macro2", + "quote", + "syn 2.0.104", + "wit-bindgen-core", + "wit-bindgen-rust", +] + +[[package]] +name = "wit-component" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d66ea20e9553b30172b5e831994e35fbde2d165325bec84fc43dbf6f4eb9cb2" +dependencies = [ + "anyhow", + "bitflags 2.11.0", + "indexmap 2.10.0", + "log", + "serde", + "serde_derive", + "serde_json", + "wasm-encoder", + "wasm-metadata", + "wasmparser", + "wit-parser", +] + +[[package]] +name = "wit-parser" +version = "0.244.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ecc8ac4bc1dc3381b7f59c34f00b67e18f910c2c0f50015669dde7def656a736" +dependencies = [ + "anyhow", + "id-arena", + "indexmap 2.10.0", + "log", + "semver 1.0.26", + "serde", + "serde_derive", + "serde_json", + "unicode-xid", + "wasmparser", ] [[package]] diff --git a/bin/katana/src/cli/rpc/starknet.rs b/bin/katana/src/cli/rpc/starknet.rs index f088c76e2..ef5e3086c 100644 --- a/bin/katana/src/cli/rpc/starknet.rs +++ b/bin/katana/src/cli/rpc/starknet.rs @@ -5,7 +5,7 @@ use clap::{Args, Subcommand}; use katana_primitives::block::{BlockHash, BlockIdOrTag, BlockNumber, ConfirmedBlockIdOrTag}; use katana_primitives::class::ClassHash; use katana_primitives::contract::StorageKey; -use katana_primitives::execution::{EntryPointSelector, FunctionCall}; +use katana_primitives::execution::{Call, EntryPointSelector}; use katana_primitives::transaction::TxHash; use katana_primitives::{ContractAddress, Felt}; use katana_rpc_types::event::{EventFilter, EventFilterWithPage, ResultPageRequest}; @@ -398,8 +398,7 @@ impl StarknetCommands { let entry_point_selector = args.selector; let calldata = args.calldata; - let function_call = - FunctionCall { contract_address, entry_point_selector, calldata }; + let function_call = Call { contract_address, entry_point_selector, calldata }; let block_id = args.block.0; let result = client.call(function_call, block_id).await?; diff --git a/crates/cartridge/Cargo.toml b/crates/cartridge/Cargo.toml index 846a12961..b84daf4c2 100644 --- a/crates/cartridge/Cargo.toml +++ b/crates/cartridge/Cargo.toml @@ -10,10 +10,11 @@ build = "build.rs" katana-contracts.workspace = true katana-genesis.workspace = true katana-primitives.workspace = true -katana-rpc-types = { path = "../rpc/rpc-types" } +katana-rpc-types = { workspace = true } anyhow.workspace = true ark-ff = "0.4.2" +cainome-cairo-serde.workspace = true lazy_static.workspace = true reqwest.workspace = true serde.workspace = true diff --git a/crates/cartridge/src/client.rs b/crates/cartridge/src/api.rs similarity index 98% rename from crates/cartridge/src/client.rs rename to crates/cartridge/src/api.rs index ca1b8b474..231588137 100644 --- a/crates/cartridge/src/client.rs +++ b/crates/cartridge/src/api.rs @@ -17,12 +17,12 @@ pub enum Error { /// Client for interacting with the Cartridge service. #[derive(Debug, Clone)] -pub struct Client { +pub struct CartridgeApiClient { url: Url, client: reqwest::Client, } -impl Client { +impl CartridgeApiClient { /// Creates a new [`CartridgeApiClient`] with the given URL. pub fn new(url: Url) -> Self { Self { url, client: reqwest::Client::new() } diff --git a/crates/cartridge/src/lib.rs b/crates/cartridge/src/lib.rs index 2d2aca8d9..082479c41 100644 --- a/crates/cartridge/src/lib.rs +++ b/crates/cartridge/src/lib.rs @@ -1,15 +1,14 @@ #![cfg_attr(not(test), warn(unused_crate_dependencies))] -pub mod client; +pub mod api; pub mod vrf; -pub use client::Client; -pub use vrf::{ - bootstrap_vrf, get_vrf_account, resolve_executable, wait_for_http_ok, InfoResponse, - RequestContext, SignedOutsideExecution, VrfAccountCredentials, VrfBootstrap, - VrfBootstrapConfig, VrfBootstrapResult, VrfClient, VrfClientError, VrfOutsideExecution, - VrfService, VrfServiceConfig, VrfServiceProcess, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT, - VRF_HARDCODED_SECRET_KEY, VRF_SERVER_PORT, +pub use api::CartridgeApiClient; +pub use vrf::server::{ + bootstrap_vrf, get_vrf_account, resolve_executable, wait_for_http_ok, VrfAccountCredentials, + VrfBootstrap, VrfBootstrapConfig, VrfBootstrapResult, VrfServer, VrfServerConfig, + VrfServiceProcess, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT, VRF_HARDCODED_SECRET_KEY, + VRF_SERVER_PORT, }; #[rustfmt::skip] diff --git a/crates/cartridge/src/vrf/client.rs b/crates/cartridge/src/vrf/client.rs index 1d6fe6d21..0ead83bd6 100644 --- a/crates/cartridge/src/vrf/client.rs +++ b/crates/cartridge/src/vrf/client.rs @@ -1,6 +1,9 @@ -use katana_primitives::Felt; +use cainome_cairo_serde::CairoSerde; +use katana_primitives::execution::Call; +use katana_primitives::{ContractAddress, Felt}; use katana_rpc_types::outside_execution::{OutsideExecutionV2, OutsideExecutionV3}; use serde::{Deserialize, Serialize}; +use starknet::macros::selector; use url::Url; #[derive(Debug, Default, Clone, serde::Serialize, serde::Deserialize)] @@ -25,11 +28,30 @@ pub enum VrfOutsideExecution { /// A signed outside execution request. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SignedOutsideExecution { - pub address: Felt, + pub address: ContractAddress, pub outside_execution: VrfOutsideExecution, pub signature: Vec, } +impl From for Call { + fn from(value: SignedOutsideExecution) -> Self { + let (entry_point_selector, mut calldata) = match &value.outside_execution { + VrfOutsideExecution::V2(v) => { + let calldata = OutsideExecutionV2::cairo_serialize(v); + (selector!("execute_from_outside_v2"), calldata) + } + VrfOutsideExecution::V3(v) => { + let calldata = OutsideExecutionV3::cairo_serialize(v); + (selector!("execute_from_outside_v3"), calldata) + } + }; + + calldata.extend(value.signature); + + Call { contract_address: value.address, entry_point_selector, calldata } + } +} + /// Response from GET /info endpoint. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct InfoResponse { diff --git a/crates/cartridge/src/vrf/mod.rs b/crates/cartridge/src/vrf/mod.rs index e7e2c0f41..0c7848cce 100644 --- a/crates/cartridge/src/vrf/mod.rs +++ b/crates/cartridge/src/vrf/mod.rs @@ -1,181 +1,6 @@ -//! VRF (Verifiable Random Function) support for Cartridge. -//! -//! This module provides: -//! - VRF client for communicating with the VRF server -//! - Bootstrap logic for deploying VRF contracts -//! - Sidecar process management +//! Cartridge VRF (Verifiable Random Function) service. -mod bootstrap; mod client; +pub mod server; -use std::net::{IpAddr, Ipv4Addr, SocketAddr}; -use std::path::{Path, PathBuf}; -use std::process::Stdio; -use std::time::{Duration, Instant}; -use std::{env, io}; - -pub use bootstrap::{ - bootstrap_vrf, get_vrf_account, VrfAccountCredentials, VrfBootstrap, VrfBootstrapConfig, - VrfBootstrapResult, BOOTSTRAP_TIMEOUT, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT, - VRF_HARDCODED_SECRET_KEY, -}; pub use client::*; -use katana_primitives::{ContractAddress, Felt}; -use tokio::process::{Child, Command}; -use tokio::time::sleep; -use tracing::{debug, info, warn}; -use url::Url; - -const LOG_TARGET: &str = "katana::cartridge::vrf::sidecar"; - -pub const VRF_SERVER_PORT: u16 = 3000; -const DEFAULT_VRF_SERVICE_PATH: &str = "vrf-server"; -pub const SIDECAR_TIMEOUT: Duration = Duration::from_secs(10); - -#[derive(Debug, thiserror::Error)] -pub enum Error { - #[error("bootstrap_result not set - call bootstrap() or bootstrap_result()")] - BootstrapResultNotSet, - #[error("sidecar binary not found at {0}")] - BinaryNotFound(PathBuf), - #[error("sidecar binary '{0}' not found in PATH")] - BinaryNotInPath(PathBuf), - #[error("PATH environment variable is not set")] - PathNotSet, - #[error("failed to spawn VRF sidecar")] - Spawn(#[source] io::Error), - #[error("VRF sidecar did not become ready before timeout")] - SidecarTimeout, - #[error("bootstrap failed")] - Bootstrap(#[source] anyhow::Error), -} - -pub type Result = std::result::Result; - -#[derive(Debug, Clone)] -pub struct VrfServiceConfig { - pub vrf_account_address: ContractAddress, - pub vrf_private_key: Felt, - pub secret_key: u64, -} - -#[derive(Debug, Clone)] -pub struct VrfService { - config: VrfServiceConfig, - path: PathBuf, -} - -impl VrfService { - pub fn new(config: VrfServiceConfig) -> Self { - Self { config, path: PathBuf::from(DEFAULT_VRF_SERVICE_PATH) } - } - - /// Sets the path to the vrf service program. - /// - /// If no path is set, the default executable name [`DEFAULT_VRF_SERVICE_PATH`] will be used. - pub fn path>(mut self, path: T) -> Self { - self.path = path.into(); - self - } - - pub async fn start(self) -> Result { - let bin = resolve_executable(&self.path)?; - - let mut command = Command::new(bin); - command - .arg("--port") - .arg(VRF_SERVER_PORT.to_string()) - .arg("--account-address") - .arg(self.config.vrf_account_address.to_hex_string()) - .arg("--account-private-key") - .arg(self.config.vrf_private_key.to_hex_string()) - .arg("--secret-key") - .arg(self.config.secret_key.to_string()) - .stdout(Stdio::inherit()) - .stderr(Stdio::inherit()) - .kill_on_drop(true); - - let process = command.spawn().map_err(Error::Spawn)?; - let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), VRF_SERVER_PORT); - - let url = Url::parse(&format!("http://{addr}")).expect("valid url"); - let client = VrfClient::new(url); - wait_for_http_ok(&client, "vrf info", SIDECAR_TIMEOUT).await?; - - info!(%addr, "VRF service started."); - - Ok(VrfServiceProcess { process, addr, inner: self }) - } -} - -/// A running VRF sidecar process. -#[derive(Debug)] -pub struct VrfServiceProcess { - process: Child, - inner: VrfService, - addr: SocketAddr, -} - -impl VrfServiceProcess { - /// Get the address of the VRF service. - pub fn addr(&self) -> &SocketAddr { - &self.addr - } - - pub fn process(&mut self) -> &mut Child { - &mut self.process - } - - pub fn config(&self) -> &VrfServiceConfig { - &self.inner.config - } - - pub async fn shutdown(&mut self) -> io::Result<()> { - self.process.kill().await - } -} - -/// Resolve an executable path, searching in PATH if necessary. -pub fn resolve_executable(path: &Path) -> Result { - if path.components().count() > 1 { - return if path.is_file() { - Ok(path.to_path_buf()) - } else { - Err(Error::BinaryNotFound(path.to_path_buf())) - }; - } - - let path_var = env::var_os("PATH").ok_or(Error::PathNotSet)?; - for dir in env::split_paths(&path_var) { - let candidate = dir.join(path); - if candidate.is_file() { - return Ok(candidate); - } - } - - Err(Error::BinaryNotInPath(path.to_path_buf())) -} - -/// Wait for the VRF sidecar to become ready by polling its `/info` endpoint. -pub async fn wait_for_http_ok(client: &VrfClient, name: &str, timeout: Duration) -> Result<()> { - let start = Instant::now(); - - loop { - match client.info().await { - Ok(_) => { - info!(target: LOG_TARGET, %name, "sidecar ready"); - return Ok(()); - } - Err(err) => { - debug!(target: LOG_TARGET, %name, error = %err, "waiting for sidecar"); - } - } - - if start.elapsed() > timeout { - warn!(target: LOG_TARGET, %name, "sidecar did not become ready in time"); - return Err(Error::SidecarTimeout); - } - - sleep(Duration::from_millis(200)).await; - } -} diff --git a/crates/cartridge/src/vrf/bootstrap.rs b/crates/cartridge/src/vrf/server/bootstrap.rs similarity index 100% rename from crates/cartridge/src/vrf/bootstrap.rs rename to crates/cartridge/src/vrf/server/bootstrap.rs diff --git a/crates/cartridge/src/vrf/server/mod.rs b/crates/cartridge/src/vrf/server/mod.rs new file mode 100644 index 000000000..02f2dcb58 --- /dev/null +++ b/crates/cartridge/src/vrf/server/mod.rs @@ -0,0 +1,174 @@ +mod bootstrap; + +use std::net::{IpAddr, Ipv4Addr, SocketAddr}; +use std::path::{Path, PathBuf}; +use std::process::Stdio; +use std::time::{Duration, Instant}; +use std::{env, io}; + +pub use bootstrap::{ + bootstrap_vrf, get_vrf_account, VrfAccountCredentials, VrfBootstrap, VrfBootstrapConfig, + VrfBootstrapResult, BOOTSTRAP_TIMEOUT, VRF_ACCOUNT_SALT, VRF_CONSUMER_SALT, + VRF_HARDCODED_SECRET_KEY, +}; +use katana_primitives::{ContractAddress, Felt}; +use tokio::process::{Child, Command}; +use tokio::time::sleep; +use tracing::{debug, info, warn}; +use url::Url; + +use crate::vrf::client::VrfClient; + +const LOG_TARGET: &str = "katana::cartridge::vrf::sidecar"; + +pub const VRF_SERVER_PORT: u16 = 3000; +const DEFAULT_VRF_SERVICE_PATH: &str = "vrf-server"; +pub const SIDECAR_TIMEOUT: Duration = Duration::from_secs(10); + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("bootstrap_result not set - call bootstrap() or bootstrap_result()")] + BootstrapResultNotSet, + #[error("sidecar binary not found at {0}")] + BinaryNotFound(PathBuf), + #[error("sidecar binary '{0}' not found in PATH")] + BinaryNotInPath(PathBuf), + #[error("PATH environment variable is not set")] + PathNotSet, + #[error("failed to spawn VRF sidecar")] + Spawn(#[source] io::Error), + #[error("VRF sidecar did not become ready before timeout")] + SidecarTimeout, + #[error("bootstrap failed")] + Bootstrap(#[source] anyhow::Error), +} + +pub type Result = std::result::Result; + +#[derive(Debug, Clone)] +pub struct VrfServerConfig { + pub vrf_account_address: ContractAddress, + pub vrf_private_key: Felt, + pub secret_key: u64, +} + +#[derive(Debug, Clone)] +pub struct VrfServer { + config: VrfServerConfig, + path: PathBuf, +} + +impl VrfServer { + pub fn new(config: VrfServerConfig) -> Self { + Self { config, path: PathBuf::from(DEFAULT_VRF_SERVICE_PATH) } + } + + /// Sets the path to the vrf service program. + /// + /// If no path is set, the default executable name [`DEFAULT_VRF_SERVICE_PATH`] will be used. + pub fn path>(mut self, path: T) -> Self { + self.path = path.into(); + self + } + + pub async fn start(self) -> Result { + let bin = resolve_executable(&self.path)?; + + let mut command = Command::new(bin); + command + .arg("--port") + .arg(VRF_SERVER_PORT.to_string()) + .arg("--account-address") + .arg(self.config.vrf_account_address.to_hex_string()) + .arg("--account-private-key") + .arg(self.config.vrf_private_key.to_hex_string()) + .arg("--secret-key") + .arg(self.config.secret_key.to_string()) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()) + .kill_on_drop(true); + + let process = command.spawn().map_err(Error::Spawn)?; + let addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::LOCALHOST), VRF_SERVER_PORT); + + let url = Url::parse(&format!("http://{addr}")).expect("valid url"); + let client = VrfClient::new(url); + wait_for_http_ok(&client, "vrf info", SIDECAR_TIMEOUT).await?; + + info!(%addr, "VRF service started."); + + Ok(VrfServiceProcess { process, addr, inner: self }) + } +} + +/// A running VRF sidecar process. +#[derive(Debug)] +pub struct VrfServiceProcess { + process: Child, + inner: VrfServer, + addr: SocketAddr, +} + +impl VrfServiceProcess { + /// Get the address of the VRF service. + pub fn addr(&self) -> &SocketAddr { + &self.addr + } + + pub fn process(&mut self) -> &mut Child { + &mut self.process + } + + pub fn config(&self) -> &VrfServerConfig { + &self.inner.config + } + + pub async fn shutdown(&mut self) -> io::Result<()> { + self.process.kill().await + } +} + +/// Resolve an executable path, searching in PATH if necessary. +pub fn resolve_executable(path: &Path) -> Result { + if path.components().count() > 1 { + return if path.is_file() { + Ok(path.to_path_buf()) + } else { + Err(Error::BinaryNotFound(path.to_path_buf())) + }; + } + + let path_var = env::var_os("PATH").ok_or(Error::PathNotSet)?; + for dir in env::split_paths(&path_var) { + let candidate = dir.join(path); + if candidate.is_file() { + return Ok(candidate); + } + } + + Err(Error::BinaryNotInPath(path.to_path_buf())) +} + +/// Wait for the VRF sidecar to become ready by polling its `/info` endpoint. +pub async fn wait_for_http_ok(client: &VrfClient, name: &str, timeout: Duration) -> Result<()> { + let start = Instant::now(); + + loop { + match client.info().await { + Ok(_) => { + info!(target: LOG_TARGET, %name, "sidecar ready"); + return Ok(()); + } + Err(err) => { + debug!(target: LOG_TARGET, %name, error = %err, "waiting for sidecar"); + } + } + + if start.elapsed() > timeout { + warn!(target: LOG_TARGET, %name, "sidecar did not become ready in time"); + return Err(Error::SidecarTimeout); + } + + sleep(Duration::from_millis(200)).await; + } +} diff --git a/crates/cli/src/args.rs b/crates/cli/src/args.rs index 318aedf74..b02c69a37 100644 --- a/crates/cli/src/args.rs +++ b/crates/cli/src/args.rs @@ -1034,7 +1034,7 @@ explorer = true #[test] #[cfg(feature = "server")] fn parse_cors_origins() { - use katana_rpc_server::cors::HeaderValue; + use katana_rpc_server::middleware::cors::HeaderValue; let result = SequencerNodeArgs::parse_from([ "katana", diff --git a/crates/cli/src/options.rs b/crates/cli/src/options.rs index 65a74f604..47d5b4892 100644 --- a/crates/cli/src/options.rs +++ b/crates/cli/src/options.rs @@ -19,7 +19,7 @@ use katana_primitives::chain::ChainId; #[cfg(feature = "vrf")] use katana_primitives::ContractAddress; #[cfg(feature = "server")] -use katana_rpc_server::cors::HeaderValue; +use katana_rpc_server::middleware::cors::HeaderValue; use katana_sequencer_node::config::execution::{ DEFAULT_INVOCATION_MAX_STEPS, DEFAULT_VALIDATION_MAX_STEPS, }; diff --git a/crates/cli/src/sidecar.rs b/crates/cli/src/sidecar.rs index 1a3b661c3..310319a1b 100644 --- a/crates/cli/src/sidecar.rs +++ b/crates/cli/src/sidecar.rs @@ -2,8 +2,8 @@ use std::net::SocketAddr; use anyhow::{anyhow, Result}; #[cfg(feature = "vrf")] -pub use cartridge::vrf::{ - get_vrf_account, VrfAccountCredentials, VrfBootstrapResult, VrfService, VrfServiceConfig, +pub use cartridge::vrf::server::{ + get_vrf_account, VrfAccountCredentials, VrfBootstrapResult, VrfServer, VrfServerConfig, VrfServiceProcess, VRF_SERVER_PORT, }; use katana_chain_spec::ChainSpec; @@ -58,13 +58,13 @@ pub async fn bootstrap_vrf( options: &VrfOptions, rpc_addr: SocketAddr, chain: &ChainSpec, -) -> Result { +) -> Result { let rpc_url = local_rpc_url(&rpc_addr); let (account_address, pk) = prefunded_account(chain, 0)?; - let result = cartridge::vrf::bootstrap_vrf(rpc_url, account_address, pk).await?; + let result = cartridge::vrf::server::bootstrap_vrf(rpc_url, account_address, pk).await?; - let mut vrf_service = VrfService::new(VrfServiceConfig { + let mut vrf_service = VrfServer::new(VrfServerConfig { secret_key: result.secret_key, vrf_account_address: result.vrf_account_address, vrf_private_key: result.vrf_account_private_key, diff --git a/crates/cli/src/utils.rs b/crates/cli/src/utils.rs index b51db2000..5266f2951 100644 --- a/crates/cli/src/utils.rs +++ b/crates/cli/src/utils.rs @@ -15,7 +15,7 @@ use katana_primitives::cairo::ShortString; use katana_primitives::chain::ChainId; use katana_primitives::class::ClassHash; use katana_primitives::contract::ContractAddress; -use katana_rpc_server::cors::HeaderValue; +use katana_rpc_server::middleware::cors::HeaderValue; use katana_tracing::LogFormat; use serde::{Deserialize, Deserializer, Serializer}; use tracing::info; diff --git a/crates/core/src/service/mod.rs b/crates/core/src/service/mod.rs index 06cd8255d..b741627db 100644 --- a/crates/core/src/service/mod.rs +++ b/crates/core/src/service/mod.rs @@ -4,7 +4,8 @@ use std::task::{Context, Poll}; use block_producer::BlockProductionError; use futures::stream::StreamExt; -use katana_pool::{PendingTransactions, PoolOrd, TransactionPool, TxPool}; +use katana_pool::api::{PendingTransactions, PoolOrd, TransactionPool}; +use katana_pool::TxPool; use katana_primitives::transaction::ExecutableTxWithHash; use katana_provider::{ProviderFactory, ProviderRO, ProviderRW}; use tracing::{error, info}; diff --git a/crates/executor/src/blockifier/call.rs b/crates/executor/src/blockifier/call.rs index a63e93c96..be5ff8b9b 100644 --- a/crates/executor/src/blockifier/call.rs +++ b/crates/executor/src/blockifier/call.rs @@ -17,7 +17,7 @@ use blockifier::state::cached_state::CachedState; use blockifier::state::state_api::StateReader; use blockifier::transaction::objects::{DeprecatedTransactionInfo, TransactionInfo}; use cairo_vm::vm::runners::cairo_runner::RunResources; -use katana_primitives::execution::{FunctionCall, TrackedResource}; +use katana_primitives::execution::{Call, TrackedResource}; use katana_primitives::Felt; use starknet_api::core::EntryPointSelector; use starknet_api::execution_resources::GasAmount; @@ -30,7 +30,7 @@ use crate::error::ExecutionError; /// /// The `max_gas` is the maximum amount of Sierra gas to allocate for the call. pub fn execute_call( - request: FunctionCall, + request: Call, state: &mut CachedState, block_context: Arc, max_gas: u64, @@ -40,7 +40,7 @@ pub fn execute_call( } fn execute_call_inner( - request: FunctionCall, + request: Call, state: &mut CachedState, block_context: Arc, max_sierra_gas: u64, @@ -153,7 +153,7 @@ mod tests { use blockifier::execution::errors::EntryPointExecutionError; use blockifier::state::cached_state::{self}; use katana_primitives::class::ContractClass; - use katana_primitives::execution::FunctionCall; + use katana_primitives::execution::Call; use katana_primitives::{address, felt}; use katana_provider::api::contract::ContractClassWriter; use katana_provider::api::state::{StateFactoryProvider, StateWriter}; @@ -193,7 +193,7 @@ mod tests { let mut state = cached_state::CachedState::new(state); let ctx = Arc::new(BlockContext::create_for_testing()); - let mut req = FunctionCall { + let mut req = Call { calldata: Vec::new(), contract_address: address, entry_point_selector: selector!("bounded_call"), @@ -279,7 +279,7 @@ mod tests { let mut state = cached_state::CachedState::new(state); let ctx = Arc::new(BlockContext::create_for_testing()); - let req = FunctionCall { + let req = Call { calldata: Vec::new(), contract_address: address, entry_point_selector: selector!("call_with_panic"), diff --git a/crates/gateway/gateway-server/src/lib.rs b/crates/gateway/gateway-server/src/lib.rs index 1bd071e3c..7120616c2 100644 --- a/crates/gateway/gateway-server/src/lib.rs +++ b/crates/gateway/gateway-server/src/lib.rs @@ -7,7 +7,7 @@ use axum::Router; use katana_core::service::block_producer::BlockProducer; use katana_pool_api::TransactionPool; use katana_provider::{ProviderFactory, ProviderRO, ProviderRW}; -use katana_rpc_server::cors::Cors; +use katana_rpc_server::middleware::cors::Cors; use katana_rpc_server::starknet::StarknetApi; use tokio::net::TcpListener; use tokio::sync::watch; diff --git a/crates/grpc/src/handlers/starknet.rs b/crates/grpc/src/handlers/starknet.rs index d3bb7b96c..5e43f26c1 100644 --- a/crates/grpc/src/handlers/starknet.rs +++ b/crates/grpc/src/handlers/starknet.rs @@ -1,6 +1,6 @@ //! Starknet service handler implementation. -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::transaction::TxHash; use katana_primitives::Felt; use katana_provider::{ProviderFactory, ProviderRO}; diff --git a/crates/messaging/src/service.rs b/crates/messaging/src/service.rs index 101992d04..f7e411797 100644 --- a/crates/messaging/src/service.rs +++ b/crates/messaging/src/service.rs @@ -5,7 +5,8 @@ use std::time::Duration; use futures::{Future, FutureExt, Stream}; use katana_chain_spec::ChainSpec; -use katana_pool::{TransactionPool, TxPool}; +use katana_pool::api::TransactionPool; +use katana_pool::TxPool; use katana_primitives::chain::ChainId; use katana_primitives::transaction::{ExecutableTxWithHash, L1HandlerTx, TxHash}; use tokio::time::{interval_at, Instant, Interval}; diff --git a/crates/node/full/src/lib.rs b/crates/node/full/src/lib.rs index 8ba193dbd..47887a097 100644 --- a/crates/node/full/src/lib.rs +++ b/crates/node/full/src/lib.rs @@ -26,7 +26,7 @@ use katana_pool::ordering::TipOrdering; use katana_provider::DbProviderFactory; use katana_rpc_api::katana::KatanaApiServer; use katana_rpc_api::starknet::{StarknetApiServer, StarknetTraceApiServer, StarknetWriteApiServer}; -use katana_rpc_server::cors::Cors; +use katana_rpc_server::middleware::cors::Cors; use katana_rpc_server::starknet::{StarknetApi, StarknetApiConfig}; use katana_rpc_server::{RpcServer, RpcServerHandle}; use katana_stage::blocks::BatchBlockDownloader; @@ -171,8 +171,6 @@ impl Node { max_concurrent_estimate_fee_requests: config.rpc.max_concurrent_estimate_fee_requests, simulation_flags: ExecutionFlags::default(), versioned_constant_overrides: None, - #[cfg(feature = "cartridge")] - paymaster: None, }; let chain_spec = match config.network { diff --git a/crates/node/sequencer/Cargo.toml b/crates/node/sequencer/Cargo.toml index b5de59e31..3830783ae 100644 --- a/crates/node/sequencer/Cargo.toml +++ b/crates/node/sequencer/Cargo.toml @@ -6,6 +6,7 @@ repository.workspace = true version.workspace = true [dependencies] +cartridge = { workspace = true, optional = true } katana-node-config.workspace = true katana-chain-spec.workspace = true katana-core.workspace = true @@ -33,6 +34,8 @@ futures.workspace = true http.workspace = true jsonrpsee.workspace = true serde.workspace = true +starknet = { workspace = true, optional = true } +tower.workspace = true tracing.workspace = true url.workspace = true @@ -46,6 +49,8 @@ vrf = [ "cartridge", ] cartridge = [ + "dep:cartridge", + "dep:starknet", "katana-node-config/cartridge", "katana-rpc-api/cartridge", "katana-rpc-server/cartridge", diff --git a/crates/node/sequencer/src/exit.rs b/crates/node/sequencer/src/exit.rs index 15eb9436d..784e26f13 100644 --- a/crates/node/sequencer/src/exit.rs +++ b/crates/node/sequencer/src/exit.rs @@ -18,7 +18,7 @@ pub struct NodeStoppedFuture<'a> { impl<'a> NodeStoppedFuture<'a> { pub(crate) fn new

(handle: &'a LaunchedNode

) -> Self where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { diff --git a/crates/node/sequencer/src/lib.rs b/crates/node/sequencer/src/lib.rs index 0f4743fb3..d49f152a9 100755 --- a/crates/node/sequencer/src/lib.rs +++ b/crates/node/sequencer/src/lib.rs @@ -11,6 +11,8 @@ use config::rpc::RpcModuleKind; use config::Config; use http::header::CONTENT_TYPE; use http::Method; +#[cfg(feature = "cartridge")] +use jsonrpsee::core::middleware::layer::Either; use jsonrpsee::RpcModule; use katana_chain_spec::{ChainSpec, SettlementLayer}; use katana_core::backend::Backend; @@ -48,24 +50,45 @@ use katana_rpc_api::tee::TeeApiServer; use katana_rpc_client::starknet::Client as StarknetClient; #[cfg(feature = "cartridge")] use katana_rpc_server::cartridge::{CartridgeApi, CartridgeConfig}; -use katana_rpc_server::cors::Cors; use katana_rpc_server::dev::DevApi; +#[cfg(feature = "cartridge")] +use katana_rpc_server::middleware::cartridge::ControllerDeploymentLayer; +use katana_rpc_server::middleware::cors::Cors; +use katana_rpc_server::middleware::logger::RpcLoggerLayer; +use katana_rpc_server::middleware::metrics::RpcServerMetricsLayer; #[cfg(feature = "paymaster")] use katana_rpc_server::paymaster::PaymasterProxy; -#[cfg(feature = "cartridge")] -use katana_rpc_server::starknet::CartridgePaymasterConfig; use katana_rpc_server::starknet::{StarknetApi, StarknetApiConfig}; #[cfg(feature = "tee")] use katana_rpc_server::tee::TeeApi; -use katana_rpc_server::{RpcServer, RpcServerHandle}; +use katana_rpc_server::{RpcServer, RpcServerHandle, RpcServiceBuilder}; use katana_rpc_types::GetBlockWithTxHashesResponse; use katana_stage::Sequencing; use katana_tasks::TaskManager; use num_traits::ToPrimitive; +#[cfg(feature = "cartridge")] +use starknet::signers::SigningKey; +use tower::layer::util::{Identity, Stack}; use tracing::info; use crate::exit::NodeStoppedFuture; +/// The concrete type of the RPC middleware stack used by the node. +#[cfg(feature = "cartridge")] +type NodeRpcMiddleware = Stack< + Either, PF>, Identity>, + Stack>, +>; + +#[cfg(not(feature = "cartridge"))] +type NodeRpcMiddleware = Stack>; + +#[cfg(feature = "cartridge")] +pub type NodeRpcServer = RpcServer>; + +#[cfg(not(feature = "cartridge"))] +pub type NodeRpcServer = RpcServer; + /// A node instance. /// /// The struct contains the handle to all the components of the node. @@ -73,7 +96,7 @@ use crate::exit::NodeStoppedFuture; #[derive(Debug)] pub struct Node

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { @@ -81,7 +104,10 @@ where provider: P, config: Arc, pool: TxPool, - rpc_server: RpcServer, + #[cfg(feature = "cartridge")] + rpc_server: NodeRpcServer

, + #[cfg(not(feature = "cartridge"))] + rpc_server: NodeRpcServer, #[cfg(feature = "grpc")] grpc_server: Option, task_manager: TaskManager, @@ -212,11 +238,15 @@ where let mut rpc_modules = RpcModule::new(()); - let cors = Cors::new() - .allow_origins(config.rpc.cors_origins.clone()) // Allow `POST` when accessing the resource - .allow_methods([Method::POST, Method::GET]) - .allow_headers([CONTENT_TYPE, "argent-client".parse().unwrap(), "argent-version".parse().unwrap()]); + let cors = Cors::new() + .allow_origins(config.rpc.cors_origins.clone()) + .allow_methods([Method::POST, Method::GET]) + .allow_headers([ + CONTENT_TYPE, + "argent-client".parse().unwrap(), + "argent-version".parse().unwrap(), + ]); #[cfg(feature = "paymaster")] if let Some(cfg) = &config.paymaster { @@ -224,63 +254,6 @@ where rpc_modules.merge(proxy.into_rpc())?; }; - #[cfg(feature = "cartridge")] - let cartridge_paymaster = if let Some(cfg) = &config.paymaster { - if let Some(cartridge_api_cfg) = &cfg.cartridge_api { - anyhow::ensure!( - config.rpc.apis.contains(&RpcModuleKind::Cartridge), - "Cartridge API should be enabled when paymaster is set" - ); - - #[cfg(feature = "vrf")] - let vrf = if let Some(vrf) = &cartridge_api_cfg.vrf { - use url::Url; - - let rpc_url = Url::parse(&format!("http://{}", config.rpc.socket_addr())) - .expect("valid rpc url"); - - Some(katana_rpc_server::cartridge::VrfServiceConfig { - rpc_url, - service_url: vrf.url.clone(), - vrf_contract: vrf.vrf_account, - }) - } else { - None - }; - - let cartridge_api_config = CartridgeConfig { - paymaster_url: cfg.url.clone(), - paymaster_api_key: cfg.api_key.clone(), - api_url: cartridge_api_cfg.cartridge_api_url.clone(), - controller_deployer_address: cartridge_api_cfg.controller_deployer_address, - controller_deployer_private_key: cartridge_api_cfg - .controller_deployer_private_key, - #[cfg(feature = "vrf")] - vrf, - }; - - let cartrige_api = CartridgeApi::new( - backend.clone(), - block_producer.clone(), - pool.clone(), - task_spawner.clone(), - cartridge_api_config, - )?; - - rpc_modules.merge(CartridgeApiServer::into_rpc(cartrige_api))?; - - Some(CartridgePaymasterConfig { - cartridge_api_url: cartridge_api_cfg.cartridge_api_url.clone(), - paymaster_address: cartridge_api_cfg.controller_deployer_address, - paymaster_private_key: cartridge_api_cfg.controller_deployer_private_key, - }) - } else { - None - } - } else { - None - }; - // --- build starknet api let starknet_api_cfg = StarknetApiConfig { @@ -290,8 +263,6 @@ where max_concurrent_estimate_fee_requests: config.rpc.max_concurrent_estimate_fee_requests, simulation_flags: execution_flags, versioned_constant_overrides, - #[cfg(feature = "cartridge")] - paymaster: cartridge_paymaster, }; let chain_spec = backend.chain_spec.clone(); @@ -331,6 +302,71 @@ where rpc_modules.merge(katana_rpc_api::txpool::TxPoolApiServer::into_rpc(api))?; } + // --- build cartridge api (plus middleware) + + #[cfg(feature = "cartridge")] + let controller_deployment_layer = if let Some(cfg) = &config.paymaster { + if let Some(cartridge_api_cfg) = &cfg.cartridge_api { + use anyhow::ensure; + use katana_rpc_server::middleware::cartridge::ControllerDeploymentLayer; + + ensure!( + config.rpc.apis.contains(&RpcModuleKind::Cartridge), + "Cartridge API should be enabled when paymaster is set" + ); + + #[cfg(feature = "vrf")] + let vrf = if let Some(vrf) = &cartridge_api_cfg.vrf { + use url::Url; + + let rpc_url = Url::parse(&format!("http://{}", config.rpc.socket_addr())) + .expect("valid rpc url"); + + Some(katana_rpc_server::cartridge::VrfServiceConfig { + rpc_url, + service_url: vrf.url.clone(), + vrf_contract: vrf.vrf_account, + }) + } else { + None + }; + + let cartridge_api_client = + cartridge::CartridgeApiClient::new(cartridge_api_cfg.cartridge_api_url.clone()); + + let cartridge_api_config = CartridgeConfig { + paymaster_url: cfg.url.clone(), + paymaster_api_key: cfg.api_key.clone(), + api_url: cartridge_api_cfg.cartridge_api_url.clone(), + #[cfg(feature = "vrf")] + vrf: vrf.clone(), + }; + + let cartrige_api = CartridgeApi::new( + backend.clone(), + block_producer.clone(), + pool.clone(), + task_spawner.clone(), + cartridge_api_config, + )?; + + rpc_modules.merge(CartridgeApiServer::into_rpc(cartrige_api))?; + + Some(ControllerDeploymentLayer::new( + starknet_api.clone(), + cartridge_api_client, + cartridge_api_cfg.controller_deployer_address, + SigningKey::from_secret_scalar( + cartridge_api_cfg.controller_deployer_private_key, + ), + )) + } else { + None + } + } else { + None + }; + // --- build tee api (if configured) #[cfg(feature = "tee")] if config.rpc.apis.contains(&RpcModuleKind::Tee) { @@ -362,9 +398,22 @@ where } } + // --- build rpc middleware + + let rpc_middleware = RpcServiceBuilder::new() + .layer(RpcServerMetricsLayer::new(&rpc_modules)) + .layer(RpcLoggerLayer::new()); + + #[cfg(feature = "cartridge")] + let rpc_middleware = rpc_middleware.option_layer(controller_deployment_layer); + #[allow(unused_mut)] - let mut rpc_server = - RpcServer::new().metrics(true).health_check(true).cors(cors).module(rpc_modules)?; + let mut rpc_server = RpcServer::new() + .rpc_middleware(rpc_middleware) + .metrics(true) + .health_check(true) + .cors(cors) + .module(rpc_modules)?; #[cfg(feature = "explorer")] { @@ -574,7 +623,7 @@ impl Node { impl

Node

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { @@ -685,7 +734,14 @@ where } /// Returns a reference to the node's JSON-RPC server. - pub fn rpc(&self) -> &RpcServer { + #[cfg(feature = "cartridge")] + pub fn rpc(&self) -> &NodeRpcServer

{ + &self.rpc_server + } + + /// Returns a reference to the node's JSON-RPC server. + #[cfg(not(feature = "cartridge"))] + pub fn rpc(&self) -> &NodeRpcServer { &self.rpc_server } @@ -709,7 +765,7 @@ where #[derive(Debug)] pub struct LaunchedNode

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { @@ -727,7 +783,7 @@ where impl

LaunchedNode

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { diff --git a/crates/pool/pool-api/src/lib.rs b/crates/pool/pool-api/src/lib.rs index 9ce9b9e44..b881332de 100644 --- a/crates/pool/pool-api/src/lib.rs +++ b/crates/pool/pool-api/src/lib.rs @@ -24,7 +24,7 @@ pub enum PoolError { #[error("Invalid transaction: {0}")] InvalidTransaction(Box), #[error("Internal error: {0}")] - Internal(Box), + Internal(Box), } pub type PoolResult = Result; diff --git a/crates/pool/pool-api/src/validation.rs b/crates/pool/pool-api/src/validation.rs index b9a0ccbef..cbb616e2e 100644 --- a/crates/pool/pool-api/src/validation.rs +++ b/crates/pool/pool-api/src/validation.rs @@ -153,11 +153,11 @@ pub struct Error { /// The hash of the transaction that failed validation. pub hash: TxHash, /// The actual error object. - pub error: Box, + pub error: Box, } impl Error { - pub fn new(hash: TxHash, error: Box) -> Self { + pub fn new(hash: TxHash, error: Box) -> Self { Self { hash, error } } } diff --git a/crates/pool/pool/src/lib.rs b/crates/pool/pool/src/lib.rs index 79537ae5a..5c7cf947d 100644 --- a/crates/pool/pool/src/lib.rs +++ b/crates/pool/pool/src/lib.rs @@ -4,7 +4,6 @@ pub mod ordering; pub mod pool; pub mod validation; -pub use katana_pool_api::{PendingTransactions, PoolOrd, PoolTransaction, TransactionPool}; use katana_primitives::transaction::ExecutableTxWithHash; use ordering::FiFo; use pool::Pool; @@ -13,6 +12,10 @@ use validation::stateful::TxValidator; /// Katana default transacstion pool type. pub type TxPool = Pool>; +pub mod api { + pub use katana_pool_api::*; +} + #[cfg(test)] mod tests { diff --git a/crates/pool/pool/src/validation/stateful.rs b/crates/pool/pool/src/validation/stateful.rs index 958a0f11a..cf6a3ba6f 100644 --- a/crates/pool/pool/src/validation/stateful.rs +++ b/crates/pool/pool/src/validation/stateful.rs @@ -213,7 +213,7 @@ fn validate( fn map_invalid_tx_err( err: StatefulValidatorError, -) -> Result> { +) -> Result> { match err { StatefulValidatorError::StateError(err) => Err(Box::new(err)), StatefulValidatorError::TransactionExecutorError(err) => map_executor_err(err), @@ -224,7 +224,7 @@ fn map_invalid_tx_err( fn map_fee_err( err: TransactionFeeError, -) -> Result> { +) -> Result> { match err { TransactionFeeError::GasBoundsExceedBalance { resource, @@ -281,7 +281,7 @@ fn map_fee_err( fn map_executor_err( err: TransactionExecutorError, -) -> Result> { +) -> Result> { match err { TransactionExecutorError::TransactionExecutionError(e) => match e { TransactionExecutionError::TransactionFeeError(e) => map_fee_err(*e), @@ -298,7 +298,7 @@ fn map_executor_err( fn map_execution_err( err: TransactionExecutionError, -) -> Result> { +) -> Result> { match err { e @ TransactionExecutionError::ValidateTransactionError { storage_address, @@ -326,7 +326,7 @@ fn map_execution_err( fn map_pre_validation_err( err: TransactionPreValidationError, -) -> Result> { +) -> Result> { match err { TransactionPreValidationError::TransactionFeeError(err) => map_fee_err(*err), TransactionPreValidationError::StateError(err) => Err(Box::new(err)), diff --git a/crates/primitives/Cargo.toml b/crates/primitives/Cargo.toml index e28d32eaa..5a9b3113a 100644 --- a/crates/primitives/Cargo.toml +++ b/crates/primitives/Cargo.toml @@ -18,6 +18,7 @@ anyhow.workspace = true arbitrary = { workspace = true, optional = true } blockifier = { workspace = true, features = [ "testing" ] } # some Clone derives are gated behind 'testing' feature cainome-cairo-serde.workspace = true +cainome.workspace = true derive_more.workspace = true lazy_static.workspace = true num-traits.workspace = true diff --git a/crates/primitives/src/execution.rs b/crates/primitives/src/execution.rs index 6776dab3a..fc452a700 100644 --- a/crates/primitives/src/execution.rs +++ b/crates/primitives/src/execution.rs @@ -12,6 +12,7 @@ pub use blockifier::fee::resources::{ ComputationResources, StarknetResources, TransactionResources, }; pub use blockifier::transaction::objects::{RevertError, TransactionExecutionInfo}; +use cainome::cairo_serde_derive; pub use cairo_vm::types::builtin_name::BuiltinName; pub use cairo_vm::vm::runners::cairo_runner::ExecutionResources as VmResources; pub use starknet_api::contract_class::EntryPointType; @@ -25,13 +26,13 @@ use crate::{ContractAddress, Felt}; /// The selector of a contract entry point (ie function selector). pub type EntryPointSelector = Felt; -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, cairo_serde_derive::CairoSerde)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] -pub struct FunctionCall { - /// The contract function selector. - pub entry_point_selector: EntryPointSelector, +pub struct Call { /// The address of the contract whose function you're calling. pub contract_address: ContractAddress, + /// The contract function selector. + pub entry_point_selector: EntryPointSelector, /// The input to the function. pub calldata: Vec, } diff --git a/crates/rpc/rpc-api/src/error/cartridge.rs b/crates/rpc/rpc-api/src/error/cartridge.rs new file mode 100644 index 000000000..4641e46ad --- /dev/null +++ b/crates/rpc/rpc-api/src/error/cartridge.rs @@ -0,0 +1,87 @@ +use jsonrpsee::types::ErrorObjectOwned; +use katana_pool_api::PoolError; +use katana_provider_api::ProviderError; + +/// Error codes for Cartridge API (starting at 200 to avoid conflicts). +const CONTROLLER_DEPLOYMENT_FAILED: i32 = 200; +const VRF_MISSING_FOLLOW_UP_CALL: i32 = 201; +const VRF_INVALID_TARGET: i32 = 202; +const VRF_EXECUTION_FAILED: i32 = 203; +const PAYMASTER_EXECUTION_FAILED: i32 = 204; +const POOL_ERROR: i32 = 205; +const PROVIDER_ERROR: i32 = 206; +const INTERNAL_ERROR: i32 = 299; + +#[derive(Debug, thiserror::Error, Clone)] +pub enum CartridgeApiError { + /// Failed to deploy a Cartridge controller account. + #[error("Controller deployment failed: {reason}")] + ControllerDeployment { reason: String }, + + /// The `request_random` call is not followed by another call in the outside execution. + #[error("request_random call must be followed by another call")] + VrfMissingFollowUpCall, + + /// The `request_random` call does not target the expected VRF account. + #[error("request_random call must target the VRF account")] + VrfInvalidTarget, + + /// The VRF outside execution request failed. + /// + /// Error returns by the VRF server. + #[error("VRF execution failed: {reason}")] + VrfExecutionFailed { reason: String }, + + /// The paymaster failed to execute the transaction. + /// + /// Error returns by the Paymaster server. + #[error("Paymaster execution failed: {reason}")] + PaymasterExecutionFailed { reason: String }, + + /// Failed to submit transaction to the pool. + #[error("Transaction pool error: {reason}")] + PoolError { reason: String }, + + /// Storage provider error. + #[error("Provider error: {reason}")] + ProviderError { reason: String }, + + /// Internal error (e.g., task execution failure). + #[error("Internal error: {reason}")] + InternalError { reason: String }, +} + +impl From for ErrorObjectOwned { + fn from(err: CartridgeApiError) -> Self { + let code = match &err { + CartridgeApiError::ControllerDeployment { .. } => CONTROLLER_DEPLOYMENT_FAILED, + CartridgeApiError::VrfMissingFollowUpCall => VRF_MISSING_FOLLOW_UP_CALL, + CartridgeApiError::VrfInvalidTarget => VRF_INVALID_TARGET, + CartridgeApiError::VrfExecutionFailed { .. } => VRF_EXECUTION_FAILED, + CartridgeApiError::PaymasterExecutionFailed { .. } => PAYMASTER_EXECUTION_FAILED, + CartridgeApiError::PoolError { .. } => POOL_ERROR, + CartridgeApiError::ProviderError { .. } => PROVIDER_ERROR, + CartridgeApiError::InternalError { .. } => INTERNAL_ERROR, + }; + + ErrorObjectOwned::owned(code, err.to_string(), None::<()>) + } +} + +impl From for CartridgeApiError { + fn from(value: ProviderError) -> Self { + CartridgeApiError::ProviderError { reason: value.to_string() } + } +} + +impl From for CartridgeApiError { + fn from(value: anyhow::Error) -> Self { + CartridgeApiError::ControllerDeployment { reason: value.to_string() } + } +} + +impl From for CartridgeApiError { + fn from(error: PoolError) -> Self { + CartridgeApiError::PoolError { reason: error.to_string() } + } +} diff --git a/crates/rpc/rpc-api/src/error/mod.rs b/crates/rpc/rpc-api/src/error/mod.rs index 2979fa8e6..0ba5da5c7 100644 --- a/crates/rpc/rpc-api/src/error/mod.rs +++ b/crates/rpc/rpc-api/src/error/mod.rs @@ -1,3 +1,4 @@ +pub mod cartridge; pub mod dev; pub mod katana; pub mod starknet; diff --git a/crates/rpc/rpc-server/Cargo.toml b/crates/rpc/rpc-server/Cargo.toml index ee39b16e7..e5fbe78e7 100644 --- a/crates/rpc/rpc-server/Cargo.toml +++ b/crates/rpc/rpc-server/Cargo.toml @@ -36,6 +36,7 @@ tokio.workspace = true tower.workspace = true tower-http = { workspace = true, features = [ "cors", "trace" ] } tracing.workspace = true +serde.workspace = true cainome = { workspace = true, optional = true } cartridge = { workspace = true, optional = true } @@ -64,6 +65,7 @@ alloy-provider = { workspace = true, default-features = false, features = [ "anv alloy-sol-types.workspace = true assert_matches.workspace = true +axum.workspace = true cainome.workspace = true cairo-lang-starknet-classes.workspace = true indexmap.workspace = true @@ -71,7 +73,6 @@ jsonrpsee = { workspace = true, features = [ "client" ] } num-traits.workspace = true rand.workspace = true rstest.workspace = true -serde.workspace = true serde_json.workspace = true similar-asserts.workspace = true starknet.workspace = true diff --git a/crates/rpc/rpc-server/src/cartridge/mod.rs b/crates/rpc/rpc-server/src/cartridge/mod.rs index d0a6bc9ad..1d4548cb7 100644 --- a/crates/rpc/rpc-server/src/cartridge/mod.rs +++ b/crates/rpc/rpc-server/src/cartridge/mod.rs @@ -36,24 +36,23 @@ use http::{HeaderMap, HeaderName, HeaderValue}; use jsonrpsee::core::{async_trait, RpcResult}; use jsonrpsee::http_client::{HttpClient, HttpClientBuilder}; use katana_core::backend::Backend; -use katana_core::service::block_producer::{BlockProducer, BlockProducerMode}; +use katana_core::service::block_producer::BlockProducer; use katana_genesis::constant::{DEFAULT_STRK_FEE_TOKEN_ADDRESS, DEFAULT_UDC_ADDRESS}; -use katana_pool::{TransactionPool, TxPool}; +use katana_pool::TxPool; use katana_primitives::chain::ChainId; use katana_primitives::contract::Nonce; +use katana_primitives::execution::Call; use katana_primitives::fee::{AllResourceBoundsMapping, ResourceBoundsMapping}; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, InvokeTx, InvokeTxV3}; use katana_primitives::{ContractAddress, Felt}; -use katana_provider::api::state::{StateFactoryProvider, StateProvider}; +use katana_provider::api::state::StateProvider; use katana_provider::{ProviderFactory, ProviderRO, ProviderRW}; use katana_rpc_api::cartridge::CartridgeApiServer; -use katana_rpc_api::error::starknet::StarknetApiError; +use katana_rpc_api::error::cartridge::CartridgeApiError; use katana_rpc_api::paymaster::PaymasterApiClient; use katana_rpc_types::broadcasted::AddInvokeTransactionResponse; use katana_rpc_types::cartridge::FeeSource; -use katana_rpc_types::outside_execution::{ - OutsideExecution, OutsideExecutionV2, OutsideExecutionV3, -}; +use katana_rpc_types::outside_execution::OutsideExecution; use katana_rpc_types::FunctionCall; use katana_tasks::{Result as TaskResult, TaskSpawner}; use paymaster_rpc::{ @@ -62,20 +61,18 @@ use paymaster_rpc::{ }; use starknet::macros::selector; use starknet::signers::{LocalWallet, Signer, SigningKey}; -use starknet_paymaster::core::types::Call as PaymasterCall; +use starknet_paymaster::core::types::Call as StarknetRsCall; use tracing::{debug, info}; use url::Url; #[cfg(feature = "vrf")] -pub use vrf::VrfServiceConfig; -use vrf::{outside_execution_calls_len, request_random_call, VrfService}; +use vrf::get_request_random_call; +pub use vrf::{VrfService, VrfServiceConfig}; #[derive(Debug, Clone)] pub struct CartridgeConfig { pub api_url: Url, pub paymaster_url: Url, pub paymaster_api_key: Option, - pub controller_deployer_address: ContractAddress, - pub controller_deployer_private_key: Felt, #[cfg(feature = "vrf")] pub vrf: Option, } @@ -86,12 +83,8 @@ pub struct CartridgeApi { backend: Arc>, block_producer: BlockProducer, pool: TxPool, - api_client: cartridge::Client, + api_client: cartridge::CartridgeApiClient, paymaster_client: HttpClient, - /// The paymaster account address used for controller deployment. - controller_deployer_address: ContractAddress, - /// The paymaster account private key. - controller_deployer_private_key: Felt, #[cfg(feature = "vrf")] vrf_service: Option, } @@ -108,8 +101,6 @@ where pool: self.pool.clone(), api_client: self.api_client.clone(), paymaster_client: self.paymaster_client.clone(), - controller_deployer_address: self.controller_deployer_address, - controller_deployer_private_key: self.controller_deployer_private_key, #[cfg(feature = "vrf")] vrf_service: self.vrf_service.clone(), } @@ -129,7 +120,7 @@ where task_spawner: TaskSpawner, config: CartridgeConfig, ) -> anyhow::Result { - let api_client = cartridge::Client::new(config.api_url); + let api_client = cartridge::CartridgeApiClient::new(config.api_url); #[cfg(feature = "vrf")] let vrf_service = config.vrf.map(VrfService::new); @@ -154,91 +145,54 @@ where pool, api_client, paymaster_client, - controller_deployer_address: config.controller_deployer_address, - controller_deployer_private_key: config.controller_deployer_private_key, #[cfg(feature = "vrf")] vrf_service, }) } - fn nonce(&self, address: ContractAddress) -> Result, StarknetApiError> { - match self.pool.get_nonce(address) { - pending_nonce @ Some(..) => Ok(pending_nonce), - None => Ok(self.state()?.nonce(address)?), - } - } - - fn state(&self) -> Result, StarknetApiError> { - match &*self.block_producer.producer.read() { - BlockProducerMode::Instant(_) => Ok(self.backend.storage.provider().latest()?), - BlockProducerMode::Interval(producer) => Ok(producer.executor().read().state()), - } - } - pub async fn execute_outside( &self, - address: ContractAddress, + contract_address: ContractAddress, outside_execution: OutsideExecution, signature: Vec, fee_source: Option, - ) -> Result { - debug!(%address, ?outside_execution, "Adding execute outside transaction."); + ) -> Result { + debug!(%contract_address, ?outside_execution, "Adding execute outside transaction."); self.on_cpu_blocking_task(move |this| async move { - let pm_address = this.controller_deployer_address; - let pm_private_key = this.controller_deployer_private_key; - - // ====================== CONTROLLER DEPLOYMENT ====================== - let state = this.state().map(Arc::new)?; - let is_controller_deployed = state.class_hash_of_contract(address)?.is_some(); - - if !is_controller_deployed { - debug!(target: "rpc::cartridge", controller = %address, "Controller not yet deployed"); - if let Some(tx) = craft_deploy_cartridge_controller_tx( - &this.api_client, - address, - pm_address, - pm_private_key, - this.backend.chain_spec.id(), - this.nonce(pm_address)?.unwrap_or_default(), - ).await? { - debug!(target: "rpc::cartridge", controller = %address, tx = format!("{:#x}", tx.hash), "Inserting Controller deployment transaction"); - this.pool.add_transaction(tx).await?; - this.block_producer.force_mine(); - } - } - // =================================================================== + let entry_point_selector = outside_execution.selector(); + let mut calldata = outside_execution.as_felts(); + calldata.extend(signature.clone()); - let mut execute_from_outside_call = - build_execute_from_outside_call(address, &outside_execution, &signature); - let mut user_address: Felt = address.into(); + let mut call: Call = Call { contract_address, entry_point_selector, calldata }; + let mut user_address: Felt = contract_address.into(); #[cfg(feature = "vrf")] if let Some(vrf_service) = &this.vrf_service { // check first if the outside execution calls include a request_random call if let Some((request_random_call, position)) = - request_random_call(&outside_execution) + get_request_random_call(&outside_execution) { - let calls_len = outside_execution_calls_len(&outside_execution); - if position + 1 >= calls_len { - return Err(StarknetApiError::unexpected( - "request_random call must be followed by another call", - )); + if position + 1 >= outside_execution.len() { + return Err(CartridgeApiError::VrfMissingFollowUpCall); } - if request_random_call.to != vrf_service.account_address() { - return Err(StarknetApiError::unexpected( - "request_random call must target the vrf account", - )); + + if request_random_call.contract_address != vrf_service.account_address() { + return Err(CartridgeApiError::VrfInvalidTarget); } // Delegate VRF computation to the VRF server let chain_id = this.backend.chain_spec.id(); let result = vrf_service - .outside_execution(address, &outside_execution, &signature, chain_id) + .outside_execution( + contract_address, + &outside_execution, + &signature, + chain_id, + ) .await?; - user_address = result.address; - execute_from_outside_call = - build_execute_from_outside_call_from_vrf_result(&result); + user_address = result.address.into(); + call = result.into(); } } @@ -247,24 +201,30 @@ where gas_token: DEFAULT_STRK_FEE_TOKEN_ADDRESS.into(), tip: Default::default(), }, - _ => FeeMode::Sponsored { - tip: Default::default(), + _ => FeeMode::Sponsored { tip: Default::default() }, + }; + + let invoke = RawInvokeParameters { + user_address, + gas_token: None, + max_gas_token_amount: None, + execute_from_outside_call: StarknetRsCall { + calldata: call.calldata, + to: call.contract_address.into(), + selector: call.entry_point_selector, }, }; let request = ExecuteRawRequest { - transaction: ExecuteRawTransactionParameters::RawInvoke { - invoke: RawInvokeParameters { - user_address, - execute_from_outside_call, - gas_token: None, - max_gas_token_amount: None, - }, - }, + transaction: ExecuteRawTransactionParameters::RawInvoke { invoke }, parameters: ExecutionParameters::V1 { fee_mode, time_bounds: None }, }; - let response = this.paymaster_client.execute_raw_transaction(request).await.map_err(StarknetApiError::unexpected)?; + let response = + this.paymaster_client.execute_raw_transaction(request).await.map_err(|e| { + CartridgeApiError::PaymasterExecutionFailed { reason: e.to_string() } + })?; + Ok(AddInvokeTransactionResponse { transaction_hash: response.transaction_hash }) }) .await? @@ -272,7 +232,7 @@ where /// Spawns an async function that is mostly CPU-bound blocking task onto the manager's blocking /// pool. - async fn on_cpu_blocking_task(&self, func: T) -> Result + async fn on_cpu_blocking_task(&self, func: T) -> Result where T: FnOnce(Self) -> F, F: Future + Send + 'static, @@ -295,9 +255,9 @@ where match self.task_spawner.cpu_bound().spawn(task).await { TaskResult::Ok(result) => Ok(result), - TaskResult::Err(err) => { - Err(StarknetApiError::unexpected(format!("internal task execution failed: {err}"))) - } + TaskResult::Err(err) => Err(CartridgeApiError::InternalError { + reason: format!("task execution failed: {err}"), + }), } } } @@ -335,13 +295,8 @@ where pub fn encode_calls(calls: Vec) -> Vec { let mut execute_calldata: Vec = vec![calls.len().into()]; for call in calls { - execute_calldata.push(call.contract_address.into()); - execute_calldata.push(call.entry_point_selector); - - execute_calldata.push(call.calldata.len().into()); - execute_calldata.extend_from_slice(&call.calldata); + execute_calldata.extend(Call::cairo_serialize(&call)); } - execute_calldata } @@ -358,7 +313,7 @@ pub async fn get_controller_deploy_tx_if_controller_address( tx: &ExecutableTxWithHash, chain_id: ChainId, state: Arc>, - cartridge_api_client: &cartridge::Client, + cartridge_api_client: &cartridge::CartridgeApiClient, ) -> anyhow::Result> { // The whole Cartridge paymaster flow would only be accessible mainly from the Controller // wallet. The Controller wallet only supports V3 transactions (considering < V3 @@ -396,7 +351,7 @@ pub async fn get_controller_deploy_tx_if_controller_address( /// /// Returns None if the provided `controller_address` is not registered in the Cartridge API. pub async fn craft_deploy_cartridge_controller_tx( - cartridge_api_client: &cartridge::Client, + cartridge_api_client: &cartridge::CartridgeApiClient, controller_address: ContractAddress, paymaster_address: ContractAddress, paymaster_private_key: Felt, @@ -444,51 +399,3 @@ pub async fn craft_deploy_cartridge_controller_tx( Ok(None) } } - -fn build_execute_from_outside_call_data( - address: ContractAddress, - outside_execution: &OutsideExecution, - signature: &Vec, -) -> katana_rpc_types::outside_execution::Call { - let entrypoint = match outside_execution { - OutsideExecution::V2(_) => selector!("execute_from_outside_v2"), - OutsideExecution::V3(_) => selector!("execute_from_outside_v3"), - }; - - let mut calldata = match outside_execution { - OutsideExecution::V2(v2) => OutsideExecutionV2::cairo_serialize(v2), - OutsideExecution::V3(v3) => OutsideExecutionV3::cairo_serialize(v3), - }; - - calldata.extend(Vec::::cairo_serialize(signature)); - - katana_rpc_types::outside_execution::Call { to: address, selector: entrypoint, calldata } -} - -fn build_execute_from_outside_call( - address: ContractAddress, - outside_execution: &OutsideExecution, - signature: &Vec, -) -> PaymasterCall { - let call = build_execute_from_outside_call_data(address, outside_execution, signature); - PaymasterCall { to: call.to.into(), selector: call.selector, calldata: call.calldata } -} - -fn build_execute_from_outside_call_from_vrf_result( - result: &cartridge::vrf::SignedOutsideExecution, -) -> PaymasterCall { - let (selector, calldata) = match &result.outside_execution { - cartridge::vrf::VrfOutsideExecution::V2(v2) => { - let mut calldata = OutsideExecutionV2::cairo_serialize(v2); - calldata.extend(Vec::::cairo_serialize(&result.signature)); - (selector!("execute_from_outside_v2"), calldata) - } - cartridge::vrf::VrfOutsideExecution::V3(v3) => { - let mut calldata = OutsideExecutionV3::cairo_serialize(v3); - calldata.extend(Vec::::cairo_serialize(&result.signature)); - (selector!("execute_from_outside_v3"), calldata) - } - }; - - PaymasterCall { to: result.address, selector, calldata } -} diff --git a/crates/rpc/rpc-server/src/cartridge/vrf.rs b/crates/rpc/rpc-server/src/cartridge/vrf.rs index e8de71329..b217135b0 100644 --- a/crates/rpc/rpc-server/src/cartridge/vrf.rs +++ b/crates/rpc/rpc-server/src/cartridge/vrf.rs @@ -1,10 +1,10 @@ //! VRF (Verifiable Random Function) service for Cartridge. -use cartridge::vrf::{RequestContext, SignedOutsideExecution, VrfOutsideExecution}; -use cartridge::VrfClient; +use cartridge::vrf::{RequestContext, SignedOutsideExecution, VrfClient, VrfOutsideExecution}; use katana_primitives::chain::ChainId; +use katana_primitives::execution::Call; use katana_primitives::{ContractAddress, Felt}; -use katana_rpc_api::error::starknet::StarknetApiError; +use katana_rpc_api::error::cartridge::CartridgeApiError; use katana_rpc_types::outside_execution::OutsideExecution; use starknet::macros::selector; use url::Url; @@ -16,7 +16,7 @@ pub struct VrfServiceConfig { pub vrf_contract: ContractAddress, } -#[derive(Clone)] +#[derive(Debug, Clone)] pub struct VrfService { client: VrfClient, account_address: ContractAddress, @@ -45,14 +45,14 @@ impl VrfService { outside_execution: &OutsideExecution, signature: &[Felt], chain_id: ChainId, - ) -> Result { + ) -> Result { let vrf_outside_execution = match outside_execution { OutsideExecution::V2(v2) => VrfOutsideExecution::V2(v2.clone()), OutsideExecution::V3(v3) => VrfOutsideExecution::V3(v3.clone()), }; let request = SignedOutsideExecution { - address: address.into(), + address, outside_execution: vrf_outside_execution, signature: signature.to_vec(), }; @@ -62,33 +62,23 @@ impl VrfService { rpc_url: Some(self.rpc_url.clone()), }; - self.client.outside_execution(request, context).await.map_err(|err| { - StarknetApiError::unexpected(format!("vrf outside_execution failed: {err}")) - }) + self.client + .outside_execution(request, context) + .await + .map_err(|err| CartridgeApiError::VrfExecutionFailed { reason: err.to_string() }) } } -pub(super) fn request_random_call( +pub(super) fn get_request_random_call( outside_execution: &OutsideExecution, -) -> Option<(katana_rpc_types::outside_execution::Call, usize)> { - let calls = match outside_execution { - OutsideExecution::V2(v2) => &v2.calls, - OutsideExecution::V3(v3) => &v3.calls, - }; - +) -> Option<(Call, usize)> { + let calls = outside_execution.calls(); calls .iter() - .position(|call| call.selector == selector!("request_random")) + .position(|call| call.entry_point_selector == selector!("request_random")) .map(|position| (calls[position].clone(), position)) } -pub(super) fn outside_execution_calls_len(outside_execution: &OutsideExecution) -> usize { - match outside_execution { - OutsideExecution::V2(v2) => v2.calls.len(), - OutsideExecution::V3(v3) => v3.calls.len(), - } -} - #[cfg(test)] mod tests { use katana_primitives::{felt, Felt}; @@ -101,15 +91,17 @@ mod tests { #[test] fn request_random_call_finds_position() { let vrf_address = ContractAddress::from(felt!("0x123")); - let other_call = katana_rpc_types::outside_execution::Call { - to: vrf_address, - selector: selector!("transfer"), + + let other_call = Call { calldata: vec![Felt::ONE], + contract_address: vrf_address, + entry_point_selector: selector!("transfer"), }; - let vrf_call = katana_rpc_types::outside_execution::Call { - to: vrf_address, - selector: selector!("request_random"), + + let vrf_call = Call { calldata: vec![Felt::TWO], + contract_address: vrf_address, + entry_point_selector: selector!("request_random"), }; let outside_execution = OutsideExecution::V2(OutsideExecutionV2 { @@ -121,9 +113,10 @@ mod tests { }); let (call, position) = - request_random_call(&outside_execution).expect("request_random found"); + get_request_random_call(&outside_execution).expect("request_random found"); + assert_eq!(position, 1); - assert_eq!(call.selector, vrf_call.selector); + assert_eq!(call.entry_point_selector, vrf_call.entry_point_selector); assert_eq!(call.calldata, vrf_call.calldata); } } diff --git a/crates/rpc/rpc-server/src/lib.rs b/crates/rpc/rpc-server/src/lib.rs index 00f5fabbd..09aa0f884 100644 --- a/crates/rpc/rpc-server/src/lib.rs +++ b/crates/rpc/rpc-server/src/lib.rs @@ -6,12 +6,14 @@ use std::net::SocketAddr; use std::time::Duration; -use jsonrpsee::core::middleware::RpcServiceBuilder; +use jsonrpsee::core::middleware::RpcServiceT; use jsonrpsee::core::{RegisterMethodError, TEN_MB_SIZE_BYTES}; +use jsonrpsee::server::middleware::rpc::RpcService; use jsonrpsee::server::{Server, ServerConfig, ServerHandle}; -use jsonrpsee::RpcModule; +use jsonrpsee::{MethodResponse, RpcModule}; use katana_tracing::gcloud::GoogleStackDriverMakeSpan; -use tower::ServiceBuilder; +use tower::layer::util::Identity; +use tower::{Layer, ServiceBuilder}; use tower_http::trace::TraceLayer; use tracing::info; @@ -23,21 +25,19 @@ pub mod paymaster; #[cfg(feature = "tee")] pub mod tee; -pub mod cors; pub mod dev; pub mod health; -pub mod metrics; +pub mod middleware; pub mod permit; pub mod starknet; pub mod txpool; -mod logger; mod utils; -use cors::Cors; use health::HealthCheck; +pub use jsonrpsee::core::middleware::RpcServiceBuilder; pub use jsonrpsee::http_client::HttpClient; pub use katana_rpc_api as api; -use metrics::RpcServerMetricsLayer; +use middleware::cors::Cors; /// The default maximum number of concurrent RPC connections. pub const DEFAULT_RPC_MAX_CONNECTIONS: u32 = 100; @@ -99,7 +99,7 @@ impl RpcServerHandle { } #[derive(Debug)] -pub struct RpcServer { +pub struct RpcServer { metrics: bool, cors: Option, health_check: bool, @@ -110,9 +110,11 @@ pub struct RpcServer { max_request_body_size: u32, max_response_body_size: u32, timeout: Duration, + + rpc_middleware: RpcServiceBuilder, } -impl RpcServer { +impl RpcServer { pub fn new() -> Self { Self { cors: None, @@ -124,9 +126,12 @@ impl RpcServer { max_request_body_size: TEN_MB_SIZE_BYTES, max_response_body_size: TEN_MB_SIZE_BYTES, timeout: DEFAULT_TIMEOUT, + rpc_middleware: RpcServiceBuilder::new(), } } +} +impl RpcServer { /// Set the maximum number of connections allowed. Default is 100. pub fn max_connections(mut self, max: u32) -> Self { self.max_connections = max; @@ -176,6 +181,22 @@ impl RpcServer { self } + /// Configure custom RPC middleware. + pub fn rpc_middleware(self, middleware: RpcServiceBuilder) -> RpcServer { + RpcServer { + rpc_middleware: middleware, + cors: self.cors, + module: self.module, + timeout: self.timeout, + metrics: self.metrics, + explorer: self.explorer, + health_check: self.health_check, + max_connections: self.max_connections, + max_request_body_size: self.max_request_body_size, + max_response_body_size: self.max_response_body_size, + } + } + /// Adds a new RPC module to the server. /// /// This can be chained with other calls to `module` to add multiple modules. @@ -189,7 +210,19 @@ impl RpcServer { self.module.merge(module)?; Ok(self) } +} +impl RpcServer +where + RpcMiddleware: Layer + Clone + Send + 'static, + >::Service: RpcServiceT< + MethodResponse = MethodResponse, + BatchResponse = MethodResponse, + NotificationResponse = MethodResponse, + > + Send + + Sync + + 'static, +{ pub async fn start(&self, addr: SocketAddr) -> Result { let mut modules = self.module.clone(); @@ -208,7 +241,6 @@ impl RpcServer { None }; - let rpc_metrics = self.metrics.then(|| RpcServerMetricsLayer::new(&modules)); let http_tracer = TraceLayer::new_for_http().make_span_with(GoogleStackDriverMakeSpan); let http_middleware = ServiceBuilder::new() @@ -220,9 +252,6 @@ impl RpcServer { #[cfg(feature = "explorer")] let http_middleware = http_middleware.option_layer(explorer_layer); - let rpc_middleware = - RpcServiceBuilder::new().option_layer(rpc_metrics).layer(logger::RpcLoggerLayer::new()); - let cfg = ServerConfig::builder() .max_connections(self.max_connections) .max_request_body_size(self.max_request_body_size) @@ -231,7 +260,7 @@ impl RpcServer { let server = Server::builder() .set_http_middleware(http_middleware) - .set_rpc_middleware(rpc_middleware) + .set_rpc_middleware(self.rpc_middleware.clone()) .set_config(cfg) .build(addr) .await?; @@ -256,7 +285,7 @@ impl RpcServer { } } -impl Default for RpcServer { +impl Default for RpcServer { fn default() -> Self { Self::new() } diff --git a/crates/rpc/rpc-server/src/middleware/cartridge.rs b/crates/rpc/rpc-server/src/middleware/cartridge.rs new file mode 100644 index 000000000..23361379b --- /dev/null +++ b/crates/rpc/rpc-server/src/middleware/cartridge.rs @@ -0,0 +1,585 @@ +use std::borrow::Cow; +use std::collections::HashSet; +use std::future::Future; + +use cartridge::CartridgeApiClient; +use jsonrpsee::core::middleware::{Batch, Notification, RpcServiceT}; +use jsonrpsee::core::traits::ToRpcParams; +use jsonrpsee::types::{ErrorObjectOwned, Request, Response, ResponsePayload}; +use jsonrpsee::{rpc_params, MethodResponse}; +use katana_genesis::constant::DEFAULT_UDC_ADDRESS; +use katana_pool::api::TransactionPool; +use katana_primitives::block::BlockIdOrTag; +use katana_primitives::contract::Nonce; +use katana_primitives::da::DataAvailabilityMode; +use katana_primitives::execution::Call; +use katana_primitives::fee::{AllResourceBoundsMapping, ResourceBoundsMapping}; +use katana_primitives::{ContractAddress, Felt}; +use katana_provider::{ProviderFactory, ProviderRO}; +use katana_rpc_api::error::cartridge::CartridgeApiError; +use katana_rpc_api::error::starknet::StarknetApiError; +use katana_rpc_types::broadcasted::{BroadcastedTx, BroadcastedTxWithChainId}; +use katana_rpc_types::{BroadcastedInvokeTx, FeeEstimate, FeeSource, OutsideExecution}; +use serde::de::DeserializeOwned; +use serde::Deserialize; +use starknet::core::types::SimulationFlagForEstimateFee; +use starknet::macros::selector; +use starknet::signers::local_wallet::SignError; +use starknet::signers::{LocalWallet, Signer, SigningKey}; +use tower::Layer; +use tracing::{debug, trace}; + +use crate::cartridge::encode_calls; +use crate::starknet::{PendingBlockProvider, StarknetApi}; + +const STARKNET_ESTIMATE_FEE: &str = "starknet_estimateFee"; +const CARTRIDGE_ADD_EXECUTE_FROM_OUTSIDE: &str = "cartridge_addExecuteFromOutside"; +const CARTRIDGE_ADD_EXECUTE_FROM_OUTSIDE_TX: &str = "cartridge_addExecuteOutsideTransaction"; + +#[derive(Debug)] +struct ControllerDeploymentContext +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + starknet: StarknetApi, + cartridge_api: CartridgeApiClient, + deployer_address: ContractAddress, + deployer_private_key: SigningKey, +} + +impl Clone for ControllerDeploymentContext +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + fn clone(&self) -> Self { + Self { + starknet: self.starknet.clone(), + cartridge_api: self.cartridge_api.clone(), + deployer_address: self.deployer_address, + deployer_private_key: self.deployer_private_key.clone(), + } + } +} + +#[derive(Debug)] +pub struct ControllerDeploymentLayer +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + context: ControllerDeploymentContext, +} + +impl ControllerDeploymentLayer +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + pub fn new( + starknet: StarknetApi, + cartridge_api: CartridgeApiClient, + deployer_address: ContractAddress, + deployer_private_key: SigningKey, + ) -> Self { + let context = ControllerDeploymentContext { + starknet, + cartridge_api, + deployer_address, + deployer_private_key, + }; + + Self { context } + } +} + +impl Layer for ControllerDeploymentLayer +where + Pool: TransactionPool + 'static, + PoolTx: From, + PP: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + type Service = ControllerDeploymentService; + + fn layer(&self, inner: S) -> Self::Service { + ControllerDeploymentService { context: self.context.clone(), service: inner } + } +} + +#[derive(Debug)] +pub struct ControllerDeploymentService +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + context: ControllerDeploymentContext, + service: S, +} + +impl ControllerDeploymentService +where + S: RpcServiceT + Send + Sync + Clone + 'static, + S: RpcServiceT, + Pool: TransactionPool + 'static, + PoolTx: From, + PP: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + fn controller_deployment_error(reason: impl Into) -> CartridgeApiError { + CartridgeApiError::ControllerDeployment { reason: reason.into() } + } + + fn estimate_fee_candidate_addresses(transactions: &[BroadcastedTx]) -> Vec { + transactions + .iter() + .filter_map(|tx| match tx { + BroadcastedTx::Invoke(tx) => Some(tx.sender_address), + BroadcastedTx::Declare(tx) => Some(tx.sender_address), + _ => None, + }) + .collect() + } + + fn build_estimate_fee_request<'a>( + request: &Request<'a>, + transactions: Vec, + simulation_flags: Vec, + block_id: BlockIdOrTag, + ) -> Result, CartridgeApiError> { + let params = rpc_params!(transactions, simulation_flags, block_id); + let params = params.to_rpc_params().map_err(|err| { + Self::controller_deployment_error(format!( + "failed to serialize augmented estimateFee params: {err}" + )) + })?; + + let mut new_request = request.clone(); + new_request.params = params.map(Cow::Owned); + + Ok(new_request) + } + + // If deployment txs are added, return the no-fee estimates for the original requests only. + async fn starknet_estimate_fee<'a>( + &self, + params: EstimateFeeParams, + request: Request<'a>, + ) -> S::MethodResponse { + let request_id = request.id().clone(); + match self.starknet_estimate_fee_inner(params, request).await { + Ok(response) => response, + Err(err) => MethodResponse::error(request_id, ErrorObjectOwned::from(err)), + } + } + + async fn cartridge_add_execute_from_outside<'a>( + &self, + params: AddExecuteOutsideParams, + request: Request<'a>, + ) -> S::MethodResponse { + if let Err(err) = self.cartridge_add_execute_from_outside_inner(params).await { + MethodResponse::error(request.id().clone(), ErrorObjectOwned::from(err)) + } else { + self.service.call(request).await + } + } + + async fn starknet_estimate_fee_inner<'a>( + &self, + params: EstimateFeeParams, + request: Request<'a>, + ) -> Result { + let EstimateFeeParams { block_id, simulation_flags, transactions } = params; + let candidate_addresses = Self::estimate_fee_candidate_addresses(&transactions); + + let deployer_nonce = self + .context + .starknet + .nonce_at(block_id, self.context.deployer_address) + .await + .map_err(|err| { + Self::controller_deployment_error(format!("failed to get deployer nonce: {err}")) + })?; + let deploy_controller_txs = self + .get_controller_deployment_txs(candidate_addresses, deployer_nonce) + .await + .map_err(|err| Self::controller_deployment_error(err.to_string()))?; + + // no Controller to deploy, simply forward the request + if deploy_controller_txs.is_empty() { + return Ok(self.service.call(request).await); + } + + let original_txs_count = transactions.len(); + let new_txs = [deploy_controller_txs, transactions].concat(); + let new_txs_count = new_txs.len(); + let new_request = + Self::build_estimate_fee_request(&request, new_txs, simulation_flags, block_id)?; + + let response = self.service.call(new_request).await; + let response_body = response.as_json().get(); + let res = serde_json::from_str::>>(response_body).map_err( + |err| { + Self::controller_deployment_error(format!( + "failed to parse estimateFee response: {err}" + )) + }, + )?; + + match res.payload { + ResponsePayload::Success(estimates) => { + if estimates.len() != new_txs_count { + return Err(Self::controller_deployment_error(format!( + "unexpected estimateFee response length: expected {new_txs_count}, got {}", + estimates.len() + ))); + } + + Ok(build_no_fee_response(&request, original_txs_count)) + } + + ResponsePayload::Error(..) => Ok(response), + } + } + + async fn cartridge_add_execute_from_outside_inner( + &self, + params: AddExecuteOutsideParams, + ) -> Result<(), CartridgeApiError> { + let address = params.address; + let block_id = BlockIdOrTag::PreConfirmed; + + // check if the address has already been deployed. + let is_deployed = match self.context.starknet.class_hash_at_address(block_id, address).await + { + Ok(..) => true, + Err(StarknetApiError::ContractNotFound) => false, + + Err(e) => { + return Err(CartridgeApiError::ControllerDeployment { + reason: format!("failed to check Controller deployment status: {e}"), + }); + } + }; + + if is_deployed { + return Ok(()); + } + + let nonce = self + .context + .starknet + .nonce_at(block_id, self.context.deployer_address) + .await + .map_err(|err| { + Self::controller_deployment_error(format!("failed to get deployer nonce: {err}")) + })?; + let deploy_tx = self + .get_controller_deployment_tx(address, nonce) + .await + .map_err(|err| Self::controller_deployment_error(err.to_string()))?; + + // None means the address is not of a Controller + if let Some(tx) = deploy_tx { + if let Err(e) = self.context.starknet.add_invoke_tx(tx).await { + return Err(CartridgeApiError::ControllerDeployment { + reason: format!("failed to submit deployment tx: {e}"), + }); + } + } + + Ok(()) + } + + async fn get_controller_deployment_txs( + &self, + controller_addresses: Vec, + initial_nonce: Nonce, + ) -> Result, Error> { + let mut deploy_transactions: Vec = Vec::new(); + let mut processed_addresses: HashSet = HashSet::new(); + + let mut deployer_nonce = initial_nonce; + + for address in controller_addresses { + // If the address has already been processed in this txs batch, just skip. + if processed_addresses.contains(&address) { + continue; + } + + let deploy_tx = self.get_controller_deployment_tx(address, deployer_nonce).await?; + + // None means the address is not a Controller + if let Some(tx) = deploy_tx { + deployer_nonce += Nonce::ONE; + processed_addresses.insert(address); + deploy_transactions.push(BroadcastedTx::Invoke(tx)); + } + } + + Ok(deploy_transactions) + } + + async fn get_controller_deployment_tx( + &self, + address: ContractAddress, + paymaster_nonce: Nonce, + ) -> Result, Error> { + let Some(ctor_calldata) = self.context.cartridge_api.get_account_calldata(address).await? + else { + // this means no controller with the given address + return Ok(None); + }; + + let call = Call { + contract_address: DEFAULT_UDC_ADDRESS, + calldata: ctor_calldata.constructor_calldata, + entry_point_selector: selector!("deployContract"), + }; + + let mut tx = BroadcastedInvokeTx { + sender_address: self.context.deployer_address, + calldata: encode_calls(vec![call]), + signature: Vec::new(), + nonce: paymaster_nonce, + paymaster_data: Vec::new(), + tip: 0u64.into(), + account_deployment_data: Vec::new(), + resource_bounds: ResourceBoundsMapping::All(AllResourceBoundsMapping::default()), + fee_data_availability_mode: DataAvailabilityMode::L1, + nonce_data_availability_mode: DataAvailabilityMode::L1, + is_query: false, + }; + + let signature = { + let chain = self.context.starknet.chain_id(); + let tx = BroadcastedTx::Invoke(tx.clone()); + let tx = BroadcastedTxWithChainId { tx, chain: chain.into() }; + + let signer = LocalWallet::from(self.context.deployer_private_key.clone()); + + let tx_hash = tx.calculate_hash(); + signer.sign_hash(&tx_hash).await.map_err(Error::SigningError)? + }; + + tx.signature = vec![signature.r, signature.s]; + + Ok(Some(tx)) + } +} + +impl RpcServiceT for ControllerDeploymentService +where + S: RpcServiceT + Send + Sync + Clone + 'static, + S: RpcServiceT, + Pool: TransactionPool + 'static, + PoolTx: From, + PP: PendingBlockProvider, + PF: ProviderFactory, + ::Provider: ProviderRO, +{ + type MethodResponse = S::MethodResponse; + type BatchResponse = S::BatchResponse; + type NotificationResponse = S::NotificationResponse; + + fn call<'a>( + &self, + request: Request<'a>, + ) -> impl Future + Send + 'a { + let this = (*self).clone(); + + async move { + let method = request.method_name(); + + match method { + STARKNET_ESTIMATE_FEE => { + trace!(%method, "Intercepting JSON-RPC method."); + if let Some(params) = parse_estimate_fee_params(&request) { + return this.starknet_estimate_fee(params, request).await; + } + } + + CARTRIDGE_ADD_EXECUTE_FROM_OUTSIDE | CARTRIDGE_ADD_EXECUTE_FROM_OUTSIDE_TX => { + trace!(%method, "Intercepting JSON-RPC method."); + if let Some(params) = parse_execute_outside_params(&request) { + return this.cartridge_add_execute_from_outside(params, request).await; + } + } + + _ => {} + } + + this.service.call(request).await + } + } + + fn batch<'a>( + &self, + requests: Batch<'a>, + ) -> impl Future + Send + 'a { + self.service.batch(requests) + } + + fn notification<'a>( + &self, + n: Notification<'a>, + ) -> impl Future + Send + 'a { + self.service.notification(n) + } +} + +impl Clone for ControllerDeploymentLayer +where + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + fn clone(&self) -> Self { + Self { context: self.context.clone() } + } +} + +impl Clone for ControllerDeploymentService +where + S: Clone, + Pool: TransactionPool + 'static, + PP: PendingBlockProvider, + PF: ProviderFactory, +{ + fn clone(&self) -> Self { + Self { context: self.context.clone(), service: self.service.clone() } + } +} + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("cartridge api error: {0}")] + Client(#[from] cartridge::api::Error), + + #[error("failed to sign deploy transaction: {0}")] + SigningError(SignError), +} + +#[allow(dead_code)] +#[derive(Deserialize)] +struct AddExecuteOutsideParams { + address: ContractAddress, + outside_execution: OutsideExecution, + signature: Vec, + fee_source: Option, +} + +#[derive(Deserialize)] +struct EstimateFeeParams { + #[serde(alias = "request")] + transactions: Vec, + #[serde(alias = "simulationFlags")] + simulation_flags: Vec, + #[serde(alias = "blockId")] + block_id: BlockIdOrTag, +} + +#[derive(Deserialize)] +struct AddExecuteOutsidePositionalParams( + ContractAddress, + OutsideExecution, + Vec, + #[serde(default)] Option, +); + +#[derive(Deserialize)] +#[serde(untagged)] +enum AddExecuteOutsideRequestParams { + Named(AddExecuteOutsideParams), + Positional(AddExecuteOutsidePositionalParams), +} + +impl From for AddExecuteOutsideParams { + fn from(value: AddExecuteOutsideRequestParams) -> Self { + match value { + AddExecuteOutsideRequestParams::Named(params) => params, + AddExecuteOutsideRequestParams::Positional(params) => Self { + address: params.0, + outside_execution: params.1, + signature: params.2, + fee_source: params.3, + }, + } + } +} + +#[derive(Deserialize)] +struct EstimateFeePositionalParams( + Vec, + Vec, + BlockIdOrTag, +); + +#[derive(Deserialize)] +#[serde(untagged)] +enum EstimateFeeRequestParams { + Named(EstimateFeeParams), + Positional(EstimateFeePositionalParams), +} + +impl From for EstimateFeeParams { + fn from(value: EstimateFeeRequestParams) -> Self { + match value { + EstimateFeeRequestParams::Named(params) => params, + EstimateFeeRequestParams::Positional(params) => { + Self { transactions: params.0, simulation_flags: params.1, block_id: params.2 } + } + } + } +} + +fn parse_params(request: &Request<'_>, method: &str) -> Option { + match request.params().parse() { + Ok(params) => Some(params), + Err(..) => { + debug!(target: "cartridge", "Failed to parse {method} params."); + None + } + } +} + +fn parse_execute_outside_params(request: &Request<'_>) -> Option { + parse_params::(request, "execute outside").map(Into::into) +} + +/// Extract estimate_fee parameters from the request. +fn parse_estimate_fee_params(request: &Request<'_>) -> Option { + parse_params::(request, "estimate fee").map(Into::into) +} + +// Temporary shim for --dev.no-fee when deployment txs are prepended for controllers. +// Remove once starknet_estimateFee natively returns zeroed fees in this scenario. +fn build_no_fee_response(request: &Request<'_>, count: usize) -> MethodResponse { + let estimate_fees = vec![ + FeeEstimate { + l1_gas_consumed: 0, + l1_gas_price: 0, + l2_gas_consumed: 0, + l2_gas_price: 0, + l1_data_gas_consumed: 0, + l1_data_gas_price: 0, + overall_fee: 0 + }; + count + ]; + + MethodResponse::response( + request.id().clone(), + jsonrpsee::ResponsePayload::success(estimate_fees), + usize::MAX, + ) +} diff --git a/crates/rpc/rpc-server/src/cors.rs b/crates/rpc/rpc-server/src/middleware/cors.rs similarity index 100% rename from crates/rpc/rpc-server/src/cors.rs rename to crates/rpc/rpc-server/src/middleware/cors.rs diff --git a/crates/rpc/rpc-server/src/logger.rs b/crates/rpc/rpc-server/src/middleware/logger.rs similarity index 95% rename from crates/rpc/rpc-server/src/logger.rs rename to crates/rpc/rpc-server/src/middleware/logger.rs index 04a530b8e..89bd9dfa1 100644 --- a/crates/rpc/rpc-server/src/logger.rs +++ b/crates/rpc/rpc-server/src/middleware/logger.rs @@ -16,6 +16,12 @@ impl RpcLoggerLayer { } } +impl Default for RpcLoggerLayer { + fn default() -> Self { + Self::new() + } +} + impl tower::Layer for RpcLoggerLayer { type Service = RpcLogger; diff --git a/crates/rpc/rpc-server/src/metrics.rs b/crates/rpc/rpc-server/src/middleware/metrics.rs similarity index 96% rename from crates/rpc/rpc-server/src/metrics.rs rename to crates/rpc/rpc-server/src/middleware/metrics.rs index 99c08f019..f68fa6fa7 100644 --- a/crates/rpc/rpc-server/src/metrics.rs +++ b/crates/rpc/rpc-server/src/middleware/metrics.rs @@ -96,7 +96,6 @@ struct RpcServerCallMetrics { } /// Tower layer for RPC server metrics -#[allow(missing_debug_implementations)] #[derive(Clone)] pub struct RpcServerMetricsLayer { metrics: RpcServerMetrics, @@ -108,6 +107,12 @@ impl RpcServerMetricsLayer { } } +impl std::fmt::Debug for RpcServerMetricsLayer { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("RpcServerMetricsLayer").field("metrics", &"..").finish() + } +} + impl Layer for RpcServerMetricsLayer { type Service = RpcRequestMetricsService; diff --git a/crates/rpc/rpc-server/src/middleware/mod.rs b/crates/rpc/rpc-server/src/middleware/mod.rs new file mode 100644 index 000000000..1dd633d79 --- /dev/null +++ b/crates/rpc/rpc-server/src/middleware/mod.rs @@ -0,0 +1,6 @@ +pub mod cors; +pub mod logger; +pub mod metrics; + +#[cfg(feature = "cartridge")] +pub mod cartridge; diff --git a/crates/rpc/rpc-server/src/starknet/config.rs b/crates/rpc/rpc-server/src/starknet/config.rs index d832797ae..eecceb070 100644 --- a/crates/rpc/rpc-server/src/starknet/config.rs +++ b/crates/rpc/rpc-server/src/starknet/config.rs @@ -40,22 +40,4 @@ pub struct StarknetApiConfig { /// [`VersionedConstants`](katana_executor::implementation::blockifier::blockifier::VersionedConstants) /// used for execution (i.e., estimates, simulation, and call) pub versioned_constant_overrides: Option, - - #[cfg(feature = "cartridge")] - pub paymaster: Option, -} - -/// Configuration for controller deployment during fee estimation. -/// -/// This is used to deploy Cartridge controller accounts during fee estimation -/// so that the fee estimation can be performed correctly. -#[cfg(feature = "cartridge")] -#[derive(Debug, Clone)] -pub struct CartridgePaymasterConfig { - /// The root URL for the Cartridge API. - pub cartridge_api_url: url::Url, - /// The paymaster account address used for controller deployment. - pub paymaster_address: katana_primitives::ContractAddress, - /// The paymaster account private key. - pub paymaster_private_key: katana_primitives::Felt, } diff --git a/crates/rpc/rpc-server/src/starknet/list.rs b/crates/rpc/rpc-server/src/starknet/list.rs index 77b4b7c71..09af290af 100644 --- a/crates/rpc/rpc-server/src/starknet/list.rs +++ b/crates/rpc/rpc-server/src/starknet/list.rs @@ -1,7 +1,7 @@ //! Implementation of list endpoints for the Starknet API. use jsonrpsee::core::{async_trait, RpcResult}; -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::transaction::TxNumber; use katana_provider::{ProviderFactory, ProviderRO}; use katana_rpc_api::starknet_ext::StarknetApiExtServer; diff --git a/crates/rpc/rpc-server/src/starknet/mod.rs b/crates/rpc/rpc-server/src/starknet/mod.rs index 87accaeaa..f7258207d 100644 --- a/crates/rpc/rpc-server/src/starknet/mod.rs +++ b/crates/rpc/rpc-server/src/starknet/mod.rs @@ -8,7 +8,7 @@ use katana_chain_spec::ChainSpec; use katana_core::utils::get_current_timestamp; use katana_executor::{ExecutionResult, ResultAndStates}; use katana_gas_price_oracle::GasPriceOracle; -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::block::{BlockHashOrNumber, BlockIdOrTag, FinalityStatus, GasPrices}; use katana_primitives::class::{ClassHash, CompiledClass}; use katana_primitives::contract::{ContractAddress, Nonce, StorageKey, StorageValue}; @@ -69,8 +69,6 @@ mod read; mod trace; mod write; -#[cfg(feature = "cartridge")] -pub use config::CartridgePaymasterConfig; pub use config::StarknetApiConfig; pub use pending::PendingBlockProvider; diff --git a/crates/rpc/rpc-server/src/starknet/read.rs b/crates/rpc/rpc-server/src/starknet/read.rs old mode 100644 new mode 100755 index 62b04c44c..ceab979d6 --- a/crates/rpc/rpc-server/src/starknet/read.rs +++ b/crates/rpc/rpc-server/src/starknet/read.rs @@ -1,16 +1,11 @@ -#[cfg(feature = "cartridge")] -use std::sync::Arc; - use jsonrpsee::core::{async_trait, RpcResult}; use jsonrpsee::types::ErrorObjectOwned; -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::block::BlockIdOrTag; use katana_primitives::class::ClassHash; use katana_primitives::contract::{Nonce, StorageKey, StorageValue}; use katana_primitives::transaction::{ExecutableTx, ExecutableTxWithHash, TxHash}; use katana_primitives::{ContractAddress, Felt}; -#[cfg(feature = "cartridge")] -use katana_provider::api::state::StateFactoryProvider; use katana_provider::{ProviderFactory, ProviderRO}; use katana_rpc_api::error::starknet::StarknetApiError; use katana_rpc_api::starknet::StarknetApiServer; @@ -31,8 +26,6 @@ use katana_rpc_types::{ }; use super::StarknetApi; -#[cfg(feature = "cartridge")] -use crate::cartridge; use crate::starknet::pending::PendingBlockProvider; #[async_trait] @@ -187,70 +180,6 @@ where .with_account_validation(should_validate) .with_nonce_check(false); - // Hook the estimate fee to pre-deploy the controller contract - // and enhance UX on the client side. - // Refer to the `handle_cartridge_controller_deploy` function in `cartridge.rs` - // for more details. - #[cfg(feature = "cartridge")] - let transactions = if let Some(paymaster) = &self.inner.config.paymaster { - let paymaster_address = paymaster.paymaster_address; - let paymaster_private_key = paymaster.paymaster_private_key; - - let state = - self.storage().provider().latest().map(Arc::new).map_err(StarknetApiError::from)?; - - let mut ctrl_deploy_txs = Vec::new(); - - // Check if any of the transactions are sent from an address associated with a Cartridge - // Controller account. If yes, we craft a Controller deployment transaction - // for each of the unique sender and push it at the beginning of the - // transaction list so that all the requested transactions are executed against a state - // with the Controller accounts deployed. - - let paymaster_nonce = match self.nonce_at(block_id, paymaster_address).await { - Ok(nonce) => nonce, - Err(err) => match err { - StarknetApiError::ContractNotFound => { - return Err(StarknetApiError::unexpected( - "Cartridge paymaster account doesn't exist", - ) - .into()); - } - _ => return Err(ErrorObjectOwned::from(err)), - }, - }; - - for tx in &transactions { - let api = ::cartridge::Client::new(paymaster.cartridge_api_url.clone()); - - let deploy_controller_tx = - cartridge::get_controller_deploy_tx_if_controller_address( - paymaster_address, - paymaster_private_key, - paymaster_nonce, - tx, - self.inner.chain_spec.id(), - state.clone(), - &api, - ) - .await - .map_err(StarknetApiError::from)?; - - if let Some(tx) = deploy_controller_tx { - ctrl_deploy_txs.push(tx); - } - } - - if !ctrl_deploy_txs.is_empty() { - ctrl_deploy_txs.extend(transactions); - ctrl_deploy_txs - } else { - transactions - } - } else { - transactions - }; - let permit = self.inner.estimate_fee_permit.acquire().await.map_err(|e| { StarknetApiError::unexpected(format!("Failed to acquire permit: {e}")) diff --git a/crates/rpc/rpc-server/src/starknet/trace.rs b/crates/rpc/rpc-server/src/starknet/trace.rs index 9658492c7..ca75c5d18 100644 --- a/crates/rpc/rpc-server/src/starknet/trace.rs +++ b/crates/rpc/rpc-server/src/starknet/trace.rs @@ -1,5 +1,5 @@ use jsonrpsee::core::{async_trait, RpcResult}; -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::block::{BlockIdOrTag, ConfirmedBlockIdOrTag}; use katana_primitives::transaction::TxHash; use katana_provider::{ProviderFactory, ProviderRO}; diff --git a/crates/rpc/rpc-server/src/starknet/write.rs b/crates/rpc/rpc-server/src/starknet/write.rs index 89f88ec1f..ad4cda2a1 100644 --- a/crates/rpc/rpc-server/src/starknet/write.rs +++ b/crates/rpc/rpc-server/src/starknet/write.rs @@ -1,7 +1,7 @@ use std::time::Duration; use jsonrpsee::core::{async_trait, RpcResult}; -use katana_pool::TransactionPool; +use katana_pool::api::TransactionPool; use katana_primitives::transaction::TxHash; use katana_provider::{ProviderFactory, ProviderRO}; use katana_rpc_api::error::starknet::StarknetApiError; diff --git a/crates/rpc/rpc-server/src/txpool.rs b/crates/rpc/rpc-server/src/txpool.rs index 324789668..9abb55c0b 100644 --- a/crates/rpc/rpc-server/src/txpool.rs +++ b/crates/rpc/rpc-server/src/txpool.rs @@ -1,7 +1,7 @@ use std::collections::BTreeMap; use jsonrpsee::core::{async_trait, RpcResult}; -use katana_pool::{PoolTransaction, TransactionPool}; +use katana_pool::api::{PoolTransaction, TransactionPool}; use katana_primitives::ContractAddress; use katana_rpc_api::txpool::TxPoolApiServer; use katana_rpc_types::txpool::{TxPoolContent, TxPoolInspect, TxPoolStatus, TxPoolTransaction}; diff --git a/crates/rpc/rpc-server/tests/controller_deployment.rs b/crates/rpc/rpc-server/tests/controller_deployment.rs new file mode 100644 index 000000000..256ce2c05 --- /dev/null +++ b/crates/rpc/rpc-server/tests/controller_deployment.rs @@ -0,0 +1,819 @@ +#![cfg(feature = "cartridge")] + +//! Integration tests for the `ControllerDeploymentService` middleware. + +use std::collections::HashMap; +use std::future::Future; +use std::sync::{Arc, Mutex}; + +use axum::extract::State; +use axum::response::IntoResponse; +use axum::routing::post; +use axum::Router; +use jsonrpsee::core::middleware::{Batch, Notification, RpcServiceT}; +use jsonrpsee::types::Request; +use jsonrpsee::MethodResponse; +use katana_chain_spec::ChainSpec; +use katana_executor::ExecutionFlags; +use katana_gas_price_oracle::GasPriceOracle; +use katana_pool::api::TransactionPool; +use katana_pool::ordering::FiFo; +use katana_pool::pool::Pool; +use katana_pool::validation::NoopValidator; +use katana_primitives::transaction::ExecutableTxWithHash; +use katana_primitives::Felt; +use katana_provider::test_utils::test_provider; +use katana_rpc_server::middleware::cartridge::ControllerDeploymentLayer; +use katana_rpc_server::starknet::{PendingBlockProvider, StarknetApi, StarknetApiConfig}; +use katana_rpc_types::*; +use katana_tasks::TaskManager; +use serde_json::json; +use starknet::signers::SigningKey; +use tokio::net::TcpListener; +use tower::Layer; +use url::Url; + +// --------------------------------------------------------------------------- +// Group 1: starknet_estimateFee +// --------------------------------------------------------------------------- + +/// ## Case: +/// +/// The sender address 0x1 already exists and requires no extra deployment. +/// +/// ## Expected: +/// +/// Since no Controllers need deployment, the request is forwarded unchanged +/// and the response is passed through. +#[tokio::test(flavor = "multi_thread")] +async fn estimate_fee_forwards_when_no_controllers() { + let inner_responses = { + let mut m = HashMap::new(); + m.insert( + "starknet_estimateFee".to_string(), + vec![FeeEstimate { + l1_gas_consumed: 1, + l1_gas_price: 2, + l2_gas_consumed: 3, + l2_gas_price: 4, + l1_data_gas_consumed: 5, + l1_data_gas_price: 6, + overall_fee: 7, + }], + ); + m + }; + + let setup = setup_test(HashMap::new(), inner_responses).await; + + let tx = make_invoke_tx_json(DEPLOYER_ADDRESS); + let params = json!([[tx], [], "latest"]); + let raw = make_rpc_request_str("starknet_estimateFee", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let response = setup.service.call(request).await; + + // The inner service should have been called exactly once. + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1, "inner service should be called once"); + assert_eq!(calls[0].method, "starknet_estimateFee"); + + // The response should contain the fee estimate from the inner service (passed through). + let response_json: serde_json::Value = serde_json::from_str(response.as_json().get()).unwrap(); + let result = response_json.get("result").expect("response should have result"); + assert!(result.is_array()); + assert_eq!(result.as_array().unwrap().len(), 1); +} + +/// ## Case: +/// +/// Address 0xDEAD is not yet deployed and belongs to a Controller account. +/// +/// ## Expected: +/// +/// The middleware prepends a deploy transaction to the estimate fee +/// request and returns estimates for the original transactions only. +#[tokio::test(flavor = "multi_thread")] +async fn estimate_fee_prepends_deploy_tx_for_controller() { + let cartridge_responses = { + let mut m = HashMap::new(); + m.insert(CONTROLLER_ADDRESS.to_string(), controller_calldata_response(CONTROLLER_ADDRESS)); + m + }; + + let inner_responses = { + let mut m = HashMap::new(); + // The inner service will receive 2 txs (1 deploy + 1 original). + m.insert( + "starknet_estimateFee".to_string(), + vec![ + FeeEstimate { + l1_gas_consumed: 0xa, + l1_gas_price: 0xb, + l2_gas_consumed: 0xc, + l2_gas_price: 0xd, + l1_data_gas_consumed: 0xe, + l1_data_gas_price: 0xf, + overall_fee: 0x10, + }, + FeeEstimate { + l1_gas_consumed: 1, + l1_gas_price: 2, + l2_gas_consumed: 3, + l2_gas_price: 4, + l1_data_gas_consumed: 5, + l1_data_gas_price: 6, + overall_fee: 7, + }, + ], + ); + m + }; + + let setup = setup_test(cartridge_responses, inner_responses).await; + + let tx = make_invoke_tx_json(CONTROLLER_ADDRESS); + let params = json!([[tx], [], "latest"]); + let raw = make_rpc_request_str("starknet_estimateFee", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let response = setup.service.call(request).await; + + // Inner service should receive 2 txs: deploy tx + original tx. + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1, "inner service should be called once"); + assert_eq!( + calls[0].tx_count, + Some(2), + "inner service should receive 2 transactions (deploy + original)" + ); + + // The middleware response should have 1 zero-fee estimate (for the original tx only). + let response_json: serde_json::Value = serde_json::from_str(response.as_json().get()).unwrap(); + let result = response_json.get("result").expect("response should have result"); + let estimates = result.as_array().unwrap(); + assert_eq!(estimates.len(), 1, "response should have 1 estimate for the original tx"); + + // All fee fields should be zero. + let est = &estimates[0]; + assert_eq!(est["overall_fee"], "0x0"); + assert_eq!(est["l1_gas_consumed"], "0x0"); + assert_eq!(est["l2_gas_consumed"], "0x0"); +} + +/// ## Case: +/// +/// Address 0xBEEF is not deployed and the Cartridge API does not recognize it as a +/// Controller. +/// +/// ## Expected: +/// +/// Even though the address is undeployed, no deploy transaction is created and the original request +/// is forwarded unchanged. +#[tokio::test(flavor = "multi_thread")] +async fn estimate_fee_forwards_for_non_controller() { + let inner_responses = { + let mut m = HashMap::new(); + m.insert( + "starknet_estimateFee".to_string(), + vec![FeeEstimate { + l1_gas_consumed: 1, + l1_gas_price: 2, + l2_gas_consumed: 3, + l2_gas_price: 4, + l1_data_gas_consumed: 5, + l1_data_gas_price: 6, + overall_fee: 7, + }], + ); + m + }; + + let setup = setup_test(HashMap::new(), inner_responses).await; + + let tx = make_invoke_tx_json(NON_CONTROLLER_ADDRESS); + let params = json!([[tx], [], "latest"]); + let raw = make_rpc_request_str("starknet_estimateFee", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let response = setup.service.call(request).await; + + // Inner service receives the request unchanged (1 tx). + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "starknet_estimateFee"); + + // Response is passed through. + let response_json: serde_json::Value = serde_json::from_str(response.as_json().get()).unwrap(); + let result = response_json.get("result").expect("response should have result"); + assert_eq!(result.as_array().unwrap().len(), 1); +} + +/// ## Case: +/// +/// Three invoke transactions all from undeployed Controller address 0xDEAD. +/// +/// ## Expected: +/// +/// The middleware deduplicates by sender address, creating only one deploy transaction +/// despite three transactions from the same sender. +/// +/// Inner service receives 4 txs (1 deploy + 3 original); middleware returns 3 zero-fee estimates. +#[tokio::test(flavor = "multi_thread")] +async fn estimate_fee_deduplicates_same_controller() { + let cartridge_responses = { + let mut m = HashMap::new(); + m.insert(CONTROLLER_ADDRESS.to_string(), controller_calldata_response(CONTROLLER_ADDRESS)); + m + }; + + let zero_fee = FeeEstimate { + l1_gas_consumed: 0, + l1_gas_price: 0, + l2_gas_consumed: 0, + l2_gas_price: 0, + l1_data_gas_consumed: 0, + l1_data_gas_price: 0, + overall_fee: 0, + }; + + let inner_responses = { + let mut m = HashMap::new(); + // Inner service receives 4 txs (1 deploy + 3 original). + m.insert( + "starknet_estimateFee".to_string(), + vec![zero_fee.clone(), zero_fee.clone(), zero_fee.clone(), zero_fee], + ); + m + }; + + let setup = setup_test(cartridge_responses, inner_responses).await; + + let tx1 = make_invoke_tx_json(CONTROLLER_ADDRESS); + let tx2 = make_invoke_tx_json(CONTROLLER_ADDRESS); + let tx3 = make_invoke_tx_json(CONTROLLER_ADDRESS); + let params = json!([[tx1, tx2, tx3], [], "latest"]); + let raw = make_rpc_request_str("starknet_estimateFee", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let response = setup.service.call(request).await; + + // Inner service should receive 4 txs: 1 deploy + 3 original. + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!( + calls[0].tx_count, + Some(4), + "inner service should receive 4 transactions (1 deploy + 3 original)" + ); + + // Middleware should return 3 zero-fee estimates (one per original tx). + let response_json: serde_json::Value = serde_json::from_str(response.as_json().get()).unwrap(); + let result = response_json.get("result").expect("response should have result"); + let estimates = result.as_array().unwrap(); + assert_eq!(estimates.len(), 3, "response should have 3 estimates for the 3 original txs"); + + for est in estimates { + assert_eq!(est["overall_fee"], "0x0"); + } +} + +// --------------------------------------------------------------------------- +// Group 2: cartridge_addExecuteFromOutside +// --------------------------------------------------------------------------- + +/// ## Case: +/// +/// The sender address (0x1) is already deployed. +/// +/// ## Expected: +/// +/// The middleware detects this and skips Controller deployment, forwarding the +/// request unchanged without querying the Cartridge API. +/// +/// Inner service receives request unchanged; pool remains empty; Cartridge API receives no +/// requests. +#[tokio::test(flavor = "multi_thread")] +async fn execute_outside_skips_deploy_when_already_deployed() { + let setup = setup_test(HashMap::new(), HashMap::new()).await; + + let params = make_execute_outside_params(DEPLOYER_ADDRESS); + let raw = make_rpc_request_str("cartridge_addExecuteFromOutside", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + // Pool should be empty — no deploy tx was added. + assert_eq!(setup.pool.size(), 0, "pool should be empty"); + + // Inner service should have been called (request forwarded). + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "cartridge_addExecuteFromOutside"); + + // Cartridge API should not have been queried. + let api_requests = setup.mock_api_state.received_requests.lock().unwrap(); + assert!(api_requests.is_empty(), "Cartridge API should not have been queried"); +} + +/// ## Case: +/// +/// The sender address (0xDEAD) is not deployed and belongs to a Controller account. +/// +/// ## Expected: +/// +/// The middleware creates a deploy transaction, adds it to the pool, and then forwards +/// the original request to the inner service. +/// +/// Pool contains 1 deploy transaction; inner service receives request. +#[tokio::test(flavor = "multi_thread")] +async fn execute_outside_deploys_controller() { + let cartridge_responses = { + let mut m = HashMap::new(); + m.insert(CONTROLLER_ADDRESS.to_string(), controller_calldata_response(CONTROLLER_ADDRESS)); + m + }; + + let setup = setup_test(cartridge_responses, HashMap::new()).await; + + let params = make_execute_outside_params(CONTROLLER_ADDRESS); + let raw = make_rpc_request_str("cartridge_addExecuteFromOutside", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + // A deploy transaction should have been added to the pool. + assert_eq!(setup.pool.size(), 1, "pool should contain 1 deploy transaction"); + + // Inner service should have been called (request forwarded). + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "cartridge_addExecuteFromOutside"); +} + +/// ## Case: +/// +/// The sender address (0xBEEF) is not deployed and is not a Controller. +/// +/// ## Expected: +/// +/// The middleware skips deployment and forwards the request unchanged. +/// +/// Pool remains empty; inner service receives request. +#[tokio::test(flavor = "multi_thread")] +async fn execute_outside_skips_deploy_for_non_controller() { + let setup = setup_test(HashMap::new(), HashMap::new()).await; + + let params = make_execute_outside_params(NON_CONTROLLER_ADDRESS); + let raw = make_rpc_request_str("cartridge_addExecuteFromOutside", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + // Pool should be empty — no deploy tx was added. + assert_eq!(setup.pool.size(), 0, "pool should be empty"); + + // Inner service should have been called. + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "cartridge_addExecuteFromOutside"); +} + +/// ## Case: +/// +/// Same scenario as `execute_outside_deploys_controller` but uses the alternate +/// method name "cartridge_addExecuteOutsideTransaction" to verify both method +/// names are intercepted by the middleware. +/// +/// ## Expected: +/// +/// Deploy transaction added to pool and request forwarded. +#[tokio::test(flavor = "multi_thread")] +async fn execute_outside_tx_method_variant() { + let cartridge_responses = { + let mut m = HashMap::new(); + m.insert(CONTROLLER_ADDRESS.to_string(), controller_calldata_response(CONTROLLER_ADDRESS)); + m + }; + + let setup = setup_test(cartridge_responses, HashMap::new()).await; + + let params = make_execute_outside_params(CONTROLLER_ADDRESS); + let raw = make_rpc_request_str("cartridge_addExecuteOutsideTransaction", ¶ms); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + // A deploy transaction should have been added to the pool. + assert_eq!(setup.pool.size(), 1, "pool should contain 1 deploy transaction"); + + // Inner service should have been called with the alternate method name. + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "cartridge_addExecuteOutsideTransaction"); +} + +// --------------------------------------------------------------------------- +// Group 3: Passthrough +// --------------------------------------------------------------------------- + +/// ## Case: +/// +/// A request for "starknet_getBlockNumber" is not intercepted by the middleware +/// and is forwarded directly to the inner service. +/// +/// ## Expected: +/// +/// inner service receives request unchanged; no Cartridge API requests made. +#[tokio::test(flavor = "multi_thread")] +async fn passthrough_other_methods() { + let setup = setup_test(HashMap::new(), HashMap::new()).await; + + let raw = make_rpc_request_str("starknet_getBlockNumber", &json!([])); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "starknet_getBlockNumber"); + + let api_requests = setup.mock_api_state.received_requests.lock().unwrap(); + assert!(api_requests.is_empty(), "Cartridge API should not have been queried"); +} + +/// ## Case: +/// +/// When starknet_estimateFee is called with malformed params, the middleware +/// should gracefully falls through to the inner service rather than erroring. +/// +/// ## Expected: +/// +/// Inner service receives request unchanged. +#[tokio::test(flavor = "multi_thread")] +async fn passthrough_malformed_estimate_fee() { + let setup = setup_test(HashMap::new(), HashMap::new()).await; + + // Malformed params — not a valid array of transactions. + let raw = make_rpc_request_str("starknet_estimateFee", &json!(["not_valid"])); + + let request: Request<'_> = serde_json::from_str(&raw).unwrap(); + let _response = setup.service.call(request).await; + + // The inner service should have received the request (fallthrough). + let calls = setup.mock_rpc.recorded_calls(); + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].method, "starknet_estimateFee"); +} + +// --------------------------------------------------------------------------- +// Test Fixtures +// --------------------------------------------------------------------------- + +type TestPool = + Pool, FiFo>; + +/// A no-op pending block provider. All methods return `Ok(None)`, matching +/// instant-mining mode behaviour. +#[derive(Debug, Clone)] +struct NoPendingBlockProvider; + +impl PendingBlockProvider for NoPendingBlockProvider { + fn pending_state( + &self, + ) -> katana_rpc_server::starknet::StarknetApiResult< + Option>, + > { + Ok(None) + } + + fn get_pending_state_update( + &self, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_block_with_txs( + &self, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_block_with_receipts( + &self, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_block_with_tx_hashes( + &self, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_transaction( + &self, + _hash: katana_primitives::transaction::TxHash, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_receipt( + &self, + _hash: katana_primitives::transaction::TxHash, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_trace( + &self, + _hash: katana_primitives::transaction::TxHash, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } + + fn get_pending_transaction_by_index( + &self, + _index: katana_primitives::transaction::TxNumber, + ) -> katana_rpc_server::starknet::StarknetApiResult> { + Ok(None) + } +} + +#[derive(Clone)] +struct MockCartridgeApiState { + /// Map from hex address (with "0x" prefix, lowercase) to the response JSON. + responses: Arc>, + /// Log of all requests received. + received_requests: Arc>>, +} + +async fn mock_cartridge_handler( + State(state): State, + axum::Json(body): axum::Json, +) -> impl IntoResponse { + state.received_requests.lock().unwrap().push(body.clone()); + + let address = body.get("address").and_then(|v| v.as_str()).unwrap_or(""); + + if let Some(response) = state.responses.get(address) { + axum::Json(response.clone()).into_response() + } else { + "Address not found".into_response() + } +} + +/// Start a mock Cartridge API server. Returns (base URL, state handle, join handle). +async fn start_mock_cartridge_api( + responses: HashMap, +) -> (Url, MockCartridgeApiState) { + let state = MockCartridgeApiState { + responses: Arc::new(responses), + received_requests: Arc::new(Mutex::new(Vec::new())), + }; + + let app = Router::new() + .route("/accounts/calldata", post(mock_cartridge_handler)) + .with_state(state.clone()); + + let listener = TcpListener::bind("127.0.0.1:0").await.unwrap(); + let addr = listener.local_addr().unwrap(); + + tokio::spawn(async move { + axum::serve(listener, app).await.unwrap(); + }); + + let url = Url::parse(&format!("http://{addr}")).unwrap(); + (url, state) +} + +// --------------------------------------------------------------------------- +// Mock inner RPC service +// --------------------------------------------------------------------------- + +/// A recorded call to the mock RPC service. +#[derive(Clone, Debug)] +struct RecordedCall { + method: String, + /// For estimate_fee, how many transactions were in the params. + tx_count: Option, +} + +#[derive(Clone)] +struct MockRpcService { + /// Records all calls. + calls: Arc>>, + /// Pre-configured response JSON per method name. + responses: Arc>>, +} + +impl MockRpcService { + fn new(responses: HashMap>) -> Self { + Self { calls: Arc::new(Mutex::new(Vec::new())), responses: Arc::new(responses) } + } + + fn recorded_calls(&self) -> Vec { + self.calls.lock().unwrap().clone() + } +} + +impl RpcServiceT for MockRpcService { + type MethodResponse = MethodResponse; + type BatchResponse = MethodResponse; + type NotificationResponse = MethodResponse; + + fn call<'a>( + &self, + request: Request<'a>, + ) -> impl Future + Send + 'a { + let method = request.method_name().to_owned(); + + // Try to count transactions if this is an estimate_fee request. + let params = request.params(); + let tx_count = if method == "starknet_estimateFee" { + // Parse the first param (array of txs) from the sequence params. + let mut seq = params.sequence(); + let txs: Result, _> = seq.next(); + txs.ok().map(|v| v.len()) + } else { + None + }; + + self.calls.lock().unwrap().push(RecordedCall { method: method.clone(), tx_count }); + + let response = if let Some(resp) = self.responses.get(&method) { + MethodResponse::response( + request.id().clone(), + jsonrpsee::ResponsePayload::success(resp.clone()), + usize::MAX, + ) + } else { + MethodResponse::response( + request.id().clone(), + jsonrpsee::ResponsePayload::success(serde_json::Value::Null), + usize::MAX, + ) + }; + + std::future::ready(response) + } + + fn batch<'a>( + &self, + _requests: Batch<'a>, + ) -> impl Future + Send + 'a { + std::future::ready(MethodResponse::response( + jsonrpsee::types::Id::Null, + jsonrpsee::ResponsePayload::success(serde_json::Value::Null), + usize::MAX, + )) + } + + fn notification<'a>( + &self, + _n: Notification<'a>, + ) -> impl Future + Send + 'a { + std::future::ready(MethodResponse::response( + jsonrpsee::types::Id::Null, + jsonrpsee::ResponsePayload::success(serde_json::Value::Null), + usize::MAX, + )) + } +} + +/// An undeployed address that the mock API will recognize as a Controller. +const CONTROLLER_ADDRESS: &str = "0xdead"; +/// An undeployed address that the mock API will NOT recognize as a Controller. +const NON_CONTROLLER_ADDRESS: &str = "0xbeef"; +/// The deployer address — matches the genesis account at 0x1 in test_provider. +const DEPLOYER_ADDRESS: &str = "0x1"; + +/// Builds a `serde_json::Value` response for the Cartridge API that represents +/// a valid Controller account with some dummy constructor calldata. +fn controller_calldata_response(address: &str) -> serde_json::Value { + json!({ + "address": address, + "username": "testuser", + "calldata": [ + "0x24a9edbfa7082accfceabf6a92d7160086f346d622f28741bf1c651c412c9ab", + "0x7465737475736572", + "0x0", + "0x2", + "0x1", + "0x2" + ] + }) +} + +/// Creates a valid V3 invoke transaction JSON for the given sender address. +fn make_invoke_tx_json(sender_address: &str) -> serde_json::Value { + json!({ + "type": "INVOKE", + "version": "0x3", + "sender_address": sender_address, + "calldata": ["0x1"], + "signature": ["0x0"], + "nonce": "0x0", + "resource_bounds": { + "l1_gas": { "max_amount": "0x0", "max_price_per_unit": "0x0" }, + "l2_gas": { "max_amount": "0x0", "max_price_per_unit": "0x0" }, + "l1_data_gas": { "max_amount": "0x0", "max_price_per_unit": "0x0" } + }, + "tip": "0x0", + "paymaster_data": [], + "account_deployment_data": [], + "nonce_data_availability_mode": "L1", + "fee_data_availability_mode": "L1" + }) +} + +/// Creates a JSON-RPC 2.0 request string and constructs the corresponding `Request<'_>`. +fn make_rpc_request_str(method: &str, params: &serde_json::Value) -> String { + json!({ + "jsonrpc": "2.0", + "id": 1, + "method": method, + "params": params + }) + .to_string() +} + +/// A complete test setup context. +struct TestSetup { + service: as Layer>::Service, + mock_rpc: MockRpcService, + mock_api_state: MockCartridgeApiState, + pool: TestPool, +} + +async fn setup_test( + cartridge_api_responses: HashMap, + inner_rpc_responses: HashMap>, +) -> TestSetup { + let (mock_url, mock_api_state) = start_mock_cartridge_api(cartridge_api_responses).await; + + let chain_spec = Arc::new(ChainSpec::dev()); + let pool = Pool::new(NoopValidator::new(), FiFo::new()); + let task_spawner = TaskManager::current().task_spawner(); + let gas_oracle = GasPriceOracle::create_for_testing(); + let storage = test_provider(); + + let config = StarknetApiConfig { + max_event_page_size: None, + max_proof_keys: None, + max_call_gas: None, + max_concurrent_estimate_fee_requests: None, + simulation_flags: ExecutionFlags::new().with_fee(false).with_account_validation(false), + versioned_constant_overrides: None, + }; + + let starknet_api = StarknetApi::new( + chain_spec, + pool.clone(), + task_spawner, + NoPendingBlockProvider, + gas_oracle, + config, + storage, + ); + + let cartridge_api = ::cartridge::CartridgeApiClient::new(mock_url); + + let deployer_address = Felt::from(1u64).into(); + let deployer_private_key = SigningKey::from_secret_scalar(Felt::from(1u64)); + + let layer = ControllerDeploymentLayer::new( + starknet_api, + cartridge_api, + deployer_address, + deployer_private_key, + ); + + let mock_rpc = MockRpcService::new(inner_rpc_responses); + let service = layer.layer(mock_rpc.clone()); + + TestSetup { service, mock_rpc, mock_api_state, pool } +} + +fn make_execute_outside_params(address: &str) -> serde_json::Value { + json!([ + address, + { + "caller": "0x414e595f43414c4c4552", + "nonce": "0x1", + "execute_after": "0x0", + "execute_before": "0xffffffffffffffff", + "calls": [{ + "to": "0x1", + "selector": "0x2", + "calldata": ["0x3"] + }] + }, + ["0x0", "0x0"], + null + ]) +} diff --git a/crates/rpc/rpc-server/tests/txpool.rs b/crates/rpc/rpc-server/tests/txpool.rs index de4f17093..2aa41b773 100644 --- a/crates/rpc/rpc-server/tests/txpool.rs +++ b/crates/rpc/rpc-server/tests/txpool.rs @@ -1,7 +1,7 @@ +use katana_pool::api::{PoolTransaction, TransactionPool}; use katana_pool::ordering::FiFo; use katana_pool::pool::Pool; use katana_pool::validation::NoopValidator; -use katana_pool::{PoolTransaction, TransactionPool}; use katana_primitives::contract::{ContractAddress, Nonce}; use katana_primitives::transaction::TxHash; use katana_primitives::Felt; diff --git a/crates/rpc/rpc-types/src/lib.rs b/crates/rpc/rpc-types/src/lib.rs index 80e9a6334..899a8d2d6 100644 --- a/crates/rpc/rpc-types/src/lib.rs +++ b/crates/rpc/rpc-types/src/lib.rs @@ -43,7 +43,7 @@ pub type BlockIdOrTag = katana_primitives::block::BlockIdOrTag; pub type ConfirmedBlockIdOrTag = katana_primitives::block::ConfirmedBlockIdOrTag; /// Request type for `starknet_call` RPC method. -pub type FunctionCall = katana_primitives::execution::FunctionCall; +pub type FunctionCall = katana_primitives::execution::Call; /// Finality status of a block or transaction. pub type FinalityStatus = katana_primitives::block::FinalityStatus; diff --git a/crates/rpc/rpc-types/src/outside_execution.rs b/crates/rpc/rpc-types/src/outside_execution.rs index b75b7d3c6..ff2113db3 100644 --- a/crates/rpc/rpc-types/src/outside_execution.rs +++ b/crates/rpc/rpc-types/src/outside_execution.rs @@ -14,20 +14,11 @@ use cainome::cairo_serde::{deserialize_from_hex, serialize_as_hex}; use cainome::cairo_serde_derive::CairoSerde; -use katana_primitives::execution::EntryPointSelector; +use cainome_cairo_serde::CairoSerde; +use katana_primitives::execution::Call; use katana_primitives::{ContractAddress, Felt}; use serde::{Deserialize, Serialize}; - -/// A single call to be executed as part of an outside execution. -#[derive(Clone, CairoSerde, Serialize, Deserialize, PartialEq, Debug)] -pub struct Call { - /// Contract address to call. - pub to: ContractAddress, - /// Function selector to invoke. - pub selector: EntryPointSelector, - /// Arguments to pass to the function. - pub calldata: Vec, -} +use starknet::macros::selector; /// Nonce channel #[derive(Clone, CairoSerde, PartialEq, Debug, Serialize, Deserialize)] @@ -50,6 +41,7 @@ pub struct OutsideExecutionV2 { #[serde(serialize_with = "serialize_as_hex", deserialize_with = "deserialize_from_hex")] pub execute_before: u64, /// Calls to execute in order. + #[serde(with = "calls_serde")] pub calls: Vec, } @@ -67,6 +59,7 @@ pub struct OutsideExecutionV3 { #[serde(serialize_with = "serialize_as_hex", deserialize_with = "deserialize_from_hex")] pub execute_before: u64, /// Calls to execute in order. + #[serde(with = "calls_serde")] pub calls: Vec, } @@ -86,6 +79,90 @@ impl OutsideExecution { OutsideExecution::V3(v3) => v3.caller, } } + + pub fn calls(&self) -> &[Call] { + match self { + Self::V2(v) => &v.calls, + Self::V3(v) => &v.calls, + } + } + + pub fn as_felts(&self) -> Vec { + match self { + Self::V2(v) => OutsideExecutionV2::cairo_serialize(v), + Self::V3(v) => OutsideExecutionV3::cairo_serialize(v), + } + } + + /// Returns the number of calls in the outside execution. + pub fn len(&self) -> usize { + match self { + Self::V2(v) => v.calls.len(), + Self::V3(v) => v.calls.len(), + } + } + + pub fn is_empty(&self) -> bool { + match self { + Self::V2(v) => v.calls.is_empty(), + Self::V3(v) => v.calls.is_empty(), + } + } + + pub fn selector(&self) -> Felt { + match self { + Self::V2(_) => selector!("execute_from_outside_v2"), + Self::V3(_) => selector!("execute_from_outside_v3"), + } + } +} + +mod calls_serde { + use katana_primitives::execution::Call; + use katana_primitives::{ContractAddress, Felt}; + use serde::{Deserialize, Deserializer, Serialize, Serializer}; + + #[derive(Serialize)] + struct CallRef<'a> { + #[serde(rename = "to")] + contract_address: &'a ContractAddress, + #[serde(rename = "selector")] + entry_point_selector: &'a Felt, + calldata: &'a Vec, + } + + #[derive(Deserialize)] + struct CallDe { + #[serde(rename = "to")] + contract_address: ContractAddress, + #[serde(rename = "selector")] + entry_point_selector: Felt, + calldata: Vec, + } + + pub fn serialize(calls: &[Call], serializer: S) -> Result { + let refs: Vec> = calls + .iter() + .map(|c| CallRef { + contract_address: &c.contract_address, + entry_point_selector: &c.entry_point_selector, + calldata: &c.calldata, + }) + .collect(); + refs.serialize(serializer) + } + + pub fn deserialize<'de, D: Deserializer<'de>>(deserializer: D) -> Result, D::Error> { + let items = Vec::::deserialize(deserializer)?; + Ok(items + .into_iter() + .map(|c| Call { + contract_address: c.contract_address, + entry_point_selector: c.entry_point_selector, + calldata: c.calldata, + }) + .collect()) + } } #[cfg(test)] @@ -105,10 +182,10 @@ mod tests { execute_before: 3000000000, calls: vec![ Call { - to: address!( + contract_address: address!( "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ), - selector: selector!("approve"), + entry_point_selector: selector!("approve"), calldata: vec![ felt!("0x50302d9f4df7a96567423f64f1271ef07537469d8e8c4dd2409cf3cc4274de4"), felt!("0x11c37937e08000"), @@ -116,10 +193,10 @@ mod tests { ], }, Call { - to: address!( + contract_address: address!( "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ), - selector: selector!("transfer"), + entry_point_selector: selector!("transfer"), calldata: vec![ felt!("0x50302d9f4df7a96567423f64f1271ef07537469d8e8c4dd2409cf3cc4274de4"), felt!("0x11c37937e08000"), @@ -170,10 +247,10 @@ mod tests { execute_before: 3000000000, calls: vec![ Call { - to: address!( + contract_address: address!( "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ), - selector: selector!("approve"), + entry_point_selector: selector!("approve"), calldata: vec![ felt!("0x50302d9f4df7a96567423f64f1271ef07537469d8e8c4dd2409cf3cc4274de4"), felt!("0x11c37937e08000"), @@ -181,10 +258,10 @@ mod tests { ], }, Call { - to: address!( + contract_address: address!( "0x49d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" ), - selector: selector!("transfer"), + entry_point_selector: selector!("transfer"), calldata: vec![ felt!("0x50302d9f4df7a96567423f64f1271ef07537469d8e8c4dd2409cf3cc4274de4"), felt!("0x11c37937e08000"), diff --git a/crates/sync/stage/src/sequencing.rs b/crates/sync/stage/src/sequencing.rs index d3a8b8b6d..73f5b2f2a 100644 --- a/crates/sync/stage/src/sequencing.rs +++ b/crates/sync/stage/src/sequencing.rs @@ -8,7 +8,8 @@ use katana_core::backend::Backend; use katana_core::service::block_producer::{BlockProducer, BlockProductionError}; use katana_core::service::{BlockProductionTask, TransactionMiner}; use katana_messaging::{MessagingConfig, MessagingService, MessagingTask}; -use katana_pool::{TransactionPool, TxPool}; +use katana_pool::api::TransactionPool; +use katana_pool::TxPool; use katana_provider::{ProviderFactory, ProviderRO, ProviderRW}; use katana_tasks::{JoinHandle, TaskSpawner}; use tracing::error; diff --git a/crates/utils/src/node.rs b/crates/utils/src/node.rs index 3900a55be..032049c0a 100644 --- a/crates/utils/src/node.rs +++ b/crates/utils/src/node.rs @@ -48,7 +48,7 @@ pub type ForkTestNode = TestNode; #[derive(Debug)] pub struct TestNode

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, { @@ -148,7 +148,7 @@ impl ForkTestNode { impl

TestNode

where - P: ProviderFactory, + P: ProviderFactory + Clone,

::Provider: ProviderRO,

::ProviderMut: ProviderRW, {