diff --git a/.github/workflows/cargo-udeps.yml b/.github/workflows/cargo-udeps.yml new file mode 100644 index 0000000000..1444fefba1 --- /dev/null +++ b/.github/workflows/cargo-udeps.yml @@ -0,0 +1,41 @@ +name: Unused dependencies + +on: + schedule: + # Triggers the workflow every Sunday + - cron: "0 18 * * 0" + +env: + CARGO_TERM_COLOR: always + +jobs: + cargo-udeps: + runs-on: ubuntu-latest + permissions: + contents: read + issues: write + steps: + - uses: actions/checkout@v3 + + - uses: dtolnay/rust-toolchain@master + with: + # cargo-udeps require nightly to run + toolchain: nightly + + - name: Install cargo-udeps + run: cargo install --locked cargo-udeps + + - name: Check for unused dependencies + run: cargo udeps --workspace --all-targets --all-features + + - name: Create github issue for failed action + uses: imjohnbo/issue-bot@v3 + if: ${{ failure() }} + with: + labels: "bug" + title: "ci: Github Action for `cargo-udeps` failed" + body: | + `cargo-udeps` failed: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} + Reference: ${{ github.ref_type }} ${{ github.ref }} (commit ${{ github.sha }}). + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d04e934abe..1bcca9cd55 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -23,7 +23,7 @@ jobs: with: repo-token: ${{ secrets.GITHUB_TOKEN }} - run: | - cargo test + cargo test --all-features ensure-wasm: runs-on: ubuntu-latest @@ -116,36 +116,6 @@ jobs: repo-token: ${{ secrets.GITHUB_TOKEN }} - run: scripts/rust_fmt.sh --check - # Check for unnecessary dependencies. - # udeps: - # runs-on: ubuntu-latest - # steps: - # - uses: actions/checkout@v3 - - # - uses: dtolnay/rust-toolchain@master - # with: - # toolchain: ${{ env.RUST_VERSION }} - - # - uses: Swatinem/rust-cache@v2 - # - uses: arduino/setup-protoc@v1 - # with: - # repo-token: ${{ secrets.GITHUB_TOKEN }} - # - id: cache-cargo-udeps - # uses: actions/cache@v2 - # with: - # path: ~/.cargo-udeps-cache - # key: cargo-udeps-v0.1.40-${{ runner.os }} - # - name: "Download and run cargo-udeps" - # run: | - # if [ ! -f ~/.cargo-udeps-cache/cargo-udeps ]; then - # wget -O - -c https://github.com/est31/cargo-udeps/releases/download/v0.1.40/cargo-udeps-v0.1.40-x86_64-unknown-linux-gnu.tar.gz | tar -xz - # mkdir -p ~/.cargo-udeps-cache - # mv cargo-udeps-*/cargo-udeps ~/.cargo-udeps-cache/cargo-udeps - # fi - # ~/.cargo-udeps-cache/cargo-udeps udeps --all-targets - # env: - # RUSTUP_TOOLCHAIN: ${{ env.RUST_VERSION }} - docs: runs-on: ubuntu-latest steps: diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000000..2ec82617d0 --- /dev/null +++ b/.gitmodules @@ -0,0 +1,3 @@ +[submodule "crates/katana/core/contracts/messaging/solidity/lib/forge-std"] + path = crates/katana/core/contracts/messaging/solidity/lib/forge-std + url = https://github.com/foundry-rs/forge-std diff --git a/Cargo.lock b/Cargo.lock index 0011618dc3..2311bed3e4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -108,9 +108,9 @@ dependencies = [ [[package]] name = "anstream" -version = "0.5.0" +version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b1f58811cfac344940f1a400b6e6231ce35171f614f26439e80f8c1465c5cc0c" +checksum = "2ab91ebe16eb252986481c5b62f6098f3b698a45e34b5b98200cf20dd2484a44" dependencies = [ "anstyle", "anstyle-parse", @@ -122,15 +122,15 @@ dependencies = [ [[package]] name = "anstyle" -version = "1.0.3" +version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b84bf0a05bbb2a83e5eb6fa36bb6e87baa08193c35ff52bbf6b38d8af2890e46" +checksum = "7079075b41f533b8c61d2a4d073c4676e1f8b249ff94a393b0595db304e0dd87" [[package]] name = "anstyle-parse" -version = "0.2.1" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "938874ff5980b03a87c5524b3ae5b59cf99b1d6bc836848df7bc5ada9643c333" +checksum = "317b9a89c1868f5ea6ff1d9539a69f45dffc21ce321ac1fd1160dfa48c8e2140" dependencies = [ "utf8parse", ] @@ -146,9 +146,9 @@ dependencies = [ [[package]] name = "anstyle-wincon" -version = "2.1.0" +version = "3.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "58f54d10c6dfa51283a066ceab3ec1ab78d13fae00aa49243a45e4571fb79dfd" +checksum = "f0699d10d2f4d628a98ee7b57b289abbc98ff3bad977cb3152709d4bf2330628" dependencies = [ "anstyle", "windows-sys 0.48.0", @@ -348,8 +348,8 @@ dependencies = [ "memchr", "pin-project-lite", "tokio", - "zstd", - "zstd-safe", + "zstd 0.12.4", + "zstd-safe 6.0.6", ] [[package]] @@ -471,6 +471,17 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "async_io_stream" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b6d7b9decdf35d8908a7e3ef02f64c5e9b1695e230154c0e8de3969142d9b94c" +dependencies = [ + "futures", + "pharos", + "rustc_version", +] + [[package]] name = "atoi" version = "1.0.0" @@ -558,6 +569,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -570,6 +587,18 @@ version = "0.21.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ba43ea6f343b788c8764558649e08df62f86c6ef251fdaeb1ffd010a9ae50a2" +[[package]] +name = "base64ct" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8c3c1a368f70d6cf7302d78f8f7093da241fb8e8807c05cc9e51a125895a6d5b" + +[[package]] +name = "bech32" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d86b93f97252c47b41663388e6d155714a9d0c398b99f1005cbc5f978b29f445" + [[package]] name = "beef" version = "0.5.2" @@ -718,6 +747,16 @@ dependencies = [ "alloc-stdlib", ] +[[package]] +name = "bs58" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5353f36341f7451062466f0b755b96ac3a9547e4d7f6b70d603fc721a7d7896" +dependencies = [ + "sha2", + "tinyvec", +] + [[package]] name = "bstr" version = "1.6.2" @@ -771,6 +810,27 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a3e368af43e418a04d52505cf3dbc23dda4e3407ae2fa99fd0e4f308ce546acc" +[[package]] +name = "bzip2" +version = "0.4.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bdb116a6ef3f6c3698828873ad02c3014b3c85cadb88496095628e3ef1e347f8" +dependencies = [ + "bzip2-sys", + "libc", +] + +[[package]] +name = "bzip2-sys" +version = "0.1.11+1.0.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "736a955f3fa7875102d57c82b8cac37ec45224a07fd32d58f9f7a186b6cd4cdc" +dependencies = [ + "cc", + "libc", + "pkg-config", +] + [[package]] name = "cairo-felt" version = "0.8.2" @@ -847,7 +907,7 @@ dependencies = [ "cairo-lang-parser", "cairo-lang-syntax", "cairo-lang-utils", - "indexmap 2.0.1", + "indexmap 2.0.2", "itertools 0.11.0", "salsa", "smol_str", @@ -874,7 +934,7 @@ checksum = "c35dddbc63b2a4870891cc74498726aa32bfaa518596352f9bb101411cc4c584" dependencies = [ "cairo-lang-utils", "good_lp", - "indexmap 2.0.1", + "indexmap 2.0.2", "itertools 0.11.0", ] @@ -962,7 +1022,7 @@ dependencies = [ "cairo-lang-syntax", "cairo-lang-utils", "id-arena", - "indexmap 2.0.1", + "indexmap 2.0.2", "itertools 0.11.0", "log", "num-bigint", @@ -1170,7 +1230,7 @@ dependencies = [ "cairo-lang-syntax", "cairo-lang-utils", "id-arena", - "indexmap 2.0.1", + "indexmap 2.0.2", "itertools 0.11.0", "num-bigint", "once_cell", @@ -1332,7 +1392,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f974b6e859f0b09c0f13ec8188c96e9e8bbb5da04214f911dbb5bcda67cb812b" dependencies = [ "env_logger", - "indexmap 2.0.1", + "indexmap 2.0.2", "itertools 0.11.0", "log", "num-bigint", @@ -1354,7 +1414,7 @@ dependencies = [ "bitvec", "cairo-felt", "generic-array", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "hex", "keccak", "lazy_static", @@ -1448,9 +1508,9 @@ dependencies = [ [[package]] name = "clap" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "824956d0dca8334758a5b7f7e50518d66ea319330cbceedcf76905c2f6ab30e3" +checksum = "d04704f56c2cde07f43e8e2c154b43f216dc5c92fc98ada720177362f953b956" dependencies = [ "clap_builder", "clap_derive", @@ -1468,9 +1528,9 @@ dependencies = [ [[package]] name = "clap_builder" -version = "4.4.5" +version = "4.4.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "122ec64120a49b4563ccaedcbea7818d069ed8e9aa6d829b82d8a4128936b2ab" +checksum = "0e231faeaca65ebd1ea3c737966bf858971cd38c3849107aa3ea7de90a804e45" dependencies = [ "anstream", "anstyle", @@ -1480,9 +1540,9 @@ dependencies = [ [[package]] name = "clap_complete" -version = "4.4.2" +version = "4.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8baeccdb91cd69189985f87f3c7e453a3a451ab5746cf3be6acc92120bd16d24" +checksum = "e3ae8ba90b9d8b007efe66e55e48fb936272f5ca00349b5b0e89877520d35ea7" dependencies = [ "clap", ] @@ -1511,6 +1571,58 @@ version = "0.6.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b8191fa7302e03607ff0e237d4246cc043ff5b3cb9409d995172ba3bea16b807" +[[package]] +name = "coins-bip32" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3b6be4a5df2098cd811f3194f64ddb96c267606bffd9689ac7b0160097b01ad3" +dependencies = [ + "bs58", + "coins-core", + "digest 0.10.7", + "hmac", + "k256", + "serde", + "sha2", + "thiserror", +] + +[[package]] +name = "coins-bip39" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3db8fba409ce3dc04f7d804074039eb68b960b0829161f8e06c95fea3f122528" +dependencies = [ + "bitvec", + "coins-bip32", + "hmac", + "once_cell", + "pbkdf2 0.12.2", + "rand", + "sha2", + "thiserror", +] + +[[package]] +name = "coins-core" +version = "0.8.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5286a0843c21f8367f7be734f89df9b822e0321d8bcce8d6e735aadff7d74979" +dependencies = [ + "base64 0.21.4", + "bech32", + "bs58", + "digest 0.10.7", + "generic-array", + "hex", + "ripemd", + "serde", + "serde_derive", + "sha2", + "sha3", + "thiserror", +] + [[package]] name = "colorchoice" version = "1.0.0" @@ -1547,6 +1659,30 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32b13ea120a812beba79e34316b3942a857c86ec1593cb34f27bb28272ce2cca" +[[package]] +name = "const-hex" +version = "1.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa72a10d0e914cad6bcad4e7409e68d230c1c2db67896e19a37f758b1fcbdab5" +dependencies = [ + "cfg-if", + "cpufeatures", + "hex", + "serde", +] + +[[package]] +name = "const-oid" +version = "0.9.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "28c122c3980598d243d63d9a704629a2d748d101f278052ff068be5a4423ab6f" + +[[package]] +name = "constant_time_eq" +version = "0.1.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc" + [[package]] name = "convert_case" version = "0.4.0" @@ -1703,6 +1839,7 @@ checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" dependencies = [ "generic-array", "rand_core", + "serdect", "subtle", "zeroize", ] @@ -1823,7 +1960,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856" dependencies = [ "cfg-if", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "lock_api", "once_cell", "parking_lot_core 0.9.8", @@ -1851,6 +1988,16 @@ dependencies = [ "tokio-util", ] +[[package]] +name = "der" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fffa369a668c8af7dbf8b5e56c9f744fbd399949ed171606040001947de40b1c" +dependencies = [ + "const-oid", + "zeroize", +] + [[package]] name = "deranged" version = "0.3.8" @@ -1952,6 +2099,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" dependencies = [ "block-buffer 0.10.4", + "const-oid", "crypto-common", "subtle", ] @@ -1965,6 +2113,15 @@ dependencies = [ "dirs-sys", ] +[[package]] +name = "dirs" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44c45a9d03d6676652bcb5e724c7e988de1acad23a711b5217ab9cbecbec2225" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs-next" version = "2.0.0" @@ -2115,7 +2272,7 @@ dependencies = [ name = "dojo-types" version = "0.2.1" dependencies = [ - "ethabi", + "crypto-bigint", "hex", "itertools 0.10.5", "serde", @@ -2175,6 +2332,20 @@ version = "1.0.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "23d2f3407d9a573d666de4b5bdf10569d73ca9478087346697dcbae6244bfbcd" +[[package]] +name = "ecdsa" +version = "0.16.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4b1e0c257a9e9f25f90ff76d7a68360ed497ee519c8e428d1825ef0000799d4" +dependencies = [ + "der", + "digest 0.10.7", + "elliptic-curve", + "rfc6979", + "signature", + "spki", +] + [[package]] name = "either" version = "1.9.0" @@ -2184,6 +2355,25 @@ dependencies = [ "serde", ] +[[package]] +name = "elliptic-curve" +version = "0.13.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "968405c8fdc9b3bf4df0a6638858cc0b52462836ab6b1c87377785dd09cf1c0b" +dependencies = [ + "base16ct", + "crypto-bigint", + "digest 0.10.7", + "ff", + "generic-array", + "group", + "pkcs8", + "rand_core", + "sec1", + "subtle", + "zeroize", +] + [[package]] name = "ena" version = "0.14.2" @@ -2208,6 +2398,24 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enr" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe81b5c06ecfdbc71dd845216f225f53b62a10cb8a16c946836a3467f701d05b" +dependencies = [ + "base64 0.21.4", + "bytes", + "hex", + "k256", + "log", + "rand", + "rlp", + "serde", + "sha3", + "zeroize", +] + [[package]] name = "env_logger" version = "0.10.0" @@ -2259,7 +2467,7 @@ dependencies = [ "digest 0.10.7", "hex", "hmac", - "pbkdf2", + "pbkdf2 0.11.0", "rand", "scrypt", "serde", @@ -2278,7 +2486,10 @@ checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" dependencies = [ "ethereum-types", "hex", + "once_cell", + "regex", "serde", + "serde_json", "sha3", "thiserror", "uint", @@ -2292,8 +2503,10 @@ checksum = "c22d4b5885b6aa2fe5e8b9329fb8d232bf739e434e6b87347c63bdd00c120f60" dependencies = [ "crunchy", "fixed-hash", + "impl-codec", "impl-rlp", "impl-serde", + "scale-info", "tiny-keccak", ] @@ -2305,18 +2518,277 @@ checksum = "02d215cbf040552efcbe99a38372fe80ab9d00268e20012b79fcd0f073edd8ee" dependencies = [ "ethbloom", "fixed-hash", + "impl-codec", "impl-rlp", "impl-serde", "primitive-types", + "scale-info", "uint", ] +[[package]] +name = "ethers" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ad13497f6e0a24292fc7b408e30d22fe9dc262da1f40d7b542c3a44e7fc0476" +dependencies = [ + "ethers-addressbook", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-middleware", + "ethers-providers", + "ethers-signers", + "ethers-solc", +] + +[[package]] +name = "ethers-addressbook" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6e9e8acd0ed348403cc73a670c24daba3226c40b98dc1a41903766b3ab6240a" +dependencies = [ + "ethers-core", + "once_cell", + "serde", + "serde_json", +] + +[[package]] +name = "ethers-contract" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d79269278125006bb0552349c03593ffa9702112ca88bc7046cc669f148fb47c" +dependencies = [ + "const-hex", + "ethers-contract-abigen", + "ethers-contract-derive", + "ethers-core", + "ethers-providers", + "futures-util", + "once_cell", + "pin-project", + "serde", + "serde_json", + "thiserror", +] + +[[package]] +name = "ethers-contract-abigen" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce95a43c939b2e4e2f3191c5ad4a1f279780b8a39139c9905b43a7433531e2ab" +dependencies = [ + "Inflector", + "const-hex", + "dunce", + "ethers-core", + "ethers-etherscan", + "eyre", + "prettyplease 0.2.15", + "proc-macro2", + "quote", + "regex", + "reqwest", + "serde", + "serde_json", + "syn 2.0.37", + "toml", + "walkdir", +] + +[[package]] +name = "ethers-contract-derive" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e9ce44906fc871b3ee8c69a695ca7ec7f70e50cb379c9b9cb5e532269e492f6" +dependencies = [ + "Inflector", + "const-hex", + "ethers-contract-abigen", + "ethers-core", + "proc-macro2", + "quote", + "serde_json", + "syn 2.0.37", +] + +[[package]] +name = "ethers-core" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c0a17f0708692024db9956b31d7a20163607d2745953f5ae8125ab368ba280ad" +dependencies = [ + "arrayvec", + "bytes", + "cargo_metadata", + "chrono", + "const-hex", + "elliptic-curve", + "ethabi", + "generic-array", + "k256", + "num_enum", + "once_cell", + "open-fastrlp", + "rand", + "rlp", + "serde", + "serde_json", + "strum 0.25.0", + "syn 2.0.37", + "tempfile", + "thiserror", + "tiny-keccak", + "unicode-xid", +] + +[[package]] +name = "ethers-etherscan" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0e53451ea4a8128fbce33966da71132cf9e1040dcfd2a2084fd7733ada7b2045" +dependencies = [ + "ethers-core", + "reqwest", + "semver", + "serde", + "serde_json", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-middleware" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "473f1ccd0c793871bbc248729fa8df7e6d2981d6226e4343e3bbaa9281074d5d" +dependencies = [ + "async-trait", + "auto_impl", + "ethers-contract", + "ethers-core", + "ethers-etherscan", + "ethers-providers", + "ethers-signers", + "futures-channel", + "futures-locks", + "futures-util", + "instant", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tracing", + "tracing-futures", + "url", +] + +[[package]] +name = "ethers-providers" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6838fa110e57d572336178b7c79e94ff88ef976306852d8cb87d9e5b1fc7c0b5" +dependencies = [ + "async-trait", + "auto_impl", + "base64 0.21.4", + "bytes", + "const-hex", + "enr", + "ethers-core", + "futures-core", + "futures-timer", + "futures-util", + "hashers", + "http", + "instant", + "jsonwebtoken", + "once_cell", + "pin-project", + "reqwest", + "serde", + "serde_json", + "thiserror", + "tokio", + "tokio-tungstenite", + "tracing", + "tracing-futures", + "url", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", + "ws_stream_wasm", +] + +[[package]] +name = "ethers-signers" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5ea44bec930f12292866166f9ddbea6aa76304850e4d8dcd66dc492b43d00ff1" +dependencies = [ + "async-trait", + "coins-bip32", + "coins-bip39", + "const-hex", + "elliptic-curve", + "eth-keystore", + "ethers-core", + "rand", + "sha2", + "thiserror", + "tracing", +] + +[[package]] +name = "ethers-solc" +version = "2.0.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "de34e484e7ae3cab99fbfd013d6c5dc7f9013676a4e0e414d8b12e1213e8b3ba" +dependencies = [ + "cfg-if", + "const-hex", + "dirs", + "dunce", + "ethers-core", + "glob", + "home", + "md-5", + "num_cpus", + "once_cell", + "path-slash", + "rayon 1.8.0", + "regex", + "semver", + "serde", + "serde_json", + "solang-parser", + "svm-rs", + "thiserror", + "tiny-keccak", + "tokio", + "tracing", + "walkdir", + "yansi", +] + [[package]] name = "event-listener" version = "2.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0206175f82b8d6bf6652ff7d71a1e27fd2e4efde587fd368662814d6ec1d9ce0" +[[package]] +name = "eyre" +version = "0.6.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c2b6b5a29c02cdc822728b7d7b8ae1bab3e3b05d44522770ddd49722eeac7eb" +dependencies = [ + "indenter", + "once_cell", +] + [[package]] name = "fast_chemail" version = "0.9.6" @@ -2341,6 +2813,16 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "ff" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ded41244b729663b1e574f1b4fb731469f69f79c17667b5d776b16cda0479449" +dependencies = [ + "rand_core", + "subtle", +] + [[package]] name = "filetime" version = "0.2.22" @@ -2414,6 +2896,16 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "fs2" +version = "0.4.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9564fc758e15025b46aa6643b1b77d047d1a56a1aea6e01002ac0c7026876213" +dependencies = [ + "libc", + "winapi", +] + [[package]] name = "fs4" version = "0.6.6" @@ -2498,6 +2990,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4fff74096e71ed47f8e023204cfd0aa1289cd54ae5430a9523be060cdb849964" +[[package]] +name = "futures-locks" +version = "0.7.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "45ec6fe3675af967e67c5536c0b9d44e34e6c52f86bedc4ea49c5317b8e94d06" +dependencies = [ + "futures-channel", + "futures-task", +] + [[package]] name = "futures-macro" version = "0.3.28" @@ -2521,6 +3023,16 @@ version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "76d3d132be6c0e6aa1534069c705a74a5997a356c0dc2f86a47765e5617c5b65" +[[package]] +name = "futures-timer" +version = "3.0.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e64b03909df88034c26dc1547e8970b91f98bdb65165d6a4e9110d94263dbb2c" +dependencies = [ + "gloo-timers", + "send_wrapper 0.4.0", +] + [[package]] name = "futures-util" version = "0.3.28" @@ -2539,6 +3051,15 @@ dependencies = [ "slab", ] +[[package]] +name = "fxhash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c31b6d751ae2c7f11320402d34e41349dd1016f8d5d45e48c4312bc8625af50c" +dependencies = [ + "byteorder", +] + [[package]] name = "genco" version = "0.17.6" @@ -2569,6 +3090,7 @@ checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" dependencies = [ "typenum", "version_check", + "zeroize", ] [[package]] @@ -2897,7 +3419,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "385f4ce6ecf3692d313ca3aa9bd3b3d8490de53368d6d94bedff3af8b6d9c58d" dependencies = [ "gix-hash", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "parking_lot 0.12.1", ] @@ -3302,6 +3824,18 @@ dependencies = [ "walkdir", ] +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + [[package]] name = "good_lp" version = "1.6.1" @@ -3312,6 +3846,17 @@ dependencies = [ "minilp", ] +[[package]] +name = "group" +version = "0.13.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" +dependencies = [ + "ff", + "rand_core", + "subtle", +] + [[package]] name = "h2" version = "0.3.21" @@ -3365,22 +3910,31 @@ dependencies = [ [[package]] name = "hashbrown" -version = "0.14.0" +version = "0.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a" +checksum = "7dfda62a12f55daeae5015f81b0baea145391cb4520f86c248fc615d72640d12" dependencies = [ "ahash 0.8.3", "allocator-api2", "serde", ] +[[package]] +name = "hashers" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2bca93b15ea5a746f220e56587f71e73c6165eab783df9e26590069953e3c30" +dependencies = [ + "fxhash", +] + [[package]] name = "hashlink" version = "0.8.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7" dependencies = [ - "hashbrown 0.14.0", + "hashbrown 0.14.1", ] [[package]] @@ -3702,6 +4256,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9f1a0777d972970f204fdf8ef319f1f4f8459131636d7e3c96c5d59570d0fa6" +[[package]] +name = "indenter" +version = "0.3.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce23b50ad8242c51a442f3ff322d56b02f08852c77e4c0b4d3fd684abc89c683" + [[package]] name = "indexmap" version = "1.9.3" @@ -3715,12 +4275,12 @@ dependencies = [ [[package]] name = "indexmap" -version = "2.0.1" +version = "2.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad227c3af19d4914570ad36d30409928b75967c298feb9ea1969db3a610bb14e" +checksum = "8adf3ddd720272c6ea8bf59463c04e0f93d0bbf7c5439b691bca2987e0270897" dependencies = [ "equivalent", - "hashbrown 0.14.0", + "hashbrown 0.14.1", "serde", ] @@ -3954,6 +4514,20 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonwebtoken" +version = "8.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6971da4d9c3aa03c3d8f3ff0f4155b534aad021292003895a469716b2a230378" +dependencies = [ + "base64 0.21.4", + "pem", + "ring", + "serde", + "serde_json", + "simple_asn1", +] + [[package]] name = "jwalk" version = "0.8.1" @@ -3964,6 +4538,20 @@ dependencies = [ "rayon 1.8.0", ] +[[package]] +name = "k256" +version = "0.13.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cadb76004ed8e97623117f3df85b17aaa6626ab0b0831e6573f104df16cd1bcc" +dependencies = [ + "cfg-if", + "ecdsa", + "elliptic-curve", + "once_cell", + "sha2", + "signature", +] + [[package]] name = "katana" version = "0.2.1" @@ -3987,19 +4575,23 @@ version = "0.2.1" dependencies = [ "anyhow", "assert_matches", + "async-trait", "blockifier", "cairo-lang-casm", "cairo-lang-starknet", "cairo-vm", "convert_case 0.6.0", + "ethers", "flate2", "futures", + "hex", "lazy_static", "parking_lot 0.12.1", "rand", "serde", "serde_json", "serde_with", + "sha3", "starknet", "starknet_api", "thiserror", @@ -4226,6 +4818,16 @@ dependencies = [ "rawpointer", ] +[[package]] +name = "md-5" +version = "0.10.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d89e7ee0cfbedfc4da3340218492196241d89eefb6dab27de5df917a6d2e78cf" +dependencies = [ + "cfg-if", + "digest 0.10.7", +] + [[package]] name = "memchr" version = "2.6.3" @@ -4515,6 +5117,27 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "70bf6736f74634d299d00086f02986875b3c2d924781a6a2cb6c201e73da0ceb" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "56ea360eafe1022f7cc56cd7b869ed57330fb2453d0c7831d99b74c65d2f5597" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.37", +] + [[package]] name = "num_threads" version = "0.1.6" @@ -4563,6 +5186,31 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "624a8340c38c1b80fd549087862da4ba43e08858af025b236e509b6649fc13d5" +[[package]] +name = "open-fastrlp" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "786393f80485445794f6043fd3138854dd109cc6c4bd1a6383db304c9ce9b9ce" +dependencies = [ + "arrayvec", + "auto_impl", + "bytes", + "ethereum-types", + "open-fastrlp-derive", +] + +[[package]] +name = "open-fastrlp-derive" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "003b2be5c6c53c1cfeb0a238b8a1c3915cd410feb684457a36c10038f764bb1c" +dependencies = [ + "bytes", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "option-ext" version = "0.2.0" @@ -4668,6 +5316,17 @@ dependencies = [ "windows-targets 0.48.5", ] +[[package]] +name = "password-hash" +version = "0.4.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7676374caaee8a325c9e7a2ae557f216c5563a171d6997b0ef8a65af35147700" +dependencies = [ + "base64ct", + "rand_core", + "subtle", +] + [[package]] name = "paste" version = "1.0.14" @@ -4689,6 +5348,12 @@ dependencies = [ "once_cell", ] +[[package]] +name = "path-slash" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1e91099d4268b0e11973f036e885d652fb0b21fedcf69738c627f94db6a44f42" + [[package]] name = "pathdiff" version = "0.2.1" @@ -4705,6 +5370,28 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "83a0692ec44e4cf1ef28ca317f14f8f07da2d95ec3fa01f86e4467b725e60917" dependencies = [ "digest 0.10.7", + "hmac", + "password-hash", + "sha2", +] + +[[package]] +name = "pbkdf2" +version = "0.12.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8ed6a7761f76e3b9f92dfb0a60a6a6477c61024b775147ff0973a02653abaf2" +dependencies = [ + "digest 0.10.7", + "hmac", +] + +[[package]] +name = "pem" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8835c273a76a90455d7344889b0964598e3316e2a79ede8e36f16bdcf2228b8" +dependencies = [ + "base64 0.13.1", ] [[package]] @@ -4765,7 +5452,17 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9" dependencies = [ "fixedbitset", - "indexmap 2.0.1", + "indexmap 2.0.2", +] + +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version", ] [[package]] @@ -4878,6 +5575,16 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" +[[package]] +name = "pkcs8" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" +dependencies = [ + "der", + "spki", +] + [[package]] name = "pkg-config" version = "0.3.27" @@ -5011,6 +5718,7 @@ dependencies = [ "impl-codec", "impl-rlp", "impl-serde", + "scale-info", "uint", ] @@ -5429,6 +6137,15 @@ dependencies = [ "winapi", ] +[[package]] +name = "ripemd" +version = "0.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" +dependencies = [ + "digest 0.10.7", +] + [[package]] name = "rlp" version = "0.5.2" @@ -5436,9 +6153,21 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb919243f34364b6bd2fc10ef797edbfa75f33c252e7998527479c6d6b47e1ec" dependencies = [ "bytes", + "rlp-derive", "rustc-hex", ] +[[package]] +name = "rlp-derive" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e33d7b2abe0c340d8797fe2907d3f20d3b5ea5908683618bfe80df7f621f672a" +dependencies = [ + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "rustc-demangle" version = "0.1.23" @@ -5468,9 +6197,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.14" +version = "0.38.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "747c788e9ce8e92b12cd485c49ddf90723550b654b32508f979b71a7b1ecda4f" +checksum = "d2f9da0cbd88f9f09e7814e388301c8414c51c62aa6ce1e4b5c551d49d96e531" dependencies = [ "bitflags 2.4.0", "errno", @@ -5605,6 +6334,30 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "scale-info" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "35c0a159d0c45c12b20c5a844feb1fe4bea86e28f17b92a5f0c42193634d3782" +dependencies = [ + "cfg-if", + "derive_more", + "parity-scale-codec", + "scale-info-derive", +] + +[[package]] +name = "scale-info-derive" +version = "2.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "912e55f6d20e0e80d63733872b40e1227c0bce1e1ab81ba67d696339bfd7fd29" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 1.0.109", +] + [[package]] name = "scarb" version = "0.7.0" @@ -5758,7 +6511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9f9e24d2b632954ded8ab2ef9fea0a0c769ea56ea98bddbafbad22caeeadf45d" dependencies = [ "hmac", - "pbkdf2", + "pbkdf2 0.11.0", "salsa20", "sha2", ] @@ -5773,6 +6526,20 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sec1" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" +dependencies = [ + "base16ct", + "der", + "generic-array", + "pkcs8", + "subtle", + "zeroize", +] + [[package]] name = "semver" version = "1.0.19" @@ -5782,6 +6549,18 @@ dependencies = [ "serde", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + +[[package]] +name = "send_wrapper" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" + [[package]] name = "serde" version = "1.0.188" @@ -5905,6 +6684,16 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -5984,12 +6773,34 @@ dependencies = [ "libc", ] +[[package]] +name = "signature" +version = "2.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5e1788eed21689f9cf370582dfc467ef36ed9c707f073528ddafa8d83e3b8500" +dependencies = [ + "digest 0.10.7", + "rand_core", +] + [[package]] name = "similar" version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "420acb44afdae038210c99e69aae24109f32f15500aa708e81d46c9f29d55fcf" +[[package]] +name = "simple_asn1" +version = "0.6.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "adc4e5204eb1910f40f9cfa375f6f05b68c3abac4b6fd879c8ff5e7ae8a0a085" +dependencies = [ + "num-bigint", + "num-traits 0.2.16", + "thiserror", + "time", +] + [[package]] name = "siphasher" version = "0.3.11" @@ -6022,9 +6833,9 @@ dependencies = [ [[package]] name = "snapbox" -version = "0.4.12" +version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad90eb3a2e3a8031d636d45bd4832751aefd58a291b553f7305a2bacae21aff3" +checksum = "7b439536a42c43be148b610c7f7f968fb79a457254910a9cb20900da73cd3271" dependencies = [ "anstream", "anstyle", @@ -6035,9 +6846,9 @@ dependencies = [ [[package]] name = "snapbox-macros" -version = "0.3.5" +version = "0.3.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "95f4ffd811b87da98d0e48285134b7847954bd76e843bb794a893b47ca3ee325" +checksum = "ed1559baff8a696add3322b9be3e940d433e7bb4e38d79017205fd37ff28b28e" dependencies = [ "anstream", ] @@ -6078,6 +6889,20 @@ dependencies = [ "sha-1", ] +[[package]] +name = "solang-parser" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7cb9fa2fa2fa6837be8a2495486ff92e3ffe68a99b6eeba288e139efdd842457" +dependencies = [ + "itertools 0.11.0", + "lalrpop", + "lalrpop-util", + "phf", + "thiserror", + "unicode-xid", +] + [[package]] name = "sozo" version = "0.2.1" @@ -6136,6 +6961,16 @@ dependencies = [ "lock_api", ] +[[package]] +name = "spki" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9d1e996ef02c474957d681f1b05213dfb0abab947b446a62d37770b23500184a" +dependencies = [ + "base64ct", + "der", +] + [[package]] name = "sprs" version = "0.7.1" @@ -6510,6 +7345,9 @@ name = "strum" version = "0.25.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "290d54ea6f91c969195bdbcd7442c8c2a2ba87da8bf60a7ee86a235d4bc1e125" +dependencies = [ + "strum_macros 0.25.2", +] [[package]] name = "strum_macros" @@ -6543,6 +7381,26 @@ version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "81cdd64d312baedb58e21336b31bc043b77e01cc99033ce76ef539f78e965ebc" +[[package]] +name = "svm-rs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597e3a746727984cb7ea2487b6a40726cad0dbe86628e7d429aa6b8c4c153db4" +dependencies = [ + "dirs", + "fs2", + "hex", + "once_cell", + "reqwest", + "semver", + "serde", + "serde_json", + "sha2", + "thiserror", + "url", + "zip", +] + [[package]] name = "syn" version = "1.0.109" @@ -6811,8 +7669,11 @@ checksum = "212d5dcb2a1ce06d81107c3d0ffa3121fe974b73f068c8282cb1c32328113b6c" dependencies = [ "futures-util", "log", + "rustls 0.21.7", "tokio", + "tokio-rustls 0.24.1", "tungstenite", + "webpki-roots 0.25.2", ] [[package]] @@ -6857,7 +7718,7 @@ version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ - "indexmap 2.0.1", + "indexmap 2.0.2", "serde", "serde_spanned", "toml_datetime", @@ -6889,9 +7750,9 @@ dependencies = [ [[package]] name = "tonic" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "14c00bc15e49625f3d2f20b17082601e5e17cf27ead69e805174026c194b6664" +checksum = "d560933a0de61cf715926b9cac824d4c883c2c43142f787595e48280c40a1d0e" dependencies = [ "async-stream", "async-trait", @@ -6929,9 +7790,9 @@ dependencies = [ [[package]] name = "tonic-build" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9d37bb15da06ae9bb945963066baca6561b505af93a52e949a85d28558459a2" +checksum = "9d021fc044c18582b9a2408cd0dd05b1596e3ecdb5c4df822bb0183545683889" dependencies = [ "prettyplease 0.2.15", "proc-macro2", @@ -6942,9 +7803,9 @@ dependencies = [ [[package]] name = "tonic-web" -version = "0.10.1" +version = "0.10.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2953fe95664e86519e0d1c4bdd65007d93bc47a59c9af512280977aa9e46b871" +checksum = "0fddb2a37b247e6adcb9f239f4e5cefdcc5ed526141a416b943929f13aea2cce" dependencies = [ "base64 0.21.4", "bytes", @@ -6953,7 +7814,7 @@ dependencies = [ "hyper", "pin-project", "tokio-stream", - "tonic 0.10.1", + "tonic 0.10.2", "tower-http", "tower-layer", "tower-service", @@ -7007,7 +7868,7 @@ dependencies = [ "starknet-crypto 0.6.0", "thiserror", "tokio", - "tonic 0.10.1", + "tonic 0.10.2", "tonic 0.9.2", "torii-grpc", "url", @@ -7025,6 +7886,7 @@ dependencies = [ "async-trait", "camino", "chrono", + "dojo-test-utils", "dojo-types", "dojo-world", "futures-channel", @@ -7033,12 +7895,15 @@ dependencies = [ "lazy_static", "log", "once_cell", + "scarb-ui", "serde", "serde_json", "slab", + "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", + "thiserror", "tokio", "tokio-stream", "tokio-util", @@ -7064,7 +7929,6 @@ dependencies = [ "scarb-ui", "serde", "serde_json", - "sozo", "sqlx", "starknet", "starknet-crypto 0.6.0", @@ -7098,9 +7962,9 @@ dependencies = [ "thiserror", "tokio", "tokio-stream", - "tonic 0.10.1", + "tonic 0.10.2", "tonic 0.9.2", - "tonic-build 0.10.1", + "tonic-build 0.10.2", "tonic-build 0.9.2", "tonic-web", "tonic-web-wasm-client", @@ -7139,7 +8003,7 @@ dependencies = [ "tokio", "tokio-stream", "tokio-util", - "tonic 0.10.1", + "tonic 0.10.2", "tonic-web", "torii-client", "torii-core", @@ -7283,6 +8147,16 @@ dependencies = [ "valuable", ] +[[package]] +name = "tracing-futures" +version = "0.2.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "97d095ae15e245a057c8e8451bab9b3ee1e1f68e9ba2b4fbc18d0ac5237835f2" +dependencies = [ + "pin-project", + "tracing", +] + [[package]] name = "tracing-log" version = "0.1.3" @@ -7331,6 +8205,7 @@ dependencies = [ "httparse", "log", "rand", + "rustls 0.21.7", "sha1", "thiserror", "url", @@ -7908,6 +8783,25 @@ dependencies = [ "windows-sys 0.48.0", ] +[[package]] +name = "ws_stream_wasm" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7999f5f4217fe3818726b66257a4475f71e74ffd190776ad053fa159e50737f5" +dependencies = [ + "async_io_stream", + "futures", + "js-sys", + "log", + "pharos", + "rustc_version", + "send_wrapper 0.6.0", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" @@ -7970,10 +8864,27 @@ version = "0.6.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "760394e246e4c28189f19d488c058bf16f564016aefac5d32bb1f3b51d5e9261" dependencies = [ + "aes", "byteorder", + "bzip2", + "constant_time_eq", "crc32fast", "crossbeam-utils", "flate2", + "hmac", + "pbkdf2 0.11.0", + "sha1", + "time", + "zstd 0.11.2+zstd.1.5.2", +] + +[[package]] +name = "zstd" +version = "0.11.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "20cc960326ece64f010d2d2107537f26dc589a6573a316bd5b1dba685fa5fde4" +dependencies = [ + "zstd-safe 5.0.2+zstd.1.5.2", ] [[package]] @@ -7982,7 +8893,17 @@ version = "0.12.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1a27595e173641171fc74a1232b7b1c7a7cb6e18222c11e9dfb9888fa424c53c" dependencies = [ - "zstd-safe", + "zstd-safe 6.0.6", +] + +[[package]] +name = "zstd-safe" +version = "5.0.2+zstd.1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2a5585e04f9eea4b2a3d1eca508c4dee9592a89ef6f450c11719da0726f4db" +dependencies = [ + "libc", + "zstd-sys", ] [[package]] diff --git a/crates/dojo-types/Cargo.toml b/crates/dojo-types/Cargo.toml index 3ef166e424..8b66ba2df2 100644 --- a/crates/dojo-types/Cargo.toml +++ b/crates/dojo-types/Cargo.toml @@ -6,7 +6,7 @@ version = "0.2.1" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ethabi = { version = "18.0.0", default-features = false, features = [ "std" ] } +crypto-bigint = { version = "0.5.3", features = [ "serde" ] } hex = "0.4.3" itertools.workspace = true serde.workspace = true diff --git a/crates/dojo-types/src/core.rs b/crates/dojo-types/src/core.rs deleted file mode 100644 index ddeb4005d0..0000000000 --- a/crates/dojo-types/src/core.rs +++ /dev/null @@ -1,152 +0,0 @@ -use ethabi::ethereum_types::U256; -use serde::{Deserialize, Serialize}; -use starknet::core::types::{FieldElement, ValueOutOfRangeError}; -use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; - -#[derive( - AsRefStr, Display, EnumIter, EnumString, Clone, Debug, Serialize, Deserialize, PartialEq, -)] -#[strum(serialize_all = "lowercase")] -pub enum CairoType { - U8(Option), - U16(Option), - U32(Option), - U64(Option), - U128(Option), - U256(Option), - USize(Option), - Bool(Option), - Felt252(Option), - #[strum(serialize = "ClassHash")] - ClassHash(Option), - #[strum(serialize = "ContractAddress")] - ContractAddress(Option), -} - -#[derive(Debug, thiserror::Error)] -pub enum CairoTypeError { - #[error("Value must have at least one FieldElement")] - MissingFieldElement, - #[error("Not enough FieldElements for U256")] - NotEnoughFieldElements, - #[error("Unsupported CairoType for SQL formatting")] - UnsupportedType, - #[error(transparent)] - ValueOutOfRange(#[from] ValueOutOfRangeError), -} - -impl CairoType { - pub fn to_sql_type(&self) -> String { - match self { - CairoType::U8(_) - | CairoType::U16(_) - | CairoType::U32(_) - | CairoType::U64(_) - | CairoType::USize(_) - | CairoType::Bool(_) => "INTEGER".to_string(), - CairoType::U128(_) - | CairoType::U256(_) - | CairoType::ContractAddress(_) - | CairoType::ClassHash(_) - | CairoType::Felt252(_) => "TEXT".to_string(), - } - } - - pub fn format_for_sql(&self, value: Vec<&FieldElement>) -> Result { - if value.is_empty() { - return Err(CairoTypeError::MissingFieldElement); - } - - match self { - CairoType::U8(_) - | CairoType::U16(_) - | CairoType::U32(_) - | CairoType::U64(_) - | CairoType::USize(_) - | CairoType::Bool(_) => Ok(format!(", '{}'", value[0])), - CairoType::U128(_) - | CairoType::ContractAddress(_) - | CairoType::ClassHash(_) - | CairoType::Felt252(_) => Ok(format!(", '{:0>64x}'", value[0])), - CairoType::U256(_) => { - if value.len() < 2 { - Err(CairoTypeError::NotEnoughFieldElements) - } else { - let mut buffer = [0u8; 32]; - let value0_bytes = value[0].to_bytes_be(); - let value1_bytes = value[1].to_bytes_be(); - buffer[..16].copy_from_slice(&value0_bytes); - buffer[16..].copy_from_slice(&value1_bytes); - Ok(format!(", '{}'", hex::encode(buffer))) - } - } - } - } - - pub fn set_value_from_felts( - &mut self, - felts: &mut Vec, - ) -> Result<(), CairoTypeError> { - if felts.is_empty() { - return Err(CairoTypeError::MissingFieldElement); - } - - match self { - CairoType::U8(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U16(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U32(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U64(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::USize(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::Bool(ref mut value) => { - let raw = felts.remove(0); - *value = Some(raw == FieldElement::ONE); - Ok(()) - } - CairoType::U128(ref mut value) => { - *value = Some(felts.remove(0).try_into().map_err(CairoTypeError::ValueOutOfRange)?); - Ok(()) - } - CairoType::U256(ref mut value) => { - if felts.len() < 2 { - return Err(CairoTypeError::NotEnoughFieldElements); - } - let value0 = felts.remove(0); - let value1 = felts.remove(0); - let value0_bytes = value0.to_bytes_be(); - let value1_bytes = value1.to_bytes_be(); - let mut bytes = [0u8; 32]; - bytes[..16].copy_from_slice(&value0_bytes); - bytes[16..].copy_from_slice(&value1_bytes); - *value = Some(U256::from(bytes)); - Ok(()) - } - CairoType::ContractAddress(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - CairoType::ClassHash(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - CairoType::Felt252(ref mut value) => { - *value = Some(felts.remove(0)); - Ok(()) - } - } - } -} diff --git a/crates/dojo-types/src/lib.rs b/crates/dojo-types/src/lib.rs index 15ac2373f5..305a20d402 100644 --- a/crates/dojo-types/src/lib.rs +++ b/crates/dojo-types/src/lib.rs @@ -5,8 +5,9 @@ use serde::Serialize; use starknet::core::types::FieldElement; use system::SystemMetadata; -pub mod core; pub mod event; +pub mod packing; +pub mod primitive; pub mod schema; pub mod storage; pub mod system; diff --git a/crates/dojo-types/src/packing.rs b/crates/dojo-types/src/packing.rs new file mode 100644 index 0000000000..d9c82604b6 --- /dev/null +++ b/crates/dojo-types/src/packing.rs @@ -0,0 +1,193 @@ +use std::str::FromStr; + +use crypto_bigint::U256; +use starknet::core::types::{FieldElement, FromStrError, ValueOutOfRangeError}; +use starknet::core::utils::{ + cairo_short_string_to_felt, parse_cairo_short_string, CairoShortStringToFeltError, + ParseCairoShortStringError, +}; + +use crate::primitive::Primitive; +use crate::schema::{self, Ty}; + +#[derive(Debug, thiserror::Error)] +pub enum ParseError { + #[error("Invalid schema")] + InvalidSchema, + #[error("Error when parsing felt: {0}")] + ValueOutOfRange(#[from] ValueOutOfRangeError), + #[error("Error when parsing felt: {0}")] + FromStr(#[from] FromStrError), + #[error(transparent)] + ParseCairoShortStringError(#[from] ParseCairoShortStringError), + #[error(transparent)] + CairoShortStringToFeltError(#[from] CairoShortStringToFeltError), +} + +#[derive(Debug, thiserror::Error)] +pub enum PackingError { + #[error(transparent)] + Parse(#[from] ParseError), + #[error("Error when unpacking entity")] + UnpackingEntityError, +} + +/// Unpacks a vector of packed values according to a given layout. +/// +/// # Arguments +/// +/// * `packed_values` - A vector of FieldElement values that are packed. +/// * `layout` - A vector of FieldElement values that describe the layout of the packed values. +/// +/// # Returns +/// +/// * `Result, PackingError>` - A Result containing a vector of unpacked +/// FieldElement values if successful, or an error if unsuccessful. +pub fn unpack( + mut packed: Vec, + layout: Vec, +) -> Result, PackingError> { + packed.reverse(); + let mut unpacked = vec![]; + + let mut unpacking: U256 = + packed.pop().ok_or(PackingError::UnpackingEntityError)?.as_ref().into(); + let mut offset = 0; + + // Iterate over the layout. + for size in layout { + let size: u8 = size.try_into().map_err(ParseError::ValueOutOfRange)?; + let size: usize = size.into(); + let remaining_bits = 251 - offset; + + // If there are less remaining bits than the size, move to the next felt for unpacking. + if remaining_bits < size { + unpacking = packed.pop().ok_or(PackingError::UnpackingEntityError)?.as_ref().into(); + offset = 0; + } + + let mut mask = U256::from(0_u8); + for _ in 0..size { + mask = (mask << 1) | U256::from(1_u8); + } + + let result = mask & (unpacking >> offset); + let result_fe = + FieldElement::from_hex_be(&result.to_string()).map_err(ParseError::FromStr)?; + unpacked.push(result_fe); + + // Update unpacking to be the shifted value after extracting the result. + offset += size; + } + + Ok(unpacked) +} + +/// Parse a raw schema of a model into a Cairo type, [Ty] +pub fn parse_ty(data: &[FieldElement]) -> Result { + let member_type: u8 = data[0].try_into()?; + match member_type { + 0 => parse_simple(&data[1..]), + 1 => parse_struct(&data[1..]), + 2 => parse_enum(&data[1..]), + 3 => parse_tuple(&data[1..]), + _ => Err(ParseError::InvalidSchema), + } +} + +fn parse_simple(data: &[FieldElement]) -> Result { + let ty = parse_cairo_short_string(&data[0])?; + Ok(Ty::Primitive(Primitive::from_str(&ty).expect("must be valid schema"))) +} + +fn parse_struct(data: &[FieldElement]) -> Result { + let name = parse_cairo_short_string(&data[0])?; + + let attrs_len: u32 = data[1].try_into()?; + let attrs_slice_start = 2; + let attrs_slice_end = attrs_slice_start + attrs_len as usize; + let _attrs = &data[attrs_slice_start..attrs_slice_end]; + + let children_len: u32 = data[attrs_slice_end].try_into()?; + let children_len = children_len as usize; + + let mut children = vec![]; + let mut offset = attrs_slice_end + 1; + + for i in 0..children_len { + let start = i + offset; + let len: u32 = data[start].try_into()?; + let slice_start = start + 1; + let slice_end = slice_start + len as usize; + children.push(parse_member(&data[slice_start..slice_end])?); + offset += len as usize; + } + + Ok(Ty::Struct(schema::Struct { name, children })) +} + +fn parse_member(data: &[FieldElement]) -> Result { + let name = parse_cairo_short_string(&data[0])?; + + let attributes_len: u32 = data[1].try_into()?; + let slice_start = 2; + let slice_end = slice_start + attributes_len as usize; + let attributes = &data[slice_start..slice_end]; + + let key = attributes.contains(&cairo_short_string_to_felt("key")?); + let ty = parse_ty(&data[slice_end..])?; + + Ok(schema::Member { name, ty, key }) +} + +fn parse_enum(data: &[FieldElement]) -> Result { + let name = parse_cairo_short_string(&data[0])?; + + let attrs_len: u32 = data[1].try_into()?; + let attrs_slice_start = 2; + let attrs_slice_end = attrs_slice_start + attrs_len as usize; + let _attrs = &data[attrs_slice_start..attrs_slice_end]; + + let values_len: u32 = data[attrs_slice_end].try_into()?; + let values_len = values_len as usize; + + let mut values = vec![]; + let mut offset = attrs_slice_end + 1; + + for i in 0..values_len { + let start = i + offset; + let name = parse_cairo_short_string(&data[start])?; + let slice_start = start + 2; + let len: u32 = data[start + 3].try_into()?; + let len = len + 1; // Account for Ty enum index + + let slice_end = slice_start + len as usize; + values.push((name, parse_ty(&data[slice_start..slice_end])?)); + offset += len as usize + 2; + } + + Ok(Ty::Enum(schema::Enum { name, option: None, options: values })) +} + +fn parse_tuple(data: &[FieldElement]) -> Result { + if data.is_empty() { + return Ok(Ty::Tuple(vec![])); + } + + let children_len: u32 = data[0].try_into()?; + let children_len = children_len as usize; + + let mut children = vec![]; + let mut offset = 1; + + for i in 0..children_len { + let start = i + offset; + let len: u32 = data[start].try_into()?; + let slice_start = start + 1; + let slice_end = slice_start + len as usize; + children.push(parse_ty(&data[slice_start..slice_end])?); + offset += len as usize; + } + + Ok(Ty::Tuple(children)) +} diff --git a/crates/dojo-types/src/primitive.rs b/crates/dojo-types/src/primitive.rs new file mode 100644 index 0000000000..0886a9b21c --- /dev/null +++ b/crates/dojo-types/src/primitive.rs @@ -0,0 +1,202 @@ +use crypto_bigint::{Encoding, U256}; +use serde::{Deserialize, Serialize}; +use starknet::core::types::{FieldElement, ValueOutOfRangeError}; +use strum_macros::{AsRefStr, Display, EnumIter, EnumString}; + +#[derive( + AsRefStr, Display, EnumIter, EnumString, Clone, Debug, Serialize, Deserialize, PartialEq, +)] +#[strum(serialize_all = "lowercase")] +pub enum Primitive { + U8(Option), + U16(Option), + U32(Option), + U64(Option), + U128(Option), + U256(Option), + USize(Option), + Bool(Option), + Felt252(Option), + #[strum(serialize = "ClassHash")] + ClassHash(Option), + #[strum(serialize = "ContractAddress")] + ContractAddress(Option), +} + +#[derive(Debug, thiserror::Error)] +pub enum PrimitiveError { + #[error("Value must have at least one FieldElement")] + MissingFieldElement, + #[error("Not enough FieldElements for U256")] + NotEnoughFieldElements, + #[error("Unsupported CairoType for SQL formatting")] + UnsupportedType, + #[error(transparent)] + ValueOutOfRange(#[from] ValueOutOfRangeError), +} + +impl Primitive { + pub fn to_sql_type(&self) -> String { + match self { + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::USize(_) + | Primitive::Bool(_) => "INTEGER".to_string(), + Primitive::U128(_) + | Primitive::U256(_) + | Primitive::ContractAddress(_) + | Primitive::ClassHash(_) + | Primitive::Felt252(_) => "TEXT".to_string(), + } + } + + pub fn to_sql_value(&self) -> Result { + let value = self.serialize()?; + + if value.is_empty() { + return Err(PrimitiveError::MissingFieldElement); + } + + match self { + Primitive::U8(_) + | Primitive::U16(_) + | Primitive::U32(_) + | Primitive::U64(_) + | Primitive::USize(_) + | Primitive::Bool(_) => Ok(format!("'{}'", value[0])), + + Primitive::U128(_) + | Primitive::ContractAddress(_) + | Primitive::ClassHash(_) + | Primitive::Felt252(_) => Ok(format!("'{:0>64x}'", value[0])), + + Primitive::U256(_) => { + if value.len() < 2 { + Err(PrimitiveError::NotEnoughFieldElements) + } else { + let mut buffer = [0u8; 32]; + let value0_bytes = value[0].to_bytes_be(); + let value1_bytes = value[1].to_bytes_be(); + buffer[..16].copy_from_slice(&value0_bytes); + buffer[16..].copy_from_slice(&value1_bytes); + Ok(format!("'{}'", hex::encode(buffer))) + } + } + } + } + + pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), PrimitiveError> { + if felts.is_empty() { + return Err(PrimitiveError::MissingFieldElement); + } + + match self { + Primitive::U8(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U16(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U32(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U64(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::USize(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::Bool(ref mut value) => { + let raw = felts.remove(0); + *value = Some(raw == FieldElement::ONE); + Ok(()) + } + Primitive::U128(ref mut value) => { + *value = Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + Ok(()) + } + Primitive::U256(ref mut value) => { + if felts.len() < 2 { + return Err(PrimitiveError::NotEnoughFieldElements); + } + let value0 = felts.remove(0); + let value1 = felts.remove(0); + let value0_bytes = value0.to_bytes_be(); + let value1_bytes = value1.to_bytes_be(); + let mut bytes = [0u8; 32]; + bytes[..16].copy_from_slice(&value0_bytes); + bytes[16..].copy_from_slice(&value1_bytes); + *value = Some(U256::from_be_bytes(bytes)); + Ok(()) + } + Primitive::ContractAddress(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + Primitive::ClassHash(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + Primitive::Felt252(ref mut value) => { + *value = Some(felts.remove(0)); + Ok(()) + } + } + } + + pub fn serialize(&self) -> Result, PrimitiveError> { + match self { + Primitive::U8(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U16(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U32(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U64(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::USize(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::Bool(value) => value + .map(|v| Ok(vec![if v { FieldElement::ONE } else { FieldElement::ZERO }])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U128(value) => value + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::U256(value) => value + .map(|v| { + let bytes: [u8; 32] = v.to_be_bytes(); + let value0_slice = &bytes[..16]; + let value1_slice = &bytes[16..]; + let mut value0_array = [0u8; 32]; + let mut value1_array = [0u8; 32]; + value0_array[16..].copy_from_slice(value0_slice); + value1_array[16..].copy_from_slice(value1_slice); + let value0 = FieldElement::from_bytes_be(&value0_array).unwrap(); + let value1 = FieldElement::from_bytes_be(&value1_array).unwrap(); + Ok(vec![value0, value1]) + }) + .unwrap_or(Err(PrimitiveError::MissingFieldElement)), + Primitive::ContractAddress(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + Primitive::ClassHash(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + Primitive::Felt252(value) => { + value.map(|v| Ok(vec![v])).unwrap_or(Err(PrimitiveError::MissingFieldElement)) + } + } + } +} diff --git a/crates/dojo-types/src/schema.rs b/crates/dojo-types/src/schema.rs index 269b656db6..f7400f070a 100644 --- a/crates/dojo-types/src/schema.rs +++ b/crates/dojo-types/src/schema.rs @@ -2,7 +2,7 @@ use itertools::Itertools; use serde::{Deserialize, Serialize}; use starknet::core::types::FieldElement; -use crate::core::{CairoType, CairoTypeError}; +use crate::primitive::{Primitive, PrimitiveError}; /// Represents a model member. #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] @@ -12,6 +12,12 @@ pub struct Member { pub key: bool, } +impl Member { + pub fn serialize(&self) -> Result, PrimitiveError> { + self.ty.serialize() + } +} + /// Represents a model of an entity #[derive(Debug, Clone, Hash, PartialEq, Eq, Serialize, Deserialize)] pub struct EntityModel { @@ -26,9 +32,10 @@ pub struct ModelMetadata { pub class_hash: FieldElement, } +/// Represents all possible types in Cairo #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum Ty { - Primitive(CairoType), + Primitive(Primitive), Struct(Struct), Enum(Enum), Tuple(Vec), @@ -48,10 +55,48 @@ impl Ty { TyIter { stack: vec![self] } } - pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), CairoTypeError> { + pub fn serialize(&self) -> Result, PrimitiveError> { + let mut felts = vec![]; + + fn serialize_inner(ty: &Ty, felts: &mut Vec) -> Result<(), PrimitiveError> { + match ty { + Ty::Primitive(c) => { + felts.extend(c.serialize()?); + } + Ty::Struct(s) => { + for child in &s.children { + serialize_inner(&child.ty, felts)?; + } + } + Ty::Enum(e) => { + let option = e + .option + .map(|v| Ok(vec![FieldElement::from(v)])) + .unwrap_or(Err(PrimitiveError::MissingFieldElement))?; + felts.extend(option); + + for (_, child) in &e.options { + serialize_inner(child, felts)?; + } + } + Ty::Tuple(tys) => { + for ty in tys { + serialize_inner(ty, felts)?; + } + } + } + Ok(()) + } + + serialize_inner(self, &mut felts)?; + + Ok(felts) + } + + pub fn deserialize(&mut self, felts: &mut Vec) -> Result<(), PrimitiveError> { match self { Ty::Primitive(c) => { - c.set_value_from_felts(felts)?; + c.deserialize(felts)?; } Ty::Struct(s) => { for child in &mut s.children { @@ -59,7 +104,9 @@ impl Ty { } } Ty::Enum(e) => { - for (_, child) in &mut e.children { + e.option = + Some(felts.remove(0).try_into().map_err(PrimitiveError::ValueOutOfRange)?); + for (_, child) in &mut e.options { child.deserialize(felts)?; } } @@ -89,7 +136,7 @@ impl<'a> Iterator for TyIter<'a> { } } Ty::Enum(e) => { - for child in &e.children { + for child in &e.options { self.stack.push(&child.1); } } @@ -115,7 +162,7 @@ impl std::fmt::Display for Ty { } Ty::Enum(e) => { let mut enum_str = format!("enum {} {{\n", e.name); - for child in &e.children { + for child in &e.options { enum_str.push_str(&format!(" {}\n", child.0)); } enum_str.push('}'); @@ -142,10 +189,45 @@ pub struct Struct { pub children: Vec, } +impl Struct { + pub fn keys(&self) -> Vec { + self.children.iter().filter(|m| m.key).cloned().collect() + } +} + +#[derive(Debug, thiserror::Error)] +pub enum EnumError { + #[error("Enum option not set")] + OptionNotSet, + #[error("Enum option invalid")] + OptionInvalid, +} + #[derive(Clone, Debug, Serialize, Deserialize, PartialEq)] pub struct Enum { pub name: String, - pub children: Vec<(String, Ty)>, + pub option: Option, + pub options: Vec<(String, Ty)>, +} + +impl Enum { + pub fn option(&self) -> Result { + let option: usize = if let Some(option) = self.option { + option as usize + } else { + return Err(EnumError::OptionNotSet); + }; + + if option >= self.options.len() { + return Err(EnumError::OptionInvalid); + } + + Ok(self.options[option].0.clone()) + } + + pub fn to_sql_value(&self) -> Result { + Ok(format!("'{}'", self.option()?)) + } } fn format_member(m: &Member) -> String { @@ -157,62 +239,67 @@ fn format_member(m: &Member) -> String { if let Ty::Primitive(ty) = &m.ty { match ty { - CairoType::U8(value) => { + Primitive::U8(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U16(value) => { + Primitive::U16(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U32(value) => { + Primitive::U32(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U64(value) => { + Primitive::U64(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U128(value) => { + Primitive::U128(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::U256(value) => { + Primitive::U256(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::USize(value) => { + Primitive::USize(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::Bool(value) => { + Primitive::Bool(value) => { if let Some(value) = value { str.push_str(&format!(" = {}", value)); } } - CairoType::Felt252(value) => { + Primitive::Felt252(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } } - CairoType::ClassHash(value) => { + Primitive::ClassHash(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } } - CairoType::ContractAddress(value) => { + Primitive::ContractAddress(value) => { if let Some(value) = value { str.push_str(&format!(" = {:#x}", value)); } } } + } else if let Ty::Enum(e) = &m.ty { + match e.option() { + Ok(option) => str.push_str(&format!(" = {option}")), + Err(_) => str.push_str(" = Invalid Option"), + } } str diff --git a/crates/katana/Cargo.toml b/crates/katana/Cargo.toml index a54c1f8672..407f9e5a34 100644 --- a/crates/katana/Cargo.toml +++ b/crates/katana/Cargo.toml @@ -20,3 +20,8 @@ url.workspace = true [dev-dependencies] assert_matches = "1.5.0" + +[features] +default = [ "messaging" ] +messaging = [ "katana-core/messaging" ] +starknet-messaging = [ "katana-core/starknet-messaging", "messaging" ] diff --git a/crates/katana/core/Cargo.toml b/crates/katana/core/Cargo.toml index 9a298c4664..3687a16940 100644 --- a/crates/katana/core/Cargo.toml +++ b/crates/katana/core/Cargo.toml @@ -8,11 +8,13 @@ version.workspace = true [dependencies] anyhow.workspace = true +async-trait.workspace = true blockifier.workspace = true cairo-lang-casm = "2.2.0" cairo-lang-starknet = "2.2.0" cairo-vm.workspace = true convert_case.workspace = true +ethers = { version = "2.0.8", optional = true } flate2.workspace = true futures.workspace = true lazy_static = "1.4.0" @@ -21,6 +23,7 @@ rand = { version = "0.8.5", features = [ "small_rng" ] } serde.workspace = true serde_json.workspace = true serde_with.workspace = true +sha3 = { version = "0.10.7", default-features = false, optional = true } starknet.workspace = true starknet_api.workspace = true thiserror.workspace = true @@ -30,3 +33,8 @@ url.workspace = true [dev-dependencies] assert_matches = "1.5.0" +hex = "0.4.3" + +[features] +messaging = [ "ethers", "sha3" ] +starknet-messaging = [ ] diff --git a/crates/katana/core/contracts/messaging/README.md b/crates/katana/core/contracts/messaging/README.md new file mode 100644 index 0000000000..0ec0c0529c --- /dev/null +++ b/crates/katana/core/contracts/messaging/README.md @@ -0,0 +1,126 @@ +# Smart contracts + +## Requirements + +Please before starting, install: + +- [scarb](https://docs.swmansion.com/scarb/) to build cairo contracts. +- [starkli](https://github.com/xJonathanLEI/starkli) to interact with Katana. +- [foundry](https://book.getfoundry.sh/getting-started/installation) to interact with Anvil. + +## Contracts + +In this folder you will find smart contracts ready to be declared / deployed +to test how the messaging can work. + +## L1 (Ethereum) - L2 (Starknet) + +The first messaging is L1-L2 messaging, where Katana is used as a dev Starknet +sequencer. In this scenario, you want to spin up Katana to test your Starknet +contracts before reaching the testnet and use Anvil to dev on Ethereum. + +To test this scenario, you can use the associated Makefiles. But the flow is the following: + +1. Starting Anvil and deploy the `StarknetMessagingLocal` that simulates the work + done by the Starknet contract on Ethereum for messaging. Then deploy the `Contract1.sol` + on ethereum to send/consume messages from Starknet. + +2. Starting Katana as your Starknet dev node and declare/deploy `contract_msg_l1.cairo` contract. This contract has example to send/receive messages from Ethereum. + +3. Then you can use `starkli` and `cast` to target the contracts on both chain to test + messaging in both ways. + +How to run the scripts: + +- Starts Anvil in a terminal. +- Starts Katana in an other terminal on default port 5050 with the messaging configuration that is inside the: + `katana --messaging ~/dojo/crates/katana/core/contracts/messaging/anvil.messaging.json` +- Open an other terminal and `cd ~/dojo/crates/katana/core/contracts/messaging`. + +Then you can then use pre-defined commands to interact with the contracts. +If you change the code or addresses, you may want to edit the Makefile. But +those Makefiles are only here for quick testing while developing on messaging +and quick demo. + +```bash +# Setup anvil with messaging + Contract1.sol deployed. +make -sC ./solidity/ deploy_messaging_contracts + +# Declare and deploy contract_msg_l1.cairo. +make -sC ./cairo/ setup_for_l1_messaging + +# Send message L1 -> L2 with a single value. +make -sC solidity/ send_msg selector_str=msg_handler_value payload="[123]" + +# Send message L1 -> L2 with a serialized struct. +make -sC solidity/ send_msg selector_str=msg_handler_struct payload="[1,2]" + +# Send message L2 -> L1 to be manually consumed. +make -sC cairo/ send_msg_value_l1 value=2 + +# Consume the messag previously sent. You can try to call it once and see the second one reverting. +make -sC solidity/ consume_msg payload="[2]" +``` + +## L2 (Starknet) - L3 (Appchain) [Experimental] + +The second messaging is when you may want your appchain (Katana based) to communicate +with Starknet. In this case, the Katana sequencer (L3) will listen to the messages +emitted by a specific messaging contract on Starknet. + +The messaging in this scenario works exactly the same way as it does for L1-L2. But in this +case, the settlement layer is not Ethereum, but Starknet. + +There is a feature that is experimental, which allows the messages to be `executed` instead +of the regular registering/consumption of the message, which is totally manual. + +You can also use the Makefile to setup the chains, but the flow is the following: + +1. Starting Katana (1) to simulate Starknet network. On this Katana instance, you will + deploy `appchain_messaging.cairo` which is the analogue contract of `StarknetMessagingLocal` in the L1-L2 messaging. This contract is responsible for sending/registering/executing messages. + Then you can deploy `contract_1.cairo` to send/consume message and test the execution + of smart contract function on Starknet from the appchain. + + You can totally deploy `appchain_messaging.cairo` on Starknet to test. **Please be aware + this contract is not audited for now and only experimental without security considerations yet**. + Be sure you control who is able to send/execute messages to be safe. + +2. Starting Katana (2) to represent your appchain. On this Katana instance, you will deploy + `contract_msg_starknet.cairo` contract. This contract can send/execute/receive message + from/to your Katana (1). + +3. Then, you can interact with `contract_msg_starknet.cairo` on the appchain (Katana 2) to send/execute messages on Starknet. On Katana (1) which is Starknet, you can interact with `contract_1.cairo` to send/consume and see contract execution. + +How to run the scripts: + +- Starts Katana (1) to simulate starknet on a new terminal with default port 5050. +- Starts Katana (2) for your appchain on a new terminal with port 6060 and the configuration for messaging: `katana --messaging crates/katana/core/contracts/messaging/l3.messaging.json -p 6060` +- Open an other terminal and `cd ~/dojo/crates/katana/core/contracts/messaging`. + +Then you can then use pre-defined commands to interact with the contracts. +If you change the code or addresses, you may want to edit the Makefile. But +those Makefiles are only here for quick testing while developing on messaging +and quick demo. + +```bash +# Setup both katana at once with appchain_messaging and contract_1 on katana 1 (starknet), +# and contract_msg_starknet on katana 2 (l3). +make -sC ./cairo/ setup_l2_messaging +make -sC ./cairo/ setup_l3_messaging + +# Send a message L3 -> L2 to be manually consumed. +make -sC ./cairo/ send_msg_value_l2 value=3 + +# Consume the message on L2 (it's a span, so length first). +make -sC ./cairo/ consume_msg_from_l3 payload="1 3" + +# Send a message L3 -> L2 to be executed directly on L2. +make -sC ./cairo/ exec_msg_l2 selector_str=set_value value=2 + +# Verify the execution by getting the value. +make -sC ./cairo/ get_value_l2 + +# Send a message L2 -> L3. +# Try to change the value to see the transaction error. +make -sC cairo/ send_msg_l3 selector_str=msg_handler_value value=888 +``` diff --git a/crates/katana/core/contracts/messaging/anvil.messaging.json b/crates/katana/core/contracts/messaging/anvil.messaging.json new file mode 100644 index 0000000000..3be405f60d --- /dev/null +++ b/crates/katana/core/contracts/messaging/anvil.messaging.json @@ -0,0 +1,9 @@ +{ + "chain": "ethereum", + "rpc_url": "http://127.0.0.1:8545", + "contract_address": "0x5FbDB2315678afecb367f032d93F642f64180aa3", + "sender_address": "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266", + "private_key": "0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80", + "interval": 2, + "from_block": 0 +} diff --git a/crates/katana/core/contracts/messaging/cairo/.gitignore b/crates/katana/core/contracts/messaging/cairo/.gitignore new file mode 100644 index 0000000000..eb5a316cbd --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/.gitignore @@ -0,0 +1 @@ +target diff --git a/crates/katana/core/contracts/messaging/cairo/Makefile b/crates/katana/core/contracts/messaging/cairo/Makefile new file mode 100644 index 0000000000..267ecb5120 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/Makefile @@ -0,0 +1,107 @@ +ACCOUNT_L2=./account_l2.json +ACCOUNT_L2_ADDR=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +L2_PRIVATE_KEY=0x1800000000300000180000000000030000000000003006001800006600 + +################# +# ** L1 <-> L2 ** +# +L1_CONTRACT_ADDR=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 +C_MSG_L1_ADDR=0x0429a64d97c1422a37a09fc7406f35c264be59b744aaff5a79d59393eb1bc7e1 +C_MSG_L1_CLASS_HASH = $(shell starkli class-hash target/dev/katana_messaging_contract_msg_l1.sierra.json) + +OPTS_L2 := --account ${ACCOUNT_L2} \ + --rpc http://0.0.0.0:5050 \ + --private-key ${L2_PRIVATE_KEY} + +setup_for_l1_messaging: + scarb build + starkli declare target/dev/katana_messaging_contract_msg_l1.sierra.json ${OPTS_L2} + starkli deploy --salt 0x1234 ${C_MSG_L1_CLASS_HASH} ${OPTS_L2} + +send_msg_value_l1_usage: + @echo make send_msg_value_l1 value=2 + +send_msg_value_l1: + starkli invoke ${C_MSG_L1_ADDR} send_message_value ${L1_CONTRACT_ADDR} $(value) ${OPTS_L2} + +send_msg_struct_l1_usage: + @echo make send_msg_struct_l1 data=\"123 88\" + +send_msg_struct_l1: + starkli invoke ${C_MSG_L1_ADDR} send_message_struct_l1 ${L1_CONTRACT_ADDR} $(data) ${OPTS_L2} + + +################# +# ** L2 <-> L3 ** +# +ACCOUNT_L3=./account_l3.json +ACCOUNT_L3_ADDR=0x5686a647a9cdd63ade617e0baf3b364856b813b508f03903eb58a7e622d5855 +L3_PRIVATE_KEY=0x33003003001800009900180300d206308b0070db00121318d17b5e6262150b + +L2_APPCHAIN_MSG_ADDR=0x046c0ea3fb2ad27053e8af3c8cfab38a51afb9fe90fcab1f75446bd41f7d3796 +L2_APPCHAIN_MSG_CLASS_HASH=$(shell starkli class-hash target/dev/katana_messaging_appchain_messaging.sierra.json) + +L2_CONTRACT1_ADDR=0x054f66c104745e27ad5194815a6c4755cf2076c4809212101dfe31563f312a34 +L2_CONTRACT1_CLASS_HASH=$(shell starkli class-hash target/dev/katana_messaging_contract_1.sierra.json) + +L3_C_MSG_ADDR=0x071278839029ab1f9fa0ce1ee01e38599736dd4e8fed2417158bec4ef5dc6d0f +L3_C_MSG_CLASS_HASH=$(shell starkli class-hash target/dev/katana_messaging_contract_msg_starknet.sierra.json) + +OPTS_L3 := --account ${ACCOUNT_L3} \ + --rpc http://0.0.0.0:6060 \ + --private-key ${L3_PRIVATE_KEY} + +setup_l2_messaging: + scarb build + starkli declare target/dev/katana_messaging_appchain_messaging.sierra.json ${OPTS_L2} + starkli declare target/dev/katana_messaging_contract_1.sierra.json ${OPTS_L2} + starkli deploy --salt 0x1234 ${L2_APPCHAIN_MSG_CLASS_HASH} ${ACCOUNT_L2_ADDR} ${ACCOUNT_L3_ADDR} ${OPTS_L2} + starkli deploy --salt 0x1234 ${L2_CONTRACT1_CLASS_HASH} ${L2_APPCHAIN_MSG_ADDR} ${OPTS_L2} + +setup_l3_messaging: + scarb build + starkli declare target/dev/katana_messaging_contract_msg_starknet.sierra.json ${OPTS_L3} + starkli deploy --salt 0x1234 ${L3_C_MSG_CLASS_HASH} ${OPTS_L3} + +send_msg_value_l2_usage: + @echo make send_msg_value_l2 value=2 + +send_msg_value_l2: + starkli invoke ${L3_C_MSG_ADDR} send_message \ + ${L2_CONTRACT1_ADDR} \ + $(value) \ + ${OPTS_L3} + +consume_msg_from_l3_usage: + @echo make consume_msg_from_l3 payload=\"1 2\" + +consume_msg_from_l3: + starkli invoke ${L2_CONTRACT1_ADDR} consume_message \ + ${ACCOUNT_L3_ADDR} \ + $(payload) \ + ${OPTS_L2} + +exec_msg_l2_usage: + @echo make exec_msg_l2 selector_str=set_value value=2 + +exec_msg_l2: + $(eval selector=$(shell starkli selector $(selector_str))) + starkli invoke ${L3_C_MSG_ADDR} execute_message \ + ${L2_CONTRACT1_ADDR} \ + ${selector} \ + $(value) \ + ${OPTS_L3} + +get_value_l2: + starkli call ${L2_CONTRACT1_ADDR} get_value --rpc http://0.0.0.0:5050 + +send_msg_l3_usage: + @echo make send_msg_l3 selector_str=msg_handler_value value=2 + +send_msg_l3: + $(eval selector=$(shell starkli selector $(selector_str))) + starkli invoke ${L2_CONTRACT1_ADDR} send_message \ + ${L3_C_MSG_ADDR} \ + $(selector) \ + $(value) \ + ${OPTS_L2} diff --git a/crates/katana/core/contracts/messaging/cairo/Scarb.toml b/crates/katana/core/contracts/messaging/cairo/Scarb.toml new file mode 100644 index 0000000000..d21d9f8451 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/Scarb.toml @@ -0,0 +1,11 @@ +[package] +name = "katana_messaging" +version = "0.1.0" + +[dependencies] +starknet = ">=2.2.0" + +[[target.starknet-contract]] +sierra = true + +[lib] diff --git a/crates/katana/core/contracts/messaging/cairo/account_l2.json b/crates/katana/core/contracts/messaging/cairo/account_l2.json new file mode 100644 index 0000000000..f879c33766 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/account_l2.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x2b191c2f3ecf685a91af7cf72a43e7b90e2e41220175de5c4f7498981b10053" + }, + "deployment": { + "status": "deployed", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "address": "0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973" + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/account_l3.json b/crates/katana/core/contracts/messaging/cairo/account_l3.json new file mode 100644 index 0000000000..3b333914a4 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/account_l3.json @@ -0,0 +1,13 @@ +{ + "version": 1, + "variant": { + "type": "open_zeppelin", + "version": 1, + "public_key": "0x4c0f884b8e5b4f00d97a3aad26b2e5de0c0c76a555060c837da2e287403c01d" + }, + "deployment": { + "status": "deployed", + "class_hash": "0x4d07e40e93398ed3c76981e72dd1fd22557a78ce36c0515f679e27f0bb5bc5f", + "address": "0x5686a647a9cdd63ade617e0baf3b364856b813b508f03903eb58a7e622d5855" + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/src/appchain_messaging.cairo b/crates/katana/core/contracts/messaging/cairo/src/appchain_messaging.cairo new file mode 100644 index 0000000000..b7d3177c8a --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/src/appchain_messaging.cairo @@ -0,0 +1,333 @@ +//! The messaging between an appchain and starknet +//! is done in a smiliar way starknet interacts with ethereum. +//! +//! This contract, deployed on starknet, will emit events. +//! An the sequencer of the appchain (katana in that case) will +//! listen for those events. When an event with a message is gathered +//! by katana, a L1 handler transaction is then created and added to the pool. +//! +//! For the appchain to send a message to starknet, the process can be done in two +//! fashions: +//! +//! 1. The appchain register messages hashes exactly as starknet does. And then +//! a transaction on starknet must be issued to consume the message. +//! +//! 2. The sequencer (katana in that case) has also the capability of directly send +//! send a transaction to "execute" the content of the message. In the appchain +//! context this is a very effective manner to have a more dynamic and real-time +//! messaging than manual consuming of a message. +//! + +/// Trait for Appchain messaging. For now, the messaging only whitelist one +/// appchain. +#[starknet::interface] +trait IAppchainMessaging { + /// Update the account address (on starknet or any chain where this contract is + /// deployed) to accept messages. + fn update_appchain_account_address(ref self: T, appchain_address: starknet::ContractAddress); + + /// Sends a message to an appchain by emitting an event. + /// Returns the message hash and the nonce. + fn send_message_to_appchain( + ref self: T, + to_address: starknet::ContractAddress, + selector: felt252, + payload: Span, + ) -> (felt252, felt252); + + /// Registers messages hashes as consumable. + /// Usually, this function is only callable by the appchain developer/owner + /// that control the appchain sequencer. + fn add_messages_hashes_from_appchain(ref self: T, messages_hashes: Span); + + /// Consumes a message registered as consumable by the appchain. + /// This is the traditional consuming as done on ethereum. + /// Returnes the message hash on success. + fn consume_message_from_appchain( + ref self: T, from_address: starknet::ContractAddress, payload: Span, + ) -> felt252; + + /// Executes a message sent from the appchain. A message to execute + /// does not need to be registered as consumable. It is automatically + /// consumed while executed. + fn execute_message_from_appchain( + ref self: T, + from_address: starknet::ContractAddress, + to_address: starknet::ContractAddress, + selector: felt252, + payload: Span, + ); +} + +#[starknet::interface] +trait IUpgradeable { + fn upgrade(ref self: T, class_hash: starknet::ClassHash); +} + +#[starknet::contract] +mod appchain_messaging { + use starknet::{ContractAddress, ClassHash}; + use debug::PrintTrait; + + use super::{IAppchainMessaging, IUpgradeable}; + + #[storage] + struct Storage { + // Owner of this contract. + owner: ContractAddress, + // The account on Starknet (or the chain where this contract is deployed) + // used by the appchain sequencer to register messages hashes / execute messages. + appchain_account: ContractAddress, + // The nonce for messages sent from Starknet. + sn_to_appc_nonce: felt252, + // Ledger of messages hashes sent from Starknet to the appchain. + sn_to_appc_messages: LegacyMap::, + // Ledger of messages hashes registered from the appchain and a refcount + // associated to it. + appc_to_sn_messages: LegacyMap::, + } + + #[event] + #[derive(Drop, starknet::Event)] + enum Event { + MessageSentToAppchain: MessageSentToAppchain, + MessagesRegisteredFromAppchain: MessagesRegisteredFromAppchain, + MessageConsumed: MessageConsumed, + MessageExecuted: MessageExecuted, + Upgraded: Upgraded, + } + + #[derive(Drop, starknet::Event)] + struct MessageSentToAppchain { + #[key] + message_hash: felt252, + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + selector: felt252, + nonce: felt252, + payload: Span, + } + + #[derive(Drop, starknet::Event)] + struct MessagesRegisteredFromAppchain { + messages_hashes: Span, + } + + #[derive(Drop, starknet::Event)] + struct MessageConsumed { + #[key] + message_hash: felt252, + #[key] + from: ContractAddress, + #[key] + to: ContractAddress, + payload: Span, + } + + #[derive(Drop, starknet::Event)] + struct MessageExecuted { + #[key] + from_address: ContractAddress, + #[key] + to_address: ContractAddress, + #[key] + selector: felt252, + payload: Span, + } + + #[derive(Drop, starknet::Event)] + struct Upgraded { + class_hash: ClassHash, + } + + #[constructor] + fn constructor( + ref self: ContractState, owner: ContractAddress, appchain_account: ContractAddress, + ) { + self.owner.write(owner); + self.appchain_account.write(appchain_account); + } + + /// Computes the starknet keccak to have a hash that fits in one felt. + fn starknet_keccak(data: Span) -> felt252 { + let mut u256_data: Array = array![]; + + let mut i = 0_usize; + loop { + if i == data.len() { + break; + } + u256_data.append((*data[i]).into()); + i += 1; + }; + + let mut hash = keccak::keccak_u256s_be_inputs(u256_data.span()); + let low = integer::u128_byte_reverse(hash.high); + let high = integer::u128_byte_reverse(hash.low); + hash = u256 { low, high }; + hash = hash & 0x03ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff_u256; + hash.try_into().expect('starknet keccak overflow') + } + + /// Computes message hash to consume messages from appchain. + /// starknet_keccak(from_address, to_address, payload_len, payload). + fn compute_hash_appc_to_sn( + from_address: ContractAddress, to_address: ContractAddress, payload: Span + ) -> felt252 { + let mut hash_data: Array = array![ + from_address.into(), to_address.into(), payload.len().into(), + ]; + + let mut i = 0_usize; + loop { + if i == payload.len() { + break; + } + hash_data.append((*payload[i])); + i += 1; + }; + + starknet_keccak(hash_data.span()) + } + + /// Computes message hash to send messages to appchain. + /// starknet_keccak(nonce, to_address, selector, payload). + fn compute_hash_sn_to_appc( + nonce: felt252, to_address: ContractAddress, selector: felt252, payload: Span + ) -> felt252 { + let mut hash_data = array![nonce, to_address.into(), selector,]; + + let mut i = 0_usize; + loop { + if i == payload.len() { + break; + } + hash_data.append((*payload[i])); + i += 1; + }; + + starknet_keccak(hash_data.span()) + } + + #[external(v0)] + impl AppchainMessagingUpgradeImpl of IUpgradeable { + fn upgrade(ref self: ContractState, class_hash: ClassHash) { + assert( + starknet::get_caller_address() == self.owner.read(), 'Unauthorized replace class' + ); + + match starknet::replace_class_syscall(class_hash) { + Result::Ok(_) => self.emit(Upgraded { class_hash }), + Result::Err(revert_reason) => panic(revert_reason), + }; + } + } + + #[external(v0)] + impl AppchainMessagingImpl of IAppchainMessaging { + fn update_appchain_account_address( + ref self: ContractState, appchain_address: ContractAddress + ) { + assert(starknet::get_caller_address() == self.owner.read(), 'Unauthorized update'); + + self.appchain_account.write(appchain_address); + } + + fn send_message_to_appchain( + ref self: ContractState, + to_address: ContractAddress, + selector: felt252, + payload: Span + ) -> (felt252, felt252) { + let nonce = self.sn_to_appc_nonce.read() + 1; + self.sn_to_appc_nonce.write(nonce); + + let msg_hash = compute_hash_sn_to_appc(nonce, to_address, selector, payload); + + self + .emit( + MessageSentToAppchain { + message_hash: msg_hash, + from: starknet::get_caller_address(), + to: to_address, + selector, + nonce, + payload, + } + ); + + self.sn_to_appc_messages.write(msg_hash, nonce); + (msg_hash, nonce) + } + + fn add_messages_hashes_from_appchain( + ref self: ContractState, messages_hashes: Span + ) { + assert( + self.appchain_account.read() == starknet::get_caller_address(), + 'Unauthorized hashes registrar', + ); + + let mut i = 0_usize; + loop { + if i == messages_hashes.len() { + break; + } + + let msg_hash = *messages_hashes[i]; + + let count = self.appc_to_sn_messages.read(msg_hash); + self.appc_to_sn_messages.write(msg_hash, count + 1); + + i += 1; + }; + + self.emit(MessagesRegisteredFromAppchain { messages_hashes }); + } + + fn consume_message_from_appchain( + ref self: ContractState, from_address: ContractAddress, payload: Span + ) -> felt252 { + let to_address = starknet::get_caller_address(); + + let msg_hash = compute_hash_appc_to_sn(from_address, to_address, payload); + + let count = self.appc_to_sn_messages.read(msg_hash); + assert(count.is_non_zero(), 'INVALID_MESSAGE_TO_CONSUME'); + + self + .emit( + MessageConsumed { + message_hash: msg_hash, from: from_address, to: to_address, payload, + } + ); + + self.appc_to_sn_messages.write(msg_hash, count - 1); + + msg_hash + } + + fn execute_message_from_appchain( + ref self: ContractState, + from_address: ContractAddress, + to_address: ContractAddress, + selector: felt252, + payload: Span, + ) { + assert( + self.appchain_account.read() == starknet::get_caller_address(), + 'Unauthorized executor', + ); + + match starknet::call_contract_syscall(to_address, selector, payload) { + Result::Ok(span) => self + .emit(MessageExecuted { from_address, to_address, selector, payload, }), + Result::Err(e) => { + panic(e) + } + } + } + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/src/contract_1.cairo b/crates/katana/core/contracts/messaging/cairo/src/contract_1.cairo new file mode 100644 index 0000000000..e53b36bd9a --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/src/contract_1.cairo @@ -0,0 +1,60 @@ +//! Simple contract to send / consume message from appchain. + +#[starknet::contract] +mod contract_1 { + use starknet::ContractAddress; + use katana_messaging::appchain_messaging::{ + IAppchainMessagingDispatcher, IAppchainMessagingDispatcherTrait, + }; + + #[storage] + struct Storage { + value: felt252, + messaging_contract: ContractAddress, + } + + #[constructor] + fn constructor(ref self: ContractState, messaging_contract: ContractAddress,) { + self.messaging_contract.write(messaging_contract); + } + + /// Sends a message with the given value. + #[external(v0)] + fn send_message( + ref self: ContractState, to_address: ContractAddress, selector: felt252, value: felt252, + ) { + let messaging = IAppchainMessagingDispatcher { + contract_address: self.messaging_contract.read() + }; + + messaging.send_message_to_appchain(to_address, selector, array![value].span(),); + } + + /// Consume a message registered by the appchain. + #[external(v0)] + fn consume_message( + ref self: ContractState, from_address: ContractAddress, payload: Span, + ) { + let messaging = IAppchainMessagingDispatcher { + contract_address: self.messaging_contract.read() + }; + + // Will revert in case of failure if the message is not registered + // as consumable. + let msg_hash = messaging.consume_message_from_appchain(from_address, payload,); + // msg successfully consumed, we can proceed and process the data + // in the payload. + } + + /// An example function to test how appchain contract can trigger + /// code execution on Starknet. + #[external(v0)] + fn set_value(ref self: ContractState, value: felt252) { + self.value.write(value); + } + + #[external(v0)] + fn get_value(self: @ContractState) -> felt252 { + self.value.read() + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/src/contract_msg_l1.cairo b/crates/katana/core/contracts/messaging/cairo/src/contract_msg_l1.cairo new file mode 100644 index 0000000000..936623ab93 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/src/contract_msg_l1.cairo @@ -0,0 +1,95 @@ +//! A simple contract that sends and receives messages from/to +//! the L1 (Ethereum). +//! +//! The reception of the messages if done using the `l1_handler` functions. +//! The messages are sent by using the `send_message_to_l1_syscall` syscall. +//! +use starknet::EthAddress; +use serde::Serde; + +// A custom serializable struct. +#[derive(Drop, Serde)] +struct MyData { + a: felt252, + b: felt252, +} + +#[starknet::interface] +trait IContractL1 { + /// Sends a message to L1 contract with a single felt252 value. + /// + /// # Arguments + /// + /// * `to_address` - Contract address on L1. + /// * `value` - Value to be sent in the payload. + fn send_message_value(ref self: T, to_address: EthAddress, value: felt252); + + /// Sends a message to L1 contract with a serialized struct. + /// + /// # Arguments + /// + /// * `to_address` - Contract address on L1. + /// * `data` - Data to be sent in the payload. + fn send_message_struct(ref self: T, to_address: EthAddress, data: MyData); +} + +#[starknet::contract] +mod contract_msg_l1 { + use super::{IContractL1, MyData}; + use starknet::{EthAddress, SyscallResultTrait}; + + #[storage] + struct Storage {} + + /// Handles a message received from L1. + /// + /// Only functions that are #[l1_handler] can + /// receive message from L1. + /// + /// # Arguments + /// + /// * `from_address` - The L1 contract sending the message. + /// * `value` - Expected value in the payload (automatically deserialized). + /// + /// In production, you must always check if the `from_address` is + /// a contract you allowed to send messages, as any contract from L1 + /// can send message to any contract on L2 and vice-versa. + /// + /// In this example, the payload is expected to be a single felt value. But it can be any + /// deserializable struct written in cairo. + #[l1_handler] + fn msg_handler_value(ref self: ContractState, from_address: felt252, value: felt252) { + // assert(from_address == ...); + + assert(value == 123, 'Invalid value'); + } + + /// Handles a message received from L1. + /// + /// # Arguments + /// + /// * `from_address` - The L1 contract sending the message. + /// * `data` - Expected data in the payload (automatically deserialized). + #[l1_handler] + fn msg_handler_struct(ref self: ContractState, from_address: felt252, data: MyData) { + // assert(from_address == ...); + + assert(data.a == 1, 'data.a is invalid'); + assert(data.b == 2, 'data.b is invalid'); + } + + #[external(v0)] + impl ContractL1Impl of IContractL1 { + fn send_message_value(ref self: ContractState, to_address: EthAddress, value: felt252) { + // Note here, we "serialized" the felt252 value. + starknet::send_message_to_l1_syscall(to_address.into(), array![value].span()) + .unwrap_syscall(); + } + + fn send_message_struct(ref self: ContractState, to_address: EthAddress, data: MyData) { + let mut buf: Array = array![]; + data.serialize(ref buf); + starknet::send_message_to_l1_syscall(to_address.into(), buf.span()).unwrap_syscall(); + } + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/src/contract_msg_starknet.cairo b/crates/katana/core/contracts/messaging/cairo/src/contract_msg_starknet.cairo new file mode 100644 index 0000000000..e228eaa696 --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/src/contract_msg_starknet.cairo @@ -0,0 +1,78 @@ +//! A simple contract aims at being deployed on an appchain +//! to send/receive messages from Starknet. +//! +//! This contract can sends messages using the send message to l1 +//! syscall as we normally do for messaging. +//! +//! If the message contains a `to_address` that is not zero, the message +//! hash will be sent to starknet to be registered. +//! If the `to_address` is zero, then the message will then fire a transaction +//! on the starknet to directly execute the message content. +use starknet::ContractAddress; + +#[starknet::interface] +trait IContractAppchain { + /// Sends a message to Starknet contract with a single felt252 value. + /// This message will simply be registered on starknet to be then consumed + /// manually. + /// + /// # Arguments + /// + /// * `to_address` - Contract address on Starknet. + /// * `value` - Value to be sent in the payload. + fn send_message(ref self: T, to_address: ContractAddress, value: felt252); + + /// Executes a message on Starknet. When the Katana will see this message + /// with `to_address` set to 0, an invoke transaction will be fired. + /// So basically this function can invoke any contract on Starknet, the fees on starknet + /// being paid by the sequencer. We can here imagine several scenarios. :) + /// The invoke though is not directly done to the destination contract, but the + /// app messaging contract that will forward the execution. + /// + /// # Arguments + /// + /// * `to_address` - Contract address on Starknet. + /// * `selector` - Selector. + /// * `value` - Value to be sent as argument to the contract being executed on starknet. + fn execute_message(ref self: T, to_address: ContractAddress, selector: felt252, value: felt252); +} + +#[starknet::contract] +mod contract_msg_starknet { + use super::IContractAppchain; + use starknet::{ContractAddress, SyscallResultTrait}; + + #[storage] + struct Storage {} + + /// Handles a message received from Starknet. + /// + /// Only functions that are #[l1_handler] can + /// receive message from Starknet, exactly as we do with L1 messaging. + /// + /// # Arguments + /// + /// * `from_address` - The Starknet contract sending the message. + /// * `value` - Expected value in the payload (automatically deserialized). + #[l1_handler] + fn msg_handler_value(ref self: ContractState, from_address: felt252, value: felt252) { + // assert(from_address == ...); + + assert(value == 888, 'Invalid value'); + } + + #[external(v0)] + impl ContractAppChainImpl of IContractAppchain { + fn send_message(ref self: ContractState, to_address: ContractAddress, value: felt252) { + let buf: Array = array![to_address.into(), value]; + starknet::send_message_to_l1_syscall('MSG', buf.span()).unwrap_syscall(); + } + + fn execute_message( + ref self: ContractState, to_address: ContractAddress, selector: felt252, value: felt252, + ) { + let buf: Array = array![to_address.into(), selector, value]; + starknet::send_message_to_l1_syscall('EXE', buf.span()).unwrap_syscall(); + } + } +} diff --git a/crates/katana/core/contracts/messaging/cairo/src/lib.cairo b/crates/katana/core/contracts/messaging/cairo/src/lib.cairo new file mode 100644 index 0000000000..eb3909d98d --- /dev/null +++ b/crates/katana/core/contracts/messaging/cairo/src/lib.cairo @@ -0,0 +1,4 @@ +mod appchain_messaging; +mod contract_msg_l1; +mod contract_msg_starknet; +mod contract_1; diff --git a/crates/katana/core/contracts/messaging/l3.messaging.json b/crates/katana/core/contracts/messaging/l3.messaging.json new file mode 100644 index 0000000000..58a5264fc0 --- /dev/null +++ b/crates/katana/core/contracts/messaging/l3.messaging.json @@ -0,0 +1,9 @@ +{ + "chain": "starknet", + "rpc_url": "http://127.0.0.1:5050", + "contract_address": "0x046c0ea3fb2ad27053e8af3c8cfab38a51afb9fe90fcab1f75446bd41f7d3796", + "sender_address": "0x5686a647a9cdd63ade617e0baf3b364856b813b508f03903eb58a7e622d5855", + "private_key": "0x33003003001800009900180300d206308b0070db00121318d17b5e6262150b", + "interval": 2, + "from_block": 0 +} diff --git a/crates/katana/core/contracts/messaging/solidity/.anvil.env b/crates/katana/core/contracts/messaging/solidity/.anvil.env new file mode 100644 index 0000000000..3228cb5583 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/.anvil.env @@ -0,0 +1,7 @@ +# General config. +ETH_RPC_URL=http://127.0.0.1:8545 +ETHERSCAN_API_KEY=0x1 + +# Account related variables (EOA account). +ACCOUNT_PRIVATE_KEY=0xac0974bec39a17e36ba4a6b4d238ff944bacb478cbed5efcae784d7bf4f2ff80 +ACCOUNT_ADDRESS=0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266 diff --git a/crates/katana/core/contracts/messaging/solidity/.gitignore b/crates/katana/core/contracts/messaging/solidity/.gitignore new file mode 100644 index 0000000000..bf3041c954 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/.gitignore @@ -0,0 +1,15 @@ +# Compiler files +cache/ +out/ + +# Ignores development broadcast logs +!/broadcast +/broadcast/*/31337/ +/broadcast/**/dry-run/ + +# Docs +docs/ + +# Dotenv file +.env +logs diff --git a/crates/katana/core/contracts/messaging/solidity/IStarknetMessagingLocal_ABI.json b/crates/katana/core/contracts/messaging/solidity/IStarknetMessagingLocal_ABI.json new file mode 100644 index 0000000000..b3e03b4cf0 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/IStarknetMessagingLocal_ABI.json @@ -0,0 +1,15 @@ +[ + { + "inputs": [ + { + "internalType": "uint256[]", + "name": "msgHashes", + "type": "uint256[]" + } + ], + "name": "addMessageHashesFromL2", + "outputs": [], + "stateMutability": "payable", + "type": "function" + } +] diff --git a/crates/katana/core/contracts/messaging/solidity/Makefile b/crates/katana/core/contracts/messaging/solidity/Makefile new file mode 100644 index 0000000000..cb6e611abc --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/Makefile @@ -0,0 +1,35 @@ +# Only .env file is loaded by foundry, and we can't specify a file. +# Do not forget to copy your config into `.env`. + +# For dev, we always take anvil config. +COPY_CONFIG:=$(shell cp .anvil.env .env) + +include .env +export $(shell sed 's/=.*//' .env) + +# Addresses fixed here for easy testing. +C_MSG_L2_ADDR=0xe7f1725E7734CE288F8367e1Bb143E90bb3F0512 +L2_ACCOUNT=0x517ececd29116499f4a1b64b094da79ba08dfd54a3edaa316134c41f8160973 +L2_CONTRACT_ADDR=0x0429a64d97c1422a37a09fc7406f35c264be59b744aaff5a79d59393eb1bc7e1 + +deploy_messaging_contracts: + forge script --broadcast --rpc-url ${ETH_RPC_URL} script/LocalTesting.s.sol:LocalSetup + +send_msg_usage: + @echo make send_msg selector_str=func_name payload=\"[1,2]\" + +send_msg: + $(eval selector=$(shell starkli selector $(selector_str))) + cast send ${C_MSG_L2_ADDR} \ + "sendMessage(uint256,uint256,uint256[])" \ + ${L2_CONTRACT_ADDR} ${selector} $(payload) \ + --private-key ${ACCOUNT_PRIVATE_KEY} --value 1 + +consume_msg_usage: + @echo make consume_msg payload=\"[1,2]\" + +consume_msg: + cast send ${C_MSG_L2_ADDR} \ + "consumeMessage(uint256,uint256[])" \ + ${L2_ACCOUNT} $(payload) \ + --private-key ${ACCOUNT_PRIVATE_KEY} diff --git a/crates/katana/core/contracts/messaging/solidity/README.md b/crates/katana/core/contracts/messaging/solidity/README.md new file mode 100644 index 0000000000..931acc023f --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/README.md @@ -0,0 +1,29 @@ +## Foundry + +Please install [foundry](https://github.com/foundry-rs/foundry) before starting. + +To deploy contracts please consider the following: + +0. Run anvil: `anvil`. +1. Copy `.anvil.env` into `.env`. +2. `source .env` +3. `forge script --broadcast --rpc-url ${ETH_RPC_URL} script/LocalTesting.s.sol:LocalSetup` + +You should now have a json file into `logs` folder with the deployed addresses. + +To interact with the node, you can use the Makefile for better UX. +If you need more customization, please check the Makefile to see the commands. + +If a command in the makefile requires argument, please use the associated `*_usage`. + +If you want to check the logs emitted by the contracts, run `cast logs`. + +Note, starknet core contract is expected at least 30k wei to work. So you must +always send a value when calling a function that will send a message to L2. + +If the message is not ready yet to be consumed, you should see an error like this +using: + +``` +(code: 3, message: execution reverted: INVALID_MESSAGE_TO_CONSUME, data: Some(String("0x08c379a00000000000000000000000000000000000000000000000000000000000000020000000000000000000000000000000000000000000000000000000000000001a494e56414c49445f4d4553534147455f544f5f434f4e53554d45000000000000"))) +``` diff --git a/crates/katana/core/contracts/messaging/solidity/foundry.toml b/crates/katana/core/contracts/messaging/solidity/foundry.toml new file mode 100644 index 0000000000..eaeb866692 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/foundry.toml @@ -0,0 +1,7 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] +fs_permissions = [{ access = "read-write", path = "./logs"}] + +# See more config options https://github.com/foundry-rs/foundry/tree/master/config diff --git a/crates/katana/core/contracts/messaging/solidity/lib/forge-std b/crates/katana/core/contracts/messaging/solidity/lib/forge-std new file mode 160000 index 0000000000..74cfb77e30 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/lib/forge-std @@ -0,0 +1 @@ +Subproject commit 74cfb77e308dd188d2f58864aaf44963ae6b88b1 diff --git a/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessaging.sol b/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessaging.sol new file mode 100644 index 0000000000..b3ebea4bc2 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessaging.sol @@ -0,0 +1,76 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessagingEvents.sol"; + +interface IStarknetMessaging is IStarknetMessagingEvents { + /** + Returns the max fee (in Wei) that StarkNet will accept per single message. + */ + function getMaxL1MsgFee() external pure returns (uint256); + + /** + Sends a message to an L2 contract. + This function is payable, the payed amount is the message fee. + + Returns the hash of the message and the nonce of the message. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable returns (bytes32, uint256); + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + returns (bytes32); + + /** + Starts the cancellation of an L1 to L2 message. + A message can be canceled messageCancellationDelay() seconds after this function is called. + + Note: This function may only be called for a message that is currently pending and the caller + must be the sender of the that message. + */ + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); + + /** + Cancels an L1 to L2 message, this function should be called at least + messageCancellationDelay() seconds after the call to startL1ToL2MessageCancellation(). + A message may only be cancelled by its sender. + If the message is missing, the call will revert. + + Note that the message fee is not refunded. + */ + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external returns (bytes32); +} diff --git a/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessagingEvents.sol b/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessagingEvents.sol new file mode 100644 index 0000000000..11937727be --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/lib/starknet/IStarknetMessagingEvents.sol @@ -0,0 +1,66 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +interface IStarknetMessagingEvents { + // This event needs to be compatible with the one defined in Output.sol. + event LogMessageToL1(uint256 indexed fromAddress, address indexed toAddress, uint256[] payload); + + // An event that is raised when a message is sent from L1 to L2. + event LogMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce, + uint256 fee + ); + + // An event that is raised when a message from L2 to L1 is consumed. + event ConsumedMessageToL1( + uint256 indexed fromAddress, + address indexed toAddress, + uint256[] payload + ); + + // An event that is raised when a message from L1 to L2 is consumed. + event ConsumedMessageToL2( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 Cancellation is started. + event MessageToL2CancellationStarted( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); + + // An event that is raised when a message from L1 to L2 is canceled. + event MessageToL2Canceled( + address indexed fromAddress, + uint256 indexed toAddress, + uint256 indexed selector, + uint256[] payload, + uint256 nonce + ); +} diff --git a/crates/katana/core/contracts/messaging/solidity/lib/starknet/NamedStorage.sol b/crates/katana/core/contracts/messaging/solidity/lib/starknet/NamedStorage.sol new file mode 100644 index 0000000000..5279f380f5 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/lib/starknet/NamedStorage.sol @@ -0,0 +1,120 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +/* + Library to provide basic storage, in storage location out of the low linear address space. + + New types of storage variables should be added here upon need. +*/ +library NamedStorage { + function bytes32ToUint256Mapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => uint256) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function bytes32ToAddressMapping(string memory tag_) + internal + pure + returns (mapping(bytes32 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function uintToAddressMapping(string memory tag_) + internal + pure + returns (mapping(uint256 => address) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function addressToBoolMapping(string memory tag_) + internal + pure + returns (mapping(address => bool) storage randomVariable) + { + bytes32 location = keccak256(abi.encodePacked(tag_)); + assembly { + randomVariable.slot := location + } + } + + function getUintValue(string memory tag_) internal view returns (uint256 retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setUintValue(string memory tag_, uint256 value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setUintValueOnce(string memory tag_, uint256 value) internal { + require(getUintValue(tag_) == 0, "ALREADY_SET"); + setUintValue(tag_, value); + } + + function getAddressValue(string memory tag_) internal view returns (address retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setAddressValue(string memory tag_, address value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } + + function setAddressValueOnce(string memory tag_, address value) internal { + require(getAddressValue(tag_) == address(0x0), "ALREADY_SET"); + setAddressValue(tag_, value); + } + + function getBoolValue(string memory tag_) internal view returns (bool retVal) { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + retVal := sload(slot) + } + } + + function setBoolValue(string memory tag_, bool value) internal { + bytes32 slot = keccak256(abi.encodePacked(tag_)); + assembly { + sstore(slot, value) + } + } +} diff --git a/crates/katana/core/contracts/messaging/solidity/lib/starknet/StarknetMessaging.sol b/crates/katana/core/contracts/messaging/solidity/lib/starknet/StarknetMessaging.sol new file mode 100644 index 0000000000..c8a1a63c78 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/lib/starknet/StarknetMessaging.sol @@ -0,0 +1,202 @@ +/* + Copyright 2019-2022 StarkWare Industries Ltd. + + Licensed under the Apache License, Version 2.0 (the "License"). + You may not use this file except in compliance with the License. + You may obtain a copy of the License at + + https://www.starkware.co/open-source-license/ + + Unless required by applicable law or agreed to in writing, + software distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions + and limitations under the License. +*/ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "./IStarknetMessaging.sol"; +import "./NamedStorage.sol"; + +/** + Implements sending messages to L2 by adding them to a pipe and consuming messages from L2 by + removing them from a different pipe. A deriving contract can handle the former pipe and add items + to the latter pipe while interacting with L2. +*/ +contract StarknetMessaging is IStarknetMessaging { + /* + Random slot storage elements and accessors. + */ + string constant L1L2_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L1TOL2_MAPPPING_V2"; + string constant L2L1_MESSAGE_MAP_TAG = "STARKNET_1.0_MSGING_L2TOL1_MAPPPING"; + + string constant L1L2_MESSAGE_NONCE_TAG = "STARKNET_1.0_MSGING_L1TOL2_NONCE"; + + string constant L1L2_MESSAGE_CANCELLATION_MAP_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_MAPPPING" + ); + + string constant L1L2_MESSAGE_CANCELLATION_DELAY_TAG = ( + "STARKNET_1.0_MSGING_L1TOL2_CANCELLATION_DELAY" + ); + + uint256 constant MAX_L1_MSG_FEE = 1 ether; + + function getMaxL1MsgFee() public pure override returns (uint256) { + return MAX_L1_MSG_FEE; + } + + /** + Returns the msg_fee + 1 for the message with the given 'msgHash', + or 0 if no message with such a hash is pending. + */ + function l1ToL2Messages(bytes32 msgHash) external view returns (uint256) { + return l1ToL2Messages()[msgHash]; + } + + function l2ToL1Messages(bytes32 msgHash) external view returns (uint256) { + return l2ToL1Messages()[msgHash]; + } + + function l1ToL2Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_MAP_TAG); + } + + function l2ToL1Messages() internal pure returns (mapping(bytes32 => uint256) storage) { + return NamedStorage.bytes32ToUint256Mapping(L2L1_MESSAGE_MAP_TAG); + } + + function l1ToL2MessageNonce() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_NONCE_TAG); + } + + function messageCancellationDelay() public view returns (uint256) { + return NamedStorage.getUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG); + } + + function messageCancellationDelay(uint256 delayInSeconds) internal { + NamedStorage.setUintValue(L1L2_MESSAGE_CANCELLATION_DELAY_TAG, delayInSeconds); + } + + /** + Returns the timestamp at the time cancelL1ToL2Message was called with a message + matching 'msgHash'. + + The function returns 0 if cancelL1ToL2Message was never called. + */ + function l1ToL2MessageCancellations(bytes32 msgHash) external view returns (uint256) { + return l1ToL2MessageCancellations()[msgHash]; + } + + function l1ToL2MessageCancellations() + internal + pure + returns (mapping(bytes32 => uint256) storage) + { + return NamedStorage.bytes32ToUint256Mapping(L1L2_MESSAGE_CANCELLATION_MAP_TAG); + } + + /** + Returns the hash of an L1 -> L2 message from msg.sender. + */ + function getL1ToL2MsgHash( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) internal view returns (bytes32) { + return + keccak256( + abi.encodePacked( + // MODIFIED HERE: adding uint160 for casting. + uint256(uint160(msg.sender)), + toAddress, + nonce, + selector, + payload.length, + payload + ) + ); + } + + /** + Sends a message to an L2 contract. + */ + function sendMessageToL2( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload + ) external payable override returns (bytes32, uint256) { + require(msg.value > 0, "L1_MSG_FEE_MUST_BE_GREATER_THAN_0"); + require(msg.value <= getMaxL1MsgFee(), "MAX_L1_MSG_FEE_EXCEEDED"); + uint256 nonce = l1ToL2MessageNonce(); + NamedStorage.setUintValue(L1L2_MESSAGE_NONCE_TAG, nonce + 1); + emit LogMessageToL2(msg.sender, toAddress, selector, payload, nonce, msg.value); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + // Note that the inclusion of the unique nonce in the message hash implies that + // l1ToL2Messages()[msgHash] was not accessed before. + l1ToL2Messages()[msgHash] = msg.value + 1; + return (msgHash, nonce); + } + + /** + Consumes a message that was sent from an L2 contract. + + Returns the hash of the message. + */ + function consumeMessageFromL2(uint256 fromAddress, uint256[] calldata payload) + external + override + returns (bytes32) + { + bytes32 msgHash = keccak256( + // MODIFIED HERE: adding uint160 for casting. + abi.encodePacked(fromAddress, uint256(uint160(msg.sender)), payload.length, payload) + ); + + require(l2ToL1Messages()[msgHash] > 0, "INVALID_MESSAGE_TO_CONSUME"); + emit ConsumedMessageToL1(fromAddress, msg.sender, payload); + l2ToL1Messages()[msgHash] -= 1; + return msgHash; + } + + function startL1ToL2MessageCancellation( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2CancellationStarted(msg.sender, toAddress, selector, payload, nonce); + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne > 0, "NO_MESSAGE_TO_CANCEL"); + l1ToL2MessageCancellations()[msgHash] = block.timestamp; + return msgHash; + } + + function cancelL1ToL2Message( + uint256 toAddress, + uint256 selector, + uint256[] calldata payload, + uint256 nonce + ) external override returns (bytes32) { + emit MessageToL2Canceled(msg.sender, toAddress, selector, payload, nonce); + // Note that the message hash depends on msg.sender, which prevents one contract from + // cancelling another contract's message. + // Trying to do so will result in NO_MESSAGE_TO_CANCEL. + bytes32 msgHash = getL1ToL2MsgHash(toAddress, selector, payload, nonce); + uint256 msgFeePlusOne = l1ToL2Messages()[msgHash]; + require(msgFeePlusOne != 0, "NO_MESSAGE_TO_CANCEL"); + + uint256 requestTime = l1ToL2MessageCancellations()[msgHash]; + require(requestTime != 0, "MESSAGE_CANCELLATION_NOT_REQUESTED"); + + uint256 cancelAllowedTime = requestTime + messageCancellationDelay(); + require(cancelAllowedTime >= requestTime, "CANCEL_ALLOWED_TIME_OVERFLOW"); + require(block.timestamp >= cancelAllowedTime, "MESSAGE_CANCELLATION_NOT_ALLOWED_YET"); + + l1ToL2Messages()[msgHash] = 0; + return (msgHash); + } +} diff --git a/crates/katana/core/contracts/messaging/solidity/script/LocalTesting.s.sol b/crates/katana/core/contracts/messaging/solidity/script/LocalTesting.s.sol new file mode 100644 index 0000000000..1d4a2a35bd --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/script/LocalTesting.s.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: UNLICENSED +pragma solidity ^0.8.0; + +import "forge-std/Script.sol"; + +import "src/Contract1.sol"; +import "src/StarknetMessagingLocal.sol"; + + +/** + Deploys the Contract1 and StarknetMessagingLocal contracts. +*/ +contract LocalSetup is Script { + function setUp() public {} + + function run() public{ + uint256 deployerPrivateKey = vm.envUint("ACCOUNT_PRIVATE_KEY"); + + string memory json = "local_testing"; + + vm.startBroadcast(deployerPrivateKey); + + address snLocalAddress = address(new StarknetMessagingLocal()); + vm.serializeString(json, "sncore_address", vm.toString(snLocalAddress)); + + address contract1 = address(new Contract1(snLocalAddress)); + vm.serializeString(json, "contract1_address", vm.toString(contract1)); + + vm.stopBroadcast(); + + string memory data = vm.serializeBool(json, "success", true); + + string memory localLogs = "./logs/"; + vm.createDir(localLogs, true); + vm.writeJson(data, string.concat(localLogs, "local_setup.json")); + } +} diff --git a/crates/katana/core/contracts/messaging/solidity/src/Contract1.sol b/crates/katana/core/contracts/messaging/solidity/src/Contract1.sol new file mode 100644 index 0000000000..2dd4c21967 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/src/Contract1.sol @@ -0,0 +1,77 @@ +// SPDX-License-Identifier: MIT + +pragma solidity ^0.8.0; + +import "starknet/IStarknetMessaging.sol"; + +/** + @title Test contract to receive / send messages to starknet. +*/ +contract Contract1 { + + // + IStarknetMessaging private _snMessaging; + + /** + @notice Constructor. + + @param snMessaging The address of Starknet Core contract, responsible + or messaging. + */ + constructor(address snMessaging) { + _snMessaging = IStarknetMessaging(snMessaging); + } + + event DebugEvent( + bytes32 indexed hash1, + uint256 indexed hash2 + ); + + /** + @notice Sends a message to Starknet contract. + + @param contractAddress The contract's address on starknet. + @param selector The l1_handler function of the contract to call. + @param payload The serialized data to be sent. + + @dev Consider that Cairo only understands felts252. + So the serialization on solidity must be adjusted. For instance, a uint256 + must be split in two uint256 with low and high part to be understood by Cairo. + */ + function sendMessage( + uint256 contractAddress, + uint256 selector, + uint256[] memory payload + ) + external + payable + { + _snMessaging.sendMessageToL2{value: msg.value}( + contractAddress, + selector, + payload + ); + } + + /** + @notice Manually consumes a message that was received from L2. + + @param fromAddress L2 contract (account) that has sent the message. + @param payload Payload of the message used to verify the hash. + + @dev A message "receive" means that the message hash is registered as consumable. + One must provide the message content, to let Starknet Core contract verify the hash + and validate the message consumption. + */ + function consumeMessage( + uint256 fromAddress, + uint256[] calldata payload + ) + external + { + // Will revert if the message is not consumable. + _snMessaging.consumeMessageFromL2(fromAddress, payload); + + // The previous call returns the message hash (bytes32). + } +} diff --git a/crates/katana/core/contracts/messaging/solidity/src/StarknetMessagingLocal.sol b/crates/katana/core/contracts/messaging/solidity/src/StarknetMessagingLocal.sol new file mode 100644 index 0000000000..9563f4f845 --- /dev/null +++ b/crates/katana/core/contracts/messaging/solidity/src/StarknetMessagingLocal.sol @@ -0,0 +1,59 @@ +// SPDX-License-Identifier: Apache-2.0. +pragma solidity ^0.8.0; + +import "starknet/StarknetMessaging.sol"; + +/** + @notice Interface related to local messaging for Starknet. +*/ +interface IStarknetMessagingLocal { + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable; +} + +/** + @title A superset of StarknetMessaging to support + local development by adding a way to directly register + a message hash ready to be consumed, without waiting the block + to be verified. + + @dev The idea is that, to not wait on the block to be proved, + this messaging contract can receive directly a hash of a message + to be considered as `received`. This message can then be consumed. + + DISCLAIMER: + The purpose of this contract is for local development only. +*/ +contract StarknetMessagingLocal is StarknetMessaging, IStarknetMessagingLocal { + + /** + @notice Hashes were added. + */ + event MessageHashesAddedFromL2( + uint256[] hashes + ); + + /** + @notice Adds the hashes of messages from L2. + + @param msgHashes Hashes to register as consumable. + */ + function addMessageHashesFromL2( + uint256[] calldata msgHashes + ) + external + payable + { + // TODO: You can add here a whitelist of senders if you wish. + for (uint256 i = 0; i < msgHashes.length; i++) { + bytes32 hash = bytes32(msgHashes[i]); + l2ToL1Messages()[hash] += 1; + } + + emit MessageHashesAddedFromL2(msgHashes); + } + +} diff --git a/crates/katana/core/src/execution.rs b/crates/katana/core/src/execution.rs index fddff2d714..fd599ffbdf 100644 --- a/crates/katana/core/src/execution.rs +++ b/crates/katana/core/src/execution.rs @@ -21,6 +21,7 @@ use crate::backend::storage::transaction::{ }; use crate::db::cached::CachedStateWrapper; use crate::db::{Database, StateExt, StateRefDb}; +use crate::utils::transaction::warn_message_transaction_error_exec_error; /// The outcome that after executing a list of transactions. pub struct ExecutionOutcome { @@ -198,7 +199,7 @@ impl<'a> TransactionExecutor<'a> { Err(err) => { if self.error_log { - warn!(target: "executor", "Transaction validation error: {err:?}"); + warn_message_transaction_error_exec_error(&err); } Err(err) diff --git a/crates/katana/core/src/sequencer.rs b/crates/katana/core/src/sequencer.rs index 59e56f650f..a510fd3968 100644 --- a/crates/katana/core/src/sequencer.rs +++ b/crates/katana/core/src/sequencer.rs @@ -27,6 +27,10 @@ use crate::execution::{MaybeInvalidExecutedTransaction, PendingState}; use crate::pool::TransactionPool; use crate::sequencer_error::SequencerError; use crate::service::block_producer::{BlockProducer, BlockProducerMode}; +#[cfg(feature = "messaging")] +use crate::service::messaging::MessagingConfig; +#[cfg(feature = "messaging")] +use crate::service::messaging::MessagingService; use crate::service::{NodeService, TransactionMiner}; use crate::utils::event::{ContinuationToken, ContinuationTokenError}; @@ -36,6 +40,8 @@ type SequencerResult = Result; pub struct SequencerConfig { pub block_time: Option, pub no_mining: bool, + #[cfg(feature = "messaging")] + pub messaging: Option, } pub struct KatanaSequencer { @@ -64,7 +70,20 @@ impl KatanaSequencer { BlockProducer::instant(Arc::clone(&backend)) }; - tokio::spawn(NodeService::new(Arc::clone(&pool), miner, block_producer.clone())); + #[cfg(feature = "messaging")] + let messaging = if let Some(config) = config.messaging.clone() { + MessagingService::new(config, Arc::clone(&pool), Arc::clone(&backend)).await.ok() + } else { + None + }; + + tokio::spawn(NodeService { + miner, + pool: Arc::clone(&pool), + block_producer: block_producer.clone(), + #[cfg(feature = "messaging")] + messaging, + }); Self { pool, config, backend, block_producer } } diff --git a/crates/katana/core/src/service/messaging/ethereum.rs b/crates/katana/core/src/service/messaging/ethereum.rs new file mode 100644 index 0000000000..d033bcd662 --- /dev/null +++ b/crates/katana/core/src/service/messaging/ethereum.rs @@ -0,0 +1,365 @@ +use std::collections::HashMap; +use std::str::FromStr; +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use ethers::prelude::*; +use ethers::providers::{Http, Provider}; +use ethers::types::{Address, BlockNumber, Log}; +use k256::ecdsa::SigningKey; +use sha3::{Digest, Keccak256}; +use starknet::core::types::{FieldElement, MsgToL1}; +use starknet_api::core::{ContractAddress, EntryPointSelector, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::stark_felt; +use starknet_api::transaction::{ + Calldata, L1HandlerTransaction as ApiL1HandlerTransaction, TransactionHash, TransactionVersion, +}; +use tracing::{debug, error, trace, warn}; + +use super::{Error, MessagingConfig, Messenger, MessengerResult, LOG_TARGET}; +use crate::backend::storage::transaction::L1HandlerTransaction; +use crate::utils::transaction::compute_l1_handler_transaction_hash; + +abigen!( + StarknetMessagingLocal, + "contracts/messaging/solidity/IStarknetMessagingLocal_ABI.json", + event_derives(serde::Serialize, serde::Deserialize) +); + +#[derive(Debug, PartialEq, Eq, EthEvent)] +pub struct LogMessageToL2 { + #[ethevent(indexed)] + from_address: Address, + #[ethevent(indexed)] + to_address: U256, + #[ethevent(indexed)] + selector: U256, + payload: Vec, + nonce: U256, + fee: U256, +} + +pub struct EthereumMessaging { + provider: Arc>, + provider_signer: Arc, Wallet>>, + messaging_contract_address: Address, +} + +impl EthereumMessaging { + pub async fn new(config: MessagingConfig) -> Result { + let provider = Provider::::try_from(&config.rpc_url)?; + + let chain_id = provider.get_chainid().await?; + + let wallet: LocalWallet = + config.private_key.parse::()?.with_chain_id(chain_id.as_u32()); + + let provider_signer = SignerMiddleware::new(provider.clone(), wallet); + let messaging_contract_address = Address::from_str(&config.contract_address)?; + + Ok(EthereumMessaging { + provider: Arc::new(provider), + provider_signer: Arc::new(provider_signer), + messaging_contract_address, + }) + } + + /// Fetches logs in given block range and returns a `HashMap` with the list of logs mapped to + /// their block number. + /// + /// There is not pagination in ethereum, and no hard limit on block range. + /// Fetching too much block may result in RPC request error. + /// For this reason, the caller may wisely choose the range. + /// + /// # Arguments + /// + /// * `from_block` - The first block of which logs must be fetched. + /// * `to_block` - The last block of which logs must be fetched. + pub async fn fetch_logs( + &self, + from_block: u64, + to_block: u64, + ) -> MessengerResult>> { + trace!(target: LOG_TARGET, "Fetching logs for blocks {} - {}.", from_block, to_block); + + let mut block_to_logs: HashMap> = HashMap::new(); + + let log_msg_to_l2_topic = + H256::from_str("0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b") + .unwrap(); + + let filters = Filter { + block_option: FilterBlockOption::Range { + from_block: Some(BlockNumber::Number(from_block.into())), + to_block: Some(BlockNumber::Number(to_block.into())), + }, + address: Some(ValueOrArray::Value(self.messaging_contract_address)), + topics: [Some(ValueOrArray::Value(Some(log_msg_to_l2_topic))), None, None, None], + }; + + self.provider + .get_logs(&filters) + .await? + .into_iter() + .filter(|log| log.block_number.is_some()) + .map(|log| { + ( + log.block_number + .unwrap() + .try_into() + .expect("Block number couldn't be converted to u64."), + log, + ) + }) + .for_each(|(block_num, log)| { + block_to_logs + .entry(block_num) + .and_modify(|v| v.push(log.clone())) + .or_insert(vec![log]); + }); + + Ok(block_to_logs) + } +} + +#[async_trait] +impl Messenger for EthereumMessaging { + type MessageHash = U256; + type MessageTransaction = L1HandlerTransaction; + + async fn gather_messages( + &self, + from_block: u64, + max_blocks: u64, + chain_id: FieldElement, + ) -> MessengerResult<(u64, Vec)> { + let chain_latest_block: u64 = self + .provider + .get_block_number() + .await? + .try_into() + .expect("Can't convert latest block number into u64."); + + // +1 as the from_block counts as 1 block fetched. + let to_block = if from_block + max_blocks + 1 < chain_latest_block { + from_block + max_blocks + } else { + chain_latest_block + }; + + let mut l1_handler_txs = vec![]; + + self.fetch_logs(from_block, to_block).await?.into_iter().for_each( + |(block_number, block_logs)| { + debug!( + target: LOG_TARGET, + "Converting logs of block {block_number} into L1HandlerTx ({} logs)", + block_logs.len(), + ); + + block_logs.into_iter().for_each(|log| { + if let Ok(tx) = l1_handler_tx_from_log(log, chain_id) { + l1_handler_txs.push(tx) + } + }) + }, + ); + + Ok((to_block, l1_handler_txs)) + } + + async fn send_messages(&self, messages: &[MsgToL1]) -> MessengerResult> { + if messages.is_empty() { + return Ok(vec![]); + } + + let starknet_messaging = StarknetMessagingLocal::new( + self.messaging_contract_address, + self.provider_signer.clone(), + ); + + let hashes = parse_messages(messages); + + debug!("Sending transaction on L1 to register messages..."); + match starknet_messaging + .add_message_hashes_from_l2(hashes.clone()) + .send() + .await + .map_err(|_| Error::SendError)? + // wait for the tx to be mined + .await? + { + Some(receipt) => { + trace!( + target: LOG_TARGET, + "Transaction sent on L1 to register {} messages: {:#x}", + hashes.len(), + receipt.transaction_hash, + ); + + Ok(hashes) + } + None => { + warn!(target: LOG_TARGET, "No receipt for L1 transaction."); + Err(Error::SendError) + } + } + } +} + +fn l1_handler_tx_from_log( + log: Log, + chain_id: FieldElement, +) -> MessengerResult { + let parsed_log = ::decode_log(&log.into()).map_err(|e| { + error!(target: LOG_TARGET, "Log parsing failed {e}"); + Error::GatherError + })?; + + let from_address = stark_felt_from_address(parsed_log.from_address); + let contract_address = stark_felt_from_u256(parsed_log.to_address); + let selector = stark_felt_from_u256(parsed_log.selector); + let nonce = stark_felt_from_u256(parsed_log.nonce); + let paid_l1_fee: u128 = parsed_log.fee.try_into().expect("Fee does not fit into u128."); + + let mut calldata_vec = vec![from_address]; + calldata_vec.extend(parsed_log.payload.into_iter().map(stark_felt_from_u256)); + + let mut inner = ApiL1HandlerTransaction { + nonce: Nonce(nonce), + calldata: Calldata(calldata_vec.into()), + transaction_hash: TransactionHash::default(), + version: TransactionVersion(stark_felt!(0_u32)), + entry_point_selector: EntryPointSelector(selector), + contract_address: ContractAddress::try_from(contract_address).unwrap(), + }; + + inner.transaction_hash = + TransactionHash(compute_l1_handler_transaction_hash(inner.clone(), chain_id).into()); + + let tx = L1HandlerTransaction { paid_l1_fee, inner }; + + Ok(tx) +} + +/// With Ethereum, the messages are following the conventional starknet messaging. +fn parse_messages(messages: &[MsgToL1]) -> Vec { + messages + .iter() + .map(|msg| { + let mut buf: Vec = vec![]; + buf.extend(msg.from_address.to_bytes_be()); + buf.extend(msg.to_address.to_bytes_be()); + buf.extend(FieldElement::from(msg.payload.len()).to_bytes_be()); + msg.payload.iter().for_each(|p| buf.extend(p.to_bytes_be())); + + let mut hasher = Keccak256::new(); + hasher.update(buf); + let hash = hasher.finalize(); + let hash_bytes = hash.as_slice(); + U256::from_big_endian(hash_bytes) + }) + .collect() +} + +fn stark_felt_from_u256(v: U256) -> StarkFelt { + stark_felt!(format!("{:#064x}", v).as_str()) +} + +fn stark_felt_from_address(v: Address) -> StarkFelt { + stark_felt!(format!("{:#064x}", v).as_str()) +} + +#[cfg(test)] +mod tests { + + use starknet::macros::selector; + + use super::*; + + #[test] + fn l1_handler_tx_from_log_parse_ok() { + let from_address = "0x000000000000000000000000be3C44c09bc1a3566F3e1CA12e5AbA0fA4Ca72Be"; + let to_address = "0x039dc79e64f4bb3289240f88e0bae7d21735bef0d1a51b2bf3c4730cb16983e1"; + let selector = "0x02f15cff7b0eed8b9beb162696cf4e3e0e35fa7032af69cd1b7d2ac67a13f40f"; + let nonce = 783082_u128; + let fee = 30000_u128; + + // Payload two values: [1, 2]. + let payload_buf = hex::decode("000000000000000000000000000000000000000000000000000000000000006000000000000000000000000000000000000000000000000000000000000bf2ea0000000000000000000000000000000000000000000000000000000000007530000000000000000000000000000000000000000000000000000000000000000200000000000000000000000000000000000000000000000000000000000000010000000000000000000000000000000000000000000000000000000000000002").unwrap(); + + let calldata: Vec = vec![ + FieldElement::from_hex_be(from_address).unwrap().into(), + FieldElement::ONE.into(), + FieldElement::TWO.into(), + ]; + + let transaction_hash: FieldElement = FieldElement::from_hex_be( + "0x6182c63599a9638272f1ce5b5cadabece9c81c2d2b8f88ab7a294472b8fce8b", + ) + .unwrap(); + + let log = Log { + address: H160::from_str("0xde29d060D45901Fb19ED6C6e959EB22d8626708e").unwrap(), + topics: vec![ + H256::from_str( + "0xdb80dd488acf86d17c747445b0eabb5d57c541d3bd7b6b87af987858e5066b2b", + ) + .unwrap(), + H256::from_str(from_address).unwrap(), + H256::from_str(to_address).unwrap(), + H256::from_str(selector).unwrap(), + ], + data: payload_buf.into(), + ..Default::default() + }; + + let expected = L1HandlerTransaction { + inner: ApiL1HandlerTransaction { + transaction_hash: TransactionHash(transaction_hash.into()), + version: TransactionVersion(stark_felt!(0_u32)), + nonce: Nonce(FieldElement::from(nonce).into()), + contract_address: ContractAddress::try_from( + >::into( + FieldElement::from_hex_be(to_address).unwrap(), + ), + ) + .unwrap(), + entry_point_selector: EntryPointSelector( + FieldElement::from_hex_be(selector).unwrap().into(), + ), + calldata: Calldata(calldata.into()), + }, + paid_l1_fee: fee, + }; + + // SN_GOERLI. + let chain_id = starknet::macros::felt!("0x534e5f474f45524c49"); + let tx: L1HandlerTransaction = + l1_handler_tx_from_log(log, chain_id).expect("bad log format"); + + assert_eq!(tx.inner, expected.inner); + } + + #[test] + fn parse_msg_to_l1() { + let from_address = selector!("from_address"); + let to_address = selector!("to_address"); + let payload = vec![FieldElement::ONE, FieldElement::TWO]; + + let messages = vec![MsgToL1 { from_address, to_address, payload }]; + + let hashes = parse_messages(&messages); + assert_eq!(hashes.len(), 1); + assert_eq!( + hashes[0], + U256::from_str_radix( + "0x5ba1d2e131360f15e26dd4f6ff10550685611cc25f75e7950b704adb04b36162", + 16 + ) + .unwrap() + ); + } +} diff --git a/crates/katana/core/src/service/messaging/mod.rs b/crates/katana/core/src/service/messaging/mod.rs new file mode 100644 index 0000000000..b6d0e3d9e2 --- /dev/null +++ b/crates/katana/core/src/service/messaging/mod.rs @@ -0,0 +1,200 @@ +//! Messaging module. +//! +//! Messaging is the capability of a sequencer to gather/send messages from/to a settlement chain. +//! By default, the messaging feature of Katana uses Ethereum as settlement chain. +//! This feature is useful to locally test the interaction of Katana used as a Starknet dev node, +//! and third party Ethereum dev node like Anvil. +//! +//! The gathering is done by fetching logs from the settlement chain to then self execute a +//! `L1HandlerTransaction`. There is no account involved to execute this transaction, fees are +//! charged on the settlement layer. +//! +//! The sending of the messages is realized by collecting all the `messages_sent` from local +//! execution of smart contracts using the `send_message_to_l1_syscall`. Once messages are +//! collected, the hash of each message is computed and then registered on the settlement layer to +//! be consumed on the latter (by manually sending a transaction on the settlement chain). The +//! hashes are registered using a custom contract that mimics the verification of Starknet state +//! updates on Ethereum, since the process of proving and verifying of state updates, and then +//! posting in on the settlement layer are not yet present in Katana. +//! +//! Katana also has a `starknet-messaging` feature, where an opiniated implementation of L2 <-> L3 +//! messaging is implemented using Starknet as settlement chain. +//! +//! With this feature, Katana also has the capability to directly send `invoke` transactions to a +//! Starknet contract. This is usually used in the L2 <-> L3 messaging configuration, to circumvent +//! the manual consumption of the message. +//! +//! In this module, the messaging service clearly separates the two implementations for each +//! settlement chain configuration in `starknet.rs` and `ethereum.rs`. The `service.rs` file aims at +//! running the common logic. +//! +//! To start Katana with the messaging enabled, the option `--messaging` must be used with a +//! configuration file following the `MessagingConfig` format. An example of this file can be found +//! in the messaging contracts. + +mod ethereum; +mod service; +#[cfg(feature = "starknet-messaging")] +mod starknet; + +use std::path::Path; + +use ::starknet::core::types::{FieldElement, MsgToL1}; +use ::starknet::providers::jsonrpc::HttpTransport; +use ::starknet::providers::{JsonRpcClient, Provider}; +use anyhow::Result; +use async_trait::async_trait; +use ethereum::EthereumMessaging; +use ethers::providers::ProviderError as EthereumProviderError; +use serde::Deserialize; +use tracing::{error, info}; + +pub use self::service::{MessagingOutcome, MessagingService}; +#[cfg(feature = "starknet-messaging")] +use self::starknet::StarknetMessaging; + +pub(crate) const LOG_TARGET: &str = "messaging"; +pub(crate) const CONFIG_CHAIN_ETHEREUM: &str = "ethereum"; +#[cfg(feature = "starknet-messaging")] +pub(crate) const CONFIG_CHAIN_STARKNET: &str = "starknet"; + +type MessengerResult = Result; + +#[derive(Debug, thiserror::Error)] +pub enum Error { + #[error("Failed to initialize messaging")] + InitError, + #[error("Unsupported settlement chain")] + UnsupportedChain, + #[error("Failed to gather messages from settlement chain")] + GatherError, + #[error("Failed to send messages to settlement chain")] + SendError, + #[error(transparent)] + Provider(ProviderError), +} + +#[derive(Debug, thiserror::Error)] +pub enum ProviderError { + #[error("Ethereum provider error: {0}")] + Ethereum(EthereumProviderError), + #[error("Starknet provider error: {0}")] + Starknet( as Provider>::Error), +} + +impl From for Error { + fn from(e: EthereumProviderError) -> Self { + Self::Provider(ProviderError::Ethereum(e)) + } +} + +/// The config used to initialize the messaging service. +#[derive(Debug, Default, Deserialize, Clone)] +pub struct MessagingConfig { + /// The settlement chain. + pub chain: String, + /// The RPC-URL of the settlement chain. + pub rpc_url: String, + /// The messaging-contract address on the settlement chain. + pub contract_address: String, + /// The address to use for settling messages. It should be a valid address that + /// can be used to initiate a transaction on the settlement chain. + pub sender_address: String, + /// The private key associated to `sender_address`. + pub private_key: String, + /// The interval, in seconds, at which the messaging service will fetch and settle messages + /// from/to the settlement chain. + pub interval: u64, + /// The block on settlement chain from where Katana will start fetching messages. + pub from_block: u64, +} + +impl MessagingConfig { + /// Load the config from a JSON file. + pub fn load(path: impl AsRef) -> Result { + let buf = std::fs::read(path)?; + serde_json::from_slice(&buf).map_err(|e| e.into()) + } + + /// This is used as the clap `value_parser` implementation + pub fn parse(path: &str) -> Result { + Self::load(path).map_err(|e| e.to_string()) + } +} + +#[async_trait] +pub trait Messenger { + /// The type of the message hash. + type MessageHash; + /// The transaction type of the message after being collected from the settlement chain. + /// This is the transaction type that the message will be converted to before being added to the + /// transaction pool. + type MessageTransaction; + + /// Gathers messages emitted on the settlement chain and convert them to their + /// corresponding transaction type on Starknet, and the latest block on the settlement until + /// which the messages were collected. + /// + /// # Arguments + /// + /// * `from_block` - From which block the messages should be gathered. + /// * `max_block` - The number of block fetched in the event/log filter. A too big value can + /// cause the RPC node to reject the query. + /// * `chain_id` - The sequencer chain id for transaction hash computation. + async fn gather_messages( + &self, + from_block: u64, + max_blocks: u64, + chain_id: FieldElement, + ) -> MessengerResult<(u64, Vec)>; + + /// Computes the hash of the given messages and sends them to the settlement chain. + /// + /// Once message's hash is settled, one must send a transaction (with the message content) + /// on the settlement chain to actually consume it. + /// + /// # Arguments + /// + /// * `messages` - Messages to settle. + async fn send_messages(&self, messages: &[MsgToL1]) -> MessengerResult>; +} + +pub enum MessengerMode { + Ethereum(EthereumMessaging), + #[cfg(feature = "starknet-messaging")] + Starknet(StarknetMessaging), +} + +impl MessengerMode { + pub async fn from_config(config: MessagingConfig) -> MessengerResult { + match config.chain.as_str() { + CONFIG_CHAIN_ETHEREUM => match EthereumMessaging::new(config).await { + Ok(m_eth) => { + info!(target: LOG_TARGET, "Messaging enabled [Ethereum]"); + Ok(MessengerMode::Ethereum(m_eth)) + } + Err(e) => { + error!(target: LOG_TARGET, "Ethereum messenger init failed: {e}"); + Err(Error::InitError) + } + }, + + #[cfg(feature = "starknet-messaging")] + CONFIG_CHAIN_STARKNET => match StarknetMessaging::new(config).await { + Ok(m_sn) => { + info!(target: LOG_TARGET, "Messaging enabled [Starknet]"); + Ok(MessengerMode::Starknet(m_sn)) + } + Err(e) => { + error!(target: LOG_TARGET, "Starknet messenger init failed: {e}"); + Err(Error::InitError) + } + }, + + chain => { + error!(target: LOG_TARGET, "Unsupported settlement chain: {}", chain); + Err(Error::UnsupportedChain) + } + } + } +} diff --git a/crates/katana/core/src/service/messaging/service.rs b/crates/katana/core/src/service/messaging/service.rs new file mode 100644 index 0000000000..2b4f2166ab --- /dev/null +++ b/crates/katana/core/src/service/messaging/service.rs @@ -0,0 +1,328 @@ +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; +use std::time::Duration; + +use ::starknet::core::types::{FieldElement, MsgToL1}; +use futures::{Future, FutureExt, Stream}; +use tokio::time::{interval_at, Instant, Interval}; +use tracing::{error, info}; + +use super::{MessagingConfig, Messenger, MessengerMode, MessengerResult, LOG_TARGET}; +use crate::backend::storage::transaction::{L1HandlerTransaction, Transaction}; +use crate::backend::Backend; +use crate::pool::TransactionPool; + +type MessagingFuture = Pin + Send>>; +type MessageGatheringFuture = MessagingFuture>; +type MessageSettlingFuture = MessagingFuture>>; + +pub struct MessagingService { + /// The interval at which the service will perform the messaging operations. + interval: Interval, + backend: Arc, + pool: Arc, + /// The messenger mode the service is running in. + messenger: Arc, + /// The block number of the settlement chain from which messages will be gathered. + gather_from_block: u64, + /// The message gathering future. + msg_gather_fut: Option, + /// The block number of the local blockchain from which messages will be sent. + send_from_block: u64, + /// The message sending future. + msg_send_fut: Option, +} + +impl MessagingService { + /// Initializes a new instance from a configuration file's path. + /// Will panic on failure to avoid continuing with invalid configuration. + pub async fn new( + config: MessagingConfig, + pool: Arc, + backend: Arc, + ) -> anyhow::Result { + let gather_from_block = config.from_block; + let interval = interval_from_seconds(config.interval); + let messenger = match MessengerMode::from_config(config).await { + Ok(m) => Arc::new(m), + Err(_) => { + panic!( + "Messaging could not be initialized.\nVerify that the messaging target node \ + (anvil or other katana) is running.\n", + ) + } + }; + + Ok(Self { + pool, + backend, + interval, + messenger, + gather_from_block, + send_from_block: 0, + msg_gather_fut: None, + msg_send_fut: None, + }) + } + + async fn gather_messages( + messenger: Arc, + pool: Arc, + backend: Arc, + from_block: u64, + ) -> MessengerResult<(u64, usize)> { + let chain_id = FieldElement::from_hex_be(&backend.env.read().block.chain_id.as_hex()) + .expect("failed to parse katana chain id"); + + // 200 avoids any possible rejection from RPC with possibly lot's of messages. + // TODO: May this be configurable? + let max_block = 200; + + match messenger.as_ref() { + MessengerMode::Ethereum(inner) => { + let (block_num, txs) = + inner.gather_messages(from_block, max_block, chain_id).await?; + let txs_count = txs.len(); + + txs.into_iter().for_each(|tx| { + trace_l1_handler_tx_exec(&tx); + pool.add_transaction(Transaction::L1Handler(tx)) + }); + + Ok((block_num, txs_count)) + } + + #[cfg(feature = "starknet-messaging")] + MessengerMode::Starknet(inner) => { + let (block_num, txs) = + inner.gather_messages(from_block, max_block, chain_id).await?; + let txs_count = txs.len(); + + txs.into_iter().for_each(|tx| { + trace_l1_handler_tx_exec(&tx); + pool.add_transaction(Transaction::L1Handler(tx)) + }); + + Ok((block_num, txs_count)) + } + } + } + + async fn send_messages( + block_num: u64, + backend: Arc, + messenger: Arc, + ) -> MessengerResult> { + let Some(messages) = backend + .blockchain + .storage + .read() + .block_by_number(block_num) + .map(|block| &block.outputs) + .map(|outputs| { + outputs.iter().flat_map(|o| o.messages_sent.clone()).collect::>() + }) + else { + return Ok(None); + }; + + if messages.is_empty() { + Ok(Some((block_num, 0))) + } else { + match messenger.as_ref() { + MessengerMode::Ethereum(inner) => { + let hashes = inner + .send_messages(&messages) + .await + .map(|hashes| hashes.iter().map(|h| format!("{h:#x}")).collect())?; + trace_msg_to_l1_sent(&messages, &hashes); + Ok(Some((block_num, hashes.len()))) + } + + #[cfg(feature = "starknet-messaging")] + MessengerMode::Starknet(inner) => { + let hashes = inner + .send_messages(&messages) + .await + .map(|hashes| hashes.iter().map(|h| format!("{h:#x}")).collect())?; + trace_msg_to_l1_sent(&messages, &hashes); + Ok(Some((block_num, hashes.len()))) + } + } + } + } +} + +pub enum MessagingOutcome { + Gather { + /// The latest block number of the settlement chain from which messages were gathered. + lastest_block: u64, + /// The number of settlement chain messages gathered up until `latest_block`. + msg_count: usize, + }, + Send { + /// The current local block number from which messages were sent. + block_num: u64, + /// The number of messages sent on `block_num`. + msg_count: usize, + }, +} + +impl Stream for MessagingService { + type Item = MessagingOutcome; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let pin = self.get_mut(); + + if pin.interval.poll_tick(cx).is_ready() { + if pin.msg_gather_fut.is_none() { + pin.msg_gather_fut = Some(Box::pin(Self::gather_messages( + pin.messenger.clone(), + pin.pool.clone(), + pin.backend.clone(), + pin.gather_from_block, + ))); + } + + if pin.msg_send_fut.is_none() { + let local_latest_block_num = pin.backend.blockchain.storage.read().latest_number; + if pin.send_from_block <= local_latest_block_num { + pin.msg_send_fut = Some(Box::pin(Self::send_messages( + pin.send_from_block, + pin.backend.clone(), + pin.messenger.clone(), + ))) + } + } + } + + // Poll the gathering future. + if let Some(mut gather_fut) = pin.msg_gather_fut.take() { + match gather_fut.poll_unpin(cx) { + Poll::Ready(Ok((last_block, msg_count))) => { + pin.gather_from_block = last_block + 1; + return Poll::Ready(Some(MessagingOutcome::Gather { + lastest_block: last_block, + msg_count, + })); + } + Poll::Ready(Err(e)) => { + error!(target: LOG_TARGET, "error gathering messages for block {}: {e}", pin.gather_from_block); + return Poll::Pending; + } + Poll::Pending => pin.msg_gather_fut = Some(gather_fut), + } + } + + // Poll the message sending future. + if let Some(mut send_fut) = pin.msg_send_fut.take() { + match send_fut.poll_unpin(cx) { + Poll::Ready(Ok(Some((block_num, msg_count)))) => { + // +1 to move to the next local block to check messages to be + // sent on the settlement chain. + pin.send_from_block += 1; + return Poll::Ready(Some(MessagingOutcome::Send { block_num, msg_count })); + } + Poll::Ready(Err(e)) => { + error!(target: LOG_TARGET, "error settling messages for block {}: {e}", pin.send_from_block); + return Poll::Pending; + } + Poll::Ready(_) => return Poll::Pending, + Poll::Pending => pin.msg_send_fut = Some(send_fut), + } + } + + Poll::Pending + } +} + +/// Returns an `Interval` from the given seconds. +fn interval_from_seconds(secs: u64) -> Interval { + let duration = Duration::from_secs(secs); + let mut interval = interval_at(Instant::now() + duration, duration); + interval.set_missed_tick_behavior(tokio::time::MissedTickBehavior::Delay); + interval +} + +fn trace_msg_to_l1_sent(messages: &Vec, hashes: &Vec) { + assert_eq!(messages.len(), hashes.len()); + + #[cfg(feature = "starknet-messaging")] + let hash_exec_str = format!("{:#064x}", super::starknet::HASH_EXEC); + + for (i, m) in messages.iter().enumerate() { + let payload_str: Vec = m.payload.iter().map(|f| format!("{:#x}", *f)).collect(); + + let hash = &hashes[i]; + + #[cfg(feature = "starknet-messaging")] + if hash == &hash_exec_str { + let to_address = &payload_str[0]; + let selector = &payload_str[1]; + let payload_str = &payload_str[2..]; + + #[rustfmt::skip] + info!( + target: LOG_TARGET, + r"Message executed on settlement layer: +| from_address | {:#x} +| to_address | {} +| selector | {} +| payload | [{}] + +", + m.from_address, + to_address, + selector, + payload_str.join(", ") + ); + + continue; + } + + // We check for magic value 'MSG' used only when we are doing L3-L2 messaging. + let (to_address, payload_str) = if format!("{:#x}", m.to_address) == "0x4d5347" { + (payload_str[0].clone(), &payload_str[1..]) + } else { + (format!("{:#x}", m.to_address), &payload_str[..]) + }; + + #[rustfmt::skip] + info!( + target: LOG_TARGET, + r#"Message sent to settlement layer: +| hash | {} +| from_address | {:#x} +| to_address | {} +| payload | [{}] + +"#, + hash.as_str(), + m.from_address, + to_address, + payload_str.join(", ") + ); + } +} + +fn trace_l1_handler_tx_exec(tx: &L1HandlerTransaction) { + let calldata_str: Vec = + tx.inner.calldata.0.iter().map(|f| format!("{:#x}", FieldElement::from(*f))).collect(); + + #[rustfmt::skip] + info!( + target: LOG_TARGET, + r"L1Handler transaction added to the pool: +| tx_hash | {:#x} +| contract_address | {:#x} +| selector | {:#x} +| calldata | [{}] + +", + FieldElement::from(tx.inner.transaction_hash.0), + FieldElement::from(*tx.inner.contract_address.0.key()), + FieldElement::from(tx.inner.entry_point_selector.0), + calldata_str.join(", ") + ); +} diff --git a/crates/katana/core/src/service/messaging/starknet.rs b/crates/katana/core/src/service/messaging/starknet.rs new file mode 100644 index 0000000000..0b5d633c27 --- /dev/null +++ b/crates/katana/core/src/service/messaging/starknet.rs @@ -0,0 +1,548 @@ +use std::collections::HashMap; +use std::sync::Arc; + +use anyhow::Result; +use async_trait::async_trait; +use starknet::accounts::{Account, Call, ExecutionEncoding, SingleOwnerAccount}; +use starknet::core::types::{BlockId, BlockTag, EmittedEvent, EventFilter, FieldElement, MsgToL1}; +use starknet::core::utils::starknet_keccak; +use starknet::macros::{felt, selector}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::{AnyProvider, JsonRpcClient, Provider}; +use starknet::signers::{LocalWallet, SigningKey}; +use starknet_api::core::{ContractAddress, EntryPointSelector, Nonce}; +use starknet_api::hash::StarkFelt; +use starknet_api::stark_felt; +use starknet_api::transaction::{ + Calldata, L1HandlerTransaction as ApiL1HandlerTransaction, TransactionHash, TransactionVersion, +}; +use tracing::{debug, error, trace, warn}; +use url::Url; + +use super::{Error, MessagingConfig, Messenger, MessengerResult, LOG_TARGET}; +use crate::backend::storage::transaction::L1HandlerTransaction; +use crate::utils::transaction::compute_l1_handler_transaction_hash_felts; + +/// As messaging in starknet is only possible with EthAddress in the `to_address` +/// field, we have to set magic value to understand what the user want to do. +/// In the case of execution -> the felt 'EXE' will be passed. +/// And for normal messages, the felt 'MSG' is used. +/// Those values are very not likely a valid account address on starknet. +const MSG_MAGIC: FieldElement = felt!("0x4d5347"); +const EXE_MAGIC: FieldElement = felt!("0x455845"); + +pub const HASH_EXEC: FieldElement = felt!("0xee"); + +pub struct StarknetMessaging { + chain_id: FieldElement, + provider: AnyProvider, + wallet: LocalWallet, + sender_account_address: FieldElement, + messaging_contract_address: FieldElement, +} + +impl StarknetMessaging { + pub async fn new(config: MessagingConfig) -> Result { + let provider = AnyProvider::JsonRpcHttp(JsonRpcClient::new(HttpTransport::new( + Url::parse(&config.rpc_url)?, + ))); + + let private_key = FieldElement::from_hex_be(&config.private_key)?; + let key = SigningKey::from_secret_scalar(private_key); + let wallet = LocalWallet::from_signing_key(key); + + let chain_id = provider.chain_id().await?; + let sender_account_address = FieldElement::from_hex_be(&config.sender_address)?; + let messaging_contract_address = FieldElement::from_hex_be(&config.contract_address)?; + + Ok(StarknetMessaging { + wallet, + provider, + chain_id, + sender_account_address, + messaging_contract_address, + }) + } + + /// Fetches events for the given blocks range. + pub async fn fetch_events( + &self, + from_block: BlockId, + to_block: BlockId, + ) -> Result>> { + trace!(target: LOG_TARGET, "Fetching blocks {:?} - {:?}.", from_block, to_block); + + let mut block_to_events: HashMap> = HashMap::new(); + + let filter = EventFilter { + from_block: Some(from_block), + to_block: Some(to_block), + address: Some(self.messaging_contract_address), + // TODO: this might come from the configuration actually. + keys: None, + }; + + // TODO: this chunk_size may also come from configuration? + let chunk_size = 200; + let mut continuation_token: Option = None; + + loop { + let event_page = + self.provider.get_events(filter.clone(), continuation_token, chunk_size).await?; + + event_page.events.into_iter().for_each(|event| { + block_to_events + .entry(event.block_number) + .and_modify(|v| v.push(event.clone())) + .or_insert(vec![event]); + }); + + continuation_token = event_page.continuation_token; + + if continuation_token.is_none() { + break; + } + } + + Ok(block_to_events) + } + + /// Sends an invoke TX on starknet. + async fn send_invoke_tx(&self, calls: Vec) -> Result { + let signer = Arc::new(&self.wallet); + + let mut account = SingleOwnerAccount::new( + &self.provider, + signer, + self.sender_account_address, + self.chain_id, + ExecutionEncoding::Legacy, + ); + + account.set_block_id(BlockId::Tag(BlockTag::Latest)); + + // TODO: we need to have maximum fee configurable. + let execution = account.execute(calls).fee_estimate_multiplier(10f64); + let estimated_fee = (execution.estimate_fee().await?.overall_fee) * 10; + let tx = execution.max_fee(estimated_fee.into()).send().await?; + + Ok(tx.transaction_hash) + } + + /// Sends messages hashes to settlement layer by sending a transaction. + async fn send_hashes(&self, mut hashes: Vec) -> MessengerResult { + hashes.retain(|&x| x != HASH_EXEC); + + if hashes.is_empty() { + return Ok(FieldElement::ZERO); + } + + let mut calldata = hashes; + calldata.insert(0, calldata.len().into()); + + let call = Call { + selector: selector!("add_messages_hashes_from_appchain"), + to: self.messaging_contract_address, + calldata, + }; + + match self.send_invoke_tx(vec![call]).await { + Ok(tx_hash) => { + trace!(target: LOG_TARGET, "Hashes sending transaction {:#064x}", tx_hash); + Ok(tx_hash) + } + Err(e) => { + error!("Error settling hashes on Starknet: {:?}", e); + Err(Error::SendError) + } + } + } +} + +#[async_trait] +impl Messenger for StarknetMessaging { + type MessageHash = FieldElement; + type MessageTransaction = L1HandlerTransaction; + + async fn gather_messages( + &self, + from_block: u64, + max_blocks: u64, + chain_id: FieldElement, + ) -> MessengerResult<(u64, Vec)> { + let chain_latest_block: u64 = match self.provider.block_number().await { + Ok(n) => n, + Err(_) => { + warn!( + "Couldn't fetch settlement chain last block number. \nSkipped, retry at the \ + next tick." + ); + return Err(Error::SendError); + } + }; + + if from_block > chain_latest_block { + // Nothing to fetch, we can skip waiting the next tick. + return Ok((chain_latest_block, vec![])); + } + + // +1 as the from_block counts as 1 block fetched. + let to_block = if from_block + max_blocks + 1 < chain_latest_block { + from_block + max_blocks + } else { + chain_latest_block + }; + + let mut l1_handler_txs: Vec = vec![]; + + self.fetch_events(BlockId::Number(from_block), BlockId::Number(to_block)) + .await + .map_err(|_| Error::SendError) + .unwrap() + .iter() + .for_each(|(block_number, block_events)| { + debug!( + target: LOG_TARGET, + "Converting events of block {} into L1HandlerTx ({} events)", + block_number, + block_events.len(), + ); + + block_events.iter().for_each(|e| { + if let Ok(tx) = l1_handler_tx_from_event(e, chain_id) { + l1_handler_txs.push(tx) + } + }) + }); + + Ok((to_block, l1_handler_txs)) + } + + async fn send_messages(&self, messages: &[MsgToL1]) -> MessengerResult> { + if messages.is_empty() { + return Ok(vec![]); + } + + let (hashes, calls) = parse_messages(messages)?; + + if !calls.is_empty() { + match self.send_invoke_tx(calls).await { + Ok(tx_hash) => { + trace!(target: LOG_TARGET, "Invoke transaction hash {:#064x}", tx_hash); + } + Err(e) => { + error!("Error sending invoke tx on Starknet: {:?}", e); + return Err(Error::SendError); + } + }; + } + + self.send_hashes(hashes.clone()).await?; + + Ok(hashes) + } +} + +/// Parses messages sent by cairo contracts to compute their hashes. +/// +/// Messages can also be labelled as EXE, which in this case generate a `Call` +/// additionally to the hash. +fn parse_messages(messages: &[MsgToL1]) -> MessengerResult<(Vec, Vec)> { + let mut hashes: Vec = vec![]; + let mut calls: Vec = vec![]; + + for m in messages { + // Field `to_address` is restricted to eth addresses space. So the + // `to_address` is set to 'EXE'/'MSG' to indicate that the message + // has to be executed or sent normally. + let magic = m.to_address; + + if magic == EXE_MAGIC { + if m.payload.len() < 2 { + error!( + target: LOG_TARGET, + "Message execution is expecting a payload of at least length \ + 2. With [0] being the contract address, and [1] the selector.", + ); + } + + let to = m.payload[0]; + let selector = m.payload[1]; + + let mut calldata = vec![]; + // We must exclude the `to_address` and `selector` from the actual payload. + if m.payload.len() >= 3 { + calldata.extend(m.payload[2..].to_vec()); + } + + calls.push(Call { to, selector, calldata }); + hashes.push(HASH_EXEC); + } else if magic == MSG_MAGIC { + // In the case or regular message, we compute the message's hash + // which will then be sent in a transaction to be registered. + + // As to_address is used by the magic, the `to_address` we want + // is the first element of the payload. + let to_address = m.payload[0]; + + // Then, the payload must be changed to only keep the rest of the + // data, without the first element that was the `to_address`. + let payload = &m.payload[1..]; + + let mut buf: Vec = vec![]; + buf.extend(m.from_address.to_bytes_be()); + buf.extend(to_address.to_bytes_be()); + buf.extend(FieldElement::from(payload.len()).to_bytes_be()); + for p in payload { + buf.extend(p.to_bytes_be()); + } + + hashes.push(starknet_keccak(&buf)); + } else { + // Skip the message if no valid magic number found. + warn!("Invalid message to_address magic value: {:?}", magic); + continue; + } + } + + Ok((hashes, calls)) +} + +fn l1_handler_tx_from_event( + event: &EmittedEvent, + chain_id: FieldElement, +) -> Result { + if event.keys[0] != selector!("MessageSentToAppchain") { + debug!( + target: LOG_TARGET, + "Event with key {:?} can't be converted into L1HandlerTransaction", event.keys[0], + ); + return Err(Error::GatherError.into()); + } + + if event.keys.len() != 4 || event.data.len() < 2 { + error!(target: LOG_TARGET, "Event MessageSentToAppchain is not well formatted"); + } + + // See contrat appchain_messaging.cairo for MessageSentToAppchain event. + let from_address = event.keys[2]; + let to_address = event.keys[3]; + let selector = event.data[0]; + let nonce = event.data[1]; + let version = 0_u32; + + // Skip the length of the serialized array for the payload which is data[2]. + // Payload starts at data[3]. + let mut calldata = vec![from_address]; + calldata.extend(&event.data[3..]); + + let tx_hash = compute_l1_handler_transaction_hash_felts( + version.into(), + to_address, + selector, + &calldata, + chain_id, + nonce, + ); + + let calldata: Vec = calldata.iter().map(|f| StarkFelt::from(*f)).collect(); + let calldata = Calldata(calldata.into()); + + let tx = L1HandlerTransaction { + inner: ApiL1HandlerTransaction { + transaction_hash: TransactionHash(tx_hash.into()), + version: TransactionVersion(stark_felt!(version)), + nonce: Nonce(nonce.into()), + contract_address: ContractAddress::try_from(>::into( + to_address, + )) + .unwrap(), + entry_point_selector: EntryPointSelector(selector.into()), + calldata, + }, + // This is the min value paid on L1 for the message to be sent to L2. + paid_l1_fee: 30000_u128, + }; + + Ok(tx) +} + +#[cfg(test)] +mod tests { + + use starknet::macros::felt; + + use super::*; + use crate::utils::transaction::stark_felt_to_field_element_array; + + #[test] + fn parse_messages_msg() { + let from_address = selector!("from_address"); + let to_address = selector!("to_address"); + let selector = selector!("selector"); + let payload_msg = vec![to_address, FieldElement::ONE, FieldElement::TWO]; + let payload_exe = vec![to_address, selector, FieldElement::ONE, FieldElement::TWO]; + + let messages = vec![ + MsgToL1 { from_address, to_address: MSG_MAGIC, payload: payload_msg }, + MsgToL1 { from_address, to_address: EXE_MAGIC, payload: payload_exe.clone() }, + ]; + + let (hashes, calls) = parse_messages(&messages).unwrap(); + + assert_eq!(hashes.len(), 2); + assert_eq!( + hashes, + vec![ + FieldElement::from_hex_be( + "0x03a1d2e131360f15e26dd4f6ff10550685611cc25f75e7950b704adb04b36162" + ) + .unwrap(), + HASH_EXEC, + ] + ); + + assert_eq!(calls.len(), 1); + assert_eq!(calls[0].to, to_address); + assert_eq!(calls[0].selector, selector); + assert_eq!(calls[0].calldata, payload_exe[2..].to_vec()); + } + + #[test] + #[should_panic] + fn parse_messages_msg_bad_payload() { + let from_address = selector!("from_address"); + let payload_msg = vec![]; + + let messages = vec![MsgToL1 { from_address, to_address: MSG_MAGIC, payload: payload_msg }]; + + parse_messages(&messages).unwrap(); + } + + #[test] + #[should_panic] + fn parse_messages_exe_bad_payload() { + let from_address = selector!("from_address"); + let payload_exe = vec![FieldElement::ONE]; + + let messages = vec![MsgToL1 { from_address, to_address: EXE_MAGIC, payload: payload_exe }]; + + parse_messages(&messages).unwrap(); + } + + #[test] + fn l1_handler_tx_from_event_parse_ok() { + let from_address = selector!("from_address"); + let to_address = selector!("to_address"); + let selector = selector!("selector"); + let chain_id = selector!("KATANA"); + let nonce = FieldElement::ONE; + let calldata: Vec = vec![from_address.into(), FieldElement::THREE.into()]; + let transaction_hash: FieldElement = compute_l1_handler_transaction_hash_felts( + FieldElement::ZERO, + to_address, + selector, + &stark_felt_to_field_element_array(&calldata), + chain_id, + nonce, + ); + + let event = EmittedEvent { + from_address: felt!( + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" + ), + keys: vec![ + selector!("MessageSentToAppchain"), + selector!("random_hash"), + from_address, + to_address, + ], + data: vec![ + selector, + nonce, + FieldElement::from(calldata.len() as u128), + FieldElement::THREE, + ], + block_hash: selector!("block_hash"), + block_number: 0, + transaction_hash, + }; + + let expected = L1HandlerTransaction { + inner: ApiL1HandlerTransaction { + transaction_hash: TransactionHash(transaction_hash.into()), + version: TransactionVersion(stark_felt!(0_u32)), + nonce: Nonce(nonce.into()), + contract_address: ContractAddress::try_from( + >::into(to_address), + ) + .unwrap(), + entry_point_selector: EntryPointSelector(selector.into()), + calldata: Calldata(calldata.into()), + }, + paid_l1_fee: 30000_u128, + }; + + let tx = l1_handler_tx_from_event(&event, chain_id).unwrap(); + + assert_eq!(tx.inner, expected.inner); + } + + #[test] + #[should_panic] + fn l1_handler_tx_from_event_parse_bad_selector() { + let from_address = selector!("from_address"); + let to_address = selector!("to_address"); + let selector = selector!("selector"); + let nonce = FieldElement::ONE; + let calldata: Vec = vec![from_address.into(), FieldElement::THREE.into()]; + let transaction_hash = FieldElement::ZERO; + + let event = EmittedEvent { + from_address: felt!( + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" + ), + keys: vec![ + selector!("AnOtherUnexpectedEvent"), + selector!("random_hash"), + from_address, + to_address, + ], + data: vec![ + selector, + nonce, + FieldElement::from(calldata.len() as u128), + FieldElement::THREE, + ], + block_hash: selector!("block_hash"), + block_number: 0, + transaction_hash, + }; + + let _tx = l1_handler_tx_from_event(&event, FieldElement::ZERO).unwrap(); + } + + #[test] + #[should_panic] + fn l1_handler_tx_from_event_parse_missing_key_data() { + let from_address = selector!("from_address"); + let _to_address = selector!("to_address"); + let _selector = selector!("selector"); + let _nonce = FieldElement::ONE; + let _calldata: Vec = vec![from_address.into(), FieldElement::THREE.into()]; + let transaction_hash = FieldElement::ZERO; + + let event = EmittedEvent { + from_address: felt!( + "0x049d36570d4e46f48e99674bd3fcc84644ddd6b96f7c741b1562b82f9e004dc7" + ), + keys: vec![selector!("AnOtherUnexpectedEvent"), selector!("random_hash"), from_address], + data: vec![], + block_hash: selector!("block_hash"), + block_number: 0, + transaction_hash, + }; + + let _tx = l1_handler_tx_from_event(&event, FieldElement::ZERO).unwrap(); + } +} diff --git a/crates/katana/core/src/service/mod.rs b/crates/katana/core/src/service/mod.rs index 3460640f9c..138fa73ab7 100644 --- a/crates/katana/core/src/service/mod.rs +++ b/crates/katana/core/src/service/mod.rs @@ -17,6 +17,11 @@ use crate::backend::storage::transaction::Transaction; use crate::pool::TransactionPool; pub mod block_producer; +#[cfg(feature = "messaging")] +pub mod messaging; + +#[cfg(feature = "messaging")] +use self::messaging::{MessagingOutcome, MessagingService}; /// The type that drives the blockchain's state /// @@ -25,21 +30,14 @@ pub mod block_producer; /// to construct a new block. pub struct NodeService { /// the pool that holds all transactions - pool: Arc, + pub(crate) pool: Arc, /// creates new blocks - block_producer: BlockProducer, + pub(crate) block_producer: BlockProducer, /// the miner responsible to select transactions from the `pool´ - miner: TransactionMiner, -} - -impl NodeService { - pub fn new( - pool: Arc, - miner: TransactionMiner, - block_producer: BlockProducer, - ) -> Self { - Self { pool, block_producer, miner } - } + pub(crate) miner: TransactionMiner, + /// The messaging service + #[cfg(feature = "messaging")] + pub(crate) messaging: Option, } impl Future for NodeService { @@ -48,6 +46,20 @@ impl Future for NodeService { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let pin = self.get_mut(); + #[cfg(feature = "messaging")] + if let Some(messaging) = pin.messaging.as_mut() { + while let Poll::Ready(Some(outcome)) = messaging.poll_next_unpin(cx) { + match outcome { + MessagingOutcome::Gather { msg_count, .. } => { + trace!(target: "node", "collected {msg_count} messages from settlement chain"); + } + MessagingOutcome::Send { msg_count, .. } => { + trace!(target: "node", "sent {msg_count} messages to the settlement chain"); + } + } + } + } + // this drives block production and feeds new sets of ready transactions to the block // producer loop { diff --git a/crates/katana/core/src/utils/transaction.rs b/crates/katana/core/src/utils/transaction.rs index 8a2c8c5f9f..e66af2f7d8 100644 --- a/crates/katana/core/src/utils/transaction.rs +++ b/crates/katana/core/src/utils/transaction.rs @@ -1,7 +1,9 @@ use std::sync::Arc; use blockifier::execution::contract_class::ContractClass; +use blockifier::execution::errors::EntryPointExecutionError; use blockifier::transaction::account_transaction::AccountTransaction; +use blockifier::transaction::errors::TransactionExecutionError; use blockifier::transaction::transaction_execution::Transaction as ExecutionTransaction; use starknet::core::crypto::compute_hash_on_elements; use starknet::core::types::{ @@ -10,7 +12,7 @@ use starknet::core::types::{ DeployAccountTransaction, DeployTransaction, FieldElement, InvokeTransaction, InvokeTransactionV0, InvokeTransactionV1, L1HandlerTransaction, Transaction as RpcTransaction, }; -use starknet::core::utils::get_contract_address; +use starknet::core::utils::{get_contract_address, parse_cairo_short_string}; use starknet_api::core::{ClassHash, CompiledClassHash, ContractAddress, Nonce, PatriciaKey}; use starknet_api::hash::{StarkFelt, StarkHash}; use starknet_api::transaction::{ @@ -60,6 +62,14 @@ const PREFIX_DEPLOY_ACCOUNT: FieldElement = FieldElement::from_mont([ 461298303000467581, ]); +/// Cairo string for "l1_handler" +const PREFIX_L1_HANDLER: FieldElement = FieldElement::from_mont([ + 1365666230910873368, + 18446744073708665300, + 18446744073709551615, + 157895833347907735, +]); + /// Compute the hash of a V1 DeployAccount transaction. #[allow(clippy::too_many_arguments)] pub fn compute_deploy_account_v1_transaction_hash( @@ -171,6 +181,53 @@ pub fn compute_invoke_v1_transaction_hash( ]) } +/// Computes the hash of a L1 handler transaction +/// from `L1HandlerApiTransaction`. +pub fn compute_l1_handler_transaction_hash( + tx: L1HandlerApiTransaction, + chain_id: FieldElement, +) -> FieldElement { + let tx = api_l1_handler_to_rpc_transaction(tx); + let version: FieldElement = tx.version.into(); + + assert_eq!(version, FieldElement::ZERO, "L1 handler transaction only supports version 0"); + + compute_l1_handler_transaction_hash_felts( + version, + tx.contract_address, + tx.entry_point_selector, + &tx.calldata, + chain_id, + tx.nonce.into(), + ) +} + +/// Computes the hash of a L1 handler transaction +/// from the fields involved in the computation, +/// as felts values. +pub fn compute_l1_handler_transaction_hash_felts( + version: FieldElement, + contract_address: FieldElement, + entry_point_selector: FieldElement, + calldata: &[FieldElement], + chain_id: FieldElement, + nonce: FieldElement, +) -> FieldElement { + // No fee on L2 for L1 handler transaction. + let fee = FieldElement::ZERO; + + compute_hash_on_elements(&[ + PREFIX_L1_HANDLER, + version, + contract_address, + entry_point_selector, + compute_hash_on_elements(calldata), + fee, + chain_id, + nonce, + ]) +} + /// Convert [StarkFelt] array to [FieldElement] array. #[inline] pub fn stark_felt_to_field_element_array(arr: &[StarkFelt]) -> Vec { @@ -512,3 +569,26 @@ mod tests { ); } } + +pub fn warn_message_transaction_error_exec_error(err: &TransactionExecutionError) { + match err { + TransactionExecutionError::EntryPointExecutionError(ref eperr) + | TransactionExecutionError::ExecutionError(ref eperr) => match eperr { + EntryPointExecutionError::ExecutionFailed { error_data } => { + let mut reasons: Vec = vec![]; + error_data.iter().for_each(|felt| { + if let Ok(s) = parse_cairo_short_string(&FieldElement::from(*felt)) { + reasons.push(s); + } + }); + + tracing::warn!(target: "executor", + "Transaction validation error: {}", reasons.join(" ")); + } + _ => tracing::warn!(target: "executor", + "Transaction validation error: {:?}", err), + }, + _ => tracing::warn!(target: "executor", + "Transaction validation error: {:?}", err), + } +} diff --git a/crates/katana/src/args.rs b/crates/katana/src/args.rs index 88f0885a99..dfa9fbd35b 100644 --- a/crates/katana/src/args.rs +++ b/crates/katana/src/args.rs @@ -53,6 +53,16 @@ pub struct KatanaArgs { #[arg(help = "Initialize the chain from a previously saved state snapshot.")] pub load_state: Option, + #[cfg(feature = "messaging")] + #[arg(long)] + #[arg(value_name = "PATH")] + #[arg(value_parser = katana_core::service::messaging::MessagingConfig::parse)] + #[arg(help = "Configure the messaging with an other chain.")] + #[arg(long_help = "Configure the messaging to allow Katana listening/sending messages on a \ + settlement chain that can be Ethereum or an other Starknet sequencer. \ + The configuration file details and examples can be found here: TODO.")] + pub messaging: Option, + #[command(flatten)] #[command(next_help_heading = "Server options")] pub server: ServerOptions, @@ -127,7 +137,12 @@ pub struct EnvironmentOptions { impl KatanaArgs { pub fn sequencer_config(&self) -> SequencerConfig { - SequencerConfig { block_time: self.block_time, no_mining: self.no_mining } + SequencerConfig { + block_time: self.block_time, + no_mining: self.no_mining, + #[cfg(feature = "messaging")] + messaging: self.messaging.clone(), + } } pub fn server_config(&self) -> ServerConfig { diff --git a/crates/katana/src/main.rs b/crates/katana/src/main.rs index ec6c9ce81e..a5eadb38fb 100644 --- a/crates/katana/src/main.rs +++ b/crates/katana/src/main.rs @@ -22,7 +22,7 @@ async fn main() { fmt::Subscriber::builder() .with_env_filter( "info,executor=trace,server=debug,katana_core=trace,blockifier=off,\ - jsonrpsee_server=off,hyper=off", + jsonrpsee_server=off,hyper=off,messaging=debug", ) .finish(), ) diff --git a/crates/torii/client/src/contract/model.rs b/crates/torii/client/src/contract/model.rs index 4968872364..8ed5d426a3 100644 --- a/crates/torii/client/src/contract/model.rs +++ b/crates/torii/client/src/contract/model.rs @@ -1,13 +1,12 @@ -use std::str::FromStr; use std::vec; -use crypto_bigint::U256; -use dojo_types::core::{CairoType, CairoTypeError}; -use dojo_types::schema::{Enum, Member, Struct, Ty}; +use dojo_types::packing::{parse_ty, unpack, PackingError, ParseError}; +use dojo_types::primitive::PrimitiveError; +use dojo_types::schema::Ty; use starknet::core::types::{BlockId, FieldElement, FunctionCall}; use starknet::core::utils::{ - cairo_short_string_to_felt, get_selector_from_name, parse_cairo_short_string, - CairoShortStringToFeltError, ParseCairoShortStringError, + cairo_short_string_to_felt, get_selector_from_name, CairoShortStringToFeltError, + ParseCairoShortStringError, }; use starknet::macros::short_string; use starknet::providers::{Provider, ProviderError}; @@ -23,20 +22,18 @@ mod model_test; pub enum ModelError

