diff --git a/Cargo.lock b/Cargo.lock index 3df58a9634e2..5b408fe71c5a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -103,9 +103,9 @@ checksum = "5c6cb57a04249c6480766f7f7cef5467412af1490f8d1e243141daddada3264f" [[package]] name = "alloy-json-abi" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ded610181f3dad5810f6ff12d1a99994cf9b42d2fcb7709029352398a5da5ae6" +checksum = "b84c506bf264110fa7e90d9924f742f40ef53c6572ea56a0b0bd714a567ed389" dependencies = [ "alloy-primitives", "alloy-sol-type-parser", @@ -115,9 +115,9 @@ dependencies = [ [[package]] name = "alloy-primitives" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd58d377699e6cfeab52c4a9d28bdc4ef37e2bd235ff2db525071fe37a2e9af5" +checksum = "9fce5dbd6a4f118eecc4719eaa9c7ffc31c315e6c5ccde3642db927802312425" dependencies = [ "alloy-rlp", "bytes", @@ -154,9 +154,9 @@ dependencies = [ [[package]] name = "alloy-sol-type-parser" -version = "0.8.11" +version = "0.8.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12c71028bfbfec210e24106a542aad3def7caf1a70e2c05710e92a98481980d3" +checksum = "aa64d80ae58ffaafdff9d5d84f58d03775f66c84433916dc9a64ed16af5755da" dependencies = [ "serde", "winnow 0.6.20", @@ -249,9 +249,9 @@ checksum = "c042108f3ed77fd83760a5fd79b53be043192bb3b9dba91d8c574c0ada7850c8" [[package]] name = "arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d5a26814d8dcb93b0e5a0ff3c6d80a8843bafb21b39e8e18a6f05471870e110" +checksum = "dde20b3d026af13f561bdd0f15edf01fc734f0dafcedbaf42bba506a9517f223" dependencies = [ "derive_arbitrary", ] @@ -262,6 +262,39 @@ version = "1.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f7f8c3906b62b754cd5326047894316021dcfe5a194c8ea52bdd94934a3457" +[[package]] +name = "ark-bn254" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d69eab57e8d2663efa5c63135b2af4f396d66424f88954c21104125ab6b3e6bc" +dependencies = [ + "ark-ec", + "ark-ff 0.5.0", + "ark-std 0.5.0", +] + +[[package]] +name = "ark-ec" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "43d68f2d516162846c1238e755a7c4d131b892b70cc70c471a8e3ca3ed818fce" +dependencies = [ + "ahash 0.8.11", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.0", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-integer", + "num-traits", + "rayon", + "zeroize", +] + [[package]] name = "ark-ff" version = "0.3.0" @@ -300,6 +333,27 @@ dependencies = [ "zeroize", ] +[[package]] +name = "ark-ff" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a177aba0ed1e0fbb62aa9f6d0502e9b46dad8c2eab04c14258a1212d2557ea70" +dependencies = [ + "ark-ff-asm 0.5.0", + "ark-ff-macros 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "educe", + "itertools 0.13.0", + "num-bigint 0.4.6", + "num-traits", + "paste", + "rayon", + "zeroize", +] + [[package]] name = "ark-ff-asm" version = "0.3.0" @@ -320,6 +374,16 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-asm" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "62945a2f7e6de02a31fe400aa489f0e0f5b2502e69f95f853adb82a96c7a6b60" +dependencies = [ + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "ark-ff-macros" version = "0.3.0" @@ -345,6 +409,35 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "ark-ff-macros" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "09be120733ee33f7693ceaa202ca41accd5653b779563608f1234f78ae07c4b3" +dependencies = [ + "num-bigint 0.4.6", + "num-traits", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + +[[package]] +name = "ark-poly" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "579305839da207f02b89cd1679e50e67b4331e2f9294a57693e5051b7703fe27" +dependencies = [ + "ahash 0.8.11", + "ark-ff 0.5.0", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "educe", + "fnv", + "hashbrown 0.15.0", + "rayon", +] + [[package]] name = "ark-serialize" version = "0.3.0" @@ -366,6 +459,31 @@ dependencies = [ "num-bigint 0.4.6", ] +[[package]] +name = "ark-serialize" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3f4d068aaf107ebcd7dfb52bc748f8030e0fc930ac8e360146ca54c1203088f7" +dependencies = [ + "ark-serialize-derive", + "ark-std 0.5.0", + "arrayvec 0.7.6", + "digest 0.10.7", + "num-bigint 0.4.6", + "rayon", +] + +[[package]] +name = "ark-serialize-derive" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "213888f660fddcca0d257e88e54ac05bca01885f258ccdf695bafd77031bb69d" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "ark-std" version = "0.3.0" @@ -386,6 +504,17 @@ dependencies = [ "rand 0.8.5", ] +[[package]] +name = "ark-std" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "246a225cc6131e9ee4f24619af0f19d67761fff15d7ccc22e42b80846e69449a" +dependencies = [ + "num-traits", + "rand 0.8.5", + "rayon", +] + [[package]] name = "arr_macro" version = "0.1.3" @@ -1966,9 +2095,9 @@ dependencies = [ [[package]] name = "const-hex" -version = "1.13.1" +version = "1.13.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0121754e84117e65f9d90648ee6aa4882a6e63110307ab73967a4c5e7e69e586" +checksum = "487981fa1af147182687064d0a2c336586d337a606595ced9ffb0c685c250c73" dependencies = [ "cfg-if", "cpufeatures", @@ -2040,6 +2169,16 @@ dependencies = [ "libc", ] +[[package]] +name = "core-foundation" +version = "0.10.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b55271e5c8c478ad3f38ad24ef34923091e0548492a266d19b3c0b4d82574c63" +dependencies = [ + "core-foundation-sys", + "libc", +] + [[package]] name = "core-foundation-sys" version = "0.8.7" @@ -2512,9 +2651,9 @@ dependencies = [ [[package]] name = "derive_arbitrary" -version = "1.3.2" +version = "1.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "67e77553c4162a157adbf834ebae5b415acbecbeafc7a74b0e886657506a7611" +checksum = "30542c1ad912e0e3d22a1935c290e12e8a29d704a420177a31faad4a601a0800" dependencies = [ "proc-macro2 1.0.89", "quote 1.0.37", @@ -2582,6 +2721,15 @@ dependencies = [ "subtle", ] +[[package]] +name = "directories" +version = "5.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a49173b84e034382284f27f1af4dcbbd231ffa358c0fe316541a7337f376a35" +dependencies = [ + "dirs-sys", +] + [[package]] name = "dirs" version = "5.0.1" @@ -2744,6 +2892,18 @@ dependencies = [ "zeroize", ] +[[package]] +name = "educe" +version = "0.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d7bc049e1bd8cdeb31b68bbd586a9464ecf9f3944af3958a7a9d0f8b9799417" +dependencies = [ + "enum-ordinalize", + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "either" version = "1.13.0" @@ -2826,6 +2986,26 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "enum-ordinalize" +version = "4.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fea0dcfa4e54eeb516fe454635a95753ddd39acda650ce703031c6973e315dd5" +dependencies = [ + "enum-ordinalize-derive", +] + +[[package]] +name = "enum-ordinalize-derive" +version = "4.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d28318a75d4aead5c4db25382e8ef717932d0346600cacae6357eb5941bc5ff" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "enum_dispatch" version = "0.3.13" @@ -4157,7 +4337,7 @@ dependencies = [ "hyper-util", "log", "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls-native-certs 0.8.1", "rustls-pki-types", "tokio", "tokio-rustls 0.26.0", @@ -5451,7 +5631,7 @@ dependencies = [ "openssl-probe", "openssl-sys", "schannel", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "tempfile", ] @@ -5769,7 +5949,7 @@ dependencies = [ "snafu", "tokio", "tower 0.5.1", - "tower-http 0.6.1", + "tower-http 0.6.2", "tracing", "url", ] @@ -6824,10 +7004,11 @@ dependencies = [ [[package]] name = "quinn-udp" -version = "0.5.5" +version = "0.5.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fe68c2e9e1a1234e218683dbdf9f9dfcb094113c5ac2b938dfcb9bab4c4140b" +checksum = "7d5a626c6807713b15cac82a6acaccd6043c9a5408c24baae07611fec3f243da" dependencies = [ + "cfg_aliases", "libc", "once_cell", "socket2", @@ -7119,7 +7300,7 @@ dependencies = [ "pin-project-lite", "quinn", "rustls 0.23.16", - "rustls-native-certs 0.8.0", + "rustls-native-certs 0.8.1", "rustls-pemfile 2.2.0", "rustls-pki-types", "serde", @@ -7332,6 +7513,32 @@ version = "1.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "48fd7bd8a6377e15ad9d42a8ec25371b94ddc67abe7c8b9127bec79bebaaae18" +[[package]] +name = "rust-kzg-bn254" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "acdae4058a9f604acf7023d99d931d6f30261fff93787bcfd1f1ccfc725b701c" +dependencies = [ + "ark-bn254", + "ark-ec", + "ark-ff 0.5.0", + "ark-poly", + "ark-serialize 0.5.0", + "ark-std 0.5.0", + "byteorder", + "crossbeam-channel", + "directories", + "hex-literal", + "num-bigint 0.4.6", + "num-traits", + "num_cpus", + "rand 0.8.5", + "rayon", + "sha2 0.10.8", + "sys-info", + "ureq", +] + [[package]] name = "rust_decimal" version = "1.36.0" @@ -7457,7 +7664,7 @@ dependencies = [ "openssl-probe", "rustls-pemfile 1.0.4", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] @@ -7470,20 +7677,19 @@ dependencies = [ "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 2.11.1", ] [[package]] name = "rustls-native-certs" -version = "0.8.0" +version = "0.8.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fcaf18a4f2be7326cd874a5fa579fae794320a0f388d365dca7e480e55f83f8a" +checksum = "7fcff2dd52b58a8d98a70243663a0d234c4e2b79235637849d15913394a247d3" dependencies = [ "openssl-probe", - "rustls-pemfile 2.2.0", "rustls-pki-types", "schannel", - "security-framework", + "security-framework 3.0.1", ] [[package]] @@ -7516,7 +7722,7 @@ version = "0.3.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "afbb878bdfdf63a336a5e63561b1835e7a8c91524f51621db870169eac84b490" dependencies = [ - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "jni", "log", @@ -7525,7 +7731,7 @@ dependencies = [ "rustls-native-certs 0.7.3", "rustls-platform-verifier-android", "rustls-webpki 0.102.8", - "security-framework", + "security-framework 2.11.1", "security-framework-sys", "webpki-roots", "winapi", @@ -7729,6 +7935,15 @@ dependencies = [ "yap", ] +[[package]] +name = "scc" +version = "2.2.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d8d25269dd3a12467afe2e510f69fb0b46b698e5afb296b59f2145259deaf8e8" +dependencies = [ + "sdd", +] + [[package]] name = "schannel" version = "0.1.26" @@ -7773,6 +7988,12 @@ dependencies = [ "untrusted", ] +[[package]] +name = "sdd" +version = "3.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "49c1eeaf4b6a87c7479688c6d52b9f1153cedd3c489300564f932b065c6eab95" + [[package]] name = "seahash" version = "4.1.0" @@ -7851,13 +8072,26 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "897b2245f0b511c87893af39b033e5ca9cce68824c4d7e7630b5a1d339658d02" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "core-foundation-sys", "libc", "num-bigint 0.4.6", "security-framework-sys", ] +[[package]] +name = "security-framework" +version = "3.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e1415a607e92bec364ea2cf9264646dcce0f91e6d65281bd6f2819cca3bf39c8" +dependencies = [ + "bitflags 2.6.0", + "core-foundation 0.10.0", + "core-foundation-sys", + "libc", + "security-framework-sys", +] + [[package]] name = "security-framework-sys" version = "2.12.0" @@ -8169,6 +8403,31 @@ dependencies = [ "unsafe-libyaml", ] +[[package]] +name = "serial_test" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b4b487fe2acf240a021cf57c6b2b4903b1e78ca0ecd862a71b71d2a51fed77d" +dependencies = [ + "futures 0.3.31", + "log", + "once_cell", + "parking_lot", + "scc", + "serial_test_derive", +] + +[[package]] +name = "serial_test_derive" +version = "3.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82fe9db325bcef1fbcde82e078a5cc4efdf787e96b3b9cf45b50b529f2083d67" +dependencies = [ + "proc-macro2 1.0.89", + "quote 1.0.37", + "syn 2.0.85", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -9224,6 +9483,16 @@ dependencies = [ "futures-core", ] +[[package]] +name = "sys-info" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b3a0d0aba8bf96a0e1ddfdc352fc53b3df7f39318c71854910c3c4b024ae52c" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "system-configuration" version = "0.5.1" @@ -9231,7 +9500,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ba3a3adc5c275d719af8cb4272ea1c4a6d668a777f37e115f6d11ddbc1c8e0e7" dependencies = [ "bitflags 1.3.2", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.5.0", ] @@ -9242,7 +9511,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3c879d448e9d986b661742763247d3693ed13609438cf3d006f51f5368a5ba6b" dependencies = [ "bitflags 2.6.0", - "core-foundation", + "core-foundation 0.9.4", "system-configuration-sys 0.6.0", ] @@ -9828,9 +10097,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.6.1" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8437150ab6bbc8c5f0f519e3d5ed4aa883a83dd4cdd3d1b21f9482936046cb97" +checksum = "403fa3b783d4b626a8ad51d766ab03cb6d2dbfc46b1c5d4448395e6628dc9697" dependencies = [ "bitflags 2.6.0", "bytes", @@ -10142,10 +10411,14 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b74fc6b57825be3373f7054754755f03ac3a8f5d70015ccad699ba2029956f4a" dependencies = [ "base64 0.22.1", + "flate2", "log", "native-tls", "once_cell", + "rustls 0.23.16", + "rustls-pki-types", "url", + "webpki-roots", ] [[package]] @@ -11583,6 +11856,7 @@ name = "zksync_da_clients" version = "0.1.0" dependencies = [ "anyhow", + "ark-bn254", "async-trait", "backon", "base58", @@ -11592,23 +11866,31 @@ dependencies = [ "blake2b_simd", "bytes", "celestia-types", + "ethabi", "flate2", "futures 0.3.31", "hex", "http 1.1.0", "jsonrpsee 0.23.2", + "num-bigint 0.4.6", "parity-scale-codec", "pbjson-types", "prost 0.12.6", + "rand 0.8.5", "reqwest 0.12.9", "ripemd", + "rlp", + "rust-kzg-bn254", "scale-encode", "secp256k1", "serde", "serde_json", + "serial_test", "sha2 0.10.8", + "sha3 0.10.8", "subxt-metadata", "subxt-signer", + "tiny-keccak 2.0.2", "tokio", "tokio-stream", "tonic 0.11.0", @@ -11617,8 +11899,10 @@ dependencies = [ "zksync_config", "zksync_da_client", "zksync_env_config", + "zksync_eth_client", "zksync_object_store", "zksync_types", + "zksync_web3_decl", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 44a00196fb76..d00de197d0a6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -222,6 +222,10 @@ pbjson-types = "0.6.0" # Eigen tokio-stream = "0.1.16" +rust-kzg-bn254 = "0.2.1" +ark-bn254 = "0.5.0" +num-bigint = "0.4.6" +serial_test = "3.1.1" # Here and below: # We *always* pin the latest version of protocol to disallow accidental changes in the execution logic. diff --git a/core/lib/config/src/configs/da_client/eigen.rs b/core/lib/config/src/configs/da_client/eigen.rs index f2c05a0f61ef..a07d932d3dc8 100644 --- a/core/lib/config/src/configs/da_client/eigen.rs +++ b/core/lib/config/src/configs/da_client/eigen.rs @@ -1,10 +1,31 @@ use serde::Deserialize; use zksync_basic_types::secrets::PrivateKey; - -#[derive(Clone, Debug, Default, PartialEq, Deserialize)] +/// Configuration for the EigenDA remote disperser client. +#[derive(Clone, Debug, PartialEq, Deserialize, Default)] pub struct EigenConfig { - pub rpc_node_url: String, - pub inclusion_polling_interval_ms: u64, + /// URL of the Disperser RPC server + pub disperser_rpc: String, + /// Block height needed to reach in order to consider the blob finalized + /// a value less or equal to 0 means that the disperser will not wait for finalization + pub settlement_layer_confirmation_depth: i32, + /// URL of the Ethereum RPC server + pub eigenda_eth_rpc: String, + /// Address of the service manager contract + pub eigenda_svc_manager_address: String, + /// Maximun amount of time in milliseconds to wait for a status query response + pub status_query_timeout: u64, + /// Interval in milliseconds to query the status of a blob + pub status_query_interval: u64, + /// Wait for the blob to be finalized before returning the response + pub wait_for_finalization: bool, + /// Authenticated dispersal + pub authenticated: bool, + /// Verify the certificate of dispatched blobs + pub verify_cert: bool, + /// Path to the file containing the points used for KZG + pub path_to_points: String, + /// Chain ID of the Ethereum network + pub chain_id: u64, } #[derive(Clone, Debug, PartialEq)] diff --git a/core/lib/env_config/src/da_client.rs b/core/lib/env_config/src/da_client.rs index 8ceeb215faf4..8cd7658cd1db 100644 --- a/core/lib/env_config/src/da_client.rs +++ b/core/lib/env_config/src/da_client.rs @@ -248,8 +248,18 @@ mod tests { let mut lock = MUTEX.lock(); let config = r#" DA_CLIENT="Eigen" - DA_RPC_NODE_URL="localhost:12345" - DA_INCLUSION_POLLING_INTERVAL_MS="1000" + DA_EIGEN_CLIENT_TYPE="Disperser" + DA_DISPERSER_RPC="http://localhost:8080" + DA_SETTLEMENT_LAYER_CONFIRMATION_DEPTH=0 + DA_EIGENDA_ETH_RPC="http://localhost:8545" + DA_EIGENDA_SVC_MANAGER_ADDRESS="0x123" + DA_STATUS_QUERY_TIMEOUT=2 + DA_STATUS_QUERY_INTERVAL=3 + DA_WAIT_FOR_FINALIZATION=true + DA_AUTHENTICATED=false + DA_VERIFY_CERT=false + DA_PATH_TO_POINTS="resources" + DA_CHAIN_ID=1 "#; lock.set_env(config); @@ -257,8 +267,17 @@ mod tests { assert_eq!( actual, DAClientConfig::Eigen(EigenConfig { - rpc_node_url: "localhost:12345".to_string(), - inclusion_polling_interval_ms: 1000, + disperser_rpc: "http://localhost:8080".to_string(), + settlement_layer_confirmation_depth: 0, + eigenda_eth_rpc: "http://localhost:8545".to_string(), + eigenda_svc_manager_address: "0x123".to_string(), + status_query_timeout: 2, + status_query_interval: 3, + wait_for_finalization: true, + authenticated: false, + verify_cert: false, + path_to_points: "resources".to_string(), + chain_id: 1 }) ); } diff --git a/core/lib/env_config/src/lib.rs b/core/lib/env_config/src/lib.rs index 325288056b35..f7c45e98500e 100644 --- a/core/lib/env_config/src/lib.rs +++ b/core/lib/env_config/src/lib.rs @@ -1,4 +1,3 @@ -use anyhow::Context as _; use serde::de::DeserializeOwned; mod api; @@ -44,5 +43,5 @@ pub trait FromEnv: Sized { pub fn envy_load(name: &str, prefix: &str) -> anyhow::Result { envy::prefixed(prefix) .from_env() - .with_context(|| format!("Cannot load config <{name}>")) + .map_err(|e| anyhow::anyhow!("Failed to load {} from env: {}", name, e)) } diff --git a/core/lib/protobuf_config/src/da_client.rs b/core/lib/protobuf_config/src/da_client.rs index 341a6a9e4f43..210171be1875 100644 --- a/core/lib/protobuf_config/src/da_client.rs +++ b/core/lib/protobuf_config/src/da_client.rs @@ -53,11 +53,31 @@ impl ProtoRepr for proto::DataAvailabilityClient { timeout_ms: *required(&conf.timeout_ms).context("timeout_ms")?, }), proto::data_availability_client::Config::Eigen(conf) => Eigen(EigenConfig { - rpc_node_url: required(&conf.rpc_node_url) - .context("rpc_node_url")? + disperser_rpc: required(&conf.disperser_rpc) + .context("disperser_rpc")? .clone(), - inclusion_polling_interval_ms: *required(&conf.inclusion_polling_interval_ms) - .context("inclusion_polling_interval_ms")?, + settlement_layer_confirmation_depth: *required( + &conf.settlement_layer_confirmation_depth, + ) + .context("settlement_layer_confirmation_depth")?, + eigenda_eth_rpc: required(&conf.eigenda_eth_rpc) + .context("eigenda_eth_rpc")? + .clone(), + eigenda_svc_manager_address: required(&conf.eigenda_svc_manager_address) + .context("eigenda_svc_manager_address")? + .clone(), + status_query_timeout: *required(&conf.status_query_timeout) + .context("status_query_timeout")?, + status_query_interval: *required(&conf.status_query_interval) + .context("status_query_interval")?, + wait_for_finalization: *required(&conf.wait_for_finalization) + .context("wait_for_finalization")?, + authenticated: *required(&conf.authenticated).context("authenticated")?, + verify_cert: *required(&conf.verify_cert).context("verify_cert")?, + path_to_points: required(&conf.path_to_points) + .context("path_to_points")? + .clone(), + chain_id: *required(&conf.chain_id).context("chain_id")?, }), proto::data_availability_client::Config::ObjectStore(conf) => { ObjectStore(object_store_proto::ObjectStore::read(conf)?) @@ -96,8 +116,19 @@ impl ProtoRepr for proto::DataAvailabilityClient { }) } Eigen(config) => proto::data_availability_client::Config::Eigen(proto::EigenConfig { - rpc_node_url: Some(config.rpc_node_url.clone()), - inclusion_polling_interval_ms: Some(config.inclusion_polling_interval_ms), + disperser_rpc: Some(config.disperser_rpc.clone()), + settlement_layer_confirmation_depth: Some( + config.settlement_layer_confirmation_depth, + ), + eigenda_eth_rpc: Some(config.eigenda_eth_rpc.clone()), + eigenda_svc_manager_address: Some(config.eigenda_svc_manager_address.clone()), + status_query_timeout: Some(config.status_query_timeout), + status_query_interval: Some(config.status_query_interval), + wait_for_finalization: Some(config.wait_for_finalization), + authenticated: Some(config.authenticated), + verify_cert: Some(config.verify_cert), + path_to_points: Some(config.path_to_points.clone()), + chain_id: Some(config.chain_id), }), ObjectStore(config) => proto::data_availability_client::Config::ObjectStore( object_store_proto::ObjectStore::build(config), diff --git a/core/lib/protobuf_config/src/proto/config/da_client.proto b/core/lib/protobuf_config/src/proto/config/da_client.proto index 0a302120d775..8463629b3240 100644 --- a/core/lib/protobuf_config/src/proto/config/da_client.proto +++ b/core/lib/protobuf_config/src/proto/config/da_client.proto @@ -37,8 +37,19 @@ message CelestiaConfig { } message EigenConfig { - optional string rpc_node_url = 1; - optional uint64 inclusion_polling_interval_ms = 2; + optional string disperser_rpc = 3; + optional int32 settlement_layer_confirmation_depth = 4; + optional string eigenda_eth_rpc = 5; + optional string eigenda_svc_manager_address = 6; + optional uint64 status_query_timeout = 7; + optional uint64 status_query_interval = 8; + optional bool wait_for_finalization = 9; + optional bool authenticated = 10; + optional bool verify_cert = 11; + optional string path_to_points = 12; + optional uint64 chain_id = 13; + reserved 1,2; + reserved "rpc_node_url","inclusion_polling_interval_ms"; } message DataAvailabilityClient { diff --git a/core/node/da_clients/Cargo.toml b/core/node/da_clients/Cargo.toml index e0c85b3030ab..f23b511818ad 100644 --- a/core/node/da_clients/Cargo.toml +++ b/core/node/da_clients/Cargo.toml @@ -55,3 +55,14 @@ pbjson-types.workspace = true # Eigen dependencies tokio-stream.workspace = true +rlp.workspace = true +rand.workspace = true +sha3.workspace = true +tiny-keccak.workspace = true +ethabi.workspace = true +rust-kzg-bn254.workspace = true +ark-bn254.workspace = true +num-bigint.workspace = true +serial_test.workspace = true +zksync_web3_decl.workspace = true +zksync_eth_client.workspace = true diff --git a/core/node/da_clients/src/eigen/blob_info.rs b/core/node/da_clients/src/eigen/blob_info.rs new file mode 100644 index 000000000000..658e3be284a8 --- /dev/null +++ b/core/node/da_clients/src/eigen/blob_info.rs @@ -0,0 +1,504 @@ +use std::fmt; + +use rlp::{Decodable, DecoderError, Encodable, Rlp, RlpStream}; + +use super::{ + common::G1Commitment as DisperserG1Commitment, + disperser::{ + BatchHeader as DisperserBatchHeader, BatchMetadata as DisperserBatchMetadata, + BlobHeader as DisperserBlobHeader, BlobInfo as DisperserBlobInfo, + BlobQuorumParam as DisperserBlobQuorumParam, + BlobVerificationProof as DisperserBlobVerificationProof, + }, +}; + +#[derive(Debug)] +pub enum ConversionError { + NotPresentError, +} + +impl fmt::Display for ConversionError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + ConversionError::NotPresentError => write!(f, "Failed to convert BlobInfo"), + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct G1Commitment { + pub x: Vec, + pub y: Vec, +} + +impl G1Commitment { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(&self.x.len().to_be_bytes()); + bytes.extend(&self.x); + bytes.extend(&self.y.len().to_be_bytes()); + bytes.extend(&self.y); + + bytes + } +} + +impl Decodable for G1Commitment { + fn decode(rlp: &Rlp) -> Result { + let x: Vec = rlp.val_at(0)?; // Decode first element as Vec + let y: Vec = rlp.val_at(1)?; // Decode second element as Vec + + Ok(G1Commitment { x, y }) + } +} + +impl Encodable for G1Commitment { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(2); + s.append(&self.x); + s.append(&self.y); + } +} + +impl From for G1Commitment { + fn from(value: DisperserG1Commitment) -> Self { + Self { + x: value.x, + y: value.y, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BlobQuorumParam { + pub quorum_number: u32, + pub adversary_threshold_percentage: u32, + pub confirmation_threshold_percentage: u32, + pub chunk_length: u32, +} + +impl BlobQuorumParam { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(&self.quorum_number.to_be_bytes()); + bytes.extend(&self.adversary_threshold_percentage.to_be_bytes()); + bytes.extend(&self.confirmation_threshold_percentage.to_be_bytes()); + bytes.extend(&self.chunk_length.to_be_bytes()); + + bytes + } +} + +impl Decodable for BlobQuorumParam { + fn decode(rlp: &Rlp) -> Result { + Ok(BlobQuorumParam { + quorum_number: rlp.val_at(0)?, + adversary_threshold_percentage: rlp.val_at(1)?, + confirmation_threshold_percentage: rlp.val_at(2)?, + chunk_length: rlp.val_at(3)?, + }) + } +} + +impl Encodable for BlobQuorumParam { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(4); + s.append(&self.quorum_number); + s.append(&self.adversary_threshold_percentage); + s.append(&self.confirmation_threshold_percentage); + s.append(&self.chunk_length); + } +} + +impl From for BlobQuorumParam { + fn from(value: DisperserBlobQuorumParam) -> Self { + Self { + quorum_number: value.quorum_number, + adversary_threshold_percentage: value.adversary_threshold_percentage, + confirmation_threshold_percentage: value.confirmation_threshold_percentage, + chunk_length: value.chunk_length, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BlobHeader { + pub commitment: G1Commitment, + pub data_length: u32, + pub blob_quorum_params: Vec, +} + +impl BlobHeader { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(self.commitment.to_bytes()); + bytes.extend(&self.data_length.to_be_bytes()); + bytes.extend(&self.blob_quorum_params.len().to_be_bytes()); + + for quorum in &self.blob_quorum_params { + bytes.extend(quorum.to_bytes()); + } + + bytes + } +} + +impl Decodable for BlobHeader { + fn decode(rlp: &Rlp) -> Result { + let commitment: G1Commitment = rlp.val_at(0)?; + let data_length: u32 = rlp.val_at(1)?; + let blob_quorum_params: Vec = rlp.list_at(2)?; + + Ok(BlobHeader { + commitment, + data_length, + blob_quorum_params, + }) + } +} + +impl Encodable for BlobHeader { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(3); + s.append(&self.commitment); + s.append(&self.data_length); + s.append_list(&self.blob_quorum_params); + } +} + +impl TryFrom for BlobHeader { + type Error = ConversionError; + fn try_from(value: DisperserBlobHeader) -> Result { + if value.commitment.is_none() { + return Err(ConversionError::NotPresentError); + } + let blob_quorum_params: Vec = value + .blob_quorum_params + .iter() + .map(|param| BlobQuorumParam::from(param.clone())) + .collect(); + Ok(Self { + commitment: G1Commitment::from(value.commitment.unwrap()), + data_length: value.data_length, + blob_quorum_params, + }) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BatchHeader { + pub batch_root: Vec, + pub quorum_numbers: Vec, + pub quorum_signed_percentages: Vec, + pub reference_block_number: u32, +} + +impl BatchHeader { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(&self.batch_root.len().to_be_bytes()); + bytes.extend(&self.batch_root); + bytes.extend(&self.quorum_numbers.len().to_be_bytes()); + bytes.extend(&self.quorum_numbers); + bytes.extend(&self.quorum_signed_percentages.len().to_be_bytes()); + bytes.extend(&self.quorum_signed_percentages); + bytes.extend(&self.reference_block_number.to_be_bytes()); + + bytes + } +} + +impl Decodable for BatchHeader { + fn decode(rlp: &Rlp) -> Result { + Ok(BatchHeader { + batch_root: rlp.val_at(0)?, + quorum_numbers: rlp.val_at(1)?, + quorum_signed_percentages: rlp.val_at(2)?, + reference_block_number: rlp.val_at(3)?, + }) + } +} + +impl Encodable for BatchHeader { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(4); + s.append(&self.batch_root); + s.append(&self.quorum_numbers); + s.append(&self.quorum_signed_percentages); + s.append(&self.reference_block_number); + } +} + +impl From for BatchHeader { + fn from(value: DisperserBatchHeader) -> Self { + Self { + batch_root: value.batch_root, + quorum_numbers: value.quorum_numbers, + quorum_signed_percentages: value.quorum_signed_percentages, + reference_block_number: value.reference_block_number, + } + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BatchMetadata { + pub batch_header: BatchHeader, + pub signatory_record_hash: Vec, + pub fee: Vec, + pub confirmation_block_number: u32, + pub batch_header_hash: Vec, +} + +impl BatchMetadata { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(self.batch_header.to_bytes()); + bytes.extend(&self.signatory_record_hash); + bytes.extend(&self.confirmation_block_number.to_be_bytes()); + + bytes + } +} + +impl Decodable for BatchMetadata { + fn decode(rlp: &Rlp) -> Result { + let batch_header: BatchHeader = rlp.val_at(0)?; + + Ok(BatchMetadata { + batch_header, + signatory_record_hash: rlp.val_at(1)?, + fee: rlp.val_at(2)?, + confirmation_block_number: rlp.val_at(3)?, + batch_header_hash: rlp.val_at(4)?, + }) + } +} + +impl Encodable for BatchMetadata { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(5); + s.append(&self.batch_header); + s.append(&self.signatory_record_hash); + s.append(&self.fee); + s.append(&self.confirmation_block_number); + s.append(&self.batch_header_hash); + } +} + +impl TryFrom for BatchMetadata { + type Error = ConversionError; + fn try_from(value: DisperserBatchMetadata) -> Result { + if value.batch_header.is_none() { + return Err(ConversionError::NotPresentError); + } + Ok(Self { + batch_header: BatchHeader::from(value.batch_header.unwrap()), + signatory_record_hash: value.signatory_record_hash, + fee: value.fee, + confirmation_block_number: value.confirmation_block_number, + batch_header_hash: value.batch_header_hash, + }) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BlobVerificationProof { + pub batch_id: u32, + pub blob_index: u32, + pub batch_medatada: BatchMetadata, + pub inclusion_proof: Vec, + pub quorum_indexes: Vec, +} + +impl BlobVerificationProof { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + bytes.extend(&self.batch_id.to_be_bytes()); + bytes.extend(&self.blob_index.to_be_bytes()); + bytes.extend(self.batch_medatada.to_bytes()); + bytes.extend(&self.inclusion_proof.len().to_be_bytes()); + bytes.extend(&self.inclusion_proof); + bytes.extend(&self.quorum_indexes.len().to_be_bytes()); + bytes.extend(&self.quorum_indexes); + + bytes + } +} + +impl Decodable for BlobVerificationProof { + fn decode(rlp: &Rlp) -> Result { + Ok(BlobVerificationProof { + batch_id: rlp.val_at(0)?, + blob_index: rlp.val_at(1)?, + batch_medatada: rlp.val_at(2)?, + inclusion_proof: rlp.val_at(3)?, + quorum_indexes: rlp.val_at(4)?, + }) + } +} + +impl Encodable for BlobVerificationProof { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(5); + s.append(&self.batch_id); + s.append(&self.blob_index); + s.append(&self.batch_medatada); + s.append(&self.inclusion_proof); + s.append(&self.quorum_indexes); + } +} + +impl TryFrom for BlobVerificationProof { + type Error = ConversionError; + fn try_from(value: DisperserBlobVerificationProof) -> Result { + if value.batch_metadata.is_none() { + return Err(ConversionError::NotPresentError); + } + Ok(Self { + batch_id: value.batch_id, + blob_index: value.blob_index, + batch_medatada: BatchMetadata::try_from(value.batch_metadata.unwrap())?, + inclusion_proof: value.inclusion_proof, + quorum_indexes: value.quorum_indexes, + }) + } +} + +#[derive(Debug, PartialEq, Clone)] +pub struct BlobInfo { + pub blob_header: BlobHeader, + pub blob_verification_proof: BlobVerificationProof, +} + +impl BlobInfo { + pub fn to_bytes(&self) -> Vec { + let mut bytes = vec![]; + let blob_header_bytes = self.blob_header.to_bytes(); + bytes.extend(blob_header_bytes.len().to_be_bytes()); + bytes.extend(blob_header_bytes); + let blob_verification_proof_bytes = self.blob_verification_proof.to_bytes(); + bytes.extend(blob_verification_proof_bytes); + bytes + } +} + +impl Decodable for BlobInfo { + fn decode(rlp: &Rlp) -> Result { + let blob_header: BlobHeader = rlp.val_at(0)?; + let blob_verification_proof: BlobVerificationProof = rlp.val_at(1)?; + + Ok(BlobInfo { + blob_header, + blob_verification_proof, + }) + } +} + +impl Encodable for BlobInfo { + fn rlp_append(&self, s: &mut RlpStream) { + s.begin_list(2); + s.append(&self.blob_header); + s.append(&self.blob_verification_proof); + } +} + +impl TryFrom for BlobInfo { + type Error = ConversionError; + fn try_from(value: DisperserBlobInfo) -> Result { + if value.blob_header.is_none() || value.blob_verification_proof.is_none() { + return Err(ConversionError::NotPresentError); + } + Ok(Self { + blob_header: BlobHeader::try_from(value.blob_header.unwrap())?, + blob_verification_proof: BlobVerificationProof::try_from( + value.blob_verification_proof.unwrap(), + )?, + }) + } +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_blob_info_encoding_and_decoding() { + let blob_info = BlobInfo { + blob_header: BlobHeader { + commitment: G1Commitment { + x: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + y: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + }, + data_length: 4, + blob_quorum_params: vec![ + BlobQuorumParam { + quorum_number: 0, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + BlobQuorumParam { + quorum_number: 1, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + ], + }, + blob_verification_proof: BlobVerificationProof { + batch_id: 66507, + blob_index: 92, + batch_medatada: BatchMetadata { + batch_header: BatchHeader { + batch_root: vec![ + 179, 187, 53, 98, 192, 80, 151, 28, 125, 192, 115, 29, 129, 238, 216, + 8, 213, 210, 203, 143, 181, 19, 146, 113, 98, 131, 39, 238, 149, 248, + 211, 43, + ], + quorum_numbers: vec![0, 1], + quorum_signed_percentages: vec![100, 100], + reference_block_number: 2624794, + }, + signatory_record_hash: vec![ + 172, 32, 172, 142, 197, 52, 84, 143, 120, 26, 190, 9, 143, 217, 62, 19, 17, + 107, 105, 67, 203, 5, 172, 249, 6, 60, 105, 240, 134, 34, 66, 133, + ], + fee: vec![0], + confirmation_block_number: 2624876, + batch_header_hash: vec![ + 122, 115, 2, 85, 233, 75, 121, 85, 51, 81, 248, 170, 198, 252, 42, 16, 1, + 146, 96, 218, 159, 44, 41, 40, 94, 247, 147, 11, 255, 68, 40, 177, + ], + }, + inclusion_proof: vec![ + 203, 160, 237, 48, 117, 255, 75, 254, 117, 144, 164, 77, 29, 146, 36, 48, 190, + 140, 50, 100, 144, 237, 125, 125, 75, 54, 210, 247, 147, 23, 48, 189, 120, 4, + 125, 123, 195, 244, 207, 239, 145, 109, 0, 21, 11, 162, 109, 79, 192, 100, 138, + 157, 203, 22, 17, 114, 234, 72, 174, 231, 209, 133, 99, 118, 201, 160, 137, + 128, 112, 84, 34, 136, 174, 139, 96, 26, 246, 148, 134, 52, 200, 229, 160, 145, + 5, 120, 18, 187, 51, 11, 109, 91, 237, 171, 215, 207, 90, 95, 146, 54, 135, + 166, 66, 157, 255, 237, 69, 183, 141, 45, 162, 145, 71, 16, 87, 184, 120, 84, + 156, 220, 159, 4, 99, 48, 191, 203, 136, 112, 127, 226, 192, 184, 110, 6, 177, + 182, 109, 207, 197, 239, 161, 132, 17, 89, 56, 137, 205, 202, 101, 97, 60, 162, + 253, 23, 169, 75, 236, 211, 126, 121, 132, 191, 68, 167, 200, 16, 154, 149, + 202, 197, 7, 191, 26, 8, 67, 3, 37, 137, 16, 153, 30, 209, 238, 53, 233, 148, + 198, 253, 94, 216, 73, 25, 190, 205, 132, 208, 255, 219, 170, 98, 17, 160, 179, + 183, 200, 17, 99, 36, 130, 216, 223, 72, 222, 250, 73, 78, 79, 72, 253, 105, + 245, 84, 244, 196, + ], + quorum_indexes: vec![0, 1], + }, + }; + + let encoded_blob_info = rlp::encode(&blob_info); + let decoded_blob_info: BlobInfo = rlp::decode(&encoded_blob_info).unwrap(); + + assert_eq!(blob_info, decoded_blob_info); + } +} diff --git a/core/node/da_clients/src/eigen/client.rs b/core/node/da_clients/src/eigen/client.rs index d977620526aa..da26519c8de2 100644 --- a/core/node/da_clients/src/eigen/client.rs +++ b/core/node/da_clients/src/eigen/client.rs @@ -1,5 +1,6 @@ use std::{str::FromStr, sync::Arc}; +use anyhow::anyhow; use async_trait::async_trait; use secp256k1::SecretKey; use subxt_signer::ExposeSecret; @@ -9,9 +10,13 @@ use zksync_da_client::{ DataAvailabilityClient, }; -use super::sdk::RawEigenClient; +use super::{blob_info::BlobInfo, sdk::RawEigenClient}; use crate::utils::to_non_retriable_da_error; +/// EigenClient is a client for the Eigen DA service. +/// It can be configured to use one of two dispersal methods: +/// - Remote: Dispatch blobs to a remote Eigen service. +/// - Memstore: Stores blobs in memory, used for testing purposes. #[derive(Debug, Clone)] pub struct EigenClient { client: Arc, @@ -22,15 +27,9 @@ impl EigenClient { let private_key = SecretKey::from_str(secrets.private_key.0.expose_secret().as_str()) .map_err(|e| anyhow::anyhow!("Failed to parse private key: {}", e))?; - Ok(EigenClient { - client: Arc::new( - RawEigenClient::new( - config.rpc_node_url, - config.inclusion_polling_interval_ms, - private_key, - ) - .await?, - ), + let client = RawEigenClient::new(private_key, config).await?; + Ok(Self { + client: Arc::new(client), }) } } @@ -51,8 +50,19 @@ impl DataAvailabilityClient for EigenClient { Ok(DispatchResponse::from(blob_id)) } - async fn get_inclusion_data(&self, _: &str) -> Result, DAError> { - Ok(Some(InclusionData { data: vec![] })) + async fn get_inclusion_data(&self, blob_id: &str) -> Result, DAError> { + let rlp_encoded_bytes = hex::decode(blob_id).map_err(|_| DAError { + error: anyhow!("Failed to decode blob_id"), + is_retriable: false, + })?; + let blob_info: BlobInfo = rlp::decode(&rlp_encoded_bytes).map_err(|_| DAError { + error: anyhow!("Failed to decode blob_info"), + is_retriable: false, + })?; + let inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + Ok(Some(InclusionData { + data: inclusion_data, + })) } fn clone_boxed(&self) -> Box { @@ -60,6 +70,216 @@ impl DataAvailabilityClient for EigenClient { } fn blob_size_limit(&self) -> Option { - Some(1920 * 1024) // 2mb - 128kb as a buffer + Some(RawEigenClient::blob_size_limit()) + } +} + +#[cfg(test)] +impl EigenClient { + pub async fn get_blob_data(&self, blob_id: &str) -> anyhow::Result>, DAError> { + self.client.get_blob_data(blob_id).await + } +} +#[cfg(test)] +mod tests { + use serial_test::serial; + use zksync_types::secrets::PrivateKey; + + use super::*; + use crate::eigen::blob_info::BlobInfo; + + #[tokio::test] + #[serial] + async fn test_non_auth_dispersal() { + let config = EigenConfig { + disperser_rpc: "https://disperser-holesky.eigenda.xyz:443".to_string(), + settlement_layer_confirmation_depth: -1, + eigenda_eth_rpc: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + eigenda_svc_manager_address: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + status_query_timeout: 1800000, // 30 minutes + status_query_interval: 5, // 5 ms + wait_for_finalization: false, + authenticated: false, + verify_cert: true, + path_to_points: "../../../resources".to_string(), + chain_id: 17000, + }; + let secrets = EigenSecrets { + private_key: PrivateKey::from_str( + "d08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6", + ) + .unwrap(), + }; + let client = EigenClient::new(config, secrets).await.unwrap(); + let data = vec![1; 20]; + let result = client.dispatch_blob(0, data.clone()).await.unwrap(); + let blob_info: BlobInfo = + rlp::decode(&hex::decode(result.blob_id.clone()).unwrap()).unwrap(); + let expected_inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + let actual_inclusion_data = client + .get_inclusion_data(&result.blob_id) + .await + .unwrap() + .unwrap() + .data; + assert_eq!(expected_inclusion_data, actual_inclusion_data); + let retrieved_data = client.get_blob_data(&result.blob_id).await.unwrap(); + assert_eq!(retrieved_data.unwrap(), data); + } + + #[tokio::test] + #[serial] + async fn test_auth_dispersal() { + let config = EigenConfig { + disperser_rpc: "https://disperser-holesky.eigenda.xyz:443".to_string(), + settlement_layer_confirmation_depth: -1, + eigenda_eth_rpc: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + eigenda_svc_manager_address: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + status_query_timeout: 1800000, // 30 minutes + status_query_interval: 5, // 5 ms + wait_for_finalization: false, + authenticated: true, + verify_cert: true, + path_to_points: "../../../resources".to_string(), + chain_id: 17000, + }; + let secrets = EigenSecrets { + private_key: PrivateKey::from_str( + "d08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6", + ) + .unwrap(), + }; + let client = EigenClient::new(config, secrets).await.unwrap(); + let data = vec![1; 20]; + let result = client.dispatch_blob(0, data.clone()).await.unwrap(); + let blob_info: BlobInfo = + rlp::decode(&hex::decode(result.blob_id.clone()).unwrap()).unwrap(); + let expected_inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + let actual_inclusion_data = client + .get_inclusion_data(&result.blob_id) + .await + .unwrap() + .unwrap() + .data; + assert_eq!(expected_inclusion_data, actual_inclusion_data); + let retrieved_data = client.get_blob_data(&result.blob_id).await.unwrap(); + assert_eq!(retrieved_data.unwrap(), data); + } + + #[tokio::test] + #[serial] + async fn test_wait_for_finalization() { + let config = EigenConfig { + disperser_rpc: "https://disperser-holesky.eigenda.xyz:443".to_string(), + status_query_timeout: 1800000, // 30 minutes + status_query_interval: 5000, // 5000 ms + wait_for_finalization: true, + authenticated: true, + verify_cert: true, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + eigenda_eth_rpc: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + eigenda_svc_manager_address: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + chain_id: 17000, + }; + let secrets = EigenSecrets { + private_key: PrivateKey::from_str( + "d08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6", + ) + .unwrap(), + }; + let client = EigenClient::new(config, secrets).await.unwrap(); + let data = vec![1; 20]; + let result = client.dispatch_blob(0, data.clone()).await.unwrap(); + let blob_info: BlobInfo = + rlp::decode(&hex::decode(result.blob_id.clone()).unwrap()).unwrap(); + let expected_inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + let actual_inclusion_data = client + .get_inclusion_data(&result.blob_id) + .await + .unwrap() + .unwrap() + .data; + assert_eq!(expected_inclusion_data, actual_inclusion_data); + let retrieved_data = client.get_blob_data(&result.blob_id).await.unwrap(); + assert_eq!(retrieved_data.unwrap(), data); + } + + #[tokio::test] + #[serial] + async fn test_settlement_layer_confirmation_depth() { + let config = EigenConfig { + disperser_rpc: "https://disperser-holesky.eigenda.xyz:443".to_string(), + settlement_layer_confirmation_depth: 5, + eigenda_eth_rpc: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + eigenda_svc_manager_address: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + status_query_timeout: 1800000, // 30 minutes + status_query_interval: 5, // 5 ms + wait_for_finalization: false, + authenticated: false, + verify_cert: true, + path_to_points: "../../../resources".to_string(), + chain_id: 17000, + }; + let secrets = EigenSecrets { + private_key: PrivateKey::from_str( + "d08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6", + ) + .unwrap(), + }; + let client = EigenClient::new(config, secrets).await.unwrap(); + let data = vec![1; 20]; + let result = client.dispatch_blob(0, data.clone()).await.unwrap(); + let blob_info: BlobInfo = + rlp::decode(&hex::decode(result.blob_id.clone()).unwrap()).unwrap(); + let expected_inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + let actual_inclusion_data = client + .get_inclusion_data(&result.blob_id) + .await + .unwrap() + .unwrap() + .data; + assert_eq!(expected_inclusion_data, actual_inclusion_data); + let retrieved_data = client.get_blob_data(&result.blob_id).await.unwrap(); + assert_eq!(retrieved_data.unwrap(), data); + } + + #[tokio::test] + #[serial] + async fn test_auth_dispersal_settlement_layer_confirmation_depth() { + let config = EigenConfig { + disperser_rpc: "https://disperser-holesky.eigenda.xyz:443".to_string(), + settlement_layer_confirmation_depth: 5, + eigenda_eth_rpc: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + eigenda_svc_manager_address: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + status_query_timeout: 1800000, // 30 minutes + status_query_interval: 5, // 5 ms + wait_for_finalization: false, + authenticated: true, + verify_cert: true, + path_to_points: "../../../resources".to_string(), + chain_id: 17000, + }; + let secrets = EigenSecrets { + private_key: PrivateKey::from_str( + "d08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6", + ) + .unwrap(), + }; + let client = EigenClient::new(config, secrets).await.unwrap(); + let data = vec![1; 20]; + let result = client.dispatch_blob(0, data.clone()).await.unwrap(); + let blob_info: BlobInfo = + rlp::decode(&hex::decode(result.blob_id.clone()).unwrap()).unwrap(); + let expected_inclusion_data = blob_info.blob_verification_proof.inclusion_proof; + let actual_inclusion_data = client + .get_inclusion_data(&result.blob_id) + .await + .unwrap() + .unwrap() + .data; + assert_eq!(expected_inclusion_data, actual_inclusion_data); + let retrieved_data = client.get_blob_data(&result.blob_id).await.unwrap(); + assert_eq!(retrieved_data.unwrap(), data); } } diff --git a/core/node/da_clients/src/eigen/eigenda-integration.md b/core/node/da_clients/src/eigen/eigenda-integration.md new file mode 100644 index 000000000000..e15178e0fcfc --- /dev/null +++ b/core/node/da_clients/src/eigen/eigenda-integration.md @@ -0,0 +1,176 @@ +# Zksync-era <> EigenDA Integration + +EigenDA is as a high-throughput data availability layer for rollups. It is an EigenLayer AVS (Actively Validated +Service), so it leverages Ethereum's economic security instead of bootstrapping a new network with its own validators. +For more information you can check the [docs](https://docs.eigenda.xyz/). + +## Scope + +The scope of this first milestone is to spin up a local EigenDA dev environment, spin up a local zksync-era dev +environment and integrate them. Instead of sending 4844 blobs, the zksync-era sends blobs to EigenDA. On L1, mock the +verification logic, such that blocks continue building. Increase the blob size from 4844 size to 2MiB blob. Deploy the +integration to Holesky testnet and provide scripts to setup a network using EigenDA as DA provider. + +## Common changes + +Changes needed both for local and mainnet/testnet setup. + +1. Add `da_client` to `etc/env/file_based/general.yaml`: + +```yaml +da_client: + eigen: + disperser_rpc: + settlement_layer_confirmation_depth: -1 + eigenda_eth_rpc: + eigenda_svc_manager_address: '0xD4A7E1Bd8015057293f0D0A557088c286942e84b' + status_query_timeout: 1800000 # ms + status_query_interval: 5 # ms + wait_for_finalization: false + authenticated: false + verify_cert: true + path_to_points: ./resources + chain_id: +``` + +Also set the private key in `etc/env/file_based/secrets.yaml`: + +```yaml +da: + eigen: + private_key: '' +``` + +2. (optional) for using pubdata with 2MiB (as per specification), modify `etc/env/file_based/general.yaml`: + +```yaml +max_pubdata_per_batch: 2097152 +``` + +## Local Setup + +1. Install `zkstack` + +```bash +cargo install --path zkstack_cli/crates/zkstack --force --locked +``` + +2. Start containers + +```bash +zkstack containers --observability true +``` + +3. Create `eigen_da` chain + +```bash +zkstack chain create \ + --chain-name eigen_da \ + --chain-id sequential \ + --prover-mode no-proofs \ + --wallet-creation localhost \ + --l1-batch-commit-data-generator-mode validium \ + --base-token-address 0x0000000000000000000000000000000000000001 \ + --base-token-price-nominator 1 \ + --base-token-price-denominator 1 \ + --set-as-default false +``` + +4. Initialize created ecosystem + +```bash +zkstack ecosystem init \ + --deploy-paymaster true \ + --deploy-erc20 true \ + --deploy-ecosystem true \ + --l1-rpc-url http://127.0.0.1:8545 \ + --server-db-url=postgres://postgres:notsecurepassword@localhost:5432 \ + --server-db-name=zksync_server_localhost_eigen_da \ + --chain eigen_da \ + --verbose +``` + +You may enable observability here if you want to. + +5. Start the server + +```bash +zkstack server --chain eigen_da +``` + +### Testing + +Modify the following flag in `core/lib/config/src/configs/da_dispatcher.rs` (then restart the server) + +```rs +pub const DEFAULT_USE_DUMMY_INCLUSION_DATA: bool = true; +``` + +And with the server running on one terminal, you can run the server integration tests on a separate terminal with the +following command: + +```bash +zkstack dev test integration --chain eigen_da +``` + +## Mainnet/Testnet setup + +### Modify localhost chain id number + +Modify line 32 in `zk_toolbox/crates/types/src/l1_network.rs`: + +```rs +L1Network::Localhost => 17000, +``` + +Then recompile the zkstack: + +```bash +cargo install --path zkstack_cli/crates/zkstack --force --locked +``` + +### Used wallets + +Modify `etc/env/file_based/wallets.yaml` and `configs/wallets.yaml` with the following wallets: + +```yaml +# Use your own holesky wallets, be sure they have enough funds +``` + +> ⚠️ Some steps distribute ~5000ETH to some wallets, modify `AMOUNT_FOR_DISTRIBUTION_TO_WALLETS` to a lower value if +> needed. + +### Create and initialize the ecosystem + +(be sure to have postgres container running on the background) + +```bash +zkstack chain create \ + --chain-name holesky_eigen_da \ + --chain-id 114411 \ + --prover-mode no-proofs \ + --wallet-creation localhost \ + --l1-batch-commit-data-generator-mode validium \ + --base-token-address 0x0000000000000000000000000000000000000001 \ + --base-token-price-nominator 1 \ + --base-token-price-denominator 1 \ + --set-as-default false + +zkstack ecosystem init \ + --deploy-paymaster true \ + --deploy-erc20 true \ + --deploy-ecosystem true \ + --l1-rpc-url $HOLESKY_RPC_URL \ + --server-db-url=postgres://postgres:notsecurepassword@localhost:5432 \ + --server-db-name=zksync_server_holesky_eigen_da \ + --prover-db-url=postgres://postgres:notsecurepassword@localhost:5432 \ + --prover-db-name=zksync_prover_holesky_eigen_da \ + --chain holesky_eigen_da \ + --verbose +``` + +### Start the server + +```bash +zkstack server --chain holesky_eigen_da +``` diff --git a/core/node/da_clients/src/eigen/lib.rs b/core/node/da_clients/src/eigen/lib.rs new file mode 100644 index 000000000000..46a7bc34bbd1 --- /dev/null +++ b/core/node/da_clients/src/eigen/lib.rs @@ -0,0 +1,3 @@ +pub const BATCH_ID_TO_METADATA_HASH_FUNCTION_SELECTOR: [u8; 4] = [236, 203, 191, 201]; +pub const QUORUM_ADVERSARY_THRESHOLD_PERCENTAGES_FUNCTION_SELECTOR: [u8; 4] = [134, 135, 254, 174]; +pub const QUORUM_NUMBERS_REQUIRED_FUNCTION_SELECTOR: [u8; 4] = [225, 82, 52, 255]; diff --git a/core/node/da_clients/src/eigen/mod.rs b/core/node/da_clients/src/eigen/mod.rs index 699eae894246..fe979becdb9f 100644 --- a/core/node/da_clients/src/eigen/mod.rs +++ b/core/node/da_clients/src/eigen/mod.rs @@ -1,5 +1,8 @@ +mod blob_info; mod client; +mod lib; mod sdk; +mod verifier; pub use self::client::EigenClient; diff --git a/core/node/da_clients/src/eigen/sdk.rs b/core/node/da_clients/src/eigen/sdk.rs index 7ab7ea3ce33b..19c1c00e1b59 100644 --- a/core/node/da_clients/src/eigen/sdk.rs +++ b/core/node/da_clients/src/eigen/sdk.rs @@ -1,61 +1,138 @@ use std::{str::FromStr, time::Duration}; +use backon::{ConstantBuilder, Retryable}; use secp256k1::{ecdsa::RecoverableSignature, SecretKey}; -use tokio::sync::mpsc; +use tokio::{sync::mpsc, time::Instant}; use tokio_stream::{wrappers::ReceiverStream, StreamExt}; use tonic::{ transport::{Channel, ClientTlsConfig, Endpoint}, Streaming, }; - +use zksync_config::EigenConfig; +#[cfg(test)] +use zksync_da_client::types::DAError; + +use super::{ + blob_info::BlobInfo, + disperser::BlobInfo as DisperserBlobInfo, + verifier::{Verifier, VerifierConfig}, +}; use crate::eigen::{ - disperser, + blob_info, disperser::{ + self, authenticated_request::Payload::{AuthenticationData, DisperseRequest}, disperser_client::DisperserClient, - AuthenticatedReply, BlobAuthHeader, BlobVerificationProof, DisperseBlobReply, + AuthenticatedReply, BlobAuthHeader, DisperseBlobReply, }, }; #[derive(Debug, Clone)] -pub struct RawEigenClient { +pub(crate) struct RawEigenClient { client: DisperserClient, - polling_interval: Duration, private_key: SecretKey, - account_id: String, + pub config: EigenConfig, + verifier: Verifier, } pub(crate) const DATA_CHUNK_SIZE: usize = 32; +pub(crate) const AVG_BLOCK_TIME: u64 = 12; impl RawEigenClient { pub(crate) const BUFFER_SIZE: usize = 1000; + const BLOB_SIZE_LIMIT: usize = 1024 * 1024 * 2; // 2 MB - pub async fn new( - rpc_node_url: String, - inclusion_polling_interval_ms: u64, - private_key: SecretKey, - ) -> anyhow::Result { + pub async fn new(private_key: SecretKey, config: EigenConfig) -> anyhow::Result { let endpoint = - Endpoint::from_str(rpc_node_url.as_str())?.tls_config(ClientTlsConfig::new())?; + Endpoint::from_str(config.disperser_rpc.as_str())?.tls_config(ClientTlsConfig::new())?; let client = DisperserClient::connect(endpoint) .await .map_err(|e| anyhow::anyhow!("Failed to connect to Disperser server: {}", e))?; - let polling_interval = Duration::from_millis(inclusion_polling_interval_ms); - - let account_id = get_account_id(&private_key); + let verifier_config = VerifierConfig { + verify_certs: true, + rpc_url: config.eigenda_eth_rpc.clone(), + svc_manager_addr: config.eigenda_svc_manager_address.clone(), + max_blob_size: Self::BLOB_SIZE_LIMIT as u32, + path_to_points: config.path_to_points.clone(), + settlement_layer_confirmation_depth: config.settlement_layer_confirmation_depth.max(0) + as u32, + private_key: hex::encode(private_key.secret_bytes()), + chain_id: config.chain_id, + }; + let verifier = Verifier::new(verifier_config) + .map_err(|e| anyhow::anyhow!(format!("Failed to create verifier {:?}", e)))?; Ok(RawEigenClient { client, - polling_interval, private_key, - account_id, + config, + verifier, }) } - pub async fn dispatch_blob(&self, data: Vec) -> anyhow::Result { + pub fn blob_size_limit() -> usize { + Self::BLOB_SIZE_LIMIT + } + + async fn dispatch_blob_non_authenticated(&self, data: Vec) -> anyhow::Result { + let padded_data = convert_by_padding_empty_byte(&data); + let request = disperser::DisperseBlobRequest { + data: padded_data, + custom_quorum_numbers: vec![], + account_id: String::default(), // Account Id is not used in non-authenticated mode + }; + + let mut client_clone = self.client.clone(); + let disperse_reply = client_clone.disperse_blob(request).await?.into_inner(); + + let disperse_time = Instant::now(); + let blob_info = self + .await_for_inclusion(client_clone, disperse_reply) + .await?; + let disperse_elapsed = Instant::now() - disperse_time; + + let blob_info = blob_info::BlobInfo::try_from(blob_info) + .map_err(|e| anyhow::anyhow!("Failed to convert blob info: {}", e))?; + self.verifier + .verify_commitment(blob_info.blob_header.commitment.clone(), data) + .map_err(|_| anyhow::anyhow!("Failed to verify commitment"))?; + + self.perform_verification(blob_info.clone(), disperse_elapsed) + .await?; + let verification_proof = blob_info.blob_verification_proof.clone(); + let blob_id = format!( + "{}:{}", + verification_proof.batch_id, verification_proof.blob_index + ); + tracing::info!("Blob dispatch confirmed, blob id: {}", blob_id); + + Ok(hex::encode(rlp::encode(&blob_info))) + } + + async fn perform_verification( + &self, + blob_info: BlobInfo, + disperse_elapsed: Duration, + ) -> anyhow::Result<()> { + (|| async { self.verifier.verify_certificate(blob_info.clone()).await }) + .retry( + &ConstantBuilder::default() + .with_delay(Duration::from_secs(AVG_BLOCK_TIME)) + .with_max_times( + (self.config.status_query_timeout + - disperse_elapsed.as_millis() as u64 / AVG_BLOCK_TIME) + as usize, + ), + ) + .await + .map_err(|_| anyhow::anyhow!("Failed to verify certificate")) + } + + async fn dispatch_blob_authenticated(&self, data: Vec) -> anyhow::Result { let mut client_clone = self.client.clone(); let (tx, rx) = mpsc::channel(Self::BUFFER_SIZE); + let disperse_time = Instant::now(); let response_stream = client_clone.disperse_blob_authenticated(ReceiverStream::new(rx)); let padded_data = convert_by_padding_empty_byte(&data); @@ -86,16 +163,36 @@ impl RawEigenClient { }; // 5. poll for blob status until it reaches the Confirmed state - let verification_proof = self + let blob_info = self .await_for_inclusion(client_clone, disperse_reply) .await?; + + let blob_info = blob_info::BlobInfo::try_from(blob_info) + .map_err(|e| anyhow::anyhow!("Failed to convert blob info: {}", e))?; + + let disperse_elapsed = Instant::now() - disperse_time; + self.verifier + .verify_commitment(blob_info.blob_header.commitment.clone(), data) + .map_err(|_| anyhow::anyhow!("Failed to verify commitment"))?; + + self.perform_verification(blob_info.clone(), disperse_elapsed) + .await?; + + let verification_proof = blob_info.blob_verification_proof.clone(); let blob_id = format!( "{}:{}", verification_proof.batch_id, verification_proof.blob_index ); tracing::info!("Blob dispatch confirmed, blob id: {}", blob_id); + Ok(hex::encode(rlp::encode(&blob_info))) + } - Ok(blob_id) + pub async fn dispatch_blob(&self, data: Vec) -> anyhow::Result { + if self.config.authenticated { + self.dispatch_blob_authenticated(data).await + } else { + self.dispatch_blob_non_authenticated(data).await + } } async fn disperse_data( @@ -107,7 +204,7 @@ impl RawEigenClient { payload: Some(DisperseRequest(disperser::DisperseBlobRequest { data, custom_quorum_numbers: vec![], - account_id: self.account_id.clone(), + account_id: get_account_id(&self.private_key), })), }; @@ -173,41 +270,103 @@ impl RawEigenClient { async fn await_for_inclusion( &self, - mut client: DisperserClient, + client: DisperserClient, disperse_blob_reply: DisperseBlobReply, - ) -> anyhow::Result { + ) -> anyhow::Result { let polling_request = disperser::BlobStatusRequest { request_id: disperse_blob_reply.request_id, }; - loop { - tokio::time::sleep(self.polling_interval).await; - let resp = client + let blob_info = (|| async { + let mut client_clone = client.clone(); + let resp = client_clone .get_blob_status(polling_request.clone()) .await? .into_inner(); match disperser::BlobStatus::try_from(resp.status)? { - disperser::BlobStatus::Processing | disperser::BlobStatus::Dispersing => {} - disperser::BlobStatus::Failed => { - return Err(anyhow::anyhow!("Blob dispatch failed")) + disperser::BlobStatus::Processing | disperser::BlobStatus::Dispersing => { + Err(anyhow::anyhow!("Blob is still processing")) } + disperser::BlobStatus::Failed => Err(anyhow::anyhow!("Blob dispatch failed")), disperser::BlobStatus::InsufficientSignatures => { - return Err(anyhow::anyhow!("Insufficient signatures")) + Err(anyhow::anyhow!("Insufficient signatures")) + } + disperser::BlobStatus::Confirmed => { + if !self.config.wait_for_finalization { + let blob_info = resp + .info + .ok_or_else(|| anyhow::anyhow!("No blob header in response"))?; + return Ok(blob_info); + } + Err(anyhow::anyhow!("Blob is still processing")) } - disperser::BlobStatus::Confirmed | disperser::BlobStatus::Finalized => { - let verification_proof = resp + disperser::BlobStatus::Finalized => { + let blob_info = resp .info - .ok_or_else(|| anyhow::anyhow!("No blob header in response"))? - .blob_verification_proof - .ok_or_else(|| anyhow::anyhow!("No blob verification proof in response"))?; - - return Ok(verification_proof); + .ok_or_else(|| anyhow::anyhow!("No blob header in response"))?; + Ok(blob_info) } - _ => return Err(anyhow::anyhow!("Received unknown blob status")), + _ => Err(anyhow::anyhow!("Received unknown blob status")), } + }) + .retry( + &ConstantBuilder::default() + .with_delay(Duration::from_millis(self.config.status_query_interval)) + .with_max_times( + (self.config.status_query_timeout / self.config.status_query_interval) as usize, + ), + ) + .when(|e| e.to_string().contains("Blob is still processing")) + .await?; + + Ok(blob_info) + } + + #[cfg(test)] + pub async fn get_blob_data(&self, blob_id: &str) -> anyhow::Result>, DAError> { + use anyhow::anyhow; + use zksync_da_client::types::DAError; + + use crate::eigen::blob_info::BlobInfo; + + let commit = hex::decode(blob_id).map_err(|_| DAError { + error: anyhow!("Failed to decode blob_id"), + is_retriable: false, + })?; + let blob_info: BlobInfo = rlp::decode(&commit).map_err(|_| DAError { + error: anyhow!("Failed to decode blob_info"), + is_retriable: false, + })?; + let blob_index = blob_info.blob_verification_proof.blob_index; + let batch_header_hash = blob_info + .blob_verification_proof + .batch_medatada + .batch_header_hash; + let get_response = self + .client + .clone() + .retrieve_blob(disperser::RetrieveBlobRequest { + batch_header_hash, + blob_index, + }) + .await + .map_err(|e| DAError { + error: anyhow!(e), + is_retriable: true, + })? + .into_inner(); + + if get_response.data.is_empty() { + return Err(DAError { + error: anyhow!("Failed to get blob data"), + is_retriable: false, + }); } + + let data = remove_empty_byte_from_padded_bytes(&get_response.data); + Ok(Some(data)) } } @@ -244,3 +403,56 @@ fn convert_by_padding_empty_byte(data: &[u8]) -> Vec { valid_data.truncate(valid_end); valid_data } + +#[cfg(test)] +fn remove_empty_byte_from_padded_bytes(data: &[u8]) -> Vec { + let parse_size = DATA_CHUNK_SIZE; + + // Calculate the number of chunks + let data_len = (data.len() + parse_size - 1) / parse_size; + + // Pre-allocate `valid_data` with enough space for all chunks + let mut valid_data = vec![0u8; data_len * (DATA_CHUNK_SIZE - 1)]; + let mut valid_end = data_len * (DATA_CHUNK_SIZE - 1); + + for (i, chunk) in data.chunks(parse_size).enumerate() { + let offset = i * (DATA_CHUNK_SIZE - 1); + + let copy_end = offset + chunk.len() - 1; + valid_data[offset..copy_end].copy_from_slice(&chunk[1..]); + + if i == data_len - 1 && chunk.len() < parse_size { + valid_end = offset + chunk.len() - 1; + } + } + + valid_data.truncate(valid_end); + valid_data +} + +#[cfg(test)] +mod test { + #[test] + fn test_pad_and_unpad() { + let data = vec![1, 2, 3, 4, 5, 6, 7, 8, 9]; + let padded_data = super::convert_by_padding_empty_byte(&data); + let unpadded_data = super::remove_empty_byte_from_padded_bytes(&padded_data); + assert_eq!(data, unpadded_data); + } + + #[test] + fn test_pad_and_unpad_large() { + let data = vec![1; 1000]; + let padded_data = super::convert_by_padding_empty_byte(&data); + let unpadded_data = super::remove_empty_byte_from_padded_bytes(&padded_data); + assert_eq!(data, unpadded_data); + } + + #[test] + fn test_pad_and_unpad_empty() { + let data = Vec::new(); + let padded_data = super::convert_by_padding_empty_byte(&data); + let unpadded_data = super::remove_empty_byte_from_padded_bytes(&padded_data); + assert_eq!(data, unpadded_data); + } +} diff --git a/core/node/da_clients/src/eigen/verifier.rs b/core/node/da_clients/src/eigen/verifier.rs new file mode 100644 index 000000000000..1dbe80a6aaec --- /dev/null +++ b/core/node/da_clients/src/eigen/verifier.rs @@ -0,0 +1,863 @@ +use std::{collections::HashMap, str::FromStr}; + +use ark_bn254::{Fq, G1Affine}; +use ethabi::{encode, Token}; +use rust_kzg_bn254::{blob::Blob, kzg::Kzg, polynomial::PolynomialFormat}; +use tiny_keccak::{Hasher, Keccak}; +use zksync_basic_types::web3::CallRequest; +use zksync_eth_client::clients::PKSigningClient; +use zksync_types::{ + url::SensitiveUrl, + web3::{BlockId, BlockNumber}, + K256PrivateKey, SLChainId, H160, U256, +}; +use zksync_web3_decl::client::{Client, DynClient, L1}; + +use super::{ + blob_info::{BatchHeader, BlobHeader, BlobInfo, G1Commitment}, + lib::{ + BATCH_ID_TO_METADATA_HASH_FUNCTION_SELECTOR, + QUORUM_ADVERSARY_THRESHOLD_PERCENTAGES_FUNCTION_SELECTOR, + QUORUM_NUMBERS_REQUIRED_FUNCTION_SELECTOR, + }, +}; + +#[derive(Debug)] +pub enum VerificationError { + ServiceManagerError, + WrongUrl, + KzgError, + WrongProof, + DifferentCommitments, + DifferentRoots, + EmptyHash, + DifferentHashes, + WrongQuorumParams, + QuorumNotConfirmed, + CommitmentNotOnCurve, + CommitmentNotOnCorrectSubgroup, +} + +/// Configuration for the verifier used for authenticated dispersals +#[derive(Debug, Clone)] +pub struct VerifierConfig { + pub verify_certs: bool, + pub rpc_url: String, + pub svc_manager_addr: String, + pub max_blob_size: u32, + pub path_to_points: String, + pub settlement_layer_confirmation_depth: u32, + pub private_key: String, + pub chain_id: u64, +} + +/// Verifier used to verify the integrity of the blob info +/// Kzg is used for commitment verification +/// EigenDA service manager is used to connect to the service manager contract +#[derive(Debug, Clone)] +pub struct Verifier { + kzg: Kzg, + cfg: VerifierConfig, + signing_client: PKSigningClient, +} + +impl Verifier { + const DEFAULT_PRIORITY_FEE_PER_GAS: u64 = 100; + pub fn new(cfg: VerifierConfig) -> Result { + let srs_points_to_load = cfg.max_blob_size / 32; + let kzg = Kzg::setup( + &format!("{}{}", cfg.path_to_points, "/g1.point"), + "", + &format!("{}{}", cfg.path_to_points, "/g2.point.powerOf2"), + 268435456, // 2 ^ 28 + srs_points_to_load, + "".to_string(), + ); + let kzg = kzg.map_err(|e| { + tracing::error!("Failed to setup KZG: {:?}", e); + VerificationError::KzgError + })?; + + let url = SensitiveUrl::from_str(&cfg.rpc_url).map_err(|_| VerificationError::WrongUrl)?; + let client: Client = Client::http(url) + .map_err(|_| VerificationError::WrongUrl)? + .build(); + let client = Box::new(client) as Box>; + let signing_client = PKSigningClient::new_raw( + K256PrivateKey::from_bytes( + zksync_types::H256::from_str(&cfg.private_key) + .map_err(|_| VerificationError::ServiceManagerError)?, + ) + .map_err(|_| VerificationError::ServiceManagerError)?, + H160::from_str(&cfg.svc_manager_addr) + .map_err(|_| VerificationError::ServiceManagerError)?, + Self::DEFAULT_PRIORITY_FEE_PER_GAS, + SLChainId(cfg.chain_id), + client, + ); + + Ok(Self { + kzg, + cfg, + signing_client, + }) + } + + /// Return the commitment from a blob + fn commit(&self, blob: Vec) -> Result { + let blob = Blob::from_bytes_and_pad(&blob.to_vec()); + self.kzg + .blob_to_kzg_commitment(&blob, PolynomialFormat::InEvaluationForm) + .map_err(|_| VerificationError::KzgError) + } + + /// Compare the given commitment with the commitment generated with the blob + pub fn verify_commitment( + &self, + expected_commitment: G1Commitment, + blob: Vec, + ) -> Result<(), VerificationError> { + let actual_commitment = self.commit(blob)?; + let expected_commitment = G1Affine::new_unchecked( + Fq::from(num_bigint::BigUint::from_bytes_be(&expected_commitment.x)), + Fq::from(num_bigint::BigUint::from_bytes_be(&expected_commitment.y)), + ); + if !expected_commitment.is_on_curve() { + return Err(VerificationError::CommitmentNotOnCurve); + } + if !expected_commitment.is_in_correct_subgroup_assuming_on_curve() { + return Err(VerificationError::CommitmentNotOnCorrectSubgroup); + } + if actual_commitment != expected_commitment { + return Err(VerificationError::DifferentCommitments); + } + Ok(()) + } + + fn hash_encode_blob_header(&self, blob_header: BlobHeader) -> Vec { + let mut blob_quorums = vec![]; + for quorum in blob_header.blob_quorum_params { + let quorum = Token::Tuple(vec![ + Token::Uint(ethabi::Uint::from(quorum.quorum_number)), + Token::Uint(ethabi::Uint::from(quorum.adversary_threshold_percentage)), + Token::Uint(ethabi::Uint::from(quorum.confirmation_threshold_percentage)), + Token::Uint(ethabi::Uint::from(quorum.chunk_length)), + ]); + blob_quorums.push(quorum); + } + let blob_header = Token::Tuple(vec![ + Token::Tuple(vec![ + Token::Uint(ethabi::Uint::from_big_endian(&blob_header.commitment.x)), + Token::Uint(ethabi::Uint::from_big_endian(&blob_header.commitment.y)), + ]), + Token::Uint(ethabi::Uint::from(blob_header.data_length)), + Token::Array(blob_quorums), + ]); + + let encoded = encode(&[blob_header]); + + let mut keccak = Keccak::v256(); + keccak.update(&encoded); + let mut hash = [0u8; 32]; + keccak.finalize(&mut hash); + hash.to_vec() + } + + fn process_inclusion_proof( + &self, + proof: &[u8], + leaf: &[u8], + index: u32, + ) -> Result, VerificationError> { + let mut index = index; + if proof.is_empty() || proof.len() % 32 != 0 { + return Err(VerificationError::WrongProof); + } + let mut computed_hash = leaf.to_vec(); + for i in 0..proof.len() / 32 { + let mut combined = proof[i * 32..(i + 1) * 32] + .iter() + .chain(computed_hash.iter()) + .cloned() + .collect::>(); + if index % 2 == 0 { + combined = computed_hash + .iter() + .chain(proof[i * 32..(i + 1) * 32].iter()) + .cloned() + .collect::>(); + }; + let mut keccak = Keccak::v256(); + keccak.update(&combined); + let mut hash = [0u8; 32]; + keccak.finalize(&mut hash); + computed_hash = hash.to_vec(); + index /= 2; + } + + Ok(computed_hash) + } + + /// Verifies the certificate's batch root + fn verify_merkle_proof(&self, cert: BlobInfo) -> Result<(), VerificationError> { + let inclusion_proof = cert.blob_verification_proof.inclusion_proof; + let root = cert + .blob_verification_proof + .batch_medatada + .batch_header + .batch_root; + let blob_index = cert.blob_verification_proof.blob_index; + let blob_header = cert.blob_header; + + let blob_header_hash = self.hash_encode_blob_header(blob_header); + let mut keccak = Keccak::v256(); + keccak.update(&blob_header_hash); + let mut leaf_hash = [0u8; 32]; + keccak.finalize(&mut leaf_hash); + + let generated_root = + self.process_inclusion_proof(&inclusion_proof, &leaf_hash, blob_index)?; + + if generated_root != root { + return Err(VerificationError::DifferentRoots); + } + Ok(()) + } + + fn hash_batch_metadata( + &self, + batch_header: BatchHeader, + signatory_record_hash: Vec, + confirmation_block_number: u32, + ) -> Vec { + let batch_header_token = Token::Tuple(vec![ + Token::FixedBytes(batch_header.batch_root), + Token::Bytes(batch_header.quorum_numbers), + Token::Bytes(batch_header.quorum_signed_percentages), + Token::Uint(ethabi::Uint::from(batch_header.reference_block_number)), + ]); + + let encoded = encode(&[batch_header_token]); + + let mut keccak = Keccak::v256(); + keccak.update(&encoded); + let mut header_hash = [0u8; 32]; + keccak.finalize(&mut header_hash); + + let hash_token = Token::Tuple(vec![ + Token::FixedBytes(header_hash.to_vec()), + Token::FixedBytes(signatory_record_hash), + ]); + + let mut hash_encoded = encode(&[hash_token]); + + hash_encoded.append(&mut confirmation_block_number.to_be_bytes().to_vec()); + + let mut keccak = Keccak::v256(); + keccak.update(&hash_encoded); + let mut hash = [0u8; 32]; + keccak.finalize(&mut hash); + + hash.to_vec() + } + + /// Retrieves the block to make the request to the service manager + async fn get_context_block(&self) -> Result { + let latest = self + .signing_client + .as_ref() + .block_number() + .await + .map_err(|_| VerificationError::ServiceManagerError)? + .as_u64(); + + if self.cfg.settlement_layer_confirmation_depth == 0 { + return Ok(latest); + } + Ok(latest - (self.cfg.settlement_layer_confirmation_depth as u64 - 1)) + } + + /// Verifies the certificate batch hash + async fn verify_batch(&self, cert: BlobInfo) -> Result<(), VerificationError> { + let context_block = self.get_context_block().await?; + + let mut data = BATCH_ID_TO_METADATA_HASH_FUNCTION_SELECTOR.to_vec(); + let mut batch_id_vec = [0u8; 32]; + U256::from(cert.blob_verification_proof.batch_id).to_big_endian(&mut batch_id_vec); + data.append(batch_id_vec.to_vec().as_mut()); + + let call_request = CallRequest { + to: Some( + H160::from_str(&self.cfg.svc_manager_addr) + .map_err(|_| VerificationError::ServiceManagerError)?, + ), + data: Some(zksync_basic_types::web3::Bytes(data)), + ..Default::default() + }; + + let res = self + .signing_client + .as_ref() + .call_contract_function( + call_request, + Some(BlockId::Number(BlockNumber::Number(context_block.into()))), + ) + .await + .map_err(|_| VerificationError::ServiceManagerError)?; + + let expected_hash = res.0.to_vec(); + + if expected_hash == vec![0u8; 32] { + return Err(VerificationError::EmptyHash); + } + + let actual_hash = self.hash_batch_metadata( + cert.blob_verification_proof.batch_medatada.batch_header, + cert.blob_verification_proof + .batch_medatada + .signatory_record_hash, + cert.blob_verification_proof + .batch_medatada + .confirmation_block_number, + ); + + if expected_hash != actual_hash { + return Err(VerificationError::DifferentHashes); + } + Ok(()) + } + + fn decode_bytes(&self, encoded: Vec) -> Result, String> { + // Ensure the input has at least 64 bytes (offset + length) + if encoded.len() < 64 { + return Err("Encoded data is too short".to_string()); + } + + // Read the offset (first 32 bytes) + let offset = { + let mut offset_bytes = [0u8; 32]; + offset_bytes.copy_from_slice(&encoded[0..32]); + usize::from_be_bytes( + offset_bytes[24..32] + .try_into() + .map_err(|_| "Offset is too large")?, + ) + }; + + // Check if offset is valid + if offset + 32 > encoded.len() { + return Err("Offset points outside the encoded data".to_string()); + } + + // Read the length (32 bytes at the offset position) + let length = { + let mut length_bytes = [0u8; 32]; + length_bytes.copy_from_slice(&encoded[offset..offset + 32]); + usize::from_be_bytes( + length_bytes[24..32] + .try_into() + .map_err(|_| "Offset is too large")?, + ) + }; + + // Check if the length is valid + if offset + 32 + length > encoded.len() { + return Err("Length extends beyond the encoded data".to_string()); + } + + // Extract the bytes data + let data = encoded[offset + 32..offset + 32 + length].to_vec(); + Ok(data) + } + + async fn get_quorum_adversary_threshold( + &self, + quorum_number: u32, + ) -> Result { + let data = QUORUM_ADVERSARY_THRESHOLD_PERCENTAGES_FUNCTION_SELECTOR.to_vec(); + + let call_request = CallRequest { + to: Some( + H160::from_str(&self.cfg.svc_manager_addr) + .map_err(|_| VerificationError::ServiceManagerError)?, + ), + data: Some(zksync_basic_types::web3::Bytes(data)), + ..Default::default() + }; + + let res = self + .signing_client + .as_ref() + .call_contract_function(call_request, None) + .await + .map_err(|_| VerificationError::ServiceManagerError)?; + + let percentages = self + .decode_bytes(res.0.to_vec()) + .map_err(|_| VerificationError::ServiceManagerError)?; + + if percentages.len() > quorum_number as usize { + return Ok(percentages[quorum_number as usize]); + } + Ok(0) + } + + /// Verifies that the certificate's blob quorum params are correct + async fn verify_security_params(&self, cert: BlobInfo) -> Result<(), VerificationError> { + let blob_header = cert.blob_header; + let batch_header = cert.blob_verification_proof.batch_medatada.batch_header; + + let mut confirmed_quorums: HashMap = HashMap::new(); + for i in 0..blob_header.blob_quorum_params.len() { + if batch_header.quorum_numbers[i] as u32 + != blob_header.blob_quorum_params[i].quorum_number + { + return Err(VerificationError::WrongQuorumParams); + } + if blob_header.blob_quorum_params[i].adversary_threshold_percentage + > blob_header.blob_quorum_params[i].confirmation_threshold_percentage + { + return Err(VerificationError::WrongQuorumParams); + } + let quorum_adversary_threshold = self + .get_quorum_adversary_threshold(blob_header.blob_quorum_params[i].quorum_number) + .await?; + + if quorum_adversary_threshold > 0 + && blob_header.blob_quorum_params[i].adversary_threshold_percentage + < quorum_adversary_threshold as u32 + { + return Err(VerificationError::WrongQuorumParams); + } + + if (batch_header.quorum_signed_percentages[i] as u32) + < blob_header.blob_quorum_params[i].confirmation_threshold_percentage + { + return Err(VerificationError::WrongQuorumParams); + } + + confirmed_quorums.insert(blob_header.blob_quorum_params[i].quorum_number, true); + } + + let data = QUORUM_NUMBERS_REQUIRED_FUNCTION_SELECTOR.to_vec(); + let call_request = CallRequest { + to: Some( + H160::from_str(&self.cfg.svc_manager_addr) + .map_err(|_| VerificationError::ServiceManagerError)?, + ), + data: Some(zksync_basic_types::web3::Bytes(data)), + ..Default::default() + }; + + let res = self + .signing_client + .as_ref() + .call_contract_function(call_request, None) + .await + .map_err(|_| VerificationError::ServiceManagerError)?; + + let required_quorums = self + .decode_bytes(res.0.to_vec()) + .map_err(|_| VerificationError::ServiceManagerError)?; + + for quorum in required_quorums { + if !confirmed_quorums.contains_key(&(quorum as u32)) { + return Err(VerificationError::QuorumNotConfirmed); + } + } + Ok(()) + } + + /// Verifies that the certificate is valid + pub async fn verify_certificate(&self, cert: BlobInfo) -> Result<(), VerificationError> { + if !self.cfg.verify_certs { + return Ok(()); + } + self.verify_batch(cert.clone()).await?; + self.verify_merkle_proof(cert.clone())?; + self.verify_security_params(cert.clone()).await?; + Ok(()) + } +} + +#[cfg(test)] +mod test { + use crate::eigen::blob_info::{ + BatchHeader, BatchMetadata, BlobHeader, BlobInfo, BlobQuorumParam, BlobVerificationProof, + G1Commitment, + }; + + #[test] + fn test_verify_commitment() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + let commitment = G1Commitment { + x: vec![ + 22, 11, 176, 29, 82, 48, 62, 49, 51, 119, 94, 17, 156, 142, 248, 96, 240, 183, 134, + 85, 152, 5, 74, 27, 175, 83, 162, 148, 17, 110, 201, 74, + ], + y: vec![ + 12, 132, 236, 56, 147, 6, 176, 135, 244, 166, 21, 18, 87, 76, 122, 3, 23, 22, 254, + 236, 148, 129, 110, 207, 131, 116, 58, 170, 4, 130, 191, 157, + ], + }; + let blob = vec![1u8; 100]; // Actual blob sent was this blob but kzg-padded, but Blob::from_bytes_and_pad padds it inside, so we don't need to pad it here. + let result = verifier.verify_commitment(commitment, blob); + assert!(result.is_ok()); + } + + #[test] + fn test_verify_merkle_proof() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + let cert = BlobInfo { + blob_header: BlobHeader { + commitment: G1Commitment { + x: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + y: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + }, + data_length: 4, + blob_quorum_params: vec![ + BlobQuorumParam { + quorum_number: 0, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + BlobQuorumParam { + quorum_number: 1, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + ], + }, + blob_verification_proof: BlobVerificationProof { + batch_id: 66507, + blob_index: 92, + batch_medatada: BatchMetadata { + batch_header: BatchHeader { + batch_root: vec![ + 179, 187, 53, 98, 192, 80, 151, 28, 125, 192, 115, 29, 129, 238, 216, + 8, 213, 210, 203, 143, 181, 19, 146, 113, 98, 131, 39, 238, 149, 248, + 211, 43, + ], + quorum_numbers: vec![0, 1], + quorum_signed_percentages: vec![100, 100], + reference_block_number: 2624794, + }, + signatory_record_hash: vec![ + 172, 32, 172, 142, 197, 52, 84, 143, 120, 26, 190, 9, 143, 217, 62, 19, 17, + 107, 105, 67, 203, 5, 172, 249, 6, 60, 105, 240, 134, 34, 66, 133, + ], + fee: vec![0], + confirmation_block_number: 2624876, + batch_header_hash: vec![ + 122, 115, 2, 85, 233, 75, 121, 85, 51, 81, 248, 170, 198, 252, 42, 16, 1, + 146, 96, 218, 159, 44, 41, 40, 94, 247, 147, 11, 255, 68, 40, 177, + ], + }, + inclusion_proof: vec![ + 203, 160, 237, 48, 117, 255, 75, 254, 117, 144, 164, 77, 29, 146, 36, 48, 190, + 140, 50, 100, 144, 237, 125, 125, 75, 54, 210, 247, 147, 23, 48, 189, 120, 4, + 125, 123, 195, 244, 207, 239, 145, 109, 0, 21, 11, 162, 109, 79, 192, 100, 138, + 157, 203, 22, 17, 114, 234, 72, 174, 231, 209, 133, 99, 118, 201, 160, 137, + 128, 112, 84, 34, 136, 174, 139, 96, 26, 246, 148, 134, 52, 200, 229, 160, 145, + 5, 120, 18, 187, 51, 11, 109, 91, 237, 171, 215, 207, 90, 95, 146, 54, 135, + 166, 66, 157, 255, 237, 69, 183, 141, 45, 162, 145, 71, 16, 87, 184, 120, 84, + 156, 220, 159, 4, 99, 48, 191, 203, 136, 112, 127, 226, 192, 184, 110, 6, 177, + 182, 109, 207, 197, 239, 161, 132, 17, 89, 56, 137, 205, 202, 101, 97, 60, 162, + 253, 23, 169, 75, 236, 211, 126, 121, 132, 191, 68, 167, 200, 16, 154, 149, + 202, 197, 7, 191, 26, 8, 67, 3, 37, 137, 16, 153, 30, 209, 238, 53, 233, 148, + 198, 253, 94, 216, 73, 25, 190, 205, 132, 208, 255, 219, 170, 98, 17, 160, 179, + 183, 200, 17, 99, 36, 130, 216, 223, 72, 222, 250, 73, 78, 79, 72, 253, 105, + 245, 84, 244, 196, + ], + quorum_indexes: vec![0, 1], + }, + }; + let result = verifier.verify_merkle_proof(cert); + assert!(result.is_ok()); + } + + #[test] + fn test_hash_blob_header() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + let blob_header = BlobHeader { + commitment: G1Commitment { + x: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, + ], + y: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 1, + ], + }, + data_length: 2, + blob_quorum_params: vec![ + BlobQuorumParam { + quorum_number: 2, + adversary_threshold_percentage: 4, + confirmation_threshold_percentage: 5, + chunk_length: 6, + }, + BlobQuorumParam { + quorum_number: 2, + adversary_threshold_percentage: 4, + confirmation_threshold_percentage: 5, + chunk_length: 6, + }, + ], + }; + let result = verifier.hash_encode_blob_header(blob_header); + let expected = "ba4675a31c9bf6b2f7abfdcedd34b74645cb7332b35db39bff00ae8516a67393"; + assert_eq!(result, hex::decode(expected).unwrap()); + } + + #[test] + fn test_inclusion_proof() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + + let proof = hex::decode("c455c1ea0e725d7ea3e5f29e9f48be8fc2787bb0a914d5a86710ba302c166ac4f626d76f67f1055bb960a514fb8923af2078fd84085d712655b58a19612e8cd15c3e4ac1cef57acde3438dbcf63f47c9fefe1221344c4d5c1a4943dd0d1803091ca81a270909dc0e146841441c9bd0e08e69ce6168181a3e4060ffacf3627480bec6abdd8d7bb92b49d33f180c42f49e041752aaded9c403db3a17b85e48a11e9ea9a08763f7f383dab6d25236f1b77c12b4c49c5cdbcbea32554a604e3f1d2f466851cb43fe73617b3d01e665e4c019bf930f92dea7394c25ed6a1e200d051fb0c30a2193c459f1cfef00bf1ba6656510d16725a4d1dc031cb759dbc90bab427b0f60ddc6764681924dda848824605a4f08b7f526fe6bd4572458c94e83fbf2150f2eeb28d3011ec921996dc3e69efa52d5fcf3182b20b56b5857a926aa66605808079b4d52c0c0cfe06923fa92e65eeca2c3e6126108e8c1babf5ac522f4d7").unwrap(); + let leaf = hex::decode("f6106e6ae4631e68abe0fa898cedbe97dbae6c7efb1b088c5aa2e8b91190ff96") + .unwrap(); + let expected_root = + hex::decode("7390b8023db8248123dcaeca57fa6c9340bef639e204f2278fc7ec3d46ad071b") + .unwrap(); + + let actual_root = verifier + .process_inclusion_proof(&proof, &leaf, 580) + .unwrap(); + + assert_eq!(actual_root, expected_root); + } + + #[tokio::test] + async fn test_verify_batch() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + let cert = BlobInfo { + blob_header: BlobHeader { + commitment: G1Commitment { + x: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + y: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + }, + data_length: 4, + blob_quorum_params: vec![ + BlobQuorumParam { + quorum_number: 0, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + BlobQuorumParam { + quorum_number: 1, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + ], + }, + blob_verification_proof: BlobVerificationProof { + batch_id: 66507, + blob_index: 92, + batch_medatada: BatchMetadata { + batch_header: BatchHeader { + batch_root: vec![ + 179, 187, 53, 98, 192, 80, 151, 28, 125, 192, 115, 29, 129, 238, 216, + 8, 213, 210, 203, 143, 181, 19, 146, 113, 98, 131, 39, 238, 149, 248, + 211, 43, + ], + quorum_numbers: vec![0, 1], + quorum_signed_percentages: vec![100, 100], + reference_block_number: 2624794, + }, + signatory_record_hash: vec![ + 172, 32, 172, 142, 197, 52, 84, 143, 120, 26, 190, 9, 143, 217, 62, 19, 17, + 107, 105, 67, 203, 5, 172, 249, 6, 60, 105, 240, 134, 34, 66, 133, + ], + fee: vec![0], + confirmation_block_number: 2624876, + batch_header_hash: vec![ + 122, 115, 2, 85, 233, 75, 121, 85, 51, 81, 248, 170, 198, 252, 42, 16, 1, + 146, 96, 218, 159, 44, 41, 40, 94, 247, 147, 11, 255, 68, 40, 177, + ], + }, + inclusion_proof: vec![ + 203, 160, 237, 48, 117, 255, 75, 254, 117, 144, 164, 77, 29, 146, 36, 48, 190, + 140, 50, 100, 144, 237, 125, 125, 75, 54, 210, 247, 147, 23, 48, 189, 120, 4, + 125, 123, 195, 244, 207, 239, 145, 109, 0, 21, 11, 162, 109, 79, 192, 100, 138, + 157, 203, 22, 17, 114, 234, 72, 174, 231, 209, 133, 99, 118, 201, 160, 137, + 128, 112, 84, 34, 136, 174, 139, 96, 26, 246, 148, 134, 52, 200, 229, 160, 145, + 5, 120, 18, 187, 51, 11, 109, 91, 237, 171, 215, 207, 90, 95, 146, 54, 135, + 166, 66, 157, 255, 237, 69, 183, 141, 45, 162, 145, 71, 16, 87, 184, 120, 84, + 156, 220, 159, 4, 99, 48, 191, 203, 136, 112, 127, 226, 192, 184, 110, 6, 177, + 182, 109, 207, 197, 239, 161, 132, 17, 89, 56, 137, 205, 202, 101, 97, 60, 162, + 253, 23, 169, 75, 236, 211, 126, 121, 132, 191, 68, 167, 200, 16, 154, 149, + 202, 197, 7, 191, 26, 8, 67, 3, 37, 137, 16, 153, 30, 209, 238, 53, 233, 148, + 198, 253, 94, 216, 73, 25, 190, 205, 132, 208, 255, 219, 170, 98, 17, 160, 179, + 183, 200, 17, 99, 36, 130, 216, 223, 72, 222, 250, 73, 78, 79, 72, 253, 105, + 245, 84, 244, 196, + ], + quorum_indexes: vec![0, 1], + }, + }; + let result = verifier.verify_batch(cert).await; + assert!(result.is_ok()); + } + + #[tokio::test] + async fn test_verify_security_params() { + let verifier = super::Verifier::new(super::VerifierConfig { + verify_certs: true, + rpc_url: "https://ethereum-holesky-rpc.publicnode.com".to_string(), + svc_manager_addr: "0xD4A7E1Bd8015057293f0D0A557088c286942e84b".to_string(), + max_blob_size: 2 * 1024 * 1024, + path_to_points: "../../../resources".to_string(), + settlement_layer_confirmation_depth: 0, + private_key: "0xd08aa7ae1bb5ddd46c3c2d8cdb5894ab9f54dec467233686ca42629e826ac4c6" + .to_string(), + chain_id: 17000, + }) + .unwrap(); + let cert = BlobInfo { + blob_header: BlobHeader { + commitment: G1Commitment { + x: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + y: vec![ + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, + ], + }, + data_length: 4, + blob_quorum_params: vec![ + BlobQuorumParam { + quorum_number: 0, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + BlobQuorumParam { + quorum_number: 1, + adversary_threshold_percentage: 33, + confirmation_threshold_percentage: 55, + chunk_length: 1, + }, + ], + }, + blob_verification_proof: BlobVerificationProof { + batch_id: 66507, + blob_index: 92, + batch_medatada: BatchMetadata { + batch_header: BatchHeader { + batch_root: vec![ + 179, 187, 53, 98, 192, 80, 151, 28, 125, 192, 115, 29, 129, 238, 216, + 8, 213, 210, 203, 143, 181, 19, 146, 113, 98, 131, 39, 238, 149, 248, + 211, 43, + ], + quorum_numbers: vec![0, 1], + quorum_signed_percentages: vec![100, 100], + reference_block_number: 2624794, + }, + signatory_record_hash: vec![ + 172, 32, 172, 142, 197, 52, 84, 143, 120, 26, 190, 9, 143, 217, 62, 19, 17, + 107, 105, 67, 203, 5, 172, 249, 6, 60, 105, 240, 134, 34, 66, 133, + ], + fee: vec![0], + confirmation_block_number: 2624876, + batch_header_hash: vec![ + 122, 115, 2, 85, 233, 75, 121, 85, 51, 81, 248, 170, 198, 252, 42, 16, 1, + 146, 96, 218, 159, 44, 41, 40, 94, 247, 147, 11, 255, 68, 40, 177, + ], + }, + inclusion_proof: vec![ + 203, 160, 237, 48, 117, 255, 75, 254, 117, 144, 164, 77, 29, 146, 36, 48, 190, + 140, 50, 100, 144, 237, 125, 125, 75, 54, 210, 247, 147, 23, 48, 189, 120, 4, + 125, 123, 195, 244, 207, 239, 145, 109, 0, 21, 11, 162, 109, 79, 192, 100, 138, + 157, 203, 22, 17, 114, 234, 72, 174, 231, 209, 133, 99, 118, 201, 160, 137, + 128, 112, 84, 34, 136, 174, 139, 96, 26, 246, 148, 134, 52, 200, 229, 160, 145, + 5, 120, 18, 187, 51, 11, 109, 91, 237, 171, 215, 207, 90, 95, 146, 54, 135, + 166, 66, 157, 255, 237, 69, 183, 141, 45, 162, 145, 71, 16, 87, 184, 120, 84, + 156, 220, 159, 4, 99, 48, 191, 203, 136, 112, 127, 226, 192, 184, 110, 6, 177, + 182, 109, 207, 197, 239, 161, 132, 17, 89, 56, 137, 205, 202, 101, 97, 60, 162, + 253, 23, 169, 75, 236, 211, 126, 121, 132, 191, 68, 167, 200, 16, 154, 149, + 202, 197, 7, 191, 26, 8, 67, 3, 37, 137, 16, 153, 30, 209, 238, 53, 233, 148, + 198, 253, 94, 216, 73, 25, 190, 205, 132, 208, 255, 219, 170, 98, 17, 160, 179, + 183, 200, 17, 99, 36, 130, 216, 223, 72, 222, 250, 73, 78, 79, 72, 253, 105, + 245, 84, 244, 196, + ], + quorum_indexes: vec![0, 1], + }, + }; + let result = verifier.verify_security_params(cert).await; + assert!(result.is_ok()); + } +} diff --git a/resources/g1.point b/resources/g1.point new file mode 100644 index 000000000000..7e9cf49e5227 Binary files /dev/null and b/resources/g1.point differ diff --git a/resources/g2.point.powerOf2 b/resources/g2.point.powerOf2 new file mode 100644 index 000000000000..58e349b6e7d5 Binary files /dev/null and b/resources/g2.point.powerOf2 differ