diff --git a/.env.example b/.env.example index 02d235e8..57635205 100644 --- a/.env.example +++ b/.env.example @@ -3,13 +3,23 @@ PORT= DATABASE_URL= MADARA_RPC_URL= DA_LAYER= +SETTLEMENT_LAYER= # Ethereum +ETHEREUM_PRIVATE_KEY= ETHEREUM_RPC_URL= MEMORY_PAGES_CONTRACT_ADDRESS= +STARKNET_SOLIDITY_CORE_CONTRACT_ADDRESS= + + +# Starknet +STARKNET_PUBLIC_KEY= +STARNET_PRIVATE_KEY= +STARKNET_RPC_URL= +STARKNET_CAIRO_CORE_CONTRACT_ADDRESS= # MongoDB connection string -MONGODB_CONNECTION_STRING +MONGODB_CONNECTION_STRING= # SQS AWS_ACCESS_KEY_ID= @@ -17,4 +27,4 @@ AWS_SECRET_ACCESS_KEY= # S3 AWS_S3_BUCKET_NAME= -AWS_S3_BUCKET_REGION= \ No newline at end of file +AWS_S3_BUCKET_REGION= diff --git a/.gitignore b/.gitignore index 0100788a..6d478ee7 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,7 @@ /target .env .idea -.DS_Store \ No newline at end of file +.DS_Store + +*.code-workspace +.vscode diff --git a/Cargo.lock b/Cargo.lock index d266015f..e1432934 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -72,43 +72,61 @@ name = "alloy" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-consensus 0.1.0", "alloy-core 0.6.4", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-network 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-provider 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-rpc-client 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-signer 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-eips 0.1.0", + "alloy-network 0.1.0", + "alloy-provider 0.1.0", + "alloy-rpc-client 0.1.0", + "alloy-signer 0.1.0", "alloy-signer-wallet", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-transport 0.1.0", + "alloy-transport-http 0.1.0", "reqwest 0.12.5", ] [[package]] name = "alloy" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9134b68e24175eff6c3c4d2bffeefb0a1b7435462130862c88d1524ca376e7e5" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-consensus 0.1.2", "alloy-contract", "alloy-core 0.7.6", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-eips 0.1.2", "alloy-genesis", - "alloy-provider 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-rpc-client 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-network 0.1.2", + "alloy-provider 0.1.2", + "alloy-pubsub", + "alloy-rpc-client 0.1.2", + "alloy-rpc-types 0.1.2", + "alloy-serde 0.1.2", + "alloy-signer 0.1.2", + "alloy-signer-local", + "alloy-transport 0.1.2", + "alloy-transport-http 0.1.2", + "alloy-transport-ipc", + "alloy-transport-ws", "reqwest 0.12.5", ] +[[package]] +name = "alloy-chains" +version = "0.1.21" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd47e5f8545bdf53beb545d3c039b4afa16040bdf69c50100581579b08776afd" +dependencies = [ + "num_enum", + "strum 0.26.2", +] + [[package]] name = "alloy-consensus" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-eips 0.1.0", "alloy-primitives 0.6.4", "alloy-rlp", "sha2", @@ -116,30 +134,33 @@ dependencies = [ [[package]] name = "alloy-consensus" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a016bfa21193744d4c38b3f3ab845462284d129e5e23c7cc0fafca7e92d9db37" dependencies = [ - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-eips 0.1.2", "alloy-primitives 0.7.6", "alloy-rlp", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-serde 0.1.2", "c-kzg", "serde", ] [[package]] name = "alloy-contract" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e47b2a620fd588d463ccf0f5931b41357664b293a8d31592768845a2a101bb9e" dependencies = [ "alloy-dyn-abi 0.7.6", "alloy-json-abi 0.7.6", - "alloy-network 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-network 0.1.2", "alloy-primitives 0.7.6", - "alloy-provider 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-provider 0.1.2", + "alloy-pubsub", + "alloy-rpc-types-eth", "alloy-sol-types 0.7.6", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-transport 0.1.2", "futures", "futures-util", "thiserror", @@ -210,7 +231,7 @@ source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7e dependencies = [ "alloy-primitives 0.6.4", "alloy-rlp", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-serde 0.1.0", "c-kzg", "once_cell", "serde", @@ -218,13 +239,15 @@ dependencies = [ [[package]] name = "alloy-eips" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32d6d8118b83b0489cfb7e6435106948add2b35217f4a5004ef895f613f60299" dependencies = [ "alloy-primitives 0.7.6", "alloy-rlp", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-serde 0.1.2", "c-kzg", + "derive_more", "once_cell", "serde", "sha2", @@ -232,13 +255,13 @@ dependencies = [ [[package]] name = "alloy-genesis" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "894f33a7822abb018db56b10ab90398e63273ce1b5a33282afd186c132d764a6" dependencies = [ "alloy-primitives 0.7.6", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-serde 0.1.2", "serde", - "serde_json", ] [[package]] @@ -278,8 +301,9 @@ dependencies = [ [[package]] name = "alloy-json-rpc" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "61f0ae6e93b885cc70fe8dae449e7fd629751dbee8f59767eaaa7285333c5727" dependencies = [ "alloy-primitives 0.7.6", "serde", @@ -293,12 +317,12 @@ name = "alloy-network" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-consensus 0.1.0", + "alloy-eips 0.1.0", + "alloy-json-rpc 0.1.0", "alloy-primitives 0.6.4", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-signer 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-rpc-types 0.1.0", + "alloy-signer 0.1.0", "async-trait", "futures-utils-wasm", "serde", @@ -307,17 +331,20 @@ dependencies = [ [[package]] name = "alloy-network" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc122cbee2b8523854cc11d87bcd5773741602c553d2d2d106d82eeb9c16924a" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-consensus 0.1.2", + "alloy-eips 0.1.2", + "alloy-json-rpc 0.1.2", "alloy-primitives 0.7.6", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-signer 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-rpc-types-eth", + "alloy-serde 0.1.2", + "alloy-signer 0.1.2", "alloy-sol-types 0.7.6", "async-trait", + "auto_impl", "futures-utils-wasm", "thiserror", ] @@ -371,14 +398,14 @@ name = "alloy-provider" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-network 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-json-rpc 0.1.0", + "alloy-network 0.1.0", "alloy-primitives 0.6.4", - "alloy-rpc-client 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-rpc-client 0.1.0", "alloy-rpc-trace-types", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-rpc-types 0.1.0", + "alloy-transport 0.1.0", + "alloy-transport-http 0.1.0", "async-stream", "async-trait", "auto_impl", @@ -393,18 +420,23 @@ dependencies = [ [[package]] name = "alloy-provider" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d5af289798fe8783acd0c5f10644d9d26f54a12bc52a083e4f3b31718e9bf92" dependencies = [ - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-network 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-chains", + "alloy-consensus 0.1.2", + "alloy-eips 0.1.2", + "alloy-json-rpc 0.1.2", + "alloy-network 0.1.2", "alloy-primitives 0.7.6", - "alloy-rpc-client 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-rpc-types-trace", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-pubsub", + "alloy-rpc-client 0.1.2", + "alloy-rpc-types-eth", + "alloy-transport 0.1.2", + "alloy-transport-http 0.1.2", + "alloy-transport-ipc", + "alloy-transport-ws", "async-stream", "async-trait", "auto_impl", @@ -412,13 +444,34 @@ dependencies = [ "futures", "futures-utils-wasm", "lru", + "pin-project", "reqwest 0.12.5", + "serde", "serde_json", "tokio", "tracing", "url", ] +[[package]] +name = "alloy-pubsub" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "702f330b7da123a71465ab9d39616292f8344a2811c28f2cc8d8438a69d79e35" +dependencies = [ + "alloy-json-rpc 0.1.2", + "alloy-primitives 0.7.6", + "alloy-transport 0.1.2", + "bimap", + "futures", + "serde", + "serde_json", + "tokio", + "tokio-stream", + "tower", + "tracing", +] + [[package]] name = "alloy-rlp" version = "0.3.5" @@ -446,9 +499,9 @@ name = "alloy-rpc-client" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-json-rpc 0.1.0", + "alloy-transport 0.1.0", + "alloy-transport-http 0.1.0", "futures", "pin-project", "reqwest 0.12.5", @@ -463,12 +516,17 @@ dependencies = [ [[package]] name = "alloy-rpc-client" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40fcb53b2a9d0a78a4968b2eca8805a4b7011b9ee3fdfa2acaf137c5128f36b" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport-http 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-json-rpc 0.1.2", + "alloy-primitives 0.7.6", + "alloy-pubsub", + "alloy-transport 0.1.2", + "alloy-transport-http 0.1.2", + "alloy-transport-ipc", + "alloy-transport-ws", "futures", "pin-project", "reqwest 0.12.5", @@ -487,8 +545,8 @@ version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ "alloy-primitives 0.6.4", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-rpc-types 0.1.0", + "alloy-serde 0.1.0", "serde", "serde_json", ] @@ -498,11 +556,11 @@ name = "alloy-rpc-types" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-consensus 0.1.0", + "alloy-eips 0.1.0", "alloy-primitives 0.6.4", "alloy-rlp", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-serde 0.1.0", "itertools 0.12.1", "serde", "serde_json", @@ -511,32 +569,49 @@ dependencies = [ [[package]] name = "alloy-rpc-types" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "50f2fbe956a3e0f0975c798f488dc6be96b669544df3737e18f4a325b42f4c86" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-eips 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-genesis", + "alloy-rpc-types-engine", + "alloy-rpc-types-eth", + "alloy-serde 0.1.2", +] + +[[package]] +name = "alloy-rpc-types-engine" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cd473d98ec552f8229cd6d566bd2b0bbfc5bb4efcefbb5288c834aa8fd832020" +dependencies = [ + "alloy-consensus 0.1.2", + "alloy-eips 0.1.2", "alloy-primitives 0.7.6", "alloy-rlp", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-sol-types 0.7.6", - "itertools 0.12.1", + "alloy-rpc-types-eth", + "alloy-serde 0.1.2", + "jsonwebtoken", + "rand", "serde", - "serde_json", "thiserror", ] [[package]] -name = "alloy-rpc-types-trace" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +name = "alloy-rpc-types-eth" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "083f443a83b9313373817236a8f4bea09cca862618e9177d822aee579640a5d6" dependencies = [ + "alloy-consensus 0.1.2", + "alloy-eips 0.1.2", "alloy-primitives 0.7.6", - "alloy-rpc-types 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-serde 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-rlp", + "alloy-serde 0.1.2", + "alloy-sol-types 0.7.6", + "itertools 0.13.0", "serde", "serde_json", + "thiserror", ] [[package]] @@ -551,8 +626,9 @@ dependencies = [ [[package]] name = "alloy-serde" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d94da1c0c4e27cc344b05626fe22a89dc6b8b531b9475f3b7691dbf6913e4109" dependencies = [ "alloy-primitives 0.7.6", "serde", @@ -574,8 +650,9 @@ dependencies = [ [[package]] name = "alloy-signer" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "58d876be3afd8b78979540084ff63995292a26aa527ad0d44276405780aa0ffd" dependencies = [ "alloy-primitives 0.7.6", "async-trait", @@ -585,15 +662,31 @@ dependencies = [ "thiserror", ] +[[package]] +name = "alloy-signer-local" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d40a37dc216c269b8a7244047cb1c18a9c69f7a0332ab2c4c2aa4cbb1a31468b" +dependencies = [ + "alloy-consensus 0.1.2", + "alloy-network 0.1.2", + "alloy-primitives 0.7.6", + "alloy-signer 0.1.2", + "async-trait", + "k256", + "rand", + "thiserror", +] + [[package]] name = "alloy-signer-wallet" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-consensus 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-network 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-consensus 0.1.0", + "alloy-network 0.1.0", "alloy-primitives 0.6.4", - "alloy-signer 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-signer 0.1.0", "async-trait", "k256", "rand", @@ -716,7 +809,7 @@ name = "alloy-transport" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-json-rpc 0.1.0", "base64 0.22.1", "futures-util", "futures-utils-wasm", @@ -731,10 +824,11 @@ dependencies = [ [[package]] name = "alloy-transport" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "245af9541f0a0dbd5258669c80dfe3af118164cacec978a520041fc130550deb" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-json-rpc 0.1.2", "base64 0.22.1", "futures-util", "futures-utils-wasm", @@ -744,7 +838,6 @@ dependencies = [ "tokio", "tower", "url", - "wasm-bindgen-futures", ] [[package]] @@ -752,8 +845,8 @@ name = "alloy-transport-http" version = "0.1.0" source = "git+https://github.com/alloy-rs/alloy?rev=68952c0#68952c00372b4c388c7effc53cd562fd321c0aa3" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy-json-rpc 0.1.0", + "alloy-transport 0.1.0", "reqwest 0.12.5", "serde_json", "tower", @@ -762,11 +855,12 @@ dependencies = [ [[package]] name = "alloy-transport-http" -version = "0.1.0" -source = "git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e#7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5619c017e1fdaa1db87f9182f4f0ed97c53d674957f4902fba655e972d359c6c" dependencies = [ - "alloy-json-rpc 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-transport 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy-json-rpc 0.1.2", + "alloy-transport 0.1.2", "reqwest 0.12.5", "serde_json", "tower", @@ -774,6 +868,43 @@ dependencies = [ "url", ] +[[package]] +name = "alloy-transport-ipc" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173cefa110afac7a53cf2e75519327761f2344d305eea2993f3af1b2c1fc1c44" +dependencies = [ + "alloy-json-rpc 0.1.2", + "alloy-pubsub", + "alloy-transport 0.1.2", + "bytes", + "futures", + "interprocess", + "pin-project", + "serde_json", + "tokio", + "tokio-util", + "tracing", +] + +[[package]] +name = "alloy-transport-ws" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9c0aff8af5be5e58856c5cdd1e46db2c67c7ecd3a652d9100b4822c96c899947" +dependencies = [ + "alloy-pubsub", + "alloy-transport 0.1.2", + "futures", + "http 1.1.0", + "rustls 0.23.10", + "serde_json", + "tokio", + "tokio-tungstenite", + "tracing", + "ws_stream_wasm", +] + [[package]] name = "amq-protocol" version = "7.2.0" @@ -1410,6 +1541,17 @@ dependencies = [ "syn 2.0.66", ] +[[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 0.4.0", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -2082,6 +2224,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bimap" +version = "0.6.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "230c5f1ca6a325a32553f8640d31ac9b49f2411e901e427570154868b46da4f7" + [[package]] name = "bincode" version = "2.0.0-rc.3" @@ -2205,8 +2353,8 @@ dependencies = [ "sha3", "starknet-crypto 0.5.2", "starknet_api", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror", ] @@ -2882,7 +3030,7 @@ dependencies = [ [[package]] name = "cairo-type-derive" version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/snos#11814f3bb8c8d9b926125aa339ed2926ebc01722" +source = "git+https://github.com/Moonsong-Labs/snos?branch=od/os-output-serde#809b3b2ad74ebde7ec48bddc10317809f8098c90" dependencies = [ "proc-macro2", "quote", @@ -3648,6 +3796,12 @@ dependencies = [ "serde_json", ] +[[package]] +name = "doctest-file" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aac81fa3e28d21450aa4d2ac065992ba96a1d7303efbce51a95f4fd175b67562" + [[package]] name = "dotenv" version = "0.15.0" @@ -3895,7 +4049,7 @@ dependencies = [ name = "ethereum-da-client" version = "0.1.0" dependencies = [ - "alloy 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=68952c0)", + "alloy 0.1.0", "async-trait", "c-kzg", "color-eyre", @@ -3912,6 +4066,27 @@ dependencies = [ "utils", ] +[[package]] +name = "ethereum-settlement-client" +version = "0.1.0" +dependencies = [ + "alloy 0.1.2", + "async-trait", + "c-kzg", + "color-eyre", + "dotenv", + "mockall", + "reqwest 0.12.5", + "rstest 0.18.2", + "serde", + "settlement-client-interface", + "snos", + "tokio", + "tokio-test", + "url", + "utils", +] + [[package]] name = "ethereum-types" version = "0.14.1" @@ -4436,7 +4611,7 @@ dependencies = [ name = "gps-fact-checker" version = "0.1.0" dependencies = [ - "alloy 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", + "alloy 0.1.2", "async-trait", "cairo-vm 1.0.0-rc3", "itertools 0.13.0", @@ -5108,6 +5283,21 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "interprocess" +version = "2.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "67bafc2f5dbdad79a6d925649758d5472647b416028099f0b829d1b67fdd47d3" +dependencies = [ + "doctest-file", + "futures-core", + "libc", + "recvmsg", + "tokio", + "widestring", + "windows-sys 0.52.0", +] + [[package]] name = "io-lifetimes" version = "1.0.11" @@ -5743,7 +5933,7 @@ dependencies = [ "trust-dns-resolver", "typed-builder", "uuid 1.8.0", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -5940,6 +6130,26 @@ dependencies = [ "libc", ] +[[package]] +name = "num_enum" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02339744ee7253741199f897151b38e72257d13802d4ee837285cc2990a90845" +dependencies = [ + "num_enum_derive", +] + +[[package]] +name = "num_enum_derive" +version = "0.7.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "681030a937600a36906c185595136d26abfebb4aa9c65701cefcaf8578bb982b" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "num_threads" version = "0.1.7" @@ -6070,7 +6280,9 @@ dependencies = [ "da-client-interface", "dotenvy", "ethereum-da-client", + "ethereum-settlement-client", "futures", + "hex", "httpmock", "hyper 0.14.29", "lazy_static", @@ -6087,9 +6299,12 @@ dependencies = [ "rstest 0.18.2", "serde", "serde_json", + "settlement-client-interface", "sharp-service", + "snos", "starknet", "starknet-core 0.9.0", + "starknet-settlement-client", "thiserror", "tokio", "tracing", @@ -6394,6 +6609,16 @@ dependencies = [ "indexmap 2.2.6", ] +[[package]] +name = "pharos" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e9567389417feee6ce15dd6527a8a1ecac205ef62c2932bcf3d9f6fc5b78b414" +dependencies = [ + "futures", + "rustc_version 0.4.0", +] + [[package]] name = "phf" version = "0.11.2" @@ -6868,6 +7093,12 @@ dependencies = [ "futures-io", ] +[[package]] +name = "recvmsg" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d3edd4d5d42c92f0a659926464d4cce56b562761267ecf0f469d85b7de384175" + [[package]] name = "redis" version = "0.24.0" @@ -7020,7 +7251,7 @@ dependencies = [ "wasm-bindgen", "wasm-bindgen-futures", "web-sys", - "webpki-roots", + "webpki-roots 0.25.4", "winreg 0.50.0", ] @@ -7374,6 +7605,7 @@ dependencies = [ "aws-lc-rs", "log", "once_cell", + "ring", "rustls-pki-types", "rustls-webpki 0.102.4", "subtle", @@ -7699,6 +7931,12 @@ dependencies = [ "pest", ] +[[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.203" @@ -7990,8 +8228,7 @@ dependencies = [ name = "sharp-service" version = "0.1.0" dependencies = [ - "alloy 0.1.0 (git+https://github.com/alloy-rs/alloy?rev=7373f6db761d5a19888e3a0c527e8a3ca31e7a1e)", - "alloy-primitives 0.7.6", + "alloy 0.1.2", "async-trait", "cairo-vm 1.0.0-rc3", "gps-fact-checker", @@ -8106,7 +8343,7 @@ dependencies = [ [[package]] name = "snos" version = "0.1.0" -source = "git+https://github.com/keep-starknet-strange/snos#11814f3bb8c8d9b926125aa339ed2926ebc01722" +source = "git+https://github.com/Moonsong-Labs/snos?branch=od/os-output-serde#809b3b2ad74ebde7ec48bddc10317809f8098c90" dependencies = [ "anyhow", "assert_matches", @@ -8433,6 +8670,27 @@ dependencies = [ "url", ] +[[package]] +name = "starknet-settlement-client" +version = "0.1.0" +dependencies = [ + "async-trait", + "c-kzg", + "color-eyre", + "dotenv", + "lazy_static", + "mockall", + "reqwest 0.12.5", + "rstest 0.18.2", + "serde", + "settlement-client-interface", + "starknet", + "tokio", + "tokio-test", + "url", + "utils", +] + [[package]] name = "starknet-signers" version = "0.7.0" @@ -8478,8 +8736,8 @@ dependencies = [ "serde", "serde_json", "starknet-crypto 0.5.2", - "strum", - "strum_macros", + "strum 0.24.1", + "strum_macros 0.24.3", "thiserror", ] @@ -8554,6 +8812,15 @@ version = "0.24.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "063e6045c0e62079840579a7e47a355ae92f60eb74daaf156fb1e84ba164e63f" +[[package]] +name = "strum" +version = "0.26.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5d8cec3501a5194c432b2b7976db6b7d10ec95c253208b45f83f7136aa985e29" +dependencies = [ + "strum_macros 0.26.4", +] + [[package]] name = "strum_macros" version = "0.24.3" @@ -8567,6 +8834,19 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "strum_macros" +version = "0.26.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c6bee85a5a24955dc440386795aa378cd9cf82acd5f764469152d2270e581be" +dependencies = [ + "heck 0.5.0", + "proc-macro2", + "quote", + "rustversion", + "syn 2.0.66", +] + [[package]] name = "subtle" version = "2.5.0" @@ -8990,6 +9270,22 @@ dependencies = [ "tokio-stream", ] +[[package]] +name = "tokio-tungstenite" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c6989540ced10490aaf14e6bad2e3d33728a2813310a0c71d1574304c49631cd" +dependencies = [ + "futures-util", + "log", + "rustls 0.23.10", + "rustls-pki-types", + "tokio", + "tokio-rustls 0.26.0", + "tungstenite", + "webpki-roots 0.26.3", +] + [[package]] name = "tokio-util" version = "0.7.11" @@ -9078,7 +9374,7 @@ dependencies = [ "tower-layer", "tower-service", "tracing", - "webpki-roots", + "webpki-roots 0.25.4", ] [[package]] @@ -9246,6 +9542,26 @@ version = "0.2.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" +[[package]] +name = "tungstenite" +version = "0.23.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e2e2ce1e47ed2994fd43b04c8f618008d4cabdd5ee34027cf14f9d918edd9c8" +dependencies = [ + "byteorder", + "bytes", + "data-encoding", + "http 1.1.0", + "httparse", + "log", + "rand", + "rustls 0.23.10", + "rustls-pki-types", + "sha1", + "thiserror", + "utf-8", +] + [[package]] name = "typed-builder" version = "0.10.0" @@ -9365,6 +9681,12 @@ version = "2.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "daf8dba3b7eb870caf1ddeed7bc9d2a049f3cfdfae7cb521b087cc33ae4c49da" +[[package]] +name = "utf-8" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9" + [[package]] name = "utf8parse" version = "0.2.2" @@ -9564,6 +9886,15 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "webpki-roots" +version = "0.26.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bd7c23921eeb1713a4e851530e9b9756e4fb0e89978582942612524cf09f01cd" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "which" version = "4.4.2" @@ -9799,6 +10130,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 0.4.0", + "send_wrapper", + "thiserror", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + [[package]] name = "wyz" version = "0.5.1" diff --git a/Cargo.toml b/Cargo.toml index 7cde6537..8e58f11e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,8 @@ members = [ "crates/prover-services/sharp-service", "crates/utils", "crates/settlement-clients/settlement-client-interface", + "crates/settlement-clients/ethereum", + "crates/settlement-clients/starknet", "e2e-tests", ] @@ -22,8 +24,7 @@ authors = ["Apoorv Sadana <@apoorvsadana>"] [workspace.dependencies] num = { version = "0.4.1" } async-trait = { version = "0.1.77" } -alloy = { git = "https://github.com/alloy-rs/alloy", rev = "7373f6db761d5a19888e3a0c527e8a3ca31e7a1e" } -alloy-primitives = "0.7.4" +alloy = { version = "0.1.2", features = ["full"] } axum = { version = "0.7.4" } axum-macros = "0.4.1" color-eyre = "0.6.2" @@ -62,8 +63,11 @@ cairo-vm = { git = "https://github.com/lambdaclass/cairo-vm", features = [ "cairo-1-hints", ] } +# TODO: we currently use the Moonsong fork & the os-output-serde branch so we +# can deserialize our snos input json into a StarknetOsInput struct. +# TODO: update back to the main repo once it's merged # Sharp (Starkware) -snos = { git = "https://github.com/keep-starknet-strange/snos" } +snos = { git = "https://github.com/Moonsong-Labs/snos", branch = "od/os-output-serde" } # Madara prover API madara-prover-common = { git = "https://github.com/Moonsong-Labs/madara-prover-api", branch = "od/use-latest-cairo-vm" } @@ -72,6 +76,11 @@ madara-prover-rpc-client = { git = "https://github.com/Moonsong-Labs/madara-prov # Project da-client-interface = { path = "crates/da-clients/da-client-interface" } ethereum-da-client = { path = "crates/da-clients/ethereum" } + +settlement-client-interface = { path = "crates/settlement-clients/settlement-client-interface" } +ethereum-settlement-client = { path = "crates/settlement-clients/ethereum" } +starknet-settlement-client = { path = "crates/settlement-clients/starknet" } + utils = { path = "crates/utils" } prover-client-interface = { path = "crates/prover-services/prover-client-interface" } gps-fact-checker = { path = "crates/prover-services/gps-fact-checker" } diff --git a/crates/da-clients/da-client-interface/src/lib.rs b/crates/da-clients/da-client-interface/src/lib.rs index 5d36d2ad..33e9de8b 100644 --- a/crates/da-clients/da-client-interface/src/lib.rs +++ b/crates/da-clients/da-client-interface/src/lib.rs @@ -3,14 +3,14 @@ use color_eyre::Result; use mockall::automock; use mockall::predicate::*; -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum DaVerificationStatus { #[allow(dead_code)] Pending, #[allow(dead_code)] Verified, #[allow(dead_code)] - Rejected, + Rejected(String), } /// Trait for every new DaClient to implement diff --git a/crates/da-clients/ethereum/src/lib.rs b/crates/da-clients/ethereum/src/lib.rs index b77376a0..b8ceb808 100644 --- a/crates/da-clients/ethereum/src/lib.rs +++ b/crates/da-clients/ethereum/src/lib.rs @@ -92,7 +92,10 @@ impl DaClient for EthereumDaClient { None => Ok(DaVerificationStatus::Pending), Some(receipt) => match receipt.status_code { Some(status) if status == U64::from(1) => Ok(DaVerificationStatus::Verified), - _ => Ok(DaVerificationStatus::Rejected), + Some(status) => { + Ok(DaVerificationStatus::Rejected(format!("Transaction failed with status code: {}", status))) + } + None => Ok(DaVerificationStatus::Rejected("Transaction status code is missing".into())), }, } } diff --git a/crates/orchestrator/Cargo.toml b/crates/orchestrator/Cargo.toml index 543f5435..b622df3f 100644 --- a/crates/orchestrator/Cargo.toml +++ b/crates/orchestrator/Cargo.toml @@ -25,7 +25,9 @@ color-eyre = { workspace = true } da-client-interface = { workspace = true } dotenvy = { workspace = true } ethereum-da-client = { workspace = true, optional = true } +ethereum-settlement-client = { workspace = true } futures = { workspace = true } +hex = { workspace = true } lazy_static = { workspace = true } log = "0.4.21" majin-blob-core = { git = "https://github.com/AbdelStark/majin-blob", branch = "main" } @@ -39,9 +41,12 @@ omniqueue = { workspace = true, optional = true } prover-client-interface = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } +settlement-client-interface = { workspace = true } sharp-service = { workspace = true } +snos = { workspace = true } starknet = { workspace = true } starknet-core = "0.9.0" +starknet-settlement-client = { workspace = true } thiserror = { workspace = true } tokio = { workspace = true, features = ["sync", "macros", "rt-multi-thread"] } tracing = { workspace = true } diff --git a/crates/orchestrator/src/config.rs b/crates/orchestrator/src/config.rs index 9d2ff112..648d36a9 100644 --- a/crates/orchestrator/src/config.rs +++ b/crates/orchestrator/src/config.rs @@ -5,10 +5,13 @@ use da_client_interface::{DaClient, DaConfig}; use dotenvy::dotenv; use ethereum_da_client::config::EthereumDaConfig; use ethereum_da_client::EthereumDaClient; +use ethereum_settlement_client::EthereumSettlementClient; use prover_client_interface::ProverClient; +use settlement_client_interface::SettlementClient; use sharp_service::SharpProverService; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::{JsonRpcClient, Url}; +use starknet_settlement_client::StarknetSettlementClient; use tokio::sync::OnceCell; use utils::env_utils::get_env_var_or_panic; use utils::settings::default::DefaultSettingsProvider; @@ -29,9 +32,10 @@ pub struct Config { da_client: Box, /// The service that produces proof and registers it onchain prover_client: Box, + /// Settlement client + settlement_client: Box, /// The database client database: Box, - /// The queue provider queue: Box, } @@ -50,10 +54,13 @@ pub async fn init_config() -> Config { // init the queue let queue = Box::new(SqsQueue {}); + let da_client = build_da_client(); + let settings_provider = DefaultSettingsProvider {}; - let prover = create_prover_service(&settings_provider); + let settlement_client = build_settlement_client(&settings_provider).await; + let prover_client = build_prover_service(&settings_provider); - Config { starknet_client: Arc::new(provider), da_client: build_da_client(), prover_client: prover, database, queue } + Config::new(Arc::new(provider), da_client, prover_client, settlement_client, database, queue) } impl Config { @@ -62,10 +69,11 @@ impl Config { starknet_client: Arc>, da_client: Box, prover_client: Box, + settlement_client: Box, database: Box, queue: Box, ) -> Self { - Self { starknet_client, da_client, prover_client, database, queue } + Self { starknet_client, da_client, prover_client, settlement_client, database, queue } } /// Returns the starknet client @@ -83,6 +91,11 @@ impl Config { self.prover_client.as_ref() } + /// Returns the settlement client + pub fn settlement_client(&self) -> &dyn SettlementClient { + self.settlement_client.as_ref() + } + /// Returns the database client pub fn database(&self) -> &dyn Database { self.database.as_ref() @@ -132,10 +145,19 @@ fn build_da_client() -> Box { } } -/// Creates prover service based on the environment variable PROVER_SERVICE -fn create_prover_service(settings_provider: &impl SettingsProvider) -> Box { +/// Builds the prover service based on the environment variable PROVER_SERVICE +fn build_prover_service(settings_provider: &impl SettingsProvider) -> Box { match get_env_var_or_panic("PROVER_SERVICE").as_str() { "sharp" => Box::new(SharpProverService::with_settings(settings_provider)), _ => panic!("Unsupported prover service"), } } + +/// Builds the settlement client depending on the env variable SETTLEMENT_LAYER +async fn build_settlement_client(settings_provider: &impl SettingsProvider) -> Box { + match get_env_var_or_panic("SETTLEMENT_LAYER").as_str() { + "ethereum" => Box::new(EthereumSettlementClient::with_settings(settings_provider)), + "starknet" => Box::new(StarknetSettlementClient::with_settings(settings_provider).await), + _ => panic!("Unsupported Settlement layer"), + } +} diff --git a/crates/orchestrator/src/database/mod.rs b/crates/orchestrator/src/database/mod.rs index c5480b91..ccf2807f 100644 --- a/crates/orchestrator/src/database/mod.rs +++ b/crates/orchestrator/src/database/mod.rs @@ -26,14 +26,8 @@ pub trait Database: Send + Sync { async fn create_job(&self, job: JobItem) -> Result; async fn get_job_by_id(&self, id: Uuid) -> Result>; async fn get_job_by_internal_id_and_type(&self, internal_id: &str, job_type: &JobType) -> Result>; + async fn update_job(&self, job: &JobItem) -> Result<()>; async fn update_job_status(&self, job: &JobItem, new_status: JobStatus) -> Result<()>; - async fn update_external_id_and_status_and_metadata( - &self, - job: &JobItem, - external_id: String, - new_status: JobStatus, - metadata: HashMap, - ) -> Result<()>; async fn update_metadata(&self, job: &JobItem, metadata: HashMap) -> Result<()>; async fn get_latest_job_by_type_and_internal_id(&self, job_type: JobType) -> Result>; async fn get_jobs_without_successor( diff --git a/crates/orchestrator/src/database/mongodb/mod.rs b/crates/orchestrator/src/database/mongodb/mod.rs index f0ea886a..0e45ba19 100644 --- a/crates/orchestrator/src/database/mongodb/mod.rs +++ b/crates/orchestrator/src/database/mongodb/mod.rs @@ -83,28 +83,19 @@ impl Database for MongoDb { Ok(self.get_job_collection().find_one(filter, None).await?) } - async fn update_job_status(&self, job: &JobItem, new_status: JobStatus) -> Result<()> { + async fn update_job(&self, job: &JobItem) -> Result<()> { + let job_doc = bson::to_document(job)?; let update = doc! { - "$set": { - "status": mongodb::bson::to_bson(&new_status)?, - } + "$set": job_doc }; self.update_job_optimistically(job, update).await?; Ok(()) } - async fn update_external_id_and_status_and_metadata( - &self, - job: &JobItem, - external_id: String, - new_status: JobStatus, - metadata: HashMap, - ) -> Result<()> { + async fn update_job_status(&self, job: &JobItem, new_status: JobStatus) -> Result<()> { let update = doc! { "$set": { "status": mongodb::bson::to_bson(&new_status)?, - "external_id": external_id, - "metadata": mongodb::bson::to_document(&metadata)? } }; self.update_job_optimistically(job, update).await?; diff --git a/crates/orchestrator/src/jobs/constants.rs b/crates/orchestrator/src/jobs/constants.rs index 0da8587d..b295e1ea 100644 --- a/crates/orchestrator/src/jobs/constants.rs +++ b/crates/orchestrator/src/jobs/constants.rs @@ -1,3 +1,10 @@ pub const JOB_PROCESS_ATTEMPT_METADATA_KEY: &str = "process_attempt_no"; + pub const JOB_VERIFICATION_ATTEMPT_METADATA_KEY: &str = "verification_attempt_no"; + pub const JOB_METADATA_CAIRO_PIE_PATH_KEY: &str = "cairo_pie_path"; + +pub const JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY: &str = "blocks_number_to_settle"; +pub const JOB_METADATA_STATE_UPDATE_FETCH_FROM_TESTS: &str = "fetch_from_test_data"; +pub const JOB_METADATA_STATE_UPDATE_ATTEMPT_PREFIX: &str = "attempt_tx_hashes_"; +pub const JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO: &str = "last_failed_block_no"; diff --git a/crates/orchestrator/src/jobs/da_job/mod.rs b/crates/orchestrator/src/jobs/da_job/mod.rs index 0566beba..0ea630a7 100644 --- a/crates/orchestrator/src/jobs/da_job/mod.rs +++ b/crates/orchestrator/src/jobs/da_job/mod.rs @@ -60,7 +60,7 @@ impl Job for DaJob { }) } - async fn process_job(&self, config: &Config, job: &JobItem) -> Result { + async fn process_job(&self, config: &Config, job: &mut JobItem) -> Result { let block_no = job.internal_id.parse::()?; let state_update = config.starknet_client().get_state_update(BlockId::Number(block_no)).await?; @@ -109,7 +109,7 @@ impl Job for DaJob { Ok(external_id) } - async fn verify_job(&self, config: &Config, job: &JobItem) -> Result { + async fn verify_job(&self, config: &Config, job: &mut JobItem) -> Result { Ok(config.da_client().verify_inclusion(job.external_id.unwrap_string()?).await?.into()) } @@ -376,7 +376,8 @@ mod tests { ) { let server = MockServer::start(); - let config = init_config(Some(format!("http://localhost:{}", server.port())), None, None, None, None).await; + let config = + init_config(Some(format!("http://localhost:{}", server.port())), None, None, None, None, None).await; get_nonce_attached(&server, nonce_file_path); diff --git a/crates/orchestrator/src/jobs/mod.rs b/crates/orchestrator/src/jobs/mod.rs index d1afdfc1..763fc4d9 100644 --- a/crates/orchestrator/src/jobs/mod.rs +++ b/crates/orchestrator/src/jobs/mod.rs @@ -35,11 +35,11 @@ pub trait Job: Send + Sync { /// Should process the job and return the external_id which can be used to /// track the status of the job. For example, a DA job will submit the state diff /// to the DA layer and return the txn hash. - async fn process_job(&self, config: &Config, job: &JobItem) -> Result; + async fn process_job(&self, config: &Config, job: &mut JobItem) -> Result; /// Should verify the job and return the status of the verification. For example, /// a DA job will verify the inclusion of the state diff in the DA layer and return /// the status of the verification. - async fn verify_job(&self, config: &Config, job: &JobItem) -> Result; + async fn verify_job(&self, config: &Config, job: &mut JobItem) -> Result; /// Should return the maximum number of attempts to process the job. A new attempt is made /// every time the verification returns `JobVerificationStatus::Rejected` fn max_process_attempts(&self) -> u64; @@ -75,12 +75,12 @@ pub async fn create_job(job_type: JobType, internal_id: String, metadata: HashMa /// DB. It then adds the job to the verification queue. pub async fn process_job(id: Uuid) -> Result<()> { let config = config().await; - let job = get_job(id).await?; + let mut job = get_job(id).await?; match job.status { // we only want to process jobs that are in the created or verification failed state. // verification failed state means that the previous processing failed and we want to retry - JobStatus::Created | JobStatus::VerificationFailed => { + JobStatus::Created | JobStatus::VerificationFailed(_) => { log::info!("Processing job with id {:?}", id); } _ => { @@ -94,13 +94,14 @@ pub async fn process_job(id: Uuid) -> Result<()> { config.database().update_job_status(&job, JobStatus::LockedForProcessing).await?; let job_handler = get_job_handler(&job.job_type); - let external_id = job_handler.process_job(config.as_ref(), &job).await?; - + let external_id = job_handler.process_job(config.as_ref(), &mut job).await?; let metadata = increment_key_in_metadata(&job.metadata, JOB_PROCESS_ATTEMPT_METADATA_KEY)?; - config - .database() - .update_external_id_and_status_and_metadata(&job, external_id, JobStatus::PendingVerification, metadata) - .await?; + + job.external_id = external_id.into(); + job.status = JobStatus::PendingVerification; + job.metadata = metadata; + + config.database().update_job(&job).await?; add_job_to_verification_queue(job.id, Duration::from_secs(job_handler.verification_polling_delay_seconds())) .await?; @@ -114,7 +115,7 @@ pub async fn process_job(id: Uuid) -> Result<()> { /// job back to the queue. pub async fn verify_job(id: Uuid) -> Result<()> { let config = config().await; - let job = get_job(id).await?; + let mut job = get_job(id).await?; match job.status { JobStatus::PendingVerification => { @@ -127,14 +128,14 @@ pub async fn verify_job(id: Uuid) -> Result<()> { } let job_handler = get_job_handler(&job.job_type); - let verification_status = job_handler.verify_job(config.as_ref(), &job).await?; + let verification_status = job_handler.verify_job(config.as_ref(), &mut job).await?; match verification_status { JobVerificationStatus::Verified => { config.database().update_job_status(&job, JobStatus::Completed).await?; } - JobVerificationStatus::Rejected => { - config.database().update_job_status(&job, JobStatus::VerificationFailed).await?; + JobVerificationStatus::Rejected(e) => { + config.database().update_job_status(&job, JobStatus::VerificationFailed(e)).await?; // retry job processing if we haven't exceeded the max limit let process_attempts = get_u64_from_metadata(&job.metadata, JOB_PROCESS_ATTEMPT_METADATA_KEY)?; diff --git a/crates/orchestrator/src/jobs/proving_job/mod.rs b/crates/orchestrator/src/jobs/proving_job/mod.rs index 8bdf229e..4453f213 100644 --- a/crates/orchestrator/src/jobs/proving_job/mod.rs +++ b/crates/orchestrator/src/jobs/proving_job/mod.rs @@ -40,7 +40,7 @@ impl Job for ProvingJob { }) } - async fn process_job(&self, config: &Config, job: &JobItem) -> Result { + async fn process_job(&self, config: &Config, job: &mut JobItem) -> Result { // TODO: allow to donwload PIE from S3 let cairo_pie_path: PathBuf = job .metadata @@ -52,14 +52,17 @@ impl Job for ProvingJob { Ok(external_id) } - async fn verify_job(&self, config: &Config, job: &JobItem) -> Result { + async fn verify_job(&self, config: &Config, job: &mut JobItem) -> Result { let task_id: String = job.external_id.unwrap_string()?.into(); match config.prover_client().get_task_status(&task_id).await? { TaskStatus::Processing => Ok(JobVerificationStatus::Pending), TaskStatus::Succeeded => Ok(JobVerificationStatus::Verified), TaskStatus::Failed(err) => { log!(Error, "Prover job #{} failed: {}", job.internal_id, err); - Ok(JobVerificationStatus::Rejected) + Ok(JobVerificationStatus::Rejected(format!( + "Prover job #{} failed with error: {}", + job.internal_id, err + ))) } } } diff --git a/crates/orchestrator/src/jobs/register_proof_job/mod.rs b/crates/orchestrator/src/jobs/register_proof_job/mod.rs index 2e9482f7..26ec573a 100644 --- a/crates/orchestrator/src/jobs/register_proof_job/mod.rs +++ b/crates/orchestrator/src/jobs/register_proof_job/mod.rs @@ -31,14 +31,14 @@ impl Job for RegisterProofJob { }) } - async fn process_job(&self, _config: &Config, _job: &JobItem) -> Result { + async fn process_job(&self, _config: &Config, _job: &mut JobItem) -> Result { // Get proof from S3 and submit on chain for verification // We need to implement a generic trait for this to support multiple // base layers todo!() } - async fn verify_job(&self, _config: &Config, _job: &JobItem) -> Result { + async fn verify_job(&self, _config: &Config, _job: &mut JobItem) -> Result { // verify that the proof transaction has been included on chain todo!() } diff --git a/crates/orchestrator/src/jobs/snos_job/mod.rs b/crates/orchestrator/src/jobs/snos_job/mod.rs index 13600d4b..be6cfeee 100644 --- a/crates/orchestrator/src/jobs/snos_job/mod.rs +++ b/crates/orchestrator/src/jobs/snos_job/mod.rs @@ -29,14 +29,14 @@ impl Job for SnosJob { }) } - async fn process_job(&self, _config: &Config, _job: &JobItem) -> Result { + async fn process_job(&self, _config: &Config, _job: &mut JobItem) -> Result { // 1. Fetch SNOS input data from Madara // 2. Import SNOS in Rust and execute it with the input data // 3. Store the received PIE in DB todo!() } - async fn verify_job(&self, _config: &Config, _job: &JobItem) -> Result { + async fn verify_job(&self, _config: &Config, _job: &mut JobItem) -> Result { // No need for verification as of now. If we later on decide to outsource SNOS run // to another servicehow a, verify_job can be used to poll on the status of the job todo!() diff --git a/crates/orchestrator/src/jobs/state_update_job/mod.rs b/crates/orchestrator/src/jobs/state_update_job/mod.rs index 6eb14bc8..d9e00222 100644 --- a/crates/orchestrator/src/jobs/state_update_job/mod.rs +++ b/crates/orchestrator/src/jobs/state_update_job/mod.rs @@ -1,15 +1,37 @@ use std::collections::HashMap; +use std::path::PathBuf; use async_trait::async_trait; +use cairo_vm::Felt252; +use color_eyre::eyre::eyre; use color_eyre::Result; +use lazy_static::lazy_static; +use snos::io::output::StarknetOsOutput; +use starknet::providers::Provider; +use starknet_core::types::{BlockId, MaybePendingStateUpdate}; use uuid::Uuid; +use settlement_client_interface::SettlementVerificationStatus; +use utils::collections::{has_dup, is_sorted}; + +use super::constants::{ + JOB_METADATA_STATE_UPDATE_ATTEMPT_PREFIX, JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO, + JOB_PROCESS_ATTEMPT_METADATA_KEY, +}; + use crate::config::Config; +use crate::jobs::constants::{ + JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY, JOB_METADATA_STATE_UPDATE_FETCH_FROM_TESTS, +}; use crate::jobs::types::{JobItem, JobStatus, JobType, JobVerificationStatus}; use crate::jobs::Job; -pub struct StateUpdateJob; +// TODO: remove when data is correctly stored in DB/S3 +lazy_static! { + pub static ref CURRENT_PATH: PathBuf = std::env::current_dir().unwrap(); +} +pub struct StateUpdateJob; #[async_trait] impl Job for StateUpdateJob { async fn create_job( @@ -21,7 +43,7 @@ impl Job for StateUpdateJob { Ok(JobItem { id: Uuid::new_v4(), internal_id, - job_type: JobType::ProofRegistration, + job_type: JobType::StateTransition, status: JobStatus::Created, external_id: String::new().into(), // metadata must contain the blocks for which state update will be performed @@ -31,26 +53,223 @@ impl Job for StateUpdateJob { }) } - async fn process_job(&self, _config: &Config, _job: &JobItem) -> Result { + async fn process_job(&self, config: &Config, job: &mut JobItem) -> Result { + let attempt_no = + job.metadata.get(JOB_PROCESS_ATTEMPT_METADATA_KEY).expect("Could not find current attempt number.").clone(); + + // TODO: remove when SNOS is correctly stored in DB/S3 + // Test metadata to fetch the snos output from the test folder, default to False + let fetch_from_tests = + job.metadata.get(JOB_METADATA_STATE_UPDATE_FETCH_FROM_TESTS).map_or(false, |value| value == "TRUE"); + // Read the metadata to get the blocks for which state update will be performed. - // For each block, get the program output (from the PIE?) and the - todo!() + // We assume that blocks nbrs are formatted as follow: "2,3,4,5,6". + let mut block_numbers = self.get_block_numbers_from_metadata(job)?; + self.validate_block_numbers(config, &block_numbers).await?; + + // If we had a block state update failing last run, we recover from this block + if let Some(last_failed_block) = job.metadata.get(JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO) { + let last_failed_block: u64 = + last_failed_block.parse().expect("last_failed_block should be a positive number"); + block_numbers = block_numbers.into_iter().filter(|&block| block >= last_failed_block).collect::>(); + } + + let mut sent_tx_hashes: Vec = Vec::with_capacity(block_numbers.len()); + for block_no in block_numbers.iter() { + let snos = self.fetch_snos_for_block(*block_no, Some(fetch_from_tests)).await; + let tx_hash = + self.update_state_for_block(config, *block_no, snos, Some(fetch_from_tests)).await.map_err(|e| { + job.metadata.insert(JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO.into(), block_no.to_string()); + self.insert_attempts_into_metadata(job, &attempt_no, &sent_tx_hashes); + eyre!("Block #{block_no} - Error occured during the state update: {e}") + })?; + sent_tx_hashes.push(tx_hash); + } + + self.insert_attempts_into_metadata(job, &attempt_no, &sent_tx_hashes); + + // external_id returned corresponds to the last block number settled + Ok(block_numbers.last().unwrap().to_string()) } - async fn verify_job(&self, _config: &Config, _job: &JobItem) -> Result { - // verify that the proof transaction has been included on chain - todo!() + /// Returns the status of the passed job. + /// Status will be verified if: + /// 1. the last settlement tx hash is successful, + /// 2. the expected last settled block from our configuration is indeed the one found in the provider. + async fn verify_job(&self, config: &Config, job: &mut JobItem) -> Result { + let attempt_no = + job.metadata.get(JOB_PROCESS_ATTEMPT_METADATA_KEY).expect("Could not find current attempt number.").clone(); + let metadata_tx_hashes = job + .metadata + .get(&format!("{}{}", JOB_METADATA_STATE_UPDATE_ATTEMPT_PREFIX, attempt_no)) + .expect("Could not find tx hashes metadata for the current attempt") + .clone() + .replace(' ', ""); + + let tx_hashes: Vec<&str> = metadata_tx_hashes.split(',').collect(); + let block_numbers = self.get_block_numbers_from_metadata(job)?; + let settlement_client = config.settlement_client(); + + for (tx_hash, block_no) in tx_hashes.iter().zip(block_numbers.iter()) { + let tx_inclusion_status = settlement_client.verify_tx_inclusion(tx_hash).await?; + match tx_inclusion_status { + SettlementVerificationStatus::Rejected(_) => { + job.metadata.insert(JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO.into(), block_no.to_string()); + return Ok(tx_inclusion_status.into()); + } + // If the tx is still pending, we wait for it to be finalized and check again the status. + SettlementVerificationStatus::Pending => { + settlement_client.wait_for_tx_finality(tx_hash).await?; + let new_status = settlement_client.verify_tx_inclusion(tx_hash).await?; + match new_status { + SettlementVerificationStatus::Rejected(_) => { + job.metadata + .insert(JOB_METADATA_STATE_UPDATE_LAST_FAILED_BLOCK_NO.into(), block_no.to_string()); + return Ok(new_status.into()); + } + SettlementVerificationStatus::Pending => { + return Err(eyre!("Tx {tx_hash} should not be pending.")) + } + SettlementVerificationStatus::Verified => {} + } + } + SettlementVerificationStatus::Verified => {} + } + } + // verify that the last settled block is indeed the one we expect to be + let expected_last_block_number = block_numbers.last().expect("Block numbers list should not be empty."); + let out_last_block_number = settlement_client.get_last_settled_block().await?; + let block_status = if out_last_block_number == *expected_last_block_number { + SettlementVerificationStatus::Verified + } else { + SettlementVerificationStatus::Rejected(format!( + "Last settle bock expected was {} but found {}", + expected_last_block_number, out_last_block_number + )) + }; + Ok(block_status.into()) } fn max_process_attempts(&self) -> u64 { - todo!() + 1 } fn max_verification_attempts(&self) -> u64 { - todo!() + 10 } fn verification_polling_delay_seconds(&self) -> u64 { - todo!() + 60 + } +} + +impl StateUpdateJob { + /// Read the metadata and parse the block numbers + fn get_block_numbers_from_metadata(&self, job: &JobItem) -> Result> { + let blocks_to_settle = job.metadata.get(JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY).ok_or_else(|| { + eyre!("Block numbers to settle must be specified (state update job #{})", job.internal_id) + })?; + self.parse_block_numbers(blocks_to_settle) + } + + /// Parse a list of blocks comma separated + fn parse_block_numbers(&self, blocks_to_settle: &str) -> Result> { + let sanitized_blocks = blocks_to_settle.replace(' ', ""); + let block_numbers: Vec = sanitized_blocks + .split(',') + .map(|block_no| block_no.parse::()) + .collect::, _>>() + .map_err(|e| eyre!("Block numbers to settle list is not correctly formatted: {e}"))?; + Ok(block_numbers) + } + + /// Validate that the list of block numbers to process is valid. + async fn validate_block_numbers(&self, config: &Config, block_numbers: &[u64]) -> Result<()> { + if block_numbers.is_empty() { + return Err(eyre!("No block numbers found.")); + } + if has_dup(block_numbers) { + return Err(eyre!("Duplicated block numbers.")); + } + if !is_sorted(block_numbers) { + return Err(eyre!("Block numbers aren't sorted in increasing order.")); + } + // Check for gap between the last settled block and the first block to settle + let last_settled_block: u64 = config.settlement_client().get_last_settled_block().await?; + if last_settled_block + 1 != block_numbers[0] { + return Err(eyre!("Gap detected between the first block to settle and the last one settled.")); + } + Ok(()) + } + + /// Update the state for the corresponding block using the settlement layer. + async fn update_state_for_block( + &self, + config: &Config, + block_no: u64, + snos: StarknetOsOutput, + fetch_from_tests: Option, + ) -> Result { + let starknet_client = config.starknet_client(); + let settlement_client = config.settlement_client(); + let last_tx_hash_executed = if snos.use_kzg_da == Felt252::ZERO { + let state_update = starknet_client.get_state_update(BlockId::Number(block_no)).await?; + let _state_update = match state_update { + MaybePendingStateUpdate::PendingUpdate(_) => { + return Err(eyre!("Block #{} - Cannot update state as it's still in pending state", block_no)); + } + MaybePendingStateUpdate::Update(state_update) => state_update, + }; + // TODO: Build the required arguments & send them to update_state_calldata + let program_output = vec![]; + let onchain_data_hash = vec![0_u8; 32].try_into().expect("onchain data hash size must be 32 bytes"); + let onchain_data_size = 0; + settlement_client.update_state_calldata(program_output, onchain_data_hash, onchain_data_size).await? + } else if snos.use_kzg_da == Felt252::ONE { + // TODO: Build the blob & the KZG proof & send them to update_state_blobs + let kzg_proof = self.fetch_kzg_proof_for_block(block_no, fetch_from_tests).await; + let kzg_proof: [u8; 48] = kzg_proof.try_into().expect("kzg proof size must be 48 bytes"); + settlement_client.update_state_blobs(vec![], kzg_proof).await? + } else { + return Err(eyre!("Block #{} - SNOS error, [use_kzg_da] should be either 0 or 1.", block_no)); + }; + Ok(last_tx_hash_executed) + } + + /// Retrieves the SNOS output for the corresponding block. + /// TODO: remove the fetch_from_tests argument once we have proper fetching (db/s3) + async fn fetch_snos_for_block(&self, block_no: u64, fetch_from_tests: Option) -> StarknetOsOutput { + let fetch_from_tests = fetch_from_tests.unwrap_or(true); + match fetch_from_tests { + true => { + let snos_path = + CURRENT_PATH.join(format!("src/jobs/state_update_job/test_data/{}/snos_output.json", block_no)); + let snos_str = std::fs::read_to_string(snos_path).expect("Failed to read the SNOS json file"); + serde_json::from_str(&snos_str).expect("Failed to deserialize JSON into SNOS") + } + false => unimplemented!("can't fetch SNOS from DB/S3"), + } + } + + /// Retrieves the KZG Proof for the corresponding block. + /// TODO: remove the fetch_from_tests argument once we have proper fetching (db/s3) + async fn fetch_kzg_proof_for_block(&self, block_no: u64, fetch_from_tests: Option) -> Vec { + let fetch_from_tests = fetch_from_tests.unwrap_or(true); + let kzg_proof_str = match fetch_from_tests { + true => { + let kzg_path = + CURRENT_PATH.join(format!("src/jobs/state_update_job/test_data/{}/kzg_proof.txt", block_no)); + std::fs::read_to_string(kzg_path).expect("Failed to read the KZG txt file").replace("0x", "") + } + false => unimplemented!("can't fetch KZG Proof from DB/S3"), + }; + hex::decode(kzg_proof_str).expect("Invalid test kzg proof") + } + + /// Insert the tx hashes into the the metadata for the attempt number - will be used later by + /// verify_job to make sure that all tx are successful. + fn insert_attempts_into_metadata(&self, job: &mut JobItem, attempt_no: &str, tx_hashes: &[String]) { + let new_attempt_metadata_key = format!("{}{}", JOB_METADATA_STATE_UPDATE_ATTEMPT_PREFIX, attempt_no); + job.metadata.insert(new_attempt_metadata_key, tx_hashes.join(",")); } } diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651053/kzg_proof.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/kzg_proof.txt new file mode 100644 index 00000000..1f2e80cc --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/kzg_proof.txt @@ -0,0 +1 @@ +0x8f5c9a9175faf290d6ebf909f586e486ee3aab47eab7888ec0f6956a6fdb2e67f878935951a56ec4cee2531454ffba65 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651053/snos_output.json b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/snos_output.json new file mode 100644 index 00000000..4da9587f --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/snos_output.json @@ -0,0 +1,12 @@ +{ + "initial_root": "0x6AD64BEBBD0A182A669D3C5C8E3B22653A140C425290CD7E441F7CA4CEDCFE5", + "final_root": "0x325BA9C26BB7960F1B200F33B7E0A625A38614BAE44C7410CE7D3E977CBF40", + "block_number": "0x9EF2D", + "block_hash": "0x286ACDAD5BA876EDAE5D0B906C216B6F16737F2F198789A9380330BAA3097E0", + "starknet_os_config_hash": "0x5BA2078240F1585F96424C2D1EE48211DA3B3F9177BF2B9880B4FC91D59E9A2", + "use_kzg_da": "0x1", + "messages_to_l1": [], + "messages_to_l2": [], + "contracts": [], + "classes": {} +} diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651053/source.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/source.txt new file mode 100644 index 00000000..c5c72882 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651053/source.txt @@ -0,0 +1 @@ +https://etherscan.io/tx/0x33e43cc5bfac30d36c5f8bd4f40c13d9d229eaa3b1ba1639be53e5fb3b026b2d \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651054/kzg_proof.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/kzg_proof.txt new file mode 100644 index 00000000..7b7e5f70 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/kzg_proof.txt @@ -0,0 +1 @@ +0x800aead553a553ace65fa1188441e153b1c70ab12eab20bbeae2f431c4b97d2b22f5c36c4549aec41219846f025d72ec \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651054/snos_output.json b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/snos_output.json new file mode 100644 index 00000000..8b75af65 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/snos_output.json @@ -0,0 +1,12 @@ +{ + "initial_root": "0x325BA9C26BB7960F1B200F33B7E0A625A38614BAE44C7410CE7D3E977CBF40", + "final_root": "0x7D7E0C5772392D15F3F0C9CFF6DFE549BE29250462485F80D7ECBBC283256CF", + "block_number": "0x9EF2E", + "block_hash": "0x698AFF98DA67A39116D90197C1E9FA4F550F93CFACFB398DC0DD14294DEFA92", + "starknet_os_config_hash": "0x5BA2078240F1585F96424C2D1EE48211DA3B3F9177BF2B9880B4FC91D59E9A2", + "use_kzg_da": "0x1", + "messages_to_l1": [], + "messages_to_l2": [], + "contracts": [], + "classes": {} +} diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651054/source.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/source.txt new file mode 100644 index 00000000..30241310 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651054/source.txt @@ -0,0 +1 @@ +https://etherscan.io/tx/0x2e50fd76b5d62dc03dccf62aa776fd59ee0889407a355fe6e044bd50bfc64916 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651055/kzg_proof.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/kzg_proof.txt new file mode 100644 index 00000000..4277dfd0 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/kzg_proof.txt @@ -0,0 +1 @@ +0x98306e5a9ef56569d7d9139c34fae7f4dcd7a54b11bc61815c2c11ad743cbd09b1ec8ffa4470230b4cf6318cc69498f2 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651055/snos_output.json b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/snos_output.json new file mode 100644 index 00000000..e606b22b --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/snos_output.json @@ -0,0 +1,12 @@ +{ + "initial_root": "0x7D7E0C5772392D15F3F0C9CFF6DFE549BE29250462485F80D7ECBBC283256CF", + "final_root": "0x4EC6744B24853BDEC273225D76232DD0A342535816FFBD578CF8A5B3D1F2AE6", + "block_number": "0x9EF2F", + "block_hash": "0x7FBECAFB986506E1EC0D529A1A706B1FF810EC48CF71BDE18825E111D1211FF", + "starknet_os_config_hash": "0x5BA2078240F1585F96424C2D1EE48211DA3B3F9177BF2B9880B4FC91D59E9A2", + "use_kzg_da": "0x1", + "messages_to_l1": [], + "messages_to_l2": [], + "contracts": [], + "classes": {} +} diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651055/source.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/source.txt new file mode 100644 index 00000000..b5cfcc54 --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651055/source.txt @@ -0,0 +1 @@ +https://etherscan.io/tx/0xe3e6c7da1fb1d68c81e4b7dd516fb26e61921f9df1e07a2a19c94eed8fca0813 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651056/kzg_proof.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/kzg_proof.txt new file mode 100644 index 00000000..1b2152fc --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/kzg_proof.txt @@ -0,0 +1 @@ +0x89d4fbb643360b23e174fc8c1c4aa693c9d7f8362de26340e52f412fa66a12b99a95fbc5ec0e4adf6d1eca04bee44a98 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651056/snos_output.json b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/snos_output.json new file mode 100644 index 00000000..7c341f2b --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/snos_output.json @@ -0,0 +1,12 @@ +{ + "initial_root": "0x4EC6744B24853BDEC273225D76232DD0A342535816FFBD578CF8A5B3D1F2AE6", + "final_root": "0x7CC8FFD5D6BBF7ED250019167419B9176FC1591DBE514AD85CC32033E216235", + "block_number": "0x9EF30", + "block_hash": "0x7160A589FD6005460FFFCFC5C938DE724FCFEE72F30D0EA8BB8B54541F9ED46", + "starknet_os_config_hash": "0x5BA2078240F1585F96424C2D1EE48211DA3B3F9177BF2B9880B4FC91D59E9A2", + "use_kzg_da": "0x1", + "messages_to_l1": [], + "messages_to_l2": [], + "contracts": [], + "classes": {} +} diff --git a/crates/orchestrator/src/jobs/state_update_job/test_data/651056/source.txt b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/source.txt new file mode 100644 index 00000000..dbfef12f --- /dev/null +++ b/crates/orchestrator/src/jobs/state_update_job/test_data/651056/source.txt @@ -0,0 +1 @@ +https://etherscan.io/tx/0x70741c778346767e8b630313de96fdeb068e8819b8a2d22871934a5fcaf2b888 \ No newline at end of file diff --git a/crates/orchestrator/src/jobs/types.rs b/crates/orchestrator/src/jobs/types.rs index 15bb4679..b8e492b4 100644 --- a/crates/orchestrator/src/jobs/types.rs +++ b/crates/orchestrator/src/jobs/types.rs @@ -6,6 +6,7 @@ use da_client_interface::DaVerificationStatus; // TODO: job types shouldn't depend on mongodb use mongodb::bson::serde_helpers::uuid_1_as_binary; use serde::{Deserialize, Serialize}; +use settlement_client_interface::SettlementVerificationStatus; use uuid::Uuid; /// An external id. @@ -97,7 +98,7 @@ pub enum JobStatus { /// The job was processed but the was unable to be verified under the given time VerificationTimeout, /// The job failed processing - VerificationFailed, + VerificationFailed(String), } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] @@ -120,14 +121,14 @@ pub struct JobItem { pub version: i32, } -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum JobVerificationStatus { #[allow(dead_code)] Pending, #[allow(dead_code)] Verified, #[allow(dead_code)] - Rejected, + Rejected(String), } impl From for JobVerificationStatus { @@ -135,7 +136,17 @@ impl From for JobVerificationStatus { match status { DaVerificationStatus::Pending => JobVerificationStatus::Pending, DaVerificationStatus::Verified => JobVerificationStatus::Verified, - DaVerificationStatus::Rejected => JobVerificationStatus::Rejected, + DaVerificationStatus::Rejected(e) => JobVerificationStatus::Rejected(e), + } + } +} + +impl From for JobVerificationStatus { + fn from(status: SettlementVerificationStatus) -> Self { + match status { + SettlementVerificationStatus::Pending => JobVerificationStatus::Pending, + SettlementVerificationStatus::Verified => JobVerificationStatus::Verified, + SettlementVerificationStatus::Rejected(e) => JobVerificationStatus::Rejected(e), } } } diff --git a/crates/orchestrator/src/tests/common/mod.rs b/crates/orchestrator/src/tests/common/mod.rs index f2ce742e..4f1e1bea 100644 --- a/crates/orchestrator/src/tests/common/mod.rs +++ b/crates/orchestrator/src/tests/common/mod.rs @@ -8,6 +8,7 @@ use constants::*; use da_client_interface::MockDaClient; use prover_client_interface::MockProverClient; use rstest::*; +use settlement_client_interface::MockSettlementClient; use starknet::providers::jsonrpc::HttpTransport; use starknet::providers::JsonRpcClient; use url::Url; @@ -25,6 +26,7 @@ pub async fn init_config( queue: Option, da_client: Option, prover_client: Option, + settlement_client: Option, ) -> Config { let _ = tracing_subscriber::fmt().with_max_level(tracing::Level::INFO).with_target(false).try_init(); @@ -33,11 +35,19 @@ pub async fn init_config( let queue = queue.unwrap_or_default(); let da_client = da_client.unwrap_or_default(); let prover_client = prover_client.unwrap_or_default(); + let settlement_client = settlement_client.unwrap_or_default(); // init starknet client let provider = JsonRpcClient::new(HttpTransport::new(Url::parse(rpc_url.as_str()).expect("Failed to parse URL"))); - Config::new(Arc::new(provider), Box::new(da_client), Box::new(prover_client), Box::new(database), Box::new(queue)) + Config::new( + Arc::new(provider), + Box::new(da_client), + Box::new(prover_client), + Box::new(settlement_client), + Box::new(database), + Box::new(queue), + ) } #[fixture] diff --git a/crates/orchestrator/src/tests/jobs/da_job/mod.rs b/crates/orchestrator/src/tests/jobs/da_job/mod.rs index 6aab1ea3..35ba74c5 100644 --- a/crates/orchestrator/src/tests/jobs/da_job/mod.rs +++ b/crates/orchestrator/src/tests/jobs/da_job/mod.rs @@ -16,7 +16,7 @@ use crate::jobs::Job; #[rstest] #[tokio::test] async fn test_create_job() { - let config = init_config(None, None, None, None, None).await; + let config = init_config(None, None, None, None, None, None).await; let job = DaJob.create_job(&config, String::from("0"), HashMap::new()).await; assert!(job.is_ok()); @@ -32,12 +32,12 @@ async fn test_create_job() { #[rstest] #[tokio::test] -async fn test_verify_job(#[from(default_job_item)] job_item: JobItem) { +async fn test_verify_job(#[from(default_job_item)] mut job_item: JobItem) { let mut da_client = MockDaClient::new(); da_client.expect_verify_inclusion().times(1).returning(|_| Ok(DaVerificationStatus::Verified)); - let config = init_config(None, None, None, Some(da_client), None).await; - assert!(DaJob.verify_job(&config, &job_item).await.is_ok()); + let config = init_config(None, None, None, Some(da_client), None, None).await; + assert!(DaJob.verify_job(&config, &mut job_item).await.is_ok()); } #[rstest] @@ -53,7 +53,7 @@ async fn test_process_job() { da_client.expect_max_blob_per_txn().times(1).returning(move || ETHEREUM_MAX_BLOB_PER_TXN); let config = - init_config(Some(format!("http://localhost:{}", server.port())), None, None, Some(da_client), None).await; + init_config(Some(format!("http://localhost:{}", server.port())), None, None, Some(da_client), None, None).await; let state_update = MaybePendingStateUpdate::Update(StateUpdate { block_hash: FieldElement::default(), new_root: FieldElement::default(), @@ -79,7 +79,7 @@ async fn test_process_job() { DaJob .process_job( &config, - &JobItem { + &mut JobItem { id: Uuid::default(), internal_id: internal_id.to_string(), job_type: JobType::DataSubmission, diff --git a/crates/orchestrator/src/tests/jobs/mod.rs b/crates/orchestrator/src/tests/jobs/mod.rs index fc670c1c..7a707131 100644 --- a/crates/orchestrator/src/tests/jobs/mod.rs +++ b/crates/orchestrator/src/tests/jobs/mod.rs @@ -6,6 +6,9 @@ pub mod da_job; #[cfg(test)] pub mod proving_job; +#[cfg(test)] +pub mod state_update_job; + #[rstest] #[tokio::test] async fn create_job_fails_job_already_exists() { diff --git a/crates/orchestrator/src/tests/jobs/proving_job/mod.rs b/crates/orchestrator/src/tests/jobs/proving_job/mod.rs index e0bf6bd1..d72adcfd 100644 --- a/crates/orchestrator/src/tests/jobs/proving_job/mod.rs +++ b/crates/orchestrator/src/tests/jobs/proving_job/mod.rs @@ -14,7 +14,7 @@ use crate::jobs::Job; #[rstest] #[tokio::test] async fn test_create_job() { - let config = init_config(None, None, None, None, None).await; + let config = init_config(None, None, None, None, None, None).await; let job = ProvingJob .create_job( &config, @@ -36,12 +36,12 @@ async fn test_create_job() { #[rstest] #[tokio::test] -async fn test_verify_job(#[from(default_job_item)] job_item: JobItem) { +async fn test_verify_job(#[from(default_job_item)] mut job_item: JobItem) { let mut prover_client = MockProverClient::new(); prover_client.expect_get_task_status().times(1).returning(|_| Ok(TaskStatus::Succeeded)); - let config = init_config(None, None, None, None, Some(prover_client)).await; - assert!(ProvingJob.verify_job(&config, &job_item).await.is_ok()); + let config = init_config(None, None, None, None, Some(prover_client), None).await; + assert!(ProvingJob.verify_job(&config, &mut job_item).await.is_ok()); } #[rstest] @@ -53,7 +53,8 @@ async fn test_process_job() { prover_client.expect_submit_task().times(1).returning(|_| Ok("task_id".to_string())); let config = - init_config(Some(format!("http://localhost:{}", server.port())), None, None, None, Some(prover_client)).await; + init_config(Some(format!("http://localhost:{}", server.port())), None, None, None, Some(prover_client), None) + .await; let cairo_pie_path = format!("{}/src/tests/artifacts/fibonacci.zip", env!("CARGO_MANIFEST_DIR")); @@ -61,7 +62,7 @@ async fn test_process_job() { ProvingJob .process_job( &config, - &JobItem { + &mut JobItem { id: Uuid::default(), internal_id: "0".into(), job_type: JobType::ProofCreation, diff --git a/crates/orchestrator/src/tests/jobs/state_update_job/mod.rs b/crates/orchestrator/src/tests/jobs/state_update_job/mod.rs new file mode 100644 index 00000000..9a1905fd --- /dev/null +++ b/crates/orchestrator/src/tests/jobs/state_update_job/mod.rs @@ -0,0 +1,151 @@ +use mockall::predicate::eq; +use rstest::*; +use settlement_client_interface::MockSettlementClient; + +use std::{collections::HashMap, fs}; + +use super::super::common::init_config; + +use crate::jobs::{ + constants::{ + JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY, JOB_METADATA_STATE_UPDATE_FETCH_FROM_TESTS, + JOB_PROCESS_ATTEMPT_METADATA_KEY, + }, + state_update_job::StateUpdateJob, + types::{JobStatus, JobType}, + Job, +}; + +use httpmock::prelude::*; + +#[rstest] +#[tokio::test] +async fn test_create_job() { + let config = init_config(None, None, None, None, None, None).await; + + let job = StateUpdateJob.create_job(&config, String::from("0"), HashMap::default()).await; + assert!(job.is_ok()); + + let job = job.unwrap(); + let job_type = job.job_type; + + assert_eq!(job_type, JobType::StateTransition, "job_type should be StateTransition"); + assert!(!(job.id.is_nil()), "id should not be nil"); + assert_eq!(job.status, JobStatus::Created, "status should be Created"); + assert_eq!(job.version, 0_i32, "version should be 0"); + assert_eq!(job.external_id.unwrap_string().unwrap(), String::new(), "external_id should be empty string"); +} + +#[rstest] +#[tokio::test] +async fn test_process_job() { + let server = MockServer::start(); + let mut settlement_client = MockSettlementClient::new(); + + // Mock the latest block settled + settlement_client.expect_get_last_settled_block().returning(|| Ok(651052_u64)); + + // TODO: have tests for update_state_calldata, only kzg for now + let block_numbers = ["651053", "651054", "651055", "651056"]; + for block_no in block_numbers { + let program_output: Vec<[u8; 32]> = vec![]; + let block_proof: Vec = load_kzg_proof(block_no); + let block_proof: [u8; 48] = block_proof.try_into().expect("test proof should be 48 bytes"); + settlement_client + .expect_update_state_blobs() + // TODO: vec![] is program_output + .with(eq(program_output), eq(block_proof)) + .returning(|_, _| Ok(String::from("0x5d17fac98d9454030426606019364f6e68d915b91f6210ef1e2628cd6987442"))); + } + + let config = init_config( + Some(format!("http://localhost:{}", server.port())), + None, + None, + None, + None, + Some(settlement_client), + ) + .await; + + let mut metadata: HashMap = HashMap::new(); + metadata.insert(String::from(JOB_METADATA_STATE_UPDATE_FETCH_FROM_TESTS), String::from("TRUE")); + metadata.insert(String::from(JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY), block_numbers.join(",")); + metadata.insert(String::from(JOB_PROCESS_ATTEMPT_METADATA_KEY), String::from("0")); + + let mut job = StateUpdateJob.create_job(&config, String::from("internal_id"), metadata).await.unwrap(); + assert_eq!(StateUpdateJob.process_job(&config, &mut job).await.unwrap(), "651056".to_string()) +} + +#[rstest] +#[case(String::from("651052, 651054, 651051, 651056"), "numbers aren't sorted in increasing order")] +#[case(String::from("651052, 651052, 651052, 651052"), "Duplicated block numbers")] +#[case(String::from("a, 651054, b, 651056"), "settle list is not correctly formatted")] +#[case(String::from("651052, 651052, 651053, 651053"), "Duplicated block numbers")] +#[case(String::from(""), "settle list is not correctly formatted")] +#[tokio::test] +async fn test_process_job_invalid_inputs(#[case] block_numbers_to_settle: String, #[case] expected_error: &str) { + let server = MockServer::start(); + let settlement_client = MockSettlementClient::new(); + let config = init_config( + Some(format!("http://localhost:{}", server.port())), + None, + None, + None, + None, + Some(settlement_client), + ) + .await; + + let mut metadata: HashMap = HashMap::new(); + metadata.insert(String::from(JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY), block_numbers_to_settle); + metadata.insert(String::from(JOB_PROCESS_ATTEMPT_METADATA_KEY), String::from("0")); + + let mut job = StateUpdateJob.create_job(&config, String::from("internal_id"), metadata).await.unwrap(); + let status = StateUpdateJob.process_job(&config, &mut job).await; + assert!(status.is_err()); + + if let Err(error) = status { + let error_message = format!("{}", error); + assert!( + error_message.contains(expected_error), + "Error message did not contain expected substring: {}", + expected_error + ); + } +} + +#[rstest] +#[tokio::test] +#[should_panic(expected = "Gap detected between the first block to settle and the last one settle")] +async fn test_process_job_invalid_input_gap() { + let server = MockServer::start(); + let mut settlement_client = MockSettlementClient::new(); + + settlement_client.expect_get_last_settled_block().returning(|| Ok(4_u64)); + + let config = init_config( + Some(format!("http://localhost:{}", server.port())), + None, + None, + None, + None, + Some(settlement_client), + ) + .await; + + let mut metadata: HashMap = HashMap::new(); + metadata.insert(String::from(JOB_METADATA_STATE_UPDATE_BLOCKS_TO_SETTLE_KEY), String::from("6, 7, 8")); + metadata.insert(String::from(JOB_PROCESS_ATTEMPT_METADATA_KEY), String::from("0")); + + let mut job = StateUpdateJob.create_job(&config, String::from("internal_id"), metadata).await.unwrap(); + let _ = StateUpdateJob.process_job(&config, &mut job).await.unwrap(); +} + +// ==================== Utility functions =========================== + +fn load_kzg_proof(block_no: &str) -> Vec { + let file_path = format!("src/jobs/state_update_job/test_data/{}/kzg_proof.txt", block_no); + let proof_str = fs::read_to_string(file_path).expect("Unable to read kzg_proof.txt").replace("0x", ""); + hex::decode(proof_str).unwrap() +} diff --git a/crates/orchestrator/src/tests/server/mod.rs b/crates/orchestrator/src/tests/server/mod.rs index bab1a5d0..fac27b5c 100644 --- a/crates/orchestrator/src/tests/server/mod.rs +++ b/crates/orchestrator/src/tests/server/mod.rs @@ -13,7 +13,7 @@ use crate::routes::app_router; #[fixture] pub async fn setup_server() -> SocketAddr { - let _config = init_config(Some("http://localhost:9944".to_string()), None, None, None, None).await; + let _config = init_config(Some("http://localhost:9944".to_string()), None, None, None, None, None).await; let host = get_env_var_or_default("HOST", "127.0.0.1"); let port = get_env_var_or_default("PORT", "3000").parse::().expect("PORT must be a u16"); diff --git a/crates/orchestrator/src/tests/workers/proving/mod.rs b/crates/orchestrator/src/tests/workers/proving/mod.rs index f1c84ea5..9baef197 100644 --- a/crates/orchestrator/src/tests/workers/proving/mod.rs +++ b/crates/orchestrator/src/tests/workers/proving/mod.rs @@ -11,6 +11,7 @@ use httpmock::MockServer; use mockall::predicate::eq; use prover_client_interface::MockProverClient; use rstest::rstest; +use settlement_client_interface::MockSettlementClient; use std::collections::HashMap; use std::error::Error; use std::time::Duration; @@ -27,6 +28,7 @@ async fn test_proving_worker(#[case] incomplete_runs: bool) -> Result<(), Box Result<(), Box Result<(), Box> { // mock block number (madara) : 5 let rpc_response_block_number = block; let response = json!({ "id": 1,"jsonrpc":"2.0","result": rpc_response_block_number }); - let config = - init_config(Some(format!("http://localhost:{}", server.port())), Some(db), Some(queue), Some(da_client), None) - .await; + let config = init_config( + Some(format!("http://localhost:{}", server.port())), + Some(db), + Some(queue), + Some(da_client), + None, + None, + ) + .await; config_force_init(config).await; // mocking block call diff --git a/crates/prover-services/sharp-service/Cargo.toml b/crates/prover-services/sharp-service/Cargo.toml index 572ff8b5..87e44a49 100644 --- a/crates/prover-services/sharp-service/Cargo.toml +++ b/crates/prover-services/sharp-service/Cargo.toml @@ -5,7 +5,6 @@ edition.workspace = true [dependencies] alloy.workspace = true -alloy-primitives.workspace = true async-trait.workspace = true cairo-vm.workspace = true gps-fact-checker.workspace = true diff --git a/crates/prover-services/sharp-service/src/error.rs b/crates/prover-services/sharp-service/src/error.rs index 082d6690..7ea08582 100644 --- a/crates/prover-services/sharp-service/src/error.rs +++ b/crates/prover-services/sharp-service/src/error.rs @@ -1,4 +1,4 @@ -use alloy_primitives::hex::FromHexError; +use alloy::primitives::hex::FromHexError; use gps_fact_checker::error::FactCheckerError; use prover_client_interface::ProverClientError; use reqwest::StatusCode; diff --git a/crates/settlement-clients/ethereum/Cargo.toml b/crates/settlement-clients/ethereum/Cargo.toml new file mode 100644 index 00000000..447485ee --- /dev/null +++ b/crates/settlement-clients/ethereum/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "ethereum-settlement-client" +version.workspace = true +edition.workspace = true + +[dependencies] +alloy = { workspace = true, features = ["full"] } +async-trait = { workspace = true } +c-kzg = "1.0.0" +color-eyre = { workspace = true } +dotenv = "0.15" +mockall = "0.12.1" +reqwest = { version = "0.12.3" } +rstest = { workspace = true } +serde = { version = "1.0.196", default-features = false, features = ["derive"] } +settlement-client-interface = { workspace = true } +snos = { workspace = true } +tokio = { workspace = true } +url = { workspace = true } +utils = { workspace = true } + +[dev-dependencies] +tokio-test = "*" diff --git a/crates/settlement-clients/ethereum/src/clients/interfaces/mod.rs b/crates/settlement-clients/ethereum/src/clients/interfaces/mod.rs new file mode 100644 index 00000000..22883820 --- /dev/null +++ b/crates/settlement-clients/ethereum/src/clients/interfaces/mod.rs @@ -0,0 +1 @@ +pub mod validity_interface; diff --git a/crates/settlement-clients/ethereum/src/clients/interfaces/validity_interface.rs b/crates/settlement-clients/ethereum/src/clients/interfaces/validity_interface.rs new file mode 100644 index 00000000..7b8a31a1 --- /dev/null +++ b/crates/settlement-clients/ethereum/src/clients/interfaces/validity_interface.rs @@ -0,0 +1,112 @@ +use std::sync::Arc; + +use async_trait::async_trait; + +use alloy::{ + network::Ethereum, + primitives::{I256, U256}, + providers::Provider, + rpc::types::eth::TransactionReceipt, + sol, + transports::{http::Http, RpcError, TransportErrorKind}, +}; + +use crate::types::LocalWalletSignerMiddleware; + +// TODO: should be moved to Zaun: +// https://github.com/keep-starknet-strange/zaun + +sol! { + #[allow(missing_docs)] + #[sol(rpc)] + interface StarknetValidityContract { + function setProgramHash(uint256 newProgramHash) external notFinalized onlyGovernance; + function setConfigHash(uint256 newConfigHash) external notFinalized onlyGovernance; + function setMessageCancellationDelay(uint256 delayInSeconds) external notFinalized onlyGovernance; + + function programHash() public view returns (uint256); + function configHash() public view returns (uint256); + + function identify() external pure override returns (string memory); + function stateRoot() external view returns (uint256); + function stateBlockNumber() external view returns (int256); + function stateBlockHash() external view returns (uint256); + + function updateState(uint256[] calldata programOutput, uint256 onchainDataHash, uint256 onchainDataSize) external onlyOperator; + function updateStateKzgDA(uint256[] calldata programOutput, bytes calldata kzgProof) external onlyOperator; + } +} + +#[async_trait] +pub trait StarknetValidityContractTrait { + /// Retrieves the last block number settled + async fn state_block_number(&self) -> Result; + + /// Update the L1 state + async fn update_state( + &self, + program_output: Vec, + onchain_data_hash: U256, + onchain_data_size: U256, + ) -> Result>; + + async fn update_state_kzg( + &self, + program_output: Vec, + kzg_proof: [u8; 48], + ) -> Result>; +} + +#[async_trait] +impl StarknetValidityContractTrait for T +where + T: AsRef< + StarknetValidityContract::StarknetValidityContractInstance< + Http, + Arc, + Ethereum, + >, + > + Send + + Sync, +{ + async fn state_block_number(&self) -> Result { + Ok(self.as_ref().stateBlockNumber().call().await?._0) + } + + async fn update_state( + &self, + program_output: Vec, + onchain_data_hash: U256, + onchain_data_size: U256, + ) -> Result> { + let base_fee = self.as_ref().provider().as_ref().get_gas_price().await.unwrap(); + let from_address = self.as_ref().provider().as_ref().get_accounts().await.unwrap()[0]; + let gas = self + .as_ref() + .updateState(program_output.clone(), onchain_data_hash, onchain_data_size) + .from(from_address) + .estimate_gas() + .await + .unwrap(); + let builder = self.as_ref().updateState(program_output, onchain_data_hash, onchain_data_size); + builder.from(from_address).nonce(2).gas(gas).gas_price(base_fee).send().await.unwrap().get_receipt().await + } + + async fn update_state_kzg( + &self, + program_output: Vec, + kzg_proof: [u8; 48], + ) -> Result> { + let base_fee = self.as_ref().provider().as_ref().get_gas_price().await.unwrap(); + let from_address = self.as_ref().provider().as_ref().get_accounts().await.unwrap()[0]; + let gas = self + .as_ref() + .updateStateKzgDA(program_output.clone(), kzg_proof.into()) + .from(from_address) + .estimate_gas() + .await + .unwrap(); + let builder = self.as_ref().updateStateKzgDA(program_output, kzg_proof.into()); + builder.from(from_address).nonce(2).gas(gas).gas_price(base_fee).send().await.unwrap().get_receipt().await + } +} diff --git a/crates/settlement-clients/ethereum/src/clients/mod.rs b/crates/settlement-clients/ethereum/src/clients/mod.rs new file mode 100644 index 00000000..2f918c04 --- /dev/null +++ b/crates/settlement-clients/ethereum/src/clients/mod.rs @@ -0,0 +1,4 @@ +pub mod interfaces; +pub mod validity; + +pub use validity::StarknetValidityContractClient; diff --git a/crates/settlement-clients/ethereum/src/clients/validity.rs b/crates/settlement-clients/ethereum/src/clients/validity.rs new file mode 100644 index 00000000..01a66d6f --- /dev/null +++ b/crates/settlement-clients/ethereum/src/clients/validity.rs @@ -0,0 +1,44 @@ +use std::sync::Arc; + +use alloy::{network::Ethereum, primitives::Address, transports::http::Http}; + +use crate::clients::interfaces::validity_interface::StarknetValidityContract; +use crate::types::LocalWalletSignerMiddleware; + +// TODO: should be moved to Zaun: +// https://github.com/keep-starknet-strange/zaun + +/// Client to interact with a Starknet core contract running in `Validity` mode +pub struct StarknetValidityContractClient { + core_contract: StarknetValidityContract::StarknetValidityContractInstance< + Http, + Arc, + Ethereum, + >, +} + +impl StarknetValidityContractClient { + pub fn new(address: Address, client: Arc) -> Self { + Self { core_contract: StarknetValidityContract::new(address, client.clone()) } + } +} + +impl + AsRef< + StarknetValidityContract::StarknetValidityContractInstance< + Http, + Arc, + Ethereum, + >, + > for StarknetValidityContractClient +{ + fn as_ref( + &self, + ) -> &StarknetValidityContract::StarknetValidityContractInstance< + Http, + Arc, + Ethereum, + > { + &self.core_contract + } +} diff --git a/crates/settlement-clients/ethereum/src/config.rs b/crates/settlement-clients/ethereum/src/config.rs new file mode 100644 index 00000000..c34c59e2 --- /dev/null +++ b/crates/settlement-clients/ethereum/src/config.rs @@ -0,0 +1,33 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use settlement_client_interface::SettlementConfig; +use url::Url; +use utils::env_utils::get_env_var_or_panic; + +pub const ENV_ETHEREUM_RPC_URL: &str = "ETHEREUM_RPC_URL"; +pub const ENV_CORE_CONTRACT_ADDRESS: &str = "STARKNET_SOLIDITY_CORE_CONTRACT_ADDRESS"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct EthereumSettlementConfig { + pub rpc_url: Url, + pub core_contract_address: String, +} + +impl SettlementConfig for EthereumSettlementConfig { + fn new_from_env() -> Self { + let rpc_url = get_env_var_or_panic(ENV_ETHEREUM_RPC_URL); + let rpc_url = Url::from_str(&rpc_url).unwrap_or_else(|_| panic!("Failed to parse {}", ENV_ETHEREUM_RPC_URL)); + let core_contract_address = get_env_var_or_panic(ENV_CORE_CONTRACT_ADDRESS); + Self { rpc_url, core_contract_address } + } +} + +impl Default for EthereumSettlementConfig { + fn default() -> Self { + Self { + rpc_url: "https://ethereum-sepolia.blockpi.network/v1/rpc/public".parse().unwrap(), + core_contract_address: "0xE2Bb56ee936fd6433DC0F6e7e3b8365C906AA057".into(), + } + } +} diff --git a/crates/settlement-clients/ethereum/src/conversion.rs b/crates/settlement-clients/ethereum/src/conversion.rs new file mode 100644 index 00000000..c86eb89a --- /dev/null +++ b/crates/settlement-clients/ethereum/src/conversion.rs @@ -0,0 +1,12 @@ +use alloy::primitives::U256; + +/// Converts a `&[Vec]` to `Vec`. Each inner slice is expected to be exactly 32 bytes long. +/// Pads with zeros if any inner slice is shorter than 32 bytes. +pub(crate) fn slice_slice_u8_to_vec_u256(slices: &[[u8; 32]]) -> Vec { + slices.iter().map(|slice| slice_u8_to_u256(slice)).collect() +} + +/// Converts a `&[u8]` to `U256`. +pub(crate) fn slice_u8_to_u256(slice: &[u8]) -> U256 { + U256::try_from_be_slice(slice).expect("could not convert u8 slice to U256") +} diff --git a/crates/settlement-clients/ethereum/src/lib.rs b/crates/settlement-clients/ethereum/src/lib.rs new file mode 100644 index 00000000..de54dc0e --- /dev/null +++ b/crates/settlement-clients/ethereum/src/lib.rs @@ -0,0 +1,115 @@ +pub mod clients; +pub mod config; +pub mod conversion; +pub mod types; + +use std::{str::FromStr, sync::Arc}; + +use alloy::{ + network::EthereumWallet, + primitives::{Address, B256, U256}, + providers::{PendingTransactionConfig, Provider, ProviderBuilder}, + rpc::types::TransactionReceipt, + signers::local::PrivateKeySigner, +}; +use async_trait::async_trait; +use color_eyre::Result; +use mockall::{automock, predicate::*}; + +use settlement_client_interface::{SettlementClient, SettlementVerificationStatus, SETTLEMENT_SETTINGS_NAME}; +use utils::{env_utils::get_env_var_or_panic, settings::SettingsProvider}; + +use crate::clients::interfaces::validity_interface::StarknetValidityContractTrait; +use crate::clients::StarknetValidityContractClient; +use crate::config::EthereumSettlementConfig; +use crate::conversion::{slice_slice_u8_to_vec_u256, slice_u8_to_u256}; +use crate::types::EthHttpProvider; + +pub const ENV_PRIVATE_KEY: &str = "ETHEREUM_PRIVATE_KEY"; + +#[allow(dead_code)] +pub struct EthereumSettlementClient { + provider: Arc, + core_contract_client: StarknetValidityContractClient, +} + +impl EthereumSettlementClient { + pub fn with_settings(settings: &impl SettingsProvider) -> Self { + let settlement_cfg: EthereumSettlementConfig = settings.get_settings(SETTLEMENT_SETTINGS_NAME).unwrap(); + + let private_key = get_env_var_or_panic(ENV_PRIVATE_KEY); + let signer: PrivateKeySigner = private_key.parse().expect("Failed to parse private key"); + let wallet = EthereumWallet::from(signer); + + let provider = + Arc::new(ProviderBuilder::new().with_recommended_fillers().wallet(wallet).on_http(settlement_cfg.rpc_url)); + let core_contract_client = StarknetValidityContractClient::new( + Address::from_slice(settlement_cfg.core_contract_address.as_bytes()).0.into(), + provider.clone(), + ); + + EthereumSettlementClient { provider, core_contract_client } + } +} + +#[automock] +#[async_trait] +impl SettlementClient for EthereumSettlementClient { + /// Should register the proof on the base layer and return an external id + /// which can be used to track the status. + #[allow(unused)] + async fn register_proof(&self, proof: [u8; 32]) -> Result { + todo!("register_proof is not implemented yet") + } + + /// Should be used to update state on core contract when DA is done in calldata + async fn update_state_calldata( + &self, + program_output: Vec<[u8; 32]>, + onchain_data_hash: [u8; 32], + onchain_data_size: usize, + ) -> Result { + let program_output: Vec = slice_slice_u8_to_vec_u256(program_output.as_slice()); + let onchain_data_hash: U256 = slice_u8_to_u256(&onchain_data_hash); + let onchain_data_size: U256 = onchain_data_size.try_into()?; + let tx_receipt = + self.core_contract_client.update_state(program_output, onchain_data_hash, onchain_data_size).await?; + Ok(format!("0x{:x}", tx_receipt.transaction_hash)) + } + + /// Should be used to update state on core contract when DA is in blobs/alt DA + async fn update_state_blobs(&self, program_output: Vec<[u8; 32]>, kzg_proof: [u8; 48]) -> Result { + let program_output: Vec = slice_slice_u8_to_vec_u256(&program_output); + let tx_receipt = self.core_contract_client.update_state_kzg(program_output, kzg_proof).await?; + Ok(format!("0x{:x}", tx_receipt.transaction_hash)) + } + + /// Should verify the inclusion of a tx in the settlement layer + async fn verify_tx_inclusion(&self, tx_hash: &str) -> Result { + let tx_hash = B256::from_str(tx_hash)?; + let maybe_tx_status: Option = self.provider.get_transaction_receipt(tx_hash).await?; + match maybe_tx_status { + Some(tx_status) => { + if tx_status.status() { + Ok(SettlementVerificationStatus::Verified) + } else { + Ok(SettlementVerificationStatus::Pending) + } + } + None => Ok(SettlementVerificationStatus::Rejected(format!("Could not find status of tx: {}", tx_hash))), + } + } + + /// Wait for a pending tx to achieve finality + async fn wait_for_tx_finality(&self, tx_hash: &str) -> Result<()> { + let tx_hash = B256::from_str(tx_hash)?; + self.provider.watch_pending_transaction(PendingTransactionConfig::new(tx_hash)).await?; + Ok(()) + } + + /// Get the last block settled through the core contract + async fn get_last_settled_block(&self) -> Result { + let block_number = self.core_contract_client.state_block_number().await?; + Ok(block_number.try_into()?) + } +} diff --git a/crates/settlement-clients/ethereum/src/types.rs b/crates/settlement-clients/ethereum/src/types.rs new file mode 100644 index 00000000..3415ee0a --- /dev/null +++ b/crates/settlement-clients/ethereum/src/types.rs @@ -0,0 +1,28 @@ +use alloy::{ + network::{Ethereum, EthereumWallet}, + providers::{ + fillers::{ChainIdFiller, FillProvider, GasFiller, JoinFill, NonceFiller, WalletFiller}, + Identity, RootProvider, + }, + transports::http::{Client, Http}, +}; + +pub type LocalWalletSignerMiddleware = FillProvider< + JoinFill< + JoinFill, NonceFiller>, ChainIdFiller>, + WalletFiller, + >, + RootProvider>, + Http, + Ethereum, +>; + +pub type EthHttpProvider = FillProvider< + JoinFill< + JoinFill, NonceFiller>, ChainIdFiller>, + WalletFiller, + >, + RootProvider>, + Http, + Ethereum, +>; diff --git a/crates/settlement-clients/settlement-client-interface/src/lib.rs b/crates/settlement-clients/settlement-client-interface/src/lib.rs index 7fc51a65..2ea25708 100644 --- a/crates/settlement-clients/settlement-client-interface/src/lib.rs +++ b/crates/settlement-clients/settlement-client-interface/src/lib.rs @@ -1,44 +1,48 @@ use async_trait::async_trait; -use color_eyre::Result; +use color_eyre::eyre::Result; use mockall::automock; use mockall::predicate::*; -use starknet::core::types::FieldElement; -#[derive(Debug, Copy, Clone, PartialEq, Eq)] +pub const SETTLEMENT_SETTINGS_NAME: &str = "settlement_settings"; + +#[derive(Debug, Clone, PartialEq, Eq)] pub enum SettlementVerificationStatus { - #[allow(dead_code)] Pending, - #[allow(dead_code)] Verified, - #[allow(dead_code)] - Rejected, + Rejected(String), } -/// Trait for every new DaClient to implement +/// Trait for every new Settlement Layer to implement #[automock] #[async_trait] pub trait SettlementClient: Send + Sync { /// Should register the proof on the base layer and return an external id /// which can be used to track the status. - async fn register_proof(&self, proof: Vec) -> Result; + async fn register_proof(&self, proof: [u8; 32]) -> Result; /// Should be used to update state on core contract when DA is done in calldata async fn update_state_calldata( &self, - program_output: Vec, - onchain_data_hash: FieldElement, - onchain_data_size: FieldElement, + program_output: Vec<[u8; 32]>, + onchain_data_hash: [u8; 32], + onchain_data_size: usize, ) -> Result; /// Should be used to update state on core contract when DA is in blobs/alt DA - async fn update_state_blobs(&self, program_output: Vec, kzg_proof: Vec) -> Result; + async fn update_state_blobs(&self, program_output: Vec<[u8; 32]>, kzg_proof: [u8; 48]) -> Result; + + /// Should verify the inclusion of a tx in the settlement layer + async fn verify_tx_inclusion(&self, tx_hash: &str) -> Result; + + /// Should wait that the pending tx_hash is finalized + async fn wait_for_tx_finality(&self, tx_hash: &str) -> Result<()>; - /// Should verify the inclusion of the state diff in the DA layer and return the status - async fn verify_inclusion(&self, external_id: &str) -> Result; + /// Should retrieves the last settled block in the settlement layer + async fn get_last_settled_block(&self) -> Result; } -/// Trait for every new DaConfig to implement +/// Trait for every new SettlementConfig to implement pub trait SettlementConfig { - /// Should create a new instance of the DaConfig from the environment variables + /// Should create a new instance of the SettlementConfig from the environment variables fn new_from_env() -> Self; } diff --git a/crates/settlement-clients/starknet/Cargo.toml b/crates/settlement-clients/starknet/Cargo.toml new file mode 100644 index 00000000..ed772c79 --- /dev/null +++ b/crates/settlement-clients/starknet/Cargo.toml @@ -0,0 +1,23 @@ +[package] +name = "starknet-settlement-client" +version.workspace = true +edition.workspace = true + +[dependencies] +async-trait = { workspace = true } +c-kzg = "1.0.0" +color-eyre = { workspace = true } +dotenv = "0.15" +lazy_static = { workspace = true } +mockall = "0.12.1" +reqwest = { version = "0.12.3" } +rstest = { workspace = true } +serde = { workspace = true } +settlement-client-interface = { workspace = true } +starknet = { workspace = true } +tokio = { workspace = true } +url = { workspace = true } +utils = { workspace = true } + +[dev-dependencies] +tokio-test = "*" diff --git a/crates/settlement-clients/starknet/src/config.rs b/crates/settlement-clients/starknet/src/config.rs new file mode 100644 index 00000000..60131bdc --- /dev/null +++ b/crates/settlement-clients/starknet/src/config.rs @@ -0,0 +1,43 @@ +use std::str::FromStr; + +use serde::{Deserialize, Serialize}; +use settlement_client_interface::SettlementConfig; +use url::Url; +use utils::env_utils::{get_env_var_or_default, get_env_var_or_panic}; + +pub const ENV_STARKNET_RPC_URL: &str = "STARKNET_RPC_URL"; +pub const ENV_CORE_CONTRACT_ADDRESS: &str = "STARKNET_CAIRO_CORE_CONTRACT_ADDRESS"; + +pub const ENV_STARKNET_FINALITY_RETRY_DELAY_IN_SECS: &str = "STARKNET_FINALITY_RETRY_WAIT_IN_SECS"; +pub const DEFAULT_FINALITY_RETRY_DELAY: &str = "60"; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct StarknetSettlementConfig { + pub rpc_url: Url, + pub core_contract_address: String, + pub tx_finality_retry_delay_in_seconds: u64, +} + +impl SettlementConfig for StarknetSettlementConfig { + /// Should create a new instance of the DaConfig from the environment variables + fn new_from_env() -> Self { + let rpc_url = get_env_var_or_panic(ENV_STARKNET_RPC_URL); + let rpc_url = Url::from_str(&rpc_url).unwrap_or_else(|_| panic!("Failed to parse {}", ENV_STARKNET_RPC_URL)); + let core_contract_address = get_env_var_or_panic(ENV_CORE_CONTRACT_ADDRESS); + let tx_finality_retry_delay_in_seconds: u64 = + get_env_var_or_default(ENV_STARKNET_FINALITY_RETRY_DELAY_IN_SECS, DEFAULT_FINALITY_RETRY_DELAY) + .parse() + .expect("STARKNET_FINALITY_RETRY_WAIT_IN_SECS should be a delay in seconds"); + StarknetSettlementConfig { rpc_url, core_contract_address, tx_finality_retry_delay_in_seconds } + } +} + +impl Default for StarknetSettlementConfig { + fn default() -> Self { + Self { + rpc_url: "https://free-rpc.nethermind.io/sepolia-juno".parse().unwrap(), + core_contract_address: "TODO:https://github.com/keep-starknet-strange/piltover".into(), + tx_finality_retry_delay_in_seconds: 60, + } + } +} diff --git a/crates/settlement-clients/starknet/src/conversion.rs b/crates/settlement-clients/starknet/src/conversion.rs new file mode 100644 index 00000000..816999ea --- /dev/null +++ b/crates/settlement-clients/starknet/src/conversion.rs @@ -0,0 +1,9 @@ +use starknet::core::types::FieldElement; + +pub(crate) fn slice_slice_u8_to_vec_field(slices: &[[u8; 32]]) -> Vec { + slices.iter().map(slice_u8_to_field).collect() +} + +pub(crate) fn slice_u8_to_field(slice: &[u8; 32]) -> FieldElement { + FieldElement::from_byte_slice_be(slice).expect("could not convert u8 slice to FieldElement") +} diff --git a/crates/settlement-clients/starknet/src/lib.rs b/crates/settlement-clients/starknet/src/lib.rs new file mode 100644 index 00000000..d89b4765 --- /dev/null +++ b/crates/settlement-clients/starknet/src/lib.rs @@ -0,0 +1,198 @@ +pub mod config; +pub mod conversion; + +use std::sync::Arc; + +use async_trait::async_trait; +use color_eyre::eyre::eyre; +use color_eyre::Result; +use lazy_static::lazy_static; +use mockall::{automock, predicate::*}; +use starknet::accounts::ConnectedAccount; +use starknet::core::types::{ExecutionResult, MaybePendingTransactionReceipt}; +use starknet::providers::Provider; +use starknet::{ + accounts::{Account, Call, ExecutionEncoding, SingleOwnerAccount}, + core::{ + types::{BlockId, BlockTag, FieldElement, FunctionCall}, + utils::get_selector_from_name, + }, + providers::{jsonrpc::HttpTransport, JsonRpcClient}, + signers::{LocalWallet, SigningKey}, +}; +use tokio::time::{sleep, Duration}; + +use settlement_client_interface::{SettlementClient, SettlementVerificationStatus, SETTLEMENT_SETTINGS_NAME}; +use utils::env_utils::get_env_var_or_panic; +use utils::settings::SettingsProvider; + +use crate::config::StarknetSettlementConfig; +use crate::conversion::{slice_slice_u8_to_vec_field, slice_u8_to_field}; + +pub struct StarknetSettlementClient { + pub account: SingleOwnerAccount>, LocalWallet>, + pub core_contract_address: FieldElement, + pub tx_finality_retry_delay_in_seconds: u64, +} + +pub const ENV_PUBLIC_KEY: &str = "STARKNET_PUBLIC_KEY"; +pub const ENV_PRIVATE_KEY: &str = "STARKNET_PRIVATE_KEY"; + +const MAX_RETRIES_VERIFY_TX_FINALITY: usize = 10; + +// Assumed the contract called for settlement l ooks like: +// https://github.com/keep-starknet-strange/piltover + +impl StarknetSettlementClient { + pub async fn with_settings(settings: &impl SettingsProvider) -> Self { + let settlement_cfg: StarknetSettlementConfig = settings.get_settings(SETTLEMENT_SETTINGS_NAME).unwrap(); + let provider = Arc::new(JsonRpcClient::new(HttpTransport::new(settlement_cfg.rpc_url))); + + let public_key = get_env_var_or_panic(ENV_PUBLIC_KEY); + let signer_address = FieldElement::from_hex_be(&public_key).expect("invalid signer address"); + + // TODO: Very insecure way of building the signer. Needs to be adjusted. + let private_key = get_env_var_or_panic(ENV_PRIVATE_KEY); + let signer = FieldElement::from_hex_be(&private_key).expect("Invalid private key"); + let signer = LocalWallet::from(SigningKey::from_secret_scalar(signer)); + + let core_contract_address = + FieldElement::from_hex_be(&settlement_cfg.core_contract_address).expect("Invalid core contract address"); + + let account = SingleOwnerAccount::new( + provider.clone(), + signer, + signer_address, + provider.chain_id().await.unwrap(), + ExecutionEncoding::Legacy, + ); + + StarknetSettlementClient { + account, + core_contract_address, + tx_finality_retry_delay_in_seconds: settlement_cfg.tx_finality_retry_delay_in_seconds, + } + } +} + +lazy_static! { + pub static ref CONTRACT_WRITE_UPDATE_STATE_SELECTOR: FieldElement = + get_selector_from_name("update_state").expect("Invalid update state selector"); + // TODO: `stateBlockNumber` does not exists yet in our implementation: + // https://github.com/keep-starknet-strange/piltover + // It should get added to match the solidity implementation of the core contract. + pub static ref CONTRACT_READ_STATE_BLOCK_NUMBER: FieldElement = + get_selector_from_name("stateBlockNumber").expect("Invalid update state selector"); +} + +// TODO: Note that we already have an implementation of the appchain core contract client available here: +// https://github.com/keep-starknet-strange/zaun/tree/main/crates/l3/appchain-core-contract-client +// However, this implementation uses different FieldElement types, and incorporating all of them +// into this repository would introduce unnecessary complexity. +// Therefore, we will wait for the update of starknet_rs in the Zaun repository before adapting +// the StarknetSettlementClient implementation. + +#[automock] +#[async_trait] +impl SettlementClient for StarknetSettlementClient { + /// Should register the proof on the base layer and return an external id + /// which can be used to track the status. + #[allow(unused)] + async fn register_proof(&self, proof: [u8; 32]) -> Result { + !unimplemented!("register_proof not implemented yet") + } + + /// Should be used to update state on core contract when DA is done in calldata + async fn update_state_calldata( + &self, + program_output: Vec<[u8; 32]>, + onchain_data_hash: [u8; 32], + onchain_data_size: usize, + ) -> Result { + let program_output = slice_slice_u8_to_vec_field(program_output.as_slice()); + let onchain_data_hash = slice_u8_to_field(&onchain_data_hash); + let mut calldata: Vec = Vec::with_capacity(program_output.len() + 2); + calldata.extend(program_output); + calldata.push(onchain_data_hash); + calldata.push(FieldElement::from(onchain_data_size)); + let invoke_result = self + .account + .execute(vec![Call { + to: self.core_contract_address, + selector: *CONTRACT_WRITE_UPDATE_STATE_SELECTOR, + calldata, + }]) + .send() + .await?; + Ok(format!("0x{:x}", invoke_result.transaction_hash)) + } + + /// Should be used to update state on core contract when DA is in blobs/alt DA + #[allow(unused)] + async fn update_state_blobs(&self, program_output: Vec<[u8; 32]>, kzg_proof: [u8; 48]) -> Result { + !unimplemented!("not available for starknet settlement layer") + } + + /// Should verify the inclusion of a tx in the settlement layer + async fn verify_tx_inclusion(&self, tx_hash: &str) -> Result { + let tx_hash = FieldElement::from_hex_be(tx_hash)?; + let tx_receipt = self.account.provider().get_transaction_receipt(tx_hash).await?; + match tx_receipt { + MaybePendingTransactionReceipt::Receipt(tx) => match tx.execution_result() { + ExecutionResult::Succeeded => Ok(SettlementVerificationStatus::Verified), + ExecutionResult::Reverted { reason } => { + Ok(SettlementVerificationStatus::Rejected(format!("Tx {} has been reverted: {}", tx_hash, reason))) + } + }, + MaybePendingTransactionReceipt::PendingReceipt(tx) => match tx.execution_result() { + ExecutionResult::Succeeded => Ok(SettlementVerificationStatus::Pending), + ExecutionResult::Reverted { reason } => Ok(SettlementVerificationStatus::Rejected(format!( + "Pending tx {} has been reverted: {}", + tx_hash, reason + ))), + }, + } + } + + /// Wait for a pending tx to achieve finality + async fn wait_for_tx_finality(&self, tx_hash: &str) -> Result<()> { + let mut retries = 0; + let duration_to_wait_between_polling = Duration::from_secs(self.tx_finality_retry_delay_in_seconds); + sleep(duration_to_wait_between_polling).await; + + let tx_hash = FieldElement::from_hex_be(tx_hash)?; + loop { + let tx_receipt = self.account.provider().get_transaction_receipt(tx_hash).await?; + if let MaybePendingTransactionReceipt::PendingReceipt(_) = tx_receipt { + retries += 1; + if retries > MAX_RETRIES_VERIFY_TX_FINALITY { + return Err(eyre!("Max retries exceeeded while waiting for tx {tx_hash} finality.")); + } + sleep(duration_to_wait_between_polling).await; + } else { + break; + } + } + Ok(()) + } + + /// Returns the last block settled from the core contract. + async fn get_last_settled_block(&self) -> Result { + let block_number = self + .account + .provider() + .call( + FunctionCall { + contract_address: self.core_contract_address, + entry_point_selector: *CONTRACT_READ_STATE_BLOCK_NUMBER, + calldata: vec![], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await?; + if block_number.is_empty() { + return Err(eyre!("Could not fetch last block number from core contract.")); + } + Ok(block_number[0].try_into()?) + } +} diff --git a/crates/utils/src/collections.rs b/crates/utils/src/collections.rs new file mode 100644 index 00000000..a7e57f12 --- /dev/null +++ b/crates/utils/src/collections.rs @@ -0,0 +1,18 @@ +pub fn has_dup(slice: &[T]) -> bool { + for i in 1..slice.len() { + if slice[i..].contains(&slice[i - 1]) { + return true; + } + } + false +} + +pub fn is_sorted(data: &[T]) -> bool +where + T: Ord, +{ + if data.len() == 1 { + return true; + } + data.windows(2).all(|w| w[0] <= w[1]) +} diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs index 277ce5ab..258d31c0 100644 --- a/crates/utils/src/lib.rs +++ b/crates/utils/src/lib.rs @@ -1,3 +1,4 @@ +pub mod collections; pub mod env_utils; pub mod settings;