diff --git a/.github/workflows/check_consts_drift.yml b/.github/workflows/check_consts_drift.yml index 43c8aaa545..bf8a47d421 100644 --- a/.github/workflows/check_consts_drift.yml +++ b/.github/workflows/check_consts_drift.yml @@ -17,7 +17,7 @@ jobs: - name: Fetch Celestia App Consts id: fetch_celestia_consts run: | - curl -sSL https://raw.githubusercontent.com/celestiaorg/celestia-app/main/pkg/appconsts/initial_consts.go -o /tmp/celestia_initial_consts.go + curl -sSL https://raw.githubusercontent.com/celestiaorg/celestia-app/refs/tags/v5.0.1/pkg/appconsts/app_consts.go -o /tmp/celestia_initial_consts.go if [ $? -ne 0 ]; then echo "Failed to download Celestia app consts file." exit 1 @@ -60,29 +60,30 @@ jobs: # Perform the diff and handle its exit code robustly diff_command_output="" - if ! diff_command_output=$(diff -u "$LOCAL_FILE_TMP" "$CELESTIA_FILE_TMP"); then - # diff exited with non-zero status - diff_exit_code=$? - if [ $diff_exit_code -eq 1 ]; then - # Exit code 1 means files are different - echo "Files are different (excluding last line)." - echo "diff_output<> $GITHUB_OUTPUT - echo "$diff_command_output" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "files_differ=true" >> $GITHUB_OUTPUT - exit 1 # Fail the step - else - # Exit code > 1 means diff encountered an error - echo "Error: diff command failed with exit code $diff_exit_code." - echo "Diff command output/error: $diff_command_output" - # Output error information for the issue - echo "diff_output<> $GITHUB_OUTPUT - echo "Diff command error (exit code $diff_exit_code):" >> $GITHUB_OUTPUT - echo "$diff_command_output" >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - echo "files_differ=true" >> $GITHUB_OUTPUT # Treat as a difference to create an issue - exit $diff_exit_code - fi + diff_exit_code=0 + diff_command_output=$(diff -u "$LOCAL_FILE_TMP" "$CELESTIA_FILE_TMP") || diff_exit_code=$? + + if [ $diff_exit_code -eq 1 ]; then + # Exit code 1 means files are different + echo "Files are different (excluding last line)." + echo "diff_output<> $GITHUB_OUTPUT + echo "$diff_command_output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "files_differ=true" >> $GITHUB_OUTPUT + echo "::error::Celestia app consts have drifted. Please update da/jsonrpc/internal/consts.go" + exit 1 # Fail the step + elif [ $diff_exit_code -gt 1 ]; then + # Exit code > 1 means diff encountered an error + echo "Error: diff command failed with exit code $diff_exit_code." + echo "Diff command output/error: $diff_command_output" + # Output error information for the issue + echo "diff_output<> $GITHUB_OUTPUT + echo "Diff command error (exit code $diff_exit_code):" >> $GITHUB_OUTPUT + echo "$diff_command_output" >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + echo "files_differ=true" >> $GITHUB_OUTPUT # Treat as a difference to create an issue + echo "::error::Failed to compare files due to diff error" + exit $diff_exit_code else # diff exited with 0, files are identical echo "Files are identical (excluding last line)." diff --git a/CHANGELOG.md b/CHANGELOG.md index f64dff673d..1e284f4a2d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -35,7 +35,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed -- +- Pass correct namespaces for header and data to the da layer for posting ([#2560](https://github.com/evstack/ev-node/pull/2560)) ### Security diff --git a/apps/evm/single/cmd/run.go b/apps/evm/single/cmd/run.go index 6e81162416..a06a175976 100644 --- a/apps/evm/single/cmd/run.go +++ b/apps/evm/single/cmd/run.go @@ -2,9 +2,11 @@ package cmd import ( "context" + "encoding/hex" "fmt" "path/filepath" + "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/da/jsonrpc" "github.com/evstack/ev-node/node" "github.com/evstack/ev-node/sequencers/single" @@ -40,7 +42,12 @@ var RunCmd = &cobra.Command{ logger := rollcmd.SetupLogger(nodeConfig.Log) - daJrpc, err := jsonrpc.NewClient(context.Background(), logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.Namespace) + headerNamespace := da.PrepareNamespace([]byte(nodeConfig.DA.HeaderNamespace)) + dataNamespace := da.PrepareNamespace([]byte(nodeConfig.DA.DataNamespace)) + + logger.Info().Str("headerNamespace", hex.EncodeToString(headerNamespace)).Str("dataNamespace", hex.EncodeToString(dataNamespace)).Msg("namespaces") + + daJrpc, err := jsonrpc.NewClient(context.Background(), logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.GasPrice, nodeConfig.DA.GasMultiplier) if err != nil { return err } diff --git a/apps/evm/single/go.mod b/apps/evm/single/go.mod index 9cb1d3bffc..fc90415966 100644 --- a/apps/evm/single/go.mod +++ b/apps/evm/single/go.mod @@ -54,7 +54,6 @@ require ( github.com/buger/goterm v1.0.4 // indirect github.com/celestiaorg/go-header v0.6.6 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect - github.com/celestiaorg/go-square/v2 v2.2.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/compose-spec/compose-go/v2 v2.6.0 // indirect diff --git a/apps/evm/single/go.sum b/apps/evm/single/go.sum index ce178d758e..0ab2579008 100644 --- a/apps/evm/single/go.sum +++ b/apps/evm/single/go.sum @@ -107,8 +107,6 @@ github.com/celestiaorg/go-header v0.6.6 h1:17GvSXU/w8L1YWHZP4pYm9/4YHA8iy5Ku2wTE github.com/celestiaorg/go-header v0.6.6/go.mod h1:RdnlTmsyuNerztNiJiQE5G/EGEH+cErhQ83xNjuGcaQ= github.com/celestiaorg/go-libp2p-messenger v0.2.2 h1:osoUfqjss7vWTIZrrDSy953RjQz+ps/vBFE7bychLEc= github.com/celestiaorg/go-libp2p-messenger v0.2.2/go.mod h1:oTCRV5TfdO7V/k6nkx7QjQzGrWuJbupv+0o1cgnY2i4= -github.com/celestiaorg/go-square/v2 v2.2.0 h1:zJnUxCYc65S8FgUfVpyG/osDcsnjzo/JSXw/Uwn8zp4= -github.com/celestiaorg/go-square/v2 v2.2.0/go.mod h1:j8kQUqJLYtcvCQMQV6QjEhUdaF7rBTXF74g8LbkR0Co= github.com/celestiaorg/utils v0.1.0 h1:WsP3O8jF7jKRgLNFmlDCwdThwOFMFxg0MnqhkLFVxPo= github.com/celestiaorg/utils v0.1.0/go.mod h1:vQTh7MHnvpIeCQZ2/Ph+w7K1R2UerDheZbgJEJD2hSU= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= diff --git a/apps/grpc/single/cmd/run.go b/apps/grpc/single/cmd/run.go index 525ffbb03c..15bac976c6 100644 --- a/apps/grpc/single/cmd/run.go +++ b/apps/grpc/single/cmd/run.go @@ -1,11 +1,13 @@ package cmd import ( + "encoding/hex" "fmt" "path/filepath" "github.com/spf13/cobra" + coreda "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/core/execution" "github.com/evstack/ev-node/da/jsonrpc" executiongrpc "github.com/evstack/ev-node/execution/grpc" @@ -45,8 +47,13 @@ The execution client must implement the Evolve execution gRPC interface.`, logger := rollcmd.SetupLogger(nodeConfig.Log) + headerNamespace := coreda.PrepareNamespace([]byte(nodeConfig.DA.HeaderNamespace)) + dataNamespace := coreda.PrepareNamespace([]byte(nodeConfig.DA.DataNamespace)) + + logger.Info().Str("headerNamespace", hex.EncodeToString(headerNamespace)).Str("dataNamespace", hex.EncodeToString(dataNamespace)).Msg("namespaces") + // Create DA client - daJrpc, err := jsonrpc.NewClient(cmd.Context(), logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.Namespace) + daJrpc, err := jsonrpc.NewClient(cmd.Context(), logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.GasPrice, nodeConfig.DA.GasMultiplier) if err != nil { return err } diff --git a/apps/grpc/single/go.mod b/apps/grpc/single/go.mod index 846a549899..3e69805f0c 100644 --- a/apps/grpc/single/go.mod +++ b/apps/grpc/single/go.mod @@ -20,7 +20,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/celestiaorg/go-header v0.6.6 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect - github.com/celestiaorg/go-square/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect diff --git a/apps/grpc/single/go.sum b/apps/grpc/single/go.sum index fc39fa5040..29c1c04902 100644 --- a/apps/grpc/single/go.sum +++ b/apps/grpc/single/go.sum @@ -27,8 +27,6 @@ github.com/celestiaorg/go-header v0.6.6 h1:17GvSXU/w8L1YWHZP4pYm9/4YHA8iy5Ku2wTE github.com/celestiaorg/go-header v0.6.6/go.mod h1:RdnlTmsyuNerztNiJiQE5G/EGEH+cErhQ83xNjuGcaQ= github.com/celestiaorg/go-libp2p-messenger v0.2.2 h1:osoUfqjss7vWTIZrrDSy953RjQz+ps/vBFE7bychLEc= github.com/celestiaorg/go-libp2p-messenger v0.2.2/go.mod h1:oTCRV5TfdO7V/k6nkx7QjQzGrWuJbupv+0o1cgnY2i4= -github.com/celestiaorg/go-square/v2 v2.2.0 h1:zJnUxCYc65S8FgUfVpyG/osDcsnjzo/JSXw/Uwn8zp4= -github.com/celestiaorg/go-square/v2 v2.2.0/go.mod h1:j8kQUqJLYtcvCQMQV6QjEhUdaF7rBTXF74g8LbkR0Co= github.com/celestiaorg/utils v0.1.0 h1:WsP3O8jF7jKRgLNFmlDCwdThwOFMFxg0MnqhkLFVxPo= github.com/celestiaorg/utils v0.1.0/go.mod h1:vQTh7MHnvpIeCQZ2/Ph+w7K1R2UerDheZbgJEJD2hSU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/apps/testapp/cmd/run.go b/apps/testapp/cmd/run.go index fee4563b5f..752c976ae1 100644 --- a/apps/testapp/cmd/run.go +++ b/apps/testapp/cmd/run.go @@ -2,12 +2,14 @@ package cmd import ( "context" + "encoding/hex" "fmt" "path/filepath" "github.com/spf13/cobra" kvexecutor "github.com/evstack/ev-node/apps/testapp/kv" + "github.com/evstack/ev-node/core/da" "github.com/evstack/ev-node/da/jsonrpc" "github.com/evstack/ev-node/node" rollcmd "github.com/evstack/ev-node/pkg/cmd" @@ -45,7 +47,12 @@ var RunCmd = &cobra.Command{ ctx, cancel := context.WithCancel(context.Background()) defer cancel() - daJrpc, err := jsonrpc.NewClient(ctx, logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.Namespace) + headerNamespace := da.PrepareNamespace([]byte(nodeConfig.DA.HeaderNamespace)) + dataNamespace := da.PrepareNamespace([]byte(nodeConfig.DA.DataNamespace)) + + logger.Info().Str("headerNamespace", hex.EncodeToString(headerNamespace)).Str("dataNamespace", hex.EncodeToString(dataNamespace)).Msg("namespaces") + + daJrpc, err := jsonrpc.NewClient(ctx, logger, nodeConfig.DA.Address, nodeConfig.DA.AuthToken, nodeConfig.DA.GasPrice, nodeConfig.DA.GasMultiplier) if err != nil { return err } diff --git a/apps/testapp/go.mod b/apps/testapp/go.mod index b4436dc569..0fcf9ffd68 100644 --- a/apps/testapp/go.mod +++ b/apps/testapp/go.mod @@ -11,6 +11,7 @@ replace ( require ( github.com/evstack/ev-node v0.0.0-00010101000000-000000000000 + github.com/evstack/ev-node/core v0.0.0-20250312114929-104787ba1a4c github.com/evstack/ev-node/da v0.0.0-00010101000000-000000000000 github.com/evstack/ev-node/sequencers/single v0.0.0-00010101000000-000000000000 github.com/ipfs/go-datastore v0.8.2 @@ -25,7 +26,6 @@ require ( github.com/beorn7/perks v1.0.1 // indirect github.com/celestiaorg/go-header v0.6.6 // indirect github.com/celestiaorg/go-libp2p-messenger v0.2.2 // indirect - github.com/celestiaorg/go-square/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/containerd/cgroups v1.1.0 // indirect github.com/coreos/go-systemd/v22 v22.5.0 // indirect @@ -37,7 +37,6 @@ require ( github.com/docker/go-units v0.5.0 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elastic/gosigar v0.14.3 // indirect - github.com/evstack/ev-node/core v0.0.0-20250312114929-104787ba1a4c // indirect github.com/filecoin-project/go-jsonrpc v0.7.1 // indirect github.com/flynn/noise v1.1.0 // indirect github.com/francoispqt/gojay v1.2.13 // indirect diff --git a/apps/testapp/go.sum b/apps/testapp/go.sum index fc39fa5040..29c1c04902 100644 --- a/apps/testapp/go.sum +++ b/apps/testapp/go.sum @@ -27,8 +27,6 @@ github.com/celestiaorg/go-header v0.6.6 h1:17GvSXU/w8L1YWHZP4pYm9/4YHA8iy5Ku2wTE github.com/celestiaorg/go-header v0.6.6/go.mod h1:RdnlTmsyuNerztNiJiQE5G/EGEH+cErhQ83xNjuGcaQ= github.com/celestiaorg/go-libp2p-messenger v0.2.2 h1:osoUfqjss7vWTIZrrDSy953RjQz+ps/vBFE7bychLEc= github.com/celestiaorg/go-libp2p-messenger v0.2.2/go.mod h1:oTCRV5TfdO7V/k6nkx7QjQzGrWuJbupv+0o1cgnY2i4= -github.com/celestiaorg/go-square/v2 v2.2.0 h1:zJnUxCYc65S8FgUfVpyG/osDcsnjzo/JSXw/Uwn8zp4= -github.com/celestiaorg/go-square/v2 v2.2.0/go.mod h1:j8kQUqJLYtcvCQMQV6QjEhUdaF7rBTXF74g8LbkR0Co= github.com/celestiaorg/utils v0.1.0 h1:WsP3O8jF7jKRgLNFmlDCwdThwOFMFxg0MnqhkLFVxPo= github.com/celestiaorg/utils v0.1.0/go.mod h1:vQTh7MHnvpIeCQZ2/Ph+w7K1R2UerDheZbgJEJD2hSU= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= diff --git a/client/crates/client/src/config.rs b/client/crates/client/src/config.rs new file mode 100644 index 0000000000..5fe4349a68 --- /dev/null +++ b/client/crates/client/src/config.rs @@ -0,0 +1,35 @@ +use crate::{client::Client, error::Result}; +use ev_types::v1::{config_service_client::ConfigServiceClient, GetNamespaceResponse}; +use tonic::Request; + +pub struct ConfigClient { + inner: ConfigServiceClient, +} + +impl ConfigClient { + /// Create a new ConfigClient from a Client + pub fn new(client: &Client) -> Self { + let inner = ConfigServiceClient::new(client.channel().clone()); + Self { inner } + } + + /// Get the namespace for this network + pub async fn get_namespace(&self) -> Result { + let request = Request::new(()); + let response = self.inner.clone().get_namespace(request).await?; + + Ok(response.into_inner()) + } + + /// Get the header namespace + pub async fn get_header_namespace(&self) -> Result { + let response = self.get_namespace().await?; + Ok(response.header_namespace) + } + + /// Get the data namespace + pub async fn get_data_namespace(&self) -> Result { + let response = self.get_namespace().await?; + Ok(response.data_namespace) + } +} diff --git a/client/crates/client/src/lib.rs b/client/crates/client/src/lib.rs index d150dcb96a..a75101c7c9 100644 --- a/client/crates/client/src/lib.rs +++ b/client/crates/client/src/lib.rs @@ -5,7 +5,7 @@ //! # Example //! //! ```no_run -//! use ev_client::{Client, HealthClient}; +//! use ev_client::{Client, HealthClient, ConfigClient}; //! //! #[tokio::main] //! async fn main() -> Result<(), Box> { @@ -17,6 +17,12 @@ //! let is_healthy = health.is_healthy().await?; //! println!("Node healthy: {}", is_healthy); //! +//! // Get namespace configuration +//! let config = ConfigClient::new(&client); +//! let namespace = config.get_namespace().await?; +//! println!("Header namespace: {}", namespace.header_namespace); +//! println!("Data namespace: {}", namespace.data_namespace); +//! //! Ok(()) //! } //! ``` @@ -71,6 +77,7 @@ //! ``` pub mod client; +pub mod config; pub mod error; pub mod health; pub mod p2p; @@ -79,6 +86,7 @@ pub mod store; // Re-export main types for convenience pub use client::{Client, ClientBuilder}; +pub use config::ConfigClient; pub use error::{ClientError, Result}; pub use health::HealthClient; pub use p2p::P2PClient; diff --git a/client/crates/types/src/proto/evnode.v1.messages.rs b/client/crates/types/src/proto/evnode.v1.messages.rs index c23d150cbc..62c5d4ba90 100644 --- a/client/crates/types/src/proto/evnode.v1.messages.rs +++ b/client/crates/types/src/proto/evnode.v1.messages.rs @@ -418,3 +418,12 @@ pub struct GetMetadataResponse { #[prost(bytes = "vec", tag = "1")] pub value: ::prost::alloc::vec::Vec, } +/// GetNamespaceResponse returns the namespace for this network +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNamespaceResponse { + #[prost(string, tag = "1")] + pub header_namespace: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub data_namespace: ::prost::alloc::string::String, +} diff --git a/client/crates/types/src/proto/evnode.v1.services.rs b/client/crates/types/src/proto/evnode.v1.services.rs index 4419fab9a7..089666949a 100644 --- a/client/crates/types/src/proto/evnode.v1.services.rs +++ b/client/crates/types/src/proto/evnode.v1.services.rs @@ -2430,3 +2430,304 @@ pub mod store_service_server { const NAME: &'static str = "evnode.v1.StoreService"; } } +/// GetNamespaceResponse returns the namespace for this network +#[allow(clippy::derive_partial_eq_without_eq)] +#[derive(Clone, PartialEq, ::prost::Message)] +pub struct GetNamespaceResponse { + #[prost(string, tag = "1")] + pub header_namespace: ::prost::alloc::string::String, + #[prost(string, tag = "2")] + pub data_namespace: ::prost::alloc::string::String, +} +/// Generated client implementations. +pub mod config_service_client { + #![allow(unused_variables, dead_code, missing_docs, clippy::let_unit_value)] + use tonic::codegen::*; + use tonic::codegen::http::Uri; + /// StoreService defines the RPC service for the store package + #[derive(Debug, Clone)] + pub struct ConfigServiceClient { + inner: tonic::client::Grpc, + } + impl ConfigServiceClient { + /// 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 ConfigServiceClient + 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, + ) -> ConfigServiceClient> + where + F: tonic::service::Interceptor, + T::ResponseBody: Default, + T: tonic::codegen::Service< + http::Request, + Response = http::Response< + >::ResponseBody, + >, + >, + , + >>::Error: Into + Send + Sync, + { + ConfigServiceClient::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 + } + /// GetNamespace returns the namespace for this network + pub async fn get_namespace( + &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( + "/evnode.v1.ConfigService/GetNamespace", + ); + let mut req = request.into_request(); + req.extensions_mut() + .insert(GrpcMethod::new("evnode.v1.ConfigService", "GetNamespace")); + self.inner.unary(req, path, codec).await + } + } +} +/// Generated server implementations. +pub mod config_service_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 ConfigServiceServer. + #[async_trait] + pub trait ConfigService: Send + Sync + 'static { + /// GetNamespace returns the namespace for this network + async fn get_namespace( + &self, + request: tonic::Request<()>, + ) -> std::result::Result< + tonic::Response, + tonic::Status, + >; + } + /// StoreService defines the RPC service for the store package + #[derive(Debug)] + pub struct ConfigServiceServer { + inner: _Inner, + accept_compression_encodings: EnabledCompressionEncodings, + send_compression_encodings: EnabledCompressionEncodings, + max_decoding_message_size: Option, + max_encoding_message_size: Option, + } + struct _Inner(Arc); + impl ConfigServiceServer { + 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 ConfigServiceServer + where + T: ConfigService, + 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() { + "/evnode.v1.ConfigService/GetNamespace" => { + #[allow(non_camel_case_types)] + struct GetNamespaceSvc(pub Arc); + impl tonic::server::UnaryService<()> + for GetNamespaceSvc { + type Response = super::GetNamespaceResponse; + 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 { + ::get_namespace(&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 = GetNamespaceSvc(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 ConfigServiceServer { + 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 ConfigServiceServer { + const NAME: &'static str = "evnode.v1.ConfigService"; + } +} diff --git a/core/da/namespace.go b/core/da/namespace.go new file mode 100644 index 0000000000..2a742f55b4 --- /dev/null +++ b/core/da/namespace.go @@ -0,0 +1,144 @@ +package da + +import ( + "crypto/sha256" + "encoding/hex" + "fmt" + "strings" +) + +// Implemented in accordance to https://celestiaorg.github.io/celestia-app/namespace.html + +const ( + // NamespaceVersionSize is the size of the namespace version in bytes + NamespaceVersionSize = 1 + // NamespaceIDSize is the size of the namespace ID in bytes + NamespaceIDSize = 28 + // NamespaceSize is the total size of a namespace (version + ID) in bytes + NamespaceSize = NamespaceVersionSize + NamespaceIDSize + + // NamespaceVersionZero is the only supported user-specifiable namespace version + NamespaceVersionZero = uint8(0) + // NamespaceVersionMax is the max namespace version + NamespaceVersionMax = uint8(255) + + // NamespaceVersionZeroPrefixSize is the number of leading zero bytes required for version 0 + NamespaceVersionZeroPrefixSize = 18 + // NamespaceVersionZeroDataSize is the number of data bytes available for version 0 + NamespaceVersionZeroDataSize = 10 +) + +// Namespace represents a Celestia namespace +type Namespace struct { + Version uint8 + ID [NamespaceIDSize]byte +} + +// Bytes returns the namespace as a byte slice +func (n Namespace) Bytes() []byte { + result := make([]byte, NamespaceSize) + result[0] = n.Version + copy(result[1:], n.ID[:]) + return result +} + +// IsValidForVersion0 checks if the namespace is valid for version 0 +// Version 0 requires the first 18 bytes of the ID to be zero +func (n Namespace) IsValidForVersion0() bool { + if n.Version != NamespaceVersionZero { + return false + } + + for i := 0; i < NamespaceVersionZeroPrefixSize; i++ { + if n.ID[i] != 0 { + return false + } + } + return true +} + +// NewNamespaceV0 creates a new version 0 namespace from the provided data +// The data should be up to 10 bytes and will be placed in the last 10 bytes of the ID +// The first 18 bytes will be zeros as required by the specification +func NewNamespaceV0(data []byte) (*Namespace, error) { + if len(data) > NamespaceVersionZeroDataSize { + return nil, fmt.Errorf("data too long for version 0 namespace: got %d bytes, max %d", + len(data), NamespaceVersionZeroDataSize) + } + + ns := &Namespace{ + Version: NamespaceVersionZero, + } + + // The first 18 bytes are already zero (Go zero-initializes) + // Copy the data to the last 10 bytes + copy(ns.ID[NamespaceVersionZeroPrefixSize:], data) + + return ns, nil +} + +// NamespaceFromBytes creates a namespace from a 29-byte slice +func NamespaceFromBytes(b []byte) (*Namespace, error) { + if len(b) != NamespaceSize { + return nil, fmt.Errorf("invalid namespace size: expected %d, got %d", NamespaceSize, len(b)) + } + + ns := &Namespace{ + Version: b[0], + } + copy(ns.ID[:], b[1:]) + + // Validate if it's version 0 + if ns.Version == NamespaceVersionZero && !ns.IsValidForVersion0() { + return nil, fmt.Errorf("invalid version 0 namespace: first %d bytes of ID must be zero", + NamespaceVersionZeroPrefixSize) + } + + return ns, nil +} + +// NamespaceFromString creates a version 0 namespace from a string identifier +// The string is hashed and the first 10 bytes of the hash are used as the namespace data +func NamespaceFromString(s string) *Namespace { + // Hash the string to get consistent bytes + hash := sha256.Sum256([]byte(s)) + + // Use the first 10 bytes of the hash for the namespace data + ns, _ := NewNamespaceV0(hash[:NamespaceVersionZeroDataSize]) + return ns +} + +// HexString returns the hex representation of the namespace +func (n Namespace) HexString() string { + return "0x" + hex.EncodeToString(n.Bytes()) +} + +// ParseHexNamespace parses a hex string into a namespace +func ParseHexNamespace(hexStr string) (*Namespace, error) { + // Remove 0x prefix if present + hexStr = strings.TrimPrefix(hexStr, "0x") + + b, err := hex.DecodeString(hexStr) + if err != nil { + return nil, fmt.Errorf("invalid hex string: %w", err) + } + + return NamespaceFromBytes(b) +} + +// PrepareNamespace converts a namespace identifier (string or bytes) into a proper Celestia namespace +// This is the main function to be used when preparing namespaces for DA operations +func PrepareNamespace(identifier []byte) []byte { + // If the identifier is already a valid namespace (29 bytes), validate and return it + if len(identifier) == NamespaceSize { + ns, err := NamespaceFromBytes(identifier) + if err == nil { + return ns.Bytes() + } + // If it's not a valid namespace, treat it as a string identifier + } + + // Convert the identifier to a string and create a namespace from it + ns := NamespaceFromString(string(identifier)) + return ns.Bytes() +} diff --git a/core/da/namespace_test.go b/core/da/namespace_test.go new file mode 100644 index 0000000000..a78d1c2f71 --- /dev/null +++ b/core/da/namespace_test.go @@ -0,0 +1,467 @@ +package da + +import ( + "bytes" + "encoding/hex" + "testing" +) + +func TestNamespaceV0Creation(t *testing.T) { + tests := []struct { + name string + data []byte + expectError bool + description string + }{ + { + name: "valid 10 byte data", + data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}, + expectError: false, + description: "Should create valid namespace with 10 bytes of data", + }, + { + name: "valid 5 byte data", + data: []byte{1, 2, 3, 4, 5}, + expectError: false, + description: "Should create valid namespace with 5 bytes of data (padded with zeros)", + }, + { + name: "empty data", + data: []byte{}, + expectError: false, + description: "Should create valid namespace with empty data (all zeros)", + }, + { + name: "data too long", + data: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11}, + expectError: true, + description: "Should fail with data longer than 10 bytes", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns, err := NewNamespaceV0(tt.data) + + if tt.expectError { + if err == nil { + t.Errorf("%s: expected error but got nil", tt.description) + } + if ns != nil { + t.Errorf("expected nil namespace but got %v", ns) + } + } else { + if err != nil { + t.Fatalf("%s: unexpected error: %v", tt.description, err) + } + if ns == nil { + t.Fatal("expected non-nil namespace but got nil") + } + + // Verify version is 0 + if ns.Version != NamespaceVersionZero { + t.Errorf("Version should be 0, got %d", ns.Version) + } + + // Verify first 18 bytes of ID are zeros + for i := 0; i < NamespaceVersionZeroPrefixSize; i++ { + if ns.ID[i] != byte(0) { + t.Errorf("First 18 bytes should be zero, but byte %d is %d", i, ns.ID[i]) + } + } + + // Verify data is in the last 10 bytes + expectedData := make([]byte, NamespaceVersionZeroDataSize) + copy(expectedData, tt.data) + actualData := ns.ID[NamespaceVersionZeroPrefixSize:] + if !bytes.Equal(expectedData, actualData) { + t.Errorf("Data should match in last 10 bytes, expected %v, got %v", expectedData, actualData) + } + + // Verify total size + if len(ns.Bytes()) != NamespaceSize { + t.Errorf("Total namespace size should be 29 bytes, got %d", len(ns.Bytes())) + } + + // Verify it's valid for version 0 + if !ns.IsValidForVersion0() { + t.Error("Should be valid for version 0") + } + } + }) + } +} + +func TestNamespaceFromBytes(t *testing.T) { + tests := []struct { + name string + input []byte + expectError bool + description string + }{ + { + name: "valid version 0 namespace", + input: append([]byte{0}, append(make([]byte, 18), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)...), + expectError: false, + description: "Should parse valid version 0 namespace", + }, + { + name: "invalid size - too short", + input: []byte{0, 0, 0}, + expectError: true, + description: "Should fail with input shorter than 29 bytes", + }, + { + name: "invalid size - too long", + input: make([]byte, 30), + expectError: true, + description: "Should fail with input longer than 29 bytes", + }, + { + name: "invalid version 0 - non-zero prefix", + input: append([]byte{0}, append([]byte{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)...), + expectError: true, + description: "Should fail when version 0 namespace has non-zero bytes in first 18 bytes of ID", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns, err := NamespaceFromBytes(tt.input) + + if tt.expectError { + if err == nil { + t.Errorf("%s: expected error but got nil", tt.description) + } + if ns != nil { + t.Errorf("expected nil namespace but got %v", ns) + } + } else { + if err != nil { + t.Fatalf("%s: unexpected error: %v", tt.description, err) + } + if ns == nil { + t.Fatal("expected non-nil namespace but got nil") + } + if !bytes.Equal(tt.input, ns.Bytes()) { + t.Errorf("Should round-trip correctly, expected %v, got %v", tt.input, ns.Bytes()) + } + } + }) + } +} + +func TestNamespaceFromString(t *testing.T) { + tests := []struct { + name string + input string + verify func(t *testing.T, ns *Namespace) + }{ + { + name: "rollkit-headers", + input: "rollkit-headers", + verify: func(t *testing.T, ns *Namespace) { + if ns.Version != NamespaceVersionZero { + t.Errorf("expected version %d, got %d", NamespaceVersionZero, ns.Version) + } + if !ns.IsValidForVersion0() { + t.Error("namespace should be valid for version 0") + } + if len(ns.Bytes()) != NamespaceSize { + t.Errorf("expected namespace size %d, got %d", NamespaceSize, len(ns.Bytes())) + } + + // The hash should be deterministic + ns2 := NamespaceFromString("rollkit-headers") + if !bytes.Equal(ns.Bytes(), ns2.Bytes()) { + t.Error("Same string should produce same namespace") + } + }, + }, + { + name: "rollkit-data", + input: "rollkit-data", + verify: func(t *testing.T, ns *Namespace) { + if ns.Version != NamespaceVersionZero { + t.Errorf("expected version %d, got %d", NamespaceVersionZero, ns.Version) + } + if !ns.IsValidForVersion0() { + t.Error("namespace should be valid for version 0") + } + if len(ns.Bytes()) != NamespaceSize { + t.Errorf("expected namespace size %d, got %d", NamespaceSize, len(ns.Bytes())) + } + + // Different strings should produce different namespaces + ns2 := NamespaceFromString("rollkit-headers") + if bytes.Equal(ns.Bytes(), ns2.Bytes()) { + t.Error("Different strings should produce different namespaces") + } + }, + }, + { + name: "empty string", + input: "", + verify: func(t *testing.T, ns *Namespace) { + if ns.Version != NamespaceVersionZero { + t.Errorf("expected version %d, got %d", NamespaceVersionZero, ns.Version) + } + if !ns.IsValidForVersion0() { + t.Error("namespace should be valid for version 0") + } + if len(ns.Bytes()) != NamespaceSize { + t.Errorf("expected namespace size %d, got %d", NamespaceSize, len(ns.Bytes())) + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + ns := NamespaceFromString(tt.input) + if ns == nil { + t.Fatal("expected non-nil namespace but got nil") + } + tt.verify(t, ns) + }) + } +} + +func TestPrepareNamespace(t *testing.T) { + tests := []struct { + name string + input []byte + description string + verify func(t *testing.T, result []byte) + }{ + { + name: "string identifier", + input: []byte("rollkit-headers"), + description: "Should convert string to valid namespace", + verify: func(t *testing.T, result []byte) { + if len(result) != NamespaceSize { + t.Errorf("expected result size %d, got %d", NamespaceSize, len(result)) + } + if result[0] != byte(0) { + t.Errorf("Should be version 0, got %d", result[0]) + } + + // Verify first 18 bytes of ID are zeros + for i := 1; i <= 18; i++ { + if result[i] != byte(0) { + t.Errorf("First 18 bytes of ID should be zero, but byte %d is %d", i, result[i]) + } + } + }, + }, + { + name: "already valid namespace", + input: append([]byte{0}, append(make([]byte, 18), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)...), + description: "Should return already valid namespace unchanged", + verify: func(t *testing.T, result []byte) { + if len(result) != NamespaceSize { + t.Errorf("expected result size %d, got %d", NamespaceSize, len(result)) + } + if result[0] != byte(0) { + t.Errorf("expected version 0, got %d", result[0]) + } + // Should pass through unchanged + expected := append([]byte{0}, append(make([]byte, 18), []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}...)...) + if !bytes.Equal(expected, result) { + t.Errorf("Should pass through unchanged, expected %v, got %v", expected, result) + } + }, + }, + { + name: "invalid 29-byte input", + input: append([]byte{0}, append([]byte{1}, make([]byte, 27)...)...), // Invalid: non-zero in prefix + description: "Should treat invalid 29-byte input as string and hash it", + verify: func(t *testing.T, result []byte) { + if len(result) != NamespaceSize { + t.Errorf("expected result size %d, got %d", NamespaceSize, len(result)) + } + if result[0] != byte(0) { + t.Errorf("expected version 0, got %d", result[0]) + } + + // Should be hashed, not passed through + invalidNs := append([]byte{0}, append([]byte{1}, make([]byte, 27)...)...) + if bytes.Equal(invalidNs, result) { + t.Error("Should not pass through invalid namespace") + } + }, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := PrepareNamespace(tt.input) + if result == nil { + t.Fatal("expected non-nil result but got nil") + } + tt.verify(t, result) + }) + } +} + +func TestHexStringConversion(t *testing.T) { + ns, err := NewNamespaceV0([]byte{1, 2, 3, 4, 5}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + // Test HexString + hexStr := ns.HexString() + if len(hexStr) <= 2 { + t.Error("Hex string should not be empty") + } + if hexStr[:2] != "0x" { + t.Errorf("Should have 0x prefix, got %s", hexStr[:2]) + } + + // Test ParseHexNamespace + parsed, err := ParseHexNamespace(hexStr) + if err != nil { + t.Fatalf("unexpected error parsing hex: %v", err) + } + if !bytes.Equal(ns.Bytes(), parsed.Bytes()) { + t.Error("Should round-trip through hex") + } + + // Test without 0x prefix + parsed2, err := ParseHexNamespace(hexStr[2:]) + if err != nil { + t.Fatalf("unexpected error parsing hex without prefix: %v", err) + } + if !bytes.Equal(ns.Bytes(), parsed2.Bytes()) { + t.Error("Should work without 0x prefix") + } + + // Test invalid hex + _, err = ParseHexNamespace("invalid-hex") + if err == nil { + t.Error("Should fail with invalid hex") + } + + // Test wrong size hex + _, err = ParseHexNamespace("0x0011") + if err == nil { + t.Error("Should fail with wrong size") + } +} + +func TestCelestiaSpecCompliance(t *testing.T) { + // Test that our implementation follows the Celestia namespace specification + + t.Run("namespace structure", func(t *testing.T) { + ns, err := NewNamespaceV0([]byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0A}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + nsBytes := ns.Bytes() + + // Check total size is 29 bytes (1 version + 28 ID) + if len(nsBytes) != 29 { + t.Errorf("Total namespace size should be 29 bytes, got %d", len(nsBytes)) + } + + // Check version byte is at position 0 + if nsBytes[0] != byte(0) { + t.Errorf("Version byte should be 0, got %d", nsBytes[0]) + } + + // Check ID is 28 bytes starting at position 1 + if len(nsBytes[1:]) != 28 { + t.Errorf("ID should be 28 bytes, got %d", len(nsBytes[1:])) + } + }) + + t.Run("version 0 requirements", func(t *testing.T) { + ns, err := NewNamespaceV0([]byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + nsBytes := ns.Bytes() + + // For version 0, first 18 bytes of ID must be zero + for i := 1; i <= 18; i++ { + if nsBytes[i] != byte(0) { + t.Errorf("Bytes 1-18 should be zero for version 0, but byte %d is %d", i, nsBytes[i]) + } + } + + // Last 10 bytes should contain our data + expectedData := []byte{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF} + actualData := nsBytes[19:29] + if !bytes.Equal(expectedData, actualData) { + t.Errorf("Last 10 bytes should contain user data, expected %v, got %v", expectedData, actualData) + } + }) + + t.Run("example from spec", func(t *testing.T) { + // Create a namespace similar to the example in the spec + ns, err := NewNamespaceV0([]byte{0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01, 0x01}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + hexStr := ns.HexString() + t.Logf("Example namespace: %s", hexStr) + + // Verify it matches the expected format + if len(hexStr) != 60 { + t.Errorf("Hex string should be 60 chars (0x + 58 hex chars), got %d", len(hexStr)) + } + // The prefix should be: 0x (2 chars) + version byte 00 (2 chars) + 18 zero bytes (36 chars) = 40 chars total + expectedPrefix := "0x00000000000000000000000000000000000000" + if hexStr[:40] != expectedPrefix { + t.Errorf("Should have correct zero prefix, expected %s, got %s", expectedPrefix, hexStr[:40]) + } + }) +} + +func TestRealWorldNamespaces(t *testing.T) { + // Test with actual namespace strings used in rollkit + namespaces := []string{ + "rollkit-headers", + "rollkit-data", + "legacy-namespace", + "test-headers", + "test-data", + } + + seen := make(map[string]bool) + + for _, nsStr := range namespaces { + t.Run(nsStr, func(t *testing.T) { + // Convert string to namespace + nsBytes := PrepareNamespace([]byte(nsStr)) + + // Verify it's valid + ns, err := NamespaceFromBytes(nsBytes) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !ns.IsValidForVersion0() { + t.Error("namespace should be valid for version 0") + } + + // Verify uniqueness + hexStr := hex.EncodeToString(nsBytes) + if seen[hexStr] { + t.Errorf("Namespace should be unique, but %s was already seen", hexStr) + } + seen[hexStr] = true + + // Verify deterministic + nsBytes2 := PrepareNamespace([]byte(nsStr)) + if !bytes.Equal(nsBytes, nsBytes2) { + t.Error("Should be deterministic") + } + + t.Logf("Namespace for '%s': %s", nsStr, hex.EncodeToString(nsBytes)) + }) + } +} \ No newline at end of file diff --git a/da/cmd/local-da/local.go b/da/cmd/local-da/local.go index 0199937977..aa10124317 100644 --- a/da/cmd/local-da/local.go +++ b/da/cmd/local-da/local.go @@ -58,6 +58,14 @@ func NewLocalDA(logger zerolog.Logger, opts ...func(*LocalDA) *LocalDA) *LocalDA var _ coreda.DA = &LocalDA{} +// validateNamespace checks that namespace is exactly 29 bytes +func validateNamespace(ns []byte) error { + if len(ns) != 29 { + return fmt.Errorf("namespace must be exactly 29 bytes, got %d", len(ns)) + } + return nil +} + // MaxBlobSize returns the max blob size in bytes. func (d *LocalDA) MaxBlobSize(ctx context.Context) (uint64, error) { d.logger.Debug().Uint64("maxBlobSize", d.maxBlobSize).Msg("MaxBlobSize called") @@ -77,7 +85,11 @@ func (d *LocalDA) GasPrice(ctx context.Context) (float64, error) { } // Get returns Blobs for given IDs. -func (d *LocalDA) Get(ctx context.Context, ids []coreda.ID, _ []byte) ([]coreda.Blob, error) { +func (d *LocalDA) Get(ctx context.Context, ids []coreda.ID, ns []byte) ([]coreda.Blob, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("Get: invalid namespace") + return nil, err + } d.logger.Debug().Interface("ids", ids).Msg("Get called") d.mu.Lock() defer d.mu.Unlock() @@ -105,7 +117,11 @@ func (d *LocalDA) Get(ctx context.Context, ids []coreda.ID, _ []byte) ([]coreda. } // GetIDs returns IDs of Blobs at given DA height. -func (d *LocalDA) GetIDs(ctx context.Context, height uint64, _ []byte) (*coreda.GetIDsResult, error) { +func (d *LocalDA) GetIDs(ctx context.Context, height uint64, ns []byte) (*coreda.GetIDsResult, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("GetIDs: invalid namespace") + return nil, err + } d.logger.Debug().Uint64("height", height).Msg("GetIDs called") d.mu.Lock() defer d.mu.Unlock() @@ -130,9 +146,13 @@ func (d *LocalDA) GetIDs(ctx context.Context, height uint64, _ []byte) (*coreda. } // GetProofs returns inclusion Proofs for all Blobs located in DA at given height. -func (d *LocalDA) GetProofs(ctx context.Context, ids []coreda.ID, _ []byte) ([]coreda.Proof, error) { +func (d *LocalDA) GetProofs(ctx context.Context, ids []coreda.ID, ns []byte) ([]coreda.Proof, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("GetProofs: invalid namespace") + return nil, err + } d.logger.Debug().Interface("ids", ids).Msg("GetProofs called") - blobs, err := d.Get(ctx, ids, nil) + blobs, err := d.Get(ctx, ids, ns) if err != nil { d.logger.Error().Err(err).Msg("GetProofs: failed to get blobs") return nil, err @@ -149,7 +169,11 @@ func (d *LocalDA) GetProofs(ctx context.Context, ids []coreda.ID, _ []byte) ([]c } // Commit returns cryptographic Commitments for given blobs. -func (d *LocalDA) Commit(ctx context.Context, blobs []coreda.Blob, _ []byte) ([]coreda.Commitment, error) { +func (d *LocalDA) Commit(ctx context.Context, blobs []coreda.Blob, ns []byte) ([]coreda.Commitment, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("Commit: invalid namespace") + return nil, err + } d.logger.Debug().Int("numBlobs", len(blobs)).Msg("Commit called") commits := make([]coreda.Commitment, len(blobs)) for i, blob := range blobs { @@ -160,8 +184,12 @@ func (d *LocalDA) Commit(ctx context.Context, blobs []coreda.Blob, _ []byte) ([] } // SubmitWithOptions stores blobs in DA layer (options are ignored). -func (d *LocalDA) SubmitWithOptions(ctx context.Context, blobs []coreda.Blob, gasPrice float64, _ []byte, _ []byte) ([]coreda.ID, error) { - d.logger.Info().Int("numBlobs", len(blobs)).Float64("gasPrice", gasPrice).Msg("SubmitWithOptions called") +func (d *LocalDA) SubmitWithOptions(ctx context.Context, blobs []coreda.Blob, gasPrice float64, ns []byte, _ []byte) ([]coreda.ID, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("SubmitWithOptions: invalid namespace") + return nil, err + } + d.logger.Info().Int("numBlobs", len(blobs)).Float64("gasPrice", gasPrice).Str("namespace", string(ns)).Msg("SubmitWithOptions called") // Validate blob sizes before processing for i, blob := range blobs { @@ -186,8 +214,12 @@ func (d *LocalDA) SubmitWithOptions(ctx context.Context, blobs []coreda.Blob, ga } // Submit stores blobs in DA layer (options are ignored). -func (d *LocalDA) Submit(ctx context.Context, blobs []coreda.Blob, gasPrice float64, _ []byte) ([]coreda.ID, error) { - d.logger.Info().Int("numBlobs", len(blobs)).Float64("gasPrice", gasPrice).Msg("Submit called") +func (d *LocalDA) Submit(ctx context.Context, blobs []coreda.Blob, gasPrice float64, ns []byte) ([]coreda.ID, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("Submit: invalid namespace") + return nil, err + } + d.logger.Info().Int("numBlobs", len(blobs)).Float64("gasPrice", gasPrice).Str("namespace", string(ns)).Msg("Submit called") // Validate blob sizes before processing for i, blob := range blobs { @@ -212,7 +244,11 @@ func (d *LocalDA) Submit(ctx context.Context, blobs []coreda.Blob, gasPrice floa } // Validate checks the Proofs for given IDs. -func (d *LocalDA) Validate(ctx context.Context, ids []coreda.ID, proofs []coreda.Proof, _ []byte) ([]bool, error) { +func (d *LocalDA) Validate(ctx context.Context, ids []coreda.ID, proofs []coreda.Proof, ns []byte) ([]bool, error) { + if err := validateNamespace(ns); err != nil { + d.logger.Error().Err(err).Msg("Validate: invalid namespace") + return nil, err + } d.logger.Debug().Int("numIDs", len(ids)).Int("numProofs", len(proofs)).Msg("Validate called") if len(ids) != len(proofs) { d.logger.Error().Int("ids", len(ids)).Int("proofs", len(proofs)).Msg("Validate: id/proof count mismatch") diff --git a/da/go.mod b/da/go.mod index 15296f02e0..7abdb9f9d1 100644 --- a/da/go.mod +++ b/da/go.mod @@ -5,7 +5,6 @@ go 1.24.1 replace github.com/evstack/ev-node/core => ../core require ( - github.com/celestiaorg/go-square/v2 v2.2.0 github.com/evstack/ev-node/core v0.0.0-20250312114929-104787ba1a4c github.com/filecoin-project/go-jsonrpc v0.7.1 github.com/rs/zerolog v1.33.0 @@ -28,10 +27,8 @@ require ( go.opencensus.io v0.24.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.uber.org/zap v1.27.0 // indirect - golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb // indirect golang.org/x/sys v0.31.0 // indirect golang.org/x/xerrors v0.0.0-20240903120638-7835f813f4da // indirect - google.golang.org/protobuf v1.36.6 // indirect gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/da/go.sum b/da/go.sum index f0ad36283c..3f723d176d 100644 --- a/da/go.sum +++ b/da/go.sum @@ -1,7 +1,5 @@ cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= -github.com/celestiaorg/go-square/v2 v2.2.0 h1:zJnUxCYc65S8FgUfVpyG/osDcsnjzo/JSXw/Uwn8zp4= -github.com/celestiaorg/go-square/v2 v2.2.0/go.mod h1:j8kQUqJLYtcvCQMQV6QjEhUdaF7rBTXF74g8LbkR0Co= github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU= github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw= github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc= @@ -105,8 +103,6 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb h1:c0vyKkb6yr3KR7jEfJaOSv4lG7xPkbN6r52aJz1d8a8= -golang.org/x/exp v0.0.0-20231206192017-f3f8817b8deb/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= @@ -165,8 +161,6 @@ google.golang.org/protobuf v1.22.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2 google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.23.1-0.20200526195155-81db48ad09cc/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU= google.golang.org/protobuf v1.25.0/go.mod h1:9JNX74DMeImyA3h4bdi1ymwjUzf21/xIlbajtzgsN7c= -google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= -google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= diff --git a/da/jsonrpc/client.go b/da/jsonrpc/client.go index f622d7f616..cba3574971 100644 --- a/da/jsonrpc/client.go +++ b/da/jsonrpc/client.go @@ -21,10 +21,11 @@ type Module interface { // API defines the jsonrpc service module API type API struct { - Logger zerolog.Logger - Namespace []byte - MaxBlobSize uint64 - Internal struct { + Logger zerolog.Logger + MaxBlobSize uint64 + gasPrice float64 + gasMultiplier float64 + Internal struct { Get func(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, error) `perm:"read"` GetIDs func(ctx context.Context, height uint64, ns []byte) (*da.GetIDsResult, error) `perm:"read"` GetProofs func(ctx context.Context, ids []da.ID, ns []byte) ([]da.Proof, error) `perm:"read"` @@ -38,9 +39,10 @@ type API struct { } // Get returns Blob for each given ID, or an error. -func (api *API) Get(ctx context.Context, ids []da.ID, _ []byte) ([]da.Blob, error) { - api.Logger.Debug().Str("method", "Get").Int("num_ids", len(ids)).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.Get(ctx, ids, api.Namespace) +func (api *API) Get(ctx context.Context, ids []da.ID, ns []byte) ([]da.Blob, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "Get").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.Get(ctx, ids, preparedNs) if err != nil { if strings.Contains(err.Error(), context.Canceled.Error()) { api.Logger.Debug().Str("method", "Get").Msg("RPC call canceled due to context cancellation") @@ -55,9 +57,10 @@ func (api *API) Get(ctx context.Context, ids []da.ID, _ []byte) ([]da.Blob, erro } // GetIDs returns IDs of all Blobs located in DA at given height. -func (api *API) GetIDs(ctx context.Context, height uint64, _ []byte) (*da.GetIDsResult, error) { - api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.GetIDs(ctx, height, api.Namespace) +func (api *API) GetIDs(ctx context.Context, height uint64, ns []byte) (*da.GetIDsResult, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "GetIDs").Uint64("height", height).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.GetIDs(ctx, height, preparedNs) if err != nil { // Using strings.contains since JSON RPC serialization doesn't preserve error wrapping // Check if the error is specifically BlobNotFound, otherwise log and return @@ -88,9 +91,10 @@ func (api *API) GetIDs(ctx context.Context, height uint64, _ []byte) (*da.GetIDs } // GetProofs returns inclusion Proofs for Blobs specified by their IDs. -func (api *API) GetProofs(ctx context.Context, ids []da.ID, _ []byte) ([]da.Proof, error) { - api.Logger.Debug().Str("method", "GetProofs").Int("num_ids", len(ids)).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.GetProofs(ctx, ids, api.Namespace) +func (api *API) GetProofs(ctx context.Context, ids []da.ID, ns []byte) ([]da.Proof, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "GetProofs").Int("num_ids", len(ids)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.GetProofs(ctx, ids, preparedNs) if err != nil { api.Logger.Error().Err(err).Str("method", "GetProofs").Msg("RPC call failed") } else { @@ -100,9 +104,10 @@ func (api *API) GetProofs(ctx context.Context, ids []da.ID, _ []byte) ([]da.Proo } // Commit creates a Commitment for each given Blob. -func (api *API) Commit(ctx context.Context, blobs []da.Blob, _ []byte) ([]da.Commitment, error) { - api.Logger.Debug().Str("method", "Commit").Int("num_blobs", len(blobs)).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.Commit(ctx, blobs, api.Namespace) +func (api *API) Commit(ctx context.Context, blobs []da.Blob, ns []byte) ([]da.Commitment, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "Commit").Int("num_blobs", len(blobs)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.Commit(ctx, blobs, preparedNs) if err != nil { api.Logger.Error().Err(err).Str("method", "Commit").Msg("RPC call failed") } else { @@ -112,9 +117,10 @@ func (api *API) Commit(ctx context.Context, blobs []da.Blob, _ []byte) ([]da.Com } // Validate validates Commitments against the corresponding Proofs. This should be possible without retrieving the Blobs. -func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, _ []byte) ([]bool, error) { - api.Logger.Debug().Str("method", "Validate").Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.Validate(ctx, ids, proofs, api.Namespace) +func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, ns []byte) ([]bool, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "Validate").Int("num_ids", len(ids)).Int("num_proofs", len(proofs)).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.Validate(ctx, ids, proofs, preparedNs) if err != nil { api.Logger.Error().Err(err).Str("method", "Validate").Msg("RPC call failed") } else { @@ -124,15 +130,16 @@ func (api *API) Validate(ctx context.Context, ids []da.ID, proofs []da.Proof, _ } // Submit submits the Blobs to Data Availability layer. -func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, _ []byte) ([]da.ID, error) { - api.Logger.Debug().Str("method", "Submit").Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.Submit(ctx, blobs, gasPrice, api.Namespace) +func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, ns []byte) ([]da.ID, error) { + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "Submit").Int("num_blobs", len(blobs)).Float64("gas_price", gasPrice).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.Submit(ctx, blobs, gasPrice, preparedNs) if err != nil { if strings.Contains(err.Error(), context.Canceled.Error()) { api.Logger.Debug().Str("method", "Submit").Msg("RPC call canceled due to context cancellation") return res, context.Canceled } - api.Logger.Error().Err(err).Str("method", "Submit").Bytes("namespace", api.Namespace).Msg("RPC call failed") + api.Logger.Error().Err(err).Str("method", "Submit").Bytes("namespace", preparedNs).Msg("RPC call failed") } else { api.Logger.Debug().Str("method", "Submit").Int("num_ids_returned", len(res)).Msg("RPC call successful") } @@ -142,7 +149,7 @@ func (api *API) Submit(ctx context.Context, blobs []da.Blob, gasPrice float64, _ // SubmitWithOptions submits the Blobs to Data Availability layer with additional options. // It validates the entire batch against MaxBlobSize before submission. // If any blob or the total batch size exceeds limits, it returns ErrBlobSizeOverLimit. -func (api *API) SubmitWithOptions(ctx context.Context, inputBlobs []da.Blob, gasPrice float64, _ []byte, options []byte) ([]da.ID, error) { +func (api *API) SubmitWithOptions(ctx context.Context, inputBlobs []da.Blob, gasPrice float64, ns []byte, options []byte) ([]da.ID, error) { maxBlobSize := api.MaxBlobSize if len(inputBlobs) == 0 { @@ -165,8 +172,9 @@ func (api *API) SubmitWithOptions(ctx context.Context, inputBlobs []da.Blob, gas return nil, da.ErrBlobSizeOverLimit } - api.Logger.Debug().Str("method", "SubmitWithOptions").Int("num_blobs", len(inputBlobs)).Uint64("total_size", totalSize).Float64("gas_price", gasPrice).Str("namespace", string(api.Namespace)).Msg("Making RPC call") - res, err := api.Internal.SubmitWithOptions(ctx, inputBlobs, gasPrice, api.Namespace, options) + preparedNs := da.PrepareNamespace(ns) + api.Logger.Debug().Str("method", "SubmitWithOptions").Int("num_blobs", len(inputBlobs)).Uint64("total_size", totalSize).Float64("gas_price", gasPrice).Str("namespace", hex.EncodeToString(preparedNs)).Msg("Making RPC call") + res, err := api.Internal.SubmitWithOptions(ctx, inputBlobs, gasPrice, preparedNs, options) if err != nil { if strings.Contains(err.Error(), context.Canceled.Error()) { api.Logger.Debug().Str("method", "SubmitWithOptions").Msg("RPC call canceled due to context cancellation") @@ -182,24 +190,14 @@ func (api *API) SubmitWithOptions(ctx context.Context, inputBlobs []da.Blob, gas func (api *API) GasMultiplier(ctx context.Context) (float64, error) { api.Logger.Debug().Str("method", "GasMultiplier").Msg("Making RPC call") - res, err := api.Internal.GasMultiplier(ctx) - if err != nil { - api.Logger.Error().Err(err).Str("method", "GasMultiplier").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "GasMultiplier").Float64("result", res).Msg("RPC call successful") - } - return res, err + + return api.gasMultiplier, nil } func (api *API) GasPrice(ctx context.Context) (float64, error) { api.Logger.Debug().Str("method", "GasPrice").Msg("Making RPC call") - res, err := api.Internal.GasPrice(ctx) - if err != nil { - api.Logger.Error().Err(err).Str("method", "GasPrice").Msg("RPC call failed") - } else { - api.Logger.Debug().Str("method", "GasPrice").Float64("result", res).Msg("RPC call successful") - } - return res, err + + return api.gasPrice, nil } // Client is the jsonrpc client @@ -232,22 +230,19 @@ func (c *Client) Close() { // NewClient creates a new Client with one connection per namespace with the // given token as the authorization token. -func NewClient(ctx context.Context, logger zerolog.Logger, addr string, token, ns string) (*Client, error) { +func NewClient(ctx context.Context, logger zerolog.Logger, addr, token string, gasPrice, gasMultiplier float64) (*Client, error) { authHeader := http.Header{"Authorization": []string{fmt.Sprintf("Bearer %s", token)}} - return newClient(ctx, logger, addr, authHeader, ns) + return newClient(ctx, logger, addr, authHeader, gasPrice, gasMultiplier) } -func newClient(ctx context.Context, logger zerolog.Logger, addr string, authHeader http.Header, namespace string) (*Client, error) { +func newClient(ctx context.Context, logger zerolog.Logger, addr string, authHeader http.Header, gasPrice, gasMultiplier float64) (*Client, error) { var multiCloser multiClientCloser var client Client client.DA.Logger = logger - client.DA.MaxBlobSize = internal.DefaultMaxBytes - namespaceBytes, err := hex.DecodeString(namespace) - if err != nil { - return nil, fmt.Errorf("failed to decode namespace: %w", err) - } - client.DA.Namespace = namespaceBytes - logger.Info().Str("namespace", namespace).Msg("creating new client") + client.DA.MaxBlobSize = uint64(internal.MaxTxSize) + client.DA.gasPrice = gasPrice + client.DA.gasMultiplier = gasMultiplier + errs := getKnownErrorsMapping() for name, module := range moduleMap(&client) { closer, err := jsonrpc.NewMergeClient(ctx, addr, name, []interface{}{module}, authHeader, jsonrpc.WithErrors(errs)) diff --git a/da/jsonrpc/client_test.go b/da/jsonrpc/client_test.go index c3555efaa6..af32882ea9 100644 --- a/da/jsonrpc/client_test.go +++ b/da/jsonrpc/client_test.go @@ -85,7 +85,6 @@ func TestSubmitWithOptions_SizeValidation(t *testing.T) { api := &API{ Logger: logger, MaxBlobSize: tc.maxBlobSize, - Namespace: []byte("test"), } // Mock the Internal.SubmitWithOptions to always succeed if called @@ -103,7 +102,7 @@ func TestSubmitWithOptions_SizeValidation(t *testing.T) { // Call SubmitWithOptions ctx := context.Background() - result, err := api.SubmitWithOptions(ctx, tc.inputBlobs, 1.0, nil, nil) + result, err := api.SubmitWithOptions(ctx, tc.inputBlobs, 1.0, []byte("test"), nil) // Verify expectations if tc.expectError { diff --git a/da/jsonrpc/internal/consts.go b/da/jsonrpc/internal/consts.go index 61b2d25f2f..9e72b9d915 100644 --- a/da/jsonrpc/internal/consts.go +++ b/da/jsonrpc/internal/consts.go @@ -1,34 +1,41 @@ package appconsts -import ( - "time" +import "time" - "github.com/celestiaorg/go-square/v2/share" -) - -// The following defaults correspond to initial parameters of the network that can be changed, not via app versions -// but other means such as on-chain governance, or the node's local config const ( - // DefaultGovMaxSquareSize is the default value for the governance modifiable - // max square size. - DefaultGovMaxSquareSize = 64 - - // DefaultMaxBytes is the default value for the governance modifiable - // maximum number of bytes allowed in a valid block. We subtract 1 to have some extra buffer. - DefaultMaxBytes = DefaultGovMaxSquareSize * DefaultGovMaxSquareSize * (share.ContinuationSparseShareContentSize - 1) - - // DefaultMinGasPrice is the default min gas price that gets set in the app.toml file. - // The min gas price acts as a filter. Transactions below that limit will not pass - // a node's `CheckTx` and thus not be proposed by that node. - DefaultMinGasPrice = 0.002 // utia - - // DefaultUnbondingTime is the default time a validator must wait - // to unbond in a proof of stake system. Any validator within this - // time can be subject to slashing under conditions of misbehavior. - DefaultUnbondingTime = 3 * 7 * 24 * time.Hour + Version uint64 = 5 + // SquareSizeUpperBound imposes an upper bound on the max effective square size. + SquareSizeUpperBound int = 128 + // SubtreeRootThreshold works as a target upper bound for the number of subtree + // roots in the share commitment. If a blob contains more shares than this + // number, then the height of the subtree roots will increase by one so that the + // number of subtree roots in the share commitment decreases by a factor of two. + // This step is repeated until the number of subtree roots is less than the + // SubtreeRootThreshold. + // + // The rationale for this value is described in more detail in ADR-013. + SubtreeRootThreshold int = 64 + TxSizeCostPerByte uint64 = 10 + GasPerBlobByte uint32 = 8 + MaxTxSize int = 2097152 // 2 MiB in bytes + TimeoutPropose = time.Millisecond * 3500 + TimeoutCommit = time.Millisecond * 4200 - // DefaultNetworkMinGasPrice is used by x/minfee to prevent transactions from being - // included in a block if they specify a gas price lower than this. - // Only applies to app version >= 2 - DefaultNetworkMinGasPrice = 0.000001 // utia + // TestUpgradeHeightDelay is the number of blocks that chain-id "test" waits + // after a MsgTryUpgrade to activate the next version. + TestUpgradeHeightDelay = int64(3) + // ArabicaUpgradeHeightDelay is the number of blocks that Arabica waits + // after a MsgTryUpgrade to activate the next version. Assuming a block + // interval of 6 seconds, this is 1 day. + ArabicaUpgradeHeightDelay = int64(14_400) + // MochaUpgradeHeightDelay is the number of blocks that Mocha waits + // after a MsgTryUpgrade to activate the next version. Assuming a block + // interval of 6 seconds, this is 2 days. + MochaUpgradeHeightDelay = int64(28_800) + // MainnetUpgradeHeightDelay is the number of blocks that Mainnet waits + // after a MsgTryUpgrade to activate the next version. Assuming a block + // interval of 6 seconds, this is 7 day. + MainnetUpgradeHeightDelay = int64(100_800) + // Deprecated: Use MainnetUpgradeHeightDelay instead. + UpgradeHeightDelay = MainnetUpgradeHeightDelay ) diff --git a/da/jsonrpc/proxy_test.go b/da/jsonrpc/proxy_test.go index f27f410097..a494836b53 100644 --- a/da/jsonrpc/proxy_test.go +++ b/da/jsonrpc/proxy_test.go @@ -31,7 +31,8 @@ const ( testMaxBlobSize = 100 ) -var testNamespace = []byte("test") +// testNamespace is a 15-byte namespace that will be hex encoded to 30 chars and truncated to 29 +var testNamespace = []byte("test-namespace1") // TestProxy runs the go-da DA test suite against the JSONRPC service // NOTE: This test requires a test JSONRPC service to run on the port @@ -54,7 +55,7 @@ func TestProxy(t *testing.T) { } }() - client, err := proxy.NewClient(context.Background(), logger, ClientURL, "", "74657374") + client, err := proxy.NewClient(context.Background(), logger, ClientURL, "74657374", 0, 1) require.NoError(t, err) t.Run("Basic DA test", func(t *testing.T) { @@ -147,7 +148,7 @@ func GetIDsTest(t *testing.T, d coreda.DA) { // As we're the only user, we don't need to handle external data (that could be submitted in real world). // There is no notion of height, so we need to scan the DA to get test data back. for i := uint64(1); !found && !time.Now().After(end); i++ { - ret, err := d.GetIDs(ctx, i, []byte{}) + ret, err := d.GetIDs(ctx, i, testNamespace) if err != nil { if strings.Contains(err.Error(), coreda.ErrHeightFromFuture.Error()) { break @@ -231,6 +232,9 @@ func HeightFromFutureTest(t *testing.T, d coreda.DA) { func TestSubmitWithOptions(t *testing.T) { ctx := context.Background() testNamespace := []byte("options_test") + // The client will convert the namespace string to a proper Celestia namespace + // using SHA256 hashing and version 0 format (1 version byte + 28 ID bytes) + encodedNamespace := coreda.PrepareNamespace(testNamespace) testOptions := []byte("test_options") gasPrice := 0.0 @@ -238,7 +242,6 @@ func TestSubmitWithOptions(t *testing.T) { createMockedClient := func(internalAPI *mocks.MockDA) *proxy.Client { client := &proxy.Client{} client.DA.Internal.SubmitWithOptions = internalAPI.SubmitWithOptions - client.DA.Namespace = testNamespace client.DA.MaxBlobSize = testMaxBlobSize client.DA.Logger = zerolog.Nop() // Test verbosity no longer needed with Nop logger @@ -252,7 +255,7 @@ func TestSubmitWithOptions(t *testing.T) { blobs := []coreda.Blob{[]byte("blob1"), []byte("blob2")} expectedIDs := []coreda.ID{[]byte("id1"), []byte("id2")} - mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, testNamespace, testOptions).Return(expectedIDs, nil).Once() + mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, encodedNamespace, testOptions).Return(expectedIDs, nil).Once() ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, testNamespace, testOptions) @@ -333,7 +336,7 @@ func TestSubmitWithOptions(t *testing.T) { blobs := []coreda.Blob{[]byte("blob1")} expectedError := errors.New("rpc submit failed") - mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, testNamespace, testOptions).Return(nil, expectedError).Once() + mockAPI.On("SubmitWithOptions", ctx, blobs, gasPrice, encodedNamespace, testOptions).Return(nil, expectedError).Once() ids, err := client.DA.SubmitWithOptions(ctx, blobs, gasPrice, testNamespace, testOptions) diff --git a/docs/guides/da/celestia-da.md b/docs/guides/da/celestia-da.md index 11c440c915..52d5aeaab6 100644 --- a/docs/guides/da/celestia-da.md +++ b/docs/guides/da/celestia-da.md @@ -102,20 +102,18 @@ The output of the command above will look similar to this: Your DA AUTH_TOKEN is eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJBbGxvdyI6WyJwdWJsaWMiLCJyZWFkIiwid3JpdGUiXX0.cSrJjpfUdTNFtzGho69V0D_8kyECn9Mzv8ghJSpKRDE ``` -Next, let's set up the namespace to be used for posting data on Celestia: +Next, let's set up the namespace to be used for posting data on Celestia. Evolve supports separate namespaces for headers and data, but for simplicity, we'll use a single namespace for both: ```bash -DA_NAMESPACE=00000000000000000000000000000000000000000008e5f679bf7116cb +DA_NAMESPACE="fancy_namespace" ``` -:::tip -`00000000000000000000000000000000000000000008e5f679bf7116cb` is a default namespace for Mocha testnet. You can set your own by using a command similar to this (or, you could get creative 😎): +**Advanced Configuration:** For production deployments, you can use separate namespaces for headers and data to optimize syncing: -```bash -openssl rand -hex 10 -``` +- `--evolve.da.header_namespace` for block headers +- `--evolve.da.data_namespace` for transaction data -Replace the last 20 characters (10 bytes) in `00000000000000000000000000000000000000000008e5f679bf7116cb` with the newly generated 10 bytes. +The namespace values are automatically encoded by the node to ensure compatibility with Celestia. [Learn more about namespaces](https://docs.celestia.org/tutorials/node-tutorial#namespaces). ::: @@ -135,7 +133,8 @@ Finally, let's initiate the chain node with all the flags: gmd start \ --evolve.node.aggregator \ --evolve.da.auth_token $AUTH_TOKEN \ - --evolve.da.namespace $DA_NAMESPACE \ + --evolve.da.header_namespace $DA_NAMESPACE \ + --evolve.da.data_namespace $DA_NAMESPACE \ --evolve.da.start_height $DA_BLOCK_HEIGHT \ --evolve.da.address $DA_ADDRESS ``` diff --git a/docs/guides/deploy/testnet.md b/docs/guides/deploy/testnet.md index ab8c20d1a6..c3d57aba8c 100644 --- a/docs/guides/deploy/testnet.md +++ b/docs/guides/deploy/testnet.md @@ -248,7 +248,7 @@ Both sequencer and full node Evolve services need to communicate with the celest #### 🔑 Common Integration Points 1. **Authentication**: Evolve requires an auth token generated by the celestia-node so that Evolve can send transactions on its behalf. Both sequencer and full node types use these JWT tokens for secure communication with celestia-node -2. **Namespace Isolation**: Data is organized using Celestia namespaces +2. **Namespace Isolation**: Data is organized using Celestia namespaces (automatically encoded by the node for proper formatting) 3. **API Endpoints**: Both sequencer and full nodes use the same celestia-node API interface 4. **Network Configuration**: All nodes must be configured to connect to the same Celestia network diff --git a/docs/learn/config.md b/docs/learn/config.md index 289090f1fb..567a84103e 100644 --- a/docs/learn/config.md +++ b/docs/learn/config.md @@ -25,6 +25,8 @@ This document provides a comprehensive reference for all configuration options a - [DA Gas Multiplier](#da-gas-multiplier) - [DA Submit Options](#da-submit-options) - [DA Namespace](#da-namespace) + - [DA Header Namespace](#da-header-namespace) + - [DA Data Namespace](#da-data-namespace) - [DA Block Time](#da-block-time) - [DA Start Height](#da-start-height) - [DA Mempool TTL](#da-mempool-ttl) @@ -396,19 +398,57 @@ da: **Description:** The namespace ID used when submitting blobs (block data) to the DA layer. This helps segregate data from different chains or applications on a shared DA layer. +**Note:** This configuration is deprecated in favor of separate header and data namespaces (see below). If only `namespace` is provided, it will be used for both headers and data for backward compatibility. + **YAML:** ```yaml da: - namespace: "MY_UNIQUE_NAMESPACE_ID" + namespace: "MY_UNIQUE_NAMESPACE_ID" # Deprecated - use header_namespace and data_namespace instead ``` **Command-line Flag:** `--rollkit.da.namespace ` *Example:* `--rollkit.da.namespace 0x1234567890abcdef` -*Default:* `""` (empty, must be configured) +*Default:* `""` (empty) *Constant:* `FlagDANamespace` +### DA Header Namespace + +**Description:** +The namespace ID specifically for submitting block headers to the DA layer. Headers are submitted separately from transaction data. The namespace value is encoded by the node to ensure proper formatting and compatibility with the DA layer. + +**YAML:** + +```yaml +da: + header_namespace: "HEADER_NAMESPACE_ID" +``` + +**Command-line Flag:** +`--rollkit.da.header_namespace ` +*Example:* `--rollkit.da.header_namespace my_header_namespace` +*Default:* Falls back to `namespace` if not set +*Constant:* `FlagDAHeaderNamespace` + +### DA Data Namespace + +**Description:** +The namespace ID specifically for submitting transaction data to the DA layer. Transaction data is submitted separately from headers, enabling nodes to sync only the data they need. The namespace value is encoded by the node to ensure proper formatting and compatibility with the DA layer. + +**YAML:** + +```yaml +da: + data_namespace: "DATA_NAMESPACE_ID" +``` + +**Command-line Flag:** +`--rollkit.da.data_namespace ` +*Example:* `--rollkit.da.data_namespace my_data_namespace` +*Default:* Falls back to `namespace` if not set +*Constant:* `FlagDADataNamespace` + ### DA Block Time **Description:** diff --git a/docs/learn/specs/block-manager.md b/docs/learn/specs/block-manager.md index 8ccbfba654..d90dab2861 100644 --- a/docs/learn/specs/block-manager.md +++ b/docs/learn/specs/block-manager.md @@ -121,8 +121,8 @@ Block manager configuration options: |GasPrice|float64|gas price for DA submissions (-1 for automatic/default)| |GasMultiplier|float64|multiplier for gas price on DA submission retries (default: 1.3)| |Namespace|da.Namespace|DA namespace ID for block submissions (deprecated, use HeaderNamespace and DataNamespace instead)| -|HeaderNamespace|string|namespace ID for submitting headers to DA layer| -|DataNamespace|string|namespace ID for submitting data to DA layer| +|HeaderNamespace|string|namespace ID for submitting headers to DA layer (automatically encoded by the node)| +|DataNamespace|string|namespace ID for submitting data to DA layer (automatically encoded by the node)| ### Block Production diff --git a/docs/src/openapi-rpc.json b/docs/src/openapi-rpc.json index 4794573062..52e53e0739 100644 --- a/docs/src/openapi-rpc.json +++ b/docs/src/openapi-rpc.json @@ -2,7 +2,7 @@ "openapi": "3.1.0", "info": { "title": "Evolve API", - "description": "This API provides access to Signer, Store, P2P, and Health services.\n\n## Services\n\n* **Signer Service** - Sign messages and retrieve public keys\n* **Store Service** - Access blocks, state, and metadata\n* **P2P Service** - Network and peer information\n* **Health Service** - Node health checks and simple HTTP endpoints\n\n## Protocols\n\n### gRPC-Web Protocol\n\nMost endpoints use gRPC-Web protocol over HTTP/1.1 with JSON encoding. Requests are made via POST with `Content-Type: application/json`.\n\n### Simple HTTP Endpoints\n\nSome endpoints (like `/health/live`) are simple HTTP GET requests that return plain text responses for basic monitoring and health checks.", + "description": "This API provides access to Signer, Store, P2P, Config, and Health services.\n\n## Services\n\n* **Signer Service** - Sign messages and retrieve public keys\n* **Store Service** - Access blocks, state, and metadata\n* **P2P Service** - Network and peer information\n* **Config Service** - Network configuration and namespace information\n* **Health Service** - Node health checks and simple HTTP endpoints\n\n## Protocols\n\n### gRPC-Web Protocol\n\nMost endpoints use gRPC-Web protocol over HTTP/1.1 with JSON encoding. Requests are made via POST with `Content-Type: application/json`.\n\n### Simple HTTP Endpoints\n\nSome endpoints (like `/health/live`) are simple HTTP GET requests that return plain text responses for basic monitoring and health checks.", "version": "1.0.0", "contact": { "name": "Evolve Team", @@ -28,6 +28,10 @@ "name": "P2P Service", "description": "Network and peer information" }, + { + "name": "Config Service", + "description": "Network configuration and namespace information" + }, { "name": "Health Service", "description": "Node health and liveness checks" @@ -355,6 +359,48 @@ } } }, + "/evnode.v1.ConfigService/GetNamespace": { + "post": { + "tags": [ + "Config Service" + ], + "summary": "Get namespace configuration", + "description": "Retrieve the header and data namespace configuration for this network.", + "operationId": "getNamespace", + "requestBody": { + "description": "Get namespace request (empty)", + "required": true, + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNamespaceRequest" + }, + "examples": { + "default": { + "summary": "Get namespace configuration", + "value": {} + } + } + } + } + }, + "responses": { + "200": { + "description": "Namespace configuration retrieved successfully", + "content": { + "application/json": { + "schema": { + "$ref": "#/components/schemas/GetNamespaceResponse" + } + } + } + }, + "500": { + "$ref": "#/components/responses/InternalError" + } + } + } + }, "/evnode.v1.HealthService/Livez": { "post": { "tags": [ @@ -627,6 +673,31 @@ } } }, + "GetNamespaceRequest": { + "type": "object", + "description": "Request to get namespace configuration (empty)", + "properties": {} + }, + "GetNamespaceResponse": { + "type": "object", + "description": "Response containing namespace configuration. Namespaces are encoded by the node to ensure proper formatting and compatibility with the DA layer.", + "required": [ + "header_namespace", + "data_namespace" + ], + "properties": { + "header_namespace": { + "type": "string", + "description": "The namespace identifier for block headers. This namespace is used exclusively for storing and retrieving block header data on the DA layer. The value is pre-encoded by the node.", + "example": "0x01234567890abcdef" + }, + "data_namespace": { + "type": "string", + "description": "The namespace identifier for block data (transactions). This namespace is used exclusively for storing and retrieving transaction data on the DA layer, separate from headers. The value is pre-encoded by the node.", + "example": "0xfedcba9876543210" + } + } + }, "LivezRequest": { "type": "object", "description": "Request to check node health (empty)", diff --git a/node/full.go b/node/full.go index d724707af7..e8b1993bf3 100644 --- a/node/full.go +++ b/node/full.go @@ -328,7 +328,7 @@ func (n *FullNode) Run(parentCtx context.Context) error { } // Start RPC server - handler, err := rpcserver.NewServiceHandler(n.Store, n.p2pClient, n.Logger) + handler, err := rpcserver.NewServiceHandler(n.Store, n.p2pClient, n.Logger, n.nodeConfig) if err != nil { return fmt.Errorf("error creating RPC handler: %w", err) } diff --git a/node/light.go b/node/light.go index da09e64442..cd8bd604d9 100644 --- a/node/light.go +++ b/node/light.go @@ -77,7 +77,7 @@ func (ln *LightNode) Run(parentCtx context.Context) error { ln.running = true // Start RPC server - handler, err := rpcserver.NewServiceHandler(ln.Store, ln.P2P, ln.Logger) + handler, err := rpcserver.NewServiceHandler(ln.Store, ln.P2P, ln.Logger, ln.nodeConfig) if err != nil { return fmt.Errorf("error creating RPC handler: %w", err) } diff --git a/pkg/p2p/client_test.go b/pkg/p2p/client_test.go index 08a8fbfcda..718848e86e 100644 --- a/pkg/p2p/client_test.go +++ b/pkg/p2p/client_test.go @@ -341,9 +341,34 @@ func TestClientInfoMethods(t *testing.T) { }) t.Run("GetPeers", func(t *testing.T) { + // Wait for peer discovery to find the peers + expectedPeerIDs := []peer.ID{client1.host.ID(), client2.host.ID()} + + err := waitForCondition(2*time.Second, func() bool { + peers, err := client0.GetPeers() + if err != nil { + return false + } + + actualPeerIDs := make(map[peer.ID]bool) + for _, p := range peers { + actualPeerIDs[p.ID] = true + } + + // Check if all expected peers are found + for _, expectedID := range expectedPeerIDs { + if !actualPeerIDs[expectedID] { + return false + } + } + return true + }) + + require.NoError(err, "Timed out waiting for GetPeers to discover all peers") + + // Now verify the peers are as expected peers, err := client0.GetPeers() assert.NoError(err) - expectedPeerIDs := []peer.ID{client1.host.ID(), client2.host.ID()} actualPeerIDs := make([]peer.ID, len(peers)) for i, p := range peers { actualPeerIDs[i] = p.ID diff --git a/pkg/rpc/client/client.go b/pkg/rpc/client/client.go index f4ddb88fe9..316b028f66 100644 --- a/pkg/rpc/client/client.go +++ b/pkg/rpc/client/client.go @@ -11,11 +11,12 @@ import ( rpc "github.com/evstack/ev-node/types/pb/evnode/v1/v1connect" ) -// Client is the client for StoreService, P2PService, and HealthService +// Client is the client for StoreService, P2PService, HealthService, and ConfigService type Client struct { storeClient rpc.StoreServiceClient p2pClient rpc.P2PServiceClient healthClient rpc.HealthServiceClient + configClient rpc.ConfigServiceClient } // NewClient creates a new RPC client @@ -24,11 +25,13 @@ func NewClient(baseURL string) *Client { storeClient := rpc.NewStoreServiceClient(httpClient, baseURL, connect.WithGRPC()) p2pClient := rpc.NewP2PServiceClient(httpClient, baseURL, connect.WithGRPC()) healthClient := rpc.NewHealthServiceClient(httpClient, baseURL, connect.WithGRPC()) + configClient := rpc.NewConfigServiceClient(httpClient, baseURL, connect.WithGRPC()) return &Client{ storeClient: storeClient, p2pClient: p2pClient, healthClient: healthClient, + configClient: configClient, } } @@ -120,3 +123,13 @@ func (c *Client) GetHealth(ctx context.Context) (pb.HealthStatus, error) { } return resp.Msg.Status, nil } + +// GetNamespace returns the namespace configuration for this network +func (c *Client) GetNamespace(ctx context.Context) (*pb.GetNamespaceResponse, error) { + req := connect.NewRequest(&emptypb.Empty{}) + resp, err := c.configClient.GetNamespace(ctx, req) + if err != nil { + return nil, err + } + return resp.Msg, nil +} diff --git a/pkg/rpc/client/client_test.go b/pkg/rpc/client/client_test.go index c86f6db4d3..42ca238eca 100644 --- a/pkg/rpc/client/client_test.go +++ b/pkg/rpc/client/client_test.go @@ -15,6 +15,7 @@ import ( "golang.org/x/net/http2" "golang.org/x/net/http2/h2c" + "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/rpc/server" "github.com/evstack/ev-node/test/mocks" @@ -31,6 +32,12 @@ func setupTestServer(t *testing.T, mockStore *mocks.MockStore, mockP2P *mocks.Mo logger := zerolog.Nop() storeServer := server.NewStoreServer(mockStore, logger) p2pServer := server.NewP2PServer(mockP2P) + healthServer := server.NewHealthServer() + + // Create config server with test config + testConfig := config.DefaultConfig + testConfig.DA.Namespace = "test-headers" + configServer := server.NewConfigServer(testConfig, logger) // Register the store service storePath, storeHandler := rpc.NewStoreServiceHandler(storeServer) @@ -40,6 +47,14 @@ func setupTestServer(t *testing.T, mockStore *mocks.MockStore, mockP2P *mocks.Mo p2pPath, p2pHandler := rpc.NewP2PServiceHandler(p2pServer) mux.Handle(p2pPath, p2pHandler) + // Register the health service + healthPath, healthHandler := rpc.NewHealthServiceHandler(healthServer) + mux.Handle(healthPath, healthHandler) + + // Register the config service + configPath, configHandler := rpc.NewConfigServiceHandler(configServer) + mux.Handle(configPath, configHandler) + // Create an HTTP server with h2c for HTTP/2 support testServer := httptest.NewServer(h2c.NewHandler(mux, &http2.Server{})) @@ -225,3 +240,41 @@ func TestClientGetNetInfo(t *testing.T) { require.Equal(t, "0.0.0.0:26656", resultNetInfo.ListenAddresses[0]) mockP2P.AssertExpectations(t) } + +func TestClientGetHealth(t *testing.T) { + // Create mocks + mockStore := mocks.NewMockStore(t) + mockP2P := mocks.NewMockP2PRPC(t) + + // Setup test server and client + testServer, client := setupTestServer(t, mockStore, mockP2P) + defer testServer.Close() + + // Call GetHealth + healthStatus, err := client.GetHealth(context.Background()) + + // Assert expectations + require.NoError(t, err) + // Health server always returns PASS in Livez + require.NotEqual(t, healthStatus.String(), "UNKNOWN") +} + +func TestClientGetNamespace(t *testing.T) { + // Create mocks + mockStore := mocks.NewMockStore(t) + mockP2P := mocks.NewMockP2PRPC(t) + + // Setup test server and client + testServer, client := setupTestServer(t, mockStore, mockP2P) + defer testServer.Close() + + // Call GetNamespace + namespaceResp, err := client.GetNamespace(context.Background()) + + // Assert expectations + require.NoError(t, err) + require.NotNil(t, namespaceResp) + // The namespace should be derived from the config we set in setupTestServer + require.NotEmpty(t, namespaceResp.HeaderNamespace) + require.NotEmpty(t, namespaceResp.DataNamespace) +} diff --git a/pkg/rpc/example/example.go b/pkg/rpc/example/example.go index 219808bd5e..dc9cb59d42 100644 --- a/pkg/rpc/example/example.go +++ b/pkg/rpc/example/example.go @@ -8,6 +8,7 @@ import ( "os" "time" + "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/rpc/client" "github.com/evstack/ev-node/pkg/rpc/server" "github.com/evstack/ev-node/pkg/store" @@ -19,7 +20,8 @@ func StartStoreServer(s store.Store, address string, logger zerolog.Logger) { // Create and start the server // Start RPC server rpcAddr := fmt.Sprintf("%s:%d", "localhost", 8080) - handler, err := server.NewServiceHandler(s, nil, logger) + cfg := config.DefaultConfig + handler, err := server.NewServiceHandler(s, nil, logger, cfg) if err != nil { panic(err) } @@ -78,7 +80,8 @@ func ExampleServer(s store.Store) { // Start RPC server rpcAddr := fmt.Sprintf("%s:%d", "localhost", 8080) - handler, err := server.NewServiceHandler(s, nil, logger) + cfg := config.DefaultConfig + handler, err := server.NewServiceHandler(s, nil, logger, cfg) if err != nil { panic(err) } diff --git a/pkg/rpc/server/server.go b/pkg/rpc/server/server.go index 31b966543c..79b3b34fa3 100644 --- a/pkg/rpc/server/server.go +++ b/pkg/rpc/server/server.go @@ -8,10 +8,12 @@ import ( "time" "encoding/binary" + "encoding/hex" "errors" "connectrpc.com/connect" "connectrpc.com/grpcreflect" + coreda "github.com/evstack/ev-node/core/da" ds "github.com/ipfs/go-datastore" "github.com/rs/zerolog" "golang.org/x/net/http2" @@ -19,6 +21,7 @@ import ( "google.golang.org/protobuf/types/known/emptypb" "google.golang.org/protobuf/types/known/timestamppb" + "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/types" @@ -162,6 +165,32 @@ func (s *StoreServer) GetMetadata( }), nil } +type ConfigServer struct { + config config.Config + logger zerolog.Logger +} + +func NewConfigServer(config config.Config, logger zerolog.Logger) *ConfigServer { + return &ConfigServer{ + config: config, + logger: logger, + } +} + +func (cs *ConfigServer) GetNamespace( + ctx context.Context, + req *connect.Request[emptypb.Empty], +) (*connect.Response[pb.GetNamespaceResponse], error) { + + hns := coreda.PrepareNamespace([]byte(cs.config.DA.HeaderNamespace)) + dns := coreda.PrepareNamespace([]byte(cs.config.DA.DataNamespace)) + + return connect.NewResponse(&pb.GetNamespaceResponse{ + HeaderNamespace: hex.EncodeToString(hns), + DataNamespace: hex.EncodeToString(dns), + }), nil +} + // P2PServer implements the P2PService defined in the proto file type P2PServer struct { // Add dependencies needed for P2P functionality @@ -239,10 +268,11 @@ func (h *HealthServer) Livez( } // NewServiceHandler creates a new HTTP handler for Store, P2P and Health services -func NewServiceHandler(store store.Store, peerManager p2p.P2PRPC, logger zerolog.Logger) (http.Handler, error) { +func NewServiceHandler(store store.Store, peerManager p2p.P2PRPC, logger zerolog.Logger, config config.Config) (http.Handler, error) { storeServer := NewStoreServer(store, logger) p2pServer := NewP2PServer(peerManager) healthServer := NewHealthServer() + configServer := NewConfigServer(config, logger) mux := http.NewServeMux() @@ -253,6 +283,7 @@ func NewServiceHandler(store store.Store, peerManager p2p.P2PRPC, logger zerolog rpc.StoreServiceName, rpc.P2PServiceName, rpc.HealthServiceName, + rpc.ConfigServiceName, ) mux.Handle(grpcreflect.NewHandlerV1(reflector, compress1KB)) mux.Handle(grpcreflect.NewHandlerV1Alpha(reflector, compress1KB)) @@ -269,6 +300,9 @@ func NewServiceHandler(store store.Store, peerManager p2p.P2PRPC, logger zerolog healthPath, healthHandler := rpc.NewHealthServiceHandler(healthServer) mux.Handle(healthPath, healthHandler) + configPath, configHandler := rpc.NewConfigServiceHandler(configServer) + mux.Handle(configPath, configHandler) + // Register custom HTTP endpoints RegisterCustomHTTPEndpoints(mux) diff --git a/pkg/rpc/server/server_test.go b/pkg/rpc/server/server_test.go index 0e5a4c2844..cb0676cc35 100644 --- a/pkg/rpc/server/server_test.go +++ b/pkg/rpc/server/server_test.go @@ -19,6 +19,7 @@ import ( "github.com/stretchr/testify/require" "google.golang.org/protobuf/types/known/emptypb" + "github.com/evstack/ev-node/pkg/config" "github.com/evstack/ev-node/pkg/p2p" "github.com/evstack/ev-node/pkg/store" "github.com/evstack/ev-node/test/mocks" @@ -314,7 +315,8 @@ func TestHealthLiveEndpoint(t *testing.T) { // Create the service handler logger := zerolog.Nop() - handler, err := NewServiceHandler(mockStore, mockP2PManager, logger) + testConfig := config.DefaultConfig + handler, err := NewServiceHandler(mockStore, mockP2PManager, logger, testConfig) assert.NoError(err) assert.NotNil(handler) diff --git a/proto/evnode/v1/config.proto b/proto/evnode/v1/config.proto new file mode 100644 index 0000000000..65ecce5630 --- /dev/null +++ b/proto/evnode/v1/config.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; +package evnode.v1; + +import "google/protobuf/empty.proto"; +import "evnode/v1/evnode.proto"; +import "evnode/v1/state.proto"; + +option go_package = "github.com/evstack/ev-node/types/pb/evnode/v1"; + +// StoreService defines the RPC service for the store package +service ConfigService { + + // GetNamespace returns the namespace for this network + rpc GetNamespace(google.protobuf.Empty) returns (GetNamespaceResponse) {} +} + +// GetNamespaceResponse returns the namespace for this network +message GetNamespaceResponse { + string header_namespace = 1; + string data_namespace = 2; +} diff --git a/types/CLAUDE.md b/types/CLAUDE.md index 93de3c8659..d056f48bf3 100644 --- a/types/CLAUDE.md +++ b/types/CLAUDE.md @@ -126,7 +126,7 @@ The types package defines the core data structures and types used throughout ev- ### Directory Structure -``` +```txt pb/evnode/v1/ ├── batch.pb.go # Batch processing messages ├── evnode.pb.go # Core ev-node messages diff --git a/types/pb/evnode/v1/batch.pb.go b/types/pb/evnode/v1/batch.pb.go index 9685e640ff..291eaebcea 100644 --- a/types/pb/evnode/v1/batch.pb.go +++ b/types/pb/evnode/v1/batch.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/batch.proto diff --git a/types/pb/evnode/v1/config.pb.go b/types/pb/evnode/v1/config.pb.go new file mode 100644 index 0000000000..bb33198765 --- /dev/null +++ b/types/pb/evnode/v1/config.pb.go @@ -0,0 +1,140 @@ +// Code generated by protoc-gen-go. DO NOT EDIT. +// versions: +// protoc-gen-go v1.36.7 +// protoc (unknown) +// source: evnode/v1/config.proto + +package v1 + +import ( + protoreflect "google.golang.org/protobuf/reflect/protoreflect" + protoimpl "google.golang.org/protobuf/runtime/protoimpl" + emptypb "google.golang.org/protobuf/types/known/emptypb" + reflect "reflect" + sync "sync" + unsafe "unsafe" +) + +const ( + // Verify that this generated code is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion) + // Verify that runtime/protoimpl is sufficiently up-to-date. + _ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20) +) + +// GetNamespaceResponse returns the namespace for this network +type GetNamespaceResponse struct { + state protoimpl.MessageState `protogen:"open.v1"` + HeaderNamespace string `protobuf:"bytes,1,opt,name=header_namespace,json=headerNamespace,proto3" json:"header_namespace,omitempty"` + DataNamespace string `protobuf:"bytes,2,opt,name=data_namespace,json=dataNamespace,proto3" json:"data_namespace,omitempty"` + unknownFields protoimpl.UnknownFields + sizeCache protoimpl.SizeCache +} + +func (x *GetNamespaceResponse) Reset() { + *x = GetNamespaceResponse{} + mi := &file_evnode_v1_config_proto_msgTypes[0] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetNamespaceResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetNamespaceResponse) ProtoMessage() {} + +func (x *GetNamespaceResponse) ProtoReflect() protoreflect.Message { + mi := &file_evnode_v1_config_proto_msgTypes[0] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetNamespaceResponse.ProtoReflect.Descriptor instead. +func (*GetNamespaceResponse) Descriptor() ([]byte, []int) { + return file_evnode_v1_config_proto_rawDescGZIP(), []int{0} +} + +func (x *GetNamespaceResponse) GetHeaderNamespace() string { + if x != nil { + return x.HeaderNamespace + } + return "" +} + +func (x *GetNamespaceResponse) GetDataNamespace() string { + if x != nil { + return x.DataNamespace + } + return "" +} + +var File_evnode_v1_config_proto protoreflect.FileDescriptor + +const file_evnode_v1_config_proto_rawDesc = "" + + "\n" + + "\x16evnode/v1/config.proto\x12\tevnode.v1\x1a\x1bgoogle/protobuf/empty.proto\x1a\x16evnode/v1/evnode.proto\x1a\x15evnode/v1/state.proto\"h\n" + + "\x14GetNamespaceResponse\x12)\n" + + "\x10header_namespace\x18\x01 \x01(\tR\x0fheaderNamespace\x12%\n" + + "\x0edata_namespace\x18\x02 \x01(\tR\rdataNamespace2Z\n" + + "\rConfigService\x12I\n" + + "\fGetNamespace\x12\x16.google.protobuf.Empty\x1a\x1f.evnode.v1.GetNamespaceResponse\"\x00B/Z-git.832008.xyz/evstack/ev-node/types/pb/evnode/v1b\x06proto3" + +var ( + file_evnode_v1_config_proto_rawDescOnce sync.Once + file_evnode_v1_config_proto_rawDescData []byte +) + +func file_evnode_v1_config_proto_rawDescGZIP() []byte { + file_evnode_v1_config_proto_rawDescOnce.Do(func() { + file_evnode_v1_config_proto_rawDescData = protoimpl.X.CompressGZIP(unsafe.Slice(unsafe.StringData(file_evnode_v1_config_proto_rawDesc), len(file_evnode_v1_config_proto_rawDesc))) + }) + return file_evnode_v1_config_proto_rawDescData +} + +var file_evnode_v1_config_proto_msgTypes = make([]protoimpl.MessageInfo, 1) +var file_evnode_v1_config_proto_goTypes = []any{ + (*GetNamespaceResponse)(nil), // 0: evnode.v1.GetNamespaceResponse + (*emptypb.Empty)(nil), // 1: google.protobuf.Empty +} +var file_evnode_v1_config_proto_depIdxs = []int32{ + 1, // 0: evnode.v1.ConfigService.GetNamespace:input_type -> google.protobuf.Empty + 0, // 1: evnode.v1.ConfigService.GetNamespace:output_type -> evnode.v1.GetNamespaceResponse + 1, // [1:2] is the sub-list for method output_type + 0, // [0:1] is the sub-list for method input_type + 0, // [0:0] is the sub-list for extension type_name + 0, // [0:0] is the sub-list for extension extendee + 0, // [0:0] is the sub-list for field type_name +} + +func init() { file_evnode_v1_config_proto_init() } +func file_evnode_v1_config_proto_init() { + if File_evnode_v1_config_proto != nil { + return + } + file_evnode_v1_evnode_proto_init() + file_evnode_v1_state_proto_init() + type x struct{} + out := protoimpl.TypeBuilder{ + File: protoimpl.DescBuilder{ + GoPackagePath: reflect.TypeOf(x{}).PkgPath(), + RawDescriptor: unsafe.Slice(unsafe.StringData(file_evnode_v1_config_proto_rawDesc), len(file_evnode_v1_config_proto_rawDesc)), + NumEnums: 0, + NumMessages: 1, + NumExtensions: 0, + NumServices: 1, + }, + GoTypes: file_evnode_v1_config_proto_goTypes, + DependencyIndexes: file_evnode_v1_config_proto_depIdxs, + MessageInfos: file_evnode_v1_config_proto_msgTypes, + }.Build() + File_evnode_v1_config_proto = out.File + file_evnode_v1_config_proto_goTypes = nil + file_evnode_v1_config_proto_depIdxs = nil +} diff --git a/types/pb/evnode/v1/evnode.pb.go b/types/pb/evnode/v1/evnode.pb.go index a1cd0fa9c2..761783a51a 100644 --- a/types/pb/evnode/v1/evnode.pb.go +++ b/types/pb/evnode/v1/evnode.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/evnode.proto diff --git a/types/pb/evnode/v1/execution.pb.go b/types/pb/evnode/v1/execution.pb.go index c27959b9e5..7c109eba89 100644 --- a/types/pb/evnode/v1/execution.pb.go +++ b/types/pb/evnode/v1/execution.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/execution.proto diff --git a/types/pb/evnode/v1/health.pb.go b/types/pb/evnode/v1/health.pb.go index c343a623e4..5fd1fe5d0d 100644 --- a/types/pb/evnode/v1/health.pb.go +++ b/types/pb/evnode/v1/health.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/health.proto diff --git a/types/pb/evnode/v1/p2p_rpc.pb.go b/types/pb/evnode/v1/p2p_rpc.pb.go index ded9517217..c36458da23 100644 --- a/types/pb/evnode/v1/p2p_rpc.pb.go +++ b/types/pb/evnode/v1/p2p_rpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/p2p_rpc.proto diff --git a/types/pb/evnode/v1/signer.pb.go b/types/pb/evnode/v1/signer.pb.go index 8296db7613..e714de9f64 100644 --- a/types/pb/evnode/v1/signer.pb.go +++ b/types/pb/evnode/v1/signer.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/signer.proto diff --git a/types/pb/evnode/v1/state.pb.go b/types/pb/evnode/v1/state.pb.go index f6362600a7..6da29cd0cc 100644 --- a/types/pb/evnode/v1/state.pb.go +++ b/types/pb/evnode/v1/state.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/state.proto diff --git a/types/pb/evnode/v1/state_rpc.pb.go b/types/pb/evnode/v1/state_rpc.pb.go index 6e904b371a..76fc790ca4 100644 --- a/types/pb/evnode/v1/state_rpc.pb.go +++ b/types/pb/evnode/v1/state_rpc.pb.go @@ -1,6 +1,6 @@ // Code generated by protoc-gen-go. DO NOT EDIT. // versions: -// protoc-gen-go v1.36.6 +// protoc-gen-go v1.36.7 // protoc (unknown) // source: evnode/v1/state_rpc.proto diff --git a/types/pb/evnode/v1/v1connect/config.connect.go b/types/pb/evnode/v1/v1connect/config.connect.go new file mode 100644 index 0000000000..7f8ddd160d --- /dev/null +++ b/types/pb/evnode/v1/v1connect/config.connect.go @@ -0,0 +1,112 @@ +// Code generated by protoc-gen-connect-go. DO NOT EDIT. +// +// Source: evnode/v1/config.proto + +package v1connect + +import ( + connect "connectrpc.com/connect" + context "context" + errors "errors" + v1 "github.com/evstack/ev-node/types/pb/evnode/v1" + emptypb "google.golang.org/protobuf/types/known/emptypb" + http "net/http" + strings "strings" +) + +// This is a compile-time assertion to ensure that this generated file and the connect package are +// compatible. If you get a compiler error that this constant is not defined, this code was +// generated with a version of connect newer than the one compiled into your binary. You can fix the +// problem by either regenerating this code with an older version of connect or updating the connect +// version compiled into your binary. +const _ = connect.IsAtLeastVersion1_13_0 + +const ( + // ConfigServiceName is the fully-qualified name of the ConfigService service. + ConfigServiceName = "evnode.v1.ConfigService" +) + +// These constants are the fully-qualified names of the RPCs defined in this package. They're +// exposed at runtime as Spec.Procedure and as the final two segments of the HTTP route. +// +// Note that these are different from the fully-qualified method names used by +// google.golang.org/protobuf/reflect/protoreflect. To convert from these constants to +// reflection-formatted method names, remove the leading slash and convert the remaining slash to a +// period. +const ( + // ConfigServiceGetNamespaceProcedure is the fully-qualified name of the ConfigService's + // GetNamespace RPC. + ConfigServiceGetNamespaceProcedure = "/evnode.v1.ConfigService/GetNamespace" +) + +// ConfigServiceClient is a client for the evnode.v1.ConfigService service. +type ConfigServiceClient interface { + // GetNamespace returns the namespace for this network + GetNamespace(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetNamespaceResponse], error) +} + +// NewConfigServiceClient constructs a client for the evnode.v1.ConfigService service. By default, +// it uses the Connect protocol with the binary Protobuf Codec, asks for gzipped responses, and +// sends uncompressed requests. To use the gRPC or gRPC-Web protocols, supply the connect.WithGRPC() +// or connect.WithGRPCWeb() options. +// +// The URL supplied here should be the base URL for the Connect or gRPC server (for example, +// http://api.acme.com or https://acme.com/grpc). +func NewConfigServiceClient(httpClient connect.HTTPClient, baseURL string, opts ...connect.ClientOption) ConfigServiceClient { + baseURL = strings.TrimRight(baseURL, "/") + configServiceMethods := v1.File_evnode_v1_config_proto.Services().ByName("ConfigService").Methods() + return &configServiceClient{ + getNamespace: connect.NewClient[emptypb.Empty, v1.GetNamespaceResponse]( + httpClient, + baseURL+ConfigServiceGetNamespaceProcedure, + connect.WithSchema(configServiceMethods.ByName("GetNamespace")), + connect.WithClientOptions(opts...), + ), + } +} + +// configServiceClient implements ConfigServiceClient. +type configServiceClient struct { + getNamespace *connect.Client[emptypb.Empty, v1.GetNamespaceResponse] +} + +// GetNamespace calls evnode.v1.ConfigService.GetNamespace. +func (c *configServiceClient) GetNamespace(ctx context.Context, req *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetNamespaceResponse], error) { + return c.getNamespace.CallUnary(ctx, req) +} + +// ConfigServiceHandler is an implementation of the evnode.v1.ConfigService service. +type ConfigServiceHandler interface { + // GetNamespace returns the namespace for this network + GetNamespace(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetNamespaceResponse], error) +} + +// NewConfigServiceHandler builds an HTTP handler from the service implementation. It returns the +// path on which to mount the handler and the handler itself. +// +// By default, handlers support the Connect, gRPC, and gRPC-Web protocols with the binary Protobuf +// and JSON codecs. They also support gzip compression. +func NewConfigServiceHandler(svc ConfigServiceHandler, opts ...connect.HandlerOption) (string, http.Handler) { + configServiceMethods := v1.File_evnode_v1_config_proto.Services().ByName("ConfigService").Methods() + configServiceGetNamespaceHandler := connect.NewUnaryHandler( + ConfigServiceGetNamespaceProcedure, + svc.GetNamespace, + connect.WithSchema(configServiceMethods.ByName("GetNamespace")), + connect.WithHandlerOptions(opts...), + ) + return "/evnode.v1.ConfigService/", http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + switch r.URL.Path { + case ConfigServiceGetNamespaceProcedure: + configServiceGetNamespaceHandler.ServeHTTP(w, r) + default: + http.NotFound(w, r) + } + }) +} + +// UnimplementedConfigServiceHandler returns CodeUnimplemented from all methods. +type UnimplementedConfigServiceHandler struct{} + +func (UnimplementedConfigServiceHandler) GetNamespace(context.Context, *connect.Request[emptypb.Empty]) (*connect.Response[v1.GetNamespaceResponse], error) { + return nil, connect.NewError(connect.CodeUnimplemented, errors.New("evnode.v1.ConfigService.GetNamespace is not implemented")) +}