diff --git a/.github/workflows/pull-request.yml b/.github/workflows/pull-request.yml index 11fe46392..e486085bb 100644 --- a/.github/workflows/pull-request.yml +++ b/.github/workflows/pull-request.yml @@ -5,6 +5,7 @@ on: workflow_dispatch: pull_request: branches: [main] + types: [opened, synchronize, reopened, ready_for_review] concurrency: group: pr-checks-${{ github.workflow }}-${{ github.head_ref || github.run_id }} @@ -16,24 +17,29 @@ permissions: jobs: changelog: name: Enforce CHANGELOG + if: github.event.pull_request.draft == false uses: ./.github/workflows/changelog.yml linters: name: Run linters + if: github.event.pull_request.draft == false uses: ./.github/workflows/linters.yml needs: changelog rust_check: name: Run check + if: github.event.pull_request.draft == false uses: ./.github/workflows/rust-check.yml needs: changelog linters_cargo: name: Run Cargo linters + if: github.event.pull_request.draft == false uses: ./.github/workflows/linters-cargo.yml needs: rust_check coverage: name: Run Coverage + if: github.event.pull_request.draft == false uses: ./.github/workflows/coverage.yml needs: changelog diff --git a/CHANGELOG.md b/CHANGELOG.md index 4d8fa566c..a1c7fe0f9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,7 @@ ## Next release +- feat: new crate gateway client & server - feat: add devnet via `--devnet` cli argument - refactor: class import from FGW - code docs: documented how get_storage_at is implemented diff --git a/Cargo.lock b/Cargo.lock index db63b951e..56b146cf5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5360,6 +5360,7 @@ dependencies = [ "mc-db", "mc-devnet", "mc-eth", + "mc-gateway", "mc-mempool", "mc-metrics", "mc-rpc", @@ -5578,6 +5579,27 @@ dependencies = [ "tokio", ] +[[package]] +name = "mc-gateway" +version = "0.7.0" +dependencies = [ + "anyhow", + "hyper 0.14.29", + "log", + "mc-db", + "mp-block", + "mp-gateway", + "mp-utils", + "reqwest 0.12.5", + "serde", + "serde_json", + "starknet-core", + "starknet-types-core", + "thiserror", + "tokio", + "url", +] + [[package]] name = "mc-mempool" version = "0.7.0" @@ -5839,12 +5861,30 @@ version = "0.7.0" dependencies = [ "assert_matches", "primitive-types", + "serde", + "serde_with 3.9.0", "starknet-core", "starknet-types-core", "starknet_api", "thiserror", ] +[[package]] +name = "mp-gateway" +version = "0.7.0" +dependencies = [ + "mp-block", + "mp-chain-config", + "mp-convert", + "mp-receipt", + "mp-state-update", + "mp-transactions", + "serde", + "serde_json", + "serde_with 3.9.0", + "starknet-types-core", +] + [[package]] name = "mp-receipt" version = "0.7.0" @@ -5887,6 +5927,7 @@ dependencies = [ "mp-convert", "num-bigint", "serde", + "serde_with 3.9.0", "starknet-core", "starknet-providers", "starknet-types-core", @@ -7531,7 +7572,25 @@ dependencies = [ "indexmap 1.9.3", "serde", "serde_json", - "serde_with_macros", + "serde_with_macros 2.3.3", + "time", +] + +[[package]] +name = "serde_with" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "69cecfa94848272156ea67b2b1a53f20fc7bc638c4a46d2f8abde08f05f4b857" +dependencies = [ + "base64 0.22.1", + "chrono", + "hex", + "indexmap 1.9.3", + "indexmap 2.2.6", + "serde", + "serde_derive", + "serde_json", + "serde_with_macros 3.9.0", "time", ] @@ -7547,6 +7606,18 @@ dependencies = [ "syn 2.0.66", ] +[[package]] +name = "serde_with_macros" +version = "3.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a8fee4991ef4f274617a51ad4af30519438dacb2f56ac773b08a1922ff743350" +dependencies = [ + "darling 0.20.9", + "proc-macro2", + "quote", + "syn 2.0.66", +] + [[package]] name = "sha-1" version = "0.9.8" @@ -7806,7 +7877,7 @@ checksum = "a5f91344f1e0b81873b6dc235c50ae4d084c6ea4dd4a1e3e27ad895803adb610" dependencies = [ "serde", "serde_json", - "serde_with", + "serde_with 2.3.3", "starknet-accounts", "starknet-core", "starknet-providers", @@ -7825,7 +7896,7 @@ dependencies = [ "serde", "serde_json", "serde_json_pythonic", - "serde_with", + "serde_with 2.3.3", "sha3", "starknet-crypto 0.7.0", "starknet-types-core", @@ -8013,7 +8084,7 @@ dependencies = [ "reqwest 0.11.27", "serde", "serde_json", - "serde_with", + "serde_with 2.3.3", "starknet-core", "thiserror", "url", diff --git a/Cargo.toml b/Cargo.toml index b4d6b69f0..905067dbd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "crates/client/sync", "crates/client/eth", "crates/client/rpc", + "crates/client/gateway", "crates/client/telemetry", "crates/client/metrics", "crates/client/devnet", @@ -15,6 +16,7 @@ members = [ "crates/primitives/convert", "crates/primitives/transactions", "crates/primitives/class", + "crates/primitives/gateway", "crates/primitives/receipt", "crates/primitives/state_update", "crates/primitives/chain_config", @@ -28,6 +30,7 @@ default-members = [ "crates/client/exec", "crates/client/sync", "crates/client/eth", + "crates/client/gateway", "crates/client/rpc", "crates/client/telemetry", "crates/client/metrics", @@ -39,6 +42,7 @@ default-members = [ "crates/primitives/convert", "crates/primitives/transactions", "crates/primitives/class", + "crates/primitives/gateway", "crates/primitives/receipt", "crates/primitives/state_update", "crates/primitives/chain_config", @@ -88,6 +92,7 @@ mp-block = { path = "crates/primitives/block", default-features = false } mp-convert = { path = "crates/primitives/convert", default-features = false } mp-transactions = { path = "crates/primitives/transactions", default-features = false } mp-class = { path = "crates/primitives/class", default-features = false } +mp-gateway = { path = "crates/primitives/gateway", default-features = false } mp-receipt = { path = "crates/primitives/receipt", default-features = false } mp-state-update = { path = "crates/primitives/state_update", default-features = false } mp-utils = { path = "crates/primitives/utils", default-features = false } @@ -98,6 +103,7 @@ mc-telemetry = { path = "crates/client/telemetry" } mc-db = { path = "crates/client/db" } mc-exec = { path = "crates/client/exec" } mc-rpc = { path = "crates/client/rpc" } +mc-gateway = { path = "crates/client/gateway" } mc-sync = { path = "crates/client/sync" } mc-eth = { path = "crates/client/eth" } mc-metrics = { path = "crates/client/metrics" } @@ -158,6 +164,7 @@ rand = "0.8" reqwest = { version = "0.12", features = ["json"] } rstest = "0.18" serde = { version = "1.0", default-features = false, features = ["std"] } +serde_with = "3.9" serde_json = { version = "1.0", default-features = false, features = ["std"] } thiserror = "1.0" tokio = { version = "1.34", features = ["signal"] } diff --git a/crates/client/block_import/src/verify_apply.rs b/crates/client/block_import/src/verify_apply.rs index 8ea914211..f6f2a1198 100644 --- a/crates/client/block_import/src/verify_apply.rs +++ b/crates/client/block_import/src/verify_apply.rs @@ -262,15 +262,15 @@ fn block_hash( parent_block_hash, block_number, global_state_root, - sequencer_address, + sequencer_address: Some(sequencer_address), block_timestamp, transaction_count, transaction_commitment, event_count, event_commitment, - state_diff_length, - state_diff_commitment, - receipt_commitment, + state_diff_length: Some(state_diff_length), + state_diff_commitment: Some(state_diff_commitment), + receipt_commitment: Some(receipt_commitment), protocol_version, l1_gas_price, l1_da_mode, diff --git a/crates/client/exec/src/block_context.rs b/crates/client/exec/src/block_context.rs index 51c4a5c33..2b96854b3 100644 --- a/crates/client/exec/src/block_context.rs +++ b/crates/client/exec/src/block_context.rs @@ -74,7 +74,7 @@ impl ExecutionContext { block.header.protocol_version, block.header.block_number, block.header.block_timestamp, - block.header.sequencer_address, + block.header.sequencer_address.unwrap_or_default(), block.header.l1_gas_price.clone(), block.header.l1_da_mode, ), diff --git a/crates/client/gateway/Cargo.toml b/crates/client/gateway/Cargo.toml new file mode 100644 index 000000000..06c0d087f --- /dev/null +++ b/crates/client/gateway/Cargo.toml @@ -0,0 +1,38 @@ +[package] +description = "Madara client rpc service" +name = "mc-gateway" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +homepage.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] + +# Deoxys +mc-db = { workspace = true } +mp-block = { workspace = true } +mp-gateway = { workspace = true } +mp-utils = { workspace = true } + +# Starknet +starknet-core = { workspace = true } +starknet-types-core = { workspace = true } + +# Other +anyhow = { workspace = true } +hyper = { workspace = true } +log = { workspace = true } +reqwest = { workspace = true } +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +thiserror = { workspace = true } +tokio = { workspace = true } +url = { workspace = true } + +[dev-dependencies] +tokio = { workspace = true } diff --git a/crates/client/gateway/src/client/builder.rs b/crates/client/gateway/src/client/builder.rs new file mode 100644 index 000000000..4b8671742 --- /dev/null +++ b/crates/client/gateway/src/client/builder.rs @@ -0,0 +1,45 @@ +use std::collections::HashMap; + +use reqwest::Client; +use url::Url; + +#[derive(Debug, Clone)] +pub struct FeederClient { + pub(crate) client: Client, + pub(crate) gateway_url: Url, + pub(crate) feeder_gateway_url: Url, + pub(crate) headers: HashMap, +} + +impl FeederClient { + pub fn new(gateway_url: Url, feeder_gateway_url: Url) -> Self { + Self { client: Client::new(), gateway_url, feeder_gateway_url, headers: HashMap::new() } + } + + pub fn new_with_headers(gateway_url: Url, feeder_gateway_url: Url, headers: &[(String, String)]) -> Self { + let headers = headers.iter().cloned().collect(); + Self { client: Client::new(), gateway_url, feeder_gateway_url, headers } + } + + pub fn add_header(&mut self, key: &str, value: &str) { + self.headers.insert(key.to_string(), value.to_string()); + } + + pub fn remove_header(&mut self, key: &str) -> Option { + self.headers.remove(key) + } + + pub fn starknet_alpha_mainnet() -> Self { + Self::new( + Url::parse("https://alpha-mainnet.starknet.io/gateway/").unwrap(), + Url::parse("https://alpha-mainnet.starknet.io/feeder_gateway/").unwrap(), + ) + } + + pub fn starknet_alpha_sepolia() -> Self { + Self::new( + Url::parse("https://alpha-sepolia.starknet.io/gateway/").unwrap(), + Url::parse("https://alpha-sepolia.starknet.io/feeder_gateway/").unwrap(), + ) + } +} diff --git a/crates/client/gateway/src/client/methods.rs b/crates/client/gateway/src/client/methods.rs new file mode 100644 index 000000000..91b93cf92 --- /dev/null +++ b/crates/client/gateway/src/client/methods.rs @@ -0,0 +1,90 @@ +use mp_block::{BlockId, BlockTag}; +use serde::{Deserialize, Serialize}; + +use crate::error::SequencerError; + +use super::{builder::FeederClient, request_builder::RequestBuilder}; + +use mp_gateway::{ + block::{BlockProvider, PendingBlockProvider, ProviderMaybePendingBlock}, + state_update::{PendingStateUpdateProvider, ProviderMaybePendingStateUpdate, StateUpdateProvider}, +}; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct StateUpdateWithBlock { + pub state_update: StateUpdateProvider, + pub block: BlockProvider, +} + +impl FeederClient { + pub async fn get_block(&self, block_id: BlockId) -> Result { + let request = RequestBuilder::new(&self.client, self.feeder_gateway_url.clone()) + .add_uri_segment("get_block") + .unwrap() + .with_block_id(block_id); + + match block_id { + BlockId::Tag(BlockTag::Pending) => { + Ok(ProviderMaybePendingBlock::Pending(request.send_get::().await?)) + } + _ => Ok(ProviderMaybePendingBlock::Block(request.send_get::().await?)), + } + } + + pub async fn get_state_update(&self, block_id: BlockId) -> Result { + let request = RequestBuilder::new(&self.client, self.feeder_gateway_url.clone()) + .add_uri_segment("get_state_update") + .unwrap() + .with_block_id(block_id); + + match block_id { + BlockId::Tag(BlockTag::Pending) => { + Ok(ProviderMaybePendingStateUpdate::Pending(request.send_get::().await?)) + } + _ => Ok(ProviderMaybePendingStateUpdate::Update(request.send_get::().await?)), + } + } +} + +#[cfg(test)] +mod tests { + use mp_block::BlockTag; + use starknet_core::types::Felt; + + use super::*; + + #[tokio::test] + async fn test_get_block() { + let client = FeederClient::starknet_alpha_mainnet(); + + let block = client.get_block(BlockId::Number(0)).await.unwrap(); + println!("parent_block_hash: 0x{:x}", block.parent_block_hash()); + let block = client + .get_block(BlockId::Hash(Felt::from_hex_unchecked( + "0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943", + ))) + .await + .unwrap(); + println!("parent_block_hash: 0x{:x}", block.parent_block_hash()); + let block = client.get_block(BlockId::Tag(BlockTag::Latest)).await.unwrap(); + println!("parent_block_hash: 0x{:x}", block.parent_block_hash()); + let block = client.get_block(BlockId::Tag(BlockTag::Pending)).await.unwrap(); + println!("parent_block_hash: 0x{:x}", block.parent_block_hash()); + } + + #[tokio::test] + async fn test_get_state_update() { + let client = FeederClient::starknet_alpha_mainnet(); + + let block = client.get_state_update(BlockId::Number(0)).await.unwrap(); + let block = client + .get_state_update(BlockId::Hash(Felt::from_hex_unchecked( + "0x47c3637b57c2b079b93c61539950c17e868a28f46cdef28f88521067f21e943", + ))) + .await + .unwrap(); + let block = client.get_state_update(BlockId::Tag(BlockTag::Latest)).await.unwrap(); + let block = client.get_state_update(BlockId::Tag(BlockTag::Pending)).await.unwrap(); + } +} diff --git a/crates/client/gateway/src/client/mod.rs b/crates/client/gateway/src/client/mod.rs new file mode 100644 index 000000000..1724cd844 --- /dev/null +++ b/crates/client/gateway/src/client/mod.rs @@ -0,0 +1,3 @@ +pub mod builder; +mod methods; +mod request_builder; diff --git a/crates/client/gateway/src/client/request_builder.rs b/crates/client/gateway/src/client/request_builder.rs new file mode 100644 index 000000000..e3e058bf3 --- /dev/null +++ b/crates/client/gateway/src/client/request_builder.rs @@ -0,0 +1,109 @@ +use std::collections::HashMap; + +use mp_block::{BlockId, BlockTag}; +use reqwest::Client; +use serde::de::DeserializeOwned; +use starknet_types_core::felt::Felt; +use url::Url; + +use crate::error::{SequencerError, StarknetError}; + +#[derive(Debug, Clone)] +pub struct RequestBuilder<'a> { + client: &'a Client, + url: Url, + params: HashMap, + headers: HashMap, +} + +impl<'a> RequestBuilder<'a> { + pub fn new(client: &'a Client, base_url: Url) -> Self { + Self { client, url: base_url, params: HashMap::new(), headers: HashMap::new() } + } + + pub fn add_uri_segment(mut self, segment: &str) -> Result { + self.url = self.url.join(segment)?; + Ok(self) + } + + pub fn add_header(mut self, name: &str, value: &str) -> Self { + self.headers.insert(name.to_string(), value.to_string()); + self + } + + pub fn add_param(mut self, name: &str, value: &str) -> Self { + self.params.insert(name.to_string(), value.to_string()); + self + } + + pub fn with_block_id(mut self, block_id: BlockId) -> Self { + match block_id { + BlockId::Hash(hash) => { + self = self.add_param("blockHash", &format!("0x{:x}", hash)); + } + BlockId::Number(number) => { + self = self.add_param("blockNumber", &number.to_string()); + } + BlockId::Tag(tag) => { + let tag = match tag { + BlockTag::Latest => "latest", + BlockTag::Pending => "pending", + }; + self = self.add_param("blockNumber", tag); + } + } + self + } + + pub fn with_class_hash(mut self, class_hash: Felt) -> Self { + self = self.add_param("classHash", &format!("0x{:x}", class_hash)); + self + } + + pub async fn send_get(self) -> Result + where + T: DeserializeOwned, + { + let mut request = self.client.get(self.url); + + for (key, value) in self.headers { + request = request.header(key, value); + } + + let response = request.query(&self.params).send().await?; + + unpack(response).await + } + + pub async fn send_post(self) -> Result + where + T: DeserializeOwned, + { + let mut request = self.client.post(self.url); + + for (key, value) in self.headers { + request = request.header(key, value); + } + + let response = request.form(&self.params).send().await?; + Ok(response.json().await?) + } +} + +async fn unpack(response: reqwest::Response) -> Result +where + T: ::serde::de::DeserializeOwned, +{ + let status = response.status(); + if status == reqwest::StatusCode::INTERNAL_SERVER_ERROR || status == reqwest::StatusCode::BAD_REQUEST { + let error = match response.json::().await { + Ok(e) => SequencerError::StarknetError(e), + Err(e) if e.is_decode() => SequencerError::InvalidStarknetErrorVariant, + Err(e) => SequencerError::ReqwestError(e), + }; + return Err(error); + } + + response.error_for_status_ref().map(|_| ())?; + Ok(response.json::().await?) +} diff --git a/crates/client/gateway/src/error.rs b/crates/client/gateway/src/error.rs new file mode 100644 index 000000000..d74747b76 --- /dev/null +++ b/crates/client/gateway/src/error.rs @@ -0,0 +1,105 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Debug, thiserror::Error)] +pub enum SequencerError { + #[error(transparent)] + StarknetError(#[from] StarknetError), + #[error(transparent)] + ReqwestError(#[from] reqwest::Error), + #[error("error decoding response body: invalid error variant")] + InvalidStarknetErrorVariant, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct StarknetError { + pub code: StarknetErrorCode, + pub message: String, +} + +impl StarknetError { + pub fn new(code: StarknetErrorCode, message: String) -> Self { + Self { code, message } + } + + pub fn block_not_found() -> Self { + Self { code: StarknetErrorCode::BlockNotFound, message: "Block not found".to_string() } + } +} + +impl std::error::Error for StarknetError {} + +impl std::fmt::Display for StarknetError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.message) + } +} + +#[derive(Copy, Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub enum StarknetErrorCode { + #[serde(rename = "StarknetErrorCode.BLOCK_NOT_FOUND")] + BlockNotFound, + #[serde(rename = "StarknetErrorCode.ENTRY_POINT_NOT_FOUND_IN_CONTRACT")] + EntryPointNotFound, + #[serde(rename = "StarknetErrorCode.OUT_OF_RANGE_CONTRACT_ADDRESS")] + OutOfRangeContractAddress, + #[serde(rename = "StarkErrorCode.SCHEMA_VALIDATION_ERROR")] + SchemaValidationError, + #[serde(rename = "StarknetErrorCode.TRANSACTION_FAILED")] + TransactionFailed, + #[serde(rename = "StarknetErrorCode.UNINITIALIZED_CONTRACT")] + UninitializedContract, + #[serde(rename = "StarknetErrorCode.OUT_OF_RANGE_BLOCK_HASH")] + OutOfRangeBlockHash, + #[serde(rename = "StarknetErrorCode.OUT_OF_RANGE_TRANSACTION_HASH")] + OutOfRangeTransactionHash, + #[serde(rename = "StarkErrorCode.MALFORMED_REQUEST")] + MalformedRequest, + #[serde(rename = "StarknetErrorCode.UNSUPPORTED_SELECTOR_FOR_FEE")] + UnsupportedSelectorForFee, + #[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_DEFINITION")] + InvalidContractDefinition, + #[serde(rename = "StarknetErrorCode.NON_PERMITTED_CONTRACT")] + NotPermittedContract, + #[serde(rename = "StarknetErrorCode.UNDECLARED_CLASS")] + UndeclaredClass, + #[serde(rename = "StarknetErrorCode.TRANSACTION_LIMIT_EXCEEDED")] + TransactionLimitExceeded, + #[serde(rename = "StarknetErrorCode.INVALID_TRANSACTION_NONCE")] + InvalidTransactionNonce, + #[serde(rename = "StarknetErrorCode.OUT_OF_RANGE_FEE")] + OutOfRangeFee, + #[serde(rename = "StarknetErrorCode.INVALID_TRANSACTION_VERSION")] + InvalidTransactionVersion, + #[serde(rename = "StarknetErrorCode.INVALID_PROGRAM")] + InvalidProgram, + #[serde(rename = "StarknetErrorCode.DEPRECATED_TRANSACTION")] + DeprecatedTransaction, + #[serde(rename = "StarknetErrorCode.INVALID_COMPILED_CLASS_HASH")] + InvalidCompiledClassHash, + #[serde(rename = "StarknetErrorCode.COMPILATION_FAILED")] + CompilationFailed, + #[serde(rename = "StarknetErrorCode.UNAUTHORIZED_ENTRY_POINT_FOR_INVOKE")] + UnauthorizedEntryPointForInvoke, + #[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_CLASS")] + InvalidContractClass, + #[serde(rename = "StarknetErrorCode.CLASS_ALREADY_DECLARED")] + ClassAlreadyDeclared, + #[serde(rename = "StarkErrorCode.INVALID_SIGNATURE")] + InvalidSignature, + #[serde(rename = "StarknetErrorCode.INSUFFICIENT_ACCOUNT_BALANCE")] + InsufficientAccountBalance, + #[serde(rename = "StarknetErrorCode.INSUFFICIENT_MAX_FEE")] + InsufficientMaxFee, + #[serde(rename = "StarknetErrorCode.VALIDATE_FAILURE")] + ValidateFailure, + #[serde(rename = "StarknetErrorCode.CONTRACT_BYTECODE_SIZE_TOO_LARGE")] + ContractBytecodeSizeTooLarge, + #[serde(rename = "StarknetErrorCode.CONTRACT_CLASS_OBJECT_SIZE_TOO_LARGE")] + ContractClassObjectSizeTooLarge, + #[serde(rename = "StarknetErrorCode.DUPLICATED_TRANSACTION")] + DuplicatedTransaction, + #[serde(rename = "StarknetErrorCode.INVALID_CONTRACT_CLASS_VERSION")] + InvalidContractClassVersion, +} diff --git a/crates/client/gateway/src/lib.rs b/crates/client/gateway/src/lib.rs new file mode 100644 index 000000000..a79a04cfd --- /dev/null +++ b/crates/client/gateway/src/lib.rs @@ -0,0 +1,3 @@ +pub mod client; +pub mod error; +pub mod server; diff --git a/crates/client/gateway/src/server/handler.rs b/crates/client/gateway/src/server/handler.rs new file mode 100644 index 000000000..159ecf814 --- /dev/null +++ b/crates/client/gateway/src/server/handler.rs @@ -0,0 +1,143 @@ +use std::sync::Arc; + +use hyper::{Body, Request, Response}; +use mc_db::MadaraBackend; +use mp_block::{BlockId, BlockTag, MadaraBlock, MadaraPendingBlock}; +use mp_gateway::{ + block::{BlockProvider, BlockStatus, PendingBlockProvider}, + state_update::{PendingStateUpdateProvider, StateUpdateProvider}, +}; +use starknet_types_core::felt::Felt; + +use crate::error::StarknetError; + +use super::helpers::{ + block_id_from_params, create_json_response, get_params_from_request, internal_error_response, + storage_error_to_response, +}; + +pub async fn handle_get_block(req: Request, backend: Arc) -> Response { + let params = get_params_from_request(&req); + let block_id = match block_id_from_params(¶ms) { + Ok(block_id) => block_id, + // Return the error response if the request is malformed + Err(e) => return e.into(), + }; + + let block = match backend.get_block(&block_id) { + Ok(Some(block)) => block, + Ok(None) => { + return StarknetError::block_not_found().into(); + } + Err(e) => return storage_error_to_response(e), + }; + + if let Ok(block) = MadaraBlock::try_from(block.clone()) { + let last_l1_confirmed_block = match backend.get_l1_last_confirmed_block() { + Ok(block) => block, + Err(e) => return storage_error_to_response(e), + }; + + let status = if Some(block.info.header.block_number) <= last_l1_confirmed_block { + BlockStatus::AcceptedOnL1 + } else { + BlockStatus::AcceptedOnL2 + }; + + let block_provider = BlockProvider::new(block, status); + create_json_response(hyper::StatusCode::OK, &block_provider) + } else if let Ok(block) = MadaraPendingBlock::try_from(block) { + let block_provider = PendingBlockProvider::new(block); + create_json_response(hyper::StatusCode::OK, &block_provider) + } else { + internal_error_response() + } +} + +pub async fn handle_get_state_update(req: Request, backend: Arc) -> Response { + let params = get_params_from_request(&req); + let block_id = match block_id_from_params(¶ms) { + Ok(block_id) => block_id, + // Return the error response if the request is malformed + Err(e) => return e.into(), + }; + + let resolved_block_id = match backend.resolve_block_id(&block_id) { + Ok(Some(block_id)) => block_id, + Ok(None) => { + return StarknetError::block_not_found().into(); + } + Err(e) => { + log::error!("Error resolving block id: {}", e); + return storage_error_to_response(e); + } + }; + + let state_diff = match backend.get_block_state_diff(&resolved_block_id) { + Ok(Some(state_diff)) => state_diff, + Ok(None) => { + return StarknetError::block_not_found().into(); + } + Err(e) => { + log::error!("Error getting contract class hash at: {}", e); + return storage_error_to_response(e); + } + }; + + match resolved_block_id.is_pending() { + true => { + let old_root = match backend.get_block_info(&BlockId::Tag(BlockTag::Latest)) { + Ok(Some(block)) => match block.as_nonpending() { + Some(block) => block.header.global_state_root, + None => return internal_error_response(), + }, + Ok(None) => Felt::ZERO, // The pending block is actually genesis, so old root is zero + Err(e) => { + log::error!("Error getting latest block from db: {}", e); + return storage_error_to_response(e); + } + }; + let state_update = PendingStateUpdateProvider { old_root, state_diff: state_diff.into() }; + create_json_response(hyper::StatusCode::OK, &state_update) + } + false => { + let block_info = match backend.get_block_info(&resolved_block_id) { + Ok(Some(block_info)) => block_info, + Ok(None) => { + return StarknetError::block_not_found().into(); + } + Err(e) => return storage_error_to_response(e), + }; + + let block_info = match block_info.as_nonpending() { + Some(block_info) => block_info, + None => return internal_error_response(), + }; + + let old_root = if let Some(val) = block_info.header.block_number.checked_sub(1) { + match backend.get_block_info(&BlockId::Number(val)) { + Ok(Some(block)) => match block.as_nonpending() { + Some(block) => block.header.global_state_root, + None => return internal_error_response(), + }, + Ok(None) => Felt::ZERO, // The pending block is actually genesis, so old root is zero + Err(e) => { + log::error!("Error getting latest block from db: {}", e); + return storage_error_to_response(e); + } + } + } else { + Felt::ZERO + }; + + let state_update = StateUpdateProvider { + block_hash: block_info.block_hash, + old_root, + new_root: block_info.header.global_state_root, + state_diff: state_diff.into(), + }; + + create_json_response(hyper::StatusCode::OK, &state_update) + } + } +} diff --git a/crates/client/gateway/src/server/helpers.rs b/crates/client/gateway/src/server/helpers.rs new file mode 100644 index 000000000..95dd47c93 --- /dev/null +++ b/crates/client/gateway/src/server/helpers.rs @@ -0,0 +1,120 @@ +use std::collections::HashMap; + +use hyper::{header, Body, Request, Response, StatusCode}; +use mc_db::MadaraStorageError; +use mp_block::{BlockId, BlockTag}; +use serde::Serialize; +use starknet_types_core::felt::Felt; + +use crate::error::{StarknetError, StarknetErrorCode}; + +pub(crate) fn service_unavailable_response(service_name: &str) -> Response { + Response::builder() + .status(StatusCode::SERVICE_UNAVAILABLE) + .body(Body::from(format!("{} Service disabled", service_name))) + .expect("Failed to build SERVICE_UNAVAILABLE response with a valid status and body") +} + +pub(crate) fn not_found_response() -> Response { + Response::builder() + .status(StatusCode::NOT_FOUND) + .body(Body::from("Not Found")) + .expect("Failed to build NOT_FOUND response with a valid status and body") +} + +pub(crate) fn internal_error_response() -> Response { + Response::builder() + .status(StatusCode::INTERNAL_SERVER_ERROR) + .body(Body::from("Internal Server Error")) + .expect("Failed to build INTERNAL_SERVER_ERROR response with a valid status and body") +} + +pub(crate) fn not_implemented_response() -> Response { + Response::builder() + .status(StatusCode::NOT_IMPLEMENTED) + .body(Body::from("Not Implemented")) + .expect("Failed to build NOT_IMPLEMENTED response with a valid status and body") +} + +/// Creates a JSON response with the given status code and a body that can be serialized to JSON. +/// +/// If the serialization fails, this function returns a 500 Internal Server Error response. +pub(crate) fn create_json_response(status: StatusCode, body: &T) -> Response +where + T: Serialize, +{ + // Serialize the body to JSON + let body = match serde_json::to_string(body) { + Ok(body) => body, + Err(e) => { + log::error!("Failed to serialize response body: {}", e); + return internal_error_response(); + } + }; + + // Build the response with the specified status code and serialized body + match Response::builder().status(status).header(header::CONTENT_TYPE, "application/json").body(Body::from(body)) { + Ok(response) => response, + Err(e) => { + log::error!("Failed to build response: {}", e); + internal_error_response() + } + } +} + +pub(crate) fn storage_error_to_response(err: MadaraStorageError) -> Response { + match err { + MadaraStorageError::InvalidNonce => { + let starknet_error = + StarknetError::new(StarknetErrorCode::InvalidTransactionNonce, "Invalid nonce".to_string()); + create_json_response(hyper::StatusCode::BAD_REQUEST, &starknet_error) + } + MadaraStorageError::InvalidBlockNumber => { + let starknet_error = + StarknetError::new(StarknetErrorCode::MalformedRequest, "Invalid block number".to_string()); + create_json_response(hyper::StatusCode::BAD_REQUEST, &starknet_error) + } + _ => internal_error_response(), + } +} + +pub(crate) fn get_params_from_request(req: &Request) -> HashMap { + let query = req.uri().query().unwrap_or(""); + let params = query.split('&'); + let mut query_params = HashMap::new(); + for param in params { + let parts: Vec<&str> = param.split('=').collect(); + if parts.len() == 2 { + query_params.insert(parts[0].to_string(), parts[1].to_string()); + } + } + query_params +} + +// blockNumber or blockHash +pub(crate) fn block_id_from_params(params: &HashMap) -> Result { + if let Some(block_number) = params.get("blockNumber") { + match block_number.as_str() { + "latest" => Ok(BlockId::Tag(BlockTag::Latest)), + "pending" => Ok(BlockId::Tag(BlockTag::Pending)), + _ => { + let block_number = block_number.parse().map_err(|e: std::num::ParseIntError| { + StarknetError::new(StarknetErrorCode::MalformedRequest, e.to_string()) + })?; + Ok(BlockId::Number(block_number)) + } + } + } else if let Some(block_hash) = params.get("blockHash") { + let block_hash = Felt::from_hex(block_hash) + .map_err(|e| StarknetError::new(StarknetErrorCode::MalformedRequest, e.to_string()))?; + Ok(BlockId::Hash(block_hash)) + } else { + Err(StarknetError::new(StarknetErrorCode::MalformedRequest, "block_number or block_hash not found".to_string())) + } +} + +impl From for hyper::Response { + fn from(error: StarknetError) -> Self { + create_json_response(hyper::StatusCode::BAD_REQUEST, &error) + } +} diff --git a/crates/client/gateway/src/server/mod.rs b/crates/client/gateway/src/server/mod.rs new file mode 100644 index 000000000..5488a4645 --- /dev/null +++ b/crates/client/gateway/src/server/mod.rs @@ -0,0 +1,4 @@ +mod handler; +mod helpers; +mod router; +pub mod worker; diff --git a/crates/client/gateway/src/server/router.rs b/crates/client/gateway/src/server/router.rs new file mode 100644 index 000000000..4e3367098 --- /dev/null +++ b/crates/client/gateway/src/server/router.rs @@ -0,0 +1,41 @@ +use std::{convert::Infallible, sync::Arc}; + +use hyper::{Body, Method, Request, Response}; +use mc_db::MadaraBackend; + +use super::handler::{handle_get_block, handle_get_state_update}; +use super::helpers::{not_found_response, not_implemented_response, service_unavailable_response}; + +// Main router to redirect to the appropriate sub-router +pub(crate) async fn main_router( + req: Request, + backend: Arc, + feeder_gateway_enable: bool, + gateway_enable: bool, +) -> Result, Infallible> { + match (req.uri().path(), feeder_gateway_enable, gateway_enable) { + ("/health", _, _) => Ok(Response::new(Body::from("OK"))), + (path, true, _) if path.starts_with("/feeder_gateway/") => feeder_gateway_router(req, backend).await, + (path, _, true) if path.starts_with("/feeder/") => gateway_router(req, backend).await, + (path, false, _) if path.starts_with("/feeder_gateway/") => Ok(service_unavailable_response("Feeder Gateway")), + (path, _, false) if path.starts_with("/feeder/") => Ok(service_unavailable_response("Feeder")), + _ => Ok(not_found_response()), + } +} + +// Router for requests related to feeder_gateway +async fn feeder_gateway_router(req: Request, backend: Arc) -> Result, Infallible> { + match (req.method(), req.uri().path()) { + (&Method::GET, "/feeder_gateway/get_block") => Ok(handle_get_block(req, backend).await), + (&Method::GET, "/feeder_gateway/get_state_update") => Ok(handle_get_state_update(req, backend).await), + _ => Ok(not_found_response()), + } +} + +// Router for requests related to feeder +async fn gateway_router(req: Request, _backend: Arc) -> Result, Infallible> { + match (req.method(), req.uri().path()) { + (&Method::POST, "/feeder/add_transaction") => Ok(not_implemented_response()), + _ => Ok(not_found_response()), + } +} diff --git a/crates/client/gateway/src/server/worker.rs b/crates/client/gateway/src/server/worker.rs new file mode 100644 index 000000000..b6a55744c --- /dev/null +++ b/crates/client/gateway/src/server/worker.rs @@ -0,0 +1,58 @@ +use std::{ + convert::Infallible, + net::{Ipv4Addr, SocketAddr}, + sync::Arc, +}; + +use anyhow::Context; +use hyper::{ + service::{make_service_fn, service_fn}, + Server, +}; +use mc_db::MadaraBackend; +use mp_utils::graceful_shutdown; +use tokio::net::TcpListener; + +use super::router::main_router; + +pub async fn start_server( + db_backend: Arc, + // add_transaction_provider: Arc, + feeder_gateway_enable: bool, + gateway_enable: bool, + gateway_external: bool, + gateway_port: u16, +) -> anyhow::Result<()> { + if !feeder_gateway_enable && !gateway_enable { + return Ok(()); + } + + let listen_addr = if gateway_external { + Ipv4Addr::UNSPECIFIED // listen on 0.0.0.0 + } else { + Ipv4Addr::LOCALHOST + }; + let addr = SocketAddr::new(listen_addr.into(), gateway_port); + + let socket = TcpListener::bind(addr).await.with_context(|| format!("Opening socket server at {addr}"))?; + + let listener = hyper::server::conn::AddrIncoming::from_listener(socket) + .with_context(|| format!("Opening socket server at {addr}"))?; + + let make_service = make_service_fn(move |_| { + let db_backend = Arc::clone(&db_backend); + async move { + Ok::<_, Infallible>(service_fn(move |req| { + main_router(req, Arc::clone(&db_backend), feeder_gateway_enable, gateway_enable) + })) + } + }); + + log::info!("🌐 Gateway endpoint started at {}", listener.local_addr()); + + let server = Server::builder(listener).serve(make_service).with_graceful_shutdown(graceful_shutdown()); + + server.await.context("gateway server")?; + + Ok(()) +} diff --git a/crates/client/rpc/src/test_utils.rs b/crates/client/rpc/src/test_utils.rs index db764c63a..f1c64713e 100644 --- a/crates/client/rpc/src/test_utils.rs +++ b/crates/client/rpc/src/test_utils.rs @@ -175,14 +175,14 @@ pub fn make_sample_chain_for_block_getters(backend: &MadaraBackend) -> SampleCha block_number: 0, transaction_count: 1, global_state_root: Felt::from_hex_unchecked("0x88912"), - sequencer_address: Felt::from_hex_unchecked("0xbabaa"), + sequencer_address: Some(Felt::from_hex_unchecked("0xbabaa")), block_timestamp: 43, transaction_commitment: Felt::from_hex_unchecked("0xbabaa0"), event_count: 0, event_commitment: Felt::from_hex_unchecked("0xb"), - state_diff_length: 5, - state_diff_commitment: Felt::from_hex_unchecked("0xb1"), - receipt_commitment: Felt::from_hex_unchecked("0xb4"), + state_diff_length: Some(5), + state_diff_commitment: Some(Felt::from_hex_unchecked("0xb1")), + receipt_commitment: Some(Felt::from_hex_unchecked("0xb4")), protocol_version: StarknetVersion::V0_13_1_1, l1_gas_price: GasPrices { eth_l1_gas_price: 123, diff --git a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_receipts.rs b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_receipts.rs index c3c062702..ef21d7f4b 100644 --- a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_receipts.rs +++ b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_receipts.rs @@ -54,7 +54,7 @@ pub fn get_block_with_receipts( block_number: block.header.block_number, new_root: block.header.global_state_root, timestamp: block.header.block_timestamp, - sequencer_address: block.header.sequencer_address, + sequencer_address: block.header.sequencer_address.unwrap_or_default(), l1_gas_price: block.header.l1_gas_price.l1_gas_price(), l1_data_gas_price: block.header.l1_gas_price.l1_data_gas_price(), l1_da_mode: block.header.l1_da_mode.into(), diff --git a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_tx_hashes.rs b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_tx_hashes.rs index 59505bd63..c66665dba 100644 --- a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_tx_hashes.rs +++ b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_tx_hashes.rs @@ -54,7 +54,7 @@ pub fn get_block_with_tx_hashes( block_number: block.header.block_number, new_root: block.header.global_state_root, timestamp: block.header.block_timestamp, - sequencer_address: block.header.sequencer_address, + sequencer_address: block.header.sequencer_address.unwrap_or_default(), l1_gas_price: block.header.l1_gas_price.l1_gas_price(), l1_data_gas_price: block.header.l1_gas_price.l1_data_gas_price(), l1_da_mode: block.header.l1_da_mode.into(), diff --git a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_txs.rs b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_txs.rs index dcd279919..e2bdb2c99 100644 --- a/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_txs.rs +++ b/crates/client/rpc/src/versions/v0_7_1/methods/read/get_block_with_txs.rs @@ -57,7 +57,7 @@ pub fn get_block_with_txs(starknet: &Starknet, block_id: BlockId) -> RpcResult anyhow::Result<()> { ) .context("Initializing rpc service")?; + let gateway_service = + GatewayService::new(&run_cmd.gateway_params, &db_service).await.context("Initializing gateway service")?; + telemetry_service.send_connected(&node_name, node_version, &chain_config.chain_name, &sys_info); let app = ServiceGroup::default() @@ -151,6 +154,7 @@ async fn main() -> anyhow::Result<()> { .with(l1_service) .with(block_provider_service) .with(rpc_service) + .with(gateway_service) .with(telemetry_service) .with(prometheus_service); diff --git a/crates/node/src/service/gateway.rs b/crates/node/src/service/gateway.rs new file mode 100644 index 000000000..a14a02f7b --- /dev/null +++ b/crates/node/src/service/gateway.rs @@ -0,0 +1,48 @@ +use crate::cli::GatewayParams; +use mc_db::{DatabaseService, MadaraBackend}; +use mp_utils::service::Service; +use std::sync::Arc; +use tokio::task::JoinSet; + +#[derive(Clone)] +pub struct GatewayService { + db_backend: Arc, + feeder_gateway_enable: bool, + gateway_enable: bool, + gateway_external: bool, + gateway_port: u16, +} + +impl GatewayService { + pub async fn new(config: &GatewayParams, db: &DatabaseService) -> anyhow::Result { + Ok(Self { + db_backend: Arc::clone(db.backend()), + feeder_gateway_enable: config.feeder_gateway_enable, + gateway_enable: config.gateway_enable, + gateway_external: config.gateway_external, + gateway_port: config.gateway_port, + }) + } +} + +#[async_trait::async_trait] +impl Service for GatewayService { + async fn start(&mut self, join_set: &mut JoinSet>) -> anyhow::Result<()> { + if self.feeder_gateway_enable || self.gateway_enable { + let GatewayService { db_backend, feeder_gateway_enable, gateway_enable, gateway_external, gateway_port } = + self.clone(); + + join_set.spawn(async move { + mc_gateway::server::worker::start_server( + db_backend, + feeder_gateway_enable, + gateway_enable, + gateway_external, + gateway_port, + ) + .await + }); + } + Ok(()) + } +} diff --git a/crates/node/src/service/mod.rs b/crates/node/src/service/mod.rs index 58aeddd33..0f5c8d1b9 100644 --- a/crates/node/src/service/mod.rs +++ b/crates/node/src/service/mod.rs @@ -1,9 +1,11 @@ mod block_production; +mod gateway; mod l1; mod rpc; mod sync; pub use block_production::BlockProductionService; +pub use gateway::GatewayService; pub use l1::L1SyncService; pub use rpc::RpcService; pub use sync::SyncService; diff --git a/crates/primitives/block/src/header.rs b/crates/primitives/block/src/header.rs index 7f3a15284..06dc194b3 100644 --- a/crates/primitives/block/src/header.rs +++ b/crates/primitives/block/src/header.rs @@ -55,7 +55,7 @@ pub struct Header { /// The state commitment after this block. pub global_state_root: Felt, /// The Starknet address of the sequencer who created this block. - pub sequencer_address: Felt, + pub sequencer_address: Option, /// The time the sequencer created this block before executing transactions pub block_timestamp: u64, /// The number of transactions in a block @@ -66,9 +66,12 @@ pub struct Header { pub event_count: u64, /// A commitment to the events produced in this block pub event_commitment: Felt, - pub state_diff_length: u64, - pub state_diff_commitment: Felt, - pub receipt_commitment: Felt, + /// The number of state diff elements + pub state_diff_length: Option, + /// A commitment to the state diff elements + pub state_diff_commitment: Option, + /// A commitment to the receipts produced in this block + pub receipt_commitment: Option, /// The version of the Starknet protocol used when creating this block pub protocol_version: StarknetVersion, /// Gas prices for this block @@ -114,6 +117,7 @@ impl GasPrices { } #[derive(Copy, Clone, Debug, Default, Eq, PartialEq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub enum L1DataAvailabilityMode { Calldata, #[default] @@ -153,15 +157,15 @@ impl Header { parent_block_hash: Felt, block_number: u64, global_state_root: Felt, - sequencer_address: Felt, + sequencer_address: Option, block_timestamp: u64, transaction_count: u64, transaction_commitment: Felt, event_count: u64, event_commitment: Felt, - state_diff_length: u64, - state_diff_commitment: Felt, - receipt_commitment: Felt, + state_diff_length: Option, + state_diff_commitment: Option, + receipt_commitment: Option, protocol_version: StarknetVersion, gas_prices: GasPrices, l1_da_mode: L1DataAvailabilityMode, @@ -191,30 +195,35 @@ impl Header { self.compute_hash_inner_pre_v0_7(chain_id) } else if self.protocol_version < StarknetVersion::V0_13_2 { Pedersen::hash_array(&[ - Felt::from(self.block_number), // block number - self.global_state_root, // global state root - self.sequencer_address, // sequencer address - Felt::from(self.block_timestamp), // block timestamp - Felt::from(self.transaction_count), // number of transactions - self.transaction_commitment, // transaction commitment - Felt::from(self.event_count), // number of events - self.event_commitment, // event commitment - Felt::ZERO, // reserved: protocol version - Felt::ZERO, // reserved: extra data - self.parent_block_hash, // parent block hash + Felt::from(self.block_number), + self.global_state_root, + self.sequencer_address.unwrap_or_default(), + Felt::from(self.block_timestamp), + Felt::from(self.transaction_count), + self.transaction_commitment, + Felt::from(self.event_count), + self.event_commitment, + Felt::ZERO, // reserved: protocol version + Felt::ZERO, // reserved: extra data + self.parent_block_hash, ]) } else { Poseidon::hash_array(&[ Felt::from_bytes_be_slice(b"STARKNET_BLOCK_HASH0"), Felt::from(self.block_number), self.global_state_root, - self.sequencer_address, + self.sequencer_address.unwrap_or_default(), Felt::from(self.block_timestamp), - concat_counts(self.transaction_count, self.event_count, self.state_diff_length, self.l1_da_mode), - self.state_diff_commitment, + concat_counts( + self.transaction_count, + self.event_count, + self.state_diff_length.unwrap_or(0), + self.l1_da_mode, + ), + self.state_diff_commitment.unwrap_or(Felt::ZERO), self.transaction_commitment, self.event_commitment, - self.receipt_commitment, + self.receipt_commitment.unwrap_or(Felt::ZERO), self.l1_gas_price.eth_l1_gas_price.into(), self.l1_gas_price.strk_l1_gas_price.into(), self.l1_gas_price.eth_l1_data_gas_price.into(), @@ -285,15 +294,15 @@ mod tests { Felt::from(1), 2, Felt::from(3), - Felt::from(4), + Some(Felt::from(4)), 5, 6, Felt::from(7), 8, Felt::from(9), - 10, - Felt::from(11), - Felt::from(12), + Some(10), + Some(Felt::from(11)), + Some(Felt::from(12)), "0.13.2".parse().unwrap(), GasPrices { eth_l1_gas_price: 14, @@ -339,15 +348,15 @@ mod tests { parent_block_hash: Felt::from(1), block_number: 2, global_state_root: Felt::from(3), - sequencer_address: Felt::from(4), + sequencer_address: Some(Felt::from(4)), block_timestamp: 5, transaction_count: 6, transaction_commitment: Felt::from(7), event_count: 8, event_commitment: Felt::from(9), - state_diff_length: 10, - state_diff_commitment: Felt::from(11), - receipt_commitment: Felt::from(12), + state_diff_length: Some(10), + state_diff_commitment: Some(Felt::from(11)), + receipt_commitment: Some(Felt::from(12)), protocol_version, l1_gas_price: GasPrices { eth_l1_gas_price: 14, diff --git a/crates/primitives/block/src/lib.rs b/crates/primitives/block/src/lib.rs index 1a525ccb4..cef33a44a 100644 --- a/crates/primitives/block/src/lib.rs +++ b/crates/primitives/block/src/lib.rs @@ -182,6 +182,12 @@ pub struct MadaraMaybePendingBlock { pub inner: MadaraBlockInner, } +impl MadaraMaybePendingBlock { + pub fn is_pending(&self) -> bool { + matches!(self.info, MadaraMaybePendingBlockInfo::Pending(_)) + } +} + /// Starknet block definition. #[derive(Clone, Debug, serde::Serialize, serde::Deserialize)] pub struct MadaraBlock { diff --git a/crates/primitives/convert/Cargo.toml b/crates/primitives/convert/Cargo.toml index 9e7201895..b366807dc 100644 --- a/crates/primitives/convert/Cargo.toml +++ b/crates/primitives/convert/Cargo.toml @@ -11,6 +11,8 @@ homepage.workspace = true [dependencies] # Starknet +serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } starknet-core = { workspace = true } starknet-types-core = { workspace = true } starknet_api = { workspace = true } diff --git a/crates/primitives/convert/src/hex_serde.rs b/crates/primitives/convert/src/hex_serde.rs new file mode 100644 index 000000000..0d08d93b0 --- /dev/null +++ b/crates/primitives/convert/src/hex_serde.rs @@ -0,0 +1,44 @@ +use serde::{Deserialize, Deserializer, Serializer}; +use serde_with::{DeserializeAs, SerializeAs}; + +pub struct U64AsHex; + +impl SerializeAs for U64AsHex { + fn serialize_as(value: &u64, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("0x{:x}", value)) + } +} + +impl<'de> DeserializeAs<'de, u64> for U64AsHex { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + u64::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) + } +} + +pub struct U128AsHex; + +impl SerializeAs for U128AsHex { + fn serialize_as(value: &u128, serializer: S) -> Result + where + S: Serializer, + { + serializer.serialize_str(&format!("0x{:x}", value)) + } +} + +impl<'de> DeserializeAs<'de, u128> for U128AsHex { + fn deserialize_as(deserializer: D) -> Result + where + D: Deserializer<'de>, + { + let s = String::deserialize(deserializer)?; + u128::from_str_radix(s.trim_start_matches("0x"), 16).map_err(serde::de::Error::custom) + } +} diff --git a/crates/primitives/convert/src/lib.rs b/crates/primitives/convert/src/lib.rs index d2c24b701..6361eb9db 100644 --- a/crates/primitives/convert/src/lib.rs +++ b/crates/primitives/convert/src/lib.rs @@ -1,4 +1,5 @@ mod felt; +pub mod hex_serde; mod to_felt; pub use felt::{felt_to_u128, felt_to_u64}; diff --git a/crates/primitives/gateway/Cargo.toml b/crates/primitives/gateway/Cargo.toml new file mode 100644 index 000000000..635146de4 --- /dev/null +++ b/crates/primitives/gateway/Cargo.toml @@ -0,0 +1,30 @@ +[package] +description = "Madara primitives for gateway" +name = "mp-gateway" +authors.workspace = true +edition.workspace = true +license.workspace = true +repository.workspace = true +version.workspace = true +homepage.workspace = true + +[package.metadata.docs.rs] +targets = ["x86_64-unknown-linux-gnu"] + +[dependencies] + +# Deoxys +mp-block = { workspace = true } +mp-chain-config = { workspace = true } +mp-convert = { workspace = true } +mp-receipt = { workspace = true } +mp-state-update = { workspace = true } +mp-transactions = { workspace = true } + +# Starknet +starknet-types-core = { workspace = true } + +# Other +serde = { workspace = true, features = ["derive"] } +serde_json = { workspace = true } +serde_with = { workspace = true } diff --git a/crates/primitives/gateway/src/block.rs b/crates/primitives/gateway/src/block.rs new file mode 100644 index 000000000..640c69b1e --- /dev/null +++ b/crates/primitives/gateway/src/block.rs @@ -0,0 +1,199 @@ +use mp_block::header::L1DataAvailabilityMode; +use mp_chain_config::StarknetVersion; +use serde::{Deserialize, Serialize}; +use starknet_types_core::felt::Felt; + +use super::{receipt::ConfirmedReceipt, transaction::Transaction}; + +#[derive(Debug, Clone, PartialEq, Serialize)] // no Deserialize because it's untagged +#[serde(untagged)] +#[allow(clippy::large_enum_variant)] +pub enum ProviderMaybePendingBlock { + Block(BlockProvider), + Pending(PendingBlockProvider), +} + +impl ProviderMaybePendingBlock { + pub fn block(&self) -> Option<&BlockProvider> { + match self { + ProviderMaybePendingBlock::Block(block) => Some(block), + ProviderMaybePendingBlock::Pending(_) => None, + } + } + + pub fn pending(&self) -> Option<&PendingBlockProvider> { + match self { + ProviderMaybePendingBlock::Block(_) => None, + ProviderMaybePendingBlock::Pending(pending) => Some(pending), + } + } + + pub fn parent_block_hash(&self) -> Felt { + match self { + ProviderMaybePendingBlock::Block(block) => block.parent_block_hash, + ProviderMaybePendingBlock::Pending(pending) => pending.parent_block_hash, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct BlockProvider { + pub block_hash: Felt, + pub block_number: u64, + pub parent_block_hash: Felt, + pub timestamp: u64, + #[serde(default)] + pub sequencer_address: Option, + #[serde(alias = "state_root")] + pub state_commitment: Felt, + pub transaction_commitment: Felt, + pub event_commitment: Felt, + #[serde(default)] + pub receipt_commitment: Option, + #[serde(default)] + pub state_diff_commitment: Option, + #[serde(default)] + pub state_diff_length: Option, + pub status: BlockStatus, + pub l1_da_mode: L1DataAvailabilityMode, + pub l1_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub transactions: Vec, + pub transaction_receipts: Vec, + #[serde(default)] + pub starknet_version: Option, +} + +impl BlockProvider { + pub fn new(block: mp_block::MadaraBlock, status: BlockStatus) -> Self { + let starknet_version = starknet_version(block.info.header.protocol_version); + + let transactions = block + .inner + .transactions + .into_iter() + .zip(block.inner.receipts.iter().map(|receipt| (receipt.transaction_hash(), receipt.contract_address()))) + .map(|(transaction, (hash, contract_address))| { + let transaction_with_hash = mp_transactions::TransactionWithHash { transaction, hash }; + Transaction::new(transaction_with_hash, contract_address) + }) + .collect(); + + let transaction_receipts = receipts(block.inner.receipts); + + Self { + block_hash: block.info.block_hash, + block_number: block.info.header.block_number, + parent_block_hash: block.info.header.parent_block_hash, + timestamp: block.info.header.block_timestamp, + sequencer_address: block.info.header.sequencer_address, + state_commitment: block.info.header.global_state_root, + transaction_commitment: block.info.header.transaction_commitment, + event_commitment: block.info.header.event_commitment, + receipt_commitment: block.info.header.receipt_commitment, + state_diff_commitment: block.info.header.state_diff_commitment, + state_diff_length: block.info.header.state_diff_length, + status, + l1_da_mode: block.info.header.l1_da_mode, + l1_gas_price: ResourcePrice { + price_in_wei: block.info.header.l1_gas_price.strk_l1_gas_price, + price_in_fri: block.info.header.l1_gas_price.eth_l1_gas_price, + }, + l1_data_gas_price: ResourcePrice { + price_in_wei: block.info.header.l1_gas_price.strk_l1_data_gas_price, + price_in_fri: block.info.header.l1_gas_price.eth_l1_data_gas_price, + }, + transactions, + transaction_receipts, + starknet_version, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct PendingBlockProvider { + pub parent_block_hash: Felt, + pub status: BlockStatus, + pub l1_da_mode: L1DataAvailabilityMode, + pub l1_gas_price: ResourcePrice, + pub l1_data_gas_price: ResourcePrice, + pub transactions: Vec, + pub timestamp: u64, + #[serde(default)] + pub sequencer_address: Felt, + pub transaction_receipts: Vec, + #[serde(default)] + pub starknet_version: Option, +} + +impl PendingBlockProvider { + pub fn new(block: mp_block::MadaraPendingBlock) -> Self { + let starknet_version = starknet_version(block.info.header.protocol_version); + + let transactions = block + .inner + .transactions + .into_iter() + .zip(block.inner.receipts.iter().map(|receipt| (receipt.transaction_hash(), receipt.contract_address()))) + .map(|(transaction, (hash, contract_address))| { + let transaction_with_hash = mp_transactions::TransactionWithHash { transaction, hash }; + Transaction::new(transaction_with_hash, contract_address) + }) + .collect(); + + let transaction_receipts = receipts(block.inner.receipts); + + Self { + parent_block_hash: block.info.header.parent_block_hash, + status: BlockStatus::Pending, + l1_da_mode: block.info.header.l1_da_mode, + l1_gas_price: ResourcePrice { + price_in_wei: block.info.header.l1_gas_price.strk_l1_gas_price, + price_in_fri: block.info.header.l1_gas_price.eth_l1_gas_price, + }, + l1_data_gas_price: ResourcePrice { + price_in_wei: block.info.header.l1_gas_price.strk_l1_data_gas_price, + price_in_fri: block.info.header.l1_gas_price.eth_l1_data_gas_price, + }, + transactions, + timestamp: block.info.header.block_timestamp, + sequencer_address: block.info.header.sequencer_address, + transaction_receipts, + starknet_version, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +pub struct ResourcePrice { + pub price_in_wei: u128, + pub price_in_fri: u128, +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +#[serde(deny_unknown_fields)] +pub enum BlockStatus { + Pending, + Aborted, + Reverted, + AcceptedOnL2, + AcceptedOnL1, +} + +fn starknet_version(version: StarknetVersion) -> Option { + match version { + version if version < StarknetVersion::V0_9_1 => None, + version => Some(version.to_string()), + } +} + +fn receipts(receipts: Vec) -> Vec { + receipts + .into_iter() + .enumerate() + .map(|(index, receipt)| ConfirmedReceipt::new(receipt, None, index as u64)) + .collect() +} diff --git a/crates/primitives/gateway/src/lib.rs b/crates/primitives/gateway/src/lib.rs new file mode 100644 index 000000000..84fdc36b6 --- /dev/null +++ b/crates/primitives/gateway/src/lib.rs @@ -0,0 +1,4 @@ +pub mod block; +pub mod receipt; +pub mod state_update; +pub mod transaction; diff --git a/crates/primitives/gateway/src/receipt.rs b/crates/primitives/gateway/src/receipt.rs new file mode 100644 index 000000000..515bbbed2 --- /dev/null +++ b/crates/primitives/gateway/src/receipt.rs @@ -0,0 +1,115 @@ +use mp_block::H160; +use mp_receipt::{Event, L1Gas, MsgToL1}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use starknet_types_core::felt::Felt; + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ConfirmedReceipt { + pub transaction_hash: Felt, + pub transaction_index: u64, + pub actual_fee: Felt, + pub execution_resources: ExecutionResources, + pub l2_to_l1_messages: Vec, + pub l1_to_l2_consumed_message: Option, + pub events: Vec, + pub execution_status: ExecutionStatus, + #[serde(default)] + #[serde(skip_serializing_if = "Option::is_none")] + pub revert_error: Option, +} + +impl ConfirmedReceipt { + pub fn new( + transaction_receipt: mp_receipt::TransactionReceipt, + l1_to_l2_consumed_message: Option, + index: u64, + ) -> Self { + let (execution_status, revert_error) = match transaction_receipt.execution_result() { + mp_receipt::ExecutionResult::Succeeded => (ExecutionStatus::Succeeded, None), + mp_receipt::ExecutionResult::Reverted { reason } => (ExecutionStatus::Reverted, Some(reason)), + }; + + Self { + transaction_hash: transaction_receipt.transaction_hash(), + transaction_index: index, + actual_fee: transaction_receipt.actual_fee().amount, + execution_resources: transaction_receipt.execution_resources().clone().into(), + l2_to_l1_messages: transaction_receipt.messages_sent().to_vec(), + l1_to_l2_consumed_message, + events: transaction_receipt.events().to_vec(), + execution_status, + revert_error, + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct ExecutionResources { + pub builtin_instance_counter: BuiltinCounters, + pub n_steps: u64, + pub n_memory_holes: u64, + pub data_availability: Option, + pub total_gas_consumed: Option, +} + +impl From for ExecutionResources { + fn from(resources: mp_receipt::ExecutionResources) -> Self { + Self { + builtin_instance_counter: BuiltinCounters { + output_builtin: 0, + pedersen_builtin: resources.pedersen_builtin_applications.unwrap_or(0), + range_check_builtin: resources.range_check_builtin_applications.unwrap_or(0), + ecdsa_builtin: resources.ecdsa_builtin_applications.unwrap_or(0), + bitwise_builtin: resources.bitwise_builtin_applications.unwrap_or(0), + ec_op_builtin: resources.ec_op_builtin_applications.unwrap_or(0), + keccak_builtin: resources.keccak_builtin_applications.unwrap_or(0), + poseidon_builtin: resources.poseidon_builtin_applications.unwrap_or(0), + segment_arena_builtin: resources.segment_arena_builtin.unwrap_or(0), + add_mod_builtin: 0, + mul_mod_builtin: 0, + }, + n_steps: resources.steps, + n_memory_holes: resources.memory_holes.unwrap_or(0), + data_availability: Some(resources.data_availability), + total_gas_consumed: Some(resources.total_gas_consumed), + } + } +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize, PartialEq, Eq)] +#[serde(default)] +pub struct BuiltinCounters { + pub output_builtin: u64, + pub pedersen_builtin: u64, + pub range_check_builtin: u64, + pub ecdsa_builtin: u64, + pub bitwise_builtin: u64, + pub ec_op_builtin: u64, + pub keccak_builtin: u64, + pub poseidon_builtin: u64, + pub segment_arena_builtin: u64, + pub add_mod_builtin: u64, + pub mul_mod_builtin: u64, +} + +#[serde_as] +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct MsgToL2 { + pub from_address: H160, + pub to_address: Felt, + pub selector: Felt, + pub payload: Vec, + pub nonce: Option, +} + +#[derive(Clone, Default, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] +pub enum ExecutionStatus { + #[default] + Succeeded, + Reverted, +} diff --git a/crates/primitives/gateway/src/state_update.rs b/crates/primitives/gateway/src/state_update.rs new file mode 100644 index 000000000..7d1951392 --- /dev/null +++ b/crates/primitives/gateway/src/state_update.rs @@ -0,0 +1,86 @@ +use std::collections::HashMap; + +use mp_state_update::{DeclaredClassItem, DeployedContractItem, StorageEntry}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use starknet_types_core::felt::Felt; + +#[derive(Debug, Clone, PartialEq, Serialize)] // no Deserialize because it's untagged +#[serde(untagged)] +pub enum ProviderMaybePendingStateUpdate { + Update(StateUpdateProvider), + Pending(PendingStateUpdateProvider), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct StateUpdateProvider { + pub block_hash: Felt, + pub new_root: Felt, + pub old_root: Felt, + pub state_diff: StateDiff, +} + +impl From for StateUpdateProvider { + fn from(state_update: mp_state_update::StateUpdate) -> Self { + Self { + block_hash: state_update.block_hash, + new_root: state_update.new_root, + old_root: state_update.old_root, + state_diff: state_update.state_diff.into(), + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct PendingStateUpdateProvider { + pub old_root: Felt, + pub state_diff: StateDiff, +} + +impl From for PendingStateUpdateProvider { + fn from(pending_state_update: mp_state_update::PendingStateUpdate) -> Self { + Self { old_root: pending_state_update.old_root, state_diff: pending_state_update.state_diff.into() } + } +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq, Default)] +#[serde(deny_unknown_fields)] +pub struct StateDiff { + pub storage_diffs: HashMap>, + pub deployed_contracts: Vec, + pub old_declared_contracts: Vec, + pub declared_classes: Vec, + pub nonces: HashMap, + pub replaced_classes: Vec, +} + +impl From for StateDiff { + fn from(state_diff: mp_state_update::StateDiff) -> Self { + Self { + storage_diffs: state_diff + .storage_diffs + .into_iter() + .map(|mp_state_update::ContractStorageDiffItem { address, storage_entries }| (address, storage_entries)) + .collect(), + deployed_contracts: state_diff.deployed_contracts, + old_declared_contracts: state_diff.deprecated_declared_classes, + declared_classes: state_diff.declared_classes, + nonces: state_diff + .nonces + .into_iter() + .map(|mp_state_update::NonceUpdate { contract_address, nonce }| (contract_address, nonce)) + .collect(), + replaced_classes: state_diff + .replaced_classes + .into_iter() + .map(|mp_state_update::ReplacedClassItem { contract_address, class_hash }| DeployedContractItem { + address: contract_address, + class_hash, + }) + .collect(), + } + } +} diff --git a/crates/primitives/gateway/src/test.rs b/crates/primitives/gateway/src/test.rs new file mode 100644 index 000000000..32a327af7 --- /dev/null +++ b/crates/primitives/gateway/src/test.rs @@ -0,0 +1,24 @@ + + + + + + +#[cfg(test)] +mod tests { + use std::fmt::Debug; + + use serde::de::DeserializeOwned; + + use super::*; + + fn test_serialize_deserialize(value: T) -> T + where + T: Serialize + DeserializeOwned + Clone + Debug + PartialEq, + { + let serialized = serde_json::to_string(&value).unwrap(); + let deserialized: T = serde_json::from_str(&serialized).unwrap(); + assert_eq!(value, deserialized); + deserialized + } +} \ No newline at end of file diff --git a/crates/primitives/gateway/src/transaction.rs b/crates/primitives/gateway/src/transaction.rs new file mode 100644 index 000000000..27e8f5331 --- /dev/null +++ b/crates/primitives/gateway/src/transaction.rs @@ -0,0 +1,422 @@ +use mp_convert::hex_serde::U64AsHex; +use mp_transactions::{DataAvailabilityMode, ResourceBoundsMapping}; +use serde::{Deserialize, Serialize}; +use serde_with::serde_as; +use starknet_types_core::felt::Felt; + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "type")] +#[serde(deny_unknown_fields)] +pub enum Transaction { + #[serde(rename = "INVOKE_FUNCTION")] + Invoke(InvokeTransaction), + #[serde(rename = "L1_HANDLER")] + L1Handler(L1HandlerTransaction), + #[serde(rename = "DECLARE")] + Declare(DeclareTransaction), + #[serde(rename = "DEPLOY")] + Deploy(DeployTransaction), + #[serde(rename = "DEPLOY_ACCOUNT")] + DeployAccount(DeployAccountTransaction), +} + +impl Transaction { + pub fn new( + mp_transactions::TransactionWithHash { transaction, hash }: mp_transactions::TransactionWithHash, + contract_address: Option, + ) -> Self { + match transaction { + mp_transactions::Transaction::Invoke(mp_transactions::InvokeTransaction::V0(tx)) => { + Transaction::Invoke(InvokeTransaction::V0(InvokeTransactionV0::new(tx, hash))) + } + mp_transactions::Transaction::Invoke(mp_transactions::InvokeTransaction::V1(tx)) => { + Transaction::Invoke(InvokeTransaction::V1(InvokeTransactionV1::new(tx, hash))) + } + mp_transactions::Transaction::Invoke(mp_transactions::InvokeTransaction::V3(tx)) => { + Transaction::Invoke(InvokeTransaction::V3(InvokeTransactionV3::new(tx, hash))) + } + mp_transactions::Transaction::L1Handler(tx) => Transaction::L1Handler(L1HandlerTransaction::new(tx, hash)), + mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V0(tx)) => { + Transaction::Declare(DeclareTransaction::V0(DeclareTransactionV0::new(tx, hash))) + } + mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V1(tx)) => { + Transaction::Declare(DeclareTransaction::V1(DeclareTransactionV1::new(tx, hash))) + } + mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V2(tx)) => { + Transaction::Declare(DeclareTransaction::V2(DeclareTransactionV2::new(tx, hash))) + } + mp_transactions::Transaction::Declare(mp_transactions::DeclareTransaction::V3(tx)) => { + Transaction::Declare(DeclareTransaction::V3(DeclareTransactionV3::new(tx, hash))) + } + mp_transactions::Transaction::Deploy(tx) => { + Transaction::Deploy(DeployTransaction::new(tx, hash, contract_address.unwrap_or_default())) + } + mp_transactions::Transaction::DeployAccount(mp_transactions::DeployAccountTransaction::V1(tx)) => { + Transaction::DeployAccount(DeployAccountTransaction::V1(DeployAccountTransactionV1::new( + tx, + hash, + contract_address.unwrap_or_default(), + ))) + } + mp_transactions::Transaction::DeployAccount(mp_transactions::DeployAccountTransaction::V3(tx)) => { + Transaction::DeployAccount(DeployAccountTransaction::V3(DeployAccountTransactionV3::new( + tx, + hash, + contract_address.unwrap_or_default(), + ))) + } + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "version")] +pub enum InvokeTransaction { + #[serde(rename = "0x0")] + V0(InvokeTransactionV0), + #[serde(rename = "0x1")] + V1(InvokeTransactionV1), + #[serde(rename = "0x3")] + V3(InvokeTransactionV3), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct InvokeTransactionV0 { + #[serde(alias = "contract_address")] + pub sender_address: Felt, + pub entry_point_selector: Felt, + pub calldata: Vec, + pub signature: Vec, + pub max_fee: Felt, + pub transaction_hash: Felt, +} + +impl InvokeTransactionV0 { + pub fn new(transaction: mp_transactions::InvokeTransactionV0, hash: Felt) -> Self { + Self { + sender_address: transaction.contract_address, + entry_point_selector: transaction.entry_point_selector, + calldata: transaction.calldata, + signature: transaction.signature, + max_fee: transaction.max_fee, + transaction_hash: hash, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct InvokeTransactionV1 { + pub sender_address: Felt, + pub calldata: Vec, + pub signature: Vec, + pub max_fee: Felt, + pub nonce: Felt, + pub transaction_hash: Felt, +} + +impl InvokeTransactionV1 { + pub fn new(transaction: mp_transactions::InvokeTransactionV1, hash: Felt) -> Self { + Self { + sender_address: transaction.sender_address, + calldata: transaction.calldata, + signature: transaction.signature, + max_fee: transaction.max_fee, + nonce: transaction.nonce, + transaction_hash: hash, + } + } +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct InvokeTransactionV3 { + pub nonce: Felt, + pub nonce_data_availability_mode: DataAvailabilityMode, + pub fee_data_availability_mode: DataAvailabilityMode, + pub resource_bounds: ResourceBoundsMapping, + #[serde_as(as = "U64AsHex")] + pub tip: u64, + pub paymaster_data: Vec, + pub sender_address: Felt, + pub signature: Vec, + pub transaction_hash: Felt, + pub calldata: Vec, + pub account_deployment_data: Vec, +} + +impl InvokeTransactionV3 { + pub fn new(transaction: mp_transactions::InvokeTransactionV3, hash: Felt) -> Self { + Self { + nonce: transaction.nonce, + nonce_data_availability_mode: transaction.nonce_data_availability_mode, + fee_data_availability_mode: transaction.fee_data_availability_mode, + resource_bounds: transaction.resource_bounds, + tip: transaction.tip, + paymaster_data: transaction.paymaster_data, + sender_address: transaction.sender_address, + signature: transaction.signature, + transaction_hash: hash, + calldata: transaction.calldata, + account_deployment_data: transaction.account_deployment_data, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct L1HandlerTransaction { + pub contract_address: Felt, + pub entry_point_selector: Felt, + #[serde(default)] + pub nonce: Felt, + pub calldata: Vec, + pub transaction_hash: Felt, + pub version: Felt, +} + +impl L1HandlerTransaction { + pub fn new(transaction: mp_transactions::L1HandlerTransaction, hash: Felt) -> Self { + Self { + contract_address: transaction.contract_address, + entry_point_selector: transaction.entry_point_selector, + nonce: transaction.nonce.into(), + calldata: transaction.calldata, + transaction_hash: hash, + version: transaction.version, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(tag = "version")] +pub enum DeclareTransaction { + #[serde(rename = "0x0")] + V0(DeclareTransactionV0), + #[serde(rename = "0x1")] + V1(DeclareTransactionV1), + #[serde(rename = "0x2")] + V2(DeclareTransactionV2), + #[serde(rename = "0x3")] + V3(DeclareTransactionV3), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeclareTransactionV0 { + pub class_hash: Felt, + pub max_fee: Felt, + pub nonce: Felt, + pub sender_address: Felt, + #[serde(default)] + pub signature: Vec, + pub transaction_hash: Felt, +} + +impl DeclareTransactionV0 { + pub fn new(transaction: mp_transactions::DeclareTransactionV0, hash: Felt) -> Self { + Self { + class_hash: transaction.class_hash, + max_fee: transaction.max_fee, + nonce: Felt::ZERO, + sender_address: transaction.sender_address, + signature: transaction.signature, + transaction_hash: hash, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeclareTransactionV1 { + pub class_hash: Felt, + pub max_fee: Felt, + pub nonce: Felt, + pub sender_address: Felt, + #[serde(default)] + pub signature: Vec, + pub transaction_hash: Felt, +} + +impl DeclareTransactionV1 { + pub fn new(transaction: mp_transactions::DeclareTransactionV1, hash: Felt) -> Self { + Self { + class_hash: transaction.class_hash, + max_fee: transaction.max_fee, + nonce: transaction.nonce, + sender_address: transaction.sender_address, + signature: transaction.signature, + transaction_hash: hash, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeclareTransactionV2 { + pub class_hash: Felt, + pub max_fee: Felt, + pub nonce: Felt, + pub sender_address: Felt, + #[serde(default)] + pub signature: Vec, + pub transaction_hash: Felt, + pub compiled_class_hash: Felt, +} + +impl DeclareTransactionV2 { + pub fn new(transaction: mp_transactions::DeclareTransactionV2, hash: Felt) -> Self { + Self { + class_hash: transaction.class_hash, + max_fee: transaction.max_fee, + nonce: transaction.nonce, + sender_address: transaction.sender_address, + signature: transaction.signature, + transaction_hash: hash, + compiled_class_hash: transaction.compiled_class_hash, + } + } +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeclareTransactionV3 { + pub class_hash: Felt, + pub nonce: Felt, + pub nonce_data_availability_mode: DataAvailabilityMode, + pub fee_data_availability_mode: DataAvailabilityMode, + pub resource_bounds: ResourceBoundsMapping, + #[serde_as(as = "U64AsHex")] + pub tip: u64, + pub paymaster_data: Vec, + pub sender_address: Felt, + #[serde(default)] + pub signature: Vec, + pub transaction_hash: Felt, + pub compiled_class_hash: Felt, + pub account_deployment_data: Vec, +} + +impl DeclareTransactionV3 { + pub fn new(transaction: mp_transactions::DeclareTransactionV3, hash: Felt) -> Self { + Self { + class_hash: transaction.class_hash, + nonce: transaction.nonce, + nonce_data_availability_mode: transaction.nonce_data_availability_mode, + fee_data_availability_mode: transaction.fee_data_availability_mode, + resource_bounds: transaction.resource_bounds, + tip: transaction.tip, + paymaster_data: transaction.paymaster_data, + sender_address: transaction.sender_address, + signature: transaction.signature, + transaction_hash: hash, + compiled_class_hash: transaction.compiled_class_hash, + account_deployment_data: transaction.account_deployment_data, + } + } +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeployTransaction { + pub constructor_calldata: Vec, + pub contract_address: Felt, + pub contract_address_salt: Felt, + pub class_hash: Felt, + pub transaction_hash: Felt, + #[serde(default)] + pub version: Felt, +} + +impl DeployTransaction { + pub fn new(transaction: mp_transactions::DeployTransaction, hash: Felt, contract_address: Felt) -> Self { + Self { + constructor_calldata: transaction.constructor_calldata, + contract_address, + contract_address_salt: transaction.contract_address_salt, + class_hash: transaction.class_hash, + transaction_hash: hash, + version: transaction.version, + } + } +} + +#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[serde(untagged)] +pub enum DeployAccountTransaction { + V1(DeployAccountTransactionV1), + V3(DeployAccountTransactionV3), +} + +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeployAccountTransactionV1 { + pub contract_address: Felt, + pub transaction_hash: Felt, + pub max_fee: Felt, + pub version: Felt, + pub signature: Vec, + pub nonce: Felt, + pub contract_address_salt: Felt, + pub constructor_calldata: Vec, + pub class_hash: Felt, +} + +impl DeployAccountTransactionV1 { + pub fn new(transaction: mp_transactions::DeployAccountTransactionV1, hash: Felt, contract_address: Felt) -> Self { + Self { + contract_address, + transaction_hash: hash, + max_fee: transaction.max_fee, + version: Felt::ONE, + signature: transaction.signature, + nonce: transaction.nonce, + contract_address_salt: transaction.contract_address_salt, + constructor_calldata: transaction.constructor_calldata, + class_hash: transaction.class_hash, + } + } +} + +#[serde_as] +#[derive(Clone, Debug, Deserialize, Serialize, PartialEq, Eq)] +#[serde(deny_unknown_fields)] +pub struct DeployAccountTransactionV3 { + pub nonce: Felt, + pub nonce_data_availability_mode: DataAvailabilityMode, + pub fee_data_availability_mode: DataAvailabilityMode, + pub resource_bounds: ResourceBoundsMapping, + #[serde_as(as = "U64AsHex")] + pub tip: u64, + pub paymaster_data: Vec, + pub sender_address: Felt, + pub signature: Vec, + pub transaction_hash: Felt, + pub version: Felt, + pub contract_address_salt: Felt, + pub constructor_calldata: Vec, + pub class_hash: Felt, +} + +impl DeployAccountTransactionV3 { + pub fn new(transaction: mp_transactions::DeployAccountTransactionV3, hash: Felt, sender_address: Felt) -> Self { + Self { + nonce: transaction.nonce, + nonce_data_availability_mode: transaction.nonce_data_availability_mode, + fee_data_availability_mode: transaction.fee_data_availability_mode, + resource_bounds: transaction.resource_bounds, + tip: transaction.tip, + paymaster_data: transaction.paymaster_data, + sender_address, + signature: transaction.signature, + transaction_hash: hash, + version: Felt::THREE, + contract_address_salt: transaction.contract_address_salt, + constructor_calldata: transaction.constructor_calldata, + class_hash: transaction.class_hash, + } + } +} diff --git a/crates/primitives/receipt/src/from_blockifier.rs b/crates/primitives/receipt/src/from_blockifier.rs index 1a50e7351..5a81463f5 100644 --- a/crates/primitives/receipt/src/from_blockifier.rs +++ b/crates/primitives/receipt/src/from_blockifier.rs @@ -7,8 +7,8 @@ use cairo_vm::types::builtin_name::BuiltinName; use starknet_types_core::felt::Felt; use crate::{ - DataAvailabilityResources, DeclareTransactionReceipt, DeployAccountTransactionReceipt, Event, ExecutionResources, - ExecutionResult, FeePayment, InvokeTransactionReceipt, MsgToL1, PriceUnit, TransactionReceipt, + DeclareTransactionReceipt, DeployAccountTransactionReceipt, Event, ExecutionResources, ExecutionResult, FeePayment, + InvokeTransactionReceipt, L1Gas, MsgToL1, PriceUnit, TransactionReceipt, }; fn blockifier_tx_fee_type(tx: &Transaction) -> FeeType { @@ -134,8 +134,8 @@ pub fn from_blockifier_execution_info(res: &TransactionExecutionInfo, tx: &Trans } } -impl From for DataAvailabilityResources { +impl From for L1Gas { fn from(value: GasVector) -> Self { - DataAvailabilityResources { l1_gas: value.l1_gas as _, l1_data_gas: value.l1_data_gas as _ } + L1Gas { l1_gas: value.l1_gas as _, l1_data_gas: value.l1_data_gas as _ } } } diff --git a/crates/primitives/receipt/src/from_starknet_provider.rs b/crates/primitives/receipt/src/from_starknet_provider.rs index bb764cf99..56e0fb97e 100644 --- a/crates/primitives/receipt/src/from_starknet_provider.rs +++ b/crates/primitives/receipt/src/from_starknet_provider.rs @@ -2,9 +2,9 @@ use mp_convert::{felt_to_u64, ToFelt}; use starknet_types_core::felt::Felt; use crate::{ - DataAvailabilityResources, DeclareTransactionReceipt, DeployAccountTransactionReceipt, DeployTransactionReceipt, - Event, ExecutionResources, ExecutionResult, FeePayment, InvokeTransactionReceipt, L1HandlerTransactionReceipt, - MsgToL1, PriceUnit, TransactionReceipt, + DeclareTransactionReceipt, DeployAccountTransactionReceipt, DeployTransactionReceipt, Event, ExecutionResources, + ExecutionResult, FeePayment, InvokeTransactionReceipt, L1Gas, L1HandlerTransactionReceipt, MsgToL1, PriceUnit, + TransactionReceipt, }; impl TransactionReceipt { @@ -154,8 +154,8 @@ impl From for Executi bitwise_builtin_applications: builtin_instance_counter.bitwise_builtin, keccak_builtin_applications: builtin_instance_counter.keccak_builtin, segment_arena_builtin: builtin_instance_counter.segment_arena_builtin, - data_availability: resources.data_availability.map(DataAvailabilityResources::from).unwrap_or_default(), - total_gas_consumed: resources.total_gas_consumed.map(DataAvailabilityResources::from).unwrap_or_default(), + data_availability: resources.data_availability.map(L1Gas::from).unwrap_or_default(), + total_gas_consumed: resources.total_gas_consumed.map(L1Gas::from).unwrap_or_default(), } } } diff --git a/crates/primitives/receipt/src/into_starknet_core.rs b/crates/primitives/receipt/src/into_starknet_core.rs index c5d67ce32..65064bd93 100644 --- a/crates/primitives/receipt/src/into_starknet_core.rs +++ b/crates/primitives/receipt/src/into_starknet_core.rs @@ -1,7 +1,7 @@ use crate::{ - DataAvailabilityResources, DeclareTransactionReceipt, DeployAccountTransactionReceipt, DeployTransactionReceipt, - Event, ExecutionResources, ExecutionResult, FeePayment, InvokeTransactionReceipt, L1HandlerTransactionReceipt, - MsgToL1, PriceUnit, TransactionReceipt, + DeclareTransactionReceipt, DeployAccountTransactionReceipt, DeployTransactionReceipt, Event, ExecutionResources, + ExecutionResult, FeePayment, InvokeTransactionReceipt, L1Gas, L1HandlerTransactionReceipt, MsgToL1, PriceUnit, + TransactionReceipt, }; impl From for TransactionReceipt { @@ -297,15 +297,15 @@ impl From for starknet_core::types::ExecutionResources { } } -impl From for DataAvailabilityResources { +impl From for L1Gas { fn from(resources: starknet_core::types::DataAvailabilityResources) -> Self { - Self { l1_gas: resources.l1_gas, l1_data_gas: resources.l1_data_gas } + Self { l1_gas: resources.l1_gas.into(), l1_data_gas: resources.l1_data_gas.into() } } } -impl From for starknet_core::types::DataAvailabilityResources { - fn from(resources: DataAvailabilityResources) -> Self { - Self { l1_gas: resources.l1_gas, l1_data_gas: resources.l1_data_gas } +impl From for starknet_core::types::DataAvailabilityResources { + fn from(resources: L1Gas) -> Self { + Self { l1_gas: resources.l1_gas as u64, l1_data_gas: resources.l1_data_gas as u64 } } } diff --git a/crates/primitives/receipt/src/lib.rs b/crates/primitives/receipt/src/lib.rs index 0ba5d3b99..5d8bc8bdc 100644 --- a/crates/primitives/receipt/src/lib.rs +++ b/crates/primitives/receipt/src/lib.rs @@ -70,7 +70,7 @@ impl TransactionReceipt { } } - pub fn data_availability(&self) -> &DataAvailabilityResources { + pub fn data_availability(&self) -> &L1Gas { match self { TransactionReceipt::Invoke(receipt) => &receipt.execution_resources.data_availability, TransactionReceipt::L1Handler(receipt) => &receipt.execution_resources.data_availability, @@ -80,7 +80,7 @@ impl TransactionReceipt { } } - pub fn total_gas_consumed(&self) -> &DataAvailabilityResources { + pub fn total_gas_consumed(&self) -> &L1Gas { match self { TransactionReceipt::Invoke(receipt) => &receipt.execution_resources.total_gas_consumed, TransactionReceipt::L1Handler(receipt) => &receipt.execution_resources.total_gas_consumed, @@ -120,6 +120,24 @@ impl TransactionReceipt { } } + pub fn execution_resources(&self) -> &ExecutionResources { + match self { + TransactionReceipt::Invoke(receipt) => &receipt.execution_resources, + TransactionReceipt::L1Handler(receipt) => &receipt.execution_resources, + TransactionReceipt::Declare(receipt) => &receipt.execution_resources, + TransactionReceipt::Deploy(receipt) => &receipt.execution_resources, + TransactionReceipt::DeployAccount(receipt) => &receipt.execution_resources, + } + } + + pub fn contract_address(&self) -> Option { + match self { + TransactionReceipt::Deploy(receipt) => Some(receipt.contract_address), + TransactionReceipt::DeployAccount(receipt) => Some(receipt.contract_address), + _ => None, + } + } + pub fn compute_hash(&self) -> Felt { Poseidon::hash_array(&[ self.transaction_hash(), @@ -217,6 +235,7 @@ pub enum PriceUnit { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct MsgToL1 { pub from_address: Felt, pub to_address: Felt, @@ -224,6 +243,7 @@ pub struct MsgToL1 { } #[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] pub struct Event { pub from_address: Felt, pub keys: Vec, @@ -264,14 +284,15 @@ pub struct ExecutionResources { pub bitwise_builtin_applications: Option, pub keccak_builtin_applications: Option, pub segment_arena_builtin: Option, - pub data_availability: DataAvailabilityResources, - pub total_gas_consumed: DataAvailabilityResources, + pub data_availability: L1Gas, + pub total_gas_consumed: L1Gas, } -#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize, Deserialize)] -pub struct DataAvailabilityResources { - pub l1_gas: u64, - pub l1_data_gas: u64, +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(deny_unknown_fields)] +pub struct L1Gas { + pub l1_gas: u128, + pub l1_data_gas: u128, } #[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] @@ -318,8 +339,8 @@ mod tests { bitwise_builtin_applications: Some(16), keccak_builtin_applications: Some(17), segment_arena_builtin: Some(18), - data_availability: DataAvailabilityResources { l1_gas: 19, l1_data_gas: 20 }, - total_gas_consumed: DataAvailabilityResources { l1_gas: 21, l1_data_gas: 22 }, + data_availability: L1Gas { l1_gas: 19, l1_data_gas: 20 }, + total_gas_consumed: L1Gas { l1_gas: 21, l1_data_gas: 22 }, }, execution_result: ExecutionResult::Succeeded, }); @@ -453,7 +474,7 @@ mod tests { bitwise_builtin_applications: Some(8), keccak_builtin_applications: Some(9), segment_arena_builtin: Some(10), - data_availability: DataAvailabilityResources { l1_gas: 11, l1_data_gas: 12 }, + data_availability: L1Gas { l1_gas: 11, l1_data_gas: 12 }, // TODO: Change with non-default values when starknet-rs supports it. total_gas_consumed: Default::default(), } diff --git a/crates/primitives/state_update/src/lib.rs b/crates/primitives/state_update/src/lib.rs index 1750657da..b43911400 100644 --- a/crates/primitives/state_update/src/lib.rs +++ b/crates/primitives/state_update/src/lib.rs @@ -159,18 +159,21 @@ impl ContractStorageDiffItem { } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct StorageEntry { pub key: Felt, pub value: Felt, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct DeclaredClassItem { pub class_hash: Felt, pub compiled_class_hash: Felt, } #[derive(Clone, Debug, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(deny_unknown_fields)] pub struct DeployedContractItem { pub address: Felt, pub class_hash: Felt, diff --git a/crates/primitives/transactions/Cargo.toml b/crates/primitives/transactions/Cargo.toml index 2951178e4..80f01ce68 100644 --- a/crates/primitives/transactions/Cargo.toml +++ b/crates/primitives/transactions/Cargo.toml @@ -32,6 +32,7 @@ starknet_api = { workspace = true } anyhow = { workspace = true } num-bigint = { workspace = true } serde = { workspace = true, features = ["derive"] } +serde_with = { workspace = true } thiserror = { workspace = true } diff --git a/crates/primitives/transactions/src/lib.rs b/crates/primitives/transactions/src/lib.rs index 22bbbd523..0db22e1a9 100644 --- a/crates/primitives/transactions/src/lib.rs +++ b/crates/primitives/transactions/src/lib.rs @@ -1,7 +1,3 @@ -use mp_convert::ToFelt; -use starknet_api::transaction::TransactionVersion; -use starknet_types_core::{felt::Felt, hash::StarkHash}; - mod broadcasted_to_blockifier; mod from_blockifier; mod from_broadcasted_transaction; @@ -12,7 +8,13 @@ mod to_starknet_core; pub mod compute_hash; pub mod utils; + +use mp_convert::{hex_serde::U128AsHex, hex_serde::U64AsHex, ToFelt}; +// pub use from_starknet_provider::TransactionTypeError; pub use broadcasted_to_blockifier::{broadcasted_to_blockifier, BroadcastedToBlockifierError}; +use serde_with::serde_as; +use starknet_api::transaction::TransactionVersion; +use starknet_types_core::{felt::Felt, hash::StarkHash}; const SIMULATE_TX_VERSION_OFFSET: Felt = Felt::from_hex_unchecked("0x100000000000000000000000000000000"); @@ -601,23 +603,49 @@ impl DeployAccountTransactionV3 { } } -#[derive(Debug, Clone, Default, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[derive(Debug, Clone, Default, Copy, PartialEq, Eq)] pub enum DataAvailabilityMode { #[default] L1 = 0, L2 = 1, } -#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +impl serde::Serialize for DataAvailabilityMode { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + serializer.serialize_u8(*self as u8) + } +} +impl<'de> serde::Deserialize<'de> for DataAvailabilityMode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value = u8::deserialize(deserializer)?; + match value { + 0 => Ok(DataAvailabilityMode::L1), + 1 => Ok(DataAvailabilityMode::L2), + _ => Err(serde::de::Error::custom("Invalid value for DataAvailabilityMode")), + } + } +} + +#[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +#[serde(rename_all = "SCREAMING_SNAKE_CASE")] pub struct ResourceBoundsMapping { pub l1_gas: ResourceBounds, pub l2_gas: ResourceBounds, } +#[serde_as] #[derive(Debug, Clone, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize)] pub struct ResourceBounds { + #[serde_as(as = "U64AsHex")] pub max_amount: u64, + #[serde_as(as = "U128AsHex")] pub max_price_per_unit: u128, } diff --git a/crates/primitives/utils/src/lib.rs b/crates/primitives/utils/src/lib.rs index 74b3f8f3f..65f6053a0 100644 --- a/crates/primitives/utils/src/lib.rs +++ b/crates/primitives/utils/src/lib.rs @@ -27,7 +27,7 @@ where static CTRL_C: AtomicBool = AtomicBool::new(false); async fn graceful_shutdown_inner() { - let sigint = async { + let sigterm = async { match tokio::signal::unix::signal(tokio::signal::unix::SignalKind::terminate()) { Ok(mut signal) => signal.recv().await, // SIGTERM not supported @@ -36,7 +36,7 @@ async fn graceful_shutdown_inner() { }; tokio::select! { _ = tokio::signal::ctrl_c() => {}, - _ = sigint => {}, + _ = sigterm => {}, }; CTRL_C.store(true, Ordering::SeqCst); } diff --git a/crates/tests/src/lib.rs b/crates/tests/src/lib.rs index 115be3594..8f026bf50 100644 --- a/crates/tests/src/lib.rs +++ b/crates/tests/src/lib.rs @@ -185,6 +185,7 @@ impl MadaraCmdBuilder { format!("{}", self.tempdir.as_ref().display()), "--rpc-port".into(), format!("{}", self.port.0), + "--".into(), ])) .stdout(Stdio::piped()) .spawn()