Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions madara/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion madara/crates/client/gateway/server/src/handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -246,7 +246,7 @@ pub async fn handle_get_block_traces(
}

let traces = v0_7_1_trace_block_transactions(
&Starknet::new(backend, add_transaction_provider, Default::default(), None, ctx),
&Starknet::new(backend, add_transaction_provider, Default::default(), None, None, ctx),
block_id,
)
.await?;
Expand Down
1 change: 1 addition & 0 deletions madara/crates/client/rpc/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ m-proc-macros = { workspace = true }
mc-block-production = { workspace = true }
mc-db = { workspace = true }
mc-exec = { workspace = true }
mc-settlement-client = { workspace = true }
mc-submit-tx = { workspace = true }
mp-block = { workspace = true, default-features = true }
mp-chain-config = { workspace = true }
Expand Down
17 changes: 16 additions & 1 deletion madara/crates/client/rpc/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub mod versions;
use jsonrpsee::RpcModule;
use mc_db::db_block_id::DbBlockIdResolvable;
use mc_db::MadaraBackend;
use mc_settlement_client::client::SettlementLayerProvider;
use mc_submit_tx::SubmitTransaction;
use mp_block::{BlockId, BlockTag, MadaraMaybePendingBlock, MadaraMaybePendingBlockInfo};
use mp_chain_config::ChainConfig;
Expand Down Expand Up @@ -49,6 +50,7 @@ pub struct Starknet {
pub(crate) add_transaction_provider: Arc<dyn SubmitTransaction>,
storage_proof_config: StorageProofConfig,
pub(crate) block_prod_handle: Option<mc_block_production::BlockProductionHandle>,
pub(crate) settlement_client: Option<Arc<dyn SettlementLayerProvider>>,
pub ctx: ServiceContext,
}

Expand All @@ -58,10 +60,19 @@ impl Starknet {
add_transaction_provider: Arc<dyn SubmitTransaction>,
storage_proof_config: StorageProofConfig,
block_prod_handle: Option<mc_block_production::BlockProductionHandle>,
settlement_client: Option<Arc<dyn SettlementLayerProvider>>,
ctx: ServiceContext,
) -> Self {
let ws_handles = Arc::new(WsSubscribeHandles::new());
Self { backend, ws_handles, add_transaction_provider, storage_proof_config, block_prod_handle, ctx }
Self {
backend,
ws_handles,
add_transaction_provider,
storage_proof_config,
block_prod_handle,
settlement_client,
ctx,
}
}

