diff --git a/API.md b/API.md index 62f1a2dd4f2..bc418574ec0 100644 --- a/API.md +++ b/API.md @@ -9,6 +9,7 @@ The `axum` (HTTP/JSON) and `tonic` (gRPC) APIs are defined in `crates/espresso/a To add a new endpoint to the API, you must: - Define new protobuf serialization types in the `serialization-api` crate at `crates/serialization/api`, and ensure that the `prost`-generated rust types are built by `cargo build -p serialization-api`. +- Optionally, add example values for any fields in the `examples.toml` file. - Extend the `ApiSerialization` trait in the `serialization-api` crate to allow the implementation to declare the corresponding internal types, as well as any necessary conversions to or from these types. - Declare any desired gRPC endpoints in the protobuf files. - Define a trait interface (or extend one of the current API traits) in `espresso-api` for the application to implement, which performs the desired logic with our internal types (e.g. queries the database, reads consensus state). diff --git a/Cargo.lock b/Cargo.lock index 8d6650092be..e1e781dacec 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -221,7 +221,7 @@ dependencies = [ "alloy", "bincode", "ethers-core", - "rand 0.8.5", + "rand 0.8.6", "serde", ] @@ -244,7 +244,7 @@ dependencies = [ "either", "k256", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "secp256k1", "serde", "serde_json", @@ -359,13 +359,14 @@ dependencies = [ [[package]] name = "alloy-eip7928" -version = "0.3.3" +version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8222b1d88f9a6d03be84b0f5e76bb60cd83991b43ad8ab6477f0e4a7809b98d" +checksum = "407510740da514b694fecb44d8b3cebdc60d448f70cc5d24743e8ba273448a6e" dependencies = [ "alloy-primitives", "alloy-rlp", "borsh", + "once_cell", "serde", ] @@ -500,7 +501,7 @@ dependencies = [ "alloy-signer-local", "k256", "libc", - "rand 0.8.5", + "rand 0.8.6", "serde_json", "tempfile", "thiserror 2.0.18", @@ -569,7 +570,7 @@ dependencies = [ "lru 0.16.4", "parking_lot", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "thiserror 2.0.18", @@ -637,7 +638,7 @@ dependencies = [ "alloy-transport-ws", "futures", "pin-project", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde", "serde_json", "tokio", @@ -762,7 +763,7 @@ dependencies = [ "coins-bip32", "coins-bip39", "k256", - "rand 0.8.5", + "rand 0.8.6", "thiserror 2.0.18", "zeroize", ] @@ -872,7 +873,7 @@ dependencies = [ "alloy-json-rpc", "alloy-transport", "itertools 0.14.0", - "reqwest 0.13.2", + "reqwest 0.13.3", "serde_json", "tower 0.5.3", "tracing", @@ -889,7 +890,7 @@ dependencies = [ "alloy-transport", "futures", "http 1.4.0", - "rustls 0.23.38", + "rustls 0.23.39", "serde_json", "tokio", "tokio-tungstenite", @@ -1359,7 +1360,7 @@ dependencies = [ "fnv", "merlin", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rayon", ] @@ -1478,7 +1479,7 @@ dependencies = [ "directories 5.0.1", "fs2", "hex-literal", - "rand 0.8.5", + "rand 0.8.6", "sha2 0.10.9", "tracing", "tracing-subscriber 0.3.23", @@ -1492,7 +1493,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1df2c09229cbc5a028b1d70e00fdb2acee28b1055dfb5ca73eea49c5a25c4e7c" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -1502,7 +1503,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "94893f1e0c6eeab764ade8dc4c0db24caf4fe7cbbaafc0eba0a9030f447b5185" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rayon", ] @@ -1514,7 +1515,7 @@ checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" dependencies = [ "colored", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rayon", ] @@ -1653,9 +1654,9 @@ checksum = "4858a9d740c5007a9069007c3b4e91152d0506f13c1b31dd49051fd537656156" [[package]] name = "assert_cmd" -version = "2.2.0" +version = "2.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a686bbee5efb88a82df0621b236e74d925f470e5445d3220a5648b892ec99c9" +checksum = "39bae1d3fa576f7c6519514180a72559268dd7d1fe104070956cb687bc6673bd" dependencies = [ "anstyle", "bstr", @@ -2220,7 +2221,7 @@ checksum = "b62ddb9cb1ec0a098ad4bbf9344d0713fa193ae1a80af55febcff2627b6a00c1" dependencies = [ "getrandom 0.2.17", "instant", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -2467,9 +2468,9 @@ dependencies = [ [[package]] name = "blake3" -version = "1.8.4" +version = "1.8.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d2d5991425dfd0785aed03aedcf0b321d61975c9b5b3689c774a2610ae0b51e" +checksum = "0aa83c34e62843d924f905e0f5c866eb1dd6545fc4d719e803d9ba6030371fce" dependencies = [ "arrayref", "arrayvec", @@ -2624,7 +2625,7 @@ dependencies = [ "hotshot-state-prover", "hotshot-types", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", - "rand 0.8.5", + "rand 0.8.6", "surf-disco", "tempfile", "test-log", @@ -2754,9 +2755,9 @@ dependencies = [ [[package]] name = "cargo-platform" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "87a0c0e6148f11f01f32650a2ea02d532b2ad4e81d8bd41e6e565b5adc5e6082" +checksum = "dd0061da739915fae12ea00e16397555ed4371a6bb285431aab930f61b0aa4ba" dependencies = [ "serde", "serde_core", @@ -2802,9 +2803,9 @@ dependencies = [ [[package]] name = "cc" -version = "1.2.60" +version = "1.2.61" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "43c5703da9466b66a946814e1adf53ea2c90f10063b86290cc9eb67ce3478a20" +checksum = "d16d90359e986641506914ba71350897565610e87ce0ad9e6f28569db3dd5c6d" dependencies = [ "find-msvc-tools", "jobserver", @@ -2827,7 +2828,7 @@ dependencies = [ "parking_lot", "portpicker", "prometheus", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "tokio", "tracing", @@ -2844,7 +2845,7 @@ dependencies = [ "derive_more 1.0.0", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", "parking_lot", - "rand 0.8.5", + "rand 0.8.6", "tokio", "tracing", "tracing-subscriber 0.3.23", @@ -2882,11 +2883,11 @@ dependencies = [ "pem", "prometheus", "quinn", - "rand 0.8.5", + "rand 0.8.6", "rcgen", "redis", "rkyv", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "sqlx", "thiserror 1.0.69", @@ -2897,12 +2898,6 @@ dependencies = [ "warp 0.3.7", ] -[[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" @@ -3225,7 +3220,7 @@ dependencies = [ "hmac 0.12.1", "once_cell", "pbkdf2", - "rand 0.8.5", + "rand 0.8.6", "sha2 0.10.9", "thiserror 1.0.69", ] @@ -3454,11 +3449,12 @@ checksum = "413d67b29ef1021b4d60f4aa1e925ca031751e213832b4b1d588fae623c05c60" [[package]] name = "const_format" -version = "0.2.35" +version = "0.2.36" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7faa7469a93a566e9ccc1c73fe783b4a65c274c5ace346038dca9c39fe0030ad" +checksum = "4481a617ad9a412be3b97c5d403fef8ed023103368908b9c50af598ff467cc1e" dependencies = [ "const_format_proc_macros", + "konst", ] [[package]] @@ -3513,7 +3509,7 @@ dependencies = [ "hkdf 0.10.0", "hmac 0.10.1", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "sha2 0.9.9", "time 0.2.27", "version_check", @@ -3586,9 +3582,9 @@ checksum = "a62ec9ff5f7965e4d7280bd5482acd20aadb50d632cf6c1d74493856b011fa73" [[package]] name = "crc-catalog" -version = "2.4.0" +version = "2.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19d374276b40fb8bbdee95aef7c7fa6b5316ec764510eb64b8dd0e2ed0d7e7f5" +checksum = "217698eaf96b4a3f0bc4f3662aaa55bdf913cd54d7204591faa790070c6d0853" [[package]] name = "crc32fast" @@ -4022,15 +4018,15 @@ dependencies = [ [[package]] name = "data-encoding" -version = "2.10.0" +version = "2.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7a1e2f27636f116493b8b860f5546edb47c8d8f8ea73e1d2a20be88e28d1fea" +checksum = "a4ae5f15dda3c708c0ade84bfee31ccab44a3da4f88015ed22f63732abe300c8" [[package]] name = "data-encoding-macro" -version = "0.1.19" +version = "0.1.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8142a83c17aa9461d637e649271eae18bf2edd00e91f2e105df36c3c16355bdb" +checksum = "3259c913752a86488b501ed8680446a5ed2d5aeac6e596cb23ba3800768ea32c" dependencies = [ "data-encoding", "data-encoding-macro-internal", @@ -4038,12 +4034,12 @@ dependencies = [ [[package]] name = "data-encoding-macro-internal" -version = "0.1.17" +version = "0.1.18" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ab67060fc6b8ef687992d439ca0fa36e7ed17e9a0b16b25b601e8757df720de" +checksum = "ccc2776f0c61eca1ca32528f85548abd1a4be8fb53d1b21c013e4f18da1e7090" dependencies = [ "data-encoding", - "syn 1.0.109", + "syn 2.0.117", ] [[package]] @@ -4657,6 +4653,7 @@ dependencies = [ "anyhow", "async-trait", "axum 0.8.9", + "base64 0.22.1", "hex", "prost 0.14.3", "schemars 0.9.0", @@ -4689,7 +4686,7 @@ dependencies = [ "espresso-utils", "hotshot-contract-adapter", "hotshot-types", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "surf-disco", @@ -4745,7 +4742,7 @@ dependencies = [ "hotshot-types", "itertools 0.14.0", "jf-merkle-tree-compat", - "rand 0.8.5", + "rand 0.8.6", "rstest", "serde", "serde_json", @@ -4786,6 +4783,8 @@ dependencies = [ "async-lock 3.4.2", "async-once-cell", "async-trait", + "base64 0.22.1", + "base64-bytes", "bincode", "blake3", "byteorder", @@ -4837,7 +4836,7 @@ dependencies = [ "parking_lot", "pretty_assertions", "priority-queue", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rand_distr", "request-response", @@ -4955,7 +4954,7 @@ dependencies = [ "parking_lot", "paste", "pretty_assertions", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "rstest", "rstest_reuse", @@ -5083,7 +5082,7 @@ dependencies = [ "k256", "num_enum", "open-fastrlp", - "rand 0.8.5", + "rand 0.8.6", "rlp", "serde", "serde_json", @@ -5193,7 +5192,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "835c052cb0c08c1acf6ffd71c022172e18723949c8282f2b9f27efbc51e64534" dependencies = [ "byteorder", - "rand 0.8.5", + "rand 0.8.6", "rustc-hex", "static_assertions", ] @@ -5434,7 +5433,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a8f2f12607f92c69b12ed746fabf9ca4f5c482cba46679c1a75b874ed7c26adb" dependencies = [ "futures-io", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", ] @@ -5506,9 +5505,9 @@ dependencies = [ [[package]] name = "generic-array" -version = "1.3.5" +version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eaf57c49a95fd1fe24b90b3033bee6dc7e8f1288d51494cb44e627c295e38542" +checksum = "e38154b42567e31b925d3859382888aa6f86e446d1f018c89ef337fc1726bcf2" dependencies = [ "rustversion", "typenum", @@ -6037,7 +6036,7 @@ dependencies = [ "lru 0.12.5", "num_enum", "parking_lot", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "test-utils", @@ -6171,7 +6170,7 @@ dependencies = [ "jf-advz", "nonempty-collections", "quick_cache", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "surf-disco", @@ -6211,7 +6210,7 @@ dependencies = [ "jf-utils", "num-bigint", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_arrays", "serde_json", @@ -6233,7 +6232,7 @@ dependencies = [ "futures", "hotshot-example-types", "hotshot-types", - "rand 0.8.5", + "rand 0.8.6", "semver 1.0.28", "serde", "snafu 0.8.9", @@ -6264,7 +6263,7 @@ dependencies = [ "hotshot-types", "hotshot-utils", "jf-advz", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "serde", "sha2 0.10.9", @@ -6299,7 +6298,7 @@ dependencies = [ "hotshot-testing", "hotshot-types", "local-ip-address", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "serde", "sha2 0.10.9", @@ -6338,7 +6337,7 @@ dependencies = [ "libp2p-swarm-derive", "parking_lot", "pin-project", - "rand 0.8.5", + "rand 0.8.6", "serde", "tokio", "tracing", @@ -6374,7 +6373,7 @@ dependencies = [ "hotshot-testing", "hotshot-types", "hotshot-utils", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_bytes", "test-utils", @@ -6474,7 +6473,7 @@ dependencies = [ "log", "pretty_assertions", "prometheus", - "rand 0.8.5", + "rand 0.8.6", "refinery", "refinery-core", "reqwest 0.12.28", @@ -6614,7 +6613,7 @@ dependencies = [ "hotshot-utils", "jf-advz", "lru 0.12.5", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "surf-disco", @@ -6655,7 +6654,7 @@ dependencies = [ "itertools 0.14.0", "jf-advz", "lru 0.12.5", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "serde", "sha2 0.10.9", @@ -6700,7 +6699,7 @@ dependencies = [ "dyn-clone", "either", "futures", - "generic-array 1.3.5", + "generic-array 1.4.0", "hotshot-utils", "jf-advz", "jf-crhf", @@ -6714,7 +6713,7 @@ dependencies = [ "mnemonic", "multiaddr", "parking_lot", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "serde", "serde-inline-default", @@ -6864,9 +6863,9 @@ dependencies = [ [[package]] name = "hybrid-array" -version = "0.4.10" +version = "0.4.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3944cf8cf766b40e2a1a333ee5e9b563f854d5fa49d6a8ca2764e97c6eddb214" +checksum = "08d46837a0ed51fe95bd3b05de33cd64a1ee88fc797477ca48446872504507c5" dependencies = [ "typenum", ] @@ -6926,7 +6925,7 @@ dependencies = [ "http 1.4.0", "hyper 1.9.0", "hyper-util", - "rustls 0.23.38", + "rustls 0.23.39", "tokio", "tokio-rustls", "tower-service", @@ -7878,27 +7877,32 @@ dependencies = [ [[package]] name = "jni" -version = "0.21.1" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" +checksum = "5efd9a482cf3a427f00d6b35f14332adc7902ce91efb778580e180ff90fa3498" dependencies = [ - "cesu8", "cfg-if", "combine", - "jni-sys 0.3.1", + "jni-macros", + "jni-sys", "log", - "thiserror 1.0.69", + "simd_cesu8", + "thiserror 2.0.18", "walkdir", - "windows-sys 0.45.0", + "windows-link", ] [[package]] -name = "jni-sys" -version = "0.3.1" +name = "jni-macros" +version = "0.22.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41a652e1f9b6e0275df1f15b32661cf0d4b78d4d87ddec5e0c3c20f097433258" +checksum = "a00109accc170f0bdb141fed3e393c565b6f5e072365c3bd58f5b062591560a3" dependencies = [ - "jni-sys 0.4.1", + "proc-macro2", + "quote", + "rustc_version 0.4.1", + "simd_cesu8", + "syn 2.0.117", ] [[package]] @@ -7987,6 +7991,21 @@ dependencies = [ "sha3-asm", ] +[[package]] +name = "konst" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "128133ed7824fcd73d6e7b17957c5eb7bacb885649bd8c69708b2331a10bcefb" +dependencies = [ + "konst_macro_rules", +] + +[[package]] +name = "konst_macro_rules" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4933f3f57a8e9d9da04db23fb153356ecaf00cbd14aee46279c33dc80925c37" + [[package]] name = "kv-log-macro" version = "1.0.7" @@ -8013,9 +8032,9 @@ checksum = "09edd9e8b54e49e587e4f6295a7d29c3ea94d469cb40ab8ca70b288248a81db2" [[package]] name = "libc" -version = "0.2.185" +version = "0.2.186" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52ff2c0fe9bc6cb6b14a0592c2ff4fa9ceb83eea9db979b0487cd054946a2b8f" +checksum = "68ab91017fe16c622486840e4c83c9a37afeff978bd239b5293d61ece587de66" [[package]] name = "libloading" @@ -8105,7 +8124,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.5", + "rand 0.8.6", "rand_core 0.6.4", "thiserror 2.0.18", "tracing", @@ -8140,7 +8159,7 @@ dependencies = [ "parking_lot", "pin-project", "quick-protobuf", - "rand 0.8.5", + "rand 0.8.6", "rw-stream-sink", "thiserror 2.0.18", "tracing", @@ -8187,7 +8206,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.5", + "rand 0.8.6", "regex", "serde", "sha2 0.10.9", @@ -8229,7 +8248,7 @@ dependencies = [ "k256", "multihash", "quick-protobuf", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "thiserror 2.0.18", @@ -8255,7 +8274,7 @@ dependencies = [ "libp2p-swarm", "quick-protobuf", "quick-protobuf-codec", - "rand 0.8.5", + "rand 0.8.6", "serde", "sha2 0.10.9", "smallvec", @@ -8277,7 +8296,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "socket2 0.5.10", "tokio", @@ -8315,9 +8334,9 @@ dependencies = [ "libp2p-identity", "libp2p-tls", "quinn", - "rand 0.8.5", + "rand 0.8.6", "ring 0.17.14", - "rustls 0.23.38", + "rustls 0.23.39", "socket2 0.5.10", "thiserror 2.0.18", "tokio", @@ -8337,7 +8356,7 @@ dependencies = [ "libp2p-core", "libp2p-identity", "libp2p-swarm", - "rand 0.8.5", + "rand 0.8.6", "serde", "smallvec", "tracing", @@ -8358,7 +8377,7 @@ dependencies = [ "libp2p-identity", "libp2p-swarm-derive", "multistream-select", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "tokio", "tracing", @@ -8404,7 +8423,7 @@ dependencies = [ "libp2p-identity", "rcgen", "ring 0.17.14", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-webpki", "thiserror 2.0.18", "x509-parser 0.17.0", @@ -8499,7 +8518,7 @@ dependencies = [ "jf-merkle-tree-compat", "light-client", "pretty_assertions", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sqlx", @@ -8555,9 +8574,9 @@ checksum = "92daf443525c4cce67b150400bc2316076100ce0b3686209eb8cf3c31612e6f0" [[package]] name = "local-ip-address" -version = "0.6.11" +version = "0.6.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4a59a0cb1c7f84471ad5cd38d768c2a29390d17f1ff2827cdf49bc53e8ac70b" +checksum = "d7b0187df4e614e42405b49511b82ff7a1774fbd9a816060ee465067847cac22" dependencies = [ "libc", "neli", @@ -8883,11 +8902,10 @@ dependencies = [ [[package]] name = "multihash" -version = "0.19.4" +version = "0.19.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89ace881e3f514092ce9efbcb8f413d0ad9763860b828981c2de51ddc666936c" +checksum = "577c63b00ad74d57e8c9aa870b5fccebf2fd64a308a5aee9f1bb88e4aea19447" dependencies = [ - "no_std_io2", "serde", "unsigned-varint 0.8.0", ] @@ -9044,15 +9062,6 @@ dependencies = [ "libc", ] -[[package]] -name = "no_std_io2" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a3564ce7035b1e4778d8cb6cacebb5d766b5e8fe5a75b9e441e33fb61a872c6" -dependencies = [ - "memchr", -] - [[package]] name = "node-metrics" version = "0.1.0" @@ -9155,7 +9164,7 @@ dependencies = [ "num-integer", "num-iter", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "smallvec", "zeroize", ] @@ -9371,9 +9380,9 @@ dependencies = [ [[package]] name = "openssl" -version = "0.10.77" +version = "0.10.78" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfe4646e360ec77dff7dde40ed3d6c5fee52d156ef4a62f53973d38294dad87f" +checksum = "f38c4372413cdaaf3cc79dd92d29d7d9f5ab09b51b10dded508fb90bb70b9222" dependencies = [ "bitflags 2.11.1", "cfg-if", @@ -9418,9 +9427,9 @@ dependencies = [ [[package]] name = "openssl-sys" -version = "0.9.113" +version = "0.9.114" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad2f2c0eba47118757e4c6d2bff2838f3e0523380021356e7875e858372ce644" +checksum = "13ce1245cd07fcc4cfdb438f7507b0c7e4f3849a69fd84d52374c66d83741bb6" dependencies = [ "cc", "libc", @@ -9883,7 +9892,7 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "be97d76faf1bfab666e1375477b23fde79eccf0276e9b63b92a39d676a889ba9" dependencies = [ - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -10188,7 +10197,7 @@ version = "0.14.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "343d3bd7056eda839b03204e68deff7d1b13aba7af2b2fd16890697274262ee7" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "itertools 0.14.0", "log", "multimap", @@ -10351,7 +10360,7 @@ dependencies = [ "quinn-proto", "quinn-udp", "rustc-hash", - "rustls 0.23.38", + "rustls 0.23.39", "socket2 0.6.3", "thiserror 2.0.18", "tokio", @@ -10372,7 +10381,7 @@ dependencies = [ "rand 0.9.4", "ring 0.17.14", "rustc-hash", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "slab", "thiserror 2.0.18", @@ -10437,9 +10446,9 @@ dependencies = [ [[package]] name = "rand" -version = "0.8.5" +version = "0.8.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" +checksum = "5ca0ecfa931c29007047d1bc58e623ab12e5590e8c7cc53200d5202b69266d8a" dependencies = [ "libc", "rand_chacha 0.3.1", @@ -10540,7 +10549,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32cb0b9bc82b0a0876c2dd994a7e7a2683d3e7390ca40e6886785ef0c7e3ee31" dependencies = [ "num-traits", - "rand 0.8.5", + "rand 0.8.6", ] [[package]] @@ -10810,7 +10819,7 @@ dependencies = [ "derive_more 2.1.1", "hotshot-types", "parking_lot", - "rand 0.8.5", + "rand 0.8.6", "serde", "thiserror 2.0.18", "tokio", @@ -10843,7 +10852,7 @@ dependencies = [ "percent-encoding", "pin-project-lite 0.2.17", "quinn", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "serde", "serde_json", @@ -10864,9 +10873,9 @@ dependencies = [ [[package]] name = "reqwest" -version = "0.13.2" +version = "0.13.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab3f43e3283ab1488b624b44b0e988d0acea0b3214e694730a055cb6b2efa801" +checksum = "62e0021ea2c22aed41653bc7e1419abb2c97e038ff2c33d0e1309e49a97deec0" dependencies = [ "base64 0.22.1", "bytes 1.11.1", @@ -10882,7 +10891,7 @@ dependencies = [ "percent-encoding", "pin-project-lite 0.2.17", "quinn", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "rustls-platform-verifier", "serde", @@ -11089,7 +11098,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b3a8fb4672e840a587a66fc577a5491375df51ddb88f2a2c2a792598c326fe14" dependencies = [ "quote", - "rand 0.8.5", + "rand 0.8.6", "syn 2.0.117", ] @@ -11113,9 +11122,9 @@ dependencies = [ [[package]] name = "ruint" -version = "1.17.2" +version = "1.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c141e807189ad38a07276942c6623032d3753c8859c146104ac2e4d68865945a" +checksum = "0298da754d1395046b0afdc2f20ee76d29a8ae310cd30ffa84ed42acba9cb12a" dependencies = [ "alloy-rlp", "ark-ff 0.3.0", @@ -11130,7 +11139,7 @@ dependencies = [ "parity-scale-codec", "primitive-types 0.12.2", "proptest", - "rand 0.8.5", + "rand 0.8.6", "rand 0.9.4", "rlp", "ruint-macro", @@ -11175,7 +11184,7 @@ dependencies = [ "borsh", "bytes 1.11.1", "num-traits", - "rand 0.8.5", + "rand 0.8.6", "rkyv", "serde", "serde_json", @@ -11278,9 +11287,9 @@ dependencies = [ [[package]] name = "rustls" -version = "0.23.38" +version = "0.23.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "69f9466fb2c14ea04357e91413efb882e2a6d4a406e625449bc0a5d360d53a21" +checksum = "7c2c118cb077cca2822033836dfb1b975355dfb784b5e8da48f7b6c5db74e60e" dependencies = [ "aws-lc-rs", "log", @@ -11306,9 +11315,9 @@ dependencies = [ [[package]] name = "rustls-pki-types" -version = "1.14.0" +version = "1.14.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" +checksum = "30a7197ae7eb376e574fe940d068c30fe0462554a3ddbe4eca7838e049c937a9" dependencies = [ "web-time", "zeroize", @@ -11316,16 +11325,16 @@ dependencies = [ [[package]] name = "rustls-platform-verifier" -version = "0.6.2" +version = "0.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d99feebc72bae7ab76ba994bb5e121b8d83d910ca40b36e0921f53becc41784" +checksum = "26d1e2536ce4f35f4846aa13bff16bd0ff40157cdb14cc056c7b14ba41233ba0" dependencies = [ "core-foundation 0.10.1", "core-foundation-sys", "jni", "log", "once_cell", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-native-certs", "rustls-platform-verifier-android", "rustls-webpki", @@ -11518,7 +11527,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b50c5943d326858130af85e049f2661ba3c78b26589b8ab98e65e80ae44a1252" dependencies = [ "bitcoin_hashes", - "rand 0.8.5", + "rand 0.8.6", "secp256k1-sys", "serde", ] @@ -11826,6 +11835,7 @@ dependencies = [ "schemars 0.9.0", "serde", "serde_json", + "toml", ] [[package]] @@ -11904,9 +11914,9 @@ dependencies = [ [[package]] name = "sha3" -version = "0.10.8" +version = "0.10.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75872d278a8f37ef87fa0ddbda7802605cb18344497949862c0d4dcb291eba60" +checksum = "77fd7028345d415a4034cf8777cd4f8ab1851274233b45f84e3d955502d93874" dependencies = [ "digest 0.10.7", "keccak", @@ -11994,6 +12004,16 @@ version = "0.3.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "703d5c7ef118737c72f1af64ad2f6f8c5e1921f818cdcb97b8fe6fc69bf66214" +[[package]] +name = "simd_cesu8" +version = "1.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94f90157bb87cddf702797c5dadfa0be7d266cdf49e22da2fcaa32eff75b2c33" +dependencies = [ + "rustc_version 0.4.1", + "simdutf8", +] + [[package]] name = "simdutf8" version = "0.1.5" @@ -12042,7 +12062,7 @@ dependencies = [ "hotshot-types", "itertools 0.14.0", "jf-merkle-tree-compat", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "rstest", "serde", @@ -12140,7 +12160,7 @@ version = "0.8.9" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c1c97747dbf44bb1ca44a561ece23508e99cb592e862f22222dcf42f51d1e451" dependencies = [ - "heck 0.4.1", + "heck 0.5.0", "proc-macro2", "quote", "syn 2.0.117", @@ -12238,7 +12258,7 @@ dependencies = [ "digest 0.10.7", "hex", "keccak", - "rand 0.8.5", + "rand 0.8.6", "sha3", "thiserror 2.0.18", "zerocopy", @@ -12362,7 +12382,7 @@ dependencies = [ "memchr", "once_cell", "percent-encoding", - "rand 0.8.5", + "rand 0.8.6", "rsa", "serde", "sha1 0.10.6", @@ -12402,7 +12422,7 @@ dependencies = [ "md-5 0.10.6", "memchr", "once_cell", - "rand 0.8.5", + "rand 0.8.6", "serde", "serde_json", "sha2 0.10.9", @@ -12472,7 +12492,7 @@ dependencies = [ "predicates", "pretty_assertions", "prometheus-parse", - "rand 0.8.5", + "rand 0.8.6", "rand_chacha 0.3.1", "reqwest 0.12.28", "rstest", @@ -12878,9 +12898,9 @@ checksum = "8f50febec83f5ee1df3015341d8bd429f2d1cc62bcba7ea2076759d315084683" [[package]] name = "test-log" -version = "0.2.19" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37d53ac171c92a39e4769491c4b4dde7022c60042254b5fc044ae409d34a24d4" +checksum = "2f46bf474f0a4afebf92f076d54fd5e63423d9438b8c278a3d2ccb0f47f7cdb3" dependencies = [ "env_logger", "test-log-macros", @@ -12888,16 +12908,26 @@ dependencies = [ ] [[package]] -name = "test-log-macros" -version = "0.2.19" +name = "test-log-core" +version = "0.2.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be35209fd0781c5401458ab66e4f98accf63553e8fae7425503e92fdd319783b" +checksum = "37d4d41320b48bc4a211a9021678fcc0c99569b594ea31c93735b8e517102b4c" dependencies = [ "proc-macro2", "quote", "syn 2.0.117", ] +[[package]] +name = "test-log-macros" +version = "0.2.20" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9beb9249a81e430dffd42400a49019bcf548444f1968ff23080a625de0d4d320" +dependencies = [ + "syn 2.0.117", + "test-log-core", +] + [[package]] name = "test-utils" version = "0.1.0" @@ -12920,7 +12950,7 @@ dependencies = [ "hotshot-state-prover", "hotshot-types", "jf-signature 0.4.0 (git+https://github.com/EspressoSystems/jellyfish?tag=jf-signature-v0.4.0)", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "serde_json", "staking-cli", @@ -13051,7 +13081,7 @@ dependencies = [ "parking_lot", "pin-project", "prometheus", - "rand 0.8.5", + "rand 0.8.6", "reqwest 0.12.28", "routefinder", "semver 1.0.28", @@ -13212,9 +13242,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" [[package]] name = "tokio" -version = "1.52.0" +version = "1.52.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a91135f59b1cbf38c91e73cf3386fca9bb77915c45ce2771460c9d92f0f3d776" +checksum = "b67dee974fe86fd92cc45b7a95fdd2f99a36a6d7b0d431a231178d3d670bbcc6" dependencies = [ "bytes 1.11.1", "libc", @@ -13272,7 +13302,7 @@ dependencies = [ "socket2 0.6.3", "tokio", "tokio-util", - "whoami 2.1.1", + "whoami 2.1.2", ] [[package]] @@ -13281,7 +13311,7 @@ version = "0.26.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" dependencies = [ - "rustls 0.23.38", + "rustls 0.23.39", "tokio", ] @@ -13305,7 +13335,7 @@ checksum = "d25a406cddcc431a75d3d9afc6a7c0f7428d4891dd973e4d54c56b46127bf857" dependencies = [ "futures-util", "log", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "tokio", "tokio-rustls", @@ -13381,7 +13411,7 @@ dependencies = [ "indexmap 2.14.0", "toml_datetime 1.1.1+spec-1.1.0", "toml_parser", - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -13390,7 +13420,7 @@ version = "1.1.2+spec-1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2abe9b86193656635d2411dc43050282ca48aa31c2451210f4202550afb7526" dependencies = [ - "winnow 1.0.1", + "winnow 1.0.2", ] [[package]] @@ -13522,7 +13552,7 @@ dependencies = [ "indexmap 1.9.3", "pin-project", "pin-project-lite 0.2.17", - "rand 0.8.5", + "rand 0.8.6", "slab", "tokio", "tokio-util", @@ -13738,7 +13768,7 @@ dependencies = [ "input_buffer", "log", "native-tls", - "rand 0.8.5", + "rand 0.8.6", "sha-1", "thiserror 1.0.69", "url", @@ -13757,7 +13787,7 @@ dependencies = [ "httparse", "log", "rand 0.9.4", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "sha1 0.10.6", "thiserror 2.0.18", @@ -13772,9 +13802,9 @@ checksum = "bc7d623258602320d5c55d1bc22793b57daff0ec7efc270ea7d55ce1d5f5471c" [[package]] name = "typenum" -version = "1.19.0" +version = "1.20.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "562d481066bde0658276a35467c4af00bdc6ee726305698a55b86e61d7ad82bb" +checksum = "40ce102ab67701b8526c123c1bab5cbe42d7040ccfd0f64af1a385808d2f43de" [[package]] name = "ucd-trie" @@ -13911,7 +13941,7 @@ dependencies = [ "flate2", "log", "once_cell", - "rustls 0.23.38", + "rustls 0.23.39", "rustls-pki-types", "url", "webpki-roots 0.26.11", @@ -14103,7 +14133,7 @@ dependencies = [ "jf-poseidon2", "jf-utils", "p3-maybe-rayon", - "rand 0.8.5", + "rand 0.8.6", "reed-solomon-simd", "serde", "sha2 0.10.9", @@ -14224,11 +14254,11 @@ dependencies = [ [[package]] name = "wasip2" -version = "1.0.2+wasi-0.2.9" +version = "1.0.3+wasi-0.2.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" +checksum = "20064672db26d7cdc89c7798c48a0fdfac8213434a1186e5ef29fd560ae223d6" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.57.1", ] [[package]] @@ -14237,7 +14267,7 @@ version = "0.4.0+wasi-0.3.0-rc-2026-01-06" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5428f8bf88ea5ddc08faddef2ac4a67e390b88186c703ce6dbd955e1c145aca5" dependencies = [ - "wit-bindgen", + "wit-bindgen 0.51.0", ] [[package]] @@ -14447,9 +14477,9 @@ dependencies = [ [[package]] name = "whoami" -version = "2.1.1" +version = "2.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6a5b12f9df4f978d2cfdb1bd3bac52433f44393342d7ee9c25f5a1c14c0f45d" +checksum = "998767ef88740d1f5b0682a9c53c24431453923962269c2db68ee43788c5a40d" dependencies = [ "libc", "libredox", @@ -14607,15 +14637,6 @@ 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" @@ -14661,21 +14682,6 @@ dependencies = [ "windows-link", ] -[[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" @@ -14733,12 +14739,6 @@ 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" @@ -14757,12 +14757,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a9d8416fa8b42f5c947f8482c43e7d89e73a173cead56d044f6a56104a6d1b53" -[[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" @@ -14781,12 +14775,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b9d782e804c2f632e395708e99a94275910eb9100b2114651e04744e9b125006" -[[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" @@ -14817,12 +14805,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fa7359d10048f68ab8b09fa71c3daccfb0e9b559aed648a8f95469c27057180c" -[[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" @@ -14841,12 +14823,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e7ac75179f18232fe9c285163565a57ef8d3c89254a30685b57d83a38d326c2" -[[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" @@ -14865,12 +14841,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c3842cdd74a865a8066ab39c8a7a473c0778a3f29370b5fd6b4b9aa7df4a499" -[[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" @@ -14889,12 +14859,6 @@ version = "0.53.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0ffa179e2d07eee8ad8f57493436566c7cc30ac536a3379fdf008f47f6bb7ae1" -[[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" @@ -14924,9 +14888,9 @@ dependencies = [ [[package]] name = "winnow" -version = "1.0.1" +version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09dac053f1cd375980747450bfc7250c264eaae0583872e845c0c7cd578872b5" +checksum = "2ee1708bef14716a11bae175f579062d4554d95be2c6829f518df847b7b3fdd0" dependencies = [ "memchr", ] @@ -14940,6 +14904,12 @@ dependencies = [ "wit-bindgen-rust-macro", ] +[[package]] +name = "wit-bindgen" +version = "0.57.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1ebf944e87a7c253233ad6766e082e3cd714b5d03812acc24c318f549614536e" + [[package]] name = "wit-bindgen-core" version = "0.51.0" diff --git a/crates/espresso/api/Cargo.toml b/crates/espresso/api/Cargo.toml index 59fce9ae95f..4fcf5530ac6 100644 --- a/crates/espresso/api/Cargo.toml +++ b/crates/espresso/api/Cargo.toml @@ -8,6 +8,7 @@ aide = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } axum = { workspace = true } +base64 = { workspace = true } hex = { workspace = true } prost = { workspace = true } schemars = { workspace = true } diff --git a/crates/espresso/api/build.rs b/crates/espresso/api/build.rs index a45a7e71e50..844cb45f812 100644 --- a/crates/espresso/api/build.rs +++ b/crates/espresso/api/build.rs @@ -13,12 +13,19 @@ fn main() -> Result<(), Box> { .extern_path(".espresso.api.v2", "::serialization_api::v2") .file_descriptor_set_path(out_dir.join("reflection_descriptor.bin")) .compile_protos( - &["v2/common.proto", "v2/rewards.proto"], + &[ + "v2/common.proto", + "v2/rewards.proto", + "v2/data.proto", + "v2/consensus.proto", + ], &[proto_root.to_str().unwrap()], )?; println!("cargo:rerun-if-changed=../../serialization/api/proto/v2/common.proto"); println!("cargo:rerun-if-changed=../../serialization/api/proto/v2/rewards.proto"); + println!("cargo:rerun-if-changed=../../serialization/api/proto/v2/data.proto"); + println!("cargo:rerun-if-changed=../../serialization/api/proto/v2/consensus.proto"); Ok(()) } diff --git a/crates/espresso/api/examples/test_api.rs b/crates/espresso/api/examples/test_api.rs index ee5cf2a892e..7bb3e35e422 100644 --- a/crates/espresso/api/examples/test_api.rs +++ b/crates/espresso/api/examples/test_api.rs @@ -8,6 +8,7 @@ use anyhow::Result; use async_trait::async_trait; +use base64::{Engine, engine::general_purpose::STANDARD}; use espresso_api::{v1, v2}; use serde::Serialize; use serialization_api::ApiSerializations; @@ -120,6 +121,66 @@ impl v1::RewardApi for TestApi { } } +// Implement v1::AvailabilityApi with test data +#[async_trait] +impl v1::AvailabilityApi for TestApi { + type NamespaceProofQueryData = (Vec, Option>); // (transactions, proof) + type IncorrectEncodingProof = Vec; + type StateCertQueryDataV1 = Vec; + type StateCertQueryDataV2 = Vec; + + async fn get_namespace_proof( + &self, + block_id: v1::availability::BlockId, + namespace: u32, + ) -> Result> { + tracing::info!( + "v1: get_namespace_proof(block_id={:?}, namespace={})", + block_id, + namespace + ); + Ok(Some((vec![0xaa, 0xbb, 0xcc], Some(vec![0x11, 0x22, 0x33])))) + } + + async fn get_namespace_proof_range( + &self, + from: u64, + until: u64, + namespace: u32, + ) -> Result> { + tracing::info!( + "v1: get_namespace_proof_range(from={}, until={}, namespace={})", + from, + until, + namespace + ); + Ok(vec![(vec![0xaa, 0xbb], Some(vec![0x11, 0x22]))]) + } + + async fn get_incorrect_encoding_proof( + &self, + block_id: v1::availability::BlockId, + namespace: u32, + ) -> Result { + tracing::info!( + "v1: get_incorrect_encoding_proof(block_id={:?}, namespace={})", + block_id, + namespace + ); + Ok(vec![0xde, 0xad, 0xbe, 0xef]) + } + + async fn get_state_cert(&self, epoch: u64) -> Result { + tracing::info!("v1: get_state_cert(epoch={})", epoch); + Ok(vec![0x01, 0x02, 0x03]) + } + + async fn get_state_cert_v2(&self, epoch: u64) -> Result { + tracing::info!("v1: get_state_cert_v2(epoch={})", epoch); + Ok(vec![0x04, 0x05, 0x06]) + } +} + // Implement v2::RewardApi (simplified API - latest-only for claim/balance/proof) #[async_trait] impl v2::RewardApi for TestApi { @@ -158,6 +219,73 @@ impl v2::RewardApi for TestApi { } } +// Implement v2::DataApi with test data +#[async_trait] +impl v2::DataApi for TestApi { + async fn get_namespace_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> Result { + tracing::info!( + "v2: get_namespace_proof(namespace_id={}, block_height={})", + namespace_id, + block_height + ); + // Return (transactions as Vec>, optional proof) + Ok((vec![vec![0xaa, 0xbb, 0xcc]], Some(vec![0x11, 0x22, 0x33]))) + } + + async fn get_namespace_proof_range( + &self, + namespace_id: u64, + from: u64, + until: u64, + ) -> Result> { + tracing::info!( + "v2: get_namespace_proof_range(namespace_id={}, from={}, until={})", + namespace_id, + from, + until + ); + Ok(vec![ + (vec![vec![0xaa, 0xbb]], Some(vec![0x11, 0x22])), + (vec![vec![0xcc, 0xdd]], Some(vec![0x33, 0x44])), + ]) + } + + async fn get_incorrect_encoding_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> Result { + tracing::info!( + "v2: get_incorrect_encoding_proof(namespace_id={}, block_height={})", + namespace_id, + block_height + ); + Ok(vec![0xde, 0xad, 0xbe, 0xef]) + } +} + +// Implement v2::ConsensusApi with test data +#[async_trait] +impl v2::ConsensusApi for TestApi { + async fn get_state_certificate(&self, epoch: u64) -> Result { + tracing::info!("v2: get_state_certificate(epoch={})", epoch); + Ok(vec![0x01, 0x02, 0x03, 0x04]) + } + + async fn get_stake_table(&self, epoch: u64) -> Result { + tracing::info!("v2: get_stake_table(epoch={})", epoch); + // Return Vec> - each entry represents a peer + Ok(vec![ + vec![0x05, 0x06, 0x07, 0x08], + vec![0x09, 0x0a, 0x0b, 0x0c], + ]) + } +} + // Implement ApiSerializations for v2 proto type conversions impl ApiSerializations for TestApi { type Address = String; @@ -167,6 +295,19 @@ impl ApiSerializations for TestApi { type RewardBalances = (Vec<(u128, u128)>, u64); type RewardMerkleTreeData = Vec; + // Data API types + type NamespaceProof = (Vec>, Option>); // (transactions, proof) + type IncorrectEncodingProof = Vec; + + // Consensus API types + type StateCertificate = Vec; + type StakeTable = Vec>; + + // Helper conversion types (dummy types for test) + type PeerConfig = Vec; + type LightClientCert = Vec; + type NsProof = Vec; + fn deserialize_address(&self, s: &str) -> Result { // Simple validation: must start with 0x and be hex if s.starts_with("0x") && s.len() == 42 { @@ -252,6 +393,125 @@ impl ApiSerializations for TestApi { data: value.clone(), }) } + + // Data API serialization methods + + fn serialize_namespace_proof( + &self, + value: &Self::NamespaceProof, + ) -> Result { + let (transactions, proof_bytes) = value; + + // Convert transactions to Transaction messages + let txs = transactions + .iter() + .map(|tx| serialization_api::v2::Transaction { + namespace: 0, + payload: STANDARD.encode(tx), + }) + .collect(); + + // Convert proof bytes to NsProof if present + let proof = proof_bytes.as_ref().map(|bytes| { + // Create a dummy NsProof for testing + serialization_api::v2::NsProof { + proof_version: Some(serialization_api::v2::ns_proof::ProofVersion::V0( + serialization_api::v2::AdvzNsProof { + namespace_id: 0, + ns_payload: String::new(), + ns_proof: Some(STANDARD.encode(bytes)), + }, + )), + } + }); + + Ok(serialization_api::v2::NamespaceProofResponse { + transactions: txs, + proof, + }) + } + + fn serialize_incorrect_encoding_proof( + &self, + value: &Self::IncorrectEncodingProof, + ) -> Result { + Ok(serialization_api::v2::IncorrectEncodingProofResponse { + proof: Some(serialization_api::v2::AvidMIncorrectEncodingNsProof { + proof_data: STANDARD.encode(value), + }), + }) + } + + // Consensus API serialization methods + + fn serialize_state_certificate( + &self, + value: &Self::StateCertificate, + ) -> Result { + Ok(serialization_api::v2::StateCertificateResponse { + certificate: Some(serialization_api::v2::LightClientStateUpdateCertificateV2 { + epoch: 0, + light_client_state: String::new(), + next_stake_table_state: String::new(), + signatures: vec![], + auth_root: STANDARD.encode(value), + }), + }) + } + + fn serialize_stake_table( + &self, + value: &Self::StakeTable, + ) -> Result { + // Convert each entry to a PeerConfig + let peers = value + .iter() + .map(|peer_bytes| serialization_api::v2::PeerConfig { + stake_table_entry: Some(serialization_api::v2::StakeTableEntry { + stake_key: Some(serialization_api::v2::BlsPublicKey { + key: STANDARD.encode(peer_bytes), + }), + stake_amount: "1000000".to_string(), + }), + state_ver_key: Some(serialization_api::v2::SchnorrPublicKey { + key: STANDARD.encode(peer_bytes), + }), + connect_info: None, + }) + .collect(); + + Ok(serialization_api::v2::StakeTableResponse { peers }) + } + + fn serialize_peer_config( + &self, + _peer: &Self::PeerConfig, + ) -> Result { + Ok(serialization_api::v2::PeerConfig { + stake_table_entry: None, + state_ver_key: None, + connect_info: None, + }) + } + + fn serialize_light_client_cert( + &self, + _cert: &Self::LightClientCert, + ) -> Result { + Ok(serialization_api::v2::LightClientStateUpdateCertificateV2 { + epoch: 0, + light_client_state: String::new(), + next_stake_table_state: String::new(), + signatures: vec![], + auth_root: String::new(), + }) + } + + fn serialize_ns_proof(&self, _proof: &Self::NsProof) -> Result { + Ok(serialization_api::v2::NsProof { + proof_version: None, + }) + } } #[tokio::main] diff --git a/crates/espresso/api/src/axum.rs b/crates/espresso/api/src/axum.rs index e2195fdebfb..d7233dc4c40 100644 --- a/crates/espresso/api/src/axum.rs +++ b/crates/espresso/api/src/axum.rs @@ -8,21 +8,21 @@ use aide::{ operation::OperationOutput, redoc::Redoc, scalar::Scalar, - swagger::Swagger, }; use axum::{ Extension, Json, Router, extract::{Path, Request, State}, http::{StatusCode, Uri}, middleware::{self, Next}, - response::{IntoResponse, Response}, + response::{Html, IntoResponse, Response}, routing::get, }; use schemars::transform::Transform; use serde::Serialize; use serialization_api::v2::{ - GetRewardAccountProofRequest, GetRewardBalanceRequest, GetRewardBalancesRequest, - GetRewardClaimInputRequest, GetRewardMerkleTreeRequest, + GetIncorrectEncodingProofRequest, GetNamespaceProofRequest, GetRewardAccountProofRequest, + GetRewardBalanceRequest, GetRewardBalancesRequest, GetRewardClaimInputRequest, + GetRewardMerkleTreeRequest, GetStakeTableRequest, GetStateCertificateRequest, }; use crate::{error::ApiError, handlers, v1, v2}; @@ -57,6 +57,11 @@ async fn serve_openapi_spec(Extension(api): Extension) -> Json Json(api) } +/// Serve custom Swagger UI with collapsed defaults +async fn serve_swagger_ui() -> Html<&'static str> { + Html(include_str!("../templates/swagger.html")) +} + /// Middleware to rewrite root paths to /v2 paths /// /// Requests to `/rewards/...` get rewritten to `/v2/rewards/...` @@ -88,10 +93,52 @@ async fn redirect_to_docs() -> axum::response::Redirect { axum::response::Redirect::permanent("/v2") } +struct SendQuery(T); + +impl axum::extract::FromRequestParts for SendQuery +where + T: serde::de::DeserializeOwned + Send, + S: Send + Sync, +{ + type Rejection = axum::extract::rejection::QueryRejection; + + async fn from_request_parts( + parts: &mut axum::http::request::Parts, + state: &S, + ) -> Result { + axum::extract::Query::::from_request_parts(parts, state) + .await + .map(|axum::extract::Query(inner)| SendQuery(inner)) + } +} + +impl aide::operation::OperationInput for SendQuery { + fn operation_input( + ctx: &mut aide::generate::GenContext, + operation: &mut aide::openapi::Operation, + ) { + let schema = ctx.schema.subschema_for::(); + let params = aide::operation::parameters_from_schema( + ctx, + schema, + aide::operation::ParamLocation::Query, + ); + aide::operation::add_parameters(ctx, operation, params); + } +} + /// Create a combined router serving both v1 and v2 APIs pub fn create_combined_router(state: S) -> Router where - S: v1::RewardApi + v2::RewardApi + Clone + Send + Sync + 'static, + S: v1::RewardApi + + v1::AvailabilityApi + + v2::RewardApi + + v2::DataApi + + v2::ConsensusApi + + Clone + + Send + + Sync + + 'static, { let router_v1 = create_router_v1(state.clone()); let router_v2 = create_router_v2(state).layer(middleware::from_fn(rewrite_root_to_v2)); @@ -102,7 +149,7 @@ where /// Create v1 router without OpenAPI documentation (internal types) pub fn create_router_v1(state: S) -> Router where - S: v1::RewardApi + Clone + Send + Sync + 'static, + S: v1::RewardApi + v1::AvailabilityApi + Clone + Send + Sync + 'static, { // Create handler closures that capture the generic state type let get_reward_claim_input = @@ -165,6 +212,78 @@ where .map_err(ApiError::Internal) }; + // Availability API handlers + // Route: /v1/availability/block/{height}/namespace/{namespace} + let get_namespace_proof_by_height = + |State(state): State, Path((height, namespace)): Path<(u64, u32)>| async move { + state + .get_namespace_proof(v1::availability::BlockId::Height(height), namespace) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + // Route: /v1/availability/block/hash/{hash}/namespace/{namespace} + let get_namespace_proof_by_hash = + |State(state): State, Path((hash, namespace)): Path<(String, u32)>| async move { + state + .get_namespace_proof(v1::availability::BlockId::Hash(hash), namespace) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + // Route: /v1/availability/block/payload-hash/{payload-hash}/namespace/{namespace} + let get_namespace_proof_by_payload_hash = + |State(state): State, Path((payload_hash, namespace)): Path<(String, u32)>| async move { + state + .get_namespace_proof( + v1::availability::BlockId::PayloadHash(payload_hash), + namespace, + ) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + // Route: /v1/availability/block/{from}/{until}/namespace/{namespace} + let get_namespace_proof_range = + |State(state): State, Path((from, until, namespace)): Path<(u64, u64, u32)>| async move { + state + .get_namespace_proof_range(from, until, namespace) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + let get_incorrect_encoding_proof = + |State(state): State, Path((block_number, namespace)): Path<(u64, u32)>| async move { + state + .get_incorrect_encoding_proof( + v1::availability::BlockId::Height(block_number), + namespace, + ) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + let get_state_cert_v1 = |State(state): State, Path(epoch): Path| async move { + state + .get_state_cert(epoch) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + + let get_state_cert_v2 = |State(state): State, Path(epoch): Path| async move { + state + .get_state_cert_v2(epoch) + .await + .map(Json) + .map_err(ApiError::Internal) + }; + // Build plain Axum router without OpenAPI (for v1 - internal types) Router::new() .route( @@ -189,13 +308,36 @@ where routes::v1::REWARD_MERKLE_TREE_V2_ROUTE, get(get_reward_merkle_tree_v2), ) + // Availability API routes + .route( + routes::v1::NAMESPACE_PROOF_BY_HEIGHT_ROUTE, + get(get_namespace_proof_by_height), + ) + .route( + routes::v1::NAMESPACE_PROOF_BY_HASH_ROUTE, + get(get_namespace_proof_by_hash), + ) + .route( + routes::v1::NAMESPACE_PROOF_BY_PAYLOAD_HASH_ROUTE, + get(get_namespace_proof_by_payload_hash), + ) + .route( + routes::v1::NAMESPACE_PROOF_RANGE_ROUTE, + get(get_namespace_proof_range), + ) + .route( + routes::v1::INCORRECT_ENCODING_PROOF_ROUTE, + get(get_incorrect_encoding_proof), + ) + .route(routes::v1::STATE_CERT_V1_ROUTE, get(get_state_cert_v1)) + .route(routes::v1::STATE_CERT_V2_ROUTE, get(get_state_cert_v2)) .with_state(state) } /// Create v2 router with OpenAPI documentation (proto types) pub fn create_router_v2(state: S) -> Router where - S: v2::RewardApi + Clone + Send + Sync + 'static, + S: v2::RewardApi + v2::DataApi + v2::ConsensusApi + Clone + Send + Sync + 'static, { let mut api = OpenApi { info: Info { @@ -207,89 +349,129 @@ where ..Default::default() }; - // Handler closures: use proto request types directly with Path - let get_reward_claim_input = - |State(state): State, Path(request): Path| async move { + |State(state): State, SendQuery(request): SendQuery| async move { handlers::get_reward_claim_input(&state, request) .await .map(Json) }; let get_reward_balance = - |State(state): State, Path(request): Path| async move { + |State(state): State, SendQuery(request): SendQuery| async move { handlers::get_reward_balance(&state, request) .await .map(Json) }; let get_reward_account_proof = - |State(state): State, Path(request): Path| async move { + |State(state): State, SendQuery(request): SendQuery| async move { handlers::get_reward_account_proof(&state, request) .await .map(Json) }; let get_reward_balances = - |State(state): State, Path(request): Path| async move { + |State(state): State, SendQuery(request): SendQuery| async move { handlers::get_reward_balances(&state, request) .await .map(Json) }; let get_reward_merkle_tree_v2 = - |State(state): State, Path(request): Path| async move { + |State(state): State, SendQuery(request): SendQuery| async move { handlers::get_reward_merkle_tree_v2(&state, request) .await .map(Json) }; + let get_state_certificate = + |State(state): State, SendQuery(request): SendQuery| async move { + handlers::get_state_certificate(&state, request) + .await + .map(Json) + }; + + let get_stake_table = + |State(state): State, SendQuery(request): SendQuery| async move { + handlers::get_stake_table(&state, request).await.map(Json) + }; + + let get_namespace_proof = + |State(state): State, SendQuery(query): SendQuery| async move { + handlers::get_namespace_proof(&state, query).await.map(Json) + }; + + let get_incorrect_encoding_proof = |State(state): State, + SendQuery(query): SendQuery< + GetIncorrectEncodingProofRequest, + >| async move { + handlers::get_incorrect_encoding_proof(&state, query) + .await + .map(Json) + }; + let router = ApiRouter::new() .api_route( routes::v2::REWARD_CLAIM_INPUT_ROUTE.http, get_with(get_reward_claim_input, |op| { - op.description( - "Get reward claim input for L1 contract submission. Returns lifetime rewards \ - and Merkle proof needed to call claimRewards() on the L1 contract.", - ) - .tag("Rewards") + op.description(routes::v2::REWARD_CLAIM_INPUT_ROUTE.description) + .tag(routes::v2::REWARD_CLAIM_INPUT_ROUTE.tag) }), ) .api_route( routes::v2::REWARD_BALANCE_ROUTE.http, get_with(get_reward_balance, |op| { - op.description("Get reward balance for an address at the latest finalized height") - .tag("Rewards") + op.description(routes::v2::REWARD_BALANCE_ROUTE.description) + .tag(routes::v2::REWARD_BALANCE_ROUTE.tag) }), ) .api_route( routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.http, get_with(get_reward_account_proof, |op| { - op.description( - "Get Merkle proof for a reward account at the latest finalized height. \ - Returns V2 proof with Keccak256 hashing", - ) - .tag("Rewards") + op.description(routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.description) + .tag(routes::v2::REWARD_ACCOUNT_PROOF_ROUTE.tag) }), ) .api_route( routes::v2::REWARD_BALANCES_ROUTE.http, get_with(get_reward_balances, |op| { - op.description( - "Get paginated list of all reward balances at a specific height. Limit must \ - be ≤ 10000", - ) - .tag("Rewards") + op.description(routes::v2::REWARD_BALANCES_ROUTE.description) + .tag(routes::v2::REWARD_BALANCES_ROUTE.tag) }), ) .api_route( routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.http, get_with(get_reward_merkle_tree_v2, |op| { - op.description( - "Get raw RewardMerkleTreeV2 snapshot at a given height. Returns serialized \ - merkle tree data", - ) - .tag("Rewards") + op.description(routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.description) + .tag(routes::v2::REWARD_MERKLE_TREE_V2_ROUTE.tag) + }), + ) + .api_route( + routes::v2::NAMESPACE_PROOF_ROUTE.http, + get_with(get_namespace_proof, |op| { + op.description(routes::v2::NAMESPACE_PROOF_ROUTE.description) + .tag(routes::v2::NAMESPACE_PROOF_ROUTE.tag) + }), + ) + .api_route( + routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.http, + get_with(get_incorrect_encoding_proof, |op| { + op.description(routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.description) + .tag(routes::v2::INCORRECT_ENCODING_PROOF_ROUTE.tag) + }), + ) + .api_route( + routes::v2::STATE_CERTIFICATE_ROUTE.http, + get_with(get_state_certificate, |op| { + op.description(routes::v2::STATE_CERTIFICATE_ROUTE.description) + .tag(routes::v2::STATE_CERTIFICATE_ROUTE.tag) + }), + ) + .api_route( + routes::v2::STAKE_TABLE_ROUTE.http, + get_with(get_stake_table, |op| { + op.description(routes::v2::STAKE_TABLE_ROUTE.description) + .tag(routes::v2::STAKE_TABLE_ROUTE.tag) }), ) .finish_api(&mut api); @@ -347,18 +529,8 @@ where router .route(routes::v2::OPENAPI_SPEC_ROUTE, get(serve_openapi_spec)) - .route( - routes::v2::SWAGGER_ROUTE, - get(Swagger::new(routes::v2::OPENAPI_SPEC_ROUTE) - .with_title("Espresso Node API v2") - .axum_handler()), - ) - .route( - "/v2/", - get(Swagger::new(routes::v2::OPENAPI_SPEC_ROUTE) - .with_title("Espresso Node API v2") - .axum_handler()), - ) + .route(routes::v2::SWAGGER_ROUTE, get(serve_swagger_ui)) + .route("/v2/", get(serve_swagger_ui)) .route( routes::v2::SCALAR_ROUTE, get(Scalar::new(routes::v2::OPENAPI_SPEC_ROUTE) diff --git a/crates/espresso/api/src/axum/routes.rs b/crates/espresso/api/src/axum/routes.rs index 130ee936274..1a5394184c2 100644 --- a/crates/espresso/api/src/axum/routes.rs +++ b/crates/espresso/api/src/axum/routes.rs @@ -22,6 +22,10 @@ pub struct Route { pub http: &'static str, /// gRPC path for Tonic service (e.g., "/espresso.api.v2.RewardService/GetRewardBalance") pub grpc: &'static str, + /// OpenAPI description for the endpoint + pub description: &'static str, + /// OpenAPI tag grouping for the endpoint + pub tag: &'static str, } //============================================================================= @@ -61,6 +65,43 @@ pub mod v1 { /// Path: GET /v1/reward-state-v2/reward-merkle-tree-v2/{height} pub const REWARD_MERKLE_TREE_V2_ROUTE: &str = "/v1/reward-state-v2/reward-merkle-tree-v2/{height}"; + + //========================================================================= + // Availability API Routes + //========================================================================= + + /// Get namespace proof by block height + /// Path: GET /v1/availability/block/{height}/namespace/{namespace} + pub const NAMESPACE_PROOF_BY_HEIGHT_ROUTE: &str = + "/v1/availability/block/{height}/namespace/{namespace}"; + + /// Get namespace proof by block hash + /// Path: GET /v1/availability/block/hash/{hash}/namespace/{namespace} + pub const NAMESPACE_PROOF_BY_HASH_ROUTE: &str = + "/v1/availability/block/hash/{hash}/namespace/{namespace}"; + + /// Get namespace proof by payload hash + /// Path: GET /v1/availability/block/payload-hash/{payload-hash}/namespace/{namespace} + pub const NAMESPACE_PROOF_BY_PAYLOAD_HASH_ROUTE: &str = + "/v1/availability/block/payload-hash/{payload-hash}/namespace/{namespace}"; + + /// Get namespace proofs for a range of blocks + /// Path: GET /v1/availability/block/{from}/{until}/namespace/{namespace} + pub const NAMESPACE_PROOF_RANGE_ROUTE: &str = + "/v1/availability/block/{from}/{until}/namespace/{namespace}"; + + /// Generate proof of incorrect encoding + /// Path: GET /v1/availability/incorrect-encoding-proof/{block_number}/{namespace} + pub const INCORRECT_ENCODING_PROOF_ROUTE: &str = + "/v1/availability/incorrect-encoding-proof/{block_number}/{namespace}"; + + /// Get light client state certificate (V1) + /// Path: GET /v1/availability/state-cert/{epoch} + pub const STATE_CERT_V1_ROUTE: &str = "/v1/availability/state-cert/{epoch}"; + + /// Get light client state certificate (V2) + /// Path: GET /v1/availability/state-cert-v2/{epoch} + pub const STATE_CERT_V2_ROUTE: &str = "/v1/availability/state-cert-v2/{epoch}"; } //============================================================================= @@ -70,64 +111,81 @@ pub mod v1 { pub mod v2 { use super::Route; - /// Get reward claim input for L1 contract submission - /// - /// HTTP: GET /v2/rewards/claim-input/{address} - /// gRPC: /espresso.api.v2.RewardService/GetRewardClaimInput pub const REWARD_CLAIM_INPUT_ROUTE: Route = Route { - http: "/v2/rewards/claim-input/{address}", + http: "/v2/rewards/claim-input", grpc: "/espresso.api.v2.RewardService/GetRewardClaimInput", + description: "Get reward claim input for L1 contract submission. Returns lifetime rewards \ + and Merkle proof needed to call claimRewards() on the L1 contract.", + tag: "Rewards", }; - /// Get reward balance at the latest finalized height - /// - /// HTTP: GET /v2/rewards/balance/{address} - /// gRPC: /espresso.api.v2.RewardService/GetRewardBalance pub const REWARD_BALANCE_ROUTE: Route = Route { - http: "/v2/rewards/balance/{address}", + http: "/v2/rewards/balance", grpc: "/espresso.api.v2.RewardService/GetRewardBalance", + description: "Get reward balance for an address at the latest finalized height", + tag: "Rewards", }; - /// Get reward account Merkle proof at the latest finalized height - /// - /// HTTP: GET /v2/rewards/proof/{address} - /// gRPC: /espresso.api.v2.RewardService/GetRewardAccountProof pub const REWARD_ACCOUNT_PROOF_ROUTE: Route = Route { - http: "/v2/rewards/proof/{address}", + http: "/v2/rewards/proof", grpc: "/espresso.api.v2.RewardService/GetRewardAccountProof", + description: "Get Merkle proof for a reward account at the latest finalized height. \ + Returns V2 proof with Keccak256 hashing", + tag: "Rewards", }; - /// Get paginated list of reward balances - /// - /// HTTP: GET /v2/rewards/balances/{height}/{offset}/{limit} - /// gRPC: /espresso.api.v2.RewardService/GetRewardBalances pub const REWARD_BALANCES_ROUTE: Route = Route { - http: "/v2/rewards/balances/{height}/{offset}/{limit}", + http: "/v2/rewards/balances", grpc: "/espresso.api.v2.RewardService/GetRewardBalances", + description: "Get paginated list of all reward balances at a specific height. Limit must \ + be ≤ 10000", + tag: "Rewards", }; - /// Get raw RewardMerkleTreeV2 snapshot - /// - /// HTTP: GET /v2/rewards/tree/{height} - /// gRPC: /espresso.api.v2.RewardService/GetRewardMerkleTreeV2 pub const REWARD_MERKLE_TREE_V2_ROUTE: Route = Route { - http: "/v2/rewards/tree/{height}", + http: "/v2/rewards/tree", grpc: "/espresso.api.v2.RewardService/GetRewardMerkleTreeV2", + description: "Get raw RewardMerkleTreeV2 snapshot at a given height. Returns serialized \ + merkle tree data", + tag: "Rewards", }; - //========================================================================= - // Documentation Routes (V2 only) - //========================================================================= - - /// OpenAPI specification endpoint pub const OPENAPI_SPEC_ROUTE: &str = "/v2/docs/openapi.json"; - - /// Swagger documentation UI endpoint pub const SWAGGER_ROUTE: &str = "/v2"; - - /// Scalar documentation UI endpoint pub const SCALAR_ROUTE: &str = "/v2/scalar"; - - /// Redoc documentation UI endpoint pub const REDOC_ROUTE: &str = "/v2/redoc"; + + pub const NAMESPACE_PROOF_ROUTE: Route = Route { + http: "/v2/data/finalized/namespace-proof", + grpc: "/espresso.api.v2.DataService/GetNamespaceProof", + description: "Get namespace proof(s) for the specified namespace. Use '?block={height}' \ + for a single block, or '?from={start}&to={end}' for a range. Returns \ + transactions for the namespace along with cryptographic proof(s) of \ + completeness.", + tag: "Data", + }; + + pub const INCORRECT_ENCODING_PROOF_ROUTE: Route = Route { + http: "/v2/data/finalized/incorrect-encoding-proof", + grpc: "/espresso.api.v2.DataService/GetIncorrectEncodingProof", + description: "Generate a fraud proof showing incorrect namespace encoding for a specific \ + block. Query param 'block' specifies the block height. Used to challenge \ + invalid block proposals.", + tag: "Data", + }; + + pub const STATE_CERTIFICATE_ROUTE: Route = Route { + http: "/v2/consensus/state-certificate", + grpc: "/espresso.api.v2.ConsensusService/GetStateCertificate", + description: "Get light client state update certificate for an epoch. Used to update L1 \ + contracts with new stake table information.", + tag: "Consensus", + }; + + pub const STAKE_TABLE_ROUTE: Route = Route { + http: "/v2/consensus/stake-table", + grpc: "/espresso.api.v2.ConsensusService/GetStakeTable", + description: "Get stake table for an epoch.", + tag: "Consensus", + }; } diff --git a/crates/espresso/api/src/espresso.api.v2.rs b/crates/espresso/api/src/espresso.api.v2.rs index 19bd6a1f385..ad8306f75fb 100644 --- a/crates/espresso/api/src/espresso.api.v2.rs +++ b/crates/espresso/api/src/espresso.api.v2.rs @@ -674,3 +674,811 @@ pub mod reward_service_server { const NAME: &'static str = SERVICE_NAME; } } +/// Generated client implementations. +pub mod data_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// gRPC service for data availability queries + #[derive(Debug, Clone)] + pub struct DataServiceClient { + inner: tonic::client::Grpc, + } + impl DataServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl DataServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> DataServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + DataServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Get namespace proof(s) - handles both single block and range queries + pub async fn get_namespace_proof( + &mut self, + request: impl tonic::IntoRequest< + ::serialization_api::v2::GetNamespaceProofRequest, + >, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::GetNamespaceProofResponse>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/espresso.api.v2.DataService/GetNamespaceProof", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("espresso.api.v2.DataService", "GetNamespaceProof"), + ); + self.inner.unary(req, path, codec).await + } + /// Get incorrect encoding proof for a block + pub async fn get_incorrect_encoding_proof( + &mut self, + request: impl tonic::IntoRequest< + ::serialization_api::v2::GetIncorrectEncodingProofRequest, + >, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::IncorrectEncodingProofResponse>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/espresso.api.v2.DataService/GetIncorrectEncodingProof", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "espresso.api.v2.DataService", + "GetIncorrectEncodingProof", + ), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod data_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with DataServiceServer. + #[async_trait] + pub trait DataService: std::marker::Send + std::marker::Sync + 'static { + /// Get namespace proof(s) - handles both single block and range queries + async fn get_namespace_proof( + &self, + request: tonic::Request<::serialization_api::v2::GetNamespaceProofRequest>, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::GetNamespaceProofResponse>, + tonic::Status, + >; + /// Get incorrect encoding proof for a block + async fn get_incorrect_encoding_proof( + &self, + request: tonic::Request< + ::serialization_api::v2::GetIncorrectEncodingProofRequest, + >, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::IncorrectEncodingProofResponse>, + tonic::Status, + >; + } + /// gRPC service for data availability queries + #[derive(Debug)] + pub struct DataServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl DataServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for DataServiceServer + where + T: DataService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/espresso.api.v2.DataService/GetNamespaceProof" => { + #[allow(non_camel_case_types)] + struct GetNamespaceProofSvc(pub Arc); + impl< + T: DataService, + > tonic::server::UnaryService< + ::serialization_api::v2::GetNamespaceProofRequest, + > for GetNamespaceProofSvc { + type Response = ::serialization_api::v2::GetNamespaceProofResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + ::serialization_api::v2::GetNamespaceProofRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_namespace_proof(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetNamespaceProofSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/espresso.api.v2.DataService/GetIncorrectEncodingProof" => { + #[allow(non_camel_case_types)] + struct GetIncorrectEncodingProofSvc(pub Arc); + impl< + T: DataService, + > tonic::server::UnaryService< + ::serialization_api::v2::GetIncorrectEncodingProofRequest, + > for GetIncorrectEncodingProofSvc { + type Response = ::serialization_api::v2::IncorrectEncodingProofResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + ::serialization_api::v2::GetIncorrectEncodingProofRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_incorrect_encoding_proof( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetIncorrectEncodingProofSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for DataServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "espresso.api.v2.DataService"; + impl tonic::server::NamedService for DataServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} +/// Generated client implementations. +pub mod consensus_service_client { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// gRPC service for consensus state queries + #[derive(Debug, Clone)] + pub struct ConsensusServiceClient { + inner: tonic::client::Grpc, + } + impl ConsensusServiceClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl ConsensusServiceClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + std::marker::Send + 'static, + ::Error: Into + std::marker::Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> ConsensusServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + std::marker::Send + std::marker::Sync, + { + ConsensusServiceClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// Get state certificate for an epoch + pub async fn get_state_certificate( + &mut self, + request: impl tonic::IntoRequest< + ::serialization_api::v2::GetStateCertificateRequest, + >, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::StateCertificateResponse>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/espresso.api.v2.ConsensusService/GetStateCertificate", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new( + "espresso.api.v2.ConsensusService", + "GetStateCertificate", + ), + ); + self.inner.unary(req, path, codec).await + } + /// Get stake table for an epoch + pub async fn get_stake_table( + &mut self, + request: impl tonic::IntoRequest< + ::serialization_api::v2::GetStakeTableRequest, + >, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::StakeTableResponse>, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::unknown( + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic_prost::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/espresso.api.v2.ConsensusService/GetStakeTable", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert( + GrpcMethod::new("espresso.api.v2.ConsensusService", "GetStakeTable"), + ); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod consensus_service_server { + #![allow( + unused_variables, + dead_code, + missing_docs, + clippy::wildcard_imports, + clippy::let_unit_value, + )] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with ConsensusServiceServer. + #[async_trait] + pub trait ConsensusService: std::marker::Send + std::marker::Sync + 'static { + /// Get state certificate for an epoch + async fn get_state_certificate( + &self, + request: tonic::Request<::serialization_api::v2::GetStateCertificateRequest>, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::StateCertificateResponse>, + tonic::Status, + >; + /// Get stake table for an epoch + async fn get_stake_table( + &self, + request: tonic::Request<::serialization_api::v2::GetStakeTableRequest>, + ) -> std::result::Result< + tonic::Response<::serialization_api::v2::StakeTableResponse>, + tonic::Status, + >; + } + /// gRPC service for consensus state queries + #[derive(Debug)] + pub struct ConsensusServiceServer { + inner: Arc, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + impl ConsensusServiceServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for ConsensusServiceServer + where + T: ConsensusService, + B: Body + std::marker::Send + 'static, + B::Error: Into + std::marker::Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + match req.uri().path() { + "/espresso.api.v2.ConsensusService/GetStateCertificate" => { + #[allow(non_camel_case_types)] + struct GetStateCertificateSvc(pub Arc); + impl< + T: ConsensusService, + > tonic::server::UnaryService< + ::serialization_api::v2::GetStateCertificateRequest, + > for GetStateCertificateSvc { + type Response = ::serialization_api::v2::StateCertificateResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + ::serialization_api::v2::GetStateCertificateRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_state_certificate( + &inner, + request, + ) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetStateCertificateSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + "/espresso.api.v2.ConsensusService/GetStakeTable" => { + #[allow(non_camel_case_types)] + struct GetStakeTableSvc(pub Arc); + impl< + T: ConsensusService, + > tonic::server::UnaryService< + ::serialization_api::v2::GetStakeTableRequest, + > for GetStakeTableSvc { + type Response = ::serialization_api::v2::StakeTableResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request< + ::serialization_api::v2::GetStakeTableRequest, + >, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::get_stake_table(&inner, request) + .await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let method = GetStakeTableSvc(inner); + let codec = tonic_prost::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + let mut response = http::Response::new( + tonic::body::Body::default(), + ); + let headers = response.headers_mut(); + headers + .insert( + tonic::Status::GRPC_STATUS, + (tonic::Code::Unimplemented as i32).into(), + ); + headers + .insert( + http::header::CONTENT_TYPE, + tonic::metadata::GRPC_CONTENT_TYPE, + ); + Ok(response) + }) + } + } + } + } + impl Clone for ConsensusServiceServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + /// Generated gRPC service name + pub const SERVICE_NAME: &str = "espresso.api.v2.ConsensusService"; + impl tonic::server::NamedService for ConsensusServiceServer { + const NAME: &'static str = SERVICE_NAME; + } +} diff --git a/crates/espresso/api/src/handlers.rs b/crates/espresso/api/src/handlers.rs index 1f98ba4580f..cb76adae247 100644 --- a/crates/espresso/api/src/handlers.rs +++ b/crates/espresso/api/src/handlers.rs @@ -1,9 +1,12 @@ -//! Shared handler functions for reward API endpoints, +//! Shared handler functions for API endpoints, //! used by both Axum and Tonic APIs. use serialization_api::v2::*; -use crate::{error::ApiError, v2::RewardApi}; +use crate::{ + error::ApiError, + v2::{ConsensusApi, DataApi, RewardApi}, +}; pub async fn get_reward_claim_input( state: &S, @@ -103,3 +106,108 @@ where .serialize_reward_merkle_tree_data(&result) .map_err(ApiError::Internal) } + +// Data API handlers + +pub async fn get_namespace_proof( + state: &S, + request: GetNamespaceProofRequest, +) -> Result +where + S: DataApi, +{ + use get_namespace_proof_response::Response; + + match (request.block, request.first, request.last) { + (Some(block), None, None) => { + let result = state + .get_namespace_proof(request.namespace_id, block) + .await + .map_err(ApiError::Internal)?; + + let serialized = state + .serialize_namespace_proof(&result) + .map_err(ApiError::Internal)?; + + Ok(GetNamespaceProofResponse { + response: Some(Response::Single(serialized)), + }) + }, + (None, Some(first), Some(last)) => { + // Range is inclusive on both ends (first and last) + // Internal API expects exclusive end, so add 1 + let proofs = state + .get_namespace_proof_range(request.namespace_id, first, last + 1) + .await + .map_err(ApiError::Internal)?; + + let serialized_proofs = proofs + .iter() + .map(|proof| state.serialize_namespace_proof(proof)) + .collect::, _>>() + .map_err(ApiError::Internal)?; + + Ok(GetNamespaceProofResponse { + response: Some(Response::Range(NamespaceProofRangeResponse { + proofs: serialized_proofs, + })), + }) + }, + _ => Err(ApiError::BadRequest(anyhow::anyhow!( + "Must specify either 'block' or both 'first' and 'last' query parameters" + ))), + } +} + +pub async fn get_incorrect_encoding_proof( + state: &S, + request: GetIncorrectEncodingProofRequest, +) -> Result +where + S: DataApi, +{ + let result = state + .get_incorrect_encoding_proof(request.namespace_id, request.block_height) + .await + .map_err(ApiError::Internal)?; + + state + .serialize_incorrect_encoding_proof(&result) + .map_err(ApiError::Internal) +} + +// Consensus API handlers + +pub async fn get_state_certificate( + state: &S, + request: GetStateCertificateRequest, +) -> Result +where + S: ConsensusApi, +{ + let result = state + .get_state_certificate(request.epoch) + .await + .map_err(ApiError::Internal)?; + + state + .serialize_state_certificate(&result) + .map_err(ApiError::Internal) +} + +pub async fn get_stake_table( + state: &S, + request: GetStakeTableRequest, +) -> Result +where + S: ConsensusApi, +{ + let result = state + .get_stake_table(request.epoch) + .await + .map_err(ApiError::Internal)?; + + state + .serialize_stake_table(&result) + .map_err(ApiError::Internal) +} diff --git a/crates/espresso/api/src/lib.rs b/crates/espresso/api/src/lib.rs index 5b409d171b3..b3201b92188 100644 --- a/crates/espresso/api/src/lib.rs +++ b/crates/espresso/api/src/lib.rs @@ -24,7 +24,15 @@ pub use self::{ /// This serves both APIs at /v1/* and /v2/* from a single state implementation. pub async fn serve_axum(port: u16, state: S) -> anyhow::Result<()> where - S: v1::RewardApi + v2::RewardApi + Clone + Send + Sync + 'static, + S: v1::RewardApi + + v1::AvailabilityApi + + v2::RewardApi + + v2::DataApi + + v2::ConsensusApi + + Clone + + Send + + Sync + + 'static, { tracing::info!("Starting Axum server on port {} with v1 and v2 APIs", port); diff --git a/crates/espresso/api/src/tonic.rs b/crates/espresso/api/src/tonic.rs index 4a4deda8b5e..35c4a2f571b 100644 --- a/crates/espresso/api/src/tonic.rs +++ b/crates/espresso/api/src/tonic.rs @@ -4,16 +4,23 @@ //! All business logic is in handlers, this just adapts to the tonic interface. use serialization_api::v2::{ + GetIncorrectEncodingProofRequest, GetNamespaceProofRequest, GetNamespaceProofResponse, GetRewardAccountProofRequest, GetRewardBalanceRequest, GetRewardBalancesRequest, - GetRewardClaimInputRequest, GetRewardMerkleTreeRequest, RewardAccountQueryDataV2, - RewardBalance, RewardBalances, RewardClaimInput, RewardMerkleTreeV2Data, + GetRewardClaimInputRequest, GetRewardMerkleTreeRequest, GetStakeTableRequest, + GetStateCertificateRequest, IncorrectEncodingProofResponse, RewardAccountQueryDataV2, + RewardBalance, RewardBalances, RewardClaimInput, RewardMerkleTreeV2Data, StakeTableResponse, + StateCertificateResponse, }; use tonic::{Request, Response, Status}; use crate::{ error::ApiError, handlers, - proto::reward_service_server::{RewardService, RewardServiceServer}, + proto::{ + consensus_service_server::{ConsensusService, ConsensusServiceServer}, + data_service_server::{DataService, DataServiceServer}, + reward_service_server::{RewardService, RewardServiceServer}, + }, v2, }; @@ -99,3 +106,97 @@ where { RewardServiceServer::new(RewardServiceImpl::new(state)) } + +/// gRPC data service implementation wrapping a DataApi implementation +pub struct DataServiceImpl { + state: S, +} + +impl DataServiceImpl { + #[allow(dead_code)] + pub fn new(state: S) -> Self { + Self { state } + } +} + +#[tonic::async_trait] +impl DataService for DataServiceImpl +where + S: v2::DataApi + Send + Sync + 'static, +{ + async fn get_namespace_proof( + &self, + request: Request, + ) -> Result, Status> { + handlers::get_namespace_proof(&self.state, request.into_inner()) + .await + .map(Response::new) + .map_err(map_error) + } + + async fn get_incorrect_encoding_proof( + &self, + request: Request, + ) -> Result, Status> { + handlers::get_incorrect_encoding_proof(&self.state, request.into_inner()) + .await + .map(Response::new) + .map_err(map_error) + } +} + +/// Create the data gRPC service +#[allow(dead_code)] +pub fn create_data_service(state: S) -> DataServiceServer> +where + S: v2::DataApi + Send + Sync + Clone + 'static, +{ + DataServiceServer::new(DataServiceImpl::new(state)) +} + +/// gRPC consensus service implementation wrapping a ConsensusApi implementation +pub struct ConsensusServiceImpl { + state: S, +} + +impl ConsensusServiceImpl { + #[allow(dead_code)] + pub fn new(state: S) -> Self { + Self { state } + } +} + +#[tonic::async_trait] +impl ConsensusService for ConsensusServiceImpl +where + S: v2::ConsensusApi + Send + Sync + 'static, +{ + async fn get_state_certificate( + &self, + request: Request, + ) -> Result, Status> { + handlers::get_state_certificate(&self.state, request.into_inner()) + .await + .map(Response::new) + .map_err(map_error) + } + + async fn get_stake_table( + &self, + request: Request, + ) -> Result, Status> { + handlers::get_stake_table(&self.state, request.into_inner()) + .await + .map(Response::new) + .map_err(map_error) + } +} + +/// Create the consensus gRPC service +#[allow(dead_code)] +pub fn create_consensus_service(state: S) -> ConsensusServiceServer> +where + S: v2::ConsensusApi + Send + Sync + Clone + 'static, +{ + ConsensusServiceServer::new(ConsensusServiceImpl::new(state)) +} diff --git a/crates/espresso/api/src/v1.rs b/crates/espresso/api/src/v1.rs index 66b0a905165..1d105389d63 100644 --- a/crates/espresso/api/src/v1.rs +++ b/crates/espresso/api/src/v1.rs @@ -1,8 +1,10 @@ -//! V1 API module - legacy reward state API +//! V1 API module - legacy APIs //! -//! Serves internal espresso-types at `/v1/reward-state-v2/*` for backward compatibility. +//! Serves internal espresso-types for backward compatibility. //! No OpenAPI documentation. +pub mod availability; pub mod reward_state_v2; +pub use availability::AvailabilityApi; pub use reward_state_v2::RewardApi; diff --git a/crates/espresso/api/src/v1/availability.rs b/crates/espresso/api/src/v1/availability.rs new file mode 100644 index 00000000000..c59de23812c --- /dev/null +++ b/crates/espresso/api/src/v1/availability.rs @@ -0,0 +1,52 @@ +use async_trait::async_trait; +use serde::Serialize; + +#[derive(Debug, Clone)] +pub enum BlockId { + Height(u64), + Hash(String), + PayloadHash(String), +} + +#[async_trait] +pub trait AvailabilityApi { + type NamespaceProofQueryData: Serialize + Send + Sync; + + type IncorrectEncodingProof: Serialize + Send + Sync; + + type StateCertQueryDataV1: Serialize + Send + Sync; + + type StateCertQueryDataV2: Serialize + Send + Sync; + + async fn get_namespace_proof( + &self, + block_id: BlockId, + namespace: u32, + ) -> anyhow::Result>; + + async fn get_namespace_proof_range( + &self, + from: u64, + until: u64, + namespace: u32, + ) -> anyhow::Result>; + + async fn stream_namespace_proofs( + &self, + start_height: u64, + namespace: u32, + ) -> anyhow::Result<()> { + let _ = (start_height, namespace); + anyhow::bail!("WebSocket streaming not yet implemented") + } + + async fn get_incorrect_encoding_proof( + &self, + block_id: BlockId, + namespace: u32, + ) -> anyhow::Result; + + async fn get_state_cert(&self, epoch: u64) -> anyhow::Result; + + async fn get_state_cert_v2(&self, epoch: u64) -> anyhow::Result; +} diff --git a/crates/espresso/api/src/v2.rs b/crates/espresso/api/src/v2.rs index 4e885e93984..60973ceca0a 100644 --- a/crates/espresso/api/src/v2.rs +++ b/crates/espresso/api/src/v2.rs @@ -1,7 +1,11 @@ -//! V2 API module - proto-based rewards API +//! V2 API module - proto-based APIs //! -//! Serves proto-generated types at `/v2/rewards/*` with OpenAPI documentation. +//! Serves proto-generated types with OpenAPI documentation and gRPC support. +pub mod consensus; +pub mod data; pub mod rewards; +pub use consensus::ConsensusApi; +pub use data::DataApi; pub use rewards::RewardApi; diff --git a/crates/espresso/api/src/v2/consensus.rs b/crates/espresso/api/src/v2/consensus.rs new file mode 100644 index 00000000000..4558a232ade --- /dev/null +++ b/crates/espresso/api/src/v2/consensus.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; +use serialization_api::ApiSerializations; + +#[async_trait] +pub trait ConsensusApi: ApiSerializations { + async fn get_state_certificate(&self, epoch: u64) -> anyhow::Result; + + async fn get_stake_table(&self, epoch: u64) -> anyhow::Result; +} diff --git a/crates/espresso/api/src/v2/data.rs b/crates/espresso/api/src/v2/data.rs new file mode 100644 index 00000000000..6c432a18c26 --- /dev/null +++ b/crates/espresso/api/src/v2/data.rs @@ -0,0 +1,24 @@ +use async_trait::async_trait; +use serialization_api::ApiSerializations; + +#[async_trait] +pub trait DataApi: ApiSerializations { + async fn get_namespace_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> anyhow::Result; + + async fn get_namespace_proof_range( + &self, + namespace_id: u64, + from: u64, + until: u64, + ) -> anyhow::Result>; + + async fn get_incorrect_encoding_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> anyhow::Result; +} diff --git a/crates/espresso/api/templates/swagger.html b/crates/espresso/api/templates/swagger.html index bded2a614a9..67157f88f30 100644 --- a/crates/espresso/api/templates/swagger.html +++ b/crates/espresso/api/templates/swagger.html @@ -12,8 +12,11 @@ diff --git a/crates/espresso/dev-node/Cargo.toml b/crates/espresso/dev-node/Cargo.toml index 24df381cefb..1325663b595 100644 --- a/crates/espresso/dev-node/Cargo.toml +++ b/crates/espresso/dev-node/Cargo.toml @@ -9,13 +9,13 @@ name = "espresso-dev-node" path = "src/main.rs" [dependencies] -espresso-node = { path = "../node", features = ["testing", "embedded-db"] } alloy = { workspace = true } anyhow = { workspace = true } async-trait = { workspace = true } clap = { workspace = true } espresso-contract-deployer = { workspace = true } +espresso-node = { path = "../node", features = ["testing", "embedded-db"] } espresso-types = { workspace = true } espresso-utils = { workspace = true } futures = { workspace = true } diff --git a/crates/espresso/node/Cargo.toml b/crates/espresso/node/Cargo.toml index 70df77f3454..933c0abff0c 100644 --- a/crates/espresso/node/Cargo.toml +++ b/crates/espresso/node/Cargo.toml @@ -35,6 +35,8 @@ async-channel = { workspace = true } async-lock = { workspace = true } async-once-cell = { workspace = true } async-trait = { workspace = true } +base64 = { workspace = true } +base64-bytes = { workspace = true } bincode = { workspace = true } blake3 = { workspace = true } byteorder = { workspace = true } @@ -124,18 +126,18 @@ vergen-gitcl = { workspace = true } [dev-dependencies] escargot = { workspace = true } espresso-macros = { workspace = true } + +# Enable "testing" feature when running tests +espresso-node = { path = ".", features = ["testing"] } hotshot-example-types = { workspace = true } hotshot-query-service = { workspace = true } hotshot-testing = { workspace = true } +light-client = { workspace = true, features = ["testing"] } pretty_assertions = { workspace = true } rand = { workspace = true } reqwest = { workspace = true } -test-log = { workspace = true } - -# Enable "testing" feature when running tests -espresso-node = { path = ".", features = ["testing"] } -light-client = { workspace = true, features = ["testing"] } tempfile = { workspace = true } +test-log = { workspace = true } [lints] workspace = true diff --git a/crates/espresso/node/src/api.rs b/crates/espresso/node/src/api.rs index 30322cd7d4e..fa765cf494f 100644 --- a/crates/espresso/node/src/api.rs +++ b/crates/espresso/node/src/api.rs @@ -7653,7 +7653,22 @@ mod test { } } else { // V2 case + + // Submit a transaction so we have a block with actual namespace data for + // availability parity tests. Both servers share the same SQL data source, so they + // must return identical responses. + let avail_ns = NamespaceId::from(42_u32); + let avail_tx = Transaction::new(avail_ns, vec![1, 2, 3]); + network + .server + .submit_transaction(avail_tx.clone()) + .await + .unwrap(); + let (avail_block, _) = wait_for_decide_on_handle(&mut events, &avail_tx).await; + wait_until_block_height(&client, "reward-state-v2/block-height", height).await; + // Wait for the availability query service to index avail_block. + wait_until_block_height(&client, "node/block-height", avail_block).await; network.stop_consensus().await; @@ -7749,6 +7764,60 @@ mod test { &format!("reward-state-v2/reward-merkle-tree-v2/{height}"), ) .await?; + + // Availability v1 parity: verify the axum v1 routes return the same JSON as tide. + + // Namespace proof by height + compare_endpoints( + &http, + api_port, + axum_port, + &format!("availability/block/{avail_block}/namespace/{avail_ns}"), + ) + .await?; + + // Namespace proof by block hash and payload hash + let avail_header: Header = client + .get(&format!("availability/header/{avail_block}")) + .send() + .await + .unwrap(); + compare_endpoints( + &http, + api_port, + axum_port, + &format!( + "availability/block/hash/{}/namespace/{avail_ns}", + avail_header.commit() + ), + ) + .await?; + compare_endpoints( + &http, + api_port, + axum_port, + &format!( + "availability/block/payload-hash/{}/namespace/{avail_ns}", + avail_header.payload_commitment() + ), + ) + .await?; + + // Namespace proof range + compare_endpoints( + &http, + api_port, + axum_port, + &format!( + "availability/block/{avail_block}/{}/namespace/{avail_ns}", + avail_block + 1 + ), + ) + .await?; + + // State certificate parity (epoch 1 is complete after 4 epochs) + compare_endpoints(&http, api_port, axum_port, "availability/state-cert/1").await?; + compare_endpoints(&http, api_port, axum_port, "availability/state-cert-v2/1").await?; } Ok(()) diff --git a/crates/espresso/node/src/api/data_source.rs b/crates/espresso/node/src/api/data_source.rs index d25611bffff..dbd393fc7b1 100644 --- a/crates/espresso/node/src/api/data_source.rs +++ b/crates/espresso/node/src/api/data_source.rs @@ -396,6 +396,169 @@ pub(crate) trait DatabaseMetadataSource { fn get_table_sizes(&self) -> impl Send + Future>>; } +// ============================================================================ +// Arc delegation implementations +// ============================================================================ +// These implementations allow Arc to implement the data source traits +// when T implements them, which is necessary for NodeApiStateImpl to work +// with Arc-wrapped data sources. + +use std::sync::Arc; + +#[async_trait] +impl StateCertDataSource for Arc +where + D: StateCertDataSource + Sync + Send, +{ + async fn get_state_cert_by_epoch( + &self, + epoch: u64, + ) -> anyhow::Result>> { + (*self).get_state_cert_by_epoch(epoch).await + } + + async fn insert_state_cert( + &self, + epoch: u64, + cert: LightClientStateUpdateCertificateV2, + ) -> anyhow::Result<()> { + (*self).insert_state_cert(epoch, cert).await + } +} + +impl RequestResponseDataSource for Arc +where + Types: NodeType, + D: RequestResponseDataSource + Send + Sync, +{ + async fn request_vid_shares( + &self, + block_number: u64, + vid_common_data: VidCommonQueryData, + timeout_duration: Duration, + ) -> BoxFuture<'static, anyhow::Result>> { + self.as_ref() + .request_vid_shares(block_number, vid_common_data, timeout_duration) + .await + } +} + +#[async_trait] +impl StateCertFetchingDataSource for Arc +where + Types: NodeType, + D: StateCertFetchingDataSource + Sync + Send, +{ + async fn request_state_cert( + &self, + epoch: u64, + timeout: Duration, + ) -> Result, StateCertFetchError> { + (*self).request_state_cert(epoch, timeout).await + } +} + +#[async_trait] +impl StakeTableDataSource for Arc +where + T: NodeType, + D: StakeTableDataSource + Sync + Send, +{ + fn get_stake_table( + &self, + epoch: Option, + ) -> impl Send + Future>>> { + let this = self.clone(); + async move { (*this).get_stake_table(epoch).await } + } + + fn get_stake_table_current( + &self, + ) -> impl Send + Future>> { + let this = self.clone(); + async move { (*this).get_stake_table_current().await } + } + + fn get_da_stake_table( + &self, + epoch: Option, + ) -> impl Send + Future>>> { + let this = self.clone(); + async move { (*this).get_da_stake_table(epoch).await } + } + + fn get_da_stake_table_current( + &self, + ) -> impl Send + Future>> { + let this = self.clone(); + async move { (*this).get_da_stake_table_current().await } + } + + fn get_validators( + &self, + epoch: EpochNumber, + ) -> impl Send + Future>>> + { + let this = self.clone(); + async move { (*this).get_validators(epoch).await } + } + + fn get_block_reward( + &self, + epoch: Option, + ) -> impl Send + Future>> { + let this = self.clone(); + async move { (*this).get_block_reward(epoch).await } + } + + fn current_proposal_participation( + &self, + ) -> impl Send + Future> { + let this = self.clone(); + async move { (*this).current_proposal_participation().await } + } + + fn proposal_participation( + &self, + epoch: EpochNumber, + ) -> impl Send + Future> { + let this = self.clone(); + async move { (*this).proposal_participation(epoch).await } + } + + fn current_vote_participation(&self) -> impl Send + Future> { + let this = self.clone(); + async move { (*this).current_vote_participation().await } + } + + fn vote_participation( + &self, + epoch: EpochNumber, + ) -> impl Send + Future> { + let this = self.clone(); + async move { (*this).vote_participation(epoch).await } + } + + fn get_all_validators( + &self, + epoch: EpochNumber, + offset: u64, + limit: u64, + ) -> impl Send + Future>>> { + let this = self.clone(); + async move { (*this).get_all_validators(epoch, offset, limit).await } + } + + fn stake_table_events( + &self, + from_l1_block: u64, + to_l1_block: u64, + ) -> impl Send + Future>> { + let this = self.clone(); + async move { (*this).stake_table_events(from_l1_block, to_l1_block).await } + } +} + /// Data source for pruning state: the oldest retained block and leaf. /// /// SQL backends return the actual oldest entry; the filesystem backend always returns `None` diff --git a/crates/espresso/node/src/api/endpoints.rs b/crates/espresso/node/src/api/endpoints.rs index 84c59d68a1e..1ca9c80b838 100644 --- a/crates/espresso/node/src/api/endpoints.rs +++ b/crates/espresso/node/src/api/endpoints.rs @@ -50,7 +50,7 @@ use super::data_source::{ }; use crate::{SeqTypes, SequencerApiVersion, SequencerPersistence, api::RewardMerkleTreeDataSource}; -mod availability; +pub(crate) mod availability; pub(super) use availability::*; pub(super) fn fee( diff --git a/crates/espresso/node/src/api/state.rs b/crates/espresso/node/src/api/state.rs index 5fbd7bc4588..394f699cd02 100644 --- a/crates/espresso/node/src/api/state.rs +++ b/crates/espresso/node/src/api/state.rs @@ -6,6 +6,7 @@ use alloy::primitives::U256; use async_trait::async_trait; use espresso_types::{ + NsProof, v0::sparse_mt::KeccakNode, v0_3::RewardAmount as InternalRewardAmount, v0_4::{ @@ -16,6 +17,7 @@ use espresso_types::{ v0_6::RewardClaimError, }; use hotshot_contract_adapter::reward::RewardClaimInput as InternalRewardClaimInput; +use hotshot_query_service::availability::AvailabilityDataSource; use jf_merkle_tree_compat::prelude::{ MerkleNode as InternalMerkleNode, MerkleProof as InternalMerkleProof, }; @@ -27,7 +29,10 @@ use serialization_api::v2::{ }; use tagged_base64::TaggedBase64; -use super::{RewardMerkleTreeDataSource, RewardMerkleTreeV2Data as InternalRewardTreeData}; +use super::{ + RewardMerkleTreeDataSource, RewardMerkleTreeV2Data as InternalRewardTreeData, + data_source::{StakeTableDataSource, StateCertDataSource, StateCertFetchingDataSource}, +}; /// Node API state implementation /// @@ -149,7 +154,8 @@ impl NodeApiStateImpl { impl serialization_api::ApiSerializations for NodeApiStateImpl where - D: RewardMerkleTreeDataSource, + D: std::ops::Deref + Send + Sync + 'static, + D::Target: RewardMerkleTreeDataSource + Send + Sync, { // Request types type Address = alloy::primitives::Address; @@ -161,7 +167,21 @@ where type RewardBalances = (Vec<(RewardAccountV2, InternalRewardAmount)>, u64); // (amounts, total) type RewardMerkleTreeData = InternalRewardTreeData; - // Deserialize proto/string types → internal types + // Data API types + type NamespaceProof = espresso_types::NamespaceProofQueryData; + type IncorrectEncodingProof = espresso_types::v0_3::AvidMIncorrectEncodingNsProof; + + // Consensus API types + type StateCertificate = espresso_types::StateCertQueryDataV2; + type StakeTable = Vec>; + + // Helper conversion types + type PeerConfig = hotshot_types::PeerConfig; + type LightClientCert = hotshot_types::simple_certificate::LightClientStateUpdateCertificateV2< + espresso_types::SeqTypes, + >; + type NsProof = espresso_types::NsProof; + fn deserialize_address(&self, s: &str) -> anyhow::Result { s.parse() .map_err(|_| anyhow::anyhow!("invalid ethereum address: {}", s)) @@ -238,6 +258,224 @@ where .map_err(|e| anyhow::anyhow!("failed to serialize RewardMerkleTreeV2Data: {}", e))?; Ok(RewardMerkleTreeV2Data { data: bytes }) } + + // Data API serialization methods + + fn serialize_namespace_proof( + &self, + value: &Self::NamespaceProof, + ) -> anyhow::Result { + // Serialize each transaction field explicitly using base64_bytes + let transactions: Vec = value + .transactions + .iter() + .map(|tx| -> anyhow::Result { + let mut payload_bytes = Vec::new(); + base64_bytes::serialize( + &tx.payload, + &mut serde_json::Serializer::new(&mut payload_bytes), + ) + .map_err(|e| anyhow::anyhow!("failed to serialize payload: {}", e))?; + // Convert to string and remove quotes added by JSON serializer + let payload_str = String::from_utf8(payload_bytes)? + .trim_matches('"') + .to_string(); + + Ok(v2::Transaction { + namespace: tx.namespace.0, + payload: payload_str, + }) + }) + .collect::>>()?; + + let proof = value + .proof + .as_ref() + .map(|p| self.serialize_ns_proof(p)) + .transpose()?; + + Ok(serialization_api::v2::NamespaceProofResponse { + transactions, + proof, + }) + } + + fn serialize_incorrect_encoding_proof( + &self, + value: &Self::IncorrectEncodingProof, + ) -> anyhow::Result { + // Serialize the VID proof to JSON string + let proof_data = serde_json::to_string(&value.0)?; + Ok(serialization_api::v2::IncorrectEncodingProofResponse { + proof: Some(v2::AvidMIncorrectEncodingNsProof { proof_data }), + }) + } + + // Consensus API serialization methods + + fn serialize_state_certificate( + &self, + value: &Self::StateCertificate, + ) -> anyhow::Result { + let certificate = self.serialize_light_client_cert(&value.0)?; + + Ok(serialization_api::v2::StateCertificateResponse { + certificate: Some(certificate), + }) + } + + fn serialize_stake_table( + &self, + value: &Self::StakeTable, + ) -> anyhow::Result { + let peers: Result, _> = value + .iter() + .map(|peer| self.serialize_peer_config(peer)) + .collect(); + + Ok(serialization_api::v2::StakeTableResponse { peers: peers? }) + } + + fn serialize_peer_config( + &self, + peer: &Self::PeerConfig, + ) -> anyhow::Result { + let stake_table_entry = v2::StakeTableEntry { + stake_key: Some(v2::BlsPublicKey { + key: peer.stake_table_entry.stake_key.to_string(), + }), + stake_amount: peer.stake_table_entry.stake_amount.to_string(), + }; + + let state_ver_key = v2::SchnorrPublicKey { + key: peer.state_ver_key.to_string(), + }; + + let connect_info = peer.connect_info.as_ref().map(|info| { + let p2p_addr = match &info.p2p_addr { + hotshot_types::addr::NetAddr::Inet(ip, port) => v2::NetAddr { + addr_type: Some(v2::net_addr::AddrType::Inet(v2::InetAddr { + host: match ip { + std::net::IpAddr::V4(_) => ip.to_string(), + std::net::IpAddr::V6(_) => format!("[{ip}]"), + }, + port: *port as u32, + })), + }, + hotshot_types::addr::NetAddr::Name(name, port) => v2::NetAddr { + addr_type: Some(v2::net_addr::AddrType::Name(v2::NameAddr { + name: name.to_string(), + port: *port as u32, + })), + }, + }; + + v2::PeerConnectInfo { + x25519_key: info.x25519_key.to_string(), + p2p_addr: Some(p2p_addr), + } + }); + + Ok(v2::PeerConfig { + stake_table_entry: Some(stake_table_entry), + state_ver_key: Some(state_ver_key), + connect_info, + }) + } + + fn serialize_light_client_cert( + &self, + cert: &Self::LightClientCert, + ) -> anyhow::Result { + let signatures: Result, anyhow::Error> = cert + .signatures + .iter() + .map( + |(key, lcv3_sig, lcv2_sig)| -> anyhow::Result { + Ok(v2::StateSignatureTuple { + state_signature_key: Some(v2::SchnorrPublicKey { + key: key.to_string(), + }), + lcv3_signature: lcv3_sig.to_string(), + lcv2_signature: lcv2_sig.to_string(), + }) + }, + ) + .collect(); + + Ok(v2::LightClientStateUpdateCertificateV2 { + epoch: cert.epoch.u64(), + light_client_state: cert.light_client_state.to_string(), + next_stake_table_state: cert.next_stake_table_state.to_string(), + signatures: signatures?, + auth_root: cert.auth_root.to_string(), + }) + } + + fn serialize_ns_proof( + &self, + proof: &Self::NsProof, + ) -> anyhow::Result { + use espresso_types::NsProof as InternalNsProof; + + let proof_version = match proof { + InternalNsProof::V0(advz_proof) => { + // Serialize the inner fields directly + let json = serde_json::json!({ + "ns_index": advz_proof.ns_index, + "ns_payload": advz_proof.ns_payload, + "ns_proof": advz_proof.ns_proof, + }); + v2::ns_proof::ProofVersion::V0(serde_json::from_value(json)?) + }, + InternalNsProof::V1(avidm_proof) => { + // Serialize ns_payload using base64_bytes + let mut ns_payload_bytes = Vec::new(); + base64_bytes::serialize( + &avidm_proof.0.ns_payload, + &mut serde_json::Serializer::new(&mut ns_payload_bytes), + ) + .map_err(|e| anyhow::anyhow!("failed to serialize ns_payload: {}", e))?; + let ns_payload_str = String::from_utf8(ns_payload_bytes)? + .trim_matches('"') + .to_string(); + + v2::ns_proof::ProofVersion::V1(v2::AvidMNsProof { + ns_index: avidm_proof.0.ns_index as u64, + ns_payload: ns_payload_str, + ns_proof: avidm_proof.0.ns_proof.to_string(), + }) + }, + InternalNsProof::V1IncorrectEncoding(incorrect_proof) => { + // Serialize the whole proof to JSON string + v2::ns_proof::ProofVersion::V1IncorrectEncoding(v2::AvidMIncorrectEncodingNsProof { + proof_data: serde_json::to_string(&incorrect_proof.0)?, + }) + }, + InternalNsProof::V2(gf2_proof) => { + // Serialize ns_payload using base64_bytes + let mut ns_payload_bytes = Vec::new(); + base64_bytes::serialize( + &gf2_proof.0.ns_payload, + &mut serde_json::Serializer::new(&mut ns_payload_bytes), + ) + .map_err(|e| anyhow::anyhow!("failed to serialize ns_payload: {}", e))?; + let ns_payload_str = String::from_utf8(ns_payload_bytes)? + .trim_matches('"') + .to_string(); + + v2::ns_proof::ProofVersion::V2(v2::AvidmGf2NsProof { + ns_index: gf2_proof.0.ns_index as u64, + ns_payload: ns_payload_str, + ns_proof: gf2_proof.0.ns_proof.to_string(), + }) + }, + }; + + Ok(v2::NsProof { + proof_version: Some(proof_version), + }) + } } // ============================================================================ @@ -247,7 +485,8 @@ where #[async_trait] impl espresso_api::v2::RewardApi for NodeApiStateImpl where - D: RewardMerkleTreeDataSource, + D: std::ops::Deref + Clone + Send + Sync + 'static, + D::Target: RewardMerkleTreeDataSource + Send + Sync, { async fn get_reward_claim_input( &self, @@ -594,3 +833,629 @@ where }) } } + +// ============================================================================ +// v2::DataApi implementation +// ============================================================================ + +#[async_trait] +impl espresso_api::v2::DataApi for NodeApiStateImpl +where + D: std::ops::Deref + Clone + Send + Sync + 'static, + D::Target: RewardMerkleTreeDataSource + + hotshot_query_service::availability::AvailabilityDataSource + + hotshot_query_service::node::NodeDataSource + + super::data_source::RequestResponseDataSource + + Sync + + Send, +{ + async fn get_namespace_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> anyhow::Result { + use espresso_types::NamespaceId; + use futures::join; + use hotshot_query_service::availability::BlockId; + + let ns_id = NamespaceId(namespace_id); + let block_id = BlockId::Number(block_height as usize); + + // Fetch block and VID common data concurrently + let ds = &*self.data_source; + let timeout = std::time::Duration::from_millis(500); + let (block_fetch, vid_fetch) = join!(ds.get_block(block_id), ds.get_vid_common(block_id)); + let (block_opt, vid_opt) = join!( + block_fetch.with_timeout(timeout), + vid_fetch.with_timeout(timeout) + ); + + let block = block_opt.ok_or_else(|| anyhow::anyhow!("block {} not found", block_height))?; + let vid_common = vid_opt.ok_or_else(|| { + anyhow::anyhow!("VID common data for block {} not found", block_height) + })?; + + // Generate namespace proof + let ns_table = block.payload().ns_table(); + let ns_index = ns_table.find_ns_id(&ns_id).ok_or_else(|| { + anyhow::anyhow!( + "namespace {} not present in block {}", + namespace_id, + block_height + ) + })?; + + let proof = + NsProof::new(block.payload(), &ns_index, vid_common.common()).ok_or_else(|| { + anyhow::anyhow!( + "failed to generate namespace proof for block {}", + block_height + ) + })?; + + let transactions = proof.export_all_txs(&ns_id); + + Ok(espresso_types::NamespaceProofQueryData { + transactions, + proof: Some(proof), + }) + } + + async fn get_namespace_proof_range( + &self, + namespace_id: u64, + from: u64, + until: u64, + ) -> anyhow::Result> { + use espresso_types::NamespaceId; + + let ns_id = NamespaceId(namespace_id); + + // Validate range + if until <= from { + return Err(anyhow::anyhow!( + "invalid range: until ({}) must be greater than from ({})", + until, + from + )); + } + + let range_size = until - from; + const MAX_RANGE: u64 = 100; // Match limit from availability.toml + if range_size > MAX_RANGE { + return Err(anyhow::anyhow!( + "range too large: {} blocks (max {})", + range_size, + MAX_RANGE + )); + } + + // Fetch blocks and VID common data for the range + use futures::{join, stream::StreamExt}; + + let (blocks_stream, vids_stream) = join!( + self.data_source + .get_block_range(from as usize..until as usize), + self.data_source + .get_vid_common_range(from as usize..until as usize) + ); + + let blocks: Vec<_> = blocks_stream + .then(|block| async move { block.resolve().await }) + .collect() + .await; + let vids: Vec<_> = vids_stream + .then(|vid| async move { vid.resolve().await }) + .collect() + .await; + + if blocks.len() != vids.len() { + return Err(anyhow::anyhow!( + "mismatch between blocks and VID common data" + )); + } + + // Generate proofs for each block + use espresso_types::NsProof; + let mut proofs = Vec::new(); + + for (block, vid) in blocks.into_iter().zip(vids) { + let ns_table = block.payload().ns_table(); + + // Check if namespace exists in this block + if let Some(ns_index) = ns_table.find_ns_id(&ns_id) { + if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) { + let transactions = proof.export_all_txs(&ns_id); + proofs.push(espresso_types::NamespaceProofQueryData { + transactions, + proof: Some(proof), + }); + } else { + // Failed to generate proof - return empty result for this block + proofs.push(espresso_types::NamespaceProofQueryData { + transactions: vec![], + proof: None, + }); + } + } else { + // Namespace not present in this block + proofs.push(espresso_types::NamespaceProofQueryData { + transactions: vec![], + proof: None, + }); + } + } + + Ok(proofs) + } + + async fn get_incorrect_encoding_proof( + &self, + namespace_id: u64, + block_height: u64, + ) -> anyhow::Result { + use std::time::Duration; + + use espresso_types::{NamespaceId, NsProof}; + use futures::join; + use hotshot_query_service::{availability::BlockId, node::NodeDataSource as _}; + use hotshot_types::{data::VidShare, vid::avidm::AvidMShare}; + + use super::data_source::RequestResponseDataSource as _; + + let ns_id = NamespaceId(namespace_id); + let block_id = BlockId::Number(block_height as usize); + + let ds = &*self.data_source; + let timeout = std::time::Duration::from_millis(500); + let (block_fetch, vid_fetch) = join!(ds.get_block(block_id), ds.get_vid_common(block_id)); + let (block, vid_common) = join!( + block_fetch.with_timeout(timeout), + vid_fetch.with_timeout(timeout) + ); + + let block = block.ok_or_else(|| anyhow::anyhow!("block {} not found", block_height))?; + let vid_common = vid_common.ok_or_else(|| { + anyhow::anyhow!("VID common data for block {} not found", block_height) + })?; + + let ns_table = block.payload().ns_table(); + let ns_index = ns_table.find_ns_id(&ns_id).ok_or_else(|| { + anyhow::anyhow!( + "namespace {} not present in block {}", + namespace_id, + block_height + ) + })?; + + if NsProof::new(block.payload(), &ns_index, vid_common.common()).is_some() { + return Err(anyhow::anyhow!( + "block {} was correctly encoded", + block_height + )); + } + + // Block has incorrect encoding — fetch VID shares to construct the proof. + let vid_shares_future = ds + .request_vid_shares(block_height, vid_common.clone(), Duration::from_secs(40)) + .await; + let mut vid_shares = vid_shares_future + .await + .map_err(|e| anyhow::anyhow!("failed to fetch VID shares: {e:#}"))?; + + if let Ok(local_share) = ds.vid_share(block_height as usize).await { + vid_shares.push(local_share); + } + + let avidm_shares: Vec = vid_shares + .into_iter() + .filter_map(|s| { + if let VidShare::V1(s) = s { + Some(s) + } else { + None + } + }) + .collect(); + + match NsProof::v1_1_new_with_incorrect_encoding( + &avidm_shares, + ns_table, + &ns_index, + &vid_common.payload_hash(), + vid_common.common(), + ) { + Some(NsProof::V1IncorrectEncoding(proof)) => Ok(proof), + _ => Err(anyhow::anyhow!( + "failed to generate incorrect encoding proof" + )), + } + } +} + +// ============================================================================ +// v2::ConsensusApi implementation +// ============================================================================ + +#[async_trait] +impl espresso_api::v2::ConsensusApi for NodeApiStateImpl +where + D: std::ops::Deref + Clone + Send + Sync + 'static, + D::Target: RewardMerkleTreeDataSource + + super::data_source::StateCertDataSource + + super::data_source::StateCertFetchingDataSource + + super::data_source::StakeTableDataSource + + Send + + Sync, +{ + async fn get_state_certificate(&self, epoch: u64) -> anyhow::Result { + use std::time::Duration; + + let ds = &*self.data_source; + + // Try to get from local storage first + let state_cert = ds.get_state_cert_by_epoch(epoch).await?; + + let cert = match state_cert { + Some(cert) => cert, + None => { + // Not found locally, try to fetch from peers + const TIMEOUT: Duration = Duration::from_secs(40); + let cert = ds.request_state_cert(epoch, TIMEOUT).await.map_err(|e| { + anyhow::anyhow!("failed to fetch state cert for epoch {}: {}", epoch, e) + })?; + + // Store the fetched certificate + ds.insert_state_cert(epoch, cert.clone()).await?; + + cert + }, + }; + + Ok(espresso_types::StateCertQueryDataV2(cert)) + } + + async fn get_stake_table(&self, epoch: u64) -> anyhow::Result { + use hotshot_types::data::EpochNumber; + + let ds = &*self.data_source; + ds.get_stake_table(Some(EpochNumber::new(epoch))).await + } +} + +// ============================================================================ +// v1::AvailabilityApi implementation +// ============================================================================ + +#[async_trait] +impl espresso_api::v1::AvailabilityApi for NodeApiStateImpl +where + D: std::ops::Deref + Clone + Send + Sync + 'static, + D::Target: RewardMerkleTreeDataSource + + hotshot_query_service::availability::AvailabilityDataSource + + hotshot_query_service::node::NodeDataSource + + super::data_source::RequestResponseDataSource + + super::data_source::StateCertDataSource + + super::data_source::StateCertFetchingDataSource + + Send + + Sync, +{ + type NamespaceProofQueryData = espresso_types::NamespaceProofQueryData; + type IncorrectEncodingProof = espresso_types::v0_3::AvidMIncorrectEncodingNsProof; + type StateCertQueryDataV1 = espresso_types::StateCertQueryDataV1; + type StateCertQueryDataV2 = espresso_types::StateCertQueryDataV2; + + async fn get_namespace_proof( + &self, + block_id: espresso_api::v1::availability::BlockId, + namespace: u32, + ) -> anyhow::Result> { + use espresso_types::NamespaceId; + use futures::join; + use hotshot_query_service::availability::BlockId as HsBlockId; + + let ns_id = NamespaceId::from(namespace); + + // Convert v1 BlockId to hotshot BlockId + let hs_block_id = match block_id { + espresso_api::v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize), + espresso_api::v1::availability::BlockId::Hash(h) => { + let hash = h + .parse() + .map_err(|_| anyhow::anyhow!("invalid block hash: {}", h))?; + HsBlockId::Hash(hash) + }, + espresso_api::v1::availability::BlockId::PayloadHash(h) => { + let payload_hash = h + .parse() + .map_err(|_| anyhow::anyhow!("invalid payload hash: {}", h))?; + HsBlockId::PayloadHash(payload_hash) + }, + }; + + // Fetch block and VID common data + let ds = &*self.data_source; + let timeout = std::time::Duration::from_millis(500); + let (block_fetch, vid_fetch) = + join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id)); + let (block, vid_common) = join!( + block_fetch.with_timeout(timeout), + vid_fetch.with_timeout(timeout) + ); + + let Some(block) = block else { + return Ok(None); + }; + let Some(vid_common) = vid_common else { + return Ok(None); + }; + + // Check if namespace is present + use espresso_types::NsProof; + let ns_table = block.payload().ns_table(); + let Some(ns_index) = ns_table.find_ns_id(&ns_id) else { + return Ok(None); + }; + + // Generate namespace proof + let Some(proof) = NsProof::new(block.payload(), &ns_index, vid_common.common()) else { + // Failed to generate proof - namespace exists but proof generation failed + return Ok(Some(espresso_types::NamespaceProofQueryData { + transactions: vec![], + proof: None, + })); + }; + + let transactions = proof.export_all_txs(&ns_id); + + Ok(Some(espresso_types::NamespaceProofQueryData { + transactions, + proof: Some(proof), + })) + } + + async fn get_namespace_proof_range( + &self, + from: u64, + until: u64, + namespace: u32, + ) -> anyhow::Result> { + use espresso_types::NamespaceId; + + let ns_id = NamespaceId::from(namespace); + + // Validate range + if until <= from { + return Err(anyhow::anyhow!( + "invalid range: until ({}) must be greater than from ({})", + until, + from + )); + } + + let range_size = until - from; + const MAX_RANGE: u64 = 100; + if range_size > MAX_RANGE { + return Err(anyhow::anyhow!( + "range too large: {} blocks (max {})", + range_size, + MAX_RANGE + )); + } + + // Fetch blocks and VID common data for the range + use futures::{join, stream::StreamExt}; + + let (blocks_stream, vids_stream) = join!( + self.data_source + .get_block_range(from as usize..until as usize), + self.data_source + .get_vid_common_range(from as usize..until as usize) + ); + + let blocks: Vec<_> = blocks_stream + .then(|block| async move { block.resolve().await }) + .collect() + .await; + let vids: Vec<_> = vids_stream + .then(|vid| async move { vid.resolve().await }) + .collect() + .await; + + if blocks.len() != vids.len() { + return Err(anyhow::anyhow!( + "mismatch between blocks and VID common data" + )); + } + + // Generate proofs for each block + use espresso_types::NsProof; + let mut proofs = Vec::new(); + + for (block, vid) in blocks.into_iter().zip(vids) { + let ns_table = block.payload().ns_table(); + + // Check if namespace exists in this block + if let Some(ns_index) = ns_table.find_ns_id(&ns_id) { + if let Some(proof) = NsProof::new(block.payload(), &ns_index, vid.common()) { + let transactions = proof.export_all_txs(&ns_id); + proofs.push(espresso_types::NamespaceProofQueryData { + transactions, + proof: Some(proof), + }); + } else { + // Failed to generate proof - return empty result for this block + proofs.push(espresso_types::NamespaceProofQueryData { + transactions: vec![], + proof: None, + }); + } + } else { + // Namespace not present in this block + proofs.push(espresso_types::NamespaceProofQueryData { + transactions: vec![], + proof: None, + }); + } + } + + Ok(proofs) + } + + async fn get_incorrect_encoding_proof( + &self, + block_id: espresso_api::v1::availability::BlockId, + namespace: u32, + ) -> anyhow::Result { + use std::time::Duration; + + use espresso_types::{NamespaceId, NsProof}; + use futures::join; + use hotshot_query_service::{ + availability::BlockId as HsBlockId, node::NodeDataSource as _, + types::HeightIndexed as _, + }; + use hotshot_types::{data::VidShare, vid::avidm::AvidMShare}; + + use super::data_source::RequestResponseDataSource as _; + + let ns_id = NamespaceId::from(namespace); + + let hs_block_id = match block_id { + espresso_api::v1::availability::BlockId::Height(h) => HsBlockId::Number(h as usize), + espresso_api::v1::availability::BlockId::Hash(h) => { + let hash = h + .parse() + .map_err(|_| anyhow::anyhow!("invalid block hash: {}", h))?; + HsBlockId::Hash(hash) + }, + espresso_api::v1::availability::BlockId::PayloadHash(h) => { + let payload_hash = h + .parse() + .map_err(|_| anyhow::anyhow!("invalid payload hash: {}", h))?; + HsBlockId::PayloadHash(payload_hash) + }, + }; + + let ds = &*self.data_source; + let timeout = std::time::Duration::from_millis(500); + let (block_fetch, vid_fetch) = + join!(ds.get_block(hs_block_id), ds.get_vid_common(hs_block_id)); + let (block, vid_common) = join!( + block_fetch.with_timeout(timeout), + vid_fetch.with_timeout(timeout) + ); + + let block = block.ok_or_else(|| anyhow::anyhow!("block not found"))?; + let vid_common = vid_common.ok_or_else(|| anyhow::anyhow!("VID common data not found"))?; + + let ns_table = block.payload().ns_table(); + let ns_index = ns_table + .find_ns_id(&ns_id) + .ok_or_else(|| anyhow::anyhow!("namespace {} not present in block", namespace))?; + + if NsProof::new(block.payload(), &ns_index, vid_common.common()).is_some() { + return Err(anyhow::anyhow!("block was correctly encoded")); + } + + // Block has incorrect encoding — fetch VID shares to construct the proof. + let vid_shares_future = ds + .request_vid_shares(block.height(), vid_common.clone(), Duration::from_secs(40)) + .await; + let mut vid_shares = vid_shares_future + .await + .map_err(|e| anyhow::anyhow!("failed to fetch VID shares: {e:#}"))?; + + if let Ok(local_share) = ds.vid_share(block.height() as usize).await { + vid_shares.push(local_share); + } + + let avidm_shares: Vec = vid_shares + .into_iter() + .filter_map(|s| { + if let VidShare::V1(s) = s { + Some(s) + } else { + None + } + }) + .collect(); + + match NsProof::v1_1_new_with_incorrect_encoding( + &avidm_shares, + ns_table, + &ns_index, + &vid_common.payload_hash(), + vid_common.common(), + ) { + Some(NsProof::V1IncorrectEncoding(proof)) => Ok(proof), + _ => Err(anyhow::anyhow!( + "failed to generate incorrect encoding proof" + )), + } + } + + async fn get_state_cert(&self, epoch: u64) -> anyhow::Result { + use std::time::Duration; + + // Try to get from local storage first + let state_cert = self.data_source.get_state_cert_by_epoch(epoch).await?; + + let cert = match state_cert { + Some(cert) => cert, + None => { + // Not found locally, try to fetch from peers + const TIMEOUT: Duration = Duration::from_secs(40); + let cert = self + .data_source + .request_state_cert(epoch, TIMEOUT) + .await + .map_err(|e| { + anyhow::anyhow!("failed to fetch state cert for epoch {}: {}", epoch, e) + })?; + + // Store the fetched certificate + self.data_source + .insert_state_cert(epoch, cert.clone()) + .await?; + + cert + }, + }; + + Ok(espresso_types::StateCertQueryDataV1::from( + espresso_types::StateCertQueryDataV2(cert), + )) + } + + async fn get_state_cert_v2(&self, epoch: u64) -> anyhow::Result { + use std::time::Duration; + + // Try to get from local storage first + let state_cert = self.data_source.get_state_cert_by_epoch(epoch).await?; + + let cert = match state_cert { + Some(cert) => cert, + None => { + // Not found locally, try to fetch from peers + const TIMEOUT: Duration = Duration::from_secs(40); + let cert = self + .data_source + .request_state_cert(epoch, TIMEOUT) + .await + .map_err(|e| { + anyhow::anyhow!("failed to fetch state cert for epoch {}: {}", epoch, e) + })?; + + // Store the fetched certificate + self.data_source + .insert_state_cert(epoch, cert.clone()) + .await?; + + cert + }, + }; + + Ok(espresso_types::StateCertQueryDataV2(cert)) + } +} diff --git a/crates/espresso/types/src/v0/v0_1/block.rs b/crates/espresso/types/src/v0/v0_1/block.rs index bf081de1eac..286fcbc6378 100644 --- a/crates/espresso/types/src/v0/v0_1/block.rs +++ b/crates/espresso/types/src/v0/v0_1/block.rs @@ -8,9 +8,9 @@ use thiserror::Error; /// Proof of correctness for namespace payload bytes in a block. #[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)] pub struct ADVZNsProof { - pub(crate) ns_index: NsIndex, - pub(crate) ns_payload: NsPayloadOwned, - pub(crate) ns_proof: Option, // `None` if ns_payload is empty + pub ns_index: NsIndex, + pub ns_payload: NsPayloadOwned, + pub ns_proof: Option, // `None` if ns_payload is empty } /// Byte lengths for the different items that could appear in a namespace table. diff --git a/crates/espresso/types/src/v0/v0_1/transaction.rs b/crates/espresso/types/src/v0/v0_1/transaction.rs index 299b11f5ca6..fe7ec88fbd6 100644 --- a/crates/espresso/types/src/v0/v0_1/transaction.rs +++ b/crates/espresso/types/src/v0/v0_1/transaction.rs @@ -14,9 +14,9 @@ use serde::{Deserialize, Serialize}; CanonicalDeserialize, )] pub struct Transaction { - pub(crate) namespace: NamespaceId, + pub namespace: NamespaceId, #[serde(with = "base64_bytes")] - pub(crate) payload: Vec, + pub payload: Vec, } #[derive( @@ -37,4 +37,4 @@ pub struct Transaction { Ord, )] #[display("{_0}")] -pub struct NamespaceId(pub(crate) u64); +pub struct NamespaceId(pub u64); diff --git a/crates/espresso/types/src/v0/v0_3/nsproof.rs b/crates/espresso/types/src/v0/v0_3/nsproof.rs index 7c630b3d0a2..ab84972ccaf 100644 --- a/crates/espresso/types/src/v0/v0_3/nsproof.rs +++ b/crates/espresso/types/src/v0/v0_3/nsproof.rs @@ -2,8 +2,8 @@ use serde::{Deserialize, Serialize}; /// Re-export the AVID-M namespace proof. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AvidMNsProof(pub(crate) vid::avidm::proofs::NsProof); +pub struct AvidMNsProof(pub vid::avidm::proofs::NsProof); /// The namespace proof for incorrect encoding. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AvidMIncorrectEncodingNsProof(pub(crate) vid::avidm::proofs::NsAvidMBadEncodingProof); +pub struct AvidMIncorrectEncodingNsProof(pub vid::avidm::proofs::NsAvidMBadEncodingProof); diff --git a/crates/espresso/types/src/v0/v0_6/nsproof.rs b/crates/espresso/types/src/v0/v0_6/nsproof.rs index 1172ea1c3bb..bda80bf80ca 100644 --- a/crates/espresso/types/src/v0/v0_6/nsproof.rs +++ b/crates/espresso/types/src/v0/v0_6/nsproof.rs @@ -2,4 +2,4 @@ use serde::{Deserialize, Serialize}; /// Re-export the AvidmGf2 namespace proof. #[derive(Clone, Debug, Serialize, Deserialize, Eq, PartialEq)] -pub struct AvidmGf2NsProof(pub(crate) vid::avidm_gf2::proofs::NsProof); +pub struct AvidmGf2NsProof(pub vid::avidm_gf2::proofs::NsProof); diff --git a/crates/serialization/api/Cargo.toml b/crates/serialization/api/Cargo.toml index b7c2f27d626..36c71cee883 100644 --- a/crates/serialization/api/Cargo.toml +++ b/crates/serialization/api/Cargo.toml @@ -16,5 +16,5 @@ serde_json = { workspace = true } [build-dependencies] prost-build = { workspace = true } - -[dev-dependencies] +serde = { workspace = true } +toml = { workspace = true } diff --git a/crates/serialization/api/build.rs b/crates/serialization/api/build.rs index 6cc3b41bed2..3900154c846 100644 --- a/crates/serialization/api/build.rs +++ b/crates/serialization/api/build.rs @@ -1,61 +1,54 @@ -use std::path::PathBuf; - -// Example values for OpenAPI documentation -const EXAMPLE_ETH_ADDRESS: &str = "0x0000000000000000000000000000000000000000"; -const EXAMPLE_HEIGHT: u64 = 1000000; -const EXAMPLE_OFFSET: u64 = 0; -const EXAMPLE_LIMIT: u64 = 100; +use std::{collections::HashMap, path::PathBuf}; fn main() -> Result<(), Box> { let proto_root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("proto"); let out_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src"); - // Generate message types with serde support for JSON serialization and OpenAPI schema - // Output directly to src/ so generated types are committed to git for visibility - prost_build::Config::new() - .type_attribute( - ".", - "#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]", - ) - // Flatten oneof fields to remove wrapper in JSON serialization - .field_attribute("RewardMerkleProofV2.proof_type", "#[serde(flatten)]") - .field_attribute("MerkleNode.node_type", "#[serde(flatten)]") - // Skip Empty's dummy field so it serializes as {} - .field_attribute("Empty.dummy", "#[serde(skip)]") - // Add OpenAPI examples for path parameters - .field_attribute( - "GetRewardClaimInputRequest.address", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_ETH_ADDRESS), - ) - .field_attribute( - "GetRewardBalanceRequest.address", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_ETH_ADDRESS), - ) - .field_attribute( - "GetRewardAccountProofRequest.address", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_ETH_ADDRESS), - ) - .field_attribute( - "GetRewardBalancesRequest.height", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_HEIGHT), - ) - .field_attribute( - "GetRewardBalancesRequest.offset", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_OFFSET), - ) - .field_attribute( - "GetRewardBalancesRequest.limit", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_LIMIT), - ) - .field_attribute( - "GetRewardMerkleTreeRequest.height", - format!(r#"#[schemars(example = "{}")]"#, EXAMPLE_HEIGHT), - ) - .out_dir(&out_dir) - .compile_protos(&["v2/common.proto", "v2/rewards.proto"], &[proto_root])?; + let examples_path = proto_root.join("v2/examples.toml"); + let examples_content = std::fs::read_to_string(&examples_path)?; + let examples: HashMap> = + toml::from_str(&examples_content)?; + + let mut config = prost_build::Config::new(); + config.type_attribute( + ".", + "#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)]", + ); + + config.field_attribute("Empty.dummy", "#[serde(skip)]"); + + for (message_name, fields) in examples { + for (field_name, value) in fields { + let field_path = format!("{}.{}", message_name, field_name); + let example_value = match value { + toml::Value::String(s) => s, + toml::Value::Integer(i) => i.to_string(), + toml::Value::Float(f) => f.to_string(), + toml::Value::Boolean(b) => b.to_string(), + _ => continue, + }; + config.field_attribute( + &field_path, + format!(r#"#[schemars(example = "{}")]"#, example_value), + ); + } + } + + config.out_dir(&out_dir).compile_protos( + &[ + "v2/common.proto", + "v2/rewards.proto", + "v2/data.proto", + "v2/consensus.proto", + ], + &[proto_root], + )?; println!("cargo:rerun-if-changed=proto/v2/common.proto"); println!("cargo:rerun-if-changed=proto/v2/rewards.proto"); + println!("cargo:rerun-if-changed=proto/v2/data.proto"); + println!("cargo:rerun-if-changed=proto/v2/consensus.proto"); + println!("cargo:rerun-if-changed=proto/v2/examples.toml"); Ok(()) } diff --git a/crates/serialization/api/proto/v2/common.proto b/crates/serialization/api/proto/v2/common.proto index 4925dd7e0d4..fe3ca068a2a 100644 --- a/crates/serialization/api/proto/v2/common.proto +++ b/crates/serialization/api/proto/v2/common.proto @@ -4,3 +4,68 @@ package espresso.api.v2; // Common types for Espresso API v2 // This file is intentionally minimal - domain-specific types live in their own proto files + +//============================================================================= +// Cryptographic Key Types +//============================================================================= + +// BLS public key (typically 48 bytes) +message BLSPublicKey { + string key = 1; +} + +// Schnorr public key (typically 32 bytes) +message SchnorrPublicKey { + string key = 1; +} + +//============================================================================= +// Network Address Types +//============================================================================= + +// Network address - either IP or domain name with port +message NetAddr { + oneof addr_type { + InetAddr inet = 1; + NameAddr name = 2; + } +} + +// IP address (v4 or v6) with port +// `host` is a socket-address-ready host string: plain dotted-decimal for IPv4 +// (e.g. "192.0.2.1") and bracket-enclosed for IPv6 (e.g. "[::1]"), so that +// consumers can form a valid address with a simple "{host}:{port}" without +// inspecting the IP version. +message InetAddr { + string host = 1; + uint32 port = 2; +} + +// Domain name with port +message NameAddr { + string name = 1; // Domain name + uint32 port = 2; +} + +//============================================================================= +// Peer Configuration Types +//============================================================================= + +// Stake table entry for a validator +message StakeTableEntry { + BLSPublicKey stake_key = 1; // BLS verification key + string stake_amount = 2; // U256 as decimal string +} + +// Peer connection information +message PeerConnectInfo { + string x25519_key = 1; // X25519 public key (32 bytes) + NetAddr p2p_addr = 2; // P2P network address +} + +// Complete peer configuration +message PeerConfig { + StakeTableEntry stake_table_entry = 1; // Stake information + SchnorrPublicKey state_ver_key = 2; // State verification key + optional PeerConnectInfo connect_info = 3; // Optional connection info +} diff --git a/crates/serialization/api/proto/v2/consensus.proto b/crates/serialization/api/proto/v2/consensus.proto new file mode 100644 index 00000000000..84967ef2276 --- /dev/null +++ b/crates/serialization/api/proto/v2/consensus.proto @@ -0,0 +1,85 @@ +syntax = "proto3"; + +package espresso.api.v2; + +import "v2/common.proto"; + +//============================================================================= +// Light Client State Types +//============================================================================= + +// Light client state +message LightClientState { + uint64 view_number = 1; + uint64 block_height = 2; + string block_comm_root = 3; // CircuitField serialized as bytes +} + +// Stake table state commitments +message StakeTableState { + string bls_key_comm = 1; // CircuitField as bytes + string schnorr_key_comm = 2; // CircuitField as bytes + string amount_comm = 3; // CircuitField as bytes + string threshold = 4; // CircuitField as bytes +} + +// State signature tuple (key + two signatures) +message StateSignatureTuple { + SchnorrPublicKey state_signature_key = 1; // Schnorr public key + string lcv3_signature = 2; // LCV3 Schnorr signature + string lcv2_signature = 3; // LCV2 Schnorr signature +} + +// Light client state update certificate V2 +message LightClientStateUpdateCertificateV2 { + uint64 epoch = 1; + string light_client_state = 2; // TaggedBase64 "LIGHT_CLIENT_STATE~..." + string next_stake_table_state = 3; // TaggedBase64 "STAKE_TABLE_STATE~..." + repeated StateSignatureTuple signatures = 4; + string auth_root = 5; // TaggedBase64 or plain base64 +} + +//============================================================================= +// State Certificate Queries +//============================================================================= + +// Request to get state certificate for an epoch +message GetStateCertificateRequest { + // Epoch number + uint64 epoch = 1; +} + +// State certificate response +message StateCertificateResponse { + // Structured light client state update certificate + LightClientStateUpdateCertificateV2 certificate = 1; +} + +//============================================================================= +// Stake Table Queries +//============================================================================= + +// Request to get stake table for an epoch +message GetStakeTableRequest { + // Epoch number + uint64 epoch = 1; +} + +// Stake table response +message StakeTableResponse { + // List of peer configurations + repeated PeerConfig peers = 1; +} + +//============================================================================= +// gRPC Service Definition +//============================================================================= + +// gRPC service for consensus state queries +service ConsensusService { + // Get state certificate for an epoch + rpc GetStateCertificate(GetStateCertificateRequest) returns (StateCertificateResponse); + + // Get stake table for an epoch + rpc GetStakeTable(GetStakeTableRequest) returns (StakeTableResponse); +} diff --git a/crates/serialization/api/proto/v2/data.proto b/crates/serialization/api/proto/v2/data.proto new file mode 100644 index 00000000000..92911e9efc7 --- /dev/null +++ b/crates/serialization/api/proto/v2/data.proto @@ -0,0 +1,124 @@ +syntax = "proto3"; + +package espresso.api.v2; + +//============================================================================= +// Namespace Proof Types +//============================================================================= + +// ADVZ (VID V0) namespace proof +message ADVZNsProof { + uint64 namespace_id = 1; + string ns_payload = 2; + optional string ns_proof = 3; // VID proof (serialized) +} + +// AvidM (VID V1) namespace proof +message AvidMNsProof { + uint64 ns_index = 1; + string ns_payload = 2; + string ns_proof = 3; // Merkle proof (serialized) +} + +// AvidM incorrect encoding proof +message AvidMIncorrectEncodingNsProof { + string proof_data = 1; // Serialized incorrect encoding proof +} + +// AvidmGf2 (VID V2) namespace proof +message AvidmGf2NsProof { + uint64 ns_index = 1; + string ns_payload = 2; + string ns_proof = 3; // Merkle proof (serialized) +} + +// Namespace proof - supports multiple VID versions +message NsProof { + oneof proof_version { + ADVZNsProof v0 = 1; + AvidMNsProof v1 = 2; + AvidMIncorrectEncodingNsProof v1_incorrect_encoding = 3; + AvidmGf2NsProof v2 = 4; + } +} + +//============================================================================= +// Namespace Proof Queries +//============================================================================= + +// Request to get namespace proof(s) +// Either specify 'block' for a single block, or 'first' and 'last' for a range +message GetNamespaceProofRequest { + // Namespace ID + uint64 namespace_id = 1; + + // Single block query + optional uint64 block = 2; + + // Range query - first block (inclusive) + optional uint64 first = 3; + + // Range query - last block (inclusive) + optional uint64 last = 4; +} + +// Transaction +message Transaction { + uint64 namespace = 1; + string payload = 2; // base64 encoded +} + +// Single block namespace proof response +message NamespaceProofResponse { + // Transactions for this namespace + repeated Transaction transactions = 1; + + // Namespace proof (optional - may be None if namespace not present) + optional NsProof proof = 2; +} + +// Response for range queries - array of namespace proofs +message NamespaceProofRangeResponse { + // Proofs for each block in the range + repeated NamespaceProofResponse proofs = 1; +} + +// Unified response for namespace proof queries +message GetNamespaceProofResponse { + oneof response { + NamespaceProofResponse single = 1; // Single block response + NamespaceProofRangeResponse range = 2; // Range response + } +} + +//============================================================================= +// Incorrect Encoding Proof +//============================================================================= + +// Request to get incorrect encoding proof +message GetIncorrectEncodingProofRequest { + // Namespace ID + uint64 namespace_id = 1; + + // Block height + uint64 block_height = 2; +} + +// Incorrect encoding proof response +message IncorrectEncodingProofResponse { + // Incorrect encoding proof (AvidM V1.1 proof) + AvidMIncorrectEncodingNsProof proof = 1; +} + +//============================================================================= +// gRPC Service Definition +//============================================================================= + +// gRPC service for data availability queries +service DataService { + // Get namespace proof(s) - handles both single block and range queries + rpc GetNamespaceProof(GetNamespaceProofRequest) returns (GetNamespaceProofResponse); + + // Get incorrect encoding proof for a block + rpc GetIncorrectEncodingProof(GetIncorrectEncodingProofRequest) returns (IncorrectEncodingProofResponse); +} diff --git a/crates/serialization/api/proto/v2/examples.toml b/crates/serialization/api/proto/v2/examples.toml new file mode 100644 index 00000000000..4e2866a2924 --- /dev/null +++ b/crates/serialization/api/proto/v2/examples.toml @@ -0,0 +1,32 @@ +[GetRewardClaimInputRequest] +address = "0x0000000000000000000000000000000000000000" + +[GetRewardBalanceRequest] +address = "0x0000000000000000000000000000000000000000" + +[GetRewardAccountProofRequest] +address = "0x0000000000000000000000000000000000000000" + +[GetRewardBalancesRequest] +height = 1000000 +offset = 0 +limit = 100 + +[GetRewardMerkleTreeRequest] +height = 1000000 + +[GetNamespaceProofRequest] +namespace_id = 10001 +block = 1000000 +first = 1000000 +last = 1000100 + +[GetIncorrectEncodingProofRequest] +namespace_id = 10001 +block_height = 1000000 + +[GetStateCertificateRequest] +epoch = 100 + +[GetStakeTableRequest] +epoch = 100 diff --git a/crates/serialization/api/src/espresso.api.v2.rs b/crates/serialization/api/src/espresso.api.v2.rs index 2f7120c2b69..27b3fee461d 100644 --- a/crates/serialization/api/src/espresso.api.v2.rs +++ b/crates/serialization/api/src/espresso.api.v2.rs @@ -1,4 +1,95 @@ // This file is @generated by prost-build. +/// BLS public key (typically 48 bytes) +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct BlsPublicKey { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, +} +/// Schnorr public key (typically 32 bytes) +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct SchnorrPublicKey { + #[prost(string, tag = "1")] + pub key: ::prost::alloc::string::String, +} +/// Network address - either IP or domain name with port +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct NetAddr { + #[prost(oneof = "net_addr::AddrType", tags = "1, 2")] + pub addr_type: ::core::option::Option, +} +/// Nested message and enum types in `NetAddr`. +pub mod net_addr { + #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum AddrType { + #[prost(message, tag = "1")] + Inet(super::InetAddr), + #[prost(message, tag = "2")] + Name(super::NameAddr), + } +} +/// IP address (v4 or v6) with port +/// `host` is a socket-address-ready host string: plain dotted-decimal for IPv4 +/// (e.g. "192.0.2.1") and bracket-enclosed for IPv6 (e.g. "\[::1\]"), so that +/// consumers can form a valid address with a simple "{host}:{port}" without +/// inspecting the IP version. +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct InetAddr { + #[prost(string, tag = "1")] + pub host: ::prost::alloc::string::String, + #[prost(uint32, tag = "2")] + pub port: u32, +} +/// Domain name with port +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct NameAddr { + /// Domain name + #[prost(string, tag = "1")] + pub name: ::prost::alloc::string::String, + #[prost(uint32, tag = "2")] + pub port: u32, +} +/// Stake table entry for a validator +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct StakeTableEntry { + /// BLS verification key + #[prost(message, optional, tag = "1")] + pub stake_key: ::core::option::Option, + /// U256 as decimal string + #[prost(string, tag = "2")] + pub stake_amount: ::prost::alloc::string::String, +} +/// Peer connection information +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PeerConnectInfo { + /// X25519 public key (32 bytes) + #[prost(string, tag = "1")] + pub x25519_key: ::prost::alloc::string::String, + /// P2P network address + #[prost(message, optional, tag = "2")] + pub p2p_addr: ::core::option::Option, +} +/// Complete peer configuration +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct PeerConfig { + /// Stake information + #[prost(message, optional, tag = "1")] + pub stake_table_entry: ::core::option::Option, + /// State verification key + #[prost(message, optional, tag = "2")] + pub state_ver_key: ::core::option::Option, + /// Optional connection info + #[prost(message, optional, tag = "3")] + pub connect_info: ::core::option::Option, +} /// U256 value encoded as hex string #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] #[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] @@ -76,7 +167,6 @@ pub struct RewardAccountProofV2 { #[derive(Clone, PartialEq, ::prost::Message)] pub struct RewardMerkleProofV2 { #[prost(oneof = "reward_merkle_proof_v2::ProofType", tags = "1, 2")] - #[serde(flatten)] pub proof_type: ::core::option::Option, } /// Nested message and enum types in `RewardMerkleProofV2`. @@ -106,7 +196,6 @@ pub struct MerkleProof { #[derive(Clone, PartialEq, ::prost::Message)] pub struct MerkleNode { #[prost(oneof = "merkle_node::NodeType", tags = "1, 2, 3, 4")] - #[serde(flatten)] pub node_type: ::core::option::Option, } /// Nested message and enum types in `MerkleNode`. @@ -235,3 +324,256 @@ pub struct GetRewardMerkleTreeRequest { #[schemars(example = "1000000")] pub height: u64, } +/// ADVZ (VID V0) namespace proof +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AdvzNsProof { + #[prost(uint64, tag = "1")] + pub namespace_id: u64, + #[prost(string, tag = "2")] + pub ns_payload: ::prost::alloc::string::String, + /// VID proof (serialized) + #[prost(string, optional, tag = "3")] + pub ns_proof: ::core::option::Option<::prost::alloc::string::String>, +} +/// AvidM (VID V1) namespace proof +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AvidMNsProof { + #[prost(uint64, tag = "1")] + pub ns_index: u64, + #[prost(string, tag = "2")] + pub ns_payload: ::prost::alloc::string::String, + /// Merkle proof (serialized) + #[prost(string, tag = "3")] + pub ns_proof: ::prost::alloc::string::String, +} +/// AvidM incorrect encoding proof +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AvidMIncorrectEncodingNsProof { + /// Serialized incorrect encoding proof + #[prost(string, tag = "1")] + pub proof_data: ::prost::alloc::string::String, +} +/// AvidmGf2 (VID V2) namespace proof +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct AvidmGf2NsProof { + #[prost(uint64, tag = "1")] + pub ns_index: u64, + #[prost(string, tag = "2")] + pub ns_payload: ::prost::alloc::string::String, + /// Merkle proof (serialized) + #[prost(string, tag = "3")] + pub ns_proof: ::prost::alloc::string::String, +} +/// Namespace proof - supports multiple VID versions +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct NsProof { + #[prost(oneof = "ns_proof::ProofVersion", tags = "1, 2, 3, 4")] + pub proof_version: ::core::option::Option, +} +/// Nested message and enum types in `NsProof`. +pub mod ns_proof { + #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] + #[derive(Clone, PartialEq, Eq, Hash, ::prost::Oneof)] + pub enum ProofVersion { + #[prost(message, tag = "1")] + V0(super::AdvzNsProof), + #[prost(message, tag = "2")] + V1(super::AvidMNsProof), + #[prost(message, tag = "3")] + V1IncorrectEncoding(super::AvidMIncorrectEncodingNsProof), + #[prost(message, tag = "4")] + V2(super::AvidmGf2NsProof), + } +} +/// Request to get namespace proof(s) +/// Either specify 'block' for a single block, or 'first' and 'last' for a range +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetNamespaceProofRequest { + /// Namespace ID + #[prost(uint64, tag = "1")] + #[schemars(example = "10001")] + pub namespace_id: u64, + /// Single block query + #[prost(uint64, optional, tag = "2")] + #[schemars(example = "1000000")] + pub block: ::core::option::Option, + /// Range query - first block (inclusive) + #[prost(uint64, optional, tag = "3")] + #[schemars(example = "1000000")] + pub first: ::core::option::Option, + /// Range query - last block (inclusive) + #[prost(uint64, optional, tag = "4")] + #[schemars(example = "1000100")] + pub last: ::core::option::Option, +} +/// Transaction +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct Transaction { + #[prost(uint64, tag = "1")] + pub namespace: u64, + /// base64 encoded + #[prost(string, tag = "2")] + pub payload: ::prost::alloc::string::String, +} +/// Single block namespace proof response +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NamespaceProofResponse { + /// Transactions for this namespace + #[prost(message, repeated, tag = "1")] + pub transactions: ::prost::alloc::vec::Vec, + /// Namespace proof (optional - may be None if namespace not present) + #[prost(message, optional, tag = "2")] + pub proof: ::core::option::Option, +} +/// Response for range queries - array of namespace proofs +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct NamespaceProofRangeResponse { + /// Proofs for each block in the range + #[prost(message, repeated, tag = "1")] + pub proofs: ::prost::alloc::vec::Vec, +} +/// Unified response for namespace proof queries +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNamespaceProofResponse { + #[prost(oneof = "get_namespace_proof_response::Response", tags = "1, 2")] + pub response: ::core::option::Option, +} +/// Nested message and enum types in `GetNamespaceProofResponse`. +pub mod get_namespace_proof_response { + #[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] + #[derive(Clone, PartialEq, ::prost::Oneof)] + pub enum Response { + /// Single block response + #[prost(message, tag = "1")] + Single(super::NamespaceProofResponse), + /// Range response + #[prost(message, tag = "2")] + Range(super::NamespaceProofRangeResponse), + } +} +/// Request to get incorrect encoding proof +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetIncorrectEncodingProofRequest { + /// Namespace ID + #[prost(uint64, tag = "1")] + #[schemars(example = "10001")] + pub namespace_id: u64, + /// Block height + #[prost(uint64, tag = "2")] + #[schemars(example = "1000000")] + pub block_height: u64, +} +/// Incorrect encoding proof response +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct IncorrectEncodingProofResponse { + /// Incorrect encoding proof (AvidM V1.1 proof) + #[prost(message, optional, tag = "1")] + pub proof: ::core::option::Option, +} +/// Light client state +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct LightClientState { + #[prost(uint64, tag = "1")] + pub view_number: u64, + #[prost(uint64, tag = "2")] + pub block_height: u64, + /// CircuitField serialized as bytes + #[prost(string, tag = "3")] + pub block_comm_root: ::prost::alloc::string::String, +} +/// Stake table state commitments +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct StakeTableState { + /// CircuitField as bytes + #[prost(string, tag = "1")] + pub bls_key_comm: ::prost::alloc::string::String, + /// CircuitField as bytes + #[prost(string, tag = "2")] + pub schnorr_key_comm: ::prost::alloc::string::String, + /// CircuitField as bytes + #[prost(string, tag = "3")] + pub amount_comm: ::prost::alloc::string::String, + /// CircuitField as bytes + #[prost(string, tag = "4")] + pub threshold: ::prost::alloc::string::String, +} +/// State signature tuple (key + two signatures) +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, Eq, Hash, ::prost::Message)] +pub struct StateSignatureTuple { + /// Schnorr public key + #[prost(message, optional, tag = "1")] + pub state_signature_key: ::core::option::Option, + /// LCV3 Schnorr signature + #[prost(string, tag = "2")] + pub lcv3_signature: ::prost::alloc::string::String, + /// LCV2 Schnorr signature + #[prost(string, tag = "3")] + pub lcv2_signature: ::prost::alloc::string::String, +} +/// Light client state update certificate V2 +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct LightClientStateUpdateCertificateV2 { + #[prost(uint64, tag = "1")] + pub epoch: u64, + /// TaggedBase64 "LIGHT_CLIENT_STATE~..." + #[prost(string, tag = "2")] + pub light_client_state: ::prost::alloc::string::String, + /// TaggedBase64 "STAKE_TABLE_STATE~..." + #[prost(string, tag = "3")] + pub next_stake_table_state: ::prost::alloc::string::String, + #[prost(message, repeated, tag = "4")] + pub signatures: ::prost::alloc::vec::Vec, + /// TaggedBase64 or plain base64 + #[prost(string, tag = "5")] + pub auth_root: ::prost::alloc::string::String, +} +/// Request to get state certificate for an epoch +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetStateCertificateRequest { + /// Epoch number + #[prost(uint64, tag = "1")] + #[schemars(example = "100")] + pub epoch: u64, +} +/// State certificate response +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StateCertificateResponse { + /// Structured light client state update certificate + #[prost(message, optional, tag = "1")] + pub certificate: ::core::option::Option, +} +/// Request to get stake table for an epoch +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, Copy, PartialEq, Eq, Hash, ::prost::Message)] +pub struct GetStakeTableRequest { + /// Epoch number + #[prost(uint64, tag = "1")] + #[schemars(example = "100")] + pub epoch: u64, +} +/// Stake table response +#[derive(serde::Serialize, serde::Deserialize, schemars::JsonSchema)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct StakeTableResponse { + /// List of peer configurations + #[prost(message, repeated, tag = "1")] + pub peers: ::prost::alloc::vec::Vec, +} diff --git a/crates/serialization/api/src/lib.rs b/crates/serialization/api/src/lib.rs index 04a130677b1..869a1c5c34c 100644 --- a/crates/serialization/api/src/lib.rs +++ b/crates/serialization/api/src/lib.rs @@ -5,70 +5,79 @@ pub mod v2 { pub use v2::*; -/// Trait for converting between implementation types and proto serialization types -/// -/// Implementations define their own internal types for addresses and rewards data, -/// then provide conversions to/from the proto types for API serialization. pub trait ApiSerializations { - // Request types (implementation-defined) - - /// Address type used by the implementation type Address; - - // Response types (implementation-defined) - - /// Reward claim input type type RewardClaimInput; - - /// Reward balance type type RewardBalance; - - /// Reward account query data type (balance + proof) type RewardAccountQueryData; - - /// Paginated reward balances type type RewardBalances; - - /// Reward merkle tree snapshot data type type RewardMerkleTreeData; + type NamespaceProof; + type IncorrectEncodingProof; + type StateCertificate; + type StakeTable; + type PeerConfig; + type LightClientCert; + type NsProof; - // Deserialize proto/string types → internal types - - /// Deserialize an address string from a proto request into the implementation's Address type fn deserialize_address(&self, s: &str) -> anyhow::Result; - // Serialize internal types → proto types - - /// Serialize implementation's RewardClaimInput to proto RewardClaimInput - /// - /// Takes the original address string since the internal type may not contain it fn serialize_reward_claim_input( &self, address: &str, value: &Self::RewardClaimInput, ) -> anyhow::Result; - /// Serialize implementation's RewardBalance to proto RewardBalance fn serialize_reward_balance( &self, value: &Self::RewardBalance, ) -> anyhow::Result; - /// Serialize implementation's RewardAccountQueryData to proto RewardAccountQueryDataV2 fn serialize_reward_account_query_data( &self, value: &Self::RewardAccountQueryData, ) -> anyhow::Result; - /// Serialize implementation's RewardBalances to proto RewardBalances fn serialize_reward_balances( &self, value: &Self::RewardBalances, ) -> anyhow::Result; - /// Serialize implementation's RewardMerkleTreeData to proto RewardMerkleTreeV2Data fn serialize_reward_merkle_tree_data( &self, value: &Self::RewardMerkleTreeData, ) -> anyhow::Result; + + fn serialize_namespace_proof( + &self, + value: &Self::NamespaceProof, + ) -> anyhow::Result; + + fn serialize_incorrect_encoding_proof( + &self, + value: &Self::IncorrectEncodingProof, + ) -> anyhow::Result; + + fn serialize_state_certificate( + &self, + value: &Self::StateCertificate, + ) -> anyhow::Result; + + fn serialize_stake_table(&self, value: &Self::StakeTable) + -> anyhow::Result; + + fn serialize_peer_config(&self, peer: &Self::PeerConfig) -> anyhow::Result + where + Self::PeerConfig: Sized; + + fn serialize_light_client_cert( + &self, + cert: &Self::LightClientCert, + ) -> anyhow::Result + where + Self::LightClientCert: Sized; + + fn serialize_ns_proof(&self, proof: &Self::NsProof) -> anyhow::Result + where + Self::NsProof: Sized; } diff --git a/hotshot-query-service-types/Cargo.toml b/hotshot-query-service-types/Cargo.toml index 0a81d3979e7..8bcef621895 100644 --- a/hotshot-query-service-types/Cargo.toml +++ b/hotshot-query-service-types/Cargo.toml @@ -12,11 +12,17 @@ testing = ["hotshot-example-types"] [dependencies] anyhow.workspace = true ark-serialize.workspace = true + +# SQL dependencies. +bincode = { workspace = true, optional = true } committable.workspace = true derivative.workspace = true derive_more.workspace = true either.workspace = true hotshot-events-service.workspace = true + +# Testing dependencies. +hotshot-example-types = { workspace = true, optional = true } hotshot-types.workspace = true itertools.workspace = true jf-advz.workspace = true @@ -24,6 +30,7 @@ jf-merkle-tree-compat.workspace = true serde.workspace = true serde_json.workspace = true snafu.workspace = true +sqlx = { workspace = true, optional = true } surf-disco.workspace = true tagged-base64.workspace = true tide-disco.workspace = true @@ -31,12 +38,5 @@ time = { workspace = true, features = ["formatting", "parsing"] } vbs.workspace = true versions.workspace = true -# Testing dependencies. -hotshot-example-types = { workspace = true, optional = true } - -# SQL dependencies. -bincode = { workspace = true, optional = true } -sqlx = { workspace = true, optional = true } - [lints] workspace = true diff --git a/light-client-query-service/Cargo.toml b/light-client-query-service/Cargo.toml index 18f8c4a052b..43c5c5087c0 100644 --- a/light-client-query-service/Cargo.toml +++ b/light-client-query-service/Cargo.toml @@ -20,9 +20,13 @@ clap = { workspace = true } espresso-node = { workspace = true, default-features = false } espresso-types = { workspace = true } hotshot-query-service = { workspace = true } + +# Optional dependencies for genesis utility. +hotshot-types = { workspace = true, optional = true } light-client = { workspace = true } log-panics = { workspace = true } semver = { workspace = true } +surf-disco = { workspace = true, optional = true } tide-disco = { workspace = true } tokio = { workspace = true } toml = { workspace = true } @@ -30,9 +34,5 @@ tracing = { workspace = true } tracing-subscriber = { workspace = true } vbs = { workspace = true } -# Optional dependencies for genesis utility. -hotshot-types = { workspace = true, optional = true } -surf-disco = { workspace = true, optional = true } - [lints] workspace = true diff --git a/node-metrics/Cargo.toml b/node-metrics/Cargo.toml index 3631b111590..feee70d4c0d 100644 --- a/node-metrics/Cargo.toml +++ b/node-metrics/Cargo.toml @@ -23,18 +23,18 @@ futures = { workspace = true } hotshot = { workspace = true } hotshot-example-types = { workspace = true } hotshot-query-service = { workspace = true } -indexmap = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -tokio = { workspace = true } # Dependencies for feature `testing` hotshot-types = { workspace = true } +indexmap = { workspace = true } prometheus-parse = { workspace = true } reqwest = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } surf-disco = { workspace = true } tide-disco = { workspace = true } time = { workspace = true } +tokio = { workspace = true } toml = { workspace = true } tracing = { workspace = true } url = { workspace = true } diff --git a/sdks/crypto-helper/Cargo.toml b/sdks/crypto-helper/Cargo.toml index 9ceb44feefc..4c8479f7d29 100644 --- a/sdks/crypto-helper/Cargo.toml +++ b/sdks/crypto-helper/Cargo.toml @@ -19,16 +19,16 @@ hotshot-types = { workspace = true } jf-crhf = { workspace = true } jf-merkle-tree-compat = { workspace = true } jf-rescue = { workspace = true } -primitive-types = { workspace = true } -serde = { workspace = true } -serde_json = { workspace = true } -sha2 = { workspace = true } -tagged-base64 = { workspace = true } # https://tikv.github.io/doc/openssl/index.html # We need this for the target `aarch64-unknown-linux-gnu` in CI. # This can be removed if we figure out this build issue. openssl = { workspace = true } +primitive-types = { workspace = true } +serde = { workspace = true } +serde_json = { workspace = true } +sha2 = { workspace = true } +tagged-base64 = { workspace = true } [lints] workspace = true diff --git a/staking-cli/Cargo.toml b/staking-cli/Cargo.toml index a962792196d..4e1a27978e0 100644 --- a/staking-cli/Cargo.toml +++ b/staking-cli/Cargo.toml @@ -21,9 +21,9 @@ dotenvy = { workspace = true } espresso-contract-deployer = { workspace = true } espresso-safe-tx-builder = { workspace = true } espresso-types = { workspace = true } -futures-util = { workspace = true } espresso-utils = { workspace = true } +futures-util = { workspace = true } hotshot-contract-adapter = { workspace = true } hotshot-state-prover = { workspace = true } hotshot-types = { workspace = true }