diff --git a/Cargo.lock b/Cargo.lock index 247d669..138188b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1361,6 +1361,12 @@ dependencies = [ "shlex", ] +[[package]] +name = "cesu8" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" + [[package]] name = "cexpr" version = "0.6.0" @@ -1514,6 +1520,16 @@ version = "1.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b05b61dc5112cbb17e4b6cd61790d9845d13888356391624cbe7e41efeac1e75" +[[package]] +name = "combine" +version = "4.6.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" +dependencies = [ + "bytes", + "memchr", +] + [[package]] name = "commonware-broadcast" version = "0.0.63" @@ -1853,6 +1869,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2606,6 +2632,10 @@ name = "futures-timer" version = "3.0.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" +dependencies = [ + "gloo-timers", + "send_wrapper", +] [[package]] name = "futures-util" @@ -2681,6 +2711,52 @@ version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8d1add55171497b4705a648c6b583acafb01d58050a51727785f0b2c8e0a2b2" +[[package]] +name = "gloo-net" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" +dependencies = [ + "futures-channel", + "futures-core", + "futures-sink", + "gloo-utils", + "http", + "js-sys", + "pin-project", + "serde", + "serde_json", + "thiserror 1.0.69", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", +] + +[[package]] +name = "gloo-timers" +version = "0.2.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b995a66bb87bebce9a0f4a95aed01daca4872c050bfcb21653361c03bc35e5c" +dependencies = [ + "futures-channel", + "futures-core", + "js-sys", + "wasm-bindgen", +] + +[[package]] +name = "gloo-utils" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" +dependencies = [ + "js-sys", + "serde", + "serde_json", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "governor" version = "0.10.2" @@ -2913,6 +2989,7 @@ dependencies = [ "http", "hyper", "hyper-util", + "log", "rustls", "rustls-pki-types", "tokio", @@ -2988,7 +3065,7 @@ dependencies = [ "js-sys", "log", "wasm-bindgen", - "windows-core 0.58.0", + "windows-core 0.61.2", ] [[package]] @@ -3252,6 +3329,28 @@ version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" +[[package]] +name = "jni" +version = "0.21.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +dependencies = [ + "cesu8", + "cfg-if", + "combine", + "jni-sys", + "log", + "thiserror 1.0.69", + "walkdir", + "windows-sys 0.45.0", +] + +[[package]] +name = "jni-sys" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" + [[package]] name = "jobserver" version = "0.1.33" @@ -3272,6 +3371,49 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "jsonrpsee" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core 0.26.0", + "jsonrpsee-http-client", + "jsonrpsee-proc-macros", + "jsonrpsee-server 0.26.0", + "jsonrpsee-types 0.26.0", + "jsonrpsee-wasm-client", + "jsonrpsee-ws-client", + "tokio", + "tracing", +] + +[[package]] +name = "jsonrpsee-client-transport" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" +dependencies = [ + "base64 0.22.1", + "futures-channel", + "futures-util", + "gloo-net", + "http", + "jsonrpsee-core 0.26.0", + "pin-project", + "rustls", + "rustls-pki-types", + "rustls-platform-verifier", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-rustls", + "tokio-util", + "tracing", + "url", +] + [[package]] name = "jsonrpsee-core" version = "0.25.1" @@ -3284,7 +3426,7 @@ dependencies = [ "http", "http-body", "http-body-util", - "jsonrpsee-types", + "jsonrpsee-types 0.25.1", "parking_lot", "pin-project", "rand 0.9.1", @@ -3297,6 +3439,70 @@ dependencies = [ "tracing", ] +[[package]] +name = "jsonrpsee-core" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" +dependencies = [ + "async-trait", + "bytes", + "futures-timer", + "futures-util", + "http", + "http-body", + "http-body-util", + "jsonrpsee-types 0.26.0", + "parking_lot", + "pin-project", + "rand 0.9.1", + "rustc-hash 2.1.1", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tower 0.5.2", + "tracing", + "wasm-bindgen-futures", +] + +[[package]] +name = "jsonrpsee-http-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" +dependencies = [ + "base64 0.22.1", + "http-body", + "hyper", + "hyper-rustls", + "hyper-util", + "jsonrpsee-core 0.26.0", + "jsonrpsee-types 0.26.0", + "rustls", + "rustls-platform-verifier", + "serde", + "serde_json", + "thiserror 2.0.12", + "tokio", + "tower 0.5.2", + "url", +] + +[[package]] +name = "jsonrpsee-proc-macros" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" +dependencies = [ + "heck", + "proc-macro-crate", + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "jsonrpsee-server" version = "0.25.1" @@ -3309,8 +3515,35 @@ dependencies = [ "http-body-util", "hyper", "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", + "jsonrpsee-core 0.25.1", + "jsonrpsee-types 0.25.1", + "pin-project", + "route-recognizer", + "serde", + "serde_json", + "soketto", + "thiserror 2.0.12", + "tokio", + "tokio-stream", + "tokio-util", + "tower 0.5.2", + "tracing", +] + +[[package]] +name = "jsonrpsee-server" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" +dependencies = [ + "futures-util", + "http", + "http-body", + "http-body-util", + "hyper", + "hyper-util", + "jsonrpsee-core 0.26.0", + "jsonrpsee-types 0.26.0", "pin-project", "route-recognizer", "serde", @@ -3336,6 +3569,44 @@ dependencies = [ "thiserror 2.0.12", ] +[[package]] +name = "jsonrpsee-types" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" +dependencies = [ + "http", + "serde", + "serde_json", + "thiserror 2.0.12", +] + +[[package]] +name = "jsonrpsee-wasm-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7902885de4779f711a95d82c8da2d7e5f9f3a7c7cfa44d51c067fd1c29d72a3c" +dependencies = [ + "jsonrpsee-client-transport", + "jsonrpsee-core 0.26.0", + "jsonrpsee-types 0.26.0", + "tower 0.5.2", +] + +[[package]] +name = "jsonrpsee-ws-client" +version = "0.26.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" +dependencies = [ + "http", + "jsonrpsee-client-transport", + "jsonrpsee-core 0.26.0", + "jsonrpsee-types 0.26.0", + "tower 0.5.2", + "url", +] + [[package]] name = "jsonwebtoken" version = "9.3.1" @@ -3641,7 +3912,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -5031,6 +5302,7 @@ version = "0.23.31" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c0ebcbd2f03de0fc1122ad9bb24b127a5a6cd51d72604a3f3c50ac459762b6cc" dependencies = [ + "log", "once_cell", "ring", "rustls-pki-types", @@ -5039,6 +5311,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-native-certs" +version = "0.8.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9980d917ebb0c0536119ba501e90834767bffc3d60641457fd84a1f3fd337923" +dependencies = [ + "openssl-probe", + "rustls-pki-types", + "schannel", + "security-framework 3.3.0", +] + [[package]] name = "rustls-pki-types" version = "1.12.0" @@ -5049,6 +5333,33 @@ dependencies = [ "zeroize", ] +[[package]] +name = "rustls-platform-verifier" +version = "0.5.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" +dependencies = [ + "core-foundation 0.10.1", + "core-foundation-sys", + "jni", + "log", + "once_cell", + "rustls", + "rustls-native-certs", + "rustls-platform-verifier-android", + "rustls-webpki", + "security-framework 3.3.0", + "security-framework-sys", + "webpki-root-certs 0.26.11", + "windows-sys 0.52.0", +] + +[[package]] +name = "rustls-platform-verifier-android" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" + [[package]] name = "rustls-webpki" version = "0.103.4" @@ -5175,7 +5486,20 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + +[[package]] +name = "security-framework" +version = "3.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80fb1d92c5028aa318b4b8bd7302a5bfcf48be96a37fc6fc790f806b0004ee0c" +dependencies = [ + "bitflags", + "core-foundation 0.10.1", "core-foundation-sys", "libc", "security-framework-sys", @@ -5215,6 +5539,12 @@ dependencies = [ "pest", ] +[[package]] +name = "send_wrapper" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f638d531eccd6e23b980caf34876660d38e265409d8e99b397ab71eb3612fad0" + [[package]] name = "serde" version = "1.0.228" @@ -5247,14 +5577,15 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.140" +version = "1.0.149" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "20068b6e96dc6c9bd23e01df8827e6c7e1f2fddd43c21810382803c136b99373" +checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" dependencies = [ "itoa", "memchr", - "ryu", "serde", + "serde_core", + "zmij", ] [[package]] @@ -5598,7 +5929,8 @@ dependencies = [ "futures", "governor", "http", - "jsonrpsee-server", + "jsonrpsee", + "jsonrpsee-server 0.25.1", "metrics", "metrics-exporter-prometheus", "metrics-process", @@ -5711,7 +6043,8 @@ version = "0.0.0" dependencies = [ "alloy-primitives", "anyhow", - "axum 0.8.4", + "async-trait", + "bytes", "commonware-codec", "commonware-consensus", "commonware-cryptography", @@ -5720,12 +6053,21 @@ dependencies = [ "dirs 5.0.1", "ethereum_ssz", "futures", + "http", + "http-body-util", + "hyper", + "jsonrpsee", + "jsonrpsee-core 0.26.0", + "jsonrpsee-types 0.26.0", "serde", "serde_json", "summit-finalizer", "summit-types", + "tempfile", "tokio", "tokio-util", + "tower 0.5.2", + "tower-http", "tracing", ] @@ -5866,7 +6208,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys", ] @@ -6667,6 +7009,24 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "webpki-root-certs" +version = "0.26.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" +dependencies = [ + "webpki-root-certs 1.0.4", +] + +[[package]] +name = "webpki-root-certs" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee3e3b5f5e80bc89f30ce8d0343bf4e5f12341c51f3e26cbeecbc7c85443e85b" +dependencies = [ + "rustls-pki-types", +] + [[package]] name = "webpki-roots" version = "1.0.2" @@ -6890,6 +7250,15 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-sys" +version = "0.45.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets 0.42.2", +] + [[package]] name = "windows-sys" version = "0.48.0" @@ -6926,6 +7295,21 @@ dependencies = [ "windows-targets 0.53.2", ] +[[package]] +name = "windows-targets" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8e5180c00cd44c9b1c88adb3693291f1cd93605ded80c250a75d472756b4d071" +dependencies = [ + "windows_aarch64_gnullvm 0.42.2", + "windows_aarch64_msvc 0.42.2", + "windows_i686_gnu 0.42.2", + "windows_i686_msvc 0.42.2", + "windows_x86_64_gnu 0.42.2", + "windows_x86_64_gnullvm 0.42.2", + "windows_x86_64_msvc 0.42.2", +] + [[package]] name = "windows-targets" version = "0.48.5" @@ -6982,6 +7366,12 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "597a5118570b68bc08d8d59125332c54f1ba9d9adeedeef5b99b02ba2b0698f8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.48.5" @@ -7000,6 +7390,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "86b8d5f90ddd19cb4a147a5fa63ca848db3df085e25fee3cc10b39b6eebae764" +[[package]] +name = "windows_aarch64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e08e8864a60f06ef0d0ff4ba04124db8b0fb3be5776a5cd47641e942e58c4d43" + [[package]] name = "windows_aarch64_msvc" version = "0.48.5" @@ -7018,6 +7414,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c7651a1f62a11b8cbd5e0d42526e55f2c99886c77e007179efff86c2b137e66c" +[[package]] +name = "windows_i686_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c61d927d8da41da96a81f029489353e68739737d3beca43145c8afec9a31a84f" + [[package]] name = "windows_i686_gnu" version = "0.48.5" @@ -7048,6 +7450,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9ce6ccbdedbf6d6354471319e781c0dfef054c81fbc7cf83f338a4296c0cae11" +[[package]] +name = "windows_i686_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "44d840b6ec649f480a41c8d80f9c65108b92d89345dd94027bfe06ac444d1060" + [[package]] name = "windows_i686_msvc" version = "0.48.5" @@ -7066,6 +7474,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "581fee95406bb13382d2f65cd4a908ca7b1e4c2f1917f143ba16efe98a589b5d" +[[package]] +name = "windows_x86_64_gnu" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8de912b8b8feb55c064867cf047dda097f92d51efad5b491dfb98f6bbb70cb36" + [[package]] name = "windows_x86_64_gnu" version = "0.48.5" @@ -7084,6 +7498,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2e55b5ac9ea33f2fc1716d1742db15574fd6fc8dadc51caab1c16a3d3b4190ba" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "26d41b46a36d453748aedef1486d5c7a85db22e56aff34643984ea85514e94a3" + [[package]] name = "windows_x86_64_gnullvm" version = "0.48.5" @@ -7102,6 +7522,12 @@ version = "0.53.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0a6e035dd0599267ce1ee132e51c27dd29437f63325753051e71dd9e42406c57" +[[package]] +name = "windows_x86_64_msvc" +version = "0.42.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9aec5da331524158c6d1a4ac0ab1541149c0b9505fde06423b02f5ef0106b9f0" + [[package]] name = "windows_x86_64_msvc" version = "0.48.5" @@ -7283,6 +7709,12 @@ dependencies = [ "syn 2.0.104", ] +[[package]] +name = "zmij" +version = "1.0.12" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fc5a66a20078bf1251bde995aa2fdcc4b800c70b5d92dd2c62abc5c60f679f8" + [[package]] name = "zstd" version = "0.13.3" diff --git a/Cargo.toml b/Cargo.toml index d5abeb9..4537a45 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -56,6 +56,7 @@ tokio = "1.46.0" tokio-util = "0.7.16" console-subscriber = "0.4" pin-project = "1.1.10" +jsonrpsee = { version = "0.26.0", features = ["http-client"] } ethereum_ssz = "0.9.0" ethereum_ssz_derive = "0.9.0" diff --git a/finalizer/src/actor.rs b/finalizer/src/actor.rs index 2cf9d6f..2d94f56 100644 --- a/finalizer/src/actor.rs +++ b/finalizer/src/actor.rs @@ -1271,10 +1271,10 @@ async fn process_execution_requests< } fn verify_deposit_request( - context: &ContextCell, + #[allow(unused)] context: &ContextCell, deposit_request: &DepositRequest, protocol_version_digest: Digest, - new_height: u64, + #[allow(unused)] new_height: u64, validator_minimum_stake: u64, ) -> bool { if deposit_request.amount != validator_minimum_stake { diff --git a/node/Cargo.toml b/node/Cargo.toml index a2ee3f3..2548337 100644 --- a/node/Cargo.toml +++ b/node/Cargo.toml @@ -61,7 +61,7 @@ tokio-util.workspace = true # stake and checkpoint bin deps alloy = "1.0.23" ethereum_ssz.workspace = true -reqwest.workspace = true +jsonrpsee.workspace = true # testnet bin deps alloy-node-bindings.workspace = true diff --git a/node/src/bin/stake_and_checkpoint.rs b/node/src/bin/stake_and_checkpoint.rs index d8fdc36..fb3139a 100644 --- a/node/src/bin/stake_and_checkpoint.rs +++ b/node/src/bin/stake_and_checkpoint.rs @@ -20,6 +20,7 @@ use commonware_cryptography::Sha256; use commonware_cryptography::{Hasher, PrivateKeyExt, Signer, bls12381, ed25519::PrivateKey}; use commonware_runtime::{Clock, Metrics as _, Runner as _, Spawner as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; +use jsonrpsee::http_client::HttpClientBuilder; use ssz::Decode; use std::collections::VecDeque; use std::time::Duration; @@ -33,13 +34,13 @@ use std::{ }; use summit::args::{RunFlags, run_node_local}; use summit::engine::VALIDATOR_MINIMUM_STAKE; +use summit_rpc::SummitApiClient; use summit_types::PROTOCOL_VERSION; use summit_types::checkpoint::Checkpoint; use summit_types::consensus_state::ConsensusState; use summit_types::execution_request::DepositRequest; use summit_types::execution_request::compute_deposit_data_root; use summit_types::reth::Reth; -use summit_types::rpc::CheckpointRes; use tokio::sync::mpsc; use tracing::Level; @@ -624,26 +625,25 @@ fn copy_dir_all(src: &str, dst: &str) -> std::io::Result<()> { } async fn get_latest_height(rpc_port: u16) -> Result> { - let url = format!("http://localhost:{}/get_latest_height", rpc_port); - let response = reqwest::get(&url).await?.text().await?; - Ok(response.parse()?) + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; + let height = client.get_latest_height().await?; + Ok(height) } async fn get_latest_checkpoint( rpc_port: u16, ) -> Result, Box> { - let url = format!("http://localhost:{}/get_latest_checkpoint", rpc_port); - let response = reqwest::get(&url).await; + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; - match response { - Ok(resp) if resp.status().is_success() => { - let checkpoint_resp: CheckpointRes = resp.json().await?; - // let bytes = from_hex_formatted(&hex_str).ok_or("Failed to decode hex")?; + match client.get_latest_checkpoint().await { + Ok(checkpoint_resp) => { let checkpoint = Checkpoint::from_ssz_bytes(&checkpoint_resp.checkpoint) .map_err(|e| format!("Failed to decode checkpoint: {:?}", e))?; Ok(Some(checkpoint)) } - _ => Ok(None), + Err(_) => Ok(None), } } diff --git a/node/src/bin/stake_and_join_with_outdated_ckpt.rs b/node/src/bin/stake_and_join_with_outdated_ckpt.rs index ea209ed..281f566 100644 --- a/node/src/bin/stake_and_join_with_outdated_ckpt.rs +++ b/node/src/bin/stake_and_join_with_outdated_ckpt.rs @@ -20,6 +20,7 @@ use commonware_cryptography::Sha256; use commonware_cryptography::{Hasher, PrivateKeyExt, Signer, bls12381, ed25519::PrivateKey}; use commonware_runtime::{Clock, Metrics as _, Runner as _, Spawner as _, tokio as cw_tokio}; use futures::{FutureExt, pin_mut}; +use jsonrpsee::http_client::HttpClientBuilder; use ssz::Decode; use std::collections::VecDeque; use std::time::Duration; @@ -33,13 +34,13 @@ use std::{ }; use summit::args::{RunFlags, run_node_local}; use summit::engine::VALIDATOR_MINIMUM_STAKE; +use summit_rpc::SummitApiClient; use summit_types::PROTOCOL_VERSION; use summit_types::checkpoint::Checkpoint; use summit_types::consensus_state::ConsensusState; use summit_types::execution_request::DepositRequest; use summit_types::execution_request::compute_deposit_data_root; use summit_types::reth::Reth; -use summit_types::rpc::CheckpointRes; use tokio::sync::mpsc; use tracing::Level; @@ -649,26 +650,25 @@ fn copy_dir_all(src: &str, dst: &str) -> std::io::Result<()> { } async fn get_latest_epoch(rpc_port: u16) -> Result> { - let url = format!("http://localhost:{}/get_latest_epoch", rpc_port); - let response = reqwest::get(&url).await?.text().await?; - Ok(response.parse()?) + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; + let epoch = client.get_latest_epoch().await?; + Ok(epoch) } async fn get_latest_checkpoint( rpc_port: u16, ) -> Result, Box> { - let url = format!("http://localhost:{}/get_latest_checkpoint", rpc_port); - let response = reqwest::get(&url).await; + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; - match response { - Ok(resp) if resp.status().is_success() => { - let checkpoint_resp: CheckpointRes = resp.json().await?; - // let bytes = from_hex_formatted(&hex_str).ok_or("Failed to decode hex")?; + match client.get_latest_checkpoint().await { + Ok(checkpoint_resp) => { let checkpoint = Checkpoint::from_ssz_bytes(&checkpoint_resp.checkpoint) .map_err(|e| format!("Failed to decode checkpoint: {:?}", e))?; Ok(Some(checkpoint)) } - _ => Ok(None), + Err(_) => Ok(None), } } diff --git a/node/src/bin/withdraw_and_exit.rs b/node/src/bin/withdraw_and_exit.rs index b7c93ae..865e050 100644 --- a/node/src/bin/withdraw_and_exit.rs +++ b/node/src/bin/withdraw_and_exit.rs @@ -19,6 +19,8 @@ use commonware_codec::DecodeExt; use commonware_runtime::{Clock, Metrics as _, Runner as _, Spawner as _, tokio as cw_tokio}; use commonware_utils::from_hex_formatted; use futures::{FutureExt, pin_mut}; +use jsonrpsee::core::ClientError; +use jsonrpsee::http_client::HttpClientBuilder; use std::collections::VecDeque; use std::time::Duration; use std::{ @@ -31,6 +33,7 @@ use std::{ }; use summit::args::{RunFlags, run_node_local}; use summit::engine::{BLOCKS_PER_EPOCH, VALIDATOR_MINIMUM_STAKE, VALIDATOR_WITHDRAWAL_NUM_EPOCHS}; +use summit_rpc::SummitApiClient; use summit_types::PublicKey; use summit_types::reth::Reth; use tokio::sync::mpsc; @@ -296,9 +299,14 @@ fn main() -> Result<(), Box> { // Check that the validator was removed from the consensus state let rpc_port = get_node_flags(0).rpc_port; let validator_balance = get_validator_balance(rpc_port, "f205c8c88d5d1753843dd0fc9810390efd00d6f752dd555c0ad4000bfcac2226".to_string()).await; - if let Err(msg) = validator_balance { - assert_eq!(msg.to_string(), "Validator not found"); - println!("Validator that withdrew is not on the consensus state anymore"); + if let Err(e) = validator_balance { + // Parse the JSON-RPC error + if let Some(ClientError::Call(err)) = e.downcast_ref::() { + assert_eq!(err.message(), "Validator not found"); + println!("Success: validator that withdrew is not on the consensus state anymore"); + } else { + panic!("Expected JSON-RPC Call error with 'Validator not found', got: {}", e); + } } else { panic!("Validator should not be on the consensus state anymore"); } @@ -357,23 +365,19 @@ where } async fn get_latest_height(rpc_port: u16) -> Result> { - let url = format!("http://localhost:{}/get_latest_height", rpc_port); - let response = reqwest::get(&url).await?.text().await?; - Ok(response.parse()?) + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; + let height = client.get_latest_height().await?; + Ok(height) } async fn get_validator_balance( rpc_port: u16, public_key: String, ) -> Result> { - let url = format!( - "http://localhost:{}/get_validator_balance?public_key={}", - rpc_port, public_key - ); - let response = reqwest::get(&url).await?.text().await?; - let Ok(balance) = response.parse() else { - return Err(response.into()); - }; + let url = format!("http://localhost:{}", rpc_port); + let client = HttpClientBuilder::default().build(&url)?; + let balance = client.get_validator_balance(public_key).await?; Ok(balance) } diff --git a/rpc/Cargo.toml b/rpc/Cargo.toml index 4dcb17a..3f974ec 100644 --- a/rpc/Cargo.toml +++ b/rpc/Cargo.toml @@ -15,7 +15,10 @@ alloy-primitives.workspace = true tokio = { workspace = true, features = ["net"] } tokio-util.workspace = true futures.workspace = true -axum = { version = "0.8.4"} +async-trait = "0.1" +jsonrpsee = { version = "0.26.0", features = ["server", "client", "macros"] } +jsonrpsee-core = "0.26.0" +jsonrpsee-types = "0.26.0" commonware-consensus = { workspace = true } commonware-cryptography = { workspace = true } commonware-utils = { workspace = true } @@ -26,3 +29,13 @@ dirs = "5.0.1" serde = { workspace = true } serde_json = { workspace = true } tracing.workspace = true +tower = { workspace = true } +tower-http = { version = "0.6", features = ["cors"] } +http = { workspace = true } +hyper = "1.0" +bytes = { workspace = true } +http-body-util = { workspace = true } + +[dev-dependencies] +tempfile = "3" +tokio = { workspace = true, features = ["net", "rt-multi-thread", "macros"] } diff --git a/rpc/src/api.rs b/rpc/src/api.rs new file mode 100644 index 0000000..5aecb23 --- /dev/null +++ b/rpc/src/api.rs @@ -0,0 +1,51 @@ +use crate::types::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, +}; +use jsonrpsee::core::RpcResult; +use jsonrpsee::proc_macros::rpc; + +#[rpc(server, client)] +pub trait SummitApi { + #[method(name = "health")] + async fn health(&self) -> RpcResult; + + #[method(name = "getPublicKeys")] + async fn get_public_keys(&self) -> RpcResult; + + #[method(name = "getCheckpoint")] + async fn get_checkpoint(&self, epoch: u64) -> RpcResult; + + #[method(name = "getLatestCheckpoint")] + async fn get_latest_checkpoint(&self) -> RpcResult; + + #[method(name = "getLatestCheckpointInfo")] + async fn get_latest_checkpoint_info(&self) -> RpcResult; + + #[method(name = "getLatestHeight")] + async fn get_latest_height(&self) -> RpcResult; + + #[method(name = "getLatestEpoch")] + async fn get_latest_epoch(&self) -> RpcResult; + + #[method(name = "getValidatorBalance")] + async fn get_validator_balance(&self, public_key: String) -> RpcResult; + + #[method(name = "getDepositSignature")] + async fn get_deposit_signature( + &self, + amount: u64, + address: String, + ) -> RpcResult; +} + +#[rpc(server, client)] +pub trait SummitGenesisApi { + #[method(name = "health")] + async fn health(&self) -> RpcResult; + + #[method(name = "getPublicKeys")] + async fn get_public_keys(&self) -> RpcResult; + + #[method(name = "sendGenesis")] + async fn send_genesis(&self, genesis_content: String) -> RpcResult; +} diff --git a/rpc/src/builder.rs b/rpc/src/builder.rs new file mode 100644 index 0000000..e585e8f --- /dev/null +++ b/rpc/src/builder.rs @@ -0,0 +1,114 @@ +use http::{HeaderValue, Method}; +use jsonrpsee::server::{ServerBuilder, ServerConfigBuilder, ServerHandle}; +use std::net::SocketAddr; +use tower::ServiceBuilder; +use tower_http::cors::{AllowOrigin, Any, CorsLayer}; + +pub struct RpcServerBuilder { + addr: SocketAddr, + config: ServerConfigBuilder, + cors_domains: Option, +} + +pub struct RpcServer { + inner: jsonrpsee::server::Server< + tower::layer::util::Stack< + tower::util::Either, + tower::layer::util::Identity, + >, + >, +} + +impl RpcServer { + pub fn start(self, methods: M) -> ServerHandle + where + M: Into, + { + self.inner.start(methods) + } + + pub fn local_addr(&self) -> anyhow::Result { + self.inner.local_addr().map_err(Into::into) + } +} + +impl RpcServerBuilder { + pub fn new(port: u16) -> Self { + Self { + addr: SocketAddr::from(([0, 0, 0, 0], port)), + config: ServerConfigBuilder::new(), + cors_domains: None, + } + } + + pub fn with_max_connections(mut self, max: u32) -> Self { + self.config = self.config.max_connections(max); + self + } + + pub fn with_max_request_body_size(mut self, max: u32) -> Self { + self.config = self.config.max_request_body_size(max); + self + } + + pub fn with_max_response_body_size(mut self, max: u32) -> Self { + self.config = self.config.max_response_body_size(max); + self + } + + pub fn with_cors(mut self, cors_domains: Option) -> Self { + self.cors_domains = cors_domains; + self + } + + pub async fn build(self) -> anyhow::Result { + let cors_layer = self + .cors_domains + .as_deref() + .map(create_cors_layer) + .transpose()?; + + let http_middleware = ServiceBuilder::new().option_layer(cors_layer); + + let server = ServerBuilder::new() + .set_config(self.config.build()) + .set_http_middleware(http_middleware) + .build(self.addr) + .await?; + + Ok(RpcServer { inner: server }) + } +} + +fn create_cors_layer(http_cors_domains: &str) -> anyhow::Result { + let cors = match http_cors_domains.trim() { + "*" => CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(Any) + .allow_headers(Any), + _ => { + let iter = http_cors_domains.split(','); + if iter.clone().any(|o| o == "*") { + anyhow::bail!( + "wildcard origin (`*`) cannot be passed as part of a list: {}", + http_cors_domains + ); + } + + let origins = iter + .map(|domain| { + domain + .parse::() + .map_err(|_| anyhow::anyhow!("{} is an invalid header value", domain)) + }) + .collect::, _>>()?; + + let origin = AllowOrigin::list(origins); + CorsLayer::new() + .allow_methods([Method::GET, Method::POST]) + .allow_origin(origin) + .allow_headers(Any) + } + }; + Ok(cors) +} diff --git a/rpc/src/error.rs b/rpc/src/error.rs new file mode 100644 index 0000000..3fbcb8e --- /dev/null +++ b/rpc/src/error.rs @@ -0,0 +1,35 @@ +use jsonrpsee::types::ErrorObjectOwned; + +pub enum RpcError { + KeyStoreError(String), + CheckpointNotFound, + ValidatorNotFound, + InvalidPublicKey(String), + GenesisPathError(String), + IoError(String), + Internal(String), +} + +impl From for ErrorObjectOwned { + fn from(err: RpcError) -> Self { + match err { + RpcError::KeyStoreError(msg) => { + ErrorObjectOwned::owned(1000, "Keystore error", Some(msg)) + } + RpcError::CheckpointNotFound => { + ErrorObjectOwned::owned(2000, "Checkpoint not found", None::<()>) + } + RpcError::ValidatorNotFound => { + ErrorObjectOwned::owned(3000, "Validator not found", None::<()>) + } + RpcError::InvalidPublicKey(msg) => { + ErrorObjectOwned::owned(3001, "Invalid public key", Some(msg)) + } + RpcError::GenesisPathError(msg) => { + ErrorObjectOwned::owned(2001, "Invalid genesis path", Some(msg)) + } + RpcError::IoError(msg) => ErrorObjectOwned::owned(2002, "I/O error", Some(msg)), + RpcError::Internal(msg) => ErrorObjectOwned::owned(5000, "Internal error", Some(msg)), + } + } +} diff --git a/rpc/src/genesis.rs b/rpc/src/genesis.rs new file mode 100644 index 0000000..40c11b9 --- /dev/null +++ b/rpc/src/genesis.rs @@ -0,0 +1,85 @@ +use crate::api::SummitGenesisApiServer; +use crate::error::RpcError; +use crate::types::PublicKeysResponse; +use async_trait::async_trait; +use futures::channel::oneshot; +use jsonrpsee::core::RpcResult; +use std::fs; +use std::sync::Mutex; +use summit_types::KeyPaths; +use summit_types::utils::get_expanded_path; + +pub struct PathSender { + pub path: String, + pub sender: Mutex>>, +} + +impl PathSender { + pub fn new(path: String, sender: Option>) -> PathSender { + PathSender { + path, + sender: Mutex::new(sender), + } + } +} + +pub struct SummitGenesisRpcServer { + key_store_path: String, + genesis: PathSender, +} + +impl SummitGenesisRpcServer { + pub fn new(key_store_path: String, genesis: PathSender) -> Self { + Self { + key_store_path, + genesis, + } + } +} + +#[async_trait] +impl SummitGenesisApiServer for SummitGenesisRpcServer { + async fn health(&self) -> RpcResult { + Ok("Ok".to_string()) + } + + async fn get_public_keys(&self) -> RpcResult { + let key_paths = KeyPaths::new(self.key_store_path.clone()); + + let node = key_paths.node_public_key().map_err(|e| { + RpcError::KeyStoreError(format!("Failed to read node public key: {}", e)) + })?; + + let consensus = key_paths.consensus_public_key().map_err(|e| { + RpcError::KeyStoreError(format!("Failed to read consensus public key: {}", e)) + })?; + + Ok(PublicKeysResponse { node, consensus }) + } + + async fn send_genesis(&self, genesis_content: String) -> RpcResult { + let path_buf = get_expanded_path(&self.genesis.path) + .map_err(|e| RpcError::GenesisPathError(format!("Invalid genesis path: {}", e)))?; + + if let Some(parent) = path_buf.parent() { + fs::create_dir_all(parent) + .map_err(|e| RpcError::IoError(format!("Failed to create directory: {}", e)))?; + } + + fs::write(&path_buf, &genesis_content) + .map_err(|e| RpcError::IoError(format!("Failed to write genesis file: {}", e)))?; + + if let Some(sender) = self.genesis.sender.lock().unwrap().take() { + let _ = sender.send(()); + Ok(format!( + "Genesis file written at location {} and node notified", + self.genesis.path + )) + } else { + Ok(format!( + "Genesis file written at location {} (no notification needed)", + self.genesis.path + )) + } + } +} diff --git a/rpc/src/lib.rs b/rpc/src/lib.rs index 2550d3e..ec61da8 100644 --- a/rpc/src/lib.rs +++ b/rpc/src/lib.rs @@ -1,78 +1,84 @@ -pub mod routes; -use crate::routes::RpcRoutes; +mod api; +mod builder; +mod error; +mod genesis; +mod server; +mod types; + +pub use genesis::{PathSender, SummitGenesisRpcServer}; +pub use server::SummitRpcServer; +pub use types::*; + +pub use api::{SummitApiClient, SummitApiServer, SummitGenesisApiClient, SummitGenesisApiServer}; + use commonware_consensus::Block as ConsensusBlock; use commonware_consensus::simplex::signing_scheme::Scheme; use commonware_cryptography::Committable; use commonware_runtime::signal::Signal; -use futures::channel::oneshot; -use std::sync::Mutex; +use jsonrpsee::server::ServerHandle; +use std::net::SocketAddr; use summit_finalizer::FinalizerMailbox; -use tokio::net::TcpListener; use tokio_util::sync::CancellationToken; -pub struct RpcState { - key_store_path: String, - finalizer_mailbox: FinalizerMailbox, -} - -impl RpcState { - pub fn new(key_store_path: String, finalizer_mailbox: FinalizerMailbox) -> Self { - Self { - key_store_path, - finalizer_mailbox, - } - } -} - -pub async fn start_rpc_server( +pub async fn start_rpc_server< + S: Scheme + Send + Sync + 'static, + B: ConsensusBlock + Committable + Send + Sync + 'static, +>( finalizer_mailbox: FinalizerMailbox, key_store_path: String, port: u16, stop_signal: Signal, ) -> anyhow::Result<()> { - let state = RpcState::new(key_store_path, finalizer_mailbox); - - let server = RpcRoutes::mount(state); + let rpc_impl = SummitRpcServer::new(key_store_path, finalizer_mailbox); - let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?; + let methods = rpc_impl.into_rpc(); - println!("RPC Server listening on http://0.0.0.0:{port}"); - axum::serve(listener, server) - .with_graceful_shutdown(async move { - let sig = stop_signal.await.unwrap(); - println!("RPC server stopped: {sig}"); - }) + let server = builder::RpcServerBuilder::new(port) + .with_max_connections(1000) + .with_max_request_body_size(10 * 1024 * 1024) + .with_max_response_body_size(10 * 1024 * 1024) + .with_cors(Some("*".to_string())) + .build() .await?; - Ok(()) -} + let handle = server.start(methods); -pub struct PathSender { - path: String, - sender: Mutex>>, -} + tracing::info!("RPC Server listening on http://0.0.0.0:{port}"); -impl PathSender { - pub fn new(path: String, sender: Option>) -> PathSender { - PathSender { - path, - sender: Mutex::new(sender), - } - } + let sig = stop_signal.await?; + tracing::info!("RPC server stopped: {sig}"); + handle.stop()?; + + Ok(()) } -pub struct GenesisRpcState { - genesis: PathSender, +/// Starts the RPC server and returns the handle and bound address (useful for testing) +pub async fn start_rpc_server_with_handle< + S: Scheme + Send + Sync + 'static, + B: ConsensusBlock + Committable + Send + Sync + 'static, +>( + finalizer_mailbox: FinalizerMailbox, key_store_path: String, -} + port: u16, +) -> anyhow::Result<(ServerHandle, SocketAddr)> { + let rpc_impl = SummitRpcServer::new(key_store_path, finalizer_mailbox); -impl GenesisRpcState { - pub fn new(genesis: PathSender, key_store_path: String) -> Self { - Self { - genesis, - key_store_path, - } - } + let methods = rpc_impl.into_rpc(); + + let server = builder::RpcServerBuilder::new(port) + .with_max_connections(1000) + .with_max_request_body_size(10 * 1024 * 1024) + .with_max_response_body_size(10 * 1024 * 1024) + .with_cors(Some("*".to_string())) + .build() + .await?; + + let addr = server.local_addr()?; + let handle = server.start(methods); + + tracing::info!("RPC Server listening on http://{}", addr); + + Ok((handle, addr)) } pub async fn start_rpc_server_for_genesis( @@ -81,19 +87,43 @@ pub async fn start_rpc_server_for_genesis( port: u16, cancel_token: CancellationToken, ) -> anyhow::Result<()> { - let state = GenesisRpcState::new(genesis, key_store_path); + let rpc_impl = SummitGenesisRpcServer::new(key_store_path, genesis); - let server = RpcRoutes::mount_for_genesis(state); + let methods = rpc_impl.into_rpc(); - let listener = TcpListener::bind(format!("0.0.0.0:{port}")).await?; + let server = builder::RpcServerBuilder::new(port) + .with_cors(Some("*".to_string())) + .build() + .await?; + let handle = server.start(methods); - println!("Genesis RPC Server listening on http://0.0.0.0:{port}"); + tracing::info!("Genesis RPC Server listening on http://0.0.0.0:{port}"); + + cancel_token.cancelled().await; + tracing::info!("Genesis RPC server stopped"); + handle.stop()?; - axum::serve(listener, server) - .with_graceful_shutdown(async move { - cancel_token.cancelled().await; - println!("Genesis RPC server stopped"); - }) - .await?; Ok(()) } + +/// Starts the genesis RPC server and returns the handle and bound address (useful for testing) +pub async fn start_rpc_server_for_genesis_with_handle( + genesis: PathSender, + key_store_path: String, + port: u16, +) -> anyhow::Result<(ServerHandle, SocketAddr)> { + let rpc_impl = SummitGenesisRpcServer::new(key_store_path, genesis); + + let methods = rpc_impl.into_rpc(); + + let server = builder::RpcServerBuilder::new(port) + .with_cors(Some("*".to_string())) + .build() + .await?; + let addr = server.local_addr()?; + let handle = server.start(methods); + + tracing::info!("Genesis RPC Server listening on http://{}", addr); + + Ok((handle, addr)) +} diff --git a/rpc/src/routes.rs b/rpc/src/routes.rs deleted file mode 100644 index c2c21d4..0000000 --- a/rpc/src/routes.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::sync::Arc; - -use crate::{GenesisRpcState, PathSender, RpcState}; -use alloy_primitives::{Address, U256, hex::FromHex as _}; -use axum::{ - Json, Router, - extract::{Path, Query, State}, - routing::{get, post}, -}; -use commonware_codec::{DecodeExt as _, Encode as _}; -use commonware_consensus::Block as ConsensusBlock; -use commonware_consensus::simplex::signing_scheme::Scheme; -use commonware_cryptography::{Committable, Hasher as _, Sha256, Signer as _}; -use commonware_utils::from_hex_formatted; -use serde::{Deserialize, Serialize}; -use ssz::Encode; -use summit_types::{ - KeyPaths, PROTOCOL_VERSION, PublicKey, - execution_request::{DepositRequest, compute_deposit_data_root}, - rpc::{CheckpointInfoRes, CheckpointRes}, - utils::get_expanded_path, -}; - -#[derive(Serialize)] -struct PublicKeysResponse { - node: String, - consensus: String, -} - -#[derive(Deserialize)] -struct ValidatorBalanceQuery { - public_key: String, -} - -#[derive(Serialize, Deserialize)] -struct DepositTransactionResponse { - node_pubkey: [u8; 32], - consensus_pubkey: Vec, // 48 bytes - withdrawal_credentials: [u8; 32], - node_signature: Vec, // 48 bytes - consensus_signature: Vec, // 96 bytes - deposit_data_root: [u8; 32], -} - -pub(crate) struct RpcRoutes; - -impl RpcRoutes { - pub fn mount( - state: RpcState, - ) -> Router { - // todo(dalton): Add cors - let state = Arc::new(state); - - Router::new() - .route("/health", get(Self::handle_health_check)) - .route("/get_public_keys", get(Self::handle_get_pub_keys::)) - .route( - "/get_checkpoint/{epoch}", - get(Self::handle_get_checkpoint::), - ) - .route( - "/get_latest_checkpoint", - get(Self::handle_get_latest_checkpoint), - ) - .route( - "/get_latest_checkpoint_info", - get(Self::handle_get_latest_checkpoint_info), - ) - .route( - "/get_latest_height", - get(Self::handle_latest_height::), - ) - .route("/get_latest_epoch", get(Self::handle_latest_epoch::)) - .route( - "/get_validator_balance", - get(Self::handle_get_validator_balance::), - ) - .route( - "/get_deposit_signature/{amount}/{address}", - get(Self::handle_get_deposit_signature), - ) - .with_state(state) - } - - pub fn mount_for_genesis(state: GenesisRpcState) -> Router { - // todo(dalton): Add cors - let state = Arc::new(state); - - Router::new() - .route("/health", get(Self::handle_health_check)) - .route("/get_public_keys", get(Self::handle_get_pub_keys_genesis)) - .route("/send_genesis", post(Self::handle_send_genesis)) - .with_state(state) - } - - async fn handle_health_check() -> &'static str { - "Ok" - } - - async fn handle_get_pub_keys( - State(state): State>>, - ) -> Result { - let key_paths = KeyPaths::new(state.key_store_path.clone()); - - let response = PublicKeysResponse { - node: key_paths.node_public_key()?, - consensus: key_paths.consensus_public_key()?, - }; - - serde_json::to_string(&response).map_err(|e| format!("Failed to serialize response: {}", e)) - } - - async fn handle_get_deposit_signature( - State(state): State>>, - Path((amount, address)): Path<(u64, String)>, - ) -> Result, String> { - // Withdrawal credentials (32 bytes) - 0x01 prefix for execution address withdrawal - // Format: 0x01 || 0x00...00 (11 bytes) || execution_address (20 bytes) - let mut withdrawal_credentials = [0u8; 32]; - withdrawal_credentials[0] = 0x01; // ETH1 withdrawal prefix - // Bytes 1-11 remain zero - // Set the last 20 bytes to the withdrawal address (using the same address as the sender) - let withdrawal_address = Address::from_hex(address).unwrap(); - withdrawal_credentials[12..32].copy_from_slice(withdrawal_address.as_slice()); - - //let amount = VALIDATOR_MINIMUM_STAKE; - - let key_paths = KeyPaths::new(state.key_store_path.clone()); - - let consenus_priv_key = key_paths.consensus_private_key()?; - let consensus_pub = consenus_priv_key.public_key(); - - let node_priv_key = key_paths.node_private_key()?; - let node_pub = node_priv_key.public_key(); - - let req = DepositRequest { - node_pubkey: node_pub.clone(), - consensus_pubkey: consensus_pub.clone(), - withdrawal_credentials, - amount, - node_signature: [0; 64], - consensus_signature: [0; 96], - index: 0, // not included in the signature - }; - - let protocol_version_digest = Sha256::hash(&PROTOCOL_VERSION.to_le_bytes()); - let message = req.as_message(protocol_version_digest); - - // Sign with node (ed25519) key - let node_signature = node_priv_key.sign(&[], &message); - let node_signature_bytes: [u8; 64] = node_signature - .as_ref() - .try_into() - .expect("ed25519 sig is alway 64 bytes"); - - // Sign with consensus (BLS) key - let consensus_signature = consenus_priv_key.sign(&[], &message); - let consensus_signature_slice: &[u8] = consensus_signature.as_ref(); - let consensus_signature_bytes: [u8; 96] = consensus_signature_slice - .try_into() - .expect("bls sig is alway 96 bytes"); - - let node_pubkey_bytes: [u8; 32] = node_pub.to_vec().try_into().expect("Cannot fail"); - let consensus_pubkey_bytes: [u8; 48] = - consensus_pub.encode().as_ref()[..48].try_into().unwrap(); - - let deposit_amount = U256::from(amount) * U256::from(1_000_000_000u64); // gwei to wei - - let deposit_root = compute_deposit_data_root( - &node_pubkey_bytes, - &consensus_pubkey_bytes, - &withdrawal_credentials, - deposit_amount, - &node_signature_bytes, - &consensus_signature_bytes, - ); - - Ok(Json(DepositTransactionResponse { - node_pubkey: node_pubkey_bytes, - consensus_pubkey: consensus_pubkey_bytes.to_vec(), - withdrawal_credentials, - node_signature: node_signature_bytes.to_vec(), - consensus_signature: consensus_signature_bytes.to_vec(), - deposit_data_root: deposit_root, - })) - } - - async fn handle_get_pub_keys_genesis( - State(state): State>, - ) -> Result { - let key_paths = KeyPaths::new(state.key_store_path.clone()); - - let response = PublicKeysResponse { - node: key_paths.node_public_key()?, - consensus: key_paths.consensus_public_key()?, - }; - - serde_json::to_string(&response).map_err(|e| format!("Failed to serialize response: {}", e)) - } - - async fn handle_get_checkpoint( - State(state): State>>, - Path(epoch): Path, - ) -> Result, String> { - let maybe_checkpoint = state.finalizer_mailbox.clone().get_checkpoint(epoch).await; - let Some(checkpoint) = maybe_checkpoint else { - return Err("checkpoint not found".into()); - }; - - Ok(Json(CheckpointRes { - checkpoint: checkpoint.data.into(), - digest: checkpoint.digest.0, - epoch, - })) - } - - async fn handle_get_latest_checkpoint( - State(state): State>>, - ) -> Result, String> { - let maybe_checkpoint = state - .finalizer_mailbox - .clone() - .get_latest_checkpoint() - .await; - let (Some(checkpoint), epoch) = maybe_checkpoint else { - return Err("checkpoint not found".into()); - }; - - Ok(Json(CheckpointRes { - checkpoint: checkpoint.as_ssz_bytes(), - digest: checkpoint.digest.0, - epoch, - })) - } - - async fn handle_get_latest_checkpoint_info( - State(state): State>>, - ) -> Result, String> { - let maybe_checkpoint = state - .finalizer_mailbox - .clone() - .get_latest_checkpoint() - .await; - let (Some(checkpoint), epoch) = maybe_checkpoint else { - return Err("checkpoint not found".into()); - }; - - Ok(Json(CheckpointInfoRes { - epoch, - digest: checkpoint.digest.0, - })) - } - - async fn handle_latest_height( - State(state): State>>, - ) -> Result { - Ok(state - .finalizer_mailbox - .get_latest_height() - .await - .to_string()) - } - - async fn handle_latest_epoch( - State(state): State>>, - ) -> Result { - Ok(state.finalizer_mailbox.get_latest_epoch().await.to_string()) - } - - async fn handle_get_validator_balance( - State(state): State>>, - Query(params): Query, - ) -> Result { - // Parse the public key from hex string - let key_bytes = - from_hex_formatted(¶ms.public_key).ok_or("Invalid hex format for public key")?; - let public_key = - PublicKey::decode(&*key_bytes).map_err(|_| "Unable to decode public key")?; - - let balance = state - .finalizer_mailbox - .get_validator_balance(public_key) - .await; - - match balance { - Some(balance) => Ok(balance.to_string()), - None => Err("Validator not found".to_string()), - } - } - - async fn handle_send_genesis( - State(state): State>, - body: String, - ) -> Result { - Self::handle_send_file(&state.genesis, body, "genesis") - } - - fn handle_send_file( - PathSender { path, sender }: &PathSender, - body: String, - kind: &'static str, - ) -> Result { - let path_buf = get_expanded_path(path).map_err(|_| format!("invalid {kind} path"))?; - - if let Some(parent) = path_buf.parent() { - std::fs::create_dir_all(parent) - .map_err(|e| format!("Failed to create directory: {e}"))?; - } - - std::fs::write(&path_buf, &body) - .map_err(|e| format!("Failed to write {kind} file: {e}"))?; - - // Signal that file is ready - if let Some(sender) = sender.lock().expect("poisoned").take() { - let _ = sender.send(()); - Ok(format!( - "{kind} file written at location {path} and node notified" - )) - } else { - Ok(format!( - "{kind} file written at location {path} (no notification needed)" - )) - } - } -} diff --git a/rpc/src/server.rs b/rpc/src/server.rs new file mode 100644 index 0000000..12780d8 --- /dev/null +++ b/rpc/src/server.rs @@ -0,0 +1,201 @@ +use crate::api::SummitApiServer; +use crate::error::RpcError; +use crate::types::{ + CheckpointInfoRes, CheckpointRes, DepositTransactionResponse, PublicKeysResponse, +}; +use alloy_primitives::{Address, U256, hex::FromHex as _}; +use async_trait::async_trait; +use commonware_codec::{DecodeExt as _, Encode as _}; +use commonware_consensus::Block as ConsensusBlock; +use commonware_consensus::simplex::signing_scheme::Scheme; +use commonware_cryptography::{Committable, Hasher as _, Sha256, Signer as _}; +use commonware_utils::from_hex_formatted; +use jsonrpsee::core::RpcResult; +use ssz::Encode as _; +use summit_finalizer::FinalizerMailbox; +use summit_types::{ + KeyPaths, PROTOCOL_VERSION, PublicKey, + execution_request::{DepositRequest, compute_deposit_data_root}, +}; + +pub struct SummitRpcServer { + key_store_path: String, + finalizer_mailbox: FinalizerMailbox, +} + +impl SummitRpcServer { + pub fn new(key_store_path: String, finalizer_mailbox: FinalizerMailbox) -> Self { + Self { + key_store_path, + finalizer_mailbox, + } + } +} + +#[async_trait] +impl SummitApiServer for SummitRpcServer +where + S: Scheme + Send + Sync + 'static, + B: ConsensusBlock + Committable + Send + Sync + 'static, +{ + async fn health(&self) -> RpcResult { + Ok("Ok".to_string()) + } + + async fn get_public_keys(&self) -> RpcResult { + let key_paths = KeyPaths::new(self.key_store_path.clone()); + + let node = key_paths.node_public_key().map_err(|e| { + RpcError::KeyStoreError(format!("Failed to read node public key: {}", e)) + })?; + + let consensus = key_paths.consensus_public_key().map_err(|e| { + RpcError::KeyStoreError(format!("Failed to read consensus public key: {}", e)) + })?; + + Ok(PublicKeysResponse { node, consensus }) + } + + async fn get_checkpoint(&self, epoch: u64) -> RpcResult { + let maybe_checkpoint = self.finalizer_mailbox.clone().get_checkpoint(epoch).await; + + let Some(checkpoint) = maybe_checkpoint else { + return Err(RpcError::CheckpointNotFound.into()); + }; + + Ok(CheckpointRes { + checkpoint: checkpoint.as_ssz_bytes(), + digest: checkpoint.digest.0, + epoch, + }) + } + + async fn get_latest_checkpoint(&self) -> RpcResult { + let maybe_checkpoint = self.finalizer_mailbox.clone().get_latest_checkpoint().await; + + let (Some(checkpoint), epoch) = maybe_checkpoint else { + return Err(RpcError::CheckpointNotFound.into()); + }; + + Ok(CheckpointRes { + checkpoint: checkpoint.as_ssz_bytes(), + digest: checkpoint.digest.0, + epoch, + }) + } + + async fn get_latest_checkpoint_info(&self) -> RpcResult { + let maybe_checkpoint = self.finalizer_mailbox.clone().get_latest_checkpoint().await; + + let (Some(checkpoint), epoch) = maybe_checkpoint else { + return Err(RpcError::CheckpointNotFound.into()); + }; + + Ok(CheckpointInfoRes { + epoch, + digest: checkpoint.digest.0, + }) + } + + async fn get_latest_height(&self) -> RpcResult { + let height = self.finalizer_mailbox.get_latest_height().await; + Ok(height) + } + + async fn get_latest_epoch(&self) -> RpcResult { + let epoch = self.finalizer_mailbox.get_latest_epoch().await; + Ok(epoch) + } + + async fn get_validator_balance(&self, public_key: String) -> RpcResult { + let key_bytes = from_hex_formatted(&public_key) + .ok_or_else(|| RpcError::InvalidPublicKey("Invalid hex format".to_string()))?; + + let public_key = PublicKey::decode(&*key_bytes) + .map_err(|_| RpcError::InvalidPublicKey("Unable to decode public key".to_string()))?; + + let balance = self + .finalizer_mailbox + .get_validator_balance(public_key) + .await; + + match balance { + Some(balance) => Ok(balance), + None => Err(RpcError::ValidatorNotFound.into()), + } + } + + async fn get_deposit_signature( + &self, + amount: u64, + address: String, + ) -> RpcResult { + let mut withdrawal_credentials = [0u8; 32]; + withdrawal_credentials[0] = 0x01; + + let withdrawal_address = Address::from_hex(address) + .map_err(|e| RpcError::InvalidPublicKey(format!("Invalid address: {}", e)))?; + withdrawal_credentials[12..32].copy_from_slice(withdrawal_address.as_slice()); + + let key_paths = KeyPaths::new(self.key_store_path.clone()); + + let consensus_priv_key = key_paths + .consensus_private_key() + .map_err(|e| RpcError::KeyStoreError(format!("Failed to read consensus key: {}", e)))?; + let consensus_pub = consensus_priv_key.public_key(); + + let node_priv_key = key_paths + .node_private_key() + .map_err(|e| RpcError::KeyStoreError(format!("Failed to read node key: {}", e)))?; + let node_pub = node_priv_key.public_key(); + + let req = DepositRequest { + node_pubkey: node_pub.clone(), + consensus_pubkey: consensus_pub.clone(), + withdrawal_credentials, + amount, + node_signature: [0; 64], + consensus_signature: [0; 96], + index: 0, + }; + + let protocol_version_digest = Sha256::hash(&PROTOCOL_VERSION.to_le_bytes()); + let message = req.as_message(protocol_version_digest); + + let node_signature = node_priv_key.sign(&[], &message); + let node_signature_bytes: [u8; 64] = node_signature + .as_ref() + .try_into() + .expect("ed25519 sig is always 64 bytes"); + + let consensus_signature = consensus_priv_key.sign(&[], &message); + let consensus_signature_slice: &[u8] = consensus_signature.as_ref(); + let consensus_signature_bytes: [u8; 96] = consensus_signature_slice + .try_into() + .expect("bls sig is always 96 bytes"); + + let node_pubkey_bytes: [u8; 32] = node_pub.to_vec().try_into().expect("Cannot fail"); + let consensus_pubkey_bytes: [u8; 48] = + consensus_pub.encode().as_ref()[..48].try_into().unwrap(); + + let deposit_amount = U256::from(amount) * U256::from(1_000_000_000u64); + + let deposit_root = compute_deposit_data_root( + &node_pubkey_bytes, + &consensus_pubkey_bytes, + &withdrawal_credentials, + deposit_amount, + &node_signature_bytes, + &consensus_signature_bytes, + ); + + Ok(DepositTransactionResponse { + node_pubkey: node_pubkey_bytes, + consensus_pubkey: consensus_pubkey_bytes.to_vec(), + withdrawal_credentials, + node_signature: node_signature_bytes.to_vec(), + consensus_signature: consensus_signature_bytes.to_vec(), + deposit_data_root: deposit_root, + }) + } +} diff --git a/rpc/src/types.rs b/rpc/src/types.rs new file mode 100644 index 0000000..eb16111 --- /dev/null +++ b/rpc/src/types.rs @@ -0,0 +1,19 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct PublicKeysResponse { + pub node: String, + pub consensus: String, +} + +#[derive(Serialize, Deserialize, Clone, Debug)] +pub struct DepositTransactionResponse { + pub node_pubkey: [u8; 32], + pub consensus_pubkey: Vec, + pub withdrawal_credentials: [u8; 32], + pub node_signature: Vec, + pub consensus_signature: Vec, + pub deposit_data_root: [u8; 32], +} + +pub use summit_types::rpc::{CheckpointInfoRes, CheckpointRes}; diff --git a/rpc/tests/integration_test.rs b/rpc/tests/integration_test.rs new file mode 100644 index 0000000..0525f5a --- /dev/null +++ b/rpc/tests/integration_test.rs @@ -0,0 +1,194 @@ +mod utils; + +use jsonrpsee::http_client::HttpClientBuilder; +use summit_rpc::{ + PathSender, start_rpc_server_for_genesis_with_handle, start_rpc_server_with_handle, +}; +use utils::{MockFinalizerState, create_test_finalizer_mailbox, create_test_keystore}; + +#[tokio::test] +async fn test_health_endpoint() { + use summit_rpc::SummitApiClient; + + let (mailbox, _finalizer_handle) = create_test_finalizer_mailbox(MockFinalizerState::default()); + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let (handle, addr) = start_rpc_server_with_handle(mailbox, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let response = client.health().await; + assert!(response.is_ok()); + assert_eq!(response.unwrap(), "Ok"); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_get_latest_height() { + use summit_rpc::SummitApiClient; + + let state = MockFinalizerState { + latest_height: 42, + ..Default::default() + }; + let (mailbox, _finalizer_handle) = create_test_finalizer_mailbox(state); + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let (handle, addr) = start_rpc_server_with_handle(mailbox, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let response = client.get_latest_height().await; + assert!(response.is_ok()); + assert_eq!(response.unwrap(), 42); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_get_latest_epoch() { + use summit_rpc::SummitApiClient; + + let state = MockFinalizerState { + latest_epoch: 10, + ..Default::default() + }; + let (mailbox, _finalizer_handle) = create_test_finalizer_mailbox(state); + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let (handle, addr) = start_rpc_server_with_handle(mailbox, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let response = client.get_latest_epoch().await; + assert!(response.is_ok()); + assert_eq!(response.unwrap(), 10); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_validator_balance_not_found() { + use summit_rpc::SummitApiClient; + + let (mailbox, _finalizer_handle) = create_test_finalizer_mailbox(MockFinalizerState::default()); + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let (handle, addr) = start_rpc_server_with_handle(mailbox, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let fake_pubkey = "0x1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef1234567890abcdef"; + let response = client.get_validator_balance(fake_pubkey.to_string()).await; + + assert!( + response.is_err(), + "Non-existent validator should return error" + ); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_get_public_keys() { + use summit_rpc::SummitGenesisApiClient; + + let (mailbox, _finalizer_handle) = create_test_finalizer_mailbox(MockFinalizerState::default()); + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let (handle, addr) = start_rpc_server_with_handle(mailbox, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let response = client.get_public_keys().await; + assert!(response.is_ok(), "getPublicKeys should succeed"); + + let keys = response.unwrap(); + assert!(!keys.node.is_empty(), "Node public key should not be empty"); + assert!( + !keys.consensus.is_empty(), + "Consensus public key should not be empty" + ); + + handle.stop().unwrap(); +} + +#[tokio::test] +async fn test_send_genesis() { + use summit_rpc::SummitGenesisApiClient; + + let temp_dir = create_test_keystore().unwrap(); + let key_store_path = temp_dir.path().to_str().unwrap().to_string(); + + let genesis_dir = tempfile::tempdir().unwrap(); + let genesis_path = genesis_dir.path().join("genesis.toml"); + let genesis_path_str = genesis_path.to_str().unwrap().to_string(); + + let path_sender = PathSender::new(genesis_path_str.clone(), None); + + let (handle, addr) = start_rpc_server_for_genesis_with_handle(path_sender, key_store_path, 0) + .await + .unwrap(); + + let url = format!("http://{}", addr); + let client = HttpClientBuilder::default().build(&url).unwrap(); + + let genesis_content = r#"eth_genesis_hash = "0x7a1a4b5e14b0e611bfe79f128bbcf2861dda517d7fc6f98c071c7e5cc349e0b8" +leader_timeout_ms = 2000 +notarization_timeout_ms = 4000 +nullify_timeout_ms = 4000 +activity_timeout_views = 256 +skip_timeout_views = 32 +max_message_size_bytes = 104857600 +namespace = "_SEISMIC_BFT" + +[[validators]] +node_public_key = "1be3cb06d7cc347602421fb73838534e4b54934e28959de98906d120d0799ef2" +consensus_public_key = "a6f61154ae7be4fd38cd43cf69adfd4896c57473cacb389702bb83f8adf923eecf4854c745e064c0a2db79db5674332b" +ip_address = "127.0.0.1:26600" +withdrawal_credentials = "0xf39Fd6e51aad88F6F4ce6aB8827279cffFb92266" + +[[validators]] +node_public_key = "32efa16e3cd62292db529e8f4babd27724b13b397edcf2b1dbe48f416ce40f0d" +consensus_public_key = "b82eaa7fbc7f9cf9d60826e5155ca8ccc46e13d87f64f7bcdcaa2972c370766b87635334bfc49b8fba7fb784e763d44e" +ip_address = "127.0.0.1:26610" +withdrawal_credentials = "0x70997970C51812dc3A010C7d01b50e0d17dc79C8" +"#; + let response = client.send_genesis(genesis_content.to_string()).await; + assert!(response.is_ok(), "sendGenesis should succeed"); + + let result = response.unwrap(); + assert!( + result.contains(&genesis_path_str), + "Response should contain the genesis path" + ); + + let written_content = std::fs::read_to_string(&genesis_path).unwrap(); + assert_eq!( + written_content, genesis_content, + "Written genesis content should match input" + ); + + handle.stop().unwrap(); +} diff --git a/rpc/tests/utils.rs b/rpc/tests/utils.rs new file mode 100644 index 0000000..b39d38b --- /dev/null +++ b/rpc/tests/utils.rs @@ -0,0 +1,103 @@ +use commonware_cryptography::{PrivateKeyExt, bls12381, ed25519}; +use futures::{StreamExt, channel::mpsc}; +use std::collections::HashMap; +use std::fs; +use summit_finalizer::{FinalizerMailbox, FinalizerMessage}; +use summit_types::account::ValidatorAccount; +use summit_types::{ + Block, + consensus_state_query::{ConsensusStateRequest, ConsensusStateResponse}, + scheme::MultisigScheme, +}; +use tokio::task::JoinHandle; + +// Use the default Block type parameters and the MultisigScheme with ed25519 + MinPk +pub type TestScheme = MultisigScheme; +pub type TestBlock = Block; + +/// Mock finalizer state that can be customized per test +#[derive(Clone, Debug)] +pub struct MockFinalizerState { + pub latest_height: u64, + pub latest_epoch: u64, + pub checkpoints: HashMap>, + pub latest_checkpoint: Option<(Option, u64)>, + pub validator_balances: HashMap>, + pub validator_accounts: HashMap>, +} + +impl Default for MockFinalizerState { + fn default() -> Self { + Self { + latest_height: 0, + latest_epoch: 0, + checkpoints: HashMap::new(), + latest_checkpoint: Some((None, 0)), + validator_balances: HashMap::new(), + validator_accounts: HashMap::new(), + } + } +} + +/// Creates a mock finalizer mailbox that responds to queries with test data +pub fn create_test_finalizer_mailbox( + state: MockFinalizerState, +) -> (FinalizerMailbox, JoinHandle<()>) { + let (tx, mut rx) = mpsc::channel::>(100); + + let handle = tokio::spawn(async move { + while let Some(msg) = rx.next().await { + match msg { + FinalizerMessage::QueryState { request, response } => match request { + ConsensusStateRequest::GetLatestHeight => { + let _ = response + .send(ConsensusStateResponse::LatestHeight(state.latest_height)); + } + ConsensusStateRequest::GetLatestEpoch => { + let _ = + response.send(ConsensusStateResponse::LatestEpoch(state.latest_epoch)); + } + ConsensusStateRequest::GetCheckpoint(epoch) => { + let checkpoint = state.checkpoints.get(&epoch).cloned().flatten(); + let _ = response.send(ConsensusStateResponse::Checkpoint(checkpoint)); + } + ConsensusStateRequest::GetLatestCheckpoint => { + let _ = response.send(ConsensusStateResponse::LatestCheckpoint( + state.latest_checkpoint.clone().unwrap_or((None, 0)), + )); + } + ConsensusStateRequest::GetValidatorBalance(public_key) => { + let balance = state.validator_balances.get(&public_key).cloned().flatten(); + let _ = response.send(ConsensusStateResponse::ValidatorBalance(balance)); + } + ConsensusStateRequest::GetValidatorAccount(public_key) => { + let account = state.validator_accounts.get(&public_key).cloned().flatten(); + let _ = response.send(ConsensusStateResponse::ValidatorAccount(account)); + } + }, + _ => {} + } + } + }); + + (FinalizerMailbox::new(tx), handle) +} + +/// Creates a temporary key store directory with test keys +pub fn create_test_keystore() -> anyhow::Result { + let temp_dir = tempfile::tempdir()?; + + // Generate ed25519 node key (deterministic for testing) + let node_private_key = ed25519::PrivateKey::from_seed(0); + let encoded_node_key = node_private_key.to_string(); + let node_key_path = temp_dir.path().join("node_key.pem"); + fs::write(node_key_path, encoded_node_key)?; + + // Generate BLS consensus key (deterministic for testing) + let consensus_private_key = bls12381::PrivateKey::from_seed(0); + let encoded_consensus_key = consensus_private_key.to_string(); + let consensus_key_path = temp_dir.path().join("consensus_key.pem"); + fs::write(consensus_key_path, encoded_consensus_key)?; + + Ok(temp_dir) +}