pub fn clone_backend(&self) -> Arc<MadaraBackend> {
Expand All @@ -72,6 +83,10 @@ impl Starknet {
Arc::clone(self.backend.chain_config())
}

pub fn settlement_client(&self) -> Option<Arc<dyn SettlementLayerProvider>> {
self.settlement_client.clone()
}

pub fn get_block_info(
&self,
block_id: &impl DbBlockIdResolvable,
Expand Down
4 changes: 4 additions & 0 deletions madara/crates/client/rpc/src/versions/user/v0_8_1/api.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use jsonrpsee::core::RpcResult;
use m_proc_macros::versioned_rpc;
use mp_block::BlockId;
use mp_rpc::v0_8_1::{L1TxnHash, TxnHashWithStatus};
use starknet_types_core::felt::Felt;

type SubscriptionItemPendingTxs = super::methods::ws::SubscriptionItem<mp_rpc::v0_8_1::PendingTxnInfo>;
Expand Down Expand Up @@ -65,4 +66,7 @@ pub trait StarknetReadRpcApi {
contract_addresses: Option<Vec<Felt>>,
contracts_storage_keys: Option<Vec<mp_rpc::v0_8_1::ContractStorageKeysItem>>,
) -> RpcResult<mp_rpc::v0_8_1::GetStorageProofResult>;

#[method(name = "getMessagesStatus")]
async fn get_messages_status(&self, tx_hash_l1: L1TxnHash) -> RpcResult<Vec<TxnHashWithStatus>>;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use mc_settlement_client::error::SettlementClientError;
use mp_rpc::v0_8_1::{L1TxnHash, TxnHashWithStatus};

use crate::{
utils::display_internal_server_error,
versions::user::v0_7_1::methods::read::get_transaction_status::get_transaction_status, Starknet,
StarknetRpcApiError, StarknetRpcResult,
};

pub async fn get_messages_status(
starknet: &Starknet,
transaction_hash: L1TxnHash,
) -> StarknetRpcResult<Vec<TxnHashWithStatus>> {
let transaction_hash = transaction_hash.into();
let settlement_client = starknet.settlement_client().ok_or(StarknetRpcApiError::InternalServerError)?;
let l1_handlers = settlement_client.get_messages_to_l2(transaction_hash).await.map_err(|e| match e {
SettlementClientError::TxNotFound => StarknetRpcApiError::TxnHashNotFound,
e => {
display_internal_server_error(format!(
"Error getting messages to L2 for L1 transaction {transaction_hash:?}: {e}"
));
StarknetRpcApiError::InternalServerError
}
})?;

let mut results = Vec::new();
for msg in l1_handlers {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh btw,

stream::iter(messages).then(|msg| async move {
  let transaction_hash = msg.tx.compute_hash(starknet.chain_id(), false, false);
  let finality_status = get_transaction_status(starknet, transaction_hash).await?.finality_status;
  Ok(TxnHashWithStatus { transaction_hash, finality_status })
}).collect().await

should work

(https://docs.rs/futures/latest/futures/stream/index.html, and https://docs.rs/futures/latest/futures/stream/trait.StreamExt.html#method.then which is the async version of iterator map)

(it doesn't change anything, the fetches will still be sequential but i think it looks nicer? maybe overcomplicated for nothing idk)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

there’s a slight overhead with the stream I think, and I’m not sure there’s more than 1 message per TX

let transaction_hash = msg.tx.compute_hash(starknet.chain_id(), false, false);
let finality_status = get_transaction_status(starknet, transaction_hash).await?.finality_status;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i think we're supposed to return something meaningful when the transaction is not on l2 here

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Alright - I am looking at the latest v0.9 spec. I also looked at the v0.8 rpc spec but I figured I would focus on v0.9 spec because we'll have to do that as well, and the corner cases are similar.

https://github.com/starkware-libs/starknet-specs/blob/release/v0.9.0-rc.3/api/starknet_api_openrpc.json

Here is the interpretation of the spec.

  • If the transaction can't be found on L1, TxnHashNotFound (correct)

  • finality_status: this can be PreConfirmed, AcceptedOnL1 and AcceptedOnL2
    You are fine here since it's get_transaction_status that handles that (esp. AcceptedOnL1).
    I don't think we're supposed to return Received status here (because messages from l1 txs can't be in the mempool) nor Candidate status because execution_status is required.

  • execution_status: you are missing this field. This field is returned by get_transaction_status, so you should just pass it to the resulting DTO. Weirdly enough, this field is required, so we can't return finality_status Candidate in this endpoint. I should bring this up with the spec authors tbh, it's probably something they haven't thought about.

  • failure_reason: you are also missing this field. This is the revert reason, and you can also get it from the output of get_transaction_status where it's also named failure_reason. It's optional and should be None for the other statuses.

  • What if the transaction is on L1 and not on l2/preconfirmed? This is not as far-fetched of an idea as you may think, the sequencer can totally just omit L1HandlerTransactions and/or miss them.
    Either, 1) none of the messages are on l2 yet. 2) the rare case where one of the messages to l2 is on l2 but one of the others are missing.
    I don't know what the proper interpretation is here - if finality_status was nullable I would say we should return a None finality_status. Since it's not, I think skipping the transaction makes sense. If we were allowed to return a null execution_status, I think it could also kinda make sense to return Received.
    I've looked at how pathfinder interprets the spec, and it's a bit weird? Here is how they interpret it: if a get_transaction_status call for one of the messages returns TxnHashNotFound, the whole getMessagesStatus returns TxnHashNotFound. Which means, the endpoint can't be used at all when case 2) hits, like, if a message is skipped by the sequencer somehow or if one of the messages arrives on l2 before another (could happen for a lot of reason). But here is the weird part: if a get_transaction_status call returns Candidate (or Received), the given transaction is skipped from the output because there is no associated execution_status. This feels weird to me? i am super confused.


I think I need to bring this up to the spec authors, but in the meantime, my opinion is that you should:

  • add the execution_status and finality_status fields to match the spec. Just use what get_transaction_status returns here.
  • I think it kind of make sense to return TxnHashNotFound like you did if a transaction is not on l2 in the meantime, to match pathfinder's interpretation. I think it would be weird for users to get an empty array if their tx has not hit l2 yet, but I haven't checked how consumers use the API.
  • candidate shenanigans might require spec change, let's worry about that later when we're doing v0.9.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this implementation is only for v0.8.1

// TODO: Add failure_reason when we support it in get_transaction_status in v0.8.1.
results.push(TxnHashWithStatus { transaction_hash, finality_status, failure_reason: None });
}
Ok(results)
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,11 @@ use crate::Starknet;
use jsonrpsee::core::{async_trait, RpcResult};
use mp_block::BlockId;
use mp_chain_config::RpcVersion;
use mp_rpc::v0_8_1::{ContractStorageKeysItem, GetStorageProofResult};
use mp_rpc::v0_8_1::{ContractStorageKeysItem, GetStorageProofResult, L1TxnHash, TxnHashWithStatus};
use starknet_types_core::felt::Felt;

pub mod get_compiled_casm;
pub mod get_messages_status;
pub mod get_storage_proof;

#[async_trait]
Expand All @@ -28,4 +29,8 @@ impl StarknetReadRpcApiV0_8_1Server for Starknet {
) -> RpcResult<GetStorageProofResult> {
get_storage_proof::get_storage_proof(self, block_id, class_hashes, contract_addresses, contracts_storage_keys)
}

async fn get_messages_status(&self, tx_hash_l1: L1TxnHash) -> RpcResult<Vec<TxnHashWithStatus>> {
Ok(get_messages_status::get_messages_status(self, tx_hash_l1).await?)
}
}
13 changes: 13 additions & 0 deletions madara/crates/client/settlement_client/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ use crate::messaging::MessageToL2WithMetadata;
use crate::state_update::{StateUpdate, StateUpdateWorker};
use async_trait::async_trait;
use futures::stream::BoxStream;
use mp_convert::L1TransactionHash;
use mp_transactions::L1HandlerTransactionWithFee;
use mp_utils::service::ServiceContext;

Expand Down Expand Up @@ -108,6 +109,18 @@ pub trait SettlementLayerProvider: Send + Sync {
/// - An Error if the call fail
async fn message_to_l2_is_pending(&self, msg_hash: &[u8]) -> Result<bool, SettlementClientError>;

/// Fetches L1 to L2 messages associated with a specific L1 transaction hash
///
/// # Arguments
/// * `l1_tx_hash` - The hash of the L1 transaction
/// # Returns
/// - A vector of L1 to L2 messages associated with the given L1 transaction hash
/// - An error if the call fails
async fn get_messages_to_l2(
&self,
l1_tx_hash: L1TransactionHash,
) -> Result<Vec<L1HandlerTransactionWithFee>, SettlementClientError>;

/// Return a block timestamp in second.
///
/// # Arguments
Expand Down
3 changes: 3 additions & 0 deletions madara/crates/client/settlement_client/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@ pub enum SettlementClientError {
#[error("Starknet client error: {0}")]
Starknet(StarknetClientError),

#[error("Transaction not found")]
TxNotFound,

#[error("Missing required field: {0}")]
MissingField(&'static str),

Expand Down
31 changes: 29 additions & 2 deletions madara/crates/client/settlement_client/src/eth/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ use crate::utils::convert_log_state_update;
use alloy::eips::{BlockId, BlockNumberOrTag};
use alloy::primitives::{keccak256, Address, B256, I256, U256};
use alloy::providers::{Provider, ProviderBuilder, ReqwestProvider, RootProvider};
use alloy::rpc::types::Filter;
use alloy::rpc::types::{Filter, FilteredParams};
use alloy::sol;
use alloy::sol_types::SolValue;
use alloy::transports::http::{Client, Http};
Expand All @@ -17,7 +17,7 @@ use bitvec::macros::internal::funty::Fundamental;
use error::EthereumClientError;
use futures::stream::BoxStream;
use futures::StreamExt;
use mp_convert::{FeltExt, ToFelt};
use mp_convert::{FeltExt, L1TransactionHash, ToFelt};
use mp_transactions::L1HandlerTransactionWithFee;
use mp_utils::service::ServiceContext;
use std::str::FromStr;
Expand Down Expand Up @@ -306,6 +306,33 @@ impl SettlementLayerProvider for EthereumClient {
Ok(cancellation_timestamp._0 != U256::ZERO)
}

async fn get_messages_to_l2(
&self,
l1_tx_hash: L1TransactionHash,
) -> Result<Vec<L1HandlerTransactionWithFee>, SettlementClientError> {
let l1_tx_hash = B256::from(l1_tx_hash.into_eth().0);
let receipt = self
.provider
.get_transaction_receipt(l1_tx_hash)
.await
.map_err(|e| -> SettlementClientError {
EthereumClientError::Rpc(format!("Failed to get transaction receipt: {}", e)).into()
})?
.ok_or(SettlementClientError::TxNotFound)?;

let filter = FilteredParams::new(Some(self.l1_core_contract.LogMessageToL2_filter().filter));

receipt
.inner
.logs()
.iter()
.filter(|log| filter.filter_address(&log.address()) && filter.filter_topics(log.topics()))
.filter_map(|log| log.log_decode::<LogMessageToL2>().ok())
.map(|log| log.inner.data)
.map(TryInto::try_into)
.collect::<Result<Vec<L1HandlerTransactionWithFee>, _>>()
}

async fn get_block_n_timestamp(&self, l1_block_n: u64) -> Result<u64, SettlementClientError> {
let block = self
.provider
Expand Down
2 changes: 1 addition & 1 deletion madara/crates/client/settlement_client/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::sync::Arc;
use tokio::sync::Notify;
use url::Url;

mod client;
pub mod client;
pub mod error;
mod eth;
pub mod gas_price;
Expand Down
52 changes: 52 additions & 0 deletions madara/crates/client/settlement_client/src/starknet/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ use async_trait::async_trait;
use bigdecimal::ToPrimitive;
use futures::stream::BoxStream;
use futures::{StreamExt, TryStreamExt};
use mp_convert::L1TransactionHash;
use mp_transactions::L1HandlerTransactionWithFee;
use mp_utils::service::ServiceContext;
use starknet_core::types::{BlockId, BlockTag, EmittedEvent, EventFilter, FunctionCall, MaybePendingBlockWithTxHashes};
Expand Down Expand Up @@ -343,6 +344,57 @@ impl SettlementLayerProvider for StarknetClient {
Ok(matches!(status, MessageToAppchainStatus::Pending(_)))
}

async fn get_messages_to_l2(
&self,
l1_tx_hash: L1TransactionHash,
) -> Result<Vec<L1HandlerTransactionWithFee>, SettlementClientError> {
let l1_tx_hash = l1_tx_hash.into_starknet().map_err(|e| -> SettlementClientError {
StarknetClientError::Conversion(format!("Invalid L1 transaction hash: {}", e)).into()
})?;
let receipt =
self.provider.get_transaction_receipt(l1_tx_hash).await.map_err(|e| -> SettlementClientError {
StarknetClientError::L1ToL2Messaging { message: format!("Failed to fetch transaction receipt: {}", e) }
.into()
})?;

let block_number = receipt.block.block_number();
let block_hash = receipt.block.block_hash();

let events = match receipt.receipt {
starknet_core::types::TransactionReceipt::Invoke(receipt) => receipt.events,
starknet_core::types::TransactionReceipt::Declare(receipt) => receipt.events,
starknet_core::types::TransactionReceipt::Deploy(receipt) => receipt.events,
starknet_core::types::TransactionReceipt::DeployAccount(receipt) => receipt.events,
starknet_core::types::TransactionReceipt::L1Handler(receipt) => receipt.events,
};

let core_address = self.core_contract_address;
events
.into_iter()
.filter(|event| {
event.from_address == core_address
&& event.keys
== vec![get_selector_from_name("MessageSent").expect("Failed to get MessageSent selector")]
})
.map(|event| EmittedEvent {
from_address: event.from_address,
keys: event.keys,
data: event.data,
block_hash,
block_number,
transaction_hash: l1_tx_hash,
})
.map(MessageToL2WithMetadata::try_from)
.map(|res| {
res.map(|m| m.message).map_err(|e| {
SettlementClientError::from(StarknetClientError::L1ToL2Messaging {
message: format!("Failed to parse MessageToL2 event: {e}"),
})
})
})
.collect::<Result<Vec<L1HandlerTransactionWithFee>, SettlementClientError>>()
}

async fn get_block_n_timestamp(&self, l1_block_n: u64) -> Result<u64, SettlementClientError> {
tracing::debug!("get_block_n_timestamp l1_block_n={l1_block_n}");
let block = self.provider.get_block_with_tx_hashes(BlockId::Number(l1_block_n)).await.map_err(
Expand Down
1 change: 1 addition & 0 deletions madara/crates/primitives/convert/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ workspace = true
targets = ["x86_64-unknown-linux-gnu"]

[dependencies]
mp-rpc.workspace = true

# Starknet
alloy.workspace = true
Expand Down
6 changes: 6 additions & 0 deletions madara/crates/primitives/convert/src/felt.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,12 @@ impl L1TransactionHash {
}
}

impl From<mp_rpc::v0_8_1::L1TxnHash> for L1TransactionHash {
fn from(value: mp_rpc::v0_8_1::L1TxnHash) -> Self {
Self(value.0)
}
}

#[derive(Clone, Copy, Debug, serde::Deserialize, serde::Serialize)]
#[serde(transparent)]
pub struct L1CoreContractNonce(u64);
Expand Down
Loading
Loading