From e29598f2b1c4526c320b939dddf32dd2cf141b52 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Fri, 31 Jan 2025 14:31:15 -0600 Subject: [PATCH 01/12] use 'tx_status' instead of 'GetTx' --- Cargo.lock | 1 + charts/sequencer-relayer/Chart.yaml | 4 +- .../templates/configmaps.yaml | 1 + charts/sequencer-relayer/values.yaml | 3 +- charts/sequencer/Chart.lock | 6 +- charts/sequencer/Chart.yaml | 4 +- crates/astria-sequencer-relayer/Cargo.toml | 1 + .../local.env.example | 3 + crates/astria-sequencer-relayer/src/config.rs | 1 + .../src/relayer/builder.rs | 12 +- .../src/relayer/celestia_client/builder.rs | 22 +- .../src/relayer/celestia_client/error.rs | 46 ++-- .../src/relayer/celestia_client/mod.rs | 198 ++++++++++-------- .../src/relayer/celestia_client/tests.rs | 123 +---------- .../src/relayer/submission.rs | 4 +- .../src/relayer/write/mod.rs | 2 +- .../src/sequencer_relayer.rs | 2 + .../helpers/mock_celestia_app_server.rs | 150 +++++++------ .../helpers/test_sequencer_relayer.rs | 32 ++- .../tests/blackbox/main.rs | 60 +++--- 20 files changed, 314 insertions(+), 361 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index cf483afff4..5b5c49deed 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -922,6 +922,7 @@ dependencies = [ "isahc", "itertools 0.12.1", "itoa", + "jsonrpsee", "k256", "pbjson-types", "pin-project-lite", diff --git a/charts/sequencer-relayer/Chart.yaml b/charts/sequencer-relayer/Chart.yaml index 77ff6e351c..b55ac29977 100644 --- a/charts/sequencer-relayer/Chart.yaml +++ b/charts/sequencer-relayer/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.1 +version: 1.1.0 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.0.0" +appVersion: "1.1.0" maintainers: - name: wafflesvonmaple diff --git a/charts/sequencer-relayer/templates/configmaps.yaml b/charts/sequencer-relayer/templates/configmaps.yaml index 97738d36e6..744377129a 100644 --- a/charts/sequencer-relayer/templates/configmaps.yaml +++ b/charts/sequencer-relayer/templates/configmaps.yaml @@ -30,6 +30,7 @@ data: ASTRIA_SEQUENCER_RELAYER_CELESTIA_CHAIN_ID: "{{ .Values.config.relayer.celestiaChainId }}" {{- if not .Values.global.dev }} {{- else }} + ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_HTTP_ENDPOINT: "{{ .Values.config.relayer.celestiaAppHttp }}" {{- end }} --- apiVersion: v1 diff --git a/charts/sequencer-relayer/values.yaml b/charts/sequencer-relayer/values.yaml index 16b9494776..35fed125b1 100644 --- a/charts/sequencer-relayer/values.yaml +++ b/charts/sequencer-relayer/values.yaml @@ -14,7 +14,7 @@ images: sequencerRelayer: repo: ghcr.io/astriaorg/sequencer-relayer pullPolicy: IfNotPresent - tag: 1.0.0 + tag: 1.1.0 devTag: latest config: @@ -22,6 +22,7 @@ config: sequencerChainId: "" celestiaChainId: "" celestiaAppGrpc: "" + celestiaAppHttp: "" cometbftRpc: "" sequencerGrpc: "" onlyIncludeRollups: "" diff --git a/charts/sequencer/Chart.lock b/charts/sequencer/Chart.lock index 7523dc332c..d1cc6f2ca1 100644 --- a/charts/sequencer/Chart.lock +++ b/charts/sequencer/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: sequencer-relayer repository: file://../sequencer-relayer - version: 1.0.1 -digest: sha256:3b3ce65ff473606fcc86027653cadd212ba45ac8b39d5806d713b48f695ad235 -generated: "2024-11-20T11:24:11.85775+02:00" + version: 1.1.0 +digest: sha256:ab846eac25dd1f19c7bed590f94bbc0eecabe3501a46b2edf5cc90306472eb2d +generated: "2025-01-31T14:27:35.263935-06:00" diff --git a/charts/sequencer/Chart.yaml b/charts/sequencer/Chart.yaml index 5107f393d6..69c8104bef 100644 --- a/charts/sequencer/Chart.yaml +++ b/charts/sequencer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.2 +version: 1.0.3 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. @@ -24,7 +24,7 @@ appVersion: "1.0.0" dependencies: - name: sequencer-relayer - version: "1.0.1" + version: "1.1.0" repository: "file://../sequencer-relayer" condition: sequencer-relayer.enabled diff --git a/crates/astria-sequencer-relayer/Cargo.toml b/crates/astria-sequencer-relayer/Cargo.toml index 1d0589e932..dd3229a3b9 100644 --- a/crates/astria-sequencer-relayer/Cargo.toml +++ b/crates/astria-sequencer-relayer/Cargo.toml @@ -25,6 +25,7 @@ humantime = { workspace = true } humantime-serde = "1.1.1" hyper = { workspace = true } itoa = { workspace = true } +jsonrpsee = { version = "0.20", features = ["client-core", "jsonrpsee-http-client", "macros"] } pbjson-types = { workspace = true } pin-project-lite = { workspace = true } prost = { workspace = true } diff --git a/crates/astria-sequencer-relayer/local.env.example b/crates/astria-sequencer-relayer/local.env.example index 81b5c75d5f..5c1a435bf8 100644 --- a/crates/astria-sequencer-relayer/local.env.example +++ b/crates/astria-sequencer-relayer/local.env.example @@ -38,6 +38,9 @@ ASTRIA_SEQUENCER_RELAYER_SEQUENCER_GRPC_ENDPOINT="http://127.0.0.1:8080" # 127.0.0.1:9090 is the default socket address for its gRPC server. ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_GRPC_ENDPOINT="http://127.0.0.1:9090" +# Address at which celestia app serves HTTP RPCs. +ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_HTTP_ENDPOINT="http://127.0.0.1:26657" + # The path to the file storing the signing key used to sign blob submissions to the celestia app. # The file should be a hex-encoded secp256k1 secret key, such as could be output via # `celestia-appd keys export --keyring-backend=... --home=... --unsafe --unarmored-hex`, diff --git a/crates/astria-sequencer-relayer/src/config.rs b/crates/astria-sequencer-relayer/src/config.rs index ee6a99e829..34427b77fe 100644 --- a/crates/astria-sequencer-relayer/src/config.rs +++ b/crates/astria-sequencer-relayer/src/config.rs @@ -31,6 +31,7 @@ pub struct Config { pub cometbft_endpoint: String, pub sequencer_grpc_endpoint: String, pub celestia_app_grpc_endpoint: String, + pub celestia_app_http_endpoint: String, pub celestia_app_key_file: String, pub block_time: u64, // Would ideally be private; accessed via the public getter which converts this to a collection diff --git a/crates/astria-sequencer-relayer/src/relayer/builder.rs b/crates/astria-sequencer-relayer/src/relayer/builder.rs index fd4d3fbf17..eab33b52eb 100644 --- a/crates/astria-sequencer-relayer/src/relayer/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/builder.rs @@ -30,6 +30,7 @@ pub(crate) struct Builder { pub(crate) sequencer_chain_id: String, pub(crate) celestia_chain_id: String, pub(crate) celestia_app_grpc_endpoint: String, + pub(crate) celestia_app_http_endpoint: String, pub(crate) celestia_app_key_file: String, pub(crate) cometbft_endpoint: String, pub(crate) sequencer_poll_period: Duration, @@ -47,6 +48,7 @@ impl Builder { sequencer_chain_id, celestia_chain_id, celestia_app_grpc_endpoint, + celestia_app_http_endpoint, celestia_app_key_file, cometbft_endpoint, sequencer_poll_period, @@ -77,8 +79,14 @@ impl Builder { .wrap_err("failed parsing provided celestia app grpc endpoint as Uri")?; let celestia_keys = CelestiaKeys::from_path(celestia_app_key_file) .wrap_err("failed to get celestia keys from file")?; - CelestiaClientBuilder::new(celestia_chain_id, uri, celestia_keys, state.clone()) - .wrap_err("failed to create celestia client builder")? + CelestiaClientBuilder::new( + celestia_chain_id, + uri, + celestia_app_http_endpoint, + celestia_keys, + state.clone(), + ) + .wrap_err("failed to create celestia client builder")? }; Ok(super::Relayer { diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs index 1f78e81b51..e001847899 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs @@ -11,6 +11,7 @@ use astria_core::generated::cosmos::{ tx::v1beta1::service_client::ServiceClient as TxClient, }; use http::Uri; +use jsonrpsee::http_client::HttpClientBuilder; use tendermint::account::Id as AccountId; use thiserror::Error; use tonic::transport::{ @@ -58,6 +59,10 @@ pub(in crate::relayer) enum BuilderError { configured: String, received: String, }, + /// Failed to construct Celestia HTTP RPC client. + #[error("failed to construct Celestia HTTP RPC client: {error}")] + // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. + HttpClient { error: String }, } /// An error while encoding a Bech32 string. @@ -71,6 +76,8 @@ pub(in crate::relayer) struct Builder { configured_celestia_chain_id: String, /// The inner `tonic` gRPC channel shared by the various generated gRPC clients. grpc_channel: Channel, + /// The HTTP RPC endpoint for querying transaction status. + tx_status_endpoint: String, /// The crypto keys associated with our Celestia account. signing_keys: CelestiaKeys, /// The Bech32-encoded address of our Celestia account. @@ -83,15 +90,19 @@ impl Builder { /// Returns a new `Builder`, or an error if Bech32-encoding the `signing_keys` address fails. pub(in crate::relayer) fn new( configured_celestia_chain_id: String, - uri: Uri, + grpc_endpoint: Uri, + tx_status_endpoint: String, signing_keys: CelestiaKeys, state: Arc, ) -> Result { - let grpc_channel = Endpoint::from(uri).timeout(REQUEST_TIMEOUT).connect_lazy(); + let grpc_channel = Endpoint::from(grpc_endpoint) + .timeout(REQUEST_TIMEOUT) + .connect_lazy(); let address = bech32_encode(&signing_keys.address)?; Ok(Self { configured_celestia_chain_id, grpc_channel, + tx_status_endpoint, signing_keys, address, state, @@ -106,6 +117,7 @@ impl Builder { let Self { configured_celestia_chain_id, grpc_channel, + tx_status_endpoint, signing_keys, address, state, @@ -121,10 +133,16 @@ impl Builder { info!(celestia_chain_id = %received_celestia_chain_id, "confirmed celestia chain id"); state.set_celestia_connected(true); + let tx_status_client = HttpClientBuilder::default() + .build(tx_status_endpoint) + .map_err(|e| BuilderError::HttpClient { + error: e.to_string(), + })?; let tx_client = TxClient::new(grpc_channel.clone()); Ok(CelestiaClient { grpc_channel, tx_client, + tx_status_client, signing_keys, address, chain_id: received_celestia_chain_id, diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs index 9309694d31..13a3535d76 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs @@ -77,26 +77,9 @@ pub(in crate::relayer) enum TrySubmitError { namespace: String, log: String, }, - /// The celestia app responded with the given error status to a `GetTxRequest`. - #[error("failed to get transaction")] - FailedToGetTx(#[source] GrpcResponseError), - /// The get transaction response was empty. - #[error("the get transaction response was empty")] - EmptyGetTxResponse, - /// The get transaction response contains an error code. - #[error( - "get transaction response contains error code `{code}`, tx `{tx_hash}`, namespace \ - `{namespace}`, log: {log}" - )] - GetTxResponseErrorCode { - tx_hash: String, - code: u32, - namespace: String, - log: String, - }, - /// The get transaction response specified a negative block height. - #[error("get transaction response specifies a negative block height ({0})")] - GetTxResponseNegativeBlockHeight(i64), + /// The transaction was either evicted from the mempool or the call to `tx_status` failed. + #[error("failed to confirm transaction submission")] + FailedToConfirmSubmission(#[source] ConfirmSubmissionError), } /// A gRPC status representing an error response from an RPC call. @@ -137,3 +120,26 @@ impl std::error::Error for GrpcResponseError { #[derive(Error, Clone, Debug)] #[error(transparent)] pub(in crate::relayer) struct ProtobufDecodeError(#[from] DecodeError); + +/// An error in getting the status of a transaction via RPC `tx_status`. +#[derive(Debug, Clone, thiserror::Error)] +pub(in crate::relayer) enum TxStatusError { + #[error("received unfamilair response for tx `{hash}` from `tx_status`: {status}")] + UnfamiliarStatus { status: String, hash: String }, + #[error("request for `tx_status` failed: {error}")] + // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. + FailedToGetTxStatus { error: String }, + #[error("failed to parse `height` into u64")] + HeightParse(#[from] std::num::ParseIntError), +} + +/// An error in confirming the submission of a transaction. +#[derive(Debug, Clone, thiserror::Error)] +pub(in crate::relayer) enum ConfirmSubmissionError { + #[error("tx `{hash}` evicted from mempool")] + Evicted { hash: String }, + #[error("received `UNKNOWN` status from `tx_status` for tx: {hash}")] + UnknownStatus { hash: String }, + #[error("failed to get tx status")] + TxStatus(#[from] TxStatusError), +} diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index 83d15478ee..b9b4ead3f6 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -57,8 +57,6 @@ use astria_core::generated::{ BroadcastTxRequest, BroadcastTxResponse, Fee, - GetTxRequest, - GetTxResponse, ModeInfo, SignDoc, SignerInfo, @@ -71,7 +69,6 @@ use astria_core::generated::{ BlobTx, }, }; -use astria_eyre::eyre::Report; pub(super) use builder::{ Builder as CelestiaClientBuilder, BuilderError, @@ -79,6 +76,10 @@ pub(super) use builder::{ use celestia_cost_params::CelestiaCostParams; pub(crate) use celestia_keys::CelestiaKeys; use celestia_types::Blob; +use error::{ + ConfirmSubmissionError, + TxStatusError, +}; pub(super) use error::{ GrpcResponseError, ProtobufDecodeError, @@ -88,6 +89,7 @@ use hex::{ FromHex, FromHexError, }; +use jsonrpsee::proc_macros::rpc; use prost::{ bytes::Bytes, Message as _, @@ -123,6 +125,24 @@ use tracing::{ // From https://github.com/celestiaorg/cosmos-sdk/blob/v1.18.3-sdk-v0.46.14/types/errors/errors.go#L75 const INSUFFICIENT_FEE_CODE: u32 = 13; +const TX_STATUS_UNKNOWN: &str = "UNKNOWN"; +const TX_STATUS_PENDING: &str = "PENDING"; +const TX_STATUS_EVICTED: &str = "EVICTED"; +const TX_STATUS_COMMITTED: &str = "COMMITTED"; + +enum TxStatus { + Unknown, + Pending, + Evicted, + Committed(u64), +} + +#[derive(Debug, Deserialize)] +struct TxStatusResponse { + pub height: String, + pub status: String, +} + /// A client using the gRPC interface of a remote Celestia app to submit blob data to the Celestia /// chain. /// @@ -133,6 +153,8 @@ pub(super) struct CelestiaClient { grpc_channel: Channel, /// A gRPC client to broadcast and get transactions. tx_client: TxClient, + /// An HTTP client to receive transaction status. + tx_status_client: jsonrpsee::http_client::HttpClient, /// The crypto keys associated with our Celestia account. signing_keys: CelestiaKeys, /// The Bech32-encoded address of our Celestia account. @@ -209,8 +231,8 @@ impl CelestiaClient { let hex_encoded_tx_hash = self.broadcast_tx(blob_tx).await?; if hex_encoded_tx_hash != blob_tx_hash.to_hex() { // This is not a critical error. Worst case, we restart the process now and try for a - // short while to `GetTx` for this tx using the wrong hash, resulting in a likely - // duplicate submission of this set of blobs. + // short while to get `tx_status` for this tx using the wrong hash, resulting in a + // likely duplicate submission of this set of blobs. warn!( "tx hash `{hex_encoded_tx_hash}` returned from celestia app is not the same as \ the locally calculated one `{blob_tx_hash}`; submission file has invalid data" @@ -218,15 +240,16 @@ impl CelestiaClient { } info!(tx_hash = %hex_encoded_tx_hash, "broadcast blob transaction succeeded"); - let height = self.confirm_submission(hex_encoded_tx_hash).await; - Ok(height) + self.confirm_submission(hex_encoded_tx_hash) + .await + .map_err(TrySubmitError::FailedToConfirmSubmission) } - /// Repeatedly sends `GetTx` until a successful response is received or `timeout` duration has - /// elapsed. + /// Repeatedly sends `tx_status` until a successful response is received or `timeout` duration + /// has elapsed. /// /// Returns the height of the Celestia block in which the blobs were submitted, or `None` if - /// timed out. + /// timed out or an error was returned. #[instrument(skip_all)] pub(super) async fn confirm_submission_with_timeout( &mut self, @@ -236,6 +259,7 @@ impl CelestiaClient { tokio::time::timeout(timeout, self.confirm_submission(blob_tx_hash.to_hex())) .await .ok() + .and_then(Result::ok) } #[instrument(skip_all, err)] @@ -322,30 +346,56 @@ impl CelestiaClient { lowercase_hex_encoded_tx_hash_from_response(response) } - /// Returns `Some(height)` if the tx submission has completed, or `None` if it is still - /// pending. - #[instrument(skip_all, err)] - async fn get_tx(&mut self, hex_encoded_tx_hash: String) -> Result, TrySubmitError> { - let request = GetTxRequest { - hash: hex_encoded_tx_hash, - }; - let response = self.tx_client.get_tx(request).await; - // trace-level logging, so using Debug format is ok. - #[cfg_attr(dylint_lib = "tracing_debug_field", allow(tracing_debug_field))] - { - trace!(?response); + /// Returns the reponse of `tx_status` RPC call given a transaction's hash. If the transaction + /// is committed, the height of the block in which it was committed will be returned with + /// `TxStatusResponse::Committed`. + /// + /// # Errors + /// Returns an error in the following cases: + /// - The call to `tx_status` failed. + /// - The status of the transaction is not recognized. + #[instrument(skip_all, err(level = Level::WARN))] + async fn tx_status(&mut self, hex_encoded_tx_hash: String) -> Result { + let response = self + .tx_status_client + .tx_status(hex_encoded_tx_hash.clone()) + .await + .map_err(|e| TxStatusError::FailedToGetTxStatus { + error: e.to_string(), + })?; + match response.status.as_str() { + TX_STATUS_UNKNOWN => Ok(TxStatus::Unknown), + TX_STATUS_PENDING => Ok(TxStatus::Pending), + TX_STATUS_EVICTED => Ok(TxStatus::Evicted), + TX_STATUS_COMMITTED => Ok(TxStatus::Committed( + response + .height + .parse::() + .map_err(TxStatusError::HeightParse)?, + )), + _ => Err(TxStatusError::UnfamiliarStatus { + status: response.status.to_string(), + hash: hex_encoded_tx_hash, + }), } - block_height_from_response(response) } - /// Repeatedly sends `GetTx` until a successful response is received. Returns the height of the - /// Celestia block in which the blobs were submitted. - #[instrument(skip_all, fields(hex_encoded_tx_hash))] - async fn confirm_submission(&mut self, hex_encoded_tx_hash: String) -> u64 { - // The min seconds to sleep after receiving a GetTx response and sending the next request. - const MIN_POLL_INTERVAL_SECS: u64 = 1; - // The max seconds to sleep after receiving a GetTx response and sending the next request. - const MAX_POLL_INTERVAL_SECS: u64 = 12; + /// Repeatedly calls `tx_status` until the transaction is committed, returning the height of the + /// block in which the transaction was included. + /// + /// # Errors + /// Returns an error in the following cases: + /// - The transaction was evicted from the mempool. + /// - The status of the transaction is unknown. + /// - An error occurred while retrieving the transaction's status. + #[instrument(skip_all, fields(hex_encoded_tx_hash), err(level = Level::DEBUG))] + async fn confirm_submission( + &mut self, + hex_encoded_tx_hash: String, + ) -> Result { + // The min seconds to sleep after receiving a TxStatus response and sending the next + // request. + const POLL_INTERVAL_SECS: u64 = 1; // How long to wait after starting `confirm_submission` before starting to log errors. const START_LOGGING_DELAY: Duration = Duration::from_secs(12); // The minimum duration between logging errors. @@ -354,13 +404,12 @@ impl CelestiaClient { let start = Instant::now(); let mut logged_at = start; - let mut log_if_due = |maybe_error: Option| { + let mut log_pending_if_due = || { if start.elapsed() <= START_LOGGING_DELAY || logged_at.elapsed() <= LOG_ERROR_INTERVAL { return; } - let reason = maybe_error.map_or(Report::msg("transaction still pending"), Report::new); - warn!( - %reason, + debug!( + reason = "transaction still pending", tx_hash = %hex_encoded_tx_hash, elapsed_seconds = start.elapsed().as_secs_f32(), "waiting to confirm blob submission" @@ -368,21 +417,27 @@ impl CelestiaClient { logged_at = Instant::now(); }; - let mut sleep_secs = MIN_POLL_INTERVAL_SECS; loop { - tokio::time::sleep(Duration::from_secs(sleep_secs)).await; - match self.get_tx(hex_encoded_tx_hash.clone()).await { - Ok(Some(height)) => return height, - Ok(None) => { - sleep_secs = MIN_POLL_INTERVAL_SECS; - log_if_due(None); + match self.tx_status(hex_encoded_tx_hash.clone()).await { + Ok(TxStatus::Unknown) => { + break Err(ConfirmSubmissionError::UnknownStatus { + hash: hex_encoded_tx_hash, + }) + } + Ok(TxStatus::Pending) => { + log_pending_if_due(); + } + Ok(TxStatus::Evicted) => { + break Err(ConfirmSubmissionError::Evicted { + hash: hex_encoded_tx_hash, + }); } + Ok(TxStatus::Committed(height)) => break Ok(height), Err(error) => { - sleep_secs = - std::cmp::min(sleep_secs.saturating_mul(2), MAX_POLL_INTERVAL_SECS); - log_if_due(Some(error)); + break Err(ConfirmSubmissionError::TxStatus(error)); } } + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; } } } @@ -494,53 +549,6 @@ fn lowercase_hex_encoded_tx_hash_from_response( Ok(tx_response.txhash) } -/// Extracts the block height from the given response if available, or `None` if the transaction is -/// not available yet. -fn block_height_from_response( - response: Result, Status>, -) -> Result, TrySubmitError> { - let ok_response = match response { - Ok(resp) => resp, - Err(status) => { - // trace-level logging, so using Debug format is ok. - #[cfg_attr(dylint_lib = "tracing_debug_field", allow(tracing_debug_field))] - { - trace!(?status); - } - if status.code() == tonic::Code::NotFound { - trace!(msg = status.message(), "transaction still pending"); - return Ok(None); - } - return Err(TrySubmitError::FailedToGetTx(GrpcResponseError::from( - status, - ))); - } - }; - let tx_response = ok_response - .into_inner() - .tx_response - .ok_or_else(|| TrySubmitError::EmptyGetTxResponse)?; - if tx_response.code != 0 { - let error = TrySubmitError::GetTxResponseErrorCode { - tx_hash: tx_response.txhash, - code: tx_response.code, - namespace: tx_response.codespace, - log: tx_response.raw_log, - }; - return Err(error); - } - if tx_response.height == 0 { - trace!(tx_hash = %tx_response.txhash, "transaction still pending"); - return Ok(None); - } - - let height = u64::try_from(tx_response.height) - .map_err(|_| TrySubmitError::GetTxResponseNegativeBlockHeight(tx_response.height))?; - - debug!(tx_hash = %tx_response.txhash, height, "transaction succeeded"); - Ok(Some(height)) -} - // Copied from https://github.com/celestiaorg/celestia-app/blob/v1.4.0/x/blob/types/payforblob.go#L174 // // `blob_sizes` is the collection of sizes in bytes of all the blobs' `data` fields. @@ -872,3 +880,9 @@ pub(in crate::relayer) enum DeserializeBlobTxHashError { #[error("failed to decode as hex for blob tx hash: {0}")] Hex(String), } + +#[rpc(client)] +pub trait TxStatusClient { + #[method(name = "tx_status")] + async fn tx_status(&self, hash: String) -> Result; +} diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/tests.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/tests.rs index f6c4106230..64c7e1a039 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/tests.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/tests.rs @@ -70,7 +70,7 @@ fn new_msg_pay_for_blobs_should_fail_for_large_blob() { } #[test] -fn account_from_good_response_should_succeed() { +fn account_from_good_query_account_response_should_succeed() { let base_account = BaseAccount { address: "address".to_string(), pub_key: None, @@ -90,7 +90,7 @@ fn account_from_good_response_should_succeed() { } #[test] -fn account_from_bad_response_should_fail() { +fn account_from_bad_query_account_response_should_fail() { // Should return `FailedToGetAccountInfo` if outer response is an error. let error = account_from_response(Err(Status::internal(""))).unwrap_err(); #[expect( @@ -155,7 +155,7 @@ fn account_from_bad_response_should_fail() { } #[test] -fn min_gas_price_from_good_response_should_succeed() { +fn min_gas_price_from_good_min_gas_price_response_should_succeed() { let min_gas_price = 1234.56_f64; let response = Response::new(MinGasPriceResponse { minimum_gas_price: format!("{min_gas_price}utia"), @@ -171,7 +171,7 @@ fn min_gas_price_from_good_response_should_succeed() { } #[test] -fn min_gas_price_from_bad_response_should_fail() { +fn min_gas_price_from_bad_min_gas_price_response_should_fail() { // Should return `FailedToGetMinGasPrice` if outer response is an error. let error = min_gas_price_from_response(Err(Status::internal(""))).unwrap_err(); #[expect( @@ -231,11 +231,6 @@ impl TxResponseBuilder { Self::default() } - fn with_height(mut self, height: i64) -> Self { - self.height = height; - self - } - fn with_tx_hash>(mut self, tx_hash: T) -> Self { self.tx_hash = tx_hash.as_ref().to_string(); self @@ -276,7 +271,7 @@ impl TxResponseBuilder { } #[test] -fn tx_hash_from_good_response_should_succeed() { +fn tx_hash_from_good_broadcast_tx_response_should_succeed() { let tx_hash = "abc"; let tx_response = TxResponseBuilder::new().with_tx_hash(tx_hash).build(); let response = Response::new(BroadcastTxResponse { @@ -288,7 +283,7 @@ fn tx_hash_from_good_response_should_succeed() { } #[test] -fn tx_hash_from_bad_response_should_fail() { +fn tx_hash_from_bad_broadcast_tx_response_should_fail() { // Should return `FailedToBroadcastTx` if outer response is an error. let error = lowercase_hex_encoded_tx_hash_from_response(Err(Status::internal(""))).unwrap_err(); #[expect( @@ -344,112 +339,6 @@ fn tx_hash_from_bad_response_should_fail() { } } -#[test] -fn block_height_from_good_response_should_succeed() { - let height = 9; - let tx_response = TxResponseBuilder::new().with_height(height).build(); - let response = Response::new(GetTxResponse { - tx: None, - tx_response: Some(tx_response), - }); - - let extracted_height = block_height_from_response(Ok(response)).unwrap(); - assert_eq!(Some(u64::try_from(height).unwrap()), extracted_height); -} - -#[test] -fn block_height_from_bad_response_should_fail() { - // Should return `FailedToGetTx` if outer response is an error other than `NotFound`. - let error = block_height_from_response(Err(Status::internal(""))).unwrap_err(); - #[expect( - clippy::manual_assert, - reason = "`assert!(matches!(..))` provides poor feedback on failure" - )] - if !matches!(error, TrySubmitError::FailedToGetTx(_)) { - panic!("expected `Error::FailedToGetTx`, got {error:?}"); - } - - // Should return `EmptyGetTxResponse` if the inner response's `tx_response` is `None`. - let response = Ok(Response::new(GetTxResponse { - tx: None, - tx_response: None, - })); - let error = block_height_from_response(response).unwrap_err(); - #[expect( - clippy::manual_assert, - reason = "`assert!(matches!(..))` provides poor feedback on failure" - )] - if !matches!(error, TrySubmitError::EmptyGetTxResponse) { - panic!("expected `Error::EmptyGetTxResponse`, got {error:?}"); - } - - // Should return `GetTxResponseErrorCode` if the inner response's `tx_response.code` is not 0. - let tx_hash = "abc"; - let code = 9; - let namespace = "def"; - let log = "ghi"; - let tx_response = TxResponseBuilder::new() - .with_tx_hash(tx_hash) - .with_code(code) - .with_codespace(namespace) - .with_raw_log(log) - .build(); - let response = Ok(Response::new(GetTxResponse { - tx: None, - tx_response: Some(tx_response), - })); - let error = block_height_from_response(response).unwrap_err(); - match error { - TrySubmitError::GetTxResponseErrorCode { - tx_hash: received_tx_hash, - code: received_code, - namespace: received_namespace, - log: received_log, - } => { - assert_eq!(tx_hash, received_tx_hash,); - assert_eq!(code, received_code,); - assert_eq!(namespace, received_namespace,); - assert_eq!(log, received_log,); - } - _ => panic!("expected `GetTxResponseErrorCode` error, but got {error:?}"), - } -} - -#[test] -fn block_height_from_response_with_negative_height_should_fail() { - let height = -9; - let tx_response = TxResponseBuilder::new().with_height(height).build(); - let response = Response::new(GetTxResponse { - tx: None, - tx_response: Some(tx_response), - }); - - let error = block_height_from_response(Ok(response)).unwrap_err(); - match error { - TrySubmitError::GetTxResponseNegativeBlockHeight(received_height) => { - assert_eq!(height, received_height); - } - _ => panic!("expected `GetTxResponseErrorCode` error, but got {error:?}"), - } -} - -#[test] -fn block_height_from_pending_response_should_return_none() { - // Should return `None` if outer response is a `NotFound` error. - let maybe_height = block_height_from_response(Err(Status::not_found(""))).unwrap(); - assert!(maybe_height.is_none()); - - // Should return `None` if the height is 0. - let tx_response = TxResponseBuilder::new().with_height(0).build(); - let response = Response::new(GetTxResponse { - tx: None, - tx_response: Some(tx_response), - }); - - let maybe_height = block_height_from_response(Ok(response)).unwrap(); - assert!(maybe_height.is_none()); -} - #[test] fn should_use_calculated_fee() { // If no last error provided, should use calculated fee. diff --git a/crates/astria-sequencer-relayer/src/relayer/submission.rs b/crates/astria-sequencer-relayer/src/relayer/submission.rs index bc9daa7493..4ff46597e8 100644 --- a/crates/astria-sequencer-relayer/src/relayer/submission.rs +++ b/crates/astria-sequencer-relayer/src/relayer/submission.rs @@ -34,7 +34,7 @@ use tracing::{ use super::BlobTxHash; /// Represents a submission made to Celestia which has been confirmed as stored via a successful -/// `GetTx` call. +/// `tx_status` call. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub(super) struct CompletedSubmission { /// The height of the Celestia block in which the submission was stored. @@ -322,7 +322,7 @@ impl PreparedSubmission { &self.blob_tx_hash } - /// Returns the maximum duration for which the Celestia app should be polled with `GetTx` + /// Returns the maximum duration for which the Celestia app should be polled with `tx_status` /// requests to confirm successful storage of the associated `BlobTx`. /// /// This is at least 15 seconds, but up to a maximum of a minute from when the submission was diff --git a/crates/astria-sequencer-relayer/src/relayer/write/mod.rs b/crates/astria-sequencer-relayer/src/relayer/write/mod.rs index aebb0debd4..80c0e9e6cb 100644 --- a/crates/astria-sequencer-relayer/src/relayer/write/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/write/mod.rs @@ -303,7 +303,7 @@ impl BlobSubmitter { /// This should only be called where submission state on startup is `Prepared`, meaning we don't yet /// know whether that final submission attempt succeeded or not. /// -/// Internally, this polls `GetTx` for up to one minute. The returned `SubmissionState` is +/// Internally, this polls `tx_status` for up to one minute. The returned `SubmissionState` is /// guaranteed to be in `Started` state, either holding the heights of the previously prepared /// submission if confirmed by Celestia, or holding the heights of the last known confirmed /// submission in the case of timing out. diff --git a/crates/astria-sequencer-relayer/src/sequencer_relayer.rs b/crates/astria-sequencer-relayer/src/sequencer_relayer.rs index 47cd5f1caf..a7cbf39478 100644 --- a/crates/astria-sequencer-relayer/src/sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/src/sequencer_relayer.rs @@ -66,6 +66,7 @@ impl SequencerRelayer { cometbft_endpoint, sequencer_grpc_endpoint, celestia_app_grpc_endpoint, + celestia_app_http_endpoint, celestia_app_key_file, block_time, api_addr, @@ -78,6 +79,7 @@ impl SequencerRelayer { sequencer_chain_id, celestia_chain_id, celestia_app_grpc_endpoint, + celestia_app_http_endpoint, celestia_app_key_file, cometbft_endpoint, sequencer_poll_period: Duration::from_millis(block_time), diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs index 940592d907..a7ed5230d1 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs @@ -83,6 +83,7 @@ use prost::{ Message, Name, }; +use serde_json::json; use tokio::task::JoinHandle; use tonic::{ transport::Server, @@ -90,19 +91,22 @@ use tonic::{ Response, Status, }; +use wiremock::MockServer as HttpMockServer; const GET_NODE_INFO_GRPC_NAME: &str = "get_node_info"; const QUERY_ACCOUNT_GRPC_NAME: &str = "query_account"; const QUERY_AUTH_PARAMS_GRPC_NAME: &str = "query_auth_params"; const QUERY_BLOB_PARAMS_GRPC_NAME: &str = "query_blob_params"; const MIN_GAS_PRICE_GRPC_NAME: &str = "min_gas_price"; -const GET_TX_GRPC_NAME: &str = "get_tx"; +const TX_STATUS_JSONRPC_NAME: &str = "tx_status"; const BROADCAST_TX_GRPC_NAME: &str = "broadcast_tx"; pub struct MockCelestiaAppServer { _server: JoinHandle>, - pub mock_server: MockServer, - pub local_addr: SocketAddr, + pub mock_grpc_server: MockServer, + pub mock_http_server: HttpMockServer, + pub grpc_local_addr: SocketAddr, + pub http_local_addr: String, pub namespaces: Arc>>, } @@ -110,18 +114,25 @@ impl MockCelestiaAppServer { pub async fn spawn(celestia_chain_id: String) -> Self { use tokio_stream::wrappers::TcpListenerStream; - let listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let local_addr = listener.local_addr().unwrap(); + let grpc_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); + let grpc_local_addr = grpc_listener.local_addr().unwrap(); + let mock_grpc_server = MockServer::new(); - let mock_server = MockServer::new(); - register_get_node_info(&mock_server, celestia_chain_id).await; - register_query_account(&mock_server).await; - register_query_auth_params(&mock_server).await; - register_query_blob_params(&mock_server).await; - register_min_gas_price(&mock_server).await; + let http_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); + let mock_http_server = HttpMockServer::builder() + .listener(http_listener) + .start() + .await; + let http_local_addr = mock_http_server.uri(); + + register_get_node_info(&mock_grpc_server, celestia_chain_id).await; + register_query_account(&mock_grpc_server).await; + register_query_auth_params(&mock_grpc_server).await; + register_query_blob_params(&mock_grpc_server).await; + register_min_gas_price(&mock_grpc_server).await; let server = { - let service_impl = CelestiaAppServiceImpl(mock_server.clone()); + let service_impl = CelestiaAppServiceImpl(mock_grpc_server.clone()); tokio::spawn(async move { Server::builder() .add_service(NodeInfoServer::new(service_impl.clone())) @@ -129,22 +140,24 @@ impl MockCelestiaAppServer { .add_service(BlobQueryServer::new(service_impl.clone())) .add_service(MinGasPriceServer::new(service_impl.clone())) .add_service(TxServer::new(service_impl)) - .serve_with_incoming(TcpListenerStream::new(listener)) + .serve_with_incoming(TcpListenerStream::new(grpc_listener)) .await .wrap_err("gRPC sequencer server failed") }) }; Self { _server: server, - mock_server, - local_addr, + mock_grpc_server, + mock_http_server, + grpc_local_addr, + http_local_addr, namespaces: Arc::new(Mutex::new(Vec::new())), } } pub async fn mount_broadcast_tx_response(&self, debug_name: impl Into) { self.prepare_broadcast_tx_response(debug_name) - .mount(&self.mock_server) + .mount(&self.mock_grpc_server) .await; } @@ -153,23 +166,7 @@ impl MockCelestiaAppServer { debug_name: impl Into, ) -> MockGuard { self.prepare_broadcast_tx_response(debug_name) - .mount_as_scoped(&self.mock_server) - .await - } - - pub async fn mount_get_tx_response(&self, height: i64, debug_name: impl Into) { - Self::prepare_get_tx_response(height, debug_name) - .mount(&self.mock_server) - .await; - } - - pub async fn mount_get_tx_response_as_scoped( - &self, - height: i64, - debug_name: impl Into, - ) -> MockGuard { - Self::prepare_get_tx_response(height, debug_name) - .mount_as_scoped(&self.mock_server) + .mount_as_scoped(&self.mock_grpc_server) .await } @@ -202,32 +199,57 @@ impl MockCelestiaAppServer { .with_name(debug_name) } - fn prepare_get_tx_response(height: i64, debug_name: impl Into) -> Mock { - let debug_name = debug_name.into(); - // We only use the `tx_response.code` and `tx_response.height` fields in the success case. - // The `txhash` would be an actual hex-encoded SHA256 in prod, but here we can just use the - // debug name for ease of debugging. - let tx_response = TxResponse { - height, - txhash: debug_name.clone(), - code: 0, - ..TxResponse::default() - }; - let response = GetTxResponse { - tx: None, - tx_response: Some(tx_response), - }; - Mock::for_rpc_given(GET_TX_GRPC_NAME, message_type::()) - .respond_with(constant_response(response)) - .up_to_n_times(1) - .expect(1) - .with_name(debug_name) + pub async fn mount_tx_status_response(&self, status: String, height: i64) { + prepare_tx_status_response(status, height) + .mount(&self.mock_http_server) + .await; + } + + pub async fn mount_tx_status_response_as_scoped( + &self, + status: String, + height: i64, + expected: u64, + ) -> wiremock::MockGuard { + prepare_tx_status_response(status, height) + .expect(expected) + .mount_as_scoped(&self.mock_http_server) + .await } } +fn prepare_tx_status_response(status: String, height: i64) -> wiremock::Mock { + use wiremock::{ + matchers::body_partial_json, + Mock, + ResponseTemplate, + }; + + Mock::given(body_partial_json(json!({ + "jsonrpc": "2.0", + "method": TX_STATUS_JSONRPC_NAME + }))) + .respond_with(move |request: &wiremock::Request| { + let body: serde_json::Value = serde_json::from_slice(&request.body).unwrap(); + ResponseTemplate::new(200).set_body_json(json!({ + "jsonrpc": "2.0", + "id": body.get("id"), + "result": { + "height": height.to_string(), + "index": 0, + "execution_code": 0, + "error": "", + "status": status + } + })) + }) + .named(TX_STATUS_JSONRPC_NAME) + .expect(1) +} + /// Registers a handler for all incoming `GetNodeInfoRequest`s which responds with the same /// `GetNodeInfoResponse` every time. -async fn register_get_node_info(mock_server: &MockServer, celestia_chain_id: String) { +async fn register_get_node_info(mock_grpc_server: &MockServer, celestia_chain_id: String) { let default_node_info = Some(DefaultNodeInfo { network: celestia_chain_id, ..Default::default() @@ -243,14 +265,14 @@ async fn register_get_node_info(mock_server: &MockServer, celestia_chain_id: Str ) .respond_with(constant_response(response)) .with_name("global get node info") - .mount(mock_server) + .mount(mock_grpc_server) .await; } /// Registers a handler for all incoming `QueryAccountRequest`s which responds with a /// `QueryAccountResponse` using the received account address, but otherwise the same data every /// time. -async fn register_query_account(mock_server: &MockServer) { +async fn register_query_account(mock_grpc_server: &MockServer) { let responder = |request: &QueryAccountRequest| { let account = BaseAccount { address: request.address.clone(), @@ -272,7 +294,7 @@ async fn register_query_account(mock_server: &MockServer) { ) .respond_with(dynamic_response(responder)) .with_name("global query account") - .mount(mock_server) + .mount(mock_grpc_server) .await; } @@ -280,7 +302,7 @@ async fn register_query_account(mock_server: &MockServer) { /// `QueryAuthParamsResponse` every time. /// /// The response is as per current values in Celestia mainnet. -async fn register_query_auth_params(mock_server: &MockServer) { +async fn register_query_auth_params(mock_grpc_server: &MockServer) { let params = AuthParams { max_memo_characters: 256, tx_sig_limit: 7, @@ -297,7 +319,7 @@ async fn register_query_auth_params(mock_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global query auth params") - .mount(mock_server) + .mount(mock_grpc_server) .await; } @@ -305,7 +327,7 @@ async fn register_query_auth_params(mock_server: &MockServer) { /// `QueryBlobParamsResponse` every time. /// /// The response is as per current values in Celestia mainnet. -async fn register_query_blob_params(mock_server: &MockServer) { +async fn register_query_blob_params(mock_grpc_server: &MockServer) { let response = QueryBlobParamsResponse { params: Some(BlobParams { gas_per_blob_byte: 8, @@ -318,7 +340,7 @@ async fn register_query_blob_params(mock_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global query blob params") - .mount(mock_server) + .mount(mock_grpc_server) .await; } @@ -326,7 +348,7 @@ async fn register_query_blob_params(mock_server: &MockServer) { /// `MinGasPriceResponse` every time. /// /// The response is as per the current value in Celestia mainnet. -async fn register_min_gas_price(mock_server: &MockServer) { +async fn register_min_gas_price(mock_grpc_server: &MockServer) { let response = MinGasPriceResponse { minimum_gas_price: "0.002000000000000000utia".to_string(), }; @@ -336,7 +358,7 @@ async fn register_min_gas_price(mock_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global min gas price") - .mount(mock_server) + .mount(mock_grpc_server) .await; } @@ -406,7 +428,7 @@ impl TxService for CelestiaAppServiceImpl { self: Arc, request: Request, ) -> Result, Status> { - self.0.handle_request(GET_TX_GRPC_NAME, request).await + self.0.handle_request("get_tx", request).await } async fn broadcast_tx( diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs index 130d0e28a5..fc37e5dd0e 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs @@ -275,32 +275,22 @@ impl TestSequencerRelayer { .await } - /// Mounts a Celestia `GetTx` response. - /// - /// The `debug_name` is assigned to the mock and is output on error to assist with debugging. - /// It is also assigned as the `TxHash` in the request and response. - pub async fn mount_celestia_app_get_tx_response( - &self, - celestia_height: i64, - debug_name: impl Into, - ) { + pub async fn mount_celestia_app_tx_status_response(&self, celestia_height: i64, status: &str) { self.celestia_app - .mount_get_tx_response(celestia_height, debug_name) + .mount_tx_status_response(status.to_string(), celestia_height) .await; } - /// Mounts a Celestia `GetTx` response and returns a `GrpcMockGuard` to allow for waiting for - /// the mock to be satisfied. - /// - /// The `debug_name` is assigned to the mock and is output on error to assist with debugging. - /// It is also assigned as the `TxHash` in the request and response. - pub async fn mount_celestia_app_get_tx_response_as_scoped( + /// Mounts a Celestia `TxStatus` response and returns a `wiremock::MockGuard` to allow for + /// awaiting its satisfaction. + pub async fn mount_celestia_app_tx_status_response_as_scoped( &self, celestia_height: i64, - debug_name: impl Into, - ) -> GrpcMockGuard { + status: &str, + expected: u64, + ) -> wiremock::MockGuard { self.celestia_app - .mount_get_tx_response_as_scoped(celestia_height, debug_name) + .mount_tx_status_response_as_scoped(status.to_string(), celestia_height, expected) .await } @@ -680,7 +670,8 @@ impl TestSequencerRelayerConfig { LazyLock::force(&TELEMETRY); let celestia_app = MockCelestiaAppServer::spawn(self.celestia_chain_id.clone()).await; - let celestia_app_grpc_endpoint = format!("http://{}", celestia_app.local_addr); + let celestia_app_grpc_endpoint = format!("http://{}", celestia_app.grpc_local_addr); + let celestia_app_http_endpoint = celestia_app.http_local_addr.clone(); let celestia_keyfile = write_file( b"c8076374e2a4a58db1c924e3dafc055e9685481054fe99e58ed67f5c6ed80e62".as_slice(), ) @@ -717,6 +708,7 @@ impl TestSequencerRelayerConfig { cometbft_endpoint: cometbft.uri(), sequencer_grpc_endpoint, celestia_app_grpc_endpoint, + celestia_app_http_endpoint, celestia_app_key_file: celestia_keyfile.path().to_string_lossy().to_string(), block_time: 1000, only_include_rollups, diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index 93770c5dce..ee1ef2b32a 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -30,16 +30,16 @@ async fn one_block_is_relayed_to_celestia() { sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 1") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) .await; // The `MIN_POLL_INTERVAL_SECS` is 1, meaning the relayer waits for 1 second before attempting - // the first `GetTx`, so we wait for 2 seconds. + // the first `tx_status`, so we wait for 2 seconds. sequencer_relayer .timeout_ms( 2_000, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; @@ -61,7 +61,7 @@ async fn one_block_is_relayed_to_celestia() { ); } -#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +#[tokio::test(flavor = "multi_thread", worker_threads = 2)] async fn report_degraded_if_block_fetch_fails() { let sequencer_relayer = TestSequencerRelayerConfig::default().spawn_relayer().await; @@ -80,8 +80,8 @@ async fn report_degraded_if_block_fetch_fails() { sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 1") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) .await; let healthz_status = sequencer_relayer .wait_for_healthz(StatusCode::OK, 2_000, "waiting for first healthz") @@ -90,8 +90,8 @@ async fn report_degraded_if_block_fetch_fails() { sequencer_relayer .timeout_ms( 2_000, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; @@ -139,14 +139,14 @@ async fn later_height_in_state_leads_to_expected_relay() { sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 1") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( 2_000, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; @@ -193,28 +193,22 @@ async fn three_blocks_are_relayed() { sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; - sequencer_relayer - .mount_celestia_app_get_tx_response(53, "get tx 1") - .await; sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 2") .await; - sequencer_relayer - .mount_celestia_app_get_tx_response(53, "get tx 2") - .await; sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 3") .await; - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 3") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 3) .await; - // Each block will have taken ~1 second due to the delay before each `GetTx`, so use 4.5 + // Each block will have taken ~1 second due to the delay before each `tx_status`, so use 4.5 // seconds. sequencer_relayer .timeout_ms( 4_500, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; @@ -272,14 +266,14 @@ async fn should_filter_rollup() { sequencer_relayer .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 1") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( 10_000, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; @@ -325,14 +319,14 @@ async fn should_shut_down() { // process. sequencer_relayer.relayer_shutdown_handle.take(); - let get_tx_guard = sequencer_relayer - .mount_celestia_app_get_tx_response_as_scoped(53, "get tx 1") + let tx_status_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( 2_000, - "waiting for get tx guard", - get_tx_guard.wait_until_satisfied(), + "waiting for tx status guard", + tx_status_guard.wait_until_satisfied(), ) .await; From 20acaf64d48907fcc1b7cdde21efdd147102a812 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Fri, 31 Jan 2025 15:10:13 -0600 Subject: [PATCH 02/12] update cargo manifest, dev values --- crates/astria-sequencer-relayer/Cargo.toml | 6 +++++- dev/argocd/pr-preview-envs/sequencer-appset.yaml | 1 + dev/values/validators/node0.yml | 1 + dev/values/validators/single.yml | 1 + 4 files changed, 8 insertions(+), 1 deletion(-) diff --git a/crates/astria-sequencer-relayer/Cargo.toml b/crates/astria-sequencer-relayer/Cargo.toml index dd3229a3b9..f06f4abb05 100644 --- a/crates/astria-sequencer-relayer/Cargo.toml +++ b/crates/astria-sequencer-relayer/Cargo.toml @@ -25,7 +25,11 @@ humantime = { workspace = true } humantime-serde = "1.1.1" hyper = { workspace = true } itoa = { workspace = true } -jsonrpsee = { version = "0.20", features = ["client-core", "jsonrpsee-http-client", "macros"] } +jsonrpsee = { version = "0.20", features = [ + "client-core", + "jsonrpsee-http-client", + "macros", +] } pbjson-types = { workspace = true } pin-project-lite = { workspace = true } prost = { workspace = true } diff --git a/dev/argocd/pr-preview-envs/sequencer-appset.yaml b/dev/argocd/pr-preview-envs/sequencer-appset.yaml index 4794c0e46a..eee5ca1a34 100644 --- a/dev/argocd/pr-preview-envs/sequencer-appset.yaml +++ b/dev/argocd/pr-preview-envs/sequencer-appset.yaml @@ -114,6 +114,7 @@ spec: config: relayer: celestiaAppGrpc: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:9090 + celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.pr-{{.number}}.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.pr-{{.number}}.svc.cluster.local:8080 diff --git a/dev/values/validators/node0.yml b/dev/values/validators/node0.yml index 9dd3f4ddf8..7e21a47f16 100644 --- a/dev/values/validators/node0.yml +++ b/dev/values/validators/node0.yml @@ -52,6 +52,7 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 + celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: diff --git a/dev/values/validators/single.yml b/dev/values/validators/single.yml index 9ec1e3cf1f..b675e571f2 100644 --- a/dev/values/validators/single.yml +++ b/dev/values/validators/single.yml @@ -36,6 +36,7 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 + celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: From 26f12ec13afbb5f5cefcaa34310f3882da0de485 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Fri, 31 Jan 2025 15:44:08 -0600 Subject: [PATCH 03/12] fix smoke tests, add metrics, keep polling if 'unknown' --- .../astria-sequencer-relayer/src/metrics.rs | 15 ++++++++- .../src/relayer/builder.rs | 1 + .../src/relayer/celestia_client/builder.rs | 6 ++++ .../src/relayer/celestia_client/mod.rs | 32 ++++++++++++------- dev/values/validators/node0.yml | 2 +- dev/values/validators/single.yml | 2 +- 6 files changed, 43 insertions(+), 15 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/metrics.rs b/crates/astria-sequencer-relayer/src/metrics.rs index 85491bd751..6cca355a68 100644 --- a/crates/astria-sequencer-relayer/src/metrics.rs +++ b/crates/astria-sequencer-relayer/src/metrics.rs @@ -26,6 +26,7 @@ pub struct Metrics { celestia_fees_total_utia: Gauge, celestia_fees_utia_per_uncompressed_blob_byte: Gauge, celestia_fees_utia_per_compressed_blob_byte: Gauge, + celestia_evicted_transaction_count: Counter, } impl Metrics { @@ -88,6 +89,10 @@ impl Metrics { pub(crate) fn set_celestia_fees_utia_per_compressed_blob_byte(&self, utia: f64) { self.celestia_fees_utia_per_compressed_blob_byte.set(utia); } + + pub(crate) fn increment_celestia_evicted_transaction_count(&self) { + self.celestia_evicted_transaction_count.increment(1); + } } impl telemetry::Metrics for Metrics { @@ -213,6 +218,12 @@ impl telemetry::Metrics for Metrics { submission", )? .register()?; + let celestia_evicted_transaction_count = builder + .new_counter_factory( + CELESTIA_EVICTED_TRANSACTION_COUNT, + "The number of transactions evicted from the Celestia mempool", + )? + .register()?; Ok(Self { celestia_submission_height, @@ -230,6 +241,7 @@ impl telemetry::Metrics for Metrics { celestia_fees_total_utia, celestia_fees_utia_per_uncompressed_blob_byte, celestia_fees_utia_per_compressed_blob_byte, + celestia_evicted_transaction_count, }) } } @@ -249,7 +261,8 @@ metric_names!(const METRICS_NAMES: COMPRESSION_RATIO_FOR_ASTRIA_BLOCK, CELESTIA_FEES_TOTAL_UTIA, CELESTIA_FEES_UTIA_PER_UNCOMPRESSED_BLOB_BYTE, - CELESTIA_FEES_UTIA_PER_COMPRESSED_BLOB_BYTE + CELESTIA_FEES_UTIA_PER_COMPRESSED_BLOB_BYTE, + CELESTIA_EVICTED_TRANSACTION_COUNT, ); #[cfg(test)] diff --git a/crates/astria-sequencer-relayer/src/relayer/builder.rs b/crates/astria-sequencer-relayer/src/relayer/builder.rs index eab33b52eb..92eed7753c 100644 --- a/crates/astria-sequencer-relayer/src/relayer/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/builder.rs @@ -85,6 +85,7 @@ impl Builder { celestia_app_http_endpoint, celestia_keys, state.clone(), + metrics, ) .wrap_err("failed to create celestia client builder")? }; diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs index e001847899..9c9f73d204 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs @@ -31,6 +31,7 @@ use super::{ CelestiaKeys, GrpcResponseError, }; +use crate::Metrics; /// All gRPCs will time out with the given duration. const REQUEST_TIMEOUT: Duration = Duration::from_secs(5); @@ -84,6 +85,7 @@ pub(in crate::relayer) struct Builder { address: Bech32Address, /// A handle to the mutable state of the relayer. state: Arc, + metrics: &'static Metrics, } impl Builder { @@ -94,6 +96,7 @@ impl Builder { tx_status_endpoint: String, signing_keys: CelestiaKeys, state: Arc, + metrics: &'static Metrics, ) -> Result { let grpc_channel = Endpoint::from(grpc_endpoint) .timeout(REQUEST_TIMEOUT) @@ -106,6 +109,7 @@ impl Builder { signing_keys, address, state, + metrics, }) } @@ -121,6 +125,7 @@ impl Builder { signing_keys, address, state, + metrics, } = self; if received_celestia_chain_id != configured_celestia_chain_id { @@ -146,6 +151,7 @@ impl Builder { signing_keys, address, chain_id: received_celestia_chain_id, + metrics, }) } diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index b9b4ead3f6..cb6bde4a14 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -122,6 +122,8 @@ use tracing::{ Level, }; +use crate::Metrics; + // From https://github.com/celestiaorg/cosmos-sdk/blob/v1.18.3-sdk-v0.46.14/types/errors/errors.go#L75 const INSUFFICIENT_FEE_CODE: u32 = 13; @@ -147,7 +149,7 @@ struct TxStatusResponse { /// chain. /// /// It is constructed using a [`CelestiaClientBuilder`]. -#[derive(Debug, Clone)] +#[derive(Clone)] pub(super) struct CelestiaClient { /// The inner `tonic` gRPC channel shared by the various generated gRPC clients. grpc_channel: Channel, @@ -161,6 +163,7 @@ pub(super) struct CelestiaClient { address: Bech32Address, /// The Celestia network ID. chain_id: String, + metrics: &'static Metrics, } impl CelestiaClient { @@ -396,20 +399,21 @@ impl CelestiaClient { // The min seconds to sleep after receiving a TxStatus response and sending the next // request. const POLL_INTERVAL_SECS: u64 = 1; - // How long to wait after starting `confirm_submission` before starting to log errors. - const START_LOGGING_DELAY: Duration = Duration::from_secs(12); - // The minimum duration between logging errors. - const LOG_ERROR_INTERVAL: Duration = Duration::from_secs(5); + // The minimum duration between logs. + const LOG_INTERVAL: Duration = Duration::from_secs(5); + // The maximum amount of time to wait for a transaction to be committed if its status is + // `UNKNOWN`. + const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(10); let start = Instant::now(); let mut logged_at = start; - let mut log_pending_if_due = || { - if start.elapsed() <= START_LOGGING_DELAY || logged_at.elapsed() <= LOG_ERROR_INTERVAL { + let mut log_if_due = |status: &str| { + if logged_at.elapsed() <= LOG_INTERVAL { return; } debug!( - reason = "transaction still pending", + reason = format!("transaction status: {status}"), tx_hash = %hex_encoded_tx_hash, elapsed_seconds = start.elapsed().as_secs_f32(), "waiting to confirm blob submission" @@ -420,14 +424,18 @@ impl CelestiaClient { loop { match self.tx_status(hex_encoded_tx_hash.clone()).await { Ok(TxStatus::Unknown) => { - break Err(ConfirmSubmissionError::UnknownStatus { - hash: hex_encoded_tx_hash, - }) + if start.elapsed() > MAX_WAIT_FOR_UNKNOWN { + break Err(ConfirmSubmissionError::UnknownStatus { + hash: hex_encoded_tx_hash, + }); + } + log_if_due("UNKNOWN"); } Ok(TxStatus::Pending) => { - log_pending_if_due(); + log_if_due("PENDING"); } Ok(TxStatus::Evicted) => { + self.metrics.increment_celestia_evicted_transaction_count(); break Err(ConfirmSubmissionError::Evicted { hash: hex_encoded_tx_hash, }); diff --git a/dev/values/validators/node0.yml b/dev/values/validators/node0.yml index 7e21a47f16..fdc6322a08 100644 --- a/dev/values/validators/node0.yml +++ b/dev/values/validators/node0.yml @@ -52,7 +52,7 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 - celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 + celestiaAppHttp: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: diff --git a/dev/values/validators/single.yml b/dev/values/validators/single.yml index b675e571f2..2e59727f27 100644 --- a/dev/values/validators/single.yml +++ b/dev/values/validators/single.yml @@ -36,7 +36,7 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 - celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 + celestiaAppHttp: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: From b7a104a94f7063c526ebfc6f6586d13a3b40728d Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Mon, 3 Feb 2025 12:14:15 -0600 Subject: [PATCH 04/12] switch to gRPC tx_status call, add more tests and metrics --- Cargo.lock | 1 - charts/sequencer-relayer/Chart.yaml | 4 +- .../templates/configmaps.yaml | 1 - charts/sequencer-relayer/values.yaml | 3 +- charts/sequencer/Chart.lock | 6 +- charts/sequencer/Chart.yaml | 4 +- crates/astria-core/src/generated/mod.rs | 29 +- crates/astria-sequencer-relayer/CHANGELOG.md | 4 + crates/astria-sequencer-relayer/Cargo.toml | 5 - .../local.env.example | 3 - crates/astria-sequencer-relayer/src/config.rs | 1 - .../astria-sequencer-relayer/src/metrics.rs | 22 ++ .../src/relayer/builder.rs | 3 - .../src/relayer/celestia_client/builder.rs | 29 +- .../src/relayer/celestia_client/error.rs | 6 +- .../src/relayer/celestia_client/mod.rs | 64 ++-- .../src/sequencer_relayer.rs | 2 - .../helpers/mock_celestia_app_server.rs | 163 ++++---- .../helpers/test_sequencer_relayer.rs | 31 +- .../tests/blackbox/main.rs | 356 +++++++++++++++++- .../pr-preview-envs/sequencer-appset.yaml | 1 - dev/values/validators/node0.yml | 1 - dev/values/validators/single.yml | 1 - 23 files changed, 554 insertions(+), 186 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 5b5c49deed..cf483afff4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -922,7 +922,6 @@ dependencies = [ "isahc", "itertools 0.12.1", "itoa", - "jsonrpsee", "k256", "pbjson-types", "pin-project-lite", diff --git a/charts/sequencer-relayer/Chart.yaml b/charts/sequencer-relayer/Chart.yaml index b55ac29977..77ff6e351c 100644 --- a/charts/sequencer-relayer/Chart.yaml +++ b/charts/sequencer-relayer/Chart.yaml @@ -15,13 +15,13 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.1.0 +version: 1.0.1 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. # It is recommended to use it with quotes. -appVersion: "1.1.0" +appVersion: "1.0.0" maintainers: - name: wafflesvonmaple diff --git a/charts/sequencer-relayer/templates/configmaps.yaml b/charts/sequencer-relayer/templates/configmaps.yaml index 744377129a..97738d36e6 100644 --- a/charts/sequencer-relayer/templates/configmaps.yaml +++ b/charts/sequencer-relayer/templates/configmaps.yaml @@ -30,7 +30,6 @@ data: ASTRIA_SEQUENCER_RELAYER_CELESTIA_CHAIN_ID: "{{ .Values.config.relayer.celestiaChainId }}" {{- if not .Values.global.dev }} {{- else }} - ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_HTTP_ENDPOINT: "{{ .Values.config.relayer.celestiaAppHttp }}" {{- end }} --- apiVersion: v1 diff --git a/charts/sequencer-relayer/values.yaml b/charts/sequencer-relayer/values.yaml index 35fed125b1..16b9494776 100644 --- a/charts/sequencer-relayer/values.yaml +++ b/charts/sequencer-relayer/values.yaml @@ -14,7 +14,7 @@ images: sequencerRelayer: repo: ghcr.io/astriaorg/sequencer-relayer pullPolicy: IfNotPresent - tag: 1.1.0 + tag: 1.0.0 devTag: latest config: @@ -22,7 +22,6 @@ config: sequencerChainId: "" celestiaChainId: "" celestiaAppGrpc: "" - celestiaAppHttp: "" cometbftRpc: "" sequencerGrpc: "" onlyIncludeRollups: "" diff --git a/charts/sequencer/Chart.lock b/charts/sequencer/Chart.lock index d1cc6f2ca1..78d4dfb84c 100644 --- a/charts/sequencer/Chart.lock +++ b/charts/sequencer/Chart.lock @@ -1,6 +1,6 @@ dependencies: - name: sequencer-relayer repository: file://../sequencer-relayer - version: 1.1.0 -digest: sha256:ab846eac25dd1f19c7bed590f94bbc0eecabe3501a46b2edf5cc90306472eb2d -generated: "2025-01-31T14:27:35.263935-06:00" + version: 1.0.1 +digest: sha256:3b3ce65ff473606fcc86027653cadd212ba45ac8b39d5806d713b48f695ad235 +generated: "2025-02-03T12:07:18.438552-06:00" diff --git a/charts/sequencer/Chart.yaml b/charts/sequencer/Chart.yaml index 69c8104bef..5107f393d6 100644 --- a/charts/sequencer/Chart.yaml +++ b/charts/sequencer/Chart.yaml @@ -15,7 +15,7 @@ type: application # This is the chart version. This version number should be incremented each time you make changes # to the chart and its templates, including the app version. # Versions are expected to follow Semantic Versioning (https://semver.org/) -version: 1.0.3 +version: 1.0.2 # This is the version number of the application being deployed. This version number should be # incremented each time you make changes to the application. Versions are not expected to # follow Semantic Versioning. They should reflect the version the application is using. @@ -24,7 +24,7 @@ appVersion: "1.0.0" dependencies: - name: sequencer-relayer - version: "1.1.0" + version: "1.0.1" repository: "file://../sequencer-relayer" condition: sequencer-relayer.enabled diff --git a/crates/astria-core/src/generated/mod.rs b/crates/astria-core/src/generated/mod.rs index 1fef28c5e7..b9eae5191b 100644 --- a/crates/astria-core/src/generated/mod.rs +++ b/crates/astria-core/src/generated/mod.rs @@ -192,14 +192,29 @@ pub mod astria { #[path = ""] pub mod celestia { - #[path = "celestia.blob.v1.rs"] - pub mod v1 { - include!("celestia.blob.v1.rs"); + pub mod blob { + #[path = "celestia.blob.v1.rs"] + pub mod v1 { + include!("celestia.blob.v1.rs"); - #[cfg(feature = "serde")] - mod _serde_impl { - use super::*; - include!("celestia.blob.v1.serde.rs"); + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("celestia.blob.v1.serde.rs"); + } + } + } + pub mod core { + pub mod v1 { + pub mod tx { + include!("celestia.core.v1.tx.rs"); + + #[cfg(feature = "serde")] + mod _serde_impl { + use super::*; + include!("celestia.core.v1.tx.serde.rs"); + } + } } } } diff --git a/crates/astria-sequencer-relayer/CHANGELOG.md b/crates/astria-sequencer-relayer/CHANGELOG.md index 0bd6e4fa60..ce9e59d798 100644 --- a/crates/astria-sequencer-relayer/CHANGELOG.md +++ b/crates/astria-sequencer-relayer/CHANGELOG.md @@ -9,6 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +- Add `celestia_app_html_endpoint` env var for polling Celestia's `tx_status` RPC [#1940](https://github.com/astriaorg/astria/pull/1940). + ### Changed - Update `idna` dependency to resolve cargo audit warning [#1869](https://github.com/astriaorg/astria/pull/1869). diff --git a/crates/astria-sequencer-relayer/Cargo.toml b/crates/astria-sequencer-relayer/Cargo.toml index f06f4abb05..1d0589e932 100644 --- a/crates/astria-sequencer-relayer/Cargo.toml +++ b/crates/astria-sequencer-relayer/Cargo.toml @@ -25,11 +25,6 @@ humantime = { workspace = true } humantime-serde = "1.1.1" hyper = { workspace = true } itoa = { workspace = true } -jsonrpsee = { version = "0.20", features = [ - "client-core", - "jsonrpsee-http-client", - "macros", -] } pbjson-types = { workspace = true } pin-project-lite = { workspace = true } prost = { workspace = true } diff --git a/crates/astria-sequencer-relayer/local.env.example b/crates/astria-sequencer-relayer/local.env.example index 5c1a435bf8..81b5c75d5f 100644 --- a/crates/astria-sequencer-relayer/local.env.example +++ b/crates/astria-sequencer-relayer/local.env.example @@ -38,9 +38,6 @@ ASTRIA_SEQUENCER_RELAYER_SEQUENCER_GRPC_ENDPOINT="http://127.0.0.1:8080" # 127.0.0.1:9090 is the default socket address for its gRPC server. ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_GRPC_ENDPOINT="http://127.0.0.1:9090" -# Address at which celestia app serves HTTP RPCs. -ASTRIA_SEQUENCER_RELAYER_CELESTIA_APP_HTTP_ENDPOINT="http://127.0.0.1:26657" - # The path to the file storing the signing key used to sign blob submissions to the celestia app. # The file should be a hex-encoded secp256k1 secret key, such as could be output via # `celestia-appd keys export --keyring-backend=... --home=... --unsafe --unarmored-hex`, diff --git a/crates/astria-sequencer-relayer/src/config.rs b/crates/astria-sequencer-relayer/src/config.rs index 34427b77fe..ee6a99e829 100644 --- a/crates/astria-sequencer-relayer/src/config.rs +++ b/crates/astria-sequencer-relayer/src/config.rs @@ -31,7 +31,6 @@ pub struct Config { pub cometbft_endpoint: String, pub sequencer_grpc_endpoint: String, pub celestia_app_grpc_endpoint: String, - pub celestia_app_http_endpoint: String, pub celestia_app_key_file: String, pub block_time: u64, // Would ideally be private; accessed via the public getter which converts this to a collection diff --git a/crates/astria-sequencer-relayer/src/metrics.rs b/crates/astria-sequencer-relayer/src/metrics.rs index 6cca355a68..a41a5012e9 100644 --- a/crates/astria-sequencer-relayer/src/metrics.rs +++ b/crates/astria-sequencer-relayer/src/metrics.rs @@ -27,6 +27,7 @@ pub struct Metrics { celestia_fees_utia_per_uncompressed_blob_byte: Gauge, celestia_fees_utia_per_compressed_blob_byte: Gauge, celestia_evicted_transaction_count: Counter, + celestia_unknown_status_transaction_count: Counter, } impl Metrics { @@ -93,6 +94,10 @@ impl Metrics { pub(crate) fn increment_celestia_evicted_transaction_count(&self) { self.celestia_evicted_transaction_count.increment(1); } + + pub(crate) fn increment_celestia_unknown_status_transaction_count(&self) { + self.celestia_unknown_status_transaction_count.increment(1); + } } impl telemetry::Metrics for Metrics { @@ -224,6 +229,13 @@ impl telemetry::Metrics for Metrics { "The number of transactions evicted from the Celestia mempool", )? .register()?; + let celestia_unknown_status_transaction_count = builder + .new_counter_factory( + CELESTIA_UNKNOWN_STATUS_TRANSACTION_COUNT, + "The number of transactions whose status in the Celestia mempool remains unknown \ + after 10s", + )? + .register()?; Ok(Self { celestia_submission_height, @@ -242,6 +254,7 @@ impl telemetry::Metrics for Metrics { celestia_fees_utia_per_uncompressed_blob_byte, celestia_fees_utia_per_compressed_blob_byte, celestia_evicted_transaction_count, + celestia_unknown_status_transaction_count, }) } } @@ -263,6 +276,7 @@ metric_names!(const METRICS_NAMES: CELESTIA_FEES_UTIA_PER_UNCOMPRESSED_BLOB_BYTE, CELESTIA_FEES_UTIA_PER_COMPRESSED_BLOB_BYTE, CELESTIA_EVICTED_TRANSACTION_COUNT, + CELESTIA_UNKNOWN_STATUS_TRANSACTION_COUNT, ); #[cfg(test)] @@ -315,5 +329,13 @@ mod tests { CELESTIA_FEES_UTIA_PER_COMPRESSED_BLOB_BYTE, "celestia_fees_utia_per_compressed_blob_byte", ); + assert_const( + CELESTIA_EVICTED_TRANSACTION_COUNT, + "celestia_evicted_transaction_count", + ); + assert_const( + CELESTIA_UNKNOWN_STATUS_TRANSACTION_COUNT, + "celestia_unknown_status_transaction_count", + ); } } diff --git a/crates/astria-sequencer-relayer/src/relayer/builder.rs b/crates/astria-sequencer-relayer/src/relayer/builder.rs index 92eed7753c..f938cb3cf1 100644 --- a/crates/astria-sequencer-relayer/src/relayer/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/builder.rs @@ -30,7 +30,6 @@ pub(crate) struct Builder { pub(crate) sequencer_chain_id: String, pub(crate) celestia_chain_id: String, pub(crate) celestia_app_grpc_endpoint: String, - pub(crate) celestia_app_http_endpoint: String, pub(crate) celestia_app_key_file: String, pub(crate) cometbft_endpoint: String, pub(crate) sequencer_poll_period: Duration, @@ -48,7 +47,6 @@ impl Builder { sequencer_chain_id, celestia_chain_id, celestia_app_grpc_endpoint, - celestia_app_http_endpoint, celestia_app_key_file, cometbft_endpoint, sequencer_poll_period, @@ -82,7 +80,6 @@ impl Builder { CelestiaClientBuilder::new( celestia_chain_id, uri, - celestia_app_http_endpoint, celestia_keys, state.clone(), metrics, diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs index 9c9f73d204..a215a4b900 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs @@ -3,15 +3,17 @@ use std::{ time::Duration, }; -use astria_core::generated::cosmos::{ - base::tendermint::v1beta1::{ - service_client::ServiceClient as NodeInfoClient, - GetNodeInfoRequest, +use astria_core::generated::{ + celestia::core::v1::tx::tx_client::TxClient as TxStatusClient, + cosmos::{ + base::tendermint::v1beta1::{ + service_client::ServiceClient as NodeInfoClient, + GetNodeInfoRequest, + }, + tx::v1beta1::service_client::ServiceClient as TxClient, }, - tx::v1beta1::service_client::ServiceClient as TxClient, }; use http::Uri; -use jsonrpsee::http_client::HttpClientBuilder; use tendermint::account::Id as AccountId; use thiserror::Error; use tonic::transport::{ @@ -60,10 +62,6 @@ pub(in crate::relayer) enum BuilderError { configured: String, received: String, }, - /// Failed to construct Celestia HTTP RPC client. - #[error("failed to construct Celestia HTTP RPC client: {error}")] - // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. - HttpClient { error: String }, } /// An error while encoding a Bech32 string. @@ -77,8 +75,6 @@ pub(in crate::relayer) struct Builder { configured_celestia_chain_id: String, /// The inner `tonic` gRPC channel shared by the various generated gRPC clients. grpc_channel: Channel, - /// The HTTP RPC endpoint for querying transaction status. - tx_status_endpoint: String, /// The crypto keys associated with our Celestia account. signing_keys: CelestiaKeys, /// The Bech32-encoded address of our Celestia account. @@ -93,7 +89,6 @@ impl Builder { pub(in crate::relayer) fn new( configured_celestia_chain_id: String, grpc_endpoint: Uri, - tx_status_endpoint: String, signing_keys: CelestiaKeys, state: Arc, metrics: &'static Metrics, @@ -105,7 +100,6 @@ impl Builder { Ok(Self { configured_celestia_chain_id, grpc_channel, - tx_status_endpoint, signing_keys, address, state, @@ -121,7 +115,6 @@ impl Builder { let Self { configured_celestia_chain_id, grpc_channel, - tx_status_endpoint, signing_keys, address, state, @@ -138,12 +131,8 @@ impl Builder { info!(celestia_chain_id = %received_celestia_chain_id, "confirmed celestia chain id"); state.set_celestia_connected(true); - let tx_status_client = HttpClientBuilder::default() - .build(tx_status_endpoint) - .map_err(|e| BuilderError::HttpClient { - error: e.to_string(), - })?; let tx_client = TxClient::new(grpc_channel.clone()); + let tx_status_client = TxStatusClient::new(grpc_channel.clone()); Ok(CelestiaClient { grpc_channel, tx_client, diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs index 13a3535d76..ddad7b38a6 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs @@ -129,8 +129,6 @@ pub(in crate::relayer) enum TxStatusError { #[error("request for `tx_status` failed: {error}")] // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. FailedToGetTxStatus { error: String }, - #[error("failed to parse `height` into u64")] - HeightParse(#[from] std::num::ParseIntError), } /// An error in confirming the submission of a transaction. @@ -139,7 +137,9 @@ pub(in crate::relayer) enum ConfirmSubmissionError { #[error("tx `{hash}` evicted from mempool")] Evicted { hash: String }, #[error("received `UNKNOWN` status from `tx_status` for tx: {hash}")] - UnknownStatus { hash: String }, + StatusUnknown { hash: String }, #[error("failed to get tx status")] TxStatus(#[from] TxStatusError), + #[error("received negative block height from Celestia: {height}")] + NegativeHeight { height: i64 }, } diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index cb6bde4a14..dc347f6365 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -22,11 +22,17 @@ use std::{ }; use astria_core::generated::{ - celestia::v1::{ - query_client::QueryClient as BlobQueryClient, - MsgPayForBlobs, - Params as BlobParams, - QueryParamsRequest as QueryBlobParamsRequest, + celestia::{ + blob::v1::{ + query_client::QueryClient as BlobQueryClient, + MsgPayForBlobs, + Params as BlobParams, + QueryParamsRequest as QueryBlobParamsRequest, + }, + core::v1::tx::{ + tx_client::TxClient as TxStatusClient, + TxStatusRequest, + }, }, cosmos::{ auth::v1beta1::{ @@ -89,7 +95,6 @@ use hex::{ FromHex, FromHexError, }; -use jsonrpsee::proc_macros::rpc; use prost::{ bytes::Bytes, Message as _, @@ -136,13 +141,7 @@ enum TxStatus { Unknown, Pending, Evicted, - Committed(u64), -} - -#[derive(Debug, Deserialize)] -struct TxStatusResponse { - pub height: String, - pub status: String, + Committed(i64), } /// A client using the gRPC interface of a remote Celestia app to submit blob data to the Celestia @@ -155,8 +154,8 @@ pub(super) struct CelestiaClient { grpc_channel: Channel, /// A gRPC client to broadcast and get transactions. tx_client: TxClient, - /// An HTTP client to receive transaction status. - tx_status_client: jsonrpsee::http_client::HttpClient, + /// A gRPC client for querying transaction status. + tx_status_client: TxStatusClient, /// The crypto keys associated with our Celestia account. signing_keys: CelestiaKeys, /// The Bech32-encoded address of our Celestia account. @@ -361,23 +360,20 @@ impl CelestiaClient { async fn tx_status(&mut self, hex_encoded_tx_hash: String) -> Result { let response = self .tx_status_client - .tx_status(hex_encoded_tx_hash.clone()) + .tx_status(TxStatusRequest { + tx_id: hex_encoded_tx_hash.clone(), + }) .await .map_err(|e| TxStatusError::FailedToGetTxStatus { error: e.to_string(), })?; - match response.status.as_str() { + match response.get_ref().status.as_str() { TX_STATUS_UNKNOWN => Ok(TxStatus::Unknown), TX_STATUS_PENDING => Ok(TxStatus::Pending), TX_STATUS_EVICTED => Ok(TxStatus::Evicted), - TX_STATUS_COMMITTED => Ok(TxStatus::Committed( - response - .height - .parse::() - .map_err(TxStatusError::HeightParse)?, - )), + TX_STATUS_COMMITTED => Ok(TxStatus::Committed(response.get_ref().height)), _ => Err(TxStatusError::UnfamiliarStatus { - status: response.status.to_string(), + status: response.get_ref().status.to_string(), hash: hex_encoded_tx_hash, }), } @@ -422,10 +418,13 @@ impl CelestiaClient { }; loop { + tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; match self.tx_status(hex_encoded_tx_hash.clone()).await { Ok(TxStatus::Unknown) => { if start.elapsed() > MAX_WAIT_FOR_UNKNOWN { - break Err(ConfirmSubmissionError::UnknownStatus { + self.metrics + .increment_celestia_unknown_status_transaction_count(); + break Err(ConfirmSubmissionError::StatusUnknown { hash: hex_encoded_tx_hash, }); } @@ -440,12 +439,17 @@ impl CelestiaClient { hash: hex_encoded_tx_hash, }); } - Ok(TxStatus::Committed(height)) => break Ok(height), + Ok(TxStatus::Committed(height)) => { + break Ok(height.try_into().map_err(|_| { + ConfirmSubmissionError::NegativeHeight { + height, + } + })?) + } Err(error) => { break Err(ConfirmSubmissionError::TxStatus(error)); } } - tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; } } } @@ -888,9 +892,3 @@ pub(in crate::relayer) enum DeserializeBlobTxHashError { #[error("failed to decode as hex for blob tx hash: {0}")] Hex(String), } - -#[rpc(client)] -pub trait TxStatusClient { - #[method(name = "tx_status")] - async fn tx_status(&self, hash: String) -> Result; -} diff --git a/crates/astria-sequencer-relayer/src/sequencer_relayer.rs b/crates/astria-sequencer-relayer/src/sequencer_relayer.rs index a7cbf39478..47cd5f1caf 100644 --- a/crates/astria-sequencer-relayer/src/sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/src/sequencer_relayer.rs @@ -66,7 +66,6 @@ impl SequencerRelayer { cometbft_endpoint, sequencer_grpc_endpoint, celestia_app_grpc_endpoint, - celestia_app_http_endpoint, celestia_app_key_file, block_time, api_addr, @@ -79,7 +78,6 @@ impl SequencerRelayer { sequencer_chain_id, celestia_chain_id, celestia_app_grpc_endpoint, - celestia_app_http_endpoint, celestia_app_key_file, cometbft_endpoint, sequencer_poll_period: Duration::from_millis(block_time), diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs index a7ed5230d1..5834b67fe2 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs @@ -7,14 +7,24 @@ use std::{ }; use astria_core::generated::{ - celestia::v1::{ - query_server::{ - Query as BlobQueryService, - QueryServer as BlobQueryServer, + celestia::{ + blob::v1::{ + query_server::{ + Query as BlobQueryService, + QueryServer as BlobQueryServer, + }, + Params as BlobParams, + QueryParamsRequest as QueryBlobParamsRequest, + QueryParamsResponse as QueryBlobParamsResponse, + }, + core::v1::tx::{ + tx_server::{ + Tx as TxStatusService, + TxServer as TxStatusServer, + }, + TxStatusRequest, + TxStatusResponse, }, - Params as BlobParams, - QueryParamsRequest as QueryBlobParamsRequest, - QueryParamsResponse as QueryBlobParamsResponse, }, cosmos::{ auth::v1beta1::{ @@ -83,7 +93,6 @@ use prost::{ Message, Name, }; -use serde_json::json; use tokio::task::JoinHandle; use tonic::{ transport::Server, @@ -91,22 +100,19 @@ use tonic::{ Response, Status, }; -use wiremock::MockServer as HttpMockServer; const GET_NODE_INFO_GRPC_NAME: &str = "get_node_info"; const QUERY_ACCOUNT_GRPC_NAME: &str = "query_account"; const QUERY_AUTH_PARAMS_GRPC_NAME: &str = "query_auth_params"; const QUERY_BLOB_PARAMS_GRPC_NAME: &str = "query_blob_params"; const MIN_GAS_PRICE_GRPC_NAME: &str = "min_gas_price"; -const TX_STATUS_JSONRPC_NAME: &str = "tx_status"; +const TX_STATUS_GRPC_NAME: &str = "tx_status"; const BROADCAST_TX_GRPC_NAME: &str = "broadcast_tx"; pub struct MockCelestiaAppServer { _server: JoinHandle>, - pub mock_grpc_server: MockServer, - pub mock_http_server: HttpMockServer, - pub grpc_local_addr: SocketAddr, - pub http_local_addr: String, + pub mock_server: MockServer, + pub local_addr: SocketAddr, pub namespaces: Arc>>, } @@ -115,31 +121,25 @@ impl MockCelestiaAppServer { use tokio_stream::wrappers::TcpListenerStream; let grpc_listener = tokio::net::TcpListener::bind("127.0.0.1:0").await.unwrap(); - let grpc_local_addr = grpc_listener.local_addr().unwrap(); - let mock_grpc_server = MockServer::new(); - - let http_listener = std::net::TcpListener::bind("127.0.0.1:0").unwrap(); - let mock_http_server = HttpMockServer::builder() - .listener(http_listener) - .start() - .await; - let http_local_addr = mock_http_server.uri(); + let local_addr = grpc_listener.local_addr().unwrap(); + let mock_server = MockServer::new(); - register_get_node_info(&mock_grpc_server, celestia_chain_id).await; - register_query_account(&mock_grpc_server).await; - register_query_auth_params(&mock_grpc_server).await; - register_query_blob_params(&mock_grpc_server).await; - register_min_gas_price(&mock_grpc_server).await; + register_get_node_info(&mock_server, celestia_chain_id).await; + register_query_account(&mock_server).await; + register_query_auth_params(&mock_server).await; + register_query_blob_params(&mock_server).await; + register_min_gas_price(&mock_server).await; let server = { - let service_impl = CelestiaAppServiceImpl(mock_grpc_server.clone()); + let service_impl = CelestiaAppServiceImpl(mock_server.clone()); tokio::spawn(async move { Server::builder() .add_service(NodeInfoServer::new(service_impl.clone())) .add_service(AuthQueryServer::new(service_impl.clone())) .add_service(BlobQueryServer::new(service_impl.clone())) .add_service(MinGasPriceServer::new(service_impl.clone())) - .add_service(TxServer::new(service_impl)) + .add_service(TxServer::new(service_impl.clone())) + .add_service(TxStatusServer::new(service_impl)) .serve_with_incoming(TcpListenerStream::new(grpc_listener)) .await .wrap_err("gRPC sequencer server failed") @@ -147,26 +147,28 @@ impl MockCelestiaAppServer { }; Self { _server: server, - mock_grpc_server, - mock_http_server, - grpc_local_addr, - http_local_addr, + mock_server, + local_addr, namespaces: Arc::new(Mutex::new(Vec::new())), } } pub async fn mount_broadcast_tx_response(&self, debug_name: impl Into) { self.prepare_broadcast_tx_response(debug_name) - .mount(&self.mock_grpc_server) + .mount(&self.mock_server) .await; } pub async fn mount_broadcast_tx_response_as_scoped( &self, debug_name: impl Into, + expected: Option, + up_to_n_times: Option, ) -> MockGuard { self.prepare_broadcast_tx_response(debug_name) - .mount_as_scoped(&self.mock_grpc_server) + .expect(expected.unwrap_or(1)) + .up_to_n_times(up_to_n_times.unwrap_or(1000)) + .mount_as_scoped(&self.mock_server) .await } @@ -199,57 +201,50 @@ impl MockCelestiaAppServer { .with_name(debug_name) } - pub async fn mount_tx_status_response(&self, status: String, height: i64) { + pub async fn mount_tx_status_response( + &self, + debug_name: impl Into, + status: String, + height: i64, + ) { prepare_tx_status_response(status, height) - .mount(&self.mock_http_server) + .with_name(debug_name) + .mount(&self.mock_server) .await; } pub async fn mount_tx_status_response_as_scoped( &self, + debug_name: impl Into, status: String, height: i64, - expected: u64, - ) -> wiremock::MockGuard { + expected: Option, + up_to_n_times: Option, + ) -> MockGuard { prepare_tx_status_response(status, height) - .expect(expected) - .mount_as_scoped(&self.mock_http_server) + .expect(expected.unwrap_or(1)) + .up_to_n_times(up_to_n_times.unwrap_or(1000)) + .with_name(debug_name) + .mount_as_scoped(&self.mock_server) .await } } -fn prepare_tx_status_response(status: String, height: i64) -> wiremock::Mock { - use wiremock::{ - matchers::body_partial_json, - Mock, - ResponseTemplate, - }; - - Mock::given(body_partial_json(json!({ - "jsonrpc": "2.0", - "method": TX_STATUS_JSONRPC_NAME - }))) - .respond_with(move |request: &wiremock::Request| { - let body: serde_json::Value = serde_json::from_slice(&request.body).unwrap(); - ResponseTemplate::new(200).set_body_json(json!({ - "jsonrpc": "2.0", - "id": body.get("id"), - "result": { - "height": height.to_string(), - "index": 0, - "execution_code": 0, - "error": "", - "status": status - } - })) - }) - .named(TX_STATUS_JSONRPC_NAME) - .expect(1) +fn prepare_tx_status_response(status: String, height: i64) -> Mock { + Mock::for_rpc_given(TX_STATUS_GRPC_NAME, message_type::()).respond_with( + constant_response(TxStatusResponse { + height, + index: 0, + execution_code: 0, + error: String::new(), + status, + }), + ) } /// Registers a handler for all incoming `GetNodeInfoRequest`s which responds with the same /// `GetNodeInfoResponse` every time. -async fn register_get_node_info(mock_grpc_server: &MockServer, celestia_chain_id: String) { +async fn register_get_node_info(mock_server: &MockServer, celestia_chain_id: String) { let default_node_info = Some(DefaultNodeInfo { network: celestia_chain_id, ..Default::default() @@ -265,14 +260,14 @@ async fn register_get_node_info(mock_grpc_server: &MockServer, celestia_chain_id ) .respond_with(constant_response(response)) .with_name("global get node info") - .mount(mock_grpc_server) + .mount(mock_server) .await; } /// Registers a handler for all incoming `QueryAccountRequest`s which responds with a /// `QueryAccountResponse` using the received account address, but otherwise the same data every /// time. -async fn register_query_account(mock_grpc_server: &MockServer) { +async fn register_query_account(mock_server: &MockServer) { let responder = |request: &QueryAccountRequest| { let account = BaseAccount { address: request.address.clone(), @@ -294,7 +289,7 @@ async fn register_query_account(mock_grpc_server: &MockServer) { ) .respond_with(dynamic_response(responder)) .with_name("global query account") - .mount(mock_grpc_server) + .mount(mock_server) .await; } @@ -302,7 +297,7 @@ async fn register_query_account(mock_grpc_server: &MockServer) { /// `QueryAuthParamsResponse` every time. /// /// The response is as per current values in Celestia mainnet. -async fn register_query_auth_params(mock_grpc_server: &MockServer) { +async fn register_query_auth_params(mock_server: &MockServer) { let params = AuthParams { max_memo_characters: 256, tx_sig_limit: 7, @@ -319,7 +314,7 @@ async fn register_query_auth_params(mock_grpc_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global query auth params") - .mount(mock_grpc_server) + .mount(mock_server) .await; } @@ -327,7 +322,7 @@ async fn register_query_auth_params(mock_grpc_server: &MockServer) { /// `QueryBlobParamsResponse` every time. /// /// The response is as per current values in Celestia mainnet. -async fn register_query_blob_params(mock_grpc_server: &MockServer) { +async fn register_query_blob_params(mock_server: &MockServer) { let response = QueryBlobParamsResponse { params: Some(BlobParams { gas_per_blob_byte: 8, @@ -340,7 +335,7 @@ async fn register_query_blob_params(mock_grpc_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global query blob params") - .mount(mock_grpc_server) + .mount(mock_server) .await; } @@ -348,7 +343,7 @@ async fn register_query_blob_params(mock_grpc_server: &MockServer) { /// `MinGasPriceResponse` every time. /// /// The response is as per the current value in Celestia mainnet. -async fn register_min_gas_price(mock_grpc_server: &MockServer) { +async fn register_min_gas_price(mock_server: &MockServer) { let response = MinGasPriceResponse { minimum_gas_price: "0.002000000000000000utia".to_string(), }; @@ -358,7 +353,7 @@ async fn register_min_gas_price(mock_grpc_server: &MockServer) { ) .respond_with(constant_response(response)) .with_name("global min gas price") - .mount(mock_grpc_server) + .mount(mock_server) .await; } @@ -447,3 +442,13 @@ fn extract_blob_namespaces(request: &BroadcastTxRequest) -> Vec { .map(|blob| Namespace::new_v0(blob.namespace_id.as_ref()).unwrap()) .collect() } + +#[async_trait::async_trait] +impl TxStatusService for CelestiaAppServiceImpl { + async fn tx_status( + self: Arc, + request: tonic::Request, + ) -> Result, tonic::Status> { + self.0.handle_request(TX_STATUS_GRPC_NAME, request).await + } +} diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs index fc37e5dd0e..d23dbe02f6 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs @@ -269,15 +269,22 @@ impl TestSequencerRelayer { pub async fn mount_celestia_app_broadcast_tx_response_as_scoped( &self, debug_name: impl Into, + expected: Option, + up_to_n_times: Option, ) -> GrpcMockGuard { self.celestia_app - .mount_broadcast_tx_response_as_scoped(debug_name) + .mount_broadcast_tx_response_as_scoped(debug_name, expected, up_to_n_times) .await } - pub async fn mount_celestia_app_tx_status_response(&self, celestia_height: i64, status: &str) { + pub async fn mount_celestia_app_tx_status_response( + &self, + debug_name: impl Into, + celestia_height: i64, + status: &str, + ) { self.celestia_app - .mount_tx_status_response(status.to_string(), celestia_height) + .mount_tx_status_response(debug_name, status.to_string(), celestia_height) .await; } @@ -285,12 +292,20 @@ impl TestSequencerRelayer { /// awaiting its satisfaction. pub async fn mount_celestia_app_tx_status_response_as_scoped( &self, + debug_name: impl Into, celestia_height: i64, status: &str, - expected: u64, - ) -> wiremock::MockGuard { + expected: Option, + up_to_n_times: Option, + ) -> GrpcMockGuard { self.celestia_app - .mount_tx_status_response_as_scoped(status.to_string(), celestia_height, expected) + .mount_tx_status_response_as_scoped( + debug_name, + status.to_string(), + celestia_height, + expected, + up_to_n_times, + ) .await } @@ -670,8 +685,7 @@ impl TestSequencerRelayerConfig { LazyLock::force(&TELEMETRY); let celestia_app = MockCelestiaAppServer::spawn(self.celestia_chain_id.clone()).await; - let celestia_app_grpc_endpoint = format!("http://{}", celestia_app.grpc_local_addr); - let celestia_app_http_endpoint = celestia_app.http_local_addr.clone(); + let celestia_app_grpc_endpoint = format!("http://{}", celestia_app.local_addr); let celestia_keyfile = write_file( b"c8076374e2a4a58db1c924e3dafc055e9685481054fe99e58ed67f5c6ed80e62".as_slice(), ) @@ -708,7 +722,6 @@ impl TestSequencerRelayerConfig { cometbft_endpoint: cometbft.uri(), sequencer_grpc_endpoint, celestia_app_grpc_endpoint, - celestia_app_http_endpoint, celestia_app_key_file: celestia_keyfile.path().to_string_lossy().to_string(), block_time: 1000, only_include_rollups, diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index ee1ef2b32a..c6f55925b4 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -11,6 +11,7 @@ use astria_core::{ primitive::v1::RollupId, protocol::test_utils::ConfigureSequencerBlock, }; +use futures::future::join; use helpers::{ SequencerBlockToMount, TestSequencerRelayerConfig, @@ -31,7 +32,13 @@ async fn one_block_is_relayed_to_celestia() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(1), + None, + ) .await; // The `MIN_POLL_INTERVAL_SECS` is 1, meaning the relayer waits for 1 second before attempting // the first `tx_status`, so we wait for 2 seconds. @@ -81,7 +88,13 @@ async fn report_degraded_if_block_fetch_fails() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(1), + None, + ) .await; let healthz_status = sequencer_relayer .wait_for_healthz(StatusCode::OK, 2_000, "waiting for first healthz") @@ -140,7 +153,13 @@ async fn later_height_in_state_leads_to_expected_relay() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(1), + None, + ) .await; sequencer_relayer .timeout_ms( @@ -200,7 +219,13 @@ async fn three_blocks_are_relayed() { .mount_celestia_app_broadcast_tx_response("broadcast tx 3") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 3) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(3), + None, + ) .await; // Each block will have taken ~1 second due to the delay before each `tx_status`, so use 4.5 // seconds. @@ -267,7 +292,13 @@ async fn should_filter_rollup() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(1), + None, + ) .await; sequencer_relayer .timeout_ms( @@ -305,7 +336,7 @@ async fn should_shut_down() { .mount_sequencer_block_response(block_to_mount, "good block 1") .await; let broadcast_guard = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1") + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", None, None) .await; sequencer_relayer .timeout_ms( @@ -320,7 +351,13 @@ async fn should_shut_down() { sequencer_relayer.relayer_shutdown_handle.take(); let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped(53, "COMMITTED", 1) + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "COMMITTED", + Some(1), + None, + ) .await; sequencer_relayer .timeout_ms( @@ -356,3 +393,308 @@ async fn should_exit_if_celestia_chain_id_mismatch() { sequencer_relayer.wait_for_relayer_shutdown(100).await; } + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn confirm_submission_loops_on_pending_status() { + let sequencer_relayer = TestSequencerRelayerConfig::default().spawn_relayer().await; + + sequencer_relayer.mount_abci_response(1).await; + let block_to_mount = SequencerBlockToMount::GoodAtHeight(1); + sequencer_relayer + .mount_sequencer_block_response(block_to_mount, "good block 1") + .await; + sequencer_relayer + .mount_celestia_app_broadcast_tx_response("broadcast tx 1") + .await; + + // Expect relayer to loop when it receives a PENDING status. Only respond up to the number of + // expected times, since a committed response will be mounted after. + let tx_pending_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "PENDING", + Some(2), + Some(2), + ) + .await; + // Allow 3 seconds for two `tx_status` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls + // we're allowing 1 extra second for this mount to be satisfied. + sequencer_relayer + .timeout_ms( + 3_000, + "waiting for tx status pending guard", + tx_pending_guard.wait_until_satisfied(), + ) + .await; + + // Mount committed tx status response after sending two pending responses. Relayer should + // continue normal execution after this. + let tx_confirmed_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 2", + 53, + "COMMITTED", + Some(1), + Some(1), + ) + .await; + sequencer_relayer + .timeout_ms( + 2_000, + "waiting for tx status confirmed guard", + tx_confirmed_guard.wait_until_satisfied(), + ) + .await; + + // Assert the relayer reports the correct Celestia and sequencer heights. + sequencer_relayer + .wait_for_latest_confirmed_celestia_height(53, 1_000) + .await; + sequencer_relayer + .wait_for_latest_fetched_sequencer_height(1, 1_000) + .await; + sequencer_relayer + .wait_for_latest_observed_sequencer_height(1, 1_000) + .await; + + assert_eq!( + sequencer_relayer.celestia_app_received_blob_count(), + 2, + "expected 2 blobs in total, 1 header blob and 1 rollup blob" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { + let sequencer_relayer = TestSequencerRelayerConfig::default().spawn_relayer().await; + + sequencer_relayer.mount_abci_response(1).await; + let block_to_mount = SequencerBlockToMount::GoodAtHeight(1); + sequencer_relayer + .mount_sequencer_block_response(block_to_mount, "good block 1") + .await; + sequencer_relayer + .mount_celestia_app_broadcast_tx_response("broadcast tx 1") + .await; + + // Expect relayer to loop when it receives a UNKNOWN status. Only respond up to the number of + // expected times, since a committed response will be mounted after. + let tx_unknown_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "UNKNOWN", + Some(2), + Some(2), + ) + .await; + // Allow 3 seconds for two `tx_status` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls + // we're allowing 1 extra second for this mount to be satisfied. + sequencer_relayer + .timeout_ms( + 3_000, + "waiting for tx status unknown guard", + tx_unknown_guard.wait_until_satisfied(), + ) + .await; + + // Mount committed tx status response after sending two unknown responses. Relayer should + // continue normal execution after this. + let tx_confirmed_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 2", + 53, + "COMMITTED", + Some(1), + Some(1), + ) + .await; + sequencer_relayer + .timeout_ms( + 2_000, + "waiting for tx status confirmed guard", + tx_confirmed_guard.wait_until_satisfied(), + ) + .await; + + // Assert the relayer reports the correct Celestia and sequencer heights. + sequencer_relayer + .wait_for_latest_confirmed_celestia_height(53, 1_000) + .await; + sequencer_relayer + .wait_for_latest_fetched_sequencer_height(1, 1_000) + .await; + sequencer_relayer + .wait_for_latest_observed_sequencer_height(1, 1_000) + .await; + + assert_eq!( + sequencer_relayer.celestia_app_received_blob_count(), + 2, + "expected 2 blobs in total, 1 header blob and 1 rollup blob" + ); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn retries_submission_after_receiving_evicted_tx_status() { + let sequencer_relayer = TestSequencerRelayerConfig::default().spawn_relayer().await; + + sequencer_relayer.mount_abci_response(1).await; + let block_to_mount = SequencerBlockToMount::GoodAtHeight(1); + sequencer_relayer + .mount_sequencer_block_response(block_to_mount, "good block 1") + .await; + let broadcast_tx_guard_1 = sequencer_relayer + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", Some(1), Some(1)) + .await; + let tx_evicted_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "EVICTED", + Some(1), + Some(1), + ) + .await; + + sequencer_relayer + .timeout_ms( + 2_000, + "waiting for first broadcast tx guard and tx status evicted guard", + join( + broadcast_tx_guard_1.wait_until_satisfied(), + tx_evicted_guard.wait_until_satisfied(), + ), + ) + .await; + + // Relayer should retry submission after receiving an EVICTED status. + + let broadcast_tx_guard_2 = sequencer_relayer + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2", Some(1), Some(1)) + .await; + let tx_confirmed_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 2", + 53, + "COMMITTED", + Some(1), + Some(1), + ) + .await; + sequencer_relayer + .timeout_ms( + 2_000, + "waiting for second broadcast tx guard and tx status confirmed guard", + join( + tx_confirmed_guard.wait_until_satisfied(), + broadcast_tx_guard_2.wait_until_satisfied(), + ), + ) + .await; + + // Assert the relayer reports the correct Celestia and sequencer heights. + sequencer_relayer + .wait_for_latest_confirmed_celestia_height(53, 1_000) + .await; + sequencer_relayer + .wait_for_latest_fetched_sequencer_height(1, 1_000) + .await; + sequencer_relayer + .wait_for_latest_observed_sequencer_height(1, 1_000) + .await; + + assert_eq!( + sequencer_relayer.celestia_app_received_blob_count(), + 4, + "expected 4 blobs in total, 2 header blobs and 2 rollup blobs" + ); + assert!(sequencer_relayer + .metrics_handle + .render() + .contains("astria_sequencer_relayer_celestia_evicted_transaction_count 1")); +} + +#[tokio::test(flavor = "multi_thread", worker_threads = 1)] +async fn confirm_submission_exits_for_unknown_status_after_time_limit() { + let sequencer_relayer = TestSequencerRelayerConfig::default().spawn_relayer().await; + + sequencer_relayer.mount_abci_response(1).await; + let block_to_mount = SequencerBlockToMount::GoodAtHeight(1); + sequencer_relayer + .mount_sequencer_block_response(block_to_mount, "good block 1") + .await; + + let broadcast_tx_guard_1 = sequencer_relayer + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", Some(1), Some(1)) + .await; + + let tx_unknown_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 1", + 53, + "UNKNOWN", + Some(10), + Some(10), + ) + .await; + + sequencer_relayer + .timeout_ms( + 11_000, + "waiting for first broadcast tx guard and tx status evicted guard", + join( + broadcast_tx_guard_1.wait_until_satisfied(), + tx_unknown_guard.wait_until_satisfied(), + ), + ) + .await; + + // Relayer should retry submission after receiving an UNKNOWN status more than 10s after + // beginning to poll. + + let broadcast_tx_guard_2 = sequencer_relayer + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2", Some(1), Some(1)) + .await; + let tx_confirmed_guard = sequencer_relayer + .mount_celestia_app_tx_status_response_as_scoped( + "tx status 2", + 53, + "COMMITTED", + Some(1), + Some(1), + ) + .await; + sequencer_relayer + .timeout_ms( + 2_000, + "waiting for second broadcast tx guard and tx status confirmed guard", + join( + tx_confirmed_guard.wait_until_satisfied(), + broadcast_tx_guard_2.wait_until_satisfied(), + ), + ) + .await; + + // Assert the relayer reports the correct Celestia and sequencer heights. + sequencer_relayer + .wait_for_latest_confirmed_celestia_height(53, 1_000) + .await; + sequencer_relayer + .wait_for_latest_fetched_sequencer_height(1, 1_000) + .await; + sequencer_relayer + .wait_for_latest_observed_sequencer_height(1, 1_000) + .await; + + assert_eq!( + sequencer_relayer.celestia_app_received_blob_count(), + 4, + "expected 4 blobs in total, 2 header blobs and 2 rollup blobs" + ); + assert!(sequencer_relayer + .metrics_handle + .render() + .contains("astria_sequencer_relayer_celestia_unknown_status_transaction_count 1")); +} diff --git a/dev/argocd/pr-preview-envs/sequencer-appset.yaml b/dev/argocd/pr-preview-envs/sequencer-appset.yaml index eee5ca1a34..4794c0e46a 100644 --- a/dev/argocd/pr-preview-envs/sequencer-appset.yaml +++ b/dev/argocd/pr-preview-envs/sequencer-appset.yaml @@ -114,7 +114,6 @@ spec: config: relayer: celestiaAppGrpc: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:9090 - celestiaAppHttp: http://celestia-app-service.pr-{{.number}}.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.pr-{{.number}}.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.pr-{{.number}}.svc.cluster.local:8080 diff --git a/dev/values/validators/node0.yml b/dev/values/validators/node0.yml index fdc6322a08..9dd3f4ddf8 100644 --- a/dev/values/validators/node0.yml +++ b/dev/values/validators/node0.yml @@ -52,7 +52,6 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 - celestiaAppHttp: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: diff --git a/dev/values/validators/single.yml b/dev/values/validators/single.yml index 2e59727f27..9ec1e3cf1f 100644 --- a/dev/values/validators/single.yml +++ b/dev/values/validators/single.yml @@ -36,7 +36,6 @@ sequencer-relayer: sequencerChainId: sequencer-test-chain-0 celestiaChainId: celestia-local-0 celestiaAppGrpc: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:9090 - celestiaAppHttp: http://celestia-app-service.astria-dev-cluster.svc.cluster.local:26657 cometbftRpc: http://node0-sequencer-rpc-service.astria-dev-cluster.svc.cluster.local:26657 sequencerGrpc: http://node0-sequencer-grpc-service.astria-dev-cluster.svc.cluster.local:8080 celestiaAppPrivateKey: From 9350f72ef22524ef6ad452a83f48f9c70fa30275 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Mon, 3 Feb 2025 12:19:30 -0600 Subject: [PATCH 05/12] Update changelog, revert chart update, add files which didn't make last commit --- charts/sequencer/Chart.lock | 2 +- .../src/generated/celestia.core.v1.tx.rs | 347 ++++++++++++++++++ .../generated/celestia.core.v1.tx.serde.rs | 259 +++++++++++++ crates/astria-sequencer-relayer/CHANGELOG.md | 5 +- .../celestia_app/celestia/core/tx/tx.proto | 36 ++ 5 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 crates/astria-core/src/generated/celestia.core.v1.tx.rs create mode 100644 crates/astria-core/src/generated/celestia.core.v1.tx.serde.rs create mode 100644 proto/vendored/celestia_app/celestia/core/tx/tx.proto diff --git a/charts/sequencer/Chart.lock b/charts/sequencer/Chart.lock index 78d4dfb84c..7523dc332c 100644 --- a/charts/sequencer/Chart.lock +++ b/charts/sequencer/Chart.lock @@ -3,4 +3,4 @@ dependencies: repository: file://../sequencer-relayer version: 1.0.1 digest: sha256:3b3ce65ff473606fcc86027653cadd212ba45ac8b39d5806d713b48f695ad235 -generated: "2025-02-03T12:07:18.438552-06:00" +generated: "2024-11-20T11:24:11.85775+02:00" diff --git a/crates/astria-core/src/generated/celestia.core.v1.tx.rs b/crates/astria-core/src/generated/celestia.core.v1.tx.rs new file mode 100644 index 0000000000..7c20152972 --- /dev/null +++ b/crates/astria-core/src/generated/celestia.core.v1.tx.rs @@ -0,0 +1,347 @@ +/// TxStatusRequest is the request type for the TxStatus gRPC method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TxStatusRequest { + /// this is the hex encoded transaction hash (should be 64 bytes long) + #[prost(string, tag = "1")] + pub tx_id: ::prost::alloc::string::String, +} +impl ::prost::Name for TxStatusRequest { + const NAME: &'static str = "TxStatusRequest"; + const PACKAGE: &'static str = "celestia.core.v1.tx"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("celestia.core.v1.tx.{}", Self::NAME) + } +} +/// TxStatusResponse is the response type for the TxStatus gRPC method. +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct TxStatusResponse { + #[prost(int64, tag = "1")] + pub height: i64, + #[prost(uint32, tag = "2")] + pub index: u32, + /// execution_code is returned when the transaction has been committed + /// and returns whether it was successful or errored. A non zero + /// execution code indicated an error. + #[prost(uint32, tag = "3")] + pub execution_code: u32, + /// error log for failed transactions. + #[prost(string, tag = "4")] + pub error: ::prost::alloc::string::String, + /// status is the status of the transaction. + #[prost(string, tag = "5")] + pub status: ::prost::alloc::string::String, +} +impl ::prost::Name for TxStatusResponse { + const NAME: &'static str = "TxStatusResponse"; + const PACKAGE: &'static str = "celestia.core.v1.tx"; + fn full_name() -> ::prost::alloc::string::String { + ::prost::alloc::format!("celestia.core.v1.tx.{}", Self::NAME) + } +} +/// Generated client implementations. +#[cfg(feature = "client")] +pub mod tx_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// Service defines a gRPC service for interacting with transactions. + #[derive(Debug, Clone)] + pub struct TxClient { + inner: tonic::client::Grpc, + } + impl TxClient { + /// Attempt to create a new client by connecting to a given endpoint. + pub async fn connect(dst: D) -> Result + where + D: TryInto, + D::Error: Into, + { + let conn = tonic::transport::Endpoint::new(dst)?.connect().await?; + Ok(Self::new(conn)) + } + } + impl TxClient + where + T: tonic::client::GrpcService, + T::Error: Into, + T::ResponseBody: Body + Send + 'static, + ::Error: Into + Send, + { + pub fn new(inner: T) -> Self { + let inner = tonic::client::Grpc::new(inner); + Self { inner } + } + pub fn with_origin(inner: T, origin: Uri) -> Self { + let inner = tonic::client::Grpc::with_origin(inner, origin); + Self { inner } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> TxClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + TxClient::new(InterceptedService::new(inner, interceptor)) + } + /// Compress requests with the given encoding. + /// + /// This requires the server to support it otherwise it might respond with an + /// error. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.send_compressed(encoding); + self + } + /// Enable decompressing responses. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.inner = self.inner.accept_compressed(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_decoding_message_size(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.inner = self.inner.max_encoding_message_size(limit); + self + } + /// TxStatus returns the status of a transaction. There are four possible states: + /// - Committed + /// - Pending + /// - Evicted + /// - Unknown + pub async fn tx_status( + &mut self, + request: impl tonic::IntoRequest, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + > { + self.inner + .ready() + .await + .map_err(|e| { + tonic::Status::new( + tonic::Code::Unknown, + format!("Service was not ready: {}", e.into()), + ) + })?; + let codec = tonic::codec::ProstCodec::default(); + let path = http::uri::PathAndQuery::from_static( + "/celestia.core.v1.tx.Tx/TxStatus", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("celestia.core.v1.tx.Tx", "TxStatus")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +#[cfg(feature = "server")] +pub mod tx_server { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + /// Generated trait containing gRPC methods that should be implemented for use with TxServer. + #[async_trait] + pub trait Tx: Send + Sync + 'static { + /// TxStatus returns the status of a transaction. There are four possible states: + /// - Committed + /// - Pending + /// - Evicted + /// - Unknown + async fn tx_status( + self: std::sync::Arc, + request: tonic::Request, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// Service defines a gRPC service for interacting with transactions. + #[derive(Debug)] + pub struct TxServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl TxServer { + pub fn new(inner: T) -> Self { + Self::from_arc(Arc::new(inner)) + } + pub fn from_arc(inner: Arc) -> Self { + let inner = _Inner(inner); + Self { + inner, + accept_compression_encodings: Default::default(), + send_compression_encodings: Default::default(), + max_decoding_message_size: None, + max_encoding_message_size: None, + } + } + pub fn with_interceptor( + inner: T, + interceptor: F, + ) -> InterceptedService + where + F: tonic::service::Interceptor, + { + InterceptedService::new(Self::new(inner), interceptor) + } + /// Enable decompressing requests with the given encoding. + #[must_use] + pub fn accept_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.accept_compression_encodings.enable(encoding); + self + } + /// Compress responses with the given encoding, if the client supports it. + #[must_use] + pub fn send_compressed(mut self, encoding: CompressionEncoding) -> Self { + self.send_compression_encodings.enable(encoding); + self + } + /// Limits the maximum size of a decoded message. + /// + /// Default: `4MB` + #[must_use] + pub fn max_decoding_message_size(mut self, limit: usize) -> Self { + self.max_decoding_message_size = Some(limit); + self + } + /// Limits the maximum size of an encoded message. + /// + /// Default: `usize::MAX` + #[must_use] + pub fn max_encoding_message_size(mut self, limit: usize) -> Self { + self.max_encoding_message_size = Some(limit); + self + } + } + impl tonic::codegen::Service> for TxServer + where + T: Tx, + B: Body + Send + 'static, + B::Error: Into + Send + 'static, + { + type Response = http::Response; + type Error = std::convert::Infallible; + type Future = BoxFuture; + fn poll_ready( + &mut self, + _cx: &mut Context<'_>, + ) -> Poll> { + Poll::Ready(Ok(())) + } + fn call(&mut self, req: http::Request) -> Self::Future { + let inner = self.inner.clone(); + match req.uri().path() { + "/celestia.core.v1.tx.Tx/TxStatus" => { + #[allow(non_camel_case_types)] + struct TxStatusSvc(pub Arc); + impl tonic::server::UnaryService + for TxStatusSvc { + type Response = super::TxStatusResponse; + type Future = BoxFuture< + tonic::Response, + tonic::Status, + >; + fn call( + &mut self, + request: tonic::Request, + ) -> Self::Future { + let inner = Arc::clone(&self.0); + let fut = async move { + ::tx_status(inner, request).await + }; + Box::pin(fut) + } + } + let accept_compression_encodings = self.accept_compression_encodings; + let send_compression_encodings = self.send_compression_encodings; + let max_decoding_message_size = self.max_decoding_message_size; + let max_encoding_message_size = self.max_encoding_message_size; + let inner = self.inner.clone(); + let fut = async move { + let inner = inner.0; + let method = TxStatusSvc(inner); + let codec = tonic::codec::ProstCodec::default(); + let mut grpc = tonic::server::Grpc::new(codec) + .apply_compression_config( + accept_compression_encodings, + send_compression_encodings, + ) + .apply_max_message_size_config( + max_decoding_message_size, + max_encoding_message_size, + ); + let res = grpc.unary(method, req).await; + Ok(res) + }; + Box::pin(fut) + } + _ => { + Box::pin(async move { + Ok( + http::Response::builder() + .status(200) + .header("grpc-status", "12") + .header("content-type", "application/grpc") + .body(empty_body()) + .unwrap(), + ) + }) + } + } + } + } + impl Clone for TxServer { + fn clone(&self) -> Self { + let inner = self.inner.clone(); + Self { + inner, + accept_compression_encodings: self.accept_compression_encodings, + send_compression_encodings: self.send_compression_encodings, + max_decoding_message_size: self.max_decoding_message_size, + max_encoding_message_size: self.max_encoding_message_size, + } + } + } + impl Clone for _Inner { + fn clone(&self) -> Self { + Self(Arc::clone(&self.0)) + } + } + impl std::fmt::Debug for _Inner { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{:?}", self.0) + } + } + impl tonic::server::NamedService for TxServer { + const NAME: &'static str = "celestia.core.v1.tx.Tx"; + } +} diff --git a/crates/astria-core/src/generated/celestia.core.v1.tx.serde.rs b/crates/astria-core/src/generated/celestia.core.v1.tx.serde.rs new file mode 100644 index 0000000000..beb209057d --- /dev/null +++ b/crates/astria-core/src/generated/celestia.core.v1.tx.serde.rs @@ -0,0 +1,259 @@ +impl serde::Serialize for TxStatusRequest { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if !self.tx_id.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("celestia.core.v1.tx.TxStatusRequest", len)?; + if !self.tx_id.is_empty() { + struct_ser.serialize_field("txId", &self.tx_id)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TxStatusRequest { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "tx_id", + "txId", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + TxId, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "txId" | "tx_id" => Ok(GeneratedField::TxId), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TxStatusRequest; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct celestia.core.v1.tx.TxStatusRequest") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut tx_id__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::TxId => { + if tx_id__.is_some() { + return Err(serde::de::Error::duplicate_field("txId")); + } + tx_id__ = Some(map_.next_value()?); + } + } + } + Ok(TxStatusRequest { + tx_id: tx_id__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("celestia.core.v1.tx.TxStatusRequest", FIELDS, GeneratedVisitor) + } +} +impl serde::Serialize for TxStatusResponse { + #[allow(deprecated)] + fn serialize(&self, serializer: S) -> std::result::Result + where + S: serde::Serializer, + { + use serde::ser::SerializeStruct; + let mut len = 0; + if self.height != 0 { + len += 1; + } + if self.index != 0 { + len += 1; + } + if self.execution_code != 0 { + len += 1; + } + if !self.error.is_empty() { + len += 1; + } + if !self.status.is_empty() { + len += 1; + } + let mut struct_ser = serializer.serialize_struct("celestia.core.v1.tx.TxStatusResponse", len)?; + if self.height != 0 { + #[allow(clippy::needless_borrow)] + struct_ser.serialize_field("height", ToString::to_string(&self.height).as_str())?; + } + if self.index != 0 { + struct_ser.serialize_field("index", &self.index)?; + } + if self.execution_code != 0 { + struct_ser.serialize_field("executionCode", &self.execution_code)?; + } + if !self.error.is_empty() { + struct_ser.serialize_field("error", &self.error)?; + } + if !self.status.is_empty() { + struct_ser.serialize_field("status", &self.status)?; + } + struct_ser.end() + } +} +impl<'de> serde::Deserialize<'de> for TxStatusResponse { + #[allow(deprecated)] + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + const FIELDS: &[&str] = &[ + "height", + "index", + "execution_code", + "executionCode", + "error", + "status", + ]; + + #[allow(clippy::enum_variant_names)] + enum GeneratedField { + Height, + Index, + ExecutionCode, + Error, + Status, + } + impl<'de> serde::Deserialize<'de> for GeneratedField { + fn deserialize(deserializer: D) -> std::result::Result + where + D: serde::Deserializer<'de>, + { + struct GeneratedVisitor; + + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = GeneratedField; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(formatter, "expected one of: {:?}", &FIELDS) + } + + #[allow(unused_variables)] + fn visit_str(self, value: &str) -> std::result::Result + where + E: serde::de::Error, + { + match value { + "height" => Ok(GeneratedField::Height), + "index" => Ok(GeneratedField::Index), + "executionCode" | "execution_code" => Ok(GeneratedField::ExecutionCode), + "error" => Ok(GeneratedField::Error), + "status" => Ok(GeneratedField::Status), + _ => Err(serde::de::Error::unknown_field(value, FIELDS)), + } + } + } + deserializer.deserialize_identifier(GeneratedVisitor) + } + } + struct GeneratedVisitor; + impl<'de> serde::de::Visitor<'de> for GeneratedVisitor { + type Value = TxStatusResponse; + + fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + formatter.write_str("struct celestia.core.v1.tx.TxStatusResponse") + } + + fn visit_map(self, mut map_: V) -> std::result::Result + where + V: serde::de::MapAccess<'de>, + { + let mut height__ = None; + let mut index__ = None; + let mut execution_code__ = None; + let mut error__ = None; + let mut status__ = None; + while let Some(k) = map_.next_key()? { + match k { + GeneratedField::Height => { + if height__.is_some() { + return Err(serde::de::Error::duplicate_field("height")); + } + height__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Index => { + if index__.is_some() { + return Err(serde::de::Error::duplicate_field("index")); + } + index__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::ExecutionCode => { + if execution_code__.is_some() { + return Err(serde::de::Error::duplicate_field("executionCode")); + } + execution_code__ = + Some(map_.next_value::<::pbjson::private::NumberDeserialize<_>>()?.0) + ; + } + GeneratedField::Error => { + if error__.is_some() { + return Err(serde::de::Error::duplicate_field("error")); + } + error__ = Some(map_.next_value()?); + } + GeneratedField::Status => { + if status__.is_some() { + return Err(serde::de::Error::duplicate_field("status")); + } + status__ = Some(map_.next_value()?); + } + } + } + Ok(TxStatusResponse { + height: height__.unwrap_or_default(), + index: index__.unwrap_or_default(), + execution_code: execution_code__.unwrap_or_default(), + error: error__.unwrap_or_default(), + status: status__.unwrap_or_default(), + }) + } + } + deserializer.deserialize_struct("celestia.core.v1.tx.TxStatusResponse", FIELDS, GeneratedVisitor) + } +} diff --git a/crates/astria-sequencer-relayer/CHANGELOG.md b/crates/astria-sequencer-relayer/CHANGELOG.md index ce9e59d798..ef7f6dd1bf 100644 --- a/crates/astria-sequencer-relayer/CHANGELOG.md +++ b/crates/astria-sequencer-relayer/CHANGELOG.md @@ -9,13 +9,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] -### Added - -- Add `celestia_app_html_endpoint` env var for polling Celestia's `tx_status` RPC [#1940](https://github.com/astriaorg/astria/pull/1940). - ### Changed - Update `idna` dependency to resolve cargo audit warning [#1869](https://github.com/astriaorg/astria/pull/1869). +- Use `TxStatus` gRPC to confirm transaction commitment instead of `GetTx` [#1940](https://github.com/astriaorg/astria/pull/1940). ## [1.0.0] - 2024-10-25 diff --git a/proto/vendored/celestia_app/celestia/core/tx/tx.proto b/proto/vendored/celestia_app/celestia/core/tx/tx.proto new file mode 100644 index 0000000000..06c819929f --- /dev/null +++ b/proto/vendored/celestia_app/celestia/core/tx/tx.proto @@ -0,0 +1,36 @@ +syntax = "proto3"; +package celestia.core.v1.tx; + +// This file contains types which are copied from +// https://github.com/celestiaorg/celestia-app/blob/8cbda26c536f0ecd5028fb6e050d18badb774e01/proto/celestia/core/v1/tx/tx.proto +// (v3.3.0 tag). + +// Service defines a gRPC service for interacting with transactions. +service Tx { + // TxStatus returns the status of a transaction. There are four possible states: + // - Committed + // - Pending + // - Evicted + // - Unknown + rpc TxStatus(TxStatusRequest) returns (TxStatusResponse); +} + +// TxStatusRequest is the request type for the TxStatus gRPC method. +message TxStatusRequest { + // this is the hex encoded transaction hash (should be 64 bytes long) + string tx_id = 1; +} + +// TxStatusResponse is the response type for the TxStatus gRPC method. +message TxStatusResponse { + int64 height = 1; + uint32 index = 2; + // execution_code is returned when the transaction has been committed + // and returns whether it was successful or errored. A non zero + // execution code indicated an error. + uint32 execution_code = 3; + // error log for failed transactions. + string error = 4; + // status is the status of the transaction. + string status = 5; +} From 45fb5b3824f0f7088b93dd438541ff8e888de5c4 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Mon, 3 Feb 2025 12:23:13 -0600 Subject: [PATCH 06/12] reflect celestia app gRPC 'TxStatus' instead of celestia core RPC 'tx_status' in naming --- .../src/relayer/celestia_client/error.rs | 10 +++++----- .../src/relayer/celestia_client/mod.rs | 10 +++++----- .../astria-sequencer-relayer/src/relayer/submission.rs | 4 ++-- .../astria-sequencer-relayer/src/relayer/write/mod.rs | 2 +- crates/astria-sequencer-relayer/tests/blackbox/main.rs | 6 +++--- 5 files changed, 16 insertions(+), 16 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs index ddad7b38a6..c192fb7e6b 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs @@ -77,7 +77,7 @@ pub(in crate::relayer) enum TrySubmitError { namespace: String, log: String, }, - /// The transaction was either evicted from the mempool or the call to `tx_status` failed. + /// The transaction was either evicted from the mempool or the call to `TxStatus` failed. #[error("failed to confirm transaction submission")] FailedToConfirmSubmission(#[source] ConfirmSubmissionError), } @@ -121,12 +121,12 @@ impl std::error::Error for GrpcResponseError { #[error(transparent)] pub(in crate::relayer) struct ProtobufDecodeError(#[from] DecodeError); -/// An error in getting the status of a transaction via RPC `tx_status`. +/// An error in getting the status of a transaction via RPC `TxStatus`. #[derive(Debug, Clone, thiserror::Error)] pub(in crate::relayer) enum TxStatusError { - #[error("received unfamilair response for tx `{hash}` from `tx_status`: {status}")] + #[error("received unfamilair response for tx `{hash}` from `TxStatus`: {status}")] UnfamiliarStatus { status: String, hash: String }, - #[error("request for `tx_status` failed: {error}")] + #[error("request for `TxStatus` failed: {error}")] // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. FailedToGetTxStatus { error: String }, } @@ -136,7 +136,7 @@ pub(in crate::relayer) enum TxStatusError { pub(in crate::relayer) enum ConfirmSubmissionError { #[error("tx `{hash}` evicted from mempool")] Evicted { hash: String }, - #[error("received `UNKNOWN` status from `tx_status` for tx: {hash}")] + #[error("received `UNKNOWN` status from `TxStatus` for tx: {hash}")] StatusUnknown { hash: String }, #[error("failed to get tx status")] TxStatus(#[from] TxStatusError), diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index dc347f6365..f91555c8ee 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -233,7 +233,7 @@ impl CelestiaClient { let hex_encoded_tx_hash = self.broadcast_tx(blob_tx).await?; if hex_encoded_tx_hash != blob_tx_hash.to_hex() { // This is not a critical error. Worst case, we restart the process now and try for a - // short while to get `tx_status` for this tx using the wrong hash, resulting in a + // short while to get `TxStatus` for this tx using the wrong hash, resulting in a // likely duplicate submission of this set of blobs. warn!( "tx hash `{hex_encoded_tx_hash}` returned from celestia app is not the same as \ @@ -247,7 +247,7 @@ impl CelestiaClient { .map_err(TrySubmitError::FailedToConfirmSubmission) } - /// Repeatedly sends `tx_status` until a successful response is received or `timeout` duration + /// Repeatedly sends `TxStatus` until a successful response is received or `timeout` duration /// has elapsed. /// /// Returns the height of the Celestia block in which the blobs were submitted, or `None` if @@ -348,13 +348,13 @@ impl CelestiaClient { lowercase_hex_encoded_tx_hash_from_response(response) } - /// Returns the reponse of `tx_status` RPC call given a transaction's hash. If the transaction + /// Returns the reponse of `TxStatus` RPC call given a transaction's hash. If the transaction /// is committed, the height of the block in which it was committed will be returned with /// `TxStatusResponse::Committed`. /// /// # Errors /// Returns an error in the following cases: - /// - The call to `tx_status` failed. + /// - The call to `TxStatus` failed. /// - The status of the transaction is not recognized. #[instrument(skip_all, err(level = Level::WARN))] async fn tx_status(&mut self, hex_encoded_tx_hash: String) -> Result { @@ -379,7 +379,7 @@ impl CelestiaClient { } } - /// Repeatedly calls `tx_status` until the transaction is committed, returning the height of the + /// Repeatedly calls `TxStatus` until the transaction is committed, returning the height of the /// block in which the transaction was included. /// /// # Errors diff --git a/crates/astria-sequencer-relayer/src/relayer/submission.rs b/crates/astria-sequencer-relayer/src/relayer/submission.rs index 4ff46597e8..82c8f2a36f 100644 --- a/crates/astria-sequencer-relayer/src/relayer/submission.rs +++ b/crates/astria-sequencer-relayer/src/relayer/submission.rs @@ -34,7 +34,7 @@ use tracing::{ use super::BlobTxHash; /// Represents a submission made to Celestia which has been confirmed as stored via a successful -/// `tx_status` call. +/// `TxStatus` call. #[derive(Clone, Copy, Debug, Deserialize, Serialize, PartialEq, Eq)] pub(super) struct CompletedSubmission { /// The height of the Celestia block in which the submission was stored. @@ -322,7 +322,7 @@ impl PreparedSubmission { &self.blob_tx_hash } - /// Returns the maximum duration for which the Celestia app should be polled with `tx_status` + /// Returns the maximum duration for which the Celestia app should be polled with `TxStatus` /// requests to confirm successful storage of the associated `BlobTx`. /// /// This is at least 15 seconds, but up to a maximum of a minute from when the submission was diff --git a/crates/astria-sequencer-relayer/src/relayer/write/mod.rs b/crates/astria-sequencer-relayer/src/relayer/write/mod.rs index 80c0e9e6cb..ec9d32c5f4 100644 --- a/crates/astria-sequencer-relayer/src/relayer/write/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/write/mod.rs @@ -303,7 +303,7 @@ impl BlobSubmitter { /// This should only be called where submission state on startup is `Prepared`, meaning we don't yet /// know whether that final submission attempt succeeded or not. /// -/// Internally, this polls `tx_status` for up to one minute. The returned `SubmissionState` is +/// Internally, this polls `TxStatus` for up to one minute. The returned `SubmissionState` is /// guaranteed to be in `Started` state, either holding the heights of the previously prepared /// submission if confirmed by Celestia, or holding the heights of the last known confirmed /// submission in the case of timing out. diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index c6f55925b4..e069216266 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -41,7 +41,7 @@ async fn one_block_is_relayed_to_celestia() { ) .await; // The `MIN_POLL_INTERVAL_SECS` is 1, meaning the relayer waits for 1 second before attempting - // the first `tx_status`, so we wait for 2 seconds. + // the first `TxStatus`, so we wait for 2 seconds. sequencer_relayer .timeout_ms( 2_000, @@ -418,7 +418,7 @@ async fn confirm_submission_loops_on_pending_status() { Some(2), ) .await; - // Allow 3 seconds for two `tx_status` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls + // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls // we're allowing 1 extra second for this mount to be satisfied. sequencer_relayer .timeout_ms( @@ -489,7 +489,7 @@ async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { Some(2), ) .await; - // Allow 3 seconds for two `tx_status` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls + // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls // we're allowing 1 extra second for this mount to be satisfied. sequencer_relayer .timeout_ms( From 9e2fc76b391ad672dd9d1cb6f06c08e1e52b8779 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Mon, 3 Feb 2025 13:22:30 -0600 Subject: [PATCH 07/12] fix name --- .../src/relayer/celestia_client/builder.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs index a215a4b900..9e573102a2 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs @@ -88,12 +88,12 @@ impl Builder { /// Returns a new `Builder`, or an error if Bech32-encoding the `signing_keys` address fails. pub(in crate::relayer) fn new( configured_celestia_chain_id: String, - grpc_endpoint: Uri, + uri: Uri, signing_keys: CelestiaKeys, state: Arc, metrics: &'static Metrics, ) -> Result { - let grpc_channel = Endpoint::from(grpc_endpoint) + let grpc_channel = Endpoint::from(uri) .timeout(REQUEST_TIMEOUT) .connect_lazy(); let address = bech32_encode(&signing_keys.address)?; From f96a9d5a7ba49c84e2f1cc5d5c813a496f5ded27 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Mon, 3 Feb 2025 13:40:04 -0600 Subject: [PATCH 08/12] rustfmt --- .../src/relayer/celestia_client/builder.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs index 9e573102a2..7c799cb5b7 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/builder.rs @@ -93,9 +93,7 @@ impl Builder { state: Arc, metrics: &'static Metrics, ) -> Result { - let grpc_channel = Endpoint::from(uri) - .timeout(REQUEST_TIMEOUT) - .connect_lazy(); + let grpc_channel = Endpoint::from(uri).timeout(REQUEST_TIMEOUT).connect_lazy(); let address = bech32_encode(&signing_keys.address)?; Ok(Self { configured_celestia_chain_id, From 1677e0fbb450b80c93bd609df51c40721b11a69a Mon Sep 17 00:00:00 2001 From: Ethan Oroshiba Date: Tue, 18 Feb 2025 10:14:02 -0600 Subject: [PATCH 09/12] Apply suggestions from code review Co-authored-by: Fraser Hutchison <190532+Fraser999@users.noreply.github.com> --- .../src/relayer/celestia_client/error.rs | 17 ++++++++--------- .../src/relayer/celestia_client/mod.rs | 4 ++-- 2 files changed, 10 insertions(+), 11 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs index c192fb7e6b..64848e58d4 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs @@ -122,24 +122,23 @@ impl std::error::Error for GrpcResponseError { pub(in crate::relayer) struct ProtobufDecodeError(#[from] DecodeError); /// An error in getting the status of a transaction via RPC `TxStatus`. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, Clone, Error)] pub(in crate::relayer) enum TxStatusError { - #[error("received unfamilair response for tx `{hash}` from `TxStatus`: {status}")] - UnfamiliarStatus { status: String, hash: String }, - #[error("request for `TxStatus` failed: {error}")] - // Using `String` here because jsonrpsee::core::Error does not implement `Clone`. - FailedToGetTxStatus { error: String }, + #[error("received unfamiliar response for tx `{hash}` from `TxStatus`: {status}")] + UnfamiliarStatus { status: String, tx_hash: String }, + #[error("failed to get transaction status")] + FailedToGetTxStatus(#[source] GrpcResponseError), } /// An error in confirming the submission of a transaction. -#[derive(Debug, Clone, thiserror::Error)] +#[derive(Debug, Clone, Error)] pub(in crate::relayer) enum ConfirmSubmissionError { #[error("tx `{hash}` evicted from mempool")] Evicted { hash: String }, #[error("received `UNKNOWN` status from `TxStatus` for tx: {hash}")] StatusUnknown { hash: String }, - #[error("failed to get tx status")] - TxStatus(#[from] TxStatusError), + #[error(transparent)] + TxStatus(TxStatusError), #[error("received negative block height from Celestia: {height}")] NegativeHeight { height: i64 }, } diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index f91555c8ee..a527fbecae 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -152,7 +152,7 @@ enum TxStatus { pub(super) struct CelestiaClient { /// The inner `tonic` gRPC channel shared by the various generated gRPC clients. grpc_channel: Channel, - /// A gRPC client to broadcast and get transactions. + /// A gRPC client to broadcast transactions. tx_client: TxClient, /// A gRPC client for querying transaction status. tx_status_client: TxStatusClient, @@ -348,7 +348,7 @@ impl CelestiaClient { lowercase_hex_encoded_tx_hash_from_response(response) } - /// Returns the reponse of `TxStatus` RPC call given a transaction's hash. If the transaction + /// Returns the response of `TxStatus` RPC call given a transaction's hash. If the transaction /// is committed, the height of the block in which it was committed will be returned with /// `TxStatusResponse::Committed`. /// From 4f304bb2e0a7837f73166d52e531e7106c561a6d Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Tue, 18 Feb 2025 10:34:09 -0600 Subject: [PATCH 10/12] requested changes --- .../src/relayer/celestia_client/error.rs | 10 +- .../src/relayer/celestia_client/mod.rs | 29 ++-- .../helpers/mock_celestia_app_server.rs | 25 +--- .../helpers/test_sequencer_relayer.rs | 21 +-- .../tests/blackbox/main.rs | 124 +++--------------- 5 files changed, 47 insertions(+), 162 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs index 64848e58d4..50e98fdfc0 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/error.rs @@ -124,7 +124,7 @@ pub(in crate::relayer) struct ProtobufDecodeError(#[from] DecodeError); /// An error in getting the status of a transaction via RPC `TxStatus`. #[derive(Debug, Clone, Error)] pub(in crate::relayer) enum TxStatusError { - #[error("received unfamiliar response for tx `{hash}` from `TxStatus`: {status}")] + #[error("received unfamiliar response for tx `{tx_hash}` from `TxStatus`: {status}")] UnfamiliarStatus { status: String, tx_hash: String }, #[error("failed to get transaction status")] FailedToGetTxStatus(#[source] GrpcResponseError), @@ -133,10 +133,10 @@ pub(in crate::relayer) enum TxStatusError { /// An error in confirming the submission of a transaction. #[derive(Debug, Clone, Error)] pub(in crate::relayer) enum ConfirmSubmissionError { - #[error("tx `{hash}` evicted from mempool")] - Evicted { hash: String }, - #[error("received `UNKNOWN` status from `TxStatus` for tx: {hash}")] - StatusUnknown { hash: String }, + #[error("tx `{tx_hash}` evicted from mempool")] + Evicted { tx_hash: String }, + #[error("received `UNKNOWN` status from `TxStatus` for tx: {tx_hash}")] + StatusUnknown { tx_hash: String }, #[error(transparent)] TxStatus(TxStatusError), #[error("received negative block height from Celestia: {height}")] diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index a527fbecae..2ba699ee29 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -132,6 +132,7 @@ use crate::Metrics; // From https://github.com/celestiaorg/cosmos-sdk/blob/v1.18.3-sdk-v0.46.14/types/errors/errors.go#L75 const INSUFFICIENT_FEE_CODE: u32 = 13; +// From https://github.com/celestiaorg/celestia-core/blob/d2ca0a2870973e17eadb62a763788bba1f04a1fb/rpc/core/tx.go#L20-L25 const TX_STATUS_UNKNOWN: &str = "UNKNOWN"; const TX_STATUS_PENDING: &str = "PENDING"; const TX_STATUS_EVICTED: &str = "EVICTED"; @@ -364,9 +365,7 @@ impl CelestiaClient { tx_id: hex_encoded_tx_hash.clone(), }) .await - .map_err(|e| TxStatusError::FailedToGetTxStatus { - error: e.to_string(), - })?; + .map_err(|e| TxStatusError::FailedToGetTxStatus(e.into()))?; match response.get_ref().status.as_str() { TX_STATUS_UNKNOWN => Ok(TxStatus::Unknown), TX_STATUS_PENDING => Ok(TxStatus::Pending), @@ -374,7 +373,7 @@ impl CelestiaClient { TX_STATUS_COMMITTED => Ok(TxStatus::Committed(response.get_ref().height)), _ => Err(TxStatusError::UnfamiliarStatus { status: response.get_ref().status.to_string(), - hash: hex_encoded_tx_hash, + tx_hash: hex_encoded_tx_hash, }), } } @@ -396,10 +395,10 @@ impl CelestiaClient { // request. const POLL_INTERVAL_SECS: u64 = 1; // The minimum duration between logs. - const LOG_INTERVAL: Duration = Duration::from_secs(5); + const LOG_INTERVAL: Duration = Duration::from_secs(3); // The maximum amount of time to wait for a transaction to be committed if its status is // `UNKNOWN`. - const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(10); + const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(6); let start = Instant::now(); let mut logged_at = start; @@ -424,8 +423,8 @@ impl CelestiaClient { if start.elapsed() > MAX_WAIT_FOR_UNKNOWN { self.metrics .increment_celestia_unknown_status_transaction_count(); - break Err(ConfirmSubmissionError::StatusUnknown { - hash: hex_encoded_tx_hash, + return Err(ConfirmSubmissionError::StatusUnknown { + tx_hash: hex_encoded_tx_hash, }); } log_if_due("UNKNOWN"); @@ -435,19 +434,19 @@ impl CelestiaClient { } Ok(TxStatus::Evicted) => { self.metrics.increment_celestia_evicted_transaction_count(); - break Err(ConfirmSubmissionError::Evicted { - hash: hex_encoded_tx_hash, + return Err(ConfirmSubmissionError::Evicted { + tx_hash: hex_encoded_tx_hash, }); } Ok(TxStatus::Committed(height)) => { - break Ok(height.try_into().map_err(|_| { - ConfirmSubmissionError::NegativeHeight { + return height + .try_into() + .map_err(|_| ConfirmSubmissionError::NegativeHeight { height, - } - })?) + }) } Err(error) => { - break Err(ConfirmSubmissionError::TxStatus(error)); + return Err(ConfirmSubmissionError::TxStatus(error)); } } } diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs index 5834b67fe2..243b5b996d 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/mock_celestia_app_server.rs @@ -162,12 +162,10 @@ impl MockCelestiaAppServer { pub async fn mount_broadcast_tx_response_as_scoped( &self, debug_name: impl Into, - expected: Option, - up_to_n_times: Option, ) -> MockGuard { self.prepare_broadcast_tx_response(debug_name) - .expect(expected.unwrap_or(1)) - .up_to_n_times(up_to_n_times.unwrap_or(1000)) + .expect(1) + .up_to_n_times(1) .mount_as_scoped(&self.mock_server) .await } @@ -201,29 +199,16 @@ impl MockCelestiaAppServer { .with_name(debug_name) } - pub async fn mount_tx_status_response( - &self, - debug_name: impl Into, - status: String, - height: i64, - ) { - prepare_tx_status_response(status, height) - .with_name(debug_name) - .mount(&self.mock_server) - .await; - } - pub async fn mount_tx_status_response_as_scoped( &self, debug_name: impl Into, status: String, height: i64, - expected: Option, - up_to_n_times: Option, + number_of_times: u64, ) -> MockGuard { prepare_tx_status_response(status, height) - .expect(expected.unwrap_or(1)) - .up_to_n_times(up_to_n_times.unwrap_or(1000)) + .expect(number_of_times) + .up_to_n_times(number_of_times) .with_name(debug_name) .mount_as_scoped(&self.mock_server) .await diff --git a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs index d23dbe02f6..55366d6109 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/helpers/test_sequencer_relayer.rs @@ -269,25 +269,12 @@ impl TestSequencerRelayer { pub async fn mount_celestia_app_broadcast_tx_response_as_scoped( &self, debug_name: impl Into, - expected: Option, - up_to_n_times: Option, ) -> GrpcMockGuard { self.celestia_app - .mount_broadcast_tx_response_as_scoped(debug_name, expected, up_to_n_times) + .mount_broadcast_tx_response_as_scoped(debug_name) .await } - pub async fn mount_celestia_app_tx_status_response( - &self, - debug_name: impl Into, - celestia_height: i64, - status: &str, - ) { - self.celestia_app - .mount_tx_status_response(debug_name, status.to_string(), celestia_height) - .await; - } - /// Mounts a Celestia `TxStatus` response and returns a `wiremock::MockGuard` to allow for /// awaiting its satisfaction. pub async fn mount_celestia_app_tx_status_response_as_scoped( @@ -295,16 +282,14 @@ impl TestSequencerRelayer { debug_name: impl Into, celestia_height: i64, status: &str, - expected: Option, - up_to_n_times: Option, + number_of_times: u64, ) -> GrpcMockGuard { self.celestia_app .mount_tx_status_response_as_scoped( debug_name, status.to_string(), celestia_height, - expected, - up_to_n_times, + number_of_times, ) .await } diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index e069216266..5877d432a0 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -32,13 +32,7 @@ async fn one_block_is_relayed_to_celestia() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(1), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 1) .await; // The `MIN_POLL_INTERVAL_SECS` is 1, meaning the relayer waits for 1 second before attempting // the first `TxStatus`, so we wait for 2 seconds. @@ -88,13 +82,7 @@ async fn report_degraded_if_block_fetch_fails() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(1), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 1) .await; let healthz_status = sequencer_relayer .wait_for_healthz(StatusCode::OK, 2_000, "waiting for first healthz") @@ -153,13 +141,7 @@ async fn later_height_in_state_leads_to_expected_relay() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(1), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -219,13 +201,7 @@ async fn three_blocks_are_relayed() { .mount_celestia_app_broadcast_tx_response("broadcast tx 3") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(3), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 3) .await; // Each block will have taken ~1 second due to the delay before each `tx_status`, so use 4.5 // seconds. @@ -292,13 +268,7 @@ async fn should_filter_rollup() { .mount_celestia_app_broadcast_tx_response("broadcast tx 1") .await; let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(1), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -336,7 +306,7 @@ async fn should_shut_down() { .mount_sequencer_block_response(block_to_mount, "good block 1") .await; let broadcast_guard = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", None, None) + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1") .await; sequencer_relayer .timeout_ms( @@ -351,13 +321,7 @@ async fn should_shut_down() { sequencer_relayer.relayer_shutdown_handle.take(); let tx_status_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "COMMITTED", - Some(1), - None, - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -410,13 +374,7 @@ async fn confirm_submission_loops_on_pending_status() { // Expect relayer to loop when it receives a PENDING status. Only respond up to the number of // expected times, since a committed response will be mounted after. let tx_pending_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "PENDING", - Some(2), - Some(2), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "PENDING", 2) .await; // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls // we're allowing 1 extra second for this mount to be satisfied. @@ -431,13 +389,7 @@ async fn confirm_submission_loops_on_pending_status() { // Mount committed tx status response after sending two pending responses. Relayer should // continue normal execution after this. let tx_confirmed_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 2", - 53, - "COMMITTED", - Some(1), - Some(1), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 2", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -481,13 +433,7 @@ async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { // Expect relayer to loop when it receives a UNKNOWN status. Only respond up to the number of // expected times, since a committed response will be mounted after. let tx_unknown_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "UNKNOWN", - Some(2), - Some(2), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 2) .await; // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls // we're allowing 1 extra second for this mount to be satisfied. @@ -502,13 +448,7 @@ async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { // Mount committed tx status response after sending two unknown responses. Relayer should // continue normal execution after this. let tx_confirmed_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 2", - 53, - "COMMITTED", - Some(1), - Some(1), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 2", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -546,16 +486,10 @@ async fn retries_submission_after_receiving_evicted_tx_status() { .mount_sequencer_block_response(block_to_mount, "good block 1") .await; let broadcast_tx_guard_1 = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", Some(1), Some(1)) + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1") .await; let tx_evicted_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "EVICTED", - Some(1), - Some(1), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "EVICTED", 1) .await; sequencer_relayer @@ -572,16 +506,10 @@ async fn retries_submission_after_receiving_evicted_tx_status() { // Relayer should retry submission after receiving an EVICTED status. let broadcast_tx_guard_2 = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2", Some(1), Some(1)) + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2") .await; let tx_confirmed_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 2", - 53, - "COMMITTED", - Some(1), - Some(1), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 2", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( @@ -627,22 +555,16 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { .await; let broadcast_tx_guard_1 = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1", Some(1), Some(1)) + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 1") .await; let tx_unknown_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 1", - 53, - "UNKNOWN", - Some(10), - Some(10), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 6) .await; sequencer_relayer .timeout_ms( - 11_000, + 7_000, "waiting for first broadcast tx guard and tx status evicted guard", join( broadcast_tx_guard_1.wait_until_satisfied(), @@ -655,16 +577,10 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { // beginning to poll. let broadcast_tx_guard_2 = sequencer_relayer - .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2", Some(1), Some(1)) + .mount_celestia_app_broadcast_tx_response_as_scoped("broadcast tx 2") .await; let tx_confirmed_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped( - "tx status 2", - 53, - "COMMITTED", - Some(1), - Some(1), - ) + .mount_celestia_app_tx_status_response_as_scoped("tx status 2", 53, "COMMITTED", 1) .await; sequencer_relayer .timeout_ms( From 6a9072f600c4cbb01bbb98ee78b85c36dcc3c166 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Wed, 5 Mar 2025 13:13:28 -0600 Subject: [PATCH 11/12] requested changes --- .../src/relayer/celestia_client/mod.rs | 45 +++++++++++++------ .../tests/blackbox/main.rs | 6 +-- 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index 2ba699ee29..09e025ae77 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -391,33 +391,52 @@ impl CelestiaClient { &mut self, hex_encoded_tx_hash: String, ) -> Result { - // The min seconds to sleep after receiving a TxStatus response and sending the next + // The amount of time to sleep after receiving a TxStatus response and sending the next // request. - const POLL_INTERVAL_SECS: u64 = 1; + const POLL_INTERVAL: Duration = Duration::from_secs(1); + // The amount of time to wait before switching to warn level logging instead of debug. + // Corresponds with the Celestia block time. + const START_WARN_DELAY: Duration = Duration::from_secs(6); // The minimum duration between logs. - const LOG_INTERVAL: Duration = Duration::from_secs(3); + const MIN_LOG_INTERVAL: Duration = Duration::from_secs(3); // The maximum amount of time to wait for a transaction to be committed if its status is - // `UNKNOWN`. - const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(6); + // `UNKNOWN`. Corresponds with Celestia block time + 1 second down time. + const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(7); let start = Instant::now(); let mut logged_at = start; + let mut log_interval = MIN_LOG_INTERVAL; let mut log_if_due = |status: &str| { - if logged_at.elapsed() <= LOG_INTERVAL { + if logged_at.elapsed() <= log_interval { return; } - debug!( - reason = format!("transaction status: {status}"), - tx_hash = %hex_encoded_tx_hash, - elapsed_seconds = start.elapsed().as_secs_f32(), - "waiting to confirm blob submission" - ); + + // If elapsed time since start is under `START_WARN_DELAY`, log at debug level at a + // constant interval. If elapsed time since start is over `START_WARN_DELAY`, this means + // at least one Celestia block has passed and the transaction should have been + // submitted. We then start logging at warn level with an exponential backoff. + if start.elapsed() > START_WARN_DELAY { + warn!( + reason = format!("transaction status: {status}"), + tx_hash = %hex_encoded_tx_hash, + elapsed_seconds = start.elapsed().as_secs_f32(), + "waiting to confirm blob submission" + ); + log_interval = log_interval.saturating_mul(2); + } else { + debug!( + reason = format!("transaction status: {status}"), + tx_hash = %hex_encoded_tx_hash, + elapsed_seconds = start.elapsed().as_secs_f32(), + "waiting to confirm blob submission" + ); + } logged_at = Instant::now(); }; loop { - tokio::time::sleep(Duration::from_secs(POLL_INTERVAL_SECS)).await; + tokio::time::sleep(POLL_INTERVAL).await; match self.tx_status(hex_encoded_tx_hash.clone()).await { Ok(TxStatus::Unknown) => { if start.elapsed() > MAX_WAIT_FOR_UNKNOWN { diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index 5877d432a0..b4987393c8 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -559,12 +559,12 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { .await; let tx_unknown_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 6) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 7) .await; sequencer_relayer .timeout_ms( - 7_000, + 8_000, "waiting for first broadcast tx guard and tx status evicted guard", join( broadcast_tx_guard_1.wait_until_satisfied(), @@ -584,7 +584,7 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { .await; sequencer_relayer .timeout_ms( - 2_000, + 4_000, "waiting for second broadcast tx guard and tx status confirmed guard", join( tx_confirmed_guard.wait_until_satisfied(), From e83e712bb17eaaad3a2f8b9af1a8e19501c18826 Mon Sep 17 00:00:00 2001 From: ethanoroshiba Date: Thu, 6 Mar 2025 08:12:09 -0600 Subject: [PATCH 12/12] use exponential backoff for RPC calls --- .../src/relayer/celestia_client/mod.rs | 23 +++++++++++-------- .../tests/blackbox/main.rs | 20 ++++++++-------- 2 files changed, 23 insertions(+), 20 deletions(-) diff --git a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs index 09e025ae77..e9ad5b1fb6 100644 --- a/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs +++ b/crates/astria-sequencer-relayer/src/relayer/celestia_client/mod.rs @@ -391,31 +391,33 @@ impl CelestiaClient { &mut self, hex_encoded_tx_hash: String, ) -> Result { - // The amount of time to sleep after receiving a TxStatus response and sending the next - // request. - const POLL_INTERVAL: Duration = Duration::from_secs(1); + // The minimum amount of time to sleep after receiving a TxStatus response and sending the + // next request. + const MIN_POLL_INTERVAL: Duration = Duration::from_secs(1); + // The maximum amount of time to sleep after receiving a TxStatus response and sending the + // next request. + const MAX_POLL_INTERVAL: Duration = Duration::from_secs(6); // The amount of time to wait before switching to warn level logging instead of debug. // Corresponds with the Celestia block time. const START_WARN_DELAY: Duration = Duration::from_secs(6); - // The minimum duration between logs. - const MIN_LOG_INTERVAL: Duration = Duration::from_secs(3); + // The duration between logs. + const LOG_INTERVAL: Duration = Duration::from_secs(3); // The maximum amount of time to wait for a transaction to be committed if its status is // `UNKNOWN`. Corresponds with Celestia block time + 1 second down time. const MAX_WAIT_FOR_UNKNOWN: Duration = Duration::from_secs(7); let start = Instant::now(); let mut logged_at = start; - let mut log_interval = MIN_LOG_INTERVAL; let mut log_if_due = |status: &str| { - if logged_at.elapsed() <= log_interval { + if logged_at.elapsed() <= LOG_INTERVAL { return; } // If elapsed time since start is under `START_WARN_DELAY`, log at debug level at a // constant interval. If elapsed time since start is over `START_WARN_DELAY`, this means // at least one Celestia block has passed and the transaction should have been - // submitted. We then start logging at warn level with an exponential backoff. + // submitted. We then start logging at the warn level. if start.elapsed() > START_WARN_DELAY { warn!( reason = format!("transaction status: {status}"), @@ -423,7 +425,6 @@ impl CelestiaClient { elapsed_seconds = start.elapsed().as_secs_f32(), "waiting to confirm blob submission" ); - log_interval = log_interval.saturating_mul(2); } else { debug!( reason = format!("transaction status: {status}"), @@ -435,8 +436,9 @@ impl CelestiaClient { logged_at = Instant::now(); }; + let mut poll_interval = MIN_POLL_INTERVAL; loop { - tokio::time::sleep(POLL_INTERVAL).await; + tokio::time::sleep(poll_interval).await; match self.tx_status(hex_encoded_tx_hash.clone()).await { Ok(TxStatus::Unknown) => { if start.elapsed() > MAX_WAIT_FOR_UNKNOWN { @@ -468,6 +470,7 @@ impl CelestiaClient { return Err(ConfirmSubmissionError::TxStatus(error)); } } + poll_interval = std::cmp::min(poll_interval.saturating_mul(2), MAX_POLL_INTERVAL); } } } diff --git a/crates/astria-sequencer-relayer/tests/blackbox/main.rs b/crates/astria-sequencer-relayer/tests/blackbox/main.rs index b4987393c8..d4ffdf96bb 100644 --- a/crates/astria-sequencer-relayer/tests/blackbox/main.rs +++ b/crates/astria-sequencer-relayer/tests/blackbox/main.rs @@ -376,11 +376,11 @@ async fn confirm_submission_loops_on_pending_status() { let tx_pending_guard = sequencer_relayer .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "PENDING", 2) .await; - // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls - // we're allowing 1 extra second for this mount to be satisfied. + // Allow 4 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1 with exponential + // backoff, so with two calls we're allowing 1 extra second for this mount to be satisfied. sequencer_relayer .timeout_ms( - 3_000, + 4_000, "waiting for tx status pending guard", tx_pending_guard.wait_until_satisfied(), ) @@ -393,7 +393,7 @@ async fn confirm_submission_loops_on_pending_status() { .await; sequencer_relayer .timeout_ms( - 2_000, + 6_000, "waiting for tx status confirmed guard", tx_confirmed_guard.wait_until_satisfied(), ) @@ -435,11 +435,11 @@ async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { let tx_unknown_guard = sequencer_relayer .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 2) .await; - // Allow 3 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1, so with two calls - // we're allowing 1 extra second for this mount to be satisfied. + // Allow 4 seconds for two `TxStatus` calls. MIN_POLL_INTERVAL_SECS is 1 with exponential + // backoff, so with two calls we're allowing 1 extra second for this mount to be satisfied. sequencer_relayer .timeout_ms( - 3_000, + 4_000, "waiting for tx status unknown guard", tx_unknown_guard.wait_until_satisfied(), ) @@ -452,7 +452,7 @@ async fn confirm_submission_loops_on_unknown_status_up_to_time_limit() { .await; sequencer_relayer .timeout_ms( - 2_000, + 6_000, "waiting for tx status confirmed guard", tx_confirmed_guard.wait_until_satisfied(), ) @@ -559,7 +559,7 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { .await; let tx_unknown_guard = sequencer_relayer - .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 7) + .mount_celestia_app_tx_status_response_as_scoped("tx status 1", 53, "UNKNOWN", 3) .await; sequencer_relayer @@ -584,7 +584,7 @@ async fn confirm_submission_exits_for_unknown_status_after_time_limit() { .await; sequencer_relayer .timeout_ms( - 4_000, + 6_000, "waiting for second broadcast tx guard and tx status confirmed guard", join( tx_confirmed_guard.wait_until_satisfied(),