From 2a4d07dc025c03e677ddccd801e63b5f98d710cc Mon Sep 17 00:00:00 2001 From: jerrybaoo <61228234+jerrybaoo@users.noreply.github.com> Date: Tue, 23 Jan 2024 19:51:26 +0800 Subject: [PATCH] pub state diff to starknet --- Cargo.lock | 5 + crates/client/data-availability/Cargo.toml | 5 + crates/client/data-availability/src/lib.rs | 2 + .../data-availability/src/starknet/config.rs | 26 +++ .../data-availability/src/starknet/mod.rs | 154 ++++++++++++++++++ crates/node/src/service.rs | 6 + 6 files changed, 198 insertions(+) create mode 100644 crates/client/data-availability/src/starknet/config.rs create mode 100644 crates/client/data-availability/src/starknet/mod.rs diff --git a/Cargo.lock b/Cargo.lock index 8f17f06687..184cc98ec0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -6315,6 +6315,11 @@ dependencies = [ "sp-io 23.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.3.0)", "sp-keyring", "sp-runtime 24.0.0 (git+https://github.com/paritytech/polkadot-sdk?branch=release-polkadot-v1.3.0)", + "starknet-accounts", + "starknet-core", + "starknet-ff 0.3.5 (git+https://github.com/xJonathanLEI/starknet-rs.git?rev=64ebc36)", + "starknet-providers", + "starknet-signers", "starknet_api", "substrate-prometheus-endpoint", "subxt", diff --git a/crates/client/data-availability/Cargo.toml b/crates/client/data-availability/Cargo.toml index e3b1f2829d..c3fcd2ade7 100644 --- a/crates/client/data-availability/Cargo.toml +++ b/crates/client/data-availability/Cargo.toml @@ -43,7 +43,12 @@ blockifier = { workspace = true, default-features = true } mc-commitment-state-diff = { workspace = true, default-features = true } mc-db = { workspace = true, default-features = true } pallet-starknet-runtime-api = { workspace = true, features = ["std"] } +starknet-accounts = { workspace = true, default-features = true } starknet_api = { workspace = true, default-features = true } +starknet-core = { workspace = true, default-features = true } +starknet-ff = { workspace = true, default-features = true } +starknet-providers = { workspace = true, default-features = true } +starknet-signers = { workspace = true, default-features = true } # Ethereum ethers = { workspace = true } diff --git a/crates/client/data-availability/src/lib.rs b/crates/client/data-availability/src/lib.rs index d5f3d63454..8061816ec9 100644 --- a/crates/client/data-availability/src/lib.rs +++ b/crates/client/data-availability/src/lib.rs @@ -4,6 +4,7 @@ pub mod avail; pub mod celestia; pub mod ethereum; mod sharp; +pub mod starknet; pub mod utils; mod da_metrics; @@ -41,6 +42,7 @@ pub enum DaLayer { Ethereum, #[cfg(feature = "avail")] Avail, + Starknet, } /// Data availability modes in which Madara can be initialized. diff --git a/crates/client/data-availability/src/starknet/config.rs b/crates/client/data-availability/src/starknet/config.rs new file mode 100644 index 0000000000..2509dd2840 --- /dev/null +++ b/crates/client/data-availability/src/starknet/config.rs @@ -0,0 +1,26 @@ +use std::fs::File; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; + +use crate::DaMode; + +#[derive(Clone, PartialEq, Serialize, Deserialize, Debug)] +pub struct StarknetConfig { + pub http_provider: String, + pub core_contracts: String, + pub sequencer_key: String, + pub account_address: String, + pub chain_id: String, + pub mode: DaMode, + pub poll_interval_ms: Option, +} + +impl TryFrom<&PathBuf> for StarknetConfig { + type Error = String; + + fn try_from(path: &PathBuf) -> Result { + let file = File::open(path).map_err(|e| format!("error opening da config: {e}"))?; + serde_json::from_reader(file).map_err(|e| format!("error parsing da config: {e}")) + } +} diff --git a/crates/client/data-availability/src/starknet/mod.rs b/crates/client/data-availability/src/starknet/mod.rs new file mode 100644 index 0000000000..1b455a9e7b --- /dev/null +++ b/crates/client/data-availability/src/starknet/mod.rs @@ -0,0 +1,154 @@ +pub mod config; + +use std::collections::HashMap; +use std::ops::Add; + +use anyhow::{anyhow, Result}; +use async_trait::async_trait; +use ethers::types::{I256, U256}; +use reqwest::Url; +use starknet_accounts::{Account, Call, ExecutionEncoding, SingleOwnerAccount}; +use starknet_core::types::{BlockId, BlockTag, FunctionCall}; +use starknet_core::utils::get_selector_from_name; +use starknet_ff::FieldElement; +use starknet_providers::jsonrpc::HttpTransport; +use starknet_providers::{JsonRpcClient, Provider}; +use starknet_signers::{LocalWallet, SigningKey}; + +use crate::{DaClient, DaMode}; + +#[derive(Debug)] +pub struct StarknetClient { + provider: JsonRpcClient, + da_contract: FieldElement, + sequencer_account: SingleOwnerAccount, LocalWallet>, + mode: DaMode, +} + +#[async_trait] +impl DaClient for StarknetClient { + fn get_mode(&self) -> DaMode { + self.mode + } + + async fn last_published_state(&self) -> Result { + let last_state = self + .provider + .call( + FunctionCall { + contract_address: self.da_contract, + entry_point_selector: get_selector_from_name("LastState")?, + calldata: vec![], + }, + BlockId::Tag(BlockTag::Latest), + ) + .await + .map_err(|e| anyhow!("call contract has error: {:?}", e))?; + if last_state.len() != 6 { + return Ok(I256::zero()); + } + + // In The current DA, the `last_published_state` ABI is as follows: + // ```LastState(self: @ContractState) -> (u256, u256, u256)```. + // The second parameter in the ABI represents the last published + // block number. Since in the Cairo contract, a `u256` is composed of two `u128` values, we + // extract the third element from the returned data as the last published block number. + last_state[2] + .try_into() + .map(|n: u128| I256::from(n)) + .map_err(|_e| anyhow!("last published block number exceed i256 max")) + } + + async fn publish_state_diff(&self, state_diff: Vec) -> Result<()> { + let state_diff_len = state_diff.len(); + if state_diff_len < 3 { + return Err(anyhow!("invalid state diff")); + } + + // In the current contract, the ABI for publishing the state diff is defined as follows: + // ```cairo + // fn UpdateState( + // ref self: ContractState, + // state_diff: Array, + // state_root: u256, + // block_number: u256, + // block_hash: u256 + // ) -> bool + // ``` + // In the Cairo contract, a `u256` is passed as two `u128` values. + // Additionally, the `Array` parameter requires passing the length at the beginning. + // Therefore, the overall calldata length is 1 + state_diff * 2 + 3 * 2. + let mut calldata = Vec::with_capacity(state_diff_len * 2 + 1); + + // push state diff data len to calldata. + calldata.push(FieldElement::from(state_diff_len - 3)); + for (index, ft) in state_diff.iter().enumerate() { + // TODO: set current block number to l2 last published block number. just for testnet. + if index == state_diff_len - 2 { + let last_published_block_number = U256::from(self.last_published_state().await?.as_u128()); + let current_block_number = last_published_block_number.add(U256::one()); + let low_ft: u128 = current_block_number.low_u128(); + let high_ft: u128 = (current_block_number >> 128).low_u128(); + calldata.push(FieldElement::from(low_ft)); + calldata.push(FieldElement::from(high_ft)); + continue; + } + + let low_ft: u128 = ft.low_u128(); + let high_ft: u128 = (ft >> 128).low_u128(); + calldata.push(FieldElement::from(low_ft)); + calldata.push(FieldElement::from(high_ft)); + } + + let res = self + .sequencer_account + .execute(vec![Call { + to: self.da_contract, + selector: get_selector_from_name("UpdateState").map_err(|e| anyhow!("get selector failed, {e}"))?, + calldata, + }]) + .send() + .await + .map_err(|e| anyhow!("send transaction failed {e}")); + + log::debug!("Publish block data to starknet result: {:#?}", res); + + Ok(()) + } + + fn get_da_metric_labels(&self) -> HashMap { + [("name".into(), "starknet".into())].iter().cloned().collect() + } +} + +fn create_provider(url: &str) -> Result, String> { + Url::parse(url).map(|url| JsonRpcClient::new(HttpTransport::new(url))).map_err(|e| format!("invalid http url, {e}")) +} + +impl TryFrom for StarknetClient { + type Error = String; + + fn try_from(conf: config::StarknetConfig) -> Result { + let signer = FieldElement::from_hex_be(&conf.sequencer_key) + .map(|elem| LocalWallet::from(SigningKey::from_secret_scalar(elem))) + .map_err(|e| format!("invalid sequencer key, {e}"))?; + + let da_contract = + FieldElement::from_hex_be(&conf.core_contracts).map_err(|e| format!("invalid da contract address, {e}"))?; + + let chain_id = + FieldElement::from_byte_slice_be(conf.chain_id.as_bytes()).map_err(|e| format!("invalid chain id {e}"))?; + + let provider = create_provider(&conf.http_provider)?; + let account = FieldElement::from_hex_be(&conf.account_address) + .map(|acc| SingleOwnerAccount::new(provider, signer, acc, chain_id, ExecutionEncoding::New)) + .map_err(|e| format!("invalid sequencer address {e}"))?; + + Ok(Self { + provider: create_provider(&conf.http_provider)?, + da_contract, + sequencer_account: account, + mode: conf.mode, + }) + } +} diff --git a/crates/node/src/service.rs b/crates/node/src/service.rs index dd58cde18a..6e62c6e42b 100644 --- a/crates/node/src/service.rs +++ b/crates/node/src/service.rs @@ -18,6 +18,8 @@ use mc_data_availability::avail::{config::AvailConfig, AvailClient}; use mc_data_availability::celestia::{config::CelestiaConfig, CelestiaClient}; use mc_data_availability::ethereum::config::EthereumConfig; use mc_data_availability::ethereum::EthereumClient; +use mc_data_availability::starknet::config::StarknetConfig; +use mc_data_availability::starknet::StarknetClient; use mc_data_availability::{DaClient, DaLayer, DataAvailabilityWorker}; use mc_genesis_data_provider::OnDiskGenesisConfig; use mc_l1_messages::config::L1MessagesWorkerConfig; @@ -450,6 +452,10 @@ pub fn new_full( let avail_conf = AvailConfig::try_from(&da_path)?; Arc::new(AvailClient::try_from(avail_conf).map_err(|e| ServiceError::Other(e.to_string()))?) } + DaLayer::Starknet => { + let starknet_conf = StarknetConfig::try_from(&da_path)?; + Arc::new(StarknetClient::try_from(starknet_conf).map_err(|e| ServiceError::Other(e.to_string()))?) + } }; task_manager.spawn_essential_handle().spawn(