diff --git a/.gitignore b/.gitignore index c41cc9e..040ea4c 100644 --- a/.gitignore +++ b/.gitignore @@ -1 +1,23 @@ -/target \ No newline at end of file +# Rust +target/ +Cargo.lock + +# Bun & JS +bun.lockb +node_modules/ + +# Foundry +contracts/cache/ +contracts/out/ +contracts/out.json +snapshots/ + +# IDE/OS +.idea/ +.vscode/ +.DS_Store +Thumbs.db + +# Logs/Env +*.log +*.env diff --git a/Cargo.lock b/Cargo.lock deleted file mode 100644 index 3bb6c1f..0000000 --- a/Cargo.lock +++ /dev/null @@ -1,3579 +0,0 @@ -# This file is automatically @generated by Cargo. -# It is not intended for manual editing. -version = 4 - -[[package]] -name = "aho-corasick" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e60d3430d3a69478ad0993f19238d2df97c507009a52b3c10addcd7f6bcb916" -dependencies = [ - "memchr", -] - -[[package]] -name = "anyhow" -version = "1.0.97" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dcfed56ad506cb2c684a14971b8861fdc3baaaae314b9e5f9bb532cbe3ba7a4f" - -[[package]] -name = "arcstr" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "03918c3dbd7701a85c6b9887732e2921175f26c350b4563841d0958c21d57e6d" - -[[package]] -name = "arrayref" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76a2e8124351fda1ef8aaaa3bbd7ebbcb486bbcd4225aca0aa0d84bb2db8fecb" - -[[package]] -name = "arrayvec" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c02d123df017efcdfbd739ef81735b36c5ba83ec3c59c80a9d7ecc718f92e50" - -[[package]] -name = "async-stream" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5a71a6f37880a80d1d7f19efd781e4b5de42c88f0722cc13bcb6cc2cfe8476" -dependencies = [ - "async-stream-impl", - "futures-core", - "pin-project-lite", -] - -[[package]] -name = "async-stream-impl" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c7c24de15d275a1ecfd47a380fb4d5ec9bfe0933f309ed5e705b775596a3574d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "async-trait" -version = "0.1.88" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e539d3fca749fcee5236ab05e93a52867dd549cc157c8cb7f99595f3cedffdb5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "atomic-waker" -version = "1.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1505bd5d3d116872e7271a6d4e16d81d0c8570876c8de68093a09ac269d8aac0" - -[[package]] -name = "autocfg" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" - -[[package]] -name = "base-x" -version = "0.2.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4cbbc9d0964165b47557570cce6c952866c2678457aca742aafc9fb771d30270" - -[[package]] -name = "base16ct" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c7f02d4ea65f2c1853089ffd8d2787bdbc63de2f0d29dedbcf8ccdfa0ccd4cf" - -[[package]] -name = "base64" -version = "0.22.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" - -[[package]] -name = "base64ct" -version = "1.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e25b6adfb930f02d1981565a6e5d9c547ac15a96606256d3b59040e5cd4ca3" - -[[package]] -name = "bech32" -version = "0.11.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d965446196e3b7decd44aa7ee49e31d630118f90ef12f97900f262eb915c951d" - -[[package]] -name = "beef" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a8241f3ebb85c056b509d4327ad0358fbbba6ffb340bf388f26350aeda225b1" - -[[package]] -name = "bitflags" -version = "2.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c8214115b7bf84099f1309324e63141d4c5d7cc26862f97a0a857dbefe165bd" - -[[package]] -name = "bitvec" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc2832c24239b0141d5674bb9174f9d68a8b5b3f2753311927c172ca46f7e9c" -dependencies = [ - "funty", - "radium", - "tap", - "wyz", -] - -[[package]] -name = "block-buffer" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4152116fd6e9dadb291ae18fc1ec3575ed6d84c29642d97890f4b4a3417297e4" -dependencies = [ - "generic-array", -] - -[[package]] -name = "block-buffer" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3078c7629b62d3f0439517fa394996acacc5cbc91c5a20d8c658e77abd503a71" -dependencies = [ - "generic-array", -] - -[[package]] -name = "blockstore" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "509096e88e431095763b3f5ee1e1cdb09212e4d5b2eccc91ddea965deefedb7d" -dependencies = [ - "cid", - "dashmap", - "multihash", - "thiserror 2.0.12", -] - -[[package]] -name = "borsh" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad8646f98db542e39fc66e68a20b2144f6a732636df7c2354e74645faaa433ce" -dependencies = [ - "borsh-derive", - "cfg_aliases", -] - -[[package]] -name = "borsh-derive" -version = "1.5.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd1d3c0c2f5833f22386f252fe8ed005c7f59fdcddeef025c01b4c3b9fd9ac3" -dependencies = [ - "once_cell", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "bs58" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf88ba1141d185c399bee5288d850d63b8369520c1eafc32a0430b5b6c287bf4" -dependencies = [ - "tinyvec", -] - -[[package]] -name = "bumpalo" -version = "3.19.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "46c5e41b57b8bba42a04676d81cb89e9ee8e859a1a66f80a5a72e1cb76b34d43" - -[[package]] -name = "byteorder" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fd0f2584146f6f2ef48085050886acf353beff7305ebd1ae69500e27c67f64b" - -[[package]] -name = "bytes" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71b6127be86fdcfddb610f7182ac57211d4b18a3e9c82eb2d17662f2227ad6a" -dependencies = [ - "serde", -] - -[[package]] -name = "cc" -version = "1.2.55" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "47b26a0954ae34af09b50f0de26458fa95369a0d478d8236d3f93082b219bd29" -dependencies = [ - "find-msvc-tools", - "shlex", -] - -[[package]] -name = "celestia-proto" -version = "0.12.1" -source = "git+https://github.com/celestiaorg/lumina.git?rev=e114c6f#e114c6ffd605ae170c6013e9ef68f3a946404722" -dependencies = [ - "bytes", - "prost", - "prost-build", - "prost-types", - "protox", - "serde", - "subtle-encoding", - "tendermint-proto", -] - -[[package]] -name = "celestia-rpc" -version = "0.16.2" -source = "git+https://github.com/celestiaorg/lumina.git?rev=e114c6f#e114c6ffd605ae170c6013e9ef68f3a946404722" -dependencies = [ - "async-stream", - "base64", - "celestia-proto", - "celestia-types", - "futures-util", - "getrandom 0.3.4", - "gloo-net", - "gloo-timers", - "http 1.4.0", - "jsonrpsee", - "libp2p-identity", - "multiaddr", - "send_wrapper", - "serde", - "serde_json", - "serde_repr", - "tendermint-proto", - "thiserror 2.0.12", - "tokio", - "tracing", - "web-sys", -] - -[[package]] -name = "celestia-types" -version = "0.20.0" -source = "git+https://github.com/celestiaorg/lumina.git?rev=e114c6f#e114c6ffd605ae170c6013e9ef68f3a946404722" -dependencies = [ - "base64", - "bech32", - "bitvec", - "blockstore", - "bytes", - "celestia-proto", - "cid", - "const_format", - "enum_dispatch", - "getrandom 0.2.15", - "k256", - "leopard-codec", - "lumina-utils", - "multihash", - "nmt-rs", - "prost", - "rust_decimal", - "serde", - "serde_repr", - "sha2 0.10.8", - "tendermint", - "tendermint-proto", - "thiserror 2.0.12", - "time", -] - -[[package]] -name = "cesu8" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d43a04d8753f35258c91f8ec639f792891f748a1edbd759cf1dcea3382ad83c" - -[[package]] -name = "cfg-if" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" - -[[package]] -name = "cfg_aliases" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" - -[[package]] -name = "cid" -version = "0.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3147d8272e8fa0ccd29ce51194dd98f79ddfb8191ba9e3409884e751798acf3a" -dependencies = [ - "core2", - "multibase", - "multihash", - "unsigned-varint", -] - -[[package]] -name = "combine" -version = "4.6.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba5a308b75df32fe02788e748662718f03fde005016435c444eea572398219fd" -dependencies = [ - "bytes", - "futures-core", - "memchr", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "const-oid" -version = "0.9.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2459377285ad874054d797f3ccebf984978aa39129f6eafde5cdc8315b612f8" - -[[package]] -name = "const_format" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "126f97965c8ad46d6d9163268ff28432e8f6a1196a55578867832e3049df63dd" -dependencies = [ - "const_format_proc_macros", -] - -[[package]] -name = "const_format_proc_macros" -version = "0.2.34" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1d57c2eccfb16dbac1f4e61e206105db5820c9d26c3c472bc17c774259ef7744" -dependencies = [ - "proc-macro2", - "quote", - "unicode-xid", -] - -[[package]] -name = "core-foundation" -version = "0.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b2a6cd9ae233e7f62ba4e9353e81a88df7fc8a5987b8d445b4d90c879bd156f6" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "core-foundation-sys" -version = "0.8.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "773648b94d0e5d620f64f280777445740e61fe701025087ec8b57f45c791888b" - -[[package]] -name = "core2" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b49ba7ef1ad6107f8824dbe97de947cbaac53c44e7f9756a1fba0d37c1eec505" -dependencies = [ - "memchr", -] - -[[package]] -name = "cpufeatures" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "59ed5838eebb26a2bb2e58f6d5b5316989ae9d08bab10e0e6d103e656d1b0280" -dependencies = [ - "libc", -] - -[[package]] -name = "crossbeam-utils" -version = "0.8.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0a5c400df2834b80a4c3327b3aad3a4c4cd4de0629063962b03235697506a28" - -[[package]] -name = "crypto-bigint" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dc92fb57ca44df6db8059111ab3af99a63d5d0f8375d9972e319a379c6bab76" -dependencies = [ - "generic-array", - "rand_core 0.6.4", - "subtle", - "zeroize", -] - -[[package]] -name = "crypto-common" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bfb12502f3fc46cca1bb51ac28df9d618d813cdc3d2f25b9fe775a34af26bb3" -dependencies = [ - "generic-array", - "typenum", -] - -[[package]] -name = "curve25519-dalek-ng" -version = "4.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1c359b7249347e46fb28804470d071c921156ad62b3eef5d34e2ba867533dec8" -dependencies = [ - "byteorder", - "digest 0.9.0", - "rand_core 0.6.4", - "subtle-ng", - "zeroize", -] - -[[package]] -name = "dashmap" -version = "6.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5041cc499144891f3790297212f32a74fb938e5136a14943f338ef9e0ae276cf" -dependencies = [ - "cfg-if", - "crossbeam-utils", - "hashbrown 0.14.5", - "lock_api", - "once_cell", - "parking_lot_core", -] - -[[package]] -name = "data-encoding" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "575f75dfd25738df5b91b8e43e14d44bda14637a58fae779fd2b064f8bf3e010" - -[[package]] -name = "data-encoding-macro" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9f9724adfcf41f45bf652b3995837669d73c4d49a1b5ac1ff82905ac7d9b5558" -dependencies = [ - "data-encoding", - "data-encoding-macro-internal", -] - -[[package]] -name = "data-encoding-macro-internal" -version = "0.1.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18e4fdb82bd54a12e42fb58a800dcae6b9e13982238ce2296dc3570b92148e1f" -dependencies = [ - "data-encoding", - "syn", -] - -[[package]] -name = "der" -version = "0.7.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f55bf8e7b65898637379c1b74eb1551107c8294ed26d855ceb9fd1a09cfc9bc0" -dependencies = [ - "const-oid", - "zeroize", -] - -[[package]] -name = "deranged" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28cfac68e08048ae1883171632c2aef3ebc555621ae56fbccce1cbf22dd7f058" -dependencies = [ - "powerfmt", -] - -[[package]] -name = "digest" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3dd60d1080a57a05ab032377049e0591415d2b31afd7028356dbf3cc6dcb066" -dependencies = [ - "generic-array", -] - -[[package]] -name = "digest" -version = "0.10.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ed9a281f7bc9b7576e61468ba615a66a5c8cfdff42420a70aa82701a3b1e292" -dependencies = [ - "block-buffer 0.10.4", - "const-oid", - "crypto-common", - "subtle", -] - -[[package]] -name = "displaydoc" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97369cbbc041bc366949bc74d34658d6cda5621039731c6310521892a3a20ae0" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "ecdsa" -version = "0.16.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee27f32b5c5292967d2d4a9d7f1e0b0aed2c15daded5a60300e4abb9d8020bca" -dependencies = [ - "der", - "digest 0.10.7", - "elliptic-curve", - "rfc6979", - "signature", - "spki", -] - -[[package]] -name = "ed25519" -version = "2.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "115531babc129696a58c64a4fef0a8bf9e9698629fb97e9e40767d235cfbcd53" -dependencies = [ - "pkcs8", - "signature", -] - -[[package]] -name = "ed25519-consensus" -version = "2.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3c8465edc8ee7436ffea81d21a019b16676ee3db267aa8d5a8d729581ecf998b" -dependencies = [ - "curve25519-dalek-ng", - "hex", - "rand_core 0.6.4", - "serde", - "sha2 0.9.9", - "thiserror 1.0.69", - "zeroize", -] - -[[package]] -name = "either" -version = "1.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48c757948c5ede0e46177b7add2e67155f70e33c07fea8284df6576da70b3719" - -[[package]] -name = "elliptic-curve" -version = "0.13.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b5e6043086bf7973472e0c7dff2142ea0b680d30e18d9cc40f267efbf222bd47" -dependencies = [ - "base16ct", - "crypto-bigint", - "digest 0.10.7", - "ff", - "generic-array", - "group", - "pkcs8", - "rand_core 0.6.4", - "sec1", - "subtle", - "zeroize", -] - -[[package]] -name = "enum_dispatch" -version = "0.3.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa18ce2bc66555b3218614519ac839ddb759a7d6720732f979ef8d13be147ecd" -dependencies = [ - "once_cell", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "equivalent" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "877a4ace8713b0bcf2a4e7eec82529c029f1d0619886d18145fea96c3ffe5c0f" - -[[package]] -name = "errno" -version = "0.3.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d" -dependencies = [ - "libc", - "windows-sys 0.59.0", -] - -[[package]] -name = "fastrand" -version = "2.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37909eebbb50d72f9059c3b6d82c0463f2ff062c9e95845c43a6c9c0355411be" - -[[package]] -name = "ff" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c0b50bfb653653f9ca9095b427bed08ab8d75a137839d9ad64eb11810d5b6393" -dependencies = [ - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "find-msvc-tools" -version = "0.1.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5baebc0774151f905a1a2cc41989300b1e6fbb29aff0ceffa1064fdd3088d582" - -[[package]] -name = "fixedbitset" -version = "0.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0ce7134b9999ecaf8bcd65542e436736ef32ddca1b3e06094cb6ec5755203b80" - -[[package]] -name = "flex-error" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c606d892c9de11507fa0dcffc116434f94e105d0bbdc4e405b61519464c49d7b" -dependencies = [ - "paste", -] - -[[package]] -name = "fnv" -version = "1.0.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f9eec918d3f24069decb9af1554cad7c880e2da24a9afd88aca000531ab82c1" - -[[package]] -name = "form_urlencoded" -version = "1.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13624c2627564efccf4934284bdd98cbaa14e79b0b5a141218e507b3a823456" -dependencies = [ - "percent-encoding", -] - -[[package]] -name = "funty" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6d5a32815ae3f33302d95fdcb2ce17862f8c65363dcfd29360480ba1001fc9c" - -[[package]] -name = "futures" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "65bc07b1a8bc7c85c5f2e110c476c7389b4554ba72af57d8445ea63a576b0876" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-sink", - "futures-task", - "futures-util", -] - -[[package]] -name = "futures-channel" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dff15bf788c671c1934e366d07e30c1814a8ef514e1af724a602e8a2fbe1b10" -dependencies = [ - "futures-core", - "futures-sink", -] - -[[package]] -name = "futures-core" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f29059c0c2090612e8d742178b0580d2dc940c837851ad723096f87af6663e" - -[[package]] -name = "futures-io" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e5c1b78ca4aae1ac06c48a526a655760685149f0d465d21f37abfe57ce075c6" - -[[package]] -name = "futures-macro" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "162ee34ebcb7c64a8abebc059ce0fee27c2262618d7b60ed8faf72fef13c3650" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "futures-sink" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e575fab7d1e0dcb8d0c7bcf9a63ee213816ab51902e6d244a95819acacf1d4f7" - -[[package]] -name = "futures-task" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988" - -[[package]] -name = "futures-timer" -version = "3.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24" - -[[package]] -name = "futures-util" -version = "0.3.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9fa08315bb612088cc391249efdc3bc77536f16c91f6cf495e6fbe85b20a4a81" -dependencies = [ - "futures-channel", - "futures-core", - "futures-io", - "futures-macro", - "futures-sink", - "futures-task", - "memchr", - "pin-project-lite", - "pin-utils", - "slab", -] - -[[package]] -name = "generic-array" -version = "0.14.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85649ca51fd72272d7821adaf274ad91c288277713d9c18820d8499a7ff69e9a" -dependencies = [ - "typenum", - "version_check", - "zeroize", -] - -[[package]] -name = "getrandom" -version = "0.2.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c4567c8db10ae91089c99af84c68c38da3ec2f087c3f82960bcdbf3656b6f4d7" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "wasi", - "wasm-bindgen", -] - -[[package]] -name = "getrandom" -version = "0.3.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "899def5c37c4fd7b2664648c28120ecec138e4d395b459e5ca34f9cce2dd77fd" -dependencies = [ - "cfg-if", - "js-sys", - "libc", - "r-efi", - "wasip2", - "wasm-bindgen", -] - -[[package]] -name = "gloo-net" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c06f627b1a58ca3d42b45d6104bf1e1a03799df472df00988b6ba21accc10580" -dependencies = [ - "gloo-utils", - "http 1.4.0", - "js-sys", - "serde", - "serde_json", - "thiserror 1.0.69", - "wasm-bindgen", - "wasm-bindgen-futures", - "web-sys", -] - -[[package]] -name = "gloo-timers" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbb143cf96099802033e0d4f4963b19fd2e0b728bcf076cd9cf7f6634f092994" -dependencies = [ - "futures-channel", - "futures-core", - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "gloo-utils" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b5555354113b18c547c1d3a98fbf7fb32a9ff4f6fa112ce823a21641a0ba3aa" -dependencies = [ - "js-sys", - "serde", - "serde_json", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "group" -version = "0.13.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0f9ef7462f7c099f518d754361858f86d8a07af53ba9af0fe635bbccb151a63" -dependencies = [ - "ff", - "rand_core 0.6.4", - "subtle", -] - -[[package]] -name = "h2" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2f44da3a8150a6703ed5d34e164b875fd14c2cdab9af1252a9a1020bde2bdc54" -dependencies = [ - "atomic-waker", - "bytes", - "fnv", - "futures-core", - "futures-sink", - "http 1.4.0", - "indexmap", - "slab", - "tokio", - "tokio-util", - "tracing", -] - -[[package]] -name = "hashbrown" -version = "0.14.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" - -[[package]] -name = "hashbrown" -version = "0.15.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf151400ff0baff5465007dd2f3e717f3fe502074ca563069ce3a6629d07b289" - -[[package]] -name = "heck" -version = "0.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2304e00983f87ffb38b55b444b5e3b60a884b5d30c0fca7d82fe33449bbe55ea" - -[[package]] -name = "hex" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f24254aa9a54b5c858eaee2f5bccdb46aaf0e486a595ed5fd8f86ba55232a70" - -[[package]] -name = "hkdf" -version = "0.12.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b5f8eb2ad728638ea2c7d47a21db23b7b58a72ed6a38256b8a1849f15fbbdf7" -dependencies = [ - "hmac", -] - -[[package]] -name = "hmac" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c49c37c09c17a53d937dfbb742eb3a961d65a994e6bcdcf37e7399d0cc8ab5e" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "http" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "601cbb57e577e2f5ef5be8e7b83f0f63994f25aa94d673e54a92d5c516d101f1" -dependencies = [ - "bytes", - "fnv", - "itoa", -] - -[[package]] -name = "http" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3ba2a386d7f85a81f119ad7498ebe444d2e22c2af0b86b069416ace48b3311a" -dependencies = [ - "bytes", - "itoa", -] - -[[package]] -name = "http-body" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7ceab25649e9960c0311ea418d17bee82c0dcec1bd053b5f9a66e265a693bed2" -dependencies = [ - "bytes", - "http 0.2.12", - "pin-project-lite", -] - -[[package]] -name = "http-body" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1efedce1fb8e6913f23e0c92de8e62cd5b772a67e7b3946df930a62566c93184" -dependencies = [ - "bytes", - "http 1.4.0", -] - -[[package]] -name = "http-body-util" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b021d93e26becf5dc7e1b75b1bed1fd93124b374ceb73f43d4d4eafec896a64a" -dependencies = [ - "bytes", - "futures-core", - "http 1.4.0", - "http-body 1.0.1", - "pin-project-lite", -] - -[[package]] -name = "http-range-header" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "add0ab9360ddbd88cfeb3bd9574a1d85cfdfa14db10b3e21d3700dbc4328758f" - -[[package]] -name = "httparse" -version = "1.10.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6dbf3de79e51f3d586ab4cb9d5c3e2c14aa28ed23d180cf89b4df0454a69cc87" - -[[package]] -name = "httpdate" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "df3b46402a9d5adb4c86a0cf463f42e19994e3ee891101b1841f30a545cb49a9" - -[[package]] -name = "hyper" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab2d4f250c3d7b1c9fcdff1cece94ea4e2dfbec68614f7b87cb205f24ca9d11" -dependencies = [ - "atomic-waker", - "bytes", - "futures-channel", - "futures-core", - "h2", - "http 1.4.0", - "http-body 1.0.1", - "httparse", - "httpdate", - "itoa", - "pin-project-lite", - "pin-utils", - "smallvec", - "tokio", - "want", -] - -[[package]] -name = "hyper-rustls" -version = "0.27.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3c93eb611681b207e1fe55d5a71ecf91572ec8a6705cdb6857f7d8d5242cf58" -dependencies = [ - "http 1.4.0", - "hyper", - "hyper-util", - "log", - "rustls", - "rustls-pki-types", - "tokio", - "tokio-rustls", - "tower-service", -] - -[[package]] -name = "hyper-util" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "96547c2556ec9d12fb1578c4eaf448b04993e7fb79cbaad930a656880a6bdfa0" -dependencies = [ - "bytes", - "futures-channel", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "hyper", - "libc", - "pin-project-lite", - "socket2", - "tokio", - "tower-service", - "tracing", -] - -[[package]] -name = "icu_collections" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "db2fa452206ebee18c4b5c2274dbf1de17008e874b4dc4f0aea9d01ca79e4526" -dependencies = [ - "displaydoc", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_locid" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13acbb8371917fc971be86fc8057c41a64b521c184808a698c02acc242dbf637" -dependencies = [ - "displaydoc", - "litemap", - "tinystr", - "writeable", - "zerovec", -] - -[[package]] -name = "icu_locid_transform" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01d11ac35de8e40fdeda00d9e1e9d92525f3f9d887cdd7aa81d727596788b54e" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_locid_transform_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_locid_transform_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7515e6d781098bf9f7205ab3fc7e9709d34554ae0b21ddbcb5febfa4bc7df11d" - -[[package]] -name = "icu_normalizer" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ce3e0da2ec68599d193c93d088142efd7f9c5d6fc9b803774855747dc6a84f" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_normalizer_data", - "icu_properties", - "icu_provider", - "smallvec", - "utf16_iter", - "utf8_iter", - "write16", - "zerovec", -] - -[[package]] -name = "icu_normalizer_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c5e8338228bdc8ab83303f16b797e177953730f601a96c25d10cb3ab0daa0cb7" - -[[package]] -name = "icu_properties" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93d6020766cfc6302c15dbbc9c8778c37e62c14427cb7f6e601d849e092aeef5" -dependencies = [ - "displaydoc", - "icu_collections", - "icu_locid_transform", - "icu_properties_data", - "icu_provider", - "tinystr", - "zerovec", -] - -[[package]] -name = "icu_properties_data" -version = "1.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85fb8799753b75aee8d2a21d7c14d9f38921b54b3dbda10f5a3c7a7b82dba5e2" - -[[package]] -name = "icu_provider" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6ed421c8a8ef78d3e2dbc98a973be2f3770cb42b606e3ab18d6237c4dfde68d9" -dependencies = [ - "displaydoc", - "icu_locid", - "icu_provider_macros", - "stable_deref_trait", - "tinystr", - "writeable", - "yoke", - "zerofrom", - "zerovec", -] - -[[package]] -name = "icu_provider_macros" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1ec89e9337638ecdc08744df490b221a7399bf8d164eb52a665454e60e075ad6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "idna" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "686f825264d630750a544639377bae737628043f20d38bbc029e8f29ea968a7e" -dependencies = [ - "idna_adapter", - "smallvec", - "utf8_iter", -] - -[[package]] -name = "idna_adapter" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "daca1df1c957320b2cf139ac61e7bd64fed304c5040df000a745aa1de3b4ef71" -dependencies = [ - "icu_normalizer", - "icu_properties", -] - -[[package]] -name = "indexmap" -version = "2.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3954d50fe15b02142bf25d3b8bdadb634ec3948f103d04ffe3031bc8fe9d7058" -dependencies = [ - "equivalent", - "hashbrown 0.15.2", -] - -[[package]] -name = "itertools" -version = "0.12.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569" -dependencies = [ - "either", -] - -[[package]] -name = "itoa" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4a5f13b858c8d314ee3e8f639011f7ccefe71f97f96e50151fb991f267928e2c" - -[[package]] -name = "jni" -version = "0.21.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a87aa2bb7d2af34197c04845522473242e1aa17c12f4935d5856491a7fb8c97" -dependencies = [ - "cesu8", - "cfg-if", - "combine", - "jni-sys", - "log", - "thiserror 1.0.69", - "walkdir", - "windows-sys 0.45.0", -] - -[[package]] -name = "jni-sys" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8eaf4bc02d17cbdd7ff4c7438cafcdf7fb9a4613313ad11b4f8fefe7d3fa0130" - -[[package]] -name = "js-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "464a3709c7f55f1f721e5389aa6ea4e3bc6aba669353300af094b29ffbdde1d8" -dependencies = [ - "once_cell", - "wasm-bindgen", -] - -[[package]] -name = "jsonrpsee" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f3f48dc3e6b8bd21e15436c1ddd0bc22a6a54e8ec46fedd6adf3425f396ec6a" -dependencies = [ - "jsonrpsee-core", - "jsonrpsee-http-client", - "jsonrpsee-proc-macros", - "jsonrpsee-server", - "jsonrpsee-types", - "jsonrpsee-ws-client", - "tokio", - "tracing", -] - -[[package]] -name = "jsonrpsee-client-transport" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cf36eb27f8e13fa93dcb50ccb44c417e25b818cfa1a481b5470cd07b19c60b98" -dependencies = [ - "base64", - "futures-util", - "http 1.4.0", - "jsonrpsee-core", - "pin-project", - "rustls", - "rustls-pki-types", - "rustls-platform-verifier", - "soketto", - "thiserror 2.0.12", - "tokio", - "tokio-rustls", - "tokio-util", - "tracing", - "url", -] - -[[package]] -name = "jsonrpsee-core" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316c96719901f05d1137f19ba598b5fe9c9bc39f4335f67f6be8613921946480" -dependencies = [ - "async-trait", - "bytes", - "futures-timer", - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "jsonrpsee-types", - "parking_lot", - "pin-project", - "rand 0.9.2", - "rustc-hash", - "serde", - "serde_json", - "thiserror 2.0.12", - "tokio", - "tokio-stream", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "jsonrpsee-http-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "790bedefcec85321e007ff3af84b4e417540d5c87b3c9779b9e247d1bcc3dab8" -dependencies = [ - "base64", - "http-body 1.0.1", - "hyper", - "hyper-rustls", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "rustls", - "rustls-platform-verifier", - "serde", - "serde_json", - "thiserror 2.0.12", - "tokio", - "tower 0.5.3", - "url", -] - -[[package]] -name = "jsonrpsee-proc-macros" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2da3f8ab5ce1bb124b6d082e62dffe997578ceaf0aeb9f3174a214589dc00f07" -dependencies = [ - "heck", - "proc-macro-crate", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "jsonrpsee-server" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4c51b7c290bb68ce3af2d029648148403863b982f138484a73f02a9dd52dbd7f" -dependencies = [ - "futures-util", - "http 1.4.0", - "http-body 1.0.1", - "http-body-util", - "hyper", - "hyper-util", - "jsonrpsee-core", - "jsonrpsee-types", - "pin-project", - "route-recognizer", - "serde", - "serde_json", - "soketto", - "thiserror 2.0.12", - "tokio", - "tokio-stream", - "tokio-util", - "tower 0.5.3", - "tracing", -] - -[[package]] -name = "jsonrpsee-types" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bc88ff4688e43cc3fa9883a8a95c6fa27aa2e76c96e610b737b6554d650d7fd5" -dependencies = [ - "http 1.4.0", - "serde", - "serde_json", - "thiserror 2.0.12", -] - -[[package]] -name = "jsonrpsee-ws-client" -version = "0.26.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b6fceceeb05301cc4c065ab3bd2fa990d41ff4eb44e4ca1b30fa99c057c3e79" -dependencies = [ - "http 1.4.0", - "jsonrpsee-client-transport", - "jsonrpsee-core", - "jsonrpsee-types", - "tower 0.5.3", - "url", -] - -[[package]] -name = "k256" -version = "0.13.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f6e3919bbaa2945715f0bb6d3934a173d1e9a59ac23767fbaaef277265a7411b" -dependencies = [ - "cfg-if", - "ecdsa", - "elliptic-curve", - "once_cell", - "sha2 0.10.8", - "signature", -] - -[[package]] -name = "lazy_static" -version = "1.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbd2bcb4c963f2ddae06a2efc7e9f3591312473c50c6685e1f298068316e66fe" - -[[package]] -name = "leopard-codec" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b397c7217467c5e5582fe413bc2f0e9f804367af8bc0f0374dc966f99d00f9c" -dependencies = [ - "bytes", - "thiserror 2.0.12", -] - -[[package]] -name = "libc" -version = "0.2.181" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "459427e2af2b9c839b132acb702a1c654d95e10f8c326bfc2ad11310e458b1c5" - -[[package]] -name = "libp2p-identity" -version = "0.2.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3104e13b51e4711ff5738caa1fb54467c8604c2e94d607e27745bcf709068774" -dependencies = [ - "bs58", - "hkdf", - "multihash", - "quick-protobuf", - "sha2 0.10.8", - "thiserror 2.0.12", - "tracing", -] - -[[package]] -name = "linux-raw-sys" -version = "0.9.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe7db12097d22ec582439daf8618b8fdd1a7bef6270e9af3b1ebcd30893cf413" - -[[package]] -name = "litemap" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "23fb14cb19457329c82206317a5663005a4d404783dc74f4252769b0d5f42856" - -[[package]] -name = "localestia" -version = "0.1.0" -dependencies = [ - "anyhow", - "async-trait", - "celestia-rpc", - "celestia-types", - "ed25519-consensus", - "hex", - "jsonrpsee", - "jsonrpsee-core", - "jsonrpsee-types", - "once_cell", - "rand 0.8.5", - "redis", - "serde", - "serde_json", - "sha2 0.10.8", - "tendermint", - "thiserror 1.0.69", - "tokio", - "tower 0.4.13", - "tower-http", - "tracing", - "tracing-subscriber", - "uuid", -] - -[[package]] -name = "lock_api" -version = "0.4.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "07af8b9cdd281b7915f413fa73f29ebd5d55d0d3f0155584dade1ff18cea1b17" -dependencies = [ - "autocfg", - "scopeguard", -] - -[[package]] -name = "log" -version = "0.4.27" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13dc2df351e3202783a1fe0d44375f7295ffb4049267b0f3018346dc122a1d94" - -[[package]] -name = "logos" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ff472f899b4ec2d99161c51f60ff7075eeb3097069a36050d8037a6325eb8154" -dependencies = [ - "logos-derive", -] - -[[package]] -name = "logos-codegen" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "192a3a2b90b0c05b27a0b2c43eecdb7c415e29243acc3f89cc8247a5b693045c" -dependencies = [ - "beef", - "fnv", - "lazy_static", - "proc-macro2", - "quote", - "regex-syntax", - "rustc_version", - "syn", -] - -[[package]] -name = "logos-derive" -version = "0.15.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "605d9697bcd5ef3a42d38efc51541aa3d6a4a25f7ab6d1ed0da5ac632a26b470" -dependencies = [ - "logos-codegen", -] - -[[package]] -name = "lumina-utils" -version = "0.5.2" -source = "git+https://github.com/celestiaorg/lumina.git?rev=e114c6f#e114c6ffd605ae170c6013e9ef68f3a946404722" -dependencies = [ - "js-sys", -] - -[[package]] -name = "memchr" -version = "2.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "78ca9ab1a0babb1e7d5695e3530886289c18cf2f87ec19a575a0abdce112e3a3" - -[[package]] -name = "miette" -version = "7.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a955165f87b37fd1862df2a59547ac542c77ef6d17c666f619d1ad22dd89484" -dependencies = [ - "cfg-if", - "miette-derive", - "thiserror 1.0.69", - "unicode-width", -] - -[[package]] -name = "miette-derive" -version = "7.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf45bf44ab49be92fd1227a3be6fc6f617f1a337c06af54981048574d8783147" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "mio" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2886843bf800fba2e3377cff24abf6379b4c4d5c6681eaf9ea5b0d15090450bd" -dependencies = [ - "libc", - "wasi", - "windows-sys 0.52.0", -] - -[[package]] -name = "multiaddr" -version = "0.18.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fe6351f60b488e04c1d21bc69e56b89cb3f5e8f5d22557d6e8031bdfd79b6961" -dependencies = [ - "arrayref", - "byteorder", - "data-encoding", - "libp2p-identity", - "multibase", - "multihash", - "percent-encoding", - "serde", - "static_assertions", - "unsigned-varint", - "url", -] - -[[package]] -name = "multibase" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b3539ec3c1f04ac9748a260728e855f261b4977f5c3406612c884564f329404" -dependencies = [ - "base-x", - "data-encoding", - "data-encoding-macro", -] - -[[package]] -name = "multihash" -version = "0.19.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b430e7953c29dd6a09afc29ff0bb69c6e306329ee6794700aee27b76a1aea8d" -dependencies = [ - "core2", - "unsigned-varint", -] - -[[package]] -name = "multimap" -version = "0.10.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "defc4c55412d89136f966bbb339008b474350e5e6e78d2714439c386b3137a03" - -[[package]] -name = "nmt-rs" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e408e823bdc9b4bb525a61b44e846239833a8f9bd86c03a43e4ca314a5497582" -dependencies = [ - "borsh", - "bytes", - "serde", - "sha2 0.10.8", -] - -[[package]] -name = "nu-ansi-term" -version = "0.46.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77a8165726e8236064dbb45459242600304b42a5ea24ee2948e18e023bf7ba84" -dependencies = [ - "overload", - "winapi", -] - -[[package]] -name = "num-bigint" -version = "0.4.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a5e44f723f1133c9deac646763579fdb3ac745e418f2a7af9cd0c431da1f20b9" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-conv" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - -[[package]] -name = "num-traits" -version = "0.2.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" -dependencies = [ - "autocfg", -] - -[[package]] -name = "once_cell" -version = "1.21.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "42f5e15c9953c5e4ccceeb2e7382a716482c34515315f7b03532b8b4e8393d2d" - -[[package]] -name = "opaque-debug" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" - -[[package]] -name = "openssl-probe" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7c87def4c32ab89d880effc9e097653c8da5d6ef28e6b539d313baaacfbafcbe" - -[[package]] -name = "overload" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39" - -[[package]] -name = "parking_lot" -version = "0.12.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e401f977ab385c9e4e3ab30627d6f26d00e2c73eef317493c4ec6d468726cf8" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall", - "smallvec", - "windows-targets 0.52.6", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" - -[[package]] -name = "petgraph" -version = "0.6.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4c5cc86750666a3ed20bdaf5ca2a0344f9c67674cae0515bec2da16fbaa47db" -dependencies = [ - "fixedbitset", - "indexmap", -] - -[[package]] -name = "pin-project" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.16" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b3cff922bd51709b605d9ead9aa71031d81447142d828eb4a6eba76fe619f9b" - -[[package]] -name = "pin-utils" -version = "0.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" - -[[package]] -name = "pkcs8" -version = "0.10.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f950b2377845cebe5cf8b5165cb3cc1a5e0fa5cfa3e1f7f55707d8fd82e0a7b7" -dependencies = [ - "der", - "spki", -] - -[[package]] -name = "powerfmt" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391" - -[[package]] -name = "ppv-lite86" -version = "0.2.21" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "85eae3c4ed2f50dcfe72643da4befc30deadb458a9b590d720cde2f2b1e97da9" -dependencies = [ - "zerocopy", -] - -[[package]] -name = "prettyplease" -version = "0.2.31" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5316f57387668042f561aae71480de936257848f9c43ce528e311d89a07cadeb" -dependencies = [ - "proc-macro2", - "syn", -] - -[[package]] -name = "proc-macro-crate" -version = "3.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edce586971a4dfaa28950c6f18ed55e0406c1ab88bbce2c6f6293a7aaba73d35" -dependencies = [ - "toml_edit", -] - -[[package]] -name = "proc-macro2" -version = "1.0.94" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a31971752e70b8b2686d7e46ec17fb38dad4051d94024c88df49b667caea9c84" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "prost" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2796faa41db3ec313a31f7624d9286acf277b52de526150b7e69f3debf891ee5" -dependencies = [ - "bytes", - "prost-derive", -] - -[[package]] -name = "prost-build" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be769465445e8c1474e9c5dac2018218498557af32d9ed057325ec9a41ae81bf" -dependencies = [ - "heck", - "itertools", - "log", - "multimap", - "once_cell", - "petgraph", - "prettyplease", - "prost", - "prost-types", - "regex", - "syn", - "tempfile", -] - -[[package]] -name = "prost-derive" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a56d757972c98b346a9b766e3f02746cde6dd1cd1d1d563472929fdd74bec4d" -dependencies = [ - "anyhow", - "itertools", - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "prost-reflect" -version = "0.15.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "37587d5a8a1b3dc9863403d084fc2254b91ab75a702207098837950767e2260b" -dependencies = [ - "logos", - "miette", - "prost", - "prost-types", -] - -[[package]] -name = "prost-types" -version = "0.13.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "52c2c1bf36ddb1a1c396b3601a3cec27c2462e45f07c386894ec3ccf5332bd16" -dependencies = [ - "prost", -] - -[[package]] -name = "protox" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "424c2bd294b69c49b949f3619362bc3c5d28298cd1163b6d1a62df37c16461aa" -dependencies = [ - "bytes", - "miette", - "prost", - "prost-reflect", - "prost-types", - "protox-parse", - "thiserror 2.0.12", -] - -[[package]] -name = "protox-parse" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57927f9dbeeffcce7192404deee6157a640cbb3fe8ac11eabbe571565949ab75" -dependencies = [ - "logos", - "miette", - "prost-types", - "thiserror 2.0.12", -] - -[[package]] -name = "quick-protobuf" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9d6da84cc204722a989e01ba2f6e1e276e190f22263d0cb6ce8526fcdb0d2e1f" -dependencies = [ - "byteorder", -] - -[[package]] -name = "quote" -version = "1.0.40" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1885c039570dc00dcb4ff087a89e185fd56bae234ddc7f056a945bf36467248d" -dependencies = [ - "proc-macro2", -] - -[[package]] -name = "r-efi" -version = "5.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "74765f6d916ee2faa39bc8e68e4f3ed8949b48cccdac59983d287a7cb71ce9c5" - -[[package]] -name = "radium" -version = "0.7.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc33ff2d4973d518d823d61aa239014831e521c75da58e3df4840d3f47749d09" - -[[package]] -name = "rand" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "34af8d1a0e25924bc5b7c43c079c942339d8f0a8b57c39049bef581b46327404" -dependencies = [ - "libc", - "rand_chacha 0.3.1", - "rand_core 0.6.4", -] - -[[package]] -name = "rand" -version = "0.9.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6db2770f06117d490610c7488547d543617b21bfa07796d7a12f6f1bd53850d1" -dependencies = [ - "rand_chacha 0.9.0", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_chacha" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e6c10a63a0fa32252be49d21e7709d4d4baf8d231c2dbce1eaa8141b9b127d88" -dependencies = [ - "ppv-lite86", - "rand_core 0.6.4", -] - -[[package]] -name = "rand_chacha" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3022b5f1df60f26e1ffddd6c66e8aa15de382ae63b3a0c1bfc0e4d3e3f325cb" -dependencies = [ - "ppv-lite86", - "rand_core 0.9.5", -] - -[[package]] -name = "rand_core" -version = "0.6.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec0be4795e2f6a28069bec0b5ff3e2ac9bafc99e6a9a7dc3547996c5c816922c" -dependencies = [ - "getrandom 0.2.15", -] - -[[package]] -name = "rand_core" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "76afc826de14238e6e8c374ddcc1fa19e374fd8dd986b0d2af0d02377261d83c" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "redis" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dbe7f6e08ce1c6a9b21684e643926f6fc3b683bc006cb89afd72a5e0eb16e3a2" -dependencies = [ - "arcstr", - "bytes", - "cfg-if", - "combine", - "futures-util", - "itoa", - "num-bigint", - "percent-encoding", - "pin-project-lite", - "ryu", - "sha1_smol", - "socket2", - "tokio", - "tokio-util", - "url", - "xxhash-rust", -] - -[[package]] -name = "redox_syscall" -version = "0.5.10" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b8c0c260b63a8219631167be35e6a988e9554dbd323f8bd08439c8ed1302bd1" -dependencies = [ - "bitflags", -] - -[[package]] -name = "regex" -version = "1.11.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b544ef1b4eac5dc2db33ea63606ae9ffcfac26c1416a2806ae0bf5f56b201191" -dependencies = [ - "aho-corasick", - "memchr", - "regex-automata", - "regex-syntax", -] - -[[package]] -name = "regex-automata" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "809e8dc61f6de73b46c85f4c96486310fe304c434cfa43669d7b40f711150908" -dependencies = [ - "aho-corasick", - "memchr", - "regex-syntax", -] - -[[package]] -name = "regex-syntax" -version = "0.8.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b15c43186be67a4fd63bee50d0303afffcef381492ebe2c5d87f324e1b8815c" - -[[package]] -name = "rfc6979" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8dd2a808d456c4a54e300a23e9f5a67e122c3024119acbfd73e3bf664491cb2" -dependencies = [ - "hmac", - "subtle", -] - -[[package]] -name = "ring" -version = "0.17.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a4689e6c2294d81e88dc6261c768b63bc4fcdb852be6d1352498b114f61383b7" -dependencies = [ - "cc", - "cfg-if", - "getrandom 0.2.15", - "libc", - "untrusted", - "windows-sys 0.52.0", -] - -[[package]] -name = "ripemd" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd124222d17ad93a644ed9d011a40f4fb64aa54275c08cc216524a9ea82fb09f" -dependencies = [ - "digest 0.10.7", -] - -[[package]] -name = "route-recognizer" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "afab94fb28594581f62d981211a9a4d53cc8130bbcbbb89a0440d9b8e81a7746" - -[[package]] -name = "rust_decimal" -version = "1.39.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "35affe401787a9bd846712274d97654355d21b2a2c092a3139aabe31e9022282" -dependencies = [ - "arrayvec", - "num-traits", - "serde", -] - -[[package]] -name = "rustc-hash" -version = "2.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "357703d41365b4b27c590e3ed91eabb1b663f07c4c084095e60cbed4362dff0d" - -[[package]] -name = "rustc_version" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cfcb3a22ef46e85b45de6ee7e79d063319ebb6594faafcf1c225ea92ab6e9b92" -dependencies = [ - "semver", -] - -[[package]] -name = "rustix" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d97817398dd4bb2e6da002002db259209759911da105da92bec29ccb12cf58bf" -dependencies = [ - "bitflags", - "errno", - "libc", - "linux-raw-sys", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls" -version = "0.23.36" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c665f33d38cea657d9614f766881e4d510e0eda4239891eea56b4cadcf01801b" -dependencies = [ - "log", - "once_cell", - "ring", - "rustls-pki-types", - "rustls-webpki", - "subtle", - "zeroize", -] - -[[package]] -name = "rustls-native-certs" -version = "0.8.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "612460d5f7bea540c490b2b6395d8e34a953e52b491accd6c86c8164c5932a63" -dependencies = [ - "openssl-probe", - "rustls-pki-types", - "schannel", - "security-framework", -] - -[[package]] -name = "rustls-pki-types" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be040f8b0a225e40375822a563fa9524378b9d63112f53e19ffff34df5d33fdd" -dependencies = [ - "zeroize", -] - -[[package]] -name = "rustls-platform-verifier" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19787cda76408ec5404443dc8b31795c87cd8fec49762dc75fa727740d34acc1" -dependencies = [ - "core-foundation", - "core-foundation-sys", - "jni", - "log", - "once_cell", - "rustls", - "rustls-native-certs", - "rustls-platform-verifier-android", - "rustls-webpki", - "security-framework", - "security-framework-sys", - "webpki-root-certs 0.26.11", - "windows-sys 0.59.0", -] - -[[package]] -name = "rustls-platform-verifier-android" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f87165f0995f63a9fbeea62b64d10b4d9d8e78ec6d7d51fb2125fda7bb36788f" - -[[package]] -name = "rustls-webpki" -version = "0.103.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7df23109aa6c1567d1c575b9952556388da57401e4ace1d15f79eedad0d8f53" -dependencies = [ - "ring", - "rustls-pki-types", - "untrusted", -] - -[[package]] -name = "rustversion" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eded382c5f5f786b989652c49544c4877d9f015cc22e145a5ea8ea66c2921cd2" - -[[package]] -name = "ryu" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "28d3b2b1366ec20994f1fd18c3c594f05c5dd4bc44d8bb0c1c632c8d6829481f" - -[[package]] -name = "same-file" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93fc1dc3aaa9bfed95e02e6eadabb4baf7e3078b0bd1b4d7b6b0b68378900502" -dependencies = [ - "winapi-util", -] - -[[package]] -name = "schannel" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "891d81b926048e76efe18581bf793546b4c0eaf8448d72be8de2bbee5fd166e1" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "scopeguard" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "94143f37725109f92c262ed2cf5e59bce7498c01bcc1502d7b9afe439a4e9f49" - -[[package]] -name = "sec1" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d3e97a565f76233a6003f9f5c54be1d9c5bdfa3eccfb189469f11ec4901c47dc" -dependencies = [ - "base16ct", - "der", - "generic-array", - "pkcs8", - "subtle", - "zeroize", -] - -[[package]] -name = "security-framework" -version = "3.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b3297343eaf830f66ede390ea39da1d462b6b0c1b000f420d0a83f898bbbe6ef" -dependencies = [ - "bitflags", - "core-foundation", - "core-foundation-sys", - "libc", - "security-framework-sys", -] - -[[package]] -name = "security-framework-sys" -version = "2.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cc1f0cbffaac4852523ce30d8bd3c5cdc873501d96ff467ca09b6767bb8cd5c0" -dependencies = [ - "core-foundation-sys", - "libc", -] - -[[package]] -name = "semver" -version = "1.0.26" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "56e6fa9c48d24d85fb3de5ad847117517440f6beceb7798af16b4a87d616b8d0" - -[[package]] -name = "send_wrapper" -version = "0.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cd0b0ec5f1c1ca621c432a25813d8d60c88abe6d3e08a3eb9cf37d97a0fe3d73" -dependencies = [ - "futures-core", -] - -[[package]] -name = "serde" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9a8e94ea7f378bd32cbbd37198a4a91436180c5bb472411e48b5ec2e2124ae9e" -dependencies = [ - "serde_core", - "serde_derive", -] - -[[package]] -name = "serde_bytes" -version = "0.11.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437fd221bde2d4ca316d61b90e337e9e702b3820b87d63caa9ba6c02bd06d96" -dependencies = [ - "serde", -] - -[[package]] -name = "serde_core" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d385c7d4ca58e59fc732af25c3983b67ac852c1a25000afe1175de458b67ad" -dependencies = [ - "serde_derive", -] - -[[package]] -name = "serde_derive" -version = "1.0.228" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d540f220d3187173da220f885ab66608367b6574e925011a9353e4badda91d79" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "serde_json" -version = "1.0.149" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "83fc039473c5595ace860d8c4fafa220ff474b3fc6bfdb4293327f1a37e94d86" -dependencies = [ - "itoa", - "memchr", - "serde", - "serde_core", - "zmij", -] - -[[package]] -name = "serde_repr" -version = "0.1.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "175ee3e80ae9982737ca543e96133087cbd9a485eecc3bc4de9c1a37b47ea59c" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "sha1" -version = "0.10.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e3bf829a2d51ab4a5ddf1352d8470c140cadc8301b2ae1789db023f01cedd6ba" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sha1_smol" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bbfa15b3dddfee50a0fff136974b3e1bde555604ba463834a7eb7deb6417705d" - -[[package]] -name = "sha2" -version = "0.9.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4d58a1e1bf39749807d89cf2d98ac2dfa0ff1cb3faa38fbb64dd88ac8013d800" -dependencies = [ - "block-buffer 0.9.0", - "cfg-if", - "cpufeatures", - "digest 0.9.0", - "opaque-debug", -] - -[[package]] -name = "sha2" -version = "0.10.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "793db75ad2bcafc3ffa7c68b215fee268f537982cd901d132f89c6343f3a3dc8" -dependencies = [ - "cfg-if", - "cpufeatures", - "digest 0.10.7", -] - -[[package]] -name = "sharded-slab" -version = "0.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40ca3c46823713e0d4209592e8d6e826aa57e928f09752619fc696c499637f6" -dependencies = [ - "lazy_static", -] - -[[package]] -name = "shlex" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64" - -[[package]] -name = "signal-hook-registry" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a9e9e0b4211b72e7b8b6e85c807d36c212bdb33ea8587f7569562a84df5465b1" -dependencies = [ - "libc", -] - -[[package]] -name = "signature" -version = "2.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77549399552de45a898a580c1b41d445bf730df867cc44e6c0233bbc4b8329de" -dependencies = [ - "digest 0.10.7", - "rand_core 0.6.4", -] - -[[package]] -name = "slab" -version = "0.4.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8f92a496fb766b417c996b9c5e57daf2f7ad3b0bebe1ccfca4856390e3d3bb67" -dependencies = [ - "autocfg", -] - -[[package]] -name = "smallvec" -version = "1.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7fcf8323ef1faaee30a44a340193b1ac6814fd9b7b4e88e9d4519a3e4abe1cfd" - -[[package]] -name = "socket2" -version = "0.6.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86f4aa3ad99f2088c990dfa82d367e19cb29268ed67c574d10d0a4bfe71f07e0" -dependencies = [ - "libc", - "windows-sys 0.60.2", -] - -[[package]] -name = "soketto" -version = "0.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e859df029d160cb88608f5d7df7fb4753fd20fdfb4de5644f3d8b8440841721" -dependencies = [ - "base64", - "bytes", - "futures", - "http 1.4.0", - "httparse", - "log", - "rand 0.8.5", - "sha1", -] - -[[package]] -name = "spki" -version = "0.7.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d91ed6c858b01f942cd56b37a94b3e0a1798290327d1236e4d9cf4eaca44d29d" -dependencies = [ - "base64ct", - "der", -] - -[[package]] -name = "stable_deref_trait" -version = "1.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a8f112729512f8e442d81f95a8a7ddf2b7c6b8a1a6f509a95864142b30cab2d3" - -[[package]] -name = "static_assertions" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" - -[[package]] -name = "subtle" -version = "2.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" - -[[package]] -name = "subtle-encoding" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dcb1ed7b8330c5eed5441052651dd7a12c75e2ed88f2ec024ae1fa3a5e59945" -dependencies = [ - "zeroize", -] - -[[package]] -name = "subtle-ng" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "734676eb262c623cec13c3155096e08d1f8f29adce39ba17948b18dad1e54142" - -[[package]] -name = "syn" -version = "2.0.100" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b09a44accad81e1ba1cd74a32461ba89dee89095ba17b32f5d03683b1b1fc2a0" -dependencies = [ - "proc-macro2", - "quote", - "unicode-ident", -] - -[[package]] -name = "sync_wrapper" -version = "1.0.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bf256ce5efdfa370213c1dabab5935a12e49f2c58d15e9eac2870d3b4f27263" - -[[package]] -name = "synstructure" -version = "0.13.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8af7666ab7b6390ab78131fb5b0fce11d6b7a6951602017c35fa82800708971" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tap" -version = "1.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "55937e1799185b12863d447f42597ed69d9928686b8d88a1df17376a097d8369" - -[[package]] -name = "tempfile" -version = "3.19.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7437ac7763b9b123ccf33c338a5cc1bac6f69b45a136c19bdd8a65e3916435bf" -dependencies = [ - "fastrand", - "getrandom 0.3.4", - "once_cell", - "rustix", - "windows-sys 0.59.0", -] - -[[package]] -name = "tendermint" -version = "0.40.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ab2972a56891bc9173eaebdd51290bc848e448aa281881f3a4712bd8fe1899b3" -dependencies = [ - "bytes", - "digest 0.10.7", - "ed25519", - "ed25519-consensus", - "flex-error", - "futures", - "k256", - "num-traits", - "once_cell", - "prost", - "ripemd", - "serde", - "serde_bytes", - "serde_json", - "serde_repr", - "sha2 0.10.8", - "signature", - "subtle", - "subtle-encoding", - "tendermint-proto", - "time", - "zeroize", -] - -[[package]] -name = "tendermint-proto" -version = "0.40.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca44b9eaaa98e8440fbafe5593b8a32a1c395c094ac566b3eb50497f8bd2bee0" -dependencies = [ - "bytes", - "flex-error", - "prost", - "serde", - "serde_bytes", - "subtle-encoding", - "time", -] - -[[package]] -name = "thiserror" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6aaf5339b578ea85b50e080feb250a3e8ae8cfcdff9a461c9ec2904bc923f52" -dependencies = [ - "thiserror-impl 1.0.69", -] - -[[package]] -name = "thiserror" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "567b8a2dae586314f7be2a752ec7474332959c6460e02bde30d702a66d488708" -dependencies = [ - "thiserror-impl 2.0.12", -] - -[[package]] -name = "thiserror-impl" -version = "1.0.69" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fee6c4efc90059e10f81e6d42c60a18f76588c3d74cb83a0b242a2b6c7504c1" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thiserror-impl" -version = "2.0.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7f7cf42b4507d8ea322120659672cf1b9dbb93f8f2d4ecfd6e51350ff5b17a1d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "thread_local" -version = "1.1.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b9ef9bad013ada3808854ceac7b46812a6465ba368859a37e2100283d2d719c" -dependencies = [ - "cfg-if", - "once_cell", -] - -[[package]] -name = "time" -version = "0.3.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40" -dependencies = [ - "deranged", - "num-conv", - "powerfmt", - "serde", - "time-core", - "time-macros", -] - -[[package]] -name = "time-core" -version = "0.1.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c" - -[[package]] -name = "time-macros" -version = "0.2.22" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49" -dependencies = [ - "num-conv", - "time-core", -] - -[[package]] -name = "tinystr" -version = "0.7.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9117f5d4db391c1cf6927e7bea3db74b9a1c1add8f7eda9ffd5364f40f57b82f" -dependencies = [ - "displaydoc", - "zerovec", -] - -[[package]] -name = "tinyvec" -version = "1.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09b3661f17e86524eccd4371ab0429194e0d7c008abb45f7a7495b1719463c71" -dependencies = [ - "tinyvec_macros", -] - -[[package]] -name = "tinyvec_macros" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20" - -[[package]] -name = "tokio" -version = "1.49.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72a2903cd7736441aac9df9d7688bd0ce48edccaadf181c3b90be801e81d3d86" -dependencies = [ - "bytes", - "libc", - "mio", - "parking_lot", - "pin-project-lite", - "signal-hook-registry", - "socket2", - "tokio-macros", - "windows-sys 0.61.2", -] - -[[package]] -name = "tokio-macros" -version = "2.6.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "af407857209536a95c8e56f8231ef2c2e2aff839b22e07a1ffcbc617e9db9fa5" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tokio-rustls" -version = "0.26.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1729aa945f29d91ba541258c8df89027d5792d85a8841fb65e8bf0f4ede4ef61" -dependencies = [ - "rustls", - "tokio", -] - -[[package]] -name = "tokio-stream" -version = "0.1.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eca58d7bba4a75707817a2c44174253f9236b2d5fbd055602e9d5c07c139a047" -dependencies = [ - "futures-core", - "pin-project-lite", - "tokio", - "tokio-util", -] - -[[package]] -name = "tokio-util" -version = "0.7.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b9590b93e6fcc1739458317cccd391ad3955e2bde8913edf6f95f9e65a8f034" -dependencies = [ - "bytes", - "futures-core", - "futures-io", - "futures-sink", - "pin-project-lite", - "tokio", -] - -[[package]] -name = "toml_datetime" -version = "0.6.8" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0dd7358ecb8fc2f8d014bf86f6f638ce72ba252a2c3a2572f2a795f1d23efb41" - -[[package]] -name = "toml_edit" -version = "0.22.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "17b4795ff5edd201c7cd6dca065ae59972ce77d1b80fa0a84d94950ece7d1474" -dependencies = [ - "indexmap", - "toml_datetime", - "winnow", -] - -[[package]] -name = "tower" -version = "0.4.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8fa9be0de6cf49e536ce1851f987bd21a43b771b09473c3549a6c853db37c1c" -dependencies = [ - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebe5ef63511595f1344e2d5cfa636d973292adc0eec1f0ad45fae9f0851ab1d4" -dependencies = [ - "futures-core", - "futures-util", - "pin-project-lite", - "sync_wrapper", - "tower-layer", - "tower-service", -] - -[[package]] -name = "tower-http" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140" -dependencies = [ - "bitflags", - "bytes", - "futures-core", - "futures-util", - "http 0.2.12", - "http-body 0.4.6", - "http-range-header", - "pin-project-lite", - "tokio", - "tower-layer", - "tower-service", - "tracing", -] - -[[package]] -name = "tower-layer" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e" - -[[package]] -name = "tower-service" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" - -[[package]] -name = "tracing" -version = "0.1.41" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "784e0ac535deb450455cbfa28a6f0df145ea1bb7ae51b821cf5e7927fdcfbdd0" -dependencies = [ - "log", - "pin-project-lite", - "tracing-attributes", - "tracing-core", -] - -[[package]] -name = "tracing-attributes" -version = "0.1.28" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "395ae124c09f9e6918a2310af6038fba074bcf474ac352496d5910dd59a2226d" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "tracing-core" -version = "0.1.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e672c95779cf947c5311f83787af4fa8fffd12fb27e4993211a84bdfd9610f9c" -dependencies = [ - "once_cell", - "valuable", -] - -[[package]] -name = "tracing-log" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ee855f1f400bd0e5c02d150ae5de3840039a3f54b025156404e34c23c03f47c3" -dependencies = [ - "log", - "once_cell", - "tracing-core", -] - -[[package]] -name = "tracing-subscriber" -version = "0.3.19" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8189decb5ac0fa7bc8b96b7cb9b2701d60d48805aca84a238004d665fcc4008" -dependencies = [ - "nu-ansi-term", - "sharded-slab", - "smallvec", - "thread_local", - "tracing-core", - "tracing-log", -] - -[[package]] -name = "try-lock" -version = "0.2.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e421abadd41a4225275504ea4d6566923418b7f05506fbc9c0fe86ba7396114b" - -[[package]] -name = "typenum" -version = "1.18.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dccffe3ce07af9386bfd29e80c0ab1a8205a2fc34e4bcd40364df902cfa8f3f" - -[[package]] -name = "unicode-ident" -version = "1.0.18" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5a5f39404a5da50712a4c1eecf25e90dd62b613502b7e925fd4e4d19b5c96512" - -[[package]] -name = "unicode-width" -version = "0.1.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7dd6e30e90baa6f72411720665d41d89b9a3d039dc45b8faea1ddd07f617f6af" - -[[package]] -name = "unicode-xid" -version = "0.2.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" - -[[package]] -name = "unsigned-varint" -version = "0.8.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "eb066959b24b5196ae73cb057f45598450d2c5f71460e98c49b738086eff9c06" - -[[package]] -name = "untrusted" -version = "0.9.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ecb6da28b8a351d773b68d5825ac39017e680750f980f3a1a85cd8dd28a47c1" - -[[package]] -name = "url" -version = "2.5.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32f8b686cadd1473f4bd0117a5d28d36b1ade384ea9b5069a1c40aefed7fda60" -dependencies = [ - "form_urlencoded", - "idna", - "percent-encoding", -] - -[[package]] -name = "utf16_iter" -version = "1.0.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" - -[[package]] -name = "utf8_iter" -version = "1.0.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b6c140620e7ffbb22c2dee59cafe6084a59b5ffc27a8859a5f0d494b5d52b6be" - -[[package]] -name = "uuid" -version = "1.16.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "458f7a779bf54acc9f347480ac654f68407d3aab21269a6e3c9f922acd9e2da9" -dependencies = [ - "getrandom 0.3.4", -] - -[[package]] -name = "valuable" -version = "0.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba73ea9cf16a25df0c8caa16c51acb937d5712a8429db78a3ee29d5dcacd3a65" - -[[package]] -name = "version_check" -version = "0.9.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" - -[[package]] -name = "walkdir" -version = "2.5.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "29790946404f91d9c5d06f9874efddea1dc06c5efe94541a7d6863108e3a5e4b" -dependencies = [ - "same-file", - "winapi-util", -] - -[[package]] -name = "want" -version = "0.3.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa7760aed19e106de2c7c0b581b509f2f25d3dacaf737cb82ac61bc6d760b0e" -dependencies = [ - "try-lock", -] - -[[package]] -name = "wasi" -version = "0.11.0+wasi-snapshot-preview1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" - -[[package]] -name = "wasip2" -version = "1.0.2+wasi-0.2.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9517f9239f02c069db75e65f174b3da828fe5f5b945c4dd26bd25d89c03ebcf5" -dependencies = [ - "wit-bindgen", -] - -[[package]] -name = "wasm-bindgen" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0d759f433fa64a2d763d1340820e46e111a7a5ab75f993d1852d70b03dbb80fd" -dependencies = [ - "cfg-if", - "once_cell", - "rustversion", - "wasm-bindgen-macro", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-futures" -version = "0.4.56" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "836d9622d604feee9e5de25ac10e3ea5f2d65b41eac0d9ce72eb5deae707ce7c" -dependencies = [ - "cfg-if", - "js-sys", - "once_cell", - "wasm-bindgen", - "web-sys", -] - -[[package]] -name = "wasm-bindgen-macro" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48cb0d2638f8baedbc542ed444afc0644a29166f1595371af4fecf8ce1e7eeb3" -dependencies = [ - "quote", - "wasm-bindgen-macro-support", -] - -[[package]] -name = "wasm-bindgen-macro-support" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cefb59d5cd5f92d9dcf80e4683949f15ca4b511f4ac0a6e14d4e1ac60c6ecd40" -dependencies = [ - "bumpalo", - "proc-macro2", - "quote", - "syn", - "wasm-bindgen-shared", -] - -[[package]] -name = "wasm-bindgen-shared" -version = "0.2.106" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cbc538057e648b67f72a982e708d485b2efa771e1ac05fec311f9f63e5800db4" -dependencies = [ - "unicode-ident", -] - -[[package]] -name = "web-sys" -version = "0.3.83" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b32828d774c412041098d182a8b38b16ea816958e07cf40eec2bc080ae137ac" -dependencies = [ - "js-sys", - "wasm-bindgen", -] - -[[package]] -name = "webpki-root-certs" -version = "0.26.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75c7f0ef91146ebfb530314f5f1d24528d7f0767efbfd31dce919275413e393e" -dependencies = [ - "webpki-root-certs 1.0.6", -] - -[[package]] -name = "webpki-root-certs" -version = "1.0.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "804f18a4ac2676ffb4e8b5b5fa9ae38af06df08162314f96a68d2a363e21a8ca" -dependencies = [ - "rustls-pki-types", -] - -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - -[[package]] -name = "winapi-util" -version = "0.1.11" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" -dependencies = [ - "windows-sys 0.61.2", -] - -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - -[[package]] -name = "windows-link" -version = "0.2.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f0805222e57f7521d6a62e36fa9163bc891acd422f971defe97d64e70d0a4fe5" - -[[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.52.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.59.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" -dependencies = [ - "windows-targets 0.52.6", -] - -[[package]] -name = "windows-sys" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f2f500e4d28234f72040990ec9d39e3a6b950f9f22d3dba18416c35882612bcb" -dependencies = [ - "windows-targets 0.53.5", -] - -[[package]] -name = "windows-sys" -version = "0.61.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ae137229bcbd6cdf0f7b80a31df61766145077ddf49416a728b02cb3921ff3fc" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" -dependencies = [ - "windows_aarch64_gnullvm 0.52.6", - "windows_aarch64_msvc 0.52.6", - "windows_i686_gnu 0.52.6", - "windows_i686_gnullvm 0.52.6", - "windows_i686_msvc 0.52.6", - "windows_x86_64_gnu 0.52.6", - "windows_x86_64_gnullvm 0.52.6", - "windows_x86_64_msvc 0.52.6", -] - -[[package]] -name = "windows-targets" -version = "0.53.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4945f9f551b88e0d65f3db0bc25c33b8acea4d9e41163edf90dcd0b19f9069f3" -dependencies = [ - "windows-link", - "windows_aarch64_gnullvm 0.53.1", - "windows_aarch64_msvc 0.53.1", - "windows_i686_gnu 0.53.1", - "windows_i686_gnullvm 0.53.1", - "windows_i686_msvc 0.53.1", - "windows_x86_64_gnu 0.53.1", - "windows_x86_64_gnullvm 0.53.1", - "windows_x86_64_msvc 0.53.1", -] - -[[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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" - -[[package]] -name = "windows_aarch64_gnullvm" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" - -[[package]] -name = "windows_aarch64_msvc" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8e9b5ad5ab802e97eb8e295ac6720e509ee4c243f69d781394014ebfe8bbfa0b" - -[[package]] -name = "windows_i686_gnu" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "960e6da069d81e09becb0ca57a65220ddff016ff2d6af6a223cf372a506593a3" - -[[package]] -name = "windows_i686_gnullvm" -version = "0.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" - -[[package]] -name = "windows_i686_gnullvm" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" - -[[package]] -name = "windows_i686_msvc" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" - -[[package]] -name = "windows_x86_64_gnu" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" - -[[package]] -name = "windows_x86_64_gnullvm" -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.52.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "589f6da84c646204747d1270a2a5661ea66ed1cced2631d546fdfb155959f9ec" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.53.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d6bbff5f0aada427a1e5a6da5f1f98158182f26556f345ac9e04d36d0ebed650" - -[[package]] -name = "winnow" -version = "0.7.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0e97b544156e9bebe1a0ffbc03484fc1ffe3100cbce3ffb17eac35f7cdd7ab36" -dependencies = [ - "memchr", -] - -[[package]] -name = "wit-bindgen" -version = "0.51.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7249219f66ced02969388cf2bb044a09756a083d0fab1e566056b04d9fbcaa5" - -[[package]] -name = "write16" -version = "1.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" - -[[package]] -name = "writeable" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9df38ee2d2c3c5948ea468a8406ff0db0b29ae1ffde1bcf20ef305bcc95c51" - -[[package]] -name = "wyz" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05f360fc0b24296329c78fda852a1e9ae82de9cf7b27dae4b7f62f118f77b9ed" -dependencies = [ - "tap", -] - -[[package]] -name = "xxhash-rust" -version = "0.8.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fdd20c5420375476fbd4394763288da7eb0cc0b8c11deed431a91562af7335d3" - -[[package]] -name = "yoke" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "120e6aef9aa629e3d4f52dc8cc43a015c7724194c97dfaf45180d2daf2b77f40" -dependencies = [ - "serde", - "stable_deref_trait", - "yoke-derive", - "zerofrom", -] - -[[package]] -name = "yoke-derive" -version = "0.7.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2380878cad4ac9aac1e2435f3eb4020e8374b5f13c296cb75b4620ff8e229154" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zerocopy" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2586fea28e186957ef732a5f8b3be2da217d65c5969d4b1e17f973ebbe876879" -dependencies = [ - "zerocopy-derive", -] - -[[package]] -name = "zerocopy-derive" -version = "0.8.24" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a996a8f63c5c4448cd959ac1bab0aaa3306ccfd060472f85943ee0750f0169be" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerofrom" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50cc42e0333e05660c3587f3bf9d0478688e15d870fab3346451ce7f8c9fbea5" -dependencies = [ - "zerofrom-derive", -] - -[[package]] -name = "zerofrom-derive" -version = "0.1.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d71e5d6e06ab090c67b5e44993ec16b72dcbaabc526db883a360057678b48502" -dependencies = [ - "proc-macro2", - "quote", - "syn", - "synstructure", -] - -[[package]] -name = "zeroize" -version = "1.8.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ced3678a2879b30306d323f4542626697a464a97c0a07c9aebf7ebca65cd4dde" -dependencies = [ - "zeroize_derive", -] - -[[package]] -name = "zeroize_derive" -version = "1.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ce36e65b0d2999d2aafac989fb249189a141aee1f53c612c1f37d72631959f69" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zerovec" -version = "0.10.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa2b893d79df23bfb12d5461018d408ea19dfafe76c2c7ef6d4eba614f8ff079" -dependencies = [ - "yoke", - "zerofrom", - "zerovec-derive", -] - -[[package]] -name = "zerovec-derive" -version = "0.10.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6eafa6dfb17584ea3e2bd6e76e0cc15ad7af12b09abdd1ca55961bed9b1063c6" -dependencies = [ - "proc-macro2", - "quote", - "syn", -] - -[[package]] -name = "zmij" -version = "1.0.20" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4de98dfa5d5b7fef4ee834d0073d560c9ca7b6c46a71d058c48db7960f8cfaf7" diff --git a/Cargo.toml b/Cargo.toml index 88a92f5..8b7ea12 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,8 @@ anyhow = "1.0.86" sha2 = { version = "0.10.8", default-features = false } tower-http = { version = "0.4", features = ["cors", "trace", "timeout"] } tower = "0.4" +ethers = "2.0" +clap = { version = "4.5.20", features = ["derive"] } [dev-dependencies] jsonrpsee-core = "0.26" diff --git a/README.md b/README.md index 7c85ba5..3b7e3ea 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,9 @@ cargo run - Rust toolchain (cargo) installed via - Redis running locally or reachable via `REDIS_URL` (install via your package manager or `docker run --rm -p 6379:6379 redis:7`) - curl installed (required for CLI-based integration tests; install via your package manager) -- Docker (optional, only needed for `LOCALESTIA_REDIS_MODE=docker` test runs), see the [docker guide](./Dockerfile) +- Docker (optional, only needed for `LOCALESTIA_REDIS_MODE=docker` test runs) +- Foundry (`forge`, `anvil`) for mock contract deployment and demo +- Bun (for the demo UI) ## Build @@ -29,6 +31,77 @@ cargo build --release REDIS_URL=redis://127.0.0.1:6379 LISTEN_ADDR=127.0.0.1:26658 cargo run ``` +## CLI Commands + +### Deploy the Mock Blobstream Contract + +```bash +localestia blobstream deploy \ + --eth-rpc-url http://127.0.0.1:8545 \ + --private-key 0x... +``` + +Optional: + +```bash + --chain-id 31337 +``` + +### Run the Full Demo Stack + +```bash +localestia demo --relayer-interval-ms 1000 --ui-port 3030 +``` + +This command starts Redis, Anvil, Localestia, deploys the mock contract, starts the relayer, +and serves the UI. + +It also auto-submits blobs (and generates headers) every 1500ms by default. To tune or disable: + +```bash +localestia demo --auto-blob-interval-ms 2000 +# set to 0 to disable auto-submission +localestia demo --auto-blob-interval-ms 0 +``` + +If `REDIS_URL` is not set, the demo command starts Redis via Docker. If `anvil` is not installed, +it attempts to run Anvil via Docker. + +### Demo Walkthrough + +1) Start the demo: + +```bash +localestia demo --relayer-interval-ms 1000 --ui-port 3030 +``` + +1) Open the UI: + +```bash +http://127.0.0.1:3030 +``` + +1) Optional flags: + +```bash +# Change relayer interval (ms) +localestia demo --relayer-interval-ms 2000 + +# Disable auto-submitted blobs +localestia demo --auto-blob-interval-ms 0 + +# Move the UI port +localestia demo --ui-port 4040 +``` + +### Troubleshooting + +- `bun: command not found`: install Bun and ensure it is on PATH (`curl -fsSL https://bun.sh/install | bash`). +- `anvil: command not found`: install Foundry or make sure Docker is running so Anvil can run in a container. +- `Failed to run bun install`: delete `ui/node_modules` and retry, or run `bun install` manually. +- `Redis is not reachable`: set `REDIS_URL` or ensure Docker is available so the demo can start Redis. +- UI is empty: wait a few seconds for auto-submitted blobs to generate headers, or submit a blob manually. + Defaults if unset: - `REDIS_URL=redis://127.0.0.1:6379` @@ -52,6 +125,40 @@ LOCALESTIA_REDIS_MODE=docker cargo test You can also use `LOCALESTIA_REDIS_MODE=auto` to prefer a local `REDIS_URL` if set and fall back to Docker when available. +### Anvil + Foundry (required for blobstream contract test) + +The blobstream contract compatibility test deploys the mock Blobstream contract to Anvil and +verifies proofs on-chain. It requires `anvil` and `forge` to be available. + +Preferred (Docker Anvil + local Foundry install): + +```bash +./scripts/setup_anvil.sh +# then export the values printed by the script +export ANVIL_RPC_URL=http://127.0.0.1:8545 +export ANVIL_PRIVATE_KEY=0x... +``` + +If Docker is not available, the script installs Foundry locally. Tests will spawn a local `anvil` +binary automatically. + +### Demo UI + +The demo command runs the UI using Bun + ElysiaJS. You can run it manually: + +```bash +cd explorer +bun install +CELESTIA_HTTP_URL=http://127.0.0.1:26658 \ +ETH_RPC_URL=http://127.0.0.1:8545 \ +BLOBSTREAM_CONTRACT_ADDRESS=0x... \ +bun src/index.ts +``` + +The explorer shows an interactive map with Celestia and Ethereum nodes, a live fiber stream +between them, and three paginated columns (Celestia blocks, Ethereum blocks, relayed batches). +Click any item to inspect details with raw JSON. + ## Supported RPC Methods Localestia implements the following JSON-RPC methods: @@ -80,6 +187,13 @@ Localestia implements the following JSON-RPC methods: | `share.GetEDS` | Gets the Extended Data Square at a height | `height: u64` | | `share.GetRange` | Gets a range of shares | `height: u64`, `start: u64`, `end: u64` | +### Blobstream Methods + +| Method | Description | Parameters | +| ------ | ----------- | ---------- | +| `blobstream.GetDataRootTupleRoot` | Computes a merkle root of data root tuples over a block range | `start: u64`, `end: u64` | +| `blobstream.GetDataRootTupleInclusionProof` | Creates an inclusion proof for a data root tuple within a range | `height: u64`, `start: u64`, `end: u64` | + ## Usage Examples ### Submit a Blob @@ -139,3 +253,45 @@ curl -X POST "http://localhost:26658" \ ## Docker Setup See the [Docker Usage Guide](DOCKER.md) for instructions on running Localestia with Docker. + +## Blobstream Mock Contract + Relayer + +Localestia ships a mock Blobstream contract and a relayer that submits data root tuple roots +to an EVM chain for local testing. + +### Deploy the Mock Contract (Foundry) + +Prerequisites: + +- Foundry installed (`forge`, `cast`) + +From the repo root: + +```bash +cd contracts +ETH_RPC_URL=http://127.0.0.1:8545 \ +ETH_PRIVATE_KEY=... \ +./scripts/deploy_mock_blobstream.sh +``` + +This script deploys and initializes the contract. It prints the deployed address; export it as: + +```bash +export BLOBSTREAM_CONTRACT_ADDRESS=0x... +``` + +### Run the Relayer + +The relayer submits one header at a time (no batching) as headers appear in Localestia. + +```bash +ETH_RPC_URL=http://127.0.0.1:8545 \ +ETH_PRIVATE_KEY=... \ +BLOBSTREAM_CONTRACT_ADDRESS=0x... \ +CELESTIA_RPC_URL=ws://127.0.0.1:26658 \ +cargo run --bin blobstream_relayer +``` + +Optional environment variables: + +- `RELAYER_POLL_MS` (default: 1000) diff --git a/assets/celestia_logo.png b/assets/celestia_logo.png new file mode 100644 index 0000000..27aee8b Binary files /dev/null and b/assets/celestia_logo.png differ diff --git a/assets/ethereum_logo.png b/assets/ethereum_logo.png new file mode 100644 index 0000000..56febcd Binary files /dev/null and b/assets/ethereum_logo.png differ diff --git a/assets/fiber_hero_premium.webm b/assets/fiber_hero_premium.webm new file mode 100644 index 0000000..3a654df Binary files /dev/null and b/assets/fiber_hero_premium.webm differ diff --git a/contracts/foundry.toml b/contracts/foundry.toml new file mode 100644 index 0000000..2bc70ba --- /dev/null +++ b/contracts/foundry.toml @@ -0,0 +1,4 @@ +[profile.default] +src = "src" +out = "out" +libs = ["lib"] diff --git a/contracts/lib/DataRootTuple.sol b/contracts/lib/DataRootTuple.sol new file mode 100644 index 0000000..cb06bb8 --- /dev/null +++ b/contracts/lib/DataRootTuple.sol @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +/// @notice A tuple of data root with metadata. Each data root is associated +/// with a Celestia block height. +/// @dev `availableDataRoot` in +/// https://github.com/celestiaorg/celestia-specs/blob/master/src/specs/data_structures.md#header +struct DataRootTuple { + // Celestia block height the data root was included in. + // Genesis block is height = 0. + // First queryable block is height = 1. + uint256 height; + // Data root. + bytes32 dataRoot; +} diff --git a/contracts/lib/IBlobstreamX.sol b/contracts/lib/IBlobstreamX.sol new file mode 100644 index 0000000..074aab6 --- /dev/null +++ b/contracts/lib/IBlobstreamX.sol @@ -0,0 +1,45 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "./DataRootTuple.sol"; +import "./tree/binary/BinaryMerkleProof.sol"; + +interface IBlobstreamX { + /// @notice Contract is frozen. + error ContractFrozen(); + + /// @notice Data commitment stored for the block range [startBlock, endBlock) with proof nonce. + /// @param proofNonce The nonce of the proof. + /// @param startBlock The start block of the block range. + /// @param endBlock The end block of the block range. + /// @param dataCommitment The data commitment for the block range. + event DataCommitmentStored( + uint256 proofNonce, + uint64 indexed startBlock, + uint64 indexed endBlock, + bytes32 indexed dataCommitment + ); + + /// @dev Latest height published to the BlobstreamX contract. + function latestBlock() external view returns (uint64); + + /// @dev Nonce for proof events. Must be incremented sequentially. + function state_proofNonce() external view returns (uint256); + + /// @dev Is the BlobstreamX contract forzen or not. + function frozen() external view returns (bool); + + /// @dev fetches data commitment from BlobstreamX state. + function state_dataCommitments(uint256) external view returns (bytes32); + + /// @notice Verify a Data Availability attestation. + /// @param _tupleRootNonce Nonce of the tuple root to prove against. + /// @param _tuple Data root tuple to prove inclusion of. + /// @param _proof Binary Merkle tree proof that `tuple` is in the root at `_tupleRootNonce`. + /// @return `true` is proof is valid, `false` otherwise. + function verifyAttestation( + uint256 _tupleRootNonce, + DataRootTuple memory _tuple, + BinaryMerkleProof memory _proof + ) external view returns (bool); +} diff --git a/contracts/lib/tree/Constants.sol b/contracts/lib/tree/Constants.sol new file mode 100644 index 0000000..d7aa2ed --- /dev/null +++ b/contracts/lib/tree/Constants.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import "./Types.sol"; + +library Constants { + /////////////// + // Constants // + /////////////// + + /// @dev Maximum tree height + uint256 internal constant MAX_HEIGHT = 256; + + /// @dev The prefixes of leaves and nodes + bytes1 internal constant LEAF_PREFIX = 0x00; + bytes1 internal constant NODE_PREFIX = 0x01; +} + +/// @dev Parity share namespace. +/// utility function to provide the parity share namespace as a Namespace struct. +function PARITY_SHARE_NAMESPACE() pure returns (Namespace memory) { + return Namespace(0xFF, 0xFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF); +} diff --git a/contracts/lib/tree/Types.sol b/contracts/lib/tree/Types.sol new file mode 100644 index 0000000..bff9eb8 --- /dev/null +++ b/contracts/lib/tree/Types.sol @@ -0,0 +1,37 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +/// @notice A representation of the Celestia-app namespace ID and its version. +/// See: https://celestiaorg.github.io/celestia-app/specs/namespace.html +struct Namespace { + // The namespace version. + bytes1 version; + // The namespace ID. + bytes28 id; +} + +using {equalTo, lessThan, greaterThan, toBytes} for Namespace global; + +function equalTo(Namespace memory l, Namespace memory r) pure returns (bool) { + return l.toBytes() == r.toBytes(); +} + +function lessThan(Namespace memory l, Namespace memory r) pure returns (bool) { + return l.toBytes() < r.toBytes(); +} + +function greaterThan(Namespace memory l, Namespace memory r) pure returns (bool) { + return l.toBytes() > r.toBytes(); +} + +function toBytes(Namespace memory n) pure returns (bytes29) { + return bytes29(abi.encodePacked(n.version, n.id)); +} + +function toNamespace(bytes29 n) pure returns (Namespace memory) { + bytes memory id = new bytes(28); + for (uint256 i = 1; i < 29; i++) { + id[i - 1] = n[i]; + } + return Namespace(n[0], bytes28(id)); +} diff --git a/contracts/lib/tree/Utils.sol b/contracts/lib/tree/Utils.sol new file mode 100644 index 0000000..b95677b --- /dev/null +++ b/contracts/lib/tree/Utils.sol @@ -0,0 +1,106 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import "./Constants.sol"; + +/// @notice Calculate the starting bit of the path to a leaf +/// @param numLeaves : The total number of leaves in the tree +/// @return startingBit : The starting bit of the path +// solhint-disable-next-line func-visibility +function getStartingBit(uint256 numLeaves) pure returns (uint256 startingBit) { + // Determine height of the left subtree. This is the maximum path length, so all paths start at this offset from the right-most bit + startingBit = 0; + while ((1 << startingBit) < numLeaves) { + startingBit += 1; + } + return Constants.MAX_HEIGHT - startingBit; +} + +/// @notice Calculate the length of the path to a leaf +/// @param key: The key of the leaf +/// @param numLeaves: The total number of leaves in the tree +/// @return pathLength : The length of the path to the leaf +// solhint-disable-next-line func-visibility +function pathLengthFromKey(uint256 key, uint256 numLeaves) pure returns (uint256 pathLength) { + if (numLeaves <= 1) { + // if the number of leaves of the tree is 1 or 0, the path always is 0. + return 0; + } + // Get the height of the left subtree. This is equal to the offset of the starting bit of the path + pathLength = Constants.MAX_HEIGHT - getStartingBit(numLeaves); + + // Determine the number of leaves in the left subtree + uint256 numLeavesLeftSubTree = (1 << (pathLength - 1)); + + // If leaf is in left subtree, path length is full height of left subtree + if (key <= numLeavesLeftSubTree - 1) { + return pathLength; + } + // If left sub tree has only one leaf but key is not there, path has one additional step + else if (numLeavesLeftSubTree == 1) { + return 1; + } + // Otherwise, add 1 to height and recurse into right subtree + else { + return 1 + pathLengthFromKey(key - numLeavesLeftSubTree, numLeaves - numLeavesLeftSubTree); + } +} + +/// @notice Returns the minimum number of bits required to represent `x`; the +/// result is 0 for `x` == 0. +/// @param x Number. +function _bitsLen(uint256 x) pure returns (uint256) { + uint256 count = 0; + + while (x != 0) { + count++; + x >>= 1; + } + + return count; +} + +/// @notice Returns the largest power of 2 less than `x`. +/// @param x Number. +function _getSplitPoint(uint256 x) pure returns (uint256) { + // Note: since `x` is always an unsigned int * 2, the only way for this + // to be violated is if the input == 0. Since the input is the end + // index exclusive, an input of 0 is guaranteed to be invalid (it would + // be a proof of inclusion of nothing, which is vacuous). + require(x >= 1); + + uint256 bitLen = _bitsLen(x); + uint256 k = 1 << (bitLen - 1); + if (k == x) { + k >>= 1; + } + return k; +} + +/// @notice Returns the size of the subtree adjacent to `begin` that does +/// not overlap `end`. +/// @param begin Begin index, inclusive. +/// @param end End index, exclusive. +function _nextSubtreeSize(uint256 begin, uint256 end) pure returns (uint256) { + uint256 ideal = _bitsTrailingZeroes(begin); + uint256 max = _bitsLen(end - begin) - 1; + if (ideal > max) { + return 1 << max; + } + return 1 << ideal; +} + +/// @notice Returns the number of trailing zero bits in `x`; the result is +/// 256 for `x` == 0. +/// @param x Number. +function _bitsTrailingZeroes(uint256 x) pure returns (uint256) { + uint256 mask = 1; + uint256 count = 0; + + while (x != 0 && mask & x == 0) { + count++; + x >>= 1; + } + + return count; +} diff --git a/contracts/lib/tree/binary/BinaryMerkleMultiproof.sol b/contracts/lib/tree/binary/BinaryMerkleMultiproof.sol new file mode 100644 index 0000000..105e41d --- /dev/null +++ b/contracts/lib/tree/binary/BinaryMerkleMultiproof.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +/// @notice Merkle Tree Proof structure. +struct BinaryMerkleMultiproof { + // List of side nodes to verify and calculate tree. + bytes32[] sideNodes; + // The (included) beginning key of the leaves to verify. + uint256 beginKey; + // The (excluded) ending key of the leaves to verify. + uint256 endKey; +} diff --git a/contracts/lib/tree/binary/BinaryMerkleProof.sol b/contracts/lib/tree/binary/BinaryMerkleProof.sol new file mode 100644 index 0000000..a7354ba --- /dev/null +++ b/contracts/lib/tree/binary/BinaryMerkleProof.sol @@ -0,0 +1,12 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +/// @notice Merkle Tree Proof structure. +struct BinaryMerkleProof { + // List of side nodes to verify and calculate tree. + bytes32[] sideNodes; + // The key of the leaf to verify. + uint256 key; + // The number of leaves in the tree + uint256 numLeaves; +} diff --git a/contracts/lib/tree/binary/BinaryMerkleTree.sol b/contracts/lib/tree/binary/BinaryMerkleTree.sol new file mode 100644 index 0000000..fe2770a --- /dev/null +++ b/contracts/lib/tree/binary/BinaryMerkleTree.sol @@ -0,0 +1,256 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import "../Constants.sol"; +import "../Utils.sol"; +import "./TreeHasher.sol"; +import "./BinaryMerkleProof.sol"; +import "./BinaryMerkleMultiproof.sol"; + +/// @title Binary Merkle Tree. +library BinaryMerkleTree { + ///////////////// + // Error codes // + ///////////////// + + enum ErrorCodes { + NoError, + /// @notice The provided side nodes count is invalid for the proof. + InvalidNumberOfSideNodes, + /// @notice The provided proof key is not part of the tree. + KeyNotInTree, + /// @notice Invalid number of leaves in proof. + InvalidNumberOfLeavesInProof, + /// @notice The proof contains unexpected side nodes. + UnexpectedInnerHashes, + /// @notice The proof verification expected at least one inner hash. + ExpectedAtLeastOneInnerHash + } + + /////////////// + // Functions // + /////////////// + + /// @notice Verify if element exists in Merkle tree, given data, proof, and root. + /// @param root The root of the tree in which to verify the given leaf. + /// @param proof Binary Merkle proof for the leaf. + /// @param data The data of the leaf to verify. + /// @return `true` if proof is valid, `false` otherwise. + /// @dev proof.numLeaves is necessary to determine height of subtree containing the data to prove. + function verify(bytes32 root, BinaryMerkleProof memory proof, bytes memory data) + internal + pure + returns (bool, ErrorCodes) + { + // Check proof is correct length for the key it is proving + if (proof.numLeaves <= 1) { + if (proof.sideNodes.length != 0) { + return (false, ErrorCodes.InvalidNumberOfSideNodes); + } + } else if (proof.sideNodes.length != pathLengthFromKey(proof.key, proof.numLeaves)) { + return (false, ErrorCodes.InvalidNumberOfSideNodes); + } + + // Check key is in tree + if (proof.key >= proof.numLeaves) { + return (false, ErrorCodes.KeyNotInTree); + } + + // A sibling at height 1 is created by getting the hash of the data to prove. + bytes32 digest = leafDigest(data); + + // Null proof is only valid if numLeaves = 1 + // If so, just verify hash(data) is root + if (proof.sideNodes.length == 0) { + if (proof.numLeaves == 1) { + return (root == digest, ErrorCodes.NoError); + } else { + return (false, ErrorCodes.NoError); + } + } + + (bytes32 computedHash, ErrorCodes error) = computeRootHash(proof.key, proof.numLeaves, digest, proof.sideNodes); + + if (error != ErrorCodes.NoError) { + return (false, error); + } + + return (computedHash == root, ErrorCodes.NoError); + } + + function verifyMulti(bytes32 root, BinaryMerkleMultiproof memory proof, bytes[] memory data) + internal + pure + returns (bool) + { + bytes32[] memory nodes = new bytes32[](data.length); + for (uint256 i = 0; i < data.length; i++) { + nodes[i] = leafDigest(data[i]); + } + + return verifyMultiHashes(root, proof, nodes); + } + + function verifyMultiHashes(bytes32 root, BinaryMerkleMultiproof memory proof, bytes32[] memory leafNodes) + internal + pure + returns (bool) + { + uint256 leafIndex = 0; + bytes32[] memory leftSubtrees = new bytes32[](proof.sideNodes.length); + + for (uint256 i = 0; leafIndex != proof.beginKey && i < proof.sideNodes.length; ++i) { + uint256 subtreeSize = _nextSubtreeSize(leafIndex, proof.beginKey); + leftSubtrees[i] = proof.sideNodes[i]; + leafIndex += subtreeSize; + } + + uint256 proofRangeSubtreeEstimate = _getSplitPoint(proof.endKey) * 2; + if (proofRangeSubtreeEstimate < 1) { + proofRangeSubtreeEstimate = 1; + } + + (bytes32 rootHash, uint256 proofHead,,) = + _computeRootMulti(proof, leafNodes, 0, proofRangeSubtreeEstimate, 0, 0); + for (uint256 i = proofHead; i < proof.sideNodes.length; ++i) { + rootHash = nodeDigest(rootHash, proof.sideNodes[i]); + } + + return (rootHash == root); + } + + function _computeRootMulti( + BinaryMerkleMultiproof memory proof, + bytes32[] memory leafNodes, + uint256 begin, + uint256 end, + uint256 headProof, + uint256 headLeaves + ) private pure returns (bytes32, uint256, uint256, bool) { + // reached a leaf + if (end - begin == 1) { + // if current range overlaps with proof range, pop and return a leaf + if (proof.beginKey <= begin && begin < proof.endKey) { + // Note: second return value is guaranteed to be `false` by + // construction. + return _popLeavesIfNonEmpty(leafNodes, headLeaves, leafNodes.length, headProof); + } + + // if current range does not overlap with proof range, + // pop and return a proof node (leaf) if present, + // else return nil because leaf doesn't exist + return _popProofIfNonEmpty(proof.sideNodes, headProof, end, headLeaves); + } + + // if current range does not overlap with proof range, + // pop and return a proof node if present, + // else return nil because subtree doesn't exist + if (end <= proof.beginKey || begin >= proof.endKey) { + return _popProofIfNonEmpty(proof.sideNodes, headProof, end, headLeaves); + } + + // Recursively get left and right subtree + uint256 k = _getSplitPoint(end - begin); + (bytes32 left, uint256 newHeadProofLeft, uint256 newHeadLeavesLeft,) = + _computeRootMulti(proof, leafNodes, begin, begin + k, headProof, headLeaves); + (bytes32 right, uint256 newHeadProof, uint256 newHeadLeaves, bool rightIsNil) = + _computeRootMulti(proof, leafNodes, begin + k, end, newHeadProofLeft, newHeadLeavesLeft); + + // only right leaf/subtree can be non-existent + if (rightIsNil == true) { + return (left, newHeadProof, newHeadLeaves, false); + } + bytes32 hash = nodeDigest(left, right); + return (hash, newHeadProof, newHeadLeaves, false); + } + + function _popProofIfNonEmpty(bytes32[] memory nodes, uint256 headProof, uint256 end, uint256 headLeaves) + private + pure + returns (bytes32, uint256, uint256, bool) + { + (bytes32 node, uint256 newHead, bool isNil) = _popIfNonEmpty(nodes, headProof, end); + return (node, newHead, headLeaves, isNil); + } + + function _popLeavesIfNonEmpty(bytes32[] memory nodes, uint256 headLeaves, uint256 end, uint256 headProof) + private + pure + returns (bytes32, uint256, uint256, bool) + { + (bytes32 node, uint256 newHead, bool isNil) = _popIfNonEmpty(nodes, headLeaves, end); + return (node, headProof, newHead, isNil); + } + + function _popIfNonEmpty(bytes32[] memory nodes, uint256 head, uint256 end) + private + pure + returns (bytes32, uint256, bool) + { + if (nodes.length == 0 || head >= nodes.length || head >= end) { + bytes32 node; + return (node, head, true); + } + return (nodes[head], head + 1, false); + } + + /// @notice Use the leafHash and innerHashes to get the root merkle hash. + /// If the length of the innerHashes slice isn't exactly correct, the result is nil. + /// Recursive impl. + function computeRootHash(uint256 key, uint256 numLeaves, bytes32 leafHash, bytes32[] memory sideNodes) + private + pure + returns (bytes32, ErrorCodes) + { + if (numLeaves == 0) { + return (leafHash, ErrorCodes.InvalidNumberOfLeavesInProof); + } + if (numLeaves == 1) { + if (sideNodes.length != 0) { + return (leafHash, ErrorCodes.UnexpectedInnerHashes); + } + return (leafHash, ErrorCodes.NoError); + } + if (sideNodes.length == 0) { + return (leafHash, ErrorCodes.ExpectedAtLeastOneInnerHash); + } + uint256 numLeft = _getSplitPoint(numLeaves); + bytes32[] memory sideNodesLeft = slice(sideNodes, 0, sideNodes.length - 1); + ErrorCodes error; + if (key < numLeft) { + bytes32 leftHash; + (leftHash, error) = computeRootHash(key, numLeft, leafHash, sideNodesLeft); + if (error != ErrorCodes.NoError) { + return (leafHash, error); + } + return (nodeDigest(leftHash, sideNodes[sideNodes.length - 1]), ErrorCodes.NoError); + } + bytes32 rightHash; + (rightHash, error) = computeRootHash(key - numLeft, numLeaves - numLeft, leafHash, sideNodesLeft); + if (error != ErrorCodes.NoError) { + return (leafHash, error); + } + return (nodeDigest(sideNodes[sideNodes.length - 1], rightHash), ErrorCodes.NoError); + } + + /// @notice creates a slice of bytes32 from the data slice of bytes32 containing the elements + /// that correspond to the provided range. + /// It selects a half-open range which includes the begin element, but excludes the end one. + /// @param _data The slice that we want to select data from. + /// @param _begin The beginning of the range (inclusive). + /// @param _end The ending of the range (exclusive). + /// @return _ the sliced data. + function slice(bytes32[] memory _data, uint256 _begin, uint256 _end) internal pure returns (bytes32[] memory) { + if (_begin > _end) { + revert("Invalid range: _begin is greater than _end"); + } + if (_begin > _data.length || _end > _data.length) { + revert("Invalid range: _begin or _end are out of bounds"); + } + bytes32[] memory out = new bytes32[](_end - _begin); + for (uint256 i = _begin; i < _end; i++) { + out[i - _begin] = _data[i]; + } + return out; + } +} diff --git a/contracts/lib/tree/binary/TreeHasher.sol b/contracts/lib/tree/binary/TreeHasher.sol new file mode 100644 index 0000000..f3ee4b3 --- /dev/null +++ b/contracts/lib/tree/binary/TreeHasher.sol @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +pragma solidity ^0.8.22; + +import "../Constants.sol"; + +/// @notice Calculate the digest of a node. +/// @param left The left child. +/// @param right The right child. +/// @return digest The node digest. +/// @dev More details in https://github.com/celestiaorg/celestia-specs/blob/master/src/specs/data_structures.md#binary-merkle-tree +// solhint-disable-next-line func-visibility +function nodeDigest(bytes32 left, bytes32 right) pure returns (bytes32 digest) { + digest = sha256(abi.encodePacked(Constants.NODE_PREFIX, left, right)); +} + +/// @notice Calculate the digest of a leaf. +/// @param data The data of the leaf. +/// @return digest The leaf digest. +/// @dev More details in https://github.com/celestiaorg/celestia-specs/blob/master/src/specs/data_structures.md#binary-merkle-tree +// solhint-disable-next-line func-visibility +function leafDigest(bytes memory data) pure returns (bytes32 digest) { + digest = sha256(abi.encodePacked(Constants.LEAF_PREFIX, data)); +} diff --git a/contracts/remappings.txt b/contracts/remappings.txt new file mode 100644 index 0000000..7c033af --- /dev/null +++ b/contracts/remappings.txt @@ -0,0 +1 @@ +@celestia/lib/=lib/ diff --git a/contracts/scripts/deploy_mock_blobstream.sh b/contracts/scripts/deploy_mock_blobstream.sh new file mode 100755 index 0000000..760f8c4 --- /dev/null +++ b/contracts/scripts/deploy_mock_blobstream.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +if ! command -v forge >/dev/null 2>&1; then + echo "forge is required; install Foundry first" + exit 1 +fi + +if ! command -v cast >/dev/null 2>&1; then + echo "cast is required; install Foundry first" + exit 1 +fi + +if [[ -z "${ETH_RPC_URL:-}" ]]; then + echo "ETH_RPC_URL is required" + exit 1 +fi + +if [[ -z "${ETH_PRIVATE_KEY:-}" ]]; then + echo "ETH_PRIVATE_KEY is required" + exit 1 +fi + +INITIAL_LATEST_BLOCK=${INITIAL_LATEST_BLOCK:-0} + +forge build + +OUTPUT=$(forge create --rpc-url "$ETH_RPC_URL" --private-key "$ETH_PRIVATE_KEY" --json src/MockBlobstream.sol:Mockstream) +echo "$OUTPUT" + +if command -v jq >/dev/null 2>&1; then + ADDRESS=$(echo "$OUTPUT" | jq -r '.deployedTo') + if [[ -z "$ADDRESS" || "$ADDRESS" == "null" ]]; then + echo "failed to read deployed address" + exit 1 + fi +else + echo "jq not found; install jq to auto-extract the deployed address" + exit 0 +fi + +cast send --rpc-url "$ETH_RPC_URL" --private-key "$ETH_PRIVATE_KEY" "$ADDRESS" "initialize(uint64)" "$INITIAL_LATEST_BLOCK" + +echo "Mockstream deployed at $ADDRESS" +echo "export BLOBSTREAM_CONTRACT_ADDRESS=$ADDRESS" diff --git a/contracts/src/MockBlobstream.sol b/contracts/src/MockBlobstream.sol new file mode 100644 index 0000000..3890ea8 --- /dev/null +++ b/contracts/src/MockBlobstream.sol @@ -0,0 +1,61 @@ +// SPDX-License-Identifier: MIT +pragma solidity ^0.8.19; + +import "@celestia/lib/DataRootTuple.sol"; +import "@celestia/lib/tree/binary/BinaryMerkleTree.sol"; +import "@celestia/lib/IBlobstreamX.sol"; + +contract Mockstream is IBlobstreamX { + uint64 public latestBlock; + uint256 public state_proofNonce; + mapping(uint256 => bytes32) public state_dataCommitments; + bool public frozen; + + function initialize(uint64 _latestBlock) external { + frozen = false; + latestBlock = _latestBlock; + state_proofNonce = 1; + } + + function updateFreeze(bool _freeze) external { + frozen = _freeze; + } + + function updateGenesisState(uint64 _height) external { + latestBlock = _height; + } + + function submitDataCommitment( + bytes32 _dataCommitment, + uint64 _beginBlock, + uint64 _endBlock + ) external { + if (latestBlock > _beginBlock || _beginBlock > _endBlock) { + revert("INVALID RANGE"); + } + state_dataCommitments[state_proofNonce] = _dataCommitment; + + emit DataCommitmentStored(state_proofNonce, _beginBlock, _endBlock, _dataCommitment); + + state_proofNonce++; + latestBlock = _endBlock; + } + + function verifyAttestation( + uint256 _proofNonce, + DataRootTuple memory _tuple, + BinaryMerkleProof memory _proof + ) external view returns (bool) { + if (frozen) { + revert ContractFrozen(); + } + + if (_proofNonce == 0 || _proofNonce >= state_proofNonce) { + return false; + } + + bytes32 root = state_dataCommitments[_proofNonce]; + (bool isProofValid, ) = BinaryMerkleTree.verify(root, _proof, abi.encode(_tuple)); + return isProofValid; + } +} diff --git a/explorer/bun.lock b/explorer/bun.lock new file mode 100644 index 0000000..2678d38 --- /dev/null +++ b/explorer/bun.lock @@ -0,0 +1,68 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "localestia-ui", + "dependencies": { + "elysia": "^1.0.0", + "redis": "^4.7.0", + }, + }, + }, + "packages": { + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + + "@redis/bloom": ["@redis/bloom@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-HG2DFjYKbpNmVXsa0keLHp/3leGJz1mjh09f2RLGGLQZzSHpkmZWuwJbAvo3QcRY8p80m5+ZdXZdYOSBLlp7Cg=="], + + "@redis/client": ["@redis/client@1.6.1", "", { "dependencies": { "cluster-key-slot": "1.1.2", "generic-pool": "3.9.0", "yallist": "4.0.0" } }, "sha512-/KCsg3xSlR+nCK8/8ZYSknYxvXHwubJrU82F3Lm1Fp6789VQ0/3RJKfsmRXjqfaTA++23CvC3hqmqe/2GEt6Kw=="], + + "@redis/graph": ["@redis/graph@1.1.1", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-FEMTcTHZozZciLRl6GiiIB4zGm5z5F3F6a6FZCyrfxdKOhFlGkiAqlexWMBzCi4DcRoyiOsuLfW+cjlGWyExOw=="], + + "@redis/json": ["@redis/json@1.0.7", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-6UyXfjVaTBTJtKNG4/9Z8PSpKE6XgSyEb8iwaqDcy+uKrd/DGYHTWkUdnQDyzm727V7p21WUMhsqz5oy65kPcQ=="], + + "@redis/search": ["@redis/search@1.2.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-tYoDBbtqOVigEDMAcTGsRlMycIIjwMCgD8eR2t0NANeQmgK/lvxNAvYyb6bZDD4frHRhIHkJu2TBRvB0ERkOmw=="], + + "@redis/time-series": ["@redis/time-series@1.1.0", "", { "peerDependencies": { "@redis/client": "^1.0.0" } }, "sha512-c1Q99M5ljsIuc4YdaCwfUEXsofakb9c8+Zse2qxTadu8TalLXuAESzLvFAvNVbkmSlvlzIQOLpBCmWI9wTOt+g=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "cluster-key-slot": ["cluster-key-slot@1.1.2", "", {}, "sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA=="], + + "cookie": ["cookie@1.1.1", "", {}, "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "elysia": ["elysia@1.4.25", "", { "dependencies": { "cookie": "^1.1.1", "exact-mirror": "^0.2.7", "fast-decode-uri-component": "^1.0.1", "memoirist": "^0.4.0" }, "peerDependencies": { "@sinclair/typebox": ">= 0.34.0 < 1", "@types/bun": ">= 1.2.0", "file-type": ">= 20.0.0", "openapi-types": ">= 12.0.0", "typescript": ">= 5.0.0" }, "optionalPeers": ["@types/bun", "typescript"] }, "sha512-liKjavH99Gpzrv9cDil6uYWmPuqESfPFV1FIaFSd3iNqo3y7e29sN43VxFIK8tWWnyi6eDAmi2SZk8hNAMQMyg=="], + + "exact-mirror": ["exact-mirror@0.2.7", "", { "peerDependencies": { "@sinclair/typebox": "^0.34.15" }, "optionalPeers": ["@sinclair/typebox"] }, "sha512-+MeEmDcLA4o/vjK2zujgk+1VTxPR4hdp23qLqkWfStbECtAq9gmsvQa3LW6z/0GXZyHJobrCnmy1cdeE7BjsYg=="], + + "fast-decode-uri-component": ["fast-decode-uri-component@1.0.1", "", {}, "sha512-WKgKWg5eUxvRZGwW8FvfbaH7AXSh2cL+3j5fMGzUMCxWBJ3dV3a7Wz8y2f/uQ0e3B6WmodD3oS54jTQ9HVTIIg=="], + + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + + "generic-pool": ["generic-pool@3.9.0", "", {}, "sha512-hymDOu5B53XvN4QT9dBmZxPX4CWhBPPLguTZ9MMFeFa/Kg0xWVfylOVNlJji/E7yTZWFd/q9GO5TxDLq156D7g=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "memoirist": ["memoirist@0.4.0", "", {}, "sha512-zxTgA0mSYELa66DimuNQDvyLq36AwDlTuVRbnQtB+VuTcKWm5Qc4z3WkSpgsFWHNhexqkIooqpv4hdcqrX5Nmg=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "openapi-types": ["openapi-types@12.1.3", "", {}, "sha512-N4YtSYJqghVu4iek2ZUvcN/0aqH1kRDuNqzcycDxhOUpg7GdvLa2F3DgS6yBNhInhv2r/6I0Flkn7CqL8+nIcw=="], + + "redis": ["redis@4.7.1", "", { "dependencies": { "@redis/bloom": "1.2.0", "@redis/client": "1.6.1", "@redis/graph": "1.1.1", "@redis/json": "1.0.7", "@redis/search": "1.2.0", "@redis/time-series": "1.1.0" } }, "sha512-S1bJDnqLftzHXHP8JsT5II/CtHWQrASX5K96REjWjlmWKrviSOLWmM7QnRLstAWsu1VBBV1ffV6DzCvxNP0UJQ=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="], + } +} diff --git a/explorer/package.json b/explorer/package.json new file mode 100644 index 0000000..79fd16a --- /dev/null +++ b/explorer/package.json @@ -0,0 +1,12 @@ +{ + "name": "localestia-ui", + "private": true, + "type": "module", + "scripts": { + "dev": "bun src/index.ts" + }, + "dependencies": { + "elysia": "^1.0.0", + "redis": "^4.7.0" + } +} diff --git a/explorer/src/index.ts b/explorer/src/index.ts new file mode 100644 index 0000000..d209b9c --- /dev/null +++ b/explorer/src/index.ts @@ -0,0 +1,1299 @@ +import { Elysia } from "elysia"; +import { createHash } from "node:crypto"; +import { readFile, readdir } from "node:fs/promises"; +import { extname, resolve } from "node:path"; +import { createClient, RedisClientType } from "redis"; + +const celestiaUrl = process.env.CELESTIA_HTTP_URL ?? "http://127.0.0.1:26658"; +const ethRpcUrl = process.env.ETH_RPC_URL ?? "http://127.0.0.1:8545"; +const contractAddress = process.env.BLOBSTREAM_CONTRACT_ADDRESS; +const port = Number(process.env.UI_PORT ?? 3030); +const celestiaLimit = Number(process.env.CELESTIA_BLOCKS_LIMIT ?? 20); +const ethereumLimit = Number(process.env.ETH_BLOCKS_LIMIT ?? 20); +const ethLogBlocks = Number(process.env.ETH_LOG_BLOCKS ?? 200); +const pageSize = 10; +const assetsDir = resolve(import.meta.dir, "../../assets"); +const rollupDir = process.env.ROLLUP_METADATA_DIR; +const rollupRedisUrl = process.env.ROLLUP_REDIS_URL; +const rollupRedisKey = process.env.ROLLUP_REDIS_KEY ?? "localestia:rollups"; + +if (!contractAddress) { + console.error("BLOBSTREAM_CONTRACT_ADDRESS is required"); + process.exit(1); +} + +type CelestiaBlock = { + height: number; + hash: string; + time: string; +}; + +type CelestiaBlockDetail = CelestiaBlock & { + squareWidth: number; + appVersion: string; + lastBlockHash: string; + dahHash: string; + raw: unknown; +}; + +type EthereumBlock = { + height: number; + hash: string; + time: string; +}; + +type EthereumBlockDetail = EthereumBlock & { + txCount: number; + gasUsed: string; + baseFeePerGas: string; + parentHash: string; + raw: unknown; +}; + +type RelayedBatch = { + proofNonce: string; + startBlock: number; + endBlock: number; + dataCommitment: string; + txHash: string; + blockNumber: number; +}; + +type RelayedBatchDetail = RelayedBatch & { + blockTime: string; + raw: unknown; +}; + +type Rollup = { + id: string; + name: string; + status: string; + chainId?: string | number; + rpcUrl?: string; + description?: string; + source: string; + raw: unknown; +}; + +type Paginated = { + items: T[]; + nextCursor: string | number | null; +}; + +const cache = new Map(); +let redisClient: RedisClientType | null = null; +let redisReady = false; +let redisUnavailable = false; + +async function withCache(key: string, ttlMs: number, fn: () => Promise): Promise { + const now = Date.now(); + const entry = cache.get(key); + if (entry && now - entry.ts < ttlMs) { + return entry.value as T; + } + const value = await fn(); + cache.set(key, { ts: now, value }); + return value; +} + +async function rpc(url: string, method: string, params: unknown[]) { + const response = await fetch(url, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jsonrpc: "2.0", id: 1, method, params }), + }); + if (!response.ok) { + throw new Error("RPC request failed: " + response.status); + } + const data = await response.json(); + if (data.error) { + throw new Error(data.error.message ?? "RPC error"); + } + return data.result; +} + +function parseCelestiaBlock(header: any): CelestiaBlock { + const height = Number(header?.header?.height ?? 0); + const hash = header?.commit?.block_id?.hash ?? ""; + const time = header?.header?.time ?? ""; + return { height, hash, time }; +} + +function parseCelestiaDetail(header: any): CelestiaBlockDetail { + const summary = parseCelestiaBlock(header); + const squareWidth = Number(header?.dah?.row_roots?.length ?? 0); + const appVersion = String(header?.header?.version?.app ?? ""); + const lastBlockHash = header?.header?.last_block_id?.hash ?? ""; + const dahHash = computeDahHash(header?.dah ?? null); + return { + ...summary, + squareWidth, + appVersion, + lastBlockHash, + dahHash, + raw: header, + }; +} + +function parseEthereumBlock(block: any): EthereumBlock { + const height = Number.parseInt(block?.number ?? "0", 16); + const time = block?.timestamp + ? new Date(Number.parseInt(block.timestamp, 16) * 1000).toISOString() + : ""; + return { height, hash: block?.hash ?? "", time }; +} + +function parseEthereumDetail(block: any): EthereumBlockDetail { + const summary = parseEthereumBlock(block); + const txCount = Array.isArray(block?.transactions) ? block.transactions.length : 0; + const gasUsed = block?.gasUsed ?? ""; + const baseFeePerGas = block?.baseFeePerGas ?? ""; + const parentHash = block?.parentHash ?? ""; + return { + ...summary, + txCount, + gasUsed, + baseFeePerGas, + parentHash, + raw: block, + }; +} + +function decodeRoot(value: string): Uint8Array { + if (!value) { + return new Uint8Array(); + } + if (/^[0-9a-fA-F]+$/.test(value) && value.length % 2 === 0) { + return Uint8Array.from(Buffer.from(value, "hex")); + } + return Uint8Array.from(Buffer.from(value, "base64")); +} + +function sha256(bytes: Uint8Array): Uint8Array { + return Uint8Array.from(createHash("sha256").update(bytes).digest()); +} + +function leafHash(bytes: Uint8Array): Uint8Array { + const prefixed = Buffer.concat([Buffer.from([0x00]), Buffer.from(bytes)]); + return sha256(prefixed); +} + +function innerHash(left: Uint8Array, right: Uint8Array): Uint8Array { + const prefixed = Buffer.concat([Buffer.from([0x01]), Buffer.from(left), Buffer.from(right)]); + return sha256(prefixed); +} + +function nextPowerOfTwo(value: number): number { + if (value <= 1) return 1; + let power = 1; + while (power < value) { + power *= 2; + } + return power; +} + +function simpleHashFromByteVectors(leaves: Uint8Array[]): Uint8Array { + const length = leaves.length; + if (length === 0) { + return sha256(new Uint8Array()); + } + if (length === 1) { + return leafHash(leaves[0]); + } + const split = nextPowerOfTwo(length) / 2; + const left = simpleHashFromByteVectors(leaves.slice(0, split)); + const right = simpleHashFromByteVectors(leaves.slice(split)); + return innerHash(left, right); +} + +function toHexUpper(bytes: Uint8Array): string { + return Buffer.from(bytes).toString("hex").toUpperCase(); +} + +function computeDahHash(dah: any): string { + if (!dah) return ""; + const rowRoots = Array.isArray(dah.row_roots) ? dah.row_roots : []; + const colRoots = Array.isArray(dah.column_roots) ? dah.column_roots : []; + const leaves = rowRoots.concat(colRoots).map((root: string) => decodeRoot(root)); + if (leaves.length === 0) { + return ""; + } + const root = simpleHashFromByteVectors(leaves); + return toHexUpper(root); +} + +function hexToNumber(hex: string | undefined): number { + if (!hex) return 0; + return Number(BigInt(hex)); +} + +function toBigInt(value: string): bigint { + try { + return BigInt(value); + } catch { + return BigInt(0); + } +} + +async function listCelestiaBlocks(limit: number, before?: number): Promise> { + const key = "celestia:" + limit + ":" + (before ?? "latest"); + return withCache(key, 750, async () => { + const head = await rpc(celestiaUrl, "header.LocalHead", []); + const headHeight = Number(head?.header?.height ?? 0); + if (!headHeight) { + return { items: [], nextCursor: null }; + } + let start = headHeight; + if (before && before > 0) { + start = Math.min(before - 1, headHeight); + } + if (start < 1) { + return { items: [], nextCursor: null }; + } + const end = Math.max(1, start - limit + 1); + const items: CelestiaBlock[] = []; + for (let height = start; height >= end; height--) { + const header = await rpc(celestiaUrl, "header.GetByHeight", [height]); + items.push(parseCelestiaBlock(header)); + } + const nextCursor = end > 1 ? end : null; + return { items, nextCursor }; + }); +} + +async function getCelestiaBlockDetail(height: number): Promise { + if (!height) return null; + const header = await rpc(celestiaUrl, "header.GetByHeight", [height]); + return parseCelestiaDetail(header); +} + +async function listEthereumBlocks(limit: number, before?: number): Promise> { + const key = "ethereum:" + limit + ":" + (before ?? "latest"); + return withCache(key, 750, async () => { + const latestHex = await rpc(ethRpcUrl, "eth_blockNumber", []); + const latest = Number.parseInt(latestHex ?? "0", 16); + if (!latest && latest !== 0) { + return { items: [], nextCursor: null }; + } + let start = latest; + if (before !== undefined && before !== null) { + start = Math.min(before - 1, latest); + } + if (start < 0) { + return { items: [], nextCursor: null }; + } + const end = Math.max(0, start - limit + 1); + const items: EthereumBlock[] = []; + for (let height = start; height >= end; height--) { + const hex = "0x" + height.toString(16); + const block = await rpc(ethRpcUrl, "eth_getBlockByNumber", [hex, false]); + items.push(parseEthereumBlock(block)); + } + const nextCursor = end > 0 ? end : null; + return { items, nextCursor }; + }); +} + +async function getEthereumBlockDetail(height: number): Promise { + if (height === null || height === undefined) return null; + const hex = "0x" + height.toString(16); + const block = await rpc(ethRpcUrl, "eth_getBlockByNumber", [hex, true]); + return parseEthereumDetail(block); +} + +async function fetchBatchLogs(): Promise { + const latestHex = await rpc(ethRpcUrl, "eth_blockNumber", []); + const latest = Number.parseInt(latestHex ?? "0", 16); + const from = Math.max(0, latest - ethLogBlocks + 1); + return rpc(ethRpcUrl, "eth_getLogs", [ + { + address: contractAddress, + fromBlock: "0x" + from.toString(16), + toBlock: "latest", + }, + ]); +} + +function parseBatch(log: any): RelayedBatch { + const topics = log.topics ?? []; + const startBlock = hexToNumber(topics[1]); + const endBlock = hexToNumber(topics[2]); + const dataCommitment = topics[3] ?? ""; + const proofNonce = log.data ? BigInt(log.data).toString() : "0"; + const blockNumber = hexToNumber(log.blockNumber); + return { + proofNonce, + startBlock, + endBlock, + dataCommitment, + txHash: log.transactionHash ?? "", + blockNumber, + }; +} + +async function listBatches(limit: number, before?: string): Promise> { + const key = "batches:" + limit + ":" + (before ?? "latest"); + return withCache(key, 750, async () => { + const logs = await fetchBatchLogs(); + const parsed = (logs ?? []).map(parseBatch); + parsed.sort((a, b) => { + const left = toBigInt(a.proofNonce); + const right = toBigInt(b.proofNonce); + if (left === right) return 0; + return left < right ? 1 : -1; + }); + const filtered = before + ? parsed.filter((item) => toBigInt(item.proofNonce) < toBigInt(before)) + : parsed; + const items = filtered.slice(0, limit); + const nextCursor = items.length === limit ? items[items.length - 1].proofNonce : null; + return { items, nextCursor }; + }); +} + +async function getBatchDetail(proofNonce: string): Promise { + const logs = await fetchBatchLogs(); + const match = (logs ?? []).find((log: any) => { + const nonce = log.data ? BigInt(log.data).toString() : "0"; + return nonce === proofNonce; + }); + if (!match) return null; + const parsed = parseBatch(match); + const block = await rpc(ethRpcUrl, "eth_getBlockByNumber", [match.blockNumber, false]); + const time = block?.timestamp + ? new Date(Number.parseInt(block.timestamp, 16) * 1000).toISOString() + : ""; + return { + ...parsed, + blockTime: time, + raw: match, + }; +} + +function normalizeRollup(raw: any, fallbackId: string, source: string): Rollup | null { + if (!raw || typeof raw !== "object") { + return null; + } + const id = String(raw.id ?? fallbackId); + const name = String(raw.name ?? raw.title ?? id); + const status = String(raw.status ?? "unknown"); + const chainId = raw.chainId ?? raw.chain_id; + const rpcUrl = raw.rpcUrl ?? raw.rpc_url; + const description = raw.description ?? raw.summary; + return { + id, + name, + status, + chainId, + rpcUrl, + description, + source, + raw, + }; +} + +async function loadRollupsFromDir(): Promise { + if (!rollupDir) return []; + try { + const files = await readdir(rollupDir); + const rollups: Rollup[] = []; + for (const file of files) { + if (!file.endsWith(".json")) continue; + const path = resolve(rollupDir, file); + try { + const data = await readFile(path, "utf-8"); + const parsed = JSON.parse(data); + if (Array.isArray(parsed)) { + for (const entry of parsed) { + const rollup = normalizeRollup(entry, file, "folder"); + if (rollup) rollups.push(rollup); + } + } else { + const rollup = normalizeRollup(parsed, file, "folder"); + if (rollup) rollups.push(rollup); + } + } catch { + continue; + } + } + return rollups; + } catch { + return []; + } +} + +async function getRedisClient(): Promise { + if (!rollupRedisUrl || redisUnavailable) { + return null; + } + if (redisClient && redisReady) { + return redisClient; + } + try { + redisClient = createClient({ url: rollupRedisUrl }); + redisClient.on("error", () => { + redisUnavailable = true; + }); + await redisClient.connect(); + redisReady = true; + return redisClient; + } catch { + redisUnavailable = true; + return null; + } +} + +async function loadRollupsFromRedis(): Promise { + const client = await getRedisClient(); + if (!client) return []; + try { + const entries = await client.lRange(rollupRedisKey, 0, -1); + const rollups: Rollup[] = []; + for (const entry of entries) { + try { + const parsed = JSON.parse(entry); + const rollup = normalizeRollup(parsed, parsed.id ?? entry, "redis"); + if (rollup) rollups.push(rollup); + } catch { + continue; + } + } + return rollups; + } catch { + return []; + } +} + +async function listRollups(): Promise { + return withCache("rollups", 1000, async () => { + const [folderRollups, redisRollups] = await Promise.all([ + loadRollupsFromDir(), + loadRollupsFromRedis(), + ]); + const merged = new Map(); + for (const rollup of folderRollups) { + merged.set(rollup.id, rollup); + } + for (const rollup of redisRollups) { + if (!merged.has(rollup.id)) { + merged.set(rollup.id, rollup); + } + } + return Array.from(merged.values()); + }); +} + +function contentTypeFor(path: string): string { + const ext = extname(path).toLowerCase(); + if (ext === ".svg") return "image/svg+xml"; + if (ext === ".png") return "image/png"; + if (ext === ".jpg" || ext === ".jpeg") return "image/jpeg"; + if (ext === ".webp") return "image/webp"; + if (ext === ".webm") return "video/webm"; + return "application/octet-stream"; +} + +function parseNumber(value: unknown): number | undefined { + if (value === undefined || value === null || value === "") { + return undefined; + } + const parsed = Number(value); + if (Number.isNaN(parsed)) { + return undefined; + } + return parsed; +} + +const app = new Elysia() + .get("/assets/:file", async ({ params, set }) => { + const filePath = resolve(assetsDir, params.file); + if (!filePath.startsWith(assetsDir)) { + set.status = 400; + return "invalid asset path"; + } + try { + const data = await readFile(filePath); + set.headers["content-type"] = contentTypeFor(filePath); + return data; + } catch { + set.status = 404; + return "asset not found"; + } + }) + .get("/", ({ set }) => { + set.headers["content-type"] = "text/html; charset=utf-8"; + return ` + + + + + Localestia Explorer + + + +
+
+

