From 5406b9470791b46c024a5a6fbbf08fe813258b46 Mon Sep 17 00:00:00 2001 From: evalir Date: Thu, 5 Feb 2026 16:49:17 +0100 Subject: [PATCH 1/6] feat(perms): send entire tx on pylon client instead of just sidecar Adds additional verification to see if the sidecar is of type 7594 before submiting. --- src/perms/pylon.rs | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/perms/pylon.rs b/src/perms/pylon.rs index 02f4c8b..8e02114 100644 --- a/src/perms/pylon.rs +++ b/src/perms/pylon.rs @@ -1,5 +1,5 @@ use crate::perms::oauth::SharedToken; -use alloy::{consensus::BlobTransactionSidecarEip7594, primitives::B256}; +use alloy::consensus::TxEnvelope; use thiserror::Error; use tracing::instrument; @@ -10,7 +10,7 @@ pub enum PylonError { #[error("invalid sidecar: {0}")] InvalidSidecar(String), - /// Sidecar already exists for this transaction hash (409). + /// Sidecar already exists for this transaction (409). #[error("sidecar already exists")] SidecarAlreadyExists, @@ -103,11 +103,18 @@ impl PylonClient { /// [`B256`]: /// [`BlobTransactionSidecarEip7594`]: #[instrument(skip_all)] - pub async fn post_sidecar( - &self, - tx_hash: B256, - sidecar: BlobTransactionSidecarEip7594, - ) -> Result<(), PylonError> { + pub async fn post_sidecar(&self, tx: TxEnvelope) -> Result<(), PylonError> { + // verify that the sidecar is in EIP-7594 format + let is_eip7594 = tx + .as_eip4844() + .and_then(|tx| tx.tx().sidecar().map(|v| v.is_eip7594())); + if is_eip7594 != Some(true) { + return Err(PylonError::InvalidSidecar( + "sidecar is not in EIP-7594 format".to_string(), + )); + } + + let tx_hash = tx.hash(); let url = self.url.join(&format!("v2/sidecar/{tx_hash}"))?; let secret = self .token @@ -118,7 +125,7 @@ impl PylonClient { let response = self .client .post(url) - .json(&sidecar) + .json(&tx) .bearer_auth(secret) .send() .await?; From f9b1600f97e717d3400b4e5cdd760df3865baafd Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 11:11:02 +0100 Subject: [PATCH 2/6] chore: use raw tx --- src/perms/pylon.rs | 43 +++++++++++++++++++++++++------------------ 1 file changed, 25 insertions(+), 18 deletions(-) diff --git a/src/perms/pylon.rs b/src/perms/pylon.rs index 8e02114..dbda59c 100644 --- a/src/perms/pylon.rs +++ b/src/perms/pylon.rs @@ -1,5 +1,8 @@ use crate::perms::oauth::SharedToken; -use alloy::consensus::TxEnvelope; +use alloy::{ + consensus::TxEnvelope, + eips::{eip2718::Eip2718Error, Decodable2718}, +}; use thiserror::Error; use tracing::instrument; @@ -29,6 +32,10 @@ pub enum PylonError { /// Missing auth token. #[error("missing auth token")] MissingAuthToken(tokio::sync::watch::error::RecvError), + + /// Invalid transaction bytes. + #[error("invalid transaction bytes: {0}")] + InvalidTransactionBytes(Eip2718Error), } /// A client for interacting with the Pylon blob server API. @@ -84,35 +91,35 @@ impl PylonClient { /// Post a blob transaction sidecar to the Pylon server. /// - /// If the sidecar is in EIP-4844 format, it will be converted to EIP-7594 - /// format before posting. + /// The transaction must be an EIP-4844 blob transaction with an EIP-7594 + /// sidecar attached. Non-EIP-7594 sidecars will be rejected. /// /// # Arguments /// - /// * `tx_hash` - The transaction hash ([`B256`]). - /// * `sidecar` - The blob transaction sidecar ([`BlobTransactionSidecarEip7594`]). + /// * `tx` - The raw EIP-2718 encoded transaction bytes ([`Bytes`]). /// /// # Errors /// /// Returns an error if: - /// - The sidecar format is invalid ([`PylonError::InvalidSidecar`]) + /// - The transaction bytes are invalid ([`PylonError::InvalidTransactionBytes`]) + /// - The sidecar is missing or not in EIP-7594 format ([`PylonError::InvalidSidecar`]) /// - A sidecar already exists for this transaction hash ([`PylonError::SidecarAlreadyExists`]) /// - An internal server error occurred ([`PylonError::InternalError`]) /// - A network error occurred ([`PylonError::Request`]) /// - /// [`B256`]: - /// [`BlobTransactionSidecarEip7594`]: + /// [`Bytes`]: https://docs.rs/alloy/latest/alloy/primitives/struct.Bytes.html #[instrument(skip_all)] - pub async fn post_sidecar(&self, tx: TxEnvelope) -> Result<(), PylonError> { - // verify that the sidecar is in EIP-7594 format - let is_eip7594 = tx - .as_eip4844() - .and_then(|tx| tx.tx().sidecar().map(|v| v.is_eip7594())); - if is_eip7594 != Some(true) { - return Err(PylonError::InvalidSidecar( - "sidecar is not in EIP-7594 format".to_string(), - )); - } + pub async fn post_sidecar(&self, tx: alloy::primitives::Bytes) -> Result<(), PylonError> { + let tx = TxEnvelope::decode_2718(&mut tx.0.as_ref()) + .map_err(PylonError::InvalidTransactionBytes)?; + + // The sidecar must be in EIP-7594 format + tx.as_eip4844() + .and_then(|tx| tx.tx().sidecar()) + .filter(|s| s.is_eip7594()) + .ok_or_else(|| { + PylonError::InvalidSidecar("sidecar is not in EIP-7594 format".into()) + })?; let tx_hash = tx.hash(); let url = self.url.join(&format!("v2/sidecar/{tx_hash}"))?; From 4b53e77c72c50c8230bcb98e737cb204833e7b70 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 14:23:41 +0100 Subject: [PATCH 3/6] chore: simplify, all validation now done in server --- src/perms/pylon.rs | 26 ++++++-------------------- 1 file changed, 6 insertions(+), 20 deletions(-) diff --git a/src/perms/pylon.rs b/src/perms/pylon.rs index dbda59c..a923ffc 100644 --- a/src/perms/pylon.rs +++ b/src/perms/pylon.rs @@ -1,8 +1,5 @@ use crate::perms::oauth::SharedToken; -use alloy::{ - consensus::TxEnvelope, - eips::{eip2718::Eip2718Error, Decodable2718}, -}; +use alloy::eips::eip2718::Eip2718Error; use thiserror::Error; use tracing::instrument; @@ -109,20 +106,8 @@ impl PylonClient { /// /// [`Bytes`]: https://docs.rs/alloy/latest/alloy/primitives/struct.Bytes.html #[instrument(skip_all)] - pub async fn post_sidecar(&self, tx: alloy::primitives::Bytes) -> Result<(), PylonError> { - let tx = TxEnvelope::decode_2718(&mut tx.0.as_ref()) - .map_err(PylonError::InvalidTransactionBytes)?; - - // The sidecar must be in EIP-7594 format - tx.as_eip4844() - .and_then(|tx| tx.tx().sidecar()) - .filter(|s| s.is_eip7594()) - .ok_or_else(|| { - PylonError::InvalidSidecar("sidecar is not in EIP-7594 format".into()) - })?; - - let tx_hash = tx.hash(); - let url = self.url.join(&format!("v2/sidecar/{tx_hash}"))?; + pub async fn post_sidecar(&self, raw_tx: alloy::primitives::Bytes) -> Result<(), PylonError> { + let url = self.url.join("v2/sidecar")?; let secret = self .token .secret() @@ -132,13 +117,14 @@ impl PylonClient { let response = self .client .post(url) - .json(&tx) + .header(reqwest::header::CONTENT_TYPE, "application/octet-stream") + .body(raw_tx.to_vec()) .bearer_auth(secret) .send() .await?; match response.status() { - reqwest::StatusCode::OK => Ok(()), + reqwest::StatusCode::CREATED => Ok(()), reqwest::StatusCode::BAD_REQUEST => { let text = response.text().await.unwrap_or_default(); Err(PylonError::InvalidSidecar(text)) From 76affbd35ee8c47ca38848387867e995359f7473 Mon Sep 17 00:00:00 2001 From: evalir Date: Fri, 6 Feb 2026 14:44:18 +0100 Subject: [PATCH 4/6] chore: simplify --- src/perms/pylon.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/perms/pylon.rs b/src/perms/pylon.rs index a923ffc..432b757 100644 --- a/src/perms/pylon.rs +++ b/src/perms/pylon.rs @@ -118,7 +118,7 @@ impl PylonClient { .client .post(url) .header(reqwest::header::CONTENT_TYPE, "application/octet-stream") - .body(raw_tx.to_vec()) + .body(raw_tx.0) .bearer_auth(secret) .send() .await?; From 41a8ba855aee01eb3b9c99effdb9a6bfebde3056 Mon Sep 17 00:00:00 2001 From: evalir Date: Mon, 9 Feb 2026 17:50:25 +0100 Subject: [PATCH 5/6] chore: rename to post_blob_tx --- src/perms/pylon.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/perms/pylon.rs b/src/perms/pylon.rs index 432b757..f96fb24 100644 --- a/src/perms/pylon.rs +++ b/src/perms/pylon.rs @@ -86,7 +86,7 @@ impl PylonClient { &self.token } - /// Post a blob transaction sidecar to the Pylon server. + /// Post a blob transaction to the Pylon server. /// /// The transaction must be an EIP-4844 blob transaction with an EIP-7594 /// sidecar attached. Non-EIP-7594 sidecars will be rejected. @@ -106,7 +106,7 @@ impl PylonClient { /// /// [`Bytes`]: https://docs.rs/alloy/latest/alloy/primitives/struct.Bytes.html #[instrument(skip_all)] - pub async fn post_sidecar(&self, raw_tx: alloy::primitives::Bytes) -> Result<(), PylonError> { + pub async fn post_blob_tx(&self, raw_tx: alloy::primitives::Bytes) -> Result<(), PylonError> { let url = self.url.join("v2/sidecar")?; let secret = self .token From c97eda35691bbed6faba8f0036afc58e7674cad4 Mon Sep 17 00:00:00 2001 From: evalir Date: Mon, 9 Feb 2026 17:51:12 +0100 Subject: [PATCH 6/6] chore: bump to rc9 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 69166fc..4548b70 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -3,7 +3,7 @@ name = "init4-bin-base" description = "Internal utilities for binaries produced by the init4 team" keywords = ["init4", "bin", "base"] -version = "0.18.0-rc.8" +version = "0.18.0-rc.9" edition = "2021" rust-version = "1.85" authors = ["init4", "James Prestwich", "evalir"]