diff --git a/.gitignore b/.gitignore index 86ed40c70417..adf3b7799618 100644 --- a/.gitignore +++ b/.gitignore @@ -115,6 +115,7 @@ prover/data/keys/setup_* # ZK Stack CLI chains/era/configs/* chains/gateway/* +chains/avail/* configs/* era-observability/ core/tests/ts-integration/deployments-zk diff --git a/Cargo.lock b/Cargo.lock index 7e4cad34cf8f..a42ef8e3fdcc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -10028,14 +10028,17 @@ version = "0.1.0" dependencies = [ "anyhow", "async-trait", + "backon", "base58", "blake2 0.10.6", "blake2b_simd", + "bytes", "flate2", "futures 0.3.30", "hex", "jsonrpsee 0.23.2", "parity-scale-codec", + "reqwest 0.12.7", "scale-encode", "serde", "serde_json", diff --git a/Cargo.toml b/Cargo.toml index f1e70e7f3028..0f8e6ba77ae6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -110,6 +110,7 @@ backon = "0.4.4" bigdecimal = "0.4.5" bincode = "1" blake2 = "0.10" +bytes = "1" chrono = "0.4" clap = "4.2.2" codegen = "0.2.0" @@ -155,7 +156,7 @@ rayon = "1.3.1" regex = "1" reqwest = "0.12" rlp = "0.5" -rocksdb = "0.21.0" +rocksdb = "0.21" rustc_version = "0.4.0" rustls = "0.23" secp256k1 = { version = "0.27.0", features = ["recovery", "global-context"] } diff --git a/core/lib/basic_types/src/api_key.rs b/core/lib/basic_types/src/api_key.rs new file mode 100644 index 000000000000..eadf4e9051b5 --- /dev/null +++ b/core/lib/basic_types/src/api_key.rs @@ -0,0 +1,20 @@ +use std::str::FromStr; + +use secrecy::{ExposeSecret, Secret}; + +#[derive(Debug, Clone)] +pub struct APIKey(pub Secret); + +impl PartialEq for APIKey { + fn eq(&self, other: &Self) -> bool { + self.0.expose_secret().eq(other.0.expose_secret()) + } +} + +impl FromStr for APIKey { + type Err = anyhow::Error; + + fn from_str(s: &str) -> Result { + Ok(APIKey(s.parse()?)) + } +} diff --git a/core/lib/basic_types/src/lib.rs b/core/lib/basic_types/src/lib.rs index 79c7b3924e34..7953f362fd42 100644 --- a/core/lib/basic_types/src/lib.rs +++ b/core/lib/basic_types/src/lib.rs @@ -24,6 +24,7 @@ use serde::{de, Deserialize, Deserializer, Serialize}; #[macro_use] mod macros; +pub mod api_key; pub mod basic_fri_types; pub mod commitment; pub mod network; diff --git a/core/lib/config/src/configs/da_client/avail.rs b/core/lib/config/src/configs/da_client/avail.rs index 590dc5fef18a..b8e9db0f3937 100644 --- a/core/lib/config/src/configs/da_client/avail.rs +++ b/core/lib/config/src/configs/da_client/avail.rs @@ -1,16 +1,38 @@ use serde::Deserialize; -use zksync_basic_types::seed_phrase::SeedPhrase; +use zksync_basic_types::{api_key::APIKey, seed_phrase::SeedPhrase}; + +pub const AVAIL_GAS_RELAY_CLIENT_NAME: &str = "GasRelay"; +pub const AVAIL_FULL_CLIENT_NAME: &str = "FullClient"; + +#[derive(Clone, Debug, PartialEq, Deserialize)] +#[serde(tag = "avail_client")] +pub enum AvailClientConfig { + FullClient(AvailDefaultConfig), + GasRelay(AvailGasRelayConfig), +} #[derive(Clone, Debug, PartialEq, Deserialize)] pub struct AvailConfig { - pub api_node_url: String, pub bridge_api_url: String, - pub app_id: u32, pub timeout: usize, + #[serde(flatten)] + pub config: AvailClientConfig, +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct AvailDefaultConfig { + pub api_node_url: String, + pub app_id: u32, +} + +#[derive(Clone, Debug, PartialEq, Deserialize)] +pub struct AvailGasRelayConfig { + pub gas_relay_api_url: String, pub max_retries: usize, } #[derive(Clone, Debug, PartialEq)] pub struct AvailSecrets { pub seed_phrase: Option, + pub gas_relay_api_key: Option, } diff --git a/core/lib/config/src/testonly.rs b/core/lib/config/src/testonly.rs index 9b1ec13e2d2e..880bc5aa98d2 100644 --- a/core/lib/config/src/testonly.rs +++ b/core/lib/config/src/testonly.rs @@ -3,6 +3,7 @@ use std::num::NonZeroUsize; use rand::{distributions::Distribution, Rng}; use secrecy::Secret; use zksync_basic_types::{ + api_key::APIKey, basic_fri_types::CircuitIdRoundTuple, commitment::L1BatchCommitmentMode, network::Network, @@ -17,7 +18,12 @@ use zksync_crypto_primitives::K256PrivateKey; use crate::{ configs::{ - self, da_client::DAClientConfig::Avail, external_price_api_client::ForcedPriceClientConfig, + self, + da_client::{ + avail::{AvailClientConfig, AvailDefaultConfig}, + DAClientConfig::Avail, + }, + external_price_api_client::ForcedPriceClientConfig, }, AvailConfig, }; @@ -935,11 +941,12 @@ impl Distribution for EncodeDist { impl Distribution for EncodeDist { fn sample(&self, rng: &mut R) -> configs::da_client::DAClientConfig { Avail(AvailConfig { - api_node_url: self.sample(rng), bridge_api_url: self.sample(rng), - app_id: self.sample(rng), timeout: self.sample(rng), - max_retries: self.sample(rng), + config: AvailClientConfig::FullClient(AvailDefaultConfig { + api_node_url: self.sample(rng), + app_id: self.sample(rng), + }), }) } } @@ -948,6 +955,7 @@ impl Distribution for EncodeDist { fn sample(&self, rng: &mut R) -> configs::secrets::DataAvailabilitySecrets { configs::secrets::DataAvailabilitySecrets::Avail(configs::da_client::avail::AvailSecrets { seed_phrase: Some(SeedPhrase(Secret::new(self.sample(rng)))), + gas_relay_api_key: Some(APIKey(Secret::new(self.sample(rng)))), }) } } diff --git a/core/lib/env_config/src/da_client.rs b/core/lib/env_config/src/da_client.rs index 0fc3ad216f87..1043786fc1eb 100644 --- a/core/lib/env_config/src/da_client.rs +++ b/core/lib/env_config/src/da_client.rs @@ -2,19 +2,34 @@ use std::env; use zksync_config::configs::{ da_client::{ - avail::AvailSecrets, DAClientConfig, AVAIL_CLIENT_CONFIG_NAME, - OBJECT_STORE_CLIENT_CONFIG_NAME, + avail::{ + AvailClientConfig, AvailSecrets, AVAIL_FULL_CLIENT_NAME, AVAIL_GAS_RELAY_CLIENT_NAME, + }, + DAClientConfig, AVAIL_CLIENT_CONFIG_NAME, OBJECT_STORE_CLIENT_CONFIG_NAME, }, secrets::DataAvailabilitySecrets, + AvailConfig, }; use crate::{envy_load, FromEnv}; impl FromEnv for DAClientConfig { fn from_env() -> anyhow::Result { - let client_tag = std::env::var("DA_CLIENT")?; + let client_tag = env::var("DA_CLIENT")?; let config = match client_tag.as_str() { - AVAIL_CLIENT_CONFIG_NAME => Self::Avail(envy_load("da_avail_config", "DA_")?), + AVAIL_CLIENT_CONFIG_NAME => Self::Avail(AvailConfig { + bridge_api_url: env::var("DA_BRIDGE_API_URL").ok().unwrap(), + timeout: env::var("DA_TIMEOUT")?.parse()?, + config: match env::var("DA_AVAIL_CLIENT_TYPE")?.as_str() { + AVAIL_FULL_CLIENT_NAME => { + AvailClientConfig::FullClient(envy_load("da_avail_full_client", "DA_")?) + } + AVAIL_GAS_RELAY_CLIENT_NAME => { + AvailClientConfig::GasRelay(envy_load("da_avail_gas_relay", "DA_")?) + } + _ => anyhow::bail!("Unknown Avail DA client type"), + }, + }), OBJECT_STORE_CLIENT_CONFIG_NAME => { Self::ObjectStore(envy_load("da_object_store", "DA_")?) } @@ -30,11 +45,21 @@ impl FromEnv for DataAvailabilitySecrets { let client_tag = std::env::var("DA_CLIENT")?; let secrets = match client_tag.as_str() { AVAIL_CLIENT_CONFIG_NAME => { - let seed_phrase = env::var("DA_SECRETS_SEED_PHRASE") - .ok() - .map(|s| s.parse()) - .transpose()?; - Self::Avail(AvailSecrets { seed_phrase }) + let seed_phrase: Option = + env::var("DA_SECRETS_SEED_PHRASE") + .ok() + .map(|s| s.parse().unwrap()); + let gas_relay_api_key: Option = + env::var("DA_SECRETS_GAS_RELAY_API_KEY") + .ok() + .map(|s| s.parse().unwrap()); + if seed_phrase.is_none() && gas_relay_api_key.is_none() { + anyhow::bail!("No secrets provided for Avail DA client"); + } + Self::Avail(AvailSecrets { + seed_phrase, + gas_relay_api_key, + }) } _ => anyhow::bail!("Unknown DA client name: {}", client_tag), }; @@ -47,7 +72,10 @@ impl FromEnv for DataAvailabilitySecrets { mod tests { use zksync_config::{ configs::{ - da_client::{DAClientConfig, DAClientConfig::ObjectStore}, + da_client::{ + avail::{AvailClientConfig, AvailDefaultConfig}, + DAClientConfig::{self, ObjectStore}, + }, object_store::ObjectStoreMode::GCS, }, AvailConfig, ObjectStoreConfig, @@ -91,14 +119,14 @@ mod tests { bridge_api_url: &str, app_id: u32, timeout: usize, - max_retries: usize, ) -> DAClientConfig { DAClientConfig::Avail(AvailConfig { - api_node_url: api_node_url.to_string(), bridge_api_url: bridge_api_url.to_string(), - app_id, timeout, - max_retries, + config: AvailClientConfig::FullClient(AvailDefaultConfig { + api_node_url: api_node_url.to_string(), + app_id, + }), }) } @@ -107,11 +135,13 @@ mod tests { let mut lock = MUTEX.lock(); let config = r#" DA_CLIENT="Avail" - DA_API_NODE_URL="localhost:12345" + DA_AVAIL_CLIENT_TYPE="FullClient" + DA_BRIDGE_API_URL="localhost:54321" - DA_APP_ID="1" DA_TIMEOUT="2" - DA_MAX_RETRIES="3" + + DA_API_NODE_URL="localhost:12345" + DA_APP_ID="1" "#; lock.set_env(config); @@ -124,7 +154,6 @@ mod tests { "localhost:54321", "1".parse::().unwrap(), "2".parse::().unwrap(), - "3".parse::().unwrap(), ) ); } @@ -139,15 +168,18 @@ mod tests { lock.set_env(config); - let actual = match DataAvailabilitySecrets::from_env().unwrap() { - DataAvailabilitySecrets::Avail(avail) => avail.seed_phrase, + let (actual_seed, actual_key) = match DataAvailabilitySecrets::from_env().unwrap() { + DataAvailabilitySecrets::Avail(avail) => (avail.seed_phrase, avail.gas_relay_api_key), }; assert_eq!( - actual.unwrap(), - "bottom drive obey lake curtain smoke basket hold race lonely fit walk" - .parse() - .unwrap() + (actual_seed.unwrap(), actual_key), + ( + "bottom drive obey lake curtain smoke basket hold race lonely fit walk" + .parse() + .unwrap(), + None + ) ); } } diff --git a/core/lib/protobuf_config/src/da_client.rs b/core/lib/protobuf_config/src/da_client.rs index 1499e88efb4c..a17a8711a27b 100644 --- a/core/lib/protobuf_config/src/da_client.rs +++ b/core/lib/protobuf_config/src/da_client.rs @@ -1,10 +1,10 @@ use anyhow::Context; -use zksync_config::{ - configs::{ - da_client::DAClientConfig::{Avail, ObjectStore}, - {self}, +use zksync_config::configs::{ + self, + da_client::{ + avail::{AvailClientConfig, AvailConfig, AvailDefaultConfig, AvailGasRelayConfig}, + DAClientConfig::{Avail, ObjectStore}, }, - AvailConfig, }; use zksync_protobuf::{required, ProtoRepr}; @@ -18,15 +18,31 @@ impl ProtoRepr for proto::DataAvailabilityClient { let client = match config { proto::data_availability_client::Config::Avail(conf) => Avail(AvailConfig { - api_node_url: required(&conf.api_node_url) - .context("api_node_url")? - .clone(), bridge_api_url: required(&conf.bridge_api_url) .context("bridge_api_url")? .clone(), - app_id: *required(&conf.app_id).context("app_id")?, timeout: *required(&conf.timeout).context("timeout")? as usize, - max_retries: *required(&conf.max_retries).context("max_retries")? as usize, + config: match conf.config.as_ref() { + Some(proto::avail_config::Config::FullClient(full_client_conf)) => { + AvailClientConfig::FullClient(AvailDefaultConfig { + api_node_url: required(&full_client_conf.api_node_url) + .context("api_node_url")? + .clone(), + app_id: *required(&full_client_conf.app_id).context("app_id")?, + }) + } + Some(proto::avail_config::Config::GasRelay(gas_relay_conf)) => { + AvailClientConfig::GasRelay(AvailGasRelayConfig { + gas_relay_api_url: required(&gas_relay_conf.gas_relay_api_url) + .context("gas_relay_api_url")? + .clone(), + max_retries: *required(&gas_relay_conf.max_retries) + .context("max_retries")? + as usize, + }) + } + None => return Err(anyhow::anyhow!("Invalid Avail DA configuration")), + }, }), proto::data_availability_client::Config::ObjectStore(conf) => { ObjectStore(object_store_proto::ObjectStore::read(conf)?) @@ -41,11 +57,22 @@ impl ProtoRepr for proto::DataAvailabilityClient { Avail(config) => Self { config: Some(proto::data_availability_client::Config::Avail( proto::AvailConfig { - api_node_url: Some(config.api_node_url.clone()), bridge_api_url: Some(config.bridge_api_url.clone()), - app_id: Some(config.app_id), timeout: Some(config.timeout as u64), - max_retries: Some(config.max_retries as u64), + config: match &config.config { + AvailClientConfig::FullClient(conf) => Some( + proto::avail_config::Config::FullClient(proto::AvailClientConfig { + api_node_url: Some(conf.api_node_url.clone()), + app_id: Some(conf.app_id), + }), + ), + AvailClientConfig::GasRelay(conf) => Some( + proto::avail_config::Config::GasRelay(proto::AvailGasRelayConfig { + gas_relay_api_url: Some(conf.gas_relay_api_url.clone()), + max_retries: Some(conf.max_retries as u64), + }), + ), + }, }, )), }, 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 d01bda2c8470..73fa2435996f 100644 --- a/core/lib/protobuf_config/src/proto/config/da_client.proto +++ b/core/lib/protobuf_config/src/proto/config/da_client.proto @@ -5,12 +5,26 @@ package zksync.config.da_client; import "zksync/config/object_store.proto"; message AvailConfig { - optional string api_node_url = 1; optional string bridge_api_url = 2; - optional uint32 app_id = 4; optional uint64 timeout = 5; - optional uint64 max_retries = 6; + oneof config { + AvailClientConfig full_client = 7; + AvailGasRelayConfig gas_relay = 8; + } + reserved 1; reserved "api_node_url"; reserved 3; reserved "seed"; + reserved 4; reserved "app_id"; + reserved 6; reserved "max_retries"; +} + +message AvailClientConfig { + optional string api_node_url = 1; + optional uint32 app_id = 2; +} + +message AvailGasRelayConfig { + optional string gas_relay_api_url = 1; + optional uint64 max_retries = 2; } message DataAvailabilityClient { diff --git a/core/lib/protobuf_config/src/proto/config/secrets.proto b/core/lib/protobuf_config/src/proto/config/secrets.proto index 17b915b3f087..43c4542783c7 100644 --- a/core/lib/protobuf_config/src/proto/config/secrets.proto +++ b/core/lib/protobuf_config/src/proto/config/secrets.proto @@ -21,6 +21,7 @@ message ConsensusSecrets { message AvailSecret { optional string seed_phrase = 1; + optional string gas_relay_api_key = 2; } message DataAvailabilitySecrets { diff --git a/core/lib/protobuf_config/src/secrets.rs b/core/lib/protobuf_config/src/secrets.rs index 587351480078..07ab340c2313 100644 --- a/core/lib/protobuf_config/src/secrets.rs +++ b/core/lib/protobuf_config/src/secrets.rs @@ -2,7 +2,7 @@ use std::str::FromStr; use anyhow::Context; use secrecy::ExposeSecret; -use zksync_basic_types::{seed_phrase::SeedPhrase, url::SensitiveUrl}; +use zksync_basic_types::{api_key::APIKey, seed_phrase::SeedPhrase, url::SensitiveUrl}; use zksync_config::configs::{ consensus::{AttesterSecretKey, ConsensusSecrets, NodeSecretKey, ValidatorSecretKey}, da_client::avail::AvailSecrets, @@ -103,14 +103,31 @@ impl ProtoRepr for proto::DataAvailabilitySecrets { let secrets = required(&self.da_secrets).context("config")?; let client = match secrets { - DaSecrets::Avail(avail_secret) => DataAvailabilitySecrets::Avail(AvailSecrets { - seed_phrase: Some( - SeedPhrase::from_str( - required(&avail_secret.seed_phrase).context("seed_phrase")?, - ) - .unwrap(), - ), - }), + DaSecrets::Avail(avail_secret) => { + let seed_phrase = match avail_secret.seed_phrase.as_ref() { + Some(seed) => match SeedPhrase::from_str(seed) { + Ok(seed) => Some(seed), + Err(_) => None, + }, + None => None, + }; + let gas_relay_api_key = match avail_secret.gas_relay_api_key.as_ref() { + Some(api_key) => match APIKey::from_str(api_key) { + Ok(api_key) => Some(api_key), + Err(_) => None, + }, + None => None, + }; + if seed_phrase.is_none() && gas_relay_api_key.is_none() { + return Err(anyhow::anyhow!( + "At least one of seed_phrase or gas_relay_api_key must be provided" + )); + } + DataAvailabilitySecrets::Avail(AvailSecrets { + seed_phrase, + gas_relay_api_key, + }) + } }; Ok(client) @@ -133,7 +150,24 @@ impl ProtoRepr for proto::DataAvailabilitySecrets { None }; - Some(DaSecrets::Avail(AvailSecret { seed_phrase })) + let gas_relay_api_key = if config.gas_relay_api_key.is_some() { + Some( + config + .clone() + .gas_relay_api_key + .unwrap() + .0 + .expose_secret() + .to_string(), + ) + } else { + None + }; + + Some(DaSecrets::Avail(AvailSecret { + seed_phrase, + gas_relay_api_key, + })) } }; diff --git a/core/node/da_clients/Cargo.toml b/core/node/da_clients/Cargo.toml index 60b65067f48d..fa2f15920bd0 100644 --- a/core/node/da_clients/Cargo.toml +++ b/core/node/da_clients/Cargo.toml @@ -37,3 +37,6 @@ blake2b_simd.workspace = true jsonrpsee = { workspace = true, features = ["ws-client"] } parity-scale-codec = { workspace = true, features = ["derive"] } subxt-signer = { workspace = true, features = ["sr25519", "native"] } +reqwest = { workspace = true } +bytes = { workspace = true } +backon.workspace = true diff --git a/core/node/da_clients/src/avail/client.rs b/core/node/da_clients/src/avail/client.rs index 7718691bf185..46d652d57137 100644 --- a/core/node/da_clients/src/avail/client.rs +++ b/core/node/da_clients/src/avail/client.rs @@ -1,34 +1,133 @@ -use std::{fmt::Debug, sync::Arc}; +use std::{fmt::Debug, sync::Arc, time::Duration}; +use anyhow::anyhow; use async_trait::async_trait; use jsonrpsee::ws_client::WsClientBuilder; +use serde::{Deserialize, Serialize}; use subxt_signer::ExposeSecret; -use zksync_config::configs::da_client::avail::{AvailConfig, AvailSecrets}; +use zksync_config::configs::da_client::avail::{AvailClientConfig, AvailConfig, AvailSecrets}; use zksync_da_client::{ types::{DAError, DispatchResponse, InclusionData}, DataAvailabilityClient, }; +use zksync_types::{ + ethabi::{self, Token}, + web3::contract::Tokenize, + H256, U256, +}; + +use crate::avail::sdk::{GasRelayClient, RawAvailClient}; -use crate::avail::sdk::RawAvailClient; +#[derive(Debug, Clone)] +enum AvailClientMode { + Default(Box), + GasRelay(GasRelayClient), +} /// An implementation of the `DataAvailabilityClient` trait that interacts with the Avail network. #[derive(Debug, Clone)] pub struct AvailClient { config: AvailConfig, - sdk_client: Arc, + sdk_client: Arc, + api_client: Arc, // bridge API reqwest client +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +#[serde(rename_all = "camelCase")] +pub struct BridgeAPIResponse { + blob_root: Option, + bridge_root: Option, + data_root_index: Option, + data_root_proof: Option>, + leaf: Option, + leaf_index: Option, + leaf_proof: Option>, + range_hash: Option, + error: Option, +} + +#[derive(Deserialize, Serialize, Debug)] +#[serde(rename_all = "camelCase")] +struct MerkleProofInput { + // proof of inclusion for the data root + data_root_proof: Vec, + // proof of inclusion of leaf within blob/bridge root + leaf_proof: Vec, + // abi.encodePacked(startBlock, endBlock) of header range commitment on vectorx + range_hash: H256, + // index of the data root in the commitment tree + data_root_index: U256, + // blob root to check proof against, or reconstruct the data root + blob_root: H256, + // bridge root to check proof against, or reconstruct the data root + bridge_root: H256, + // leaf being proven + leaf: H256, + // index of the leaf in the blob/bridge root tree + leaf_index: U256, +} + +impl Tokenize for MerkleProofInput { + fn into_tokens(self) -> Vec { + vec![Token::Tuple(vec![ + Token::Array( + self.data_root_proof + .iter() + .map(|x| Token::FixedBytes(x.as_bytes().to_vec())) + .collect(), + ), + Token::Array( + self.leaf_proof + .iter() + .map(|x| Token::FixedBytes(x.as_bytes().to_vec())) + .collect(), + ), + Token::FixedBytes(self.range_hash.as_bytes().to_vec()), + Token::Uint(self.data_root_index), + Token::FixedBytes(self.blob_root.as_bytes().to_vec()), + Token::FixedBytes(self.bridge_root.as_bytes().to_vec()), + Token::FixedBytes(self.leaf.as_bytes().to_vec()), + Token::Uint(self.leaf_index), + ])] + } } impl AvailClient { pub async fn new(config: AvailConfig, secrets: AvailSecrets) -> anyhow::Result { - let seed_phrase = secrets - .seed_phrase - .ok_or_else(|| anyhow::anyhow!("seed phrase"))?; - let sdk_client = RawAvailClient::new(config.app_id, seed_phrase.0.expose_secret()).await?; - - Ok(Self { - config, - sdk_client: Arc::new(sdk_client), - }) + let api_client = Arc::new(reqwest::Client::new()); + match config.config.clone() { + AvailClientConfig::GasRelay(conf) => { + let gas_relay_api_key = secrets + .gas_relay_api_key + .ok_or_else(|| anyhow::anyhow!("Gas relay API key is missing"))?; + let gas_relay_client = GasRelayClient::new( + &conf.gas_relay_api_url, + gas_relay_api_key.0.expose_secret(), + conf.max_retries, + Arc::clone(&api_client), + ) + .await?; + Ok(Self { + config, + sdk_client: Arc::new(AvailClientMode::GasRelay(gas_relay_client)), + api_client, + }) + } + AvailClientConfig::FullClient(conf) => { + let seed_phrase = secrets + .seed_phrase + .ok_or_else(|| anyhow::anyhow!("Seed phrase is missing"))?; + // these unwraps are safe because we validate in protobuf config + let sdk_client = + RawAvailClient::new(conf.app_id, seed_phrase.0.expose_secret()).await?; + + Ok(Self { + config, + sdk_client: Arc::new(AvailClientMode::Default(Box::new(sdk_client))), + api_client, + }) + } + } } } @@ -39,37 +138,83 @@ impl DataAvailabilityClient for AvailClient { _: u32, // batch_number data: Vec, ) -> anyhow::Result { - let client = WsClientBuilder::default() - .build(self.config.api_node_url.as_str()) - .await - .map_err(to_non_retriable_da_error)?; + match self.sdk_client.as_ref() { + AvailClientMode::Default(client) => { + let default_config = match &self.config.config { + AvailClientConfig::FullClient(conf) => conf, + _ => unreachable!(), // validated in protobuf config + }; + let ws_client = WsClientBuilder::default() + .build(default_config.api_node_url.clone().as_str()) + .await + .map_err(to_non_retriable_da_error)?; - let extrinsic = self - .sdk_client - .build_extrinsic(&client, data) - .await - .map_err(to_non_retriable_da_error)?; + let extrinsic = client + .build_extrinsic(&ws_client, data) + .await + .map_err(to_non_retriable_da_error)?; - let block_hash = self - .sdk_client - .submit_extrinsic(&client, extrinsic.as_str()) - .await - .map_err(to_non_retriable_da_error)?; - let tx_id = self - .sdk_client - .get_tx_id(&client, block_hash.as_str(), extrinsic.as_str()) - .await - .map_err(to_non_retriable_da_error)?; - - Ok(DispatchResponse::from(format!("{}:{}", block_hash, tx_id))) + let block_hash = client + .submit_extrinsic(&ws_client, extrinsic.as_str()) + .await + .map_err(to_non_retriable_da_error)?; + let tx_id = client + .get_tx_id(&ws_client, block_hash.as_str(), extrinsic.as_str()) + .await + .map_err(to_non_retriable_da_error)?; + Ok(DispatchResponse::from(format!("{}:{}", block_hash, tx_id))) + } + AvailClientMode::GasRelay(client) => { + let (block_hash, extrinsic_index) = client + .post_data(data) + .await + .map_err(to_retriable_da_error)?; + Ok(DispatchResponse { + blob_id: format!("{:x}:{}", block_hash, extrinsic_index), + }) + } + } } async fn get_inclusion_data( &self, - _blob_id: &str, + blob_id: &str, ) -> anyhow::Result, DAError> { - // TODO: implement inclusion data retrieval - Ok(Some(InclusionData { data: vec![] })) + let (block_hash, tx_idx) = blob_id.split_once(':').ok_or_else(|| DAError { + error: anyhow!("Invalid blob ID format"), + is_retriable: false, + })?; + let url = format!( + "{}/eth/proof/{}?index={}", + self.config.bridge_api_url, block_hash, tx_idx + ); + + let response = self + .api_client + .get(&url) + .timeout(Duration::from_secs(self.config.timeout as u64)) + .send() + .await + .map_err(to_retriable_da_error)?; + + let bridge_api_data = response + .json::() + .await + .map_err(to_retriable_da_error)?; + + let attestation_data: MerkleProofInput = MerkleProofInput { + data_root_proof: bridge_api_data.data_root_proof.unwrap(), + leaf_proof: bridge_api_data.leaf_proof.unwrap(), + range_hash: bridge_api_data.range_hash.unwrap(), + data_root_index: bridge_api_data.data_root_index.unwrap(), + blob_root: bridge_api_data.blob_root.unwrap(), + bridge_root: bridge_api_data.bridge_root.unwrap(), + leaf: bridge_api_data.leaf.unwrap(), + leaf_index: bridge_api_data.leaf_index.unwrap(), + }; + Ok(Some(InclusionData { + data: ethabi::encode(&attestation_data.into_tokens()), + })) } fn clone_boxed(&self) -> Box { @@ -87,3 +232,10 @@ pub fn to_non_retriable_da_error(error: impl Into) -> DAError { is_retriable: false, } } + +pub fn to_retriable_da_error(error: impl Into) -> DAError { + DAError { + error: error.into(), + is_retriable: true, + } +} diff --git a/core/node/da_clients/src/avail/sdk.rs b/core/node/da_clients/src/avail/sdk.rs index 002422109d05..f693280ba4a9 100644 --- a/core/node/da_clients/src/avail/sdk.rs +++ b/core/node/da_clients/src/avail/sdk.rs @@ -1,18 +1,22 @@ //! Minimal reimplementation of the Avail SDK client required for the DA client implementation. //! This is considered to be a temporary solution until a mature SDK is available on crates.io -use std::fmt::Debug; +use std::{fmt::Debug, sync::Arc, time}; +use backon::{ConstantBuilder, Retryable}; +use bytes::Bytes; use jsonrpsee::{ core::client::{Client, ClientT, Subscription, SubscriptionClientT}, rpc_params, }; use parity_scale_codec::{Compact, Decode, Encode}; use scale_encode::EncodeAsFields; +use serde::{Deserialize, Serialize}; use subxt_signer::{ bip39::Mnemonic, sr25519::{Keypair, Signature}, }; +use zksync_types::H256; use crate::avail::client::to_non_retriable_da_error; @@ -287,7 +291,7 @@ impl RawAvailClient { let status = sub.next().await.transpose()?; if status.is_some() && status.as_ref().unwrap().is_object() { - if let Some(block_hash) = status.unwrap().get("inBlock") { + if let Some(block_hash) = status.unwrap().get("finalized") { break block_hash .as_str() .ok_or_else(|| anyhow::anyhow!("Invalid block hash"))? @@ -369,3 +373,95 @@ fn ss58hash(data: &[u8]) -> Vec { ctx.update(data); ctx.finalize().to_vec() } + +/// An implementation of the `DataAvailabilityClient` trait that interacts with the Avail network. +#[derive(Debug, Clone)] +pub(crate) struct GasRelayClient { + api_url: String, + api_key: String, + max_retries: usize, + api_client: Arc, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct GasRelayAPISubmissionResponse { + submission_id: String, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct GasRelayAPIStatusResponse { + submission: GasRelayAPISubmission, +} + +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct GasRelayAPISubmission { + block_hash: Option, + extrinsic_index: Option, +} + +impl GasRelayClient { + const DEFAULT_INCLUSION_DELAY: time::Duration = time::Duration::from_secs(60); + const RETRY_DELAY: time::Duration = time::Duration::from_secs(5); + pub(crate) async fn new( + api_url: &str, + api_key: &str, + max_retries: usize, + api_client: Arc, + ) -> anyhow::Result { + Ok(Self { + api_url: api_url.to_owned(), + api_key: api_key.to_owned(), + max_retries, + api_client, + }) + } + + pub(crate) async fn post_data(&self, data: Vec) -> anyhow::Result<(H256, u64)> { + let submit_url = format!("{}/user/submit_raw_data?token=ethereum", &self.api_url); + // send the data to the gas relay + let submit_response = self + .api_client + .post(&submit_url) + .body(Bytes::from(data)) + .header("Content-Type", "text/plain") + .header("Authorization", &self.api_key) + .send() + .await?; + + let submit_response = submit_response + .json::() + .await?; + + let status_url = format!( + "{}/user/get_submission_info?submission_id={}", + self.api_url, submit_response.submission_id + ); + + tokio::time::sleep(Self::DEFAULT_INCLUSION_DELAY).await; + let status_response = (|| async { + self.api_client + .get(&status_url) + .header("Authorization", &self.api_key) + .send() + .await + }) + .retry( + &ConstantBuilder::default() + .with_delay(Self::RETRY_DELAY) + .with_max_times(self.max_retries), + ) + .await?; + + let status_response = status_response.json::().await?; + let (block_hash, extrinsic_index) = ( + status_response.submission.block_hash.ok_or_else(|| { + anyhow::anyhow!("Block hash not found in the response from the gas relay") + })?, + status_response.submission.extrinsic_index.ok_or_else(|| { + anyhow::anyhow!("Extrinsic index not found in the response from the gas relay") + })?, + ); + + Ok((block_hash, extrinsic_index)) + } +}