Localestia Explorer

+
+
+ +
Celestia (local)
+
Waiting for blocks...
+
+
+ +
+
+ +
Ethereum (Anvil)
+
Waiting for blocks...
+
+
+ +
+
+
+
Celestia Blocks
+
+ + 1 + +
+
+
    +
    +
    +
    +
    Ethereum Blocks
    +
    + + 1 + +
    +
    +
      +
      +
      +
      +
      Relayed Batches
      +
      + + 1 + +
      +
      +
        +
        +
        + +
        + + +`; + }) + .get("/api/state", async () => { + const [celestia, ethereum, batches] = await Promise.all([ + listCelestiaBlocks(pageSize), + listEthereumBlocks(pageSize), + listBatches(pageSize), + ]); + return { celestia: celestia.items, ethereum: ethereum.items, batches: batches.items }; + }) + .get("/api/celestia/blocks", async ({ query }) => { + const parsedLimit = parseNumber(query.limit) ?? 20; + const limit = Math.max(1, Math.min(100, parsedLimit)); + const before = parseNumber(query.before); + return listCelestiaBlocks(limit, before); + }) + .get("/api/celestia/blocks/:height", async ({ params, set }) => { + const height = Number(params.height); + if (!height) { + set.status = 400; + return { error: "invalid height" }; + } + const detail = await getCelestiaBlockDetail(height); + if (!detail) { + set.status = 404; + return { error: "not found" }; + } + return detail; + }) + .get("/api/ethereum/blocks", async ({ query }) => { + const parsedLimit = parseNumber(query.limit) ?? 20; + const limit = Math.max(1, Math.min(100, parsedLimit)); + const before = parseNumber(query.before); + return listEthereumBlocks(limit, before); + }) + .get("/api/ethereum/blocks/:height", async ({ params, set }) => { + const height = Number(params.height); + if (height === null || Number.isNaN(height)) { + set.status = 400; + return { error: "invalid height" }; + } + const detail = await getEthereumBlockDetail(height); + if (!detail) { + set.status = 404; + return { error: "not found" }; + } + return detail; + }) + .get("/api/batches", async ({ query }) => { + const parsedLimit = parseNumber(query.limit) ?? 20; + const limit = Math.max(1, Math.min(100, parsedLimit)); + const before = query.before ? String(query.before) : undefined; + return listBatches(limit, before); + }) + .get("/api/batches/:proofNonce", async ({ params, set }) => { + const proofNonce = params.proofNonce; + if (!proofNonce) { + set.status = 400; + return { error: "invalid proof nonce" }; + } + const detail = await getBatchDetail(proofNonce); + if (!detail) { + set.status = 404; + return { error: "not found" }; + } + return detail; + }) + .get("/api/rollups", async () => { + const items = await listRollups(); + return { items, nextCursor: null }; + }) + .get("/api/rollups/:id", async ({ params, set }) => { + const id = params.id; + if (!id) { + set.status = 400; + return { error: "invalid rollup id" }; + } + const rollups = await listRollups(); + const rollup = rollups.find((entry) => entry.id === id); + if (!rollup) { + set.status = 404; + return { error: "not found" }; + } + return rollup; + }) + .listen(port); + +console.log("UI listening on http://127.0.0.1:" + port); +console.log("Celestia RPC: " + celestiaUrl); +console.log("Ethereum RPC: " + ethRpcUrl); diff --git a/scripts/setup_anvil.sh b/scripts/setup_anvil.sh new file mode 100755 index 0000000..f7c9bde --- /dev/null +++ b/scripts/setup_anvil.sh @@ -0,0 +1,51 @@ +#!/usr/bin/env bash +set -euo pipefail + +ANVIL_PORT=${ANVIL_PORT:-8545} +ANVIL_CONTAINER_NAME=${ANVIL_CONTAINER_NAME:-localestia-anvil} +ANVIL_IMAGE=${ANVIL_IMAGE:-ghcr.io/foundry-rs/foundry:latest} +ANVIL_MNEMONIC=${ANVIL_MNEMONIC:-"test test test test test test test test test test test junk"} + +install_foundry() { + if ! command -v foundryup >/dev/null 2>&1; then + curl -L https://foundry.paradigm.xyz | bash + fi + + export PATH="$HOME/.foundry/bin:$PATH" + foundryup +} + +ensure_foundry() { + if ! command -v forge >/dev/null 2>&1; then + install_foundry + fi +} + +if command -v docker >/dev/null 2>&1; then + ensure_foundry + + docker rm -f "$ANVIL_CONTAINER_NAME" >/dev/null 2>&1 || true + docker run -d --name "$ANVIL_CONTAINER_NAME" -p "$ANVIL_PORT:8545" "$ANVIL_IMAGE" \ + anvil --host 0.0.0.0 --port 8545 -m "$ANVIL_MNEMONIC" >/dev/null + + for _ in {1..50}; do + if docker logs "$ANVIL_CONTAINER_NAME" 2>&1 | grep -q "Listening on"; then + break + fi + sleep 0.1 + done + + PRIVATE_KEY=$(docker logs "$ANVIL_CONTAINER_NAME" 2>&1 | awk '/Private Keys/{found=1;next} found && $1 ~ /^\(0\)/ {print $2; exit}') + if [[ -z "$PRIVATE_KEY" ]]; then + echo "failed to read the first Anvil private key from docker logs" + exit 1 + fi + + echo "Anvil is running in docker as $ANVIL_CONTAINER_NAME" + echo "export ANVIL_RPC_URL=http://127.0.0.1:${ANVIL_PORT}" + echo "export ANVIL_PRIVATE_KEY=$PRIVATE_KEY" + exit 0 +fi + +ensure_foundry +echo "Foundry installed. Run tests and local Anvil will be spawned automatically." diff --git a/src/bin/blobstream_relayer.rs b/src/bin/blobstream_relayer.rs new file mode 100644 index 0000000..c147912 --- /dev/null +++ b/src/bin/blobstream_relayer.rs @@ -0,0 +1,7 @@ +use anyhow::Result; +use localestia::relayer; + +#[tokio::main] +async fn main() -> Result<()> { + relayer::run_from_env().await +} diff --git a/src/cli.rs b/src/cli.rs new file mode 100644 index 0000000..87d2dad --- /dev/null +++ b/src/cli.rs @@ -0,0 +1,873 @@ +use std::env; +use std::fs; +use std::io::{BufRead, BufReader}; +use std::net::SocketAddr; +use std::path::PathBuf; +use std::process::{Child, Command, Stdio}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use clap::{Args, Parser, Subcommand}; +use ethers::abi::Abi; +use ethers::contract::ContractFactory; +use ethers::middleware::SignerMiddleware; +use ethers::providers::{Http, Middleware, Provider}; +use ethers::signers::{LocalWallet, Signer}; +use ethers::types::Bytes; +use jsonrpsee::server::{ServerBuilder, ServerHandle}; +use rand::RngCore; +use serde::Deserialize; +use tokio::sync::watch; +use tokio::time::sleep; +use tracing::{error, info, warn}; +use uuid::Uuid; + +use crate::error::LocalError; +use crate::rpc::LocalestiaServer; +use crate::storage::RedisStorage; +use celestia_rpc::{ + BlobClient, BlobRpcServer, BlobstreamRpcServer, Client, HeaderRpcServer, TxConfig, +}; +use celestia_types::nmt::Namespace; +use celestia_types::{AppVersion, Blob}; +use localestia::relayer::{self, RelayerConfig}; + +use crate::rpc::ShareRpcServer; + +const DEFAULT_LISTEN_ADDR: &str = "127.0.0.1:26658"; +const DEFAULT_REDIS_URL: &str = "redis://127.0.0.1:6379"; +const DEFAULT_ANVIL_PORT: u16 = 8545; +const DEFAULT_UI_PORT: u16 = 3030; +const DEFAULT_REDIS_IMAGE: &str = "redis:7"; +const DEFAULT_ANVIL_IMAGE: &str = "ghcr.io/foundry-rs/foundry:latest"; +const DEFAULT_ANVIL_MNEMONIC: &str = "test test test test test test test test test test test junk"; + +#[derive(Parser)] +#[command( + name = "localestia", + version, + about = "Local Celestia JSON-RPC emulator" +)] +struct Cli { + #[command(subcommand)] + command: Option, +} + +#[derive(Subcommand)] +enum Commands { + Blobstream { + #[command(subcommand)] + command: BlobstreamCommands, + }, + Demo(DemoArgs), +} + +#[derive(Subcommand)] +enum BlobstreamCommands { + Deploy(DeployArgs), +} + +#[derive(Args)] +struct DeployArgs { + #[arg(long)] + eth_rpc_url: String, + #[arg(long)] + private_key: String, + #[arg(long)] + chain_id: Option, +} + +#[derive(Args)] +struct DemoArgs { + #[arg(long, default_value = DEFAULT_LISTEN_ADDR)] + listen_addr: String, + #[arg(long)] + redis_url: Option, + #[arg(long, default_value_t = DEFAULT_ANVIL_PORT)] + anvil_port: u16, + #[arg(long, default_value = DEFAULT_ANVIL_MNEMONIC)] + anvil_mnemonic: String, + #[arg(long, default_value_t = DEFAULT_UI_PORT)] + ui_port: u16, + #[arg(long, default_value_t = 1000)] + relayer_interval_ms: u64, + #[arg(long, default_value_t = 1500)] + auto_blob_interval_ms: u64, +} + +#[derive(Deserialize)] +struct ForgeArtifact { + abi: serde_json::Value, + bytecode: ForgeBytecode, +} + +#[derive(Deserialize)] +struct ForgeBytecode { + object: String, +} + +pub async fn run() -> Result<(), Box> { + tracing_subscriber::fmt::init(); + + let cli = Cli::parse(); + match cli.command { + None => run_server_from_env().await, + Some(Commands::Blobstream { + command: BlobstreamCommands::Deploy(args), + }) => deploy_command(args).await, + Some(Commands::Demo(args)) => run_demo(args).await, + } +} + +async fn run_server_from_env() -> Result<(), Box> { + let redis_url = env::var("REDIS_URL").unwrap_or_else(|_| DEFAULT_REDIS_URL.to_string()); + let listen_addr = env::var("LISTEN_ADDR").unwrap_or_else(|_| DEFAULT_LISTEN_ADDR.to_string()); + let server_handle = start_rpc_server(&redis_url, &listen_addr).await?; + + info!("Server started successfully"); + info!("The server is ready to accept RPC calls at ws://{listen_addr}"); + info!("Connect using: celestia-rpc::Client::new(\"ws://{listen_addr}\", None)"); + + match tokio::signal::ctrl_c().await { + Ok(_) => info!("Received shutdown signal"), + Err(e) => error!("Failed to listen for ctrl-c: {e}"), + } + + info!("Shutting down server..."); + if let Err(e) = server_handle.stop() { + error!("Error stopping server: {e}"); + } + + info!("Server shutdown complete"); + Ok(()) +} + +async fn deploy_command(args: DeployArgs) -> Result<(), Box> { + let address = deploy_mockstream(&args.eth_rpc_url, &args.private_key, args.chain_id).await?; + println!("{address}"); + Ok(()) +} + +async fn run_demo(args: DemoArgs) -> Result<(), Box> { + let redis_env = env::var("REDIS_URL").ok(); + let redis_url = args.redis_url.or(redis_env).unwrap_or_default(); + let mut redis_guard = None; + let redis_url = if redis_url.is_empty() { + let guard = start_redis_docker()?; + let url = guard.url.clone(); + redis_guard = Some(guard); + url + } else { + redis_url + }; + wait_for_redis(&redis_url).await?; + + let anvil_guard = start_anvil(args.anvil_port, &args.anvil_mnemonic)?; + let anvil_rpc_url = anvil_guard.endpoint.clone(); + let anvil_private_key = anvil_guard.private_key.clone(); + + let server_handle = start_rpc_server(&redis_url, &args.listen_addr).await?; + + let contract_address = deploy_mockstream( + &anvil_rpc_url, + &anvil_private_key, + Some(anvil_guard.chain_id), + ) + .await?; + + let (shutdown_tx, shutdown_rx) = watch::channel(false); + let relayer_handle = tokio::spawn(relayer::run_with_shutdown( + RelayerConfig { + celestia_ws_url: format!("ws://{}", args.listen_addr), + eth_rpc_url: anvil_rpc_url.clone(), + eth_private_key: anvil_private_key.clone(), + contract_address: contract_address.clone(), + poll_ms: args.relayer_interval_ms, + }, + shutdown_rx, + )); + + let auto_blob_handle = if args.auto_blob_interval_ms > 0 { + Some(tokio::spawn(auto_blob_loop( + format!("ws://{}", args.listen_addr), + args.auto_blob_interval_ms, + shutdown_tx.subscribe(), + ))) + } else { + None + }; + + let mut ui_child = start_ui_server( + &args.listen_addr, + &anvil_rpc_url, + &contract_address, + args.ui_port, + &redis_url, + )?; + + info!("Demo ready:"); + info!("Localestia WS: ws://{}", args.listen_addr); + info!("Localestia HTTP: http://{}", args.listen_addr); + info!("Anvil RPC: {anvil_rpc_url}"); + info!("Mock contract: {contract_address}"); + info!("UI: http://127.0.0.1:{}", args.ui_port); + + match tokio::signal::ctrl_c().await { + Ok(_) => info!("Received shutdown signal"), + Err(e) => error!("Failed to listen for ctrl-c: {e}"), + } + + let _ = shutdown_tx.send(true); + relayer_handle.abort(); + if let Some(handle) = auto_blob_handle { + handle.abort(); + } + + if let Err(e) = server_handle.stop() { + error!("Error stopping server: {e}"); + } + + if let Err(err) = ui_child.kill() { + warn!("Failed to stop UI server: {err}"); + } + + drop(redis_guard); + drop(anvil_guard); + + Ok(()) +} + +async fn auto_blob_loop(ws_url: String, interval_ms: u64, mut shutdown: watch::Receiver) { + let mut client: Option = None; + let mut counter = 0u64; + + loop { + if *shutdown.borrow() { + break; + } + + if client.is_none() { + match Client::new(&ws_url, None, None, None).await { + Ok(new_client) => client = Some(new_client), + Err(err) => { + warn!("auto-blob: failed to connect to Localestia: {err}"); + sleep(Duration::from_millis(interval_ms)).await; + continue; + } + } + } + + if let Some(client) = &client { + let mut namespace_bytes = [0u8; 8]; + rand::thread_rng().fill_bytes(&mut namespace_bytes); + let namespace = match Namespace::new_v0(&namespace_bytes) { + Ok(namespace) => namespace, + Err(err) => { + warn!("auto-blob: failed to create namespace: {err}"); + continue; + } + }; + + counter += 1; + let data = format!("demo blob {counter}").into_bytes(); + let blob = match Blob::new(namespace, data, None, AppVersion::V3) { + Ok(blob) => blob, + Err(err) => { + warn!("auto-blob: failed to build blob: {err}"); + continue; + } + }; + + if let Err(err) = client + .blob_submit(std::slice::from_ref(&blob), TxConfig::default()) + .await + { + warn!("auto-blob: submit failed: {err}"); + } + } + + tokio::select! { + _ = shutdown.changed() => {}, + _ = sleep(Duration::from_millis(interval_ms)) => {}, + } + } +} + +async fn start_rpc_server( + redis_url: &str, + listen_addr: &str, +) -> Result> { + let addr: SocketAddr = listen_addr.parse().map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to parse address: {e}" + ))) + })?; + + info!("Starting local Celestia Blob RPC server..."); + info!("Redis URL: {redis_url}"); + info!("Listening on: {listen_addr}"); + + let storage = match RedisStorage::new(redis_url) { + Ok(storage) => Arc::new(storage), + Err(e) => { + error!("Failed to initialize Redis storage: {e}"); + return Err(Box::new(e)); + } + }; + + if let Err(e) = storage.connect().await { + error!("Failed to connect to Redis: {e}"); + return Err(Box::new(e)); + } + + if let Err(e) = storage.clear_database().await { + error!("Failed to clear Redis database: {e}"); + return Err(Box::new(e)); + } + + let rpc_server = LocalestiaServer::new(storage.clone()); + let mut module = BlobRpcServer::into_rpc(rpc_server.clone()); + module + .merge(HeaderRpcServer::into_rpc(rpc_server.clone())) + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to merge header RPC: {e}" + ))) + })?; + module + .merge(ShareRpcServer::into_rpc(rpc_server.clone())) + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to merge share RPC: {e}" + ))) + })?; + module + .merge(BlobstreamRpcServer::into_rpc(rpc_server)) + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to merge blobstream RPC: {e}" + ))) + })?; + + let server = ServerBuilder::default().build(addr).await.map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to build server: {e}" + ))) + })?; + + Ok(server.start(module)) +} + +async fn deploy_mockstream( + eth_rpc_url: &str, + private_key: &str, + chain_id: Option, +) -> Result> { + ensure_forge_artifact()?; + let (abi, bytecode) = load_mockstream_artifact()?; + + let provider = Provider::::try_from(eth_rpc_url).map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to connect to ETH RPC: {e}" + ))) + })?; + let chain_id = match chain_id { + Some(chain_id) => chain_id, + None => provider + .get_chainid() + .await + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to fetch chain id: {e}" + ))) + })? + .as_u64(), + }; + + let wallet: LocalWallet = private_key + .parse::() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to parse private key: {e}" + ))) + })? + .with_chain_id(chain_id); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + let factory = ContractFactory::new(abi, bytecode, client); + + let contract = factory + .deploy(()) + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to deploy contract: {e}" + ))) + })? + .send() + .await + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to send deploy tx: {e}" + ))) + })?; + + let init_call = contract.method::<_, ()>("initialize", 0u64).map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to build initialize call: {e}" + ))) + })?; + let init = init_call.send().await.map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to initialize contract: {e}" + ))) + })?; + init.await.map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to confirm initialize: {e}" + ))) + })?; + + Ok(format!("0x{}", hex::encode(contract.address().as_bytes()))) +} + +fn ensure_forge_artifact() -> Result<(), Box> { + let artifact_path = contracts_dir() + .join("out") + .join("MockBlobstream.sol") + .join("Mockstream.json"); + if artifact_path.exists() { + return Ok(()); + } + + let status = Command::new("forge") + .arg("build") + .current_dir(contracts_dir()) + .status() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to run forge build: {e}" + ))) + })?; + + if !status.success() { + return Err(Box::new(LocalError::TransactionError(format!( + "forge build failed with status {status}" + )))); + } + + Ok(()) +} + +fn load_mockstream_artifact() -> Result<(Abi, Bytes), Box> { + let artifact_path = contracts_dir() + .join("out") + .join("MockBlobstream.sol") + .join("Mockstream.json"); + let bytes = fs::read(&artifact_path).map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to read {artifact_path:?}: {e}" + ))) + })?; + let artifact: ForgeArtifact = + serde_json::from_slice(&bytes).map_err(|e| Box::new(LocalError::SerializationError(e)))?; + let abi: Abi = serde_json::from_value(artifact.abi) + .map_err(|e| Box::new(LocalError::SerializationError(e)))?; + + let object = artifact.bytecode.object; + if object.is_empty() { + return Err(Box::new(LocalError::TransactionError( + "Forge artifact bytecode is empty".to_string(), + ))); + } + let object = object.strip_prefix("0x").unwrap_or(&object); + let bytes = hex::decode(object).map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Invalid bytecode hex: {e}" + ))) + })?; + + Ok((abi, Bytes::from(bytes))) +} + +fn contracts_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("contracts") +} + +fn start_ui_server( + listen_addr: &str, + eth_rpc_url: &str, + contract_address: &str, + ui_port: u16, + rollup_redis_url: &str, +) -> Result> { + let ui_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("explorer"); + if !ui_dir.exists() { + return Err(Box::new(LocalError::TransactionError( + "UI directory not found; expected ./explorer".to_string(), + ))); + } + + let node_modules = ui_dir.join("node_modules"); + let redis_module = node_modules.join("redis"); + if !node_modules.exists() || !redis_module.exists() { + let status = Command::new("bun") + .arg("install") + .current_dir(&ui_dir) + .status() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to run bun install: {e}" + ))) + })?; + if !status.success() { + return Err(Box::new(LocalError::TransactionError(format!( + "bun install failed with status {status}" + )))); + } + } + + let mut command = Command::new("bun"); + command + .arg("src/index.ts") + .current_dir(ui_dir) + .env("CELESTIA_HTTP_URL", format!("http://{listen_addr}")) + .env("ETH_RPC_URL", eth_rpc_url) + .env("BLOBSTREAM_CONTRACT_ADDRESS", contract_address) + .env("UI_PORT", ui_port.to_string()) + .env("ROLLUP_REDIS_URL", rollup_redis_url) + .stdout(Stdio::inherit()) + .stderr(Stdio::inherit()); + + if let Ok(value) = env::var("ROLLUP_REDIS_KEY") { + command.env("ROLLUP_REDIS_KEY", value); + } + if let Ok(value) = env::var("ROLLUP_METADATA_DIR") { + command.env("ROLLUP_METADATA_DIR", value); + } + + let child = command.spawn().map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to start UI server: {e}" + ))) + })?; + + Ok(child) +} + +struct RedisGuard { + url: String, + container_name: String, +} + +impl Drop for RedisGuard { + fn drop(&mut self) { + let _ = Command::new("docker") + .arg("rm") + .arg("-f") + .arg(&self.container_name) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } +} + +fn start_redis_docker() -> Result> { + if !docker_available() { + return Err(Box::new(LocalError::TransactionError( + "Docker is required to start Redis automatically".to_string(), + ))); + } + + let name = format!("localestia-demo-redis-{}", Uuid::new_v4()); + let output = Command::new("docker") + .arg("run") + .arg("-d") + .arg("-P") + .arg("--name") + .arg(&name) + .arg(DEFAULT_REDIS_IMAGE) + .output() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to start Redis docker: {e}" + ))) + })?; + + if !output.status.success() { + return Err(Box::new(LocalError::TransactionError(format!( + "Docker run failed: {}", + String::from_utf8_lossy(&output.stderr) + )))); + } + + let port = docker_host_port(&name, "6379/tcp")?; + Ok(RedisGuard { + url: format!("redis://127.0.0.1:{port}"), + container_name: name, + }) +} + +struct AnvilGuard { + endpoint: String, + private_key: String, + chain_id: u64, + container_name: Option, + child: Option, +} + +impl Drop for AnvilGuard { + fn drop(&mut self) { + if let Some(mut child) = self.child.take() { + let _ = child.kill(); + let _ = child.wait(); + } + + if let Some(name) = self.container_name.take() { + let _ = Command::new("docker") + .arg("rm") + .arg("-f") + .arg(name) + .stdout(Stdio::null()) + .stderr(Stdio::null()) + .status(); + } + } +} + +fn start_anvil(port: u16, mnemonic: &str) -> Result> { + if Command::new("anvil").arg("--version").output().is_ok() { + return start_anvil_local(port, mnemonic); + } + + if docker_available() { + return start_anvil_docker(port, mnemonic); + } + + Err(Box::new(LocalError::TransactionError( + "Anvil not found; install Foundry or enable Docker".to_string(), + ))) +} + +fn start_anvil_local(port: u16, mnemonic: &str) -> Result> { + let mut child = Command::new("anvil") + .arg("--port") + .arg(port.to_string()) + .arg("--mnemonic") + .arg(mnemonic) + .stdout(Stdio::piped()) + .stderr(Stdio::inherit()) + .spawn() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to start anvil: {e}" + ))) + })?; + + let stdout = child.stdout.take().ok_or_else(|| { + Box::new(LocalError::TransactionError( + "Failed to capture anvil stdout".to_string(), + )) + })?; + let reader = BufReader::new(stdout); + let mut private_key = None; + let mut chain_id = None; + let mut in_keys = false; + + let start = Instant::now(); + for line in reader.lines() { + let line = line.map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to read anvil output: {e}" + ))) + })?; + if line.contains("Listening on") { + break; + } + if line.starts_with("Private Keys") { + in_keys = true; + continue; + } + if in_keys && line.trim_start().starts_with("(0)") { + if let Some(key) = line.split("0x").last() { + private_key = Some(format!("0x{}", key.trim())); + } + } + if let Some(idx) = line.find("Chain ID:") { + let rest = &line[idx + "Chain ID:".len()..]; + if let Ok(value) = rest + .split_whitespace() + .next() + .unwrap_or("") + .parse::() + { + chain_id = Some(value); + } + } + + if start.elapsed() > Duration::from_secs(10) { + return Err(Box::new(LocalError::TransactionError( + "Timed out waiting for anvil to start".to_string(), + ))); + } + } + + let private_key = private_key.ok_or_else(|| { + Box::new(LocalError::TransactionError( + "Failed to parse anvil private key".to_string(), + )) + })?; + let chain_id = chain_id.unwrap_or(31337); + + Ok(AnvilGuard { + endpoint: format!("http://127.0.0.1:{port}"), + private_key, + chain_id, + container_name: None, + child: Some(child), + }) +} + +fn start_anvil_docker(port: u16, mnemonic: &str) -> Result> { + let name = format!("localestia-demo-anvil-{}", Uuid::new_v4()); + let output = Command::new("docker") + .arg("run") + .arg("-d") + .arg("--name") + .arg(&name) + .arg("-p") + .arg(format!("{port}:8545")) + .arg(DEFAULT_ANVIL_IMAGE) + .arg("anvil") + .arg("--host") + .arg("0.0.0.0") + .arg("--port") + .arg("8545") + .arg("--mnemonic") + .arg(mnemonic) + .output() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to start anvil docker: {e}" + ))) + })?; + + if !output.status.success() { + return Err(Box::new(LocalError::TransactionError(format!( + "Docker run failed: {}", + String::from_utf8_lossy(&output.stderr) + )))); + } + + let mut private_key = None; + let start = Instant::now(); + loop { + let logs = Command::new("docker") + .arg("logs") + .arg(&name) + .output() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to read anvil logs: {e}" + ))) + })?; + let logs = String::from_utf8_lossy(&logs.stdout); + if logs.contains("Listening on") { + for line in logs.lines() { + if line.trim_start().starts_with("(0)") { + if let Some(key) = line.split("0x").last() { + private_key = Some(format!("0x{}", key.trim())); + break; + } + } + } + if private_key.is_some() { + break; + } + } + + if start.elapsed() > Duration::from_secs(10) { + return Err(Box::new(LocalError::TransactionError( + "Timed out waiting for anvil docker".to_string(), + ))); + } + std::thread::sleep(Duration::from_millis(200)); + } + + let private_key = private_key.ok_or_else(|| { + Box::new(LocalError::TransactionError( + "Failed to parse anvil docker private key".to_string(), + )) + })?; + + Ok(AnvilGuard { + endpoint: format!("http://127.0.0.1:{port}"), + private_key, + chain_id: 31337, + container_name: Some(name), + child: None, + }) +} + +fn docker_available() -> bool { + Command::new("docker") + .arg("info") + .output() + .map(|output| output.status.success()) + .unwrap_or(false) +} + +fn docker_host_port(name: &str, port: &str) -> Result> { + let output = Command::new("docker") + .arg("inspect") + .arg("-f") + .arg(format!( + "{{{{(index (index .NetworkSettings.Ports \"{}\") 0).HostPort}}}}", + port + )) + .arg(name) + .output() + .map_err(|e| { + Box::new(LocalError::TransactionError(format!( + "Failed to inspect docker port: {e}" + ))) + })?; + + if !output.status.success() { + return Err(Box::new(LocalError::TransactionError( + String::from_utf8_lossy(&output.stderr).to_string(), + ))); + } + + let port = String::from_utf8_lossy(&output.stdout).trim().to_string(); + if port.is_empty() { + return Err(Box::new(LocalError::TransactionError( + "Docker inspect returned empty port".to_string(), + ))); + } + + Ok(port) +} + +async fn wait_for_redis(redis_url: &str) -> Result<(), Box> { + let start = Instant::now(); + let timeout = Duration::from_secs(10); + + loop { + if let Ok(client) = redis::Client::open(redis_url) { if let Ok(mut conn) = client.get_multiplexed_async_connection().await { + let pong: redis::RedisResult = + redis::cmd("PING").query_async(&mut conn).await; + if let Ok(pong) = pong { + if pong == "PONG" { + return Ok(()); + } + } + } } + + if start.elapsed() > timeout { + return Err(Box::new(LocalError::TransactionError(format!( + "Redis is not reachable at {redis_url}" + )))); + } + + sleep(Duration::from_millis(200)).await; + } +} diff --git a/src/lib.rs b/src/lib.rs index 75d24d4..6aa381f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,5 @@ pub mod error; +pub mod relayer; pub mod rpc; pub mod storage; pub mod types; diff --git a/src/main.rs b/src/main.rs index e03d725..de638c8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,120 +1,12 @@ -use jsonrpsee::server::ServerBuilder; -use std::net::SocketAddr; -use std::sync::Arc; -use tracing::{error, info}; - +mod cli; mod error; mod rpc; mod storage; mod types; mod utils; -use celestia_rpc::BlobRpcServer; -use celestia_rpc::HeaderRpcServer; -use crate::rpc::ShareRpcServer; -use error::LocalError; -use rpc::LocalestiaServer; -use storage::RedisStorage; #[tokio::main] -async fn main() -> Result<(), Box> { - // Initialize logging - tracing_subscriber::fmt::init(); - - // Parse command line arguments - let redis_url = - std::env::var("REDIS_URL").unwrap_or_else(|_| "redis://127.0.0.1:6379".to_string()); - let listen_addr = - std::env::var("LISTEN_ADDR").unwrap_or_else(|_| "127.0.0.1:26658".to_string()); - - // Create a new error variant for AddrParseError - let addr: SocketAddr = listen_addr.parse().map_err(|e| { - Box::new(LocalError::TransactionError(format!( - "Failed to parse address: {}", - e - ))) - })?; - - info!("Starting local Celestia Blob RPC server..."); - info!("Redis URL: {}", redis_url); - info!("Listening on: {}", listen_addr); - - // Initialize Redis storage - let storage = match RedisStorage::new(&redis_url) { - Ok(storage) => Arc::new(storage), - Err(e) => { - error!("Failed to initialize Redis storage: {}", e); - return Err(Box::new(e)); - } - }; - - // Connect to Redis - if let Err(e) = storage.connect().await { - error!("Failed to connect to Redis: {}", e); - return Err(Box::new(e)); - } - - if let Err(e) = storage.clear_database().await { - error!("Failed to clear Redis database: {}", e); - return Err(Box::new(e)); - } - - // Create our RPC services - let rpc_server = LocalestiaServer::new(storage.clone()); - - let mut module = BlobRpcServer::into_rpc(rpc_server.clone()); - module - .merge(HeaderRpcServer::into_rpc(rpc_server.clone())) - .map_err(|e| { - Box::new(LocalError::TransactionError(format!( - "Failed to merge header RPC: {}", - e - ))) - })?; - module - .merge(ShareRpcServer::into_rpc(rpc_server)) - .map_err(|e| { - Box::new(LocalError::TransactionError(format!( - "Failed to merge share RPC: {}", - e - ))) - })?; - - // Build and start the JSON-RPC server - let server = ServerBuilder::default().build(addr).await.map_err(|e| { - Box::new(LocalError::TransactionError(format!( - "Failed to build server: {}", - e - ))) - })?; - - // Register our RPC methods (both Blob and Header) - let server_handle = server.start(module); - - info!("Server started successfully"); - info!( - "The server is ready to accept RPC calls at ws://{}", - listen_addr - ); - info!( - "Connect using: celestia-rpc::Client::new(\"ws://{}\", None)", - listen_addr - ); - - // Keep the server running until Ctrl+C - match tokio::signal::ctrl_c().await { - Ok(_) => info!("Received shutdown signal"), - Err(e) => error!("Failed to listen for ctrl-c: {}", e), - } - - info!("Shutting down server..."); - - // Stop the server - if let Err(e) = server_handle.stop() { - error!("Error stopping server: {}", e); - } - - info!("Server shutdown complete"); - - Ok(()) +async fn main() -> Result<(), Box> { + cli::run().await } diff --git a/src/relayer.rs b/src/relayer.rs new file mode 100644 index 0000000..af42e54 --- /dev/null +++ b/src/relayer.rs @@ -0,0 +1,186 @@ +use std::sync::Arc; +use std::time::Duration; + +use anyhow::{Context, Result}; +use celestia_rpc::blobstream::BlobstreamClient; +use celestia_rpc::{Client, HeaderClient}; +use celestia_types::hash::Hash as CelestiaHash; +use ethers::prelude::*; +use tokio::sync::watch; +use tokio::time::sleep; +use tracing::{info, warn}; + +abigen!( + MockBlobstream, + r#"[ + function latestBlock() view returns (uint64) + function submitDataCommitment(bytes32,uint64,uint64) + ]"# +); + +pub struct RelayerConfig { + pub celestia_ws_url: String, + pub eth_rpc_url: String, + pub eth_private_key: String, + pub contract_address: String, + pub poll_ms: u64, +} + +pub async fn run_from_env() -> Result<()> { + let celestia_ws_url = + std::env::var("CELESTIA_RPC_URL").unwrap_or_else(|_| "ws://127.0.0.1:26658".to_string()); + let eth_rpc_url = std::env::var("ETH_RPC_URL").context("ETH_RPC_URL is required")?; + let eth_private_key = + std::env::var("ETH_PRIVATE_KEY").context("ETH_PRIVATE_KEY is required")?; + let contract_address = std::env::var("BLOBSTREAM_CONTRACT_ADDRESS") + .context("BLOBSTREAM_CONTRACT_ADDRESS is required")?; + let poll_ms: u64 = std::env::var("RELAYER_POLL_MS") + .ok() + .and_then(|value| value.parse().ok()) + .unwrap_or(1000); + + run(RelayerConfig { + celestia_ws_url, + eth_rpc_url, + eth_private_key, + contract_address, + poll_ms, + }) + .await +} + +pub async fn run(config: RelayerConfig) -> Result<()> { + let (shutdown_tx, shutdown_rx) = watch::channel(false); + let run = run_with_shutdown(config, shutdown_rx); + tokio::pin!(run); + tokio::select! { + result = &mut run => result, + _ = tokio::signal::ctrl_c() => { + let _ = shutdown_tx.send(true); + Ok(()) + } + } +} + +pub async fn run_with_shutdown( + config: RelayerConfig, + mut shutdown: watch::Receiver, +) -> Result<()> { + let _ = tracing_subscriber::fmt::try_init(); + + let local_client = Client::new(&config.celestia_ws_url, None, None, None) + .await + .context("failed to connect to Localestia RPC")?; + + let provider = Provider::::try_from(config.eth_rpc_url) + .context("failed to create ETH RPC provider")?; + let chain_id = provider + .get_chainid() + .await + .context("failed to fetch chain id")?; + let wallet: LocalWallet = config + .eth_private_key + .parse::() + .context("failed to parse ETH_PRIVATE_KEY")? + .with_chain_id(chain_id.as_u64()); + let address: Address = config + .contract_address + .parse() + .context("failed to parse BLOBSTREAM_CONTRACT_ADDRESS")?; + + let provider = Arc::new(provider); + let signer = Arc::new(SignerMiddleware::new(provider, wallet)); + let contract = MockBlobstream::new(address, signer); + + info!("starting blobstream relayer"); + + loop { + if *shutdown.borrow() { + break; + } + + if let Err(err) = relay_once(&local_client, &contract).await { + warn!("relayer error: {err}"); + } + + tokio::select! { + _ = shutdown.changed() => {}, + _ = sleep(Duration::from_millis(config.poll_ms)) => {}, + } + } + + Ok(()) +} + +async fn relay_once( + local_client: &Client, + contract: &MockBlobstream, +) -> Result<()> { + let local_head = match local_client.header_local_head().await { + Ok(header) => header.height(), + Err(err) => { + let msg = err.to_string(); + if msg.contains("Header not found") { + return Ok(()); + } + warn!("failed to read local head: {err}"); + return Ok(()); + } + }; + + let contract_latest = match contract.latest_block().call().await { + Ok(value) => value, + Err(err) => { + warn!("failed to read contract latestBlock: {err}"); + return Ok(()); + } + }; + + let mut next_height = if contract_latest == 0 { + 1 + } else { + contract_latest + }; + + if next_height > local_head { + return Ok(()); + } + + while next_height <= local_head { + let end = next_height + 1; + let tuple_root = local_client + .blobstream_get_data_root_tuple_root(next_height, end) + .await + .with_context(|| format!("failed to fetch data root tuple root for {next_height}"))?; + let tuple_root = hash_to_bytes32(tuple_root)?; + + let call = contract.submit_data_commitment(tuple_root, next_height, end); + let pending = call + .send() + .await + .with_context(|| format!("failed to submit data commitment for {next_height}"))?; + + let receipt = pending + .await + .context("failed to fetch transaction receipt")?; + if let Some(receipt) = receipt { + info!( + "relayed height {next_height} in tx {:#x}", + receipt.transaction_hash + ); + } else { + warn!("relayed height {next_height} but receipt missing"); + } + + next_height = end; + } + + Ok(()) +} + +fn hash_to_bytes32(hash: CelestiaHash) -> Result<[u8; 32]> { + match hash { + CelestiaHash::Sha256(bytes) => Ok(bytes), + CelestiaHash::None => Err(anyhow::anyhow!("empty tuple root hash")), + } +} diff --git a/src/rpc/blob.rs b/src/rpc/blob.rs index 4104928..c4af0ff 100644 --- a/src/rpc/blob.rs +++ b/src/rpc/blob.rs @@ -67,56 +67,3 @@ impl BlobRpcServer for LocalestiaServer { .map_err(|e| rpc_error(format!("Failed to get blob proof: {}", e))) } } - -// // Header methods implementation -// async fn header_get_by_hash(&self, hash: Hash) -> RpcResult { -// self.storage -// .get_header_by_hash(&hash) -// .await -// .map_err(|e| jsonrpsee::core::Error::Custom(format!("Failed to get header: {}", e))) -// } - -// async fn header_get_by_height(&self, height: u64) -> RpcResult { -// self.storage -// .get_header_by_height(height) -// .await -// .map_err(|e| jsonrpsee::core::Error::Custom(format!("Failed to get header: {}", e))) -// } - -// async fn header_get_range_by_height( -// &self, -// from: u64, -// to: u64, -// ) -> RpcResult> { -// self.storage -// .get_headers_by_range(from, to) -// .await -// .map_err(|e| jsonrpsee::core::Error::Custom(format!("Failed to get headers: {}", e))) -// } - -// async fn header_wait_for_height(&self, height: u64) -> RpcResult { -// self.storage.wait_for_header(height).await.map_err(|e| { -// jsonrpsee::core::Error::Custom(format!("Failed to wait for header: {}", e)) -// }) -// } - -// async fn share_get_eds(&self, height: u64) -> RpcResult { -// self.storage -// .get_eds_at_height(height) -// .await -// .map_err(|e| jsonrpsee::core::Error::Custom(format!("Failed to get EDS: {}", e))) -// } - -// async fn share_get_range( -// &self, -// height: u64, -// start: u64, -// end: u64, -// ) -> RpcResult { -// self.storage -// .get_share_range(height, start, end) -// .await -// .map_err(|e| { -// jsonrpsee::core::Error::Custom(format!("Failed to get share range: {}", e)) -// }) -// } diff --git a/src/rpc/blobstream.rs b/src/rpc/blobstream.rs new file mode 100644 index 0000000..f4bf05e --- /dev/null +++ b/src/rpc/blobstream.rs @@ -0,0 +1,120 @@ +use celestia_rpc::BlobstreamRpcServer; +use celestia_types::hash::Hash; +use celestia_types::MerkleProof; +use jsonrpsee::core::{async_trait as jsonrpsee_async_trait, RpcResult}; + +use crate::rpc::{rpc_error, LocalestiaServer}; + +const DATA_ROOT_TUPLE_ROOT_BLOCKS_LIMIT: u64 = 10_000; + +fn encode_data_root_tuple(height: u64, data_root: Hash) -> Result, String> { + let data_root = match data_root { + Hash::Sha256(bytes) => bytes, + Hash::None => return Err("data root hash is empty".to_string()), + }; + + let mut encoded = vec![0u8; 32]; + encoded[24..].copy_from_slice(&height.to_be_bytes()); + encoded.extend_from_slice(&data_root); + Ok(encoded) +} + +async fn validate_data_root_tuple_root_range( + server: &LocalestiaServer, + start: u64, + end: u64, +) -> Result<(), String> { + if start == 0 { + return Err("start height must be greater than zero".to_string()); + } + if start >= end { + return Err("end block is smaller or equal to the start block".to_string()); + } + + let heights_range = end - start; + if heights_range > DATA_ROOT_TUPLE_ROOT_BLOCKS_LIMIT { + return Err(format!( + "the query exceeds the limit of allowed blocks {}", + DATA_ROOT_TUPLE_ROOT_BLOCKS_LIMIT + )); + } + + let current_height = server.storage.get_current_height().await.map_err(|e| { + format!("could not get local head to validate the data root tuple range: {e}") + })?; + + if end > current_height.saturating_add(1) { + return Err(format!( + "end block {end} is higher than local chain height {current_height}. Wait for the node until it syncs up to {end}", + )); + } + + Ok(()) +} + +async fn fetch_encoded_data_root_tuples( + server: &LocalestiaServer, + start: u64, + end: u64, +) -> Result>, String> { + let mut encoded = Vec::with_capacity((end - start) as usize); + for height in start..end { + let header = server + .storage + .get_header_by_height(height) + .await + .map_err(|e| format!("failed to get header at height {height}: {e}"))?; + let tuple = encode_data_root_tuple(height, header.dah.hash())?; + encoded.push(tuple); + } + + Ok(encoded) +} + +#[jsonrpsee_async_trait] +impl BlobstreamRpcServer for LocalestiaServer { + async fn blobstream_get_data_root_tuple_root(&self, start: u64, end: u64) -> RpcResult { + validate_data_root_tuple_root_range(self, start, end) + .await + .map_err(rpc_error)?; + + let encoded = fetch_encoded_data_root_tuples(self, start, end) + .await + .map_err(rpc_error)?; + + let (_, root) = MerkleProof::new(0, &encoded) + .map_err(|e| rpc_error(format!("failed to compute data root tuple root: {e}")))?; + + Ok(hex::encode_upper(root)) + } + + async fn blobstream_get_data_root_tuple_inclusion_proof( + &self, + height: u64, + start: u64, + end: u64, + ) -> RpcResult { + validate_data_root_tuple_root_range(self, start, end) + .await + .map_err(rpc_error)?; + + if height < start || height >= end { + return Err(rpc_error(format!( + "height {height} should be in the end exclusive interval first_block {start} last_block {end}", + ))); + } + + let encoded = fetch_encoded_data_root_tuples(self, start, end) + .await + .map_err(rpc_error)?; + + // test and double check this logic + let index = usize::try_from(height - start) + .map_err(|_| rpc_error("height offset is out of range"))?; + + let (proof, _) = MerkleProof::new(index, &encoded) + .map_err(|e| rpc_error(format!("failed to compute data root tuple proof: {e}")))?; + + Ok(proof) + } +} diff --git a/src/rpc/mod.rs b/src/rpc/mod.rs index 294ba93..65414e0 100644 --- a/src/rpc/mod.rs +++ b/src/rpc/mod.rs @@ -1,4 +1,5 @@ mod blob; +mod blobstream; mod header; pub mod server; mod share; diff --git a/src/storage/redis_storage.rs b/src/storage/redis_storage.rs index 5ccdf16..216ba66 100644 --- a/src/storage/redis_storage.rs +++ b/src/storage/redis_storage.rs @@ -77,9 +77,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let height_namespaces_key = format!("height_namespaces:{}", height); @@ -205,9 +205,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; // Store the blob with timeout match tokio::time::timeout( @@ -326,9 +326,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let ns_hex = hex::encode(namespace.as_bytes()); let commitment_hex = hex::encode(commitment.hash()); @@ -539,9 +539,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let mut all_blobs = Vec::new(); @@ -575,7 +575,7 @@ impl RedisStorage { // Get blob data with timeout let blob_data: Option = match tokio::time::timeout( std::time::Duration::from_secs(5), - conn.get(&blob_key), + conn.get(blob_key), ) .await { @@ -631,9 +631,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let hash = header.hash(); let hash_hex = hex::encode(hash.as_bytes()); @@ -717,9 +717,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let header_key = format!("header:{}", height); let header_data: Option = conn @@ -758,9 +758,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let hash_hex = hex::encode(hash.as_bytes()); let hash_key = format!("header_hash:{}", hash_hex); @@ -840,7 +840,7 @@ impl RedisStorage { } pub async fn get_latest_head(&self) -> Result { - let height = self.get_current_height().await.map_err(|e| e)?; + let height = self.get_current_height().await?; match self.get_header_by_height(height).await { Ok(header) => Ok(header), @@ -868,9 +868,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; let eds_key = format!("eds:{}", height); let eds_data: Option = conn.get(&eds_key).await.map_err(LocalError::RedisError)?; @@ -1221,9 +1221,9 @@ impl RedisStorage { .get_multiplexed_async_connection() .await .map_err(|e| { - error!("Failed to get Redis connection: {}", e); - LocalError::RedisError(e) - })?; + error!("Failed to get Redis connection: {}", e); + LocalError::RedisError(e) + })?; // Explicitly specify the return type as String for the FLUSHDB command let _: String = redis::cmd("FLUSHDB") diff --git a/tests/cli_curl.rs b/tests/cli_curl.rs index 5113f46..43a917d 100644 --- a/tests/cli_curl.rs +++ b/tests/cli_curl.rs @@ -26,14 +26,11 @@ fn curl_rpc(url: &str, payload: &Value) -> Value { }); if !output.status.success() { - panic!( - "curl failed: {}", - String::from_utf8_lossy(&output.stderr) - ); + panic!("curl failed: {}", String::from_utf8_lossy(&output.stderr)); } - let value: Value = serde_json::from_slice(&output.stdout) - .expect("curl returned invalid JSON-RPC response"); + let value: Value = + serde_json::from_slice(&output.stdout).expect("curl returned invalid JSON-RPC response"); if let Some(error) = value.get("error") { panic!("JSON-RPC error response: {error}"); diff --git a/tests/compat_blobstream.rs b/tests/compat_blobstream.rs new file mode 100644 index 0000000..5b54546 --- /dev/null +++ b/tests/compat_blobstream.rs @@ -0,0 +1,265 @@ +mod utils; + +use celestia_rpc::prelude::*; +use celestia_types::hash::Hash; +use ethers::abi::Abi; +use ethers::contract::{Contract, ContractFactory}; +use ethers::core::utils::{Anvil, AnvilInstance}; +use ethers::middleware::SignerMiddleware; +use ethers::providers::{Middleware, Provider}; +use ethers::signers::{LocalWallet, Signer}; +use ethers::types::{Bytes, H256, U256}; +use serde::Deserialize; +use std::env; +use std::fs; +use std::path::PathBuf; +use std::process::Command; +use std::sync::{Arc, Once}; + +use utils::{make_blob, make_namespace, setup, submit_blob}; + +#[tokio::test] +async fn blobstream_get_root_and_proof_roundtrip() { + let ctx = setup().await; + let namespace = make_namespace(50); + + let height_a = submit_blob(&ctx.client, &make_blob(namespace, b"tuple a".to_vec())).await; + let height_b = submit_blob(&ctx.client, &make_blob(namespace, b"tuple b".to_vec())).await; + let height_c = submit_blob(&ctx.client, &make_blob(namespace, b"tuple c".to_vec())).await; + + let start = height_a; + let end = height_c + 1; + let target_height = height_b; + + let tuple_root = ctx + .client + .blobstream_get_data_root_tuple_root(start, end) + .await + .expect("blobstream_get_data_root_tuple_root failed"); + + let proof = ctx + .client + .blobstream_get_data_root_tuple_inclusion_proof(target_height, start, end) + .await + .expect("blobstream_get_data_root_tuple_inclusion_proof failed"); + + let data_root = ctx + .client + .header_get_by_height(target_height) + .await + .expect("header_get_by_height failed") + .dah + .hash(); + + let leaf = encode_data_root_tuple(target_height, &data_root); + let root = hash_to_bytes(tuple_root); + + proof + .verify(leaf, root) + .expect("failed to verify data root tuple proof"); +} + +#[tokio::test] +async fn blobstream_proof_verifies_on_mock_contract() { + let ctx = setup().await; + let namespace = make_namespace(51); + + let height = submit_blob( + &ctx.client, + &make_blob(namespace, b"contract proof".to_vec()), + ) + .await; + + let (anvil, contract) = deploy_mockstream_contract().await; + + let tuple_root = ctx + .client + .blobstream_get_data_root_tuple_root(height, height + 1) + .await + .expect("blobstream_get_data_root_tuple_root failed"); + let tuple_root = hash_to_h256(tuple_root); + + let submit_call = contract + .method::<_, ()>("submitDataCommitment", (tuple_root, height, height + 1)) + .expect("failed to build submitDataCommitment call"); + let pending_tx = submit_call + .send() + .await + .expect("submitDataCommitment transaction failed"); + let _ = pending_tx + .await + .expect("failed to confirm submitDataCommitment"); + + let proof = ctx + .client + .blobstream_get_data_root_tuple_inclusion_proof(height, height, height + 1) + .await + .expect("blobstream_get_data_root_tuple_inclusion_proof failed"); + let data_root = ctx + .client + .header_get_by_height(height) + .await + .expect("header_get_by_height failed") + .dah + .hash(); + + let tuple = (U256::from(height), hash_to_h256(data_root)); + let side_nodes: Vec = proof + .aunts + .iter() + .map(|hash| H256::from_slice(hash)) + .collect(); + let proof_tuple = ( + side_nodes, + U256::from(proof.index as u64), + U256::from(proof.total as u64), + ); + + let verified: bool = contract + .method("verifyAttestation", (U256::from(1u64), tuple, proof_tuple)) + .expect("failed to build verifyAttestation call") + .call() + .await + .expect("verifyAttestation call failed"); + assert!(verified, "verifyAttestation returned false"); + + drop(anvil); +} + +#[derive(Deserialize)] +struct ForgeArtifact { + abi: serde_json::Value, + bytecode: ForgeBytecode, +} + +#[derive(Deserialize)] +struct ForgeBytecode { + object: String, +} + +static FORGE_BUILD: Once = Once::new(); + +fn ensure_forge_build() { + FORGE_BUILD.call_once(|| { + let status = Command::new("forge") + .arg("build") + .current_dir(contracts_dir()) + .status() + .unwrap_or_else(|err| { + panic!("failed to execute forge build: {err}"); + }); + + if !status.success() { + panic!("forge build failed with status {status}"); + } + }); +} + +async fn deploy_mockstream_contract() -> ( + Option, + Contract, LocalWallet>>, +) { + ensure_forge_build(); + + let (anvil, client) = anvil_client().await; + + let (abi, bytecode) = load_mockstream_artifact(); + let factory = ContractFactory::new(abi, bytecode, client.clone()); + let contract = factory + .deploy(()) + .expect("failed to build mock contract deployment") + .send() + .await + .expect("failed to deploy mock contract"); + + let init_call = contract + .method::<_, ()>("initialize", 0u64) + .expect("failed to build initialize call"); + let pending_tx = init_call + .send() + .await + .expect("initialize transaction failed"); + let _ = pending_tx.await.expect("failed to confirm initialize tx"); + + (anvil, contract) +} + +async fn anvil_client() -> ( + Option, + Arc, LocalWallet>>, +) { + if let Ok(rpc_url) = env::var("ANVIL_RPC_URL") { + let private_key = env::var("ANVIL_PRIVATE_KEY") + .expect("ANVIL_PRIVATE_KEY is required when ANVIL_RPC_URL is set"); + let provider = + Provider::try_from(rpc_url).expect("failed to create provider from ANVIL_RPC_URL"); + let chain_id = provider + .get_chainid() + .await + .expect("failed to fetch chain id from ANVIL_RPC_URL"); + let wallet: LocalWallet = private_key + .parse::() + .expect("failed to parse ANVIL_PRIVATE_KEY") + .with_chain_id(chain_id.as_u64()); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + return (None, client); + } + + let anvil = Anvil::new().spawn(); + let wallet: LocalWallet = anvil.keys()[0].clone().into(); + let wallet = wallet.with_chain_id(anvil.chain_id()); + let provider = Provider::try_from(anvil.endpoint()).expect("failed to create Anvil provider"); + let client = Arc::new(SignerMiddleware::new(provider, wallet)); + + (Some(anvil), client) +} + +fn load_mockstream_artifact() -> (Abi, Bytes) { + let artifact_path = contracts_dir() + .join("out") + .join("MockBlobstream.sol") + .join("Mockstream.json"); + let bytes = fs::read(&artifact_path) + .unwrap_or_else(|err| panic!("failed to read {artifact_path:?}: {err}")); + let artifact: ForgeArtifact = serde_json::from_slice(&bytes) + .unwrap_or_else(|err| panic!("failed to parse forge artifact: {err}")); + + let abi: Abi = serde_json::from_value(artifact.abi) + .unwrap_or_else(|err| panic!("failed to parse contract ABI: {err}")); + let bytecode = artifact.bytecode.object; + if bytecode.is_empty() { + panic!("forge artifact bytecode is empty"); + } + let bytecode = bytecode.strip_prefix("0x").unwrap_or(&bytecode); + let bytecode = + hex::decode(bytecode).unwrap_or_else(|err| panic!("failed to decode bytecode hex: {err}")); + + (abi, Bytes::from(bytecode)) +} + +fn contracts_dir() -> PathBuf { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("contracts") +} + +fn encode_data_root_tuple(height: u64, data_root: &Hash) -> Vec { + let mut result = vec![0u8; 32]; + result[24..].copy_from_slice(&height.to_be_bytes()); + + match data_root { + Hash::Sha256(bytes) => result.extend_from_slice(bytes), + Hash::None => panic!("empty data root hash"), + } + + result +} + +fn hash_to_bytes(hash: Hash) -> [u8; 32] { + match hash { + Hash::Sha256(bytes) => bytes, + Hash::None => panic!("empty tuple root hash"), + } +} + +fn hash_to_h256(hash: Hash) -> H256 { + H256::from(hash_to_bytes(hash)) +} diff --git a/tests/compat_report.rs b/tests/compat_report.rs index a41561d..6021291 100644 --- a/tests/compat_report.rs +++ b/tests/compat_report.rs @@ -4,7 +4,7 @@ use celestia_rpc::prelude::*; use celestia_types::AppVersion; use jsonrpsee_core::client::Error as RpcError; -use utils::{RpcOutcome, classify_error, make_blob, make_namespace, setup, submit_blob}; +use utils::{classify_error, make_blob, make_namespace, setup, submit_blob, RpcOutcome}; fn record(name: &str, result: Result) -> (String, RpcOutcome) { let outcome = match result { @@ -48,7 +48,10 @@ async fn compatibility_report() { "header.WaitForHeight", ctx.client.header_wait_for_height(height).await, )); - results.push(record("header.LocalHead", ctx.client.header_local_head().await)); + results.push(record( + "header.LocalHead", + ctx.client.header_local_head().await, + )); results.push(record( "header.NetworkHead", ctx.client.header_network_head().await, @@ -57,7 +60,10 @@ async fn compatibility_report() { "header.SyncState", ctx.client.header_sync_state().await, )); - results.push(record("header.SyncWait", ctx.client.header_sync_wait().await)); + results.push(record( + "header.SyncWait", + ctx.client.header_sync_wait().await, + )); results.push(record( "blob.Get", @@ -68,7 +74,10 @@ async fn compatibility_report() { results.push(record( "blob.Submit", ctx.client - .blob_submit(std::slice::from_ref(&blob), celestia_rpc::TxConfig::default()) + .blob_submit( + std::slice::from_ref(&blob), + celestia_rpc::TxConfig::default(), + ) .await, )); results.push(record( @@ -97,10 +106,7 @@ async fn compatibility_report() { } } Err(err) => { - results.push(( - "blob.GetProof".to_string(), - classify_error(err), - )); + results.push(("blob.GetProof".to_string(), classify_error(err))); results.push(( "blob.Included".to_string(), RpcOutcome::Skipped("proof unavailable"), @@ -108,13 +114,28 @@ async fn compatibility_report() { } } + results.push(record( + "blobstream.GetDataRootTupleRoot", + ctx.client + .blobstream_get_data_root_tuple_root(height, height + 1) + .await, + )); + results.push(record( + "blobstream.GetDataRootTupleInclusionProof", + ctx.client + .blobstream_get_data_root_tuple_inclusion_proof(height, height, height + 1) + .await, + )); + results.push(record( "share.GetEDS", ctx.client.share_get_eds(height, AppVersion::V3).await, )); results.push(record( "share.GetRange", - ctx.client.share_get_range(height, AppVersion::V3, 0, 1).await, + ctx.client + .share_get_range(height, AppVersion::V3, 0, 1) + .await, )); results.push(record( "share.GetSamples", diff --git a/tests/process_server.rs b/tests/process_server.rs index 70a393f..ac002a9 100644 --- a/tests/process_server.rs +++ b/tests/process_server.rs @@ -52,7 +52,10 @@ async fn process_server_supports_all_methods() { .await .expect("blob_get failed"); ctx.client - .blob_submit(std::slice::from_ref(&blob), celestia_rpc::TxConfig::default()) + .blob_submit( + std::slice::from_ref(&blob), + celestia_rpc::TxConfig::default(), + ) .await .expect("blob_submit failed"); ctx.client @@ -74,6 +77,15 @@ async fn process_server_supports_all_methods() { .expect("blob_included failed"); assert!(included, "blob_included reported false for stored blob"); + ctx.client + .blobstream_get_data_root_tuple_root(height, height + 1) + .await + .expect("blobstream_get_data_root_tuple_root failed"); + ctx.client + .blobstream_get_data_root_tuple_inclusion_proof(height, height, height + 1) + .await + .expect("blobstream_get_data_root_tuple_inclusion_proof failed"); + ctx.client .share_get_eds(height, AppVersion::V3) .await diff --git a/tests/utils/mod.rs b/tests/utils/mod.rs index 2a8269a..844184e 100644 --- a/tests/utils/mod.rs +++ b/tests/utils/mod.rs @@ -1,14 +1,14 @@ #![allow(dead_code)] use celestia_rpc::prelude::*; -use celestia_rpc::{BlobRpcServer, HeaderRpcServer}; +use celestia_rpc::{BlobRpcServer, BlobstreamRpcServer, HeaderRpcServer}; use celestia_rpc::{Client, TxConfig}; use celestia_types::nmt::Namespace; use celestia_types::{AppVersion, Blob}; use jsonrpsee::server::{ServerBuilder, ServerHandle}; use jsonrpsee_core::client::Error as RpcError; use jsonrpsee_types::error::{INVALID_PARAMS_CODE, METHOD_NOT_FOUND_CODE}; -use localestia::rpc::{LocalestiaServer, ShareRpcServer as LocalShareRpcServer}; +use localestia::rpc::{LocalestiaServer, ShareRpcServer}; use localestia::storage::RedisStorage; use once_cell::sync::Lazy; use std::env; @@ -82,8 +82,11 @@ pub async fn setup() -> TestContext { .merge(HeaderRpcServer::into_rpc(rpc_server.clone())) .expect("failed to merge header RPC module"); module - .merge(LocalShareRpcServer::into_rpc(rpc_server)) + .merge(ShareRpcServer::into_rpc(rpc_server.clone())) .expect("failed to merge share RPC module"); + module + .merge(BlobstreamRpcServer::into_rpc(rpc_server)) + .expect("failed to merge blobstream RPC module"); let server_handle = server.start(module); let ws_url = format!("ws://{}", local_addr); @@ -258,7 +261,11 @@ fn target_profile_dir() -> PathBuf { .map(PathBuf::from) .unwrap_or_else(|_| PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target")); let profile = env::var("PROFILE").unwrap_or_else(|_| "debug".to_string()); - let profile = if profile == "release" { "release" } else { "debug" }; + let profile = if profile == "release" { + "release" + } else { + "debug" + }; target_dir.join(profile) }