{ #[error(transparent)] ProviderError(ProviderError

), - #[error("Invalid schema")] - InvalidSchema, #[error(transparent)] ParseCairoShortStringError(ParseCairoShortStringError), #[error(transparent)] CairoShortStringToFeltError(CairoShortStringToFeltError), - #[error("Converting felt")] - ConvertingFelt, - #[error("Unpacking entity")] - UnpackingEntity, #[error(transparent)] ContractReaderError(ContractReaderError

), #[error(transparent)] - CairoTypeError(CairoTypeError), + CairoTypeError(PrimitiveError), + #[error(transparent)] + Parse(#[from] ParseError), + #[error(transparent)] + Packing(#[from] PackingError), } pub struct ModelReader<'a, P: Provider + Sync> { @@ -82,7 +79,7 @@ impl<'a, P: Provider + Sync> ModelReader<'a, P> { .await .map_err(ModelError::ContractReaderError)?; - parse_ty::

(&res[1..]) + Ok(parse_ty(&res[1..])?) } pub async fn packed_size( @@ -150,7 +147,7 @@ impl<'a, P: Provider + Sync> ModelReader<'a, P> { packed.push(value); } - let unpacked = unpack::

(packed, layout.clone())?; + let unpacked = unpack(packed, layout.clone())?; Ok(unpacked) } @@ -171,166 +168,3 @@ impl<'a, P: Provider + Sync> ModelReader<'a, P> { Ok(schema) } } - -/// Unpacks a vector of packed values according to a given layout. -/// -/// # Arguments -/// -/// * `packed_values` - A vector of FieldElement values that are packed. -/// * `layout` - A vector of FieldElement values that describe the layout of the packed values. -/// -/// # Returns -/// -/// * `Result, ModelError>` - A Result containing a vector of unpacked -/// FieldElement values if successful, or an error if unsuccessful. -pub fn unpack( - mut packed: Vec, - layout: Vec, -) -> Result, ModelError> { - packed.reverse(); - let mut unpacked = vec![]; - - let mut unpacking: U256 = packed.pop().ok_or(ModelError::UnpackingEntity)?.as_ref().into(); - let mut offset = 0; - - // Iterate over the layout. - for size in layout { - let size: u8 = size.try_into().map_err(|_| ModelError::ConvertingFelt)?; - let size: usize = size.into(); - let remaining_bits = 251 - offset; - - // If there are less remaining bits than the size, move to the next felt for unpacking. - if remaining_bits < size { - unpacking = packed.pop().ok_or(ModelError::UnpackingEntity)?.as_ref().into(); - offset = 0; - } - - let mut mask = U256::from(0_u8); - for _ in 0..size { - mask = (mask << 1) | U256::from(1_u8); - } - - let result = mask & (unpacking >> offset); - let result_fe = FieldElement::from_hex_be(&result.to_string()) - .map_err(|_| ModelError::ConvertingFelt)?; - unpacked.push(result_fe); - - // Update unpacking to be the shifted value after extracting the result. - offset += size; - } - - Ok(unpacked) -} - -fn parse_ty(data: &[FieldElement]) -> Result> { - let member_type: u8 = data[0].try_into().unwrap(); - match member_type { - 0 => parse_simple::

(&data[1..]), - 1 => parse_struct::

(&data[1..]), - 2 => parse_enum::

(&data[1..]), - 3 => parse_tuple::

(&data[1..]), - _ => Err(ModelError::InvalidSchema), - } -} - -fn parse_simple(data: &[FieldElement]) -> Result> { - let ty = parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; - Ok(Ty::Primitive(CairoType::from_str(&ty).unwrap())) -} - -fn parse_struct(data: &[FieldElement]) -> Result> { - let name = - parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; - - let attrs_len: u32 = data[1].try_into().unwrap(); - let attrs_slice_start = 2; - let attrs_slice_end = attrs_slice_start + attrs_len as usize; - let _attrs = &data[attrs_slice_start..attrs_slice_end]; - - let children_len: u32 = data[attrs_slice_end].try_into().unwrap(); - let children_len = children_len as usize; - - let mut children = vec![]; - let mut offset = attrs_slice_end + 1; - - for i in 0..children_len { - let start = i + offset; - let len: u32 = data[start].try_into().unwrap(); - let slice_start = start + 1; - let slice_end = slice_start + len as usize; - children.push(parse_member::

(&data[slice_start..slice_end])?); - offset += len as usize; - } - - Ok(Ty::Struct(Struct { name, children })) -} - -fn parse_member(data: &[FieldElement]) -> Result> { - let name = - parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; - - let attributes_len: u32 = data[1].try_into().unwrap(); - let slice_start = 2; - let slice_end = slice_start + attributes_len as usize; - let attributes = &data[slice_start..slice_end]; - - let key = attributes.contains(&cairo_short_string_to_felt("key").unwrap()); - - let ty = parse_ty::

(&data[slice_end..])?; - - Ok(Member { name, ty, key }) -} - -fn parse_enum(data: &[FieldElement]) -> Result> { - let name = - parse_cairo_short_string(&data[0]).map_err(ModelError::ParseCairoShortStringError)?; - - let attrs_len: u32 = data[1].try_into().unwrap(); - let attrs_slice_start = 2; - let attrs_slice_end = attrs_slice_start + attrs_len as usize; - let _attrs = &data[attrs_slice_start..attrs_slice_end]; - - let values_len: u32 = data[attrs_slice_end].try_into().unwrap(); - let values_len = values_len as usize; - - let mut values = vec![]; - let mut offset = attrs_slice_end + 1; - - for i in 0..values_len { - let start = i + offset; - let name = parse_cairo_short_string(&data[start]) - .map_err(ModelError::ParseCairoShortStringError)?; - let slice_start = start + 2; - let len: u32 = data[start + 3].try_into().unwrap(); - let len = len + 1; // Account for Ty enum index - - let slice_end = slice_start + len as usize; - values.push((name, parse_ty::

(&data[slice_start..slice_end])?)); - offset += len as usize + 2; - } - - Ok(Ty::Enum(Enum { name, children: values })) -} - -fn parse_tuple(data: &[FieldElement]) -> Result> { - if data.is_empty() { - return Ok(Ty::Tuple(vec![])); - } - - let children_len: u32 = data[0].try_into().unwrap(); - let children_len = children_len as usize; - - let mut children = vec![]; - let mut offset = 1; - - for i in 0..children_len { - let start = i + offset; - let len: u32 = data[start].try_into().unwrap(); - let slice_start = start + 1; - let slice_end = slice_start + len as usize; - children.push(parse_ty::

(&data[slice_start..slice_end])?); - offset += len as usize; - } - - Ok(Ty::Tuple(children)) -} diff --git a/crates/torii/client/src/contract/model_test.rs b/crates/torii/client/src/contract/model_test.rs index c67056ca3c..cc6dab73ef 100644 --- a/crates/torii/client/src/contract/model_test.rs +++ b/crates/torii/client/src/contract/model_test.rs @@ -2,7 +2,7 @@ use camino::Utf8PathBuf; use dojo_test_utils::sequencer::{ get_default_test_starknet_config, SequencerConfig, TestSequencer, }; -use dojo_types::core::CairoType; +use dojo_types::primitive::Primitive; use dojo_types::schema::{Enum, Member, Struct, Ty}; use starknet::accounts::ConnectedAccount; use starknet::core::types::{BlockId, BlockTag, FieldElement}; @@ -34,7 +34,7 @@ async fn test_model() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Primitive(CairoType::ContractAddress(None)), + ty: Ty::Primitive(Primitive::ContractAddress(None)), key: true }, Member { @@ -44,12 +44,12 @@ async fn test_model() { children: vec![ Member { name: "x".to_string(), - ty: Ty::Primitive(CairoType::U32(None)), + ty: Ty::Primitive(Primitive::U32(None)), key: false }, Member { name: "y".to_string(), - ty: Ty::Primitive(CairoType::U32(None)), + ty: Ty::Primitive(Primitive::U32(None)), key: false } ] @@ -78,19 +78,20 @@ async fn test_model() { children: vec![ Member { name: "player".to_string(), - ty: Ty::Primitive(CairoType::ContractAddress(None)), + ty: Ty::Primitive(Primitive::ContractAddress(None)), key: true }, Member { name: "remaining".to_string(), - ty: Ty::Primitive(CairoType::U8(None)), + ty: Ty::Primitive(Primitive::U8(None)), key: false }, Member { name: "last_direction".to_string(), ty: Ty::Enum(Enum { name: "Direction".to_string(), - children: vec![ + option: None, + options: vec![ ("None".to_string(), Ty::Tuple(vec![])), ("Left".to_string(), Ty::Tuple(vec![])), ("Right".to_string(), Ty::Tuple(vec![])), diff --git a/crates/torii/client/wasm/Cargo.lock b/crates/torii/client/wasm/Cargo.lock index 20b89fd017..21c9a5335c 100644 --- a/crates/torii/client/wasm/Cargo.lock +++ b/crates/torii/client/wasm/Cargo.lock @@ -316,6 +316,12 @@ dependencies = [ "rustc-demangle", ] +[[package]] +name = "base16ct" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" + [[package]] name = "base64" version = "0.13.1" @@ -550,6 +556,7 @@ checksum = "740fe28e594155f10cfc383984cbefd529d7396050557148f79cb0f621204124" dependencies = [ "generic-array", "rand_core", + "serdect", "subtle", "zeroize", ] @@ -643,7 +650,7 @@ dependencies = [ name = "dojo-types" version = "0.2.1" dependencies = [ - "ethabi", + "crypto-bigint", "hex", "itertools 0.10.5", "serde", @@ -726,20 +733,6 @@ dependencies = [ "uuid 0.8.2", ] -[[package]] -name = "ethabi" -version = "18.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7413c5f74cc903ea37386a8965a936cbeb334bd270862fdece542c1b2dcbc898" -dependencies = [ - "ethereum-types", - "hex", - "serde", - "sha3", - "thiserror", - "uint", -] - [[package]] name = "ethbloom" version = "0.13.0" @@ -2283,6 +2276,16 @@ dependencies = [ "syn 2.0.37", ] +[[package]] +name = "serdect" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a84f14a19e9a014bb9f4512488d9829a68e04ecabffb0f9904cd1ace94598177" +dependencies = [ + "base16ct", + "serde", +] + [[package]] name = "sha1" version = "0.10.6" diff --git a/crates/torii/core/Cargo.toml b/crates/torii/core/Cargo.toml index 12591f12f5..930d4c550c 100644 --- a/crates/torii/core/Cargo.toml +++ b/crates/torii/core/Cargo.toml @@ -21,12 +21,14 @@ hex = "0.4.3" lazy_static = "1.4.0" log = "0.4.17" once_cell.workspace = true +scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true slab = "0.4.2" sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } starknet-crypto.workspace = true starknet.workspace = true +thiserror.workspace = true tokio = { version = "1.32.0", features = [ "sync" ], default-features = true } tokio-stream = "0.1.11" tokio-util = "0.7.7" @@ -35,3 +37,5 @@ tracing.workspace = true [dev-dependencies] camino.workspace = true +dojo-test-utils = { path = "../../dojo-test-utils" } +sozo = { path = "../../sozo" } diff --git a/crates/torii/core/src/engine.rs b/crates/torii/core/src/engine.rs index 88aca228a1..1e5a7c1be7 100644 --- a/crates/torii/core/src/engine.rs +++ b/crates/torii/core/src/engine.rs @@ -68,8 +68,9 @@ where } pub async fn start(&mut self, cts: CancellationToken) -> Result<(), Box> { - if self.db.head().await? == 0 { - self.db.set_head(self.config.start_block).await?; + let mut head = self.db.head().await?; + if head == 0 { + head = self.config.start_block; } else if self.config.start_block != 0 { warn!("start block ignored, stored head exists and will be used instead"); } @@ -79,9 +80,8 @@ where break Ok(()); } - let head = self.db.head().await?; match self.sync_to_head(head).await { - Ok(block_with_txs) => block_with_txs, + Ok(latest_block_number) => head = latest_block_number, Err(e) => { error!("getting block: {}", e); continue; @@ -123,7 +123,7 @@ where self.process(block_with_txs).await?; - self.db.set_head(from).await?; + self.db.set_head(from); self.db.execute().await?; from += 1; } @@ -237,7 +237,7 @@ async fn process_event( event: &Event, event_idx: usize, ) -> Result<(), Box> { - db.store_event(event, event_idx, invoke_receipt.transaction_hash).await?; + db.store_event(event, event_idx, invoke_receipt.transaction_hash); for processor in processors { if get_selector_from_name(&processor.event_key())? == event.keys[0] { diff --git a/crates/torii/core/src/lib.rs b/crates/torii/core/src/lib.rs index 668937bca8..6049e63d98 100644 --- a/crates/torii/core/src/lib.rs +++ b/crates/torii/core/src/lib.rs @@ -9,6 +9,7 @@ pub mod simple_broker; pub mod sql; pub mod types; +#[allow(dead_code)] #[derive(FromRow, Deserialize)] pub struct World { #[sqlx(try_from = "String")] diff --git a/crates/torii/core/src/processors/store_set_record.rs b/crates/torii/core/src/processors/store_set_record.rs index ce62b968e4..1f7ccce3f5 100644 --- a/crates/torii/core/src/processors/store_set_record.rs +++ b/crates/torii/core/src/processors/store_set_record.rs @@ -1,6 +1,6 @@ use anyhow::{Error, Ok, Result}; use async_trait::async_trait; -use starknet::core::types::{BlockWithTxs, Event, InvokeTransactionReceipt}; +use starknet::core::types::{BlockId, BlockTag, BlockWithTxs, Event, InvokeTransactionReceipt}; use starknet::core::utils::parse_cairo_short_string; use starknet::providers::Provider; use starknet_crypto::FieldElement; @@ -17,14 +17,14 @@ const MODEL_INDEX: usize = 0; const NUM_KEYS_INDEX: usize = 1; #[async_trait] -impl EventProcessor

for StoreSetRecordProcessor { +impl EventProcessor

for StoreSetRecordProcessor { fn event_key(&self) -> String { "StoreSetRecord".to_string() } async fn process( &self, - _world: &WorldContractReader<'_, P>, + world: &WorldContractReader<'_, P>, db: &mut Sql, _provider: &P, _block: &BlockWithTxs, @@ -34,10 +34,10 @@ impl EventProcessor

for StoreSetRecordProcessor { let name = parse_cairo_short_string(&event.data[MODEL_INDEX])?; info!("store set record: {}", name); + let model = world.model(&name, BlockId::Tag(BlockTag::Pending)).await?; let keys = values_at(&event.data, NUM_KEYS_INDEX)?; - let values_index = keys.len() + NUM_KEYS_INDEX + 2; - let values = values_at(&event.data, values_index)?; - db.set_entity(name, keys, values).await?; + let entity = model.entity(keys, BlockId::Tag(BlockTag::Pending)).await?; + db.set_entity(entity).await?; Ok(()) } } diff --git a/crates/torii/core/src/sql.rs b/crates/torii/core/src/sql.rs index 4991574bc8..34bca747a3 100644 --- a/crates/torii/core/src/sql.rs +++ b/crates/torii/core/src/sql.rs @@ -1,12 +1,11 @@ use std::str::FromStr; -use anyhow::Result; +use anyhow::{anyhow, Result}; use chrono::{DateTime, Utc}; -use dojo_types::core::CairoType; +use dojo_types::primitive::Primitive; use dojo_types::schema::Ty; -use dojo_world::manifest::{Manifest, System}; +use dojo_world::manifest::System; use sqlx::pool::PoolConnection; -use sqlx::sqlite::SqliteRow; use sqlx::{Executor, Pool, Row, Sqlite}; use starknet::core::types::{Event, FieldElement}; use starknet_crypto::poseidon_hash_many; @@ -49,30 +48,6 @@ impl Sql { Ok(Self { pool, world_address, query_queue: vec![] }) } - pub async fn load_from_manifest(&mut self, manifest: Manifest) -> Result<()> { - let mut updates = vec![ - format!("world_address = '{:#x}'", self.world_address), - format!("world_class_hash = '{:#x}'", manifest.world.class_hash), - format!("executor_class_hash = '{:#x}'", manifest.executor.class_hash), - ]; - - if let Some(executor_address) = manifest.executor.address { - updates.push(format!("executor_address = '{:#x}'", executor_address)); - } - - self.query_queue.push(format!( - "UPDATE worlds SET {} WHERE id = '{:#x}'", - updates.join(","), - self.world_address - )); - - for system in manifest.systems { - self.register_system(system).await?; - } - - self.execute().await - } - pub async fn head(&self) -> Result { let mut conn: PoolConnection = self.pool.acquire().await?; let indexer: (i64,) = sqlx::query_as(&format!( @@ -84,12 +59,11 @@ impl Sql { Ok(indexer.0.try_into().expect("doesnt fit in u64")) } - pub async fn set_head(&mut self, head: u64) -> Result<()> { + pub fn set_head(&mut self, head: u64) { self.query_queue.push(format!( "UPDATE indexers SET head = {head} WHERE id = '{:#x}'", self.world_address )); - Ok(()) } pub async fn world(&self) -> Result { @@ -102,19 +76,6 @@ impl Sql { Ok(meta) } - pub async fn set_world(&mut self, world: World) -> Result<()> { - self.query_queue.push(format!( - "UPDATE worlds SET world_address='{:#x}', world_class_hash='{:#x}', \ - executor_address='{:#x}', executor_class_hash='{:#x}' WHERE id = '{:#x}'", - world.world_address, - world.world_class_hash, - world.executor_address, - world.executor_class_hash, - world.world_address, - )); - Ok(()) - } - pub async fn register_model( &mut self, model: Ty, @@ -133,7 +94,7 @@ impl Sql { )); let mut model_idx = 0_usize; - self.build_queries_recursive(&model, vec![model.name()], &mut model_idx); + self.build_register_queries_recursive(&model, vec![model.name()], &mut model_idx); // Since previous query has not been executed, we have to make sure created_at exists let created_at: DateTime = @@ -166,20 +127,31 @@ impl Sql { Ok(()) } - pub async fn set_entity( - &mut self, - model: String, - keys: Vec, - values: Vec, - ) -> Result<()> { + pub async fn set_entity(&mut self, entity: Ty) -> Result<()> { + let keys = if let Ty::Struct(s) = &entity { + let mut keys = Vec::new(); + for m in s.keys() { + keys.extend(m.serialize()?); + } + keys + } else { + return Err(anyhow!("Entity is not a struct")); + }; + let entity_id = format!("{:#x}", poseidon_hash_many(&keys)); - let entity_result = sqlx::query("SELECT * FROM entities WHERE id = ?") - .bind(&entity_id) - .fetch_optional(&self.pool) - .await?; + let existing: Option<(String,)> = + sqlx::query_as("SELECT model_names FROM entities WHERE id = ?") + .bind(&entity_id) + .fetch_optional(&self.pool) + .await?; + + let model_names = if let Some((model_names,)) = existing { + format!("{},{}", model_names, entity.name()) + } else { + entity.name() + }; let keys_str = felts_sql_string(&keys); - let model_names = model_names_sql_string(entity_result, &model)?; self.query_queue.push(format!( "INSERT INTO entities (id, keys, model_names) VALUES ('{}', '{}', '{}') ON \ CONFLICT(id) DO UPDATE SET model_names=excluded.model_names, \ @@ -187,36 +159,9 @@ impl Sql { entity_id, keys_str, model_names )); - let members: Vec<(String, String, String)> = sqlx::query_as( - "SELECT id, name, type FROM model_members WHERE model_id = ? ORDER BY model_idx, \ - member_idx ASC", - ) - .bind(model.clone()) - .fetch_all(&self.pool) - .await?; + let path = vec![entity.name()]; + self.build_set_entity_queries_recursive(path, &entity_id, &entity); - let (primitive_members, _): (Vec<_>, Vec<_>) = - members.into_iter().partition(|member| CairoType::from_str(&member.2).is_ok()); - - // keys are part of model members, so combine keys and model values array - let mut member_values: Vec = Vec::new(); - member_values.extend(keys.clone()); - member_values.extend(values); - - let insert_models: Vec<_> = primitive_members - .into_iter() - .zip(member_values.into_iter()) - .map(|((id, name, ty), value)| { - format!( - "INSERT OR REPLACE INTO [{id}] (entity_id, external_{name}) VALUES \ - ('{entity_id}' {})", - CairoType::from_str(&ty).unwrap().format_for_sql(vec![&value]).unwrap() - ) - }) - .collect(); - - // tx commit required - self.query_queue.extend(insert_models); self.execute().await?; let query_result = sqlx::query("SELECT created_at FROM entities WHERE id = ?") @@ -235,10 +180,9 @@ impl Sql { Ok(()) } - pub async fn delete_entity(&mut self, model: String, key: FieldElement) -> Result<()> { + pub fn delete_entity(&mut self, model: String, key: FieldElement) { let query = format!("DELETE FROM {model} WHERE id = {key}"); self.query_queue.push(query); - Ok(()) } pub async fn entity(&self, model: String, key: FieldElement) -> Result> { @@ -256,12 +200,12 @@ impl Sql { Ok(rows.drain(..).map(|row| serde_json::from_str(&row.2).unwrap()).collect()) } - pub async fn store_system_call( + pub fn store_system_call( &mut self, system: String, transaction_hash: FieldElement, calldata: &[FieldElement], - ) -> Result<()> { + ) { let query = format!( "INSERT OR IGNORE INTO system_calls (data, transaction_hash, system_id) VALUES ('{}', \ '{:#x}', '{}')", @@ -270,15 +214,9 @@ impl Sql { system ); self.query_queue.push(query); - Ok(()) } - pub async fn store_event( - &mut self, - event: &Event, - event_idx: usize, - transaction_hash: FieldElement, - ) -> Result<()> { + pub fn store_event(&mut self, event: &Event, event_idx: usize, transaction_hash: FieldElement) { let keys_str = felts_sql_string(&event.keys); let data_str = felts_sql_string(&event.data); @@ -290,84 +228,127 @@ impl Sql { ); self.query_queue.push(query); - Ok(()) } - fn build_queries_recursive( + fn build_register_queries_recursive( &mut self, model: &Ty, - table_path: Vec, + path: Vec, model_idx: &mut usize, ) { - self.build_model_query(table_path.clone(), model, *model_idx); + if let Ty::Enum(_) = model { + // Complex enum values not supported yet. + return; + } - match model { + self.build_model_query(path.clone(), model, *model_idx); + + if let Ty::Struct(s) = model { + for member in s.children.iter() { + if let Ty::Primitive(_) = member.ty { + continue; + } + + let mut path_clone = path.clone(); + path_clone.push(member.ty.name()); + + self.build_register_queries_recursive( + &member.ty, + path_clone, + &mut (*model_idx + 1), + ); + } + } + } + + fn build_set_entity_queries_recursive(&mut self, path: Vec, id: &str, entity: &Ty) { + match entity { Ty::Struct(s) => { + let table_id = path.join("$"); + let mut columns = vec!["entity_id".to_string()]; + let mut values = vec![format!("'{id}'")]; + for member in s.children.iter() { - if let Ty::Primitive(_) = member.ty { - continue; + match &member.ty { + Ty::Primitive(ty) => { + columns.push(format!("external_{}", &member.name)); + values.push(ty.to_sql_value().unwrap()); + } + Ty::Enum(e) => { + columns.push(format!("external_{}", &member.name)); + values.push(e.to_sql_value().unwrap()); + } + _ => {} } + } - let mut table_path_clone = table_path.clone(); - table_path_clone.push(member.ty.name()); + self.query_queue.push(format!( + "INSERT OR REPLACE INTO [{table_id}] ({}) VALUES ({})", + columns.join(", "), + values.join(", ") + )); - self.build_queries_recursive( - &member.ty, - table_path_clone, - &mut (*model_idx + 1), - ); + for member in s.children.iter() { + if let Ty::Struct(_) = &member.ty { + let mut path_clone = path.clone(); + path_clone.push(member.ty.name()); + + self.build_set_entity_queries_recursive(path_clone, id, &member.ty); + } } } Ty::Enum(e) => { - for child in e.children.iter() { - let mut table_path_clone = table_path.clone(); - table_path_clone.push(child.1.name()); - self.build_model_query(table_path_clone.clone(), &child.1, *model_idx); - self.build_queries_recursive(&child.1, table_path_clone, &mut (*model_idx + 1)); + for child in e.options.iter() { + let mut path_clone = path.clone(); + path_clone.push(child.1.name()); + // self.build_entity_query(path_clone.clone(), id, &child.1); + self.build_set_entity_queries_recursive(path_clone, id, &child.1); } } _ => {} } } - fn build_model_query(&mut self, table_path: Vec, model: &Ty, model_idx: usize) { - let table_id = table_path.join("$"); + fn build_model_query(&mut self, path: Vec, model: &Ty, model_idx: usize) { + let table_id = path.join("$"); let mut query = format!( "CREATE TABLE IF NOT EXISTS [{table_id}] (entity_id TEXT NOT NULL PRIMARY KEY, " ); - match model { - Ty::Struct(s) => { - for (member_idx, member) in s.children.iter().enumerate() { - if let Ok(cairo_type) = CairoType::from_str(&member.ty.name()) { - query.push_str(&format!( - "external_{} {}, ", - member.name, - cairo_type.to_sql_type() - )); - }; - - self.query_queue.push(format!( - "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, \ - member_idx, name, type, key) VALUES ('{table_id}', '{}', '{model_idx}', \ - '{member_idx}', '{}', '{}', {})", - table_path[0], - member.name, - member.ty.name(), - member.key, + if let Ty::Struct(s) = model { + for (member_idx, member) in s.children.iter().enumerate() { + let name = member.name.clone(); + if let Ok(cairo_type) = Primitive::from_str(&member.ty.name()) { + query.push_str(&format!("external_{name} {}, ", cairo_type.to_sql_type())); + } else if let Ty::Enum(e) = &member.ty { + let options = e + .options + .iter() + .map(|c| format!("'{}'", c.0)) + .collect::>() + .join(", "); + query.push_str(&format!( + "external_{name} TEXT CHECK(external_{name} IN ({options})) NOT NULL, ", )); } + + self.query_queue.push(format!( + "INSERT OR IGNORE INTO model_members (id, model_id, model_idx, member_idx, \ + name, type, key) VALUES ('{table_id}', '{}', '{model_idx}', '{member_idx}', \ + '{name}', '{}', {})", + path[0], + member.ty.name(), + member.key, + )); } - Ty::Enum(_) => {} - _ => {} } query.push_str("created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "); // If this is not the Model's root table, create a reference to the parent. - if table_path.len() > 1 { - let parent_table_id = table_path[..table_path.len() - 1].join("$"); + if path.len() > 1 { + let parent_table_id = path[..path.len() - 1].join("$"); query.push_str(&format!( "FOREIGN KEY (entity_id) REFERENCES {parent_table_id} (entity_id), " )); @@ -392,22 +373,6 @@ impl Sql { } } -fn model_names_sql_string(entity_result: Option, new_model: &str) -> Result { - let model_names = match entity_result { - Some(entity) => { - let existing = entity.try_get::("model_names")?; - if existing.contains(new_model) { - existing - } else { - format!("{},{}", existing, new_model) - } - } - None => new_model.to_string(), - }; - - Ok(model_names) -} - fn felts_sql_string(felts: &[FieldElement]) -> String { felts.iter().map(|k| format!("{:#x}", k)).collect::>().join("/") + "/" } diff --git a/crates/torii/core/src/sql_test.rs b/crates/torii/core/src/sql_test.rs index 1090cc09da..7ba279366e 100644 --- a/crates/torii/core/src/sql_test.rs +++ b/crates/torii/core/src/sql_test.rs @@ -1,138 +1,102 @@ -use camino::Utf8PathBuf; -use dojo_types::core::CairoType; -use dojo_types::schema::{Member, Struct, Ty}; -use dojo_world::manifest::System; -use sqlx::sqlite::SqlitePool; -use starknet::core::types::{Event, FieldElement}; - +use dojo_test_utils::migration::prepare_migration; +use dojo_test_utils::sequencer::{ + get_default_test_starknet_config, SequencerConfig, TestSequencer, +}; +use dojo_world::migration::strategy::MigrationStrategy; +use scarb_ui::{OutputFormat, Ui, Verbosity}; +use sozo::ops::migration::execute_strategy; +use sqlx::sqlite::SqlitePoolOptions; +use starknet::core::types::{BlockId, BlockTag, Event, FieldElement}; +use starknet::providers::jsonrpc::HttpTransport; +use starknet::providers::JsonRpcClient; +use torii_client::contract::world::WorldContractReader; + +use crate::engine::{Engine, EngineConfig, Processors}; +use crate::processors::register_model::RegisterModelProcessor; +use crate::processors::register_system::RegisterSystemProcessor; +use crate::processors::store_set_record::StoreSetRecordProcessor; use crate::sql::Sql; -#[sqlx::test(migrations = "../migrations")] -async fn test_load_from_manifest(pool: SqlitePool) { - let manifest = dojo_world::manifest::Manifest::load_from_path( - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) - .unwrap(), - ) - .unwrap(); +pub async fn bootstrap_engine<'a>( + world: &'a WorldContractReader<'a, JsonRpcClient>, + db: &'a mut Sql, + provider: &'a JsonRpcClient, + migration: &MigrationStrategy, + sequencer: &TestSequencer, +) -> Result>, Box> { + let mut account = sequencer.account(); + account.set_block_id(BlockId::Tag(BlockTag::Pending)); + + let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); + execute_strategy(migration, &account, &ui, None).await.unwrap(); + + let mut engine = Engine::new( + world, + db, + provider, + Processors { + event: vec![ + Box::new(RegisterModelProcessor), + Box::new(RegisterSystemProcessor), + Box::new(StoreSetRecordProcessor), + ], + ..Processors::default() + }, + EngineConfig::default(), + None, + ); + + let _ = engine.sync_to_head(0).await?; - let mut state = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - state.load_from_manifest(manifest.clone()).await.unwrap(); + Ok(engine) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_load_from_remote() { + let pool = + SqlitePoolOptions::new().max_connections(5).connect("sqlite::memory:").await.unwrap(); + sqlx::migrate!("../migrations").run(&pool).await.unwrap(); + let migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); + let sequencer = + TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()).await; + let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); + let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); + + let mut db = Sql::new(pool.clone(), migration.world_address().unwrap()).await.unwrap(); + let _ = bootstrap_engine(&world, &mut db, &provider, &migration, &sequencer).await; let models = sqlx::query("SELECT * FROM models").fetch_all(&pool).await.unwrap(); - assert_eq!(models.len(), 0); - - let mut world = state.world().await.unwrap(); - - assert_eq!(world.world_address.0, FieldElement::ZERO); - assert_eq!(world.world_class_hash.0, manifest.world.class_hash); - assert_eq!(world.executor_address.0, FieldElement::ZERO); - assert_eq!(world.executor_class_hash.0, manifest.executor.class_hash); - - world.executor_address.0 = FieldElement::ONE; - state.set_world(world).await.unwrap(); - state.execute().await.unwrap(); - - let world = state.world().await.unwrap(); - assert_eq!(world.executor_address.0, FieldElement::ONE); - - let head = state.head().await.unwrap(); - assert_eq!(head, 0); - - state.set_head(1).await.unwrap(); - state.execute().await.unwrap(); - - let head = state.head().await.unwrap(); - assert_eq!(head, 1); - - state - .register_model( - Ty::Struct(Struct { - name: "Position".into(), - children: vec![Member { - name: "test".into(), - ty: Ty::Primitive(CairoType::U32(None)), - key: false, - }], - }), - vec![], - FieldElement::TWO, - ) - .await - .unwrap(); - state.execute().await.unwrap(); - - let (id, name, class_hash): (String, String, String) = - sqlx::query_as("SELECT id, name, class_hash FROM models WHERE id = 'Position'") + assert_eq!(models.len(), 2); + + let (id, name): (String, String) = + sqlx::query_as("SELECT id, name FROM models WHERE id = 'Position'") .fetch_one(&pool) .await .unwrap(); assert_eq!(id, "Position"); assert_eq!(name, "Position"); - assert_eq!(class_hash, format!("{:#x}", FieldElement::TWO)); - - let position_models = sqlx::query("SELECT * FROM [Position]").fetch_all(&pool).await.unwrap(); - assert_eq!(position_models.len(), 0); - - state - .register_system(System { - name: "Position".into(), - inputs: vec![], - outputs: vec![], - class_hash: FieldElement::THREE, - dependencies: vec![], - ..Default::default() - }) - .await - .unwrap(); - state.execute().await.unwrap(); - - let (id, name, class_hash): (String, String, String) = - sqlx::query_as("SELECT id, name, class_hash FROM systems WHERE id = 'Position'") + + let (id, name): (String, String) = + sqlx::query_as("SELECT id, name FROM models WHERE id = 'Moves'") .fetch_one(&pool) .await .unwrap(); - assert_eq!(id, "Position"); - assert_eq!(name, "Position"); - assert_eq!(class_hash, format!("{:#x}", FieldElement::THREE)); - - state - .set_entity( - "Position".to_string(), - vec![FieldElement::ONE], - vec![ - FieldElement::ONE, - FieldElement::from_dec_str("42").unwrap(), - FieldElement::from_dec_str("69").unwrap(), - ], - ) - .await - .unwrap(); - - // state - // .store_system_call( - // "Test".into(), - // FieldElement::from_str("0x4").unwrap(), - // &[FieldElement::ONE, FieldElement::TWO, FieldElement::THREE], - // ) - // .await - // .unwrap(); - - state - .store_event( - &Event { - from_address: FieldElement::ONE, - keys: Vec::from([FieldElement::TWO]), - data: Vec::from([FieldElement::TWO, FieldElement::THREE]), - }, - 0, - FieldElement::THREE, - ) - .await - .unwrap(); - - state.execute().await.unwrap(); + assert_eq!(id, "Moves"); + assert_eq!(name, "Moves"); + + db.store_event( + &Event { + from_address: FieldElement::ONE, + keys: Vec::from([FieldElement::TWO]), + data: Vec::from([FieldElement::TWO, FieldElement::THREE]), + }, + 0, + FieldElement::THREE, + ); + + db.execute().await.unwrap(); let keys = format!("{:#x}/", FieldElement::TWO); let query = format!("SELECT data, transaction_hash FROM events WHERE keys = '{}'", keys); diff --git a/crates/torii/graphql/Cargo.toml b/crates/torii/graphql/Cargo.toml index 05e33083c2..5842bf0917 100644 --- a/crates/torii/graphql/Cargo.toml +++ b/crates/torii/graphql/Cargo.toml @@ -19,7 +19,6 @@ indexmap = "1.9.3" scarb-ui.workspace = true serde.workspace = true serde_json.workspace = true -sozo = { path = "../../sozo" } sqlx = { version = "0.6.2", features = [ "chrono", "macros", "offline", "runtime-actix-rustls", "sqlite", "uuid" ] } tokio-stream = "0.1.11" tokio-util = "0.7.7" diff --git a/crates/torii/graphql/src/tests/entities_test.rs b/crates/torii/graphql/src/tests/entities_test.rs index 3fc6fbdc67..d4216c2cc6 100644 --- a/crates/torii/graphql/src/tests/entities_test.rs +++ b/crates/torii/graphql/src/tests/entities_test.rs @@ -1,37 +1,22 @@ #[cfg(test)] mod tests { - use dojo_test_utils::migration::prepare_migration; - use dojo_test_utils::sequencer::{ - get_default_test_starknet_config, SequencerConfig, TestSequencer, - }; + use sqlx::SqlitePool; - use starknet::providers::jsonrpc::HttpTransport; - use starknet::providers::JsonRpcClient; use starknet_crypto::{poseidon_hash_many, FieldElement}; - use torii_client::contract::world::WorldContractReader; use torii_core::sql::Sql; use crate::tests::{ - bootstrap_engine, create_pool, entity_fixtures, paginate, run_graphql_query, Entity, Moves, - Paginate, Position, + entity_fixtures, paginate, run_graphql_query, Entity, Moves, Paginate, Position, }; - #[tokio::test(flavor = "multi_thread")] - async fn test_entity() { - let pool = create_pool().await; + #[sqlx::test(migrations = "../migrations")] + async fn test_entity(pool: SqlitePool) { let mut db = Sql::new(pool.clone(), FieldElement::ZERO).await.unwrap(); - let migration = prepare_migration("../../../examples/ecs/target/dev".into()).unwrap(); - let sequencer = - TestSequencer::start(SequencerConfig::default(), get_default_test_starknet_config()) - .await; - let provider = JsonRpcClient::new(HttpTransport::new(sequencer.url())); - let world = WorldContractReader::new(migration.world_address().unwrap(), &provider); - - let _ = bootstrap_engine(&world, &mut db, &provider, &migration, &sequencer).await; entity_fixtures(&mut db).await; let entity_id = poseidon_hash_many(&[FieldElement::ONE]); + println!("{:#x}", entity_id); let query = format!( r#" {{ diff --git a/crates/torii/graphql/src/tests/mod.rs b/crates/torii/graphql/src/tests/mod.rs index 8742025a3b..7eb3c1fd7a 100644 --- a/crates/torii/graphql/src/tests/mod.rs +++ b/crates/torii/graphql/src/tests/mod.rs @@ -1,21 +1,10 @@ -use camino::Utf8PathBuf; -use dojo_test_utils::sequencer::TestSequencer; -use dojo_world::migration::strategy::MigrationStrategy; -use scarb_ui::{OutputFormat, Ui, Verbosity}; +use dojo_types::primitive::Primitive; +use dojo_types::schema::{Enum, Member, Struct, Ty}; use serde::Deserialize; use serde_json::Value; -use sozo::ops::migration::execute_strategy; -use sqlx::sqlite::SqlitePoolOptions; use sqlx::SqlitePool; -use starknet::core::types::{BlockId, BlockTag, FieldElement}; -use starknet::providers::jsonrpc::HttpTransport; -use starknet::providers::JsonRpcClient; +use starknet::core::types::FieldElement; use tokio_stream::StreamExt; -use torii_client::contract::world::WorldContractReader; -use torii_core::engine::{Engine, EngineConfig, Processors}; -use torii_core::processors::register_model::RegisterModelProcessor; -use torii_core::processors::register_system::RegisterSystemProcessor; -use torii_core::processors::store_set_record::StoreSetRecordProcessor; use torii_core::sql::Sql; mod entities_test; @@ -67,7 +56,6 @@ pub enum Paginate { Backward, } -#[allow(dead_code)] pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { let schema = build_schema(pool).await.unwrap(); let res = schema.execute(query).await; @@ -76,55 +64,6 @@ pub async fn run_graphql_query(pool: &SqlitePool, query: &str) -> Value { serde_json::to_value(res.data).expect("Failed to serialize GraphQL response") } -pub async fn create_pool() -> SqlitePool { - let pool = - SqlitePoolOptions::new().max_connections(5).connect("sqlite::memory:").await.unwrap(); - sqlx::migrate!("../migrations").run(&pool).await.unwrap(); - pool -} - -pub async fn bootstrap_engine<'a>( - world: &'a WorldContractReader<'a, JsonRpcClient>, - db: &'a mut Sql, - provider: &'a JsonRpcClient, - migration: &MigrationStrategy, - sequencer: &TestSequencer, -) -> Result>, Box> { - let mut account = sequencer.account(); - account.set_block_id(BlockId::Tag(BlockTag::Pending)); - - let manifest = dojo_world::manifest::Manifest::load_from_path( - Utf8PathBuf::from_path_buf("../../../examples/ecs/target/dev/manifest.json".into()) - .unwrap(), - ) - .unwrap(); - - db.load_from_manifest(manifest.clone()).await.unwrap(); - - let ui = Ui::new(Verbosity::Verbose, OutputFormat::Text); - execute_strategy(migration, &account, &ui, None).await.unwrap(); - - let mut engine = Engine::new( - world, - db, - provider, - Processors { - event: vec![ - Box::new(RegisterModelProcessor), - Box::new(RegisterSystemProcessor), - Box::new(StoreSetRecordProcessor), - ], - ..Processors::default() - }, - EngineConfig::default(), - None, - ); - - let _ = engine.sync_to_head(0).await?; - - Ok(engine) -} - #[allow(dead_code)] pub async fn run_graphql_subscription( pool: &SqlitePool, @@ -137,34 +76,209 @@ pub async fn run_graphql_subscription( } pub async fn entity_fixtures(db: &mut Sql) { - // Set entity with one moves model - // remaining: 10, last_direction: 0 - let key = vec![FieldElement::ONE]; - let moves_values = vec![FieldElement::from_hex_be("0xa").unwrap(), FieldElement::ZERO]; - db.set_entity("Moves".to_string(), key, moves_values.clone()).await.unwrap(); - - // Set entity with one position model - // x: 42 - // y: 69 - let key = vec![FieldElement::TWO]; - let position_values = vec![ - FieldElement::from_hex_be("0x2a").unwrap(), - FieldElement::from_hex_be("0x45").unwrap(), - ]; - db.set_entity("Position".to_string(), key, position_values.clone()).await.unwrap(); + db.register_model( + Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(None)), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(None)), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: None, + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + }), + vec![], + FieldElement::ONE, + ) + .await + .unwrap(); + + db.register_model( + Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(None)), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(None)), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(None)), + }, + ], + }), + }, + ], + }), + vec![], + FieldElement::TWO, + ) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::ONE))), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(Some(10))), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: Some(1), + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + })) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::TWO))), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(42))), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(69))), + }, + ], + }), + }, + ], + })) + .await + .unwrap(); // Set an entity with both moves and position models - // remaining: 1, last_direction: 0 - // x: 69 - // y: 42 - let key = vec![FieldElement::THREE]; - let moves_values = vec![FieldElement::from_hex_be("0x1").unwrap(), FieldElement::ZERO]; - let position_values = vec![ - FieldElement::from_hex_be("0x45").unwrap(), - FieldElement::from_hex_be("0x2a").unwrap(), - ]; - db.set_entity("Moves".to_string(), key.clone(), moves_values).await.unwrap(); - db.set_entity("Position".to_string(), key, position_values).await.unwrap(); + db.set_entity(Ty::Struct(Struct { + name: "Moves".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), + }, + Member { + name: "remaining".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U8(Some(10))), + }, + Member { + name: "last_direction".to_string(), + key: false, + ty: Ty::Enum(Enum { + name: "Direction".to_string(), + option: Some(2), + options: vec![ + ("None".to_string(), Ty::Tuple(vec![])), + ("Left".to_string(), Ty::Tuple(vec![])), + ("Right".to_string(), Ty::Tuple(vec![])), + ("Up".to_string(), Ty::Tuple(vec![])), + ("Down".to_string(), Ty::Tuple(vec![])), + ], + }), + }, + ], + })) + .await + .unwrap(); + + db.set_entity(Ty::Struct(Struct { + name: "Position".to_string(), + children: vec![ + Member { + name: "player".to_string(), + key: true, + ty: Ty::Primitive(Primitive::ContractAddress(Some(FieldElement::THREE))), + }, + Member { + name: "vec".to_string(), + key: false, + ty: Ty::Struct(Struct { + name: "Vec2".to_string(), + children: vec![ + Member { + name: "x".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(42))), + }, + Member { + name: "y".to_string(), + key: false, + ty: Ty::Primitive(Primitive::U32(Some(69))), + }, + ], + }), + }, + ], + })) + .await + .unwrap(); db.execute().await.unwrap(); } diff --git a/crates/torii/server/src/cli.rs b/crates/torii/server/src/cli.rs index d800d5bdb3..b0718e59b2 100644 --- a/crates/torii/server/src/cli.rs +++ b/crates/torii/server/src/cli.rs @@ -1,14 +1,7 @@ -use std::env; use std::net::SocketAddr; -use std::str::FromStr; use std::sync::Arc; -use anyhow::{anyhow, Context}; -use camino::Utf8PathBuf; use clap::Parser; -use dojo_world::manifest::Manifest; -use dojo_world::metadata::{dojo_metadata_from_workspace, Environment}; -use scarb::core::Config; use sqlx::sqlite::SqlitePoolOptions; use starknet::core::types::FieldElement; use starknet::providers::jsonrpc::HttpTransport; @@ -32,23 +25,20 @@ mod server; struct Args { /// The world to index #[arg(short, long = "world", env = "DOJO_WORLD_ADDRESS")] - world_address: Option, + world_address: FieldElement, /// The rpc endpoint to use #[arg(long, default_value = "http://localhost:5050")] rpc: String, /// Database url #[arg(short, long, default_value = "sqlite::memory:")] database_url: String, - /// Specify a local manifest to intiailize from - #[arg(short, long, env = "DOJO_MANIFEST_FILE")] - manifest: Option, /// Specify a block to start indexing from, ignored if stored head exists #[arg(short, long, default_value = "0")] start_block: u64, - /// Host address for GraphQL/gRPC endpoints + /// Host address for api endpoints #[arg(long, default_value = "0.0.0.0")] host: String, - /// Port number for GraphQL/gRPC endpoints + /// Port number for api endpoints #[arg(long, default_value = "8080")] port: u16, } @@ -80,15 +70,10 @@ async fn main() -> anyhow::Result<()> { let provider: Arc<_> = JsonRpcClient::new(HttpTransport::new(Url::parse(&args.rpc)?)).into(); - let (manifest, env) = get_manifest_and_env(args.manifest.as_ref()) - .with_context(|| "Failed to get manifest file".to_string())?; - // Get world address - let world_address = get_world_address(&args, &manifest, env.as_ref())?; - let world = WorldContractReader::new(world_address, &provider); + let world = WorldContractReader::new(args.world_address, &provider); - let mut db = Sql::new(pool.clone(), world_address).await?; - db.load_from_manifest(manifest.clone()).await?; + let mut db = Sql::new(pool.clone(), args.world_address).await?; let processors = Processors { event: vec![ Box::new(RegisterModelProcessor), @@ -119,7 +104,7 @@ async fn main() -> anyhow::Result<()> { } } - res = server::spawn_server(&addr, &pool, world_address, block_receiver, Arc::clone(&provider)) => { + res = server::spawn_server(&addr, &pool, args.world_address, block_receiver, Arc::clone(&provider)) => { if let Err(e) = res { error!("Server failed with error: {e}"); } @@ -132,69 +117,3 @@ async fn main() -> anyhow::Result<()> { Ok(()) } - -// Tries to find scarb manifest first for env variables -// -// Use manifest path from cli args, -// else uses scarb manifest to derive path of dojo manifest file, -// else try to derive manifest path from scarb manifest -// else try `./target/dev/manifest.json` as dojo manifest path -// -// If neither of this work return an error and exit -fn get_manifest_and_env( - args_path: Option<&Utf8PathBuf>, -) -> anyhow::Result<(Manifest, Option)> { - let config; - let ws = if let Ok(scarb_manifest_path) = scarb::ops::find_manifest_path(None) { - config = Config::builder(scarb_manifest_path) - .log_filter_directive(env::var_os("SCARB_LOG")) - .build() - .with_context(|| "Couldn't build scarb config".to_string())?; - scarb::ops::read_workspace(config.manifest_path(), &config).ok() - } else { - None - }; - - let manifest = if let Some(manifest_path) = args_path { - Manifest::load_from_path(manifest_path)? - } else if let Some(ref ws) = ws { - let target_dir = ws.target_dir().path_existent()?; - let target_dir = target_dir.join(ws.config().profile().as_str()); - let manifest_path = target_dir.join("manifest.json"); - Manifest::load_from_path(manifest_path)? - } else { - return Err(anyhow!( - "Cannot find Scarb manifest file. Either run this command from within a Scarb project \ - or specify it using `--manifest` argument" - )); - }; - let env = if let Some(ws) = ws { - dojo_metadata_from_workspace(&ws).and_then(|inner| inner.env().cloned()) - } else { - None - }; - Ok((manifest, env)) -} - -fn get_world_address( - args: &Args, - manifest: &Manifest, - env_metadata: Option<&Environment>, -) -> anyhow::Result { - if let Some(address) = args.world_address { - return Ok(address); - } - - if let Some(world_address) = env_metadata.and_then(|env| env.world_address()) { - return Ok(FieldElement::from_str(world_address)?); - } - - if let Some(address) = manifest.world.address { - Ok(address) - } else { - Err(anyhow!( - "Could not find World address. Please specify it with --world, or in manifest.json or \ - [tool.dojo.env] in Scarb.toml" - )) - } -}